确认优先、单步推进、先样张再逐张生成,不能一上来完整批量生成

main
maomomo 2026-06-27 23:54:18 +08:00
parent 5d5e485e38
commit 8536f5ebce
7 changed files with 578 additions and 42 deletions

126
SKILL.md
View File

@ -25,11 +25,20 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
## 硬性约束 ## 硬性约束
- 如果本技能任何旧规则与“确认优先、单步推进”的流程冲突,以本节和“默认工作流”中的阶段门禁为准。
- 确认优先,单步推进;不要一上来就完整生成文章、全部配图或 ZIP。
- 读写 Markdown、JSON、CSV、HTML、代码或文本文件时显式使用 UTF-8。 - 读写 Markdown、JSON、CSV、HTML、代码或文本文件时显式使用 UTF-8。
- 处理用户上传或提供的 Markdown / PDF / 官方链接 / 参考文章时,必须先完整读取或说明无法读取的部分,再写最终稿。 - 处理用户上传或提供的 Markdown / PDF / 官方链接 / 参考文章时,必须先完整读取或说明无法读取的部分,再写最终稿。
- 费用、活动日期、奖励门槛、账户规则、返现、券商迎新、转账路径、数字货币出入金和合规事项,必须优先核查最新信息。优先级:用户提供的最新官方材料 > 官方页面 / 条款 PDF / App 显示 > 用户实测 > MAOMOMO 旧文 > 其他参考。 - 费用、活动日期、奖励门槛、账户规则、返现、券商迎新、转账路径、数字货币出入金和合规事项,必须优先核查最新信息。优先级:用户提供的最新官方材料 > 官方页面 / 条款 PDF / App 显示 > 用户实测 > MAOMOMO 旧文 > 其他参考。
- 没有被确认的数字、日期、奖励、链接、邀请码、产品名和规则,不得编造;可用 `【待确认:...】` 标出缺口。 - 没有被确认的数字、日期、奖励、链接、邀请码、产品名和规则,不得编造;可用 `【待确认:...】` 标出缺口。
- 用户素材包含明确“实测”结果时,必须用确定语气写结论;包含“以往惯例”“过往尿性”“过往经验”等历史依据时,必须用“大概率 / 按过往案例判断”的强概率语气,不要降级成“可能 / 疑似 / 大概”。 - 用户素材包含明确“实测”结果时,必须用确定语气写结论;包含“以往惯例”“过往尿性”“过往经验”等历史依据时,必须用“大概率 / 按过往案例判断”的强概率语气,不要降级成“可能 / 疑似 / 大概”。
- 写作前必须先给用户确认标题或标题方向、文章核心口径、`outline.md` 大纲、来源缺口、必选素材映射和配图风格。用户明确要求跳过确认时,未确认事实仍必须标 `【待确认:...】`
- 大纲未确认前,不得创建最终 `deck_spec.json`、`speech.md`、图片 prompt jobs、文章配图图片、ZIP 或用户指定的最终文章包。
- 出图前必须先把全部图片 prompt 写入文件,包含文件名、用途、放置位置、核心文案、完整 prompt 和依赖素材;不要在聊天里输出超长 prompt。
- 先只生成一张代表性风格确认图,优先封面图,保存为正式命名,例如 `origin_image/hsbc-mastercard-alipay-campaign-01-cover.png`。用户批准样张前不得批量生成剩余图片。
- 样张批准后,后续每张图片必须继承同一个生图后端、同一套视觉系统和同一种质量配置;不得为了加快速度擅自改成 `low`
- 每张最终图片必须来自确认后的图片生成后端:当前 agent 的内置图片生成工具,或用户确认后的 `scripts/maomomo_image_gen.py` API/CLI fallback。不得用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工文字覆盖冒充最终生成图。
- 生成图片按“一张图一个 job / 一次请求”推进。运行环境支持子 agent 时,样张通过后可以一图一个子 agent 并发处理;每个 dispatch、结果和 blocker 都必须写入状态文件,不能只靠聊天说明。
- 完整交付模式下,不能只在聊天里输出正文;必须生成 Markdown 文件、`assets/` 目录和 ZIP 包,除非用户明确说只要文字、只要大纲、不要图片、不要打包或先不生成文件。 - 完整交付模式下,不能只在聊天里输出正文;必须生成 Markdown 文件、`assets/` 目录和 ZIP 包,除非用户明确说只要文字、只要大纲、不要图片、不要打包或先不生成文件。
- 配图文件必须是独立 PNG不要用一张大拼图代替多张图不要只给提示词冒充图片文件。 - 配图文件必须是独立 PNG不要用一张大拼图代替多张图不要只给提示词冒充图片文件。
- 后续改稿必须更新文件本体;影响图片、来源区、标题、摘要或 ZIP 时同步更新。 - 后续改稿必须更新文件本体;影响图片、来源区、标题、摘要或 ZIP 时同步更新。
@ -38,49 +47,118 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
复杂任务使用一个用户可见 checklist并且同一时间只推进一个步骤 复杂任务使用一个用户可见 checklist并且同一时间只推进一个步骤
1. 识别任务模式、来源和事实缺口 1. 读取资料,整理来源状态和事实表
2. 确认文章大纲、口径和素材映射 2. 确认标题、核心口径、`outline.md` 大纲、素材映射、配图风格和生图后端
3. 写作或优化发布用 Markdown。 3. 写作或优化发布用 Markdown。
4. 规划并生成 / 准备配图 assets。 4. 输出全部图片 prompt 文件和图片任务状态文件。
5. QA、重新打包并交付最终文件。 5. 生成并确认一张样张。
6. 逐张生成剩余图片并记录 dispatch / result / blocker。
7. QA、返修、生成 `speech.md` 或备注稿。
8. 打包并交付最终文件。
不要因为聊天里说“已完成”就标记完成;用实际文件、已读取来源、已生成图片和 ZIP 作为完成证据。 不要因为聊天里说“已完成”就标记完成;用实际文件、已读取来源、已生成图片和 ZIP 作为完成证据。
## 默认工作流 ## 默认工作流
一句话版:读资料 -> 给标题 / 大纲 / 风格让用户确认 -> 写文章 -> 输出图片 prompts 文件让用户改 -> 先出一张图确认 -> 逐张生成剩余图 -> QA -> 打包交付。
1. 判断任务模式。 1. 判断任务模式。
- 完整交付、只要大纲、只要正文、只要配图、SEO 优化、改稿更新、小红书发布版。 - 完整交付、只要大纲、只要正文、只要配图、SEO 优化、改稿更新、小红书发布版。
- 判断是否涉及金融 / 活动 / 合规事实核查。 - 判断是否涉及金融 / 活动 / 合规事实核查。
- 完整交付或打包任务先读 `references/delivery-rules.md` - 完整交付或打包任务先读 `references/delivery-rules.md`;需要配图时先读 `references/visual-assets.md`
2. 读取来源并建立事实表。 2. 读取源材料并建立事实表。
- 先读用户提供的官方页面、PDF、旧文、参考链接、截图或素材不先动手写最终稿。
- 涉及官方规则、活动或金融产品时,先读 `references/source-and-fact-check.md` - 涉及官方规则、活动或金融产品时,先读 `references/source-and-fact-check.md`
- 列出用户提供的所有来源,并标记已读 / 未读 / 无法读取。 - 列出所有来源的已读 / 未读 / 无法读取状态。
- 提炼日期、门槛、奖励、限制、费用、适用对象、实测口径和冲突点。 - 明确区分:已官宣事实、用户实测、过往惯例判断、待确认内容。
- 确认主题、目标读者、文章目标、章节数量或图片数量、品牌 / 风格限制,以及必须放入文章的图片素材。
3. 确认范围和大纲。 3. 规划并确认 `outline.md`
- 写最终稿前先给出文章类型、目标读者、标题方向、核心口径、章节结构、来源缺口和图片规划。 - 写最终稿前先给出推荐标题或使用用户指定标题、文章核心口径、章节大纲、来源缺口、图片规划和必选素材映射。
- `outline.md` 应包含每个大小标题、3-5 个要点、页面 / 图位角色、视觉想法、必选素材路径或附件名。
- 有必选素材时,必须先确认“素材到章节 / 图位”的映射,再进入风格选择或图片生成。
- 若存在多个活动分支、旧版活动、小众资格或来源冲突,先建议主文范围,等用户确认后再写最终稿。 - 若存在多个活动分支、旧版活动、小众资格或来源冲突,先建议主文范围,等用户确认后再写最终稿。
- 用户明确要求“直接生成”时,可以继续执行,但所有未确认事实必须标 `【待确认:...】` - 用户明确要求“直接生成”时,可以继续执行,但所有未确认事实必须标 `【待确认:...】`
4. 写作或优化文章。 4. 确认视觉风格。
- 写作前读 `references/style-guide.md` - 若用户没有指定视觉方向,读 `references/recommended-styles.md`,给 2-3 个 MAOMOMO 配图风格选项并推荐一个。
- 先结论、再步骤、再坑点段落短数字具体App 菜单名 / 操作名尽量写清楚。 - 用户提供参考图、PPT、PDF 或旧文章风格时,先总结可复用的配色、版式、字体气质、猫咪元素、信息密度和禁用项。
- 确认后,整篇文章保持同一视觉系统:稳定配色、猫咪造型、标题区、图标语言、品牌识别和信息密度;不同图位可以变化构图。
5. 确认生图后端。
- 出第一张图前读 `references/image-generation.md`
- 优先主动检查当前 agent 是否可调用内置图片生成工具,并向用户说明检查结果和准备使用的后端。
- 只有内置工具不可用、能力不足、用户明确要求 API/CLI或需要使用第三方图片 API 时,才走 `scripts/maomomo_image_gen.py` fallback。
- 相信用户环境变量或配置文件里的 baseURL、model、quality除非报错或用户要求不要重新索要配置。
6. 写作或优化文章。
- 用户确认标题、大纲、口径和素材映射后,再读 `references/style-guide.md` 写发布用 Markdown。
- 不要把“推测 / 大概率 / 过往惯例”写成“已官宣”。
- 金融活动文章尤其要分清官方条款、App 显示、用户实测和经验判断。
- 发布用 Markdown 使用 frontmatter、H1、太长不看版、正文、最后提醒和来源区。 - 发布用 Markdown 使用 frontmatter、H1、太长不看版、正文、最后提醒和来源区。
- SEO 优化既要改标题、摘要、slug / tags / categories、图片 alt也要清理无效来源和发布残留。 - SEO 优化既要改标题、摘要、slug / tags / categories、图片 alt也要清理无效来源和发布残留。
5. 配图规划和生成。 7. 创建项目目录和任务文件。
- 需要配图时读 `references/visual-assets.md` - 完整交付默认使用独立项目目录;如果用户没指定位置,使用当前工作目录或源文件所在目录。
- 若用户没有指定视觉方向,读 `references/recommended-styles.md`,先提供 2-3 个 MAOMOMO 配图风格选项并推荐一个;确认后整篇文章保持同一视觉身份。 - 推荐结构:
- 先生成图片清单类型、标题、放置位置、核心文案、文件名、alt text、风格、是否依赖用户截图。
- 每张图独立生成或准备为高清 PNG放入 `assets/`,用相对路径插入 Markdown。
- 优先使用当前 agent 的内置图片生成工具;当用户要求 API/CLI、内置工具不可用或需要批量 manifest 时,读 `references/image-generation.md` 并使用 `scripts/maomomo_image_gen.py`
- 没有真实截图时只能生成“示意图 / 信息图”,不能伪造官方 App 截图。
6. 打包与最终 QA。 ```text
{base_dir}/{article_slug}/
├── origin_image/
│ ├── {article_slug}_01_cover.png
│ └── ...
├── assets/
│ ├── {article_slug}_01_cover.png
│ └── ...
├── prompts/
│ ├── {article_slug}_01.json
│ └── ...
├── deck_spec.json
├── slide_jobs.json
├── slide_run_state.json
├── outline.md
├── article.md
├── speech.md
└── {article_slug}.zip
```
- `origin_image/` 保存图片后端生成并通过 QA 的原始最终图;`assets/` 保存 Markdown 实际引用的发布图片。两者可以是同名复制件,但不得只保留临时图。
- `deck_spec.json` 记录文章级上下文、确认后的风格、生图后端、样张生成方法 `sample_generation_method` 和图片任务列表。
- `prompts/{article_slug}_XX.json` 记录每张图的文件名、用途、放置位置、核心文案、完整 prompt、依赖素材和输出路径。
- `slide_jobs.json``slide_run_state.json` 记录每个图片 job 的 pending / dispatched / recorded / blocked 状态;使用 `scripts/maomomo_job_state.py` 初始化和记录状态。
8. 出图前整理全部 prompt。
- 每张图的文件名、用途、放置位置、核心文案、完整 prompt、alt text、风格、依赖素材和输出路径先写入 `prompts/` 或 manifest 文件。
- 不要在聊天里输出超长 prompt告诉用户可直接编辑 prompt 文件。
- 如果用户修改 prompt先同步文件再生成图片。
9. 先生成一张样张。
- 优先生成封面图或最能代表文章视觉节奏的一张内容图。
- 样张直接保存为正式文件名,例如 `origin_image/hsbc-mastercard-alipay-campaign-01-cover.png`
- 展示样张并等待用户确认风格、内容、中文文字质量和视觉密度。
- 如果用户不满意,先改该图 prompt再重出同一张样张确认前不得生成剩余图片。
- 样张确认后,在 `deck_spec.json` 记录 `sample_generation_method`,包括 backend、tool/command、mode、quality、model/config、prompt 文件、approved_sample_path 和 handoff_rule。
10. 逐张生成剩余图片。
- 样张批准后,剩余图片必须使用同一后端、同一质量配置和同一视觉系统。
- 每张图独立生成或准备为高清 PNG先进入 `origin_image/`,通过 QA 后同步到 `assets/`,并用相对路径插入 Markdown。
- 图片生成必须一张张来:一张图一个 prompt job、一次图片请求、一次 QA。不要一次请求多张图不要为了快降低质量。
- 如果运行环境支持子 agent样张通过后可以一图一个子 agent 并发处理;父 agent 负责状态文件、QA、Markdown、`speech.md` 和打包。
- 没有真实截图时只能生成“示意图 / 信息图”,不能伪造官方 App 截图。
- 不要用 Playwright 替代图片生成Playwright 只用于已有 HTML / SVG / 网页截图检查。
11. 记录状态、QA 和返修。
- 每个图片 job 的 dispatch、结果和 blocker 都用 `scripts/maomomo_job_state.py` 写入 `slide_jobs.json` / `slide_run_state.json`,不能只靠聊天说明完成。
- 检查文字清晰度、中文乱码、金额 / 日期 / 规则是否残留旧口径、标题截断、风格一致性、必选素材是否正确、元素是否重叠、是否伪造截图或 Logo。
- 严重问题重新生成该单张;局部问题优先使用已确认后端的编辑能力修复,不得本地手工覆盖关键文字冒充模型输出。
12. 生成 `speech.md`、组装文章包并最终 QA。
- `speech.md` 用作每张图的备注 / 讲解稿,标题格式使用 `## Slide N: 标题`,便于和图片顺序映射。
- 组装最终文章包时,把 `origin_image/{article_slug}_XX.png` 按顺序同步到 Markdown 引用的 `assets/`,并确保 `article.md`、图片路径、`speech.md`、来源区和 ZIP 都是最新版本。
- 完整交付、文件交付或改稿打包前读 `references/delivery-rules.md` - 完整交付、文件交付或改稿打包前读 `references/delivery-rules.md`
- 检查 Markdown 图片路径、来源区、编辑痕迹、待确认项、金融风险提示、用户要求删除内容和 ZIP 内容。 - QA 必须检查 Markdown 图片路径、assets 是否存在、来源区只保留实际使用来源、正文无编辑痕迹、图片无错误日期 / 金额 / 未确认结论、用户要求删除内容不残留、ZIP 内容正确
- 最终回复保持简短:给出 Markdown、assets 目录和 ZIP 路径,以及必要限制。 - 最终回复保持简短:给出项目目录、文章路径、图片目录、ZIP 路径、图片数量、生图后端、状态记录情况、备注写入情况和已知限制。
## 模式选择 ## 模式选择
@ -123,5 +201,5 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
- `references/visual-assets.md`配图类型、猫咪视觉、文件命名、Markdown 插图、禁止伪造截图和图片 QA。 - `references/visual-assets.md`配图类型、猫咪视觉、文件命名、Markdown 插图、禁止伪造截图和图片 QA。
- `references/recommended-styles.md`:推荐配图风格、适用场景、推荐话术和提示词片段。 - `references/recommended-styles.md`:推荐配图风格、适用场景、推荐话术和提示词片段。
- `references/image-styles/*.md`:具体出图风格 brief借鉴 `codex-ppt-skill` 风格库结构并适配 MAOMOMO 文章配图。 - `references/image-styles/*.md`:具体出图风格 brief借鉴 `codex-ppt-skill` 风格库结构并适配 MAOMOMO 文章配图。
- `references/image-generation.md`:图片生成后端选择、`scripts/maomomo_image_gen.py` 用法、manifest 和故障处理。 - `references/image-generation.md`:图片生成后端选择、`scripts/maomomo_image_gen.py` 用法、`scripts/maomomo_job_state.py` 状态记录、manifest 和故障处理。
- `references/delivery-rules.md`完整交付、ZIP、更新打包、附件策略和最终检查清单。 - `references/delivery-rules.md`完整交付、ZIP、更新打包、附件策略和最终检查清单。

