392 lines
9.4 KiB
Go
392 lines
9.4 KiB
Go
package util
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"model-gateway/model/entity"
|
||
"model-gateway/service/gateway"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/gogf/gf/v2/encoding/gjson"
|
||
"github.com/gogf/gf/v2/util/gconv"
|
||
)
|
||
|
||
// ======================== 表单校验与构建 ========================
|
||
|
||
// ValidateAndParseForm 校验表单并转为嵌套 map
|
||
func ValidateAndParseForm(forms []entity.Form) (map[string]any, error) {
|
||
result := gjson.New("{}")
|
||
for _, form := range forms {
|
||
if form.Key == "" {
|
||
continue
|
||
}
|
||
if form.Required && (form.Value == nil || gconv.String(form.Value) == "") {
|
||
return nil, fmt.Errorf("字段 %s 为必填", form.Label)
|
||
}
|
||
if form.Value == nil {
|
||
continue
|
||
}
|
||
|
||
val, err := validateAndConvert(form)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
_ = result.Set(form.Key, val)
|
||
}
|
||
return result.Map(), nil
|
||
}
|
||
|
||
// validateAndConvert 验证表单字段并转为标准格式
|
||
func validateAndConvert(form entity.Form) (any, error) {
|
||
val := form.Value
|
||
fc := form.FieldConstraint
|
||
|
||
switch form.Type {
|
||
case "string":
|
||
s := gconv.String(val)
|
||
if fc.MaxLength > 0 && len(s) > fc.MaxLength {
|
||
return nil, fmt.Errorf("字段 %s 超过最大长度 %d", form.Label, fc.MaxLength)
|
||
}
|
||
if fc.MinLength > 0 && len(s) < fc.MinLength {
|
||
return nil, fmt.Errorf("字段 %s 不足最小长度 %d", form.Label, fc.MinLength)
|
||
}
|
||
return s, nil
|
||
|
||
case "number":
|
||
f := gconv.Float64(val)
|
||
if fc.Min != nil && f < gconv.Float64(fc.Min) {
|
||
return nil, fmt.Errorf("字段 %s 不能小于 %v", form.Label, fc.Min)
|
||
}
|
||
if fc.Max != nil && f > gconv.Float64(fc.Max) {
|
||
return nil, fmt.Errorf("字段 %s 不能大于 %v", form.Label, fc.Max)
|
||
}
|
||
switch fc.NumberType {
|
||
case "float", "positiveFloat", "negativeFloat":
|
||
return f, nil
|
||
default:
|
||
return int(f), nil
|
||
}
|
||
|
||
case "select", "radio":
|
||
v, ok := val.(map[string]any)
|
||
if !ok {
|
||
return nil, fmt.Errorf("字段 %s 格式错误", form.Label)
|
||
}
|
||
return v, nil
|
||
|
||
case "upload":
|
||
var urls []string
|
||
switch v := val.(type) {
|
||
case []any:
|
||
for _, u := range v {
|
||
urls = append(urls, gconv.String(u))
|
||
}
|
||
case []string:
|
||
urls = v
|
||
case string:
|
||
if v != "" {
|
||
urls = []string{v}
|
||
}
|
||
}
|
||
if fc.MaxCount > 0 && len(urls) > fc.MaxCount {
|
||
return nil, fmt.Errorf("字段 %s 上传数量超过上限 %d", form.Label, fc.MaxCount)
|
||
}
|
||
if fc.Accept != "" && len(urls) > 0 {
|
||
allowed := strings.Split(fc.Accept, ",")
|
||
for _, fileUrl := range urls {
|
||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(fileUrl), "."))
|
||
if !containsExt(allowed, ext) {
|
||
return nil, fmt.Errorf("字段 %s 不支持的文件格式: %s", form.Label, ext)
|
||
}
|
||
}
|
||
}
|
||
return urls, nil
|
||
|
||
default:
|
||
return gconv.String(val), nil
|
||
}
|
||
}
|
||
|
||
func containsExt(allowed []string, ext string) bool {
|
||
for _, a := range allowed {
|
||
if strings.TrimSpace(a) == ext {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// BuildTemplateFromForm 从 Form 构建模板
|
||
func BuildTemplateFromForm(forms []entity.Form) map[string]any {
|
||
jsonObj := gjson.New("{}")
|
||
for _, form := range forms {
|
||
if form.Key == "" {
|
||
continue
|
||
}
|
||
if form.DefaultValue != nil {
|
||
_ = jsonObj.Set(form.Key, form.DefaultValue)
|
||
} else {
|
||
switch form.Type {
|
||
case "number":
|
||
_ = jsonObj.Set(form.Key, 0)
|
||
case "boolean":
|
||
_ = jsonObj.Set(form.Key, false)
|
||
default:
|
||
_ = jsonObj.Set(form.Key, "")
|
||
}
|
||
}
|
||
}
|
||
return jsonObj.Map()
|
||
}
|
||
|
||
// ======================== 提示词拼接 ========================
|
||
|
||
// GetFormByRole 根据 role 获取单个 Form
|
||
func GetFormByRole(forms []entity.Form, role string) (entity.Form, bool) {
|
||
for _, f := range forms {
|
||
if f.Role == role {
|
||
return f, true
|
||
}
|
||
}
|
||
return entity.Form{}, false
|
||
}
|
||
|
||
// GetRoleContentPath 从 Form 数组中提取指定 role 的 Key 路径
|
||
func GetRoleContentPath(forms []entity.Form, role string) string {
|
||
for _, form := range forms {
|
||
if form.Role == role {
|
||
return form.Key
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// MergePrompt 将多个提示词拼接到指定路径的 content 中
|
||
func MergePrompt(messages map[string]any, contentPath string, prompts ...string) map[string]any {
|
||
var parts []string
|
||
for _, p := range prompts {
|
||
if p != "" {
|
||
parts = append(parts, p)
|
||
}
|
||
}
|
||
if len(parts) == 0 {
|
||
return messages
|
||
}
|
||
|
||
appendContent := strings.Join(parts, "\n")
|
||
msgJson := gjson.New(messages)
|
||
|
||
existing := msgJson.Get(contentPath).String()
|
||
if existing != "" {
|
||
appendContent = existing + "\n" + appendContent
|
||
}
|
||
_ = msgJson.Set(contentPath, appendContent)
|
||
|
||
return msgJson.Map()
|
||
}
|
||
|
||
// ExtractUserContent 将 messages 转为自然语言文本
|
||
func ExtractUserContent(messages map[string]any) string {
|
||
return formatMapToText(messages, "")
|
||
}
|
||
|
||
func formatMapToText(v any, prefix string) string {
|
||
var b strings.Builder
|
||
switch val := v.(type) {
|
||
case map[string]any:
|
||
for k, vv := range val {
|
||
switch vv.(type) {
|
||
case map[string]any, []any:
|
||
b.WriteString(formatMapToText(vv, prefix))
|
||
default:
|
||
b.WriteString(fmt.Sprintf("%s:%v,", k, vv))
|
||
}
|
||
}
|
||
case []any:
|
||
for _, elem := range val {
|
||
b.WriteString(formatMapToText(elem, ""))
|
||
}
|
||
case string:
|
||
b.WriteString(fmt.Sprintf("%s,", val))
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// InjectHistory 拼接历史提示词
|
||
func InjectHistory(messages map[string]any, history []gateway.SessionHistoryItem, mergeOrder []string) map[string]any {
|
||
msgs, _ := messages["messages"].([]any)
|
||
|
||
// 按 role 分组
|
||
grouped := make(map[string][]any)
|
||
for _, m := range msgs {
|
||
if msg, ok := m.(map[string]any); ok {
|
||
role := gconv.String(msg["role"])
|
||
grouped[role] = append(grouped[role], msg)
|
||
}
|
||
}
|
||
|
||
result := make([]any, 0, len(msgs)+len(history))
|
||
for _, part := range mergeOrder {
|
||
switch part {
|
||
case "history":
|
||
for _, h := range history {
|
||
result = append(result, map[string]any{
|
||
"role": h.Role,
|
||
"content": h.Content,
|
||
})
|
||
}
|
||
default:
|
||
if msgs, ok := grouped[part]; ok {
|
||
result = append(result, msgs...)
|
||
}
|
||
}
|
||
}
|
||
|
||
messages["messages"] = result
|
||
return messages
|
||
}
|
||
|
||
// SplitByAttachment 根据附件数量拆分多轮,未超出返回空
|
||
func SplitByAttachment(messages map[string]any, forms []entity.Form) []map[string]any {
|
||
// 1) 获取约束
|
||
maxVideo := getMaxCount(forms, "video")
|
||
maxImage := getMaxCount(forms, "image")
|
||
maxAudio := getMaxCount(forms, "audio")
|
||
|
||
// 2) 提取 system 消息和 user content
|
||
msgs := gjson.New(messages).Get("messages").Array()
|
||
var systemMsg map[string]any
|
||
var userContent []any
|
||
|
||
for _, m := range msgs {
|
||
msg := m.(map[string]any)
|
||
switch msg["role"] {
|
||
case "system":
|
||
systemMsg = msg
|
||
case "user":
|
||
if c, ok := msg["content"].([]any); ok {
|
||
userContent = c
|
||
}
|
||
}
|
||
}
|
||
if systemMsg == nil || len(userContent) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 3) 分离 text 和各类附件
|
||
var texts []any
|
||
var videos, images, audios []any
|
||
|
||
for _, c := range userContent {
|
||
item := c.(map[string]any)
|
||
switch item["type"] {
|
||
case "video_url":
|
||
videos = append(videos, item)
|
||
case "image_url":
|
||
images = append(images, item)
|
||
case "input_audio":
|
||
audios = append(audios, item)
|
||
default:
|
||
texts = append(texts, item)
|
||
}
|
||
}
|
||
|
||
// 4) 判断是否需要拆分
|
||
if len(videos) <= maxVideo && len(images) <= maxImage && len(audios) <= maxAudio {
|
||
return nil
|
||
}
|
||
|
||
// 5) 按约束分片
|
||
videoChunks := chunk(videos, maxVideo)
|
||
imageChunks := chunk(images, maxImage)
|
||
audioChunks := chunk(audios, maxAudio)
|
||
|
||
maxRounds := len(videoChunks)
|
||
if len(imageChunks) > maxRounds {
|
||
maxRounds = len(imageChunks)
|
||
}
|
||
if len(audioChunks) > maxRounds {
|
||
maxRounds = len(audioChunks)
|
||
}
|
||
|
||
// 6) 构建每轮 messages
|
||
var rounds []map[string]any
|
||
for i := 0; i < maxRounds; i++ {
|
||
var newContent []any
|
||
newContent = append(newContent, texts...)
|
||
if i < len(videoChunks) {
|
||
newContent = append(newContent, videoChunks[i]...)
|
||
} else {
|
||
newContent = append(newContent, videos...) // 未超出,每轮都带
|
||
}
|
||
if i < len(imageChunks) {
|
||
newContent = append(newContent, imageChunks[i]...)
|
||
} else {
|
||
newContent = append(newContent, images...)
|
||
}
|
||
if i < len(audioChunks) {
|
||
newContent = append(newContent, audioChunks[i]...)
|
||
} else {
|
||
newContent = append(newContent, audios...)
|
||
}
|
||
|
||
rounds = append(rounds, map[string]any{
|
||
"model": gjson.New(messages).Get("model").Val(),
|
||
"max_tokens": gjson.New(messages).Get("max_tokens").Val(),
|
||
"messages": []any{
|
||
systemMsg,
|
||
map[string]any{"role": "user", "content": newContent},
|
||
},
|
||
})
|
||
}
|
||
|
||
return rounds
|
||
}
|
||
|
||
// getMaxCount 从表单获取指定 role 的最大数量
|
||
func getMaxCount(forms []entity.Form, role string) int {
|
||
for _, f := range forms {
|
||
if f.Role == role {
|
||
return f.FieldConstraint.MaxCount
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// chunk 将数组按 size 分片
|
||
func chunk(items []any, size int) [][]any {
|
||
if size <= 0 {
|
||
return nil
|
||
}
|
||
var chunks [][]any
|
||
for i := 0; i < len(items); i += size {
|
||
end := i + size
|
||
if end > len(items) {
|
||
end = len(items)
|
||
}
|
||
chunks = append(chunks, items[i:end])
|
||
}
|
||
return chunks
|
||
}
|
||
|
||
// ======================== 请求模板 ========================
|
||
|
||
// BuildRequestBody 替换模板占位符构建请求体
|
||
func BuildRequestBody(template map[string]any, modelName, systemPrompt, userContent string) map[string]any {
|
||
escapedSystem := escapeJSON(systemPrompt)
|
||
escapedUser := escapeJSON(userContent)
|
||
|
||
str := gjson.New(template).MustToJsonString()
|
||
str = strings.ReplaceAll(str, `"{{model}}"`, `"`+modelName+`"`)
|
||
str = strings.ReplaceAll(str, `"{{system}}"`, escapedSystem)
|
||
str = strings.ReplaceAll(str, `"{{user}}"`, escapedUser)
|
||
|
||
return gjson.New(str).Map()
|
||
}
|
||
|
||
func escapeJSON(s string) string {
|
||
b, _ := json.Marshal(s)
|
||
return string(b)
|
||
}
|