受控的豆瓣电影小助手 Demo:新人 30 分钟内即可读懂前置条件、安装、离线验证、受控运行、断点续跑、显式重试与故障处置。 本文档面向需要在本仓库
D:\DataAnt中复现 Demo 的人;所有命令均假定在 PowerShell 中以工作目录为根执行。
- 操作系统:Windows 10 / Windows 11。
- Python:3.11 或 3.12(项目
pyproject.toml限定>=3.11,<3.13)。 - 浏览器:本机已安装 Google Chrome 或 Microsoft Edge,且 Chromium 内核版本 ≥ 100。DrissionPage 启动时按以下顺序自动探测可执行文件:
--browser-path显式指定的路径;%ProgramFiles%\Google\Chrome\Application\chrome.exe;%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe;%LocalAppData%\Google\Chrome\Application\chrome.exe;%ProgramFiles%\Microsoft\Edge\Application\msedge.exe;%ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe。 全部缺失时启动即抛FileNotFoundError,退出码 5。
- DrissionPage 许可:DrissionPage 仅可用于非商业用途;若用于商业场景必须先取得版权方书面许可(见上游项目协议)。
- Live-run 授权门禁:任何对真实豆瓣(
movie.douban.com)的 live run 必须在 CLI 上显式传--live-approved与--max-queries <1–10>,并且app.main在启浏览器前会校验 headed 模式与--min-interval >= 5;缺任一参数直接返回退出码 2,浏览器根本不会启动。这条把"操作员是否知情同意"这件事从外部审批表挪到了 CLI 上的不可绕过开关。 - 数据边界:只收集获批的公开字段(
task_id、query、query_year、matched_title、matched_year、director、rating、detail_url、match_method、status、error_message、collected_at共 12 列)。其它一切字段都不抓取。 - 运行节奏:默认串行执行,相邻任务间隔至少
5秒(--min-interval),不代表任何形式的站点授权。
下列命令在仓库根目录 D:\DataAnt 下以 PowerShell 执行。安装命令仅在首次克隆或重建环境时执行一次;本任务以及 CI 中都禁止重建 venv 或重装依赖。
Set-Location -LiteralPath 'D:\DataAnt'
py -3.12 -m venv .venv
& 'D:\DataAnt\.venv\Scripts\Activate.ps1'
python -m pip install -e ".[dev]"
python -m pytest -q
python -m app.main run --help逐条说明:
| 命令 | 作用 | 预期 |
|---|---|---|
py -3.12 -m venv .venv |
创建本地虚拟环境 | 退出码 0,生成 .venv/ |
pip install -e ".[dev]" |
安装项目本体 + pytest / pytest-cov | 退出码 0 |
python -m pytest -q |
跑全量离线单测 | 所有用例通过,退出码 0 |
python -m app.main run --help |
打印 CLI 帮助 | 退出码 0,且参数表与本文第 3 节一致 |
python -m pytest -q期间不会连接真实豆瓣;所有测试都使用tests/fixtures/*.html的本地 HTML 快照。
任何对真实豆瓣的 run 都必须显式传 --live-approved 与 --max-queries <N>(1 ≤ N ≤ 10),并满足 headed 模式与 --min-interval >= 5,否则 app.main 会在启浏览器前直接拒绝(退出码 2)。DrissionPage 启动的浏览器以 --headed 模式可见,必须使用项目自带的独立 browser-profile/douban,不得接管日常 Chrome 登录态。
python -m app.main run --input .\inputs\queries.example.csv --output .\outputs\douban_movies.xlsx --live-approved --max-queries 1 --headed --min-interval 5 --profile-dir .\browser-profile\douban要点:
- 重跑时稳定终态(
SUCCESS、NOT_FOUND、REVIEW_REQUIRED)自动跳过,默认仅对NETWORK_ERROR、OUTPUT_LOCKED、UNEXPECTED_ERROR三个瞬态状态重试;结果按task_idupsert,不会重复追加行。 - 退出码 0 表示本次处理成功;非零见第 4 节故障矩阵。
- 网络瞬态走内置退避:第 1 次立即重试,第 2 次等 2s,第 3 次等 5s;三次都失败才上报
NETWORK_ERROR,再由下一次 run 兜底。
第二条命令只在定位器已修复、离线 parser 单测全部通过之后,才允许显式重跑 PAGE_CHANGED;同样要带 --live-approved 与 --max-queries:
python -m app.main run --input .\inputs\queries.example.csv --output .\outputs\douban_movies.xlsx --live-approved --max-queries 1 --headed --min-interval 5 --profile-dir .\browser-profile\douban --retry-status PAGE_CHANGED--retry-status只在默认三状态之上追加,不会覆盖。- 不得用
--retry-status BLOCKED盲目重试。BLOCKED是站点拒绝信号,先解决授权或站点保护条件,再决定是否重跑。
task_id = sha256(query.casefold() ‖ U+001F ‖ year ‖ U+001F ‖ occurrence),前 20 hex。Excel 12 列是按 task_id upsert,因此只要输入 CSV 顺序与内容不变,重跑不会产生重复行;重复 query 必须在 CSV 中显式出现并被 README 解释(示例 CSV 故意不重复,避免歧义)。
- MVP 不假设登录:除非显式接入登录流程,否则不应在受控 Demo 中凭账号运行。
- 若确实需要人工登录,只能使用项目自带
browser-profile/douban/,不得接管或复制日常浏览器的 profile / Cookie / 已登录会话。 - 任何"借号跑一下"都不在本 Demo 范围内。
| 状态 | 含义 | 推荐处置 | CLI 退出码 |
|---|---|---|---|
SUCCESS |
已匹配并落库 | 无 | 0 |
NOT_FOUND |
搜索无候选 | 人工确认标题/年份写法 | 0 |
REVIEW_REQUIRED |
匹配器犹豫 | 人工二选一后用新 query 跑 | 0 |
NETWORK_ERROR |
网络瞬态失败 | 2s/5s 退避后下次 run 兜底 | 0 |
PAGE_CHANGED |
页面结构变化 | 先修定位器 + 离线 parser,跑通后 --retry-status PAGE_CHANGED |
0 |
BLOCKED |
站点拒绝(403/418/429 或敏感词) | 立即停止,不绕过 | 3 |
OUTPUT_LOCKED |
Excel 被 Excel 打开占用 | 关闭 Excel,按原命令重跑 | 4 |
UNEXPECTED_ERROR |
未归类异常 | 查看诊断,定位根因后再跑 | 5 |
补充说明:
- 出现
BLOCKED或退出码 3 → 立即停止,不添加代理、验证码识别、Cookie 注入、降低间隔等绕过手段。 - 出现
OUTPUT_LOCKED或退出码 4 → 关闭打开该 Excel 的进程(任务管理器 /Stop-Process -Name EXCEL),再按原命令重跑;输出目录是outputs/,文件后缀固定.xlsx。 PAGE_CHANGED→ 先用tests/fixtures/*.html复现并修定位器;离线pytest通过后再显式--retry-status PAGE_CHANGED重跑。NETWORK_ERROR→ 有限退避后由下次 run 兜底;不修改--min-interval来"补偿"。UNEXPECTED_ERROR→ 先看artifacts/下的截图 + 脱敏 HTML,定位根因再修。- 退出码 2 = 输入/配置错误(CSV 不存在、状态名拼错、
--output路径不可写等);退出码 5 = 浏览器启动或未归类全局异常。
- 仅当状态属于
NETWORK_ERROR/PAGE_CHANGED/BLOCKED/UNEXPECTED_ERROR时捕获;SUCCESS/NOT_FOUND/REVIEW_REQUIRED故意不截图。 - 每次失败落两个文件(都进
artifacts/,但不进 git):<task_id>.png— 全页截图(来自 DrissionPagetab.get_screenshot(path, name, full_page=True));<task_id>.html—tab.html经redact()脱敏后截到 ≤ 200,000 字符。
- 脱敏规则:API Key / Cookie 字面值替换为
***,日志和 HTML 都不出现明文。 - 诊断路径仅限定在仓库内
artifacts/目录,不得指向目录外路径。下列命令先预览再删除,不跟随artifacts/之外的路径:# 预览:列出 7 天前的失败产物(不会删除) Get-ChildItem -LiteralPath 'D:\DataAnt\artifacts' -File -Recurse | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } | Select-Object FullName, LastWriteTime # 删除:确认上面输出后,单独跑 Get-ChildItem -LiteralPath 'D:\DataAnt\artifacts' -File -Recurse | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } | Remove-Item -Force
- 保留期默认 7 天;超过 7 天的失败产物按上面命令清理,
artifacts/目录本身保留(.gitkeep占位)。
.env、browser-profile/、outputs/、artifacts/、日志、截图、HTML、API Key、Cookie、请求头、用户身份相关字段一律不得 commit。.gitignore 已托管上述路径,提交前 git status 应仅显示本次任务批准的 README.md 与 inputs/queries.example.csv。
outputs/<run>.xlsx 表头严格 12 列,每一列在每次 run 都存在:
| # | 列名 | 说明 |
|---|---|---|
| 1 | task_id |
sha256 前 20 hex,按 query+year+occurrence 派生 |
| 2 | query |
原始查询字符串 |
| 3 | query_year |
原始年份(4 位数字或空) |
| 4 | matched_title |
命中标题,空表示未匹配 |
| 5 | matched_year |
命中详情页年份 |
| 6 | director |
导演列表(/ 分隔) |
| 7 | rating |
评分(浮点,允许为空) |
| 8 | detail_url |
https://movie.douban.com/subject/<id>/ 形式的规范链接 |
| 9 | match_method |
RULE_EXACT / RULE_YEAR / LLM / NONE |
| 10 | status |
八个状态之一 |
| 11 | error_message |
错误描述;成功行可为空 |
| 12 | collected_at |
本地时区 ISO8601 秒级时间戳 |
- 每个输入 query 在 Excel 中只有一行(按
task_idupsert;稳定状态不会重复追加)。 - 评分允许为空(如
NOT_FOUND/PAGE_CHANGED行)。 collected_at永远是本地时区、ISO8601、带秒级精度的字符串。
执行下列步骤完成一次受控 Demo 验收:
- 显式传
--live-approved:在 CLI 上确认本次 run 由操作员显式授权; --max-queries1–10:确认--max-queries与输入 CSV 的查询数相匹配(输入不能多于上限);- 固定输入:使用
inputs/queries.example.csv(表头query,year,无敏感数据),必要时用本任务 commit 后的同一份文件; - 关闭 Excel:在执行 run 命令前,关闭任何会打开
outputs/douban_movies.xlsx的 Excel 进程; - 有头运行:用第 3.1 节命令(含
--live-approved --max-queries --headed --min-interval 5)跑一次,目视确认浏览器以headed模式启动、browser-profile/douban是独立目录; - 观察阻断:故意把示例 CSV 里的 query 改成"高频敏感词"或临时阻断的样本,观察
BLOCKED状态与退出码 3;sec.douban.com 重定向、accounts.douban.com/passport/login 跳转会立即被is_blocked()捕获并抛BlockedError; - 核对:每条
task_id唯一一行;artifacts/下的截图 + HTML 已脱敏(不含 Cookie / Key); - 记录:在项目日志里写明日期、环境、输入规模、状态数量、耗时。
完成上述八步且 python -m pytest -q、python -m app.main run --help 全部退出码 0,即视为本次受控 Demo 通过。
inputs/queries.example.csv 是本仓库提供的最小示例:
- 编码:UTF-8,无 BOM;
- 表头严格
query,year; - 内容只包含公开电影标题 + 公开年份,且至少有一个空年份(用于演示"年份未知"分支);
- 不含姓名、账户、内部 ID、审批号、Cookie、API Key 等敏感数据;
- 不故意重复 query(重复会让
task_id派生需要occurrence计数,本示例保持唯一以避免歧义;如确需重复,必须在 README 中显式说明)。
| 项 | 离线(fixture / data: 页面) |
Live run(真实豆瓣) |
|---|---|---|
| 是否需要 CLI 显式授权 | 否 | 是(--live-approved --max-queries N) |
| 跑什么 | pytest -q |
python -m app.main run --live-approved --max-queries N ... |
| 是否启浏览器 | 否 | 是(headed 模式) |
--min-interval |
n/a | >= 5 秒(CLI 强校验) |
| 数据落盘 | 不落 Excel | outputs/*.xlsx(1–10 行,verify_core 校验) |
| 失败诊断 | 不截图 | artifacts/<task_id>.{png,html} |
是否允许 --retry-status BLOCKED |
n/a | 禁止 |
| sec.douban.com / accounts.douban.com 重定向 | n/a | 立即判定 BLOCKED,停止批次 |
如果操作员没有显式传 --live-approved,本次任务只跑离线自检,不执行 live run,并在交付回报中显式说明"live run 因 CLI 授权门禁跳过",而不是测试失败。
本仓库在豆瓣电影 Demo 之外,独立接入第二个明确用于网页抓取练习的目标网站 web-scraping.dev。web-scraping.dev 自述为面向网页抓取开发者的安全、合法的练习平台,其 robots.txt 声明 2 秒抓取间隔并显式禁止 /robots-disallowed 路径。本节只描述受控受权的商品采集与本地画廊验收,不复用豆瓣的 12 列电影契约、不复用豆瓣适配器、也不复用电影 Excel schema。
为什么是第二个站点
- 提供服务端分页的商品列表(
/products)和稳定 ID 的商品详情(/product/<id>),适合作为新增站点适配器的最小目标。 - 与豆瓣电影契约完全独立:商品价格、分类、品牌、变体等信息不会被塞进 12 列电影工作簿。
- 离线 fixture 友好:列表/详情/阻断等场景可被完全本地化为 HTML 快照,CI 永远不连真实站点。
允许访问的路径
https://web-scraping.dev/products列表首页;/products服务端分页产生的合法下一页 URL;- 由列表页发现、经 URL 规范化校验后的
https://web-scraping.dev/product/<id>详情页。
禁止访问(命中立即停止)
/robots-disallowed及其下属任何路径;- 登录 / iframe 登录 / 凭证检查页面;
- 购物车、购买与评论 Load More 等交互接口;
- GraphQL、CSRF、文件下载、Antibot Challenge;
- 任意站外 URL(含被重定向后的
sec.douban.com之类)。
python -m app.main collect-products `
--site web-scraping.dev `
--output-dir .\outputs\web-scraping-dev-demo `
--live-approved `
--max-products 10 `
--headed `
--min-interval 2 `
--profile-dir .\browser-profile\web-scraping-dev要点:
- 必须在浏览器启动前显式传入
--live-approved,否则app.main直接返回退出码 2; --max-products取值[1, 10],命令在启浏览器前完成强校验;--min-interval 2是平台声明的下限,命令禁止下调到 2 秒以下;--profile-dir必须落在仓库browser-profile/内(browser-profile/web-scraping-dev),禁止接管日常浏览器 profile;--output-dir必须落在仓库outputs/内(outputs/web-scraping-dev-demo),禁止写到仓库外;- 受控验收必须至少跨两个
/products分页,证明分页发现 + 去重 + 详情解析共同工作。
--output-dir 指定目录内同时生成三个产物,必须来自同一次不可变的 ProductCollection:
| 产物 | 角色 | 用途 |
|---|---|---|
products.xlsx |
结构化主表 | 12 列固定顺序、按 product_id upsert;列顺序与商品领域模型一致,不复用豆瓣电影 12 列。 |
products.json |
机器可读快照 | 含 schema_version / source_site / generated_at / summary / products;金额为 JSON number,时间为带时区的 ISO 8601。 |
gallery.html |
静态可视化画廊 | 商品卡片 + 采集证据侧栏;嵌入本地数据,离线双击即可打开。 |
输出被 Excel 占用时返回 OUTPUT_LOCKED 语义(退出码 4),不覆盖也不损坏旧文件。任何单个输出器失败都不会产生彼此不一致的产物。
三件套在落地 target_dir 之前先写到 sibling staging 目录,原子 rename 才把三个文件一次性呈现为同一份 ProductCollection;live 访问始终是单 tab 串行且受 --min-interval 2 节流(web-scraping.dev)与 --min-interval 5 节流(豆瓣),不做多 tab、多浏览器、多进程 live 抓取,也不做降速绕过。本地最多开 3 个 writer 线程并发生成 products.xlsx / products.json / gallery.html(product_rows 复用同一份 primitive rows,不二次 to_primitive()),JSON 改用 compact separators=(",", ":") 但 schema_version=1 不变,跨产物一致性由 BundleWriteReceipt(product_ids, excel, bytes_by_file) 校验。
可重复的离线性能与清理工具:
# 离线 benchmark:仅用 tests/fixtures + tests/helpers_product_performance.py fixture
# 不启浏览器、不访问网络、stdout 输出 JSON 报告
& .\.venv\Scripts\python.exe -m scripts.benchmark_products --counts 1,5,10 --iterations 5
# The offline report includes parser-backed fixture samples, serial baseline,
# optimized phase timings, and the comparison summary.
# artifacts 清理:默认 DRY-RUN,只 --apply 才真删
# validate_artifacts_root hard-block 仓库外 root,越界直接抛 ValueError
& .\.venv\Scripts\python.exe -m scripts.prune_artifacts --root .\artifacts --older-than-days 7要点:
scripts.benchmark_products不实例化BrowserSession、不调用tab.get()、不访问真实网络;峰值max_queue_depth <= 2、writer_workers == 3、parser_workers == 2。scripts.prune_artifacts默认 DRY-RUN 列出候选文件;只有--apply才执行删除;root 必须解析到<repo>/artifacts,其它路径直接拒绝;跳过.gitkeep占位符,不删除目录本身。bundle.write()头部会先调cleanup_stale_siblings(max_age_seconds=24h)回收陈旧 staging/backup sibling(只匹配本 task 生成的.{name}.staging-*/.{name}.backup-*命名),不删.venv/、.worktrees/或任何未跟踪目录;本轮 finally 仍即时清理自己的 staging。- 网络等待(
--min-intervalsleep、live fetch 阻塞)不算本地优化指标;ProductRunMetrics只统计paced_operations / network_retry_count / detail_records / discovery_seconds / detail_seconds与local_output_ms / writers / records / bundle_bytes,日志里不出现 HTML、Cookie、请求头、profile 路径、API Key。 git diff --check在每次 commit 前必须无输出;outputs/、artifacts/、browser-profile/在仓库里只保留.gitkeep占位符。
# 推荐:用默认浏览器直接打开
Start-Process .\outputs\web-scraping-dev-demo\gallery.html
# 或在文件资源管理器双击
explorer .\outputs\web-scraping-dev-demo\gallery.html打开前请确认:
- 当前网络已断开,或 DevTools Network 面板已开启;
- 页面加载完成后 Network 面板无任何自动请求到
web-scraping.dev、CDN 或外部域名; - 商品卡片封面始终由内置 CSS/SVG 按分类和名称本地生成,不下载任何远程图片。
gallery.html 在纯 HTML + CSS + 原生 JavaScript 下提供以下能力,不依赖任何 CDN、字体或前端框架:
- 按商品名称模糊搜索;
- 按
category分类下拉筛选; - 按
status采集状态下拉筛选; - 按
current_price升序 / 降序切换; - 点击任意商品卡片 → 右侧/下方证据侧栏更新,展示完整名称、描述、价格、原价、币种、品牌、变体数、
product_id、采集时间、规范来源 URL、状态徽标与失败/部分成功原因; - 窄屏下卡片网格自动折叠为单列,侧栏始终可读;
- 失败商品仍以卡片形式展示,并在侧栏明确写出
error_message。
| 状态 | 含义 | 推荐处置 | CLI 退出码 |
|---|---|---|---|
SUCCESS |
必填字段全部解析成功 | 无 | 0 |
PARTIAL |
商品身份与价格成立,可选展示字段缺失 | 接受结果,记录原因 | 0 |
PAGE_CHANGED |
列表或详情关键结构不符合契约 | 先修适配器 + 离线 parser,跑通后再受控重跑 | 0 |
NETWORK_ERROR |
有限重试(2s / 5s 退避)后仍无法访问 | 下次 run 兜底 | 0 |
BLOCKED |
429、阻断页或明确访问限制 | 立即停止,不绕过 | 3 |
UNEXPECTED_ERROR |
未归类异常 | 查看诊断,定位根因后再跑 | 5 |
补充:
PARTIAL不用于掩盖必填字段缺失(缺 ID / 缺名称 / 缺当前价 / 页面已不是详情形态)—— 这些情况必须落到PAGE_CHANGED。- 失败记录仍进入
products.xlsx/products.json/gallery.html,保留product_id、来源 URL、状态、错误信息与采集时间,不为缺失字段编造默认值。 - 任何状态都不修改豆瓣电影 12 列 schema、不复用
movies工作表。
下列任意一种命中,适配器立即抛错、运行器立即终止本批,不做代理、验证码识别、Cookie 注入、降速绕过等任何形式的“再试一次”:
- HTTP 429;
- 明确阻断页(站方声明的 block / challenge 页面);
- 重定向到目标站阻断或挑战页;
- 登录、安全检查或凭证页面;
- 跳转到
web-scraping.dev之外域名; - 命中
/robots-disallowed等 robots 禁止路径; - 连续多页表明适配器已失效。
- CI(
.github/workflows/core-offline.yml)只做离线 fixture 测试:跑全量pytest、跑scripts.verify_core覆盖率门禁、跑pip check、跑git diff --check。不启动浏览器、不访问web-scraping.dev、不生成或上传products.xlsx/products.json/gallery.html。 - 验收离线产物时直接跑
python -m pytest tests/test_product_output_bundle.py tests/test_product_gallery.py -q,使用 fixture 在临时目录构造products.xlsx/products.json/gallery.html;不要把临时文件复制到 Git 跟踪目录。 - 人工受控验收必须显式传
--live-approved --max-products N,有头浏览器运行、至少跨两个列表分页、间隔>= 2秒;至少要核对三个产物的商品数与product_id完全一致。
outputs/、browser-profile/、artifacts/、日志、截图、HTML 证据、API Key、Cookie、请求头、用户身份相关字段一律不进 Git。.gitignore 已托管上述路径,本节新增的 outputs/web-scraping-dev-demo/、browser-profile/web-scraping-dev/ 也只保留 .gitkeep 占位。提交前 git diff --name-only 应只看到本任务批准的 README.md、tests/test_project_config.py(必要时加上 pyproject.toml)。
.github/workflows/core-offline.yml 是离线核心 release gate,故意做得很窄:
- CI 证明的:在干净 Python 3.11 runner 上,离线确定性正确性(
pytest -q+pytest-cov覆盖率 JSON 通过scripts.verify_core阈值检查 +git diff --check+ tracked runtime artifact / secret scan)。 - CI 故意不做的:
- 不启动浏览器(
DrissionPage/ Chromium / Edge /headed模式都不会出现); - 不访问
movie.douban.com或任何 live host; - 不读
MINIMAX_API_KEY或调用api.minimax.com; - 不执行真实 LLM 调用;
- 不创建或伪造
outputs/*.xlsx受控 workbook; - 不上传
browser-profile/、outputs/、artifacts/为 workflow artifact; - 不重建本地
.venv,直接pip install -e ".[dev]"。
- 不启动浏览器(
tests/test_project_config.py::test_core_ci_is_offline_and_runs_portable_verification把上述约束编码成配置契约,CI 改了工作流就必须同步通过这个测试。
一句话:CI 绿 ≠ release 绿。 CI 只证明"在隔离环境里离线契约没破",它无法证明"在 release host 上能起浏览器、能在真实豆瓣上工作、且本次 run 已被合规批准"。
一个 release 真正可发,除 CI 绿以外还至少需要:
- 本地
data:browser smoke:scripts/browser_smoke.py(data: URL、一次性、零网络)必须在 release host 上退出码 0;这证明浏览器路径在目标机器上仍可启动,CI runner 没机会验证这件事。 - 受控 workbook 校验:
- 落盘到
outputs/<run>.xlsx的 12 列、1–10 行(任意 1 ≤ N ≤ 10)受控 workbook; scripts/verify_core.verify_controlled_workbook(Path("outputs/<run>.xlsx"))必须返回{"data_rows": N, "unique_ids": N},workbook 任务 id 唯一、状态合法、collected_at全填;- 本任务不再要求独立的受控 evidence 文件,也不再读取任何外部审批字段 —— 那个责任现在落到 CLI 上的
--live-approved开关 + 1–10 行--max-queries上限。
- 落盘到
- CLI 授权记录可追溯:本次 run 的命令行必须能复现出来(
--live-approved --max-queries N --headed --min-interval 5),run 日志里要能引用到。
- 缺本地 browser smoke → BLOCKED,不发布。
- 缺受控 workbook /
verify_controlled_workbook不通过 → BLOCKED,不发布。 - 缺 CLI 授权的运行日志(
--live-approved与--max-queries)→ BLOCKED,不允许通过"跑一次真实豆瓣补一下"绕过 — 那等于把 CI 之外的 live 访问偷偷放进来,违反本仓库零网络、零 API Key 的红线。 - BLOCKED 必须显式记录在交付回报里("因 X 证据缺失,release 仍处 BLOCKED"),而不是悄悄标"完成"。
MINIMAX_API_KEY 继续延期接入,本轮 CI 与 release 流程不读这个变量、不调用 api.minimax.com,LLM 路径在 CI 里被彻底切断。等专门的 MiniMax 接入任务上线时,再把 secret scan pattern 同步扩展并复用同一份 scripts.verify_core 阈值。
把 release 从"CI 绿"推进到"READY_TO_PUSH"需要 12 步可逐条复制的 PowerShell 门禁:
- 精确 worktree 路径;
- 初始 git status 干净;
pytest -q;pytest --cov=app --cov-report=json:artifacts/coverage.json -v;python -m scripts.verify_core --coverage-json artifacts/coverage.json;python -m pip check;python -m scripts.browser_smoke(本地data:页面);- 受控 workbook 校验(
verify_controlled_workbook(Path("outputs/<run>.xlsx"))必须返回 1–10 个唯一 task); - secret scan;
- tracked runtime artifact scan;
git diff --check+git status --short;- 全部通过才能输出
READY_TO_PUSH。
每一步的命令、退出码、关键证据与失败处置都写在 docs/superpowers/tasks/core-13-release-readiness.md。本 README 不复制完整 PowerShell,避免与该 spec 漂移;遇到 release 决策时直接打开该 spec 跑第 1–12 步,再按 spec 末尾的 Acceptance checklist 与最终报告模板汇报。
第 8 步是合规硬门禁:
outputs/<run>.xlsx必须由本任务之外的真实受控 Demo run 落盘(带--live-approved --max-queries N启动),不能由 release 流程自己跑、不能伪造、不能下载。workbook 验证失败时整轮报告NOT_READY,绝不能为了得到绿色结果而访问真实豆瓣。