feat(gateway): 配置只读事实面 + 后台思考行「档位/穿透」(契约 v0.68)
- 新增 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 下限限定)
This commit is contained in:
1 parent
a057a0f61f
commit
34cef95e17
25 files changed
+1741
-14
No files matched your search
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user