package controllers import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" "server/models" "server/pkg/identity" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // PlatformAiController 平台�?AI 管理(platform「AI管理」页�? // 服务商配置存 yz_platform_ai_provider;OCR 配置暂存 yz_platform_site_setting 标签�? type PlatformAiController struct { beego.Controller } func (c *PlatformAiController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]any{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func maskKey(key string) string { key = strings.TrimSpace(key) if len(key) <= 8 { if key == "" { return "" } return "***" } return key[:4] + "***" + key[len(key)-4:] } func fillProvider(m *models.PlatformAiProvider) { if m.Models != "" { // 兼容两种存储格式:[{"id","name"}] 对象数组 �?["model-id"] 字符串数�? var raw []json.RawMessage if err := json.Unmarshal([]byte(m.Models), &raw); err == nil { for _, r := range raw { var item models.AiModelItem if err := json.Unmarshal(r, &item); err == nil && item.ID != "" { m.ModelsList = append(m.ModelsList, item) continue } var s string if err := json.Unmarshal(r, &s); err == nil && s != "" { m.ModelsList = append(m.ModelsList, models.AiModelItem{ID: s, Name: s}) } } } } m.ApiKeyMasked = maskKey(m.ApiKey) m.ApiKey = "" } // List GET /platform/ai/provider/list?provider_type= func (c *PlatformAiController) List() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } ptype := strings.TrimSpace(c.GetString("provider_type")) qs := models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("delete_time__isnull", true) if ptype != "" { qs = qs.Filter("provider_type", ptype) } var rows []models.PlatformAiProvider _, _ = qs.OrderBy("-is_default", "-id").All(&rows) for i := range rows { fillProvider(&rows[i]) } c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": rows} _ = c.ServeJSON() } type platformAiProviderPayload struct { ID uint64 `json:"id"` ProviderType string `json:"provider_type"` ApiType string `json:"api_type"` Name string `json:"name"` ApiBase string `json:"api_base"` ApiKey string `json:"api_key"` Models []models.AiModelItem `json:"models"` DefaultModel string `json:"default_model"` IsDefault int8 `json:"is_default"` Status int8 `json:"status"` Remark string `json:"remark"` } func readProviderPayload(c *PlatformAiController) (*platformAiProviderPayload, bool) { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return nil, false } var p platformAiProviderPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return nil, false } p.ProviderType = strings.TrimSpace(p.ProviderType) p.ApiType = strings.TrimSpace(p.ApiType) if p.ApiType == "" { p.ApiType = "chat_completions" } switch p.ApiType { case "chat_completions", "responses", "anthropic": default: c.jsonErr(400, 400, "不支持的协议类型") return nil, false } p.Name = strings.TrimSpace(p.Name) p.ApiBase = strings.TrimRight(strings.TrimSpace(p.ApiBase), "/") if p.ProviderType == "" || p.Name == "" || p.ApiBase == "" { c.jsonErr(400, 400, "服务商类型、名称、API基址不能为空") return nil, false } return &p, true } func (p *platformAiProviderPayload) toModel() *models.PlatformAiProvider { modelsJSON, _ := json.Marshal(p.Models) return &models.PlatformAiProvider{ ID: p.ID, ProviderType: p.ProviderType, ApiType: p.ApiType, Name: p.Name, ApiBase: p.ApiBase, ApiKey: p.ApiKey, Models: string(modelsJSON), DefaultModel: strings.TrimSpace(p.DefaultModel), IsDefault: p.IsDefault, Status: p.Status, Remark: strings.TrimSpace(p.Remark), } } // Create POST /platform/ai/provider func (c *PlatformAiController) Create() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } p, ok := readProviderPayload(c) if !ok { return } m := p.toModel() if m.Status != 0 { m.Status = 1 } if p.IsDefault == 1 { _, _ = models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("delete_time__isnull", true).Update(map[string]any{"is_default": 0}) } id, err := models.Orm.Insert(m) if err != nil { c.jsonErr(500, 500, "保存失败: "+err.Error()) return } c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功", "data": map[string]any{"id": id}} _ = c.ServeJSON() } // Update PUT /platform/ai/provider/:id func (c *PlatformAiController) Update() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } id, _ := c.GetInt64(":id", 0) if id <= 0 { c.jsonErr(400, 400, "无效ID") return } p, ok := readProviderPayload(c) if !ok { return } m := p.toModel() // api_key 传空表示不修�? update := map[string]any{ "provider_type": m.ProviderType, "api_type": m.ApiType, "name": m.Name, "api_base": m.ApiBase, "models": m.Models, "default_model": m.DefaultModel, "is_default": m.IsDefault, "status": m.Status, "remark": m.Remark, } if strings.TrimSpace(p.ApiKey) != "" { update["api_key"] = p.ApiKey } if p.IsDefault == 1 { _, _ = models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("delete_time__isnull", true).Filter("id__ne", id). Update(map[string]any{"is_default": 0}) } n, err := models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("id", id).Filter("delete_time__isnull", true).Update(update) if err != nil { c.jsonErr(500, 500, "保存失败: "+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "配置不存在") return } c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功"} _ = c.ServeJSON() } // Delete DELETE /platform/ai/provider/:id func (c *PlatformAiController) Delete() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } id, _ := c.GetInt64(":id", 0) n, err := models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("id", id).Filter("delete_time__isnull", true). Update(map[string]any{"delete_time": time.Now()}) if err != nil { c.jsonErr(500, 500, "删除失败: "+err.Error()) return } if n == 0 { c.jsonErr(404, 404, "配置不存在") return } c.Data["json"] = map[string]any{"code": 200, "msg": "删除成功"} _ = c.ServeJSON() } // loadProvider 取一条启用中的服务商配置(优先指�?id,否则平台默认) func loadPlatformProvider(id uint64) (*models.PlatformAiProvider, error) { var m models.PlatformAiProvider qs := models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("delete_time__isnull", true).Filter("status", 1) if id > 0 { err := qs.Filter("id", id).One(&m) return &m, err } err := qs.Filter("is_default", 1).One(&m) if err != nil { err = models.Orm.QueryTable(new(models.PlatformAiProvider)). Filter("delete_time__isnull", true).Filter("status", 1). OrderBy("-id").One(&m) } return &m, err } // chatCompletion 按服务商协议类型分发调用,返回助手回复文�? func chatCompletion(provider *models.PlatformAiProvider, model string, messages []map[string]string) (string, error) { switch provider.ApiType { case "responses": return chatResponses(provider, model, messages) case "anthropic": return chatAnthropic(provider, model, messages) default: return chatCompletions(provider, model, messages) } } func doAIRequest(req *http.Request, apiKey string) ([]byte, int, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 90 * time.Second} resp, err := client.Do(req) if err != nil { return nil, 0, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return body, resp.StatusCode, nil } // chatCompletions OpenAI Chat Completions:POST {api_base}/chat/completions func chatCompletions(provider *models.PlatformAiProvider, model string, messages []map[string]string) (string, error) { payload := map[string]any{"model": model, "messages": messages, "stream": false} body, _ := json.Marshal(payload) req, err := http.NewRequest("POST", provider.ApiBase+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", err } respBody, status, err := doAIRequest(req, provider.ApiKey) if err != nil { return "", err } if status != 200 { return "", errUpstream(string(respBody)) } var out struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` Error *struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(respBody, &out); err != nil { return "", err } if out.Error != nil && out.Error.Message != "" { return "", errUpstream(out.Error.Message) } if len(out.Choices) == 0 { return "", errUpstream("上游返回为空") } return out.Choices[0].Message.Content, nil } // chatResponses OpenAI Responses:POST {api_base}/responses // input 直接复用 messages 数组(role/content 结构兼容�? func chatResponses(provider *models.PlatformAiProvider, model string, messages []map[string]string) (string, error) { payload := map[string]any{"model": model, "input": messages} body, _ := json.Marshal(payload) req, err := http.NewRequest("POST", provider.ApiBase+"/responses", bytes.NewReader(body)) if err != nil { return "", err } respBody, status, err := doAIRequest(req, provider.ApiKey) if err != nil { return "", err } if status != 200 { return "", errUpstream(string(respBody)) } var out struct { OutputText string `json:"output_text"` Output []struct { Type string `json:"type"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` } `json:"output"` Error *struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(respBody, &out); err != nil { return "", err } if out.Error != nil && out.Error.Message != "" { return "", errUpstream(out.Error.Message) } if out.OutputText != "" { return out.OutputText, nil } for _, item := range out.Output { for _, c := range item.Content { if c.Text != "" { return c.Text, nil } } } return "", errUpstream("上游返回为空") } // chatAnthropic Anthropic Messages:POST {api_base}/v1/messages // system 消息提取到顶�?system 字段;x-api-key + anthropic-version 鉴权 func chatAnthropic(provider *models.PlatformAiProvider, model string, messages []map[string]string) (string, error) { var systemParts []string var msgs []map[string]string for _, m := range messages { role := m["role"] if role == "system" { systemParts = append(systemParts, m["content"]) continue } if role == "assistant" { role = "assistant" } else { role = "user" } msgs = append(msgs, map[string]string{"role": role, "content": m["content"]}) } payload := map[string]any{ "model": model, "max_tokens": 4096, "messages": msgs, } if len(systemParts) > 0 { payload["system"] = strings.Join(systemParts, "\n") } body, _ := json.Marshal(payload) base := strings.TrimRight(provider.ApiBase, "/") if !strings.HasSuffix(base, "/v1") { base += "/v1" } req, err := http.NewRequest("POST", base+"/messages", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("x-api-key", provider.ApiKey) req.Header.Set("anthropic-version", "2023-06-01") req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 90 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return "", errUpstream(string(respBody)) } var out struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` Error *struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(respBody, &out); err != nil { return "", err } if out.Error != nil && out.Error.Message != "" { return "", errUpstream(out.Error.Message) } for _, c := range out.Content { if c.Text != "" { return c.Text, nil } } return "", errUpstream("上游返回为空") } type upstreamError string func (e upstreamError) Error() string { return "AI服务调用失败: " + string(e) } func errUpstream(msg string) error { return upstreamError(msg) } // Test POST /platform/ai/provider/test { id } �?{ provider_id, model } // 用一�?ping 验证连通�? func (c *PlatformAiController) Test() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } var req struct { ID uint64 `json:"id"` Model string `json:"model"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) _ = json.Unmarshal(raw, &req) provider, err := loadPlatformProvider(req.ID) if err != nil { c.jsonErr(400, 400, "服务商配置不存在或未启用") return } model := strings.TrimSpace(req.Model) if model == "" { model = provider.DefaultModel } if model == "" { var list []models.AiModelItem _ = json.Unmarshal([]byte(provider.Models), &list) if len(list) > 0 { model = list[0].ID } } if model == "" { c.jsonErr(400, 400, "未配置可用模型") return } start := time.Now() content, err := chatCompletion(provider, model, []map[string]string{ {"role": "user", "content": "请只回复两个字:正常"}, }) if err != nil { c.jsonErr(502, 502, err.Error()) return } c.Data["json"] = map[string]any{ "code": 200, "msg": "连接成功", "data": map[string]any{ "latency_ms": int(time.Since(start).Milliseconds()), "reply": content, "model": model, }, } _ = c.ServeJSON() } // Chat POST /platform/ai/chat { provider_id, model, messages:[{role,content}] } // Playground 对话代理:平台管理员�?AI管理页测试各服务�? func (c *PlatformAiController) Chat() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } var req struct { ProviderID uint64 `json:"provider_id"` Model string `json:"model"` Messages []map[string]string `json:"messages"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &req); err != nil || len(req.Messages) == 0 { c.jsonErr(400, 400, "参数错误") return } provider, err := loadPlatformProvider(req.ProviderID) if err != nil { c.jsonErr(400, 400, "服务商配置不存在或未启用") return } model := strings.TrimSpace(req.Model) if model == "" { model = provider.DefaultModel } if model == "" { c.jsonErr(400, 400, "未选择模型") return } // 只保�?role/content,避免把多余字段透传给上�? clean := make([]map[string]string, 0, len(req.Messages)) for _, m := range req.Messages { role := strings.TrimSpace(m["role"]) if role != "user" && role != "assistant" && role != "system" { continue } clean = append(clean, map[string]string{"role": role, "content": m["content"]}) } if len(clean) == 0 { c.jsonErr(400, 400, "消息为空") return } content, err := chatCompletion(provider, model, clean) if err != nil { c.jsonErr(502, 502, err.Error()) return } c.Data["json"] = map[string]any{ "code": 200, "msg": "success", "data": map[string]any{"content": content, "model": model}, } _ = c.ServeJSON() } // OCR 配置标签(存 yz_platform_site_setting,OCR 识别实现后续接入�? var ocrConfigTags = []string{"ocr_enabled", "ocr_provider_id", "ocr_model", "ocr_prompt"} // OcrConfig GET /platform/ai/ocrConfig func (c *PlatformAiController) OcrConfig() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } all := models.GetPlatformSiteTagMap() data := make(map[string]string, len(ocrConfigTags)) for _, tag := range ocrConfigTags { data[tag] = all[tag] } c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } // SaveOcrConfig POST /platform/ai/ocrConfig { ocr_enabled, ocr_provider_id, ocr_model, ocr_prompt } func (c *PlatformAiController) SaveOcrConfig() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var payload map[string]string if err := json.Unmarshal(raw, &payload); err != nil { c.jsonErr(400, 400, "参数错误") return } values := make(map[string]string, len(ocrConfigTags)) for _, tag := range ocrConfigTags { if v, ok := payload[tag]; ok { values[tag] = strings.TrimSpace(v) } } if len(values) == 0 { c.jsonErr(400, 400, "参数错误") return } if err := models.SavePlatformSiteSettings(values); err != nil { c.jsonErr(500, 500, "保存失败: "+err.Error()) return } c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功"} _ = c.ServeJSON() } // RecognizeOcr POST /platform/ai/ocr/recognize —�?OCR 识别占位接口 // 具体实现(视觉模�?/ 专用 OCR 服务)后续接入;当前返回配置状态供前端联调�? func (c *PlatformAiController) RecognizeOcr() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } all := models.GetPlatformSiteTagMap() enabled := strings.TrimSpace(all["ocr_enabled"]) == "1" c.Data["json"] = map[string]any{ "code": 200, "msg": "success", "data": map[string]any{ "enabled": enabled, "provider_id": all["ocr_provider_id"], "model": all["ocr_model"], "recognized": false, "message": "OCR 识别服务待接入,配置已保存", "placeholder": true, }, } _ = c.ServeJSON() } // ==================== 流式对话(Playground 打字机效果)==================== // 归一�?SSE 事件:{"t":"delta"|"think"|"done"|"error","c":"..."} func writeStreamEvent(w http.ResponseWriter, flusher http.Flusher, ev map[string]any) { b, _ := json.Marshal(ev) fmt.Fprintf(w, "data: %s\n\n", b) if flusher != nil { flusher.Flush() } } // streamHTTPRequest 构造带 stream:true 的上游请求,返回 *http.Request 与鉴权头已设置的 client 请求 // messages 的 content 支持字符串或多模态内容块数组(附件直传大模型,不落库) func buildStreamRequest(provider *models.PlatformAiProvider, model string, messages []map[string]any) (*http.Request, error) { var url string var payload map[string]any switch provider.ApiType { case "responses": url = provider.ApiBase + "/responses" payload = map[string]any{"model": model, "input": messages, "stream": true} case "anthropic": base := strings.TrimRight(provider.ApiBase, "/") if !strings.HasSuffix(base, "/v1") { base += "/v1" } url = base + "/messages" var systemParts []string var msgs []map[string]any for _, m := range messages { role, _ := m["role"].(string) if role == "system" { if s, ok := m["content"].(string); ok { systemParts = append(systemParts, s) } continue } if role != "assistant" { role = "user" } msgs = append(msgs, map[string]any{"role": role, "content": anthropicContent(m["content"])}) } payload = map[string]any{"model": model, "max_tokens": 4096, "messages": msgs, "stream": true} if len(systemParts) > 0 { payload["system"] = strings.Join(systemParts, "\n") } default: url = provider.ApiBase + "/chat/completions" payload = map[string]any{"model": model, "messages": messages, "stream": true} } body, _ := json.Marshal(payload) req, err := http.NewRequest("POST", url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if provider.ApiType == "anthropic" { req.Header.Set("x-api-key", provider.ApiKey) req.Header.Set("anthropic-version", "2023-06-01") } else { req.Header.Set("Authorization", "Bearer "+provider.ApiKey) } return req, nil } // normalizeStreamData 把上�?SSE data 负载转成归一化事件列�? // 返回 (events, isDone, isError) // anthropicContent 把 OpenAI 风格的 message content 转成 Anthropic content blocks // 字符串 → [{type:text}];数组 → text 块 + base64 图片块(data URL 拆分) func anthropicContent(content any) []map[string]any { if s, ok := content.(string); ok { return []map[string]any{{"type": "text", "text": s}} } arr, ok := content.([]any) if !ok { return []map[string]any{{"type": "text", "text": ""}} } var blocks []map[string]any for _, item := range arr { m, ok := item.(map[string]any) if !ok { continue } switch m["type"] { case "text": if t, ok := m["text"].(string); ok { blocks = append(blocks, map[string]any{"type": "text", "text": t}) } case "image_url": if iu, ok := m["image_url"].(map[string]any); ok { if u, ok := iu["url"].(string); ok { if media, data, ok := parseDataURL(u); ok { blocks = append(blocks, map[string]any{ "type": "image", "source": map[string]any{ "type": "base64", "media_type": media, "data": data, }, }) } } } } } if len(blocks) == 0 { blocks = append(blocks, map[string]any{"type": "text", "text": ""}) } return blocks } // parseDataURL 解析 data:[mediatype];base64,xxxx func parseDataURL(u string) (media, data string, ok bool) { if !strings.HasPrefix(u, "data:") { return "", "", false } rest := strings.TrimPrefix(u, "data:") idx := strings.Index(rest, ";base64,") if idx < 0 { return "", "", false } return rest[:idx], rest[idx+len(";base64,"):], true } func normalizeStreamData(apiType string, data string) ([]map[string]any, bool) { data = strings.TrimSpace(data) if data == "" || data == "[DONE]" { return nil, data == "[DONE]" } var raw map[string]any if err := json.Unmarshal([]byte(data), &raw); err != nil { return nil, false } switch apiType { case "responses": typ, _ := raw["type"].(string) switch typ { case "response.output_text.delta": if d, ok := raw["delta"].(string); ok && d != "" { return []map[string]any{{"t": "delta", "c": d}}, false } case "response.reasoning_summary_text.delta", "response.reasoning_text.delta": if d, ok := raw["delta"].(string); ok && d != "" { return []map[string]any{{"t": "think", "c": d}}, false } case "response.completed": return nil, true case "response.failed", "error": msg := "上游返回错误" if e, ok := raw["error"].(map[string]any); ok { if m, ok := e["message"].(string); ok { msg = m } } if r, ok := raw["response"].(map[string]any); ok { if e, ok := r["error"].(map[string]any); ok { if m, ok := e["message"].(string); ok { msg = m } } } return []map[string]any{{"t": "error", "c": msg}}, true } case "anthropic": typ, _ := raw["type"].(string) switch typ { case "content_block_delta": if d, ok := raw["delta"].(map[string]any); ok { dt, _ := d["type"].(string) if dt == "text_delta" { if t, ok := d["text"].(string); ok && t != "" { return []map[string]any{{"t": "delta", "c": t}}, false } } if dt == "thinking_delta" { if t, ok := d["thinking"].(string); ok && t != "" { return []map[string]any{{"t": "think", "c": t}}, false } } } case "message_stop": return nil, true case "error": msg := "上游返回错误" if e, ok := raw["error"].(map[string]any); ok { if m, ok := e["message"].(string); ok { msg = m } } return []map[string]any{{"t": "error", "c": msg}}, true } default: // chat_completions choices, _ := raw["choices"].([]any) if len(choices) > 0 { if ch, ok := choices[0].(map[string]any); ok { if delta, ok := ch["delta"].(map[string]any); ok { var evs []map[string]any if t, ok := delta["reasoning_content"].(string); ok && t != "" { evs = append(evs, map[string]any{"t": "think", "c": t}) } if t, ok := delta["content"].(string); ok && t != "" { evs = append(evs, map[string]any{"t": "delta", "c": t}) } return evs, false } } } if e, ok := raw["error"].(map[string]any); ok { if m, ok := e["message"].(string); ok { return []map[string]any{{"t": "error", "c": m}}, true } } } return nil, false } // ChatStream POST /platform/ai/chat/stream // SSE 代理:把三种上游协议的流式响应归一化为 {"t":"delta|think|done|error","c":"..."} 转发给前�? func (c *PlatformAiController) ChatStream() { defer c.StopRun() // 阻止 Beego 在 handler 返回后继续模板渲染 if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) c.jsonErr(code, code, err.Error()) return } var req struct { ProviderID uint64 `json:"provider_id"` Model string `json:"model"` Messages []map[string]any `json:"messages"` SessionID uint64 `json:"session_id"` } raw, _ := io.ReadAll(c.Ctx.Request.Body) if err := json.Unmarshal(raw, &req); err != nil || len(req.Messages) == 0 { c.jsonErr(400, 400, "参数错误") return } // 落库用户消息(最后一条) if req.SessionID > 0 { persistUserMessage(req.SessionID, req.Messages[len(req.Messages)-1]["content"]) } provider, err := loadPlatformProvider(req.ProviderID) if err != nil { c.jsonErr(400, 400, "服务商配置不存在或未启用") return } model := strings.TrimSpace(req.Model) if model == "" { model = provider.DefaultModel } if model == "" { c.jsonErr(400, 400, "未选择模型") return } clean := make([]map[string]any, 0, len(req.Messages)) for _, m := range req.Messages { role, _ := m["role"].(string) role = strings.TrimSpace(role) if role != "user" && role != "assistant" && role != "system" { continue } clean = append(clean, map[string]any{"role": role, "content": m["content"]}) } if len(clean) == 0 { c.jsonErr(400, 400, "消息为空") return } upReq, err := buildStreamRequest(provider, model, clean) if err != nil { c.jsonErr(500, 500, "构建请求失败") return } client := &http.Client{Timeout: 300 * time.Second} resp, err := client.Do(upReq) if err != nil { c.jsonErr(502, 502, "AI服务调用失败: "+err.Error()) return } defer resp.Body.Close() // 切换为 SSE 响应(Beego ResponseWriter 是包装类型,取底层 http.ResponseWriter) rw := c.Ctx.ResponseWriter.ResponseWriter rw.Header().Set("Content-Type", "text/event-stream; charset=utf-8") rw.Header().Set("Cache-Control", "no-cache") rw.Header().Set("Connection", "keep-alive") rw.Header().Set("X-Accel-Buffering", "no") rw.WriteHeader(200) flusher, _ := rw.(http.Flusher) if resp.StatusCode != 200 { body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) writeStreamEvent(rw, flusher, map[string]any{"t": "error", "c": "AI服务调用失败: " + string(body)}) writeStreamEvent(rw, flusher, map[string]any{"t": "done"}) return } scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 64*1024), 1024*1024) var accContent, accThink strings.Builder for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data:") { continue } payload := strings.TrimSpace(strings.TrimPrefix(line, "data:")) events, isDone := normalizeStreamData(provider.ApiType, payload) for _, ev := range events { writeStreamEvent(rw, flusher, ev) if ev["t"] == "delta" { if s, ok := ev["c"].(string); ok { accContent.WriteString(s) } } else if ev["t"] == "think" { if s, ok := ev["c"].(string); ok { accThink.WriteString(s) } } } if isDone { break } } writeStreamEvent(rw, flusher, map[string]any{"t": "done"}) persistStreamResult(req.SessionID, accContent.String(), accThink.String()) } // persistStreamResult 流式结束后落库:assistant 消息 + 会话更新时间/标题 func persistStreamResult(sessionID uint64, content, think string) { if sessionID == 0 { return } var last models.PlatformAiChatMessage _ = models.Orm.QueryTable(new(models.PlatformAiChatMessage)). Filter("session_id", sessionID).OrderBy("-seq").Limit(1).One(&last, "seq") _, _ = models.Orm.Insert(&models.PlatformAiChatMessage{ SessionID: sessionID, Role: "assistant", Content: content, Think: think, Seq: last.Seq + 1, CreateTime: time.Now(), }) _, _ = models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("id", sessionID). Update(orm.Params{"update_time": time.Now()}) } // persistUserMessage 流式开始前落库 user 消息(content 存原始 JSON,含附件) func persistUserMessage(sessionID uint64, content any) { if sessionID == 0 { return } b, _ := json.Marshal(content) var last models.PlatformAiChatMessage _ = models.Orm.QueryTable(new(models.PlatformAiChatMessage)). Filter("session_id", sessionID).OrderBy("-seq").Limit(1).One(&last, "seq") _, _ = models.Orm.Insert(&models.PlatformAiChatMessage{ SessionID: sessionID, Role: "user", Content: string(b), Seq: last.Seq + 1, CreateTime: time.Now(), }) // 首条消息时回填会话标题 var cnt int64 _, _ = models.Orm.QueryTable(new(models.PlatformAiChatMessage)). Filter("session_id", sessionID).Count() if cnt == 1 { if s := userContentText(content); s != "" { if r := []rune(s); len(r) > 30 { s = string(r[:30]) + "..." } _, _ = models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("id", sessionID).Update(orm.Params{"title": s}) } } _, _ = models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("id", sessionID). Update(orm.Params{"update_time": time.Now()}) } // userContentText 从 user content(字符串或多模态数组)提取纯文本用于标题 func userContentText(content any) string { switch v := content.(type) { case string: return v case []any: var sb strings.Builder for _, item := range v { if m, ok := item.(map[string]any); ok && m["type"] == "text" { if t, ok := m["text"].(string); ok { sb.WriteString(t) } } } return sb.String() } return "" } // ==================== 会话管理 ==================== // ListChatSessions GET /platform/ai/chat/sessions func (c *PlatformAiController) ListChatSessions() { if _, err := requirePlatform(&c.Controller); err != nil { c.jsonErr(403, 403, "无权限") return } var list []models.PlatformAiChatSession _, err := models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("delete_time__isnull", true). OrderBy("-is_top", "-update_time").Limit(100).All(&list) if err != nil { c.jsonErr(500, 500, "查询失败") return } c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": list} _ = c.ServeJSON() } // CreateChatSession POST /platform/ai/chat/session {provider_id, model} func (c *PlatformAiController) CreateChatSession() { if _, err := requirePlatform(&c.Controller); err != nil { c.jsonErr(403, 403, "无权限") return } var req struct { ProviderID uint64 `json:"provider_id"` Model string `json:"model"` } _ = json.Unmarshal(c.Ctx.Input.RequestBody, &req) s := models.PlatformAiChatSession{ ProviderID: req.ProviderID, Model: req.Model, CreateTime: time.Now(), UpdateTime: time.Now(), } id, err := models.Orm.Insert(&s) if err != nil { c.jsonErr(500, 500, "创建失败") return } s.ID = uint64(id) c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": s} _ = c.ServeJSON() } // GetChatSessionMessages GET /platform/ai/chat/session/:id/messages func (c *PlatformAiController) GetChatSessionMessages() { if _, err := requirePlatform(&c.Controller); err != nil { c.jsonErr(403, 403, "无权限") return } id, _ := c.GetInt64(":id") var msgs []models.PlatformAiChatMessage _, _ = models.Orm.QueryTable(new(models.PlatformAiChatMessage)). Filter("session_id", id).OrderBy("seq").All(&msgs) c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": msgs} _ = c.ServeJSON() } // UpdateChatSession PUT /platform/ai/chat/session/:id {title?, is_top?} func (c *PlatformAiController) UpdateChatSession() { if _, err := requirePlatform(&c.Controller); err != nil { c.jsonErr(403, 403, "无权限") return } id, _ := c.GetInt64(":id") var req struct { Title *string `json:"title"` IsTop *int8 `json:"is_top"` } _ = json.Unmarshal(c.Ctx.Input.RequestBody, &req) params := orm.Params{} if req.Title != nil { t := strings.TrimSpace(*req.Title) if t == "" { t = "新对话" } if r := []rune(t); len(r) > 100 { t = string(r[:100]) } params["title"] = t } if req.IsTop != nil { params["is_top"] = *req.IsTop } if len(params) == 0 { c.jsonErr(400, 400, "无更新内容") return } _, err := models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("id", id).Update(params) if err != nil { c.jsonErr(500, 500, "更新失败") return } c.Data["json"] = map[string]any{"code": 200, "msg": "success"} _ = c.ServeJSON() } // DeleteChatSession DELETE /platform/ai/chat/session/:id func (c *PlatformAiController) DeleteChatSession() { if _, err := requirePlatform(&c.Controller); err != nil { c.jsonErr(403, 403, "无权限") return } id, _ := c.GetInt64(":id") now := time.Now() _, _ = models.Orm.QueryTable(new(models.PlatformAiChatSession)). Filter("id", id).Update(orm.Params{"delete_time": now}) _, _ = models.Orm.QueryTable(new(models.PlatformAiChatMessage)). Filter("session_id", id).Delete() c.Data["json"] = map[string]any{"code": 200, "msg": "success"} _ = c.ServeJSON() }