Merge pull request #1468 from trheyi/main
Enhance guard handling and template configuration merging
This commit is contained in:
commit
846acd1882
10 changed files with 172 additions and 59 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
44
cmd/start.go
44
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 = ""
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue