- Deleted obsolete agent API files (agent.go, api.go, api_test.go, types.go) to streamline the codebase. - Refactored the agent loading logic to initialize the API instance correctly, ensuring proper integration with the new structure. - Updated context handling to improve clarity and maintainability across the agent's functionality. - Enhanced error handling and cache management in the agent's initialization process.
313 lines
8.1 KiB
Go
313 lines
8.1 KiB
Go
package context
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/gou/plan"
|
|
"github.com/yaoapp/gou/store"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
)
|
|
|
|
// NewOpenAPI create a new context from openapi context
|
|
func NewOpenAPI(c *gin.Context, cache store.Store) Context {
|
|
// Get authorized information
|
|
authInfo := authorized.GetInfo(c)
|
|
|
|
// Extract assistant ID (route parameter takes priority, handled in GetAssistantID)
|
|
assistantID, _ := GetAssistantID(c)
|
|
|
|
// Extract chat ID (may generate from messages if not provided)
|
|
// GetChatID internally calls GetChatIDByMessages which auto-caches
|
|
chatID, _ := GetChatID(c, cache)
|
|
|
|
// Parse client information from User-Agent header
|
|
userAgent := c.GetHeader("User-Agent")
|
|
clientType := getClientType(userAgent)
|
|
clientIP := c.ClientIP()
|
|
|
|
// Create context with extracted parameters
|
|
ctx := Context{
|
|
Context: c.Request.Context(),
|
|
Space: plan.NewMemorySharedSpace(),
|
|
Authorized: authInfo,
|
|
ChatID: chatID,
|
|
AssistantID: assistantID,
|
|
Locale: GetLocale(c),
|
|
Theme: GetTheme(c),
|
|
Referer: GetReferer(c),
|
|
Accept: GetAccept(c),
|
|
Client: Client{
|
|
Type: clientType,
|
|
UserAgent: userAgent,
|
|
IP: clientIP,
|
|
},
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
// getClientType parses the client type from User-Agent header
|
|
func getClientType(userAgent string) string {
|
|
if userAgent == "" {
|
|
return "web" // Default to web
|
|
}
|
|
|
|
ua := strings.ToLower(userAgent)
|
|
|
|
// Check for specific client types
|
|
switch {
|
|
case strings.Contains(ua, "yao-agent") || strings.Contains(ua, "agent"):
|
|
return "agent"
|
|
case strings.Contains(ua, "yao-jssdk") || strings.Contains(ua, "jssdk"):
|
|
return "jssdk"
|
|
case strings.Contains(ua, "android"):
|
|
return "android"
|
|
case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") || strings.Contains(ua, "ipod"):
|
|
return "ios"
|
|
case strings.Contains(ua, "windows"):
|
|
return "windows"
|
|
case strings.Contains(ua, "mac os x") || strings.Contains(ua, "macintosh"):
|
|
return "macos"
|
|
case strings.Contains(ua, "linux"):
|
|
return "linux"
|
|
default:
|
|
return "web"
|
|
}
|
|
}
|
|
|
|
// getPayloadField reads a string field from request body
|
|
func getPayloadField(c *gin.Context, fieldName string) string {
|
|
if c.Request.Body == nil {
|
|
return ""
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Restore body for further use
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
|
|
|
if len(body) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Parse JSON to extract field
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return ""
|
|
}
|
|
|
|
if value, ok := payload[fieldName]; ok {
|
|
if strValue, ok := value.(string); ok {
|
|
return strValue
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetAssistantID extracts assistant ID from request with priority:
|
|
// 1. Query parameter "assistant_id"
|
|
// 2. Header "X-Yao-Assistant"
|
|
// 3. Query parameter "model" - splits by "-" takes last field, extracts ID from "yao_xxx" prefix
|
|
// 4. Payload "model" field - same parsing as query parameter
|
|
func GetAssistantID(c *gin.Context) (string, error) {
|
|
// Priority 1: Query parameter assistant_id
|
|
if assistantID := c.Query("assistant_id"); assistantID != "" {
|
|
return assistantID, nil
|
|
}
|
|
|
|
// Priority 2: Header X-Yao-Assistant
|
|
if assistantID := c.GetHeader("X-Yao-Assistant"); assistantID != "" {
|
|
return assistantID, nil
|
|
}
|
|
|
|
// Priority 3 & 4: Extract from model parameter (Query or Payload)
|
|
model := c.Query("model")
|
|
if model == "" {
|
|
model = getPayloadField(c, "model")
|
|
}
|
|
|
|
if model != "" {
|
|
// Split by "-" and get the last field
|
|
parts := strings.Split(model, "-")
|
|
lastField := strings.TrimSpace(parts[len(parts)-1])
|
|
|
|
// Check if it has yao_ prefix
|
|
if strings.HasPrefix(lastField, "yao_") {
|
|
assistantID := strings.TrimPrefix(lastField, "yao_")
|
|
if assistantID != "" {
|
|
return assistantID, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no assistant ID found, return error
|
|
return "", fmt.Errorf("assistant_id is required")
|
|
}
|
|
|
|
// GetMessages extracts messages from the request
|
|
// Priority:
|
|
// 1. Query parameter "messages" (JSON string)
|
|
// 2. Request body "messages" field
|
|
func GetMessages(c *gin.Context) ([]Message, error) {
|
|
// Try query parameter first
|
|
if messagesJSON := c.Query("messages"); messagesJSON != "" {
|
|
var messages []Message
|
|
if err := json.Unmarshal([]byte(messagesJSON), &messages); err == nil && len(messages) > 0 {
|
|
return messages, nil
|
|
}
|
|
}
|
|
|
|
// Check if request body exists
|
|
if c.Request.Body == nil {
|
|
return nil, fmt.Errorf("messages field is required")
|
|
}
|
|
|
|
// Try request body
|
|
// Read body carefully to allow reuse
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read request body: %w", err)
|
|
}
|
|
|
|
// Restore body for further processing
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
|
|
// If body is empty, return error
|
|
if len(body) == 0 {
|
|
return nil, fmt.Errorf("messages field is required")
|
|
}
|
|
|
|
var requestBody struct {
|
|
Messages []Message `json:"messages"`
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &requestBody); err != nil {
|
|
return nil, fmt.Errorf("failed to parse messages from request body: %w", err)
|
|
}
|
|
|
|
if len(requestBody.Messages) == 0 {
|
|
return nil, fmt.Errorf("messages field is required and must not be empty")
|
|
}
|
|
|
|
return requestBody.Messages, nil
|
|
}
|
|
|
|
// GetLocale extracts locale from request with priority:
|
|
// 1. Query parameter "locale"
|
|
// 2. Header "Accept-Language"
|
|
func GetLocale(c *gin.Context) string {
|
|
// Priority 1: Query parameter
|
|
if locale := c.Query("locale"); locale != "" {
|
|
return strings.ToLower(locale)
|
|
}
|
|
|
|
// Priority 2: Header Accept-Language
|
|
if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
|
|
// Parse Accept-Language header (e.g., "en-US,en;q=0.9,zh;q=0.8")
|
|
// Take the first language
|
|
parts := strings.Split(acceptLang, ",")
|
|
if len(parts) > 0 {
|
|
// Remove quality value if present
|
|
lang := strings.Split(parts[0], ";")[0]
|
|
return strings.ToLower(strings.TrimSpace(lang))
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetTheme extracts theme from request with priority:
|
|
// 1. Query parameter "theme"
|
|
// 2. Header "X-Yao-Theme"
|
|
func GetTheme(c *gin.Context) string {
|
|
// Priority 1: Query parameter
|
|
if theme := c.Query("theme"); theme != "" {
|
|
return strings.ToLower(theme)
|
|
}
|
|
|
|
// Priority 2: Header
|
|
if theme := c.GetHeader("X-Yao-Theme"); theme != "" {
|
|
return strings.ToLower(theme)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetReferer extracts referer from request with priority:
|
|
// 1. Query parameter "referer"
|
|
// 2. Header "X-Yao-Referer"
|
|
// 3. Default to "api"
|
|
func GetReferer(c *gin.Context) string {
|
|
// Priority 1: Query parameter
|
|
if referer := c.Query("referer"); referer != "" {
|
|
return validateReferer(referer)
|
|
}
|
|
|
|
// Priority 2: Header
|
|
if referer := c.GetHeader("X-Yao-Referer"); referer != "" {
|
|
return validateReferer(referer)
|
|
}
|
|
|
|
// Priority 3: Default
|
|
return RefererAPI
|
|
}
|
|
|
|
// GetAccept extracts accept type from request with priority:
|
|
// 1. Query parameter "accept"
|
|
// 2. Header "X-Yao-Accept"
|
|
// 3. Parse from client type (User-Agent)
|
|
func GetAccept(c *gin.Context) Accept {
|
|
// Priority 1: Query parameter
|
|
if accept := c.Query("accept"); accept != "" {
|
|
return validateAccept(accept)
|
|
}
|
|
|
|
// Priority 2: Header
|
|
if accept := c.GetHeader("X-Yao-Accept"); accept != "" {
|
|
return validateAccept(accept)
|
|
}
|
|
|
|
// Priority 3: Parse from User-Agent
|
|
userAgent := c.GetHeader("User-Agent")
|
|
clientType := getClientType(userAgent)
|
|
return parseAccept(clientType)
|
|
}
|
|
|
|
// GetChatID get the chat ID from the request
|
|
// Priority:
|
|
// 1. Query parameter "chat_id"
|
|
// 2. Header "X-Yao-Chat"
|
|
// 3. Generate from messages using GetChatIDByMessages
|
|
func GetChatID(c *gin.Context, cache store.Store) (string, error) {
|
|
// Priority 1: Query parameter chat_id
|
|
if chatID := c.Query("chat_id"); chatID != "" {
|
|
return chatID, nil
|
|
}
|
|
|
|
// Priority 2: Header X-Yao-Chat
|
|
if chatID := c.GetHeader("X-Yao-Chat"); chatID != "" {
|
|
return chatID, nil
|
|
}
|
|
|
|
// Priority 3: Generate from messages
|
|
messages, err := GetMessages(c)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get messages for chat ID generation: %w", err)
|
|
}
|
|
|
|
chatID, err := GetChatIDByMessages(cache, messages)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate chat ID from messages: %w", err)
|
|
}
|
|
|
|
return chatID, nil
|
|
}
|