Skip to content

fix(server): request illegal 不再永久禁用账号,改为软冷却 - #198

Closed
luohaonan365 wants to merge 1 commit into
linguo2625469:mainfrom
luohaonan365:fix/account-fault-no-permanent-disable
Closed

luohaonan365 wants to merge 1 commit into
linguo2625469:mainfrom
luohaonan365:fix/account-fault-no-permanent-disable

Conversation

@luohaonan365

Copy link
Copy Markdown

问题

上游对「渠道校验失败」的响应是 HTTP 400,body 形如:

{"code":"11-128","msg":"request illegal","requestId":"...",
 "displayMsg":{"zh":"请求被拦截,请重新发送"},
 "displayTips":{"zh":"渠道校验失败,可能不是从官方客户端发出的,请用官方客户端重新发送"}}

上游自己的措辞是可重试的请求级拦截("请重新发送"),不是账号级封禁。

但 applyErrorPolicy 在 ErrAccountFault 分支按 msg 字面量匹配 request illegal,直接 Pool.Disable():

h.cfg.Pool.Disable(uid, "account banned by upstream (11140 request illegal), re-login required")

账号被永久踢出池子,且必须人工解冻才能恢复。这正是 #197 的现象。

为什么这是误判

用与网关完全相同的出站头部直连 https://www.workbuddy.ai/v2/chat/completions 验证:

账号 结果
被禁用的 3 个号 稳定 400 11-128 request illegal
同时期的健康号 稳定 200

补充排除项,全部证明账号本身没问题:

  • 凭证/余额/JWT 全部正常:余额接口正常返回,JWT 的 exp 到 2027-10,azp/aud 与健康号完全一致
  • 换任何模型(global:deepseek-v4.1-flash / global:step-5-preview …)都同样 400 → 在模型解析之前就被拦
  • header 变体全部同样 400:去掉 X-Machine-ID/X-Session-ID、换随机 UUID、换 UA 平台段、加 X-Device-Token、去掉 X-No-Enterprise-Id、去掉全部 X-IDE-*/X-Product 归属头 —— 无一改变结果
  • 同一账号在别的客户端里正常调用(显示账号请求不合法 #197 里多位用户同样反馈)
  • 只有 realm=global 受影响,cn 号全部正常

改动

request illegal 从 Disable 改为软冷却:

h.cfg.Pool.CooldownSoftRate(uid, h.softCooldown(), time.Time{}, "channel verification blocked (11-128 request illegal)")

账号暂时出池、自动轮换,上游解除标记后自愈,不再需要人工解冻。

与 #190 的关系

触发这个 400 的根因是 X-Conversation-ID 缺失,#190 已在 internal/upstream/headers.go 层修复,本 PR 不重复那部分。

本 PR 是互补的防御面:即使将来上游换个措辞、或出现别的触发路径,误判的代价也从「账号永久禁用、必须人工解冻」降为「短暂轮换、自动恢复」。

验证

  • go build ./cmd/server、go vet ./internal/server/ 通过
  • go test ./internal/server/ ./internal/upstream/ 通过
  • 生产环境(本机 v1.13.0,global 池 5 个号)实测:修复前同一请求三次出站全部 400 且账号被禁用;修复后 4/4 → 200,账号零冷却零禁用

Refs #190
Refs #197

上游对「渠道校验失败」的响应是 HTTP 400 code 11-128 "request illegal",body 自带
displayMsg「请求被拦截,请重新发送」与 displayTips「渠道校验失败,可能不是从官方
客户端发出的,请用官方客户端重新发送」——上游明示这是可重试的请求级拦截,
不是账号级封禁。

但 applyErrorPolicy 在 ErrAccountFault 分支按 msg 字面量匹配 "request illegal",
直接调用 Pool.Disable(),把账号永久踢出池子并要求人工解冻:

  h.cfg.Pool.Disable(uid, "account banned by upstream (11140 request illegal), re-login required")

实测该账号凭证/余额/JWT 全部正常,同一套出站头部换健康账号即可 200,换任何模型、
任何 header 变体都稳定复现——账号本身没有任何问题,禁用纯属误判。

改为软冷却:账号暂时出池、自动轮换,上游解除标记后自愈,绝不永久禁用。

触发该 400 的根因(缺 X-Conversation-ID)已由 linguo2625469#190 在 internal/upstream/headers.go
层修复;本 PR 是互补的防御面:即使将来上游换个措辞、或出现别的触发路径,误判的
代价也从「账号永久禁用、必须人工解冻」降为「短暂轮换、自动恢复」。

Refs linguo2625469#190
Refs linguo2625469#197
@luohaonan365

Copy link
Copy Markdown
Author

补充一个实现细节,供权衡(本 PR 不涉及这部分,只在 applyErrorPolicy 里把 request illegal 从 Disable 改为软冷却)。

#190 在 ChatHeaders 里对缺失的 ConversationID 生成每次全新的 32-hex ID,其测试也显式断言了 freshness:

} else if len(id) != 32 || !validTraceID(id) || id == previousID {
    t.Fatalf("fallback ID must be fresh 32 hex: %q (previous %q)", id, previousID)
}

