Compare commits

...
11 Commits
26 changed files with 3488 additions and 301 deletions
+1
View File
@@ -1 +1,2 @@
*.zip *.zip
.DS_Store
+74 -4
View File
@@ -1,6 +1,8 @@
# MAOMOMO 文章生成器 # MAOMOMO 文章生成器
这是一个面向 Codex 的中文写作技能,用来生成 MAOMOMO / 猫MOMO 风格的出海金融实操文章、文章大纲、发布用 Markdown 和配图提示词。 这是一个面向 Codex 的中文写作技能,用来生成 MAOMOMO / 猫MOMO 风格的出海金融实操文章、文章大纲、发布用 Markdown、SEO 优化稿、独立配图 assets、ZIP 发布包,以及小红书卡片 / 多图 / 3:4 / 9:16 竖版 PNG 交付。
新版流程参考了 `codex-ppt-skill` 的阶段化设计:先确认来源和文章口径,再确认大纲 / 卡片结构与配图规划,最后写作或整理小红书文案、生成配图、QA、写入 `final_manifest.json`,并按渠道打包或 GitHub 分批交付。这样可以降低金融活动文章里最常见的翻车点:旧规则混入新稿、来源没读全、图片残留旧口径、只给正文不交付文件。
## 适用场景 ## 适用场景
@@ -11,7 +13,7 @@
- 开户教程、保号教程、转账 / 入金 / 出金教程 - 开户教程、保号教程、转账 / 入金 / 出金教程
- 美股券商、数字货币、欧元通道、汇款路径 - 美股券商、数字货币、欧元通道、汇款路径
- 避坑经验、实测记录、活动规则整理 - 避坑经验、实测记录、活动规则整理
- MAOMOMO 风格封面图、截图槽位和信息图提示词 - MAOMOMO 风格封面图、截图槽位、信息图、小红书卡片和配图平铺水印
不适合直接生成投资建议、法律意见、税务结论或任何“保证获批 / 稳赚 / 一定到账”的承诺型内容。 不适合直接生成投资建议、法律意见、税务结论或任何“保证获批 / 稳赚 / 一定到账”的承诺型内容。
@@ -23,8 +25,17 @@ maomomo-article-writer/
├── README.md # 面向使用者的说明文档 ├── README.md # 面向使用者的说明文档
├── agents/ ├── agents/
│ └── openai.yaml # 技能展示名称、简介和默认提示语 │ └── openai.yaml # 技能展示名称、简介和默认提示语
├── scripts/
│ └── maomomo_image_gen.py # OpenAI 兼容 API/CLI 配图 fallback
└── references/ └── references/
└── style-guide.md # MAOMOMO 写作风格、模板和图片规则 ├── source-and-fact-check.md # 来源读取、事实核查和风险口径
├── style-guide.md # MAOMOMO 写作风格、模板和文章骨架
├── visual-assets.md # 配图清单、视觉规则和图片 QA
├── recommended-styles.md # 推荐配图风格和提示词片段
├── image-styles/ # 参考 codex-ppt 风格库拆分的出图风格 brief
├── image-generation.md # 图片生成脚本、manifest、final_manifest 和后端选择
├── github-delivery.md # GitHub 交付仓库、专用 KEY 和推送门禁
└── delivery-rules.md # 完整交付、GitHub 分批、打包和最终检查
``` ```
## 安装方式 ## 安装方式
@@ -59,6 +70,10 @@ maomomo-article-writer
把这个银行活动整理成猫MOMO风格文章,重点写怎么吃满和常见翻车点。 把这个银行活动整理成猫MOMO风格文章,重点写怎么吃满和常见翻车点。
``` ```
```text
把这个活动做成小红书 3:4 多图卡片,先给我确认 2D / 3D 视觉方向。
```
## 推荐提示词 ## 推荐提示词
生成完整文章: 生成完整文章:
@@ -81,6 +96,18 @@ maomomo-article-writer
使用 $maomomo-article-writer,为【主题】生成 MAOMOMO 风格封面图提示词、正文截图槽位和一张信息图提示词。不要写完整正文。 使用 $maomomo-article-writer,为【主题】生成 MAOMOMO 风格封面图提示词、正文截图槽位和一张信息图提示词。不要写完整正文。
``` ```
小红书卡片:
```text
使用 $maomomo-article-writer,做一组小红书 9:16 多图卡片。默认 2D 扁平风,要求每张独立 PNG,最后走 GitHub 分批交付。
```
GitHub 交付配置:
```text
使用 $maomomo-article-writer,先引导我配置 GitHub 交付仓库和专用 KEY,之后小红书卡片都走 GitHub 分批交付。
```
改写已有文章: 改写已有文章:
```text ```text
@@ -103,9 +130,43 @@ maomomo-article-writer
- 最后提醒和免责声明 - 最后提醒和免责声明
- 来源链接 - 来源链接
- 正文截图槽位和必要的信息图提示词 - 正文截图槽位和必要的信息图提示词
- `article.md`
- `assets/` 独立 PNG 配图目录
- `article_package.zip`
小红书卡片通常包含:
- 20 字内标题
- 小红书正文和 3-6 个话题标签
- 多张 3:4 或 9:16 独立 PNG
- `prompts/`、`slide_jobs.json`、`slide_run_state.json`
- `final_manifest.json`
- 可选 `watermarked_assets/`:用 `uv run python scripts/maomomo_tile_watermark.py` 生成 `MAOMOMO.COM` 45° 自动对比色半透明平铺水印版
- GitHub 分批链接
GitHub 分批交付前,技能会先引导配置:
- 专用私有交付仓库,例如 `OWNER/maomomo-delivery`
- `MAOMOMO_GITHUB_REPO`、`MAOMOMO_GITHUB_BRANCH`、`MAOMOMO_GITHUB_BASE_PATH`
- GitHub CLI 登录、fine-grained PAT,或 SSH deploy key
- 最小权限原则:只给交付仓库写入权限
- 密钥不进入聊天、prompt、manifest、状态文件、README、日志或 commit
如果事实不完整,技能会使用 `【待确认:...】` 标记缺口,而不是编造数字、日期、奖励或链接。 如果事实不完整,技能会使用 `【待确认:...】` 标记缺口,而不是编造数字、日期、奖励或链接。
## 默认流程
完整交付任务通常按以下步骤执行:
1. 识别任务模式:完整文章、大纲、SEO 优化、改稿、配图或小红书卡片。
2. 读取来源并列出已读 / 未读材料,整理事实表和待确认项。
3. 确认文章范围、大纲 / 卡片结构、标题方向、核心口径和图片规划。
4. 写作或优化发布用 Markdown。
5. 确认配图风格;小红书卡片需先确认 2D / 3D 和 3:4 / 9:16,再生成或准备独立 PNG。
6. 检查来源、图片路径、风险提示、编辑痕迹、ZIP 或 `final_manifest.json`;需要 GitHub 交付时先确认仓库和专用 KEY 配置,最后交付文件路径或 GitHub 分批链接。
用户明确说“只要文字”“只要大纲”“不要图片”“不要打包”时,会按用户限制执行,不强行创建完整发布包。
## 资料准备建议 ## 资料准备建议
为了得到更可靠的文章,最好提供: 为了得到更可靠的文章,最好提供:
@@ -126,6 +187,8 @@ maomomo-article-writer
- 语气像懂行朋友,不像官方宣传稿 - 语气像懂行朋友,不像官方宣传稿
- 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞 - 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞
- 配图统一使用白橘猫、暖橙色、干净金融科技教程风 - 配图统一使用白橘猫、暖橙色、干净金融科技教程风
- 如果用户没有指定配图风格,先在 `recommended-styles.md` 和 `image-styles/` 中按文章类型推荐 2-3 个方向,例如 `clean-professional`、`data-card-dashboard`、`handdrawn-note`、`e-ink-editorial` 或 `xiaohongshu-vertical`。
- 小红书卡片出图前必须确认 `2D 扁平风` / `3D 轻拟物风` 和 `3:4` / `9:16`;用户没指定时默认推荐 2D,并按原生竖版重新构图,不能把横版图压进竖图。用户要求防盗图 / 水印时,可用 `uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets` 生成 `MAOMOMO.COM` 45° 自动对比色半透明平铺水印版,并保留无水印原图。
文章不应当: 文章不应当:
@@ -137,6 +200,13 @@ maomomo-article-writer
## 维护说明 ## 维护说明
- 主要触发说明和执行规则写在 `SKILL.md`。 - 主要触发说明和执行规则写在 `SKILL.md`。
- 详细文章模板、标题套路、常用词和图片规则写在 `references/style-guide.md`。 - 详细文章模板、标题套路和常用词写在 `references/style-guide.md`。
- 来源核查、时效性、冲突口径和风险表达写在 `references/source-and-fact-check.md`。
- 配图类型、猫咪视觉、文件命名和图片 QA 写在 `references/visual-assets.md`。
- 推荐配图风格、适用场景和提示词片段写在 `references/recommended-styles.md`。
- 具体出图风格 brief 放在 `references/image-styles/`,结构参考 `codex-ppt-skill` 的风格库。
- 图片 API/CLI fallback、批量 manifest、final manifest、GitHub 批次记录和脚本用法写在 `references/image-generation.md`,脚本为 `scripts/maomomo_image_gen.py` 和 `scripts/maomomo_job_state.py`。
- GitHub 交付仓库、专用 KEY / token / deploy key、环境变量、隐私边界和推送前检查写在 `references/github-delivery.md`。
- 完整交付、ZIP、GitHub 分批、附件和最终检查写在 `references/delivery-rules.md`。
- 修改技能展示名称或默认提示语时,同步更新 `agents/openai.yaml`。 - 修改技能展示名称或默认提示语时,同步更新 `agents/openai.yaml`。
- 如果新增具体行业资料、平台规则或长期复用模板,优先放入 `references/`,避免让 `SKILL.md` 过长。 - 如果新增具体行业资料、平台规则或长期复用模板,优先放入 `references/`,避免让 `SKILL.md` 过长。
+241 -262
View File
@@ -1,275 +1,254 @@
--- ---
name: maomomo-article-writer name: maomomo-article-writer
description: 生成符合 maomomo / 猫momo 风格的中文实操文章、发布用 markdown、文章大纲、seo 优化稿、独立高清配图和 zip 打包交付。适用于 maomomo.com、出海金融、港卡、香港银行、香港信用卡、美股券商、数字货币、汇款、支付优化、羊毛活动、开户教程、保号教程、避坑经验等主题。用户要求使用 skill 生成文章、配图、发布包、打包、一键下载、完整交付,或说“配图也按 skill”时,默认必须输出 markdown 文件、assets 独立配图目录和 zip 包,而不是只输出正文、片段或提示词。 description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融实操文章、发布用 Markdown、SEO 稿、配图规划、独立高清配图 assets、ZIP 发布包,以及小红书卡片 / 多图 / 3:4 / 9:16 竖版 PNG 交付、配图平铺水印;包含 GitHub 交付仓库和专用 KEY / token / deploy key 配置引导。适用于 maomomo.com、港卡、香港银行、香港信用卡、美股券商、数字货币、汇款、支付优化、返现活动、开户教程、保号教程、避坑经验、官方条款整理和实测攻略。用户要求使用 skill、按猫MOMO风格、完整文章、发布用 Markdown、配图、打包、一键下载、最终交付、小红书、卡片、多图、3:4、9:16、水印、平铺水印、防盗图、GitHub 交付、仓库配置或专用 KEY 配置时触发。
--- ---
# MAOMOMO 文章生成器 # MAOMOMO 文章生成器
使用本技能生成 MAOMOMO / 猫MOMO 风格的中文实操文章、文章大纲、发布用 Markdown、SEO 优化稿、配图提示词、独立配图和打包交付文件。 本技能用于把主题、资料、链接、PDF、截图或已有 Markdown 转成 MAOMOMO / 猫MOMO 风格的中文实操文章,并在需要时交付可发布文件包。
输出要像一篇一手整理的出海金融攻略:结论靠前、步骤清楚、坑点明确、语气轻松但不轻浮,并且对金融、活动、合规、费用、奖励等时效性内容保持谨慎。 文章要像一手整理的出海金融攻略:结论靠前、步骤清楚、坑点明确、语气轻松但不轻浮。涉及金融、活动、合规、费用、奖励、账户规则和数字货币时,必须谨慎核查并避免承诺式表达。
## 工作流程 ## 使用边界
1. 先判断文章类型:活动 / 羊毛攻略、开户教程、转账 / 出入金教程、保号 / 账户维护、用卡姿势、对比总结、避坑经验。 适合:
2. 遇到费用、活动时间、奖励门槛、账户规则、信用卡返现、券商迎新、转账路径、数字货币出入金、合规要求等内容,先查证最新信息。优先使用官方条款、官方 App 显示、用户提供材料和 MAOMOMO 现有文章。
3. 默认使用中文写作。段落要短,数字要具体,App 菜单名 / 操作名要尽量写清楚,多用“怎么做”“哪里会翻车”“怎么吃满”的表达。
4. 每篇完整文章都要包含猫咪主题视觉规划和实际交付文件:封面图、正文截图槽位、必要时的流程图 / 表格图 / 路线图,以及 `article.md`、`assets/` 和 ZIP 包。
5. 涉及金融产品、银行、券商、数字货币、汇款、奖励活动或合规事项时,要写明来源链接和有效期,并加入保守的风险提示。
6. 用户上传或提供现有 Markdown 时,先完整阅读原文,再判断主题、核心概念、文章结构和需要配图的位置;不要沿用旧文章的银行名、邀请码、日期、收益率、奖励金额、产品名、图名或图片内容。
7. 交付前执行质量检查,避免写成官方宣传稿、SEO 空文或未经确认的承诺式内容。
生成完整文章、配图或打包交付时,必须先读取 `references/delivery-rules.md` 并按其中规则执行;同时读取 `references/style-guide.md` 获取更细的文章模板、标题套路、常用词、图片规则和不同类型文章骨架。 - 银行、港卡、香港信用卡、券商、数字货币、汇款、支付、账户维护、返现和羊毛活动。
- 活动规则整理、开户教程、转账 / 入金 / 出金教程、保号方案、用卡姿势、对比总结、避坑经验。
- 已有文章 SEO 优化、发布用 Markdown、配图规划、独立配图、ZIP 打包和后续改稿。
- 小红书卡片、多图信息卡、3:4 或 9:16 竖版 PNG 系列图、封面 / 内容卡 / 结尾卡分批交付。
- 对最终配图批量或单张叠加 `MAOMOMO.COM` 45° 自动对比色半透明平铺水印。
不适合:
- 给出投资、法律、税务或合规结论。
- 承诺一定获批、一定到账、稳赚、无风险、绝对安全。
- 伪造真实 App 截图、账单、错误或未经核对的官方 Logo 使用场景,或用户没有提供的实测证据。
## 硬性约束
- 如果本技能任何旧规则与“确认优先、单步推进”的流程冲突,以本节和“默认工作流”中的阶段门禁为准。
- 确认优先,单步推进;不要一上来就完整生成文章、全部配图或 ZIP。
- 用户提到“小红书”“卡片”“多图”“3:4”“9:16”任一触发词时,默认进入“小红书卡片模式”:不默认走完整文章 + Markdown + ZIP 流程,输出重点改为多张独立 PNG、统一视觉风格、适合小红书发布。只有用户明确要求文章、Markdown 或 ZIP 时,才叠加对应交付物。
- 读写 Markdown、JSON、CSV、HTML、代码或文本文件时显式使用 UTF-8。
- 处理用户上传或提供的 Markdown / PDF / 官方链接 / 参考文章时,必须先完整读取或说明无法读取的部分,再写最终稿。
- 费用、活动日期、奖励门槛、账户规则、返现、券商迎新、转账路径、数字货币出入金和合规事项,必须优先核查最新信息。优先级:用户提供的最新官方材料 > 官方页面 / 条款 PDF / App 显示 > 用户实测 > MAOMOMO 旧文 > 其他参考。
- 没有被确认的数字、日期、奖励、链接、邀请码、产品名和规则,不得编造;可用 `【待确认:...】` 标出缺口。
- 用户素材包含明确“实测”结果时,必须用确定语气写结论;包含“以往惯例”“过往尿性”“过往经验”等历史依据时,必须用“大概率 / 按过往案例判断”的强概率语气,不要降级成“可能 / 疑似 / 大概”。
- 写作前必须先给用户确认标题或标题方向、文章核心口径、`outline.md` 大纲、来源缺口、必选素材映射和配图风格。用户明确要求跳过确认时,未确认事实仍必须标 `【待确认:...】`。
- 大纲未确认前,不得创建最终 `deck_spec.json`、`speech.md`、图片 prompt jobs、文章配图图片、ZIP 或用户指定的最终文章包。
- 出图前必须先把全部图片 prompt 写入文件,包含文件名、用途、放置位置、核心文案、完整 prompt 和依赖素材;不要在聊天里输出超长 prompt。
- 小红书卡片模式出图前必须先让用户确认视觉方向:`2D 扁平风` 或 `3D 轻拟物风`。用户没指定时,默认推荐 `2D 扁平风`,因为小红书信息图更稳、更不容易文字翻车;一旦确认,整组图片必须保持同一种视觉方向,不能中途混用。
- 小红书竖图必须按原生竖版重新构图,禁止把横版设计硬塞进 3:4 或 9:16。每个小红书图片 prompt 必须包含英文约束:`native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
- 图片 prompt 中的视觉风格只能作为后台画面描述,不得成为画面文字;prompt 必须限制可见文字只包含指定业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,不得出现风格标签、prompt 描述、额外 slogan 或无关文字。
- 发布稿、图片文案、prompt、manifest 和 `deck_spec.json` 中金额统一写 `HKD`,不要使用港币符号;来源原文抽取文件可保留原文写法。
- 金融活动配图如出现信用卡、支付卡、卡组织、支付网络或机构 Logo,必须按官方材料或用户素材核对并记录来源;用户没有提供 Logo / 卡面素材但画面需要品牌或支付标识时,应主动尽力从官方活动页、品牌资源页、条款 PDF、App / 官网截图等来源查找准确标识,保存到 `sources/` 并记录来源。不得把银联 / UnionPay 画成 Mastercard、Visa 等其他卡组织。已核对正确的 Mastercard / Visa / UnionPay / 银行 / 支付宝等标识可以辅助出现,但不能抢占或替代 `MAOMOMO` 主标识位置。
- 先只生成一张代表性风格确认图,优先封面图,保存为正式命名,例如 `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、本地拼贴或手工文字覆盖冒充最终生成图。例外:用户提供真实银行 / 券商 / 钱包截图且需要上传图片 API 做编辑时,必须先向用户确认实际 API 地址 / baseURL、接口归属和是否允许外发;该 API 可能是用户自有兼容服务,不能默认按公共第三方处理。若未确认、确认不允许或安全策略不允许上传,则先用图片后端生成不含真实截图的空白模板,再在本地把原截图完整嵌入模板;本地步骤只能做排版嵌入、圆角、阴影和尺寸适配,不得改动截图内金额、日期、姓名、账号、交易描述等关键内容,并必须在状态文件和最终回复说明这是“图片后端模板 + 本地嵌入真实截图”的混合流程。
- 生成图片按“一张图一个 job / 一次请求 / 一次 QA”推进;通过 QA 后才用状态脚本记录 result 并同步到 `assets/`,不能只生成到 `origin_image/` 后声称完成。运行环境支持子 agent 时,样张通过后可以一图一个子 agent 并发处理;每个 dispatch、结果和 blocker 都必须写入状态文件,不能只靠聊天说明。
- 完整交付模式下,不能只在聊天里输出正文;必须生成 Markdown 文件、`assets/` 目录和 ZIP 包,除非用户明确说只要文字、只要大纲、不要图片、不要打包或先不生成文件。
- 图片交付完成后必须生成或更新 `final_manifest.json`,最终推送、打包或链接交付只能读取 `final_manifest.json` 或状态文件里明确标记为 final 的 assets;不得把 `draft`、`v2`、旧比例、失败图或未通过 QA 的图一起交付。
- 面向 Telegram / 聊天渠道的大文件交付默认改为 GitHub 分批推送:例如 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets`。Telegram 只负责通知进度和给 GitHub 文件路径、commit、release 或下载链接,不承载大 ZIP 或大图片包。
- 需要 GitHub 分批交付,或用户提到 GitHub 仓库、专用 KEY、token、PAT、deploy key 时,必须先读 `references/github-delivery.md`,引导配置专用私有交付仓库和最小权限凭据;不得要求用户在聊天里粘贴密钥,不得把 token / 私钥写入 prompt、manifest、状态文件、README、日志或提交记录。
- 用户要求“水印”“平铺水印”“防盗图”“打水印”时,先读 `references/visual-assets.md` 的平铺水印规则;水印只处理最终 QA 通过的交付图或交付副本,默认保留无水印原图。水印必须用 `uv run python scripts/maomomo_tile_watermark.py` 生成,不得手工覆盖。默认自动对比色:浅底黑字、深底白字;默认 alpha=70、font-size=50、spacing=28;每张图至少可见 2 个完整 `MAOMOMO.COM`。
- 每张图都必须在状态文件记录:prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub。中断恢复时先读状态文件和 `final_manifest.json`,不要依赖聊天记忆。
- 配图文件必须是独立 PNG;不要用一张大拼图代替多张图,不要只给提示词冒充图片文件。
- 后续改稿必须更新文件本体;影响图片、来源区、标题、摘要或 ZIP 时同步更新。
## 可见进度
复杂任务使用一个用户可见 checklist,并且同一时间只推进一个步骤:
1. 读取资料,整理来源状态和事实表。
2. 确认标题、核心口径、`outline.md` 大纲、素材映射、配图风格和生图后端。
3. 写作或优化发布用 Markdown。
4. 输出全部图片 prompt 文件和图片任务状态文件。
5. 生成并确认一张样张。
6. 逐张生成剩余图片并记录 dispatch / result / blocker。
7. QA、返修、生成 `speech.md`、小红书发布文案或备注稿。
8. 写入 `final_manifest.json`,按渠道打包或 GitHub 分批推送最终文件。
不要因为聊天里说“已完成”就标记完成;用实际文件、已读取来源、已生成图片、`final_manifest.json`、ZIP 或 GitHub 批次记录作为完成证据。
## 默认工作流
一句话版:判断模式 -> 读资料 -> 给标题 / 大纲 / 卡片结构 / 风格让用户确认 -> 写文章或小红书文案 -> 输出图片 prompts 文件让用户改 -> 先出一张图确认 -> 逐张生成剩余图 -> QA -> 写 `final_manifest.json` -> 打包或 GitHub 分批交付。
1. 判断任务模式。
- 完整交付、只要大纲、只要正文、只要配图、SEO 优化、改稿更新、小红书卡片模式。
- 判断是否涉及金融 / 活动 / 合规事实核查。
- 完整交付、打包、GitHub 分批推送或小红书卡片任务先读 `references/delivery-rules.md`;需要配图时先读 `references/visual-assets.md`;需要 GitHub 推送或仓库 / KEY 配置时先读 `references/github-delivery.md`。
2. 读取源材料并建立事实表。
- 先读用户提供的官方页面、PDF、旧文、参考链接、截图或素材,不先动手写最终稿。
- 涉及官方规则、活动或金融产品时,先读 `references/source-and-fact-check.md`。
- 列出所有来源的已读 / 未读 / 无法读取状态。
- 明确区分:已官宣事实、用户实测、过往惯例判断、待确认内容。
- 确认主题、目标读者、文章目标、章节数量或图片数量、品牌 / 风格限制,以及必须放入文章的图片素材。
3. 规划并确认 `outline.md`。
- 写最终稿前先给出推荐标题或使用用户指定标题、文章核心口径、章节大纲、来源缺口、图片规划和必选素材映射。
- `outline.md` 应包含每个大小标题、3-5 个要点、页面 / 图位角色、视觉想法、必选素材路径或附件名。
- 有必选素材时,必须先确认“素材到章节 / 图位”的映射,再进入风格选择或图片生成。
- 若存在多个活动分支、旧版活动、小众资格或来源冲突,先建议主文范围,等用户确认后再写最终稿。
- 用户明确要求“直接生成”时,可以继续执行,但所有未确认事实必须标 `【待确认:...】`。
- 小红书卡片模式的 `outline.md` 可以简化为卡片结构:图片数量、比例、每张卡片角色、核心文案、话题标签、必选素材映射和分批交付计划。
4. 确认视觉风格。
- 小红书卡片模式必须先确认比例 `3:4` 或 `9:16`,以及视觉方向 `2D 扁平风` 或 `3D 轻拟物风`。
- 小红书卡片模式用户没指定视觉方向时,推荐 `2D 扁平风`:清爽、信息卡片、适合教程 / 规则说明,文字更稳;`3D 轻拟物风` 更有质感,适合封面 / 活动感 / 金融产品展示。
- 小红书卡片模式一旦确认 2D / 3D,所有 prompt、样张、剩余图片和 final manifest 都必须记录同一 `visual_direction`,不得混用。
- 若用户没有指定视觉方向,读 `references/recommended-styles.md`,给 2-3 个 MAOMOMO 配图风格选项并推荐一个。
- 用户提供参考图、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、太长不看版、正文、最后提醒和来源区。
- SEO 优化既要改标题、摘要、slug / tags / categories、图片 alt,也要清理无效来源和发布残留。
7. 创建项目目录和任务文件。
- 完整交付默认使用独立项目目录;如果用户没指定位置,使用当前工作目录或源文件所在目录。
- 推荐结构:
```text
{base_dir}/{article_slug}/
├── origin_image/
│ ├── {article_slug}_01_cover.png
│ └── ...
├── assets/
│ ├── {article_slug}_01_cover.png
│ └── ...
├── sources/
│ ├── card-reference.png
│ └── ...
├── prompts/
│ ├── {article_slug}_01.json
│ └── ...
├── deck_spec.json
├── slide_jobs.json
├── slide_run_state.json
├── final_manifest.json
├── github_batches.json
├── watermarked_assets/ # 可选,最终图的 MAOMOMO.COM 平铺水印版本
├── outline.md
├── article.md
├── speech.md
└── {article_slug}.zip
```
- `origin_image/` 保存图片后端生成并通过 QA 的原始最终图;`assets/` 保存 Markdown 实际引用的发布图片。两者可以是同名复制件,但不得只保留临时图。
- `sources/` 保存用户提供或主动查找的卡面、Logo、支付网络、官方截图或其他参考素材,供 prompt、manifest 和 `deck_spec.json` 映射引用。
- `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` 初始化和记录状态。
- `final_manifest.json` 只记录通过 QA 且进入 `assets/` 的最终交付文件;`github_batches.json` 记录每批 GitHub 推送的 batch id、文件路径、commit、release 或下载链接。
- 用户要求水印时,使用 `uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets` 生成水印副本;水印图可进入 GitHub 批次中的 `watermarked/`,但无水印原图仍保留。
- 小红书卡片项目没有文章正文时,可以省略 `article.md` 和 ZIP,但仍必须保留 `origin_image/`、`assets/`、`prompts/`、`slide_jobs.json`、`slide_run_state.json`、`final_manifest.json` 和 `github_batches.json`。
8. 出图前整理全部 prompt。
- 每张图的文件名、用途、放置位置、核心文案、完整 prompt、alt text、风格、依赖素材和输出路径先写入 `prompts/` 或 manifest 文件。
- prompt 中不要写“暖橙金融教程风”“数据仪表盘风”“手绘便签风”等容易被模型照抄的中文风格标签;改写为白底、暖橙强调、信息卡片、猫咪向导、清晰标题等视觉描述。
- 每张图的 prompt 必须明确:只允许出现指定业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识;不得添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。
- 小红书 3:4 / 9:16 prompt 必须明确原生竖版重构,并包含 `native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
- 封面只放最高优先级结论和少量核心数字;复杂计算、全年理论上限、长公式和详细规则放到计算图或正文。
- 不要在聊天里输出超长 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`,不能只靠聊天说明完成。
- 每张图的状态必须记录 prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub;缺失记录时不得进入最终交付。
- 每张图通过前必须检查日期、金额、公式、卡组织 / 支付网络、禁用分支、风格说明文字、伪造截图、乱码和多余文案。
- 检查文字清晰度、中文乱码、金额 / 日期 / 规则是否残留旧口径、标题截断、风格一致性、必选素材是否正确、元素是否重叠、是否伪造截图或 Logo。
- 小红书 3:4 / 9:16 图额外检查:比例正确、竖版原生构图、元素没有被压扁、卡片 / Logo / 文字没有拉伸、不是横图硬塞进竖版。
- 严重问题重新生成该单张;局部问题优先使用已确认后端的编辑能力修复,不得本地手工覆盖关键文字冒充模型输出。
12. 生成 `speech.md`、`final_manifest.json`、组装文章包或 GitHub 批次并最终 QA。
- `speech.md` 用作每张图的备注 / 讲解稿,标题格式使用 `## Slide N: 标题`,便于和图片顺序映射。
- 组装最终文章包时,把 `origin_image/{article_slug}_XX.png` 按顺序同步到 Markdown 引用的 `assets/`,并确保 `article.md`、图片路径、`speech.md`、来源区和 ZIP 都是最新版本。
- 最终交付只能读取 `final_manifest.json` 或状态文件中 `qa_status=passed`、`entered_assets=true`、`final=true` 的文件,不扫描整个目录交付。
- 若交付水印版,在最终 QA 后用 `uv run python scripts/maomomo_tile_watermark.py` 对 final assets 生成 `watermarked_assets/`,并在 `final_manifest.json` 记录 `watermark` 字段:text、angle、color、alpha、output_dir。
- 需要 GitHub 分批交付时,先按 `references/github-delivery.md` 确认交付仓库、认证方式、目标路径和仓库隐私级别;缺配置时先给用户本机配置步骤并暂停 push。配置就绪后按 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets` 等批次推送;每批推送后记录 GitHub 文件路径、commit、release 或下载链接,并写入 `github_batches.json` / 状态文件。
- 完整交付、文件交付或改稿打包前读 `references/delivery-rules.md`。
- QA 必须用脚本检查 Markdown 中所有图片路径真实存在,并检查 assets 是否存在、来源区只保留实际使用来源、正文无编辑痕迹、图片无错误日期 / 金额 / 未确认结论、用户要求删除内容不残留、ZIP 内容正确。
- 小红书卡片模式最终回复固定包含:GitHub 链接、图片数量、风格:2D / 3D、比例:3:4 / 9:16、批次数量、QA 是否完成、已知限制。
- 完整文章模式最终回复保持简短:给出项目目录、文章路径、图片目录、ZIP 或 GitHub 链接、图片数量、生图后端、状态记录情况、备注写入情况和已知限制。
## 模式选择
- “生成文章”“完整文章”“发布用 Markdown”“使用 skill”“按 skill”“文章和配图一起”“打包”“一键下载”“最终交付”:完整交付模式。
- “只要大纲”“先列结构”:只输出标题备选、大纲、事实缺口、来源清单和图片规划。
- “只要文字”“不要图片”“不要打包”“先不要生成文件”:按用户限制输出,不强制创建文件包。
- “SEO 优化”“配图”“打包”且用户提供已有 Markdown:读取原文后输出优化版 Markdown、assets 和 ZIP。
- “小红书”“卡片”“多图”“4 图看完”“发小红书”“3:4”“9:16”:小红书卡片模式;读 `references/style-guide.md` 和 `references/visual-assets.md` 的小红书规则,按 3:4 或 9:16 原生竖版规划,不默认生成完整文章、Markdown 或 ZIP。
- 后续说“删掉 X”“合并 3、4”“不要写 Y”“改成 Z”:视为编辑指令,不要把指令原样写进正文。
## 改稿协作规则 ## 改稿协作规则
用户后续改稿时,所有要求都先视为编辑指令,不得原样写入文章正文。 - 用户的修改要求默认是编辑指令,不是正文素材。
- 只改用户指定范围;不要因为“删掉几个字”误删整段。
- 用户说“不要写 X”:删除或改写 X,不要在正文解释“不要写 X”。 - 用户确认的实测口径优先写成结论,不反复声明“本文以实测为主”等元叙述。
- 用户说“应当写成 Y”:把 Y 转成自然文章表达,不要写“应当写成”。 - 用户给出实测或过往惯例依据时,不要把确定事实和强概率判断改软;风险提示只能作为边界补充,不能推翻主句判断。
- 用户说“这个口径 / 我的要求”:这是写作指令,不是正文素材。 - 对同类模糊词同步检查,例如“更像”“可能”“大致”“疑似”,但不要机械删除必要的不确定表达。
- 用户说“删掉几个字 / 某个词”:只删指定词,不扩大成删除整条、整段或整节。 - 改完必须检查正文是否残留“不要写”“用户要求”“准确写法是”“本文口径”“应当写成”等编辑痕迹。
- 用户说“合并 3、4”或“6、7 合并”:只合并对应条目,保留原信息,必要时重排编号。 - 文件交付任务改完后重新检查 Markdown、assets 和 ZIP 是否是最新版本。
- 用户反复纠正某个词,例如“更像”“可能”“最高”等,后续同文档同类位置都要检查并同步清理。
- 用户确认的实测口径优先写成结论,不要反复声明“本文以实测为主”“官方条款仅供参考”等元叙述。 ## 验收标准
- 改完必须检查正文是否残留编辑痕迹,例如“准确写法是”“不要写”“用户要求”“本文口径”“应当”。
完整交付必须满足:
## 实测活动文章规则
- `article.md` 或用户指定的 Markdown 文件存在。
处理银行、支付、返现、优惠券、券商迎新等活动时,如果用户提供了明确实测经验,正文应优先呈现实操结论。 - `assets/` 存在,且 Markdown 中每个图片路径都能对应真实文件。
- 需要配图时,每张配图是独立 PNG,并且含 MAOMOMO 品牌识别。
- 官方条款用于确认活动边界、日期、排除项和来源区。 - ZIP 包存在,包含最终 Markdown 和 `assets/`。
- 实测经验用于组织“怎么吃”“怎么判断出券”“哪里会翻车”。 - 来源区只保留文章实际使用的来源;未读或无法确认的来源不得假装已确认。
- 不要写成官方宣传稿,也不要把“官方条款仅供参考”作为正文口号反复出现。 - 正文没有 `contentReference`、`oaicite`、调试标记、模型解释或编辑指令残留。
- 实测结论要直接写,例如“按支付宝账号算”“按卡算”“先刷后返”“不出券先看账号”。 - 金融、银行、券商、数字货币、汇款、税务或合规内容包含保守风险提示。
- 对用户明确确认的口径,不要再写“更像”“可能”“大致”等模糊词。 - 用户要求删除或收敛的分支,不残留在标题、摘要、正文、图片 alt、配图清单、来源区或 ZIP。
- 仍要避免绝对承诺到账、稳赚、一定成功;可用“直到名额耗尽或风控拦截”等实际限制表达。
小红书卡片模式必须满足:
## 输出模式
- 每张最终图是独立 PNG,比例为用户确认的 `3:4` 或 `9:16`。
根据用户要求选择输出形态: - 已确认 `2D 扁平风` 或 `3D 轻拟物风`,且全组一致。
- prompt、状态文件和 QA 记录包含原生竖版构图、防压扁和防拉伸约束。
- 用户说“生成文章”“写完整草稿”“发布用 Markdown”“使用 skill 生成文章”“完整文章”“打包”“一键下载”“文章和配图一起”:默认进入完整交付模式,输出 Markdown 文件、`assets/` 独立配图目录和 ZIP 包。 - `final_manifest.json` 只包含通过 QA 且进入 `assets/` 的最终图片。
- 用户说“只要大纲”“先列结构”:输出标题备选、文章结构、需要确认的关键事实、图片规划。 - GitHub 分批推送记录已写入状态文件或 `github_batches.json`;Telegram 只发送进度和链接。
- 用户说“图片风格”“配图提示词”“封面图”:只输出封面图提示词、正文截图槽位、信息图提示词和替代文本。 - GitHub 交付仓库和专用 KEY / token / deploy key 已按 `references/github-delivery.md` 配置或明确记录为阻塞项。
- 用户上传 Markdown 并要求“SEO 优化”“配图”“独立生成图片”“打包”“一键下载”:输出 SEO 优化后的 Markdown、插入正文的多张独立配图、`assets/` 文件夹和 ZIP 包;最后只给下载链接或本地文件路径以及简短说明。 - 最终回复包含 GitHub 链接、图片数量、风格、比例、批次数量、QA 是否完成和已知限制。
- 用户只给了主题但事实不全:可以先出草稿,但所有未确认内容必须标成 `【待确认:...】`,不要编造数字、链接、奖励或规则。
- 用户明确说“只要文字”“不要图片”“不要打包”“先不要生成文件”“先只要大纲”时,按用户的限制输出,不强行生成完整文件包。 如果无法完成,最终回复必须说明:阻塞阶段、缺失材料或失败工具、已完成文件路径、未完成项和下一步需要什么。
## 强制完整交付规则 ## Reference Map
当用户请求包含“使用 skill 生成文章”“按 skill 生成”“配图也按 skill”“完整文章”“发布用 Markdown”“打包”“一键下载”“给我文件”“文章和配图一起”“生成文章 + 配图”或“最终交付”等表达时,必须默认进入完整交付模式,除非用户明确要求只要文字、大纲、不要图片、不要打包或先不生成文件。 - `references/source-and-fact-check.md`:来源读取、事实表、时效性、冲突口径和金融风险核查。
- `references/style-guide.md`:MAOMOMO 文章类型、标题套路、语气、结构模板、小红书模式和写作细节。
完整交付模式必须一次性完成:读取并核对所有用户提供的官方链接、PDF、参考文章和条款;生成发布用 Markdown 文件;生成或准备多张独立高清 PNG 配图并放入 `assets/`;用相对路径插入 Markdown;创建 ZIP 包;最终回复只给 Markdown 和 ZIP 下载链接或本地路径以及极简说明。若工具、图片生成、文件写入或资料缺失导致无法完成,必须明确失败点并尽量交付已完成部分,不要用正文或配图提示词代替文件。 - `references/visual-assets.md`:配图类型、猫咪视觉、文件命名、Markdown 插图、金融卡面核对、可见文字限制、禁止伪造截图和图片 QA。
- `references/recommended-styles.md`:推荐配图风格、适用场景、推荐话术和提示词片段。
## 文章生成任务的默认交付物 - `references/image-styles/*.md`:具体出图风格 brief,借鉴 `codex-ppt-skill` 风格库结构并适配 MAOMOMO 文章配图。
- `references/image-generation.md`:图片生成后端选择、`scripts/maomomo_image_gen.py` 用法、`scripts/maomomo_job_state.py` 状态记录、final manifest、GitHub 批次记录和故障处理。
当用户没有特别说明“只要草稿”“先别生成文件”时,MAOMOMO 文章任务默认交付: - `references/github-delivery.md`:GitHub 交付仓库、专用 KEY / token / deploy key、环境变量、隐私边界和分批推送前置检查。
- `references/delivery-rules.md`:完整交付、ZIP、GitHub 分批推送、更新打包、附件策略和最终检查清单。
- `article.md`
- `assets/` 独立配图目录
- `article_package.zip`
聊天框正文只用于简短说明,不要把完整文章作为唯一交付物。用户后续要求修改文章内容时,必须更新 Markdown 文件本体;若修改影响配图、配图清单、来源区或 ZIP 包,必须同步更新并重新打包。
## 官方来源完整性检查
处理金融活动、银行活动、券商活动、返现活动、奖励活动时,必须先列出用户提供的所有来源链接,并逐个确认是否已读取:
```text
已读取来源:
- 官方活动页:已读 / 未读
- 主活动条款 PDF:已读 / 未读
- 额外活动条款 PDF:已读 / 未读
- 参考文章:已读 / 未读
```
如果用户提供了多个 PDF,不能只读其中一部分就开始写最终稿。后续补充新的官方条款 PDF 时,必须重新校正文章中相关规则并说明更新点。若主活动 PDF、补充条款 PDF、活动页存在口径差异,优先以最新官方条款 PDF 为准,并在文章里提示“以官方条款和 App 实际显示为准”。
## 范围收敛规则
如果活动有主活动、额外会员活动、年龄限定活动、旧版活动对比或小众资格优惠等多个分支,先判断是否适合放入主文。
默认主文优先保留大多数读者适用的主活动;小众分支只放到“补充说明”“关联阅读”或“可选阅读”。用户要求删除某个分支时,必须同步删除正文、标题、摘要、标签、配图清单、图片 alt 文案、来源区和 ZIP 包里的相关内容,不要让已删除分支残留在来源区、配图清单或图片文件名里。
## SEO 优化与配图打包模式
当用户提供现有 Markdown 并要求优化、配图或打包时,按这个顺序执行:
1. 读取原文,判断文章主题、核心概念、目标读者和现有结构。
2. 在不编造事实的前提下优化 `title`、`description` / `excerpt`、`slug`、`categories`、`tags`、标题层级、太长不看版、图片 alt text、风险提示、来源区和正文逻辑顺序。
3. 自动规划正文配图清单,标明每张图的类型、标题、放置位置、核心文案、关键数字、文件名和 alt text。
4. 逐张独立生成或准备配图,保存到 `assets/`,再把最终图片插入 Markdown 对应位置。
5. 创建 ZIP 包,结构应包含优化后的 Markdown 和 `assets/` 目录。
6. 最终回复保持简短,只提供 ZIP 和 Markdown 的下载链接或本地路径;不要长篇解释过程。
配图数量按内容决定:短文章 4-6 张,普通攻略 6-8 张,长文章 / 多步骤教程 / 多活动整理 8-12 张。默认目标是 6-10 张独立配图;不要为了凑数量硬加图,也不要把多个不同主题硬塞进一张大图。
常见配图类型:
- 封面图:文章主题、核心利益点 / 核心问题、产品或场景元素、MAOMOMO 白橘猫视觉元素。
- 总览图:适合活动整理、产品对比、教程总览,展示主要活动、核心条件、关键数字、适合人群和结论。
- 流程图:适合开户、转账、入金、注册、申请、使用路径,展示 Step 1 / Step 2 / Step 3、操作顺序、注意事项和关键入口。
- 表格图 / 对比图:适合多个产品、多个方案、多个活动对比,展示名称、条件、奖励、成本、适合谁和不适合谁。
- 计算图:适合收益、返现、年化、费用测算,展示公式、输入数字、结果、保守假设和风险提醒。
- 时间线图:适合活动日期、到账时间、任务周期、持有期、截止日,展示开始日期、截止日期、关键节点和最容易错过的时间。
- 避坑图:适合“常见翻车点”“注意事项”“风险提醒”,展示容易犯的错、不能补救的点、费用 / 规则 / 时效风险和正确姿势。
- FAQ 图:适合文章最后的常见问题,展示 3-5 个高频问题、简短答案和决策提示。
配图生成硬性要求:
- 每张图都要独立生成高清 PNG;不要拼图,不要从拼图里裁切,不要只生成一张总图。
- 如果用户要求最终图片文件,不要只给提示词;必须把图片文件插入 Markdown 并纳入 ZIP。
- 不要伪造真实 App 截图;没有真实截图时,只能做“示意图”或“插画示意”。
- 涉及银行、支付机构、卡组织、券商或品牌时,可以使用中性的产品场景元素,但不要把第三方品牌 LOGO 放在猫咪身上、猫咪衣服上或猫咪身体部位上;例如 HSBC / 汇丰 LOGO 不得出现在猫身上。
- 每张图都必须打上清晰的 `MAOMOMO` 文字作为站点 LOGO / 品牌标识,可放在角落、水印、标题区或猫咪旁边,但不要遮挡关键信息。
- 每张图都要有独立主题,信息量适中,文字清晰,文件名 SEO 友好。
- 图片文件名必须包含文章标题 / 主题前缀,例如 `hsbc-mastercard-alipay-campaign-01-cover.png`;不要使用 `01-cover.png`、`02-overview.png` 这类缺少主题前缀的通用文件名。
- 图片说明必须根据图片内容写,不要都写成“配图”。
图片文件建议放在,并且文件名必须加文章标题 / 主题前缀,避免只用 `01-cover.png` 这类通用名。前缀用英文 slug 或拼音 slug,保持 SEO 友好,例如:
```text
assets/
hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png
hsbc-mastercard-alipay-campaign-03-step-guide.png
hsbc-mastercard-alipay-campaign-04-comparison.png
hsbc-mastercard-alipay-campaign-05-calculation.png
hsbc-mastercard-alipay-campaign-06-timeline.png
hsbc-mastercard-alipay-campaign-07-common-pitfalls.png
hsbc-mastercard-alipay-campaign-08-faq.png
```
也可以按具体主题使用更具体的英文或拼音文件名,例如 `hsbc-mastercard-alipay-campaign-01-cover.png`、`hong-kong-bank-account-03-application-flow.png`、`alipay-hk-cashback-05-return-calculation.png`。不要沿用上一篇文章的固定文件名或无主题前缀文件名,除非主题刚好一致。
Markdown 插图路径统一使用:
```markdown
![图片说明](assets/文件名.png)
```
插图位置要贴合正文:封面图放在标题下方;总览图放在活动 / 产品介绍后;流程图放在步骤段落前后;计算图放在公式或测算表格后;时间线图放在日期说明后;避坑图放在翻车点段落后;FAQ 图放在常见问题段落前后。
ZIP 结构:
```text
article_seo_with_images_package.zip
article_seo_with_images.md
assets/
hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png
hsbc-mastercard-alipay-campaign-03-step-guide.png
```
完成后回复格式可参考:
```markdown
已完成。
- [下载 ZIP:Markdown + 独立配图](...)
- [下载 Markdown](...)
ZIP 内包含:
- SEO 优化后的 Markdown
- assets 文件夹
- 多张独立生成的 PNG 配图
```
## 发布用文章结构
当用户需要发布用 Markdown 草稿时,开头使用头部元数据:
```yaml
---
title: "文章标题"
excerpt: "一句话说明本文整理什么、适合谁、核心收益或风险点。"
categories:
- "银行"
tags:
- "港卡"
---
```
默认正文结构如下;如主题更适合其他结构,可按文章类型调整:
```markdown
# 标题
![封面图提示:...](image-placeholder)
## 太长不看版
> 1. ...
> 2. ...
> 3. ...
## 一、为什么值得看 / 活动简介 / 背景
## 二、怎么操作 / 怎么吃满
## 三、关键条件
## 四、常见翻车点
## 五、实测 / 时间线 / 常见答疑
## 最后提醒
## 来源
```
标题和小标题要偏实操,不要写得太学院派。优先使用“太长不看版”“保姆式教程”“怎么吃满”“常见翻车点”“最后提醒”“如果已经这样操作还有救吗”这类 MAOMOMO 式标题。
## 写作口吻
- 像一个懂行朋友在分享可执行路线,不要像银行、券商或 SEO 营销号。
- 先给结论,再讲原因和步骤。
- 句子直接,段落短。一个段落通常只讲一个实操点。
- 可以自然使用“众所周知”“重点来了”“翻车点”“闭眼薅”“无脑刷”“保姆式教程”“只看这个就够”“实测”“以官方条款 / App 实际显示为准”等表达。
- 轻微俏皮可以,但不要堆梗、堆感叹号、堆 emoji,也不要制造焦虑。
- 不承诺一定获批、一定到账、一定拿奖、稳赚、绝对安全,也不替用户做法律、税务、投资或合规判断。
## 图片与视觉方向
完整文章至少包含:
- 1 个带 MAOMOMO 猫咪识别的封面图提示词。
- 关键 App 操作、证明材料或结果展示的正文截图槽位。
- 涉及转账路径、返现计算、活动门槛、路线对比时,补充一张表格图 / 路线图 / 时间线图提示词。
猫咪视觉识别:
- 友好的白橘猫吉祥物,圆脸、小爪子、暖橙点缀,整体是干净的金融科技教程风。
- 猫咪可以作为向导、指示贴纸、水印或提醒牌,不要抢走数字、步骤和截图的主视觉。
- 猫咪身上不要出现第三方品牌 LOGO、银行 LOGO、卡组织 LOGO 或支付机构 LOGO;品牌元素应放在卡片、页面、标签或背景场景里,且不能让人误以为是猫咪本身的标识。
- 每张图都要包含清晰可见的 `MAOMOMO` 文字 LOGO,作为站点品牌识别;位置可统一放在右上角、右下角或封面标题区。
- 封面图要把产品语境和猫咪元素结合起来,例如猫咪拿着港卡、指向 App 页面、守着转账路线图、坐在返现计算器旁边。
- 不伪造银行或券商 App 截图。没有真实截图时,只能写成“示意图”或“插画示意”。
## 来源区规则
- 活动时间、截止日期、条款变更、实测时间线必须写具体日期。
- 奖励、费用、资格、监管产品、账户规则等信息,尽量链接官方条款或官方页面。
- 引用 MAOMOMO 时,要链接具体文章或页面,不只放首页。
- 发布用 Markdown 正文不要保留模型内部引用残留、`contentReference`、`oaicite` 或其他不可发布标记;正文里可用自然语言说明“根据官方条款整理”。
- 所有链接统一放在文末 `## 来源` 区;来源区只保留最终文章实际使用的来源。用户要求删除某个活动分支时,对应来源也必须删除。
- 涉及金融、银行、券商、数字货币、汇款、税务或合规时,加入简短免责声明:
```text
本文为经验整理,不构成金融、投资、法律或税务建议;活动和规则可能随时调整,请以官方条款、App 实际显示及当地法规为准。
```
- 无法确认的信息要明确标注,例如“公开页面暂时无法确认”“需要二次确认”“以到账 / 账单 / MCC 实际显示为准”。
## 质量检查
交付前确认:
- 标题包含产品 / 主题,以及具体利益点或问题点。
- 开头第一屏已经给出实操结论。
- 数字、门槛、日期、邀请码、链接、币种和单位写清楚。
- 至少有一个“常见翻车点 / 风险提醒”部分。
- 图片提示词包含猫咪元素;每张图都有 `MAOMOMO` 文字 LOGO;猫咪身上没有第三方品牌 LOGO;并且没有把生成图伪装成真实截图。
- 来源放在文末,官方来源和 MAOMOMO 来源分清楚。
- 正文没有 `contentReference`、`oaicite`、调试标记或工具残留。
- 语气不像官方营销稿,也没有无依据保证。
+2 -2
View File
@@ -1,7 +1,7 @@
interface: interface:
display_name: "MAOMOMO 文章生成器" display_name: "MAOMOMO 文章生成器"
short_description: "生成 MAOMOMO 文章、配图和 ZIP 发布包" short_description: "分阶段确认后生成 MAOMOMO 文章、配图、小红书卡片和 GitHub 交付包"
default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章,并交付 Markdown、assets 独立配图目录和 ZIP 包。" default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章或小红书卡片。请先核查来源和事实表,再确认标题、口径、大纲或卡片结构、素材映射、2D/3D 视觉方向、比例和生图后端;如需 GitHub 交付,先引导配置专用私有仓库和最小权限 KEY / token / deploy key;先输出图片 prompts 文件,只生成一张样张确认,再逐张生成剩余图片,最后用 final_manifest.json 控制最终 assets,并按需要交付 Markdown、ZIP 或 GitHub 分批链接。"
policy: policy:
allow_implicit_invocation: true allow_implicit_invocation: true
+101 -24
View File
@@ -1,6 +1,8 @@
# MAOMOMO 完整交付规则 # MAOMOMO 完整交付规则
本文件用于完整文章、配图、发布包、打包、一键下载和后续更新任务。只要用户没有明确说“只要文字”“不要图片”“不要打包”“先不要生成文件”“先只要大纲”,默认按完整交付模式执行。 本文件用于完整文章、配图、发布包、打包、一键下载、GitHub 分批推送和后续更新任务。只要用户没有明确说“只要文字”“不要图片”“不要打包”“先不要生成文件”“先只要大纲”,默认按完整交付模式执行。用户提到“小红书”“卡片”“多图”“3:4”“9:16”时,默认进入小红书卡片模式,不默认生成完整文章、Markdown 或 ZIP。
完整交付模式指“最终交付物必须完整”,不代表可以跳过确认一次性全量生成。执行时必须遵守 `SKILL.md` 的阶段门禁:先读资料和事实表,再确认标题 / 口径 / 大纲 / 素材映射 / 配图风格 / 生图后端,随后写文章、整理 prompts、先出一张样张,样张确认后再逐张生成剩余图片。
## 强制完整交付规则 ## 强制完整交付规则
@@ -19,52 +21,98 @@
- “生成文章 + 配图” - “生成文章 + 配图”
- “最终交付” - “最终交付”
完整交付模式必须一次性完成: 完整交付模式最终必须完成以下交付物,但必须分阶段推进:
1. 读取并核对用户提供的所有官方链接、PDF、参考文章和条款。 1. 读取并核对用户提供的所有官方链接、PDF、参考文章和条款。
2. 输出发布用 Markdown 文件,而不是只在聊天框展示正文。 2. 在写最终稿前给用户确认标题、核心口径、`outline.md` 大纲、来源缺口、必选素材映射和配图风格。
3. 生成或准备多张独立高清 PNG 配图,放入 `assets/` 目录。 3. 用户确认后输出发布用 Markdown 文件,而不是只在聊天框展示正文。
4. 将图片以相对路径插入 Markdown,例如 `![图片说明](assets/01-cover.png)`。 4. 出图前把全部图片 prompt 写入 `prompts/` 或 manifest 文件,让用户可直接编辑。
5. 创建 ZIP 包,结构类似: 5. 先生成并确认一张样张,优先封面图;样张确认后再逐张生成剩余独立高清 PNG。
6. 将通过 QA 的图片同步到 `assets/` 目录,并以相对路径插入 Markdown,例如 `![图片说明](assets/hsbc-mastercard-alipay-campaign-01-cover.png)`。
7. 用脚本检查 Markdown 中所有本地图片路径真实存在。
8. 创建 ZIP 包,结构类似:
```text ```text
article_package.zip article_package.zip
article.md article.md
assets/ assets/
01-cover.png hsbc-mastercard-alipay-campaign-01-cover.png
02-overview.png hsbc-mastercard-alipay-campaign-02-overview.png
03-timeline.png hsbc-mastercard-alipay-campaign-03-timeline.png
``` ```
6. 最终回复只给下载链接和极简说明。 9. 最终回复只给路径和极简说明,同时说明图片数量、生图后端、状态记录和已知限制。
如果因为工具限制、图片生成失败、文件写入失败或资料缺失导致无法完成完整交付,必须明确说明失败点,并尽量交付已经完成的部分。不要只输出配图提示词代替图片文件,不要只输出文章正文代替 Markdown 文件。 如果因为工具限制、图片生成失败、文件写入失败或资料缺失导致无法完成完整交付,必须明确说明失败点,并尽量交付已经完成的部分。不要只输出配图提示词代替图片文件,不要只输出文章正文代替 Markdown 文件。
## 多渠道交付策略 ## 多渠道交付策略
完整交付默认生成 Markdown、assets 和 ZIP,但最终发送方式按渠道和用户要求调整。 完整文章交付默认生成 Markdown、assets 和 ZIP;小红书卡片和大图片交付默认生成 `assets/`、`final_manifest.json` 和 GitHub 分批链接。最终发送方式按渠道和用户要求调整。
- 本地 / Web:优先给 Markdown + ZIP;如用户要图片,也附独立图片。 - 本地 / Web:完整文章可给 Markdown + ZIP;小红书卡片给 `assets/`、`final_manifest.json` 和 GitHub 链接。
- Telegram / 聊天:ZIP 可能失败,优先发送 Markdown + 多张图片独立附件;ZIP 作为可选补充。 - GitHub:图片较多、文件较大、Telegram 可能失败或用户要求最终交付时,先读 `references/github-delivery.md`,确认交付仓库、专用 KEY / token / deploy key、仓库隐私级别和目标路径;配置就绪后按批次推送,例如 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets`。每批推送后记录 GitHub 文件路径、commit、release 或下载链接。
- Telegram / 聊天:只负责通知进度和给 GitHub 链接;不要依赖 Telegram 直接承载 ZIP 或大图片包。少量小文件附件只能作为补充,不作为唯一交付证据。
- Email:按用户要求选择 ZIP 或独立附件。 - Email:按用户要求选择 ZIP 或独立附件。
- 用户说“打包发我 email”:发送 ZIP。 - 用户说“打包发我 email”:发送 ZIP。
- 用户说“不打包”“分多个附件”:发送 Markdown 和图片作为多个独立附件,不再附 ZIP。 - 用户说“不打包”“分多个附件”:发送 Markdown 和图片作为多个独立附件,不再附 ZIP。
- 若附件发送失败,不要反复重复发送同一批附件;改用更稳定的渠道或分批发送,并说明只保留最后版本。 - 若附件发送失败,不要反复重复发送同一批附件;改用更稳定的渠道或分批发送,并说明只保留最后版本。
- 发送前确认附件对应的是最后确认版,不要把旧图、旧 Markdown 或临时文件发出。 - 发送前确认附件对应的是最后确认版,不要把旧图、旧 Markdown 或临时文件发出。
## 小红书卡片交付模式
触发词包括“小红书”“卡片”“多图”“4 图看完”“发小红书”“3:4”“9:16”。
执行规则:
1. 不默认走完整文章 + Markdown + ZIP;除非用户明确要求,只交付小红书标题、正文、话题标签和多张独立 PNG。
2. 出图前确认比例 `3:4` 或 `9:16`,确认视觉方向 `2D 扁平风` 或 `3D 轻拟物风`。用户没指定时推荐 `2D 扁平风`。
3. 每张图 prompt 必须包含原生竖版防压扁规则:`native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
4. 每张图按一个 job / 一次请求 / 一次 QA 推进,状态文件记录 prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub。
5. QA 通过后生成 `final_manifest.json`,只包含最终图;推送或打包只能读取该 manifest 或状态文件里标记为 final 的 assets。
6. GitHub 分批推送前必须按 `references/github-delivery.md` 完成或确认仓库和专用凭据配置。缺少配置时先输出本机配置步骤并暂停 push,不生成假链接。
7. GitHub 分批推送建议:
- `batch-01-cover`:封面图和封面 prompt。
- `batch-02-content-cards`:内容卡、流程卡、避坑卡。
- `batch-03-final-assets`:`final_manifest.json`、全部 final assets、状态文件。
8. 每批推送后立即回报 GitHub 文件路径、commit、release 或下载链接,并写入 `github_batches.json` 或状态文件。
9. 最终回复固定包含 GitHub 链接、图片数量、风格:2D / 3D、比例:3:4 / 9:16、批次数量、QA 是否完成、已知限制。
## GitHub 交付配置门禁
GitHub 交付前必须先完成以下检查:
- 已读取 `references/github-delivery.md`。
- 已确认目标仓库,例如 `OWNER/maomomo-delivery`。
- 仓库默认是私有;公开仓库必须经用户明确确认。
- 已确认认证方式:`gh auth login`、`GH_TOKEN` fine-grained PAT,或 SSH deploy key。
- 凭据必须是专用且最小权限:fine-grained PAT 只给交付仓库 `Contents: Read and write`;deploy key 只授权单仓库 write access。
- 不要求用户在聊天里粘贴 token 或私钥;不把密钥写入项目文件、manifest、状态文件、日志或 commit。
- `github_batches.json` 只记录 batch、文件路径、commit、release / url 和备注。
## 配图执行规则 ## 配图执行规则
具体视觉风格、图片清单、文件命名、Markdown 插图和图片 QA 规则见 `references/visual-assets.md`。本节只规定完整交付时的执行边界。
当用户要求“配图也按 Skill”“高清配图”“精美配图”“生成配图”“文章和配图一起”时: 当用户要求“配图也按 Skill”“高清配图”“精美配图”“生成配图”“文章和配图一起”时:
- 必须生成独立 PNG 图片文件,而不是只给提示词。 - 必须生成独立 PNG 图片文件,而不是只给提示词。
- 出图前必须先写入 prompt 文件,包含文件名、用途、放置位置、核心文案、完整 prompt、alt text、风格和依赖素材。
- 必须先生成一张样张,优先封面图;用户确认前不得生成剩余图片。
- 样张确认后,每张图按一个 job / 一次请求 / 一次 QA 推进;运行环境支持子 agent 时,可以一图一个子 agent 并发。
- 不要只生成一张大长图替代所有配图。 - 不要只生成一张大长图替代所有配图。
- 每张图必须有独立主题,例如封面图、活动总览图、时间线图、重点规则图、交易法图、避坑图、FAQ 图。 - 每张图必须有独立主题,例如封面图、活动总览图、时间线图、重点规则图、交易法图、避坑图、FAQ 图。
- 不得使用未经用户提供的真实银行 Logo、券商 Logo 或支付品牌 Logo。 - 银行、券商、支付品牌、卡组织或支付网络 Logo 只要已由官方材料、用户素材或用户明确口径核对正确,就可以作为辅助识别出现;用户未提供素材时,应主动尽力从官方活动页、条款 PDF、品牌资源页、官网 / App 截图等来源查找准确 Logo。无法核对时使用通用符号。
- 出现信用卡、支付卡、卡组织、支付网络或机构 Logo 时,必须按官方材料或用户素材核对,并在 prompt、manifest 或 `deck_spec.json` 记录参考素材、卡组织、`allowed_brand_marks` 和 `brand_mark_sources`;不得把银联 / UnionPay 画成 Mastercard、Visa 等其他卡组织。
- 第三方 Logo 必须尺寸克制、位置辅助,不能抢占、替代或压过 `MAOMOMO` 主标识。
- 不得伪造真实 App 截图。 - 不得伪造真实 App 截图。
- 生成图应明确是“MAOMOMO 风格示意图 / 信息图”。 - prompt 中的视觉风格只能写成后台画面描述,不要写容易被模型照抄的中文风格标签;可见文字只允许出现指定业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识。
- 生成后必须保存到 `assets/`,并插入 Markdown。 - 小红书 3:4 / 9:16 图片必须原生竖版重新构图,不能把横版设计压进竖图;prompt 必须包含防压扁和防拉伸英文约束。
- 图片文件名使用稳定英文小写命名,例如 `01-cover.png`、`02-overview.png`、`03-timeline.png`、`04-key-rule.png`、`05-strategy.png`、`06-pitfalls.png`。 - 生成图应明确是 MAOMOMO 示意图 / 信息图,不伪装成真实截图或官方页面。
- 不要使用中文文件名作为最终 assets 文件名,避免站点路径兼容问题。 - 生成后的原始最终图先保存到 `origin_image/`,通过 QA 后同步到 `assets/`,并插入 Markdown。
- 港币金额在发布稿、图片文案、prompt、manifest 和 `deck_spec.json` 中统一写 `HKD`,来源原文抽取文件除外。
- 图片文件名使用稳定英文小写命名,并包含主题前缀,例如 `hsbc-mastercard-alipay-campaign-01-cover.png`。
- 不要使用中文文件名或缺少主题前缀的通用文件名作为最终 assets 文件名,避免站点路径兼容和多文章混淆问题。
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代 GPT Image2 / 图片 API 生成最终配图。
## 图片生成偏差处理 ## 图片生成偏差处理
@@ -82,7 +130,9 @@ article_package.zip
- 只输出配图提示词,不生成图片文件。 - 只输出配图提示词,不生成图片文件。
- 只生成图片,不插入 Markdown。 - 只生成图片,不插入 Markdown。
- 只生成一张总图,不生成多张独立配图。 - 只生成一张总图,不生成多张独立配图。
- 只给 assets,不打 ZIP。 - 完整文章模式只给 assets,不打 ZIP。
- 小红书卡片只扫描目录交付,未生成 `final_manifest.json`,导致草稿、失败图、旧比例图混入最终包。
- Telegram 发送失败后仍反复传大 ZIP,而不是改用 GitHub 分批链接。
- 只给一段新增内容,不输出更新后的完整文章。 - 只给一段新增内容,不输出更新后的完整文章。
- 只给旧文件链接,没有确认文件内容已更新。 - 只给旧文件链接,没有确认文件内容已更新。
- 修改了正文,但没有同步更新标题、摘要、配图清单、来源区和 ZIP 包。 - 修改了正文,但没有同步更新标题、摘要、配图清单、来源区和 ZIP 包。
@@ -111,25 +161,52 @@ article_package.zip
4. 如果用户要求文件或打包,必须重新生成 ZIP。 4. 如果用户要求文件或打包,必须重新生成 ZIP。
5. 最终回复给出最新 Markdown 和 ZIP 链接,避免继续给旧文件。 5. 最终回复给出最新 Markdown 和 ZIP 链接,避免继续给旧文件。
## 远程图片路径替换
当用户提供 WordPress / CDN 上传后的 `<img ... src="...">` 片段或远程图片 URL,并要求替换文章图片路径时:
1. 只替换 Markdown 中的图片 URL,不改正文、alt text、图片顺序和本地 assets 文件。
2. 优先按文件名或 title 中的序号 / 语义匹配,例如 `01-cover` 对封面图,`04-rebate-card` 对到账截图卡片;不要按用户粘贴顺序机械替换,除非用户明确说按粘贴顺序。
3. 替换后用 `rg` 或等效检查确认没有残留本地 `assets/` / `origin_image/` 引用。
4. 若文章已全部改为远程图,Markdown 本地图片路径检查可以跳过本地资产存在性要求,但仍要检查远程 URL 已写入正确位置。
5. 如果需要重新打包,ZIP 至少包含最新 `article.md`;是否继续附本地 `assets/` 取决于用户要“发布稿”还是“完整归档包”,最终回复要说明包内内容。
## 最终交付前检查清单 ## 最终交付前检查清单
交付前必须逐项确认: 交付前必须逐项确认:
- Markdown 文件已生成。 - Markdown 文件已生成。
- `outline.md` 已按用户确认版本保存。
- `prompts/` 中每张图的 prompt 文件已保存,且与最终图片数量和顺序一致。
- 一张样张已确认,后续图片继承同一生图后端和视觉系统。
- `slide_jobs.json` / `slide_run_state.json` 已记录图片任务状态;如缺少记录,最终回复必须说明原因。
- 每张图的状态记录包含 prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub。
- `final_manifest.json` 已生成,并且最终交付只读取 manifest 中列出的 final assets。
- 需要 GitHub 交付时,已确认 `references/github-delivery.md` 中的仓库、专用凭据、隐私级别和目标路径;缺失时最终回复说明配置阻塞项。
- `origin_image/` 中最终原始图片存在。
- `assets/` 目录已生成。 - `assets/` 目录已生成。
- 所有 Markdown 图片路径都能对应到真实图片文件。 - 已运行 `uv run python scripts/maomomo_check_markdown_assets.py article/article.md` 或等效脚本,确认所有 Markdown 图片路径都能对应到真实图片文件。
- 图片是多张独立 PNG,不是一张大图代替全部。 - 图片是多张独立 PNG,不是一张大图代替全部。
- 图片文件名包含文章主题前缀,未沿用旧文章文件名。
- 图片可见文字没有出现风格标签、prompt 描述、额外 slogan 或无关文案;除业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识外,没有多余文字。
- 图片日期、金额、公式、卡组织 / 支付网络 / 机构 Logo 和禁用分支都已核对。
- 用户未提供 Logo / 卡面素材时,已尽力查找准确标识,并在 `sources/`、prompt、manifest 或 `deck_spec.json` 记录素材来源和核对结论;无法核对时没有凭记忆画真实 Logo。
- 第三方 Logo 没有抢占、替代或压过 `MAOMOMO` 主标识。
- 发布稿、图片文案、prompt、manifest、`deck_spec.json` 和 `speech.md` 中港币金额统一使用 `HKD`。
- 文末来源区只包含实际使用的来源。 - 文末来源区只包含实际使用的来源。
- 正文没有 `contentReference`、`oaicite`、调试标记或工具残留。 - 正文没有 `contentReference`、`oaicite`、调试标记或工具残留。
- 用户要求删除的分支,标题、正文、配图清单和来源区都已删除干净。 - 用户要求删除的分支,标题、正文、配图清单和来源区都已删除干净。
- ZIP 包已创建。 - 完整文章模式下 ZIP 包已创建;小红书卡片模式下 `final_manifest.json` 和 GitHub 批次记录已创建。
- 用户最后确认的标题是否已同步到 frontmatter 和 H1。 - 用户最后确认的标题是否已同步到 frontmatter 和 H1。
- 用户要求删除的词是否只删除指定词,未误删整条信息。 - 用户要求删除的词是否只删除指定词,未误删整条信息。
- 正文是否残留编辑指令或模型解释。 - 正文是否残留编辑指令或模型解释。
- 用户不喜欢的模糊词,例如“更像”“可能”“大致”,是否已按上下文清理。 - 用户不喜欢的模糊词,例如“更像”“可能”“大致”,是否已按上下文清理。
- 用户提供的明确实测结论是否写成确定语气,未被改软成“可能 / 疑似 / 大概”。
- 用户提供的“以往惯例 / 过往尿性 / 过往经验”是否写成“大概率 / 按过往案例判断”的强概率语气,并保留合理边界。
- TLDR 是否过长,是否适合当前平台。 - TLDR 是否过长,是否适合当前平台。
- 小红书图是否为 9:16 竖版,是否每张都有 MAOMOMO 标识。 - 小红书图是否为用户确认的 3:4 或 9:16,是否每张都有 MAOMOMO 标识,是否为原生竖版重构,是否没有压扁或拉伸。
- 小红书图是否全组保持同一 2D / 3D 视觉方向。
- 图片是否匹配最终文章口径,未残留旧规则、旧金额、旧标题。 - 图片是否匹配最终文章口径,未残留旧规则、旧金额、旧标题。
- 邮件/聊天附件是否按用户要求选择 ZIP 或多个独立附件。 - 邮件/聊天附件是否按用户要求选择 ZIP、多个独立附件或 GitHub 分批链接;Telegram 是否只承载通知和链接。
- 若已发送过旧版本,最终回复要明确这是最后版本,避免重复发同一批图片。 - 若已发送过旧版本,最终回复要明确这是最后版本,避免重复发同一批图片。
- 最终回复包含 Markdown 下载链接和 ZIP 下载链接。 - 完整文章最终回复包含 Markdown 下载链接和 ZIP 或 GitHub 链接;小红书卡片最终回复包含 GitHub 链接、图片数量、风格、比例、批次数量、QA 是否完成和已知限制。
+138
View File
@@ -0,0 +1,138 @@
# GitHub 交付配置
本文件用于 GitHub 分批交付前的仓库和专用凭据配置。只要任务需要 GitHub push、release、下载链接,或用户提到 GitHub 仓库、专用 KEY、token、PAT、deploy key,先读本文件。
## 目标
- 使用专门的交付仓库,不和源码仓库混放。
- 默认使用私有仓库,除非用户明确要求公开交付。
- 使用专用、最小权限凭据;不要复用高权限个人 token。
- 不在聊天、prompt、manifest、状态文件、README、日志或提交记录里暴露完整 token / 私钥。
## 推荐配置
推荐环境变量:
```bash
export MAOMOMO_GITHUB_REPO="OWNER/maomomo-delivery"
export MAOMOMO_GITHUB_BRANCH="main"
export MAOMOMO_GITHUB_BASE_PATH="deliveries"
```
认证方式任选其一:
1. GitHub CLI 登录,适合本机交互使用:
```bash
gh auth login --web --git-protocol https
gh auth status
```
2. Fine-grained PAT,适合脚本或无头环境:
- Repository access:只选交付仓库。
- Repository permissions:只给 `Contents: Read and write`。
- 过期时间:建议 30-90 天。
- 本机设置为环境变量,不要发到聊天里:
```bash
export GH_TOKEN="只在本机 shell 中设置,不写入项目文件"
gh auth status
```
3. SSH deploy key,适合只给单仓库写权限:
```bash
ssh-keygen -t ed25519 -f "$HOME/.ssh/maomomo_delivery_ed25519" -C "maomomo-delivery"
```
把 `.pub` 公钥加入 GitHub 仓库 Settings -> Deploy keys,并勾选 write access。私钥只留本机,推送时使用:
```bash
export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/maomomo_delivery_ed25519 -o IdentitiesOnly=yes"
```
## 首次仓库准备
如果用户还没有交付仓库,先引导其创建或确认仓库名。不要擅自创建公开仓库。
```bash
gh repo create "$MAOMOMO_GITHUB_REPO" --private --clone=false
```
检查仓库:
```bash
gh repo view "$MAOMOMO_GITHUB_REPO" --json nameWithOwner,visibility,defaultBranchRef
```
如果 `visibility` 不是 `PRIVATE`,先提醒用户可能公开图片、prompt、状态文件和活动信息;除非用户明确确认,否则不要继续推送。
## 本地工作目录
GitHub 交付工作目录默认放在 `$HOME/dev` 下,避免散落到桌面、下载目录或系统目录:
```bash
mkdir -p "$HOME/dev/maomomo-delivery-work"
cd "$HOME/dev/maomomo-delivery-work"
```
如果仓库尚未 clone:
```bash
gh repo clone "$MAOMOMO_GITHUB_REPO" repo
```
目标路径建议:
```text
deliveries/{project_slug}/
├── batch-01-cover/
├── batch-02-content-cards/
├── batch-03-final-assets/
├── final_manifest.json
├── github_batches.json
└── slide_run_state.json
```
## 推送前检查
推送前必须确认:
- `MAOMOMO_GITHUB_REPO` 已设置,或用户已明确给出目标仓库。
- 已通过 `gh auth status`,或 SSH deploy key 已能写入目标仓库。
- 仓库默认为私有;公开仓库必须经用户明确确认。
- `final_manifest.json` 已生成,最终文件只从 manifest 或状态文件里的 final assets 读取。
- 本批文件不包含 token、私钥、账号、真实个人信息或未确认敏感素材。
- `github_batches.json` 只记录 batch、路径、commit、release / url,不记录任何密钥。
## 分批推送流程
每批只复制本批需要交付的文件,不扫描整个项目目录。提交信息优先中文:
```bash
git add deliveries/{project_slug}/batch-01-cover deliveries/{project_slug}/github_batches.json
git commit -m "交付:{project_slug} batch-01-cover"
git push origin "$MAOMOMO_GITHUB_BRANCH"
```
推送后用状态脚本记录:
```bash
uv run python scripts/maomomo_job_state.py github-batch article \
--batch batch-01-cover \
--commit "<commit sha>" \
--url "https://github.com/OWNER/REPO/commit/<commit sha>" \
--paths assets/example-01-cover.png prompts/example_01.json final_manifest.json
```
每批推送后回复用户:GitHub 文件路径、commit、release 或下载链接,并说明下一批状态。
## 阻塞处理
如果缺少仓库或凭据,不要继续生成假链接,也不要要求用户把 token 粘贴到聊天里。回复应包含:
- 当前阻塞:缺少交付仓库、未登录 GitHub CLI、token 权限不足、deploy key 未授权等。
- 本机配置命令。
- 需要用户确认的仓库名或认证方式。
- 已完成的本地文件路径和下一步。
+270
View File
@@ -0,0 +1,270 @@
# 图片生成脚本与后端选择
需要实际生成配图文件、使用 API/CLI fallback、批量生成 assets,或排查图片接口配置时,先读本文件。
## 后端选择
优先顺序:
1. Codex / 当前 agent 的内置图片生成工具。适合大多数配图任务。
2. `scripts/maomomo_image_gen.py` API/CLI fallback。适合用户明确要求 API/CLI、内置工具不可用、需要批量 manifest、或希望把生成过程记录到文件时。
不要因为脚本存在就强制使用脚本。若内置图片工具可用,仍优先使用内置工具生成独立 PNG,再保存到 `assets/`。
生成第一张图前,必须向用户确认:
- 已检查哪种内置图片生成工具是否可用。
- 准备使用的生图后端。
- 是否需要 `scripts/maomomo_image_gen.py` fallback,以及原因。
- 当前会沿用环境变量或配置文件中的 baseURL、model、quality,不会擅自改成 `low`。
确认后,整篇文章所有图片保持同一个生图后端和质量配置。只有用户明确要求切换,或当前后端无法完成必需能力时,才重新确认后端。
## 配置来源
脚本按以下顺序读取配置:
- `MAOMOMO_IMAGE_API_KEY` / `OPENAI_API_KEY`
- `MAOMOMO_IMAGE_BASE_URL` / `OPENAI_BASE_URL`
- `MAOMOMO_IMAGE_MODEL` / `CODEX_PPT_IMAGE_MODEL`
- `~/.codex/config.toml` 中的 `base_url`
- `~/.codex/auth.json` 中可识别的 API key
`scripts/maomomo_image_gen.py` 必须兼容 Python 3.10 及以下没有内置 `tomllib` 的环境;读取 `~/.codex/config.toml` 时应使用脚本内置的轻量 TOML fallback,不要求用户额外安装 `tomli`。
不要在日志、回答、Markdown 或 manifest 中写入完整 API key。
## 单张生成
```bash
uv run python scripts/maomomo_image_gen.py generate \
--out article/origin_image/hsbc-mastercard-alipay-campaign-01-cover.png \
--style warm-fintech-guide \
--title "汇丰 Mastercard 支付宝活动" \
--image-type "封面图" \
--core-text "先看报名、门槛和封顶,避免白刷"
```
先只看 prompt:
```bash
uv run python scripts/maomomo_image_gen.py generate \
--out article/origin_image/hsbc-mastercard-alipay-campaign-01-cover.png \
--style data-card-dashboard \
--title "返现计算图" \
--image-type "计算图" \
--core-text "HKD 190 ÷ 9% ≈ HKD 2,112,超过部分没有额外收益" \
--dry-run
```
## prompt 文件输入
- `prompts/*.json` 用于管理图片 job、路径、参考素材、可见文字白名单和完整 prompt。
- 如果生成脚本的 `--prompt-file` 读取纯文本,必须先从 JSON 的 `prompt` 字段抽取为同名 `.prompt.txt`,再把 `.prompt.txt` 传给图片接口。
- 不要把整个 JSON 文件作为 `--prompt-file` 传入;否则模型可能把 JSON 外壳、字段名或管理信息画进图片。
- 抽取出的 `.prompt.txt` 仍必须包含可见文字限制:只允许出现指定业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,不得出现风格标签、prompt 描述、额外 slogan 或无关文字。
推荐抽取方式:
```bash
uv run python - <<'PY'
from pathlib import Path
import json
path = Path("article/prompts/hsbc-mastercard-alipay-campaign_01.json")
data = json.loads(path.read_text(encoding="utf-8"))
prompt = str(data["prompt"]).strip()
out = path.with_suffix(".prompt.txt")
out.write_text(prompt + "\n", encoding="utf-8", newline="\n")
print(out)
PY
```
然后生成:
```bash
uv run python scripts/maomomo_image_gen.py generate \
--out article/origin_image/hsbc-mastercard-alipay-campaign-01-cover.png \
--prompt-file article/prompts/hsbc-mastercard-alipay-campaign_01.prompt.txt
```
## 批量 manifest
manifest 示例:
```json
{
"images": [
{
"file_name": "origin_image/hsbc-mastercard-alipay-campaign-01-cover.png",
"type": "封面图",
"title": "汇丰 Mastercard 支付宝活动",
"core_text": "先报名,再看门槛和封顶",
"alt_text": "MAOMOMO 汇丰 Mastercard 支付宝返现活动封面图",
"style": "warm-fintech-guide",
"visual_direction": "2D 扁平风",
"delivery_batch": "batch-01-cover",
"allowed_brand_marks": ["HSBC", "Mastercard", "支付宝"],
"brand_mark_sources": [
{
"name": "Mastercard",
"source_url": "https://example.com/official-source",
"source_type": "官方活动页 / 官方品牌资源页 / 用户素材",
"checked_at": "YYYY-MM-DD"
}
],
"brand_mark_rule": "这些标识必须来自官方活动材料或用户素材核对;只能小尺寸辅助出现,不能抢 MAOMOMO 主标识位置",
"aspect_ratio": "16:9"
},
{
"file_name": "origin_image/hsbc-mastercard-alipay-campaign-02-calculation.png",
"type": "计算图",
"title": "怎么刷到刚刚好",
"core_text": "HKD 190 ÷ 9% ≈ HKD 2,112,超过部分没有额外收益",
"style": "data-card-dashboard"
}
]
}
```
运行:
```bash
uv run python scripts/maomomo_image_gen.py batch \
--manifest article/image_manifest.json \
--base-dir article
```
`file_name` 必须与本次生成目录一致,且最终文件必须真实存在。发布用 Markdown 默认引用 `assets/`,因此输出到 `origin_image/` 后要在 QA 通过时同步到同名 `assets/` 文件。
重要限制:
- 不要在样张确认前运行批量生成。
- 默认用 `generate` 单张生成;`batch` 只用于用户明确授权的全量生成或 dry-run prompt 输出。
- 即使使用 `batch`,脚本也是逐项请求图片接口;生成后仍要逐张 QA,并把通过的图片同步到 `assets/`。
- 发布用 Markdown 默认引用 `assets/`,因此如果 manifest 输出到 `origin_image/`,必须在 QA 后复制到对应 `assets/` 路径。
- 金融活动 manifest 中涉及信用卡、支付卡、支付网络或机构 Logo 时,必须记录 `reference_assets`、`payment_network`、`allowed_brand_marks`、`brand_mark_sources` 和 `card_art_rule`,并在 QA 中核对卡组织和第三方 Logo 没有画错,也没有抢 MAOMOMO 主标识位置。用户未提供 Logo / 卡面素材时,先尽力查找官方或可信来源并保存到 `sources/`;无法核对时使用通用符号,不要凭记忆画真实 Logo。
- manifest、prompt、`deck_spec.json` 和发布稿中的港币金额统一使用 `HKD`,来源原文抽取文件除外。
- 小红书卡片 manifest 必须记录 `aspect_ratio`、`visual_direction`、`delivery_batch` 和 `portrait_composition_rules`;prompt 必须包含 `native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
- 小红书卡片默认不生成完整文章、Markdown 或 ZIP;最终文件清单以 `final_manifest.json` 为准。
## 内置风格名
可用:
- `warm-fintech-guide`
- `clean-professional`
- `clean-editorial`
- `data-card-dashboard`
- `handdrawn-note`
- `creative-magazine`
- `retro-flat-illustration`
- `e-ink-editorial`
- `scientific-defense`
- `mckinsey-brief`
- `xiaohongshu-vertical`
查看完整描述:
```bash
uv run python scripts/maomomo_image_gen.py styles
```
## 失败处理
- API key 缺失:提示用户配置环境变量或检查 Codex 配置,不要编造图片文件。
- 接口失败:保留已生成图片,说明失败项,必要时用 `--dry-run` 交付 prompt 清单作为临时排查材料。
- 如果脚本实际请求到了默认 `https://api.openai.com/v1`,但项目配置应使用兼容接口,先检查 `MAOMOMO_IMAGE_BASE_URL` / `OPENAI_BASE_URL` 和 `~/.codex/config.toml` 解析结果;Python 3.10 及以下必须走脚本内置 TOML fallback。不要把兼容接口的 key 误发到默认 OpenAI API 后反复重试。
- 真实银行 / 券商 / 钱包截图等敏感素材需要上传图片 API 做 edit 前,先向用户确认实际 API 地址 / baseURL、接口归属、是否为用户自有兼容服务以及是否允许外发。确认且安全策略允许后才上传;未确认、确认不允许或安全策略不允许时,先生成“不含真实截图”的空白模板,再本地嵌入真实截图;状态记录 `backend_used` 写成类似 `GPT Image2 template + local screenshot embedding`,并在 QA note 说明截图未外发。
- 图片文字乱码或金额错误:重新生成该单张图片;不要手工覆盖文字伪装成模型输出。
- 图片不符合风格:先更新 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、失败原因
- 小红书卡片字段:`aspect_ratio`、`visual_direction`、`delivery_batch`
- 最终交付字段:`qa_status`、`entered_assets`、`final`、`github_pushed`、`github_batch`
使用 `scripts/maomomo_job_state.py` 更新状态,不要手工改 JSON 后声称完成。
初始化状态:
```bash
uv run python scripts/maomomo_job_state.py init article \
--selected-backend "built-in image tool"
```
查看状态:
```bash
uv run python scripts/maomomo_job_state.py status article
```
记录 dispatch:
```bash
uv run python 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
```
单张 QA 通过后,记录结果并同步到 `origin_image/` 和 `assets/`:
```bash
uv run python 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
uv run python scripts/maomomo_job_state.py blocker article \
--job hsbc-mastercard-alipay-campaign_02 \
--agent-id "<agent id>" \
--reason "选定图片后端在 worker 中不可用"
```
如果状态脚本失败,最终报告必须说明原因,并附上状态文件当前路径和未完成 job。
生成最终 manifest:
```bash
uv run python scripts/maomomo_job_state.py final-manifest article
```
`final_manifest.json` 只写入状态为 `recorded`、QA 已通过、且 `assets/` 文件真实存在的图片。最终推送、打包或给链接时只能读取该文件,不能扫描整个目录。
记录 GitHub 批次:
运行前必须先按 `references/github-delivery.md` 完成交付仓库和专用凭据配置;`github-batch` 只记录已经真实推送后的批次结果,不负责创建仓库、保存密钥或生成假链接。
```bash
uv run python scripts/maomomo_job_state.py github-batch article \
--batch batch-01-cover \
--commit "<commit sha>" \
--url "https://github.com/org/repo/commit/<commit sha>" \
--paths assets/example-01-cover.png prompts/example_01.json
```
每批推送后都要写入 batch 记录。状态文件里的单图记录会同步标记 `github_pushed=true`、`github_batch=batch-xx`;最终回复从 `github_batches.json` 或状态文件读取批次数量和链接。
@@ -0,0 +1,49 @@
# 创意杂志风
## 适用场景
- 传播型封面
- 活动盘点、经验总结、观点型文章
- 需要更强点击感但不夸张的 MAOMOMO 头图
## MAOMOMO 出图 Brief
```json
{
"style_name": "creative-magazine",
"visual_direction": "Chinese editorial magazine cover for fintech tutorial, strong headline, generous whitespace, confident but not sensational",
"canvas": {
"aspect_ratio": "16:9",
"background": "off-white, warm gray, or soft brand color field",
"composition": "oversized headline, one hero financial object, short metadata labels, cat mascot as brand accent",
"density": "low to medium"
},
"color_palette": {
"primary": "ink black or deep slate",
"accent": "MAOMOMO orange plus one supporting color",
"rule": "use 2-3 strong colors only"
},
"typography": {
"title": "large Chinese display headline",
"metadata": "small label for date, guide type, risk note",
"text_quality": "short Chinese phrases only"
},
"layout_patterns": [
"large headline with financial object collage",
"split editorial cover with title and single illustration",
"big number cover with tiny caveat line"
],
"avoid": [
"marketing hype",
"too many stickers",
"random stock photo collage",
"unreadable small text"
]
}
```
## Prompt 片段
```text
中文金融文章封面,大标题、强留白、暖橙品牌点缀,金融物件原创插画,白橘猫作为角落品牌元素,画面像编辑部攻略封面,不像银行广告,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,50 @@
# 复古扁平插画风
## 适用场景
- 轻松羊毛攻略
- 用卡姿势、活动经验
- 不想太严肃但仍要保持金融可信度的配图
## MAOMOMO 出图 Brief
```json
{
"style_name": "retro-flat-illustration",
"visual_direction": "retro flat Chinese fintech illustration, friendly, practical, low-saturation warm palette",
"canvas": {
"aspect_ratio": "16:9",
"background": "warm off-white or muted pastel",
"composition": "flat illustrated financial objects with title cards and short labels",
"density": "medium"
},
"color_palette": {
"primary": "warm orange, muted teal, cream, brick red",
"neutral": "soft brown-gray text",
"rule": "low saturation, no luxury gold"
},
"typography": {
"title": "bold rounded Chinese title",
"labels": "short tag labels",
"text_quality": "large and simple"
},
"layout_patterns": [
"cat with cards, receipt, calendar and calculator",
"flat route map with currency labels",
"coupon / cashback object board",
"risk and correct action two-column illustration"
],
"avoid": [
"overly cute childish poster",
"fake brand logos",
"busy retro texture",
"tiny text"
]
}
```
## Prompt 片段
```text
低饱和暖色,扁平信用卡、小票、日历、计算器和白橘猫,中文标题清楚,轻松但不夸张,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,54 @@
# 小红书竖版攻略风
## 适用场景
- 小红书发布版
- 4 图看完
- 多图卡片 / 3:4 / 9:16
- 手机端快速理解的活动 / 避坑 / 教程内容
## MAOMOMO 出图 Brief
```json
{
"style_name": "xiaohongshu-vertical",
"visual_direction": "2D flat infographic card by default, optional 3D soft neumorphic direction only after user confirmation, vertical mobile-first Chinese finance guide card, big readable text, series layout",
"canvas": {
"aspect_ratio": "3:4 or 9:16, confirm before generation",
"background": "warm white or light orange",
"composition": "native portrait composition, large hook title, 2-4 key blocks, cat watermark or small mascot, vertical layout redesigned for Xiaohongshu",
"density": "low, mobile readable"
},
"color_palette": {
"primary": "warm orange and ink black",
"secondary": "soft cream or pale blue",
"rule": "high contrast text, no tiny paragraph"
},
"typography": {
"title": "very large Chinese title",
"body": "short phrases and key numbers",
"text_quality": "mobile readable at thumbnail size"
},
"layout_patterns": [
"cover: one hook plus three tags",
"core split: two lines or three numbers",
"steps: 3-5 actions",
"pitfalls: do / don't list"
],
"avoid": [
"reuse of horizontal article image",
"squeezed elements",
"stretched card / logo / text",
"mixing 2D and 3D visual directions in one image set",
"long source paragraph",
"tiny screenshot-like UI",
"exaggerated earning promise"
]
}
```
## Prompt 片段
```text
3:4 或 9:16 原生竖版,native portrait composition,vertical layout redesigned for Xiaohongshu,大字少字,手机端可读,暖橙白底,白橘猫水印,适合多图系列,包含 MAOMOMO 标识。no squeezed elements,no stretched card / logo / text。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,50 @@
# 手绘便签风
## 适用场景
- 保姆式教程
- 避坑经验、实测提醒
- 账户维护、保号、操作步骤说明
## MAOMOMO 出图 Brief
```json
{
"style_name": "handdrawn-note",
"visual_direction": "clean handdrawn Chinese fintech note, paper-like background, warm sticky notes, arrows and circles",
"canvas": {
"aspect_ratio": "16:9",
"background": "near-white paper",
"composition": "short title, 3-5 annotated steps, cat holding warning sign",
"density": "medium-low, one clear idea per image"
},
"color_palette": {
"line": "soft dark gray",
"accent": "MAOMOMO orange, pale yellow sticky notes, light blue marks",
"rule": "warm but clean, not messy whiteboard"
},
"typography": {
"title": "handwritten-style Chinese title",
"labels": "short handwritten labels",
"text_quality": "avoid long sentences"
},
"layout_patterns": [
"three-step path with handdrawn arrows",
"pitfall map with warning tags",
"before / correct way comparison",
"checklist with cat reminder card"
],
"avoid": [
"messy doodle board",
"too many handwritten words",
"childish cartoon dominance",
"fake screenshots"
]
}
```
## Prompt 片段
```text
近白纸感背景,暖橙便利贴,手绘箭头和圈注,白橘猫举提示牌,中文步骤短而清晰,适合避坑和保姆式教程,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,51 @@
# 数据仪表盘风
## 适用场景
- 返现计算、费用测算
- 活动门槛、封顶、时间线
- 多路径、多产品、多活动对比
## MAOMOMO 出图 Brief
```json
{
"style_name": "data-card-dashboard",
"visual_direction": "bright fintech analytics dashboard for Chinese practical article, KPI cards and rule panels",
"canvas": {
"aspect_ratio": "16:9",
"background": "white or very light blue-gray",
"composition": "dashboard grid with title, KPI cards, calculation panel, timeline or warning list",
"density": "medium-high but readable"
},
"color_palette": {
"primary": "data blue",
"secondary": "cyan or slate",
"status": "green, orange, red used sparingly",
"brand": "MAOMOMO warm orange for key highlights"
},
"typography": {
"numbers": "large tabular numerals",
"labels": "short Chinese labels",
"text_quality": "amounts, dates, percentages must be large and legible"
},
"layout_patterns": [
"top KPI strip plus lower explanation cards",
"formula card plus before / after comparison",
"timeline plus status badges",
"route comparison table with risk dots"
],
"avoid": [
"dark monitoring wall",
"fake real dashboard screenshot",
"random numbers",
"dense unreadable table"
]
}
```
## Prompt 片段
```text
白底,KPI 卡片、计算公式、时间线和风险标签清晰分区,金额和日期大号可读,白橘猫只做小型提示元素,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,56 @@
# 暖橙金融教程风
## 适用场景
- MAOMOMO 默认站内文章配图
- 港卡、银行活动、返现攻略
- 开户、转账、保号等实操教程
- 需要亲和但可信的封面、总览图和流程图
## MAOMOMO 出图 Brief
```json
{
"style_name": "warm-fintech-guide",
"visual_direction": "MAOMOMO warm fintech guide visual, friendly Chinese practical finance tutorial, orange-white cat mascot and clean cards",
"canvas": {
"aspect_ratio": "16:9 by default",
"background": "clean white or warm off-white",
"composition": "large practical title, 3-5 key information cards, cat guide, clear warning or takeaway area",
"density": "medium, practical and readable"
},
"color_palette": {
"primary": "MAOMOMO warm orange",
"secondary": "soft blue-gray or teal",
"neutral": "white, light gray, slate text",
"rule": "warm and clean; avoid luxury finance gold and dark cyber style"
},
"typography": {
"title": "bold readable Chinese title",
"body": "short Chinese labels and key numbers",
"text_quality": "all Chinese text must be clear and exact"
},
"maomomo_elements": {
"cat": "friendly white-orange cat guide, small to medium size",
"logo": "visible MAOMOMO text mark"
},
"layout_patterns": [
"cover title plus cat and financial objects",
"overview cards for date, threshold, reward and pitfall",
"step flow with arrows and warning tags",
"final reminder card with cat holding sign"
],
"avoid": [
"fake screenshots",
"bank advertisement tone",
"too childish mascot",
"unverified brand logos"
]
}
```
## Prompt 片段
```text
明亮白底,暖橙强调色,干净信息卡片,友好的白橘猫作为向导,中文标题和关键数字清晰,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,56 @@
# 清爽专业风
## 适用场景
- 银行 / 券商规则整理
- 开户、保号、转账教程
- 正式但不沉闷的 MAOMOMO 站内文章
- 需要兼顾可信度和实操感的封面、总览图、流程图
## MAOMOMO 出图 Brief
```json
{
"style_name": "clean-professional",
"visual_direction": "clean professional Chinese fintech article visual, calm tutorial layout, structured evidence cards, readable and trustworthy",
"canvas": {
"aspect_ratio": "16:9 by default",
"background": "warm white, very light gray, or pale blue-gray",
"composition": "clear title zone, structured content cards, one concise takeaway strip",
"density": "medium, enough detail without looking like a bank advertisement"
},
"color_palette": {
"primary": "professional blue or slate",
"secondary": "teal or muted cyan",
"accent": "MAOMOMO warm orange for highlights",
"neutral": "slate gray, pale border, white card background"
},
"typography": {
"title": "bold readable Chinese sans-serif",
"body": "clean Chinese sans-serif, left aligned",
"text_quality": "Chinese text must be exact and readable"
},
"maomomo_elements": {
"cat": "small white-orange cat guide near title or takeaway, not the main subject",
"logo": "MAOMOMO text mark in a corner or title area"
},
"layout_patterns": [
"title plus three key cards",
"left process diagram plus right pitfall cards",
"top conclusion strip plus lower rule table",
"timeline plus final reminder"
],
"avoid": [
"official bank advertisement tone",
"luxury finance gold",
"dense tiny tables",
"fake app screenshots"
]
}
```
## Prompt 片段
```text
浅色背景,蓝绿与暖橙强调色,结构化信息卡片,清晰中文标题和步骤,白橘猫作为小型向导,包含 MAOMOMO 标识,不伪造真实 App 截图。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,51 @@
# 清爽编辑部风
## 适用场景
- SEO 长文封面
- 解释型文章、经验总结
- 品牌感更强但仍克制的站内头图
## MAOMOMO 出图 Brief
```json
{
"style_name": "clean-editorial",
"visual_direction": "clean editorial Chinese fintech article image, calm magazine-like cover, readable and restrained",
"canvas": {
"aspect_ratio": "16:9",
"background": "white, off-white, or light warm gray",
"composition": "large title, one clear visual metaphor, small MAOMOMO brand mark and cat accent",
"density": "low to medium"
},
"color_palette": {
"primary": "ink black or deep slate",
"accent": "MAOMOMO orange",
"secondary": "soft blue-gray or light beige",
"rule": "minimal palette, no noisy decoration"
},
"typography": {
"title": "large clean Chinese headline",
"body": "one short subtitle or 2-3 labels",
"text_quality": "title must be exact and readable"
},
"layout_patterns": [
"headline left, visual object right",
"centered title with small metadata and cat mark",
"big keyword plus supporting labels",
"calm article cover with one metaphor object"
],
"avoid": [
"clickbait poster",
"dense dashboard cards",
"too many icons",
"official logo misuse"
]
}
```
## Prompt 片段
```text
白底大标题,轻杂志排版,暖橙点缀,留白充足,白橘猫作为角落品牌元素,适合中文金融经验文章封面,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,50 @@
# 电子墨水杂志风
## 适用场景
- 深度解释型长文
- 观点文章、行业观察
- 需要更克制、更高级的站内封面
## MAOMOMO 出图 Brief
```json
{
"style_name": "e-ink-editorial",
"visual_direction": "electronic ink editorial Chinese finance article visual, paper texture, strong headline, restrained layout",
"canvas": {
"aspect_ratio": "16:9",
"background": "off-white paper, ink black, or deep indigo",
"composition": "editorial grid with large headline, metadata strip, one abstract financial visual",
"density": "low to medium"
},
"color_palette": {
"base": "off-white and ink black",
"accent": "one restrained MAOMOMO orange or indigo accent",
"rule": "avoid colorful dashboard look"
},
"typography": {
"title": "large editorial Chinese headline",
"metadata": "small source/date/risk labels",
"text_quality": "few words, exact wording"
},
"layout_patterns": [
"large headline with metadata strip",
"big quote and one visual plate",
"huge number with footnote",
"two-column argument spread"
],
"avoid": [
"cute sticker overload",
"dashboard cards",
"shiny gradients",
"dense tables"
]
}
```
## Prompt 片段
```text
纸感背景,黑白灰为主,少量暖橙强调,大标题和元信息条,原创抽象金融视觉,克制高级,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,51 @@
# 科研答辩风
## 适用场景
- 多来源条款核对
- 规则严谨拆解
- 政策、合规、券商或银行条款解释
- 需要“证据链”感的文章配图
## MAOMOMO 出图 Brief
```json
{
"style_name": "scientific-defense",
"visual_direction": "clean academic defense style adapted for Chinese fintech article, evidence-driven, source-aware",
"canvas": {
"aspect_ratio": "16:9",
"background": "white or pale gray",
"composition": "research question / evidence / conclusion / caveat sections",
"density": "medium-high but organized"
},
"color_palette": {
"primary": "deep blue or slate",
"accent": "MAOMOMO orange for caveats and key conclusion",
"neutral": "gray lines and pale panels"
},
"typography": {
"title": "formal bold Chinese title",
"labels": "source, condition, limitation labels",
"text_quality": "precise Chinese terms"
},
"layout_patterns": [
"source comparison matrix",
"condition tree plus final conclusion",
"timeline of official rules",
"evidence cards with risk caveat"
],
"avoid": [
"over-promotional activity poster",
"uncited absolute claim",
"fake official document screenshot",
"decorative cat dominance"
]
}
```
## Prompt 片段
```text
浅色严谨版式,来源卡片、条件树、证据矩阵和结论区清晰,暖橙强调风险提示,白橘猫只作为小型品牌元素,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
@@ -0,0 +1,51 @@
# 麦肯锡简报风
## 适用场景
- 多方案路径选择
- 银行 / 券商 / 通道对比
- 成本、收益、风险取舍
- 给读者一个“怎么选”的决策图
## MAOMOMO 出图 Brief
```json
{
"style_name": "mckinsey-brief",
"visual_direction": "consulting brief style adapted for MAOMOMO practical finance article, conclusion first, precise matrix and tradeoff chart",
"canvas": {
"aspect_ratio": "16:9",
"background": "white",
"composition": "top takeaway, 2x2 matrix or comparison table, bottom recommendation",
"density": "medium-high"
},
"color_palette": {
"primary": "deep navy or black",
"accent": "MAOMOMO orange plus one strong blue",
"neutral": "gray grid lines and white panels"
},
"typography": {
"title": "assertive Chinese takeaway headline",
"body": "short consulting-style labels",
"text_quality": "precise, not salesy"
},
"layout_patterns": [
"2x2 decision matrix",
"option comparison table",
"cost-risk-benefit tradeoff chart",
"recommendation waterfall"
],
"avoid": [
"pretending to give financial advice",
"luxury consulting cosplay",
"tiny footnotes",
"overconfident guarantees"
]
}
```
## Prompt 片段
```text
结论先行,白底深色标题,2x2 矩阵或方案对比表,暖橙突出推荐路径和风险提醒,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
+223
View File
@@ -0,0 +1,223 @@
# MAOMOMO 推荐配图风格
需要选择、推荐、复用或调整配图风格时,读本文件。风格名要与 `scripts/maomomo_image_gen.py` 的 `--style` 保持一致。
## 风格参考文件
详细出图 brief 放在 `references/image-styles/`,需要使用某个风格生成图片前,优先读取对应文件:
- `references/image-styles/清爽专业风.md` -> `clean-professional`
- `references/image-styles/创意杂志风.md` -> `creative-magazine`
- `references/image-styles/暖橙金融教程风.md` -> `warm-fintech-guide`
- `references/image-styles/清爽编辑部风.md` -> `clean-editorial`
- `references/image-styles/数据仪表盘风.md` -> `data-card-dashboard`
- `references/image-styles/手绘便签风.md` -> `handdrawn-note`
- `references/image-styles/复古扁平插画风.md` -> `retro-flat-illustration`
- `references/image-styles/电子墨水杂志风.md` -> `e-ink-editorial`
- `references/image-styles/科研答辩风.md` -> `scientific-defense`
- `references/image-styles/麦肯锡简报风.md` -> `mckinsey-brief`
- `references/image-styles/小红书竖版攻略风.md` -> `xiaohongshu-vertical`
这些风格参考借鉴 `codex-ppt-skill` 的风格 brief 结构,但已改写为 MAOMOMO 文章配图场景:必须保留 MAOMOMO 标识、白橘猫品牌元素、中文可读性和“不伪造真实截图”的边界。
## 推荐顺序
默认优先推荐 `warm-fintech-guide` 或 `clean-professional`。如果文章明显更偏数字、对比、避坑、深度解释或小红书,再切换到更合适的风格。
| 风格名 | 优先使用场景 | 不适合 |
|---|---|---|
| `warm-fintech-guide` | 常规 MAOMOMO 文章、港卡、银行活动、返现攻略、开户教程 | 数字极密集的计算页、小红书竖版 |
| `clean-professional` | 正式规则整理、开户教程、保号教程、综合攻略 | 传播型封面、小红书竖版 |
| `data-card-dashboard` | 返现计算、费用测算、活动门槛、产品对比、时间线 | 情绪化避坑故事、截图驱动步骤 |
| `clean-editorial` | SEO 长文、总结盘点、品牌感封面、解释型文章 | 需要强步骤感或大量数字卡片的图 |
| `creative-magazine` | 活动盘点、传播型封面、观点总结 | 严肃规则核对、密集数据表 |
| `handdrawn-note` | 避坑经验、保号教程、操作步骤、实测提醒 | 严肃金融报告感、密集表格 |
| `retro-flat-illustration` | 轻松羊毛攻略、用卡姿势、经验分享 | 严肃合规解释、复杂来源核对 |
| `e-ink-editorial` | 深度解释、行业观察、观点长文封面 | 活动步骤教程、返现计算图 |
| `scientific-defense` | 条款核对、政策规则、证据链解释 | 轻松传播封面、小红书图 |
| `mckinsey-brief` | 多方案对比、路径选择、决策矩阵 | 情绪化故事、轻松插画 |
| `xiaohongshu-vertical` | 小红书、4 图看完、手机端竖版内容 | 常规网站横版封面和正文配图 |
## warm-fintech-guide
推荐语:
```text
我建议用 warm-fintech-guide,因为它最贴近 MAOMOMO 默认视觉:白底、暖橙、猫咪向导和金融教程卡片,适合大多数港卡、银行活动和返现攻略。
```
视觉要点:
- 明亮白底,暖橙色强调。
- 白橘猫作为向导、提示牌或角落品牌元素。
- 信息卡片清晰,数字、日期和步骤不拥挤。
- 适合 16:9 横版封面、总览图、流程图。
提示词片段:
```text
明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,现代 App 信息卡片质感,中文标题清楚,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
## data-card-dashboard
推荐语:
```text
我建议用 data-card-dashboard,因为这篇重点是规则、门槛和金额,读者需要一眼看懂数字和限制。
```
视觉要点:
- 指标卡、对比表、时间线、计算公式。
- 数字层级强,适合“封顶”“门槛”“成本”“到账时间”。
- 猫咪比例较小,只做提示和品牌识别。
- 适合返现计算图、路线对比图、活动总览图。
提示词片段:
```text
白底,暖橙和蓝灰辅助色,使用指标卡、表格、时间线和计算公式展示关键信息,中文数字清晰可读,猫咪作为小型提示元素,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
## clean-professional
推荐语:
```text
我建议用 clean-professional,因为这篇需要可信、清楚、像一份认真整理过的规则说明,但又不能像官方宣传稿。
```
详细 brief:`references/image-styles/清爽专业风.md`
## creative-magazine
推荐语:
```text
我建议用 creative-magazine,因为这篇更适合作为传播型封面,需要标题有吸引力,同时保持 MAOMOMO 的实操可信度。
```
详细 brief:`references/image-styles/创意杂志风.md`
## clean-editorial
推荐语:
```text
我建议用 clean-editorial,因为这篇更像站内解释型长文,需要封面干净、标题有传播感,但不要太像促销海报。
```
视觉要点:
- 大标题、留白、轻杂志感。
- 猫咪放在角落或标题区附近,不抢正文信息。
- 更适合封面、文章总结图、观点图。
- 画面克制,避免过度卡通。
提示词片段:
```text
白底大标题,轻杂志排版,暖橙点缀,留白充足,白橘猫作为角落品牌元素,适合中文金融经验文章封面,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
## retro-flat-illustration
推荐语:
```text
我建议用 retro-flat-illustration,因为这篇偏轻松经验分享,用低饱和扁平插画能更亲和,但不会显得像银行广告。
```
详细 brief:`references/image-styles/复古扁平插画风.md`
## handdrawn-note
推荐语:
```text
我建议用 handdrawn-note,因为这篇是避坑 / 步骤教程,手绘箭头、便签和圈注更容易把翻车点讲明白。
```
视觉要点:
- 手绘箭头、便签、圈注、风险提示贴纸。
- 适合“不要这样做”“先做这一步”“最大坑点”。
- 信息密度中等,避免塞太多长句。
- 猫咪可以举提示牌或指向关键步骤。
提示词片段:
```text
白底纸感,暖橙便利贴,手绘箭头和圈注,白橘猫举提示牌,中文步骤清楚,适合避坑经验和保姆式教程,包含 MAOMOMO 标识。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
## e-ink-editorial
推荐语:
```text
我建议用 e-ink-editorial,因为这篇更偏深度解释或观点长文,电子墨水杂志风会更克制、更有阅读质感。
```
详细 brief:`references/image-styles/电子墨水杂志风.md`
## scientific-defense
推荐语:
```text
我建议用 scientific-defense,因为这篇需要核对多份官方材料,重点不是好看,而是把证据、条件和结论讲清楚。
```
详细 brief:`references/image-styles/科研答辩风.md`
## mckinsey-brief
推荐语:
```text
我建议用 mckinsey-brief,因为这篇要帮读者在多个路径里做选择,用矩阵和取舍图会更直接。
```
详细 brief:`references/image-styles/麦肯锡简报风.md`
## xiaohongshu-vertical
推荐语:
```text
我建议用 xiaohongshu-vertical,并默认选 2D 扁平风,因为用户要发小红书,应该重新做 3:4 或 9:16 原生竖图,而不是复用站内横版图;2D 信息卡片更清爽,中文文字也更稳。
```
视觉要点:
- 3:4 或 9:16 竖版,手机端可读。
- 出图前先确认 `2D 扁平风` 或 `3D 轻拟物风`;默认推荐 2D,3D 适合封面、活动感和金融产品展示。
- 全组保持同一 `visual_direction`,不得中途混用。
- 大字少字,标题钩子强。
- 4 图系列统一:封面、核心拆解、操作流程、避坑总结。
- 每张图都有 MAOMOMO 标识或猫咪水印。
- 原生竖版重新构图,禁止横版硬塞;元素、卡片、Logo 和文字不能压扁或拉伸。
提示词片段:
```text
3:4 或 9:16 原生竖版,native portrait composition,vertical layout redesigned for Xiaohongshu,手机端可读,大标题,少量关键数字,暖橙白底,白橘猫水印,适合多图系列的中文金融攻略,包含 MAOMOMO 标识。no squeezed elements,no stretched card / logo / text。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
## 风格选择输出格式
向用户推荐时使用:
```text
我建议用 A,因为【一句话说明与文章类型的匹配】。
A. warm-fintech-guide(推荐):【适合原因】
B. data-card-dashboard:【适合原因】
C. handdrawn-note:【适合原因】
你选哪个?如果是小红书卡片,也请确认用 2D 扁平风还是 3D 轻拟物风;没指定我会默认用 2D。
```
用户选择后,把风格名写入图片清单的 `风格` 字段或 manifest 的 `style` 字段。
+112
View File
@@ -0,0 +1,112 @@
# 来源与事实核查规则
写最终稿前,只要涉及费用、活动、返现、奖励、账户规则、开户资格、转账路径、数字货币出入金、税务、法律或合规事项,就先读本文件。
## 来源读取顺序
优先级从高到低:
1. 用户提供的最新官方材料:活动页、条款 PDF、App 截图、邮件、公告、客服记录。
2. 官方网站、官方 App、官方 PDF 或官方公告。
3. 用户提供的实测记录、账单截图、到账记录和社群反馈。
4. MAOMOMO 现有文章或用户指定参考文章。
5. 其他公开资料。
如果用户要求“最新”“当前”“今天还能不能用”等时效性判断,必须联网或读取用户提供的新材料确认。不要只依赖旧文章或模型记忆。
## 已读来源清单
涉及活动、金融产品或官方规则时,先向用户列出来源读取状态:
```text
已读取来源:
- 官方活动页:已读 / 未读 / 无法读取
- 主活动条款 PDF:已读 / 未读 / 无法读取
- 补充条款 PDF:已读 / 未读 / 无法读取
- App / 邮件 / 客服截图:已读 / 未读 / 无法读取
- 用户实测记录:已读 / 未读 / 无法读取
- 参考文章:已读 / 未读 / 无法读取
```
如果有多个 PDF 或多个官方页面,不能只读其中一部分就写最终稿。无法读取时要说明影响,例如“无法确认活动截止日,只能标待确认”。
## 事实表
写稿前整理这些字段,缺失就标 `【待确认:...】`:
| 字段 | 要确认的内容 |
|---|---|
| 时间 | 活动开始日、截止日、登记日、交易日、入账日、奖励发放日 |
| 对象 | 地区、年龄、账户类型、新老客户、卡种、会员等级、邀请渠道 |
| 门槛 | 单笔 / 累计金额、交易次数、入金金额、持有期、任务顺序 |
| 奖励 | 比例、金额、封顶、名额、是否先到先得、是否可叠加 |
| 排除项 | 不支持渠道、排除 MCC、转账 / 充值 / 现金类交易、退款规则 |
| 成本 | 年费、汇款费、点差、交易费、税费、潜在机会成本 |
| 实测 | 用户实测是否成功、按账号还是按卡、多久到账、账单怎么显示 |
| 风险 | 风控、活动变更、资格审核、投资波动、汇率、税务和合规限制 |
## 口径冲突处理
- 最新官方条款优先于旧活动页。
- App 实际显示优先于旧文章截图。
- 官方条款用于确定边界、日期、排除项和资格;用户实测用于组织“怎么吃”“哪里会翻车”。
- 用户确认的实测口径必须直接写成结论,不要写成模棱两可的“可能”“疑似”“大概”。例如素材写“实测 CPF 现场办理成功立即生效”,正文应写“实测 CPF 现场办理成功后立即生效”,不要写“可能立即生效”。
- 用户素材包含“以往惯例”“过往尿性”“过往经验”“按之前案例”“历史上很多人这样成功”等表达时,正文要写成强概率判断,而不是弱猜测。推荐写法是“按过往案例判断,大概率仍然可以...”“从以往执行口径看,本次大概率还是...”。不要降级成“可能可以”“或许可以”。
- 当官方条款和用户实测 / 过往经验冲突时,写法要分层:先交代官方条款,再给实测或惯例判断。例如“官方写明合资格消费不包含支付宝 / 微信,但按照建亚 Travo 过往案例,很多人仍然用支付宝 / 微信消费达标并拿到 4% 返现和迎新奖励;所以本次活动大概率仍可用支付宝 / 微信消费达标,最终以银行系统入账和奖励判定为准。”
- 如果主活动、额外会员活动、年龄限定活动或旧版活动混在一起,先建议主文只保留大多数读者适用内容,小众分支放“补充说明”或删去。
- 来源差异无法解决时,在正文用保守表达,并在来源区说明以官方条款和 App 实际显示为准。
## 实测和惯例语气规则
用户给出的素材里,如果明确包含以下证据类型,按对应语气写:
| 素材类型 | 正文语气 | 示例 |
|---|---|---|
| 明确实测成功 | 确定语气,直接写结果 | “实测现场办理成功后立即生效。” |
| 明确实测失败 | 确定语气,直接写不能用 / 不生效 / 不计入 | “实测这条路径不会计入任务。” |
| 用户确认的账单 / 到账 / 出券结果 | 确定语气,说明观察到的结果 | “账单显示按支付宝账号计算。” |
| 过往惯例 / 过往尿性 / 历史案例 | 强概率判断 | “按过往案例判断,本次大概率仍可用支付宝 / 微信消费达标。” |
| 只有猜测、没有实测或历史依据 | 保守语气 | “这部分需要二次确认。” |
禁止把强证据写弱:
- 不要把“实测立即生效”写成“可能立即生效”。
- 不要把“很多人过往用支付宝 / 微信达标成功”写成“也许可以用支付宝 / 微信”。
- 不要把“用户确认按账号算”写成“更像是按账号算”。
允许保留的风险尾巴:
- “最终以银行系统记录和奖励入账为准。”
- “如果这次银行调整后台判定,结果可能变化。”
- “操作前仍建议看一眼 App 活动页是否有新增限制。”
风险尾巴不能推翻主句。主句应先给明确判断,再补边界。例如写“本次大概率仍可用支付宝 / 微信消费达标,最终以银行系统判定为准”,不要写“可能可以用支付宝 / 微信”。
## 风险表达
禁止:
- “稳赚”“无风险套利”“一定到账”“一定获批”“闭眼稳赚”“保证拿满”。
- 把股票、ETF、基金、数字货币、汇率敞口说成存款或保本产品。
- 替用户给出法律、税务、投资或合规结论。
推荐:
- “适合本来就有相关消费 / 入金 / 交易需求的人。”
- “最终是否计入活动,以银行 / 券商系统记录为准。”
- “活动可能随时调整,操作前建议二次确认 App 和官方条款。”
- “股票 / ETF / 数字货币存在价格波动、汇率、买卖差价、成交滑点、交易成本和税务风险。”
默认免责声明:
```text
本文为经验整理,不构成金融、投资、法律或税务建议;活动和规则可能随时调整,请以官方条款、App 实际显示及当地法规为准。
```
## 来源区
- 所有链接统一放在文末 `## 来源` 区。
- 来源区只保留最终文章实际使用的来源。
- 引用 MAOMOMO 时链接具体文章或页面,不只放首页。
- 用户要求删除某个活动分支时,对应来源也要删除。
- 发布用 Markdown 不保留 `contentReference`、`oaicite`、模型内部引用或工具残留。
+32 -9
View File
@@ -27,6 +27,11 @@ MAOMOMO 是偏实操的出海经验平台。公开站点标语是:“打破信
- 空泛的 SEO 背景介绍; - 空泛的 SEO 背景介绍;
- 投资建议、法律建议、税务建议或结果保证。 - 投资建议、法律建议、税务建议或结果保证。
金额写法:
- 发布稿、图片文案、prompt、manifest、`deck_spec.json` 和 `speech.md` 中,港币金额统一写 `HKD 1,000`。
- 不使用 `HK$1,000`、`港币 1,000`、`1,000 港币` 作为发布写法;来源原文摘录或 OCR 可保留原样。
## 常见文章类型 ## 常见文章类型
### 1. 活动 / 返现 / 奖励攻略 ### 1. 活动 / 返现 / 奖励攻略
@@ -62,7 +67,7 @@ MAOMOMO 是偏实操的出海经验平台。公开站点标语是:“打破信
写法要点: 写法要点:
- 把实用公式放前面,例如 `HK$190 ÷ 9% ≈ HK$2112`。 - 把实用公式放前面,例如 `HKD 190 ÷ 9% ≈ HKD 2,112`。
- 直接写出“超过部分 = 没额外收益(不用多刷)”。 - 直接写出“超过部分 = 没额外收益(不用多刷)”。
- 不支持的渠道要明确点名,例如“不支持支付宝 / 微信”。 - 不支持的渠道要明确点名,例如“不支持支付宝 / 微信”。
- 活动要写开始日期、结束日期,以及本文是否“持续更新”。 - 活动要写开始日期、结束日期,以及本文是否“持续更新”。
@@ -171,18 +176,21 @@ MAOMOMO 是偏实操的出海经验平台。公开站点标语是:“打破信
- 核心结论可以放在引用块里。 - 核心结论可以放在引用块里。
- 结尾提醒政策变化风险,例如“直至另行通知”“随时可能取消”。 - 结尾提醒政策变化风险,例如“直至另行通知”“随时可能取消”。
### 6. 小红书发布版 / 4 图看完 ### 6. 小红书卡片模式 / 多图发布
用户说“小红书”“发小红书”“4图看完”“超短文字”“20字内标题”时,进入小红书模式。 用户说“小红书”“卡片”“多图”“发小红书”“4图看完”“3:4”“9:16”“超短文字”“20字内标题”时,进入小红书卡片模式。
小红书卡片模式是独立交付模式,不默认走完整文章 + Markdown + ZIP 流程。除非用户明确要求文章或打包,否则重点交付多张统一风格的独立 PNG,以及可复制的小红书标题、正文和话题标签。
交付物: 交付物:
- 20 字内标题。 - 20 字内标题。
- 超短正文,适合直接复制发布。 - 超短正文,适合直接复制发布。
- 3-6 个话题标签。 - 3-6 个话题标签。
- 4 张 9:16 竖版 PNG 图,不复用文章横版图。 - 多张 3:4 或 9:16 竖版 PNG 图,默认 4 张;不复用文章横版图。
- 每张图加入 MAOMOMO logo / 猫咪水印。 - 每张图加入 MAOMOMO logo / 猫咪水印。
- 如果用户要求 email,按用户要求打包或多附件发送。 - `final_manifest.json`,只列通过 QA 且进入 `assets/` 的最终图片。
- GitHub 分批交付链接;Telegram / 聊天只通知进度和链接,不承载大 ZIP 或大图包。
文字规则: 文字规则:
@@ -200,12 +208,27 @@ MAOMOMO 是偏实操的出海经验平台。公开站点标语是:“打破信
视觉规则: 视觉规则:
- 纵向 9:16,手机端可读,大字少字。 - 出图前必须先确认比例:`3:4` 或 `9:16`。
- 出图前必须先确认视觉方向:`2D 扁平风` 或 `3D 轻拟物风`。
- 用户没指定时,默认推荐 `2D 扁平风`:清爽、信息卡片、适合教程 / 规则说明,中文文字更稳。
- `3D 轻拟物风` 更适合封面、活动感、金融产品展示;一旦确认,整组不能和 2D 混用。
- 纵向原生构图,手机端可读,大字少字。
- 每张图独立,不要拼图。 - 每张图独立,不要拼图。
- 每张图有 MAOMOMO 标识。 - 每张图有 MAOMOMO 标识。
- 文章横图不能直接当小红书图;需要重新设计。 - 文章横图不能直接当小红书图;需要重新设计。
- 禁止把横版设计硬塞进 3:4 或 9:16;prompt 必须包含 `native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
- 如果用户提供卡片、截图或 Logo 参考,只做原创示意,不复制敏感信息,不伪造真实 App 页面。 - 如果用户提供卡片、截图或 Logo 参考,只做原创示意,不复制敏感信息,不伪造真实 App 页面。
最终回复固定包含:
- GitHub 链接。
- 图片数量。
- 风格:2D / 3D。
- 比例:3:4 / 9:16。
- 批次数量。
- QA 是否完成。
- 已知限制。
### 5. 避坑经验 ### 5. 避坑经验
用于“不要浪费时间试了”、DCC、支付失败、转账姓名顺序问题、线下网点提醒等。 用于“不要浪费时间试了”、DCC、支付失败、转账姓名顺序问题、线下网点提醒等。
@@ -364,12 +387,12 @@ tags:
基础方向: 基础方向:
```text ```text
MAOMOMO 风格封面图,干净的中文金融教程缩略图,友好的白橘猫吉祥物,圆脸、小爪子、暖橙色点缀,结合香港金融 / 信用卡 / 券商 / 转账场景,预留清晰中文标题区域,现代 App 仪表盘质感,实用而非奢华,明亮白色背景,少量爪印细节,不使用未经提供的真实银行标志,不伪造真实截图界面,16:9。 干净的中文金融实操文章封面,友好的白橘猫吉祥物,圆脸、小爪子,暖橙色点缀,结合香港金融 / 信用卡 / 券商 / 转账场景,预留清晰中文标题区域,现代 App 信息卡片质感,实用而非奢华,明亮白色背景,少量爪印细节,包含清晰的 MAOMOMO 主标识,已核对的银行 / 支付 / 卡组织标识只做小型辅助识别,不伪造真实截图界面,16:9。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
``` ```
按主题追加对象: 按主题追加对象:
- 信用卡 / 返现:香港信用卡、超市购物篮、返现计算器、小票、银联 / Apple Pay 风格的通用符号。 - 信用卡 / 返现:香港信用卡、超市购物篮、返现计算器、小票、通用支付符号;只有官方材料或用户素材确认后才画具体卡组织 / 支付网络 / 支付机构 Logo,且不能抢 MAOMOMO 主标识位置。
- 银行账户 / 保号:保险库、账户余额仪表、ETF / 股票图表卡片、日历、警示标。 - 银行账户 / 保号:保险库、账户余额仪表、ETF / 股票图表卡片、日历、警示标。
- 转账 / 入金:路线箭头、HKD / EUR / USD 币种标签、手机转账界面插画、勾选标记。 - 转账 / 入金:路线箭头、HKD / EUR / USD 币种标签、手机转账界面插画、勾选标记。
- 券商活动:股票礼品卡、K 线图、欢迎信封、香港天际线线稿。 - 券商活动:股票礼品卡、K 线图、欢迎信封、香港天际线线稿。
@@ -408,7 +431,7 @@ MAOMOMO 风格封面图,干净的中文金融教程缩略图,友好的白橘
提示词模板: 提示词模板:
```text ```text
生成一张干净的 MAOMOMO 风格中文信息图,使用暖橙色点缀和小型白橘猫吉祥物。展示【计算公式 / 路线 / 时间线】。画面使用简洁卡片、箭头、勾选标记、风险提示标签和清晰中文标注。整体为实用金融教程风,白色背景,不使用伪造官方标志,16:9。 生成一张干净的中文金融实操信息图,使用暖橙色点缀和小型白橘猫吉祥物。展示【计算公式 / 路线 / 时间线】。画面使用简洁卡片、箭头、勾选标记、风险提示标签和清晰中文标注,白色背景,不伪造官方标志,16:9。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
``` ```
## 来源处理 ## 来源处理
+364
View File
@@ -0,0 +1,364 @@
# MAOMOMO 配图与视觉资产规则
需要生成、准备、插入或更新配图时,先读本文件。
## 配图交付原则
- 完整文章默认配独立 PNG:短文章 4-6 张,普通攻略 6-8 张,长教程 / 多活动整理 8-12 张。
- 不为凑数量硬加图;每张图必须服务正文。
- 每张图独立生成或准备,不要用一张拼图、长图或总图代替全部。
- 出图前先把全部图片 prompt 写入 `prompts/` 或 manifest 文件;不要在聊天里输出超长 prompt 代替文件。
- 先只生成一张样张,优先封面图;用户确认样张前不得生成剩余图片。
- 样张确认后,后续图片使用同一生图后端、同一视觉系统和同一质量配置。
- 图片生成按“一张图一个 job / 一次请求 / 一次 QA”推进;运行环境支持子 agent 时,可以一图一个子 agent 并发,但不要一次请求多张图。
- 最终原始图保存到 `origin_image/`,通过 QA 后同步到 Markdown 引用的 `assets/`。
- 生成图必须是“示意图 / 信息图 / 教程视觉”,不得伪造真实 App 截图、账单或官方页面。
- 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。
- 真实银行 / 券商 / 钱包截图、账单、交易记录、客服记录等默认视为敏感素材;需要上传图片 API 做编辑或重绘前,必须先向用户确认实际 API 地址 / baseURL、接口归属、是否为用户自有兼容服务以及是否允许外发。不要默认把配置里的图片 API 视为公共第三方,也不要在未确认具体接收方时上传真实截图。若用户未确认、确认不允许或安全策略 / 上下文不允许外发,即使用户要求“用 GPT Image2 美化截图”,也改用“GPT Image2 生成空白展示模板 + 本地嵌入真实截图”的流程。
- 空白模板流程:图片后端只生成不含真实截图、账单文字、交易数字和伪造 App UI 的展示框 / 卡片背景;随后用本地工具把真实截图完整嵌入预留区域。本地嵌入可以做裁切适配、圆角、阴影、边框和背景排版,但不得修改截图内部文字、金额、日期、账户、交易描述或任何证据内容。
- 使用空白模板流程时,`prompt` / manifest / `deck_spec.json` / 状态文件必须记录模板生成后端、真实截图本地嵌入方式、原截图路径和 QA 结论;最终回复说明真实截图未上传到图片接口。
- 每张图必须包含清晰可见的 `MAOMOMO` 标识。
- 每张图的可见文字必须受控:只允许出现该图指定的业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,不得添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代图片生成后端。
## 平铺水印
用户要求“水印”“平铺水印”“防盗图”“打水印”时,对最终配图生成水印副本。默认规格:
- 水印文字:`MAOMOMO.COM`
- 角度:45°
- 颜色:自动对比色:浅底黑字 `#000000`,深底白字 `#FFFFFF`
- 透明度:半透明,默认 alpha `70/255`
- 排列:对角线平铺
- 工具:`uv run python scripts/maomomo_tile_watermark.py`
- 密度:每张图至少可见 2 个完整 `MAOMOMO.COM`;默认 `font-size=50`、`spacing=28`。
执行示例:
```bash
uv run python scripts/maomomo_tile_watermark.py \
--input assets \
--output watermarked_assets
```
单张图:
```bash
uv run python scripts/maomomo_tile_watermark.py \
--input assets/example.png \
--output watermarked_assets/example.png
```
规则:
- 水印只加到最终 QA 通过的 `assets/` 或交付副本;不要在样张确认前加水印,避免影响用户判断画面文字质量。
- 默认保留无水印原图在 `origin_image/` 和 / 或 `assets/`;水印图输出到 `watermarked_assets/`,或 GitHub 交付批次中的 `watermarked/`。
- 不要对官方截图证据原图、需要继续发给图片模型编辑的素材、尚未 QA 的图直接覆盖水印。
- `final_manifest.json` 应记录 `watermark` 字段,至少包含 `text`、`angle`、`color_rule`、`brightness_threshold`、`alpha`、`font_size`、`spacing`、`density_rule`、`output_dir` 和水印文件清单。
- 交付回复中说明是否提供水印版,以及无水印原图是否保留。
## 可见文字与 prompt 边界
- 视觉风格只作为后台画面描述,不得成为画面文字。
- 生成用 prompt 不要写容易被模型照抄到画面里的中文风格标签,例如“暖橙金融教程风”“数据仪表盘风”“手绘便签风”“清爽编辑部风”。
- 把风格名改写成可观察的视觉描述:白底、暖橙强调、信息卡片、猫咪向导、清晰标题、留白充足、表格感、时间线、轻纸感等。
- prompt 必须写明:画面只允许出现指定业务文案、`MAOMOMO` 和 `allowed_brand_marks` 中已核对允许出现的品牌 / 支付标识;不要添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。
- `style` 字段可保留内部风格 ID,例如 `warm-fintech-guide`,但完整 `prompt` 字段里不要把中文风格名当成可见文案写入。
推荐写法:
```text
明亮白底,暖橙强调色,干净信息卡片,友好的白橘猫作为向导,中文标题清楚,画面包含 MAOMOMO 标识。可见文字只允许出现:「先报名,再看门槛和封顶」和「MAOMOMO」。
```
不推荐写法:
```text
MAOMOMO 暖橙金融教程风,画出一张手绘便签风仪表盘图。
```
## 金融卡面与支付网络核对
- 金融活动配图中只要出现信用卡、支付卡、卡组织、支付网络或银行卡样式,必须先根据官方材料、用户截图或用户提供的卡面参考图核对。
- 用户提供卡面或 Logo 参考图时,把原始素材保存到 `sources/`,例如 `sources/gba-unionpay-card-reference.png`;不要只留在聊天上下文里。
- 用户没有提供 Logo / 卡面素材,但画面需要银行、卡组织、支付机构、酒店、OTA 或活动品牌标识时,主动尽力查找准确标识,不要先默认画通用符号。
- 查找优先级:用户素材 > 官方活动页 / 条款 PDF / App 或官网截图 > 官方品牌资源页 / media kit / press kit > 官方社交账号或应用商店页面 > 可信百科或新闻配图。只有前几类无法获得时,才使用第三方汇总站,并必须交叉核对。
- 找到标识后,把原始文件、截图或网页证据保存到 `sources/`,并记录来源 URL、读取日期、用途和核对结论;不要只把来源写在聊天里。
- 在 `prompt`、manifest 和 `deck_spec.json` 中记录参考素材路径、卡组织 / 支付网络、允许出现的品牌 / 支付标识、来源 URL 和核对依据。
- 不得把银联 / UnionPay 卡画成 Mastercard、Visa、American Express 或其他卡组织;不确定时画“无卡组织标识的通用支付卡”。
- Mastercard、Visa、UnionPay、银行、支付宝、微信支付、酒店、OTA 等 Logo 或支付网络标识,只要已经由官方材料、用户素材或用户明确口径核对正确,就可以作为辅助识别出现。
- 已核对的第三方 Logo 必须尺寸克制、位置辅助,不得抢占、替代或压过 `MAOMOMO` 主标识;封面尤其要让 `MAOMOMO` 仍是最清楚的品牌锚点。
- 如果当前环境无法联网、官方素材无法读取或标识版本仍不确定,记录 blocker;除非用户确认,否则不要凭记忆画真实 Logo,改用通用卡片、通用 POS、通用酒店图标或文字描述。
- 如果官方活动限定某卡组织或指定卡面,图片 QA 必须把卡组织 / 支付网络列为必检项。
推荐记录字段:
```json
{
"reference_assets": ["sources/gba-unionpay-card-reference.png"],
"brand_mark_sources": [
{
"name": "UnionPay",
"source_url": "https://example.com/official-unionpay-source",
"source_type": "官方活动页 / 官方品牌资源页 / 用户素材",
"checked_at": "YYYY-MM-DD"
}
],
"payment_network": "UnionPay / 银联",
"allowed_brand_marks": ["UnionPay", "中信银行(国际) GBA 卡面识别"],
"card_art_rule": "根据用户卡面参考图画银联 GBA 双币信用卡;不得出现 Mastercard 或 Visa 标识;UnionPay 标识只能小尺寸辅助出现,不能抢 MAOMOMO 主标识位置"
}
```
## 封面信息层级
- 封面只放最高优先级结论和少量核心数字,服务“读者一眼知道这篇讲什么、值不值得点开”。
- 不把复杂计算结论、全年理论上限、长公式、多重限制、详细规则或小字说明塞进封面。
- 计算过程、月度 / 年度理论上限、封顶拆解、资格分支和详细条款,应放到计算图、总览图或正文。
- 封面核心文案建议 1 句主标题 + 1-3 个短数字 / 短标签;如果超过手机端一眼可读的密度,拆到下一张图。
## 金额与币种写法
- 发布稿、图片文案、prompt、manifest、`deck_spec.json`、alt text 和 `speech.md` 中,港币金额统一写 `HKD 1,000`。
- 不使用港币符号写法,例如 `HK$1,000`、`港币 1,000`、`1,000 港币`。
- 来源原文抽取文件、官方截图 OCR 或原文摘录可以保留来源写法;它们不作为发布内容检查对象。
- 如果来源使用港币符号,改写到发布内容时同步转换为 `HKD`。
## 风格选择
如果用户没有指定视觉方向,生成图片前先读 `references/recommended-styles.md`,给 2-3 个可选风格,并推荐一个。确认后,整篇文章的配图保持同一套视觉身份:稳定的配色、猫咪造型、标题区、图标语言和信息密度;不同图片可以根据用途变化构图。
小红书卡片模式先确认两件事:
- 比例:`3:4` 或 `9:16`。
- 视觉方向:`2D 扁平风` 或 `3D 轻拟物风`。用户没指定时,推荐 `2D 扁平风`,因为信息卡片更清爽、教程 / 规则说明更稳,也更不容易出现中文文字翻车;`3D 轻拟物风` 更适合封面、活动感和金融产品展示。
小红书卡片一旦确认 `2D` 或 `3D`,整组图片必须继承同一个 `visual_direction`,不得中途混用。
默认推荐逻辑:
- 活动 / 返现 / 港卡攻略:优先 `warm-fintech-guide`。
- 规则、门槛、费用、返现计算和产品对比:优先 `data-card-dashboard`。
- SEO 长文、经验整理、品牌感更强的文章:优先 `clean-editorial`。
- 保姆式教程、避坑经验和轻量操作步骤:优先 `handdrawn-note`。
- 小红书 / 4 图看完 / 多图卡片:必须用 `xiaohongshu-vertical`,并按用户确认生成 3:4 或 9:16 竖图。
可选风格:
| 风格名 | 适合场景 | 视觉方向 |
|---|---|---|
| `warm-fintech-guide` | 默认 MAOMOMO 金融攻略、港卡、银行活动 | 白底、暖橙强调色、干净卡片、白橘猫向导 |
| `clean-professional` | 正式规则整理、开户 / 保号教程 | 浅色背景、蓝绿和暖橙强调、可信结构化卡片 |
| `clean-editorial` | SEO 长文、解释型文章、站内封面 | 大标题、留白、轻杂志感、猫咪作为角落品牌元素 |
| `creative-magazine` | 传播型封面、活动盘点 | 大标题、编辑部排版、强留白、原创金融视觉 |
| `data-card-dashboard` | 返现计算、活动对比、费用路径、时间线 | 指标卡、表格感、仪表盘、清晰数字层级 |
| `handdrawn-note` | 避坑、步骤教程、保号经验 | 手绘箭头、便签、圈注、猫咪提示牌 |
| `retro-flat-illustration` | 轻松羊毛攻略、用卡姿势 | 低饱和复古扁平插画、金融小物件 |
| `e-ink-editorial` | 深度解释、观点长文 | 纸感、黑白灰、少量强调色、元信息条 |
| `scientific-defense` | 条款核对、规则证据链 | 严谨浅色版式、来源卡片、条件树 |
| `mckinsey-brief` | 多方案对比、路径选择 | 结论先行、矩阵、取舍图、商业简报感 |
| `xiaohongshu-vertical` | 小红书发布版、4 图看完、多图卡片 | 3:4 或 9:16 竖版、大字少字、强钩子、系列化版式 |
示例确认话术:
```text
我建议用 A,因为这篇是活动攻略,读者最需要快速看懂门槛、封顶和翻车点。
A. warm-fintech-guide(推荐):白底暖橙、猫咪向导、金融教程感,适合 MAOMOMO 常规文章。
B. data-card-dashboard:更强调数字卡片和计算表,适合门槛 / 返现 / 费用信息密集的文章。
C. handdrawn-note:便签和手绘箭头更明显,适合避坑经验和保姆式步骤。
你选哪个?也可以指定颜色、猫咪比例或是否更像小红书图。
```
## 常见配图类型
| 类型 | 用途 |
|---|---|
| 封面图 | 文章主题、核心利益点 / 核心问题、猫咪识别和产品场景 |
| 总览图 | 活动、产品、规则或教程的核心信息总览 |
| 流程图 | 开户、注册、转账、入金、申请、报名、设置路径 |
| 表格图 / 对比图 | 多产品、多方案、多活动、支持 / 不支持渠道对比 |
| 计算图 | 返现、收益、费用、年化、门槛和封顶测算 |
| 时间线图 | 活动日期、到账时间、任务周期、持有期、截止日 |
| 避坑图 | 常见翻车点、不能补救的点、费用 / 规则 / 时效风险 |
| FAQ 图 | 3-5 个高频问题和简短答案 |
## 图片清单格式
生成图片前先规划清单,并同步写入 `prompts/{article_slug}_XX.json` 或图片 manifest。每个 prompt 文件至少包含:文件名、类型、放置位置、核心文案、可见文字白名单、alt text、风格、依赖素材、输出路径和完整 prompt。小红书卡片还必须包含 `aspect_ratio`、`visual_direction`、`portrait_composition_rules` 和分批交付批次。涉及信用卡、支付卡、支付网络或机构 Logo 时,还要包含参考素材、卡组织 / 支付网络、`allowed_brand_marks`、`brand_mark_sources` 和核对依据。
```markdown
| 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 风格 | 依赖素材 |
|---|---|---|---|---|---|---|
| hong-kong-bank-account-01-cover.png | 封面 | 标题下方 | 港卡保号关键点 | MAOMOMO 港卡保号教程封面图 | warm-fintech-guide | 无 |
| hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | handdrawn-note | 用户截图 1 |
```
确认用户不需要先看清单时,可以不在聊天里展开清单,但仍必须写入 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": "先报名,再看门槛和封顶",
"visible_text_allowed": ["先报名,再看门槛和封顶", "MAOMOMO"],
"alt_text": "MAOMOMO 汇丰 Mastercard 支付宝返现活动封面图",
"style": "warm-fintech-guide",
"aspect_ratio": "16:9",
"visual_direction": "2D 扁平风",
"delivery_batch": "batch-01-cover",
"portrait_composition_rules": [],
"required_assets": [],
"reference_assets": [],
"brand_mark_sources": [],
"payment_network": "",
"allowed_brand_marks": [],
"card_art_rule": "如无官方或用户参考素材确认,画无卡组织标识的通用支付卡;已核对的品牌 / 支付标识只能辅助出现,不能抢 MAOMOMO 主标识位置",
"forbidden_visible_text": ["风格标签", "prompt 描述", "额外 slogan", "无关说明文字"],
"prompt": "完整图片生成 prompt,包含视觉描述、指定业务文案、可见文字限制和禁止项"
}
```
小红书卡片 prompt JSON 中,`portrait_composition_rules` 必须包含:
```json
[
"native portrait composition",
"no squeezed elements",
"no stretched card / logo / text",
"vertical layout redesigned for Xiaohongshu"
]
```
## 文件命名
图片文件名必须 SEO 友好,并包含主题前缀:
```text
origin_image/
hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png
assets/
hsbc-mastercard-alipay-campaign-01-cover.png
hsbc-mastercard-alipay-campaign-02-overview.png
hsbc-mastercard-alipay-campaign-03-step-guide.png
hsbc-mastercard-alipay-campaign-04-comparison.png
hsbc-mastercard-alipay-campaign-05-calculation.png
hsbc-mastercard-alipay-campaign-06-timeline.png
hsbc-mastercard-alipay-campaign-07-common-pitfalls.png
hsbc-mastercard-alipay-campaign-08-faq.png
```
规则:
- 使用英文小写、数字和连字符。
- 不使用中文文件名。
- 不使用 `01-cover.png`、`02-overview.png` 这类缺少主题前缀的通用文件名。
- 后续改稿删除某个活动分支时,相关文件名、alt text、正文引用和 ZIP 中旧图都要同步清理。
## Markdown 插图
统一使用相对路径:
```markdown
![图片说明](assets/文件名.png)
```
插图位置:
- 封面图放在标题下方。
- 总览图放在活动 / 产品介绍之后。
- 流程图放在步骤段落前后。
- 计算图放在公式或测算表格后。
- 时间线图放在日期说明后。
- 避坑图放在翻车点段落后。
- FAQ 图放在常见问题段落前后。
图片说明必须根据图片内容写,不要都写成“配图”。
`origin_image/` 不直接写进 Markdown,除非用户明确要求引用原始图目录。发布用 Markdown 默认引用 `assets/`。
## 猫咪视觉识别
基础方向:
```text
中文金融实操信息图,干净明亮白色背景,暖橙色点缀,友好的白橘猫吉祥物,圆脸、小爪子,现代 App 信息卡片质感,清晰中文标题和卡片信息层级,实用而非奢华,包含清晰的 MAOMOMO 主标识,已核对的第三方品牌 / 支付标识只做辅助识别,不伪造真实截图。可见文字只允许出现指定业务文案、MAOMOMO 和已核对允许出现的品牌 / 支付标识。
```
猫咪可以:
- 指向关键数字。
- 拿着通用卡片。
- 坐在手机、账本、路线图或计算器旁。
- 举“实测”“注意”“先登记”提示牌。
猫咪不能:
- 穿戴第三方银行、卡组织、券商、支付机构 Logo;如果猫咪拿着带标识的卡片或支付牌,标识必须已核对、尺寸小、位置辅助,不能盖住或替代 MAOMOMO。
- 盖住关键数字、日期、步骤和风险提示。
- 变成幼稚主角,压过金融教程信息。
## 主题追加元素
- 信用卡 / 返现:通用信用卡、小票、购物篮、返现计算器、通用支付符号;只有在官方材料或用户素材确认后,才画具体卡组织 / 支付网络 / 支付机构 Logo,且只能作为辅助识别。
- 银行账户 / 保号:账户余额仪表、保险库、日历、警示标、活跃度卡片。
- 转账 / 入金:路线箭头、HKD / EUR / USD 标签、手机转账示意界面、勾选标记。
- 券商活动:股票礼品卡、K 线图、欢迎信封、香港天际线线稿。
- 数字货币:硬件钱包、钱包 App、欧元通道、区块链节点路线;画面克制,避免暴富感。
## 小红书图
用户说“小红书”“卡片”“多图”“4 图看完”“发小红书”“3:4”“9:16”时:
- 进入小红书卡片模式,不默认生成完整文章、Markdown 或 ZIP。
- 生成多张 3:4 或 9:16 竖版 PNG,默认 4 张;不复用文章横版图。
- 出图前先确认 `2D 扁平风` / `3D 轻拟物风`;默认推荐 `2D 扁平风`。
- 每张图有 MAOMOMO 标识或猫咪水印。
- 手机端可读,大字少字。
- 默认结构:封面、核心拆解、操作流程、避坑总结。
- 风格固定为 `xiaohongshu-vertical`,同组保持同一 `visual_direction`,不直接复用横版文章图。
- prompt 必须包含 `native portrait composition`、`no squeezed elements`、`no stretched card / logo / text`、`vertical layout redesigned for Xiaohongshu`。
- 通过 QA 后写入 `final_manifest.json`;最终交付只读取 final manifest 或状态文件中的 final assets。
## 图片 QA
每张图通过前检查:
- 样张已被用户确认,后续图片没有切换生图后端或视觉风格。
- 文件真实存在,格式为 PNG。
- 小红书卡片比例为已确认的 `3:4` 或 `9:16`,不是横版图压缩或拉伸后的结果。
- 小红书卡片是原生竖版重构,元素没有压扁,卡片、Logo 和文字没有拉伸。
- `origin_image/` 原始最终图和 `assets/` 发布图对应。
- Markdown 路径能打开对应文件。
- 图片文字清楚,无乱码、错字、截断和多余文案。
- 可见文字只包含指定业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,没有副标题、风格说明文字、prompt 描述、额外 slogan 或无关说明。
- 标题、金额、币种、日期、公式、规则与最终文章口径一致;港币金额发布写法统一为 `HKD`。
- 卡组织 / 支付网络 / 机构 Logo 与官方材料或用户参考素材一致;银联 / UnionPay 不得被画成 Mastercard、Visa 或其他卡组织。
- 用户未提供 Logo / 卡面素材时,已尽力查找官方或可信来源;`sources/`、prompt、manifest 和 `deck_spec.json` 已记录标识素材、来源 URL、读取日期和核对结论。
- 第三方 Logo 没有抢占、替代或压过 MAOMOMO 主标识位置;MAOMOMO 仍然是画面主品牌识别。
- 没有出现用户已删除、已排除或禁止使用的活动分支、旧版规则、旧金额或旧标题。
- 没有真实个人信息。
- 没有伪造官方 App 页面,也没有错误、未经核对或喧宾夺主的品牌 Logo 场景。
- 没有把示意图伪装成真实截图、账单、订单、App 页面或官方页面。
- 每张图独立主题明确,不是同一画面的机械变体。
- 单张 QA 通过后,才用 `scripts/maomomo_job_state.py result` 记录结果并同步到 `assets/`;不得只生成到 `origin_image/` 后声称完成。
- 单张 QA 通过后,状态文件必须显示 `qa_status=passed`、`entered_assets=true`;GitHub 推送后必须记录 `github_pushed=true` 或对应 batch 记录。
- 最终交付前必须生成 `final_manifest.json`,且只包含通过 QA 并进入 `assets/` 的最终文件。
- `article.md` 已插入对应章节,且用脚本检查 Markdown 中所有图片路径真实存在。
- `slide_jobs.json` / `slide_run_state.json` 中没有未处理的 `pending`、`dispatched` 或 `blocked` job;如有,不能声称完整交付完成。
路径检查命令:
```bash
uv run python scripts/maomomo_check_markdown_assets.py article/article.md
```
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""检查 Markdown 中引用的本地图片是否真实存在。"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import sys
from typing import Iterable, List, Optional
from urllib.parse import urlparse
IMAGE_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)")
def die(message: str, code: int = 1) -> None:
print(f"错误:{message}", file=sys.stderr)
raise SystemExit(code)
def normalize_target(raw: str) -> str:
value = raw.strip()
if value.startswith("<") and value.endswith(">"):
return value[1:-1].strip()
if " " in value:
value = value.split()[0]
return value.strip("'\"")
def is_remote_or_anchor(target: str) -> bool:
parsed = urlparse(target)
return bool(parsed.scheme in {"http", "https", "data", "mailto"} or target.startswith("#"))
def find_missing(markdown_path: Path) -> List[str]:
text = markdown_path.read_text(encoding="utf-8")
base_dir = markdown_path.parent
missing: List[str] = []
for match in IMAGE_RE.finditer(text):
target = normalize_target(match.group(1))
if not target or is_remote_or_anchor(target):
continue
image_path = Path(target)
if not image_path.is_absolute():
image_path = base_dir / image_path
if not image_path.exists():
missing.append(target)
return missing
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="检查 Markdown 本地图片路径是否存在。")
parser.add_argument("markdown", help="要检查的 article.md 或其他 Markdown 文件。")
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)
markdown_path = Path(args.markdown).expanduser().resolve()
if not markdown_path.exists():
die(f"Markdown 文件不存在:{markdown_path}")
missing = find_missing(markdown_path)
result = {
"markdown": str(markdown_path),
"missing_count": len(missing),
"missing": missing,
}
print(json.dumps(result, ensure_ascii=False, indent=2))
return 1 if missing else 0
if __name__ == "__main__":
raise SystemExit(main())
+504
View File
@@ -0,0 +1,504 @@
#!/usr/bin/env python3
"""MAOMOMO article image generation CLI.
This is a lightweight OpenAI-compatible fallback for article assets. Prefer the
agent's built-in image tool when it is available; use this script when the user
explicitly wants API/CLI generation or the built-in backend is unavailable.
"""
from __future__ import annotations
import argparse
import base64
import json
import os
from pathlib import Path
import sys
import time
from typing import Any, Dict, Iterable, List, Optional
import urllib.error
import urllib.request
DEFAULT_BASE_URL = "https://api.openai.com/v1"
DEFAULT_MODEL = "gpt-image-2"
DEFAULT_SIZE = "1536x1024"
DEFAULT_QUALITY = "medium"
DEFAULT_STYLE = "warm-fintech-guide"
STYLE_PRESETS: Dict[str, str] = {
"warm-fintech-guide": (
"明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,"
"适合港卡、银行活动、返现攻略和教程总览。"
),
"clean-editorial": (
"大标题、留白充足、少量暖橙和黑灰文字,像一张信息密度适中的中文攻略头图。"
),
"data-card-dashboard": (
"指标卡、时间线、计算公式和对比表清晰分区,适合返现、费用、门槛和路径对比。"
),
"handdrawn-note": (
"白底纸感、手绘箭头、便利贴、重点圈注和轻量猫咪贴纸,适合避坑经验和保姆式步骤。"
),
"xiaohongshu-vertical": (
"3:4 或 9:16 原生竖版,手机端可读,大字少字,强标题钩子,多图系列一致视觉,"
"默认 2D 扁平信息卡片,用户确认后可用 3D 轻拟物风,带 MAOMOMO 标识。"
),
"clean-professional": (
"浅色背景、蓝绿或暖橙强调、结构化信息卡片和清晰层级,适合正式银行规则、开户教程和综合攻略。"
),
"creative-magazine": (
"大标题、强留白、编辑部排版和轻视觉冲击,适合传播型封面、观点总结和活动盘点。"
),
"retro-flat-illustration": (
"低饱和暖色、扁平金融小物件、轻复古海报感,适合轻松羊毛攻略和经验分享。"
),
"e-ink-editorial": (
"纸感背景、黑白灰为主、少量强调色、强标题和元信息条,适合深度解释和观点型长文。"
),
"scientific-defense": (
"严谨浅色版式、证据图、流程框和来源标注,适合规则拆解、政策解读和多来源核对。"
),
"mckinsey-brief": (
"结论先行、矩阵、2x2、瀑布图和高对比商业配色,适合方案对比、路径选择和决策建议。"
),
}
def die(message: str, code: int = 1) -> None:
print(f"错误:{message}", file=sys.stderr)
raise SystemExit(code)
def warn(message: str) -> None:
print(f"警告:{message}", file=sys.stderr)
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def write_text(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
def read_json(path: Path) -> Any:
return json.loads(read_text(path))
def strip_toml_comment(line: str) -> str:
quote = ""
escaped = False
for index, char in enumerate(line):
if escaped:
escaped = False
continue
if quote and char == "\\":
escaped = True
continue
if char in ("'", '"'):
if not quote:
quote = char
elif quote == char:
quote = ""
continue
if char == "#" and not quote:
return line[:index]
return line
def parse_toml_scalar(value: str) -> Any:
raw = strip_toml_comment(value).strip()
if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ("'", '"'):
inner = raw[1:-1]
if raw[0] == '"':
return inner.replace(r"\"", '"').replace(r"\\", "\\")
return inner
if raw.lower() == "true":
return True
if raw.lower() == "false":
return False
return raw
def parse_basic_toml(text: str) -> Dict[str, Any]:
"""解析 Codex 配置需要的 TOML 子集,避免 Python 3.10 及以下缺少 tomllib。"""
data: Dict[str, Any] = {}
current: Dict[str, Any] = data
for raw_line in text.splitlines():
line = strip_toml_comment(raw_line).strip()
if not line:
continue
if line.startswith("[") and line.endswith("]"):
section = line.strip("[]").strip()
current = data
for part in section.split("."):
key = part.strip().strip("'\"")
if not key:
continue
child = current.get(key)
if not isinstance(child, dict):
child = {}
current[key] = child
current = child
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
current[key.strip().strip("'\"")] = parse_toml_scalar(value)
return data
def load_prompt_file(path: Path) -> str:
text = read_text(path).strip()
stripped = text.lstrip()
if path.suffix.lower() == ".json" or stripped.startswith("{"):
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
die(f"--prompt-file 看起来是 JSON,但解析失败:{path}: {exc}")
if not isinstance(data, dict) or not isinstance(data.get("prompt"), str) or not data["prompt"].strip():
die("--prompt-file 不能传入缺少 prompt 字段的 JSON。请先抽取 prompt 字段生成 .prompt.txt。")
prompt = data["prompt"].strip()
prompt_out = path.with_suffix(".prompt.txt")
write_text(prompt_out, prompt + "\n")
warn(f"--prompt-file 收到 JSON,已抽取 prompt 字段到:{prompt_out};后续请直接传入该 .prompt.txt。")
return prompt
return text
def load_codex_config_base_url() -> Optional[str]:
config_path = Path.home() / ".codex" / "config.toml"
if not config_path.exists():
return None
text = read_text(config_path)
try:
import tomllib
data = tomllib.loads(text)
except ModuleNotFoundError:
data = parse_basic_toml(text)
except Exception:
data = parse_basic_toml(text)
try:
if not isinstance(data, dict):
data = parse_basic_toml(text)
except Exception:
data = parse_basic_toml(text)
provider_name = data.get("model_provider")
providers = data.get("model_providers")
if isinstance(provider_name, str) and isinstance(providers, dict):
provider = providers.get(provider_name)
if isinstance(provider, dict) and isinstance(provider.get("base_url"), str):
return provider["base_url"].strip()
if isinstance(data.get("base_url"), str):
return data["base_url"].strip()
if isinstance(providers, dict):
for provider in providers.values():
if isinstance(provider, dict) and isinstance(provider.get("base_url"), str):
return provider["base_url"].strip()
return None
def find_secret(value: Any) -> Optional[str]:
if isinstance(value, dict):
for key in ("MAOMOMO_IMAGE_API_KEY", "OPENAI_API_KEY", "api_key", "openai_api_key", "token"):
raw = value.get(key)
if isinstance(raw, str) and raw.strip():
return raw.strip()
for child in value.values():
found = find_secret(child)
if found:
return found
if isinstance(value, list):
for child in value:
found = find_secret(child)
if found:
return found
return None
def load_codex_auth_api_key() -> Optional[str]:
auth_path = Path.home() / ".codex" / "auth.json"
if not auth_path.exists():
return None
try:
return find_secret(read_json(auth_path))
except Exception:
return None
def api_key() -> str:
value = os.getenv("MAOMOMO_IMAGE_API_KEY") or os.getenv("OPENAI_API_KEY") or load_codex_auth_api_key()
if not value:
die(
"未找到 API Key。请设置 MAOMOMO_IMAGE_API_KEY 或 OPENAI_API_KEY,"
"或确保 ~/.codex/auth.json 中存在可用 key。"
)
return value
def base_url() -> str:
return (
os.getenv("MAOMOMO_IMAGE_BASE_URL")
or os.getenv("OPENAI_BASE_URL")
or load_codex_config_base_url()
or DEFAULT_BASE_URL
).rstrip("/")
def image_model() -> str:
return os.getenv("MAOMOMO_IMAGE_MODEL") or os.getenv("CODEX_PPT_IMAGE_MODEL") or DEFAULT_MODEL
def style_prompt(style: str) -> str:
if style in STYLE_PRESETS:
return STYLE_PRESETS[style]
return style
def build_prompt(
*,
title: str,
image_type: str,
core_text: str,
style: str,
aspect_ratio: str,
visual_direction: str = "",
portrait_rules: str = "",
extra: str = "",
) -> str:
allowed_visible = [value for value in (title, core_text, "MAOMOMO") if value.strip()]
allowed_visible_text = "、".join(f"「{value}」" for value in allowed_visible)
parts = [
"生成一张 MAOMOMO 中文金融实操文章配图。",
f"图片类型:{image_type}",
f"标题 / 主题:{title}",
f"核心文案:{core_text}",
f"画幅:{aspect_ratio}",
f"画面呈现:{style_prompt(style)}",
f"可见文字限制:画面只允许出现这些指定业务文案:{allowed_visible_text};如补充要求明确列出已核对的品牌或支付标识,可作为小型辅助标识出现;不要添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。",
"硬性要求:包含清晰可见的 MAOMOMO 主标识;中文文字清楚可读;信息层级明确;港币金额统一使用 HKD 写法;不要伪造真实 App 截图;Mastercard、Visa、UnionPay、银行、支付宝等 Logo 或支付网络标识只有在官方材料、用户素材或补充要求明确核对正确时才可出现,并且只能作为辅助识别,不能抢占或替代 MAOMOMO 主标识位置;未确认时使用通用卡片或通用支付符号,不要凭空加入或替换卡组织;不要出现真实个人信息;猫咪不穿戴第三方 Logo。",
]
if visual_direction.strip():
parts.append(f"确认视觉方向:{visual_direction.strip()}。整组图片必须保持同一视觉方向,不得混用 2D 和 3D。")
if style == "xiaohongshu-vertical" or aspect_ratio in {"3:4", "9:16"}:
rules = portrait_rules.strip() or (
"native portrait composition;no squeezed elements;"
"no stretched card / logo / text;vertical layout redesigned for Xiaohongshu;"
"禁止把横版设计硬塞进竖版画幅。"
)
parts.append(f"小红书竖图要求:{rules}")
if extra.strip():
parts.append(f"补充要求:{extra.strip()}")
return "\n".join(parts)
def request_image(prompt: str, *, size: str, quality: str, output_format: str) -> bytes:
payload = {
"model": image_model(),
"prompt": prompt,
"size": size,
"quality": quality,
"n": 1,
"response_format": "b64_json",
}
if output_format:
payload["output_format"] = output_format
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
base_url() + "/images/generations",
data=body,
headers={
"Authorization": f"Bearer {api_key()}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=300) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
die(f"图片接口返回 HTTP {exc.code}: {detail}")
except Exception as exc:
die(f"图片接口请求失败:{exc}")
items = data.get("data")
if not isinstance(items, list) or not items:
die("图片接口响应缺少 data。")
first = items[0]
if not isinstance(first, dict):
die("图片接口响应格式不正确。")
if isinstance(first.get("b64_json"), str):
return base64.b64decode(first["b64_json"])
if isinstance(first.get("url"), str):
with urllib.request.urlopen(first["url"], timeout=300) as resp:
return resp.read()
die("图片接口响应中没有 b64_json 或 url。")
return b""
def normalize_output(path: str, *, base_dir: Optional[Path]) -> Path:
out = Path(path)
if not out.is_absolute() and base_dir is not None:
out = base_dir / out
return out.resolve()
def generate_one(args: argparse.Namespace) -> Dict[str, str]:
if args.prompt_file:
prompt = load_prompt_file(Path(args.prompt_file))
elif args.prompt:
prompt = args.prompt.strip()
else:
prompt = build_prompt(
title=args.title,
image_type=args.image_type,
core_text=args.core_text,
style=args.style,
aspect_ratio=args.aspect_ratio,
visual_direction=args.visual_direction,
portrait_rules=args.portrait_rules,
extra=args.extra,
)
out = normalize_output(args.out, base_dir=None)
if args.dry_run:
write_text(out.with_suffix(".prompt.txt"), prompt + "\n")
return {"out": str(out), "prompt": str(out.with_suffix(".prompt.txt")), "status": "dry-run"}
image_bytes = request_image(prompt, size=args.size, quality=args.quality, output_format=args.output_format)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(image_bytes)
return {"out": str(out), "status": "generated"}
def manifest_items(path: Path) -> List[Dict[str, Any]]:
data = read_json(path)
if isinstance(data, dict):
items = data.get("images")
else:
items = data
if not isinstance(items, list):
die("manifest 必须是图片数组,或包含 images 数组的对象。")
normalized: List[Dict[str, Any]] = []
for index, item in enumerate(items, start=1):
if not isinstance(item, dict):
die(f"manifest 第 {index} 项不是对象。")
normalized.append(item)
return normalized
def batch(args: argparse.Namespace) -> List[Dict[str, str]]:
manifest_path = Path(args.manifest).resolve()
base_dir = Path(args.base_dir).resolve() if args.base_dir else manifest_path.parent
results: List[Dict[str, str]] = []
for index, item in enumerate(manifest_items(manifest_path), start=1):
filename = item.get("file_name") or item.get("filename") or item.get("out")
if not isinstance(filename, str) or not filename.strip():
die(f"manifest 第 {index} 项缺少 file_name。")
style = str(item.get("style") or args.style or DEFAULT_STYLE)
aspect_ratio = str(item.get("aspect_ratio") or args.aspect_ratio)
visual_direction = str(item.get("visual_direction") or args.visual_direction or "")
raw_portrait_rules = item.get("portrait_composition_rules") or args.portrait_rules
if isinstance(raw_portrait_rules, list):
portrait_rules = ";".join(str(value) for value in raw_portrait_rules)
else:
portrait_rules = str(raw_portrait_rules or "")
prompt = item.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
prompt = build_prompt(
title=str(item.get("title") or item.get("alt_text") or filename),
image_type=str(item.get("type") or "文章配图"),
core_text=str(item.get("core_text") or item.get("caption") or item.get("alt_text") or ""),
style=style,
aspect_ratio=aspect_ratio,
visual_direction=visual_direction,
portrait_rules=portrait_rules,
extra=str(item.get("extra") or item.get("requirements") or ""),
)
out = normalize_output(filename, base_dir=base_dir)
if args.dry_run:
write_text(out.with_suffix(".prompt.txt"), prompt + "\n")
results.append({"out": str(out), "prompt": str(out.with_suffix(".prompt.txt")), "status": "dry-run"})
continue
image_bytes = request_image(
prompt,
size=str(item.get("size") or args.size),
quality=str(item.get("quality") or args.quality),
output_format=str(item.get("output_format") or args.output_format),
)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(image_bytes)
results.append({"out": str(out), "status": "generated"})
if args.sleep > 0:
time.sleep(args.sleep)
return results
def print_styles(_: argparse.Namespace) -> int:
print(json.dumps(STYLE_PRESETS, ensure_ascii=False, indent=2))
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Generate MAOMOMO article images with an OpenAI-compatible API.")
subparsers = parser.add_subparsers(dest="command", required=True)
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--style", default=DEFAULT_STYLE, help="Style preset name or custom style text.")
common.add_argument("--size", default=DEFAULT_SIZE)
common.add_argument("--quality", default=DEFAULT_QUALITY)
common.add_argument("--output-format", default="png")
common.add_argument("--aspect-ratio", default="16:9")
common.add_argument("--visual-direction", default="")
common.add_argument("--portrait-rules", default="")
common.add_argument("--dry-run", action="store_true", help="Only write prompt files; do not call the image API.")
generate = subparsers.add_parser("generate", parents=[common], help="Generate one image.")
generate.add_argument("--out", required=True)
generate.add_argument("--prompt")
generate.add_argument("--prompt-file")
generate.add_argument("--title", default="MAOMOMO 文章配图")
generate.add_argument("--image-type", default="文章配图")
generate.add_argument("--core-text", default="")
generate.add_argument("--extra", default="")
batch_parser = subparsers.add_parser("batch", parents=[common], help="Generate images from a JSON manifest.")
batch_parser.add_argument("--manifest", required=True)
batch_parser.add_argument("--base-dir", help="Base directory for relative file_name paths. Defaults to manifest dir.")
batch_parser.add_argument("--sleep", type=float, default=0.0, help="Seconds to sleep between API calls.")
subparsers.add_parser("styles", help="Print bundled style presets.")
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)
if args.command == "styles":
return print_styles(args)
if args.command == "generate":
result = generate_one(args)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if args.command == "batch":
results = batch(args)
print(json.dumps(results, ensure_ascii=False, indent=2))
return 0
parser.print_help()
return 1
if __name__ == "__main__":
raise SystemExit(main())
+554
View File
@@ -0,0 +1,554 @@
#!/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"
FINAL_MANIFEST_FILE = "final_manifest.json"
GITHUB_BATCHES_FILE = "github_batches.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 relative_path_string(project: Path, path: Path) -> str:
try:
return str(path.resolve().relative_to(project))
except ValueError:
return str(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 load_batches_doc(project: Path) -> Dict[str, Any]:
path = project / GITHUB_BATCHES_FILE
if not path.exists():
return {"version": 1, "batches": []}
raw = read_json(path)
if not isinstance(raw, dict):
die(f"{GITHUB_BATCHES_FILE} 必须是对象。")
if not isinstance(raw.get("batches"), list):
raw["batches"] = []
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,
"aspect_ratio": data.get("aspect_ratio", ""),
"visual_direction": data.get("visual_direction", ""),
"delivery_batch": data.get("delivery_batch", ""),
"qa_status": "pending",
"entered_assets": False,
"final": False,
"github_pushed": False,
"github_batch": "",
"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):
github = "github" if job.get("github_pushed") else "local"
qa = job.get("qa_status", "")
print(f"{job_key(job) or '<unknown>'}\t{job.get('status', 'unknown')}\t{qa}\t{github}\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")
entered_assets = False
if isinstance(asset_value, str) and asset_value:
copy_if_needed(origin_out, resolve_in_project(project, asset_value))
entered_assets = True
result = {
"agent_id": args.agent_id,
"backend_used": args.backend_used,
"selected_source": str(source),
"origin_out": relative_path_string(project, origin_out),
"asset_out": asset_value,
"qa_status": "passed",
"entered_assets": entered_assets,
"qa_note": args.qa_note,
"time": now_iso(),
}
job["status"] = "recorded"
job["qa_status"] = "passed"
job["entered_assets"] = entered_assets
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 cmd_final_manifest(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
state_doc = load_state_doc(project)
assets: List[Dict[str, Any]] = []
incomplete: List[str] = []
for job in job_doc["jobs"]:
if not isinstance(job, dict):
continue
key = job_key(job) or "<unknown>"
if job.get("status") != "recorded":
incomplete.append(f"{key}: status={job.get('status', 'unknown')}")
continue
if job.get("qa_status") != "passed":
incomplete.append(f"{key}: qa_status={job.get('qa_status', 'unknown')}")
continue
if job.get("entered_assets") is not True:
incomplete.append(f"{key}: entered_assets=false")
continue
result = job.get("result") if isinstance(job.get("result"), dict) else {}
asset_value = result.get("asset_out") or job.get("asset_out")
if not isinstance(asset_value, str) or not asset_value:
incomplete.append(f"{key}: 缺少 asset_out")
continue
asset_path = resolve_in_project(project, asset_value)
if not asset_path.exists():
incomplete.append(f"{key}: assets 文件不存在:{asset_value}")
continue
job["final"] = True
assets.append(
{
"id": key,
"prompt_file": job.get("prompt_file", ""),
"origin_out": result.get("origin_out") or job.get("origin_out", ""),
"asset_out": relative_path_string(project, asset_path),
"qa_status": job.get("qa_status", ""),
"qa_note": result.get("qa_note", ""),
"aspect_ratio": job.get("aspect_ratio", ""),
"visual_direction": job.get("visual_direction", ""),
"delivery_batch": job.get("delivery_batch", ""),
"github_pushed": bool(job.get("github_pushed")),
"github_batch": job.get("github_batch", ""),
}
)
if incomplete and not args.allow_incomplete:
die("仍有未进入最终 manifest 的图片 job:\n" + "\n".join(incomplete))
if not assets:
die("没有可写入 final_manifest.json 的最终图片。")
manifest_path = resolve_in_project(project, args.out)
manifest = {
"version": 1,
"generated_at": now_iso(),
"project": project.name,
"asset_count": len(assets),
"assets": assets,
}
write_json(manifest_path, manifest)
event(
state_doc,
"final_manifest",
"*",
{
"path": relative_path_string(project, manifest_path),
"asset_count": len(assets),
"allow_incomplete": args.allow_incomplete,
},
)
save_docs(project, job_doc, state_doc)
print(
json.dumps(
{
"status": "final_manifest_written",
"path": relative_path_string(project, manifest_path),
"assets": len(assets),
},
ensure_ascii=False,
indent=2,
)
)
return 0
def cmd_github_batch(args: argparse.Namespace) -> int:
project = project_path(args.project)
job_doc = load_job_doc(project)
state_doc = load_state_doc(project)
batches_doc = load_batches_doc(project)
rel_paths: List[str] = []
for value in args.paths:
path = resolve_in_project(project, value)
if not path.exists():
die(f"批次文件不存在:{value}")
rel_paths.append(relative_path_string(project, path))
path_set = set(rel_paths)
pushed_jobs: List[str] = []
for job in job_doc["jobs"]:
if not isinstance(job, dict):
continue
result = job.get("result") if isinstance(job.get("result"), dict) else {}
candidate_values = [job.get("asset_out"), result.get("asset_out")]
normalized = {
relative_path_string(project, resolve_in_project(project, value))
for value in candidate_values
if isinstance(value, str) and value
}
if normalized.intersection(path_set):
job["github_pushed"] = True
job["github_batch"] = args.batch
if isinstance(job.get("result"), dict):
job["result"]["github_pushed"] = True
job["result"]["github_batch"] = args.batch
pushed_jobs.append(job_key(job) or "<unknown>")
batch_record = {
"batch": args.batch,
"commit": args.commit,
"url": args.url,
"release": args.release,
"paths": rel_paths,
"pushed_jobs": pushed_jobs,
"note": args.note,
"time": now_iso(),
}
batches = [item for item in batches_doc["batches"] if not (isinstance(item, dict) and item.get("batch") == args.batch)]
batches.append(batch_record)
batches_doc["batches"] = batches
manifest_path = project / FINAL_MANIFEST_FILE
if manifest_path.exists():
manifest = read_json(manifest_path)
if isinstance(manifest, dict) and isinstance(manifest.get("assets"), list):
for item in manifest["assets"]:
if not isinstance(item, dict):
continue
asset_value = item.get("asset_out")
if not isinstance(asset_value, str) or not asset_value:
continue
normalized = relative_path_string(project, resolve_in_project(project, asset_value))
if normalized in path_set:
item["github_pushed"] = True
item["github_batch"] = args.batch
item["github_commit"] = args.commit
item["github_url"] = args.url
write_json(manifest_path, manifest)
write_json(project / GITHUB_BATCHES_FILE, batches_doc)
event(state_doc, "github_batch", "*", batch_record)
save_docs(project, job_doc, state_doc)
print(
json.dumps(
{
"status": "github_batch_recorded",
"batch": args.batch,
"paths": len(rel_paths),
"pushed_jobs": pushed_jobs,
},
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)
final_manifest = subparsers.add_parser("final-manifest", help="Write final_manifest.json from recorded assets.")
final_manifest.add_argument("project")
final_manifest.add_argument("--out", default=FINAL_MANIFEST_FILE)
final_manifest.add_argument("--allow-incomplete", action="store_true")
final_manifest.set_defaults(func=cmd_final_manifest)
github_batch = subparsers.add_parser("github-batch", help="Record a GitHub delivery batch.")
github_batch.add_argument("project")
github_batch.add_argument("--batch", required=True)
github_batch.add_argument("--commit", required=True)
github_batch.add_argument("--url", required=True)
github_batch.add_argument("--release", default="")
github_batch.add_argument("--paths", nargs="+", required=True)
github_batch.add_argument("--note", default="")
github_batch.set_defaults(func=cmd_github_batch)
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())
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""为 MAOMOMO 配图叠加 45° 平铺半透明水印。
默认水印:MAOMOMO.COM,45° 对角线平铺;自动对比色,浅底黑字、深底白字。
用法:
uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets
uv run python scripts/maomomo_tile_watermark.py --input assets/example.png --output assets/example-watermarked.png
说明:
脚本使用 Pillow。本机 Python 没有 Pillow 且存在 uv 时,会自动用
`uv run --with pillow python ...` 重启一次,不需要把依赖写入项目。
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import sys
from typing import Iterable, Iterator, Optional, Tuple, Any
SUPPORTED_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
def die(message: str, code: int = 1) -> None:
print(f"错误:{message}", file=sys.stderr)
raise SystemExit(code)
def ensure_pillow() -> Tuple[Any, Any, Any]:
try:
from PIL import Image, ImageDraw, ImageFont
return Image, ImageDraw, ImageFont
except ModuleNotFoundError as exc:
if exc.name != "PIL":
raise
uv = shutil.which("uv")
if uv and os.environ.get("MAOMOMO_WATERMARK_REEXEC") != "1":
env = os.environ.copy()
env["MAOMOMO_WATERMARK_REEXEC"] = "1"
cmd = [uv, "run", "--with", "pillow>=10.0.0", "python", str(Path(__file__).resolve()), *sys.argv[1:]]
raise SystemExit(subprocess.call(cmd, env=env))
die("缺少 Pillow。请使用 `uv run python scripts/maomomo_tile_watermark.py ...` 运行,或安装 Pillow。")
def parse_color(value: str) -> Tuple[int, int, int]:
text = value.strip()
if text.lower() == "auto":
die("auto color must be resolved after reading each image")
if text.startswith("#"):
text = text[1:]
if len(text) != 6:
die("颜色必须是 auto 或 #RRGGBB,例如 #000000 / #FFFFFF")
try:
return tuple(int(text[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
except ValueError:
die("颜色必须是有效十六进制,例如 #000000 / #FFFFFF")
def auto_contrast_color(image_path: Path, threshold: float = 150.0) -> Tuple[int, int, int]:
Image, _, _ = ensure_pillow()
with Image.open(image_path) as src:
r, g, b = src.convert("RGB").resize((1, 1)).getpixel((0, 0))
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
return (0, 0, 0) if luminance >= threshold else (255, 255, 255)
def load_font(ImageFont: Any, font_path: Optional[str], font_size: int) -> Any:
candidates = []
if font_path:
candidates.append(Path(font_path).expanduser())
candidates.extend(
Path(p)
for p in [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
]
)
for candidate in candidates:
if candidate.exists():
try:
return ImageFont.truetype(str(candidate), font_size)
except OSError:
continue
return ImageFont.load_default()
def iter_images(input_path: Path) -> Iterator[Path]:
if input_path.is_file():
if input_path.suffix.lower() not in SUPPORTED_EXTS:
die(f"不支持的图片格式:{input_path}")
yield input_path
return
if not input_path.is_dir():
die(f"输入不存在:{input_path}")
for path in sorted(input_path.rglob("*")):
if path.is_file() and path.suffix.lower() in SUPPORTED_EXTS:
yield path
def resolve_output(input_root: Path, image_path: Path, output_root: Path) -> Path:
if input_root.is_file():
return output_root
rel = image_path.relative_to(input_root)
return output_root / rel
def make_tile(
Image: Any,
ImageDraw: Any,
text: str,
font: Any,
color: Tuple[int, int, int],
alpha: int,
angle: float,
padding: int,
) -> Any:
dummy = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
draw = ImageDraw.Draw(dummy)
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
tile_w = max(1, text_w + padding * 2)
tile_h = max(1, text_h + padding * 2)
tile = Image.new("RGBA", (tile_w, tile_h), (0, 0, 0, 0))
tile_draw = ImageDraw.Draw(tile)
tile_draw.text((padding, padding), text, font=font, fill=(*color, alpha))
return tile.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC)
def apply_watermark(
image_path: Path,
output_path: Path,
*,
text: str,
angle: float,
alpha: int,
color: Tuple[int, int, int],
font_size: int,
font_path: Optional[str],
spacing: int,
quality: int,
) -> None:
if alpha < 0 or alpha > 255:
die("alpha 必须在 0-255 之间")
if spacing < 0:
die("spacing 不能为负数")
Image, ImageDraw, ImageFont = ensure_pillow()
font = load_font(ImageFont, font_path, font_size)
with Image.open(image_path) as src:
base = src.convert("RGBA")
width, height = base.size
diagonal = int((width * width + height * height) ** 0.5)
tile = make_tile(Image, ImageDraw, text, font, color, alpha, angle, spacing)
overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0))
step_x = max(1, tile.width + spacing)
step_y = max(1, tile.height + spacing)
start_x = -diagonal
start_y = -diagonal
for y in range(start_y, height + diagonal, step_y):
for x in range(start_x, width + diagonal, step_x):
overlay.alpha_composite(tile, (x, y))
out = Image.alpha_composite(base, overlay)
output_path.parent.mkdir(parents=True, exist_ok=True)
suffix = output_path.suffix.lower()
if suffix in {".jpg", ".jpeg"}:
out = out.convert("RGB")
out.save(output_path, quality=quality, optimize=True)
elif suffix == ".webp":
out.save(output_path, quality=quality, method=6)
else:
out.save(output_path)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="为图片叠加 MAOMOMO.COM 45° 平铺半透明水印。")
parser.add_argument("--input", required=True, help="输入图片文件或目录。")
parser.add_argument("--output", required=True, help="输出图片文件或目录。目录模式会保持相对路径。")
parser.add_argument("--text", default="MAOMOMO.COM", help="水印文字,默认 MAOMOMO.COM。")
parser.add_argument("--angle", type=float, default=45.0, help="水印旋转角度,默认 45。")
parser.add_argument("--alpha", type=int, default=70, help="透明度 0-255,默认 70。")
parser.add_argument("--color", default="auto", help="水印颜色,默认 auto:浅底黑字、深底白字;也可指定 #000000 / #FFFFFF。")
parser.add_argument("--font-size", type=int, default=50, help="字体大小,默认 50。")
parser.add_argument("--font", default=None, help="可选字体路径。")
parser.add_argument("--spacing", type=int, default=28, help="水印间距 / 内边距,默认 28;目标是每张图至少可见 2 个完整 MAOMOMO.COM。")
parser.add_argument("--quality", type=int, default=95, help="JPG/WEBP 输出质量,默认 95。")
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)
input_path = Path(args.input).expanduser().resolve()
output_path = Path(args.output).expanduser().resolve()
images = list(iter_images(input_path))
if not images:
die(f"没有找到支持的图片:{input_path}")
if input_path.is_dir() and output_path.exists() and output_path.is_file():
die("输入为目录时,输出必须是目录路径")
results = []
for image_path in images:
target = resolve_output(input_path, image_path, output_path)
apply_watermark(
image_path,
target,
text=args.text,
angle=args.angle,
alpha=args.alpha,
color=auto_contrast_color(image_path) if str(args.color).lower() == "auto" else parse_color(args.color),
font_size=args.font_size,
font_path=args.font,
spacing=args.spacing,
quality=args.quality,
)
results.append((image_path, target))
for src, dst in results:
print(f"watermarked: {src} -> {dst}")
return 0
if __name__ == "__main__":
raise SystemExit(main())