Compare commits

...

10 Commits

Author SHA1 Message Date
Jim Liu 宝玉 21a5167b61 chore: release v1.24.4 2026-01-28 14:54:44 -06:00
Jim Liu 宝玉 4d3957ca06 fix(baoyu-post-to-x): ensure Apply button click closes modal before continuing 2026-01-28 14:54:39 -06:00
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
16 changed files with 327 additions and 35 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
}, },
"metadata": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.24.0" "version": "1.24.4"
}, },
"plugins": [ "plugins": [
{ {
+5
View File
@@ -152,3 +152,8 @@ slide-deck/
infographic/ infographic/
illustrations/ illustrations/
comic/ comic/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
+23
View File
@@ -2,6 +2,29 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.24.4 - 2026-01-28
### Fixes
- `baoyu-post-to-x`: fix Apply button click for cover image modal; add retry logic and wait for modal close.
## 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 ## 1.24.0 - 2026-01-27
### Features ### Features
+23
View File
@@ -2,6 +2,29 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.24.4 - 2026-01-28
### 修复
- `baoyu-post-to-x`:修复封面图上传后 Apply 按钮点击问题;增加重试逻辑并等待弹窗关闭后再继续。
## 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 ## 1.24.0 - 2026-01-27
### 新功能 ### 新功能
+14 -3
View File
@@ -543,7 +543,7 @@ AI-powered generation backends.
#### baoyu-image-gen #### 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 ```bash
# Basic generation (auto-detect provider) # Basic generation (auto-detect provider)
@@ -558,6 +558,9 @@ AI SDK-based image generation using official OpenAI and Google APIs. Supports te
# Specific provider # Specific provider
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai /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) # With reference images (Google multimodal only)
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png /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 | | `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) | | `--promptfiles` | Read prompt from files (concatenated) |
| `--image` | Output image path (required) | | `--image` | Output image path (required) |
| `--provider` | `google` or `openai` (default: google) | | `--provider` | `google`, `openai` or `dashscope` (default: google) |
| `--model`, `-m` | Model ID | | `--model`, `-m` | Model ID |
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) | | `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size` | Size (e.g., `1024x1024`) | | `--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 | - | | `OPENAI_API_KEY` | OpenAI API key | - |
| `GOOGLE_API_KEY` | Google API key | - | | `GOOGLE_API_KEY` | Google API key | - |
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` | | `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` | | `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `z-image-turbo` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - | | `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - | | `GOOGLE_BASE_URL` | Custom Google endpoint | - |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
**Provider Auto-Selection**: **Provider Auto-Selection**:
1. If `--provider` specified → use it 1. If `--provider` specified → use it
2. If only one API key available → use that provider 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 #### baoyu-danger-gemini-web
@@ -699,6 +705,11 @@ OPENAI_IMAGE_MODEL=gpt-image-1.5
GOOGLE_API_KEY=xxx GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta # 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 EOF
``` ```
+14 -3
View File
@@ -543,7 +543,7 @@ AI 驱动的生成后端。
#### baoyu-image-gen #### baoyu-image-gen
基于 AI SDK 的图像生成,使用官方 OpenAIGoogle API。支持文生图、参考图、宽高比和质量预设。 基于 AI SDK 的图像生成,使用官方 OpenAIGoogle 和 DashScope(阿里通义万相)API。支持文生图、参考图、宽高比和质量预设。
```bash ```bash
# 基础生成(自动检测服务商) # 基础生成(自动检测服务商)
@@ -558,6 +558,9 @@ AI 驱动的生成后端。
# 指定服务商 # 指定服务商
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai /baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
# DashScope(阿里通义万相)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
# 带参考图(仅 Google 多模态支持) # 带参考图(仅 Google 多模态支持)
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png /baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
``` ```
@@ -568,7 +571,7 @@ AI 驱动的生成后端。
| `--prompt`, `-p` | 提示词文本 | | `--prompt`, `-p` | 提示词文本 |
| `--promptfiles` | 从文件读取提示词(多文件拼接) | | `--promptfiles` | 从文件读取提示词(多文件拼接) |
| `--image` | 输出图片路径(必需) | | `--image` | 输出图片路径(必需) |
| `--provider` | `google``openai`(默认:google | | `--provider` | `google``openai``dashscope`(默认:google |
| `--model`, `-m` | 模型 ID | | `--model`, `-m` | 模型 ID |
| `--ar` | 宽高比(如 `16:9``1:1``4:3` | | `--ar` | 宽高比(如 `16:9``1:1``4:3` |
| `--size` | 尺寸(如 `1024x1024` | | `--size` | 尺寸(如 `1024x1024` |
@@ -580,15 +583,18 @@ AI 驱动的生成后端。
|------|------|--------| |------|------|--------|
| `OPENAI_API_KEY` | OpenAI API 密钥 | - | | `OPENAI_API_KEY` | OpenAI API 密钥 | - |
| `GOOGLE_API_KEY` | Google API 密钥 | - | | `GOOGLE_API_KEY` | Google API 密钥 | - |
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` | | `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` | | `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `z-image-turbo` |
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - | | `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - | | `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
**服务商自动选择** **服务商自动选择**
1. 如果指定了 `--provider` → 使用指定的 1. 如果指定了 `--provider` → 使用指定的
2. 如果只有一个 API 密钥 → 使用对应服务商 2. 如果只有一个 API 密钥 → 使用对应服务商
3. 如果两个都有 → 默认使用 Google 3. 如果多个可用 → 默认使用 Google
#### baoyu-danger-gemini-web #### baoyu-danger-gemini-web
@@ -699,6 +705,11 @@ OPENAI_IMAGE_MODEL=gpt-image-1.5
GOOGLE_API_KEY=xxx GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com/v1beta # 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 EOF
``` ```
+6 -1
View File
@@ -303,10 +303,15 @@ illustrations/{topic-slug}/
| Action | Steps | | 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 | | **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
| **Delete** | Delete files → Remove reference → Update outline | | **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 ## References
| File | Content | | 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/first-time-setup.md](references/config/first-time-setup.md) - First-time setup
- [config/watermark-guide.md](references/config/watermark-guide.md) - Watermark configuration - [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 ## Notes
- Image generation: 10-30 seconds per page - Image generation: 10-30 seconds per page
+4 -2
View File
@@ -258,8 +258,10 @@ Files:
| Action | Steps | | Action | Steps |
|--------|-------| |--------|-------|
| **Regenerate** | Backup existing → Update prompt → Regenerate with same settings | | **Regenerate** | Backup existing → **Update prompt file FIRST** → Regenerate with same settings |
| **Change dimension** | Backup existing → Confirm new value → Update prompt → Regenerate | | **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. All modifications automatically backup existing `cover.png` before regenerating.
+23 -10
View File
@@ -1,11 +1,11 @@
--- ---
name: baoyu-image-gen 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) # 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 ## Script Directory
@@ -63,6 +63,9 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --r
# Specific provider # Specific provider
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai 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 ## 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 | | `--prompt <text>`, `-p` | Prompt text |
| `--promptfiles <files...>` | Read prompt from files (concatenated) | | `--promptfiles <files...>` | Read prompt from files (concatenated) |
| `--image <path>` | Output image path (required) | | `--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 | | `--model <id>`, `-m` | Model ID |
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) | | `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size <WxH>` | Size (e.g., `1024x1024`) | | `--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 | | `OPENAI_API_KEY` | OpenAI API key |
| `GOOGLE_API_KEY` | Google API key | | `GOOGLE_API_KEY` | Google API key |
| `DASHSCOPE_API_KEY` | DashScope API key (阿里云) |
| `OPENAI_IMAGE_MODEL` | OpenAI model override | | `OPENAI_IMAGE_MODEL` | OpenAI model override |
| `GOOGLE_IMAGE_MODEL` | Google model override | | `GOOGLE_IMAGE_MODEL` | Google model override |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model override (default: z-image-turbo) |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | | `OPENAI_BASE_URL` | Custom OpenAI endpoint |
| `GOOGLE_BASE_URL` | Custom Google 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` **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 1. `--provider` specified → use it
2. Only one API key available → use that provider 2. Only one API key available → use that provider
3. Both available → default to Google 3. Multiple available → default to Google
## Quality Presets ## 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 - Google Imagen: uses `aspectRatio` parameter
- OpenAI: maps to closest supported size - 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 | | Setting | Value |
|---------|-------| |---------|-------|
| Recommended concurrency | 4 subagents | | Recommended concurrency | 4 subagents |
| Max concurrency | 8 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 # Launch multiple generations in parallel using Task tool
# Each Task runs as background subagent with run_in_background=true # Each Task runs as background subagent with run_in_background=true
# Collect results via TaskOutput when all complete # 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 ## Error Handling
- Missing API key → error with setup instructions - Missing API key → error with setup instructions
+14 -6
View File
@@ -14,7 +14,7 @@ Options:
-p, --prompt <text> Prompt text -p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from files (concatenated) --promptfiles <files...> Read prompt from files (concatenated)
--image <path> Output image path (required) --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 -m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3) --ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024) --size <WxH> Size (e.g., 1024x1024)
@@ -29,10 +29,13 @@ Environment variables:
OPENAI_API_KEY OpenAI API key OPENAI_API_KEY OpenAI API key
GOOGLE_API_KEY Google API key GOOGLE_API_KEY Google API key
GEMINI_API_KEY Gemini API key (alias for 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) OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview) 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 OPENAI_BASE_URL Custom OpenAI endpoint
GOOGLE_BASE_URL Custom Google 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`); 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") { if (a === "--provider") {
const v = argv[++i]; 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; out.provider = v;
continue; continue;
} }
@@ -243,13 +246,15 @@ function detectProvider(args: CliArgs): Provider {
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY); const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
const hasOpenai = !!process.env.OPENAI_API_KEY; const hasOpenai = !!process.env.OPENAI_API_KEY;
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
if (hasGoogle && !hasOpenai) return "google"; const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope"].filter(Boolean) as Provider[];
if (hasOpenai && !hasGoogle) return "openai";
if (hasGoogle && hasOpenai) return "google"; if (available.length === 1) return available[0]!;
if (available.length > 1) return available[0]!;
throw new Error( 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." "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") { if (provider === "google") {
return (await import("./providers/google")) as ProviderModule; return (await import("./providers/google")) as ProviderModule;
} }
if (provider === "dashscope") {
return (await import("./providers/dashscope")) as ProviderModule;
}
return (await import("./providers/openai")) 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 Quality = "normal" | "2k";
export type CliArgs = { export type CliArgs = {
+45 -5
View File
@@ -229,11 +229,51 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
console.log('[x-article] Waiting for Apply button...'); console.log('[x-article] Waiting for Apply button...');
const applyFound = await waitForElement('[data-testid="applyButton"]', 15_000); const applyFound = await waitForElement('[data-testid="applyButton"]', 15_000);
if (applyFound) { if (applyFound) {
await cdp.send('Runtime.evaluate', { // Check if modal is present
expression: `document.querySelector('[data-testid="applyButton"]')?.click()`, const isModalOpen = async (): Promise<boolean> => {
}, { sessionId }); const result = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
console.log('[x-article] Cover image applied'); expression: `!!document.querySelector('[role="dialog"][aria-modal="true"]')`,
await sleep(1000); returnByValue: true,
}, { sessionId });
return result.result.value;
};
// Click Apply button with retry logic
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
console.log(`[x-article] Clicking Apply button (attempt ${attempt}/${maxRetries})...`);
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('[data-testid="applyButton"]')?.click()`,
}, { sessionId });
// Wait for modal to close (up to 5 seconds per attempt)
const closeTimeout = 5000;
const checkInterval = 300;
const startTime = Date.now();
let modalClosed = false;
while (Date.now() - startTime < closeTimeout) {
await sleep(checkInterval);
const stillOpen = await isModalOpen();
if (!stillOpen) {
modalClosed = true;
break;
}
}
if (modalClosed) {
console.log('[x-article] Cover image applied, modal closed');
await sleep(500);
break;
}
if (attempt < maxRetries) {
console.log('[x-article] Modal still open, retrying...');
} else {
console.log('[x-article] Modal did not close after all attempts, continuing anyway...');
}
}
} else { } else {
console.log('[x-article] Apply button not found, continuing...'); console.log('[x-article] Apply button not found, continuing...');
} }
+4 -2
View File
@@ -626,16 +626,18 @@ Flow:
| Action | Command | Manual Steps | | 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 | | **Add** | Manual | Create prompt → Generate image → Renumber subsequent → Update outline → Regenerate PDF |
| **Delete** | Manual | Remove files → Renumber subsequent → Update outline → Regenerate PDF | | **Delete** | Manual | Remove files → Renumber subsequent → Update outline → Regenerate PDF |
### Edit Single Slide ### 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` 2. Run: `/baoyu-slide-deck <dir> --regenerate N`
3. Or manually regenerate image + PDF 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 ### Add New Slide
1. Create prompt at position: `prompts/NN-slide-{new-slug}.md` 1. Create prompt at position: `prompts/NN-slide-{new-slug}.md`
+3 -1
View File
@@ -403,10 +403,12 @@ Files:
| Action | Steps | | 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 | | **Add** | Specify position → Create prompt → Generate → Renumber subsequent files (NN+1) → Update outline |
| **Delete** | Remove files → Renumber subsequent (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 ## Content Breakdown Principles
1. **Cover (Image 1)**: Hook + visual impact → `sparse` layout 1. **Cover (Image 1)**: Hook + visual impact → `sparse` layout