mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-15 06:59:48 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dad8f3a800 | |||
| 35298d7c9d | |||
| f22374ab62 | |||
| d4e80b1bc3 | |||
| a5761dc71a | |||
| a5189dff37 | |||
| 39fe872bf3 | |||
| 52813504f8 | |||
| a4d4108cd1 | |||
| d7e763f1f5 | |||
| 097c09c59b | |||
| e4cd8bfefc |
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -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/`.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+105
-1
@@ -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",
|
||||||
|
|||||||
@@ -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].
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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("");
|
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(""));
|
||||||
|
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");
|
||||||
"[](/dotey/article/2035141635713941927/media/2)"
|
assert.ok(result.markdown.includes("## 要点速览"));
|
||||||
);
|
assert.ok(
|
||||||
expect(result?.markdown).toContain("写代码已经不是对的动词了。");
|
result.markdown.includes(
|
||||||
|
"[](/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 居中,四周放射状展示功能点
|
||||||
深色科技背景,霓虹绿点缀
|
深色科技背景,霓虹绿点缀
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user