fix: 凭证一致性 + env override + 限流鲁棒性 (issue #3)#5
Merged
Conversation
子项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 目录不可写降级无锁
排查发现: 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 引入的)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
解决 #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 优先 + 调试命令
{**creds, **os.environ}→ 显式 env 覆盖 config (ZCODE_MODEL=xxx临时覆盖生效)--print-injected-env: 脱敏打印会注入的凭证 + 来源 (config vs env)子项3: 限流鲁棒性
实测依据
测试 (套件 82 → 127)
质量门
✅ ruff 全绿 / ✅ 127 测试 / ✅ exec bit 100755。待 Codex deep review。
关闭 #3