- 新增 GET /api/v1/admin/config-facts:记忆整理 7 键 + 安静时段 2 键的**进程生效值** + 声明默认值(反射读 default 标签,不读 .env) - 面板:记忆整理卡 + 安静时段卡(Miuix 只读),文案按新语义「只服务主动搭话,不静默回复」 - 后台思考行加 urgency(**只能是下限**,存档不可分 high/critical)+ quietHoursBypassed(与既有 bypassed 是两件事) - 红线实测:往响应里塞 JWT ⇒ TestAdminConfigFacts_NeverLeaksSecretValues 打红(连断言消息都把整个响应打出来了) - 前端:configFacts 24 checks / ConfigFactsCards 29 / BackthinkUrgencyTags 19 全过 - 契约 v0.68(纯增量:B12 新端点 + B8 两字段 + §4.10 形状与语义 + §7.4-17 下限限定)
108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
package gateway
|
||
|
||
import (
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件从 `internal/config` 的 env 标签里读出各键的**声明默认值**,供只读配置
|
||
// 事实面(admin_config_facts.go)如实展示「默认 / 已改」。
|
||
//
|
||
// 为什么用标签而不是另一份默认值清单:`default:"…"` 标签本身就是配置层的唯一
|
||
// 真源(`config.lookupRaw` 在进程环境与 `.env` 都没给值时正是用它)。网关再抄
|
||
// 一份等于制造第二份事实,两边迟早对不上;而反射读标签既不需要 fopen `.env`
|
||
// (主人红线:键名单读 `.env.example`,**绝不读 `.env`**),也不需要新增配置包
|
||
// 导出面。
|
||
|
||
// envTagName 与 config 包的内部约定一致:env 标签可带逗号分隔的选项,
|
||
// 变量名是逗号前的部分。
|
||
func envTagName(tag string) string {
|
||
name, _, _ := strings.Cut(tag, ",")
|
||
return strings.TrimSpace(name)
|
||
}
|
||
|
||
// declaredEnvDefaults 遍历配置结构体,返回 `env:"KEY"` 字段的声明默认值
|
||
// (`default:"…"` 标签),按字段类型归一化成字符串,键与取值口径与
|
||
// `buildConfigFacts` 对齐:
|
||
//
|
||
// - bool → "true"/"false"(标签里的写法大小写、多余空白都被吸收);
|
||
// - int → 十进制;
|
||
// - float → 归一化小数(`0.80` ⇒ `0.8`);
|
||
// - string→ 原样(去首尾空白)。
|
||
//
|
||
// 没有 `default` 标签的字段不进表(其「未设置」语义由各自的 Set 标志单独表达,
|
||
// 不是本事实面的内容);无法归一化的值原样保留,宁可显示原始文本也不猜。
|
||
func declaredEnvDefaults(root any) map[string]string {
|
||
out := make(map[string]string)
|
||
if root == nil {
|
||
return out
|
||
}
|
||
collectEnvDefaults(reflect.ValueOf(root), out, 0)
|
||
return out
|
||
}
|
||
|
||
// maxConfigDepth 是递归下探嵌套配置组的深度上限(防御性:配置是静态结构,
|
||
// 但它必须是常量而不是魔法数字)。
|
||
const maxConfigDepth = 8
|
||
|
||
// collectEnvDefaults 递归收集默认值。非结构体、指针、不可寻址的字段一律跳过:
|
||
// 配置结构体是静态的平面嵌套,够不到的东西不猜。
|
||
func collectEnvDefaults(v reflect.Value, out map[string]string, depth int) {
|
||
if depth > maxConfigDepth {
|
||
return
|
||
}
|
||
if v.Kind() == reflect.Pointer {
|
||
if v.IsNil() {
|
||
return
|
||
}
|
||
v = v.Elem()
|
||
}
|
||
if v.Kind() != reflect.Struct {
|
||
return
|
||
}
|
||
t := v.Type()
|
||
for i := 0; i < t.NumField(); i++ {
|
||
field := t.Field(i)
|
||
if !field.IsExported() {
|
||
continue
|
||
}
|
||
fv := v.Field(i)
|
||
tag, hasEnv := field.Tag.Lookup("env")
|
||
if !hasEnv {
|
||
// 没有 env 标签的字段是嵌套配置组。
|
||
collectEnvDefaults(fv, out, depth+1)
|
||
continue
|
||
}
|
||
name := envTagName(tag)
|
||
if name == "" {
|
||
continue
|
||
}
|
||
def, hasDefault := field.Tag.Lookup("default")
|
||
if !hasDefault {
|
||
continue
|
||
}
|
||
out[name] = normalizeDefault(fv.Kind(), def)
|
||
}
|
||
}
|
||
|
||
// normalizeDefault 把标签里的默认值按字段类型归一化,形状与事实面的生效值一致。
|
||
func normalizeDefault(kind reflect.Kind, raw string) string {
|
||
text := strings.TrimSpace(raw)
|
||
switch kind {
|
||
case reflect.Bool:
|
||
if b, err := strconv.ParseBool(strings.ToLower(text)); err == nil {
|
||
return strconv.FormatBool(b)
|
||
}
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
if n, err := strconv.ParseInt(text, 10, 64); err == nil {
|
||
return strconv.FormatInt(n, 10)
|
||
}
|
||
case reflect.Float32, reflect.Float64:
|
||
if f, err := strconv.ParseFloat(text, 64); err == nil {
|
||
return formatRatio(f)
|
||
}
|
||
}
|
||
return text
|
||
}
|