增加相关功能

This commit is contained in:
2026-09-03 17:54:49 +08:00
parent 44a03186da
commit d8936d9730
30 changed files with 4503 additions and 2412 deletions
+794
View File
@@ -0,0 +1,794 @@
package controllers
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendAiChatController AI聊天消息控制器
type BackendAiChatController struct {
beego.Controller
}
func (c *BackendAiChatController) chatClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendAiChatController) chatJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendAiChatController) chatOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type chatSendPayload struct {
SessionID uint64 `json:"session_id"`
Content string `json:"content"`
ProviderID uint64 `json:"provider_id"`
Model string `json:"model"`
}
type openaiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type openaiRequest struct {
Model string `json:"model"`
Messages []openaiMessage `json:"messages"`
}
type openaiResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
type anthropicRequest struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []openaiMessage `json:"messages"`
}
type anthropicResponse struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
// MessageList GET /backend/ai/chat/message/list?session_id=xxx
func (c *BackendAiChatController) MessageList() {
claims, err := c.chatClaims()
if err != nil {
c.chatJsonErr(401, 401, err.Error())
return
}
sessionIDStr := strings.TrimSpace(c.GetString("session_id"))
if sessionIDStr == "" {
c.chatJsonErr(400, 400, "缺少会话ID")
return
}
sessionID, err := strconv.ParseUint(sessionIDStr, 10, 64)
if err != nil {
c.chatJsonErr(400, 400, "会话ID格式错误")
return
}
// 验证会话归属
session := models.BackendAiChatSession{ID: sessionID}
if err := models.Orm.Read(&session); err != nil {
c.chatJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权访问")
return
}
var list []models.BackendAiChatMessage
_, err = models.Orm.QueryTable(new(models.BackendAiChatMessage)).
Filter("session_id", sessionID).
OrderBy("id").
All(&list)
if err != nil {
c.chatJsonErr(500, 500, "查询失败: "+err.Error())
return
}
c.chatOk(map[string]interface{}{"list": list, "title": session.Title})
}
// DeleteMessage DELETE /backend/ai/chat/message/:id
func (c *BackendAiChatController) DeleteMessage() {
claims, err := c.chatClaims()
if err != nil {
c.chatJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.chatJsonErr(400, 400, "消息ID格式错误")
return
}
msg := models.BackendAiChatMessage{ID: id}
if err := models.Orm.Read(&msg); err != nil {
c.chatJsonErr(404, 404, "消息不存在")
return
}
// 验证会话归属
session := models.BackendAiChatSession{ID: msg.SessionID}
if err := models.Orm.Read(&session); err != nil {
c.chatJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权操作")
return
}
if _, err := models.Orm.Delete(&msg); err != nil {
c.chatJsonErr(500, 500, "删除失败: "+err.Error())
return
}
c.chatOk(nil)
}
// Send POST /backend/ai/chat/send
func (c *BackendAiChatController) Send() {
claims, err := c.chatClaims()
if err != nil {
c.chatJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.chatJsonErr(400, 400, "读取请求体失败")
return
}
var p chatSendPayload
if err := json.Unmarshal(body, &p); err != nil {
c.chatJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.Content) == "" {
c.chatJsonErr(400, 400, "消息内容不能为空")
return
}
// 查找或创建会话
var session models.BackendAiChatSession
if p.SessionID > 0 {
session = models.BackendAiChatSession{ID: p.SessionID}
if err := models.Orm.Read(&session); err != nil {
c.chatJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权操作")
return
}
} else {
// 新建会话,标题取消息前20字
title := p.Content
if len([]rune(title)) > 20 {
title = string([]rune(title)[:20]) + "..."
}
session = models.BackendAiChatSession{
TenantID: fmt.Sprintf("%d", claims.TenantId),
UserID: uint64(claims.UserID),
ProviderID: p.ProviderID,
Title: title,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
id, err := models.Orm.Insert(&session)
if err != nil {
c.chatJsonErr(500, 500, "创建会话失败: "+err.Error())
return
}
session.ID = uint64(id)
}
// 查找AI接入配置
providerID := p.ProviderID
if providerID == 0 {
providerID = session.ProviderID
}
var provider models.BackendAiProvider
if providerID > 0 {
provider = models.BackendAiProvider{ID: providerID}
if err := models.Orm.Read(&provider); err != nil {
c.chatJsonErr(400, 400, "指定的AI接入配置不存在,请先在设置中配置")
return
}
if provider.TenantID != fmt.Sprintf("%d", claims.TenantId) || provider.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权使用该配置")
return
}
if provider.Status != 1 {
c.chatJsonErr(400, 400, "该AI接入配置已被禁用")
return
}
} else {
// 找第一个启用的配置
var providers []models.BackendAiProvider
_, err := models.Orm.QueryTable(new(models.BackendAiProvider)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("status", 1).
Filter("delete_time__isnull", true).
OrderBy("-id").
Limit(1).
All(&providers)
if err != nil || len(providers) == 0 {
c.chatJsonErr(400, 400, "尚未配置AI接入,请先在设置中配置OpenAI或Anthropic接入")
return
}
provider = providers[0]
}
// 加载历史消息(最近20条)
var history []models.BackendAiChatMessage
_, _ = models.Orm.QueryTable(new(models.BackendAiChatMessage)).
Filter("session_id", session.ID).
OrderBy("-id").
Limit(20).
All(&history)
// 反转顺序
for i, j := 0, len(history)-1; i < j; i, j = i+1, j-1 {
history[i], history[j] = history[j], history[i]
}
// 构建消息列表
messages := make([]openaiMessage, 0, len(history)+1)
for _, m := range history {
messages = append(messages, openaiMessage{Role: m.Role, Content: m.Content})
}
messages = append(messages, openaiMessage{Role: "user", Content: p.Content})
// 保存用户消息
userMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "user",
Content: p.Content,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&userMsg)
// 确定使用的模型:用户指定 > provider第一个模型
useModel := strings.TrimSpace(p.Model)
if useModel == "" {
providerModels := parseProviderModels(provider.Models)
if len(providerModels) > 0 {
useModel = providerModels[0]
}
}
if useModel == "" {
c.chatJsonErr(400, 400, "未指定模型且该接入配置无可用模型")
return
}
// 查询用户默认角色预设
var systemPrompt string
var presets []models.BackendAiChatPreset
_, _ = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("is_default", 1).
Filter("delete_time__isnull", true).
Limit(1).
All(&presets)
if len(presets) > 0 {
systemPrompt = presets[0].Content
}
// 调用AI
reply, err := callAI(provider, useModel, systemPrompt, messages)
if err != nil {
c.chatJsonErr(500, 500, "AI调用失败: "+err.Error())
return
}
// 保存AI回复
assistantMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: reply,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&assistantMsg)
// 更新会话时间
session.UpdateTime = time.Now()
_, _ = models.Orm.Update(&session, "update_time")
c.chatOk(map[string]interface{}{
"session_id": session.ID,
"reply": reply,
"message_id": assistantMsg.ID,
})
}
func callAI(provider models.BackendAiProvider, model string, systemPrompt string, messages []openaiMessage) (string, error) {
client := &http.Client{Timeout: 120 * time.Second}
if provider.ProviderType == "openai" {
// OpenAI 兼容格式
reqMessages := messages
if systemPrompt != "" {
reqMessages = append([]openaiMessage{{Role: "system", Content: systemPrompt}}, messages...)
}
reqBody := openaiRequest{
Model: model,
Messages: reqMessages,
}
jsonData, _ := json.Marshal(reqBody)
// 处理URL:如果已经包含/chat/completions,直接使用;否则拼接
url := strings.TrimRight(provider.ApiBase, "/")
if !strings.HasSuffix(url, "/chat/completions") {
url = url + "/chat/completions"
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+provider.ApiKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result openaiResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析响应失败: %s", string(body))
}
if result.Error != nil {
return "", fmt.Errorf(result.Error.Message)
}
if len(result.Choices) == 0 {
return "", fmt.Errorf("AI未返回内容")
}
return result.Choices[0].Message.Content, nil
} else {
// Anthropic 格式
reqBody := anthropicRequest{
Model: model,
MaxTokens: 4096,
System: systemPrompt,
Messages: messages,
}
jsonData, _ := json.Marshal(reqBody)
// 处理URL:如果已经包含/v1/messages,直接使用;否则拼接
url := strings.TrimRight(provider.ApiBase, "/")
if !strings.HasSuffix(url, "/v1/messages") {
url = url + "/v1/messages"
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", provider.ApiKey)
req.Header.Set("anthropic-version", "2023-06-01")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result anthropicResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析响应失败: %s", string(body))
}
if result.Error != nil {
return "", fmt.Errorf(result.Error.Message)
}
if len(result.Content) == 0 {
return "", fmt.Errorf("AI未返回内容")
}
return result.Content[0].Text, nil
}
}
// SendStream POST /backend/ai/chat/send-stream
// 流式响应(SSE),逐块返回AI回复
func (c *BackendAiChatController) SendStream() {
claims, err := c.chatClaims()
if err != nil {
c.chatJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.chatJsonErr(400, 400, "读取请求体失败")
return
}
var p chatSendPayload
if err := json.Unmarshal(body, &p); err != nil {
c.chatJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.Content) == "" {
c.chatJsonErr(400, 400, "消息内容不能为空")
return
}
// 查找或创建会话
var session models.BackendAiChatSession
if p.SessionID > 0 {
session = models.BackendAiChatSession{ID: p.SessionID}
if err := models.Orm.Read(&session); err != nil {
c.chatJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权操作")
return
}
} else {
title := p.Content
if len([]rune(title)) > 20 {
title = string([]rune(title)[:20]) + "..."
}
session = models.BackendAiChatSession{
TenantID: fmt.Sprintf("%d", claims.TenantId),
UserID: uint64(claims.UserID),
ProviderID: p.ProviderID,
Title: title,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
id, err := models.Orm.Insert(&session)
if err != nil {
c.chatJsonErr(500, 500, "创建会话失败: "+err.Error())
return
}
session.ID = uint64(id)
}
// 查找AI接入配置
providerID := p.ProviderID
if providerID == 0 {
providerID = session.ProviderID
}
var provider models.BackendAiProvider
if providerID > 0 {
provider = models.BackendAiProvider{ID: providerID}
if err := models.Orm.Read(&provider); err != nil {
c.chatJsonErr(400, 400, "指定的AI接入配置不存在")
return
}
if provider.TenantID != fmt.Sprintf("%d", claims.TenantId) || provider.UserID != uint64(claims.UserID) {
c.chatJsonErr(403, 403, "无权使用该配置")
return
}
if provider.Status != 1 {
c.chatJsonErr(400, 400, "该AI接入配置已被禁用")
return
}
} else {
var providers []models.BackendAiProvider
_, err := models.Orm.QueryTable(new(models.BackendAiProvider)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("status", 1).
Filter("delete_time__isnull", true).
OrderBy("-id").
Limit(1).
All(&providers)
if err != nil || len(providers) == 0 {
c.chatJsonErr(400, 400, "尚未配置AI接入,请先在设置中配置")
return
}
provider = providers[0]
}
// 确定模型
useModel := strings.TrimSpace(p.Model)
if useModel == "" {
providerModels := parseProviderModels(provider.Models)
if len(providerModels) > 0 {
useModel = providerModels[0]
}
}
if useModel == "" {
c.chatJsonErr(400, 400, "未指定模型且该接入配置无可用模型")
return
}
// 查询默认预设
var systemPrompt string
var presets []models.BackendAiChatPreset
_, _ = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("is_default", 1).
Filter("delete_time__isnull", true).
Limit(1).
All(&presets)
if len(presets) > 0 {
systemPrompt = presets[0].Content
}
// 加载历史消息
var history []models.BackendAiChatMessage
_, _ = models.Orm.QueryTable(new(models.BackendAiChatMessage)).
Filter("session_id", session.ID).
OrderBy("-id").
Limit(20).
All(&history)
for i, j := 0, len(history)-1; i < j; i, j = i+1, j-1 {
history[i], history[j] = history[j], history[i]
}
messages := make([]openaiMessage, 0, len(history)+1)
for _, m := range history {
messages = append(messages, openaiMessage{Role: m.Role, Content: m.Content})
}
messages = append(messages, openaiMessage{Role: "user", Content: p.Content})
// 保存用户消息
userMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "user",
Content: p.Content,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&userMsg)
// 设置SSE响应头 - 直接操作底层ResponseWriter,绕过beego包装层
rw := c.Ctx.ResponseWriter.ResponseWriter
rw.Header().Set("Content-Type", "text/event-stream")
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Connection", "keep-alive")
rw.Header().Set("X-Accel-Buffering", "no")
rw.WriteHeader(200)
var flusher http.Flusher
if f, ok := rw.(http.Flusher); ok {
flusher = f
}
writeSSE := func(event string, data string) {
rw.Write([]byte(fmt.Sprintf("event: %s\ndata: %s\n\n", event, data)))
if flusher != nil {
flusher.Flush()
}
}
// 发送session_id事件
writeSSE("session", fmt.Sprintf("{\"session_id\":%d}", session.ID))
// 流式调用AI
fullReply := ""
streamErr := callAIStream(provider, useModel, systemPrompt, messages, func(chunk string) {
fullReply += chunk
chunkJSON, _ := json.Marshal(map[string]string{"content": chunk})
writeSSE("content", string(chunkJSON))
})
if streamErr != nil {
errJSON, _ := json.Marshal(map[string]string{"error": streamErr.Error()})
writeSSE("error", string(errJSON))
return
}
// 保存AI回复
assistantMsg := models.BackendAiChatMessage{
TenantID: fmt.Sprintf("%d", claims.TenantId),
SessionID: session.ID,
Role: "assistant",
Content: fullReply,
CreateTime: time.Now(),
}
_, _ = models.Orm.Insert(&assistantMsg)
// 更新会话时间
session.UpdateTime = time.Now()
_, _ = models.Orm.Update(&session, "update_time")
// 发送done事件
doneJSON, _ := json.Marshal(map[string]interface{}{
"session_id": session.ID,
"message_id": assistantMsg.ID,
})
writeSSE("done", string(doneJSON))
}
// callAIStream 流式调用AI,onChunk回调每收到一个内容块就调用
func callAIStream(provider models.BackendAiProvider, model string, systemPrompt string, messages []openaiMessage, onChunk func(string)) error {
client := &http.Client{Timeout: 120 * time.Second}
if provider.ProviderType == "openai" {
// OpenAI 兼容格式 - 流式
reqMessages := messages
if systemPrompt != "" {
reqMessages = append([]openaiMessage{{Role: "system", Content: systemPrompt}}, messages...)
}
reqBody := map[string]interface{}{
"model": model,
"messages": reqMessages,
"stream": true,
}
jsonData, _ := json.Marshal(reqBody)
// 处理URL:如果已经包含/chat/completions,直接使用;否则拼接
url := strings.TrimRight(provider.ApiBase, "/")
if !strings.HasSuffix(url, "/chat/completions") {
url = url + "/chat/completions"
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+provider.ApiKey)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API返回错误状态 %d: %s", resp.StatusCode, string(body))
}
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
break
}
var chunk struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue
}
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
onChunk(chunk.Choices[0].Delta.Content)
}
}
return scanner.Err()
} else {
// Anthropic 格式 - 流式
reqBody := map[string]interface{}{
"model": model,
"max_tokens": 4096,
"messages": messages,
"stream": true,
}
if systemPrompt != "" {
reqBody["system"] = systemPrompt
}
jsonData, _ := json.Marshal(reqBody)
// 处理URL:如果已经包含/v1/messages,直接使用;否则拼接
url := strings.TrimRight(provider.ApiBase, "/")
if !strings.HasSuffix(url, "/v1/messages") {
url = url + "/v1/messages"
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", provider.ApiKey)
req.Header.Set("anthropic-version", "2023-06-01")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API返回错误状态 %d: %s", resp.StatusCode, string(body))
}
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
currentEvent := ""
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "event: ") {
currentEvent = strings.TrimPrefix(line, "event: ")
continue
}
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if currentEvent == "content_block_delta" {
var chunk struct {
Delta struct {
Text string `json:"text"`
} `json:"delta"`
}
if err := json.Unmarshal([]byte(data), &chunk); err == nil && chunk.Delta.Text != "" {
onChunk(chunk.Delta.Text)
}
}
if currentEvent == "message_stop" {
break
}
}
}
return scanner.Err()
}
}
+288
View File
@@ -0,0 +1,288 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendAiChatPresetController AI聊天角色预设控制器
type BackendAiChatPresetController struct {
beego.Controller
}
func (c *BackendAiChatPresetController) presetClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendAiChatPresetController) presetJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendAiChatPresetController) presetOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type aiPresetPayload struct {
Name string `json:"name"`
Content string `json:"content"`
IsDefault int8 `json:"is_default"`
}
// List GET /backend/ai/preset/list
func (c *BackendAiChatPresetController) List() {
claims, err := c.presetClaims()
if err != nil {
c.presetJsonErr(401, 401, err.Error())
return
}
var list []models.BackendAiChatPreset
_, err = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("delete_time__isnull", true).
OrderBy("-is_default", "-id").
All(&list)
if err != nil {
c.presetJsonErr(500, 500, "查询失败: "+err.Error())
return
}
c.presetOk(map[string]interface{}{"list": list})
}
// Create POST /backend/ai/preset
func (c *BackendAiChatPresetController) Create() {
claims, err := c.presetClaims()
if err != nil {
c.presetJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.presetJsonErr(400, 400, "读取请求体失败")
return
}
var p aiPresetPayload
if err := json.Unmarshal(body, &p); err != nil {
c.presetJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.Name) == "" {
c.presetJsonErr(400, 400, "预设名称不能为空")
return
}
if strings.TrimSpace(p.Content) == "" {
c.presetJsonErr(400, 400, "预设内容不能为空")
return
}
// 如果设为默认,先取消其他默认
if p.IsDefault == 1 {
var defaults []models.BackendAiChatPreset
_, _ = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("is_default", 1).
All(&defaults)
for _, d := range defaults {
d.IsDefault = 0
d.UpdateTime = time.Now()
_, _ = models.Orm.Update(&d, "is_default", "update_time")
}
}
preset := models.BackendAiChatPreset{
TenantID: fmt.Sprintf("%d", claims.TenantId),
UserID: uint64(claims.UserID),
Name: strings.TrimSpace(p.Name),
Content: strings.TrimSpace(p.Content),
IsDefault: p.IsDefault,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
id, err := models.Orm.Insert(&preset)
if err != nil {
c.presetJsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.presetOk(map[string]interface{}{"id": id})
}
// Update PUT /backend/ai/preset/:id
func (c *BackendAiChatPresetController) Update() {
claims, err := c.presetClaims()
if err != nil {
c.presetJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.presetJsonErr(400, 400, "ID格式错误")
return
}
preset := models.BackendAiChatPreset{ID: id}
if err := models.Orm.Read(&preset); err != nil {
c.presetJsonErr(404, 404, "预设不存在")
return
}
if preset.TenantID != fmt.Sprintf("%d", claims.TenantId) || preset.UserID != uint64(claims.UserID) {
c.presetJsonErr(403, 403, "无权操作")
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.presetJsonErr(400, 400, "读取请求体失败")
return
}
var p aiPresetPayload
if err := json.Unmarshal(body, &p); err != nil {
c.presetJsonErr(400, 400, "参数格式错误")
return
}
// 如果设为默认,先取消其他默认
if p.IsDefault == 1 && preset.IsDefault != 1 {
var defaults []models.BackendAiChatPreset
_, _ = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("is_default", 1).
All(&defaults)
for _, d := range defaults {
d.IsDefault = 0
d.UpdateTime = time.Now()
_, _ = models.Orm.Update(&d, "is_default", "update_time")
}
}
preset.Name = strings.TrimSpace(p.Name)
preset.Content = strings.TrimSpace(p.Content)
preset.IsDefault = p.IsDefault
preset.UpdateTime = time.Now()
_, err = models.Orm.Update(&preset)
if err != nil {
c.presetJsonErr(500, 500, "更新失败: "+err.Error())
return
}
c.presetOk(nil)
}
// SetDefault PUT /backend/ai/preset/:id/default
func (c *BackendAiChatPresetController) SetDefault() {
claims, err := c.presetClaims()
if err != nil {
c.presetJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.presetJsonErr(400, 400, "ID格式错误")
return
}
preset := models.BackendAiChatPreset{ID: id}
if err := models.Orm.Read(&preset); err != nil {
c.presetJsonErr(404, 404, "预设不存在")
return
}
if preset.TenantID != fmt.Sprintf("%d", claims.TenantId) || preset.UserID != uint64(claims.UserID) {
c.presetJsonErr(403, 403, "无权操作")
return
}
// 取消其他默认
var defaults []models.BackendAiChatPreset
_, _ = models.Orm.QueryTable(new(models.BackendAiChatPreset)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("is_default", 1).
All(&defaults)
for _, d := range defaults {
d.IsDefault = 0
d.UpdateTime = time.Now()
_, _ = models.Orm.Update(&d, "is_default", "update_time")
}
// 设为默认
preset.IsDefault = 1
preset.UpdateTime = time.Now()
_, _ = models.Orm.Update(&preset, "is_default", "update_time")
c.presetOk(nil)
}
// Delete DELETE /backend/ai/preset/:id
func (c *BackendAiChatPresetController) Delete() {
claims, err := c.presetClaims()
if err != nil {
c.presetJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.presetJsonErr(400, 400, "ID格式错误")
return
}
preset := models.BackendAiChatPreset{ID: id}
if err := models.Orm.Read(&preset); err != nil {
c.presetJsonErr(404, 404, "预设不存在")
return
}
if preset.TenantID != fmt.Sprintf("%d", claims.TenantId) || preset.UserID != uint64(claims.UserID) {
c.presetJsonErr(403, 403, "无权操作")
return
}
now := time.Now()
preset.DeleteTime = &now
_, err = models.Orm.Update(&preset, "delete_time")
if err != nil {
c.presetJsonErr(500, 500, "删除失败: "+err.Error())
return
}
c.presetOk(nil)
}
+209
View File
@@ -0,0 +1,209 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendAiChatSessionController AI聊天会话控制器
type BackendAiChatSessionController struct {
beego.Controller
}
func (c *BackendAiChatSessionController) sessionClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendAiChatSessionController) sessionJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendAiChatSessionController) sessionOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type chatSessionPayload struct {
Title string `json:"title"`
ProviderID uint64 `json:"provider_id"`
}
// List GET /backend/ai/chat/session/list
func (c *BackendAiChatSessionController) List() {
claims, err := c.sessionClaims()
if err != nil {
c.sessionJsonErr(401, 401, err.Error())
return
}
var list []models.BackendAiChatSession
_, err = models.Orm.QueryTable(new(models.BackendAiChatSession)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("delete_time__isnull", true).
OrderBy("-update_time").
All(&list)
if err != nil {
c.sessionJsonErr(500, 500, "查询失败: "+err.Error())
return
}
c.sessionOk(map[string]interface{}{"list": list})
}
// Create POST /backend/ai/chat/session
func (c *BackendAiChatSessionController) Create() {
claims, err := c.sessionClaims()
if err != nil {
c.sessionJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.sessionJsonErr(400, 400, "读取请求体失败")
return
}
var p chatSessionPayload
if err := json.Unmarshal(body, &p); err != nil {
c.sessionJsonErr(400, 400, "参数格式错误")
return
}
title := strings.TrimSpace(p.Title)
if title == "" {
title = "新对话"
}
session := models.BackendAiChatSession{
TenantID: fmt.Sprintf("%d", claims.TenantId),
UserID: uint64(claims.UserID),
ProviderID: p.ProviderID,
Title: title,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
id, err := models.Orm.Insert(&session)
if err != nil {
c.sessionJsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.sessionOk(map[string]interface{}{"id": id, "title": title})
}
// Update PUT /backend/ai/chat/session/:id(重命名)
func (c *BackendAiChatSessionController) Update() {
claims, err := c.sessionClaims()
if err != nil {
c.sessionJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.sessionJsonErr(400, 400, "ID格式错误")
return
}
session := models.BackendAiChatSession{ID: id}
if err := models.Orm.Read(&session); err != nil {
c.sessionJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.sessionJsonErr(403, 403, "无权操作")
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.sessionJsonErr(400, 400, "读取请求体失败")
return
}
var p chatSessionPayload
if err := json.Unmarshal(body, &p); err != nil {
c.sessionJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.Title) != "" {
session.Title = strings.TrimSpace(p.Title)
}
if p.ProviderID > 0 {
session.ProviderID = p.ProviderID
}
session.UpdateTime = time.Now()
_, err = models.Orm.Update(&session)
if err != nil {
c.sessionJsonErr(500, 500, "更新失败: "+err.Error())
return
}
c.sessionOk(nil)
}
// Delete DELETE /backend/ai/chat/session/:id
func (c *BackendAiChatSessionController) Delete() {
claims, err := c.sessionClaims()
if err != nil {
c.sessionJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.sessionJsonErr(400, 400, "ID格式错误")
return
}
session := models.BackendAiChatSession{ID: id}
if err := models.Orm.Read(&session); err != nil {
c.sessionJsonErr(404, 404, "会话不存在")
return
}
if session.TenantID != fmt.Sprintf("%d", claims.TenantId) || session.UserID != uint64(claims.UserID) {
c.sessionJsonErr(403, 403, "无权操作")
return
}
now := time.Now()
session.DeleteTime = &now
_, err = models.Orm.Update(&session, "delete_time")
if err != nil {
c.sessionJsonErr(500, 500, "删除失败: "+err.Error())
return
}
c.sessionOk(nil)
}
+360
View File
@@ -0,0 +1,360 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendAiProviderController AI接入配置控制器
type BackendAiProviderController struct {
beego.Controller
}
func (c *BackendAiProviderController) aiClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendAiProviderController) aiJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendAiProviderController) aiOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type aiProviderPayload struct {
ProviderType string `json:"provider_type"`
Name string `json:"name"`
ApiBase string `json:"api_base"`
ApiKey string `json:"api_key"`
Models []string `json:"models"`
IsDefault int8 `json:"is_default"`
Status int8 `json:"status"`
Remark string `json:"remark"`
}
// clearTenantDefault 取消租户内所有默认配置
func clearTenantDefault(tenantID string) {
var defaults []models.BackendAiProvider
_, _ = models.Orm.QueryTable(new(models.BackendAiProvider)).
Filter("tenant_id", tenantID).
Filter("is_default", 1).
All(&defaults)
for _, d := range defaults {
d.IsDefault = 0
d.UpdateTime = time.Now()
_, _ = models.Orm.Update(&d, "is_default", "update_time")
}
}
// parseModels 将模型JSON字符串解析为数组
func parseProviderModels(m string) []string {
if m == "" {
return []string{}
}
var list []string
if err := json.Unmarshal([]byte(m), &list); err != nil {
// 兼容旧格式:逗号分隔的单个模型
return []string{m}
}
return list
}
// List GET /backend/ai/provider/list
func (c *BackendAiProviderController) List() {
claims, err := c.aiClaims()
if err != nil {
c.aiJsonErr(401, 401, err.Error())
return
}
var list []models.BackendAiProvider
_, err = models.Orm.QueryTable(new(models.BackendAiProvider)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("user_id", uint64(claims.UserID)).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&list)
if err != nil {
c.aiJsonErr(500, 500, "查询失败: "+err.Error())
return
}
// 脱敏api_key + 解析模型列表
for i := range list {
if list[i].ApiKey != "" && len(list[i].ApiKey) > 8 {
list[i].ApiKey = list[i].ApiKey[:4] + "****" + list[i].ApiKey[len(list[i].ApiKey)-4:]
}
list[i].ModelsList = parseProviderModels(list[i].Models)
}
c.aiOk(map[string]interface{}{"list": list})
}
// Create POST /backend/ai/provider
func (c *BackendAiProviderController) Create() {
claims, err := c.aiClaims()
if err != nil {
c.aiJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.aiJsonErr(400, 400, "读取请求体失败")
return
}
var p aiProviderPayload
if err := json.Unmarshal(body, &p); err != nil {
c.aiJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.Name) == "" {
c.aiJsonErr(400, 400, "配置名称不能为空")
return
}
if p.ProviderType != "openai" && p.ProviderType != "anthropic" {
c.aiJsonErr(400, 400, "接入类型必须是 openai 或 anthropic")
return
}
if strings.TrimSpace(p.ApiBase) == "" || strings.TrimSpace(p.ApiKey) == "" {
c.aiJsonErr(400, 400, "接口地址、API Key不能为空")
return
}
if len(p.Models) == 0 {
c.aiJsonErr(400, 400, "至少配置一个模型")
return
}
// 模型数组序列化为JSON
modelsJSON, _ := json.Marshal(p.Models)
// 如果设为默认,先取消租户内其他默认
if p.IsDefault == 1 {
clearTenantDefault(fmt.Sprintf("%d", claims.TenantId))
}
provider := models.BackendAiProvider{
TenantID: fmt.Sprintf("%d", claims.TenantId),
UserID: uint64(claims.UserID),
ProviderType: p.ProviderType,
Name: strings.TrimSpace(p.Name),
ApiBase: strings.TrimSpace(p.ApiBase),
ApiKey: strings.TrimSpace(p.ApiKey),
Models: string(modelsJSON),
IsDefault: p.IsDefault,
Status: p.Status,
Remark: strings.TrimSpace(p.Remark),
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
id, err := models.Orm.Insert(&provider)
if err != nil {
c.aiJsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.aiOk(map[string]interface{}{"id": id})
}
// Update PUT /backend/ai/provider/:id
func (c *BackendAiProviderController) Update() {
claims, err := c.aiClaims()
if err != nil {
c.aiJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.aiJsonErr(400, 400, "ID格式错误")
return
}
provider := models.BackendAiProvider{ID: id}
if err := models.Orm.Read(&provider); err != nil {
c.aiJsonErr(404, 404, "配置不存在")
return
}
if provider.TenantID != fmt.Sprintf("%d", claims.TenantId) {
c.aiJsonErr(403, 403, "无权操作")
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.aiJsonErr(400, 400, "读取请求体失败")
return
}
var p aiProviderPayload
if err := json.Unmarshal(body, &p); err != nil {
c.aiJsonErr(400, 400, "参数格式错误")
return
}
// 如果设为默认,先取消租户内其他默认
if p.IsDefault == 1 && provider.IsDefault != 1 {
clearTenantDefault(fmt.Sprintf("%d", claims.TenantId))
}
provider.Name = strings.TrimSpace(p.Name)
provider.ProviderType = p.ProviderType
provider.ApiBase = strings.TrimSpace(p.ApiBase)
// 只有传入了非脱敏的key才更新
if p.ApiKey != "" && !strings.Contains(p.ApiKey, "****") {
provider.ApiKey = strings.TrimSpace(p.ApiKey)
}
// 更新模型列表
if len(p.Models) > 0 {
modelsJSON, _ := json.Marshal(p.Models)
provider.Models = string(modelsJSON)
}
provider.IsDefault = p.IsDefault
provider.Status = p.Status
provider.Remark = strings.TrimSpace(p.Remark)
provider.UpdateTime = time.Now()
_, err = models.Orm.Update(&provider)
if err != nil {
c.aiJsonErr(500, 500, "更新失败: "+err.Error())
return
}
c.aiOk(nil)
}
// Delete DELETE /backend/ai/provider/:id
func (c *BackendAiProviderController) Delete() {
claims, err := c.aiClaims()
if err != nil {
c.aiJsonErr(401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.aiJsonErr(400, 400, "ID格式错误")
return
}
provider := models.BackendAiProvider{ID: id}
if err := models.Orm.Read(&provider); err != nil {
c.aiJsonErr(404, 404, "配置不存在")
return
}
if provider.TenantID != fmt.Sprintf("%d", claims.TenantId) {
c.aiJsonErr(403, 403, "无权操作")
return
}
now := time.Now()
provider.DeleteTime = &now
_, err = models.Orm.Update(&provider, "delete_time")
if err != nil {
c.aiJsonErr(500, 500, "删除失败: "+err.Error())
return
}
c.aiOk(nil)
}
type aiTestPayload struct {
ProviderType string `json:"provider_type"`
ApiBase string `json:"api_base"`
ApiKey string `json:"api_key"`
Models []string `json:"models"`
}
type modelTestResult struct {
Model string `json:"model"`
Success bool `json:"success"`
Error string `json:"error"`
}
// Test POST /backend/ai/provider/test
// 批量测试模型连通性
func (c *BackendAiProviderController) Test() {
if _, err := c.aiClaims(); err != nil {
c.aiJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.aiJsonErr(400, 400, "读取请求体失败")
return
}
var p aiTestPayload
if err := json.Unmarshal(body, &p); err != nil {
c.aiJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.ApiBase) == "" || strings.TrimSpace(p.ApiKey) == "" {
c.aiJsonErr(400, 400, "接口地址和API Key不能为空")
return
}
if len(p.Models) == 0 {
c.aiOk(map[string]interface{}{"results": []modelTestResult{}})
return
}
// 构造临时provider
provider := models.BackendAiProvider{
ProviderType: p.ProviderType,
ApiBase: strings.TrimSpace(p.ApiBase),
ApiKey: strings.TrimSpace(p.ApiKey),
}
testMessages := []openaiMessage{{Role: "user", Content: "hi"}}
// 并发测试
results := make([]modelTestResult, len(p.Models))
var wg sync.WaitGroup
for i, model := range p.Models {
wg.Add(1)
go func(idx int, m string) {
defer wg.Done()
_, err := callAI(provider, m, "", testMessages)
if err != nil {
results[idx] = modelTestResult{Model: m, Success: false, Error: err.Error()}
} else {
results[idx] = modelTestResult{Model: m, Success: true, Error: ""}
}
}(i, model)
}
wg.Wait()
c.aiOk(map[string]interface{}{"results": results})
}
+184
View File
@@ -0,0 +1,184 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// BackendAiSmartGenerateController AI智能生成控制器
type BackendAiSmartGenerateController struct {
beego.Controller
}
func (c *BackendAiSmartGenerateController) sgClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendAiSmartGenerateController) sgJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendAiSmartGenerateController) sgOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type smartGeneratePayload struct {
CompanyName string `json:"company_name"`
Type string `json:"type"` // supplier / customer
}
// GenerateCompany POST /backend/ai/smart-generate/company
// 根据公司名称智能生成详细信息
func (c *BackendAiSmartGenerateController) GenerateCompany() {
claims, err := c.sgClaims()
if err != nil {
c.sgJsonErr(401, 401, err.Error())
return
}
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.sgJsonErr(400, 400, "读取请求体失败")
return
}
var p smartGeneratePayload
if err := json.Unmarshal(body, &p); err != nil {
c.sgJsonErr(400, 400, "参数格式错误")
return
}
if strings.TrimSpace(p.CompanyName) == "" {
c.sgJsonErr(400, 400, "公司名称不能为空")
return
}
if p.Type != "supplier" && p.Type != "customer" {
c.sgJsonErr(400, 400, "类型必须是 supplier 或 customer")
return
}
// 查询租户默认AI配置
var providers []models.BackendAiProvider
_, err = models.Orm.QueryTable(new(models.BackendAiProvider)).
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
Filter("is_default", 1).
Filter("status", 1).
Filter("delete_time__isnull", true).
Limit(1).
All(&providers)
if err != nil || len(providers) == 0 {
c.sgJsonErr(400, 400, "当前企业没有配置智能体无法使用本功能,请联系管理员")
return
}
provider := providers[0]
// 确定使用的模型(第一个)
providerModels := parseProviderModels(provider.Models)
if len(providerModels) == 0 {
c.sgJsonErr(400, 400, "默认接入配置没有可用模型")
return
}
useModel := providerModels[0]
// 构造prompt
typeLabel := "供应商"
nameField := "supplier_name"
if p.Type == "customer" {
typeLabel = "客户"
nameField = "customer_name"
}
prompt := fmt.Sprintf(`你是一个企业信息助手。请根据%s名称"%s",生成该公司的详细信息,以JSON格式返回,包含以下字段:
- %s: 公司全称
- contact_person: 联系人
- contact_phone: 联系电话
- contact_email: 邮箱
- address: 公司地址
- industry: 所属行业
- registered_capital: 注册资本(如"100万元")
- paid_capital: 实缴资本(如"50万元")
- establish_date: 成立日期(YYYY-MM-DD格式,不确定则留空)
- administrative_division: 行政区划(如"江苏省连云港市海州区")
- enterprise_type: 企业类型(如"有限责任公司")
- taxpayer_qualification: 纳税人资质("一般纳税人"或"小规模纳税人")
- business_scope: 经营范围
- invoice_title: 发票抬头(公司全称)
- tax_number: 统一社会信用代码/税号(18位,不确定则留空)
- bank_name: 开户银行
- bank_account: 银行账号
- registered_address: 注册地址
- registered_phone: 注册电话
- remark: 备注
请只返回JSON对象,不要返回其他任何文字、解释或markdown代码块标记。如果某些信息不确定,请留空字符串。`, typeLabel, p.CompanyName, nameField)
messages := []openaiMessage{
{Role: "user", Content: prompt},
}
// 调用AI(非流式,超时120s)
reply, err := callAI(provider, useModel, "", messages)
if err != nil {
c.sgJsonErr(500, 500, "AI生成失败: "+err.Error())
return
}
// 解析AI返回的JSON
reply = strings.TrimSpace(reply)
// 去掉可能的markdown代码块标记
reply = strings.TrimPrefix(reply, "```json")
reply = strings.TrimPrefix(reply, "```")
reply = strings.TrimSuffix(reply, "```")
reply = strings.TrimSpace(reply)
var result map[string]interface{}
if err := json.Unmarshal([]byte(reply), &result); err != nil {
// 解析失败,返回原始文本让用户自己处理
c.sgOk(map[string]interface{}{
"raw": reply,
"parsed": false,
"name": p.CompanyName,
"type": p.Type,
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
})
return
}
// 确保公司名称字段存在
if _, ok := result[nameField]; !ok || result[nameField] == "" {
result[nameField] = p.CompanyName
}
c.sgOk(map[string]interface{}{
"parsed": true,
"data": result,
"name": p.CompanyName,
"type": p.Type,
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
})
}
+17
View File
@@ -0,0 +1,17 @@
package models
import "time"
// BackendAiChatMessage AI聊天消息表
type BackendAiChatMessage struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
SessionID uint64 `orm:"column(session_id)" json:"session_id"`
Role string `orm:"column(role);size(20)" json:"role"` // user / assistant
Content string `orm:"column(content);type(text)" json:"content"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
}
func (m *BackendAiChatMessage) TableName() string {
return "yz_backend_ai_chat_message"
}
+20
View File
@@ -0,0 +1,20 @@
package models
import "time"
// BackendAiChatPreset AI聊天角色预设表(用户级)
type BackendAiChatPreset struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
UserID uint64 `orm:"column(user_id);default(0)" json:"user_id"`
Name string `orm:"column(name);size(100)" json:"name"`
Content string `orm:"column(content);type(text)" json:"content"`
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"` // 0-否 1-是
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *BackendAiChatPreset) TableName() string {
return "yz_backend_ai_chat_preset"
}
+19
View File
@@ -0,0 +1,19 @@
package models
import "time"
// BackendAiChatSession AI聊天会话表
type BackendAiChatSession struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
UserID uint64 `orm:"column(user_id);default(0)" json:"user_id"`
ProviderID uint64 `orm:"column(provider_id);default(0)" json:"provider_id"`
Title string `orm:"column(title);size(200)" json:"title"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *BackendAiChatSession) TableName() string {
return "yz_backend_ai_chat_session"
}
+26
View File
@@ -0,0 +1,26 @@
package models
import "time"
// BackendAiProvider AI接入配置表(租户隔离)
type BackendAiProvider struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
UserID uint64 `orm:"column(user_id);default(0)" json:"user_id"`
ProviderType string `orm:"column(provider_type);size(20)" json:"provider_type"` // openai / anthropic
Name string `orm:"column(name);size(100)" json:"name"`
ApiBase string `orm:"column(api_base);size(255)" json:"api_base"`
ApiKey string `orm:"column(api_key);size(255)" json:"api_key"`
Models string `orm:"column(models);type(text)" json:"-"` // JSON数组存储,如 ["gpt-4o","gpt-3.5-turbo"]
ModelsList []string `orm:"-" json:"models"` // 返回给前端的模型列表
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"` // 0-否 1-是(租户默认模型,用于智能添加等企业级功能)
Status int8 `orm:"column(status);default(1)" json:"status"` // 0-禁用 1-启用
Remark string `orm:"column(remark);size(255)" json:"remark"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *BackendAiProvider) TableName() string {
return "yz_backend_ai_provider"
}
+4
View File
@@ -77,6 +77,10 @@ func Init(_ string) {
new(ErpAccountSet),
new(ErpNormalSetting),
new(ErpCompanyContact),
new(BackendAiProvider),
new(BackendAiChatSession),
new(BackendAiChatMessage),
new(BackendAiChatPreset),
new(CmsArticleCategory),
new(CmsArticle),
+26
View File
@@ -338,4 +338,30 @@ func registerOrganizationRoutes(module string) {
beego.Router("/backend/erp/company-contact/list", &controllers.BackendErpCompanyContactController{}, "get:List")
beego.Router("/backend/erp/company-contact", &controllers.BackendErpCompanyContactController{}, "post:Create")
beego.Router("/backend/erp/company-contact/:id", &controllers.BackendErpCompanyContactController{}, "put:Update;delete:Delete")
// AI接入配置
beego.Router("/backend/ai/provider/list", &controllers.BackendAiProviderController{}, "get:List")
beego.Router("/backend/ai/provider", &controllers.BackendAiProviderController{}, "post:Create")
beego.Router("/backend/ai/provider/test", &controllers.BackendAiProviderController{}, "post:Test")
beego.Router("/backend/ai/provider/:id", &controllers.BackendAiProviderController{}, "put:Update;delete:Delete")
// AI聊天会话
beego.Router("/backend/ai/chat/session/list", &controllers.BackendAiChatSessionController{}, "get:List")
beego.Router("/backend/ai/chat/session", &controllers.BackendAiChatSessionController{}, "post:Create")
beego.Router("/backend/ai/chat/session/:id", &controllers.BackendAiChatSessionController{}, "put:Update;delete:Delete")
// AI聊天消息
beego.Router("/backend/ai/chat/message/list", &controllers.BackendAiChatController{}, "get:MessageList")
beego.Router("/backend/ai/chat/message/:id", &controllers.BackendAiChatController{}, "delete:DeleteMessage")
beego.Router("/backend/ai/chat/send", &controllers.BackendAiChatController{}, "post:Send")
beego.Router("/backend/ai/chat/send-stream", &controllers.BackendAiChatController{}, "post:SendStream")
// AI智能生成
beego.Router("/backend/ai/smart-generate/company", &controllers.BackendAiSmartGenerateController{}, "post:GenerateCompany")
// AI聊天角色预设
beego.Router("/backend/ai/preset/list", &controllers.BackendAiChatPresetController{}, "get:List")
beego.Router("/backend/ai/preset", &controllers.BackendAiChatPresetController{}, "post:Create")
beego.Router("/backend/ai/preset/:id", &controllers.BackendAiChatPresetController{}, "put:Update;delete:Delete")
beego.Router("/backend/ai/preset/:id/default", &controllers.BackendAiChatPresetController{}, "put:SetDefault")
}