我最初在 handler 层的做法是按轮级键派生一个稳定值:

convID = session.TurnRequestID("conv:" + turnKey)

两者都能过渠道校验,差别在于:

同一会话的多轮请求 同一轮内的重试
全新随机(#190) 每轮都是不同的 X-Conversation-ID 每次不同
稳定派生(本 PR 原方案) 共享同一个 ID 复用同一个 ID

如果上游的 X-Conversation-ID 参与会话级上下文或前缀缓存(从命名看像是),稳定值更贴近官方客户端的行为——官方客户端不会每轮换一个 conversation id。

需要说明的是:这一条我没有上游行为的直接证据,只是从字段命名和语义推断。#190 的测试断言了 freshness,说明作者是有意选择全新随机的。如果那个选择有依据(比如上游要求每次都新),请忽略本条;如果没有特别理由,建议考虑改成稳定派生。

jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 17, 2026
jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 17, 2026
jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 17, 2026
jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 19, 2026
jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 19, 2026
jin66god pushed a commit to jin66god/workbuddy2api-panel that referenced this pull request Oct 19, 2026
@linguo2625469

Copy link
Copy Markdown
Owner

感谢实现。已由 #233(2d9e749)以更精确的形态合入:

  • 11-128 通道校验失败(msg 同为 request illegal)→ 分流为 ErrChannelRefused,不罚号(不冷却/不禁用),与你的目标一致;
  • 11140(账号被上游限制)仍保持硬禁用——依据是 fix(upstream): 国际版 11-128 通道校验分流 + 始终发送 X-Conversation-ID #233 的实测:官方客户端可正常使用的是 11-128 场景,11140 属于账号级限制(官方渠道申诉才能解除),软冷却只会让账号反复被选中、反复撞限制。

你的核心痛点(健康账号被误判成封禁永久踢出)在分类层已根治。若后续实测发现 11140 也存在「申诉外自愈」的形态,欢迎单独开 issue 带证据讨论。多谢!

zhf883680 pushed a commit to zhf883680/workbuddy2api-panel that referenced this pull request Oct 21, 2026
上游对渠道校验失败的响应是 HTTP 400 + {"code":"11-128","msg":"request illegal",
displayTips:{"zh":"渠道校验失败,可能不是从官方客户端发出的,请用官方客户端重新发送"}}——
它自述是「请求被拦截,请重新发送」,请求级可重试信号,不是账号级封禁。而我们的
applyErrorPolicy 在 ErrAccountFault 分支按 msg 字面量匹配 "request illegal" 就 Disable()
(理由写的是 11140 auth_forbidden),于是一个健康的号被永久踢出池子 + 要人工解冻。

**只取有证据那一半**:同一条 msg 骑两种 code,故按 code 分野——
- code "11-128"(字符串)→ 渠道校验拦截:CooldownSoftRate 软冷却,上游解除标记后自愈;
- code 11140(数字)→ auth_forbidden 账号级授权风控:保持硬禁用(软冷却只会反复送死);
- 14017 不变(本就走软冷却)。
判据复用 upstream 的 codeMarker(空白容差),新导出 HasCodeMarker 供 server 侧调用,
不把匹配口径散成两份。

线上证据(甲骨文):60 天 journal 里 "account banned by upstream" 0 次、11140/11-128 0 次,
当前 22 个号 0 个禁用——即本次是防御性收紧:错判的两个方向代价不对称(误禁用=永久丢号+
人工解冻;误软冷却=短暂轮换+退避自愈)。

测试:TestApplyErrorPolicyChannelVerificationKeepsAccount(11-128 → 未禁用 + soft_rate 冷却)、
TestApplyErrorPolicyAuthForbiddenStillDisables(11140 → 仍硬禁用);变异(11-128 又走硬禁用、
判据写成 11140)均红。
qcgzxw added a commit to qcgzxw/workbuddy2api-panel that referenced this pull request Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants