Compare commits

...

16 Commits

Author SHA1 Message Date
Jim Liu 宝玉 013c72fce7 chore: release v1.24.3 2026-01-28 13:56:48 -06:00
Jim Liu 宝玉 40fadcb1f6 docs: emphasize updating prompt files before regenerating 2026-01-28 13:56:35 -06:00
Jim Liu 宝玉 499a1ee478 chore: release v1.24.2 2026-01-28 13:14:44 -06:00
Jim Liu 宝玉 fa89eaf2f7 refactor(baoyu-image-gen): default to sequential generation 2026-01-28 13:14:39 -06:00
Jim Liu 宝玉 7964e20bc3 chore: release v1.24.1 2026-01-28 12:29:43 -06:00
Jim Liu 宝玉 45c109ce88 Merge pull request #27 from JianJang2017/my-baoyu-skills
Adapting baoyu-image-gen to the Tongyi Text-to-Image Model
2026-01-28 12:23:34 -06:00
jianzhang50 74c08def06 docs: add Aliyun text-to-image model config to README.md 2026-01-28 22:04:53 +08:00
jianzhang50 907c8ab852 Adapting baoyu-image-gen to the Tongyi Text-to-Image Model 2026-01-28 09:04:57 +08:00
Jim Liu 宝玉 64945f3341 chore: release v1.24.0 2026-01-27 10:10:44 -06:00
Jim Liu 宝玉 050d37c344 docs: backfill third-party contributor attributions in changelogs 2026-01-27 10:06:22 -06:00
Jim Liu 宝玉 f9f168fc1f docs(release-skills): add third-party contributor attribution to release workflow 2026-01-27 10:06:15 -06:00
Jim Liu 宝玉 b1af3a3e45 fix(baoyu-post-to-wechat): improve title extraction, summary auto-fill, and content verification 2026-01-27 10:06:11 -06:00
Jim Liu 宝玉 0727296592 Merge pull request #23 from AliceLJY/feat/reuse-existing-chrome
feat(baoyu-post-to-wechat): reuse existing Chrome instead of requiring all windows closed
2026-01-27 08:03:46 -06:00
Jim Liu 宝玉 0e571b72fb chore: release v1.23.1 2026-01-27 00:59:34 -06:00
Jim Liu 宝玉 366630f8c3 fix(baoyu-compress-image): rename original as backup instead of deleting 2026-01-27 00:59:30 -06:00
安闲静雅 6bfafe0ec5 feat(baoyu-post-to-wechat): reuse existing Chrome instead of requiring all windows closed
Previously, the script always launched a new Chrome instance, which failed
when Chrome was already running due to profile lock conflicts on macOS.
Users had to close all Chrome windows before publishing.

This change adds auto-detection of existing Chrome debug ports and reuses
an already-logged-in WeChat tab when available, falling back to launching
a new instance only when no existing Chrome is found.

Changes:
- cdp.ts: add tryConnectExisting() and findExistingChromeDebugPort()
- wechat-article.ts: try existing Chrome before launching new one,
  reuse logged-in WeChat tab (identified by token= in URL),
  add waitForElement() for reliable menu detection,
  add --cdp-port option for manual override,
  fix process not exiting after completion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:16:33 +08:00
