Compare commits

..

12 Commits

Author SHA1 Message Date
Jim Liu 宝玉 dad8f3a800 chore: release v1.79.2 2026-03-23 22:39:14 -05:00
Jim Liu 宝玉 35298d7c9d fix(baoyu-post-to-weibo): add no-theme rule for article markdown-to-HTML conversion 2026-03-23 22:38:30 -05:00
Jim Liu 宝玉 f22374ab62 fix(baoyu-cover-image): simplify reference image handling based on model capability 2026-03-23 22:38:27 -05:00
Jim Liu 宝玉 d4e80b1bc3 Fix Node-compatible parser tests (#107)
* Fix Node-compatible parser tests

* Add parser test dependencies to root test env
2026-03-23 15:30:42 -05:00
Jim Liu 宝玉 a5761dc71a chore: release v1.79.1 2026-03-23 12:02:44 -05:00
Jim Liu 宝玉 a5189dff37 fix(baoyu-xhs-images): remove opacity from watermark prompt and fix CJK spacing 2026-03-23 12:01:03 -05:00
Jim Liu 宝玉 39fe872bf3 fix(baoyu-comic): fix Doraemon naming spacing and remove opacity from watermark prompt 2026-03-23 12:01:00 -05:00
Jim Liu 宝玉 52813504f8 fix(baoyu-article-illustrator): remove opacity parameter from watermark prompt 2026-03-23 12:00:53 -05:00
Jim Liu 宝玉 a4d4108cd1 docs(project): update documentation to reflect single-plugin architecture 2026-03-23 12:00:38 -05:00
Yizhou Qian 钱亦舟 d7e763f1f5 fix: consolidate to single plugin to prevent duplicate skill registration (#106)
Merge the three plugins (content-skills, ai-generation-skills,
utility-skills) into one plugin entry. Since all three shared the same
source ("./"), Claude Code cached every skill three times. A single
plugin with one source keeps the flat skills/ layout while ensuring
each skill is registered exactly once.
2026-03-23 09:46:50 -05:00
Jim Liu 宝玉 097c09c59b chore: release v1.79.0 2026-03-22 15:42:33 -05:00
Jim Liu 宝玉 e4cd8bfefc feat(baoyu-post-to-wechat): improve credential loading with multi-source resolution and diagnostics 2026-03-22 15:42:08 -05:00
19 changed files with 549 additions and 187 deletions
+14 -30
View File
@@ -6,48 +6,32 @@
}, },
"metadata": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.78.0" "version": "1.79.2"
}, },
"plugins": [ "plugins": [
{ {
"name": "content-skills", "name": "baoyu-skills",
"description": "Content generation and publishing skills", "description": "Content generation, AI backends, and utility tools for daily work efficiency",
"source": "./", "source": "./",
"strict": true, "strict": true,
"skills": [ "skills": [
"./skills/baoyu-xhs-images",
"./skills/baoyu-post-to-x",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-post-to-weibo",
"./skills/baoyu-article-illustrator", "./skills/baoyu-article-illustrator",
"./skills/baoyu-cover-image",
"./skills/baoyu-slide-deck",
"./skills/baoyu-comic", "./skills/baoyu-comic",
"./skills/baoyu-infographic"
]
},
{
"name": "ai-generation-skills",
"description": "AI-powered generation backends",
"source": "./",
"strict": true,
"skills": [
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-image-gen"
]
},
{
"name": "utility-skills",
"description": "Utility tools for content processing",
"source": "./",
"strict": true,
"skills": [
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-compress-image", "./skills/baoyu-compress-image",
"./skills/baoyu-url-to-markdown", "./skills/baoyu-cover-image",
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-format-markdown", "./skills/baoyu-format-markdown",
"./skills/baoyu-image-gen",
"./skills/baoyu-infographic",
"./skills/baoyu-markdown-to-html", "./skills/baoyu-markdown-to-html",
"./skills/baoyu-post-to-weibo",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-post-to-x",
"./skills/baoyu-slide-deck",
"./skills/baoyu-translate", "./skills/baoyu-translate",
"./skills/baoyu-url-to-markdown",
"./skills/baoyu-xhs-images",
"./skills/baoyu-youtube-transcript" "./skills/baoyu-youtube-transcript"
] ]
} }
+25
View File
@@ -2,6 +2,31 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.79.2 - 2026-03-23
### Fixes
- `baoyu-cover-image`: simplify reference image handling — use `--ref` when model supports it, only create description files for models without reference image support
- `baoyu-post-to-weibo`: add no-theme rule for article markdown-to-HTML conversion
### Tests
- Fix Node-compatible parser tests and add parser test dependencies
## 1.79.1 - 2026-03-23
### Fixes
- Consolidate to single plugin to prevent duplicate skill registration (by @TyrealQ)
- `baoyu-article-illustrator`: remove opacity parameter from watermark prompt
- `baoyu-comic`: fix Doraemon naming spacing and remove opacity from watermark prompt
- `baoyu-xhs-images`: remove opacity from watermark prompt and fix CJK spacing
### Documentation
- Update project documentation to reflect single-plugin architecture
## 1.79.0 - 2026-03-22
### Features
- `baoyu-post-to-wechat`: improve credential loading with multi-source resolution, priority ordering, and diagnostics for skipped incomplete sources
## 1.78.0 - 2026-03-22 ## 1.78.0 - 2026-03-22
### Features ### Features
+25
View File
@@ -2,6 +2,31 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.79.2 - 2026-03-23
### 修复
- `baoyu-cover-image`:简化参考图片处理流程 — 模型支持 `--ref` 时直接传递,仅在模型不支持参考图时创建描述文件
- `baoyu-post-to-weibo`:文章 Markdown 转 HTML 时不传递 --theme 参数
### 测试
- 修复 Node 兼容的解析器测试,添加解析器测试依赖
## 1.79.1 - 2026-03-23
### 修复
- 合并为单一插件,防止 skill 重复注册 (by @TyrealQ)
- `baoyu-article-illustrator`:移除水印提示词中的不透明度参数
- `baoyu-comic`:修正哆啦 A 梦命名间距,移除水印不透明度参数
- `baoyu-xhs-images`:移除水印不透明度参数,修正中英文间距
### 文档
- 更新项目文档以反映单一插件架构
## 1.79.0 - 2026-03-22
### 新功能
- `baoyu-post-to-wechat`:改进凭据加载机制,支持多来源优先级解析,并提供不完整凭据来源的诊断信息
## 1.78.0 - 2026-03-22 ## 1.78.0 - 2026-03-22
### 新功能 ### 新功能
+7 -7
View File
@@ -1,16 +1,16 @@
# CLAUDE.md # CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.78.0**. Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.79.2**.
## Architecture ## Architecture
Skills organized into three categories in `.claude-plugin/marketplace.json` (defines plugin metadata, version, and skill paths): Skills are exposed through the single `baoyu-skills` plugin in `.claude-plugin/marketplace.json` (which defines plugin metadata, version, and skill paths). The repo docs still group them into three logical areas:
| Category | Description | | Group | Description |
|----------|-------------| |-------|-------------|
| `content-skills` | Generate or publish content (images, slides, comics, posts) | | Content Skills | Generate or publish content (images, slides, comics, posts) |
| `ai-generation-skills` | AI generation backends | | AI Generation Skills | AI generation backends |
| `utility-skills` | Content processing (conversion, compression, translation) | | Utility Skills | Content processing (conversion, compression, translation) |
Each skill contains `SKILL.md` (YAML front matter + docs), optional `scripts/`, `references/`, `prompts/`. Each skill contains `SKILL.md` (YAML front matter + docs), optional `scripts/`, `references/`, `prompts/`.
+9 -11
View File
@@ -52,16 +52,14 @@ Run the following command in Claude Code:
1. Select **Browse and install plugins** 1. Select **Browse and install plugins**
2. Select **baoyu-skills** 2. Select **baoyu-skills**
3. Select the plugin(s) you want to install 3. Select the **baoyu-skills** plugin
4. Select **Install now** 4. Select **Install now**
**Option 2: Direct Install** **Option 2: Direct Install**
```bash ```bash
# Install specific plugin # Install the marketplace's single plugin
/plugin install content-skills@baoyu-skills /plugin install baoyu-skills@baoyu-skills
/plugin install ai-generation-skills@baoyu-skills
/plugin install utility-skills@baoyu-skills
``` ```
**Option 3: Ask the Agent** **Option 3: Ask the Agent**
@@ -70,13 +68,13 @@ Simply tell Claude Code:
> Please install Skills from github.com/JimLiu/baoyu-skills > Please install Skills from github.com/JimLiu/baoyu-skills
### Available Plugins ### Available Plugin
| Plugin | Description | Skills | The marketplace now exposes a single plugin so each skill is registered exactly once.
|--------|-------------|--------|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) | | Plugin | Description | Includes |
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) | |--------|-------------|----------|
| **utility-skills** | Utility tools for content processing | [youtube-transcript](#baoyu-youtube-transcript), [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) | | **baoyu-skills** | Content generation, AI backends, and utility tools for daily work efficiency | All skills in this repository, organized below as Content Skills, AI Generation Skills, and Utility Skills |
## Update Skills ## Update Skills
+7 -9
View File
@@ -52,16 +52,14 @@ clawhub install baoyu-markdown-to-html
1. 选择 **Browse and install plugins** 1. 选择 **Browse and install plugins**
2. 选择 **baoyu-skills** 2. 选择 **baoyu-skills**
3. 选择要安装的插件 3. 选择 **baoyu-skills** 插件
4. 选择 **Install now** 4. 选择 **Install now**
**方式二:直接安装** **方式二:直接安装**
```bash ```bash
# 安装指定插件 # 安装 marketplace 中唯一的插件
/plugin install content-skills@baoyu-skills /plugin install baoyu-skills@baoyu-skills
/plugin install ai-generation-skills@baoyu-skills
/plugin install utility-skills@baoyu-skills
``` ```
**方式三:告诉 Agent** **方式三:告诉 Agent**
@@ -72,11 +70,11 @@ clawhub install baoyu-markdown-to-html
### 可用插件 ### 可用插件
| 插件 | 说明 | 包含技能 | 现在 marketplace 只暴露一个插件,这样每个 skill 只会注册一次。
| 插件 | 说明 | 包含内容 |
|------|------|----------| |------|------|----------|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) | | **baoyu-skills** | 提供内容生成、AI 后端和日常效率工具技能 | 仓库中的全部 skills,仍按下方的内容技能、AI 生成技能、工具技能三个分类展示 |
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [youtube-transcript](#baoyu-youtube-transcript), [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) |
## 更新技能 ## 更新技能
+11 -9
View File
@@ -34,20 +34,22 @@ metadata:
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter 1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter
2. Add TypeScript in `skills/baoyu-<name>/scripts/` (if applicable) 2. Add TypeScript in `skills/baoyu-<name>/scripts/` (if applicable)
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed 3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
4. Register in `marketplace.json` under appropriate category 4. Register the skill in `.claude-plugin/marketplace.json` under the `baoyu-skills` plugin entry
5. Add Script Directory section to SKILL.md if skill has scripts 5. Add Script Directory section to SKILL.md if skill has scripts
6. Add openclaw metadata to frontmatter 6. Add openclaw metadata to frontmatter
## Category Selection ## Skill Grouping
| If your skill... | Use category | All skills are registered under the single `baoyu-skills` plugin. Use these logical groups when deciding where the skill should appear in the docs:
|------------------|--------------|
| Generates visual content (images, slides, comics) | `content-skills` |
| Publishes to platforms (X, WeChat, Weibo) | `content-skills` |
| Provides AI generation backend | `ai-generation-skills` |
| Converts or processes content | `utility-skills` |
New category: add plugin object to `marketplace.json` with `name`, `description`, `skills[]`. | If your skill... | Use group |
|------------------|-----------|
| Generates visual content (images, slides, comics) | Content Skills |
| Publishes to platforms (X, WeChat, Weibo) | Content Skills |
| Provides AI generation backend | AI Generation Skills |
| Converts or processes content | Utility Skills |
If you add a new logical group, update the docs that present grouped skills, but keep the skill registered under the single `baoyu-skills` plugin entry.
## Writing Descriptions ## Writing Descriptions
+105 -1
View File
@@ -9,7 +9,11 @@
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"tsx": "^4.20.5" "@mozilla/readability": "^0.6.0",
"linkedom": "^0.18.12",
"tsx": "^4.20.5",
"turndown": "^7.2.2",
"turndown-plugin-gfm": "^1.0.2"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@@ -454,6 +458,23 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/@mozilla/readability": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -615,6 +636,13 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -896,6 +924,13 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
"dev": true,
"license": "MIT"
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
@@ -984,6 +1019,51 @@
"node": ">=18.17" "node": ">=18.17"
} }
}, },
"node_modules/linkedom": {
"version": "0.18.12",
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
"integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
"dev": true,
"license": "ISC",
"dependencies": {
"css-select": "^5.1.0",
"cssom": "^0.5.0",
"html-escaper": "^3.0.3",
"htmlparser2": "^10.0.0",
"uhyphen": "^0.2.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"canvas": ">= 2"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/linkedom/node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/longest-streak": { "node_modules/longest-streak": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -1768,6 +1848,30 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
} }
}, },
"node_modules/turndown": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
}
},
"node_modules/turndown-plugin-gfm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
"dev": true,
"license": "MIT"
},
"node_modules/uhyphen": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
"dev": true,
"license": "ISC"
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.24.0", "version": "6.24.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
+4
View File
@@ -10,6 +10,10 @@
"test:coverage": "node --import tsx --experimental-test-coverage --test" "test:coverage": "node --import tsx --experimental-test-coverage --test"
}, },
"devDependencies": { "devDependencies": {
"@mozilla/readability": "^0.6.0",
"linkedom": "^0.18.12",
"turndown": "^7.2.2",
"turndown-plugin-gfm": "^1.0.2",
"tsx": "^4.20.5" "tsx": "^4.20.5"
} }
} }
@@ -280,5 +280,5 @@ TEXTURE: Halftone transitions between sides
If watermark enabled in preferences, append: If watermark enabled in preferences, append:
``` ```
Include a subtle watermark "[content]" positioned at [position] with approximately [opacity*100]% visibility. Include a subtle watermark "[content]" positioned at [position].
``` ```
+4 -5
View File
@@ -278,7 +278,7 @@ Create storyboard and character definitions using the confirmed style from Step
| Role | Character | Visual Description | | Role | Character | Visual Description |
|------|-----------|-------------------| |------|-----------|-------------------|
| Student | 大雄 (Nobita) | Japanese boy, 10yo, round glasses, black hair parted in middle, yellow shirt, navy shorts | | Student | 大雄 (Nobita) | Japanese boy, 10yo, round glasses, black hair parted in middle, yellow shirt, navy shorts |
| Mentor | 哆啦A梦 (Doraemon) | Round blue robot cat, big white eyes, red nose, whiskers, white belly with 4D pocket, golden bell, no ears | | Mentor | 哆啦 A 梦 (Doraemon) | Round blue robot cat, big white eyes, red nose, whiskers, white belly with 4D pocket, golden bell, no ears |
| Challenge | 胖虎 (Gian) | Stocky boy, rough features, small eyes, orange shirt | | Challenge | 胖虎 (Gian) | Stocky boy, rough features, small eyes, orange shirt |
| Support | 静香 (Shizuka) | Cute girl, black short hair, pink dress, gentle expression | | Support | 静香 (Shizuka) | Cute girl, black short hair, pink dress, gentle expression |
@@ -359,8 +359,7 @@ Art: [art style] | Tone: [tone] | Layout: [layout type]
**Watermark Application** (if enabled in preferences): **Watermark Application** (if enabled in preferences):
Add to each prompt: Add to each prompt:
``` ```
Include a subtle watermark "[content]" positioned at [position] Include a subtle watermark "[content]" positioned at [position]. The watermark should
with approximately [opacity*100]% visibility. The watermark should
be legible but not distracting from the comic panels and storytelling. be legible but not distracting from the comic panels and storytelling.
Ensure watermark does not overlap speech bubbles or key action. Ensure watermark does not overlap speech bubbles or key action.
``` ```
@@ -452,8 +451,8 @@ When skill does NOT support reference images, create combined prompt files:
## Character Reference (maintain consistency) ## Character Reference (maintain consistency)
[Copy relevant sections from characters/characters.md here] [Copy relevant sections from characters/characters.md here]
- 大雄: Japanese boy, round glasses, yellow shirt, navy shorts... - 大雄Japanese boy, round glasses, yellow shirt, navy shorts...
- 哆啦A梦: Round blue robot cat, white belly, red nose, golden bell... - 哆啦 A 梦:Round blue robot cat, white belly, red nose, golden bell...
## Page Content ## Page Content
[Original page prompt here] [Original page prompt here]
+4 -5
View File
@@ -162,15 +162,14 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
5. **Detect language**: Compare source, user input, EXTEND.md preference 5. **Detect language**: Compare source, user input, EXTEND.md preference
6. **Determine output directory**: Per File Structure rules 6. **Determine output directory**: Per File Structure rules
**⚠️ People in Reference Images — MUST follow all 3 rules:** **⚠️ People in Reference Images:**
If reference images contain **people** who should appear in the cover: If reference images contain **people** who should appear in the cover:
1. **`usage: direct`** — MUST set in refs description file. NEVER use `style` or `palette` when people need to appear - **Model supports `--ref`** (default): Copy image to `refs/`, pass via `--ref` at generation. No description file needed — the model sees the face directly.
2. **Per-character description** — MUST describe each person's distinctive features (hair, glasses, skin tone, clothing) in `refs/ref-NN-{slug}.md`. Vague descriptions like "a man" will fail - **Model does NOT support `--ref`** (Jimeng, Seedream 3.0): Create `refs/ref-NN-{slug}.md` with per-character description (hair, glasses, skin tone, clothing). Embed as MUST/REQUIRED instructions in prompt text.
3. **`--ref` flag** — MUST pass reference image via `--ref` in Step 4 so the model sees actual faces
See [reference-images.md § Character Analysis](references/workflow/reference-images.md) for description format. See [reference-images.md](references/workflow/reference-images.md) for full decision table.
### Step 2: Confirm Options ⚠️ ### Step 2: Confirm Options ⚠️
@@ -16,17 +16,24 @@ Guide for processing user-provided reference images in cover generation.
**If user provides file path**: **If user provides file path**:
1. Copy to `refs/ref-NN-{slug}.{ext}` (NN = 01, 02, ...) 1. Copy to `refs/ref-NN-{slug}.{ext}` (NN = 01, 02, ...)
2. Create description: `refs/ref-NN-{slug}.md` 2. **Only** create description file `refs/ref-NN-{slug}.md` when model does NOT support `--ref` (see below)
3. Verify files exist before proceeding 3. Verify image file exists before proceeding
**Description File Format**: **When to create description file**:
| Situation | Action |
|-----------|--------|
| Model supports `--ref` (Google, OpenAI, OpenRouter, Replicate, Seedream 4.0+) | Copy image only. **No description file needed.** Pass via `--ref` at generation. |
| Model does NOT support `--ref` (Jimeng, Seedream 3.0) | Copy image + create description file. Embed description in prompt text. |
**Description File Format** (only when needed):
```yaml ```yaml
--- ---
ref_id: NN ref_id: NN
filename: ref-NN-{slug}.{ext} filename: ref-NN-{slug}.{ext}
usage: direct | style | palette usage: direct | style | palette
--- ---
[User's description or auto-generated description] [Character or style description to embed in prompt]
``` ```
| Usage | When to Use | | Usage | When to Use |
@@ -694,6 +694,10 @@ async function main(): Promise<void> {
} }
const creds = loadCredentials(resolved); const creds = loadCredentials(resolved);
for (const skippedSource of creds.skippedSources) {
console.error(`[wechat-api] Skipped incomplete credential source: ${skippedSource}`);
}
console.error(`[wechat-api] Credentials source: ${creds.source}`);
console.error("[wechat-api] Fetching access token..."); console.error("[wechat-api] Fetching access token...");
const accessToken = await fetchAccessToken(creds.appId, creds.appSecret); const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);
@@ -0,0 +1,139 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import test, { type TestContext } from "node:test";
import { loadCredentials } from "./wechat-extend-config.ts";
function useCwd(t: TestContext, cwd: string): void {
const previous = process.cwd();
process.chdir(cwd);
t.after(() => {
process.chdir(previous);
});
}
function useHome(t: TestContext, home: string): void {
const previous = process.env.HOME;
process.env.HOME = home;
t.after(() => {
if (previous === undefined) {
delete process.env.HOME;
return;
}
process.env.HOME = previous;
});
}
function useWechatEnv(
t: TestContext,
values: Partial<Record<"WECHAT_APP_ID" | "WECHAT_APP_SECRET", string | undefined>>,
): void {
const previous = {
WECHAT_APP_ID: process.env.WECHAT_APP_ID,
WECHAT_APP_SECRET: process.env.WECHAT_APP_SECRET,
};
if (values.WECHAT_APP_ID === undefined) {
delete process.env.WECHAT_APP_ID;
} else {
process.env.WECHAT_APP_ID = values.WECHAT_APP_ID;
}
if (values.WECHAT_APP_SECRET === undefined) {
delete process.env.WECHAT_APP_SECRET;
} else {
process.env.WECHAT_APP_SECRET = values.WECHAT_APP_SECRET;
}
t.after(() => {
if (previous.WECHAT_APP_ID === undefined) {
delete process.env.WECHAT_APP_ID;
} else {
process.env.WECHAT_APP_ID = previous.WECHAT_APP_ID;
}
if (previous.WECHAT_APP_SECRET === undefined) {
delete process.env.WECHAT_APP_SECRET;
} else {
process.env.WECHAT_APP_SECRET = previous.WECHAT_APP_SECRET;
}
});
}
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
async function writeEnvFile(root: string, content: string): Promise<void> {
const envPath = path.join(root, ".baoyu-skills", ".env");
await fs.mkdir(path.dirname(envPath), { recursive: true });
await fs.writeFile(envPath, content);
}
test("loadCredentials selects the first complete source without mixing values across sources", async (t) => {
const cwdRoot = await makeTempDir("wechat-creds-cwd-");
const homeRoot = await makeTempDir("wechat-creds-home-");
useCwd(t, cwdRoot);
useHome(t, homeRoot);
useWechatEnv(t, {
WECHAT_APP_ID: undefined,
WECHAT_APP_SECRET: "stale-secret-from-process-env",
});
await writeEnvFile(cwdRoot, "WECHAT_APP_ID=cwd-app-id\nWECHAT_APP_SECRET=cwd-app-secret\n");
await writeEnvFile(homeRoot, "WECHAT_APP_ID=home-app-id\nWECHAT_APP_SECRET=home-app-secret\n");
const credentials = loadCredentials();
assert.equal(credentials.appId, "cwd-app-id");
assert.equal(credentials.appSecret, "cwd-app-secret");
assert.equal(credentials.source, "<cwd>/.baoyu-skills/.env");
assert.deepEqual(credentials.skippedSources, [
"process.env missing WECHAT_APP_ID",
]);
});
test("loadCredentials prefers a complete process.env pair over lower-priority files", async (t) => {
const cwdRoot = await makeTempDir("wechat-creds-cwd-");
const homeRoot = await makeTempDir("wechat-creds-home-");
useCwd(t, cwdRoot);
useHome(t, homeRoot);
useWechatEnv(t, {
WECHAT_APP_ID: "env-app-id",
WECHAT_APP_SECRET: "env-app-secret",
});
await writeEnvFile(cwdRoot, "WECHAT_APP_ID=cwd-app-id\nWECHAT_APP_SECRET=cwd-app-secret\n");
await writeEnvFile(homeRoot, "WECHAT_APP_ID=home-app-id\nWECHAT_APP_SECRET=home-app-secret\n");
const credentials = loadCredentials();
assert.equal(credentials.appId, "env-app-id");
assert.equal(credentials.appSecret, "env-app-secret");
assert.equal(credentials.source, "process.env");
assert.deepEqual(credentials.skippedSources, []);
});
test("loadCredentials reports skipped incomplete sources when no complete pair exists", async (t) => {
const cwdRoot = await makeTempDir("wechat-creds-cwd-");
const homeRoot = await makeTempDir("wechat-creds-home-");
useCwd(t, cwdRoot);
useHome(t, homeRoot);
useWechatEnv(t, {
WECHAT_APP_ID: "env-app-id",
WECHAT_APP_SECRET: undefined,
});
await writeEnvFile(cwdRoot, "WECHAT_APP_SECRET=cwd-app-secret\n");
assert.throws(
() => loadCredentials(),
/Incomplete credential sources skipped:\n- process\.env missing WECHAT_APP_SECRET\n- <cwd>\/\.baoyu-skills\/\.env missing WECHAT_APP_ID/,
);
});
@@ -196,48 +196,116 @@ function aliasToEnvKey(alias: string): string {
return alias.toUpperCase().replace(/-/g, "_"); return alias.toUpperCase().replace(/-/g, "_");
} }
export function loadCredentials(account?: ResolvedAccount): { appId: string; appSecret: string } { interface CredentialSource {
if (account?.app_id && account?.app_secret) { name: string;
return { appId: account.app_id, appSecret: account.app_secret }; appIdKey: string;
appSecretKey: string;
appId?: string;
appSecret?: string;
}
export interface LoadedCredentials {
appId: string;
appSecret: string;
source: string;
skippedSources: string[];
}
function normalizeCredentialValue(value?: string): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function describeMissingKeys(source: CredentialSource): string {
const missingKeys: string[] = [];
if (!source.appId) missingKeys.push(source.appIdKey);
if (!source.appSecret) missingKeys.push(source.appSecretKey);
return `${source.name} missing ${missingKeys.join(" and ")}`;
}
function buildCredentialSource(
name: string,
values: Record<string, string | undefined>,
appIdKey: string,
appSecretKey: string,
): CredentialSource {
return {
name,
appIdKey,
appSecretKey,
appId: normalizeCredentialValue(values[appIdKey]),
appSecret: normalizeCredentialValue(values[appSecretKey]),
};
}
function resolveCredentialSource(
sources: CredentialSource[],
account?: ResolvedAccount,
): LoadedCredentials {
const skippedSources: string[] = [];
for (const source of sources) {
if (source.appId && source.appSecret) {
return {
appId: source.appId,
appSecret: source.appSecret,
source: source.name,
skippedSources,
};
}
if (source.appId || source.appSecret) {
skippedSources.push(describeMissingKeys(source));
}
} }
const hint = account?.alias ? ` (account: ${account.alias})` : "";
const partialHint = skippedSources.length > 0
? `\nIncomplete credential sources skipped:\n- ${skippedSources.join("\n- ")}`
: "";
throw new Error(
`Missing WECHAT_APP_ID or WECHAT_APP_SECRET${hint}.\n` +
"Set via EXTEND.md account config, environment variables, or .baoyu-skills/.env file." +
partialHint
);
}
export function loadCredentials(account?: ResolvedAccount): LoadedCredentials {
const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env"); const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env");
const homeEnvPath = path.join(os.homedir(), ".baoyu-skills", ".env"); const homeEnvPath = path.join(os.homedir(), ".baoyu-skills", ".env");
const cwdEnv = loadEnvFile(cwdEnvPath); const cwdEnv = loadEnvFile(cwdEnvPath);
const homeEnv = loadEnvFile(homeEnvPath); const homeEnv = loadEnvFile(homeEnvPath);
const sources: CredentialSource[] = [];
if (account?.app_id || account?.app_secret) {
sources.push({
name: account.alias ? `EXTEND.md account "${account.alias}"` : "EXTEND.md account config",
appIdKey: "app_id",
appSecretKey: "app_secret",
appId: normalizeCredentialValue(account.app_id),
appSecret: normalizeCredentialValue(account.app_secret),
});
}
const prefix = account?.alias ? `WECHAT_${aliasToEnvKey(account.alias)}_` : ""; const prefix = account?.alias ? `WECHAT_${aliasToEnvKey(account.alias)}_` : "";
let appId = "";
let appSecret = "";
if (prefix) { if (prefix) {
appId = process.env[`${prefix}APP_ID`] const prefixedKeyLabel = `${prefix}APP_ID/${prefix}APP_SECRET`;
|| cwdEnv[`${prefix}APP_ID`] sources.push(
|| homeEnv[`${prefix}APP_ID`] buildCredentialSource(`process.env (${prefixedKeyLabel})`, process.env, `${prefix}APP_ID`, `${prefix}APP_SECRET`),
|| ""; buildCredentialSource(`<cwd>/.baoyu-skills/.env (${prefixedKeyLabel})`, cwdEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),
appSecret = process.env[`${prefix}APP_SECRET`] buildCredentialSource(`~/.baoyu-skills/.env (${prefixedKeyLabel})`, homeEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),
|| cwdEnv[`${prefix}APP_SECRET`]
|| homeEnv[`${prefix}APP_SECRET`]
|| "";
}
if (!appId) {
appId = process.env.WECHAT_APP_ID || cwdEnv.WECHAT_APP_ID || homeEnv.WECHAT_APP_ID || "";
}
if (!appSecret) {
appSecret = process.env.WECHAT_APP_SECRET || cwdEnv.WECHAT_APP_SECRET || homeEnv.WECHAT_APP_SECRET || "";
}
if (!appId || !appSecret) {
const hint = account?.alias ? ` (account: ${account.alias})` : "";
throw new Error(
`Missing WECHAT_APP_ID or WECHAT_APP_SECRET${hint}.\n` +
"Set via EXTEND.md account config, environment variables, or .baoyu-skills/.env file."
); );
} }
return { appId, appSecret }; sources.push(
buildCredentialSource("process.env", process.env, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),
buildCredentialSource("<cwd>/.baoyu-skills/.env", cwdEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),
buildCredentialSource("~/.baoyu-skills/.env", homeEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),
);
return resolveCredentialSource(sources, account);
} }
export function listAccounts(config: WechatExtendConfig): string[] { export function listAccounts(config: WechatExtendConfig): string[] {
+2
View File
@@ -123,6 +123,8 @@ ${BUN_X} {baseDir}/scripts/weibo-article.ts article.md --cover ./cover.jpg
- Title: 32 characters max (truncated with warning if longer) - Title: 32 characters max (truncated with warning if longer)
- Summary/导语: 44 characters max (auto-regenerated from content if longer) - Summary/导语: 44 characters max (auto-regenerated from content if longer)
**Markdown-to-HTML**: Do NOT pass any `--theme` parameter when converting markdown to HTML. Use the default theme (no theme argument).
**Article Workflow**: **Article Workflow**:
1. Opens `https://card.weibo.com/article/v3/editor` 1. Opens `https://card.weibo.com/article/v3/editor`
2. Clicks "写文章" button, waits for editor to become editable 2. Clicks "写文章" button, waits for editor to become editable
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test"; import assert from "node:assert/strict";
import test from "node:test";
import { import {
createMarkdownDocument, createMarkdownDocument,
@@ -129,73 +130,77 @@ function parse(html: string, url: string) {
return tryUrlRuleParsers(html, url, baseMetadata); return tryUrlRuleParsers(html, url, baseMetadata);
} }
describe("url rule parsers", () => { test("parses archive.ph pages from CONTENT and restores the original URL", () => {
test("parses archive.ph pages from CONTENT and restores the original URL", () => { const result = parse(ARCHIVE_HTML, "https://archive.ph/SMcX5");
const result = parse(ARCHIVE_HTML, "https://archive.ph/SMcX5");
expect(result).not.toBeNull(); assert.ok(result);
expect(result?.conversionMethod).toBe("parser:archive-ph"); assert.equal(result.conversionMethod, "parser:archive-ph");
expect(result?.metadata.url).toBe( assert.equal(
"https://www.newscientist.com/article/2520204-major-leap-towards-reanimation-after-death-as-mammals-brain-preserved/" result.metadata.url,
); "https://www.newscientist.com/article/2520204-major-leap-towards-reanimation-after-death-as-mammals-brain-preserved/"
expect(result?.metadata.title).toBe( );
"Major leap towards reanimation after death as mammal brain preserved" assert.equal(
); result.metadata.title,
expect(result?.metadata.coverImage).toBe("https://cdn.example.com/brain.jpg"); "Major leap towards reanimation after death as mammal brain preserved"
expect(result?.markdown).toContain("Researchers say the preserved structure"); );
expect(result?.markdown).toContain("![Brain tissue](https://cdn.example.com/brain.jpg)"); assert.equal(result.metadata.coverImage, "https://cdn.example.com/brain.jpg");
expect(result?.markdown).not.toContain("Archive shell text that should be ignored"); assert.ok(result.markdown.includes("Researchers say the preserved structure"));
}); assert.ok(result.markdown.includes("![Brain tissue](https://cdn.example.com/brain.jpg)"));
assert.ok(!result.markdown.includes("Archive shell text that should be ignored"));
test("falls back to body when archive.ph CONTENT is missing", () => { });
const result = parse(ARCHIVE_FALLBACK_HTML, "https://archive.ph/fallback");
test("falls back to body when archive.ph CONTENT is missing", () => {
expect(result).not.toBeNull(); const result = parse(ARCHIVE_FALLBACK_HTML, "https://archive.ph/fallback");
expect(result?.conversionMethod).toBe("parser:archive-ph");
expect(result?.metadata.url).toBe("https://example.com/fallback-story"); assert.ok(result);
expect(result?.metadata.title).toBe("Fallback body parsing still works"); assert.equal(result.conversionMethod, "parser:archive-ph");
expect(result?.markdown).toContain("When CONTENT is absent"); assert.equal(result.metadata.url, "https://example.com/fallback-story");
}); assert.equal(result.metadata.title, "Fallback body parsing still works");
assert.ok(result.markdown.includes("When CONTENT is absent"));
test("parses X article pages from HTML", () => { });
const result = parse(
ARTICLE_HTML, test("parses X article pages from HTML", () => {
"https://x.com/dotey/article/2035141635713941927" const result = parse(
); ARTICLE_HTML,
"https://x.com/dotey/article/2035141635713941927"
expect(result).not.toBeNull(); );
expect(result?.conversionMethod).toBe("parser:x-article");
expect(result?.metadata.title).toBe("Karpathy\"写代码\"已经不是对的动词了"); assert.ok(result);
expect(result?.metadata.author).toBe("宝玉 (@dotey)"); assert.equal(result.conversionMethod, "parser:x-article");
expect(result?.metadata.coverImage).toBe("https://pbs.twimg.com/media/article-cover.jpg"); assert.equal(result.metadata.title, "Karpathy\"写代码\"已经不是对的动词了");
expect(result?.metadata.published).toBe("2026-03-20T23:49:11.000Z"); assert.equal(result.metadata.author, "宝玉 (@dotey)");
expect(result?.metadata.language).toBe("zh"); assert.equal(result.metadata.coverImage, "https://pbs.twimg.com/media/article-cover.jpg");
expect(result?.markdown).toContain("## 要点速览"); assert.equal(result.metadata.published, "2026-03-20T23:49:11.000Z");
expect(result?.markdown).toContain( assert.equal(result.metadata.language, "zh");
"[![](https://pbs.twimg.com/media/article-inline.jpg)](/dotey/article/2035141635713941927/media/2)" assert.ok(result.markdown.includes("## 要点速览"));
); assert.ok(
expect(result?.markdown).toContain("写代码已经不是对的动词了。"); result.markdown.includes(
"[![](https://pbs.twimg.com/media/article-inline.jpg)](/dotey/article/2035141635713941927/media/2)"
const document = createMarkdownDocument(result!); )
expect(document).toContain("# Karpathy\"写代码\"已经不是对的动词了"); );
}); assert.ok(result.markdown.includes("写代码已经不是对的动词了。"));
test("parses X status pages from HTML without duplicating the title heading", () => { const document = createMarkdownDocument(result);
const result = parse( assert.ok(document.includes("# Karpathy\"写代码\"已经不是对的动词了"));
STATUS_HTML, });
"https://x.com/dotey/status/2035590649081196710"
); test("parses X status pages from HTML without duplicating the title heading", () => {
const result = parse(
expect(result).not.toBeNull(); STATUS_HTML,
expect(result?.conversionMethod).toBe("parser:x-status"); "https://x.com/dotey/status/2035590649081196710"
expect(result?.metadata.author).toBe("宝玉 (@dotey)"); );
expect(result?.metadata.coverImage).toBe("https://pbs.twimg.com/media/tweet-main.jpg");
expect(result?.metadata.language).toBe("zh"); assert.ok(result);
expect(result?.markdown).toContain("转译:把下面这段加到你的 Codex 自定义指令里"); assert.equal(result.conversionMethod, "parser:x-status");
expect(result?.markdown).toContain("> Quote from Matt Shumer (@mattshumer_)"); assert.equal(result.metadata.author, "宝玉 (@dotey)");
expect(result?.markdown).toContain("!["); assert.equal(result.metadata.coverImage, "https://pbs.twimg.com/media/tweet-main.jpg");
assert.equal(result.metadata.language, "zh");
const document = createMarkdownDocument(result!); assert.ok(result.markdown.includes("转译:把下面这段加到你的 Codex 自定义指令里"));
expect(document).not.toContain("\n\n# 转译:把下面这段加到你的 Codex 自定义指令里,体验会好太多:\n\n"); assert.ok(result.markdown.includes("> Quote from Matt Shumer (@mattshumer_)"));
}); assert.ok(result.markdown.includes("!["));
const document = createMarkdownDocument(result);
assert.ok(
!document.includes("\n\n# 转译:把下面这段加到你的 Codex 自定义指令里,体验会好太多:\n\n")
);
}); });
@@ -151,8 +151,7 @@ From outline entry:
```markdown ```markdown
## Watermark ## Watermark
Include a subtle watermark "{content}" positioned at {position} Include a subtle watermark "{content}" positioned at {position}. The watermark should
with approximately {opacity*100}% visibility. The watermark should
be legible but not distracting from the main content. be legible but not distracting from the main content.
``` ```
@@ -295,19 +294,19 @@ Create a Xiaohongshu (Little Red Book) style infographic following these guideli
## Content ## Content
**Position**: Content (Page 3 of 6) **Position**: Content (Page 3 of 6)
**Core Message**: ChatGPT使用技巧 **Core Message**: ChatGPT 使用技巧
**Text Content**: **Text Content**:
- Title: 「ChatGPT」 - Title: 「ChatGPT」
- Subtitle: 最强AI助手 - Subtitle: 最强 AI 助手
- Points: - Points:
- 写文案:给出框架,秒出初稿 - 写文案:给出框架,秒出初稿
- 改文章:润色、翻译、总结 - 改文章:润色、翻译、总结
- 编程:写代码、找bug - 编程:写代码、找 bug
- 学习:解释概念、出题练习 - 学习:解释概念、出题练习
**Visual Concept**: **Visual Concept**:
ChatGPT logo居中,四周放射状展示功能点 ChatGPT logo 居中,四周放射状展示功能点
深色科技背景,霓虹绿点缀 深色科技背景,霓虹绿点缀
--- ---