Files
model-gateway/service/model_resolve.go
T
19904408334andClaude bc78ad2b2f feat: 模型引用解析层 resolveModelConfig + {apiKey} 占位替换
- resolveModelConfig:引用行实时取系统行配置+本人 apiKey,Id 覆盖为系统模型 id
- mergeReferenceConfig 纯函数:配置取系统行,个人字段取引用行,enabled 取 AND
- substituteAPIPlaceholder 非破坏式替换(BaseURL/头/体/业务字段/异步映射)
- 纯函数单测 4 个全过(service 包测试需临时 config.yml 兜底)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 14:36:35 +08:00

122 lines
4.1 KiB
Go

package service
import (
"context"
"fmt"
"strings"
"model-gateway/dao"
"model-gateway/model/dto"
"model-gateway/model/entity"
"github.com/gogf/gf/v2/util/gconv"
)
// ====================== 模型引用解析层 ======================
// 引用行(ref_system_model_id>0) 调用时实时取系统模型配置 + 本人 apiKey 合成可执行配置;
// 系统模型调整零同步。Id 被覆盖为系统模型 id → 计价/并发键按系统模型走(会话/任务落库仍用引用行 id)。
const apiKeyPlaceholder = "{apiKey}"
// replacePlaceholder 递归替换 map/slice/string 中的占位符(泛化自 task_end 的 replaceTaskPlaceholder,非破坏式)。
func replacePlaceholder(v any, from, to string) any {
switch val := v.(type) {
case string:
return strings.ReplaceAll(val, from, to)
case map[string]any:
m := make(map[string]any, len(val))
for k, x := range val {
m[k] = replacePlaceholder(x, from, to)
}
return m
case map[string]string:
m := make(map[string]string, len(val))
for k, x := range val {
m[k] = strings.ReplaceAll(x, from, to)
}
return m
case []any:
arr := make([]any, len(val))
for i, x := range val {
arr[i] = replacePlaceholder(x, from, to)
}
return arr
default:
return v
}
}
// copyAndReplaceStringMap 非破坏式替换 map[string]string 值(新建 map,不污染入参)
func copyAndReplaceStringMap(src map[string]string, from, to string) map[string]string {
if src == nil {
return nil
}
m := make(map[string]string, len(src))
for k, v := range src {
m[k] = strings.ReplaceAll(v, from, to)
}
return m
}
// substituteAPIPlaceholder 把输入侧配置中的 {apiKey} 替换为生效 key(非破坏式:新建 map/struct,不污染入参)。
// 覆盖 BaseURL / RequestHeadMapping / RequestBodyMapping / RequestBusinessFieldMapping /
// AsyncTaskMapping(Url/RequestHeadMapping/RequestBodyMapping)。
func substituteAPIPlaceholder(m *entity.ModelManage, key string) {
m.BaseURL = strings.ReplaceAll(m.BaseURL, apiKeyPlaceholder, key)
m.RequestHeadMapping = copyAndReplaceStringMap(m.RequestHeadMapping, apiKeyPlaceholder, key)
m.RequestBusinessFieldMapping = copyAndReplaceStringMap(m.RequestBusinessFieldMapping, apiKeyPlaceholder, key)
if v, ok := replacePlaceholder(m.RequestBodyMapping, apiKeyPlaceholder, key).(map[string]any); ok {
m.RequestBodyMapping = v
}
if a := m.AsyncTaskMapping; a != nil {
ac := *a
ac.Url = strings.ReplaceAll(a.Url, apiKeyPlaceholder, key)
ac.RequestHeadMapping = copyAndReplaceStringMap(a.RequestHeadMapping, apiKeyPlaceholder, key)
if v, ok := replacePlaceholder(a.RequestBodyMapping, apiKeyPlaceholder, key).(map[string]any); ok {
ac.RequestBodyMapping = v
}
m.AsyncTaskMapping = &ac
}
}
// mergeReferenceConfig 引用行 + 系统行 → 有效配置(纯函数,便于单测)。
// 配置字段取系统行;个人字段(apiKey/enabled/chatModel)取引用行;enabled 取 AND(系统停用=引用失效)。
func mergeReferenceConfig(stub, sys *entity.ModelManage) *entity.ModelManage {
out := *sys
out.ApiKey = stub.ApiKey
if stub.Enabled != nil {
out.Enabled = stub.Enabled
}
if stub.ChatModel != nil {
out.ChatModel = stub.ChatModel
}
if sys.Enabled != nil && !*sys.Enabled {
out.Enabled = gconv.PtrBool(false)
}
return &out
}
// resolveModelConfig 把请求命中的模型行解析为可执行配置:
// 引用行 → 系统行配置 + 引用行 apiKey(Id 覆盖为系统模型 id);非引用行 → 原配置 + 自身 apiKey 替换占位。
// 引用系统模型已删除 → 报错(调用方阻塞)。
func resolveModelConfig(ctx context.Context, m *entity.ModelManage) (*entity.ModelManage, error) {
if m == nil {
return nil, nil
}
if m.RefSystemModelId > 0 {
sys, err := dao.ModelManage.GetNotTenantId(ctx, &dto.GetModelManageReq{Id: m.RefSystemModelId})
if err != nil {
return nil, err
}
if sys == nil {
return nil, fmt.Errorf("引用的系统模型已删除")
}
out := mergeReferenceConfig(m, sys)
substituteAPIPlaceholder(out, out.ApiKey)
return out, nil
}
out := *m
substituteAPIPlaceholder(&out, out.ApiKey)
return &out, nil
}