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
This commit is contained in:
parent
5ec7801689
commit
c366ce4d0a
8 changed files with 473 additions and 110 deletions
|
|
@ -479,11 +479,16 @@ func (ast *Assistant) GetInfo(locale ...string) *store.AssistantInfo {
|
|||
}
|
||||
|
||||
info := &store.AssistantInfo{
|
||||
AssistantID: ast.ID,
|
||||
Avatar: ast.Avatar,
|
||||
AssistantID: ast.ID,
|
||||
Avatar: ast.Avatar,
|
||||
Connector: ast.Connector,
|
||||
ConnectorOptions: ast.ConnectorOptions,
|
||||
Modes: ast.Modes,
|
||||
DefaultMode: ast.DefaultMode,
|
||||
Sandbox: ast.IsSandbox,
|
||||
ComputerFilter: ast.ComputerFilter,
|
||||
}
|
||||
|
||||
// Apply i18n translation if locale is provided
|
||||
if loc != "" {
|
||||
info.Name = ast.GetName(loc)
|
||||
info.Description = ast.GetDescription(loc)
|
||||
|
|
|
|||
|
|
@ -402,6 +402,12 @@ func LoadPath(path string) (*Assistant, error) {
|
|||
ast.SandboxV2 = sbCfg
|
||||
}
|
||||
|
||||
// Extract Sandbox flag and ComputerFilter from V2 sandbox config.
|
||||
if ast.SandboxV2 != nil {
|
||||
ast.IsSandbox = true
|
||||
ast.ComputerFilter = ast.SandboxV2.Filter
|
||||
}
|
||||
|
||||
// Compute config hash for V2 sandbox.
|
||||
if ast.SandboxV2 != nil {
|
||||
var mcpServers []store.MCPServerConfig
|
||||
|
|
@ -772,6 +778,8 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
return nil, err
|
||||
}
|
||||
assistant.SandboxV2 = sb
|
||||
assistant.IsSandbox = true
|
||||
assistant.ComputerFilter = sb.Filter
|
||||
} else {
|
||||
sb, err := store.ToSandbox(sandbox)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type SandboxConfig struct {
|
|||
Prepare []PrepareStep `json:"prepare,omitempty" yaml:"prepare,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty" yaml:"environment,omitempty"`
|
||||
Secrets map[string]string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
|
||||
Filter *ComputerFilter `json:"filter,omitempty" yaml:"filter,omitempty"`
|
||||
|
||||
// Populated by the framework at runtime (never serialized).
|
||||
Owner string `json:"-" yaml:"-"`
|
||||
|
|
@ -33,6 +34,19 @@ type SandboxConfig struct {
|
|||
WorkspaceID string `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// ComputerFilter defines the query parameters for GET /computer/options.
|
||||
// Declared in DSL sandbox.filter; frontend passes it through to the API.
|
||||
type ComputerFilter struct {
|
||||
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
|
||||
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
||||
VNC *bool `json:"vnc,omitempty" yaml:"vnc,omitempty"`
|
||||
OS string `json:"os,omitempty" yaml:"os,omitempty"`
|
||||
Arch string `json:"arch,omitempty" yaml:"arch,omitempty"`
|
||||
MinCPUs float64 `json:"min_cpus,omitempty" yaml:"min_cpus,omitempty"`
|
||||
MinMem string `json:"min_mem,omitempty" yaml:"min_mem,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// ComputerConfig describes the execution environment (container or host).
|
||||
type ComputerConfig struct {
|
||||
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
||||
|
|
|
|||
|
|
@ -207,10 +207,16 @@ type AssistantList struct {
|
|||
// AssistantInfo contains basic assistant information for display
|
||||
// Used in chat history to show assistant details with i18n support
|
||||
type AssistantInfo struct {
|
||||
AssistantID string `json:"assistant_id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AssistantID string `json:"assistant_id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Connector string `json:"connector,omitempty"`
|
||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"`
|
||||
Modes []string `json:"modes,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
Sandbox bool `json:"sandbox,omitempty"`
|
||||
ComputerFilter *sandboxTypes.ComputerFilter `json:"computer_filter,omitempty"`
|
||||
}
|
||||
|
||||
// Tag represents a tag
|
||||
|
|
@ -422,44 +428,46 @@ type ConnectorOptions struct {
|
|||
|
||||
// AssistantModel the assistant database model
|
||||
type AssistantModel struct {
|
||||
ID string `json:"assistant_id"` // Assistant ID
|
||||
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||
Name string `json:"name,omitempty"` // Assistant Name
|
||||
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
||||
Connector string `json:"connector"` // AI Connector (default connector)
|
||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"` // Connector selection options for user to choose from
|
||||
Path string `json:"path,omitempty"` // Assistant Path
|
||||
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
||||
Sort int `json:"sort,omitempty"` // Assistant Sort
|
||||
Description string `json:"description,omitempty"` // Assistant Description
|
||||
Capabilities string `json:"capabilities,omitempty"` // Assistant capabilities description (useful for Robot orchestration)
|
||||
Tags []string `json:"tags,omitempty"` // Assistant Tags
|
||||
Modes []string `json:"modes,omitempty"` // Supported modes (e.g., ["task", "chat"]), null means all modes are supported
|
||||
DefaultMode string `json:"default_mode,omitempty"` // Default mode, can be empty
|
||||
Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly
|
||||
Public bool `json:"public,omitempty"` // Whether this assistant is shared across all teams in the platform
|
||||
Share string `json:"share,omitempty"` // Assistant sharing scope (private/team)
|
||||
Mentionable bool `json:"mentionable,omitempty"` // Whether this assistant is mentionable
|
||||
Automated bool `json:"automated,omitempty"` // Whether this assistant is automated
|
||||
Options map[string]interface{} `json:"options,omitempty"` // AI Options
|
||||
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts (default prompts)
|
||||
PromptPresets map[string][]Prompt `json:"prompt_presets,omitempty"` // Prompt presets organized by mode (e.g., "chat", "task", etc.)
|
||||
DisableGlobalPrompts bool `json:"disable_global_prompts,omitempty"` // Whether to disable global prompts, default is false
|
||||
KB *KnowledgeBase `json:"kb,omitempty"` // Knowledge base configuration
|
||||
DB *Database `json:"db,omitempty"` // Database configuration
|
||||
MCP *MCPServers `json:"mcp,omitempty"` // MCP servers configuration
|
||||
Workflow *Workflow `json:"workflow,omitempty"` // Workflow configuration
|
||||
Sandbox *Sandbox `json:"sandbox,omitempty"` // Sandbox configuration for coding agents (V1)
|
||||
SandboxV2 *sandboxTypes.SandboxConfig `json:"-"` // V2 sandbox configuration (runtime only, not persisted in DB)
|
||||
ConfigHash string `json:"-"` // V2 sandbox config fingerprint for hot-reload
|
||||
Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder
|
||||
Source string `json:"source,omitempty"` // Hook script source code
|
||||
Locales i18n.Map `json:"locales,omitempty"` // Assistant Locales
|
||||
Uses *context.Uses `json:"uses,omitempty"` // Assistant-specific wrapper configurations for vision, audio, etc. If not set, use global settings
|
||||
Search *searchTypes.Config `json:"search,omitempty"` // Search configuration (web, kb, db, citation, weights, etc.)
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"` // Dependencies on other MCP Clients (name -> version constraint)
|
||||
CreatedAt int64 `json:"created_at"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
|
||||
ID string `json:"assistant_id"` // Assistant ID
|
||||
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||
Name string `json:"name,omitempty"` // Assistant Name
|
||||
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
||||
Connector string `json:"connector"` // AI Connector (default connector)
|
||||
ConnectorOptions *ConnectorOptions `json:"connector_options,omitempty"` // Connector selection options for user to choose from
|
||||
Path string `json:"path,omitempty"` // Assistant Path
|
||||
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
||||
Sort int `json:"sort,omitempty"` // Assistant Sort
|
||||
Description string `json:"description,omitempty"` // Assistant Description
|
||||
Capabilities string `json:"capabilities,omitempty"` // Assistant capabilities description (useful for Robot orchestration)
|
||||
Tags []string `json:"tags,omitempty"` // Assistant Tags
|
||||
Modes []string `json:"modes,omitempty"` // Supported modes (e.g., ["task", "chat"]), null means all modes are supported
|
||||
DefaultMode string `json:"default_mode,omitempty"` // Default mode, can be empty
|
||||
Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly
|
||||
Public bool `json:"public,omitempty"` // Whether this assistant is shared across all teams in the platform
|
||||
Share string `json:"share,omitempty"` // Assistant sharing scope (private/team)
|
||||
Mentionable bool `json:"mentionable,omitempty"` // Whether this assistant is mentionable
|
||||
Automated bool `json:"automated,omitempty"` // Whether this assistant is automated
|
||||
Options map[string]interface{} `json:"options,omitempty"` // AI Options
|
||||
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts (default prompts)
|
||||
PromptPresets map[string][]Prompt `json:"prompt_presets,omitempty"` // Prompt presets organized by mode (e.g., "chat", "task", etc.)
|
||||
DisableGlobalPrompts bool `json:"disable_global_prompts,omitempty"` // Whether to disable global prompts, default is false
|
||||
KB *KnowledgeBase `json:"kb,omitempty"` // Knowledge base configuration
|
||||
DB *Database `json:"db,omitempty"` // Database configuration
|
||||
MCP *MCPServers `json:"mcp,omitempty"` // MCP servers configuration
|
||||
Workflow *Workflow `json:"workflow,omitempty"` // Workflow configuration
|
||||
Sandbox *Sandbox `json:"sandbox,omitempty"` // Sandbox configuration for coding agents (V1)
|
||||
SandboxV2 *sandboxTypes.SandboxConfig `json:"-"` // V2 sandbox configuration (runtime only, not persisted in DB)
|
||||
IsSandbox bool `json:"-"` // Whether this is a Sandbox assistant (derived from SandboxV2 presence)
|
||||
ComputerFilter *sandboxTypes.ComputerFilter `json:"-"` // Computer filter from DSL sandbox.filter (runtime only)
|
||||
ConfigHash string `json:"-"` // V2 sandbox config fingerprint for hot-reload
|
||||
Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder
|
||||
Source string `json:"source,omitempty"` // Hook script source code
|
||||
Locales i18n.Map `json:"locales,omitempty"` // Assistant Locales
|
||||
Uses *context.Uses `json:"uses,omitempty"` // Assistant-specific wrapper configurations for vision, audio, etc. If not set, use global settings
|
||||
Search *searchTypes.Config `json:"search,omitempty"` // Search configuration (web, kb, db, citation, weights, etc.)
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"` // Dependencies on other MCP Clients (name -> version constraint)
|
||||
CreatedAt int64 `json:"created_at"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
|
||||
|
||||
// Permission management fields (not exposed in JSON API responses)
|
||||
YaoCreatedBy string `json:"-"` // User who created the assistant (not exposed in JSON)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/agent"
|
||||
"github.com/yaoapp/yao/agent/assistant"
|
||||
assistantPkg "github.com/yaoapp/yao/agent/assistant"
|
||||
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
|
|
@ -415,13 +415,13 @@ func CreateAssistant(c *gin.Context) {
|
|||
assistantData["assistant_id"] = id
|
||||
|
||||
// Clear cache and reload assistant to make it effective
|
||||
cache := assistant.GetCache()
|
||||
cache := assistantPkg.GetCache()
|
||||
if cache != nil {
|
||||
cache.Remove(id)
|
||||
}
|
||||
|
||||
// Reload the assistant to ensure it's available in cache with updated data
|
||||
_, err = assistant.Get(id)
|
||||
_, err = assistantPkg.Get(id)
|
||||
if err != nil {
|
||||
// Just log the error, don't fail the request
|
||||
log.Error("Error reloading assistant %s: %v", id, err)
|
||||
|
|
@ -520,13 +520,13 @@ func UpdateAssistant(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Clear cache and reload assistant to make it effective
|
||||
cache := assistant.GetCache()
|
||||
cache := assistantPkg.GetCache()
|
||||
if cache != nil {
|
||||
cache.Remove(assistantID)
|
||||
}
|
||||
|
||||
// Reload the assistant to ensure it's available in cache with updated data
|
||||
_, err = assistant.Get(assistantID)
|
||||
_, err = assistantPkg.Get(assistantID)
|
||||
if err != nil {
|
||||
// Just log the error, don't fail the request
|
||||
log.Error("Error reloading assistant %s: %v", assistantID, err)
|
||||
|
|
@ -539,24 +539,10 @@ func UpdateAssistant(c *gin.Context) {
|
|||
}
|
||||
|
||||
// GetAssistantInfo retrieves essential assistant information for InputArea component
|
||||
// Returns only the fields needed for UI display: id, name, avatar, description, connector, connector_options, modes, default_mode
|
||||
func GetAssistantInfo(c *gin.Context) {
|
||||
|
||||
// Get authorized information
|
||||
authInfo := authorized.GetInfo(c)
|
||||
|
||||
// Get Agent instance from global variable
|
||||
agentInstance := agent.GetAgent()
|
||||
if agentInstance == nil || agentInstance.Store == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Agent store not initialized",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get assistant ID from URL parameter
|
||||
assistantID := c.Param("id")
|
||||
if assistantID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
@ -567,37 +553,11 @@ func GetAssistantInfo(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Parse locale (optional - defaults to "en-us")
|
||||
locale := "en-us"
|
||||
if loc := c.Query("locale"); loc != "" {
|
||||
locale = strings.ToLower(strings.TrimSpace(loc))
|
||||
}
|
||||
|
||||
// Define fields needed for InputArea
|
||||
infoFields := []string{
|
||||
"assistant_id",
|
||||
"name",
|
||||
"avatar",
|
||||
"description",
|
||||
"connector",
|
||||
"connector_options",
|
||||
"modes",
|
||||
"default_mode",
|
||||
}
|
||||
|
||||
// Get assistant with specific fields and locale
|
||||
assistant, err := agentInstance.Store.GetAssistant(assistantID, infoFields, locale)
|
||||
if err != nil {
|
||||
log.Error("Failed to get assistant info %s: %v", assistantID, err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Assistant not found: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check read permission (same as GetAssistant)
|
||||
hasPermission, err := checkAssistantPermission(authInfo, assistantID, true)
|
||||
if err != nil {
|
||||
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
|
||||
|
|
@ -618,28 +578,18 @@ func GetAssistantInfo(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Build response with only the required fields
|
||||
infoResponse := map[string]interface{}{
|
||||
"assistant_id": assistant.ID,
|
||||
"name": assistant.Name,
|
||||
"avatar": assistant.Avatar,
|
||||
"description": assistant.Description,
|
||||
"connector": assistant.Connector,
|
||||
ast, err := assistantPkg.Get(assistantID)
|
||||
if err != nil || ast == nil {
|
||||
log.Error("Failed to get assistant info %s: %v", assistantID, err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Assistant not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Add optional fields if they exist
|
||||
if assistant.ConnectorOptions != nil {
|
||||
infoResponse["connector_options"] = assistant.ConnectorOptions
|
||||
}
|
||||
if len(assistant.Modes) > 0 {
|
||||
infoResponse["modes"] = assistant.Modes
|
||||
}
|
||||
if assistant.DefaultMode != "" {
|
||||
infoResponse["default_mode"] = assistant.DefaultMode
|
||||
}
|
||||
|
||||
// Return the result with standard response format
|
||||
response.RespondWithSuccess(c, response.StatusOK, infoResponse)
|
||||
response.RespondWithSuccess(c, response.StatusOK, ast.GetInfo(locale))
|
||||
}
|
||||
|
||||
// checkAssistantPermission checks if the user has permission to access the assistant
|
||||
|
|
|
|||
344
openapi/computer/computer.go
Normal file
344
openapi/computer/computer.go
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
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))
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/app"
|
||||
"github.com/yaoapp/yao/openapi/captcha"
|
||||
"github.com/yaoapp/yao/openapi/chat"
|
||||
openapiComputer "github.com/yaoapp/yao/openapi/computer"
|
||||
"github.com/yaoapp/yao/openapi/dsl"
|
||||
"github.com/yaoapp/yao/openapi/file"
|
||||
"github.com/yaoapp/yao/openapi/hello"
|
||||
|
|
@ -181,6 +182,9 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
|||
sandbox.Attach(sandboxGroup, openapi.OAuth)
|
||||
sandbox.AttachManage(sandboxGroup)
|
||||
|
||||
// Computer option handlers (for InputArea selector)
|
||||
openapiComputer.Attach(group.Group("/computer"), openapi.OAuth)
|
||||
|
||||
// Workspace handlers
|
||||
openapiWorkspace.Attach(group.Group("/workspace"), openapi.OAuth)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
group.Use(oauth.Guard)
|
||||
|
||||
group.GET("", handleList)
|
||||
group.GET("/options", handleOptions)
|
||||
group.POST("", handleCreate)
|
||||
group.GET("/:id", handleGet)
|
||||
group.PUT("/:id", handleUpdate)
|
||||
|
|
@ -170,6 +171,35 @@ func handleList(c *gin.Context) {
|
|||
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// handleOptions returns workspace options for the InputArea selector.
|
||||
// Reuses the same logic as handleList (Manager.List with owner+node filter).
|
||||
// Separated as a dedicated endpoint for clear API responsibility boundary.
|
||||
func handleOptions(c *gin.Context) {
|
||||
m := mgr()
|
||||
if m == nil {
|
||||
response.RespondWithSuccess(c, http.StatusOK, []workspaceResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
authInfo := authorized.GetInfo(c)
|
||||
owner := resolveOwner(authInfo)
|
||||
|
||||
list, err := m.List(context.Background(), ws.ListOptions{
|
||||
Owner: owner,
|
||||
Node: c.Query("node"),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]workspaceResponse, 0, len(list))
|
||||
for _, w := range list {
|
||||
result = append(result, toResponse(w))
|
||||
}
|
||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func handleCreate(c *gin.Context) {
|
||||
m := mgr()
|
||||
if m == nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue