fix(gateway): directMsgPerMin 的写入路径 5 处透传(关掉「面板保存把 -1 静默重置为 0」)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 缺口:后端解析与面板都已就绪,但 gateway 没让该字段通过 ⇒ 面板保存一次渠道配置就把主人手配的「关闭流控(-1)」重置为 0=保护性默认 6 条/分
- 5 处:AdminChannelAccount DTO / channelConfigRequest / config.ChannelAccount 拷贝 / adminChannelAccountOf 映射 / 微信扫码重建账号
- 语义零改动(只透传);变异实测:删拷贝 ⇒ channels.json 变 0(want -1)、删微信重建拷贝 ⇒ 同红、删 GET 映射 ⇒ 回显缺失
- 顺带登记一条负载诱发 flake(capability/plugin 的 TestManagerInstall_ConnectFailureDoesNotPersist,靠「端口被拒」断言,满载时端口被抢)⇒ 非本切片引入,已进 00-索引 §四
This commit is contained in:
AskaEth committed 2026-10-05 14:01:52 +08:00
1 parent 321cc3b763
commit 7aaf7731dc
3 files changed
+208 -30

No files matched your search

@@ -41,6 +41,13 @@ type AdminChannelAccount struct {
GroupMsgPerMin int `json:"groupMsgPerMin,omitempty"`
MsgPerMin int `json:"msgPerMin,omitempty"`
MaxMessages int `json:"maxMessages,omitempty"`
// DirectMsgPerMin 是**非主人私聊(陌生人)每分钟每发送者上限**
// (design/12 §6 策略三;主人 2026-10-05 裁决③与裁决 13)。它是账号
// **顶层**字段(与 GroupMsgPerMin 同一条写路径,**不在 config 里**),
// 语义与 `channelbridge.directLimit` 逐字同口径:0 或未设置为保护性
// 默认 6 条/分、正值固定为该值、**负值 = 关闭该限制**。面板必须能把它
// 原样回显,否则「已关闭」会被静默显示成「未设置」。
DirectMsgPerMin int `json:"directMsgPerMin,omitempty"`
// PersonaID 是该账号应答所用的角色(design/22 §2 方案 b),账号
// 未绑定任何角色时省略。它是人格 id,不是秘密。
PersonaID string `json:"personaId,omitempty"`
@@ -101,6 +108,12 @@ type channelConfigRequest struct {
GroupMsgPerMin int `json:"groupMsgPerMin"`
MsgPerMin int `json:"msgPerMin"`
MaxMessages int `json:"maxMessages"`
// DirectMsgPerMin 是账号顶层的陌生人私聊每分钟每发送者上限,与
// GroupMsgPerMin 同一口径的**值语义**(对非 config 字段是整值替换):
// 缺失或 0 → 保护性默认 6 条/分,正值 → 该值,**负值 = 关闭该限制**。
// 它必须原样落到 config.ChannelAccount,绝不能被吞成 0 —— 否则主人显式
// 关掉的限制会被一次面板保存静默重置回默认 6(见 adminSaveChannelConfig)。
DirectMsgPerMin int `json:"directMsgPerMin"`
// PersonaID 是该账号应答所用的角色(design/22 §2 方案 b)。它是
// 指针,因为线上契约是三态的,而普通 string 无法
// 区分键缺失与显式 "":缺失保留已存绑定,
@@ -160,17 +173,20 @@ func (s *Server) adminSaveChannelConfig(w http.ResponseWriter, r *http.Request)
accounts := s.channels.Load()
// patch 语义:未在 cfg 里出现的 config 键保留盘上已存值;cfg 里显式为
// null/"" 的键被清除。非 config 字段(黑白名单 / 限流 / personaId)仍是整值替换。
// DirectMsgPerMin 必须**逐字**落到账号上:它就是面板「关闭流控(-1)」的
// 保存路径,吞成 0 等于把主人显式关掉的限制静默重置回保护性默认 6 条/分。
updated := runtimeconfig.PatchChannel(accounts, platform, config.ChannelAccount{
ID: strings.TrimSpace(req.ID),
Config: cfg,
AllowGroups: req.AllowGroups,
DenyGroups: req.DenyGroups,
AllowUsers: req.AllowUsers,
DenyUsers: req.DenyUsers,
GroupMsgPerMin: req.GroupMsgPerMin,
MsgPerMin: req.MsgPerMin,
MaxMessages: req.MaxMessages,
PersonaID: channelPersonaBinding(req.PersonaID, existingChannelAccount(accounts[platform], req.ID)),
ID: strings.TrimSpace(req.ID),
Config: cfg,
AllowGroups: req.AllowGroups,
DenyGroups: req.DenyGroups,
AllowUsers: req.AllowUsers,
DenyUsers: req.DenyUsers,
GroupMsgPerMin: req.GroupMsgPerMin,
MsgPerMin: req.MsgPerMin,
MaxMessages: req.MaxMessages,
DirectMsgPerMin: req.DirectMsgPerMin,
PersonaID: channelPersonaBinding(req.PersonaID, existingChannelAccount(accounts[platform], req.ID)),
})
if err := runtimeconfig.SaveChannels(s.channels.Path(), updated); err != nil {
s.auditClaims(r, "channel.save", "error")
@@ -413,16 +429,17 @@ func savedAccountID(list []config.ChannelAccount, supplied string) string {
// adminChannelAccountOf 把一个(已脱敏的)账号映射为其管理侧线上结构。
func adminChannelAccountOf(acct config.ChannelAccount) AdminChannelAccount {
return AdminChannelAccount{
ID: acct.ID,
Config: acct.Config,
AllowGroups: acct.AllowGroups,
DenyGroups: acct.DenyGroups,
AllowUsers: acct.AllowUsers,
DenyUsers: acct.DenyUsers,
GroupMsgPerMin: acct.GroupMsgPerMin,
MsgPerMin: acct.MsgPerMin,
MaxMessages: acct.MaxMessages,
PersonaID: acct.PersonaID,
ID: acct.ID,
Config: acct.Config,
AllowGroups: acct.AllowGroups,
DenyGroups: acct.DenyGroups,
AllowUsers: acct.AllowUsers,
DenyUsers: acct.DenyUsers,
GroupMsgPerMin: acct.GroupMsgPerMin,
MsgPerMin: acct.MsgPerMin,
MaxMessages: acct.MaxMessages,
DirectMsgPerMin: acct.DirectMsgPerMin,
PersonaID: acct.PersonaID,
}
}