确认优先、单步推进、先样张再逐张生成,不能一上来完整批量生成
parent
5d5e485e38
commit
8536f5ebce
126
SKILL.md
126
SKILL.md
|
|
@ -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、更新打包、附件策略和最终检查清单。
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,例如 ``。
|
4. 出图前把全部图片 prompt 写入 `prompts/` 或 manifest 文件,让用户可直接编辑。
|
||||||
5. 创建 ZIP 包,结构类似:
|
5. 先生成并确认一张样张,优先封面图;样张确认后再逐张生成剩余独立高清 PNG。
|
||||||
|
6. 将通过 QA 的图片同步到 `assets/` 目录,并以相对路径插入 Markdown,例如 ``。
|
||||||
|
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,不是一张大图代替全部。
|
||||||
|
|
|
||||||
|
|
@ -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。
|
||||||
|
|
|
||||||
|
|
@ -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;如有,不能声称完整交付完成。
|
||||||
|
|
|
||||||
|
|
@ -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""
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
Loading…
Reference in New Issue