View File

@ -1,7 +1,7 @@
interface: interface:
display_name: "MAOMOMO 文章生成器" display_name: "MAOMOMO 文章生成器"
short_description: "阶段化生成 MAOMOMO 文章、多风格配图和 ZIP 发布包" short_description: "分阶段确认后生成 MAOMOMO 文章、配图和 ZIP 发布包"
default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和口径,再确认配图风格,最后交付 Markdown、assets 独立配图目录和 ZIP 包。" default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和事实表,再确认标题、口径、大纲、素材映射、配图风格和生图后端;写文章后先输出图片 prompts 文件,只生成一张样张确认,再逐张生成剩余图片,最后交付 Markdown、assets 独立配图目录和 ZIP 包。"
policy: policy:
allow_implicit_invocation: true allow_implicit_invocation: true

View File

@ -2,6 +2,8 @@
本文件用于完整文章、配图、发布包、打包、一键下载和后续更新任务。只要用户没有明确说“只要文字”“不要图片”“不要打包”“先不要生成文件”“先只要大纲”,默认按完整交付模式执行。 本文件用于完整文章、配图、发布包、打包、一键下载和后续更新任务。只要用户没有明确说“只要文字”“不要图片”“不要打包”“先不要生成文件”“先只要大纲”,默认按完整交付模式执行。
完整交付模式指“最终交付物必须完整”,不代表可以跳过确认一次性全量生成。执行时必须遵守 `SKILL.md` 的阶段门禁:先读资料和事实表,再确认标题 / 口径 / 大纲 / 素材映射 / 配图风格 / 生图后端,随后写文章、整理 prompts、先出一张样张样张确认后再逐张生成剩余图片。
## 强制完整交付规则 ## 强制完整交付规则
当用户请求包含以下任一表达时,必须默认进入“完整交付模式”: 当用户请求包含以下任一表达时,必须默认进入“完整交付模式”:
@ -19,13 +21,15 @@
- “生成文章 + 配图” - “生成文章 + 配图”
- “最终交付” - “最终交付”
完整交付模式必须一次性完成: 完整交付模式最终必须完成以下交付物,但必须分阶段推进
1. 读取并核对用户提供的所有官方链接、PDF、参考文章和条款。 1. 读取并核对用户提供的所有官方链接、PDF、参考文章和条款。
2. 输出发布用 Markdown 文件,而不是只在聊天框展示正文。 2. 在写最终稿前给用户确认标题、核心口径、`outline.md` 大纲、来源缺口、必选素材映射和配图风格。
3. 生成或准备多张独立高清 PNG 配图,放入 `assets/` 目录。 3. 用户确认后输出发布用 Markdown 文件,而不是只在聊天框展示正文。
4. 将图片以相对路径插入 Markdown例如 `![图片说明](assets/hsbc-mastercard-alipay-campaign-01-cover.png)` 4. 出图前把全部图片 prompt 写入 `prompts/` 或 manifest 文件,让用户可直接编辑。
5. 创建 ZIP 包,结构类似: 5. 先生成并确认一张样张,优先封面图;样张确认后再逐张生成剩余独立高清 PNG。
6. 将通过 QA 的图片同步到 `assets/` 目录,并以相对路径插入 Markdown例如 `![图片说明](assets/hsbc-mastercard-alipay-campaign-01-cover.png)`
7. 创建 ZIP 包,结构类似:
```text ```text
article_package.zip article_package.zip
@ -36,7 +40,7 @@ article_package.zip
hsbc-mastercard-alipay-campaign-03-timeline.png hsbc-mastercard-alipay-campaign-03-timeline.png
``` ```
6. 最终回复只给下载链接和极简说明 8. 最终回复只给路径和极简说明,同时说明图片数量、生图后端、状态记录和已知限制
如果因为工具限制、图片生成失败、文件写入失败或资料缺失导致无法完成完整交付,必须明确说明失败点,并尽量交付已经完成的部分。不要只输出配图提示词代替图片文件,不要只输出文章正文代替 Markdown 文件。 如果因为工具限制、图片生成失败、文件写入失败或资料缺失导致无法完成完整交付,必须明确说明失败点,并尽量交付已经完成的部分。不要只输出配图提示词代替图片文件,不要只输出文章正文代替 Markdown 文件。
@ -59,14 +63,18 @@ article_package.zip
当用户要求“配图也按 Skill”“高清配图”“精美配图”“生成配图”“文章和配图一起”时 当用户要求“配图也按 Skill”“高清配图”“精美配图”“生成配图”“文章和配图一起”时
- 必须生成独立 PNG 图片文件,而不是只给提示词。 - 必须生成独立 PNG 图片文件,而不是只给提示词。
- 出图前必须先写入 prompt 文件,包含文件名、用途、放置位置、核心文案、完整 prompt、alt text、风格和依赖素材。
- 必须先生成一张样张,优先封面图;用户确认前不得生成剩余图片。
- 样张确认后,每张图按一个 job / 一次请求 / 一次 QA 推进;运行环境支持子 agent 时,可以一图一个子 agent 并发。
- 不要只生成一张大长图替代所有配图。 - 不要只生成一张大长图替代所有配图。
- 每张图必须有独立主题例如封面图、活动总览图、时间线图、重点规则图、交易法图、避坑图、FAQ 图。 - 每张图必须有独立主题例如封面图、活动总览图、时间线图、重点规则图、交易法图、避坑图、FAQ 图。
- 不得使用未经用户提供的真实银行 Logo、券商 Logo 或支付品牌 Logo。 - 不得使用未经用户提供的真实银行 Logo、券商 Logo 或支付品牌 Logo。
- 不得伪造真实 App 截图。 - 不得伪造真实 App 截图。
- 生成图应明确是“MAOMOMO 风格示意图 / 信息图”。 - 生成图应明确是“MAOMOMO 风格示意图 / 信息图”。
- 生成后必须保存`assets/`,并插入 Markdown。 - 生成后的原始最终图先保存到 `origin_image/`,通过 QA 后同步`assets/`,并插入 Markdown。
- 图片文件名使用稳定英文小写命名,并包含主题前缀,例如 `hsbc-mastercard-alipay-campaign-01-cover.png` - 图片文件名使用稳定英文小写命名,并包含主题前缀,例如 `hsbc-mastercard-alipay-campaign-01-cover.png`
- 不要使用中文文件名或缺少主题前缀的通用文件名作为最终 assets 文件名,避免站点路径兼容和多文章混淆问题。 - 不要使用中文文件名或缺少主题前缀的通用文件名作为最终 assets 文件名,避免站点路径兼容和多文章混淆问题。
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代 GPT Image2 / 图片 API 生成最终配图。
## 图片生成偏差处理 ## 图片生成偏差处理
@ -118,6 +126,11 @@ article_package.zip
交付前必须逐项确认: 交付前必须逐项确认:
- Markdown 文件已生成。 - Markdown 文件已生成。
- `outline.md` 已按用户确认版本保存。
- `prompts/` 中每张图的 prompt 文件已保存,且与最终图片数量和顺序一致。
- 一张样张已确认,后续图片继承同一生图后端和视觉系统。
- `slide_jobs.json` / `slide_run_state.json` 已记录图片任务状态;如缺少记录,最终回复必须说明原因。
- `origin_image/` 中最终原始图片存在。
- `assets/` 目录已生成。 - `assets/` 目录已生成。
- 所有 Markdown 图片路径都能对应到真实图片文件。 - 所有 Markdown 图片路径都能对应到真实图片文件。
- 图片是多张独立 PNG不是一张大图代替全部。 - 图片是多张独立 PNG不是一张大图代替全部。

