From 621290d5782a795b6e92e993324b9c70a0997268 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 16:34:31 +0800 Subject: [PATCH 1/6] 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. --- service/middleware.go | 14 +++++++---- sui/api/guards.go | 19 ++++++++++++--- sui/api/request.go | 48 +++++++++++++++++++++---------------- sui/core/interfaces.go | 5 ++++ sui/core/types.go | 5 ++-- sui/storages/agent/agent.go | 6 +++++ sui/storages/agent/page.go | 48 ++++++++++++++++++++++++++++++++++--- 7 files changed, 111 insertions(+), 34 deletions(-) diff --git a/service/middleware.go b/service/middleware.go index 12771702..7d3772c1 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -77,7 +77,6 @@ func withStaticFileServer(c *gin.Context) { // Sui file server if strings.HasSuffix(c.Request.URL.Path, ".sui") { - // Default index.sui if filepath.Base(c.Request.URL.Path) == ".sui" { c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".sui") + "index.sui" @@ -92,10 +91,15 @@ func withStaticFileServer(c *gin.Context) { html, code, err := r.Render() if err != nil { - if code == 301 || code == 302 { - url := err.Error() - // fmt.Println("Redirect to: ", url) - c.Redirect(code, url) + if code == 301 || code == 302 { + url := err.Error() + c.Redirect(code, url) + c.Done() + return + } + + // Guard already sent response (e.g., OAuth writes its own 401) + if c.Writer.Written() { c.Done() return } diff --git a/sui/api/guards.go b/sui/api/guards.go index 74606341..b53ce74b 100644 --- a/sui/api/guards.go +++ b/sui/api/guards.go @@ -97,6 +97,8 @@ func guardCookieTrace(r *Request) error { // OAuth 2.1 guard - authentication only // This guard validates the token and sets authorized info // ACL checks are performed separately in Run() for API calls +// NOTE: This guard does NOT write HTTP responses on failure, so that +// the caller (Guard/apiGuard) can handle redirects or custom error responses. func guardOAuth(r *Request) error { if r.context == nil { return fmt.Errorf("Context is nil") @@ -108,11 +110,22 @@ func guardOAuth(r *Request) error { c := r.context - // Authenticate only (validates token and sets authorized info) - if !oauth.OAuth.Authenticate(c) { - return fmt.Errorf("Not authenticated") + // Check token first without writing response. + // oauth.Authenticate() writes JSON + aborts on failure, which prevents + // the caller from doing redirects. So we check the token manually first. + token := oauth.OAuth.GetAccessToken(c) + if token == "" { + return fmt.Errorf("Exception|401:Not authenticated") } + if _, err := oauth.OAuth.VerifyToken(token); err != nil { + return fmt.Errorf("Exception|401:Invalid or expired token") + } + + // Token is valid, now call Authenticate to set up the full context + // (session ID, authorized info, etc.). This will succeed since token is valid. + oauth.OAuth.Authenticate(c) + // Get authorized info from context info := authorized.GetInfo(c) if info != nil { diff --git a/sui/api/request.go b/sui/api/request.go index a49716f2..941fbd6d 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -237,6 +237,13 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { guardRedirect = parts[1] } + // Fallback: if guard has no redirect, check template default redirect + if guardRedirect == "" && guard != "" && guard != "-" { + if defaultRedirect, has := core.DefaultGuardRedirects[guard]; has { + guardRedirect = defaultRedirect + } + } + // Cache store cacheStore = conf.CacheStore cacheTime = conf.Cache @@ -303,8 +310,8 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { // Guard the page func (r *Request) Guard(c *core.Cache) (int, error) { - // Guard not set - if c.Guard == "" || r.context == nil { + // Guard not set or explicitly disabled + if c.Guard == "" || c.Guard == "-" || r.context == nil { return 200, nil } @@ -312,32 +319,27 @@ func (r *Request) Guard(c *core.Cache) (int, error) { if guard, has := Guards[c.Guard]; has { err := guard(r) if err != nil { - // Redirect the page (should refector before release) + // Redirect the page (takes priority over guard's own response) if c.GuardRedirect != "" { redirect := c.GuardRedirect - data := core.Data{} - // Here may have a security issue, should be refector, in the future. - // Copy the script pointer to the request For page backend script execution - r.Request.Script = c.Script - if c.Data != "" { - data, err = r.Request.ExecString(c.Data) - if err != nil { - return 500, fmt.Errorf("data error, please re-complie the page %s", err.Error()) - } + + // Append error code and message as query parameters + ex := exception.Err(err, 403) + msg := url.QueryEscape(ex.Message) + if strings.Contains(redirect, "?") { + redirect = fmt.Sprintf("%s&code=%d&message=%s", redirect, ex.Code, msg) + } else { + redirect = fmt.Sprintf("%s?code=%d&message=%s", redirect, ex.Code, msg) } - if c.Global != "" { - global, err := r.Request.ExecString(c.Global) - if err != nil { - return 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error()) - } - data["$global"] = global - } - - redirect, _ = data.Replace(redirect) return 302, fmt.Errorf("%s", redirect) } + // Guard already sent response (e.g., OAuth writes its own 401) + if r.context != nil && r.context.IsAborted() { + return 403, err + } + // Return the error ex := exception.Err(err, 403) return ex.Code, fmt.Errorf("%s", ex.Message) @@ -348,6 +350,10 @@ func (r *Request) Guard(c *core.Cache) (int, error) { // Developer custom guard err := r.processGuard(c.Guard) if err != nil { + // Guard already sent response + if r.context != nil && r.context.IsAborted() { + return 403, err + } ex := exception.Err(err, 403) return ex.Code, fmt.Errorf("%s", ex.Message) } diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index 109274a4..fdf22edd 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -9,6 +9,11 @@ import ( // SUIs the loaded SUI instances var SUIs = map[string]SUI{} +// DefaultGuardRedirects stores default guard redirect URLs from template configs. +// Key is guard name (e.g. "oauth"), value is redirect URL (e.g. "/dashboard/auth/entry"). +// Registered by template loading (e.g. agent storage) and used by MakeCache as fallback. +var DefaultGuardRedirects = map[string]string{} + // RouteMatchers the route matchers for the SUI instance var RouteMatchers = map[*regexp.Regexp][][]*Matcher{} diff --git a/sui/core/types.go b/sui/core/types.go index 91bf556e..38545155 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -185,8 +185,9 @@ type Template struct { GlobalData []byte `json:"-"` Scripts *TemplateScirpts `json:"scripts,omitempty"` Translator string `json:"translator,omitempty"` - BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js - GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js + Config *PageSetting `json:"config,omitempty"` // Default page config (guard, api, etc.) + BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js + GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js } // TemplateScirpts is the struct for the template scripts diff --git a/sui/storages/agent/agent.go b/sui/storages/agent/agent.go index edd4dbbf..d5eb3c95 100644 --- a/sui/storages/agent/agent.go +++ b/sui/storages/agent/agent.go @@ -90,6 +90,12 @@ func (agent *Agent) GetTemplate(id string) (core.ITemplate, error) { } } + // 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) { diff --git a/sui/storages/agent/page.go b/sui/storages/agent/page.go index 067bba4e..f85df7c3 100644 --- a/sui/storages/agent/page.go +++ b/sui/storages/agent/page.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" jsoniter "github.com/json-iterator/go" @@ -157,17 +158,52 @@ func (page *Page) GetConfig() *core.PageConfig { if fs.IsFile(confFile) { content, err := fs.ReadFile(confFile) if err != nil { - return nil + return page.mergeTemplateConfig(nil) } var config core.PageConfig if err := jsoniter.Unmarshal(content, &config); err == nil { p.Config = &config - return p.Config + return page.mergeTemplateConfig(p.Config) } } - return nil + return page.mergeTemplateConfig(nil) +} + +// mergeTemplateConfig merges template default config into page config (page config takes priority). +// Use guard: "-" in page config to explicitly disable guard inheritance. +func (page *Page) mergeTemplateConfig(cfg *core.PageConfig) *core.PageConfig { + tmplConfig := page.tmpl.Template.Config + if tmplConfig == nil { + return cfg + } + + if cfg == nil { + cfg = &core.PageConfig{PageSetting: *tmplConfig} + page.Page.Config = cfg + return cfg + } + + // Merge guard (page config takes priority, "-" means explicitly no guard) + if cfg.Guard == "" { + // Page has no guard, use template's guard (with redirect) + cfg.Guard = tmplConfig.Guard + } else if !strings.Contains(cfg.Guard, ":") && strings.Contains(tmplConfig.Guard, ":") { + // Page has guard without redirect (e.g. "oauth"), template has redirect (e.g. "oauth:/login") + // Inherit redirect from template if same guard type + tmplParts := strings.SplitN(tmplConfig.Guard, ":", 2) + if tmplParts[0] == cfg.Guard { + cfg.Guard = tmplConfig.Guard + } + } + + // Merge API guard config + if cfg.API == nil && tmplConfig.API != nil { + cfg.API = tmplConfig.API + } + + return cfg } // SaveTemp save the page temporarily (not supported for agent pages) @@ -293,6 +329,9 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp } } + // Merge template default config before compile (page config takes priority) + page.GetConfig() + html, config, warnings, err := page.Page.Compile(ctx, option) if err != nil { return warnings, fmt.Errorf("Compile the page %s error: %s", page.Route, err.Error()) @@ -444,6 +483,9 @@ func (page *Page) Trans(globalCtx *core.GlobalBuildContext, option *core.BuildOp warnings := []string{} ctx := core.NewBuildContext(globalCtx) + // Merge template default config before compile + page.GetConfig() + _, _, messages, err := page.Page.Compile(ctx, option) if err != nil { return warnings, err From c7cdfbdfe6f416d2d8df2006ca399f58a2b05676 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 17:56:54 +0800 Subject: [PATCH 2/6] Refactor error handling in middleware and clean up template struct comments - Adjust error handling in the middleware to ensure proper redirection for HTTP status codes 301 and 302. - Clean up comments in the Template struct for better readability and consistency. --- service/middleware.go | 6 +++--- sui/core/types.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/service/middleware.go b/service/middleware.go index 7d3772c1..6589c92c 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -91,9 +91,9 @@ func withStaticFileServer(c *gin.Context) { html, code, err := r.Render() if err != nil { - if code == 301 || code == 302 { - url := err.Error() - c.Redirect(code, url) + if code == 301 || code == 302 { + url := err.Error() + c.Redirect(code, url) c.Done() return } diff --git a/sui/core/types.go b/sui/core/types.go index 38545155..2f43fa25 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -185,9 +185,9 @@ type Template struct { GlobalData []byte `json:"-"` Scripts *TemplateScirpts `json:"scripts,omitempty"` Translator string `json:"translator,omitempty"` - Config *PageSetting `json:"config,omitempty"` // Default page config (guard, api, etc.) - BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js - GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js + Config *PageSetting `json:"config,omitempty"` // Default page config (guard, api, etc.) + BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js + GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js } // TemplateScirpts is the struct for the template scripts From 6c51e74b02de7a4b73843709716cda9ba4d11dd2 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 18:48:27 +0800 Subject: [PATCH 3/6] Refactor start command and enhance tool availability reporting - Remove unnecessary timing and progress callback logic from the start command in `cmd/start.go`. - Update the output message for the admin URL to reflect a change to the dashboard URL. - Introduce a new tools configuration in `widgets/app/app.go` to report the availability of external tools like FFmpeg, Docker, and others, enhancing the application's tool inspection capabilities. --- cmd/start.go | 22 ++-------------------- widgets/app/app.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index d27df94a..597b88aa 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -7,7 +7,6 @@ import ( "path/filepath" "strings" "syscall" - "time" "github.com/fatih/color" "github.com/spf13/cobra" @@ -82,32 +81,15 @@ var startCmd = &cobra.Command{ config.Development() } - startTime := time.Now() - // load the application engine - var progressCallback func(string, string) - if config.Conf.Mode == "development" { - fmt.Println(color.CyanString("Loading application engine...")) - progressCallback = func(name string, duration string) { - fmt.Printf(" %s %s %s\n", color.GreenString("✓"), name, color.GreenString("(%s)", duration)) - } - } - loadWarnings, err := engine.Load(config.Conf, engine.LoadOption{ Action: "start", - }, progressCallback) + }) if err != nil { fmt.Println(color.RedString(L("Load: %s"), err.Error())) os.Exit(1) } - loadDuration := time.Since(startTime) - if config.Conf.Mode == "development" { - fmt.Printf("\n%s Engine loaded successfully in %s\n\n", - color.GreenString("✓"), - color.CyanString("%v", loadDuration)) - } - port := fmt.Sprintf(":%d", config.Conf.Port) if port == ":80" { port = "" @@ -209,7 +191,7 @@ var startCmd = &cobra.Command{ fmt.Println(color.CyanString("\n%s", endpoint.Interface)) fmt.Println(color.WhiteString("--------------------------")) fmt.Println(color.WhiteString(L("Website")), color.GreenString(" %s", endpoint.URL)) - fmt.Println(color.WhiteString(L("Admin")), color.GreenString(" %s/%s/login/admin", endpoint.URL, strings.Trim(root, "/"))) + fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" %s/%s/auth/entry", endpoint.URL, strings.Trim(root, "/"))) fmt.Println(color.WhiteString(L("API")), color.GreenString(" %s/api", endpoint.URL)) } fmt.Println("") diff --git a/widgets/app/app.go b/widgets/app/app.go index 3dc77881..36eb98cf 100644 --- a/widgets/app/app.go +++ b/widgets/app/app.go @@ -580,6 +580,36 @@ func processXgen(process *process.Process) interface{} { // agentConfig["connectors"] = connector.AIConnectors } + // External tools availability (safe subset for frontend) + toolsConfig := map[string]interface{}{} + if share.Tools != nil { + safeTool := func(info *share.ExtToolInfo) map[string]interface{} { + if info == nil { + return map[string]interface{}{"available": false} + } + return map[string]interface{}{ + "available": info.Available, + "name": info.Name, + } + } + toolsConfig["ffmpeg"] = safeTool(share.Tools.FFmpeg) + toolsConfig["ffprobe"] = safeTool(share.Tools.FFprobe) + toolsConfig["pdftoppm"] = safeTool(share.Tools.Pdftoppm) + toolsConfig["mutool"] = safeTool(share.Tools.Mutool) + toolsConfig["imagemagick"] = safeTool(share.Tools.ImageMagick) + + if share.Tools.Docker != nil { + docker := map[string]interface{}{ + "available": share.Tools.Docker.Available, + "name": "docker", + } + if share.Tools.Docker.Mode != "" { + docker["mode"] = share.Tools.Docker.Mode + } + toolsConfig["docker"] = docker + } + } + // OpenAPI Settings openapiConfig := map[string]interface{}{} if openapi.Server != nil { @@ -688,6 +718,7 @@ func processXgen(process *process.Process) interface{} { "optional": Setting.Optional, "login": xgenLogin, "agent": agentConfig, + "tools": toolsConfig, "openapi": openapiConfig, "kb": kbConfig, } From 71eab0689e2c3c269e494ac6132e122807207ebd Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 18:54:17 +0800 Subject: [PATCH 4/6] Enhance API output formatting in start command - Update the output for API and OpenAPI endpoints in `cmd/start.go` to conditionally display the correct URL based on the OpenAPI server status. - Remove redundant OpenAPI mode information display when the OpenAPI server is enabled, streamlining the API list output. --- cmd/start.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index 597b88aa..9b221474 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -187,12 +187,20 @@ var startCmd = &cobra.Command{ fmt.Println(color.WhiteString("\n---------------------------------")) fmt.Println(color.WhiteString(L("Access Points"))) fmt.Println(color.WhiteString("---------------------------------")) + apiRoot := "/api" + if openapi.Server != nil { + apiRoot = openapi.Server.Config.BaseURL + } for _, endpoint := range endpoints { fmt.Println(color.CyanString("\n%s", endpoint.Interface)) fmt.Println(color.WhiteString("--------------------------")) fmt.Println(color.WhiteString(L("Website")), color.GreenString(" %s", endpoint.URL)) fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" %s/%s/auth/entry", endpoint.URL, strings.Trim(root, "/"))) - fmt.Println(color.WhiteString(L("API")), color.GreenString(" %s/api", endpoint.URL)) + if openapi.Server != nil { + fmt.Println(color.WhiteString(L("OpenAPI")), color.GreenString(" %s%s", endpoint.URL, apiRoot)) + } else { + fmt.Println(color.WhiteString(L("API")), color.GreenString(" %s%s", endpoint.URL, apiRoot)) + } } fmt.Println("") @@ -454,17 +462,15 @@ func printApis(silent bool) { return } + // Skip detailed API list when OpenAPI is enabled + if openapi.Server != nil { + return + } + fmt.Println(color.WhiteString("\n---------------------------------")) fmt.Println(color.WhiteString(L("APIs List"))) fmt.Println(color.WhiteString("---------------------------------")) - // Show OpenAPI mode info if enabled - if openapi.Server != nil { - fmt.Println(color.CyanString("\nOpenAPI Mode: %s", apiRoot)) - fmt.Println(color.WhiteString("Developer APIs: %s/api/*", apiRoot)) - fmt.Println(color.WhiteString("Widgets: %s/__yao/*", apiRoot)) - } - for _, api := range api.APIs { // API info if len(api.HTTP.Paths) <= 0 { continue From a8fc21d070cdf1529291820e4e2de400a3b69638 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 21:30:52 +0800 Subject: [PATCH 5/6] Add initial ChunkToolCall handling in Anthropic provider - Implement functionality to send an initial ChunkToolCall with the event's ID and function name, aligning with OpenAI's format for tool name resolution. - Enhance message tracking by incrementing the chunk count after sending the tool call data, improving the overall message handling process in the streamWithRetry function. --- agent/llm/providers/anthropic/anthropic.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/agent/llm/providers/anthropic/anthropic.go b/agent/llm/providers/anthropic/anthropic.go index a1843d5c..2c9aec9c 100644 --- a/agent/llm/providers/anthropic/anthropic.go +++ b/agent/llm/providers/anthropic/anthropic.go @@ -362,6 +362,23 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess Index: event.Index, } startToolCallMessage(msgTracker, toolCallInfo, handler) + + // Send initial ChunkToolCall with id and function name + // to match OpenAI format so CUI can resolve tool name from stored chunks + if handler != nil { + toolCallData, _ := jsoniter.Marshal([]map[string]interface{}{ + { + "index": event.Index, + "id": event.ContentBlock.ID, + "type": "function", + "function": map[string]interface{}{ + "name": event.ContentBlock.Name, + }, + }, + }) + handler(message.ChunkToolCall, toolCallData) + incrementChunk(msgTracker) + } } } From 8a3dd148c763234368955e49a01851b26d6dbc51 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Feb 2026 21:31:16 +0800 Subject: [PATCH 6/6] Refactor ChunkToolCall handling in Anthropic provider - Clean up the code for sending the initial ChunkToolCall, improving readability and maintaining alignment with OpenAI's format for tool name resolution. - Ensure that the chunk count is incremented after sending the tool call data, enhancing the message handling process in the streamWithRetry function. --- agent/llm/providers/anthropic/anthropic.go | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/agent/llm/providers/anthropic/anthropic.go b/agent/llm/providers/anthropic/anthropic.go index 2c9aec9c..decfb949 100644 --- a/agent/llm/providers/anthropic/anthropic.go +++ b/agent/llm/providers/anthropic/anthropic.go @@ -363,22 +363,22 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess } startToolCallMessage(msgTracker, toolCallInfo, handler) - // Send initial ChunkToolCall with id and function name - // to match OpenAI format so CUI can resolve tool name from stored chunks - if handler != nil { - toolCallData, _ := jsoniter.Marshal([]map[string]interface{}{ - { - "index": event.Index, - "id": event.ContentBlock.ID, - "type": "function", - "function": map[string]interface{}{ - "name": event.ContentBlock.Name, + // Send initial ChunkToolCall with id and function name + // to match OpenAI format so CUI can resolve tool name from stored chunks + if handler != nil { + toolCallData, _ := jsoniter.Marshal([]map[string]interface{}{ + { + "index": event.Index, + "id": event.ContentBlock.ID, + "type": "function", + "function": map[string]interface{}{ + "name": event.ContentBlock.Name, + }, }, - }, - }) - handler(message.ChunkToolCall, toolCallData) - incrementChunk(msgTracker) - } + }) + handler(message.ChunkToolCall, toolCallData) + incrementChunk(msgTracker) + } } }