Files
yunzerwebsiteallinone/go/controllers/platform_ai.go
T

1150 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}