package agent import ( "fmt" "path/filepath" "strings" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/fs" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" ) // New create a new agent sui storage func New(dsl *core.DSL) (*Agent, error) { // Use "app" filesystem which is rooted at application source directory appFS, err := fs.Get("app") if err != nil { return nil, err } // Set default public settings for agent if dsl.Public == nil { dsl.Public = &core.Public{} } if dsl.Public.Root == "" { dsl.Public.Root = "/agents" } if dsl.Public.Host == "" { dsl.Public.Host = "/" } if dsl.Public.Index == "" { dsl.Public.Index = "/index" } return &Agent{ root: "/agent/template", assistantsRoot: "/assistants", fs: appFS, DSL: dsl, }, nil } // GetTemplates get the templates (returns single agent template) func (agent *Agent) GetTemplates() ([]core.ITemplate, error) { tmpl, err := agent.GetTemplate("agent") if err != nil { return nil, err } return []core.ITemplate{tmpl}, nil } // GetTemplate get the template func (agent *Agent) GetTemplate(id string) (core.ITemplate, error) { if id != "agent" { return nil, fmt.Errorf("Agent storage only supports 'agent' template, got: %s", id) } // Check if /agent directory exists if !agent.fs.IsDir(agent.root) { return nil, fmt.Errorf("Agent template directory not found: %s", agent.root) } // Create agent template tmpl := &Template{ Root: agent.root, agent: agent, Template: &core.Template{ ID: "agent", Name: "Agent", Version: 1, Screenshots: []string{}, Themes: []core.SelectOption{}, }, } // Load template.json if exists configFile := filepath.Join(agent.root, "template.json") if agent.fs.IsFile(configFile) { configBytes, err := agent.fs.ReadFile(configFile) if err != nil { return nil, err } err = jsoniter.Unmarshal(configBytes, tmpl.Template) if err != nil { return nil, err } } // Register default guard redirect from template config if tmpl.Template.Config != nil && strings.Contains(tmpl.Template.Config.Guard, ":") { parts := strings.SplitN(tmpl.Template.Config.Guard, ":", 2) core.DefaultGuardRedirects[parts[0]] = parts[1] } // Load __document.html documentFile := filepath.Join(agent.root, "__document.html") if agent.fs.IsFile(documentFile) { documentBytes, err := agent.fs.ReadFile(documentFile) if err != nil { return nil, err } tmpl.Document = documentBytes } // Load __data.json dataFile := filepath.Join(agent.root, "__data.json") if agent.fs.IsFile(dataFile) { dataBytes, err := agent.fs.ReadFile(dataFile) if err != nil { return nil, err } tmpl.GlobalData = dataBytes } // Load build script err := tmpl.loadBuildScript() if err != nil { log.Warn("[Agent] Failed to load build script: %v", err) } return tmpl, nil } // UploadTemplate upload the template (not supported for agent) func (agent *Agent) UploadTemplate(src string, dst string) (core.ITemplate, error) { return nil, fmt.Errorf("UploadTemplate is not supported for agent storage") } // PublicRootMatcher get the public root matcher func (agent *Agent) PublicRootMatcher() *core.Matcher { return &core.Matcher{Exact: agent.DSL.Public.Root} } // Setting get the setting func (agent *Agent) Setting() (*core.Setting, error) { return &core.Setting{ ID: agent.DSL.ID, Guard: agent.DSL.Guard, Option: map[string]interface{}{ "disableCodeEditor": true, }, }, nil } // PublicRoot get the public root func (agent *Agent) PublicRoot(data map[string]interface{}) (string, error) { return agent.DSL.Public.Root, nil } // WithSid set the session id func (agent *Agent) WithSid(sid string) { agent.DSL.Sid = sid } // getAssistants get all assistant directories that have pages (supports nested assistants) // Returns assistant IDs like: ["expense", "tasks", "tests.nested.demo"] // Nested paths are joined with "." to form the assistant ID func (agent *Agent) getAssistants() ([]string, error) { if !agent.fs.IsDir(agent.assistantsRoot) { return []string{}, nil } assistants := []string{} err := agent.scanAssistantsRecursive(agent.assistantsRoot, "", &assistants) if err != nil { return nil, err } return assistants, nil } // scanAssistantsRecursive recursively scans directories for assistants with pages // prefix is the accumulated path prefix (e.g., "tests.nested") func (agent *Agent) scanAssistantsRecursive(dir string, prefix string, assistants *[]string) error { dirs, err := agent.fs.ReadDir(dir, false) if err != nil { return err } for _, subdir := range dirs { if !agent.fs.IsDir(subdir) { continue } name := filepath.Base(subdir) // Skip hidden directories and special directories if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "__") { continue } // Build the assistant ID with prefix assistantID := name if prefix != "" { assistantID = prefix + "." + name } // Check if this directory has a pages subdirectory pagesDir := filepath.Join(subdir, "pages") if agent.fs.IsDir(pagesDir) { *assistants = append(*assistants, assistantID) } // Recursively scan subdirectories for nested assistants // Only scan if there's no pages directory (to avoid scanning inside pages/) // or if there are other subdirectories that might contain nested assistants if !agent.fs.IsDir(pagesDir) { err := agent.scanAssistantsRecursive(subdir, assistantID, assistants) if err != nil { log.Warn("[Agent] Error scanning subdirectory %s: %v", subdir, err) continue } } else { // Even if this has pages, check for nested assistants in other subdirectories subdirs, _ := agent.fs.ReadDir(subdir, false) for _, nested := range subdirs { nestedName := filepath.Base(nested) if agent.fs.IsDir(nested) && nestedName != "pages" && !strings.HasPrefix(nestedName, ".") && !strings.HasPrefix(nestedName, "__") { err := agent.scanAssistantsRecursive(nested, assistantID+"."+nestedName, assistants) if err != nil { log.Warn("[Agent] Error scanning nested directory %s: %v", nested, err) continue } } } } } return nil } // getAssistantPagesRoot get the pages root for an assistant // assistantID can be "expense" or "tests.nested.demo" // Returns the actual filesystem path like "/assistants/tests/nested/demo/pages" func (agent *Agent) getAssistantPagesRoot(assistantID string) string { // Convert dot notation to path: "tests.nested.demo" -> "tests/nested/demo" pathParts := strings.Split(assistantID, ".") assistantPath := filepath.Join(pathParts...) return filepath.Join(agent.assistantsRoot, assistantPath, "pages") } // Exists check if the agent storage is available func Exists() bool { appFS, err := fs.Get("app") if err != nil { return false } return appFS.IsDir("/agent/template") } // HasAssistantPages check if any assistant has pages (supports nested assistants) func HasAssistantPages() bool { appFS, err := fs.Get("app") if err != nil { return false } if !appFS.IsDir("/assistants") { return false } return hasAssistantPagesRecursive(appFS, "/assistants") } // hasAssistantPagesRecursive recursively checks for assistants with pages func hasAssistantPagesRecursive(appFS fs.FileSystem, dir string) bool { dirs, err := appFS.ReadDir(dir, false) if err != nil { return false } for _, subdir := range dirs { if !appFS.IsDir(subdir) { continue } name := filepath.Base(subdir) // Skip hidden directories and special directories if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "__") { continue } // Check if this directory has a pages subdirectory pagesDir := filepath.Join(subdir, "pages") if appFS.IsDir(pagesDir) { return true } // Recursively check subdirectories if hasAssistantPagesRecursive(appFS, subdir) { return true } } return false } // log helper func init() { _ = log.Debug _ = strings.TrimPrefix }