yao/openapi/computer/computer.go
Max c366ce4d0a feat(assistant): enhance assistant info structure and sandbox integration
- Updated the AssistantInfo struct to include new fields: Connector, ConnectorOptions, Modes, DefaultMode, Sandbox, and ComputerFilter for improved assistant configuration.
- Enhanced the loading process to extract Sandbox flag and ComputerFilter from V2 sandbox configuration.
- Refactored GetInfo method to return comprehensive assistant details for better UI integration.
- Introduced new endpoint for workspace options to streamline InputArea selector functionality.

Made-with: Cursor
2026-03-12 10:42:22 +08:00

344 lines
7.8 KiB
Go

package computer
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
sandboxv2 "github.com/yaoapp/yao/sandbox/v2"
"github.com/yaoapp/yao/tai"
"github.com/yaoapp/yao/tai/registry"
"github.com/yaoapp/yao/openapi/oauth/authorized"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Attach registers computer option routes on the given group.
// - GET /options — list available computers (filtered by ComputerFilter query params)
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
group.Use(oauth.Guard)
group.GET("/options", handleOptions)
}
type computerSystemInfo struct {
OS string `json:"os"`
Arch string `json:"arch"`
Hostname string `json:"hostname"`
NumCPU int `json:"num_cpu"`
TotalMem int64 `json:"total_mem,omitempty"`
}
type computerOption struct {
Kind string `json:"kind"`
ID string `json:"id"`
DisplayName string `json:"display_name"`
NodeID string `json:"node_id"`
Status string `json:"status"`
Mode string `json:"mode,omitempty"`
Addr string `json:"addr,omitempty"`
Image string `json:"image,omitempty"`
Policy string `json:"policy,omitempty"`
VNC bool `json:"vnc"`
Labels map[string]string `json:"labels,omitempty"`
System computerSystemInfo `json:"system"`
}
func handleOptions(c *gin.Context) {
authInfo := authorized.GetInfo(c)
kindFilter := c.Query("kind")
imageFilter := c.Query("image")
osFilter := c.Query("os")
archFilter := c.Query("arch")
var vncFilter *bool
if v := c.Query("vnc"); v != "" {
b, _ := strconv.ParseBool(v)
vncFilter = &b
}
var minCPUs float64
if v := c.Query("min_cpus"); v != "" {
minCPUs, _ = strconv.ParseFloat(v, 64)
}
var minMem int64
if v := c.Query("min_mem"); v != "" {
minMem = parseMemString(v)
}
var result []computerOption
reg := registry.Global()
if reg == nil {
response.RespondWithSuccess(c, http.StatusOK, []computerOption{})
return
}
snaps := reg.List()
// Host entries: nodes with host_exec capability
if kindFilter == "" || kindFilter == "host" {
for i := range snaps {
s := &snaps[i]
if !nodeOwnedBy(s, authInfo) {
continue
}
if !s.Capabilities["host_exec"] {
continue
}
if !matchNodeFilter(s, osFilter, archFilter, minCPUs, minMem) {
continue
}
result = append(result, nodeToHostOption(*s))
}
}
// Node entries: nodes with container runtime capability
if kindFilter == "" || kindFilter == "node" {
for i := range snaps {
s := &snaps[i]
if !nodeOwnedBy(s, authInfo) {
continue
}
hasRuntime := s.Capabilities["docker"] || s.Capabilities["k8s"]
if !hasRuntime {
continue
}
if !matchNodeFilter(s, osFilter, archFilter, minCPUs, minMem) {
continue
}
result = append(result, nodeToNodeOption(*s))
}
}
// Box entries: persistent/longrunning boxes only
if kindFilter == "" || kindFilter == "box" {
if mgr := getManager(); mgr != nil {
owner := resolveOwner(authInfo)
boxes, err := mgr.List(context.Background(), sandboxv2.ListOptions{})
if err == nil {
for _, b := range boxes {
snap := b.Snapshot()
if snap.Owner != owner {
continue
}
if snap.Policy != sandboxv2.Persistent && snap.Policy != sandboxv2.LongRunning {
continue
}
if imageFilter != "" && snap.Image != imageFilter {
continue
}
if vncFilter != nil && snap.VNC != *vncFilter {
continue
}
result = append(result, boxToOption(b))
}
}
}
}
if result == nil {
result = []computerOption{}
}
response.RespondWithSuccess(c, http.StatusOK, result)
}
func matchNodeFilter(s *registry.NodeSnapshot, osFilter, archFilter string, minCPUs float64, minMem int64) bool {
if osFilter != "" && !strings.EqualFold(s.System.OS, osFilter) {
return false
}
if archFilter != "" && !strings.EqualFold(s.System.Arch, archFilter) {
return false
}
if minCPUs > 0 && float64(s.System.NumCPU) < minCPUs {
return false
}
if minMem > 0 && s.System.TotalMem < minMem {
return false
}
return true
}
func nodeToHostOption(s registry.NodeSnapshot) computerOption {
displayName := s.DisplayName
if displayName == "" {
displayName = s.System.Hostname
}
if displayName == "" {
displayName = s.TaiID
}
status := "stopped"
if s.Status == "online" {
status = "running"
}
addr := s.Addr
if addr == "" {
scheme := s.Mode
if scheme == "" {
scheme = "tai"
}
addr = scheme + "://" + s.TaiID
}
return computerOption{
Kind: "host",
ID: s.TaiID,
DisplayName: displayName,
NodeID: s.TaiID,
Status: status,
Mode: s.Mode,
Addr: addr,
System: computerSystemInfo{
OS: s.System.OS,
Arch: s.System.Arch,
Hostname: s.System.Hostname,
NumCPU: s.System.NumCPU,
TotalMem: s.System.TotalMem,
},
}
}
func nodeToNodeOption(s registry.NodeSnapshot) computerOption {
displayName := s.DisplayName
if displayName == "" {
displayName = s.System.Hostname
}
if displayName == "" {
displayName = s.TaiID
}
status := "stopped"
if s.Status == "online" {
status = "running"
}
addr := s.Addr
if addr == "" {
scheme := s.Mode
if scheme == "" {
scheme = "tai"
}
addr = scheme + "://" + s.TaiID
}
return computerOption{
Kind: "node",
ID: s.TaiID,
DisplayName: displayName,
NodeID: s.TaiID,
Status: status,
Mode: s.Mode,
Addr: addr,
System: computerSystemInfo{
OS: s.System.OS,
Arch: s.System.Arch,
Hostname: s.System.Hostname,
NumCPU: s.System.NumCPU,
TotalMem: s.System.TotalMem,
},
}
}
func boxToOption(b *sandboxv2.Box) computerOption {
snap := b.Snapshot()
info := b.ComputerInfo()
displayName := info.System.Hostname
if displayName == "" {
displayName = snap.ID
}
var mode, addr string
if ns, ok := tai.GetNodeSnapshot(snap.NodeID); ok {
mode = ns.Mode
addr = ns.Addr
}
if addr == "" && snap.NodeID != "" {
scheme := mode
if scheme == "" {
scheme = "local"
}
addr = scheme + "://" + snap.NodeID
}
return computerOption{
Kind: "box",
ID: snap.ID,
DisplayName: displayName,
NodeID: snap.NodeID,
Status: snap.Status,
Mode: mode,
Addr: addr,
Image: snap.Image,
Policy: string(snap.Policy),
VNC: snap.VNC,
Labels: snap.Labels,
System: computerSystemInfo{
OS: info.System.OS,
Arch: info.System.Arch,
Hostname: info.System.Hostname,
NumCPU: info.System.NumCPU,
TotalMem: info.System.TotalMem,
},
}
}
func nodeOwnedBy(snap *registry.NodeSnapshot, authInfo *oauthTypes.AuthorizedInfo) bool {
if authInfo == nil {
return true
}
if authInfo.TeamID != "" {
return snap.Auth.TeamID == authInfo.TeamID
}
if authInfo.UserID != "" {
return snap.Auth.TeamID == "" && snap.Auth.UserID == authInfo.UserID
}
return true
}
func resolveOwner(authInfo *oauthTypes.AuthorizedInfo) string {
if authInfo != nil && authInfo.TeamID != "" {
return authInfo.TeamID
}
if authInfo != nil {
return authInfo.UserID
}
return ""
}
func getManager() *sandboxv2.Manager {
defer func() { recover() }()
return sandboxv2.M()
}
func parseMemString(s string) int64 {
s = strings.TrimSpace(strings.ToLower(s))
if s == "" {
return 0
}
multiplier := int64(1)
switch {
case strings.HasSuffix(s, "g"):
multiplier = 1024 * 1024 * 1024
s = strings.TrimSuffix(s, "g")
case strings.HasSuffix(s, "m"):
multiplier = 1024 * 1024
s = strings.TrimSuffix(s, "m")
case strings.HasSuffix(s, "k"):
multiplier = 1024
s = strings.TrimSuffix(s, "k")
}
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return int64(val * float64(multiplier))
}