- 语义:只把 .env **确实声明过**的键覆盖回 env 值;env 未声明的键(fallback/multimodal/maxTokens/reasoning/personaModels)原样保留;**组集合不增减**(不复活被面板删的组、不删面板新增的组);无键可改时不写盘。成功即热重载 LLM 栈。返回 {reset:[{group,keys}], skipped:[{group,reason}], path},**只回键名、不回显凭据值**。
- 护栏:必须 confirm:true(缺失/false/非法 body ⇒ 400,且**先于任何读写**);**owner-only**(与同页 POST /admin/llm、/admin/llm/test 同门 —— 这是破坏性写,门不比同族更宽);审计动作 llm.resetFromEnv(camelCase,全仓多词动作的既有约定)。失败路径明确报错(env_defaults_unreadable / models_unreadable / llm_unavailable),不假成功。
- 组合根注入 Deps.EnvPath(网关不 fopen .env):请求时按既有解析入口重读,语义 =「退回 .env 现在声明的默认值」;.env 层里的 provider 是同义反复,不计入「env 为该组声明了什么」的判定(有专门用例)。
- 前端:LLM 页「重置为 env 默认」卡片(中文二次确认 → 提交 → 按 reset/skipped 渲染 → 成功后重拉配置),标签 owner 专属(与 System.tsx 同款)。
- 契约 v0.62:api-client(权限矩阵改回 12+10+38=60、端点表 9b、重置契约小节、写操作契约、changelog、banner)与 api-reference(端点表 #70、§5.13a 详情含 curl、§8.2/§8.3/§8.4)同步;字段名三方逐字对齐。
- 主会话独立验收:隔离 worktree(R1 的 19 文件)build/vet 绿、gateway+config 三包绿、R1 用例 10 PASS、reset_from_env 残留 0;**变异亲自复现**(去掉 confirm 校验 ⇒ RequiresConfirm 红:status = 200, want 400,且响应体显示面板配置真的被改)。
- 子代理自报并已处理:测试文件触 500 行上限 ⇒ 拆成语义侧 + 护栏侧两份;门改回 owner-only 后前端标签会误导 admin ⇒ 改「owner 专属」并重建 bundle。任务书本身仍是旧口径(owner|admin / snake_case)—— 已由主会话登记修正说明,避免压缩后有人按任务书回退。
- 未做(诚实边界):不支持按组重置;不纳入 channels.json/plugins.json;不自动重启(contextWindow 的预算推导仍只在启动时算一次)。新端点需下一次 build+restart 才在 :10942 生效。
38 lines
1.5 KiB
Go
38 lines
1.5 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"reflect"
|
||
)
|
||
|
||
// LoadLLMDefaults 按既有解析入口读取 `.env` 默认层的 LLM 块。它是面板
|
||
// 「重置为 env 默认」动作(切片 R1,见 runtimeconfig.ResetFromEnv)的**对照基准**:
|
||
// 那个动作要知道「`.env` 到底声明了哪些键」,而网关自己绝不 fopen `.env`。
|
||
//
|
||
// 它复用 parseDotEnv 与同一份 `env:"…"` 标签解码器,因此优先级与 Load 逐字一致:
|
||
// 进程环境变量 > `.env` > 标签默认值(生产走 systemd/env_file 时不配 `.env`,
|
||
// 结果就是进程环境里那一份)。envPath 为空表示不看 `.env`(与 Load 的约定一致)。
|
||
//
|
||
// 为什么只解 LLM 子树、而不是整份 Load:整份 Load 会在缺 JWT_SECRET 这类必填项时
|
||
// 失败,而本读取只需要 LLM 块的声明;窗口负数在 Load 阶段就会报错(窗口没有负数
|
||
// 语义),这里保持同一口径,不另立一套判定。
|
||
func LoadLLMDefaults(envPath string) (LLMConfig, error) {
|
||
fromEnv := map[string]string{}
|
||
if envPath != "" {
|
||
m, err := parseDotEnv(envPath)
|
||
if err != nil {
|
||
return LLMConfig{}, fmt.Errorf("config: read %s: %w", envPath, err)
|
||
}
|
||
fromEnv = m
|
||
}
|
||
var llm LLMConfig
|
||
if err := decodeValue(reflect.ValueOf(&llm), fromEnv); err != nil {
|
||
return LLMConfig{}, err
|
||
}
|
||
if llm.ContextWindow < 0 {
|
||
return LLMConfig{}, fmt.Errorf("config: LLM_CONTEXT_WINDOW 不能为负(%d):0 = 未声明(按回落窗口推导)",
|
||
llm.ContextWindow)
|
||
}
|
||
return llm, nil
|
||
}
|