feat(channels): 流控可配 —— 陌生人私聊额度走账号配置 directMsgPerMin(裁决 13)
- 后端:账号顶层 directMsgPerMin(缺失/0 = 保护性默认 6 条/分;正值 = 该值;负值 = 关闭并留带 source 的 WARN)
- 日志如实报出**实际生效**额度(adapter.(*FlowGate).DirectPerMin()),告警文案不再写死 6
- 面板:渠道配置表单加字段(含帮助文案)+ 回显/写回链路(LIMIT_KEYS 四键)+ 账号卡片摘要
- 文案纠错:AdapterConfigSummary 的 msgPerMin 由「私聊每分钟上限」改为「非主人消息每分钟总上限」(原描述是误导),并列补 directMsgPerMin 行
- 变异实测:缺键丢默认 / 负值当 1 条每分 / 前端吞负值 —— 三个都精确打红并还原
- ⚠️ 已知缺口(下一片补,gateway 面):GET/PUT /admin/channels 尚未下发/解码该键 ⇒ 面板暂时回显空、保存会把 -1 重置为 0
This commit is contained in:
1 parent
ba17820040
commit
a057a0f61f
18 files changed
+339
-70
No files matched your search
@@ -176,16 +176,17 @@ type namedAdapter struct {
|
||||
denyUsers []string
|
||||
// 流控限制(design/12 §6 策略三/四)。它们始终经 Config() 暴露,使桥能构建按账号
|
||||
// 的入口闸门;桥把零值默认映射为保护性取值,把负值映射为「关闭」。
|
||||
groupMsgPerMin int
|
||||
msgPerMin int
|
||||
maxMessages int
|
||||
groupMsgPerMin int
|
||||
directMsgPerMin int
|
||||
msgPerMin int
|
||||
maxMessages int
|
||||
}
|
||||
|
||||
func (n *namedAdapter) Name() string { return n.name }
|
||||
|
||||
func (n *namedAdapter) Config() map[string]any {
|
||||
cfg := n.PlatformAdapter.Config()
|
||||
out := make(map[string]any, len(cfg)+7)
|
||||
out := make(map[string]any, len(cfg)+8)
|
||||
for k, v := range cfg {
|
||||
out[k] = v
|
||||
}
|
||||
@@ -194,6 +195,7 @@ func (n *namedAdapter) Config() map[string]any {
|
||||
out["allowUsers"] = n.allowUsers
|
||||
out["denyUsers"] = n.denyUsers
|
||||
out["groupMsgPerMin"] = n.groupMsgPerMin
|
||||
out["directMsgPerMin"] = n.directMsgPerMin
|
||||
out["msgPerMin"] = n.msgPerMin
|
||||
out["maxMessages"] = n.maxMessages
|
||||
return out
|
||||
@@ -282,6 +284,7 @@ func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.
|
||||
allowUsers: acct.AllowUsers,
|
||||
denyUsers: acct.DenyUsers,
|
||||
groupMsgPerMin: acct.GroupMsgPerMin,
|
||||
directMsgPerMin: acct.DirectMsgPerMin,
|
||||
msgPerMin: acct.MsgPerMin,
|
||||
maxMessages: acct.MaxMessages,
|
||||
}, nil
|
||||
|
||||
@@ -118,7 +118,7 @@ func hasAdapter(adapters []adapter.PlatformAdapter, name string) bool {
|
||||
// save → Reload path uses exactly this builder.
|
||||
func TestChannelFactoryBuildFromFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
if err := os.WriteFile(path, []byte(`{"kook":[{"id":"conf0","config":{"bot_token":"bt"},"denyGroups":["g-1"],"allowUsers":["u-1"],"groupMsgPerMin":3}]}`), 0o600); err != nil {
|
||||
if err := os.WriteFile(path, []byte(`{"kook":[{"id":"conf0","config":{"bot_token":"bt"},"denyGroups":["g-1"],"allowUsers":["u-1"],"groupMsgPerMin":3,"directMsgPerMin":-1}]}`), 0o600); err != nil {
|
||||
t.Fatalf("write channels.json: %v", err)
|
||||
}
|
||||
f := &channelFactory{path: path, fallback: channelAccountsFromEnv(), log: testLog()}
|
||||
@@ -146,6 +146,11 @@ func TestChannelFactoryBuildFromFile(t *testing.T) {
|
||||
if got, _ := cfg["groupMsgPerMin"].(int); got != 3 {
|
||||
t.Errorf("groupMsgPerMin = %v, want 3", cfg["groupMsgPerMin"])
|
||||
}
|
||||
// directMsgPerMin 同一条链:channels.json 的账号顶层字段 → namedAdapter.Config() →
|
||||
// 桥的 gateOptions。断桥这一环就等于「面板/文件配了却没人读」(负值必须原样透出)。
|
||||
if got, _ := cfg["directMsgPerMin"].(int); got != -1 {
|
||||
t.Errorf("directMsgPerMin = %v, want -1", cfg["directMsgPerMin"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user