优化SKILL

main
maomomo 2026-06-27 21:37:38 +08:00
parent 286e28b7f3
commit 68c9ed7bcb
7 changed files with 670 additions and 8 deletions

View File

@ -25,10 +25,14 @@ maomomo-article-writer/
├── README.md # 面向使用者的说明文档 ├── README.md # 面向使用者的说明文档
├── agents/ ├── agents/
│ └── openai.yaml # 技能展示名称、简介和默认提示语 │ └── openai.yaml # 技能展示名称、简介和默认提示语
├── scripts/
│ └── maomomo_image_gen.py # OpenAI 兼容 API/CLI 配图 fallback
└── references/ └── references/
├── source-and-fact-check.md # 来源读取、事实核查和风险口径 ├── source-and-fact-check.md # 来源读取、事实核查和风险口径
├── style-guide.md # MAOMOMO 写作风格、模板和文章骨架 ├── style-guide.md # MAOMOMO 写作风格、模板和文章骨架
├── visual-assets.md # 配图清单、视觉规则和图片 QA ├── visual-assets.md # 配图清单、视觉规则和图片 QA
├── recommended-styles.md # 推荐配图风格和提示词片段
├── image-generation.md # 图片生成脚本、manifest 和后端选择
└── delivery-rules.md # 完整交付、打包和最终检查 └── delivery-rules.md # 完整交付、打包和最终检查
``` ```
@ -122,7 +126,7 @@ maomomo-article-writer
2. 读取来源并列出已读 / 未读材料,整理事实表和待确认项。 2. 读取来源并列出已读 / 未读材料,整理事实表和待确认项。
3. 确认文章范围、大纲、标题方向、核心口径和图片规划。 3. 确认文章范围、大纲、标题方向、核心口径和图片规划。
4. 写作或优化发布用 Markdown。 4. 写作或优化发布用 Markdown。
5. 生成或准备独立 PNG 配图,插入 Markdown。 5. 确认配图风格,生成或准备独立 PNG 配图,插入 Markdown。
6. 检查来源、图片路径、风险提示、编辑痕迹和 ZIP 内容,最后交付文件路径。 6. 检查来源、图片路径、风险提示、编辑痕迹和 ZIP 内容,最后交付文件路径。
用户明确说“只要文字”“只要大纲”“不要图片”“不要打包”时,会按用户限制执行,不强行创建完整发布包。 用户明确说“只要文字”“只要大纲”“不要图片”“不要打包”时,会按用户限制执行,不强行创建完整发布包。
@ -147,6 +151,7 @@ maomomo-article-writer
- 语气像懂行朋友,不像官方宣传稿 - 语气像懂行朋友,不像官方宣传稿
- 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞 - 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞
- 配图统一使用白橘猫、暖橙色、干净金融科技教程风 - 配图统一使用白橘猫、暖橙色、干净金融科技教程风
- 如果用户没有指定配图风格,先在 `warm-fintech-guide`、`data-card-dashboard`、`clean-editorial`、`handdrawn-note`、`xiaohongshu-vertical` 中推荐 2-3 个方向。
文章不应当: 文章不应当:
@ -161,6 +166,8 @@ maomomo-article-writer
- 详细文章模板、标题套路和常用词写在 `references/style-guide.md` - 详细文章模板、标题套路和常用词写在 `references/style-guide.md`
- 来源核查、时效性、冲突口径和风险表达写在 `references/source-and-fact-check.md` - 来源核查、时效性、冲突口径和风险表达写在 `references/source-and-fact-check.md`
- 配图类型、猫咪视觉、文件命名和图片 QA 写在 `references/visual-assets.md` - 配图类型、猫咪视觉、文件命名和图片 QA 写在 `references/visual-assets.md`
- 推荐配图风格、适用场景和提示词片段写在 `references/recommended-styles.md`
- 图片 API/CLI fallback、批量 manifest 和脚本用法写在 `references/image-generation.md`,脚本为 `scripts/maomomo_image_gen.py`
- 完整交付、ZIP、附件和最终检查写在 `references/delivery-rules.md` - 完整交付、ZIP、附件和最终检查写在 `references/delivery-rules.md`
- 修改技能展示名称或默认提示语时,同步更新 `agents/openai.yaml` - 修改技能展示名称或默认提示语时,同步更新 `agents/openai.yaml`
- 如果新增具体行业资料、平台规则或长期复用模板,优先放入 `references/`,避免让 `SKILL.md` 过长。 - 如果新增具体行业资料、平台规则或长期复用模板,优先放入 `references/`,避免让 `SKILL.md` 过长。

