yao/sui/storages/agent/agent.go
Max 621290d578 Enhance guard handling and template configuration merging
- Update the OAuth guard to prevent automatic response writing on failure, allowing for custom error handling.
- Introduce a mechanism to register default guard redirects from template configurations, improving guard management.
- Refactor the page configuration merging process to prioritize page-specific settings while allowing inheritance from templates.
- Ensure guards can be explicitly disabled in page configurations, enhancing flexibility in guard application.
2026-02-15 16:34:31 +08:00

307 lines
7.8 KiB
Go

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
}