Merge pull request #1468 from trheyi/main

Enhance guard handling and template configuration merging
This commit is contained in:
Max 2026-02-16 16:57:13 +08:00 committed by GitHub
commit 846acd1882
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 172 additions and 59 deletions

View file

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

View file

@ -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 = ""
@ -205,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("Admin")), color.GreenString(" %s/%s/login/admin", endpoint.URL, strings.Trim(root, "/")))
fmt.Println(color.WhiteString(L("API")), color.GreenString(" %s/api", endpoint.URL))
fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" %s/%s/auth/entry", endpoint.URL, strings.Trim(root, "/")))
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("")
@ -472,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

View file

@ -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"
@ -94,12 +93,17 @@ func withStaticFileServer(c *gin.Context) {
if err != nil {
if code == 301 || code == 302 {
url := err.Error()
// fmt.Println("Redirect to: ", url)
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
}
log.Error("Sui Render Error: %s", err.Error())
c.AbortWithStatusJSON(code, gin.H{"code": code, "message": err.Error()})
return

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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