Enhance OpenAPI Integration and API Routing
- Introduced support for OpenAPI mode, allowing dynamic routing and OAuth guards for API endpoints. - Updated API root path handling to accommodate OpenAPI configurations, ensuring consistent URL structures. - Added well-known routes for Yao metadata and OAuth discovery, improving API discoverability. - Refactored middleware and guards to streamline OpenAPI integration, enhancing overall service functionality. - Removed deprecated agent TypeScript file, simplifying the codebase and improving maintainability.
This commit is contained in:
parent
05cb4b9199
commit
787954f0af
17 changed files with 597 additions and 823 deletions
19
cmd/start.go
19
cmd/start.go
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/engine"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
ischedule "github.com/yaoapp/yao/schedule"
|
||||
"github.com/yaoapp/yao/service"
|
||||
"github.com/yaoapp/yao/setup"
|
||||
|
|
@ -470,6 +471,11 @@ func printTasks(silent bool) {
|
|||
}
|
||||
|
||||
func printApis(silent bool) {
|
||||
// Determine API root based on OpenAPI mode
|
||||
apiRoot := "/api"
|
||||
if openapi.Server != nil {
|
||||
apiRoot = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
if silent {
|
||||
for _, api := range api.APIs {
|
||||
|
|
@ -478,7 +484,7 @@ func printApis(silent bool) {
|
|||
}
|
||||
log.Info("[API] %s(%d)", api.ID, len(api.HTTP.Paths))
|
||||
for _, p := range api.HTTP.Paths {
|
||||
log.Info("%s %s %s", p.Method, filepath.Join("/api", api.HTTP.Group, p.Path), p.Process)
|
||||
log.Info("%s %s %s", p.Method, filepath.Join(apiRoot, api.HTTP.Group, p.Path), p.Process)
|
||||
}
|
||||
}
|
||||
for name, upgrader := range websocket.Upgraders { // WebSocket
|
||||
|
|
@ -491,7 +497,14 @@ func printApis(silent bool) {
|
|||
fmt.Println(color.WhiteString(L("APIs List")))
|
||||
fmt.Println(color.WhiteString("---------------------------------"))
|
||||
|
||||
for _, api := range api.APIs { // API信息
|
||||
// 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
|
||||
}
|
||||
|
|
@ -505,7 +518,7 @@ func printApis(silent bool) {
|
|||
for _, p := range api.HTTP.Paths {
|
||||
fmt.Println(
|
||||
colorMehtod(p.Method),
|
||||
color.WhiteString(filepath.Join("/api", api.HTTP.Group, p.Path)),
|
||||
color.WhiteString(filepath.Join(apiRoot, api.HTTP.Group, p.Path)),
|
||||
"\tprocess:", p.Process)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
355
data/bindata.go
355
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -440,13 +440,40 @@ func (m *ScopeManager) addEndpointRule(method, path, action string, scopes []str
|
|||
|
||||
// Classify path type and add to appropriate index
|
||||
if strings.Contains(path, "*") {
|
||||
// Wildcard path
|
||||
// Wildcard path - merge with existing if present (support multiple scopes per endpoint)
|
||||
prefix := strings.TrimSuffix(path, "*")
|
||||
matcher.wildcardPaths = append(matcher.wildcardPaths, &WildcardPath{
|
||||
Pattern: path,
|
||||
Prefix: prefix,
|
||||
Endpoint: info,
|
||||
})
|
||||
merged := false
|
||||
for _, existing := range matcher.wildcardPaths {
|
||||
if existing.Pattern == path {
|
||||
// Endpoint already exists, merge scopes and constraints
|
||||
existing.Endpoint.RequiredScopes = append(existing.Endpoint.RequiredScopes, info.RequiredScopes...)
|
||||
existing.Endpoint.OwnerOnly = existing.Endpoint.OwnerOnly || info.OwnerOnly
|
||||
existing.Endpoint.CreatorOnly = existing.Endpoint.CreatorOnly || info.CreatorOnly
|
||||
existing.Endpoint.EditorOnly = existing.Endpoint.EditorOnly || info.EditorOnly
|
||||
existing.Endpoint.TeamOnly = existing.Endpoint.TeamOnly || info.TeamOnly
|
||||
if info.Extra != nil {
|
||||
if existing.Endpoint.Extra == nil {
|
||||
existing.Endpoint.Extra = make(map[string]interface{})
|
||||
}
|
||||
for key, value := range info.Extra {
|
||||
existing.Endpoint.Extra[key] = deepCopyValue(value)
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Merged wildcard endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, existing.Endpoint.RequiredScopes, existing.Endpoint.OwnerOnly, existing.Endpoint.TeamOnly)
|
||||
merged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !merged {
|
||||
matcher.wildcardPaths = append(matcher.wildcardPaths, &WildcardPath{
|
||||
Pattern: path,
|
||||
Prefix: prefix,
|
||||
Endpoint: info,
|
||||
})
|
||||
log.Trace("[ACL] Added wildcard endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, info.RequiredScopes, info.OwnerOnly, info.TeamOnly)
|
||||
}
|
||||
} else if strings.Contains(path, ":") {
|
||||
// Parameter path - merge with existing if present (support multiple scopes per endpoint)
|
||||
if existing := matcher.paramPaths[path]; existing != nil {
|
||||
|
|
@ -718,25 +745,37 @@ func (m *ScopeManager) matchParameterPath(pattern, path string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// expandUserScopes expands user scopes by resolving aliases
|
||||
// expandUserScopes expands user scopes by resolving aliases recursively
|
||||
// This allows nested aliases like: system:root -> *:*:* -> matches any scope
|
||||
func (m *ScopeManager) expandUserScopes(scopes []string) []string {
|
||||
var expanded []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, scope := range scopes {
|
||||
// Use a queue for iterative expansion to avoid deep recursion
|
||||
queue := make([]string, len(scopes))
|
||||
copy(queue, scopes)
|
||||
|
||||
for len(queue) > 0 {
|
||||
scope := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
// Skip if already processed
|
||||
if seen[scope] {
|
||||
continue
|
||||
}
|
||||
seen[scope] = true
|
||||
|
||||
// Check if it's an alias
|
||||
if aliasScopes := m.aliasIndex[scope]; aliasScopes != nil {
|
||||
// Add alias expansions to queue for further processing
|
||||
for _, s := range aliasScopes {
|
||||
if !seen[s] {
|
||||
expanded = append(expanded, s)
|
||||
seen[s] = true
|
||||
queue = append(queue, s)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !seen[scope] {
|
||||
expanded = append(expanded, scope)
|
||||
seen[scope] = true
|
||||
}
|
||||
// Not an alias, add to expanded list
|
||||
expanded = append(expanded, scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -745,9 +784,15 @@ func (m *ScopeManager) expandUserScopes(scopes []string) []string {
|
|||
|
||||
// matchesWildcardScope checks if a user scope (potentially with wildcards) matches a required scope
|
||||
// Supports patterns like:
|
||||
// - *:*:* matches everything
|
||||
// - *:*:* matches any 3-part scope (e.g., sui:run:execute)
|
||||
// - *:*:*:* matches any 4-part scope (e.g., sui:run:execute:all)
|
||||
// - *:*:*:*:* matches any 5-part scope
|
||||
// - resource:*:* matches resource:action:level
|
||||
// - resource:action:* matches resource:action:level
|
||||
// - resource:*:*:* matches resource:action:level:sublevel
|
||||
//
|
||||
// The wildcard pattern must have the same number of parts as the required scope.
|
||||
// Use multiple wildcard patterns in alias.yml to cover different scope depths.
|
||||
func (m *ScopeManager) matchesWildcardScope(userScope, requiredScope string) bool {
|
||||
// No wildcard, no match (exact match already checked)
|
||||
if !strings.Contains(userScope, "*") {
|
||||
|
|
@ -758,7 +803,7 @@ func (m *ScopeManager) matchesWildcardScope(userScope, requiredScope string) boo
|
|||
userParts := strings.Split(userScope, ":")
|
||||
requiredParts := strings.Split(requiredScope, ":")
|
||||
|
||||
// If lengths don't match and user scope isn't full wildcard, no match
|
||||
// Lengths must match for wildcard matching
|
||||
if len(userParts) != len(requiredParts) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package openapi
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/share"
|
||||
)
|
||||
|
||||
// attachWellKnown attaches the well-known handlers to the router
|
||||
func (openapi *OpenAPI) attachWellKnown(router *gin.Engine) {
|
||||
|
|
@ -8,6 +11,9 @@ func (openapi *OpenAPI) attachWellKnown(router *gin.Engine) {
|
|||
// OAuth Discovery and Metadata Endpoints
|
||||
wellKnown := router.Group("/.well-known")
|
||||
|
||||
// Yao Configuration Metadata - for client discovery
|
||||
wellKnown.GET("/yao", openapi.yaoMetadata)
|
||||
|
||||
// OAuth Authorization Server Metadata - RFC 8414 (Required by MCP)
|
||||
wellKnown.GET("/oauth-authorization-server", openapi.oauthServerMetadata)
|
||||
|
||||
|
|
@ -18,6 +24,51 @@ func (openapi *OpenAPI) attachWellKnown(router *gin.Engine) {
|
|||
wellKnown.GET("/oauth-protected-resource", openapi.oauthProtectedResourceMetadata)
|
||||
}
|
||||
|
||||
// YaoMetadata represents the Yao server configuration metadata
|
||||
type YaoMetadata struct {
|
||||
// Application information
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// OpenAPI configuration
|
||||
OpenAPI string `json:"openapi"` // OpenAPI base URL (e.g., "/v1")
|
||||
IssuerURL string `json:"issuer_url,omitempty"` // OAuth issuer URL
|
||||
|
||||
// Dashboard configuration
|
||||
Dashboard string `json:"dashboard,omitempty"` // Admin dashboard root path
|
||||
Optional map[string]interface{} `json:"optional,omitempty"` // Optional settings
|
||||
|
||||
// Developer information
|
||||
Developer *share.Developer `json:"developer,omitempty"`
|
||||
}
|
||||
|
||||
// yaoMetadata returns Yao server configuration metadata
|
||||
func (openapi *OpenAPI) yaoMetadata(c *gin.Context) {
|
||||
// Get admin root path
|
||||
dashboard := share.App.AdminRoot
|
||||
if dashboard == "" {
|
||||
dashboard = "yao"
|
||||
}
|
||||
|
||||
metadata := YaoMetadata{
|
||||
Name: share.App.Name,
|
||||
Version: share.App.Version,
|
||||
Description: share.App.Description,
|
||||
OpenAPI: openapi.Config.BaseURL,
|
||||
IssuerURL: openapi.Config.OAuth.IssuerURL,
|
||||
Dashboard: "/" + dashboard,
|
||||
Optional: share.App.Optional,
|
||||
}
|
||||
|
||||
// Include developer info if available
|
||||
if share.App.Developer.ID != "" || share.App.Developer.Name != "" {
|
||||
metadata.Developer = &share.App.Developer
|
||||
}
|
||||
|
||||
c.JSON(200, metadata)
|
||||
}
|
||||
|
||||
// oauthServerMetadata returns authorization server metadata - RFC 8414
|
||||
func (openapi *OpenAPI) oauthServerMetadata(c *gin.Context) {}
|
||||
|
||||
|
|
|
|||
87
service/dynamic.go
Normal file
87
service/dynamic.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/api"
|
||||
)
|
||||
|
||||
// DynamicAPIHandler is a dynamic API proxy handler that dispatches requests
|
||||
// to the appropriate handler based on the route table.
|
||||
// This enables hot-reloading of API definitions without server restart.
|
||||
func DynamicAPIHandler(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
method := c.Request.Method
|
||||
|
||||
// Ensure path starts with /
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
// Find handler from route table
|
||||
apiDef, pathDef, handler, params, err := api.FindHandler(method, path)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"code": 404, "message": "API not found"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set path parameters to gin.Context
|
||||
for key, value := range params {
|
||||
c.Params = append(c.Params, gin.Param{Key: key, Value: value})
|
||||
}
|
||||
|
||||
// Apply guard
|
||||
guard := pathDef.Guard
|
||||
if guard == "" {
|
||||
guard = apiDef.HTTP.Guard
|
||||
}
|
||||
|
||||
if guard != "" && guard != "-" {
|
||||
if err := applyGuard(c, guard); err != nil {
|
||||
return // Guard already handled the response
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the actual handler
|
||||
handler(c)
|
||||
}
|
||||
|
||||
// applyGuard applies the guard middleware(s) to the request
|
||||
func applyGuard(c *gin.Context, guardName string) error {
|
||||
guards := strings.Split(guardName, ",")
|
||||
for _, name := range guards {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get guard from HTTPGuards (set at Start time)
|
||||
if handler, has := api.HTTPGuards[name]; has {
|
||||
handler(c)
|
||||
if c.IsAborted() {
|
||||
return fmt.Errorf("guard aborted")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Custom guard via process
|
||||
api.ProcessGuard(name)(c)
|
||||
if c.IsAborted() {
|
||||
return fmt.Errorf("guard aborted")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadAPIs reloads all API definitions from the apis directory
|
||||
// This function is thread-safe and can be called at runtime
|
||||
func ReloadAPIs() error {
|
||||
err := api.ReloadAPIs("apis")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
83
service/dynamic_test.go
Normal file
83
service/dynamic_test.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package service_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/gou/api"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/engine"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/service"
|
||||
)
|
||||
|
||||
func TestDynamicAPIHandler(t *testing.T) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
cfg := config.Conf
|
||||
cfg.Port = 0
|
||||
_, err := engine.Load(cfg, engine.LoadOption{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Temporarily disable OpenAPI for this test
|
||||
savedOpenAPIServer := openapi.Server
|
||||
openapi.Server = nil
|
||||
defer func() { openapi.Server = savedOpenAPIServer }()
|
||||
|
||||
// Set up guards
|
||||
api.SetGuards(service.Guards)
|
||||
|
||||
// Load and build route table
|
||||
api.BuildRouteTable()
|
||||
|
||||
// Create test router
|
||||
router := gin.New()
|
||||
router.Any("/api/*path", service.DynamicAPIHandler)
|
||||
|
||||
// Test: API not found
|
||||
response := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/nonexistent", nil)
|
||||
router.ServeHTTP(response, req)
|
||||
assert.Equal(t, 404, response.Code)
|
||||
|
||||
// Test: Exact match (app setting API)
|
||||
response = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/api/__yao/app/setting", nil)
|
||||
router.ServeHTTP(response, req)
|
||||
// Note: This may return 403 if guard is not satisfied, which is expected
|
||||
assert.True(t, response.Code == 200 || response.Code == 403)
|
||||
}
|
||||
|
||||
func TestReloadAPIs(t *testing.T) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
cfg := config.Conf
|
||||
cfg.Port = 0
|
||||
_, err := engine.Load(cfg, engine.LoadOption{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initial build
|
||||
api.BuildRouteTable()
|
||||
|
||||
// Reload should not error
|
||||
err = service.ReloadAPIs()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGuardSelection(t *testing.T) {
|
||||
// Verify traditional Guards exist
|
||||
assert.NotNil(t, service.Guards["bearer-jwt"])
|
||||
assert.NotNil(t, service.Guards["cookie-jwt"])
|
||||
assert.NotNil(t, service.Guards["cross-origin"])
|
||||
|
||||
// Note: OpenAPIGuards() requires oauth.OAuth to be initialized,
|
||||
// which happens during engine load with OpenAPI config.
|
||||
// The guard mapping is tested implicitly through integration tests.
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/yaoapp/yao/helper"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
|
||||
"github.com/yaoapp/yao/widgets/chart"
|
||||
|
|
@ -16,7 +15,7 @@ import (
|
|||
"github.com/yaoapp/yao/widgets/table"
|
||||
)
|
||||
|
||||
// Guards middlewares
|
||||
// Guards middlewares for traditional JWT mode
|
||||
var Guards = map[string]gin.HandlerFunc{
|
||||
"bearer-jwt": guardBearerJWT, // Bearer JWT
|
||||
"query-jwt": guardQueryJWT, // Get JWT Token from query string "__tk"
|
||||
|
|
@ -30,6 +29,24 @@ var Guards = map[string]gin.HandlerFunc{
|
|||
"widget-dashboard": dashboard.Guard, // Widget Dashboard Guard
|
||||
}
|
||||
|
||||
// OpenAPIGuards returns middlewares for OpenAPI OAuth mode
|
||||
// All JWT-related guards are mapped to OAuth for backward compatibility
|
||||
// This is a function because oauth.OAuth is initialized at runtime
|
||||
func OpenAPIGuards() map[string]gin.HandlerFunc {
|
||||
return map[string]gin.HandlerFunc{
|
||||
"bearer-jwt": oauth.OAuth.Guard, // JWT -> OAuth
|
||||
"query-jwt": oauth.OAuth.Guard, // JWT -> OAuth
|
||||
"cookie-jwt": oauth.OAuth.Guard, // JWT -> OAuth
|
||||
"cookie-trace": oauth.OAuth.Guard, // Session -> OAuth (OAuth manages sessions)
|
||||
"cross-origin": guardCrossOrigin, // CORS remains unchanged
|
||||
"widget-table": table.Guard, // Widget Guard remains unchanged
|
||||
"widget-list": list.Guard, // Widget List Guard
|
||||
"widget-form": form.Guard, // Widget Form Guard
|
||||
"widget-chart": chart.Guard, // Widget Chart Guard
|
||||
"widget-dashboard": dashboard.Guard, // Widget Dashboard Guard
|
||||
}
|
||||
}
|
||||
|
||||
// guardCookieTrace set sid cookie
|
||||
func guardCookieTrace(c *gin.Context) {
|
||||
sid, err := c.Cookie("sid")
|
||||
|
|
@ -43,16 +60,8 @@ func guardCookieTrace(c *gin.Context) {
|
|||
c.Set("__sid", sid)
|
||||
}
|
||||
|
||||
// Cookie Cookie JWT
|
||||
// guardCookieJWT validates JWT token from cookie
|
||||
func guardCookieJWT(c *gin.Context) {
|
||||
|
||||
// OpenAPI OAuth
|
||||
if openapi.Server != nil {
|
||||
guardOpenapiOauth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
tokenString, err := c.Cookie("__tk")
|
||||
if err != nil {
|
||||
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
|
||||
|
|
@ -70,16 +79,8 @@ func guardCookieJWT(c *gin.Context) {
|
|||
c.Set("__sid", claims.SID)
|
||||
}
|
||||
|
||||
// JWT Bearer JWT
|
||||
// guardBearerJWT validates Bearer JWT token from Authorization header
|
||||
func guardBearerJWT(c *gin.Context) {
|
||||
|
||||
// OpenAPI OAuth
|
||||
if openapi.Server != nil {
|
||||
guardOpenapiOauth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
tokenString := c.Request.Header.Get("Authorization")
|
||||
tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer "))
|
||||
if tokenString == "" {
|
||||
|
|
@ -117,52 +118,3 @@ func guardCrossOrigin(c *gin.Context) {
|
|||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// Openapi Oauth
|
||||
func guardOpenapiOauth(c *gin.Context) {
|
||||
s := oauth.OAuth
|
||||
token := getAccessToken(c)
|
||||
if token == "" {
|
||||
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the token
|
||||
_, err := s.VerifyToken(token)
|
||||
if err != nil {
|
||||
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the session ID
|
||||
sid := getSessionID(c)
|
||||
if sid == "" {
|
||||
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("__sid", sid)
|
||||
}
|
||||
|
||||
func getAccessToken(c *gin.Context) string {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" || token == "Bearer undefined" {
|
||||
cookie, err := c.Cookie("__Host-access_token")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
token = cookie
|
||||
}
|
||||
return strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
func getSessionID(c *gin.Context) string {
|
||||
sid, err := c.Cookie("__Host-session_id")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return sid
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,16 @@ func withStaticFileServer(c *gin.Context) {
|
|||
|
||||
// Handle OpenAPI server
|
||||
if openapi.Server != nil && openapi.Server.Config != nil && openapi.Server.Config.BaseURL != "" {
|
||||
// OpenAPI base URL routes
|
||||
if strings.HasPrefix(c.Request.URL.Path, openapi.Server.Config.BaseURL+"/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
// Well-known routes (OAuth discovery, Yao metadata, etc.)
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/.well-known/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API & websocket
|
||||
|
|
|
|||
|
|
@ -25,21 +25,39 @@ func Start(cfg config.Config) (*http.Server, error) {
|
|||
|
||||
router := gin.New()
|
||||
router.Use(Middlewares...)
|
||||
api.SetGuards(Guards)
|
||||
api.SetRoutes(router, "/api", cfg.AllowFrom...)
|
||||
|
||||
var apiRoot string
|
||||
if openapi.Server != nil {
|
||||
// OpenAPI mode: use OAuth guards and dynamic routing
|
||||
apiRoot = openapi.Server.Config.BaseURL
|
||||
api.SetGuards(OpenAPIGuards())
|
||||
|
||||
// Developer APIs: use dynamic proxy (supports hot-reload)
|
||||
router.Any(apiRoot+"/api/*path", DynamicAPIHandler)
|
||||
|
||||
// Widgets and system APIs: static registration
|
||||
api.SetRoutes(router, apiRoot, cfg.AllowFrom...)
|
||||
|
||||
// Build route table for dynamic lookup
|
||||
api.BuildRouteTable()
|
||||
|
||||
// Attach OpenAPI built-in features
|
||||
openapi.Server.Attach(router)
|
||||
} else {
|
||||
// Traditional mode: unchanged
|
||||
apiRoot = "/api"
|
||||
api.SetGuards(Guards)
|
||||
api.SetRoutes(router, "/api", cfg.AllowFrom...)
|
||||
}
|
||||
|
||||
srv := http.New(router, http.Option{
|
||||
Host: cfg.Host,
|
||||
Port: cfg.Port,
|
||||
Root: "/api",
|
||||
Root: apiRoot,
|
||||
Allows: cfg.AllowFrom,
|
||||
Timeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
// OpenAPI Server
|
||||
if openapi.Server != nil {
|
||||
openapi.Server.Attach(router)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = srv.Start()
|
||||
}()
|
||||
|
|
@ -51,8 +69,21 @@ func Start(cfg config.Config) (*http.Server, error) {
|
|||
func Restart(srv *http.Server, cfg config.Config) error {
|
||||
router := gin.New()
|
||||
router.Use(Middlewares...)
|
||||
api.SetGuards(Guards)
|
||||
api.SetRoutes(router, "/api", cfg.AllowFrom...)
|
||||
|
||||
if openapi.Server != nil {
|
||||
// OpenAPI mode
|
||||
baseURL := openapi.Server.Config.BaseURL
|
||||
api.SetGuards(OpenAPIGuards())
|
||||
router.Any(baseURL+"/api/*path", DynamicAPIHandler)
|
||||
api.SetRoutes(router, baseURL, cfg.AllowFrom...)
|
||||
api.BuildRouteTable()
|
||||
openapi.Server.Attach(router)
|
||||
} else {
|
||||
// Traditional mode: unchanged
|
||||
api.SetGuards(Guards)
|
||||
api.SetRoutes(router, "/api", cfg.AllowFrom...)
|
||||
}
|
||||
|
||||
srv.Reset(router)
|
||||
return srv.Restart()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/engine"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
|
|
@ -21,6 +22,11 @@ func TestStartStop(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Temporarily disable OpenAPI for this test
|
||||
savedOpenAPIServer := openapi.Server
|
||||
openapi.Server = nil
|
||||
defer func() { openapi.Server = savedOpenAPIServer }()
|
||||
|
||||
srv, err := Start(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/yaoapp/gou/server/http"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/engine"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
)
|
||||
|
||||
// Watch the application code change for hot update
|
||||
|
|
@ -36,14 +37,25 @@ func Watch(srv *http.Server, interrupt chan uint8) (err error) {
|
|||
fmt.Println(color.GreenString("[Watch] Model: %s changed (Please run yao migrate manually)", name))
|
||||
}
|
||||
|
||||
// Restart
|
||||
// API changes: hot reload or restart
|
||||
if strings.HasPrefix(name, "/apis") {
|
||||
err = Restart(srv, config.Conf)
|
||||
if err != nil {
|
||||
fmt.Println(color.RedString("[Watch] Restart: %s", err.Error()))
|
||||
return
|
||||
if openapi.Server != nil {
|
||||
// OpenAPI mode: hot reload (no server restart needed)
|
||||
err = ReloadAPIs()
|
||||
if err != nil {
|
||||
fmt.Println(color.RedString("[Watch] Reload APIs: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
fmt.Println(color.GreenString("[Watch] APIs Reloaded"))
|
||||
} else {
|
||||
// Traditional mode: restart server
|
||||
err = Restart(srv, config.Conf)
|
||||
if err != nil {
|
||||
fmt.Println(color.RedString("[Watch] Restart: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
fmt.Println(color.GreenString("[Watch] Restart Completed"))
|
||||
}
|
||||
fmt.Println(color.GreenString("[Watch] Restart Completed"))
|
||||
}
|
||||
|
||||
}, interrupt)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ var dsl = []byte(`
|
|||
"label": "Run",
|
||||
"description": "Run the backend script, with Api prefix method",
|
||||
"path": "/run/*route",
|
||||
"guard": "-",
|
||||
"method": "POST",
|
||||
"process": "sui.Run",
|
||||
"in": [":context", "$param.route", ":payload"],
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ func Run(process *process.Process) interface{} {
|
|||
}
|
||||
defer scriptCtx.Close()
|
||||
|
||||
// Pass authorized info to V8 context
|
||||
if authorized := process.GetAuthorized(); authorized != nil {
|
||||
if authMap := authorized.AuthorizedToMap(); len(authMap) > 0 {
|
||||
scriptCtx.WithAuthorized(authMap)
|
||||
}
|
||||
}
|
||||
|
||||
global := scriptCtx.Global()
|
||||
if !global.Has(prefix + method) {
|
||||
exception.New("Method %s not found", 500, method).Throw()
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ func LibSUI() ([]byte, []byte, error) {
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Read agent source code from bindata
|
||||
agent, err := data.Read("libsui/agent.ts")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Read openapi source code from bindata
|
||||
openapi, err := data.Read("libsui/openapi.ts")
|
||||
if err != nil {
|
||||
|
|
@ -42,7 +36,7 @@ func LibSUI() ([]byte, []byte, error) {
|
|||
}
|
||||
|
||||
// Merge the source code
|
||||
source := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", index, utils, yao, agent, openapi)
|
||||
source := fmt.Sprintf("%s\n%s\n%s\n%s", index, utils, yao, openapi)
|
||||
|
||||
// Build the source code
|
||||
js, sm, err := transform.TypeScriptWithSourceMap(string(source), api.TransformOptions{
|
||||
|
|
|
|||
|
|
@ -1,501 +0,0 @@
|
|||
/**
|
||||
* Yao AI Agent Pure JavaScript SDK
|
||||
* @author Max<max@iqka.com>
|
||||
* @maintainer https://yaoapps.com
|
||||
*/
|
||||
|
||||
/**
|
||||
* Message structure for agent responses
|
||||
*/
|
||||
interface AgentMessage {
|
||||
text: string;
|
||||
type?: string;
|
||||
done?: boolean;
|
||||
is_neo?: boolean;
|
||||
assistant_id?: string;
|
||||
assistant_name?: string;
|
||||
assistant_avatar?: string;
|
||||
props?: Record<string, any>;
|
||||
tool_id?: string;
|
||||
new?: boolean;
|
||||
delta?: boolean;
|
||||
result?: any;
|
||||
previous_assistant_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Done event data structure
|
||||
*/
|
||||
type AgentDoneData = AgentMessage[];
|
||||
|
||||
/**
|
||||
* Event handler function types
|
||||
*/
|
||||
interface MessageHandler {
|
||||
(message: AgentMessage): void;
|
||||
}
|
||||
|
||||
interface DoneHandler {
|
||||
(messages: AgentDoneData): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event types that can be listened to
|
||||
*/
|
||||
type AgentEvent = "message" | "done";
|
||||
|
||||
/**
|
||||
* Event handlers record type
|
||||
*/
|
||||
interface EventHandlers {
|
||||
message?: MessageHandler;
|
||||
done?: DoneHandler;
|
||||
}
|
||||
|
||||
class Agent {
|
||||
private host: string;
|
||||
private token: string;
|
||||
private events: EventHandlers;
|
||||
private assistant_id: string;
|
||||
private chat_id?: string;
|
||||
private es: EventSource | null;
|
||||
private context: Record<string, any>;
|
||||
private silent?: boolean = false;
|
||||
private history_visible?: boolean = false;
|
||||
|
||||
/**
|
||||
* Agent constructor
|
||||
* @param option Agent initialization options
|
||||
*/
|
||||
constructor(assistant_id: string, option: AgentOption) {
|
||||
this.host = option.host || "/api/__yao/neo";
|
||||
this.token = option.token;
|
||||
this.events = {};
|
||||
this.assistant_id = assistant_id;
|
||||
this.chat_id = option.chat_id;
|
||||
this.es = null;
|
||||
this.context = option.context || {};
|
||||
|
||||
// Set silent mode, default is true
|
||||
if (option.silent !== undefined) {
|
||||
this.silent =
|
||||
option.silent === true ||
|
||||
option.silent === "true" ||
|
||||
option.silent === 1 ||
|
||||
option.silent === "1"
|
||||
? false
|
||||
: true;
|
||||
}
|
||||
|
||||
// Set history visible mode, default is false
|
||||
if (option.history_visible !== undefined) {
|
||||
this.history_visible =
|
||||
option.history_visible === true ||
|
||||
option.history_visible === "true" ||
|
||||
option.history_visible === 1 ||
|
||||
option.history_visible === "1"
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a chat ID
|
||||
* @returns A unique chat ID in the format of chat_[timestamp]_[random]
|
||||
*/
|
||||
private makeChatID(): string {
|
||||
const random = Math.random().toString(36).substring(2, 15);
|
||||
const ts = Date.now();
|
||||
return `chat_${ts}_${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event handler
|
||||
* @param event Event type to listen for ("message" or "done")
|
||||
* @param handler Function to handle the event
|
||||
* @returns The Agent instance for chaining
|
||||
*/
|
||||
On<E extends AgentEvent>(
|
||||
event: E,
|
||||
handler: E extends "message" ? MessageHandler : DoneHandler
|
||||
): Agent {
|
||||
if (event === "message") {
|
||||
this.events.message = handler as MessageHandler;
|
||||
} else if (event === "done") {
|
||||
this.events.done = handler as DoneHandler;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the agent
|
||||
*/
|
||||
Cancel() {
|
||||
if (this.es) {
|
||||
this.es.close();
|
||||
this.es = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the AI Agent
|
||||
* @param input Text message or input object with text and optional attachments
|
||||
* @param args Additional arguments to pass to the agent
|
||||
*/
|
||||
async Call(input: AgentInput, ...args: any[]): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messages: AgentMessage[] = [];
|
||||
let lastAssistant = {
|
||||
assistant_id: null as string | null,
|
||||
assistant_name: null as string | null,
|
||||
assistant_avatar: null as string | null,
|
||||
};
|
||||
|
||||
// Process input content
|
||||
let content: AgentInputContent;
|
||||
if (typeof input === "string") {
|
||||
content = { text: input };
|
||||
} else {
|
||||
content = { text: input.text };
|
||||
if (input.attachments && input.attachments.length > 0) {
|
||||
content.attachments = input.attachments.map((attachment) => ({
|
||||
name: attachment.name,
|
||||
url: attachment.url,
|
||||
type: attachment.type,
|
||||
content_type: attachment.content_type,
|
||||
bytes: attachment.bytes,
|
||||
created_at: attachment.created_at,
|
||||
file_id: attachment.file_id,
|
||||
chat_id: attachment.chat_id,
|
||||
assistant_id: attachment.assistant_id,
|
||||
description: attachment.description,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Add context to the content
|
||||
const context = { ...this.context, args };
|
||||
const contentRaw = encodeURIComponent(JSON.stringify(content));
|
||||
const contextRaw = encodeURIComponent(JSON.stringify(context));
|
||||
const token = this.token;
|
||||
const silent = this.silent ? "true" : "false";
|
||||
const history_visible = this.history_visible ? "true" : "false";
|
||||
const chatId = this.chat_id || this.makeChatID();
|
||||
const assistantParam = `&assistant_id=${this.assistant_id}`;
|
||||
const status_endpoint = `${this.host}/status?content=${contentRaw}&context=${contextRaw}&token=${token}&chat_id=${chatId}${assistantParam}`;
|
||||
const endpoint = `${this.host}?client_type=jssdk&content=${contentRaw}&context=${contextRaw}&token=${token}&silent=${silent}&history_visible=${history_visible}&chat_id=${chatId}${assistantParam}`;
|
||||
|
||||
const handleError = async (error: any) => {
|
||||
try {
|
||||
const response = await fetch(status_endpoint, {
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
|
||||
if (response.status === 200 || response.status === 201) return;
|
||||
|
||||
const data = await response.json().catch(() => ({
|
||||
message: `HTTP ${response.status}`,
|
||||
}));
|
||||
|
||||
let errorMessage = "Network error, please try again later";
|
||||
if (data?.message) {
|
||||
errorMessage = data.message;
|
||||
} else if (error.message?.includes("401")) {
|
||||
errorMessage = "Session expired: Please login again";
|
||||
} else if (error.message?.includes("403")) {
|
||||
errorMessage =
|
||||
"Access denied: Please check your permissions or login again";
|
||||
} else if (error.message?.includes("500")) {
|
||||
errorMessage =
|
||||
"Server error: The service is temporarily unavailable";
|
||||
} else if (error.message?.includes("404")) {
|
||||
errorMessage =
|
||||
"AI service not found: Please check your configuration";
|
||||
} else if (error.name === "TypeError") {
|
||||
errorMessage =
|
||||
"Connection failed: Please check your network connection";
|
||||
}
|
||||
|
||||
const messageHandler = this.events["message"] as MessageHandler;
|
||||
if (messageHandler) {
|
||||
messageHandler({
|
||||
text: errorMessage,
|
||||
type: "error",
|
||||
is_neo: true,
|
||||
done: true,
|
||||
});
|
||||
}
|
||||
|
||||
return reject(new Error(errorMessage));
|
||||
} catch (statusError) {
|
||||
const messageHandler = this.events["message"] as MessageHandler;
|
||||
if (messageHandler) {
|
||||
messageHandler({
|
||||
text: "Service unavailable, please try again later",
|
||||
type: "error",
|
||||
is_neo: true,
|
||||
done: true,
|
||||
});
|
||||
}
|
||||
|
||||
return reject(
|
||||
new Error("Service unavailable, please try again later")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let last_type: string | null = null;
|
||||
const es = new EventSource(endpoint, { withCredentials: true });
|
||||
this.es = es;
|
||||
|
||||
es.onopen = () => {};
|
||||
es.onmessage = ({ data }: { data: string }) => {
|
||||
try {
|
||||
const formated_data = JSON.parse(data);
|
||||
if (!formated_data) return;
|
||||
|
||||
const messageHandler = this.events["message"] as MessageHandler;
|
||||
if (!messageHandler) return;
|
||||
|
||||
const {
|
||||
tool_id,
|
||||
begin,
|
||||
type,
|
||||
end,
|
||||
text,
|
||||
props,
|
||||
done,
|
||||
assistant_id,
|
||||
assistant_name,
|
||||
assistant_avatar,
|
||||
new: is_new,
|
||||
delta,
|
||||
result,
|
||||
} = formated_data;
|
||||
|
||||
// Handle action message type
|
||||
if (type === "action") {
|
||||
const { namespace, primary, data_item, action, extra } =
|
||||
props || {};
|
||||
if (action && Array.isArray(action)) {
|
||||
const actionMessage = {
|
||||
text: text || "",
|
||||
type: "action",
|
||||
props: {
|
||||
namespace: namespace || "chat",
|
||||
primary: primary || "id",
|
||||
data_item: data_item || {},
|
||||
action,
|
||||
extra,
|
||||
},
|
||||
is_neo: true,
|
||||
done: !!done,
|
||||
};
|
||||
|
||||
messages.push(actionMessage);
|
||||
messageHandler(actionMessage);
|
||||
|
||||
if (done) {
|
||||
const doneHandler = this.events["done"] as DoneHandler;
|
||||
doneHandler?.(messages);
|
||||
es.close();
|
||||
}
|
||||
return resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to create a new message
|
||||
const shouldCreateNewMessage =
|
||||
(type !== last_type &&
|
||||
(!done || (done === true && (text || props)))) || // if type changed or done is true and there is text or props
|
||||
messages.length === 0 ||
|
||||
(assistant_id &&
|
||||
messages[messages.length - 1].assistant_id !== assistant_id) ||
|
||||
(is_new && !delta); // Only create new message if it's new and not a delta update
|
||||
|
||||
// Update last type
|
||||
last_type = type;
|
||||
|
||||
// Update assistant information
|
||||
if (assistant_id) lastAssistant.assistant_id = assistant_id;
|
||||
if (assistant_name) lastAssistant.assistant_name = assistant_name;
|
||||
if (assistant_avatar)
|
||||
lastAssistant.assistant_avatar = assistant_avatar;
|
||||
|
||||
if (shouldCreateNewMessage) {
|
||||
// Mark the last message as done if it exists
|
||||
if (messages.length > 0 && messages[messages.length - 1].is_neo) {
|
||||
messages[messages.length - 1] = {
|
||||
...messages[messages.length - 1],
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Create new message with all original properties
|
||||
const newMessage = {
|
||||
text: text || "",
|
||||
type: type || "text",
|
||||
props,
|
||||
is_neo: true,
|
||||
new: is_new, // Only set new if it's from the original message
|
||||
tool_id,
|
||||
result: result,
|
||||
assistant_id: lastAssistant.assistant_id || undefined,
|
||||
assistant_name: lastAssistant.assistant_name || undefined,
|
||||
assistant_avatar: lastAssistant.assistant_avatar || undefined,
|
||||
};
|
||||
|
||||
messages.push(newMessage);
|
||||
messageHandler(newMessage);
|
||||
|
||||
// If the message is done, close the event source
|
||||
if (done) {
|
||||
const doneHandler = this.events["done"] as DoneHandler;
|
||||
doneHandler?.(messages);
|
||||
es.close();
|
||||
return resolve(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current message (we know it exists because we checked messages.length above)
|
||||
const current_answer = messages[messages.length - 1];
|
||||
|
||||
// Set previous assistant id
|
||||
if (messages.length > 1) {
|
||||
const previous_message = messages[messages.length - 2];
|
||||
if (previous_message.assistant_id) {
|
||||
current_answer.previous_assistant_id =
|
||||
previous_message.assistant_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle message completion (done flag is set)
|
||||
if (done) {
|
||||
if (text) {
|
||||
current_answer.text = text;
|
||||
}
|
||||
if (type) {
|
||||
current_answer.type = type;
|
||||
}
|
||||
if (props) {
|
||||
current_answer.props = props;
|
||||
}
|
||||
|
||||
// Set result if available
|
||||
if (result) {
|
||||
current_answer.result = result;
|
||||
}
|
||||
|
||||
// Mark all previous neo messages as done
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.is_neo) {
|
||||
if (message.done) break;
|
||||
messages[i] = { ...message, done: true };
|
||||
}
|
||||
}
|
||||
|
||||
const doneHandler = this.events["done"] as DoneHandler;
|
||||
doneHandler?.(messages);
|
||||
es.close();
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
// Skip processing if no content to update
|
||||
if (!text && !props && !type) return;
|
||||
|
||||
// Update props if available
|
||||
if (props) {
|
||||
if (type === "think" || type === "tool") {
|
||||
current_answer.props = {
|
||||
...(current_answer.props || {}),
|
||||
id: tool_id,
|
||||
begin,
|
||||
end,
|
||||
};
|
||||
} else {
|
||||
current_answer.props = props;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text content
|
||||
if (text) {
|
||||
if (delta) {
|
||||
current_answer.text = (current_answer.text || "") + text;
|
||||
if (text.startsWith("\r")) {
|
||||
current_answer.text = text.replace("\r", "");
|
||||
}
|
||||
} else {
|
||||
current_answer.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
// Send current message to handler
|
||||
messageHandler(current_answer);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err.message || JSON.stringify(err) || "未知错误";
|
||||
console.error("Failed to parse message:", err);
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
};
|
||||
|
||||
es.onerror = (ev) => {
|
||||
handleError(ev);
|
||||
es.close();
|
||||
};
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment information for file uploads
|
||||
*/
|
||||
interface AgentAttachment {
|
||||
name: string;
|
||||
url: string;
|
||||
type: string;
|
||||
content_type: string;
|
||||
bytes: number;
|
||||
created_at: string;
|
||||
file_id: string;
|
||||
chat_id?: string;
|
||||
assistant_id?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input content structure for agent calls
|
||||
*/
|
||||
interface AgentInputContent {
|
||||
text: string;
|
||||
attachments?: AgentAttachment[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for agent calls, can be either a string or a structured input
|
||||
*/
|
||||
type AgentInput =
|
||||
| string
|
||||
| {
|
||||
text: string;
|
||||
attachments?: AgentAttachment[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Agent initialization options
|
||||
*/
|
||||
interface AgentOption {
|
||||
host?: string;
|
||||
token: string;
|
||||
silent?: boolean | string | number;
|
||||
history_visible?: boolean | string | number;
|
||||
chat_id?: string;
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
|
@ -274,7 +274,7 @@ async function __sui_backend_call(
|
|||
method: string,
|
||||
...args: any
|
||||
): Promise<any> {
|
||||
const url = `/api/__yao/sui/v1/run${route}`;
|
||||
const url = `/v1/__yao/sui/v1/run${route}`;
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
Referer: window.location.href,
|
||||
|
|
@ -369,7 +369,7 @@ async function __sui_render(
|
|||
: option.route || window.location.pathname;
|
||||
option.component = (routeAttr && comp.root.getAttribute("s:cn")) || "";
|
||||
|
||||
const url = `/api/__yao/sui/v1/render${route}`;
|
||||
const url = `/v1/__yao/sui/v1/render${route}`;
|
||||
const payload = { name, data: _data, option };
|
||||
|
||||
// merge the user data
|
||||
|
|
|
|||
|
|
@ -505,10 +505,11 @@ func processXgen(process *process.Process) interface{} {
|
|||
layout = new
|
||||
}
|
||||
|
||||
apiBase := getAPIBase()
|
||||
xgenLogin["entry"]["admin"] = admin.Layout.Entry
|
||||
xgenLogin["admin"] = map[string]interface{}{
|
||||
"captcha": "/api/__yao/login/admin/captcha?type=digit",
|
||||
"login": "/api/__yao/login/admin",
|
||||
"captcha": fmt.Sprintf("%s/__yao/login/admin/captcha?type=digit", apiBase),
|
||||
"login": fmt.Sprintf("%s/__yao/login/admin", apiBase),
|
||||
"layout": layout,
|
||||
}
|
||||
|
||||
|
|
@ -541,10 +542,11 @@ func processXgen(process *process.Process) interface{} {
|
|||
if new, ok := newLayout.(map[string]interface{}); ok {
|
||||
layout = new
|
||||
}
|
||||
apiBase := getAPIBase()
|
||||
xgenLogin["entry"]["user"] = user.Layout.Entry
|
||||
xgenLogin["user"] = map[string]interface{}{
|
||||
"captcha": "/api/__yao/login/user/captcha?type=digit",
|
||||
"login": "/api/__yao/login/user",
|
||||
"captcha": fmt.Sprintf("%s/__yao/login/user/captcha?type=digit", apiBase),
|
||||
"login": fmt.Sprintf("%s/__yao/login/user", apiBase),
|
||||
"layout": layout,
|
||||
}
|
||||
|
||||
|
|
@ -690,12 +692,24 @@ func processXgen(process *process.Process) interface{} {
|
|||
"kb": kbConfig,
|
||||
}
|
||||
|
||||
// Set logo and favicon with dynamic API base
|
||||
apiBase := getAPIBase()
|
||||
if Setting.Logo != "" {
|
||||
xgenSetting["logo"] = Setting.Logo
|
||||
// Replace /api/ prefix with current API base if needed
|
||||
logo := Setting.Logo
|
||||
if strings.HasPrefix(logo, "/api/") {
|
||||
logo = apiBase + strings.TrimPrefix(logo, "/api")
|
||||
}
|
||||
xgenSetting["logo"] = logo
|
||||
}
|
||||
|
||||
if Setting.Favicon != "" {
|
||||
xgenSetting["favicon"] = Setting.Favicon
|
||||
// Replace /api/ prefix with current API base if needed
|
||||
favicon := Setting.Favicon
|
||||
if strings.HasPrefix(favicon, "/api/") {
|
||||
favicon = apiBase + strings.TrimPrefix(favicon, "/api")
|
||||
}
|
||||
xgenSetting["favicon"] = favicon
|
||||
}
|
||||
|
||||
setting, err := i18n.Trans(session.Lang(process, config.Conf.Lang), []string{"app.app"}, xgenSetting)
|
||||
|
|
@ -726,20 +740,18 @@ func (dsl *DSL) replaceAdminRoot() error {
|
|||
|
||||
// icons
|
||||
func (dsl *DSL) icons(cfg config.Config) {
|
||||
|
||||
dsl.Favicon = "/api/__yao/app/icons/app.ico"
|
||||
dsl.Logo = "/api/__yao/app/icons/app.png"
|
||||
apiBase := getAPIBase()
|
||||
dsl.Favicon = fmt.Sprintf("%s/__yao/app/icons/app.ico", apiBase)
|
||||
dsl.Logo = fmt.Sprintf("%s/__yao/app/icons/app.png", apiBase)
|
||||
log.Trace("CFG %v", cfg.Root)
|
||||
}
|
||||
|
||||
// favicon := filepath.Join(cfg.Root, "icons", "app.ico")
|
||||
// if _, err := os.Stat(favicon); err == nil {
|
||||
// dsl.Favicon = fmt.Sprintf("/api/__yao/app/icons/app.ico")
|
||||
// }
|
||||
|
||||
// logo := filepath.Join(cfg.Root, "icons", "app.png")
|
||||
// if _, err := os.Stat(logo); err == nil {
|
||||
// dsl.Logo = fmt.Sprintf("/api/__yao/app/icons/app.png")
|
||||
// }
|
||||
// getAPIBase returns the API base path based on OpenAPI mode
|
||||
func getAPIBase() string {
|
||||
if openapi.Server != nil && openapi.Server.Config != nil && openapi.Server.Config.BaseURL != "" {
|
||||
return openapi.Server.Config.BaseURL
|
||||
}
|
||||
return "/api"
|
||||
}
|
||||
|
||||
// Permissions get the permission blacklist
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue