refactor(service): 重构服务代码结构并更新配置

This commit is contained in:
2026-05-18 19:19:17 +08:00
parent 5f98e52b34
commit c49144794d
35 changed files with 1281 additions and 1162 deletions

View File

@@ -1,144 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"prompts-core/model/dto"
"prompts-core/model/entity"
"strings"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
// 获取请求模型的提示词
func GetModelPrompt(ctx context.Context, Type int) string {
return g.Cfg().MustGet(ctx, "modelPrompts.types."+gconv.String(Type), "").String()
}
// 获取构建提示词
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{}
switch req.BuildType {
//构建提示词请求
case 1:
//1. 构建系统提示词
messages = append(messages, map[string]any{
"role": "system",
"content": promptBuild(ctx, req, model),
})
// 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": chatModel.ModelName,
"bizName": "prompts-core",
"callbackUrl": "/prompt/callback",
"requestPayload": map[string]any{
"model": chatModel.ModelName,
"messages": messages,
"stream": false,
},
}, nil
}
// ============================================
// 构建用户提示词
// ============================================
func buildUserPrompt(ctx context.Context, req *dto.ComposeMessagesReq, prompt string) string {
payload := map[string]any{
"model": req.ModelName,
//数据库提示信息
"promptInfo": prompt,
// 系统表单
"form": req.Form,
// 用户表单
"userForm": req.UserForm,
//文件url
"userFiles": req.UserFiles,
//解读文件(只支持可读类型 如xmljson,yaml
"userFilesText": FetchFileTexts(ctx, req.UserFiles),
//skill 相关(根据传入的 skillName 获取 zip 内所有 md 文件拼接内容)
"skills": SkillMdContent(ctx, req.SkillName),
}
return mustMarshal(payload)
}
// promptBuild 提示词构建
func promptBuild(ctx context.Context, req *dto.ComposeMessagesReq, model *entity.AsynchModel) string {
// 1. 从配置文件读取提示词模板
promptTpl := GetBuildPrompt(ctx, req.BuildType)
if promptTpl == "" {
return ""
}
// 2. 构建字段映射说明
mappingBytes, _ := json.Marshal(model.RequestMapping)
mappingStr := string(mappingBytes)
var mapping map[string]string
_ = json.Unmarshal(mappingBytes, &mapping)
var fieldDesc strings.Builder
for key, path := range mapping {
fieldDesc.WriteString(fmt.Sprintf("- %s → %s\n", key, path))
}
// 3. 拼接 UserForm 全文(必须完整阅读)
var userFormContent strings.Builder
for k, v := range req.UserForm {
userFormContent.WriteString(fmt.Sprintf("%s=%v", k, v))
}
userFormFullText := strings.TrimSuffix(userFormContent.String(), "")
// 4. 双表单信息
formInfo := fmt.Sprintf(`
【系统表单(系统提示词/参数)】
%s
【用户表单全文(必须完整阅读,全部作为用户提示词来源)】
%s
`, formToJSON(req.Form), userFormFullText)
// 5. 格式化最终提示词(替换配置里的 %s
return fmt.Sprintf(promptTpl, mappingStr, fieldDesc.String(), formInfo)
}
// NodeBuid 节点构建
func NodeBuid(ctx context.Context, req *dto.ComposeMessagesReq) string {
promptTpl := GetBuildPrompt(ctx, req.BuildType)
if promptTpl == "" {
return ""
}
formStr := formToJSON(req.Form)
userFormStr := formToJSON(req.UserForm)
return fmt.Sprintf(promptTpl, formStr, userFormStr)
}

View File

