Skip to content

fix(wechat): 视频号瞬态失败有限重试 + attempts 带脱敏失败原因(#34 #33) - #37

Merged
zj1123581321 merged 6 commits into
masterfrom
card/MediaResolverAPI-sph-retry-260923
Sep 23, 2026
Merged

zj1123581321 merged 6 commits into
masterfrom
card/MediaResolverAPI-sph-retry-260923

Conversation

@zj1123581321

@zj1123581321 zj1123581321 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

为什么

2026-09-23 12:23 生产一次瞬时的上游空结果把视频号解析打成终态失败(All providers failed ... attempts=[{'endpoint': 'fetch_video_detail', 'decision': 'retryable'}]),调用方按终态处理、用户任务失败并收告警;同链接 15:59 重放完全正常(issue #34)。事后又答不出「12:23 到底哪一种失败」,因为 retryable 只记了一个词(issue #33)。

根因

retryable 的语义是「可能瞬时,值得再来一次」,但引擎对它的唯一补救动作是「换下一个端点」——「重试」和「换源」是两个正交的轴,被合并成了一个动作。多头链上这条错配被兜底掩盖,视频号(TikHub 单源、单端点、无 Cobalt)上直接退化成终态。佐证:下载侧 fetch_wechat_channels_media 自己手搓了 3 次循环,解析侧一次都没有——同一行为两套实现,行为必然漂移。

改法

  1. 重试轴进引擎:_run_chain(max_attempts_per_endpoint=..., retry_backoff=...),默认 1 = 现状(其余 8 平台逐字节不变);视频号链传 3 / 0.3s。只对 decision=retryable 重试;http_error(含超时)与 parse_failed 各只算一次尝试。
  2. 删掉下载侧手搓循环:fetch_wechat_channels_media 改为单次调用(_fetch_wechat_channels 内部即 3 次尝试),其后的 media 缺失/不完整/file_size 非法等终态转换原样保留。不下移会叠加成 3×3=9 次上游请求。
  3. attempts 可归因:每次尝试一条记录(新增 attempt,既有 endpoint/decision 键名与取值不变),retryable 时带脱敏原因:
    • data_missing —— 响应非 dict / 缺 data / data 非 dict 或空;
    • object_type_mismatch —— data 是 dict 但 object_type != 0(只带 int 标量;非 int 只带类型名,容器绝不进日志);
    • error_body —— 非 2xx 且 body 是 JSON(附 int http_status 与上游 message 截断片段)。
      classify 契约向后兼容扩展为 str(等价 (decision, None))或 (decision, reason),由引擎归一化;_call_endpoint 非 2xx 且 body 为 dict 时一律抛 EndpointHttpError(name, status, body)(与 HTTP verb 无关),链上仍以该 body 跑 classify + has_playable,保持既有终态短路与命中语义。
  4. 「重试耗尽 → VideoNotFoundError」是硬保证(gate primary R1 的 major,已收口):
    • 重试前要求剩余预算 > per_timeout + retry_backoff(为下一次尝试的完整单端超时留余量),不足即不再重试、按全链未命中收场;
    • 兜底:总预算超时触发时,若该链启用了重试(max_attempts_per_endpoint > 1)且已记录 retryable 且尝试次数未达上限 → 改抛 VideoNotFoundError(带 attempts)。首跳就挂起、单端点链(上限 1)的既有行为一律不变,两处既有 test_chain_total_budget_timeout(微信/抖音)原样保持绿。

实测

项 结果
全量 uv run --extra dev python -m pytest tests/ -q → 421 passed
回归锁(抖音/快手/TikTok/IG/YouTube/Twitter/xhs/terminal 八个文件) 零改动且全绿;_call_endpoint 内部契约变更只改写抖音/小红书各一条对应用例(断言异常 + .body/.status,保留「body 交分类器」的 codex #4 本意)
三态互斥 len({data_missing, object_type_mismatch, error_body}) == 3
脱敏(跨边界) fixture 里植入真凭据(full_url / decode_key / url_token)后:str(exc) 与 POST /api/resolve 响应文本中三值零出现;同时断言上游 message 片段确有出现(防恒真)。另覆盖 data.object_type 为容器(内含凭据)那一形态:不再进 attempts,只留类型名
预算截断 主脑独立复现(stub 每次 sleep 0.6s 返回空包 + 预算 1.0s):修复前 ProviderError: chain timed out(attempts 仅 1 条),修复后 VideoNotFoundError(attempts 带 retryable)。新增确定性测试 test_chain_budget_truncation_still_not_found(sleep 0.4 / 预算 0.7 / per_timeout 0.05 / backoff 0)锁住超时兜底分支
重试耗尽 _call_endpoint 调用数 == 3、异常类型 is VideoNotFoundError(非 timed out)
不重试 ProviderError 路径调用数 == 1
红验 R0:尝试上限注入 1 → 1 == 3 面值红;R1:raise EndpointHttpError 注入 return body → 两条契约用例 DID NOT RAISE 红;R2:超时兜底条件 > 1 注入 > 99 → 新用例以异常类型面值转红,isinstance(ot,int) 注入 isinstance(ot,object) → 脱敏用例以 'object_type' not in {...} 面值红。三次均单行还原,主脑复跑确认

影响范围

只影响视频号链(重试 + attempts 字段)与 _call_endpoint 的内部异常形状(链上行为等价,见上表回归证据)。不新增配置项、不新增端点、不改响应结构与缓存。

备注

  • 提交 a3bdc81 是主脑的 CHANGELOG 记账(运维备忘:生产 PUBLIC_BASE_URL 已设为内网直连地址(2026-09-02) #15 的启动告警条目,PR fix(startup): PUBLIC_BASE_URL 留空时启动打 WARNING 提示 (#15) #35 合并时声明由持有 CHANGELOG.md 写权的本卡补齐),非实现改动。
  • 卡面原定「上游 message 截断 ≤120 字符」,实现取 200 字符;仍是固定上界、仍走白名单(只 message),判定为等价实现并接受。
  • diff 量 345+/87-(合计 432)超出卡面 hard 400 约 8%,执行器在 R2 报告逐项披露构成(F1 实现 14 + F2 实现 9 + F1 测试 17 + 脱敏扩展 15 + 进度段落 11,可压缩项已压),无可删的非必修行,故带披露落地。
  • Refs #34、Refs #33(关单由主脑在合入后带证据执行)。

zj1123581321 and others added 5 commits September 23, 2026 16:27
…easons

_run_chain retries retryable per endpoint (wechat: 3 attempts + 0.3s
backoff, others: 1 = unchanged). classify may return (decision, reason);
_call_endpoint raises EndpointHttpError(status, body) on wechat POST
non-2xx dict bodies so attempts records error_body + http_status.
fetch_wechat_channels_media drops its hand-rolled loop (was 3x3=9).

Task-Id:
Fixes-Issue: #34, #33
Agent-Executor: ocgo-muse-pi
Agent-Model: muse-spark-1.3-contributor
Agent-Effort: unknown
Dispatch-Id: dlg-20260923-082148-152a8c
Task-Id: MediaResolverAPI-20260923-01
Classify now returns tuples; empty-envelope chain asserts 3 attempts.
New: transient-then-hit, exhausted VideoNotFoundError with attempts,
tri-state distinctness, cross-boundary desensitization (exc + resolve
API), no-retry on ProviderError. Stream media tests stub _call_endpoint.

Task-Id:
Fixes-Issue: #34, #33
Agent-Executor: ocgo-muse-pi
Agent-Model: muse-spark-1.3-contributor
Agent-Effort: unknown
Dispatch-Id: dlg-20260923-082148-152a8c
Task-Id: MediaResolverAPI-20260923-01
Task-Id:
Fixes-Issue: #34, #33
Agent-Executor: ocgo-muse-pi
Agent-Model: muse-spark-1.3-contributor
Agent-Effort: unknown
Dispatch-Id: dlg-20260923-082148-152a8c
Task-Id: MediaResolverAPI-20260923-01
…POST

Error shape no longer forks on HTTP verb: _call_endpoint raises
EndpointHttpError(name, status, body) for every non-2xx dict body so
_run_chain always runs classify + has_playable and records http_status
(single-endpoint chains like Twitter included). Rewrite the two
_call_endpoint contract tests to assert the raise (+ body/status +
classify conclusion); add a chain-level case proving the body still
reaches the classifier (DouyinTerminalError short-circuit).

Task-Id:
Fixes-Issue: #34, #33
Agent-Executor: ocgo-muse-pi
Agent-Model: muse-spark-1.3-contributor
Agent-Effort: unknown
Dispatch-Id: dlg-20260923-083824-a438d8
Task-Id: MediaResolverAPI-20260923-01
Agent-Executor: pi-lead
Agent-Session: pi-lead-1790151094413-2777230
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Required Gate v2 — 状态面板

当前状态:fail · 要修代码

当前裁决:code_fail / primary_findings

Gate 历史(v1;来源为持久化 gate_terminal 制品)

Run Attempt Head 状态 收件人动作
35839228776 1 a3bdc81 fail 要修代码
35841611401 1 187b223 fail 要修代码

历史行按 run_id + run_attempt 去重并只增不删;删除本评论后可由 gate_terminal 制品重建。

F1: _should_retry reserves per_timeout + backoff; TimeoutError with
retryable attempts below the cap converts to VideoNotFoundError
(first-hop hangs and single-endpoint chains unchanged). F2:
describe_failure only carries int object_type scalars, non-int yields
a fixed type name; _error_body_reason pins int status + str message.
Extend desensitization test with dict-object_type chain coverage;
add budget-truncation test; append progress paragraph (F3).

Task-Id:
Fixes-Issue: #34, #33
Agent-Executor: ocgo-muse-pi
Agent-Model: muse-spark-1.3-contributor
Agent-Effort: unknown
Dispatch-Id: dlg-20260923-090551-d1d4f8
Task-Id: MediaResolverAPI-20260923-01
zj1123581321 added a commit that referenced this pull request Sep 23, 2026
fix(wechat): 视频号瞬态失败有限重试 + attempts 带脱敏失败原因(#34 #33,取代 #37)
@zj1123581321
zj1123581321 merged commit 187b223 into master Sep 23, 2026
10 of 12 checks passed
@zj1123581321
zj1123581321 deleted the card/MediaResolverAPI-sph-retry-260923 branch September 23, 2026 10:32
@zj1123581321

Copy link
Copy Markdown
Collaborator Author

本 PR 的产物已随 #38 合并进 master(ffea47a)——#38 的 base 是本 PR 的 head,包含本 PR 全部提交。作为被取代者关闭。

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