- 面板读写面与 maxTokens 逐处对齐:读 DTO 增 contextWindow(omitempty)、写请求 ContextWindow *int(缺失=保留、显式 0=未声明同义、负数 400)、回写 models.json;负数红线仍由 runtimeconfig.Group.Validate 唯一裁决,网关守卫只做「更早失败 + 面板可读字段名」。前端:类型 + 表单字段(提交前 /^\d+$/ 校验)+ 分组行角标 + Notice + 目录候选用早已解析却没人用的 contextLimit 预填。文案写明「重启后生效」(推导只在进程启动时算一次)。 - 顺手修既有红线违反:ProviderFields 87→76(抽 ApiKeyAndBudgetField)、LlmProviderForm 113→74(buildRequest 提为模块级纯函数)——本次碰过的文件里已无 >80 的函数,行为/文案逐字未变。 - DocumentRef 的 chars/truncated 注释从「抽取侧」改为「本回合注入侧」(全文长度只在 document-read 的 totalChars、extractTruncated 专指撞 8 MiB 安全上限);机械校验为空 ⇒ 代码零改动。 - 主会话独立验收:隔离 worktree Go 侧 build/vet + gateway/history 两包绿;主树 npx tsc --noEmit exit 0、npm run build 成功并 copy 到 internal/webui/dist;注释机械校验为空;变异由子代理复现(写路径不认该键 ⇒ 3 红;去掉网关负数守卫 ⇒ 400 变 500)。
69 lines
2.7 KiB
Go
69 lines
2.7 KiB
Go
package gateway
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||
)
|
||
|
||
// LLM provider 分组的线上投影:读(GET /admin/llm)与写(POST /admin/llm 的
|
||
// 响应)**必须同一套映射**。面板是「读回来 → 改一处 → 整组提交」,两个方向
|
||
// 一旦不一致(例如读侧漏了 reasoning),面板保存一次就会把那把键抹掉。
|
||
|
||
// adminLlmProviderDTO 把存储分组映射成线上的 AdminLlmProvider。API key 绝不
|
||
// 进入线上结构。reasoning 无条件下发(kind 可能为空串 = 未声明形状),
|
||
// persona 路由表按原值深拷贝,使调用方拿到的结构不会被后续改动影响。
|
||
func adminLlmProviderDTO(g runtimeconfig.Group, name string, enabled bool) AdminLlmProvider {
|
||
return AdminLlmProvider{
|
||
ID: g.ID,
|
||
Name: name,
|
||
Provider: g.Provider,
|
||
BaseURL: g.BaseURL,
|
||
Model: g.Model,
|
||
Multimodal: g.Multimodal,
|
||
Enabled: enabled,
|
||
Fallback: g.Fallback,
|
||
MaxTokens: g.MaxTokens,
|
||
ContextWindow: g.ContextWindow,
|
||
Reasoning: &AdminLlmReasoning{Kind: strings.TrimSpace(g.Reasoning.Kind)},
|
||
PersonaModels: cloneStringMap(g.PersonaModels),
|
||
PersonaProviders: cloneStringMap(g.PersonaProviders),
|
||
}
|
||
}
|
||
|
||
// cloneStringMap 浅拷贝一张 persona 路由表。nil 保持 nil,因此 omitempty 仍会
|
||
// 省略该键——「该组没有 per-persona 绑定」与「绑定表为空对象」在线上是同一种
|
||
// 状态(都表示没有路由),不需要两套表示。
|
||
func cloneStringMap(m map[string]string) map[string]string {
|
||
if m == nil {
|
||
return nil
|
||
}
|
||
out := make(map[string]string, len(m))
|
||
for k, v := range m {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// normalizePersonaMap 规整面板提交的 persona 路由表:键与值去除首尾空白。
|
||
// 只做规整,绝不丢弃或改写成别的值——空键/空值/未知协议名一律留给
|
||
// runtimeconfig.Group.Validate 报错(写端点据此回 400),因此非法输入
|
||
// 永远不会被静默吸收。规整的理由是路由查找用的是 trim 后的 persona id,
|
||
// 带空白的键会变成永远匹配不上的静默死配置;两个键在 trim 后撞到一起
|
||
// (" cyrene " 与 "cyrene")则直接报错,而不是悄悄让其中一个消失。
|
||
func normalizePersonaMap(field string, m map[string]string) (map[string]string, error) {
|
||
if m == nil {
|
||
return nil, nil
|
||
}
|
||
out := make(map[string]string, len(m))
|
||
for k, v := range m {
|
||
key := strings.TrimSpace(k)
|
||
if _, dup := out[key]; dup {
|
||
return nil, fmt.Errorf("%s: persona id %q 去除首尾空白后与另一条目重复", field, key)
|
||
}
|
||
out[key] = strings.TrimSpace(v)
|
||
}
|
||
return out, nil
|
||
}
|