prompts-core
This commit is contained in:
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"prompts-core/model/dto"
|
||||
"prompts-core/model/entity"
|
||||
@@ -12,45 +13,61 @@ import (
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// 获取系统模型提示词
|
||||
func getConfPrompt(ctx context.Context, modelType int) string {
|
||||
return g.Cfg().MustGet(ctx, "modelPrompts.types."+gconv.String(modelType), "").String()
|
||||
// 获取请求模型的提示词
|
||||
func GetModelPrompt(ctx context.Context, Type int) string {
|
||||
return g.Cfg().MustGet(ctx, "modelPrompts.types."+gconv.String(Type), "").String()
|
||||
}
|
||||
|
||||
func buildInferenceRequest(ctx context.Context, req *dto.ComposeMessagesReq, sessionModel *entity.AsynchModel, model *entity.AsynchModel, historyMessages []Message) (map[string]any, error) {
|
||||
// 读取 task 相关的配置
|
||||
// 构建消息数组
|
||||
// 1. 系统提示词(不动)
|
||||
|
||||
fmt.Println("打印sessionModel结果", sessionModel)
|
||||
fmt.Println("打印model结果", model)
|
||||
// 获取构建提示词
|
||||
func GetBuildPrompt(ctx context.Context, Type int) string {
|
||||
return g.Cfg().MustGet(ctx, "buildProject.types."+gconv.String(Type), "").String()
|
||||
}
|
||||
|
||||
// buildInferenceRequest 构建返回请求
|
||||
func buildInferenceRequest(ctx context.Context, req *dto.ComposeMessagesReq, chatModel *entity.AsynchModel, model *entity.AsynchModel, history []map[string]any) (map[string]any, error) {
|
||||
messages := []map[string]any{}
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "system",
|
||||
"content": GetSystemPrompt(ctx, req, model),
|
||||
})
|
||||
|
||||
// 2. 历史对话 - 动态添加(新增部分)
|
||||
for _, msg := range historyMessages {
|
||||
switch req.BuildType {
|
||||
//构建提示词请求
|
||||
case 1:
|
||||
//1. 构建系统提示词
|
||||
messages = append(messages, map[string]any{
|
||||
"role": msg.Role,
|
||||
"content": msg.GetContentString(),
|
||||
"role": "system",
|
||||
"content": promptBuild(ctx, req, model),
|
||||
})
|
||||
}
|
||||
// 3. 当前用户问题(原来的最后一条)
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": buildCombinedInput(req, getConfPrompt(ctx, model.ModelsType)),
|
||||
})
|
||||
|
||||
// 2. 构建历史会话提示词
|
||||
for _, msg := range history {
|
||||
role := gconv.String(msg["role"])
|
||||
content := gconv.String(msg["content"])
|
||||
if role != "user" && role != "assistant" {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, map[string]any{
|
||||
"role": role,
|
||||
"content": content,
|
||||
})
|
||||
}
|
||||
// 3. 当前用户问题(原来的最后一条)
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": buildUserPrompt(ctx, req, GetModelPrompt(ctx, model.ModelType)),
|
||||
})
|
||||
//构建节点请求
|
||||
case 2:
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": NodeBuid(ctx, req),
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("不支持的构建类型")
|
||||
}
|
||||
// 构建请求体
|
||||
return map[string]any{
|
||||
"modelName": sessionModel.ModelName,
|
||||
"modelName": chatModel.ModelName,
|
||||
"bizName": "prompts-core",
|
||||
"callbackUrl": "/prompt/callback",
|
||||
"requestPayload": map[string]any{
|
||||
"model": sessionModel.ModelName,
|
||||
"model": chatModel.ModelName,
|
||||
"messages": messages,
|
||||
"stream": false,
|
||||
},
|
||||
@@ -58,10 +75,11 @@ func buildInferenceRequest(ctx context.Context, req *dto.ComposeMessagesReq, ses
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 输入构建
|
||||
// 构建用户提示词
|
||||
// ============================================
|
||||
func buildCombinedInput(req *dto.ComposeMessagesReq, prompt string) string {
|
||||
func buildUserPrompt(ctx context.Context, req *dto.ComposeMessagesReq, prompt string) string {
|
||||
payload := map[string]any{
|
||||
"model": req.ModelName,
|
||||
//数据库提示信息
|
||||
"promptInfo": prompt,
|
||||
// 系统表单
|
||||
@@ -71,20 +89,20 @@ func buildCombinedInput(req *dto.ComposeMessagesReq, prompt string) string {
|
||||
//文件url
|
||||
"userFiles": req.UserFiles,
|
||||
//解读文件(只支持可读类型 如:xml,json,yaml)
|
||||
"userFilesText": fetchFileTexts(context.Background(), req.UserFiles),
|
||||
"userFilesText": FetchFileTexts(ctx, req.UserFiles),
|
||||
//skill 相关(根据传入的 skillName 获取 zip 内所有 md 文件拼接内容)
|
||||
"skills": SkillMdContent(ctx, req.SkillName),
|
||||
}
|
||||
return mustMarshal(payload)
|
||||
}
|
||||
|
||||
// GetSystemPrompt 定义获取系统提示词的函数
|
||||
// GetSystemPrompt 从配置文件读取提示词 + 格式化变量
|
||||
func GetSystemPrompt(ctx context.Context, req *dto.ComposeMessagesReq, model *entity.AsynchModel) string {
|
||||
// promptBuild 提示词构建
|
||||
func promptBuild(ctx context.Context, req *dto.ComposeMessagesReq, model *entity.AsynchModel) string {
|
||||
// 1. 从配置文件读取提示词模板
|
||||
promptTpl := g.Cfg().MustGet(ctx, "projectPrompts", "").String()
|
||||
promptTpl := GetBuildPrompt(ctx, req.BuildType)
|
||||
if promptTpl == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 2. 构建字段映射说明
|
||||
mappingBytes, _ := json.Marshal(model.RequestMapping)
|
||||
mappingStr := string(mappingBytes)
|
||||
@@ -115,11 +133,13 @@ func GetSystemPrompt(ctx context.Context, req *dto.ComposeMessagesReq, model *en
|
||||
return fmt.Sprintf(promptTpl, mappingStr, fieldDesc.String(), formInfo)
|
||||
}
|
||||
|
||||
// formToJSON 工具函数不变
|
||||
func formToJSON(form map[string]any) string {
|
||||
if form == nil {
|
||||
return "{}"
|
||||
// NodeBuid 节点构建
|
||||
func NodeBuid(ctx context.Context, req *dto.ComposeMessagesReq) string {
|
||||
promptTpl := GetBuildPrompt(ctx, req.BuildType)
|
||||
if promptTpl == "" {
|
||||
return ""
|
||||
}
|
||||
b, _ := json.Marshal(form)
|
||||
return string(b)
|
||||
formStr := formToJSON(req.Form)
|
||||
userFormStr := formToJSON(req.UserForm)
|
||||
return fmt.Sprintf(promptTpl, formStr, userFormStr)
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"prompts-core/model/dto"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
// ============================================
|
||||
|
||||
// modelOutput 推理模型的标准输出格式
|
||||
type modelOutput struct {
|
||||
Messages []dto.Message `json:"messages"`
|
||||
System any `json:"system"`
|
||||
User any `json:"user"`
|
||||
}
|
||||
|
||||
// gatewayResponse 模型网关的标准响应格式
|
||||
type gatewayResponse struct {
|
||||
Choices []choice `json:"choices"`
|
||||
}
|
||||
|
||||
type choice struct {
|
||||
Message message `json:"message"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 核心解析函数
|
||||
// ============================================
|
||||
|
||||
// ParseModelResponse 解析推理模型的文本响应,返回消息列表
|
||||
// 支持三种格式:
|
||||
// 1. 标准 messages 格式: {"messages": [...]}
|
||||
// 2. 简化 system/user 格式: {"system": "...", "user": "..."}
|
||||
// 3. 网关包装格式: {"choices": [{"message": {"content": "..."}}]}
|
||||
func ParseModelResponse(text string) ([]dto.Message, error) {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, errors.New("模型响应为空")
|
||||
}
|
||||
|
||||
// 1. 尝试解包网关响应
|
||||
if content := unwrapGatewayResponse(text); content != "" {
|
||||
text = content
|
||||
}
|
||||
|
||||
// 2. 解析为标准格式
|
||||
output, err := parseAsModelOutput(text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析模型输出失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 优先使用 messages 字段
|
||||
if len(output.Messages) > 0 {
|
||||
messages := normalizeMessageContents(output.Messages)
|
||||
if err := validateMessageList(messages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// 4. 兼容 system/user 格式
|
||||
return buildMessagesFromSystemUser(output)
|
||||
}
|
||||
|
||||
// ParseStoredMessages 从数据库存储的数据中解析消息列表
|
||||
func ParseStoredMessages(data any) []dto.Message {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 统一序列化
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试直接解析
|
||||
var messages []dto.Message
|
||||
if err := json.Unmarshal(jsonBytes, &messages); err == nil {
|
||||
return messages
|
||||
}
|
||||
|
||||
// 尝试解析为 JSON 字符串再解析
|
||||
var jsonStr string
|
||||
if err := json.Unmarshal(jsonBytes, &jsonStr); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &messages); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 内部解析函数
|
||||
// ============================================
|
||||
|
||||
// parseAsModelOutput 将文本解析为 modelOutput 结构
|
||||
func parseAsModelOutput(text string) (*modelOutput, error) {
|
||||
// 清理可能的 Markdown 代码块标记
|
||||
text = cleanMarkdownCodeBlock(text)
|
||||
|
||||
var output modelOutput
|
||||
if err := json.Unmarshal([]byte(text), &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &output, nil
|
||||
}
|
||||
|
||||
// unwrapGatewayResponse 解包网关的标准响应格式
|
||||
func unwrapGatewayResponse(text string) string {
|
||||
// 快速检查是否可能是网关响应
|
||||
if !strings.Contains(text, `"choices"`) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var resp gatewayResponse
|
||||
if err := json.Unmarshal([]byte(text), &resp); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(resp.Choices[0].Message.Content)
|
||||
return content
|
||||
}
|
||||
|
||||
// buildMessagesFromSystemUser 从 system/user 字段构建消息列表
|
||||
func buildMessagesFromSystemUser(output *modelOutput) ([]dto.Message, error) {
|
||||
messages := make([]dto.Message, 0, 2)
|
||||
|
||||
// 添加 user 消息
|
||||
if !isEmptyValue(output.User) {
|
||||
messages = append(messages, dto.Message{
|
||||
Role: "user",
|
||||
Content: normalizeContent(output.User),
|
||||
})
|
||||
}
|
||||
|
||||
// 添加 system 消息
|
||||
if !isEmptyValue(output.System) {
|
||||
messages = append(messages, dto.Message{
|
||||
Role: "system",
|
||||
Content: normalizeContent(output.System),
|
||||
})
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
return nil, errors.New("未解析到有效的 system 或 user 内容")
|
||||
}
|
||||
|
||||
if err := validateMessageList(messages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 内容规范化
|
||||
// ============================================
|
||||
|
||||
// normalizeMessageContents 规范化消息列表中的所有内容
|
||||
func normalizeMessageContents(messages []dto.Message) []dto.Message {
|
||||
for i := range messages {
|
||||
messages[i].Content = normalizeContent(messages[i].Content)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
// normalizeContent 规范化单个消息内容
|
||||
// - 如果是 JSON 字符串,尝试解析为对象/数组
|
||||
// - 否则保持原样
|
||||
func normalizeContent(content any) any {
|
||||
switch v := content.(type) {
|
||||
case string:
|
||||
return tryUnmarshalJSON(v)
|
||||
default:
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// tryUnmarshalJSON 尝试将 JSON 字符串解析为结构化对象
|
||||
func tryUnmarshalJSON(s string) any {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
// 只处理看起来像 JSON 的内容
|
||||
if !looksLikeJSON(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
var result any
|
||||
if err := json.Unmarshal([]byte(s), &result); err != nil || result == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// looksLikeJSON 判断字符串是否可能是 JSON
|
||||
func looksLikeJSON(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[")
|
||||
}
|
||||
|
||||
// cleanMarkdownCodeBlock 清理 Markdown 代码块标记
|
||||
func cleanMarkdownCodeBlock(text string) string {
|
||||
// 去除可能的 ```json 和 ``` 标记
|
||||
text = strings.TrimPrefix(text, "```json")
|
||||
text = strings.TrimPrefix(text, "```JSON")
|
||||
text = strings.TrimPrefix(text, "```")
|
||||
text = strings.TrimSuffix(text, "```")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 验证
|
||||
// ============================================
|
||||
|
||||
// validateMessageList 验证消息列表的合法性
|
||||
func validateMessageList(messages []dto.Message) error {
|
||||
if len(messages) == 0 {
|
||||
return errors.New("消息列表不能为空")
|
||||
}
|
||||
|
||||
hasUser := false
|
||||
for i, msg := range messages {
|
||||
if err := validateMessage(msg); err != nil {
|
||||
return fmt.Errorf("消息[%d]验证失败: %w", i, err)
|
||||
}
|
||||
if msg.Role == "user" {
|
||||
hasUser = true
|
||||
}
|
||||
}
|
||||
|
||||
// 至少需要一条 user 消息
|
||||
if !hasUser {
|
||||
return errors.New("消息列表必须包含至少一条 user 角色消息")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMessage 验证单条消息的合法性
|
||||
func validateMessage(msg dto.Message) error {
|
||||
role := strings.TrimSpace(msg.Role)
|
||||
if role == "" {
|
||||
return errors.New("role 不能为空")
|
||||
}
|
||||
|
||||
if !isValidRole(role) {
|
||||
return fmt.Errorf("role 值非法: %s (仅允许 system/user/assistant)", role)
|
||||
}
|
||||
|
||||
// user 角色的 content 不能为空
|
||||
if role == "user" && isEmptyValue(msg.Content) {
|
||||
return errors.New("user 角色的 content 不能为空")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidRole 判断角色是否合法
|
||||
func isValidRole(role string) bool {
|
||||
switch role {
|
||||
case "system", "user", "assistant":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// HasUserMessage 判断消息列表中是否包含非空的 user 消息
|
||||
func HasUserMessage(messages []dto.Message) bool {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == "user" && !isEmptyValue(msg.Content) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasSystemMessage 判断消息列表中是否包含非空的 system 消息
|
||||
func HasSystemMessage(messages []dto.Message) bool {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == "system" && !isEmptyValue(msg.Content) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractUserContent 提取消息列表中第一个 user 角色的内容
|
||||
func ExtractUserContent(messages []dto.Message) any {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == "user" {
|
||||
return msg.Content
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractSystemContent 提取消息列表中第一个 system 角色的内容
|
||||
func ExtractSystemContent(messages []dto.Message) any {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == "system" {
|
||||
return msg.Content
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 测试辅助函数 (可选)
|
||||
// ============================================
|
||||
|
||||
// MockModelResponse 创建模拟的模型响应用于测试
|
||||
func MockModelResponse(systemContent, userContent string) string {
|
||||
output := modelOutput{
|
||||
Messages: []dto.Message{
|
||||
{Role: "system", Content: systemContent},
|
||||
{Role: "user", Content: userContent},
|
||||
},
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(output)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
// MockGatewayResponse 创建模拟的网关响应用于测试
|
||||
func MockGatewayResponse(innerJSON string) string {
|
||||
resp := gatewayResponse{
|
||||
Choices: []choice{
|
||||
{
|
||||
Message: message{
|
||||
Content: innerJSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(resp)
|
||||
return string(bytes)
|
||||
}
|
||||
@@ -24,12 +24,98 @@ import (
|
||||
// ComposeMessages 拼接提示词主流程
|
||||
func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (*dto.ComposeMessagesRes, error) {
|
||||
var (
|
||||
epicycleId int64
|
||||
err error
|
||||
historyMessages []Message // 用来存放历史会话
|
||||
epicycleId int64
|
||||
taskID string
|
||||
history []map[string]any
|
||||
message map[string]any
|
||||
err error
|
||||
taskRecord *entity.ComposeTask
|
||||
)
|
||||
// 1. 如果不需要构建返回记录id
|
||||
if req.IsBuilder == false {
|
||||
// 获取模型信息
|
||||
chatModel, model, err := s.GetModelMessage(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 根据构建类型进行判断处理
|
||||
switch req.BuildType {
|
||||
//提示词构建
|
||||
case 1:
|
||||
maxRetryTimes := g.Cfg().MustGet(ctx, "promptsRetry.maxRetryTimes", 3).Int()
|
||||
//1. 获取历史会话
|
||||
history, err = Session.GetHistoryMessages(ctx, req.SessionId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取历史会话失败: %v,将不使用历史会话", err)
|
||||
history = nil // 出错就用空的,不影响主流程
|
||||
}
|
||||
// 重试循环
|
||||
for attempt := 0; attempt <= maxRetryTimes; attempt++ {
|
||||
if attempt > 0 {
|
||||
g.Log().Warningf(ctx, "[重试]第 %d/%d 次调用推理模型", attempt, maxRetryTimes)
|
||||
}
|
||||
// 2. 调用推理模型
|
||||
taskID, err = s.callInferenceModel(ctx, req, chatModel, model, history)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "调用推理模型失败(第%d次): %v", attempt+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. 保存记录
|
||||
_, err = dao.ComposeTask.Insert(ctx, &entity.ComposeTask{
|
||||
TaskId: taskID,
|
||||
ModelName: req.ModelName,
|
||||
SkillName: req.SkillName,
|
||||
RequestPayload: mustMarshal(req),
|
||||
Status: public.ComposeStatusPending,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存任务记录失败(第%d次): %v", attempt+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 4. 等待结果
|
||||
taskRecord, err = s.waitForResult(ctx, taskID)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "等待结果失败(第%d次): %v", attempt+1, err)
|
||||
continue
|
||||
}
|
||||
// 校验结果
|
||||
message = s.parsePromptBuild(taskRecord, chatModel)
|
||||
if message != nil && isMessageValid(message) {
|
||||
break
|
||||
}
|
||||
g.Log().Warningf(ctx, "[重试] 推理结果不合法(第%d次),准备重新请求", attempt+1)
|
||||
message = nil
|
||||
}
|
||||
if message == nil {
|
||||
return nil, errors.New("推理模型调用失败,请稍后再试")
|
||||
}
|
||||
//5.创建会话记录
|
||||
epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
|
||||
SessionId: req.SessionId,
|
||||
RequestContent: message,
|
||||
})
|
||||
//节点构建
|
||||
case 2:
|
||||
//1. 调用推理模型
|
||||
taskID, err = s.callInferenceModel(ctx, req, chatModel, model, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//2. 保存相关记录
|
||||
_, err = dao.ComposeTask.Insert(ctx, &entity.ComposeTask{
|
||||
TaskId: taskID,
|
||||
ModelName: req.ModelName,
|
||||
SkillName: req.SkillName,
|
||||
RequestPayload: mustMarshal(req),
|
||||
Status: public.ComposeStatusPending,
|
||||
})
|
||||
//5. 等待结果
|
||||
taskRecord, err := s.waitForResult(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message = s.parseNodeBuild(taskRecord)
|
||||
default:
|
||||
epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
|
||||
SessionId: req.SessionId,
|
||||
Remark: req.Cause,
|
||||
@@ -38,80 +124,8 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
|
||||
EpicycleId: epicycleId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 2. 获取当前用户模型信息
|
||||
sessionModel, err := dao.Model.GetByIsChatModel(ctx) //获取会话模型
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sessionModel == nil {
|
||||
return nil, errors.New("当前没有对话模型,请添加")
|
||||
}
|
||||
model, err := dao.Model.GetByModelName(ctx, req.ModelName) //获取模型信息
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("模型 %s 不存在", sessionModel.ModelName)
|
||||
}
|
||||
|
||||
// 3 获取历史会话
|
||||
historyMessages, err = Session.GetSessionHistoryForInference(ctx, req.SessionId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取历史会话失败: %v,将不使用历史会话", err)
|
||||
historyMessages = nil // 出错就用空的,不影响主流程
|
||||
}
|
||||
|
||||
// 4. 调用推理模型
|
||||
taskID, err := s.callInferenceModel(ctx, req, sessionModel, model, historyMessages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 保存相关记录
|
||||
_, err = dao.ComposeTask.Insert(ctx, &entity.ComposeTask{
|
||||
TaskId: taskID,
|
||||
ModelName: req.ModelName,
|
||||
SkillName: req.SkillName,
|
||||
RequestPayload: mustMarshal(req),
|
||||
Status: public.ComposeStatusPending,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 等待结果
|
||||
taskRecord, err := s.waitForResult(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. 处理返回结果
|
||||
messages := s.processResult(taskRecord)
|
||||
|
||||
//8.1 数据库查询当前会话是否存在
|
||||
session, err := dao.ComposeSession.GetBySessionId(ctx, req.SessionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if session == nil {
|
||||
//8.2 不存在则创建新会话记录
|
||||
epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
|
||||
SessionId: req.SessionId,
|
||||
RequestContent: messages,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 9. 更新历史会话
|
||||
_, err = dao.ComposeSession.UpdateById(ctx, epicycleId, map[string]any{
|
||||
entity.ComposeSessionCol.RequestContent: messages,
|
||||
})
|
||||
|
||||
return &dto.ComposeMessagesRes{
|
||||
Messages: messages,
|
||||
Messages: message,
|
||||
EpicycleId: epicycleId,
|
||||
}, nil
|
||||
}
|
||||
@@ -139,7 +153,7 @@ func (s *promptService) Callback(ctx context.Context, req *dto.CallbackReq) erro
|
||||
}
|
||||
// ======================================
|
||||
// 成功:解析模型输出
|
||||
result, err := parseModelOutput(req.Text)
|
||||
result, err := parseOutput(req.Text)
|
||||
if err != nil {
|
||||
_, updateErr := dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{
|
||||
entity.ComposeTaskCol.Status: public.ComposeStatusFailed,
|
||||
@@ -195,13 +209,31 @@ func (s *promptService) GetComposeTask(ctx context.Context, taskID string) (*dto
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 步骤4:调用推理模型
|
||||
// ============================================
|
||||
// GetModelMessage 获取模型信息
|
||||
func (s *promptService) GetModelMessage(ctx context.Context, req *dto.ComposeMessagesReq) (*entity.AsynchModel, *entity.AsynchModel, error) {
|
||||
// 1. 获取当前用户的会话模型
|
||||
chatModel, err := dao.Model.GetByIsChatModel(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if chatModel == nil {
|
||||
return nil, nil, errors.New("当前没有对话模型,请添加")
|
||||
}
|
||||
// 2. 获取要构建的模型信息
|
||||
model, err := dao.Model.GetByModelName(ctx, req.ModelName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if model == nil {
|
||||
return nil, nil, fmt.Errorf("需要构建的模型 %s 不存在", req.ModelName)
|
||||
}
|
||||
return chatModel, model, nil
|
||||
}
|
||||
|
||||
func (s *promptService) callInferenceModel(ctx context.Context, req *dto.ComposeMessagesReq, sessionModel *entity.AsynchModel, model *entity.AsynchModel, historyMessages []Message) (string, error) {
|
||||
// callInferenceModel 调用推理模型
|
||||
func (s *promptService) callInferenceModel(ctx context.Context, req *dto.ComposeMessagesReq, chatModel *entity.AsynchModel, model *entity.AsynchModel, history []map[string]any) (string, error) {
|
||||
// 构建推理模型请求
|
||||
taskReq, err := buildInferenceRequest(ctx, req, sessionModel, model, historyMessages)
|
||||
taskReq, err := buildInferenceRequest(ctx, req, chatModel, model, history)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("构建推理请求失败: %w", err)
|
||||
}
|
||||
@@ -222,16 +254,27 @@ func (s *promptService) callInferenceModel(ctx context.Context, req *dto.Compose
|
||||
// ============================================
|
||||
// 步骤6:等待结果
|
||||
// ============================================
|
||||
|
||||
func (s *promptService) waitForResult(ctx context.Context, taskID string) (*entity.ComposeTask, error) {
|
||||
timeout := time.Duration(getIntConfig(ctx, "task.waitTimeoutSeconds", 30)) * time.Second
|
||||
pollInterval := time.Duration(getIntConfig(ctx, "task.pollIntervalMillis", 500)) * time.Millisecond
|
||||
timeout := time.Duration(g.Cfg().MustGet(ctx, "task.waitTimeoutSeconds", 300).Int()) * time.Second
|
||||
pollInterval := time.Duration(g.Cfg().MustGet(ctx, "task.pollIntervalMillis", 500).Int()) * time.Millisecond
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for {
|
||||
// ===================== 修复点 1:检查上下文是否取消 =====================
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 请求已被取消,直接返回,不继续查库
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 1. 查数据库
|
||||
record, err := dao.ComposeTask.GetByTaskId(ctx, taskID)
|
||||
if err != nil {
|
||||
// ===================== 修复点 2:如果是上下文取消,直接返回 =====================
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if record != nil {
|
||||
@@ -239,19 +282,22 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
|
||||
case public.ComposeStatusSuccess:
|
||||
return record, nil
|
||||
case public.ComposeStatusFailed:
|
||||
return nil, formatTaskError(taskID, record.ErrorMessage)
|
||||
if strings.TrimSpace(record.ErrorMessage) == "" {
|
||||
return nil, fmt.Errorf("任务失败(taskId=%s)", taskID)
|
||||
}
|
||||
return nil, fmt.Errorf("任务失败(taskId=%s): %s", taskID, record.ErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查网关状态
|
||||
state, err := queryGatewayTaskState(ctx, taskID)
|
||||
if err != nil {
|
||||
// ============ 网关不可达不终止,继续轮询 ============
|
||||
// 网关不可达不终止,继续轮询
|
||||
g.Log().Warningf(ctx, "[waitForResult] 查询网关失败 taskId=%s err=%v", taskID, err)
|
||||
} else {
|
||||
switch state {
|
||||
case 2: // 网关成功
|
||||
// ============ 网关已成功,主动更新数据库 ============
|
||||
// 网关已成功,主动更新数据库
|
||||
if record != nil {
|
||||
dao.ComposeTask.UpdateByTaskId(ctx, taskID, map[string]any{
|
||||
entity.ComposeTaskCol.Status: public.ComposeStatusSuccess,
|
||||
@@ -272,239 +318,97 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
|
||||
if time.Now().After(deadline) {
|
||||
return nil, fmt.Errorf("等待任务回调超时(taskId=%s)", taskID)
|
||||
}
|
||||
time.Sleep(pollInterval)
|
||||
|
||||
// ===================== 修复点3:sleep 也要监听 ctx 取消 =====================
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(pollInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 步骤6:处理结果
|
||||
// ============================================
|
||||
func (s *promptService) processResult(taskRecord *entity.ComposeTask) map[string]any {
|
||||
// parsePromptBuild 解析提示词构建结果(BuildType == 1)
|
||||
func (s *promptService) parsePromptBuild(taskRecord *entity.ComposeTask, model *entity.AsynchModel) map[string]any {
|
||||
if taskRecord == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 1. 解析 Messages 获取 content
|
||||
var contentStr string
|
||||
// 1. 解析 Messages
|
||||
var mapped map[string]any
|
||||
switch v := taskRecord.Messages.(type) {
|
||||
case *gvar.Var:
|
||||
if v != nil {
|
||||
var mapped map[string]any
|
||||
json.Unmarshal([]byte(v.String()), &mapped)
|
||||
if c, ok := mapped["content"].(string); ok {
|
||||
contentStr = c
|
||||
}
|
||||
}
|
||||
case string:
|
||||
var mapped map[string]any
|
||||
json.Unmarshal([]byte(v), &mapped)
|
||||
if c, ok := mapped["content"].(string); ok {
|
||||
contentStr = c
|
||||
}
|
||||
case map[string]any:
|
||||
if c, ok := v["content"].(string); ok {
|
||||
contentStr = c
|
||||
mapped = v
|
||||
default:
|
||||
b, _ := json.Marshal(v)
|
||||
json.Unmarshal(b, &mapped)
|
||||
}
|
||||
|
||||
// 2. 解析模型 ResponseMapping 获取 content 字段名
|
||||
contentField := "content" // 默认值
|
||||
if model != nil {
|
||||
var respMapping map[string]string
|
||||
switch v := model.ResponseMapping.(type) {
|
||||
case *gvar.Var:
|
||||
if v != nil {
|
||||
json.Unmarshal([]byte(v.String()), &respMapping)
|
||||
}
|
||||
case string:
|
||||
json.Unmarshal([]byte(v), &respMapping)
|
||||
case map[string]interface{}:
|
||||
respMapping = make(map[string]string)
|
||||
for k, val := range v {
|
||||
if s, ok := val.(string); ok {
|
||||
respMapping[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从映射中找到 content 对应的字段名
|
||||
for k, v := range respMapping {
|
||||
if strings.Contains(v, "content") {
|
||||
contentField = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理并解析
|
||||
contentStr = cleanJSONString(contentStr)
|
||||
// 3. 提取 content 的值
|
||||
contentStr, ok := mapped[contentField].(string)
|
||||
if !ok || contentStr == "" {
|
||||
return mapped
|
||||
}
|
||||
|
||||
// 4. 解析 content 内的 JSON
|
||||
var innerData map[string]any
|
||||
json.Unmarshal([]byte(contentStr), &innerData)
|
||||
|
||||
return innerData
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 消息处理管道
|
||||
// ============================================
|
||||
|
||||
// parseStoredMessages 从数据库存储的数据中解析消息列表
|
||||
// 处理多层 JSON 嵌套的情况
|
||||
func parseStoredMessages(data any) []dto.Message {
|
||||
if data == nil {
|
||||
// parseNodeBuild 解析节点构建结果(BuildType == 2)
|
||||
func (s *promptService) parseNodeBuild(taskRecord *entity.ComposeTask) map[string]any {
|
||||
if taskRecord == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 统一序列化为 JSON
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 第一层解析:尝试直接解析为消息数组
|
||||
var messages []dto.Message
|
||||
if err := json.Unmarshal(jsonBytes, &messages); err == nil {
|
||||
// 成功解析,但需要处理 content 可能是 JSON 字符串的情况
|
||||
return deepNormalizeMessages(messages)
|
||||
}
|
||||
|
||||
// 第二层解析:可能是 JSON 字符串包裹的数组
|
||||
var rawStr string
|
||||
if err := json.Unmarshal(jsonBytes, &rawStr); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试解析字符串为消息数组
|
||||
if err := json.Unmarshal([]byte(rawStr), &messages); err == nil {
|
||||
return deepNormalizeMessages(messages)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deepNormalizeMessages 深度规范化消息,处理 content 为 JSON 字符串的情况
|
||||
func deepNormalizeMessages(messages []dto.Message) []dto.Message {
|
||||
for i, msg := range messages {
|
||||
messages[i].Content = deepNormalizeContent(msg.Content)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
// deepNormalizeContent 递归处理 content,支持多层 JSON 嵌套
|
||||
func deepNormalizeContent(content any) any {
|
||||
switch v := content.(type) {
|
||||
var result map[string]any
|
||||
switch v := taskRecord.Messages.(type) {
|
||||
case *gvar.Var:
|
||||
if v != nil {
|
||||
json.Unmarshal([]byte(v.String()), &result)
|
||||
}
|
||||
case string:
|
||||
// 尝试解析 JSON 字符串
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return v
|
||||
}
|
||||
|
||||
// 如果看起来像 JSON,尝试解析
|
||||
if looksLikeJSON(v) {
|
||||
var parsed any
|
||||
if err := json.Unmarshal([]byte(v), &parsed); err == nil {
|
||||
// 递归处理解析后的内容
|
||||
return deepNormalizeContent(parsed)
|
||||
}
|
||||
}
|
||||
return v
|
||||
|
||||
case []any:
|
||||
// 递归处理数组中的每个元素
|
||||
result := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
result[i] = deepNormalizeContent(item)
|
||||
}
|
||||
return result
|
||||
|
||||
json.Unmarshal([]byte(v), &result)
|
||||
case map[string]any:
|
||||
// 递归处理 map 中的每个值
|
||||
result := make(map[string]any, len(v))
|
||||
for k, val := range v {
|
||||
result[k] = deepNormalizeContent(val)
|
||||
}
|
||||
return result
|
||||
|
||||
result = v
|
||||
default:
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeToTwoPart(messages []dto.Message, req *dto.ComposeMessagesReq) []dto.Message {
|
||||
var result []dto.Message
|
||||
|
||||
// 1. 提取 system
|
||||
sysContent := extractByRole(messages, "system")
|
||||
if sysContent == nil {
|
||||
sysContent = renderFormText(req.Form, false)
|
||||
}
|
||||
result = append(result, dto.Message{Role: "system", Content: sysContent})
|
||||
|
||||
// 2. 提取 form
|
||||
formContent := extractByRole(messages, "form")
|
||||
if formContent != nil {
|
||||
result = append(result, dto.Message{Role: "form", Content: formContent})
|
||||
} else if req != nil {
|
||||
result = append(result, dto.Message{Role: "form", Content: renderFormJSON(req.Form)})
|
||||
}
|
||||
|
||||
// 3. 提取 skill
|
||||
skillContent := extractByRole(messages, "skill")
|
||||
if skillContent != nil {
|
||||
result = append(result, dto.Message{Role: "skill", Content: skillContent})
|
||||
} else if req != nil && req.SkillName != "" {
|
||||
result = append(result, dto.Message{Role: "skill", Content: req.SkillName})
|
||||
}
|
||||
|
||||
// 4. 提取 history(如果模型返回了压缩后的历史)
|
||||
historyContent := extractByRole(messages, "history")
|
||||
if historyContent != nil {
|
||||
result = append(result, dto.Message{Role: "history", Content: historyContent})
|
||||
}
|
||||
|
||||
// 5. 提取 user
|
||||
usrContent := extractByRole(messages, "user")
|
||||
if usrContent == nil {
|
||||
usrContent = renderUserText(req.UserForm, req.Form)
|
||||
}
|
||||
result = append(result, dto.Message{Role: "user", Content: usrContent})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 辅助函数:按 role 提取第一个非空 content
|
||||
// ============================================
|
||||
|
||||
func extractByRole(messages []dto.Message, role string) any {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == role && !isEmptyValue(msg.Content) {
|
||||
return msg.Content
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 辅助函数:将 form 渲染为 JSON 对象
|
||||
// ============================================
|
||||
|
||||
func renderFormJSON(form map[string]any) map[string]any {
|
||||
if form == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]any)
|
||||
for k, v := range form {
|
||||
result[k] = v
|
||||
b, _ := json.Marshal(v)
|
||||
json.Unmarshal(b, &result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func enrichSystemMessages(messages []dto.Message, req *dto.ComposeMessagesReq) []dto.Message {
|
||||
if len(messages) == 0 {
|
||||
return messages
|
||||
}
|
||||
|
||||
// 获取系统字段的值映射
|
||||
systemValues := extractSystemValues(req)
|
||||
|
||||
for i, msg := range messages {
|
||||
if msg.Role != "system" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 为 schema 数组补充 value
|
||||
switch content := msg.Content.(type) {
|
||||
case []any:
|
||||
messages[i].Content = enrichSchemaWithValues(content, systemValues)
|
||||
case []map[string]any:
|
||||
arr := make([]any, len(content))
|
||||
for j, item := range content {
|
||||
arr[j] = item
|
||||
}
|
||||
messages[i].Content = enrichSchemaWithValues(arr, systemValues)
|
||||
case map[string]any:
|
||||
// 合并但不覆盖已有值
|
||||
for k, v := range systemValues {
|
||||
if _, exists := content[k]; !exists {
|
||||
content[k] = v
|
||||
}
|
||||
}
|
||||
messages[i].Content = content
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
@@ -1,21 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 文件处理
|
||||
// 文件处理(配置直接内联 + zip 支持)
|
||||
// ============================================
|
||||
|
||||
func fetchFileTexts(ctx context.Context, urls []string) map[string]string {
|
||||
// 允许的文本类 MIME 类型前缀
|
||||
var allowedMIMEPrefixes = []string{
|
||||
"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/x-yaml",
|
||||
"application/yaml",
|
||||
"application/toml",
|
||||
"application/x-httpd-php",
|
||||
"application/x-sh",
|
||||
"application/x-python",
|
||||
"application/x-perl",
|
||||
"application/x-ruby",
|
||||
}
|
||||
|
||||
// 禁止的文件扩展名
|
||||
var bannedExtensions = map[string]bool{
|
||||
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".bmp": true,
|
||||
".webp": true, ".svg": true, ".ico": true, ".tiff": true, ".tif": true,
|
||||
".mp3": true, ".wav": true, ".ogg": true, ".flac": true, ".aac": true,
|
||||
".wma": true, ".m4a": true,
|
||||
".mp4": true, ".avi": true, ".mkv": true, ".mov": true, ".wmv": true,
|
||||
".flv": true, ".webm": true,
|
||||
".tar": true, ".gz": true, ".rar": true, ".7z": true,
|
||||
".exe": true, ".dll": true, ".so": true, ".bin": true, ".dat": true,
|
||||
".class": true, ".pyc": true,
|
||||
".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
|
||||
".ppt": true, ".pptx": true,
|
||||
}
|
||||
|
||||
var symbolCleaner = regexp.MustCompile(`[\x00-\x08\x0B\x0C\x0E-\x1F]`)
|
||||
|
||||
// FetchFileTexts 从 URL 列表获取文件内容(支持 zip 内文件)
|
||||
func FetchFileTexts(ctx context.Context, urls []string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
if len(urls) == 0 {
|
||||
@@ -23,7 +61,7 @@ func fetchFileTexts(ctx context.Context, urls []string) map[string]string {
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 8 * time.Second,
|
||||
Timeout: time.Duration(g.Cfg().MustGet(ctx, "userFiles.httpTimeoutSec", 8).Int()) * time.Second,
|
||||
}
|
||||
|
||||
for _, rawURL := range urls {
|
||||
@@ -32,13 +70,20 @@ func fetchFileTexts(ctx context.Context, urls []string) map[string]string {
|
||||
continue
|
||||
}
|
||||
|
||||
if isBannedExtension(url) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isZipExtension(url) {
|
||||
zipTexts := fetchZipFileTexts(ctx, client, url)
|
||||
for k, v := range zipTexts {
|
||||
result[k] = v
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
text, err := fetchFileContent(ctx, client, url)
|
||||
if err != nil {
|
||||
glog.Warningf(ctx,
|
||||
"[FetchFile] failed url=%s err=%v",
|
||||
url,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -46,24 +91,131 @@ func fetchFileTexts(ctx context.Context, urls []string) map[string]string {
|
||||
continue
|
||||
}
|
||||
|
||||
text = cleanSymbols(text)
|
||||
result[url] = text
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchFileContent(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
url string,
|
||||
) (string, error) {
|
||||
func isZipExtension(url string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(url))
|
||||
if idx := strings.Index(ext, "?"); idx != -1 {
|
||||
ext = ext[:idx]
|
||||
}
|
||||
return ext == ".zip"
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
url,
|
||||
nil,
|
||||
func fetchZipFileTexts(ctx context.Context, client *http.Client, url string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
zipBytes, err := downloadFile(client, url,
|
||||
int64(g.Cfg().MustGet(ctx, "userFiles.zipMaxSizeMB", 10).Int())*1024*1024,
|
||||
)
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
entryMaxSize := int64(g.Cfg().MustGet(ctx, "userFiles.zipEntryMaxSizeKB", 500).Int()) * 1024
|
||||
|
||||
for _, file := range reader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := file.Name
|
||||
|
||||
if isBannedExtension(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isZipExtension(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(rc, entryMaxSize))
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
contentType := http.DetectContentType(content)
|
||||
if !isReadableContentType(contentType) {
|
||||
continue
|
||||
}
|
||||
|
||||
text := cleanSymbols(string(content))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := url + "::" + fileName
|
||||
result[key] = text
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func downloadFile(client *http.Client, url string, maxSize int64) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxSize))
|
||||
}
|
||||
|
||||
func isBannedExtension(url string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(url))
|
||||
if idx := strings.Index(ext, "?"); idx != -1 {
|
||||
ext = ext[:idx]
|
||||
}
|
||||
return bannedExtensions[ext]
|
||||
}
|
||||
|
||||
func isReadableContentType(contentType string) bool {
|
||||
if contentType == "" {
|
||||
return false
|
||||
}
|
||||
ct := strings.ToLower(contentType)
|
||||
for _, prefix := range allowedMIMEPrefixes {
|
||||
if strings.HasPrefix(ct, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cleanSymbols(text string) string {
|
||||
text = symbolCleaner.ReplaceAllString(text, "")
|
||||
text = strings.ReplaceAll(text, "\r\n", "\n")
|
||||
text = strings.ReplaceAll(text, "\r", "\n")
|
||||
text = regexp.MustCompile(`\n{3,}`).ReplaceAllString(text, "\n\n")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func fetchFileContent(ctx context.Context, client *http.Client, url string) (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -74,21 +226,19 @@ func fetchFileContent(
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// HTTP状态检查
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Content-Type检查
|
||||
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
|
||||
|
||||
if !isTextContentType(contentType) {
|
||||
return "", fmt.Errorf("unsupported content-type: %s", contentType)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !isReadableContentType(contentType) {
|
||||
return "", fmt.Errorf("unreadable content-type: %s", contentType)
|
||||
}
|
||||
|
||||
// 最大读取20KB
|
||||
body, err := io.ReadAll(
|
||||
io.LimitReader(resp.Body, 20*1024),
|
||||
io.LimitReader(resp.Body,
|
||||
int64(g.Cfg().MustGet(ctx, "userFiles.textFileMaxSizeKB", 500).Int())*1024,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -97,35 +247,94 @@ func fetchFileContent(
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// 判断是否为文本类型
|
||||
func isTextContentType(contentType string) bool {
|
||||
|
||||
// text/*
|
||||
if strings.HasPrefix(contentType, "text/") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 常见文本类型
|
||||
allowTypes := []string{
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/x-yaml",
|
||||
"application/yaml",
|
||||
"application/toml",
|
||||
}
|
||||
|
||||
for _, t := range allowTypes {
|
||||
if strings.Contains(contentType, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func sanitizeURL(raw string) string {
|
||||
s := strings.TrimSpace(raw)
|
||||
s = strings.Trim(s, "`\"")
|
||||
return s
|
||||
}
|
||||
|
||||
// SkillMdContent 根据 skillName 获取 zip 内所有 md 文件拼接内容
|
||||
func SkillMdContent(ctx context.Context, skillName string) string {
|
||||
// 1. 请求接口获取 SkillUserVO
|
||||
skillResp, err := GetSkillUser(ctx, skillName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fullUrl := skillResp.ImgAddressPrefix + skillResp.FileUrl
|
||||
// 2. 下载 zip 文件
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(g.Cfg().MustGet(ctx, "skillFiles.httpTimeoutSec", 30).Int()) * time.Second,
|
||||
}
|
||||
|
||||
zipBytes, err := downloadFile(client, fullUrl,
|
||||
int64(g.Cfg().MustGet(ctx, "skillFiles.zipMaxSizeMB", 10).Int())*1024*1024,
|
||||
)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 3. 解压 zip 并提取所有 md 文件内容
|
||||
mdContents, err := extractMdFiles(ctx, zipBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(mdContents) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 4. 拼接所有 md 内容
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("# Skill: %s\n\n", skillResp.Name))
|
||||
if skillResp.Description != "" {
|
||||
builder.WriteString(fmt.Sprintf("> %s\n\n", skillResp.Description))
|
||||
}
|
||||
|
||||
for fileName, content := range mdContents {
|
||||
builder.WriteString(fmt.Sprintf("## %s\n\n", fileName))
|
||||
builder.WriteString(content)
|
||||
builder.WriteString("\n\n---\n\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
// extractMdFiles 解压 zip 并提取所有 .md 文件内容
|
||||
func extractMdFiles(ctx context.Context, zipBytes []byte) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entryMaxSize := int64(g.Cfg().MustGet(ctx, "skillFiles.mdMaxSizeKB", 500).Int()) * 1024
|
||||
|
||||
for _, file := range reader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(strings.ToLower(file.Name), ".md") {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(rc, entryMaxSize))
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(content) > 0 {
|
||||
result[file.Name] = strings.TrimSpace(string(content))
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 表单处理
|
||||
// ============================================
|
||||
|
||||
// FormProcessor 表单处理器
|
||||
type FormProcessor struct {
|
||||
SystemForm map[string]any
|
||||
UserForm map[string]any
|
||||
}
|
||||
|
||||
// NewFormProcessor 创建表单处理器
|
||||
func NewFormProcessor(systemForm, userForm map[string]any) *FormProcessor {
|
||||
return &FormProcessor{
|
||||
SystemForm: systemForm,
|
||||
UserForm: userForm,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge 合并表单,用户表单覆盖系统表单
|
||||
func (p *FormProcessor) Merge() map[string]any {
|
||||
if len(p.SystemForm) == 0 {
|
||||
return p.SystemForm
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
for k, v := range p.SystemForm {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
if len(p.UserForm) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// 构建用户表单索引
|
||||
userIndex := buildFieldIndex(p.UserForm)
|
||||
|
||||
// 覆盖匹配的字段
|
||||
for key, value := range result {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(item, key)
|
||||
|
||||
if userItem, exists := findInIndex(userIndex, field, getLabel(item)); exists {
|
||||
if userValue := getValue(userItem); !isNilOrEmpty(userValue) {
|
||||
result[key] = cloneWithValue(item, userValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// RemoveDuplicates 移除被用户表单覆盖的字段
|
||||
func (p *FormProcessor) RemoveDuplicates() map[string]any {
|
||||
if len(p.SystemForm) == 0 || len(p.UserForm) == 0 {
|
||||
return p.SystemForm
|
||||
}
|
||||
|
||||
userFields := buildFieldSet(p.UserForm)
|
||||
result := make(map[string]any)
|
||||
|
||||
for key, value := range p.SystemForm {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
result[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(item, key)
|
||||
label := getLabel(item)
|
||||
|
||||
// 跳过重复字段
|
||||
if userFields.contains(field) || userFields.containsLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// RemoveSemanticDuplicates 语义去重
|
||||
func (p *FormProcessor) RemoveSemanticDuplicates() map[string]any {
|
||||
if len(p.SystemForm) == 0 || len(p.UserForm) == 0 {
|
||||
return p.SystemForm
|
||||
}
|
||||
|
||||
userText := renderUserTextOnly(p.UserForm)
|
||||
if userText == "" {
|
||||
return p.SystemForm
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
for key, value := range p.SystemForm {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
result[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
if isDuplicate(userText, getField(item, key), getLabel(item), getValue(item)) {
|
||||
continue
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// RenderSystemText 渲染系统提示词文本
|
||||
func (p *FormProcessor) RenderSystemText() string {
|
||||
return renderFormText(p.SystemForm, false)
|
||||
}
|
||||
|
||||
// RenderUserText 渲染用户提示词文本
|
||||
func (p *FormProcessor) RenderUserText() string {
|
||||
return renderUserText(p.UserForm, p.SystemForm)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 表单处理辅助方法
|
||||
// ============================================
|
||||
|
||||
type fieldSet struct {
|
||||
fields map[string]bool
|
||||
labels map[string]bool
|
||||
}
|
||||
|
||||
func buildFieldSet(form map[string]any) *fieldSet {
|
||||
fs := &fieldSet{
|
||||
fields: make(map[string]bool),
|
||||
labels: make(map[string]bool),
|
||||
}
|
||||
|
||||
for key, value := range form {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.ToLower(getField(item, key))
|
||||
if field != "" {
|
||||
fs.fields[field] = true
|
||||
}
|
||||
|
||||
if label := strings.ToLower(getLabel(item)); label != "" {
|
||||
fs.labels[label] = true
|
||||
}
|
||||
}
|
||||
|
||||
return fs
|
||||
}
|
||||
|
||||
func (fs *fieldSet) contains(field string) bool {
|
||||
return fs.fields[strings.ToLower(field)]
|
||||
}
|
||||
|
||||
func (fs *fieldSet) containsLabel(label string) bool {
|
||||
return label != "" && fs.labels[strings.ToLower(label)]
|
||||
}
|
||||
|
||||
func buildFieldIndex(form map[string]any) map[string]map[string]any {
|
||||
index := make(map[string]map[string]any)
|
||||
|
||||
for key, value := range form {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.ToLower(getField(item, key))
|
||||
if field != "" {
|
||||
index[field] = item
|
||||
}
|
||||
|
||||
if label := strings.ToLower(getLabel(item)); label != "" {
|
||||
if _, exists := index[label]; !exists {
|
||||
index[label] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
func findInIndex(index map[string]map[string]any, field, label string) (map[string]any, bool) {
|
||||
key := strings.ToLower(field)
|
||||
if item, ok := index[key]; ok {
|
||||
return item, true
|
||||
}
|
||||
|
||||
if label != "" {
|
||||
key = strings.ToLower(label)
|
||||
if item, ok := index[key]; ok {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 表单渲染
|
||||
// ============================================
|
||||
|
||||
func renderFormText(form map[string]any, isUserForm bool) string {
|
||||
if len(form) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 用户表单只有一个文本字段时,直接返回值
|
||||
if isUserForm && len(form) == 1 {
|
||||
for _, value := range form {
|
||||
if item, ok := value.(map[string]any); ok {
|
||||
return strings.TrimSpace(asString(getValue(item)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接渲染
|
||||
items := extractFormItems(form)
|
||||
if isUserForm {
|
||||
return renderUserFormItems(items)
|
||||
}
|
||||
return renderSystemFormItems(items)
|
||||
}
|
||||
|
||||
type formItem struct {
|
||||
Key string
|
||||
Field string
|
||||
Label string
|
||||
Value any
|
||||
}
|
||||
|
||||
func extractFormItems(form map[string]any) []formItem {
|
||||
var items []formItem
|
||||
|
||||
keys := sortedKeys(form)
|
||||
for _, key := range keys {
|
||||
item, ok := form[key].(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(item, key)
|
||||
value := getValue(item)
|
||||
|
||||
// 跳过敏感字段和空值
|
||||
if isSensitiveField(field) || isNilOrEmpty(value) {
|
||||
continue
|
||||
}
|
||||
|
||||
items = append(items, formItem{
|
||||
Key: key,
|
||||
Field: field,
|
||||
Label: getLabel(item),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func renderUserFormItems(items []formItem) string {
|
||||
// 只有一个文本类型字段时,直接返回值
|
||||
if len(items) == 1 && isTextType(items[0].Field, items[0].Label) {
|
||||
return formatValue(items[0].Value)
|
||||
}
|
||||
|
||||
// 拼接
|
||||
var parts []string
|
||||
for _, item := range items {
|
||||
if isTextType(item.Field, item.Label) {
|
||||
parts = append(parts, formatValue(item.Value))
|
||||
} else {
|
||||
label := item.Label
|
||||
if label == "" {
|
||||
label = item.Field
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", label, formatValue(item.Value)))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func renderSystemFormItems(items []formItem) string {
|
||||
var parts []string
|
||||
for _, item := range items {
|
||||
label := item.Label
|
||||
if label == "" {
|
||||
label = item.Field
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", label, formatValue(item.Value)))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func renderUserText(userForm, systemForm map[string]any) string {
|
||||
if text := renderFormText(userForm, true); text != "" {
|
||||
return text
|
||||
}
|
||||
// 用户表单为空时,使用系统表单生成
|
||||
if text := renderFormText(systemForm, false); text != "" {
|
||||
return "参考系统字段生成用户提示词:" + text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func renderUserTextOnly(userForm map[string]any) string {
|
||||
return renderFormText(userForm, true)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// asyncCtx 固化异步执行所需的 token/user,避免请求结束后丢失(仅在“同请求内起 goroutine”有用)。
|
||||
@@ -52,18 +51,3 @@ func forwardHeaders(ctx context.Context) map[string]string {
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
// GetTenantId 获取租户ID
|
||||
func GetTenantId(ctx context.Context) int64 {
|
||||
var currentTenantID int64
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
currentTenantID = gconv.Int64(r.Header.Get("X-Tenant-Id"))
|
||||
}
|
||||
if currentTenantID == 0 {
|
||||
userInfo, err := utils.GetUserInfo(ctx)
|
||||
if err == nil && userInfo != nil {
|
||||
currentTenantID = int64(userInfo.TenantId)
|
||||
}
|
||||
}
|
||||
return currentTenantID
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
commonHttp "gitea.com/red-future/common/http"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// model-gateway 网关交互
|
||||
// ============================================
|
||||
|
||||
// CreateTaskReq 创建任务请求
|
||||
type CreateTaskReq struct {
|
||||
TaskId string `json:"task_id"`
|
||||
@@ -52,3 +49,27 @@ func queryGatewayTaskState(ctx context.Context, taskID string) (int, error) {
|
||||
}
|
||||
return req.State, nil
|
||||
}
|
||||
|
||||
// SkillUserVO 技能用户视图对象
|
||||
type SkillUserVO struct {
|
||||
Id int64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
FileName string `json:"fileName"`
|
||||
FileUrl string `json:"fileUrl"` // html 后缀
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
ImgAddressPrefix string `json:"imgAddressPrefix"` // htmml 前缀
|
||||
}
|
||||
|
||||
// GetSkillUser 根据 name 获取技能用户信息
|
||||
func GetSkillUser(ctx context.Context, name string) (*SkillUserVO, error) {
|
||||
fullURL := fmt.Sprintf("ai-agent/skill/user/getUserOrTemplate?name=%s", name)
|
||||
headers := forwardHeaders(ctx)
|
||||
var resp SkillUserVO
|
||||
var req struct{}
|
||||
if err := commonHttp.Get(ctx, fullURL, headers, &resp, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 消息解析
|
||||
// ============================================
|
||||
func parseModelOutput(text string) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析模型输出失败: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// cleanJSONString 清理字符串中可能导致JSON解析失败的字符
|
||||
func cleanJSONString(s string) string {
|
||||
s = strings.ReplaceAll(s, "\u2018", "'") // 左单引号 ‘
|
||||
s = strings.ReplaceAll(s, "\u2019", "'") // 右单引号 ’
|
||||
s = strings.ReplaceAll(s, "\u201c", "\"") // 左双引号 “
|
||||
s = strings.ReplaceAll(s, "\u201d", "\"") // 右双引号 ”
|
||||
return s
|
||||
}
|
||||
|
||||
func truncateStr(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen]
|
||||
}
|
||||
|
||||
// sessionParseModelOutput 解析会话模型输出
|
||||
func sessionParseModelOutput(text string) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析模型输出失败: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -4,113 +4,43 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// Message 消息结构(content 支持 string 或 []string)
|
||||
type Message struct {
|
||||
Role string `json:"role"` // user / assistant / system
|
||||
Content any `json:"content"` // 内容:string 或 []string
|
||||
Type string `json:"type,omitempty"` // text / file(可选扩展)
|
||||
}
|
||||
|
||||
// GetContentString 获取 Content 的字符串形式
|
||||
func (m Message) GetContentString() string {
|
||||
switch v := m.Content.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []interface{}:
|
||||
var parts []string
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
parts = append(parts, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
default:
|
||||
b, _ := json.Marshal(m.Content)
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// SessionRoundData Redis存储的单轮会话数据
|
||||
type SessionRoundData struct {
|
||||
SessionId string `json:"sessionId"` // 会话ID
|
||||
RequestContent []Message `json:"requestContent"` // 用户请求会话
|
||||
ResponseContent []Message `json:"responseContent"` // AI回调会话
|
||||
Timestamp int64 `json:"timestamp"` // 存入时间戳
|
||||
}
|
||||
|
||||
// GetSessionHistory 获取多轮会话历史(供推理时使用)
|
||||
func (s *sessionService) GetSessionHistory(ctx context.Context, sessionId string) ([]SessionRoundData, error) {
|
||||
return s.getFromRedis(ctx, sessionId)
|
||||
}
|
||||
|
||||
// BuildMessages 根据Redis历史构建完整的Messages数组
|
||||
func (s *sessionService) BuildMessages(ctx context.Context, sessionId string, currentMessages []Message) ([]Message, error) {
|
||||
// 获取历史会话
|
||||
history, err := s.getFromRedis(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取历史会话失败: %w", err)
|
||||
}
|
||||
|
||||
var allMessages []Message
|
||||
|
||||
// 按时间顺序拼接历史消息
|
||||
for _, round := range history {
|
||||
allMessages = append(allMessages, round.RequestContent...)
|
||||
allMessages = append(allMessages, round.ResponseContent...)
|
||||
}
|
||||
|
||||
// 添加当前轮次的请求消息
|
||||
allMessages = append(allMessages, currentMessages...)
|
||||
|
||||
return allMessages, nil
|
||||
}
|
||||
|
||||
// ==================== Redis 操作 ====================
|
||||
|
||||
// saveToRedis 保存会话数据到Redis
|
||||
// sessionId: 会话ID作为key
|
||||
// 最大10轮,超出替换最早的,过期时间30分钟
|
||||
func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requestMessages []Message, responseMessages []Message) error {
|
||||
func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requestMessages []map[string]any, responseMessages []map[string]any) error {
|
||||
key := fmt.Sprintf("chat:session:%s", sessionId)
|
||||
|
||||
// 从配置读取,提供默认值
|
||||
maxRounds := g.Cfg().MustGet(ctx, "session.maxRounds", 10).Int()
|
||||
expireSeconds := g.Cfg().MustGet(ctx, "session.expireTime", 1800).Int64()
|
||||
expireTime := time.Duration(expireSeconds) * time.Second
|
||||
|
||||
// 构造存储数据
|
||||
data := SessionRoundData{
|
||||
SessionId: sessionId,
|
||||
RequestContent: requestMessages,
|
||||
ResponseContent: responseMessages,
|
||||
Timestamp: time.Now().Unix(),
|
||||
data := map[string]any{
|
||||
"sessionId": sessionId,
|
||||
"requestContent": requestMessages,
|
||||
"responseContent": responseMessages,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// 序列化
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化会话数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 写入 Redis(LPUSH 添加到最前面,新的在前)
|
||||
_, err = g.Redis().Do(ctx, "LPUSH", key, string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入Redis失败: %w", err)
|
||||
}
|
||||
|
||||
// 裁剪到最新10轮(保留前10条)
|
||||
_, err = g.Redis().Do(ctx, "LTRIM", key, 0, maxRounds-1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("裁剪Redis列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 重置过期时间
|
||||
_, err = g.Redis().Do(ctx, "EXPIRE", key, int64(expireTime.Seconds()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置过期时间失败: %w", err)
|
||||
@@ -120,26 +50,22 @@ func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requ
|
||||
}
|
||||
|
||||
// getFromRedis 从Redis获取会话历史
|
||||
func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]SessionRoundData, error) {
|
||||
func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]map[string]any, error) {
|
||||
key := fmt.Sprintf("chat:session:%s", sessionId)
|
||||
|
||||
// 获取列表中所有数据(最多10条)
|
||||
result, err := g.Redis().Do(ctx, "LRANGE", key, 0, -1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("从Redis获取数据失败: %w", err)
|
||||
}
|
||||
|
||||
if result == nil || result.IsNil() {
|
||||
return []SessionRoundData{}, nil
|
||||
return []map[string]any{}, nil
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
var sessions []SessionRoundData
|
||||
|
||||
// 将结果转换为字符串数组
|
||||
var sessions []map[string]any
|
||||
values := result.Strings()
|
||||
for _, str := range values {
|
||||
var data SessionRoundData
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal([]byte(str), &data); err != nil {
|
||||
g.Log().Warningf(ctx, "[会话] 解析Redis数据失败 err=%v", err)
|
||||
continue
|
||||
@@ -147,7 +73,7 @@ func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]
|
||||
sessions = append(sessions, data)
|
||||
}
|
||||
|
||||
// 反转顺序(Redis存储最新在前,使用时按时间正序)
|
||||
// 反转(Redis 最新在前 → 时间正序)
|
||||
for i, j := 0, len(sessions)-1; i < j; i, j = i+1, j-1 {
|
||||
sessions[i], sessions[j] = sessions[j], sessions[i]
|
||||
}
|
||||
@@ -155,26 +81,33 @@ func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// GetSessionHistoryForInference 获取历史会话,直接返回Message数组(给推理用)
|
||||
func (s *sessionService) GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]Message, error) {
|
||||
// 从Redis获取历史会话数据
|
||||
// GetSessionHistoryForInference 获取历史会话,返回扁平消息数组(给推理用)
|
||||
func (s *sessionService) GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]map[string]any, error) {
|
||||
historyData, err := s.getFromRedis(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取历史会话失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有任何历史数据,返回空
|
||||
if len(historyData) == 0 {
|
||||
return []Message{}, nil
|
||||
return []map[string]any{}, nil
|
||||
}
|
||||
|
||||
// 把SessionRoundData转换成扁平的Message数组
|
||||
var messages []Message
|
||||
var messages []map[string]any
|
||||
for _, round := range historyData {
|
||||
// 先加用户的请求
|
||||
messages = append(messages, round.RequestContent...)
|
||||
// 再加AI的回答
|
||||
messages = append(messages, round.ResponseContent...)
|
||||
if reqMsgs, ok := round["requestContent"].([]interface{}); ok {
|
||||
for _, m := range reqMsgs {
|
||||
if msg, ok := m.(map[string]interface{}); ok {
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if respMsgs, ok := round["responseContent"].([]interface{}); ok {
|
||||
for _, m := range respMsgs {
|
||||
if msg, ok := m.(map[string]interface{}); ok {
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
|
||||
@@ -2,46 +2,50 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"prompts-core/dao"
|
||||
"prompts-core/model/dto"
|
||||
"prompts-core/model/entity"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Session = &sessionService{}
|
||||
|
||||
type sessionService struct{}
|
||||
|
||||
// SessionCallback 会话回调处理
|
||||
func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCallbackReq) (res *beans.ResponseEmpty, err error) {
|
||||
// 1. 解析AI返回的文本
|
||||
result, err := sessionParseModelOutput(req.Text)
|
||||
result, err := parseOutput(req.Text)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "[会话回调] 解析模型输出失败 epicycleId=%d err=%v", req.EpicycleId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 更新数据库(AI回调内容)
|
||||
_, err = dao.ComposeSession.UpdateById(ctx, req.EpicycleId, map[string]any{
|
||||
entity.ComposeSessionCol.ResponseContent: result,
|
||||
// 2. 更新数据库
|
||||
result["role"] = "assistant"
|
||||
_, err = dao.ComposeSession.Update(ctx, &entity.ComposeSession{
|
||||
SQLBaseDO: beans.SQLBaseDO{Id: req.EpicycleId},
|
||||
ResponseContent: result,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "[会话回调] 更新数据库失败 epicycleId=%d err=%v", req.EpicycleId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 获取当前轮次的完整数据
|
||||
// 3. 获取当前轮次完整数据
|
||||
session, err := dao.ComposeSession.GetById(ctx, req.EpicycleId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "[会话回调] 获取会话数据失败 epicycleId=%d err=%v", req.EpicycleId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 写入 Redis(多轮记忆)
|
||||
requestMessages := s.convertToMessages(session.RequestContent)
|
||||
responseMessages := s.convertToMessages(session.ResponseContent)
|
||||
// 4. 转换 json 并存入 Redis
|
||||
requestMessages := convertToMessages(session.RequestContent)
|
||||
responseMessages := convertToMessages(session.ResponseContent)
|
||||
|
||||
if err = s.saveToRedis(ctx, session.SessionId, requestMessages, responseMessages); err != nil {
|
||||
g.Log().Errorf(ctx, "[会话回调] Redis存储失败 sessionId=%s id=%d err=%v",
|
||||
session.SessionId, session.Id, err)
|
||||
@@ -52,3 +56,57 @@ func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCa
|
||||
session.SessionId, session.Id, len(requestMessages), len(responseMessages))
|
||||
return &beans.ResponseEmpty{}, nil
|
||||
}
|
||||
|
||||
// GetHistoryMessages 获取历史信息
|
||||
func (s *sessionService) GetHistoryMessages(ctx context.Context, sessionId string) ([]map[string]any, error) {
|
||||
maxRounds := g.Cfg().MustGet(ctx, "session.maxRounds", 10).Int()
|
||||
|
||||
// 1. 先从 Redis 拿
|
||||
redisHistory, err := s.GetSessionHistoryForInference(ctx, sessionId)
|
||||
if err == nil && len(redisHistory) > 0 {
|
||||
return redisHistory, nil
|
||||
}
|
||||
|
||||
// 2. Redis 没有 → fallback DB
|
||||
sessions, err := dao.ComposeSession.GetListBySessionId(ctx, sessionId, maxRounds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DB获取历史失败: %w", err)
|
||||
}
|
||||
|
||||
var messages []map[string]any
|
||||
|
||||
for _, session := range sessions {
|
||||
// request
|
||||
reqMsgs := convertToMessages(session.RequestContent)
|
||||
for _, m := range reqMsgs {
|
||||
role := gconv.String(m["role"])
|
||||
if role == "user" || role == "assistant" {
|
||||
messages = append(messages, m)
|
||||
}
|
||||
}
|
||||
|
||||
// response
|
||||
respMsgs := convertToMessages(session.ResponseContent)
|
||||
for _, m := range respMsgs {
|
||||
if m["role"] == nil {
|
||||
m["role"] = "assistant"
|
||||
}
|
||||
messages = append(messages, m)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 回写 Redis
|
||||
for _, session := range sessions {
|
||||
reqMsgs := convertToMessages(session.RequestContent)
|
||||
respMsgs := convertToMessages(session.ResponseContent)
|
||||
for i := range respMsgs {
|
||||
if respMsgs[i]["role"] == nil {
|
||||
respMsgs[i]["role"] = "assistant"
|
||||
}
|
||||
}
|
||||
if len(reqMsgs) > 0 || len(respMsgs) > 0 {
|
||||
_ = s.saveToRedis(ctx, session.SessionId, reqMsgs, respMsgs)
|
||||
}
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
338
service/utils.go
338
service/utils.go
@@ -1,138 +1,59 @@
|
||||
// utils 工具函数
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"prompts-core/model/dto"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 工具函数
|
||||
// json 相关处理
|
||||
// ============================================
|
||||
|
||||
func getField(item map[string]any, fallback string) string {
|
||||
if field := asString(item["field"]); field != "" {
|
||||
return field
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getLabel(item map[string]any) string {
|
||||
return asString(item["label"])
|
||||
}
|
||||
|
||||
func getValue(item map[string]any) any {
|
||||
return item["value"]
|
||||
}
|
||||
|
||||
func cloneWithValue(item map[string]any, value any) map[string]any {
|
||||
cloned := make(map[string]any)
|
||||
for k, v := range item {
|
||||
cloned[k] = v
|
||||
}
|
||||
cloned["value"] = value
|
||||
return cloned
|
||||
}
|
||||
|
||||
func isSensitiveField(field string) bool {
|
||||
f := strings.ToLower(field)
|
||||
return f == "apikey" || f == "authorization"
|
||||
}
|
||||
|
||||
func isAPIKeyField(field string) bool {
|
||||
f := strings.ToLower(field)
|
||||
return f == "apikey" || f == "authorization"
|
||||
}
|
||||
|
||||
func isTextType(field, label string) bool {
|
||||
f := strings.ToLower(field)
|
||||
l := strings.ToLower(label)
|
||||
return f == "prompt" || f == "text" ||
|
||||
l == "提示词" || l == "文本内容" || l == "prompt" || l == "text"
|
||||
}
|
||||
|
||||
func isDuplicate(userText, field, label string, value any) bool {
|
||||
lowerText := strings.ToLower(userText)
|
||||
|
||||
if label != "" && strings.Contains(lowerText, strings.ToLower(label)) {
|
||||
return true
|
||||
}
|
||||
if field != "" && strings.Contains(lowerText, strings.ToLower(field)) {
|
||||
return true
|
||||
// parseOutput 解析模型输出为 JSON 格式
|
||||
func parseOutput(text string) (map[string]any, error) {
|
||||
j, err := gjson.LoadJson([]byte(text))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析模型输出失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查值
|
||||
if v := asString(value); v != "" && strings.Contains(lowerText, strings.ToLower(v)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return j.Map(), nil
|
||||
}
|
||||
|
||||
func isEmptyValue(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
func convertToMessages(raw any) []map[string]any {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
return strings.TrimSpace(s) == ""
|
||||
j, err := gjson.LoadJson(gconv.Bytes(raw))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNilOrEmpty(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
// 1. 如果有 messages
|
||||
if j.Contains("messages") {
|
||||
return gconv.Maps(j.Get("messages").Array())
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
return strings.TrimSpace(s) == ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
b, _ := json.Marshal(t)
|
||||
return strings.Trim(string(b), "\"")
|
||||
// 2. 否则当成单条 message
|
||||
return []map[string]any{
|
||||
j.Map(),
|
||||
}
|
||||
}
|
||||
|
||||
func formatValue(v any) string {
|
||||
return strings.TrimSpace(asString(v))
|
||||
// isMessageValid 校验推理结果是否合法
|
||||
func isMessageValid(message map[string]any) bool {
|
||||
if message == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mapToText(m map[string]any) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
func formToJSON(form map[string]any) string {
|
||||
if form == nil {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
keys := sortedKeys(m)
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
if isNilOrEmpty(m[k]) {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", k, formatValue(m[k])))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]any) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
b, _ := json.Marshal(form)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func mustMarshal(v any) string {
|
||||
@@ -142,196 +63,3 @@ func mustMarshal(v any) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func formatTaskError(taskID, errMsg string) error {
|
||||
if strings.TrimSpace(errMsg) == "" {
|
||||
return fmt.Errorf("任务失败(taskId=%s)", taskID)
|
||||
}
|
||||
return fmt.Errorf("任务失败(taskId=%s): %s", taskID, errMsg)
|
||||
}
|
||||
|
||||
func getIntConfig(ctx context.Context, key string, fallback int) int {
|
||||
v := g.Cfg().MustGet(ctx, key)
|
||||
if v.IsEmpty() {
|
||||
return fallback
|
||||
}
|
||||
return v.Int()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Schema 处理
|
||||
// ============================================
|
||||
|
||||
func enrichSchemaWithValues(schema []any, values map[string]any) []any {
|
||||
if len(schema) == 0 || len(values) == 0 {
|
||||
return schema
|
||||
}
|
||||
|
||||
result := make([]any, len(schema))
|
||||
copy(result, schema)
|
||||
|
||||
for i, item := range result {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(m, "")
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 已有 value 则跳过
|
||||
if _, hasValue := m["value"]; hasValue {
|
||||
continue
|
||||
}
|
||||
|
||||
// 补充 value
|
||||
if v, exists := values[field]; exists {
|
||||
m["value"] = v
|
||||
result[i] = m
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// extractContentFromResponse 从模型完整响应中提取 content 字段
|
||||
func extractContentFromResponse(text string) string {
|
||||
// 尝试解析为完整的 choices 响应
|
||||
var response struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(text), &response); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(response.Choices) > 0 && response.Choices[0].Message.Content != "" {
|
||||
return response.Choices[0].Message.Content
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 值提取
|
||||
// ============================================
|
||||
|
||||
func extractSystemValues(req *dto.ComposeMessagesReq) map[string]any {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := make(map[string]any)
|
||||
|
||||
for _, value := range req.Form {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok || len(item) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(item, "")
|
||||
if field == "" || isSensitiveField(field) {
|
||||
continue
|
||||
}
|
||||
|
||||
if v := getValue(item); !isNilOrEmpty(v) {
|
||||
values[field] = v
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func extractModelKey(form map[string]any) string {
|
||||
for _, value := range form {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getField(item, "")
|
||||
if isAPIKeyField(field) {
|
||||
key := strings.TrimSpace(asString(getValue(item)))
|
||||
if key != "" {
|
||||
if strings.Contains(key, ":") {
|
||||
return key
|
||||
}
|
||||
return "Authorization:" + key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
// convertToMessages 将数据库 any 类型转换为 []Message
|
||||
// 支持:JSON字符串、[]byte、[]interface{}、以及 content 为字符串数组的格式
|
||||
func (s *sessionService) convertToMessages(data any) []Message {
|
||||
if data == nil {
|
||||
return []Message{}
|
||||
}
|
||||
|
||||
// 处理 *gvar.Var
|
||||
if v, ok := data.(*gvar.Var); ok {
|
||||
if v == nil || v.IsNil() || v.IsEmpty() {
|
||||
return []Message{}
|
||||
}
|
||||
data = v.Val()
|
||||
}
|
||||
|
||||
var rawList []any
|
||||
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(v), &rawList); err != nil {
|
||||
g.Log().Warningf(context.Background(), "[会话] 解析JSON字符串失败 err=%v data=%.200s", err, v)
|
||||
return []Message{}
|
||||
}
|
||||
case []byte:
|
||||
if err := json.Unmarshal(v, &rawList); err != nil {
|
||||
g.Log().Warningf(context.Background(), "[会话] 解析字节数组失败 err=%v", err)
|
||||
return []Message{}
|
||||
}
|
||||
case []interface{}:
|
||||
rawList = v
|
||||
default:
|
||||
b, _ := json.Marshal(v)
|
||||
if err := json.Unmarshal(b, &rawList); err != nil {
|
||||
g.Log().Warningf(context.Background(), "[会话] 解析未知类型失败 err=%v type=%T", err, v)
|
||||
return []Message{}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换每个元素为 Message
|
||||
var messages []Message
|
||||
for _, item := range rawList {
|
||||
var msg Message
|
||||
switch val := item.(type) {
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(val), &msg); err != nil {
|
||||
g.Log().Warningf(context.Background(), "[会话] 解析消息元素失败 err=%v data=%s", err, val)
|
||||
continue
|
||||
}
|
||||
case map[string]interface{}:
|
||||
b, _ := json.Marshal(val)
|
||||
json.Unmarshal(b, &msg)
|
||||
default:
|
||||
b, _ := json.Marshal(val)
|
||||
json.Unmarshal(b, &msg)
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
if messages == nil {
|
||||
messages = []Message{}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user