- 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
344 lines
7.8 KiB
Go
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))
|
|
}
|