fix(notify): 目的地字段留空不应被判成地址变更

Telegram 渠道从第二次保存起会永久失败,用户什么都没改也存不上:

  新建时 base_url 留空(用官方 API 地址),创建路径直接存下 base_url:""
  → 第一次保存,MergeConfig 把空串当显式清空、delete 掉该键
  → 第二次保存,前端照常提交 base_url:"",而 stored 里已经没这个键
  → sameConfigValue("", nil) 比较 `""` 与 `null`,判为不等 → changed=[base_url]
  → bot_token 是掩码回显值 → 400「目标地址已变更,请同时重新填写凭据字段」

此后每次保存都失败,除非重新粘贴一遍 Bot Token,而错误提示还把人
往「凭据有问题」上引。

根因是 MergeConfig 与 sameConfigValue 对「空」的口径不一致:前者认为
空串就是「该删掉这个键」,后者认为 "" 与键不存在是两个不同的值。
现在比较前先归一化,口径与 MergeConfig 的清空判定对齐。

这是上一轮修「改目标地址、保留凭据」那条凭据外发路径时引入的回归——
守卫本身是对的,只是把一个没变的空字段误判成了地址变更。配套用例覆盖了
两个方向的真实变更(空→自建反代、自建反代→空)仍必须要求重填 Token,
确认防护没有被削弱。
This commit is contained in:
Autumn-27 committed 2026-10-01 06:43:48 -04:00
1 parent 93c1d39df3
commit 77834f0080
2 files changed
+95 -1

No files matched your search

+23 -1
View File
@@ -183,8 +183,19 @@ func rejectMaskedInContainers(incoming map[string]any, secretKeys []string) erro
// sameConfigValue 比较两个配置值是否等价。用 JSON 序列化比较是为了顺带处理
// 类型差异——前端提交的端口是 number,而库里读回来的是 float64,直接 == 会误判。
//
// 「空」必须先归一化再比较:空串与「键不存在」在这个配置模型里是同一个状态,
// 因为 MergeConfig 把空串当显式清空、直接 delete 掉该键。不归一化的话,一个
// 始终留空的可选目的地字段(Telegram 的 base_url 是唯一这样的字段:留空即用
// 官方地址)会走成这条路径——
//
// 新建时存下 base_url:"" → 第一次保存被 MergeConfig 删键
// → 第二次保存时 incoming 是 ""、stored 缺键,被判成「地址变了」
// → 凭据是掩码值 → 400「目标地址已变更,请同时重新填写凭据字段」
//
// 此后每次保存都失败,除非用户重新粘贴一遍 Bot Token,而他什么都没改。
func sameConfigValue(a, b any) bool {
if a == nil && b == nil {
if isBlankConfigValue(a) && isBlankConfigValue(b) {
return true
}
ra, errA := json.Marshal(a)
@@ -195,6 +206,17 @@ func sameConfigValue(a, b any) bool {
return string(ra) == string(rb)
}
// isBlankConfigValue 判定一个配置值是否为「空」。
// 口径必须与 MergeConfig 的清空判定一致(strings.TrimSpace(s) == ""),
// 否则会出现「MergeConfig 认为该删、sameConfigValue 认为有值」的夹缝。
func isBlankConfigValue(v any) bool {
if v == nil {
return true
}
s, ok := v.(string)
return ok && strings.TrimSpace(s) == ""
}
// MergeConfig 把 incoming 合并到 stored 之上,用于更新渠道配置。
//
// 规则: