Compare commits

8 Commits

24 changed files with 1021 additions and 1829 deletions

44
Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# 多阶段构建 - 第一阶段:编译(使用已安装的镜像)
FROM golang:alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOTOOLCHAIN=auto
ENV GOPRIVATE=gitea.com/red-future/common
# 配置git使用私有Gitea仓库带Token认证
RUN git config --global url."http://x-token-auth:619679cd366aefea3a50f0622d842a41f2209e08595767bba49c3836ef57d415@116.204.74.41:3000/red-future/common.git".insteadOf "https://gitea.com/red-future/common.git" && \
git config --global credential.helper store
WORKDIR /build
# 复制父目录的 common 模块(因为 go.mod 中使用了本地 replace)
#COPY ../common /build/common
COPY . .
RUN go mod download && go mod tidy
RUN go build -ldflags="-s -w" -o main ./main.go
# 第二阶段:运行
FROM alpine:3.19
ENV TIME_ZONE=Asia/Shanghai
RUN apk add --no-cache ca-certificates tzdata && \
ln -sf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime
WORKDIR /app
# 复制编译好的二进制文件
COPY --from=builder /build/main .
COPY --from=builder /build/config.yml ./
# 创建日志目录
RUN mkdir -p /logs /app/resource/log/run /app/resource/log/server
EXPOSE 3009
CMD ["./main"]

View File

@@ -1,5 +1,5 @@
server: server:
address: ":8002" address: ":3009"
name: "prompts-core" name: "prompts-core"
workerId: 1 # 雪花算法 worker ID用于 common/db/gfdb workerId: 1 # 雪花算法 worker ID用于 common/db/gfdb
@@ -7,7 +7,7 @@ server:
database: database:
default: default:
- type: "pgsql" - type: "pgsql"
host: "116.204.74.41" host: "192.168.0.169"
port: "15432" port: "15432"
user: "postgres" user: "postgres"
pass: "Bjang09@686^*^" pass: "Bjang09@686^*^"
@@ -29,17 +29,16 @@ database:
redis: redis:
default: default:
address: 116.204.74.41:6379 address: 192.168.0.169:6379
db: 0 db: 0
consul: consul:
address: 116.204.74.41:8500 address: 192.168.0.169:8500
jaeger: jaeger:
addr: 116.204.74.41:4318 addr: 192.168.0.169:4318
task: task:
modelKey: "" # 动态请求头;会透传给 model-gateway用于覆盖/补充模型配置中的 head_msg没有可留空
waitTimeoutSeconds: 300 # /composeMessages 同步等待最终结果的最长时间(秒) waitTimeoutSeconds: 300 # /composeMessages 同步等待最终结果的最长时间(秒)
pollIntervalMillis: 500 # 同步等待期间,轮询本地任务表 / 网关状态的时间间隔(毫秒) pollIntervalMillis: 500 # 同步等待期间,轮询本地任务表 / 网关状态的时间间隔(毫秒)
@@ -47,6 +46,19 @@ session:
maxRounds: 10 # 最大轮数 maxRounds: 10 # 最大轮数
expireTime: 1800 # 过期时间30分钟 expireTime: 1800 # 过期时间30分钟
# 文件处理配置
userFiles:
zipMaxSizeMB: 10 # zip 下载最大大小MB
zipEntryMaxSizeKB: 500 # zip 内单文件最大读取大小KB
textFileMaxSizeKB: 500 # 普通文本文件最大读取大小KB
httpTimeoutSec: 8 # HTTP 请求超时(秒)
skillFiles:
httpTimeoutSec: 500 # zip 下载超时(秒)
zipMaxSizeMB: 10 # zip 最大下载大小MB
mdMaxSizeKB: 5000 # 单个 md 文件最大读取大小KB
promptsRetry:
maxRetryTimes: 3
modelPrompts: modelPrompts:
types: types:
1: | 1: |
@@ -63,10 +75,62 @@ modelPrompts:
当用户提供具体音频需求时,需要结合音色、语速、语言类型、情绪风格、背景环境、输出格式等参数完成对应处理;若涉及语音识别或音频分析,则需要尽可能保留原始语义与声音特征,并明确标注不确定内容。 当用户提供具体音频需求时,需要结合音色、语速、语言类型、情绪风格、背景环境、输出格式等参数完成对应处理;若涉及语音识别或音频分析,则需要尽可能保留原始语义与声音特征,并明确标注不确定内容。
4: | 4: |
你是一个智能向量化处理助手,专注于文本向量化、语义检索、知识索引、相似度计算与语义聚类任务,能够将文本内容转换为高维语义向量,并基于向量相似度完成语义搜索、知识召回、内容聚类、文档匹配与知识库构建等处理流程。 你是一个智能向量化处理助手,专注于文本向量化、语义检索、知识索引、相似度计算与语义聚类任务,能够将文本内容转换为高维语义向量,并基于向量相似度完成语义搜索、知识召回、内容聚类、文档匹配与知识库构建等处理流程。
在执行向量化任务时你需要以语义检索工程师、知识库架构师、AI检索系统专家的身份进行处理重点保证语义表达准确性、向量一致性、检索稳定性与召回有效性同时确保不同文本之间的语义关系能够被正确表达与计算。 在执行向量化任务时你需要以语义检索工程师、知识库架构师、AI检索系统专家的身份进行处理重点保证语义表达准确性、向量一致性、检索稳定性与召回有效性同时确保不同文本之间的语义关系能够被正确表达与计算。
当用户提供文本集合、知识内容或检索需求时,需要结合文本上下文、主题方向、检索目标、相似度要求与业务场景生成最终结果;若涉及聚类或知识库构建,则必须明确类别关系、索引结构与召回逻辑。 当用户提供文本集合、知识内容或检索需求时,需要结合文本上下文、主题方向、检索目标、相似度要求与业务场景生成最终结果;若涉及聚类或知识库构建,则必须明确类别关系、索引结构与召回逻辑。
5: | 5: |
你是一个全模态智能处理助手,能够同时理解、分析与生成文本、图片、音频、视频等多种模态内容,并支持跨模态转换、多模态融合推理、联合内容生成与复杂场景交互,能够根据不同输入形式自动匹配最合理的处理策略与输出方式。 你是一个全模态智能处理助手,能够同时理解、分析与生成文本、图片、音频、视频等多种模态内容,并支持跨模态转换、多模态融合推理、联合内容生成与复杂场景交互,能够根据不同输入形式自动匹配最合理的处理策略与输出方式。
在执行多模态任务时你需要以全链路AI内容架构师、多模态交互专家、综合内容生成系统的身份完成处理重点保证不同模态之间的语义一致性、风格统一性、信息完整性与交互连贯性避免出现跨模态语义断裂或输出不一致的问题。 在执行多模态任务时你需要以全链路AI内容架构师、多模态交互专家、综合内容生成系统的身份完成处理重点保证不同模态之间的语义一致性、风格统一性、信息完整性与交互连贯性避免出现跨模态语义断裂或输出不一致的问题。
当用户提供混合输入内容时,需要结合文本、图片、音频、视频等多种信息共同分析用户真实目标,并根据任务场景自动决定最终输出形式;若涉及跨模态生成,则必须保证生成结果能够准确映射原始语义与核心信息。 当用户提供混合输入内容时,需要结合文本、图片、音频、视频等多种信息共同分析用户真实目标,并根据任务场景自动决定最终输出形式;若涉及跨模态生成,则必须保证生成结果能够准确映射原始语义与核心信息。
buildProject:
types:
1: |
你是专业的JSON结构生成专家必须严格遵守以下全部规则。
【强制规则】
必须根据【输出结构】里面返回的JSON结构进行生成不得任何更改最终内容与输出结构返回一致
完整阅读所有文本、规则、表单内容,禁止跳读、漏读;
完整读取UserForm所有字段不得忽略任何字段
如果有skill相关内容必须完整的将内容拼接到system角色描述中
理解全部语义后再输出,禁止断章取义;
UserForm所有字段内容必须完整拼接赋值到user角色描述中不得有任何遗漏。
【优先级】
用户自然语言 > UserForm > Form
UserForm与Form同名字段时仅保留UserForm值
Form仅用于组装system角色内容。
【表单处理】
Form系统提示词、默认参数、基础配置 → 专属填充system角色
UserForm用户业务输入、文案、配图数量、比例、prompt等 → 全部解析后拼接进user角色content
自动提取UserForm中每条文案的配图数量总图片数 = 各文案配图数累加求和示例10条文案各配5张图 → 总50张parameters.n=50,用户没有相关数量必须默认1
图片尺寸为空时自动填充size=1024*1024。
【结构铁律】
严格沿用固定输出结构,不增删字段或修改层级;
messages元素必须按结构返回
禁止将role对象转为字符串、禁止嵌套错乱
输出纯净JSON无多余转义符、无换行符、无额外字符
所有括号、引号必须成对闭合保证JSON合法。
【参数赋值】
model固定沿用传入值
返回结构里面的参数,需要根据语意进行赋值,缺失补默认值;
history历史信息必须结合UserForm里的内容对用户描述部分进行补充
从UserForm提取信息整合进user描述确保数量、尺寸、文案语义无遗漏。
【输出要求】
仅输出单行纯净JSON无任何解释、备注、Markdown或多余符号
完整合UserForm全部字段语义到user描述
生成后自检JSON语法、结构、数量错误则自动重新生成。
【输出结构】
%s
【字段映射】
%s
【完整输入信息】
%s
直接输出最终JSON
2: |
你是流程路由助手你的任务是根据上下文选择一个正确的节点ID返回。
规则:
1. 只允许从下面的可选节点ID列表中选择一个返回
2. 不要返回任何多余文字、标点、解释、标题
3. 只返回纯节点ID
可选节点IDID: 节点描述):
%s
上下文内容:
%s

