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
|
|
@ -481,9 +481,14 @@ func (ast *Assistant) GetInfo(locale ...string) *store.AssistantInfo {
|
||||||
info := &store.AssistantInfo{
|
info := &store.AssistantInfo{
|
||||||
AssistantID: ast.ID,
|
AssistantID: ast.ID,
|
||||||
Avatar: ast.Avatar,
|
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 != "" {
|
if loc != "" {
|
||||||
info.Name = ast.GetName(loc)
|
info.Name = ast.GetName(loc)
|
||||||
info.Description = ast.GetDescription(loc)
|
info.Description = ast.GetDescription(loc)
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,12 @@ func LoadPath(path string) (*Assistant, error) {
|
||||||
ast.SandboxV2 = sbCfg
|
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.
|
// Compute config hash for V2 sandbox.
|
||||||
if ast.SandboxV2 != nil {
|
if ast.SandboxV2 != nil {
|
||||||
var mcpServers []store.MCPServerConfig
|
var mcpServers []store.MCPServerConfig
|
||||||
|
|
@ -772,6 +778,8 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
assistant.SandboxV2 = sb
|
assistant.SandboxV2 = sb
|
||||||
|
assistant.IsSandbox = true
|
||||||
|
assistant.ComputerFilter = sb.Filter
|
||||||
} else {
|
} else {
|
||||||
sb, err := store.ToSandbox(sandbox)
|
sb, err := store.ToSandbox(sandbox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type SandboxConfig struct {
|
||||||
Prepare []PrepareStep `json:"prepare,omitempty" yaml:"prepare,omitempty"`
|
Prepare []PrepareStep `json:"prepare,omitempty" yaml:"prepare,omitempty"`
|
||||||
Environment map[string]string `json:"environment,omitempty" yaml:"environment,omitempty"`
|
Environment map[string]string `json:"environment,omitempty" yaml:"environment,omitempty"`
|
||||||
Secrets map[string]string `json:"secrets,omitempty" yaml:"secrets,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).
|
// Populated by the framework at runtime (never serialized).
|
||||||
Owner string `json:"-" yaml:"-"`
|
Owner string `json:"-" yaml:"-"`
|
||||||
|
|
@ -33,6 +34,19 @@ type SandboxConfig struct {
|
||||||
WorkspaceID string `json:"-" yaml:"-"`
|
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).
|
// ComputerConfig describes the execution environment (container or host).
|
||||||
type ComputerConfig struct {
|
type ComputerConfig struct {
|
||||||
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,12 @@ type AssistantInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
Description string `json:"description,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
|
// Tag represents a tag
|
||||||
|
|
@ -451,6 +457,8 @@ type AssistantModel struct {
|
||||||
Workflow *Workflow `json:"workflow,omitempty"` // Workflow configuration
|
Workflow *Workflow `json:"workflow,omitempty"` // Workflow configuration
|
||||||
Sandbox *Sandbox `json:"sandbox,omitempty"` // Sandbox configuration for coding agents (V1)
|
Sandbox *Sandbox `json:"sandbox,omitempty"` // Sandbox configuration for coding agents (V1)
|
||||||
SandboxV2 *sandboxTypes.SandboxConfig `json:"-"` // V2 sandbox configuration (runtime only, not persisted in DB)
|
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
|
ConfigHash string `json:"-"` // V2 sandbox config fingerprint for hot-reload
|
||||||
Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder
|
Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder
|
||||||
Source string `json:"source,omitempty"` // Hook script source code
|
Source string `json:"source,omitempty"` // Hook script source code
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/agent"
|
"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"
|
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
|
@ -415,13 +415,13 @@ func CreateAssistant(c *gin.Context) {
|
||||||
assistantData["assistant_id"] = id
|
assistantData["assistant_id"] = id
|
||||||
|
|
||||||
// Clear cache and reload assistant to make it effective
|
// Clear cache and reload assistant to make it effective
|
||||||
cache := assistant.GetCache()
|
cache := assistantPkg.GetCache()
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
cache.Remove(id)
|
cache.Remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the assistant to ensure it's available in cache with updated data
|
// Reload the assistant to ensure it's available in cache with updated data
|
||||||
_, err = assistant.Get(id)
|
_, err = assistantPkg.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Just log the error, don't fail the request
|
// Just log the error, don't fail the request
|
||||||
log.Error("Error reloading assistant %s: %v", id, err)
|
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
|
// Clear cache and reload assistant to make it effective
|
||||||
cache := assistant.GetCache()
|
cache := assistantPkg.GetCache()
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
cache.Remove(assistantID)
|
cache.Remove(assistantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the assistant to ensure it's available in cache with updated data
|
// Reload the assistant to ensure it's available in cache with updated data
|
||||||
_, err = assistant.Get(assistantID)
|
_, err = assistantPkg.Get(assistantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Just log the error, don't fail the request
|
// Just log the error, don't fail the request
|
||||||
log.Error("Error reloading assistant %s: %v", assistantID, err)
|
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
|
// 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) {
|
func GetAssistantInfo(c *gin.Context) {
|
||||||
|
|
||||||
// Get authorized information
|
|
||||||
authInfo := authorized.GetInfo(c)
|
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")
|
assistantID := c.Param("id")
|
||||||
if assistantID == "" {
|
if assistantID == "" {
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
|
|
@ -567,37 +553,11 @@ func GetAssistantInfo(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse locale (optional - defaults to "en-us")
|
|
||||||
locale := "en-us"
|
locale := "en-us"
|
||||||
if loc := c.Query("locale"); loc != "" {
|
if loc := c.Query("locale"); loc != "" {
|
||||||
locale = strings.ToLower(strings.TrimSpace(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)
|
hasPermission, err := checkAssistantPermission(authInfo, assistantID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
|
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
|
||||||
|
|
@ -618,28 +578,18 @@ func GetAssistantInfo(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build response with only the required fields
|
ast, err := assistantPkg.Get(assistantID)
|
||||||
infoResponse := map[string]interface{}{
|
if err != nil || ast == nil {
|
||||||
"assistant_id": assistant.ID,
|
log.Error("Failed to get assistant info %s: %v", assistantID, err)
|
||||||
"name": assistant.Name,
|
errorResp := &response.ErrorResponse{
|
||||||
"avatar": assistant.Avatar,
|
Code: response.ErrInvalidRequest.Code,
|
||||||
"description": assistant.Description,
|
ErrorDescription: "Assistant not found",
|
||||||
"connector": assistant.Connector,
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add optional fields if they exist
|
response.RespondWithSuccess(c, response.StatusOK, ast.GetInfo(locale))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAssistantPermission checks if the user has permission to access the assistant
|
// 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/app"
|
||||||
"github.com/yaoapp/yao/openapi/captcha"
|
"github.com/yaoapp/yao/openapi/captcha"
|
||||||
"github.com/yaoapp/yao/openapi/chat"
|
"github.com/yaoapp/yao/openapi/chat"
|
||||||
|
openapiComputer "github.com/yaoapp/yao/openapi/computer"
|
||||||
"github.com/yaoapp/yao/openapi/dsl"
|
"github.com/yaoapp/yao/openapi/dsl"
|
||||||
"github.com/yaoapp/yao/openapi/file"
|
"github.com/yaoapp/yao/openapi/file"
|
||||||
"github.com/yaoapp/yao/openapi/hello"
|
"github.com/yaoapp/yao/openapi/hello"
|
||||||
|
|
@ -181,6 +182,9 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
||||||
sandbox.Attach(sandboxGroup, openapi.OAuth)
|
sandbox.Attach(sandboxGroup, openapi.OAuth)
|
||||||
sandbox.AttachManage(sandboxGroup)
|
sandbox.AttachManage(sandboxGroup)
|
||||||
|
|
||||||
|
// Computer option handlers (for InputArea selector)
|
||||||
|
openapiComputer.Attach(group.Group("/computer"), openapi.OAuth)
|
||||||
|
|
||||||
// Workspace handlers
|
// Workspace handlers
|
||||||
openapiWorkspace.Attach(group.Group("/workspace"), openapi.OAuth)
|
openapiWorkspace.Attach(group.Group("/workspace"), openapi.OAuth)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
group.Use(oauth.Guard)
|
group.Use(oauth.Guard)
|
||||||
|
|
||||||
group.GET("", handleList)
|
group.GET("", handleList)
|
||||||
|
group.GET("/options", handleOptions)
|
||||||
group.POST("", handleCreate)
|
group.POST("", handleCreate)
|
||||||
group.GET("/:id", handleGet)
|
group.GET("/:id", handleGet)
|
||||||
group.PUT("/:id", handleUpdate)
|
group.PUT("/:id", handleUpdate)
|
||||||
|
|
@ -170,6 +171,35 @@ func handleList(c *gin.Context) {
|
||||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
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) {
|
func handleCreate(c *gin.Context) {
|
||||||
m := mgr()
|
m := mgr()
|
||||||
if m == nil {
|
if m == nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue