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:
Max 2026-03-12 10:42:22 +08:00
parent 5ec7801689
commit c366ce4d0a
8 changed files with 473 additions and 110 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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"`

View file

@ -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)

View file

@ -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

View 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))
}

View file

@ -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)

View file

@ -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 {