View File

@ -70,8 +70,10 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
5. 配图规划和生成。 5. 配图规划和生成。
- 需要配图时读 `references/visual-assets.md` - 需要配图时读 `references/visual-assets.md`
- 先生成图片清单类型、标题、放置位置、核心文案、文件名、alt text、是否依赖用户截图。 - 若用户没有指定视觉方向,读 `references/recommended-styles.md`,先提供 2-3 个 MAOMOMO 配图风格选项并推荐一个;确认后整篇文章保持同一视觉身份。
- 先生成图片清单类型、标题、放置位置、核心文案、文件名、alt text、风格、是否依赖用户截图。
- 每张图独立生成或准备为高清 PNG放入 `assets/`,用相对路径插入 Markdown。 - 每张图独立生成或准备为高清 PNG放入 `assets/`,用相对路径插入 Markdown。
- 优先使用当前 agent 的内置图片生成工具;当用户要求 API/CLI、内置工具不可用或需要批量 manifest 时,读 `references/image-generation.md` 并使用 `scripts/maomomo_image_gen.py`
- 没有真实截图时只能生成“示意图 / 信息图”,不能伪造官方 App 截图。 - 没有真实截图时只能生成“示意图 / 信息图”,不能伪造官方 App 截图。
6. 打包与最终 QA。 6. 打包与最终 QA。
@ -117,4 +119,6 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
- `references/source-and-fact-check.md`:来源读取、事实表、时效性、冲突口径和金融风险核查。 - `references/source-and-fact-check.md`:来源读取、事实表、时效性、冲突口径和金融风险核查。
- `references/style-guide.md`MAOMOMO 文章类型、标题套路、语气、结构模板、小红书模式和写作细节。 - `references/style-guide.md`MAOMOMO 文章类型、标题套路、语气、结构模板、小红书模式和写作细节。
- `references/visual-assets.md`配图类型、猫咪视觉、文件命名、Markdown 插图、禁止伪造截图和图片 QA。 - `references/visual-assets.md`配图类型、猫咪视觉、文件命名、Markdown 插图、禁止伪造截图和图片 QA。
- `references/recommended-styles.md`:推荐配图风格、适用场景、推荐话术和提示词片段。
- `references/image-generation.md`:图片生成后端选择、`scripts/maomomo_image_gen.py` 用法、manifest 和故障处理。
- `references/delivery-rules.md`完整交付、ZIP、更新打包、附件策略和最终检查清单。 - `references/delivery-rules.md`完整交付、ZIP、更新打包、附件策略和最终检查清单。

View File

@ -1,7 +1,7 @@
interface: interface:
display_name: "MAOMOMO 文章生成器" display_name: "MAOMOMO 文章生成器"
short_description: "阶段化生成 MAOMOMO 文章、配图和 ZIP 发布包" short_description: "阶段化生成 MAOMOMO 文章、多风格配图和 ZIP 发布包"
default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和口径,再交付 Markdown、assets 独立配图目录和 ZIP 包。" default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和口径,再确认配图风格,最后交付 Markdown、assets 独立配图目录和 ZIP 包。"
policy: policy:
allow_implicit_invocation: true allow_implicit_invocation: true

View File