@@ -1,9 +1,10 @@
package service
package gateway
import (
"context"
"encoding/json"
"fmt"
"prompts-core/common/util"
commonHttp "gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/os/gtime"
@@ -19,10 +20,10 @@ type CreateTaskReq struct {
ErrorMsg string `json:"error_msg"`
}
// createGatewayTask 调用 model-gateway 异步任务并同步等待结果
func createGatewayTask(ctx context.Context, payload map[string]any) (string, error) {
// CreateGatewayTask 创建网关异步任务
func CreateGatewayTask(ctx context.Context, payload map[string]any) (string, error) {
fullURL := "model-gateway/task/createTask"
headers := forwardHeaders(ctx)
headers := util.ForwardHeaders(ctx)
var req CreateTaskReq
body, err := json.Marshal(payload)
if err != nil {
@@ -34,15 +35,16 @@ func createGatewayTask(ctx context.Context, payload map[string]any) (string, err
return req.TaskId, nil
}
// GetTaskResultRes 任务结果响应
type GetTaskResultRes struct {
OssFile string `json:"ossFile" dc:"结果文件OSS地址"`
State int `json:"state" dc:"任务状态"`
}
// queryGatewayTaskState 查询网关任务状态
func queryGatewayTaskState(ctx context.Context, taskID string) (int, error) {
// QueryGatewayTaskState 查询网关任务状态
func QueryGatewayTaskState(ctx context.Context, taskID string) (int, error) {
fullURL := fmt.Sprintf("model-gateway/task/getTaskResult?taskId=%s", taskID)
headers := forwardHeaders(ctx)
headers := util.ForwardHeaders(ctx)
var req GetTaskResultRes
if err := commonHttp.Get(ctx, fullURL, headers, &req, nil); err != nil {
return 0, err
@@ -56,16 +58,16 @@ type SkillUserVO struct {
Name string `json:"name"`
Description string `json:"description"`
FileName string `json:"fileName"`
FileUrl string `json:"fileUrl"` // html 后缀
FileUrl string `json:"fileUrl"`
CreatedAt *gtime.Time `json:"createdAt"`
UpdatedAt *gtime.Time `json:"updatedAt"`
ImgAddressPrefix string `json:"imgAddressPrefix"` // htmml 前缀
ImgAddressPrefix string `json:"imgAddressPrefix"`
}
// GetSkillUser 根据 name 获取技能用户信息
// GetSkillUser 获取技能用户信息
func GetSkillUser(ctx context.Context, name string) (*SkillUserVO, error) {
fullURL := fmt.Sprintf("ai-agent/skill/user/getUserOrTemplate?name=%s", name)
headers := forwardHeaders(ctx)
headers := util.ForwardHeaders(ctx)
var resp SkillUserVO
var req struct{}
if err := commonHttp.Get(ctx, fullURL, headers, &resp, req); err != nil {

View File

@@ -1,53 +0,0 @@
package service
import (
"context"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
)
// asyncCtx 固化异步执行所需的 token/user避免请求结束后丢失仅在“同请求内起 goroutine”有用
// 本项目当前是“落库 + 后台 worker”模式因此还会把必要信息持久化到任务表的 request_payload 中。
func asyncCtx(ctx context.Context) context.Context {
asyncCtx := context.WithoutCancel(ctx)
if r := g.RequestFromCtx(ctx); r != nil {
if token := r.Header.Get("Authorization"); token != "" {
asyncCtx = context.WithValue(asyncCtx, "token", token)
}
if userInfo := r.Header.Get("X-User-Info"); userInfo != "" {
asyncCtx = context.WithValue(asyncCtx, "xUserInfo", userInfo)
}
}
if user, err := utils.GetUserInfo(ctx); err == nil && user != nil {
asyncCtx = context.WithValue(asyncCtx, "user", user)
}
return asyncCtx
}
// forwardHeaders 透传调用链路中必须的头信息(优先使用 ctx 里固化的 token / xUserInfo
func forwardHeaders(ctx context.Context) map[string]string {
headers := make(map[string]string)
if token, ok := ctx.Value("token").(string); ok && token != "" {
headers["Authorization"] = token
}
if x, ok := ctx.Value("xUserInfo").(string); ok && x != "" {
headers["X-User-Info"] = x
}
// 兜底:从请求头拿
if r := g.RequestFromCtx(ctx); r != nil {
if headers["Authorization"] == "" {
if token := r.Header.Get("Authorization"); token != "" {
headers["Authorization"] = token
}
}
if headers["X-User-Info"] == "" {
if userInfo := r.Header.Get("X-User-Info"); userInfo != "" {
headers["X-User-Info"] = userInfo
}
}
}
return headers
}

View File

@@ -0,0 +1,112 @@
package prompt
import (
"context"
"errors"
"fmt"
"strings"
"prompts-core/common/util"
"prompts-core/dao"
"prompts-core/model/dto/prompt"
"prompts-core/model/entity"
"github.com/gogf/gf/v2/util/gconv"
)
// buildInferenceRequest 构建返回请求
func buildInferenceRequest(ctx context.Context, req *prompt.ComposeMessagesReq, chatModel *entity.AsynchModel, model *entity.AsynchModel, history []map[string]any) (map[string]any, error) {
ir := NewPromptIR()
// 1. 统一 Prompt IR
switch req.BuildType {
case 1: //构建提示词请求
ir.AddSystem(promptBuild(ctx, req, model))
for _, msg := range history {
role := gconv.String(msg["role"])
if role != "user" && role != "assistant" {
continue
}
ir.AddHistory(role, gconv.String(msg["content"]))
}
ir.AddUser(buildUserPrompt(ctx, req, util.GetModelPrompt(ctx, model.ModelType)))
case 2: //构建节点请求
ir.AddUser(NodeBuild(ctx, req))
default:
return nil, errors.New("不支持的构建类型")
}
// 2. 获取协议配置
protocol, err := GetProtocolByProvider(ctx, "qwen")
if err != nil {
return nil, err
}
if protocol == nil {
return nil, errors.New("协议配置不存在")
}
// 3. 编译为 Provider Request
providerReq, err := Compile(ir, protocol, chatModel)
if err != nil {
return nil, err
}
// 4. 构建请求体
return map[string]any{
"modelName": chatModel.ModelName,
"bizName": "prompts-core",
"callbackUrl": "/prompt/callback",
"requestPayload": providerReq,
}, nil
}
// promptBuild 构建系统提示词
func promptBuild(ctx context.Context, req *prompt.ComposeMessagesReq, model *entity.AsynchModel) string {
providerProtocol, err := dao.ProviderProtocol.Get(ctx, &entity.ProviderProtocol{
ProviderName: "qwen",
Status: 1,
})
if err != nil || providerProtocol == nil {
return ""
}
outputJSON := util.JSONPretty(model.RequestMapping)
var userFormContent strings.Builder
for k, v := range req.UserForm {
userFormContent.WriteString(fmt.Sprintf("%s=%v", k, v))
}
userFormFullText := strings.TrimSuffix(userFormContent.String(), "")
formInfo := fmt.Sprintf(`
【系统表单(系统提示词/参数)】
%s
【用户表单全文(必须完整阅读,全部作为用户提示词来源)】
%s
`, util.FormToJSON(req.Form), userFormFullText)
return fmt.Sprintf(providerProtocol.SystemPromptTemplate, outputJSON, formInfo)
}
// 构建用户提示词
func buildUserPrompt(ctx context.Context, req *prompt.ComposeMessagesReq, prompt string) string {
payload := map[string]any{
"model": req.ModelName, // 请求模型名称
"promptInfo": prompt, // 数据库提示信息
"form": req.Form, // 系统表单
"userForm": req.UserForm, // 用户表单
"userFiles": req.UserFiles, //文件url
"userFilesText": FetchFileTexts(ctx, req.UserFiles), //解读文件(只支持可读类型 如xmljson,yaml
"skills": SkillMdContent(ctx, req.SkillName), //skill 相关(根据传入的 skillName 获取 zip 内所有 md 文件拼接内容)
}
return util.MustMarshal(payload)
}
// NodeBuild 节点构建
func NodeBuild(ctx context.Context, req *prompt.ComposeMessagesReq) string {
promptTpl := util.GetBuildPrompt(ctx, req.BuildType)
if promptTpl == "" {
return ""
}
formStr := util.FormToJSON(req.Form)
userFormStr := util.FormToJSON(req.UserForm)
return fmt.Sprintf(promptTpl, formStr, userFormStr)
}

View File

@@ -1,28 +1,28 @@
package service
package prompt
import (
"context"
"encoding/json"
"errors"
"fmt"
"prompts-core/dao"
"prompts-core/model/entity"
"strings"
"time"
"prompts-core/common/util"
"prompts-core/consts/public"
"prompts-core/dao"
"prompts-core/model/dto"
"prompts-core/model/entity"
promptDto "prompts-core/model/dto/prompt"
"prompts-core/service/gateway"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/frame/g"
)
// ============================================
// 核心业务流程
// ============================================
// ComposeMessages 拼接提示词主流程
func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (*dto.ComposeMessagesRes, error) {
// ComposeMessages 核心拼接提示词主流程
func ComposeMessages(ctx context.Context, req *promptDto.ComposeMessagesReq) (*promptDto.ComposeMessagesRes, error) {
var (
epicycleId int64
taskID string
@@ -32,7 +32,7 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
taskRecord *entity.ComposeTask
)
// 获取模型信息
chatModel, model, err := s.GetModelMessage(ctx, req)
chatModel, aiModel, err := GetModelMessage(ctx, req)
if err != nil {
return nil, err
}
@@ -42,18 +42,18 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
case 1:
maxRetryTimes := g.Cfg().MustGet(ctx, "promptsRetry.maxRetryTimes", 3).Int()
//1. 获取历史会话
history, err = Session.GetHistoryMessages(ctx, req.SessionId)
history, err = GetHistoryMessages(ctx, req.SessionId)
if err != nil {
g.Log().Errorf(ctx, "获取历史会话失败: %v将不使用历史会话", err)
history = nil // 出错就用空的,不影响主流程
}
// 重试循环
for attempt := 0; attempt <= maxRetryTimes; attempt++ {
for attempt := 0; attempt <= 0; attempt++ {
if attempt > 0 {
g.Log().Warningf(ctx, "[重试]第 %d/%d 次调用推理模型", attempt, maxRetryTimes)
}
// 2. 调用推理模型
taskID, err = s.callInferenceModel(ctx, req, chatModel, model, history)
taskID, err = callInferenceModel(ctx, req, chatModel, aiModel, history)
if err != nil {
g.Log().Errorf(ctx, "调用推理模型失败(第%d次): %v", attempt+1, err)
continue
@@ -64,7 +64,7 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
TaskId: taskID,
ModelName: req.ModelName,
SkillName: req.SkillName,
RequestPayload: mustMarshal(req),
RequestPayload: util.MustMarshal(req),
Status: public.ComposeStatusPending,
})
if err != nil {
@@ -73,14 +73,14 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
}
// 4. 等待结果
taskRecord, err = s.waitForResult(ctx, taskID)
taskRecord, err = 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) {
message = parsePromptBuild(taskRecord, chatModel)
if message != nil && util.IsMessageValid(message) {
break
}
g.Log().Warningf(ctx, "[重试] 推理结果不合法(第%d次),准备重新请求", attempt+1)
@@ -97,7 +97,7 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
//节点构建
case 2:
//1. 调用推理模型
taskID, err = s.callInferenceModel(ctx, req, chatModel, model, nil)
taskID, err = callInferenceModel(ctx, req, chatModel, aiModel, nil)
if err != nil {
return nil, err
}
@@ -106,115 +106,41 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
TaskId: taskID,
ModelName: req.ModelName,
SkillName: req.SkillName,
RequestPayload: mustMarshal(req),
RequestPayload: util.MustMarshal(req),
Status: public.ComposeStatusPending,
})
//5. 等待结果
taskRecord, err := s.waitForResult(ctx, taskID)
taskRecord, err := waitForResult(ctx, taskID)
if err != nil {
return nil, err
}
fmt.Println("构建节点前", taskRecord)
message = s.parseNodeBuild(taskRecord)
fmt.Println("构建节点后", message)
message = parseNodeBuild(taskRecord)
default:
epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
SessionId: req.SessionId,
Remark: req.Cause,
})
return &dto.ComposeMessagesRes{
return &promptDto.ComposeMessagesRes{
EpicycleId: epicycleId,
}, nil
}
return &dto.ComposeMessagesRes{
return &promptDto.ComposeMessagesRes{
Messages: message,
EpicycleId: epicycleId,
}, nil
}
func (s *promptService) Callback(ctx context.Context, req *dto.CallbackReq) error {
g.Log().Infof(ctx, "[Callback][RECV] taskId=%s state=%d ossFile=%s fileType=%s textLen=%d",
req.TaskId, req.State, req.OssFile, req.FileType, len(req.Text))
// ============ 先查任务是否存在 ============
task, err := dao.ComposeTask.GetByTaskId(ctx, req.TaskId)
if err != nil {
return err
}
if task == nil {
return fmt.Errorf("任务不存在: %s", req.TaskId)
}
// ============ 根据状态区分处理 ============
if req.State == 3 {
// 失败:直接更新状态
_, err = dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusFailed,
entity.ComposeTaskCol.ErrorMessage: req.ErrorMsg,
})
return err
}
// ======================================
// 成功:解析模型输出
result, err := parseOutput(req.Text)
if err != nil {
_, updateErr := dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusFailed,
entity.ComposeTaskCol.ErrorMessage: err.Error(),
})
if updateErr != nil {
g.Log().Warningf(ctx, "[Callback] 更新失败状态出错 taskId=%s err=%v", req.TaskId, updateErr)
}
return err
}
// ============ result 可能为 nil ============
var messages any
if result != nil {
messages = result
}
// =======================================
_, err = dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusSuccess,
entity.ComposeTaskCol.Messages: messages,
})
if err != nil {
g.Log().Errorf(ctx, "[Callback] 更新任务失败 taskId=%s err=%v", req.TaskId, err)
}
return err
}
// GetComposeTask 查询任务结果
func (s *promptService) GetComposeTask(ctx context.Context, taskID string) (*dto.GetComposeTaskRes, error) {
record, err := dao.ComposeTask.GetByTaskId(ctx, taskID)
if err != nil {
return nil, err
}
if record == nil {
return nil, fmt.Errorf("未找到任务(taskId=%s)", taskID)
}
// 如果 Messages 是字符串,反序列化为 JSON 数组
messages := record.Messages
if str, ok := messages.(string); ok && str != "" {
var parsed any
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
messages = parsed
}
}
return &dto.GetComposeTaskRes{
TaskId: record.TaskId,
Status: record.Status,
ErrorMessage: record.ErrorMessage,
Messages: messages,
}, nil
}
// GetModelMessage 获取模型信息
func (s *promptService) GetModelMessage(ctx context.Context, req *dto.ComposeMessagesReq) (*entity.AsynchModel, *entity.AsynchModel, error) {
func GetModelMessage(ctx context.Context, req *promptDto.ComposeMessagesReq) (*entity.AsynchModel, *entity.AsynchModel, error) {
userInfo, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, nil, err
}
// 1. 获取当前用户的会话模型
chatModel, err := dao.Model.GetByIsChatModel(ctx)
chatModel, err := dao.Model.Get(ctx, &entity.AsynchModel{
SQLBaseDO: beans.SQLBaseDO{Creator: userInfo.UserName},
IsChatModel: 1,
})
if err != nil {
return nil, nil, err
}
@@ -222,18 +148,21 @@ func (s *promptService) GetModelMessage(ctx context.Context, req *dto.ComposeMes
return nil, nil, errors.New("当前没有对话模型,请添加")
}
// 2. 获取要构建的模型信息
model, err := dao.Model.GetByModelName(ctx, req.ModelName)
aiModel, err := dao.Model.Get(ctx, &entity.AsynchModel{
SQLBaseDO: beans.SQLBaseDO{Creator: userInfo.UserName},
ModelName: req.ModelName,
})
if err != nil {
return nil, nil, err
}
if model == nil {
if aiModel == nil {
return nil, nil, fmt.Errorf("需要构建的模型 %s 不存在", req.ModelName)
}
return chatModel, model, nil
return chatModel, aiModel, nil
}
// callInferenceModel 调用推理模型
func (s *promptService) callInferenceModel(ctx context.Context, req *dto.ComposeMessagesReq, chatModel *entity.AsynchModel, model *entity.AsynchModel, history []map[string]any) (string, error) {
func callInferenceModel(ctx context.Context, req *promptDto.ComposeMessagesReq, chatModel *entity.AsynchModel, model *entity.AsynchModel, history []map[string]any) (string, error) {
// 构建推理模型请求
taskReq, err := buildInferenceRequest(ctx, req, chatModel, model, history)
if err != nil {
@@ -241,7 +170,7 @@ func (s *promptService) callInferenceModel(ctx context.Context, req *dto.Compose
}
// 创建网关任务
taskID, err := createGatewayTask(ctx, taskReq)
taskID, err := gateway.CreateGatewayTask(ctx, taskReq)
if err != nil {
return "", fmt.Errorf("创建网关任务失败: %w", err)
}
@@ -253,10 +182,8 @@ func (s *promptService) callInferenceModel(ctx context.Context, req *dto.Compose
return taskID, nil
}
// ============================================
// 步骤6等待结果
// ============================================
func (s *promptService) waitForResult(ctx context.Context, taskID string) (*entity.ComposeTask, error) {
// waitForResult 等待结果
func waitForResult(ctx context.Context, taskID string) (*entity.ComposeTask, error) {
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)
@@ -271,7 +198,9 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
}
// 1. 查数据库
record, err := dao.ComposeTask.GetByTaskId(ctx, taskID)
record, err := dao.ComposeTask.Get(ctx, &entity.ComposeTask{
TaskId: taskID,
})
if err != nil {
// ===================== 修复点 2如果是上下文取消直接返回 =====================
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
@@ -292,7 +221,7 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
}
// 2. 查网关状态
state, err := queryGatewayTaskState(ctx, taskID)
state, err := gateway.QueryGatewayTaskState(ctx, taskID)
if err != nil {
// 网关不可达不终止,继续轮询
g.Log().Warningf(ctx, "[waitForResult] 查询网关失败 taskId=%s err=%v", taskID, err)
@@ -301,16 +230,24 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
case 2: // 网关成功
// 网关已成功,主动更新数据库
if record != nil {
dao.ComposeTask.UpdateByTaskId(ctx, taskID, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusSuccess,
_, err = dao.ComposeTask.Update(ctx, &entity.ComposeTask{
TaskId: taskID,
Status: public.ComposeStatusSuccess,
})
if err != nil {
g.Log().Warningf(ctx, "[waitForResult] 更新任务状态失败 taskId=%s err=%v", taskID, err)
}
}
case 3: // 网关失败
if record != nil {
dao.ComposeTask.UpdateByTaskId(ctx, taskID, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusFailed,
entity.ComposeTaskCol.ErrorMessage: "model-gateway 任务执行失败",
_, err = dao.ComposeTask.Update(ctx, &entity.ComposeTask{
TaskId: taskID,
Status: public.ComposeStatusFailed,
ErrorMessage: "model-gateway 任务执行失败",
})
if err != nil {
g.Log().Warningf(ctx, "[waitForResult] 更新任务状态失败 taskId=%s err=%v", taskID, err)
}
}
return nil, fmt.Errorf("model-gateway 任务执行失败(taskId=%s)", taskID)
}
@@ -331,7 +268,7 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
}
// parsePromptBuild 解析提示词构建结果BuildType == 1
func (s *promptService) parsePromptBuild(taskRecord *entity.ComposeTask, model *entity.AsynchModel) map[string]any {
func parsePromptBuild(taskRecord *entity.ComposeTask, model *entity.AsynchModel) map[string]any {
if taskRecord == nil {
return nil
}
@@ -394,7 +331,7 @@ func (s *promptService) parsePromptBuild(taskRecord *entity.ComposeTask, model *
}
// parseNodeBuild 解析节点构建结果BuildType == 2
func (s *promptService) parseNodeBuild(taskRecord *entity.ComposeTask) map[string]any {
func parseNodeBuild(taskRecord *entity.ComposeTask) map[string]any {
if taskRecord == nil {
return nil
}
@@ -414,3 +351,90 @@ func (s *promptService) parseNodeBuild(taskRecord *entity.ComposeTask) map[strin
}
return result
}
// Callback 回调处理
func Callback(ctx context.Context, req *promptDto.CallbackReq) error {
g.Log().Infof(ctx, "[Callback][RECV] taskId=%s state=%d ossFile=%s fileType=%s textLen=%d",
req.TaskId, req.State, req.OssFile, req.FileType, len(req.Text))
// ============ 先查任务是否存在 ============
task, err := dao.ComposeTask.Get(ctx, &entity.ComposeTask{
TaskId: req.TaskId,
})
if err != nil {
return err
}
if task == nil {
return fmt.Errorf("任务不存在: %s", req.TaskId)
}
// ============ 根据状态区分处理 ============
if req.State == 3 {
// 失败:直接更新状态
_, err = dao.ComposeTask.Update(ctx, &entity.ComposeTask{
TaskId: req.TaskId,
Status: public.ComposeStatusFailed,
ErrorMessage: req.ErrorMsg,
})
return err
}
// ======================================
// 成功:解析模型输出
result, err := util.ParseOutput(req.Text)
if err != nil {
_, updateErr := dao.ComposeTask.Update(ctx, &entity.ComposeTask{
TaskId: req.TaskId,
Status: public.ComposeStatusFailed,
ErrorMessage: req.ErrorMsg,
})
if updateErr != nil {
g.Log().Warningf(ctx, "[Callback] 更新失败状态出错 taskId=%s err=%v", req.TaskId, updateErr)
}
return err
}
// ============ result 可能为 nil ============
var messages any
if result != nil {
messages = result
}
// =======================================
_, err = dao.ComposeTask.Update(ctx, &entity.ComposeTask{
TaskId: req.TaskId,
Status: public.ComposeStatusSuccess,
Messages: messages,
})
if err != nil {
g.Log().Errorf(ctx, "[Callback] 更新任务失败 taskId=%s err=%v", req.TaskId, err)
}
return err
}
// GetComposeTask 查询任务结果
func GetComposeTask(ctx context.Context, taskID string) (*promptDto.GetComposeTaskRes, error) {
record, err := dao.ComposeTask.Get(ctx, &entity.ComposeTask{
TaskId: taskID,
})
if err != nil {
return nil, err
}
if record == nil {
return nil, fmt.Errorf("未找到任务(taskId=%s)", taskID)
}
// 如果 Messages 是字符串,反序列化为 JSON 数组
messages := record.Messages
if str, ok := messages.(string); ok && str != "" {
var parsed any
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
messages = parsed
}
}
return &promptDto.GetComposeTaskRes{
TaskId: record.TaskId,
Status: record.Status,
ErrorMessage: record.ErrorMessage,
Messages: messages,
}, nil
}

View File

@@ -1,4 +1,4 @@
package service
package prompt
import (
"archive/zip"
@@ -7,52 +7,16 @@ import (
"fmt"
"io"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
"prompts-core/common/util"
"prompts-core/service/gateway"
"github.com/gogf/gf/v2/frame/g"
)
// ============================================
// 文件处理(配置直接内联 + zip 支持)
// ============================================
// 允许的文本类 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 内文件)
// FetchFileTexts 从 URL 列表获取文件内容,支持 zip 内文件
func FetchFileTexts(ctx context.Context, urls []string) map[string]string {
result := make(map[string]string)
@@ -65,16 +29,16 @@ func FetchFileTexts(ctx context.Context, urls []string) map[string]string {
}
for _, rawURL := range urls {
url := sanitizeURL(rawURL)
url := util.SanitizeURL(rawURL)
if url == "" {
continue
}
if isBannedExtension(url) {
if util.IsBannedExtension(url) {
continue
}
if isZipExtension(url) {
if util.IsZipExtension(url) {
zipTexts := fetchZipFileTexts(ctx, client, url)
for k, v := range zipTexts {
result[k] = v
@@ -91,21 +55,14 @@ func FetchFileTexts(ctx context.Context, urls []string) map[string]string {
continue
}
text = cleanSymbols(text)
text = util.CleanSymbols(text)
result[url] = text
}
return result
}
func isZipExtension(url string) bool {
ext := strings.ToLower(filepath.Ext(url))
if idx := strings.Index(ext, "?"); idx != -1 {
ext = ext[:idx]
}
return ext == ".zip"
}
// fetchZipFileTexts 下载并解压 zip 文件,提取可读文本内容
func fetchZipFileTexts(ctx context.Context, client *http.Client, url string) map[string]string {
result := make(map[string]string)
@@ -130,11 +87,11 @@ func fetchZipFileTexts(ctx context.Context, client *http.Client, url string) map
fileName := file.Name
if isBannedExtension(fileName) {
if util.IsBannedExtension(fileName) {
continue
}
if isZipExtension(fileName) {
if util.IsZipExtension(fileName) {
continue
}
@@ -150,11 +107,11 @@ func fetchZipFileTexts(ctx context.Context, client *http.Client, url string) map
}
contentType := http.DetectContentType(content)
if !isReadableContentType(contentType) {
if !util.IsReadableContentType(contentType) {
continue
}
text := cleanSymbols(string(content))
text := util.CleanSymbols(string(content))
if text == "" {
continue
}
@@ -166,6 +123,7 @@ func fetchZipFileTexts(ctx context.Context, client *http.Client, url string) map
return result
}
// downloadFile 下载文件,限制最大大小
func downloadFile(client *http.Client, url string, maxSize int64) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -185,35 +143,7 @@ func downloadFile(client *http.Client, url string, maxSize int64) ([]byte, error
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)
}
// fetchFileContent 获取单个文本文件内容
func fetchFileContent(ctx context.Context, client *http.Client, url string) (string, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -231,7 +161,7 @@ func fetchFileContent(ctx context.Context, client *http.Client, url string) (str
}
contentType := resp.Header.Get("Content-Type")
if !isReadableContentType(contentType) {
if !util.IsReadableContentType(contentType) {
return "", fmt.Errorf("unreadable content-type: %s", contentType)
}
@@ -247,22 +177,15 @@ func fetchFileContent(ctx context.Context, client *http.Client, url string) (str
return strings.TrimSpace(string(body)), nil
}
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)
skillResp, err := gateway.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,
}
@@ -274,7 +197,6 @@ func SkillMdContent(ctx context.Context, skillName string) string {
return ""
}
// 3. 解压 zip 并提取所有 md 文件内容
mdContents, err := extractMdFiles(ctx, zipBytes)
if err != nil {
return ""
@@ -284,7 +206,6 @@ func SkillMdContent(ctx context.Context, skillName string) string {
return ""
}
// 4. 拼接所有 md 内容
var builder strings.Builder
builder.WriteString(fmt.Sprintf("# Skill: %s\n\n", skillResp.Name))
if skillResp.Description != "" {

View File

@@ -0,0 +1,264 @@
package prompt
import (
"context"
"encoding/json"
"fmt"
"prompts-core/common/util"
"strings"
"prompts-core/dao"
"prompts-core/model/entity"
)
// PromptIR 统一 Prompt 中间表示
type PromptIR struct {
System []Segment `json:"system"`
History []Segment `json:"history"`
User []Segment `json:"user"`
}
// Segment 消息片段
type Segment struct {
Type string `json:"type"` // text/image
Content string `json:"content"`
Role string `json:"role,omitempty"`
}
// NewPromptIR 创建空 PromptIR
func NewPromptIR() *PromptIR {
return &PromptIR{
System: make([]Segment, 0),
History: make([]Segment, 0),
User: make([]Segment, 0),
}
}
// AddSystem 添加系统提示
func (ir *PromptIR) AddSystem(content string) *PromptIR {
if content != "" {
ir.System = append(ir.System, Segment{Type: "text", Content: content})
}
return ir
}
// AddUser 添加用户消息
func (ir *PromptIR) AddUser(content string) *PromptIR {
if content != "" {
ir.User = append(ir.User, Segment{Type: "text", Content: content})
}
return ir
}
// AddHistory 添加历史消息
func (ir *PromptIR) AddHistory(role, content string) *PromptIR {
if content != "" {
ir.History = append(ir.History, Segment{Type: "text", Content: content, Role: role})
}
return ir
}
// ToMessages 转换为 OpenAI 兼容的 messages 格式MVP 默认)
func (ir *PromptIR) ToMessages() []map[string]any {
var messages []map[string]any
// 1. 系统消息
for _, seg := range ir.System {
messages = append(messages, map[string]any{
"role": "system",
"content": seg.Content,
})
}
// 2. 历史消息
for _, seg := range ir.History {
messages = append(messages, map[string]any{
"role": seg.Role,
"content": seg.Content,
})
}
// 3. 用户消息
for _, seg := range ir.User {
messages = append(messages, map[string]any{
"role": "user",
"content": seg.Content,
})
}
return messages
}
// GetProtocolByProvider 根据 provider_name 获取协议配置
func GetProtocolByProvider(ctx context.Context, providerName string) (*ProviderProtocol, error) {
entity, err := dao.ProviderProtocol.Get(ctx, &entity.ProviderProtocol{
ProviderName: providerName,
Status: 1,
})
if err != nil || entity == nil {
return nil, err
}
entity.MergeOrder = util.ParseJSONField(entity.MergeOrder)
entity.RoleMapping = util.ParseJSONField(entity.RoleMapping)
entity.ContentMapping = util.ParseJSONField(entity.ContentMapping)
entity.RequestTemplate = util.ParseJSONField(entity.RequestTemplate)
entity.ContentMapping = util.ParseJSONField(entity.ContentMapping)
return parseProtocol(entity), nil
}
// parseProtocol 将 DB entity 转为编译用协议配置
func parseProtocol(e *entity.ProviderProtocol) *ProviderProtocol {
p := &ProviderProtocol{
TargetField: e.TargetField,
}
// MergeOrder: any → []string
if e.MergeOrder != nil {
b, _ := json.Marshal(e.MergeOrder)
json.Unmarshal(b, &p.MergeOrder)
}
// RoleMapping: any → map[string]string
if e.RoleMapping != nil {
b, _ := json.Marshal(e.RoleMapping)
json.Unmarshal(b, &p.RoleMapping)
}
// ContentMapping: any → ContentMapping
if e.ContentMapping != nil {
b, _ := json.Marshal(e.ContentMapping)
json.Unmarshal(b, &p.ContentMapping)
}
// RequestTemplate: any → map[string]any
if e.RequestTemplate != nil {
b, _ := json.Marshal(e.RequestTemplate)
json.Unmarshal(b, &p.RequestTemplate)
}
fmt.Printf("parseProtocol: %+v\n", p)
return p
}
// ProviderProtocol 协议编译配置(从 DB JSONB 字段解析)
type ProviderProtocol struct {
TargetField string `json:"target_field"`
MergeOrder []string `json:"merge_order"`
RoleMapping map[string]string `json:"role_mapping"`
ContentMapping ContentMapping `json:"content_mapping"`
RequestTemplate map[string]any `json:"request_template"`
}
// ContentMapping 内容字段映射
type ContentMapping struct {
Type string `json:"type"` // direct/parts
Field string `json:"field"` // content/text
}
// Compile 将 PromptIR 按协议配置编译为 Provider Request
func Compile(ir *PromptIR, p *ProviderProtocol, chatModel *entity.AsynchModel) (map[string]any, error) {
if ir == nil || p == nil {
return nil, fmt.Errorf("ir and protocol are required")
}
// 1. 按 merge_order 拼接消息
messages := mergeByOrder(ir, p.MergeOrder)
// 2. 角色映射
messages = mapRoles(messages, p.RoleMapping)
// 3. 内容字段映射
messages = mapContent(messages, p.ContentMapping)
// 4. 按 target_field + request_template 构建请求体
return buildRequest(messages, p, chatModel), nil
}
// mergeByOrder 按协议配置顺序拼接消息
func mergeByOrder(ir *PromptIR, order []string) []map[string]any {
var messages []map[string]any
for _, part := range order {
switch part {
case "system":
for _, seg := range ir.System {
messages = append(messages, map[string]any{
"role": "system",
"content": seg.Content,
})
}
case "history":
for _, seg := range ir.History {
messages = append(messages, map[string]any{
"role": seg.Role,
"content": seg.Content,
})
}
case "user":
for _, seg := range ir.User {
messages = append(messages, map[string]any{
"role": "user",
"content": seg.Content,
})
}
}
}
return messages
}
// mapRoles 角色映射
func mapRoles(messages []map[string]any, mapping map[string]string) []map[string]any {
if len(mapping) == 0 {
return messages
}
for i, msg := range messages {
role, ok := msg["role"].(string)
if !ok {
continue
}
if mapped, exists := mapping[role]; exists {
messages[i]["role"] = mapped
}
}
return messages
}
// mapContent 内容字段映射
func mapContent(messages []map[string]any, cm ContentMapping) []map[string]any {
for _, msg := range messages {
content := msg["content"]
delete(msg, "content")
switch cm.Type {
case "parts":
// Gemini 格式: {"parts": [{"text": "..."}]}
msg["parts"] = []map[string]any{
{cm.Field: content},
}
default:
// direct: {"content": "..."}
msg[cm.Field] = content
}
}
return messages
}
// buildRequest 按 target_field 和 request_template 构建请求体
func buildRequest(messages []map[string]any, p *ProviderProtocol, chatModel *entity.AsynchModel) map[string]any {
if len(p.RequestTemplate) > 0 {
return renderTemplate(p.RequestTemplate, messages, chatModel)
}
return map[string]any{
p.TargetField: messages,
}
}
// renderTemplate 简单的 {{key}} 模板替换
func renderTemplate(tmpl map[string]any, messages []map[string]any, chatModel *entity.AsynchModel) map[string]any {
b, _ := json.Marshal(tmpl)
str := string(b)
// 替换 {{model}}
str = strings.ReplaceAll(str, `"{{model}}"`, `"`+chatModel.ModelName+`"`)
// 替换 {{messages}}
msgBytes, _ := json.Marshal(messages)
str = strings.ReplaceAll(str, `"{{messages}}"`, string(msgBytes))
var result map[string]any
json.Unmarshal([]byte(str), &result)
return result
}

View File

@@ -1,4 +1,4 @@
package service
package prompt
import (
"context"
@@ -12,7 +12,7 @@ import (
// ==================== Redis 操作 ====================
// saveToRedis 保存会话数据到Redis
func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requestMessages []map[string]any, responseMessages []map[string]any) error {
func 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()
@@ -50,7 +50,7 @@ func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requ
}
// getFromRedis 从Redis获取会话历史
func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]map[string]any, error) {
func getFromRedis(ctx context.Context, sessionId string) ([]map[string]any, error) {
key := fmt.Sprintf("chat:session:%s", sessionId)
result, err := g.Redis().Do(ctx, "LRANGE", key, 0, -1)
@@ -82,8 +82,8 @@ func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]
}
// GetSessionHistoryForInference 获取历史会话,返回扁平消息数组(给推理用)
func (s *sessionService) GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]map[string]any, error) {
historyData, err := s.getFromRedis(ctx, sessionId)
func GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]map[string]any, error) {
historyData, err := getFromRedis(ctx, sessionId)
if err != nil {
return nil, fmt.Errorf("获取历史会话失败: %w", err)
}

View File

@@ -1,24 +1,22 @@
package service
package prompt
import (
"context"
"fmt"
"prompts-core/dao"
"prompts-core/model/dto"
sessionDao "prompts-core/dao"
"prompts-core/model/entity"
"prompts-core/common/util"
sessionDto "prompts-core/model/dto/prompt"
"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{}
func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCallbackReq) (res *beans.ResponseEmpty, err error) {
func SessionCallback(ctx context.Context, req *sessionDto.SessionCallbackReq) (res *sessionDto.SessionCallbackRes, err error) {
// 1. 解析AI返回的文本
result, err := parseOutput(req.Text)
result, err := util.ParseOutput(req.Text)
if err != nil {
g.Log().Errorf(ctx, "[会话回调] 解析模型输出失败 epicycleId=%d err=%v", req.EpicycleId, err)
return nil, err
@@ -26,7 +24,7 @@ func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCa
// 2. 更新数据库
result["role"] = "assistant"
_, err = dao.ComposeSession.Update(ctx, &entity.ComposeSession{
_, err = sessionDao.ComposeSession.Update(ctx, &entity.ComposeSession{
SQLBaseDO: beans.SQLBaseDO{Id: req.EpicycleId},
ResponseContent: result,
})
@@ -36,17 +34,19 @@ func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCa
}
// 3. 获取当前轮次完整数据
session, err := dao.ComposeSession.GetById(ctx, req.EpicycleId)
session, err := sessionDao.ComposeSession.Get(ctx, &entity.ComposeSession{
SQLBaseDO: beans.SQLBaseDO{Id: req.EpicycleId},
})
if err != nil {
g.Log().Errorf(ctx, "[会话回调] 获取会话数据失败 epicycleId=%d err=%v", req.EpicycleId, err)
return nil, err
}
// 4. 转换 json 并存入 Redis
requestMessages := convertToMessages(session.RequestContent)
responseMessages := convertToMessages(session.ResponseContent)
requestMessages := util.ConvertToMessages(session.RequestContent)
responseMessages := util.ConvertToMessages(session.ResponseContent)
if err = s.saveToRedis(ctx, session.SessionId, requestMessages, responseMessages); err != nil {
if err = saveToRedis(ctx, session.SessionId, requestMessages, responseMessages); err != nil {
g.Log().Errorf(ctx, "[会话回调] Redis存储失败 sessionId=%s id=%d err=%v",
session.SessionId, session.Id, err)
return nil, err
@@ -54,21 +54,23 @@ func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCa
g.Log().Infof(ctx, "[会话回调] 存储成功 sessionId=%s id=%d requestLen=%d responseLen=%d",
session.SessionId, session.Id, len(requestMessages), len(responseMessages))
return &beans.ResponseEmpty{}, nil
return &sessionDto.SessionCallbackRes{}, nil
}
// GetHistoryMessages 获取历史信息
func (s *sessionService) GetHistoryMessages(ctx context.Context, sessionId string) ([]map[string]any, error) {
func 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)
redisHistory, err := GetSessionHistoryForInference(ctx, sessionId)
if err == nil && len(redisHistory) > 0 {
return redisHistory, nil
}
// 2. Redis 没有 → fallback DB
sessions, err := dao.ComposeSession.GetListBySessionId(ctx, sessionId, maxRounds)
sessions, _, err := sessionDao.ComposeSession.List(ctx, &entity.ComposeSession{
SessionId: sessionId,
}, 1, maxRounds)
if err != nil {
return nil, fmt.Errorf("DB获取历史失败: %w", err)
}
@@ -77,7 +79,7 @@ func (s *sessionService) GetHistoryMessages(ctx context.Context, sessionId strin
for _, session := range sessions {
// request
reqMsgs := convertToMessages(session.RequestContent)
reqMsgs := util.ConvertToMessages(session.RequestContent)
for _, m := range reqMsgs {
role := gconv.String(m["role"])
if role == "user" || role == "assistant" {
@@ -86,7 +88,7 @@ func (s *sessionService) GetHistoryMessages(ctx context.Context, sessionId strin
}
// response
respMsgs := convertToMessages(session.ResponseContent)
respMsgs := util.ConvertToMessages(session.ResponseContent)
for _, m := range respMsgs {
if m["role"] == nil {
m["role"] = "assistant"
@@ -97,15 +99,15 @@ func (s *sessionService) GetHistoryMessages(ctx context.Context, sessionId strin
// 3. 回写 Redis
for _, session := range sessions {
reqMsgs := convertToMessages(session.RequestContent)
respMsgs := convertToMessages(session.ResponseContent)
reqMsgs := util.ConvertToMessages(session.RequestContent)
respMsgs := util.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)
_ = saveToRedis(ctx, session.SessionId, reqMsgs, respMsgs)
}
}
return messages, nil

View File

@@ -1,92 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"prompts-core/dao"
"prompts-core/model/dto"
"prompts-core/model/entity"
)
var Prompt = &promptService{}
type promptService struct{}
func (s *promptService) Create(ctx context.Context, req *dto.CreatePromptReq) (res *dto.CreatePromptRes, err error) {
// promptInfo 兜底校验:必须可序列化为 JSON
if req.PromptInfo == nil {
return nil, errors.New("promptInfo不能为空")
}
if _, err := json.Marshal(req.PromptInfo); err != nil {
return nil, errors.New("promptInfo不是合法JSON")
}
if req.ResponseJsonSchema == nil {
return nil, errors.New("responseJsonSchema不能为空")
}
if _, err := json.Marshal(req.ResponseJsonSchema); err != nil {
return nil, errors.New("responseJsonSchema不是合法JSON")
}
m := &entity.PromptConfig{
ModelTypeId: req.ModelTypeId,
ModelType: req.ModelType,
PromptInfo: req.PromptInfo,
ResponseJsonSchema: req.ResponseJsonSchema,
Enabled: 1,
Version: req.Version,
}
id, err := dao.Prompt.Insert(ctx, m)
if err != nil {
return nil, err
}
return &dto.CreatePromptRes{ID: id}, nil
}
func (s *promptService) Update(ctx context.Context, req *dto.UpdatePromptReq) error {
data := map[string]any{}
if req.ModelTypeId != nil && *req.ModelTypeId > 0 {
data[entity.PromptConfigCol.ModelTypeId] = *req.ModelTypeId
}
if req.ModelType != nil && *req.ModelType != "" {
data[entity.PromptConfigCol.ModelType] = *req.ModelType
}
if req.PromptInfo != nil {
if _, err := json.Marshal(req.PromptInfo); err != nil {
return errors.New("promptInfo不是合法JSON")
}
data[entity.PromptConfigCol.PromptInfo] = req.PromptInfo
}
if req.ResponseJsonSchema != nil {
if _, err := json.Marshal(req.ResponseJsonSchema); err != nil {
return errors.New("responseJsonSchema不是合法JSON")
}
data[entity.PromptConfigCol.ResponseJsonSchema] = req.ResponseJsonSchema
}
if req.Enabled != nil {
data[entity.PromptConfigCol.Enabled] = *req.Enabled
}
if req.Version != nil {
data[entity.PromptConfigCol.Version] = *req.Version
}
if len(data) == 0 {
return errors.New("无可更新字段")
}
_, err := dao.Prompt.UpdateByID(ctx, req.ID, data)
return err
}
func (s *promptService) Delete(ctx context.Context, id int64) error {
_, err := dao.Prompt.DeleteByID(ctx, id)
return err
}
func (s *promptService) Get(ctx context.Context, id int64) (*entity.PromptConfig, error) {
return dao.Prompt.GetByID(ctx, id)
}
func (s *promptService) List(ctx context.Context, pageNum, pageSize int, modelTypeID *int, modelTypeLike string) (list []*entity.PromptConfig, total int64, err error) {
return dao.Prompt.List(ctx, pageNum, pageSize, modelTypeID, modelTypeLike)
}

View File

@@ -1,65 +0,0 @@
// utils 工具函数
package service
import (
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/util/gconv"
)
// ============================================
// json 相关处理
// ============================================
// 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)
}
return j.Map(), nil
}
func convertToMessages(raw any) []map[string]any {
if raw == nil {
return nil
}
j, err := gjson.LoadJson(gconv.Bytes(raw))
if err != nil {
return nil
}
// 1. 如果有 messages
if j.Contains("messages") {
return gconv.Maps(j.Get("messages").Array())
}
// 2. 否则当成单条 message
return []map[string]any{
j.Map(),
}
}
// isMessageValid 校验推理结果是否合法
func isMessageValid(message map[string]any) bool {
if message == nil {
return false
}
return true
}
func formToJSON(form map[string]any) string {
if form == nil {
return "{}"
}
b, _ := json.Marshal(form)
return string(b)
}
func mustMarshal(v any) string {
b, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(b)
}