View File

@@ -14,7 +14,7 @@ type prompt struct{}
// Prompt 提示词配置控制器 // Prompt 提示词配置控制器
var Prompt = new(prompt) var Prompt = new(prompt)
// ComposeMessages 调用 model-gateway 异步任务并同步等待结果 // ComposeMessages 调用 model-gateway 异步任务并同步等待结果,
func (c *prompt) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (res *dto.ComposeMessagesRes, err error) { func (c *prompt) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (res *dto.ComposeMessagesRes, err error) {
return service.Prompt.ComposeMessages(ctx, req) return service.Prompt.ComposeMessages(ctx, req)
} }
@@ -58,23 +58,7 @@ func (c *prompt) GetPrompt(ctx context.Context, req *dto.GetPromptReq) (res *dto
// ListPrompt 配置列表 // ListPrompt 配置列表
func (c *prompt) ListPrompt(ctx context.Context, req *dto.ListPromptReq) (res *dto.ListPromptRes, err error) { func (c *prompt) ListPrompt(ctx context.Context, req *dto.ListPromptReq) (res *dto.ListPromptRes, err error) {
pageNum, pageSize := 1, 10 list, total, err := service.Prompt.List(ctx, int(req.Page.PageNum), int(req.Page.PageSize), req.ModelTypeId, req.ModelType)
if req != nil && req.Page != nil {
if req.Page.PageNum > 0 {
pageNum = int(req.Page.PageNum)
}
if req.Page.PageSize > 0 {
pageSize = int(req.Page.PageSize)
}
}
var modelTypeID *int
modelType := ""
if req != nil {
modelTypeID = req.ModelTypeId
modelType = req.ModelType
}
list, total, err := service.Prompt.List(ctx, pageNum, pageSize, modelTypeID, modelType)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -21,41 +21,11 @@ func (d *composeSessionDao) Insert(ctx context.Context, m *entity.ComposeSession
return r.LastInsertId() return r.LastInsertId()
} }
func (d *composeSessionDao) GetById(ctx context.Context, Id int64) (m *entity.ComposeSession, err error) { func (d *composeSessionDao) Update(ctx context.Context, m *entity.ComposeSession) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession). r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where("deleted_at IS NULL"). Where(entity.ComposeSessionCol.Id, m.Id).
Where(entity.ComposeSessionCol.Id, Id). Data(m).
One() OmitEmpty().
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *composeSessionDao) GetBySessionId(ctx context.Context, sessionId string) (m *entity.ComposeSession, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where("deleted_at IS NULL").
Where(entity.ComposeSessionCol.SessionId, sessionId).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *composeSessionDao) UpdateById(ctx context.Context, id int64, data map[string]any) (rows int64, err error) {
data[entity.ComposeSessionCol.Updater] = ""
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where(entity.ComposeSessionCol.Id, id).
Data(data).
Update() Update()
if err != nil { if err != nil {
return 0, err return 0, err
@@ -86,6 +56,52 @@ func (d *composeSessionDao) List(ctx context.Context, page, size int, where map[
return return
} }
func (d *composeSessionDao) GetListBySessionId(ctx context.Context, sessionId string, limit int) ([]*entity.ComposeSession, error) {
var sessions []*entity.ComposeSession
err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where(entity.ComposeSessionCol.SessionId, sessionId).
WhereNull(entity.ComposeSessionCol.DeletedAt).
OrderDesc(entity.ComposeSessionCol.Id).
Limit(limit).
Scan(&sessions)
if err != nil {
return nil, err
}
// 反转成时间正序
for i, j := 0, len(sessions)-1; i < j; i, j = i+1, j-1 {
sessions[i], sessions[j] = sessions[j], sessions[i]
}
return sessions, nil
}
func (d *composeSessionDao) GetById(ctx context.Context, Id int64) (m *entity.ComposeSession, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where(entity.ComposeSessionCol.Id, Id).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *composeSessionDao) GetBySessionId(ctx context.Context, sessionId string) (m *entity.ComposeSession, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where(entity.ComposeSessionCol.SessionId, sessionId).
One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&m)
return
}
func (d *composeSessionDao) DeleteBySessionId(ctx context.Context, sessionId string) (rows int64, err error) { func (d *composeSessionDao) DeleteBySessionId(ctx context.Context, sessionId string) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession). r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeSession).
Where(entity.ComposeSessionCol.SessionId, sessionId). Where(entity.ComposeSessionCol.SessionId, sessionId).

View File

@@ -23,7 +23,6 @@ func (d *composeTaskDao) Insert(ctx context.Context, m *entity.ComposeTask) (id
func (d *composeTaskDao) GetByTaskId(ctx context.Context, taskId string) (m *entity.ComposeTask, err error) { func (d *composeTaskDao) GetByTaskId(ctx context.Context, taskId string) (m *entity.ComposeTask, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeTask). r, err := gfdb.DB(ctx).Model(ctx, public.TableNameComposeTask).
Where("deleted_at IS NULL").
Where(entity.ComposeTaskCol.TaskId, taskId). Where(entity.ComposeTaskCol.TaskId, taskId).
One() One()
if err != nil { if err != nil {

View File

@@ -7,6 +7,7 @@ import (
"prompts-core/model/entity" "prompts-core/model/entity"
"gitea.com/red-future/common/db/gfdb" "gitea.com/red-future/common/db/gfdb"
"gitea.com/red-future/common/utils"
) )
var Model = &modelDao{} var Model = &modelDao{}
@@ -28,8 +29,13 @@ func (d *modelDao) GetByModelName(ctx context.Context, modelName string) (m *ent
} }
func (d *modelDao) GetByIsChatModel(ctx context.Context) (m *entity.AsynchModel, err error) { func (d *modelDao) GetByIsChatModel(ctx context.Context) (m *entity.AsynchModel, err error) {
userInfo, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel). r, err := gfdb.DB(ctx).Model(ctx, public.TableNameModel).
Where(entity.AsynchModelCol.IsChatModel, 1). Where(entity.AsynchModelCol.IsChatModel, 1).
Where(entity.AsynchModelCol.Creator, userInfo.UserName).
One() One()
if err != nil { if err != nil {
return nil, err return nil, err

8
go.mod
View File

@@ -3,17 +3,12 @@ module prompts-core
go 1.26.0 go 1.26.0
require ( require (
gitea.com/red-future/common v0.0.19 gitea.com/red-future/common v0.0.21
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0
github.com/gogf/gf/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.0
) )
require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)
require ( require (
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect
@@ -68,7 +63,6 @@ require (
github.com/r3labs/diff/v2 v2.15.1 // indirect github.com/r3labs/diff/v2 v2.15.1 // indirect
github.com/redis/go-redis/v9 v9.12.1 // indirect github.com/redis/go-redis/v9 v9.12.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/tidwall/gjson v1.19.0
github.com/tiger1103/gfast-token v1.0.10 // indirect github.com/tiger1103/gfast-token v1.0.10 // indirect
github.com/vcaesar/cedar v0.30.0 // indirect github.com/vcaesar/cedar v0.30.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect

10
go.sum
View File

@@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/red-future/common v0.0.19 h1:9/WrfCFUCeFUYwuhBYF+JOQi5F5xuOy+gVnf2ZvHZu4= gitea.com/red-future/common v0.0.21 h1:8w30HmCVmFG/hphH3ODJs1KxDEGmRpq+/PXI0pQjJKc=
gitea.com/red-future/common v0.0.19/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo= gitea.com/red-future/common v0.0.21/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -288,12 +288,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s= github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s=
github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s= github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=

View File

@@ -10,8 +10,8 @@ type Message struct {
type ComposeMessagesReq struct { type ComposeMessagesReq struct {
g.Meta `path:"/composeMessages" method:"post" tags:"提示词处理" summary:"拼接提示词" dc:"按 modelTypeId 读取 prompts_model_prompt.prompt_info 与 response_json_schemaform 作为系统表单userForm 作为用户表单,结合 userFiles 调用 model-gateway并直接返回最终 messages"` g.Meta `path:"/composeMessages" method:"post" tags:"提示词处理" summary:"拼接提示词" dc:"按 modelTypeId 读取 prompts_model_prompt.prompt_info 与 response_json_schemaform 作为系统表单userForm 作为用户表单,结合 userFiles 调用 model-gateway并直接返回最终 messages"`
ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"实际请求的网关模型名称"` ModelName string `p:"modelName" json:"modelName" v:"required#modelName不能为空" dc:"实际请求的网关模型名称"`
BuildType int `p:"buildType" json:"buildType" v:"required#buildType不能为空" dc:"构建类型"` //判断节点
SessionId string `p:"sessionId" json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"` SessionId string `p:"sessionId" json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"`
IsBuilder bool `p:"isBuild" json:"isBuild" v:"required#isBuild不能为空" dc:"是否构建"`
Cause string `p:"cause" json:"cause" v:"required-if:IsBuilder,false#原因不能为空" dc:"原因"` Cause string `p:"cause" json:"cause" v:"required-if:IsBuilder,false#原因不能为空" dc:"原因"`
Form map[string]any `p:"form" json:"form" dc:"系统表单form 下所有字段都作为系统提示词来源"` Form map[string]any `p:"form" json:"form" dc:"系统表单form 下所有字段都作为系统提示词来源"`
UserForm map[string]any `p:"userForm" json:"userForm" dc:"用户表单userForm 下所有字段都作为用户提示词来源;若与 form 含义接近则严格覆盖系统字段"` UserForm map[string]any `p:"userForm" json:"userForm" dc:"用户表单userForm 下所有字段都作为用户提示词来源;若与 form 含义接近则严格覆盖系统字段"`

View File

@@ -5,7 +5,7 @@ import "gitea.com/red-future/common/beans"
type asynchModelCol struct { type asynchModelCol struct {
beans.SQLBaseCol beans.SQLBaseCol
ModelName string ModelName string
ModelsType string ModelType string
BaseURL string BaseURL string
HttpMethod string HttpMethod string
HeadMsg string HeadMsg string
@@ -32,7 +32,7 @@ type asynchModelCol struct {
var AsynchModelCol = asynchModelCol{ var AsynchModelCol = asynchModelCol{
SQLBaseCol: beans.DefSQLBaseCol, SQLBaseCol: beans.DefSQLBaseCol,
ModelName: "model_name", ModelName: "model_name",
ModelsType: "models_type", ModelType: "model_type",
BaseURL: "base_url", BaseURL: "base_url",
HttpMethod: "http_method", HttpMethod: "http_method",
HeadMsg: "head_msg", HeadMsg: "head_msg",
@@ -60,7 +60,7 @@ var AsynchModelCol = asynchModelCol{
type AsynchModel struct { type AsynchModel struct {
beans.SQLBaseDO `orm:",inline"` beans.SQLBaseDO `orm:",inline"`
ModelName string `orm:"model_name" json:"modelName"` ModelName string `orm:"model_name" json:"modelName"`
ModelsType int `orm:"models_type" json:"modelsType"` ModelType int `orm:"model_type" json:"modelType"`
BaseURL string `orm:"base_url" json:"baseUrl"` BaseURL string `orm:"base_url" json:"baseUrl"`
HttpMethod string `orm:"http_method" json:"httpMethod"` HttpMethod string `orm:"http_method" json:"httpMethod"`
HeadMsg string `orm:"head_msg" json:"headMsg"` HeadMsg string `orm:"head_msg" json:"headMsg"`

144
service/build_prompt.go Normal file
View File

@@ -0,0 +1,144 @@
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,162 +0,0 @@
package service
import (
"context"
"encoding/json"
"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 getConfPrompt(ctx context.Context, modelType int) string {
return g.Cfg().MustGet(ctx, "modelPrompts.types."+gconv.String(modelType), "").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)
messages := []map[string]any{}
messages = append(messages, map[string]any{
"role": "system",
"content": GetSystemPrompt(req, model),
})
// 2. 历史对话 - 动态添加(新增部分)
for _, msg := range historyMessages {
messages = append(messages, map[string]any{
"role": msg.Role,
"content": msg.GetContentString(),
})
}
// 3. 当前用户问题(原来的最后一条)
messages = append(messages, map[string]any{
"role": "user",
"content": buildCombinedInput(req, getConfPrompt(ctx, model.ModelsType)),
})
// 构建请求体
return map[string]any{
"modelName": sessionModel.ModelName,
"bizName": "prompts-core",
"callbackUrl": "/prompt/callback",
"requestPayload": map[string]any{
"model": sessionModel.ModelName,
"messages": messages,
"stream": false,
},
}, nil
}
// ============================================
// 输入构建
// ============================================
func buildCombinedInput(req *dto.ComposeMessagesReq, prompt string) string {
payload := map[string]any{
//数据库提示信息
"promptInfo": prompt,
// 系统表单
"form": req.Form,
// 用户表单
"userForm": req.UserForm,
//文件url
"userFiles": req.UserFiles,
//解读文件(只支持可读类型 如xmljson,yaml
"userFilesText": fetchFileTexts(context.Background(), req.UserFiles),
}
return mustMarshal(payload)
}
// GetSystemPrompt 定义获取系统提示词的函数
func GetSystemPrompt(req *dto.ComposeMessagesReq, model *entity.AsynchModel) string {
mappingBytes, _ := json.Marshal(model.RequestMapping)
mappingStr := string(mappingBytes)
// 解析 mapping
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))
}
// ======================
// 【核心】UserForm 全部内容完整展开,让模型必须全文阅读
// 严格按你的业务定义:所有字段作为用户提示词来源
// ======================
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
`, formToJSON(req.Form), userFormFullText)
// 最终提示词(严格遵守你所有规则)
systemPrompt := fmt.Sprintf(`
你是【语义理解 + 结构对齐】的JSON生成专家必须严格遵守以下所有规则。
【强制阅读规则 · 必须100%%遵守】
1. 必须完整通读全部文本、上下文、规则、表单内容,严禁跳读、略读;
2. 未读完全部信息前,禁止输出任何内容;
3. 必须全覆盖所有约束、所有细节、所有字段后再推理;
4. 禁止断章取义,禁止遗漏任何参数;
5. 必须严格区分系统表单、用户表单。
【核心语义规则】
1. Form = 系统提示词、系统参数、默认配置
2. UserForm = 用户真实输入全文,所有字段都必须作为用户提示词来源
3. 若 UserForm 字段与 Form 含义相同 → UserForm 严格覆盖 Form
4. 必须完整使用 UserForm 所有内容,不得遗漏任何一个字段
【任务】
根据双表单内容智能填充JSON结构
1. 理解意图:图片/文案
2. 自动推导数量各2张=4一共3张=3
3. 自动补全默认值size=1024*1024
4. 严格按结构输出,不修改字段
【输出结构】
%s
【字段映射关系】
%s
【完整输入信息】
%s
【输出铁律】
1. 只输出单行JSON无任何多余字符
2. 禁止换行、禁止转义、禁止解释
3. 内容准确、无废话、不编造
4. 必须完整读取 UserForm 全部内容
请输出最终JSON
`, mappingStr, fieldDesc.String(), formInfo)
return systemPrompt
}
func formToJSON(form map[string]any) string {
if form == nil {
return "{}"
}
b, _ := json.Marshal(form)
return string(b)
}

View File

@@ -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)
}

View File

@@ -25,50 +25,41 @@ import (
func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (*dto.ComposeMessagesRes, error) { func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMessagesReq) (*dto.ComposeMessagesRes, error) {
var ( var (
epicycleId int64 epicycleId int64
taskID string
history []map[string]any
message map[string]any
err error err error
historyMessages []Message // 用来存放历史会话 taskRecord *entity.ComposeTask
) )
// 1. 如果不需要构建返回记录id // 获取模型信息
if req.IsBuilder == false { chatModel, model, err := s.GetModelMessage(ctx, req)
epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
SessionId: req.SessionId,
Remark: req.Cause,
})
return &dto.ComposeMessagesRes{
EpicycleId: epicycleId,
}, nil
}
// 2. 获取当前用户模型信息
sessionModel, err := dao.Model.GetByIsChatModel(ctx) //获取会话模型
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sessionModel == nil { // 根据构建类型进行判断处理
return nil, errors.New("当前没有对话模型,请添加") switch req.BuildType {
} //提示词构建
model, err := dao.Model.GetByModelName(ctx, req.ModelName) //获取模型信息 case 1:
if err != nil { maxRetryTimes := g.Cfg().MustGet(ctx, "promptsRetry.maxRetryTimes", 3).Int()
return nil, err //1. 获取历史会话
} history, err = Session.GetHistoryMessages(ctx, req.SessionId)
if model == nil {
return nil, fmt.Errorf("模型 %s 不存在", sessionModel.ModelName)
}
// 3 获取历史会话
historyMessages, err = Session.GetSessionHistoryForInference(ctx, req.SessionId)
if err != nil { if err != nil {
g.Log().Errorf(ctx, "获取历史会话失败: %v将不使用历史会话", err) g.Log().Errorf(ctx, "获取历史会话失败: %v将不使用历史会话", err)
historyMessages = nil // 出错就用空的,不影响主流程 history = nil // 出错就用空的,不影响主流程
} }
// 重试循环
// 4. 调用推理模型 for attempt := 0; attempt <= maxRetryTimes; attempt++ {
taskID, err := s.callInferenceModel(ctx, req, sessionModel, model, historyMessages) if attempt > 0 {
g.Log().Warningf(ctx, "[重试]第 %d/%d 次调用推理模型", attempt, maxRetryTimes)
}
// 2. 调用推理模型
taskID, err = s.callInferenceModel(ctx, req, chatModel, model, history)
if err != nil { if err != nil {
return nil, err g.Log().Errorf(ctx, "调用推理模型失败(第%d次): %v", attempt+1, err)
continue
} }
// 5. 保存相关记录 // 3. 保存记录
_, err = dao.ComposeTask.Insert(ctx, &entity.ComposeTask{ _, err = dao.ComposeTask.Insert(ctx, &entity.ComposeTask{
TaskId: taskID, TaskId: taskID,
ModelName: req.ModelName, ModelName: req.ModelName,
@@ -77,41 +68,66 @@ func (s *promptService) ComposeMessages(ctx context.Context, req *dto.ComposeMes
Status: public.ComposeStatusPending, Status: public.ComposeStatusPending,
}) })
if err != nil { if err != nil {
return nil, err g.Log().Errorf(ctx, "保存任务记录失败(第%d次): %v", attempt+1, err)
continue
} }
// 6. 等待结果 // 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) taskRecord, err := s.waitForResult(ctx, taskID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Println("构建节点前", taskRecord)
// 7. 处理返回结果 message = s.parseNodeBuild(taskRecord)
messages := s.processResult(taskRecord) fmt.Println("构建节点后", message)
default:
//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{ epicycleId, err = dao.ComposeSession.Insert(ctx, &entity.ComposeSession{
SessionId: req.SessionId, SessionId: req.SessionId,
RequestContent: messages, Remark: req.Cause,
}) })
if err != nil {
return nil, err
}
}
// 9. 更新历史会话
_, err = dao.ComposeSession.UpdateById(ctx, epicycleId, map[string]any{
entity.ComposeSessionCol.RequestContent: messages,
})
return &dto.ComposeMessagesRes{ return &dto.ComposeMessagesRes{
Messages: messages, EpicycleId: epicycleId,
}, nil
}
return &dto.ComposeMessagesRes{
Messages: message,
EpicycleId: epicycleId, EpicycleId: epicycleId,
}, nil }, nil
} }
@@ -139,7 +155,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 { if err != nil {
_, updateErr := dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{ _, updateErr := dao.ComposeTask.UpdateByTaskId(ctx, req.TaskId, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusFailed, entity.ComposeTaskCol.Status: public.ComposeStatusFailed,
@@ -195,13 +211,31 @@ func (s *promptService) GetComposeTask(ctx context.Context, taskID string) (*dto
}, nil }, nil
} }
// ============================================ // GetModelMessage 获取模型信息
// 步骤4调用推理模型 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 { if err != nil {
return "", fmt.Errorf("构建推理请求失败: %w", err) return "", fmt.Errorf("构建推理请求失败: %w", err)
} }
@@ -222,16 +256,27 @@ func (s *promptService) callInferenceModel(ctx context.Context, req *dto.Compose
// ============================================ // ============================================
// 步骤6等待结果 // 步骤6等待结果
// ============================================ // ============================================
func (s *promptService) waitForResult(ctx context.Context, taskID string) (*entity.ComposeTask, error) { func (s *promptService) waitForResult(ctx context.Context, taskID string) (*entity.ComposeTask, error) {
timeout := time.Duration(getIntConfig(ctx, "task.waitTimeoutSeconds", 30)) * time.Second timeout := time.Duration(g.Cfg().MustGet(ctx, "task.waitTimeoutSeconds", 300).Int()) * time.Second
pollInterval := time.Duration(getIntConfig(ctx, "task.pollIntervalMillis", 500)) * time.Millisecond pollInterval := time.Duration(g.Cfg().MustGet(ctx, "task.pollIntervalMillis", 500).Int()) * time.Millisecond
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
for { for {
// ===================== 修复点 1检查上下文是否取消 =====================
select {
case <-ctx.Done():
// 请求已被取消,直接返回,不继续查库
return nil, ctx.Err()
default:
}
// 1. 查数据库 // 1. 查数据库
record, err := dao.ComposeTask.GetByTaskId(ctx, taskID) record, err := dao.ComposeTask.GetByTaskId(ctx, taskID)
if err != nil { if err != nil {
// ===================== 修复点 2如果是上下文取消直接返回 =====================
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
return nil, err return nil, err
} }
if record != nil { if record != nil {
@@ -239,19 +284,22 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
case public.ComposeStatusSuccess: case public.ComposeStatusSuccess:
return record, nil return record, nil
case public.ComposeStatusFailed: 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. 查网关状态 // 2. 查网关状态
state, err := queryGatewayTaskState(ctx, taskID) state, err := queryGatewayTaskState(ctx, taskID)
if err != nil { if err != nil {
// ============ 网关不可达不终止,继续轮询 ============ // 网关不可达不终止,继续轮询
g.Log().Warningf(ctx, "[waitForResult] 查询网关失败 taskId=%s err=%v", taskID, err) g.Log().Warningf(ctx, "[waitForResult] 查询网关失败 taskId=%s err=%v", taskID, err)
} else { } else {
switch state { switch state {
case 2: // 网关成功 case 2: // 网关成功
// ============ 网关已成功,主动更新数据库 ============ // 网关已成功,主动更新数据库
if record != nil { if record != nil {
dao.ComposeTask.UpdateByTaskId(ctx, taskID, map[string]any{ dao.ComposeTask.UpdateByTaskId(ctx, taskID, map[string]any{
entity.ComposeTaskCol.Status: public.ComposeStatusSuccess, entity.ComposeTaskCol.Status: public.ComposeStatusSuccess,
@@ -272,239 +320,97 @@ func (s *promptService) waitForResult(ctx context.Context, taskID string) (*enti
if time.Now().After(deadline) { if time.Now().After(deadline) {
return nil, fmt.Errorf("等待任务回调超时(taskId=%s)", taskID) return nil, fmt.Errorf("等待任务回调超时(taskId=%s)", taskID)
} }
time.Sleep(pollInterval)
// ===================== 修复点3sleep 也要监听 ctx 取消 =====================
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
}
} }
} }
// ============================================ // parsePromptBuild 解析提示词构建结果BuildType == 1
// 步骤6处理结果 func (s *promptService) parsePromptBuild(taskRecord *entity.ComposeTask, model *entity.AsynchModel) map[string]any {
// ============================================
func (s *promptService) processResult(taskRecord *entity.ComposeTask) map[string]any {
if taskRecord == nil { if taskRecord == nil {
return nil return nil
} }
// 1. 解析 Messages 获取 content // 1. 解析 Messages
var contentStr string var mapped map[string]any
switch v := taskRecord.Messages.(type) { switch v := taskRecord.Messages.(type) {
case *gvar.Var: case *gvar.Var:
if v != nil { if v != nil {
var mapped map[string]any
json.Unmarshal([]byte(v.String()), &mapped) json.Unmarshal([]byte(v.String()), &mapped)
if c, ok := mapped["content"].(string); ok {
contentStr = c
}
} }
case string: case string:
var mapped map[string]any
json.Unmarshal([]byte(v), &mapped) json.Unmarshal([]byte(v), &mapped)
if c, ok := mapped["content"].(string); ok {
contentStr = c
}
case map[string]any: case map[string]any:
if c, ok := v["content"].(string); ok { mapped = v
contentStr = c 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. 清理并解析 // 3. 提取 content 的值
contentStr = cleanJSONString(contentStr) contentStr, ok := mapped[contentField].(string)
if !ok || contentStr == "" {
return mapped
}
// 4. 解析 content 内的 JSON
var innerData map[string]any var innerData map[string]any
json.Unmarshal([]byte(contentStr), &innerData) json.Unmarshal([]byte(contentStr), &innerData)
return innerData return innerData
} }
// ============================================ // parseNodeBuild 解析节点构建结果BuildType == 2
// 消息处理管道 func (s *promptService) parseNodeBuild(taskRecord *entity.ComposeTask) map[string]any {
// ============================================ if taskRecord == nil {
// parseStoredMessages 从数据库存储的数据中解析消息列表
// 处理多层 JSON 嵌套的情况
func parseStoredMessages(data any) []dto.Message {
if data == nil {
return nil return nil
} }
var result map[string]any
// 统一序列化为 JSON switch v := taskRecord.Messages.(type) {
jsonBytes, err := json.Marshal(data) case *gvar.Var:
if err != nil { if v != nil {
return nil json.Unmarshal([]byte(v.String()), &result)
} }
// 第一层解析:尝试直接解析为消息数组
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) {
case string: case string:
// 尝试解析 JSON 字符串 json.Unmarshal([]byte(v), &result)
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
case map[string]any: case map[string]any:
// 递归处理 map 中的每个值 result = v
result := make(map[string]any, len(v))
for k, val := range v {
result[k] = deepNormalizeContent(val)
}
return result
default: default:
return content b, _ := json.Marshal(v)
} json.Unmarshal(b, &result)
}
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
} }
return 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
}

340
service/files_handle.go Normal file
View File

@@ -0,0 +1,340 @@
package service
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
"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 内文件)
func FetchFileTexts(ctx context.Context, urls []string) map[string]string {
result := make(map[string]string)
if len(urls) == 0 {
return result
}
client := &http.Client{
Timeout: time.Duration(g.Cfg().MustGet(ctx, "userFiles.httpTimeoutSec", 8).Int()) * time.Second,
}
for _, rawURL := range urls {
url := sanitizeURL(rawURL)
if url == "" {
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 {
continue
}
if text == "" {
continue
}
text = 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"
}
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
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if !isReadableContentType(contentType) {
return "", fmt.Errorf("unreadable content-type: %s", contentType)
}
body, err := io.ReadAll(
io.LimitReader(resp.Body,
int64(g.Cfg().MustGet(ctx, "userFiles.textFileMaxSizeKB", 500).Int())*1024,
),
)
if err != nil {
return "", err
}
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)
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
}

View File

@@ -1,131 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gogf/gf/v2/os/glog"
)
// ============================================
// 文件处理
// ============================================
func fetchFileTexts(ctx context.Context, urls []string) map[string]string {
result := make(map[string]string)
if len(urls) == 0 {
return result
}
client := &http.Client{
Timeout: 8 * time.Second,
}
for _, rawURL := range urls {
url := sanitizeURL(rawURL)
if url == "" {
continue
}
text, err := fetchFileContent(ctx, client, url)
if err != nil {
glog.Warningf(ctx,
"[FetchFile] failed url=%s err=%v",
url,
err,
)
continue
}
if text == "" {
continue
}
result[url] = text
}
return result
}
func fetchFileContent(
ctx context.Context,
client *http.Client,
url string,
) (string, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
url,
nil,
)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
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)
}
// 最大读取20KB
body, err := io.ReadAll(
io.LimitReader(resp.Body, 20*1024),
)
if err != nil {
return "", err
}
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
}

View File

@@ -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)
}

View File

@@ -5,7 +5,6 @@ import (
"gitea.com/red-future/common/utils" "gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
) )
// asyncCtx 固化异步执行所需的 token/user避免请求结束后丢失仅在“同请求内起 goroutine”有用 // asyncCtx 固化异步执行所需的 token/user避免请求结束后丢失仅在“同请求内起 goroutine”有用
@@ -52,18 +51,3 @@ func forwardHeaders(ctx context.Context) map[string]string {
} }
return headers 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
}

View File

@@ -6,12 +6,9 @@ import (
"fmt" "fmt"
commonHttp "gitea.com/red-future/common/http" commonHttp "gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/os/gtime"
) )
// ============================================
// model-gateway 网关交互
// ============================================
// CreateTaskReq 创建任务请求 // CreateTaskReq 创建任务请求
type CreateTaskReq struct { type CreateTaskReq struct {
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
@@ -52,3 +49,27 @@ func queryGatewayTaskState(ctx context.Context, taskID string) (int, error) {
} }
return req.State, nil 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
}

View File

@@ -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
}

