Add files via upload

This commit is contained in:
公明
2026-06-03 17:16:48 +08:00
committed by GitHub
parent da8fdafe59
commit e606369e31
11 changed files with 1041 additions and 2 deletions
+1 -1
View File
@@ -491,7 +491,7 @@ func appendAttachmentsToMessage(msg string, attachments []ChatAttachment, savedP
}
var b strings.Builder
b.WriteString(msg)
b.WriteString("\n\n[用户上传的文件已保存到以下路径(请按需读取文件内容,而不是依赖内联内容)]\n")
b.WriteString("\n\n[用户上传的文件]\n")
for i, a := range attachments {
if i < len(savedPaths) && savedPaths[i] != "" {
b.WriteString(fmt.Sprintf("- %s: %s\n", a.FileName, savedPaths[i]))
+147
View File
@@ -237,6 +237,7 @@ func (h *ConfigHandler) ApplyWechatRobotBinding(wc config.RobotWechatConfig) err
// GetConfigResponse 获取配置响应
type GetConfigResponse struct {
OpenAI config.OpenAIConfig `json:"openai"`
Vision config.VisionConfig `json:"vision"`
FOFA config.FofaConfig `json:"fofa"`
MCP config.MCPConfig `json:"mcp"`
Tools []ToolConfigInfo `json:"tools"`
@@ -333,6 +334,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
c.JSON(http.StatusOK, GetConfigResponse{
OpenAI: h.config.OpenAI,
Vision: h.config.Vision,
FOFA: h.config.FOFA,
MCP: h.config.MCP,
Tools: tools,
@@ -638,6 +640,7 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
// UpdateConfigRequest 更新配置请求
type UpdateConfigRequest struct {
OpenAI *config.OpenAIConfig `json:"openai,omitempty"`
Vision *config.VisionConfig `json:"vision,omitempty"`
FOFA *config.FofaConfig `json:"fofa,omitempty"`
MCP *config.MCPConfig `json:"mcp,omitempty"`
Tools []ToolEnableStatus `json:"tools,omitempty"`
@@ -707,6 +710,14 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
)
}
if req.Vision != nil {
h.config.Vision = *req.Vision
h.logger.Info("更新 Vision 配置",
zap.Bool("enabled", h.config.Vision.Enabled),
zap.String("model", h.config.Vision.Model),
)
}
// 更新FOFA配置
if req.FOFA != nil {
h.config.FOFA = *req.FOFA
@@ -1031,6 +1042,99 @@ func (h *ConfigHandler) TestOpenAI(c *gin.Context) {
})
}
// TestVisionRequest 测试 Vision 模型连接;vision.api_key/base_url 留空时可传 openai 段作回退。
type TestVisionRequest struct {
Vision config.VisionConfig `json:"vision"`
OpenAI config.OpenAIConfig `json:"openai,omitempty"`
}
// TestVision 测试视觉模型 API 连接(最小 chat completion)。
func (h *ConfigHandler) TestVision(c *gin.Context) {
var req TestVisionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
return
}
oa := req.Vision.OpenAICfgEffective(req.OpenAI)
if strings.TrimSpace(oa.APIKey) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "API Key 不能为空(可填写 vision.api_key 或 openai.api_key"})
return
}
if strings.TrimSpace(oa.Model) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "视觉模型不能为空"})
return
}
baseURL := strings.TrimSuffix(strings.TrimSpace(oa.BaseURL), "/")
if baseURL == "" {
if strings.EqualFold(strings.TrimSpace(oa.Provider), "claude") {
baseURL = "https://api.anthropic.com"
} else {
baseURL = "https://api.openai.com/v1"
}
}
payload := map[string]interface{}{
"model": oa.Model,
"messages": []map[string]string{
{"role": "user", "content": "Hi"},
},
"max_completion_tokens": 5,
}
tmpCfg := &config.OpenAIConfig{
Provider: oa.Provider,
BaseURL: baseURL,
APIKey: strings.TrimSpace(oa.APIKey),
Model: oa.Model,
}
client := openai.NewClient(tmpCfg, nil, h.logger)
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
start := time.Now()
var chatResp struct {
Model string `json:"model"`
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
err := client.ChatCompletion(ctx, payload, &chatResp)
latency := time.Since(start)
if err != nil {
if apiErr, ok := err.(*openai.APIError); ok {
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": fmt.Sprintf("API 返回错误 (HTTP %d): %s", apiErr.StatusCode, apiErr.Body),
"status_code": apiErr.StatusCode,
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": "连接失败: " + err.Error(),
})
return
}
if len(chatResp.Choices) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": "API 响应缺少 choices 字段,请检查 Base URL 与视觉模型名称",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"model": chatResp.Model,
"latency_ms": latency.Milliseconds(),
})
}
// ApplyConfig 应用配置(重新加载并重启相关服务)
func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
// 先检查是否需要动态初始化知识库(在锁外执行,避免阻塞其他请求)
@@ -1286,6 +1390,7 @@ func (h *ConfigHandler) saveConfig() error {
updateAgentConfig(root, h.config.Agent)
updateMCPConfig(root, h.config.MCP)
updateOpenAIConfig(root, h.config.OpenAI)
updateVisionConfig(root, h.config.Vision)
updateFOFAConfig(root, h.config.FOFA)
updateKnowledgeConfig(root, h.config.Knowledge)
updateC2Config(root, h.config.C2)
@@ -1406,6 +1511,48 @@ func updateMCPConfig(doc *yaml.Node, cfg config.MCPConfig) {
setIntInMap(mcpNode, "port", cfg.Port)
}
func updateVisionConfig(doc *yaml.Node, cfg config.VisionConfig) {
root := doc.Content[0]
visionNode := ensureMap(root, "vision")
setBoolInMap(visionNode, "enabled", cfg.Enabled)
if strings.TrimSpace(cfg.APIKey) != "" {
setStringInMap(visionNode, "api_key", cfg.APIKey)
} else {
setStringInMap(visionNode, "api_key", "")
}
if strings.TrimSpace(cfg.BaseURL) != "" {
setStringInMap(visionNode, "base_url", cfg.BaseURL)
} else {
setStringInMap(visionNode, "base_url", "")
}
setStringInMap(visionNode, "model", cfg.Model)
if strings.TrimSpace(cfg.Provider) != "" {
setStringInMap(visionNode, "provider", cfg.Provider)
}
if cfg.TimeoutSeconds > 0 {
setIntInMap(visionNode, "timeout_seconds", cfg.TimeoutSeconds)
}
if cfg.MaxImageBytes > 0 {
setIntInMap(visionNode, "max_image_bytes", int(cfg.MaxImageBytes))
}
if cfg.MaxDimension > 0 {
setIntInMap(visionNode, "max_dimension", cfg.MaxDimension)
}
if cfg.JPEGQuality > 0 {
setIntInMap(visionNode, "jpeg_quality", cfg.JPEGQuality)
}
if cfg.MaxPayloadBytes > 0 {
setIntInMap(visionNode, "max_payload_bytes", int(cfg.MaxPayloadBytes))
}
setIntInMap(visionNode, "skip_preprocess_below_bytes", int(cfg.SkipPreprocessBelowBytes))
if strings.TrimSpace(cfg.Detail) != "" {
setStringInMap(visionNode, "detail", cfg.Detail)
}
if len(cfg.AllowedRoots) > 0 {
setStringSliceInMap(visionNode, "allowed_roots", cfg.AllowedRoots)
}
}
func updateOpenAIConfig(doc *yaml.Node, cfg config.OpenAIConfig) {
root := doc.Content[0]
openaiNode := ensureMap(root, "openai")
+91 -1
View File
@@ -778,11 +778,55 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
"ConfigResponse": map[string]interface{}{
"type": "object",
"description": "配置信息",
"description": "配置信息(含 openai、vision、multi_agent 等)",
"properties": map[string]interface{}{
"vision": map[string]interface{}{
"$ref": "#/components/schemas/VisionConfig",
},
},
},
"UpdateConfigRequest": map[string]interface{}{
"type": "object",
"description": "更新配置请求",
"properties": map[string]interface{}{
"vision": map[string]interface{}{
"$ref": "#/components/schemas/VisionConfig",
},
},
},
"VisionConfig": map[string]interface{}{
"type": "object",
"description": "视觉分析(analyze_image MCP 工具);enabled 且 model 非空时注册工具",
"properties": map[string]interface{}{
"enabled": map[string]interface{}{"type": "boolean", "description": "是否启用 analyze_image"},
"model": map[string]interface{}{"type": "string", "description": "视觉模型名(必填)", "example": "qwen-vl-max"},
"api_key": map[string]interface{}{"type": "string", "description": "API Key;留空复用 openai.api_key"},
"base_url": map[string]interface{}{"type": "string", "description": "Base URL;留空复用 openai.base_url"},
"provider": map[string]interface{}{"type": "string", "description": "提供商;留空复用 openai.provider"},
"timeout_seconds": map[string]interface{}{"type": "integer", "description": "VL 调用超时(秒)"},
"max_image_bytes": map[string]interface{}{"type": "integer", "description": "原始文件大小上限(字节)"},
"max_dimension": map[string]interface{}{"type": "integer", "description": "长边缩放像素"},
"jpeg_quality": map[string]interface{}{"type": "integer", "description": "JPEG 质量 60-100"},
"max_payload_bytes": map[string]interface{}{"type": "integer", "description": "送 API 体积上限(字节)"},
"skip_preprocess_below_bytes": map[string]interface{}{"type": "integer", "description": "低于该字节且尺寸合规时可原图直传;0=始终压缩"},
"detail": map[string]interface{}{"type": "string", "enum": []string{"low", "high", "auto"}, "description": "OpenAI 兼容 image detail"},
"allowed_roots": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "额外允许读取的绝对路径根"},
},
},
"AnalyzeImageToolCall": map[string]interface{}{
"type": "object",
"description": "内置 MCP 工具 analyze_image:分析服务器本地图片,返回纯文本(验证码/UI/报错等)",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "图片路径(cwd、chat_uploads、result_storage_dir 或 allowed_roots 下)",
},
"question": map[string]interface{}{
"type": "string",
"description": "可选:重点问题;验证码建议「只输出验证码字符」",
},
},
"required": []string{"path"},
},
"ExternalMCPConfig": map[string]interface{}{
"type": "object",
@@ -4900,6 +4944,52 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
// ==================== 配置管理 - 缺失端点 ====================
"/api/config/test-vision": map[string]interface{}{
"post": map[string]interface{}{
"tags": []string{"配置管理"},
"summary": "测试视觉模型连接",
"description": "测试 Vision 模型 API 是否可用。vision.api_key/base_url 留空时可传 openai 段作回退。",
"operationId": "testVision",
"requestBody": map[string]interface{}{
"required": true,
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"required": []string{"vision"},
"properties": map[string]interface{}{
"vision": map[string]interface{}{"$ref": "#/components/schemas/VisionConfig"},
"openai": map[string]interface{}{
"type": "object",
"description": "主 LLM 配置(vision 字段留空时用于 API Key/Base URL 回退)",
},
},
},
},
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "测试结果",
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"success": map[string]interface{}{"type": "boolean"},
"error": map[string]interface{}{"type": "string"},
"model": map[string]interface{}{"type": "string"},
"latency_ms": map[string]interface{}{"type": "number"},
},
},
},
},
},
"400": map[string]interface{}{"description": "参数错误"},
"401": map[string]interface{}{"description": "未授权"},
},
},
},
"/api/config/test-openai": map[string]interface{}{
"post": map[string]interface{}{
"tags": []string{"配置管理"},