Skip to content

fix: 凭证一致性 + env override + 限流鲁棒性 (issue #3)#5

Merged
tizerluo merged 9 commits into
mainfrom
fix/issue-3-creds-robustness
Jun 30, 2026
Merged

fix: 凭证一致性 + env override + 限流鲁棒性 (issue #3)#5
tizerluo merged 9 commits into
mainfrom
fix/issue-3-creds-robustness

Conversation

@tizerluo

Copy link
Copy Markdown
Owner

解决 #3 的三个子项, 全部基于实测确认。

子项1: Model ID 一致性

统一 canonical model id = config 里 models 的 key 原样 (如 GLM-5.2), 不加 provider 前缀。实测 zai/GLM-5.2 也兼容但非 canonical。SKILL env wrapper 去掉 zai/ 前缀逻辑, shared/ACP/MCP 三处代码原本就一致。

子项2: 显式 env 优先 + 调试命令

  • ACP/MCP 凭证合并顺序翻转: {**creds, **os.environ} → 显式 env 覆盖 config (ZCODE_MODEL=xxx 临时覆盖生效)
  • agent-help 新增 --print-injected-env: 脱敏打印会注入的凭证 + 来源 (config vs env)

子项3: 限流鲁棒性

  • provider 错误解析 (shared/provider_error.py): 识别 429/1302/Too Many Requests/请求频繁/retry-after, 区分 rate_limit/quota/provider_error, 提取 retry_after (bounded [1,300])
  • 进程级文件锁 (MCP): ReviewFileLock (fcntl) 串行化并发 headless review, ZCODE_BRIDGE_REVIEW_LOCK=0 可关
  • 有限重试退避 (MCP): 仅限流错误重试, max_retries=3 (ZCODE_BRIDGE_MAX_RETRIES 可调), 优先用 retry-after
  • 轮询失败检测 (ACP): 修复 PR#2 遗留 — zcode 只在 turn.failed 事件 payload 给 resultType, projection/messages 无失败标志 (实测确认)。turn 完成但无实质输出 (text/tool/patch) 时返回 -32603 而非静默 end_turn

实测依据

  • zcode headless 错误全在 stderr (stdout 空), 错误类 APICallError/AI_APICallError (Vercel AI SDK), zcode 内部已有 _retryWithExponentialBackoff
  • 错误文本变体 (bundle 确认): 429 (73×) / RateLimit (50×) / rate_limit (15×) / retryAfter (14×) / 1302 (2×)
  • projection turn 失败时无 resultType 字段 (实测), messages 只留 step-start 半成品

测试 (套件 82 → 127)

  • test_credentials.py (12): model id 原始格式 + env 优先级 + 边界
  • test_provider_error.py (22): 分类 + retry-after 提取
  • test_mcp_retry_lock.py (8): 重试逻辑 + 文件锁 (monkeypatch subprocess)
  • test_polling_failure.py (3): 轮询无输出 → -32603

质量门

✅ ruff 全绿 / ✅ 127 测试 / ✅ exec bit 100755。待 Codex deep review。

关闭 #3

tizerluo added 5 commits July 1, 2026 00:47
子项1 model id 统一为原始格式:
- shared/credentials.py 文档用法说明 canonical = config models key 原样 (如 GLM-5.2)
- 实测 zai/GLM-5.2 也兼容但非 canonical, 本项目统一用原始 id

子项2 显式 env 优先 + 调试命令:
- ACP/MCP 凭证合并顺序翻转: {**creds, **os.environ} → 显式 env 覆盖 config
  (ZCODE_MODEL=xxx 临时覆盖生效)
- agent-help 新增 --print-injected-env: 脱敏打印会注入的凭证 + 来源 (config vs env)

子项3 限流鲁棒性:
- shared/provider_error.py: parse_provider_error 纯函数, 识别 429/1302/Too Many
  Requests/请求频繁/retry-after, 区分 rate_limit/quota/provider_error, 提取 retry_after
- MCP server: 内嵌副本 + ReviewFileLock (fcntl 文件锁防并发) + 有限重试退避
  (仅限流重试, max_retries=3, ZCODE_BRIDGE_MAX_RETRIES 可调, ZCODE_BRIDGE_REVIEW_LOCK=0 关锁)
- ACP bridge: 修复 PR#2 遗留 — 轮询/停滞收尾路径不感知 turn 失败。zcode 只在
  turn.failed 事件 payload 给 resultType, projection/messages 无失败标志 (实测确认)。
  修法: turn 完成但无实质输出 (text/tool/patch) 时返回 -32603 而非静默 end_turn
- test_credentials.py (12): model id 原始格式 + env 优先级 + 边界 (空/缺失/损坏/多 provider)
- test_provider_error.py (22): 限流/配额/其他分类 + retry-after 提取 (header/json/中文/英文)
- test_mcp_retry_lock.py (8): 限流重试 (耗尽/非限流不重试) + 文件锁 (获取/禁用/重入), monkeypatch subprocess
- test_polling_failure.py (3): 轮询 turn 完成无输出 → -32603 (PR#2 遗留修复验证)
README 新增'凭证与限流 (自动化集成必读)'章节:
- canonical model id (原始格式, 不加 zai/ 前缀)
- 显式 env 优先 + 临时覆盖用法
- --print-injected-env 诊断命令
- 限流三层防护表 (文件锁/错误解析/有限重试) + env 配置
- 聚焦审查 prompt 建议

SKILL env wrapper 去掉 zai/ 前缀逻辑, 加 canonical 说明
P0 空串 env 覆盖: 新增 merge_env_with_creds (shared) + ACP/MCP 内嵌副本,
  显式非空 env 才覆盖 config; 空串 (如 ZCODE_MODEL="") 视为未设置, 保留 config 值。
  agent-help --print-injected-env 同步语义 (空串显示'视为未设置')。

P1-1 MCP parser 副本漂移: 补 provider_error 分支 (APICallError/Unauthorized/
  invalid key/service unavailable), 修复 exit=0+stderr=Unauthorized 被当成功空输出。

P1-2 文件锁健壮性: mkdir(parents) 确保目录存在; 打开/加锁失败降级无锁而非崩
  (权限受限/沙箱不阻断 review); ZCODE_BRIDGE_LOCK_DIR 可覆盖 (测试隔离);
  __exit__ 用 acquired 标志避免对未持锁 fd 解锁。

P2 文档: '指数+jitter' → '指数退避 (2^n+1)' (实现本无 jitter)。

测试 +6 (套件 127→133):
  C9 merge_env_with_creds 空串不覆盖 / C9c apiKey 空串不清 config
  RT5 exit=0+stderr=Unauthorized 当错误 (P1-1 验证)
  RL3 锁文件在临时目录 / RL4 目录不可写降级无锁
tizerluo added 4 commits July 1, 2026 01:50
排查发现: ZCode App 切换过订阅 plan 的用户, 旧 plan 的 baseURL 会残留在子进程
env (即使该 plan 已 not_entitled)。例如 env=ZCODE_BASE_URL=https://zcode.z.ai
(start-plan 残留) 与 config enabled 的 zai-coding-plan (api.z.ai) 不一致, 导致
headless 调用 404。这是'切换过 plan 的用户'会踩的坑 (非个例)。

产品级解法 (纯 bridge 代码层, 不碰用户凭证):
- shared: 新增 is_stale_env_base_url() + _all_provider_base_urls() + _safe_host()
  host 匹配判定: env baseURL 是 config 另一 provider 的官方 endpoint → 残留
- merge_env_with_creds (shared + ACP/MCP 内嵌副本): 检测残留 → 自动用 config 值 + 告警
  用户自建 endpoint (不在 config 任何 provider) 则尊重 env (调试场景不受影响)
- agent-help --print-injected-env: 残留时标 🚫 并提示 bridge 会自愈
- 修复 _safe_host 的 urlparse import bug (函数内 import 未提到模块级)

测试 +8 (套件 133→140):
  C10 残留自愈 / C10b 告警触发 / C10c 自定义 endpoint 尊重 / C10d 一致不告警
  C10e 完整URL也自愈 / C10f host 匹配 helper / C10g 无 config_path 安全

真实冒烟验证 (当前环境真有 zcode.z.ai 残留):
  ✅ shared/MCP/ACP 三处 _merge_env_with_creds 均自愈 → 注入 api.z.ai 正确值
  ✅ --print-injected-env 标红 🚫 残留 + 提示自愈
CI 环境无 ~/.zcode/v2/config.json, merge_env_with_creds 的残留检测读不到
config → 检测不生效。C10/C10b/C10e 需传 config_path 指向测试临时 config。

本地能过是因为本地有真实 config (恰好含 zcode.z.ai), 掩盖了测试缺陷。
已用空 HOME (模拟 CI) 验证 140 测试全过。
Claude Opus 4.8 (effort high) + Codex (reasoning high) 双引擎串流 review,
两个都结论 REQUEST CHANGES。交叉比对后修 3 个真 P1 + 3 个 P2:

P1-1 (Claude): MCP 把 stdout 审查正文喂进 parse_provider_error, 审查'限流/
  配额'相关代码时会被误判限流而白重试, 甚至把成功结果截断成报错。
  修: 只解析 stderr (stdout 是审查结论, 错误在 stderr)。

P1#2 (Codex): 'rate limit exceeded' 被 quota 的 'exceeded.*limit' 吃掉。
  修: 限流/配额消歧 — 限流用明确词 (429/rate limit/1302), 配额用精确词
  (quota/credit/配额不足), retry-after 作为兜底 (两者都没命中时)。
  同步 shared + MCP 副本。

P1#3 (Codex): 事件停滞收尾只追踪 emitted_text (TextDelta), 纯工具 turn
  (有 ToolCallNew 无文本) 会误判失败。修: 新增 emitted_output 追踪
  text/tool/patch, 与轮询路径 has_output 一致。

P2: shared docstring 改用 merge_env_with_creds (原示例会绕过空串保护);
  max_retries clamp >=0 (防 -1 不执行 zcode); MCP parser 中文收紧。

测试 +3 (套件 140→143):
  PE5d rate limit exceeded 不被 quota 吃 / PE5e quota vs 限流消歧
  RT6 审查正文含 429/频繁 + stderr 非空 → 不误判限流重试
ZCode App 3.2.0 → 3.2.1 (build 2135→2175), CLI 仍 0.15.0。
协议层无变化 (14 个方法 + 流式事件全保留, bundle 仅 +2455B)。
PR#5 全部功能在 3.2.1 上真实业务实测通过 (7/7)。

更新:
- agent-help version_described: 0.15.0 (App 3.2.0) → 0.15.0 (App 3.2.1)
- README/SKILL/drive-zcode: 'App 3.2.0' → 'App 3.2.0+' (带 +, 兼容 3.2.0 及之后含 3.2.1, 避免每次补丁版都改)
- 方法引入版本注释保留 3.2.0 (历史事实, 这些方法是 3.2.0 引入的)
@tizerluo tizerluo merged commit 72f8c80 into main Jun 30, 2026
4 checks passed
@tizerluo tizerluo deleted the fix/issue-3-creds-robustness branch June 30, 2026 18:31
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.

1 participant