View File

@@ -4,113 +4,43 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/gogf/gf/v2/frame/g" "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 操作 ==================== // ==================== Redis 操作 ====================
// saveToRedis 保存会话数据到Redis // saveToRedis 保存会话数据到Redis
// sessionId: 会话ID作为key func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requestMessages []map[string]any, responseMessages []map[string]any) error {
// 最大10轮超出替换最早的过期时间30分钟
func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requestMessages []Message, responseMessages []Message) error {
key := fmt.Sprintf("chat:session:%s", sessionId) key := fmt.Sprintf("chat:session:%s", sessionId)
// 从配置读取,提供默认值
maxRounds := g.Cfg().MustGet(ctx, "session.maxRounds", 10).Int() maxRounds := g.Cfg().MustGet(ctx, "session.maxRounds", 10).Int()
expireSeconds := g.Cfg().MustGet(ctx, "session.expireTime", 1800).Int64() expireSeconds := g.Cfg().MustGet(ctx, "session.expireTime", 1800).Int64()
expireTime := time.Duration(expireSeconds) * time.Second expireTime := time.Duration(expireSeconds) * time.Second
// 构造存储数据 data := map[string]any{
data := SessionRoundData{ "sessionId": sessionId,
SessionId: sessionId, "requestContent": requestMessages,
RequestContent: requestMessages, "responseContent": responseMessages,
ResponseContent: responseMessages, "timestamp": time.Now().Unix(),
Timestamp: time.Now().Unix(),
} }
// 序列化
b, err := json.Marshal(data) b, err := json.Marshal(data)
if err != nil { if err != nil {
return fmt.Errorf("序列化会话数据失败: %w", err) return fmt.Errorf("序列化会话数据失败: %w", err)
} }
// 写入 RedisLPUSH 添加到最前面,新的在前)
_, err = g.Redis().Do(ctx, "LPUSH", key, string(b)) _, err = g.Redis().Do(ctx, "LPUSH", key, string(b))
if err != nil { if err != nil {
return fmt.Errorf("写入Redis失败: %w", err) return fmt.Errorf("写入Redis失败: %w", err)
} }
// 裁剪到最新10轮保留前10条
_, err = g.Redis().Do(ctx, "LTRIM", key, 0, maxRounds-1) _, err = g.Redis().Do(ctx, "LTRIM", key, 0, maxRounds-1)
if err != nil { if err != nil {
return fmt.Errorf("裁剪Redis列表失败: %w", err) return fmt.Errorf("裁剪Redis列表失败: %w", err)
} }
// 重置过期时间
_, err = g.Redis().Do(ctx, "EXPIRE", key, int64(expireTime.Seconds())) _, err = g.Redis().Do(ctx, "EXPIRE", key, int64(expireTime.Seconds()))
if err != nil { if err != nil {
return fmt.Errorf("设置过期时间失败: %w", err) return fmt.Errorf("设置过期时间失败: %w", err)
@@ -120,26 +50,22 @@ func (s *sessionService) saveToRedis(ctx context.Context, sessionId string, requ
} }
// getFromRedis 从Redis获取会话历史 // 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) key := fmt.Sprintf("chat:session:%s", sessionId)
// 获取列表中所有数据最多10条
result, err := g.Redis().Do(ctx, "LRANGE", key, 0, -1) result, err := g.Redis().Do(ctx, "LRANGE", key, 0, -1)
if err != nil { if err != nil {
return nil, fmt.Errorf("从Redis获取数据失败: %w", err) return nil, fmt.Errorf("从Redis获取数据失败: %w", err)
} }
if result == nil || result.IsNil() { if result == nil || result.IsNil() {
return []SessionRoundData{}, nil return []map[string]any{}, nil
} }
// 解析数据 var sessions []map[string]any
var sessions []SessionRoundData
// 将结果转换为字符串数组
values := result.Strings() values := result.Strings()
for _, str := range values { for _, str := range values {
var data SessionRoundData var data map[string]any
if err := json.Unmarshal([]byte(str), &data); err != nil { if err := json.Unmarshal([]byte(str), &data); err != nil {
g.Log().Warningf(ctx, "[会话] 解析Redis数据失败 err=%v", err) g.Log().Warningf(ctx, "[会话] 解析Redis数据失败 err=%v", err)
continue continue
@@ -147,7 +73,7 @@ func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]
sessions = append(sessions, data) sessions = append(sessions, data)
} }
// 反转顺序Redis存储最新在前,使用时按时间正序) // 反转Redis 最新在前时间正序)
for i, j := 0, len(sessions)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(sessions)-1; i < j; i, j = i+1, j-1 {
sessions[i], sessions[j] = sessions[j], sessions[i] sessions[i], sessions[j] = sessions[j], sessions[i]
} }
@@ -155,26 +81,33 @@ func (s *sessionService) getFromRedis(ctx context.Context, sessionId string) ([]
return sessions, nil return sessions, nil
} }
// GetSessionHistoryForInference 获取历史会话,直接返回Message数组(给推理用) // GetSessionHistoryForInference 获取历史会话,返回扁平消息数组(给推理用)
func (s *sessionService) GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]Message, error) { func (s *sessionService) GetSessionHistoryForInference(ctx context.Context, sessionId string) ([]map[string]any, error) {
// 从Redis获取历史会话数据
historyData, err := s.getFromRedis(ctx, sessionId) historyData, err := s.getFromRedis(ctx, sessionId)
if err != nil { if err != nil {
return nil, fmt.Errorf("获取历史会话失败: %w", err) return nil, fmt.Errorf("获取历史会话失败: %w", err)
} }
// 如果没有任何历史数据,返回空
if len(historyData) == 0 { if len(historyData) == 0 {
return []Message{}, nil return []map[string]any{}, nil
} }
// 把SessionRoundData转换成扁平的Message数组 var messages []map[string]any
var messages []Message
for _, round := range historyData { for _, round := range historyData {
// 先加用户的请求 if reqMsgs, ok := round["requestContent"].([]interface{}); ok {
messages = append(messages, round.RequestContent...) for _, m := range reqMsgs {
// 再加AI的回答 if msg, ok := m.(map[string]interface{}); ok {
messages = append(messages, round.ResponseContent...) 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 return messages, nil

View File

@@ -2,46 +2,50 @@ package service
import ( import (
"context" "context"
"fmt"
"prompts-core/dao" "prompts-core/dao"
"prompts-core/model/dto" "prompts-core/model/dto"
"prompts-core/model/entity" "prompts-core/model/entity"
"gitea.com/red-future/common/beans" "gitea.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
) )
var Session = &sessionService{} var Session = &sessionService{}
type sessionService struct{} type sessionService struct{}
// SessionCallback 会话回调处理
func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCallbackReq) (res *beans.ResponseEmpty, err error) { func (s *sessionService) SessionCallback(ctx context.Context, req *dto.SessionCallbackReq) (res *beans.ResponseEmpty, err error) {
// 1. 解析AI返回的文本 // 1. 解析AI返回的文本
result, err := sessionParseModelOutput(req.Text) result, err := parseOutput(req.Text)
if err != nil { if err != nil {
g.Log().Errorf(ctx, "[会话回调] 解析模型输出失败 epicycleId=%d err=%v", req.EpicycleId, err) g.Log().Errorf(ctx, "[会话回调] 解析模型输出失败 epicycleId=%d err=%v", req.EpicycleId, err)
return nil, err return nil, err
} }
// 2. 更新数据库AI回调内容 // 2. 更新数据库
_, err = dao.ComposeSession.UpdateById(ctx, req.EpicycleId, map[string]any{ result["role"] = "assistant"
entity.ComposeSessionCol.ResponseContent: result, _, err = dao.ComposeSession.Update(ctx, &entity.ComposeSession{
SQLBaseDO: beans.SQLBaseDO{Id: req.EpicycleId},
ResponseContent: result,
}) })
if err != nil { if err != nil {
g.Log().Errorf(ctx, "[会话回调] 更新数据库失败 epicycleId=%d err=%v", req.EpicycleId, err) g.Log().Errorf(ctx, "[会话回调] 更新数据库失败 epicycleId=%d err=%v", req.EpicycleId, err)
return nil, err return nil, err
} }
// 3. 获取当前轮次完整数据 // 3. 获取当前轮次完整数据
session, err := dao.ComposeSession.GetById(ctx, req.EpicycleId) session, err := dao.ComposeSession.GetById(ctx, req.EpicycleId)
if err != nil { if err != nil {
g.Log().Errorf(ctx, "[会话回调] 获取会话数据失败 epicycleId=%d err=%v", req.EpicycleId, err) g.Log().Errorf(ctx, "[会话回调] 获取会话数据失败 epicycleId=%d err=%v", req.EpicycleId, err)
return nil, err return nil, err
} }
// 4. 入 Redis(多轮记忆) // 4. 转换 json 并存入 Redis
requestMessages := s.convertToMessages(session.RequestContent) requestMessages := convertToMessages(session.RequestContent)
responseMessages := s.convertToMessages(session.ResponseContent) responseMessages := convertToMessages(session.ResponseContent)
if err = s.saveToRedis(ctx, session.SessionId, requestMessages, responseMessages); err != nil { if err = s.saveToRedis(ctx, session.SessionId, requestMessages, responseMessages); err != nil {
g.Log().Errorf(ctx, "[会话回调] Redis存储失败 sessionId=%s id=%d err=%v", g.Log().Errorf(ctx, "[会话回调] Redis存储失败 sessionId=%s id=%d err=%v",
session.SessionId, session.Id, err) 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)) session.SessionId, session.Id, len(requestMessages), len(responseMessages))
return &beans.ResponseEmpty{}, nil 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
}

View File

@@ -1,138 +1,59 @@
// utils 工具函数
package service package service
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"prompts-core/model/dto"
"sort"
"strings"
"github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv"
) )
// ============================================ // ============================================
// 工具函数 // json 相关处理
// ============================================ // ============================================
// parseOutput 解析模型输出为 JSON 格式
func getField(item map[string]any, fallback string) string { func parseOutput(text string) (map[string]any, error) {
if field := asString(item["field"]); field != "" { j, err := gjson.LoadJson([]byte(text))
return field if err != nil {
} return nil, fmt.Errorf("解析模型输出失败: %w", err)
return fallback
} }
func getLabel(item map[string]any) string { return j.Map(), nil
return asString(item["label"])
} }
func getValue(item map[string]any) any { func convertToMessages(raw any) []map[string]any {
return item["value"] if raw == nil {
} return nil
}
func cloneWithValue(item map[string]any, value any) map[string]any { j, err := gjson.LoadJson(gconv.Bytes(raw))
cloned := make(map[string]any) if err != nil {
for k, v := range item { return nil
cloned[k] = v }
} // 1. 如果有 messages
cloned["value"] = value if j.Contains("messages") {
return cloned return gconv.Maps(j.Get("messages").Array())
} }
// 2. 否则当成单条 message
func isSensitiveField(field string) bool { return []map[string]any{
f := strings.ToLower(field) j.Map(),
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
}
// 检查值
if v := asString(value); v != "" && strings.Contains(lowerText, strings.ToLower(v)) {
return true
} }
// isMessageValid 校验推理结果是否合法
func isMessageValid(message map[string]any) bool {
if message == nil {
return false return false
} }
func isEmptyValue(v any) bool {
if v == nil {
return true return true
} }
if s, ok := v.(string); ok {
return strings.TrimSpace(s) == ""
}
return false
}
func isNilOrEmpty(v any) bool { func formToJSON(form map[string]any) string {
if v == nil { if form == nil {
return true return "{}"
} }
if s, ok := v.(string); ok { b, _ := json.Marshal(form)
return strings.TrimSpace(s) == "" return string(b)
}
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), "\"")
}
}
func formatValue(v any) string {
return strings.TrimSpace(asString(v))
}
func mapToText(m map[string]any) string {
if len(m) == 0 {
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
} }
func mustMarshal(v any) string { func mustMarshal(v any) string {
@@ -142,196 +63,3 @@ func mustMarshal(v any) string {
} }
return string(b) 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
}

View File

@@ -95,8 +95,8 @@ CREATE TABLE IF NOT EXISTS prompts_compose_session (
deleted_at TIMESTAMP(6), deleted_at TIMESTAMP(6),
session_id VARCHAR(64) NOT NULL, session_id VARCHAR(64) NOT NULL,
request_content JSONB NOT NULL DEFAULT '[]'::jsonb, request_content JSONB NOT NULL DEFAULT '{}'::jsonb,
response_content JSONB NOT NULL DEFAULT '[]'::jsonb, response_content JSONB NOT NULL DEFAULT '{}'::jsonb,
remark VARCHAR(500) NOT NULL DEFAULT '' remark VARCHAR(500) NOT NULL DEFAULT ''
); );