View File

@ -11,6 +11,15 @@
不要因为脚本存在就强制使用脚本。若内置图片工具可用,仍优先使用内置工具生成独立 PNG再保存到 `assets/` 不要因为脚本存在就强制使用脚本。若内置图片工具可用,仍优先使用内置工具生成独立 PNG再保存到 `assets/`
生成第一张图前,必须向用户确认:
- 已检查哪种内置图片生成工具是否可用。
- 准备使用的生图后端。
- 是否需要 `scripts/maomomo_image_gen.py` fallback以及原因。
- 当前会沿用环境变量或配置文件中的 baseURL、model、quality不会擅自改成 `low`
确认后,整篇文章所有图片保持同一个生图后端和质量配置。只有用户明确要求切换,或当前后端无法完成必需能力时,才重新确认后端。
## 配置来源 ## 配置来源
脚本按以下顺序读取配置: 脚本按以下顺序读取配置:
@ -27,7 +36,7 @@
```bash ```bash
python3 scripts/maomomo_image_gen.py generate \ python3 scripts/maomomo_image_gen.py generate \
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \ --out article/origin_image/hsbc-mastercard-alipay-campaign-01-cover.png \
--style warm-fintech-guide \ --style warm-fintech-guide \
--title "汇丰 Mastercard 支付宝活动" \ --title "汇丰 Mastercard 支付宝活动" \
--image-type "封面图" \ --image-type "封面图" \
@ -38,7 +47,7 @@ python3 scripts/maomomo_image_gen.py generate \
```bash ```bash
python3 scripts/maomomo_image_gen.py generate \ python3 scripts/maomomo_image_gen.py generate \
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \ --out article/origin_image/hsbc-mastercard-alipay-campaign-01-cover.png \
--style data-card-dashboard \ --style data-card-dashboard \
--title "返现计算图" \ --title "返现计算图" \
--image-type "计算图" \ --image-type "计算图" \
@ -54,7 +63,7 @@ manifest 示例:
{ {
"images": [ "images": [
{ {
"file_name": "assets/hsbc-mastercard-alipay-campaign-01-cover.png", "file_name": "origin_image/hsbc-mastercard-alipay-campaign-01-cover.png",
"type": "封面图", "type": "封面图",
"title": "汇丰 Mastercard 支付宝活动", "title": "汇丰 Mastercard 支付宝活动",
"core_text": "先报名,再看门槛和封顶", "core_text": "先报名,再看门槛和封顶",
@ -63,7 +72,7 @@ manifest 示例:
"aspect_ratio": "16:9" "aspect_ratio": "16:9"
}, },
{ {
"file_name": "assets/hsbc-mastercard-alipay-campaign-02-calculation.png", "file_name": "origin_image/hsbc-mastercard-alipay-campaign-02-calculation.png",
"type": "计算图", "type": "计算图",
"title": "怎么刷到刚刚好", "title": "怎么刷到刚刚好",
"core_text": "HK$190 ÷ 9% ≈ HK$2112超过部分没有额外收益", "core_text": "HK$190 ÷ 9% ≈ HK$2112超过部分没有额外收益",
@ -81,7 +90,14 @@ python3 scripts/maomomo_image_gen.py batch \
--base-dir article --base-dir article
``` ```
`file_name` 必须与 Markdown 中的相对路径一致,且最终文件必须真实存在。 `file_name` 必须与本次生成目录一致,且最终文件必须真实存在。发布用 Markdown 默认引用 `assets/`,因此输出到 `origin_image/` 后要在 QA 通过时同步到同名 `assets/` 文件。
重要限制:
- 不要在样张确认前运行批量生成。
- 默认用 `generate` 单张生成;`batch` 只用于用户明确授权的全量生成或 dry-run prompt 输出。
- 即使使用 `batch`,脚本也是逐项请求图片接口;生成后仍要逐张 QA并把通过的图片同步到 `assets/`
- 发布用 Markdown 默认引用 `assets/`,因此如果 manifest 输出到 `origin_image/`,必须在 QA 后复制到对应 `assets/` 路径。
## 内置风格名 ## 内置风格名
@ -111,3 +127,69 @@ python3 scripts/maomomo_image_gen.py styles
- 接口失败:保留已生成图片,说明失败项,必要时用 `--dry-run` 交付 prompt 清单作为临时排查材料。 - 接口失败:保留已生成图片,说明失败项,必要时用 `--dry-run` 交付 prompt 清单作为临时排查材料。
- 图片文字乱码或金额错误:重新生成该单张图片;不要手工覆盖文字伪装成模型输出。 - 图片文字乱码或金额错误:重新生成该单张图片;不要手工覆盖文字伪装成模型输出。
- 图片不符合风格:先更新 manifest 的 `style` / `core_text` / `extra`,再重跑对应项。 - 图片不符合风格:先更新 manifest 的 `style` / `core_text` / `extra`,再重跑对应项。
- 图片生成超时:可以对同一张图重试较长超时;不要为了快擅自降低 `quality`
- Playwright 只用于已有 HTML / SVG / 网页截图检查,不能替代 GPT Image2 / 图片 API 生成海报、OG 图、卡片图或文章配图。
## 状态记录
每张图片 job 的 dispatch、result 和 blocker 都要写入项目状态文件。推荐状态文件:
```text
{article_dir}/slide_jobs.json
{article_dir}/slide_run_state.json
```
记录字段至少包括:
- job id例如 `hsbc-mastercard-alipay-campaign_01`
- prompt 文件,例如 `prompts/hsbc-mastercard-alipay-campaign_01.json`
- 输出原图,例如 `origin_image/hsbc-mastercard-alipay-campaign-01-cover.png`
- 发布图,例如 `assets/hsbc-mastercard-alipay-campaign-01-cover.png`
- 状态:`pending`、`dispatched`、`recorded`、`blocked`
- 生图后端、agent id、QA note、失败原因
使用 `scripts/maomomo_job_state.py` 更新状态,不要手工改 JSON 后声称完成。
初始化状态:
```bash
python3 scripts/maomomo_job_state.py init article \
--selected-backend "built-in image tool"
```
查看状态:
```bash
python3 scripts/maomomo_job_state.py status article
```
记录 dispatch
```bash
python3 scripts/maomomo_job_state.py dispatch article \
--job hsbc-mastercard-alipay-campaign_02 \
--agent-id "<agent id>" \
--prompt-file prompts/hsbc-mastercard-alipay-campaign_02.json
```
记录结果并同步到 `origin_image/``assets/`
```bash
python3 scripts/maomomo_job_state.py result article \
--job hsbc-mastercard-alipay-campaign_02 \
--agent-id "<agent id>" \
--backend-used "built-in image tool" \
--selected-source /absolute/path/to/generated.png \
--qa-note "文字清楚,风格与样张一致。"
```
记录 blocker
```bash
python3 scripts/maomomo_job_state.py blocker article \
--job hsbc-mastercard-alipay-campaign_02 \
--agent-id "<agent id>" \
--reason "选定图片后端在 worker 中不可用"
```
如果状态脚本失败,最终报告必须说明原因,并附上状态文件当前路径和未完成 job。

View File

@ -7,9 +7,15 @@
- 完整文章默认配独立 PNG短文章 4-6 张,普通攻略 6-8 张,长教程 / 多活动整理 8-12 张。 - 完整文章默认配独立 PNG短文章 4-6 张,普通攻略 6-8 张,长教程 / 多活动整理 8-12 张。
- 不为凑数量硬加图;每张图必须服务正文。 - 不为凑数量硬加图;每张图必须服务正文。
- 每张图独立生成或准备,不要用一张拼图、长图或总图代替全部。 - 每张图独立生成或准备,不要用一张拼图、长图或总图代替全部。
- 出图前先把全部图片 prompt 写入 `prompts/` 或 manifest 文件;不要在聊天里输出超长 prompt 代替文件。
- 先只生成一张样张,优先封面图;用户确认样张前不得生成剩余图片。
- 样张确认后,后续图片使用同一生图后端、同一视觉系统和同一质量配置。
- 图片生成按“一张图一个 job / 一次请求 / 一次 QA”推进运行环境支持子 agent 时,可以一图一个子 agent 并发,但不要一次请求多张图。
- 最终原始图保存到 `origin_image/`,通过 QA 后同步到 Markdown 引用的 `assets/`
- 生成图必须是“示意图 / 信息图 / 教程视觉”,不得伪造真实 App 截图、账单或官方页面。 - 生成图必须是“示意图 / 信息图 / 教程视觉”,不得伪造真实 App 截图、账单或官方页面。
- 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。 - 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。
- 每张图必须包含清晰可见的 `MAOMOMO` 标识。 - 每张图必须包含清晰可见的 `MAOMOMO` 标识。
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代图片生成后端。
## 风格选择 ## 风格选择
@ -66,7 +72,7 @@ C. handdrawn-note便签和手绘箭头更明显适合避坑经验和保姆
## 图片清单格式 ## 图片清单格式
生成图片前先规划清单: 生成图片前先规划清单,并同步写入 `prompts/{article_slug}_XX.json` 或图片 manifest。每个 prompt 文件至少包含文件名、类型、放置位置、核心文案、alt text、风格、依赖素材、输出路径和完整 prompt。
```markdown ```markdown
| 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 风格 | 依赖素材 | | 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 风格 | 依赖素材 |
@ -75,13 +81,35 @@ C. handdrawn-note便签和手绘箭头更明显适合避坑经验和保姆
| hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | handdrawn-note | 用户截图 1 | | hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | handdrawn-note | 用户截图 1 |
``` ```
确认用户不需要先看清单时可以直接执行但最终必须让文件名、alt text 和插图位置与正文一致。 确认用户不需要先看清单时,可以不在聊天里展开清单,但仍必须写入 prompt 文件并遵守“先样张、后逐张生成”的门禁。最终必须让文件名、alt text 和插图位置与正文一致。
推荐单图 prompt JSON
```json
{
"id": "hsbc-mastercard-alipay-campaign_01",
"file_name": "hsbc-mastercard-alipay-campaign-01-cover.png",
"origin_out": "origin_image/hsbc-mastercard-alipay-campaign-01-cover.png",
"asset_out": "assets/hsbc-mastercard-alipay-campaign-01-cover.png",
"type": "封面图",
"placement": "标题下方",
"core_text": "先报名,再看门槛和封顶",
"alt_text": "MAOMOMO 汇丰 Mastercard 支付宝返现活动封面图",
"style": "warm-fintech-guide",
"required_assets": [],
"prompt": "完整图片生成 prompt包含风格、文案、约束和禁止项"
}
```
## 文件命名 ## 文件命名
图片文件名必须 SEO 友好,并包含主题前缀: 图片文件名必须 SEO 友好,并包含主题前缀:
```text ```text
origin_image/
hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png
assets/ assets/
hsbc-mastercard-alipay-campaign-01-cover.png hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png hsbc-mastercard-alipay-campaign-02-overview.png
@ -120,6 +148,8 @@ assets/
图片说明必须根据图片内容写,不要都写成“配图”。 图片说明必须根据图片内容写,不要都写成“配图”。
`origin_image/` 不直接写进 Markdown除非用户明确要求引用原始图目录。发布用 Markdown 默认引用 `assets/`
## 猫咪视觉识别 ## 猫咪视觉识别
基础方向: 基础方向:
@ -163,10 +193,13 @@ MAOMOMO 风格中文金融教程信息图,干净明亮白色背景,暖橙色
交付前检查: 交付前检查:
- 样张已被用户确认,后续图片没有切换生图后端或视觉风格。
- 文件真实存在,格式为 PNG。 - 文件真实存在,格式为 PNG。
- `origin_image/` 原始最终图和 `assets/` 发布图对应。
- Markdown 路径能打开对应文件。 - Markdown 路径能打开对应文件。
- 图片文字清楚,无乱码、错字和过期金额。 - 图片文字清楚,无乱码、错字和过期金额。
- 标题、金额、日期、规则与最终文章口径一致。 - 标题、金额、日期、规则与最终文章口径一致。
- 没有真实个人信息。 - 没有真实个人信息。
- 没有伪造官方 App 页面或未经授权的品牌 Logo 场景。 - 没有伪造官方 App 页面或未经授权的品牌 Logo 场景。
- 每张图独立主题明确,不是同一画面的机械变体。 - 每张图独立主题明确,不是同一画面的机械变体。
- `slide_jobs.json` / `slide_run_state.json` 中没有未处理的 `pending`、`dispatched` 或 `blocked` job如有不能声称完整交付完成。

View File

@ -221,7 +221,7 @@ def request_image(prompt: str, *, size: str, quality: str, output_format: str) -
) )
try: try:
with urllib.request.urlopen(req, timeout=180) as resp: with urllib.request.urlopen(req, timeout=300) as resp:
data = json.loads(resp.read().decode("utf-8")) data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc: except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace") detail = exc.read().decode("utf-8", "replace")
@ -238,7 +238,7 @@ def request_image(prompt: str, *, size: str, quality: str, output_format: str) -
if isinstance(first.get("b64_json"), str): if isinstance(first.get("b64_json"), str):
return base64.b64decode(first["b64_json"]) return base64.b64decode(first["b64_json"])
if isinstance(first.get("url"), str): if isinstance(first.get("url"), str):
with urllib.request.urlopen(first["url"], timeout=180) as resp: with urllib.request.urlopen(first["url"], timeout=300) as resp:
return resp.read() return resp.read()
die("图片接口响应中没有 b64_json 或 url。") die("图片接口响应中没有 b64_json 或 url。")
return b"" return b""

View File

@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""记录 MAOMOMO 文章配图任务状态。
本脚本用于把图片生成进度写入状态文件避免只用聊天记录声称已完成
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import shutil
import sys
from typing import Any, Dict, Iterable, List, Optional, Tuple
JOBS_FILE = "slide_jobs.json"
STATE_FILE = "slide_run_state.json"
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def die(message: str, code: int = 1) -> None:
print(f"错误:{message}", file=sys.stderr)
raise SystemExit(code)
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def write_json(path: Path, data: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as handle:
json.dump(data, handle, ensure_ascii=False, indent=2)
handle.write("\n")
def read_json(path: Path) -> Any:
try:
return json.loads(read_text(path))
except FileNotFoundError:
die(f"找不到文件:{path}")
except json.JSONDecodeError as exc:
die(f"JSON 格式错误:{path}: {exc}")
def project_path(project: str) -> Path:
path = Path(project).expanduser().resolve()
if not path.exists():
die(f"项目目录不存在:{path}")
if not path.is_dir():
die(f"不是项目目录:{path}")
return path
def resolve_in_project(project: Path, value: str) -> Path:
path = Path(value)
if not path.is_absolute():
path = project / path
return path.resolve()
def load_job_doc(project: Path) -> Dict[str, Any]:
raw = read_json(project / JOBS_FILE)
if isinstance(raw, list):
return {"version": 1, "jobs": raw}
if not isinstance(raw, dict):
die(f"{JOBS_FILE} 必须是对象或数组。")
jobs = raw.get("jobs")
if not isinstance(jobs, list):
die(f"{JOBS_FILE} 缺少 jobs 数组。")
return raw
def load_state_doc(project: Path) -> Dict[str, Any]:
path = project / STATE_FILE
if not path.exists():
return {"version": 1, "events": []}
raw = read_json(path)
if not isinstance(raw, dict):
die(f"{STATE_FILE} 必须是对象。")
if not isinstance(raw.get("events"), list):
raw["events"] = []
return raw
def save_docs(project: Path, job_doc: Dict[str, Any], state_doc: Dict[str, Any]) -> None:
write_json(project / JOBS_FILE, job_doc)
write_json(project / STATE_FILE, state_doc)
def job_key(job: Dict[str, Any]) -> Optional[str]:
for key in ("id", "job_id", "slide", "name"):
value = job.get(key)
if isinstance(value, str) and value:
return value
return None
def find_job(job_doc: Dict[str, Any], wanted: str) -> Dict[str, Any]:
for job in job_doc["jobs"]:
if isinstance(job, dict) and job_key(job) == wanted:
return job
die(f"未找到图片 job{wanted}")
return {}
def event(state_doc: Dict[str, Any], kind: str, job_id: str, payload: Dict[str, Any]) -> None:
state_doc.setdefault("events", []).append(
{
"time": now_iso(),
"event": kind,
"job": job_id,
**payload,
}
)
def infer_origin_out(prompt_path: Path, data: Dict[str, Any]) -> str:
for key in ("origin_out", "out", "output", "selected_output"):
value = data.get(key)
if isinstance(value, str) and value:
return value
file_name = data.get("file_name")
if isinstance(file_name, str) and file_name:
return str(Path("origin_image") / Path(file_name).name)
return str(Path("origin_image") / f"{prompt_path.stem}.png")
def infer_asset_out(data: Dict[str, Any], origin_out: str) -> str:
value = data.get("asset_out")
if isinstance(value, str) and value:
return value
return str(Path("assets") / Path(origin_out).name)
def cmd_init(args: argparse.Namespace) -> int:
project = project_path(args.project)
prompts_dir = resolve_in_project(project, args.prompts_dir)
if not prompts_dir.exists():
die(f"prompts 目录不存在:{prompts_dir}")
jobs: List[Dict[str, Any]] = []
for prompt_path in sorted(prompts_dir.glob("*.json")):
data = read_json(prompt_path)
if not isinstance(data, dict):
die(f"prompt job 必须是 JSON 对象:{prompt_path}")
job_id = str(data.get("id") or data.get("job_id") or prompt_path.stem)
origin_out = infer_origin_out(prompt_path, data)
jobs.append(
{
"id": job_id,
"prompt_file": str(prompt_path.relative_to(project)),
"origin_out": origin_out,
"asset_out": infer_asset_out(data, origin_out),
"status": "pending",
"selected_backend": args.selected_backend,
"created_at": now_iso(),
}
)
if not jobs:
die(f"没有找到 prompt JSON{prompts_dir}")
job_doc = {"version": 1, "jobs": jobs}
state_doc = {"version": 1, "events": [{"time": now_iso(), "event": "init", "job_count": len(jobs)}]}
save_docs(project, job_doc, state_doc)
print(json.dumps({"status": "initialized", "jobs": len(jobs)}, ensure_ascii=False, indent=2))
return 0
def cmd_status(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
counts: Dict[str, int] = {}
for job in job_doc["jobs"]:
if not isinstance(job, dict):
continue
status = str(job.get("status") or "unknown")
counts[status] = counts.get(status, 0) + 1
print(json.dumps({"project": str(project), "counts": counts}, ensure_ascii=False, indent=2))
for job in job_doc["jobs"]:
if isinstance(job, dict):
print(f"{job_key(job) or '<unknown>'}\t{job.get('status', 'unknown')}\t{job.get('origin_out', '')}")
return 0
def cmd_dispatch(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
state_doc = load_state_doc(project)
job = find_job(job_doc, args.job)
prompt_file = args.prompt_file or job.get("prompt_file")
if not isinstance(prompt_file, str) or not prompt_file:
die("缺少 prompt 文件路径。")
job["status"] = "dispatched"
job["dispatch"] = {
"agent_id": args.agent_id,
"agent_nickname": args.agent_nickname,
"prompt_file": prompt_file,
"time": now_iso(),
}
event(state_doc, "dispatch", args.job, job["dispatch"])
save_docs(project, job_doc, state_doc)
print(json.dumps({"status": "dispatched", "job": args.job}, ensure_ascii=False, indent=2))
return 0
def copy_if_needed(source: Path, target: Path) -> None:
if not source.exists():
die(f"结果图片不存在:{source}")
target.parent.mkdir(parents=True, exist_ok=True)
try:
if source.resolve() == target.resolve():
return
except FileNotFoundError:
pass
shutil.copyfile(source, target)
def cmd_result(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
state_doc = load_state_doc(project)
job = find_job(job_doc, args.job)
source = resolve_in_project(project, args.selected_source)
origin_value = args.origin_out or job.get("origin_out")
if not isinstance(origin_value, str) or not origin_value:
die("缺少 origin_out。")
origin_out = resolve_in_project(project, origin_value)
copy_if_needed(source, origin_out)
asset_value = args.asset_out or job.get("asset_out")
if isinstance(asset_value, str) and asset_value:
copy_if_needed(origin_out, resolve_in_project(project, asset_value))
result = {
"agent_id": args.agent_id,
"backend_used": args.backend_used,
"selected_source": str(source),
"origin_out": str(origin_out.relative_to(project) if origin_out.is_relative_to(project) else origin_out),
"asset_out": asset_value,
"qa_note": args.qa_note,
"time": now_iso(),
}
job["status"] = "recorded"
job["result"] = result
event(state_doc, "result", args.job, result)
save_docs(project, job_doc, state_doc)
print(json.dumps({"status": "recorded", "job": args.job, "origin_out": result["origin_out"]}, ensure_ascii=False, indent=2))
return 0
def cmd_blocker(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
state_doc = load_state_doc(project)
job = find_job(job_doc, args.job)
blocker = {
"agent_id": args.agent_id,
"reason": args.reason,
"evidence": args.evidence,
"time": now_iso(),
}
job["status"] = "blocked"
job["blocker"] = blocker
event(state_doc, "blocker", args.job, blocker)
save_docs(project, job_doc, state_doc)
print(json.dumps({"status": "blocked", "job": args.job, "reason": args.reason}, ensure_ascii=False, indent=2))
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Record MAOMOMO article image job state.")
subparsers = parser.add_subparsers(dest="command", required=True)
init = subparsers.add_parser("init", help="Create slide_jobs.json from prompts/*.json.")
init.add_argument("project")
init.add_argument("--prompts-dir", default="prompts")
init.add_argument("--selected-backend", required=True)
init.set_defaults(func=cmd_init)
status = subparsers.add_parser("status", help="Print job status summary.")
status.add_argument("project")
status.set_defaults(func=cmd_status)
dispatch = subparsers.add_parser("dispatch", help="Record a dispatched image job.")
dispatch.add_argument("project")
dispatch.add_argument("--job", required=True)
dispatch.add_argument("--agent-id", required=True)
dispatch.add_argument("--agent-nickname", default="")
dispatch.add_argument("--prompt-file")
dispatch.set_defaults(func=cmd_dispatch)
result = subparsers.add_parser("result", help="Record a generated image result.")
result.add_argument("project")
result.add_argument("--job", required=True)
result.add_argument("--agent-id", required=True)
result.add_argument("--backend-used", required=True)
result.add_argument("--selected-source", required=True)
result.add_argument("--origin-out")
result.add_argument("--asset-out")
result.add_argument("--qa-note", default="")
result.set_defaults(func=cmd_result)
blocker = subparsers.add_parser("blocker", help="Record a blocked image job.")
blocker.add_argument("project")
blocker.add_argument("--job", required=True)
blocker.add_argument("--agent-id", required=True)
blocker.add_argument("--reason", required=True)
blocker.add_argument("--evidence", default="")
blocker.set_defaults(func=cmd_blocker)
return parser
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(list(argv) if argv is not None else None)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())