Compare commits

..

4 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
10 changed files with 220 additions and 81 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.79.1" "version": "1.79.2"
}, },
"plugins": [ "plugins": [
{ {
+9
View File
@@ -2,6 +2,15 @@
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 ## 1.79.1 - 2026-03-23
### Fixes ### Fixes
+9
View File
@@ -2,6 +2,15 @@
[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 ## 1.79.1 - 2026-03-23
### 修复 ### 修复
+1 -1
View File
@@ -1,6 +1,6 @@
# CLAUDE.md # CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.79.1**. Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.79.2**.
## Architecture ## Architecture
+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"
} }
} }
+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 |
+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")
);
}); });