20 changed files with 514 additions and 59 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.23.0"
"version": "1.24.3"
},
"plugins": [
{
+18 -5
View File
@@ -114,11 +114,17 @@ Display version change: `1.2.3 → 1.3.0`
For each detected changelog file:
1. **Identify language** from filename suffix
2. **Generate content in that language**:
2. **Detect third-party contributors**:
- Check merge commits: `git log ${LAST_TAG}..HEAD --merges --pretty=format:"%H %s"`
- For each merged PR, identify the PR author via `gh pr view <number> --json author --jq '.author.login'`
- Compare against repo owner (`gh repo view --json owner --jq '.owner.login'`)
- If PR author ≠ repo owner → third-party contributor
3. **Generate content in that language**:
- Section titles in target language
- Change descriptions written naturally in target language (not translated)
- Date format: YYYY-MM-DD (universal)
3. **Insert at file head** (preserve existing content)
- **Third-party contributions**: Append contributor attribution `(by @username)` to the changelog entry
4. **Insert at file head** (preserve existing content)
**Section Title Translations** (built-in):
@@ -138,6 +144,7 @@ For each detected changelog file:
### Features
- Description of new feature
- Description of third-party contribution (by @username)
### Fixes
- Description of fix
@@ -148,6 +155,12 @@ For each detected changelog file:
Only include sections that have changes. Omit empty sections.
**Third-Party Attribution Rules**:
- Only add `(by @username)` for contributors who are NOT the repo owner
- Use GitHub username with `@` prefix
- Place at the end of the changelog entry line
- Apply to all languages consistently (always use `(by @username)` format, not translated)
**Multi-language Example**:
English (CHANGELOG.md):
@@ -155,7 +168,7 @@ English (CHANGELOG.md):
## 1.3.0 - 2026-01-22
### Features
- Add user authentication module
- Add user authentication module (by @contributor1)
- Support OAuth2 login
### Fixes
@@ -167,7 +180,7 @@ Chinese (CHANGELOG.zh.md):
## 1.3.0 - 2026-01-22
### 新功能
- 新增用户认证模块
- 新增用户认证模块 (by @contributor1)
- 支持 OAuth2 登录
### 修复
@@ -179,7 +192,7 @@ Japanese (CHANGELOG.ja.md):
## 1.3.0 - 2026-01-22
### 新機能
- ユーザー認証モジュールを追加
- ユーザー認証モジュールを追加 (by @contributor1)
- OAuth2 ログインをサポート
### 修正
+5
View File
@@ -152,3 +152,8 @@ slide-deck/
infographic/
illustrations/
comic/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
+42 -4
View File
@@ -2,6 +2,41 @@
English | [中文](./CHANGELOG.zh.md)
## 1.24.3 - 2026-01-28
### Documentation
- Emphasize updating prompt files before regenerating images in modification workflows (article-illustrator, slide-deck, xhs-images, cover-image, comic).
## 1.24.2 - 2026-01-28
### Refactor
- `baoyu-image-gen`: default to sequential generation; parallel available on request.
## 1.24.1 - 2026-01-28
### Features
- `baoyu-image-gen`: add Aliyun Tongyi Wanxiang (DashScope) text-to-image model support (by @JianJang2017).
### Documentation
- Add Aliyun text-to-image model configuration to README.
## 1.24.0 - 2026-01-27
### Features
- `baoyu-post-to-wechat`: reuse existing Chrome browser instead of requiring all windows closed (by @AliceLJY).
### Fixes
- `baoyu-post-to-wechat`: improves title extraction to support h1/h2 headings; adds summary auto-fill and content verification after paste/type; supports flexible HTML meta tag attribute ordering.
### Documentation
- `release-skills`: adds third-party contributor attribution rules to changelog workflow.
- Backfills missing third-party contributor attributions across historical changelog entries.
## 1.23.1 - 2026-01-27
### Fixes
- `baoyu-compress-image`: rename original file as `_original` backup instead of deleting after compression.
## 1.23.0 - 2026-01-26
### Refactor
@@ -20,7 +55,7 @@ English | [中文](./CHANGELOG.zh.md)
## 1.21.4 - 2026-01-25
### Fixes
- `baoyu-post-to-wechat`: adds Windows compatibility—uses `fileURLToPath` for correct path resolution, replaces system-dependent copy/paste tools (osascript/xdotool) with CDP keyboard events for cross-platform support.
- `baoyu-post-to-wechat`: adds Windows compatibility—uses `fileURLToPath` for correct path resolution, replaces system-dependent copy/paste tools (osascript/xdotool) with CDP keyboard events for cross-platform support (by @JadeLiang003).
- `baoyu-post-to-wechat`: fixes regressions from Windows compatibility PR—corrects broken `-fixed` filename references, restores frontmatter quote stripping, restores `--title` CLI parameter, fixes summary extraction to skip headings/quotes/lists, fixes argument parsing for single-dash flags, removes debug logs.
- `baoyu-article-illustrator`, `baoyu-cover-image`, `baoyu-xhs-images`: removes opacity option from watermark configuration.
@@ -170,7 +205,7 @@ English | [中文](./CHANGELOG.zh.md)
## 1.14.0 - 2026-01-22
### Fixes
- `baoyu-post-to-x`: improves video ready detection for more reliable video posting.
- `baoyu-post-to-x`: improves video ready detection for more reliable video posting (by @fkysly).
### Documentation
- `baoyu-slide-deck`: comprehensive SKILL.md enhancement—adds slide count guidance (recommended 8-25, max 30), audience guidelines table with audience-specific principles, style selection principles with content-type recommendations, layout selection tips with common mistakes to avoid, visual hierarchy principles, content density guidelines (McKinsey-style high-density principles), color selection guide, typography principles with font recommendations (English and Chinese fonts with multilingual pairing), and visual elements reference (backgrounds, typography treatments, geometric accents).
@@ -185,6 +220,9 @@ English | [中文](./CHANGELOG.zh.md)
## 1.12.0 - 2026-01-21
### Features
- `baoyu-post-to-x`: adds quote tweet support (by @threehotpot-bot).
### Refactor
- `baoyu-post-to-x`: extracts shared utilities to `x-utils.ts`—consolidates Chrome detection, CDP connection, clipboard operations, and helper functions from `x-article.ts`, `x-browser.ts`, `x-quote.ts`, and `x-video.ts` into a single reusable module.
@@ -200,7 +238,7 @@ English | [中文](./CHANGELOG.zh.md)
## 1.10.0 - 2026-01-21
### Features
- `baoyu-post-to-x`: adds video posting support—new `x-video.ts` script for posting text with video files (MP4, MOV, WebM). Supports preview mode and handles video processing timeouts.
- `baoyu-post-to-x`: adds video posting support—new `x-video.ts` script for posting text with video files (MP4, MOV, WebM). Supports preview mode and handles video processing timeouts (by @fkysly).
## 1.9.0 - 2026-01-20
@@ -256,7 +294,7 @@ English | [中文](./CHANGELOG.zh.md)
## 1.4.1 - 2026-01-18
### Fixes
- `baoyu-post-to-x`: supports multi-language UI selectors for X Articles (contributed by [@ianchenx](https://github.com/ianchenx)).
- `baoyu-post-to-x`: supports multi-language UI selectors for X Articles (by @ianchenx).
## 1.4.0 - 2026-01-18
+42 -4
View File
@@ -2,6 +2,41 @@
[English](./CHANGELOG.md) | 中文
## 1.24.3 - 2026-01-28
### 文档
- 在修改工作流中强调先更新提示词文件再生成图片(article-illustrator、slide-deck、xhs-images、cover-image、comic)。
## 1.24.2 - 2026-01-28
### 重构
- `baoyu-image-gen`:默认改为顺序生成图片;并行生成需明确请求。
## 1.24.1 - 2026-01-28
### 新功能
- `baoyu-image-gen`:新增阿里云通义万象(DashScope)文生图模型支持 (by @JianJang2017)。
### 文档
- README 中新增阿里云文生图模型配置说明。
## 1.24.0 - 2026-01-27
### 新功能
- `baoyu-post-to-wechat`:复用已打开的 Chrome 浏览器,无需关闭所有窗口 (by @AliceLJY)。
### 修复
- `baoyu-post-to-wechat`:改进标题提取,支持 h1/h2 标题;新增摘要自动填充和粘贴/输入后内容验证;支持 HTML meta 标签属性顺序灵活匹配。
### 文档
- `release-skills`:在发布流程中新增第三方贡献者署名规则。
- 补全历史 changelog 中缺失的第三方贡献者署名。
## 1.23.1 - 2026-01-27
### 修复
- `baoyu-compress-image`:压缩后将原始文件重命名为 `_original` 备份,不再删除。
## 1.23.0 - 2026-01-26
### 重构
@@ -20,7 +55,7 @@
## 1.21.4 - 2026-01-25
### 修复
- `baoyu-post-to-wechat`:新增 Windows 兼容性——使用 `fileURLToPath` 正确解析路径,将系统依赖的复制粘贴工具(osascript/xdotool)替换为 CDP 键盘事件,实现跨平台支持。
- `baoyu-post-to-wechat`:新增 Windows 兼容性——使用 `fileURLToPath` 正确解析路径,将系统依赖的复制粘贴工具(osascript/xdotool)替换为 CDP 键盘事件,实现跨平台支持 (by @JadeLiang003)
- `baoyu-post-to-wechat`:修复 Windows 兼容性 PR 引入的回退问题——修正错误的 `-fixed` 文件名引用、恢复 frontmatter 引号剥离、恢复 `--title` CLI 参数、修复摘要提取逻辑以正确跳过标题/引用/列表、修复单横线参数解析、移除调试日志。
- `baoyu-article-illustrator``baoyu-cover-image``baoyu-xhs-images`:移除水印配置中的透明度选项。
@@ -170,7 +205,7 @@
## 1.14.0 - 2026-01-22
### 修复
- `baoyu-post-to-x`:改进视频就绪检测,提升视频发布稳定性。
- `baoyu-post-to-x`:改进视频就绪检测,提升视频发布稳定性 (by @fkysly)
### 文档
- `baoyu-slide-deck`:SKILL.md 全面增强——新增幻灯片数量指南(推荐 8-25 张,最多 30 张)、受众指南表格及各受众特定原则、风格选择原则与内容类型推荐、布局选择技巧与常见错误提示、视觉层次原则、内容密度指南(麦肯锡风格高密度原则)、配色选择指南、字体排版原则与字体推荐(中英文字体及多语言搭配方案)、视觉元素参考(背景处理、字体处理、几何装饰)。
@@ -185,6 +220,9 @@
## 1.12.0 - 2026-01-21
### 新功能
- `baoyu-post-to-x`:新增引用推文(Quote Tweet)支持 (by @threehotpot-bot)。
### 重构
- `baoyu-post-to-x`:提取公共工具函数到 `x-utils.ts`——将 `x-article.ts``x-browser.ts``x-quote.ts``x-video.ts` 中重复的 Chrome 检测、CDP 连接、剪贴板操作等功能整合为统一的可复用模块。
@@ -200,7 +238,7 @@
## 1.10.0 - 2026-01-21
### 新功能
- `baoyu-post-to-x`:新增视频发布支持——新增 `x-video.ts` 脚本,支持发布带视频的推文(MP4、MOV、WebM 格式)。支持预览模式,自动处理视频上传等待。
- `baoyu-post-to-x`:新增视频发布支持——新增 `x-video.ts` 脚本,支持发布带视频的推文(MP4、MOV、WebM 格式)。支持预览模式,自动处理视频上传等待 (by @fkysly)
## 1.9.0 - 2026-01-20
@@ -256,7 +294,7 @@
## 1.4.1 - 2026-01-18
### 修复
- `baoyu-post-to-x`:支持 X Articles 多语言 UI 选择器(感谢 [@ianchenx](https://github.com/ianchenx) 贡献)
- `baoyu-post-to-x`:支持 X Articles 多语言 UI 选择器 (by @ianchenx)
## 1.4.0 - 2026-01-18
+14 -3
View File
@@ -543,7 +543,7 @@ AI-powered generation backends.
#### baoyu-image-gen
AI SDK-based image generation using official OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyun Tongyi Wanxiang) APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
```bash
# Basic generation (auto-detect provider)
@@ -558,6 +558,9 @@ AI SDK-based image generation using official OpenAI and Google APIs. Supports te
# Specific provider
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
# DashScope (Aliyun Tongyi Wanxiang)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
# With reference images (Google multimodal only)
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
```
@@ -568,7 +571,7 @@ AI SDK-based image generation using official OpenAI and Google APIs. Supports te
| `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) |
| `--image` | Output image path (required) |
| `--provider` | `google` or `openai` (default: google) |
| `--provider` | `google`, `openai` or `dashscope` (default: google) |
| `--model`, `-m` | Model ID |
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size` | Size (e.g., `1024x1024`) |
@@ -580,15 +583,18 @@ AI SDK-based image generation using official OpenAI and Google APIs. Supports te
|----------|-------------|---------|
| `OPENAI_API_KEY` | OpenAI API key | - |
| `GOOGLE_API_KEY` | Google API key | - |
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `z-image-turbo` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
**Provider Auto-Selection**:
1. If `--provider` specified → use it
2. If only one API key available → use that provider
3. If both available → default to Google
3. If multiple available → default to Google
#### baoyu-danger-gemini-web
@@ -699,6 +705,11 @@ OPENAI_IMAGE_MODEL=gpt-image-1.5
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta
# DashScope (Aliyun Tongyi Wanxiang)
DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=z-image-turbo
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
EOF
```
+14 -3
View File
@@ -543,7 +543,7 @@ AI 驱动的生成后端。
#### baoyu-image-gen
基于 AI SDK 的图像生成,使用官方 OpenAIGoogle API。支持文生图、参考图、宽高比和质量预设。
基于 AI SDK 的图像生成,使用官方 OpenAIGoogle 和 DashScope(阿里通义万相)API。支持文生图、参考图、宽高比和质量预设。
```bash
# 基础生成(自动检测服务商)
@@ -558,6 +558,9 @@ AI 驱动的生成后端。
# 指定服务商
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
# DashScope(阿里通义万相)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
# 带参考图(仅 Google 多模态支持)
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
```
@@ -568,7 +571,7 @@ AI 驱动的生成后端。
| `--prompt`, `-p` | 提示词文本 |
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
| `--image` | 输出图片路径(必需) |
| `--provider` | `google``openai`(默认:google |
| `--provider` | `google``openai``dashscope`(默认:google |
| `--model`, `-m` | 模型 ID |
| `--ar` | 宽高比(如 `16:9``1:1``4:3` |
| `--size` | 尺寸(如 `1024x1024` |
@@ -580,15 +583,18 @@ AI 驱动的生成后端。
|------|------|--------|
| `OPENAI_API_KEY` | OpenAI API 密钥 | - |
| `GOOGLE_API_KEY` | Google API 密钥 | - |
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `z-image-turbo` |
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
**服务商自动选择**
1. 如果指定了 `--provider` → 使用指定的
2. 如果只有一个 API 密钥 → 使用对应服务商
3. 如果两个都有 → 默认使用 Google
3. 如果多个可用 → 默认使用 Google
#### baoyu-danger-gemini-web
@@ -699,6 +705,11 @@ OPENAI_IMAGE_MODEL=gpt-image-1.5
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta
# DashScope(阿里通义万相)
DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=z-image-turbo
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
EOF
```
+6 -1
View File
@@ -303,10 +303,15 @@ illustrations/{topic-slug}/
| Action | Steps |
|--------|-------|
| **Edit** | Update prompt → Regenerate → Update reference |
| **Edit** | **Update prompt file FIRST** → Regenerate → Update reference |
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
| **Delete** | Delete files → Remove reference → Update outline |
**IMPORTANT**: When updating illustrations, ALWAYS update the prompt file (`prompts/illustration-{slug}.md`) FIRST before regenerating. This ensures:
1. Changes are documented and reproducible
2. The prompt reflects the new requirements
3. Future regeneration uses the correct prompt
## References
| File | Content |
+10
View File
@@ -259,6 +259,16 @@ Schema: [references/config/preferences-schema.md](references/config/preferences-
- [config/first-time-setup.md](references/config/first-time-setup.md) - First-time setup
- [config/watermark-guide.md](references/config/watermark-guide.md) - Watermark configuration
## Page Modification
| Action | Steps |
|--------|-------|
| **Edit** | **Update prompt file FIRST**`--regenerate N` → Regenerate PDF |
| **Add** | Create prompt at position → Generate with character ref → Renumber subsequent → Update storyboard → Regenerate PDF |
| **Delete** | Remove files → Renumber subsequent → Update storyboard → Regenerate PDF |
**IMPORTANT**: When updating pages, ALWAYS update the prompt file (`prompts/NN-{cover|page}-[slug].md`) FIRST before regenerating. This ensures changes are documented and reproducible.
## Notes
- Image generation: 10-30 seconds per page
+3 -1
View File
@@ -147,7 +147,9 @@ async function processFile(
const outputSize = statSync(tempOutput).size;
if (!opts.keep && absInput !== output) {
unlinkSync(absInput);
const ext = extname(absInput);
const base = absInput.slice(0, -ext.length);
renameSync(absInput, `${base}_original${ext}`);
}
renameSync(tempOutput, output);
+4 -2
View File
@@ -258,8 +258,10 @@ Files:
| Action | Steps |
|--------|-------|
| **Regenerate** | Backup existing → Update prompt → Regenerate with same settings |
| **Change dimension** | Backup existing → Confirm new value → Update prompt → Regenerate |
| **Regenerate** | Backup existing → **Update prompt file FIRST** → Regenerate with same settings |
| **Change dimension** | Backup existing → Confirm new value → **Update prompt file FIRST** → Regenerate |
**IMPORTANT**: When regenerating, ALWAYS update the prompt file (`prompts/cover.md`) FIRST before regenerating. This ensures changes are documented and reproducible.
All modifications automatically backup existing `cover.png` before regenerating.
+23 -10
View File
@@ -1,11 +1,11 @@
---
name: baoyu-image-gen
description: AI image generation with OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and parallel generation (recommended 4 concurrent subagents). Use when user asks to generate, create, or draw images.
description: AI image generation with OpenAI, Google and DashScope APIs. Supports text-to-image, reference images, aspect ratios. Sequential by default; parallel generation available on request. Use when user asks to generate, create, or draw images.
---
# Image Generation (AI SDK)
Official API-based image generation. Supports OpenAI and Google providers.
Official API-based image generation. Supports OpenAI, Google and DashScope (阿里通义万象) providers.
## Script Directory
@@ -63,6 +63,9 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --r
# Specific provider
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
# DashScope (阿里通义万象)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image out.png --provider dashscope
```
## Options
@@ -72,7 +75,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provi
| `--prompt <text>`, `-p` | Prompt text |
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
| `--image <path>` | Output image path (required) |
| `--provider google\|openai` | Force provider (default: google) |
| `--provider google\|openai\|dashscope` | Force provider (default: google) |
| `--model <id>`, `-m` | Model ID |
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size <WxH>` | Size (e.g., `1024x1024`) |
@@ -88,10 +91,13 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provi
|----------|-------------|
| `OPENAI_API_KEY` | OpenAI API key |
| `GOOGLE_API_KEY` | Google API key |
| `DASHSCOPE_API_KEY` | DashScope API key (阿里云) |
| `OPENAI_IMAGE_MODEL` | OpenAI model override |
| `GOOGLE_IMAGE_MODEL` | Google model override |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model override (default: z-image-turbo) |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint |
| `GOOGLE_BASE_URL` | Custom Google endpoint |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
**Load Priority**: CLI args > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
@@ -99,7 +105,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provi
1. `--provider` specified → use it
2. Only one API key available → use that provider
3. Both available → default to Google
3. Multiple available → default to Google
## Quality Presets
@@ -118,25 +124,32 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
- Google Imagen: uses `aspectRatio` parameter
- OpenAI: maps to closest supported size
## Parallel Generation
## Generation Mode
Supports concurrent image generation via background subagents for batch operations.
**Default**: Sequential generation (one image at a time). This ensures stable output and easier debugging.
**Parallel Generation**: Only use when user explicitly requests parallel/concurrent generation.
| Mode | When to Use |
|------|-------------|
| Sequential (default) | Normal usage, single images, small batches |
| Parallel | User explicitly requests, large batches (10+) |
**Parallel Settings** (when requested):
| Setting | Value |
|---------|-------|
| Recommended concurrency | 4 subagents |
| Max concurrency | 8 subagents |
| Use case | Batch generation (slides, comics, infographics) |
| Use case | Large batch generation when user requests parallel |
**Agent Implementation**:
**Agent Implementation** (parallel mode only):
```
# Launch multiple generations in parallel using Task tool
# Each Task runs as background subagent with run_in_background=true
# Collect results via TaskOutput when all complete
```
**Best Practice**: When generating 4+ images, spawn background subagents (recommended 4 concurrent) instead of sequential execution.
## Error Handling
- Missing API key → error with setup instructions
+14 -6
View File
@@ -14,7 +14,7 @@ Options:
-p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from files (concatenated)
--image <path> Output image path (required)
--provider google|openai Force provider (auto-detect by default)
--provider google|openai|dashscope Force provider (auto-detect by default)
-m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024)
@@ -29,10 +29,13 @@ Environment variables:
OPENAI_API_KEY OpenAI API key
GOOGLE_API_KEY Google API key
GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY)
DASHSCOPE_API_KEY DashScope API key ()
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
DASHSCOPE_IMAGE_MODEL Default DashScope model (z-image-turbo)
OPENAI_BASE_URL Custom OpenAI endpoint
GOOGLE_BASE_URL Custom Google endpoint
DASHSCOPE_BASE_URL Custom DashScope endpoint
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
}
@@ -105,7 +108,7 @@ function parseArgs(argv: string[]): CliArgs {
if (a === "--provider") {
const v = argv[++i];
if (v !== "google" && v !== "openai") throw new Error(`Invalid provider: ${v}`);
if (v !== "google" && v !== "openai" && v !== "dashscope") throw new Error(`Invalid provider: ${v}`);
out.provider = v;
continue;
}
@@ -243,13 +246,15 @@ function detectProvider(args: CliArgs): Provider {
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
const hasOpenai = !!process.env.OPENAI_API_KEY;
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
if (hasGoogle && !hasOpenai) return "google";
if (hasOpenai && !hasGoogle) return "openai";
if (hasGoogle && hasOpenai) return "google";
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope"].filter(Boolean) as Provider[];
if (available.length === 1) return available[0]!;
if (available.length > 1) return available[0]!;
throw new Error(
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY.\n" +
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, or DASHSCOPE_API_KEY.\n" +
"Create ~/.baoyu-skills/.env or <cwd>/.baoyu-skills/.env with your keys."
);
}
@@ -263,6 +268,9 @@ async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
if (provider === "google") {
return (await import("./providers/google")) as ProviderModule;
}
if (provider === "dashscope") {
return (await import("./providers/dashscope")) as ProviderModule;
}
return (await import("./providers/openai")) as ProviderModule;
}
@@ -0,0 +1,137 @@
import type { CliArgs } from "../types";
export function getDefaultModel(): string {
return process.env.DASHSCOPE_IMAGE_MODEL || "z-image-turbo";
}
function getApiKey(): string | null {
return process.env.DASHSCOPE_API_KEY || null;
}
function getBaseUrl(): string {
const base = process.env.DASHSCOPE_BASE_URL || "https://dashscope.aliyuncs.com";
return base.replace(/\/+$/g, "");
}
function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
const w = parseFloat(match[1]!);
const h = parseFloat(match[2]!);
if (w <= 0 || h <= 0) return null;
return { width: w, height: h };
}
function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
const baseSize = quality === "2k" ? 1440 : 1024;
if (!ar) return `${baseSize}*${baseSize}`;
const parsed = parseAspectRatio(ar);
if (!parsed) return `${baseSize}*${baseSize}`;
const ratio = parsed.width / parsed.height;
if (Math.abs(ratio - 1) < 0.1) {
return `${baseSize}*${baseSize}`;
}
if (ratio > 1) {
const w = Math.round(baseSize * ratio);
return `${w}*${baseSize}`;
}
const h = Math.round(baseSize / ratio);
return `${baseSize}*${h}`;
}
function normalizeSize(size: string): string {
return size.replace("x", "*");
}
export async function generateImage(
prompt: string,
model: string,
args: CliArgs
): Promise<Uint8Array> {
const apiKey = getApiKey();
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
if (args.referenceImages.length > 0) {
console.error("Warning: Reference images not yet supported with DashScope, ignoring.");
}
const size = args.size ? normalizeSize(args.size) : getSizeFromAspectRatio(args.aspectRatio, args.quality);
const url = `${getBaseUrl()}/api/v1/services/aigc/multimodal-generation/generation`;
const body = {
model,
input: {
messages: [
{
role: "user",
content: [{ text: prompt }],
},
],
},
parameters: {
prompt_extend: false,
size,
},
};
console.log(`Generating image with DashScope (${model})...`, { size });
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`DashScope API error (${res.status}): ${err}`);
}
const result = await res.json() as {
output?: {
result_image?: string;
choices?: Array<{
message?: {
content?: Array<{ image?: string }>;
};
}>;
};
};
let imageData: string | null = null;
if (result.output?.result_image) {
imageData = result.output.result_image;
} else if (result.output?.choices?.[0]?.message?.content) {
const content = result.output.choices[0].message.content;
for (const item of content) {
if (item.image) {
imageData = item.image;
break;
}
}
}
if (!imageData) {
console.error("Response:", JSON.stringify(result, null, 2));
throw new Error("No image in response");
}
if (imageData.startsWith("http://") || imageData.startsWith("https://")) {
const imgRes = await fetch(imageData);
if (!imgRes.ok) throw new Error("Failed to download image");
const buf = await imgRes.arrayBuffer();
return new Uint8Array(buf);
}
return Uint8Array.from(Buffer.from(imageData, "base64"));
}
+1 -1
View File
@@ -1,4 +1,4 @@
export type Provider = "google" | "openai";
export type Provider = "google" | "openai" | "dashscope";
export type Quality = "normal" | "2k";
export type CliArgs = {
@@ -171,6 +171,37 @@ export interface ChromeSession {
targetId: string;
}
export async function tryConnectExisting(port: number): Promise<CdpConnection | null> {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`);
if (version.webSocketDebuggerUrl) {
const cdp = await CdpConnection.connect(version.webSocketDebuggerUrl, 5_000);
return cdp;
}
} catch {}
return null;
}
export async function findExistingChromeDebugPort(): Promise<number | null> {
if (process.platform !== 'darwin' && process.platform !== 'linux') return null;
try {
const { execSync } = await import('node:child_process');
const cmd = process.platform === 'darwin'
? `lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -i 'google\\|chrome' | awk '{print $9}' | sed 's/.*://'`
: `ss -tlnp 2>/dev/null | grep -i chrome | awk '{print $4}' | sed 's/.*://'`;
const output = execSync(cmd, { encoding: 'utf-8', timeout: 5_000 }).trim();
if (!output) return null;
const ports = output.split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p) && p > 0);
for (const port of ports) {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`);
if (version.webSocketDebuggerUrl) return port;
} catch {}
}
} catch {}
return null;
}
export async function launchChrome(url: string, profileDir?: string): Promise<{ cdp: CdpConnection; chrome: ReturnType<typeof spawn> }> {
const chromePath = findChromeExecutable();
if (!chromePath) throw new Error('Chrome not found. Set WECHAT_BROWSER_CHROME_PATH env var.');
@@ -121,8 +121,14 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
let title = options?.title ?? frontmatter.title ?? '';
if (!title) {
const h1Match = body.match(/^#\s+(.+)$/m);
if (h1Match) title = h1Match[1]!;
const lines = body.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const headingMatch = trimmed.match(/^#{1,2}\s+(.+)$/);
if (headingMatch) title = headingMatch[1]!;
break;
}
}
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
const author = frontmatter.author || '';
@@ -3,7 +3,7 @@ import path from 'node:path';
import { spawnSync } from 'node:child_process';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { launchChrome, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts';
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts';
const WECHAT_URL = 'https://mp.weixin.qq.com/';
@@ -25,6 +25,7 @@ interface ArticleOptions {
contentImages?: ImageInfo[];
submit?: boolean;
profileDir?: string;
cdpPort?: number;
}
async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promise<boolean> {
@@ -37,6 +38,16 @@ async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promis
return false;
}
async function waitForElement(session: ChromeSession, selector: string, timeoutMs = 10_000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const found = await evaluate<boolean>(session, `!!document.querySelector('${selector}')`);
if (found) return true;
await sleep(500);
}
return false;
}
async function clickMenuByText(session: ChromeSession, text: string): Promise<void> {
console.log(`[wechat] Clicking "${text}" menu...`);
const posResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
@@ -173,11 +184,13 @@ function parseHtmlMeta(htmlPath: string): { title: string; author: string; summa
if (titleMatch) title = titleMatch[1]!;
let author = '';
const authorMatch = content.match(/<meta\s+name=["']author["']\s+content=["']([^"']+)["']/i);
const authorMatch = content.match(/<meta\s+name=["']author["']\s+content=["']([^"']+)["']/i)
|| content.match(/<meta\s+content=["']([^"']+)["']\s+name=["']author["']/i);
if (authorMatch) author = authorMatch[1]!;
let summary = '';
const descMatch = content.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i);
const descMatch = content.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i)
|| content.match(/<meta\s+content=["']([^"']+)["']\s+name=["']description["']/i);
if (descMatch) summary = descMatch[1]!;
if (!summary) {
@@ -234,7 +247,7 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
}
export async function postArticle(options: ArticleOptions): Promise<void> {
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir } = options;
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
let { contentImages = [] } = options;
let effectiveTitle = title || '';
let effectiveAuthor = author || '';
@@ -268,16 +281,67 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`);
if (!content && !effectiveHtmlFile) throw new Error('Either --content, --html, or --markdown is required');
const { cdp, chrome } = await launchChrome(WECHAT_URL, profileDir);
let cdp: CdpConnection;
let chrome: ReturnType<typeof import('node:child_process').spawn> | null = null;
// Try connecting to existing Chrome: explicit port > auto-detect > launch new
const portToTry = cdpPort ?? await findExistingChromeDebugPort();
if (portToTry) {
const existing = await tryConnectExisting(portToTry);
if (existing) {
console.log(`[cdp] Connected to existing Chrome on port ${portToTry}`);
cdp = existing;
} else {
console.log(`[cdp] Port ${portToTry} not available, launching new Chrome...`);
const launched = await launchChrome(WECHAT_URL, profileDir);
cdp = launched.cdp;
chrome = launched.chrome;
}
} else {
const launched = await launchChrome(WECHAT_URL, profileDir);
cdp = launched.cdp;
chrome = launched.chrome;
}
try {
console.log('[wechat] Waiting for page load...');
await sleep(3000);
let session = await getPageSession(cdp, 'mp.weixin.qq.com');
let session: ChromeSession;
if (!chrome) {
// Reusing existing Chrome: find an already-logged-in tab (has token in URL)
const allTargets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
const loggedInTab = allTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('mp.weixin.qq.com') && t.url.includes('token='));
const wechatTab = loggedInTab || allTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('mp.weixin.qq.com'));
if (wechatTab) {
console.log(`[wechat] Reusing existing tab: ${wechatTab.url.substring(0, 80)}...`);
const { sessionId: reuseSid } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId: wechatTab.targetId, flatten: true });
await cdp.send('Page.enable', {}, { sessionId: reuseSid });
await cdp.send('Runtime.enable', {}, { sessionId: reuseSid });
await cdp.send('DOM.enable', {}, { sessionId: reuseSid });
session = { cdp, sessionId: reuseSid, targetId: wechatTab.targetId };
// Navigate to home if not already there
const currentUrl = await evaluate<string>(session, 'window.location.href');
if (!currentUrl.includes('/cgi-bin/home')) {
console.log('[wechat] Navigating to home...');
await evaluate(session, `window.location.href = '${WECHAT_URL}cgi-bin/home?t=home/index'`);
await sleep(5000);
}
} else {
// No WeChat tab found, create one
console.log('[wechat] No WeChat tab found, opening...');
await cdp.send('Target.createTarget', { url: WECHAT_URL });
await sleep(5000);
session = await getPageSession(cdp, 'mp.weixin.qq.com');
}
} else {
session = await getPageSession(cdp, 'mp.weixin.qq.com');
}
const url = await evaluate<string>(session, 'window.location.href');
if (!url.includes('/cgi-bin/home')) {
if (!url.includes('/cgi-bin/')) {
console.log('[wechat] Not logged in. Please scan QR code...');
const loggedIn = await waitForLogin(session);
if (!loggedIn) throw new Error('Login timeout');
@@ -285,6 +349,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log('[wechat] Logged in.');
await sleep(2000);
// Wait for menu to be ready
const menuReady = await waitForElement(session, '.new-creation__menu', 20_000);
if (!menuReady) throw new Error('Home page menu did not load');
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
const initialIds = new Set(targets.targetInfos.map(t => t.targetId));
@@ -313,6 +381,31 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
}
if (effectiveSummary) {
console.log(`[wechat] Filling summary: ${effectiveSummary}`);
await evaluate(session, `document.querySelector('#js_description').value = ${JSON.stringify(effectiveSummary)}; document.querySelector('#js_description').dispatchEvent(new Event('input', { bubbles: true }));`);
}
await sleep(500);
if (effectiveTitle) {
const actualTitle = await evaluate<string>(session, `document.querySelector('#title')?.value || ''`);
if (actualTitle === effectiveTitle) {
console.log('[wechat] Title verified OK.');
} else {
console.warn(`[wechat] Title verification failed. Expected: "${effectiveTitle}", got: "${actualTitle}"`);
}
}
if (effectiveSummary) {
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
if (actualSummary === effectiveSummary) {
console.log('[wechat] Summary verified OK.');
} else {
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
}
}
console.log('[wechat] Clicking on editor...');
await clickElement(session, '.ProseMirror');
await sleep(1000);
@@ -329,6 +422,20 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await pasteFromClipboardInEditor(session);
await sleep(3000);
const editorHasContent = await evaluate<boolean>(session, `
(function() {
const editor = document.querySelector('.ProseMirror');
if (!editor) return false;
const text = editor.innerText?.trim() || '';
return text.length > 0;
})()
`);
if (editorHasContent) {
console.log('[wechat] Body content verified OK.');
} else {
console.warn('[wechat] Body content verification failed: editor appears empty after paste.');
}
if (contentImages.length > 0) {
console.log(`[wechat] Inserting ${contentImages.length} images...`);
for (let i = 0; i < contentImages.length; i++) {
@@ -371,11 +478,20 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log('[wechat] Typing content...');
await typeText(session, content);
await sleep(1000);
}
if (effectiveSummary) {
console.log(`[wechat] Filling summary: ${effectiveSummary}`);
await evaluate(session, `document.querySelector('#js_description').value = ${JSON.stringify(effectiveSummary)}; document.querySelector('#js_description').dispatchEvent(new Event('input', { bubbles: true }));`);
const editorHasContent = await evaluate<boolean>(session, `
(function() {
const editor = document.querySelector('.ProseMirror');
if (!editor) return false;
const text = editor.innerText?.trim() || '';
return text.length > 0;
})()
`);
if (editorHasContent) {
console.log('[wechat] Body content verified OK.');
} else {
console.warn('[wechat] Body content verification failed: editor appears empty after typing.');
}
}
console.log('[wechat] Saving as draft...');
@@ -413,6 +529,7 @@ Options:
--image <path> Content image, can repeat (only with --content)
--submit Save as draft
--profile <dir> Chrome profile directory
--cdp-port <port> Connect to existing Chrome debug port instead of launching new instance
Examples:
npx -y bun wechat-article.ts --markdown article.md
@@ -442,6 +559,7 @@ async function main(): Promise<void> {
let summary: string | undefined;
let submit = false;
let profileDir: string | undefined;
let cdpPort: number | undefined;
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
@@ -455,15 +573,18 @@ async function main(): Promise<void> {
else if (arg === '--image' && args[i + 1]) images.push(args[++i]!);
else if (arg === '--submit') submit = true;
else if (arg === '--profile' && args[i + 1]) profileDir = args[++i];
else if (arg === '--cdp-port' && args[i + 1]) cdpPort = parseInt(args[++i]!, 10);
}
if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); }
if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); }
await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, author, summary, images, submit, profileDir });
await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, author, summary, images, submit, profileDir, cdpPort });
}
await main().catch((err) => {
await main().then(() => {
process.exit(0);
}).catch((err) => {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});
+4 -2
View File
@@ -626,16 +626,18 @@ Flow:
| Action | Command | Manual Steps |
|--------|---------|--------------|
| **Edit** | `--regenerate N` | Update prompt → Regenerate image → Regenerate PDF |
| **Edit** | `--regenerate N` | **Update prompt file FIRST** → Regenerate image → Regenerate PDF |
| **Add** | Manual | Create prompt → Generate image → Renumber subsequent → Update outline → Regenerate PDF |
| **Delete** | Manual | Remove files → Renumber subsequent → Update outline → Regenerate PDF |
### Edit Single Slide
1. Update prompt in `prompts/NN-slide-{slug}.md`
1. **Update prompt file FIRST** in `prompts/NN-slide-{slug}.md`
2. Run: `/baoyu-slide-deck <dir> --regenerate N`
3. Or manually regenerate image + PDF
**IMPORTANT**: When updating slides, ALWAYS update the prompt file (`prompts/NN-slide-{slug}.md`) FIRST before regenerating. This ensures changes are documented and reproducible.
### Add New Slide
1. Create prompt at position: `prompts/NN-slide-{new-slug}.md`
+3 -1
View File
@@ -403,10 +403,12 @@ Files:
| Action | Steps |
|--------|-------|
| **Edit** | Update prompt → Regenerate with same session ID |
| **Edit** | **Update prompt file FIRST** → Regenerate with same session ID |
| **Add** | Specify position → Create prompt → Generate → Renumber subsequent files (NN+1) → Update outline |
| **Delete** | Remove files → Renumber subsequent (NN-1) → Update outline |
**IMPORTANT**: When updating images, ALWAYS update the prompt file (`prompts/NN-{type}-[slug].md`) FIRST before regenerating. This ensures changes are documented and reproducible.
## Content Breakdown Principles
1. **Cover (Image 1)**: Hook + visual impact → `sparse` layout