@ -0,0 +1,107 @@
# 图片生成脚本与后端选择
需要实际生成配图文件、使用 API/CLI fallback、批量生成 assets或排查图片接口配置时先读本文件。
## 后端选择
优先顺序:
1. Codex / 当前 agent 的内置图片生成工具。适合大多数配图任务。
2. `scripts/maomomo_image_gen.py` API/CLI fallback。适合用户明确要求 API/CLI、内置工具不可用、需要批量 manifest、或希望把生成过程记录到文件时。
不要因为脚本存在就强制使用脚本。若内置图片工具可用,仍优先使用内置工具生成独立 PNG再保存到 `assets/`
## 配置来源
脚本按以下顺序读取配置:
- `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
不要在日志、回答、Markdown 或 manifest 中写入完整 API key。
## 单张生成
```bash
python3 scripts/maomomo_image_gen.py generate \
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \
--style warm-fintech-guide \
--title "汇丰 Mastercard 支付宝活动" \
--image-type "封面图" \
--core-text "先看报名、门槛和封顶,避免白刷"
```
先只看 prompt
```bash
python3 scripts/maomomo_image_gen.py generate \
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \
--style data-card-dashboard \
--title "返现计算图" \
--image-type "计算图" \
--core-text "HK$190 ÷ 9% ≈ HK$2112超过部分没有额外收益" \
--dry-run
```
## 批量 manifest
manifest 示例:
```json
{
"images": [
{
"file_name": "assets/hsbc-mastercard-alipay-campaign-01-cover.png",
"type": "封面图",
"title": "汇丰 Mastercard 支付宝活动",
"core_text": "先报名,再看门槛和封顶",
"alt_text": "MAOMOMO 汇丰 Mastercard 支付宝返现活动封面图",
"style": "warm-fintech-guide",
"aspect_ratio": "16:9"
},
{
"file_name": "assets/hsbc-mastercard-alipay-campaign-02-calculation.png",
"type": "计算图",
"title": "怎么刷到刚刚好",
"core_text": "HK$190 ÷ 9% ≈ HK$2112超过部分没有额外收益",
"style": "data-card-dashboard"
}
]
}
```
运行:
```bash
python3 scripts/maomomo_image_gen.py batch \
--manifest article/image_manifest.json \
--base-dir article
```
`file_name` 必须与 Markdown 中的相对路径一致,且最终文件必须真实存在。
## 内置风格名
可用:
- `warm-fintech-guide`
- `clean-editorial`
- `data-card-dashboard`
- `handdrawn-note`
- `xiaohongshu-vertical`
查看完整描述:
```bash
python3 scripts/maomomo_image_gen.py styles
```
## 失败处理
- API key 缺失:提示用户配置环境变量或检查 Codex 配置,不要编造图片文件。
- 接口失败:保留已生成图片,说明失败项,必要时用 `--dry-run` 交付 prompt 清单作为临时排查材料。
- 图片文字乱码或金额错误:重新生成该单张图片;不要手工覆盖文字伪装成模型输出。
- 图片不符合风格:先更新 manifest 的 `style` / `core_text` / `extra`,再重跑对应项。

View File

@ -0,0 +1,136 @@
# MAOMOMO 推荐配图风格
需要选择、推荐、复用或调整配图风格时,读本文件。风格名要与 `scripts/maomomo_image_gen.py``--style` 保持一致。
## 推荐顺序
默认优先推荐 `warm-fintech-guide`。如果文章明显更偏数字、对比、避坑或小红书,再切换到更合适的风格。
| 风格名 | 优先使用场景 | 不适合 |
|---|---|---|
| `warm-fintech-guide` | 常规 MAOMOMO 文章、港卡、银行活动、返现攻略、开户教程 | 数字极密集的计算页、小红书竖版 |
| `data-card-dashboard` | 返现计算、费用测算、活动门槛、产品对比、时间线 | 情绪化避坑故事、截图驱动步骤 |
| `clean-editorial` | SEO 长文、总结盘点、品牌感封面、解释型文章 | 需要强步骤感或大量数字卡片的图 |
| `handdrawn-note` | 避坑经验、保号教程、操作步骤、实测提醒 | 严肃金融报告感、密集表格 |
| `xiaohongshu-vertical` | 小红书、4 图看完、手机端竖版内容 | 常规网站横版封面和正文配图 |
## warm-fintech-guide
推荐语:
```text
我建议用 warm-fintech-guide因为它最贴近 MAOMOMO 默认视觉:白底、暖橙、猫咪向导和金融教程卡片,适合大多数港卡、银行活动和返现攻略。
```
视觉要点:
- 明亮白底,暖橙色强调。
- 白橘猫作为向导、提示牌或角落品牌元素。
- 信息卡片清晰,数字、日期和步骤不拥挤。
- 适合 16:9 横版封面、总览图、流程图。
提示词片段:
```text
MAOMOMO 暖橙金融教程风,明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,现代 App 教程质感,中文标题清楚,包含 MAOMOMO 标识。
```
## data-card-dashboard
推荐语:
```text
我建议用 data-card-dashboard因为这篇重点是规则、门槛和金额读者需要一眼看懂数字和限制。
```
视觉要点:
- 指标卡、对比表、时间线、计算公式。
- 数字层级强,适合“封顶”“门槛”“成本”“到账时间”。
- 猫咪比例较小,只做提示和品牌识别。
- 适合返现计算图、路线对比图、活动总览图。
提示词片段:
```text
MAOMOMO 数据卡片仪表盘风,白底,暖橙和蓝灰辅助色,使用指标卡、表格、时间线和计算公式展示关键信息,中文数字清晰可读,猫咪作为小型提示元素。
```
## clean-editorial
推荐语:
```text
我建议用 clean-editorial因为这篇更像站内解释型长文需要封面干净、标题有传播感但不要太像促销海报。
```
视觉要点:
- 大标题、留白、轻杂志感。
- 猫咪放在角落或标题区附近,不抢正文信息。
- 更适合封面、文章总结图、观点图。
- 画面克制,避免过度卡通。
提示词片段:
```text
MAOMOMO 清爽编辑部风,白底大标题,轻杂志排版,暖橙点缀,留白充足,白橘猫作为角落品牌元素,适合中文金融经验文章封面。
```
## handdrawn-note
推荐语:
```text
我建议用 handdrawn-note因为这篇是避坑 / 步骤教程,手绘箭头、便签和圈注更容易把翻车点讲明白。
```
视觉要点:
- 手绘箭头、便签、圈注、风险提示贴纸。
- 适合“不要这样做”“先做这一步”“最大坑点”。
- 信息密度中等,避免塞太多长句。
- 猫咪可以举提示牌或指向关键步骤。
提示词片段:
```text
MAOMOMO 手绘便签风,白底纸感,暖橙便利贴,手绘箭头和圈注,白橘猫举提示牌,中文步骤清楚,适合避坑经验和保姆式教程。
```
## xiaohongshu-vertical
推荐语:
```text
我建议用 xiaohongshu-vertical因为用户要发小红书应该重新做 9:16 竖图,而不是复用站内横版图。
```
视觉要点:
- 9:16 竖版,手机端可读。
- 大字少字,标题钩子强。
- 4 图系列统一:封面、核心拆解、操作流程、避坑总结。
- 每张图都有 MAOMOMO 标识或猫咪水印。
提示词片段:
```text
MAOMOMO 小红书竖版攻略风9:16手机端可读大标题少量关键数字暖橙白底白橘猫水印适合 4 图看完的中文金融攻略。
```
## 风格选择输出格式
向用户推荐时使用:
```text
我建议用 A因为【一句话说明与文章类型的匹配】。
A. warm-fintech-guide推荐【适合原因】
B. data-card-dashboard【适合原因】
C. handdrawn-note【适合原因】
你选哪个?也可以指定更偏简洁、数据感、手绘感或小红书感。
```
用户选择后,把风格名写入图片清单的 `风格` 字段或 manifest 的 `style` 字段。

View File

@ -11,6 +11,40 @@
- 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。 - 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。
- 每张图必须包含清晰可见的 `MAOMOMO` 标识。 - 每张图必须包含清晰可见的 `MAOMOMO` 标识。
## 风格选择
如果用户没有指定视觉方向,生成图片前先读 `references/recommended-styles.md`,给 2-3 个可选风格,并推荐一个。确认后,整篇文章的配图保持同一套视觉身份:稳定的配色、猫咪造型、标题区、图标语言和信息密度;不同图片可以根据用途变化构图。
默认推荐逻辑:
- 活动 / 返现 / 港卡攻略:优先 `warm-fintech-guide`
- 规则、门槛、费用、返现计算和产品对比:优先 `data-card-dashboard`
- SEO 长文、经验整理、品牌感更强的文章:优先 `clean-editorial`
- 保姆式教程、避坑经验和轻量操作步骤:优先 `handdrawn-note`
- 小红书 / 4 图看完:必须用 `xiaohongshu-vertical`,并生成 9:16 竖图。
可选风格:
| 风格名 | 适合场景 | 视觉方向 |
|---|---|---|
| `warm-fintech-guide` | 默认 MAOMOMO 金融攻略、港卡、银行活动 | 白底、暖橙强调色、干净卡片、白橘猫向导 |
| `clean-editorial` | SEO 长文、解释型文章、站内封面 | 大标题、留白、轻杂志感、猫咪作为角落品牌元素 |
| `data-card-dashboard` | 返现计算、活动对比、费用路径、时间线 | 指标卡、表格感、仪表盘、清晰数字层级 |
| `handdrawn-note` | 避坑、步骤教程、保号经验 | 手绘箭头、便签、圈注、猫咪提示牌 |
| `xiaohongshu-vertical` | 小红书发布版、4 图看完 | 9:16 竖版、大字少字、强钩子、系列化版式 |
示例确认话术:
```text
我建议用 A因为这篇是活动攻略读者最需要快速看懂门槛、封顶和翻车点。
A. warm-fintech-guide推荐白底暖橙、猫咪向导、金融教程感适合 MAOMOMO 常规文章。
B. data-card-dashboard更强调数字卡片和计算表适合门槛 / 返现 / 费用信息密集的文章。
C. handdrawn-note便签和手绘箭头更明显适合避坑经验和保姆式步骤。
你选哪个?也可以指定颜色、猫咪比例或是否更像小红书图。
```
## 常见配图类型 ## 常见配图类型
| 类型 | 用途 | | 类型 | 用途 |
@ -29,10 +63,10 @@
生成图片前先规划清单: 生成图片前先规划清单:
```markdown ```markdown
| 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 依赖素材 | | 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 风格 | 依赖素材 |
|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| hong-kong-bank-account-01-cover.png | 封面 | 标题下方 | 港卡保号关键点 | MAOMOMO 港卡保号教程封面图 | 无 | | hong-kong-bank-account-01-cover.png | 封面 | 标题下方 | 港卡保号关键点 | MAOMOMO 港卡保号教程封面图 | warm-fintech-guide | 无 |
| hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | 用户截图 1 | | hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | handdrawn-note | 用户截图 1 |
``` ```
确认用户不需要先看清单时可以直接执行但最终必须让文件名、alt text 和插图位置与正文一致。 确认用户不需要先看清单时可以直接执行但最终必须让文件名、alt text 和插图位置与正文一致。
@ -117,6 +151,7 @@ MAOMOMO 风格中文金融教程信息图,干净明亮白色背景,暖橙色
- 每张图有 MAOMOMO 标识或猫咪水印。 - 每张图有 MAOMOMO 标识或猫咪水印。
- 手机端可读,大字少字。 - 手机端可读,大字少字。
- 默认结构:封面、核心拆解、操作流程、避坑总结。 - 默认结构:封面、核心拆解、操作流程、避坑总结。
- 风格固定为 `xiaohongshu-vertical`,不直接复用横版文章图。
## 图片 QA ## 图片 QA

View File

@ -0,0 +1,373 @@
#!/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": (
"MAOMOMO 暖橙金融教程风:明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,"
"适合港卡、银行活动、返现攻略和教程总览。"
),
"clean-editorial": (
"清爽编辑部风:大标题、留白充足、少量暖橙和黑灰文字,像一张信息密度适中的中文攻略头图。"
),
"data-card-dashboard": (
"数据卡片仪表盘风:指标卡、时间线、计算公式和对比表清晰分区,适合返现、费用、门槛和路径对比。"
),
"handdrawn-note": (
"手绘便签风:白底纸感、手绘箭头、便利贴、重点圈注和轻量猫咪贴纸,适合避坑经验和保姆式步骤。"
),
"xiaohongshu-vertical": (
"小红书竖版攻略风9:16 手机端可读大字少字强标题钩子4 图系列一致视觉,带 MAOMOMO 标识。"
),
}
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 load_codex_config_base_url() -> Optional[str]:
config_path = Path.home() / ".codex" / "config.toml"
if not config_path.exists():
return None
try:
import tomllib
except ModuleNotFoundError:
return None
try:
data = tomllib.loads(read_text(config_path))
except Exception:
return None
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,
extra: str = "",
) -> str:
parts = [
"生成一张 MAOMOMO 中文金融实操文章配图。",
f"图片类型:{image_type}",
f"标题 / 主题:{title}",
f"核心文案:{core_text}",
f"画幅:{aspect_ratio}",
f"视觉风格:{style_prompt(style)}",
"硬性要求:包含清晰可见的 MAOMOMO 标识;中文文字清楚可读;信息层级明确;不要伪造真实 App 截图;不要使用未经提供的银行、券商、支付机构或卡组织 Logo不要出现真实个人信息猫咪不能承载第三方品牌 Logo。",
]
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=180) 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=180) 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 = read_text(Path(args.prompt_file)).strip()
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,
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)
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,
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("--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())