mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-10 05:01:40 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 013c72fce7 | |||
| 40fadcb1f6 | |||
| 499a1ee478 | |||
| fa89eaf2f7 | |||
| 7964e20bc3 | |||
| 45c109ce88 | |||
| 74c08def06 | |||
| 907c8ab852 | |||
| 64945f3341 | |||
| 050d37c344 | |||
| f9f168fc1f | |||
| b1af3a3e45 | |||
| 0727296592 | |||
| 0e571b72fb | |||
| 366630f8c3 | |||
| 6bfafe0ec5 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.23.0"
|
||||
"version": "1.24.3"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -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 ログインをサポート
|
||||
|
||||
### 修正
|
||||
|
||||
@@ -152,3 +152,8 @@ slide-deck/
|
||||
infographic/
|
||||
illustrations/
|
||||
comic/
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
+42
-4
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -543,7 +543,7 @@ AI 驱动的生成后端。
|
||||
|
||||
#### baoyu-image-gen
|
||||
|
||||
基于 AI SDK 的图像生成,使用官方 OpenAI 和 Google API。支持文生图、参考图、宽高比和质量预设。
|
||||
基于 AI SDK 的图像生成,使用官方 OpenAI、Google 和 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
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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,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);
|
||||
});
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user