Compare commits

..

5 Commits

11 changed files with 150 additions and 61 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.21.4"
"version": "1.22.0"
},
"plugins": [
{
+10
View File
@@ -2,6 +2,16 @@
English | [中文](./CHANGELOG.zh.md)
## 1.22.0 - 2026-01-25
### Features
- `baoyu-article-illustrator`: adds `imgs-subdir` output directory option; improves style selection to always ask and show preferred_style from EXTEND.md.
- `baoyu-cover-image`: adds `default_output_dir` preference supporting `same-dir`, `imgs-subdir`, and `independent` options with Step 1.5 for output directory selection.
- `baoyu-post-to-wechat`: adds theme selection (default/grace/simple) with AskUserQuestion before posting; adds HTML preview step; simplifies image placeholders to `WECHATIMGPH_N` format; refactors copy/paste to cross-platform helpers.
### Refactor
- `baoyu-post-to-x`: simplifies image placeholders from `[[IMAGE_PLACEHOLDER_N]]` to `XIMGPH_N` format.
## 1.21.4 - 2026-01-25
### Fixes
+10
View File
@@ -2,6 +2,16 @@
[English](./CHANGELOG.md) | 中文
## 1.22.0 - 2026-01-25
### 新功能
- `baoyu-article-illustrator`:新增 `imgs-subdir` 输出目录选项;改进风格选择,始终询问并展示 EXTEND.md 中的 preferred_style。
- `baoyu-cover-image`:新增 `default_output_dir` 偏好设置,支持 `same-dir``imgs-subdir``independent` 选项,新增 Step 1.5 输出目录选择流程。
- `baoyu-post-to-wechat`:发布前新增主题选择(default/grace/simple);新增 HTML 预览步骤;图片占位符简化为 `WECHATIMGPH_N` 格式;重构复制粘贴为跨平台辅助函数。
### 重构
- `baoyu-post-to-x`:图片占位符从 `[[IMAGE_PLACEHOLDER_N]]` 简化为 `XIMGPH_N` 格式。
## 1.21.4 - 2026-01-25
### 修复
+18 -5
View File
@@ -85,12 +85,14 @@ Check `default_output_dir` in preferences:
| Preference Value | Action |
|------------------|--------|
| `same-dir` | Use `{article-dir}/`, display "Output: {path}" |
| `imgs-subdir` | Use `{article-dir}/imgs/`, display "Output: {path}" |
| `illustrations-subdir` | Use `{article-dir}/illustrations/`, display "Output: {path}" |
| `independent` | Use `illustrations/{topic-slug}/`, display "Output: {path}" |
| Not configured | **MUST** ask with AskUserQuestion ↓ |
**AskUserQuestion** (when no preference):
- `{article-dir}/` - Same directory as article
- `{article-dir}/imgs/` - Images subdirectory
- `{article-dir}/illustrations/` - Illustrations subdirectory (Recommended)
- `illustrations/{topic-slug}/` - Independent directory
- Save as default - Remember this choice for future runs
@@ -175,11 +177,22 @@ test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
- balanced (3-5) (Recommended) - Major sections
- rich (6+) - Comprehensive support
**Q3: Style**
Based on Type, suggest compatible styles from matrix:
- [Best compatible] (Recommended)
- [Other ✓✓ styles]
- [Other ✓ styles]
**Q3: Style** (ALWAYS ask, even with preferred_style in EXTEND.md)
If EXTEND.md has `preferred_style`:
- [Custom style name + brief description] (Recommended)
- [Top compatible built-in style 1]
- [Top compatible built-in style 2]
- [Top compatible built-in style 3]
If no `preferred_style`:
- [Best compatible from matrix] (Recommended)
- [Other ✓✓ style 1]
- [Other ✓✓ style 2]
- [Other ✓ style]
Style selection based on Type × Style compatibility matrix (references/styles.md).
Full specs: `references/styles/<style>.md`
**Q4** (only if source ≠ user language):
- Language: Source / User language
+54 -9
View File
@@ -218,10 +218,17 @@ Default: `balanced`
## File Structure
Each session creates an independent directory named by content slug:
Output directory depends on `default_output_dir` preference:
| Preference | Output Path |
|------------|-------------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `independent` (default) | `cover-image/{topic-slug}/` |
| Pasted content | `cover-image/{topic-slug}/` (always) |
```
cover-image/{topic-slug}/
<output-dir>/
├── source-{slug}.{ext} # Source files (text, images, etc.)
├── prompts/cover.md # Generation prompt
└── cover.png # Output image
@@ -250,7 +257,7 @@ Copy and track progress:
```
Cover Image Progress:
- [ ] Step 0: Check preferences (EXTEND.md) ⚠️ REQUIRED if not found
- [ ] Step 1: Analyze content
- [ ] Step 1: Analyze content + determine output directory ⚠️ MUST ask if not configured
- [ ] Step 2: Confirm options (4 dimensions) ⚠️ REQUIRED unless --quick or all specified
- [ ] Step 3: Create prompt
- [ ] Step 4: Generate image
@@ -260,9 +267,9 @@ Cover Image Progress:
### Flow
```
Input → [Step 0: Preferences/Setup] → Analyze → [Confirm: 4 Dimensions] → Prompt → Generate → Complete
(skip if --quick or all dimensions specified)
Input → [Step 0: Preferences/Setup] → Analyze → [Output Dir ⚠️] → [Confirm: 4 Dimensions] → Prompt → Generate → Complete
(skip if --quick or all specified)
```
### Step 0: Load Preferences (EXTEND.md) ⚠️
@@ -307,6 +314,7 @@ Preferences loaded from [project/user]:
• Text: [preferred_text or "title-only"]
• Mood: [preferred_mood or "balanced"]
• Aspect: [default_aspect]
• Output: [default_output_dir or "not set — will ask in Step 1.5"]
• Quick mode: [enabled/disabled]
• Language: [language or "auto"]
```
@@ -369,7 +377,20 @@ options:
Note: More ratios (4:3, 3:2) available during generation. This sets the default recommendation.
**Q5: Quick Mode**
**Q5: Default Output Directory**
```yaml
header: "Output"
question: "Default output directory for cover images?"
options:
- label: "Independent (Recommended)"
description: "cover-image/{topic-slug}/ - separate from article"
- label: "Same directory"
description: "{article-dir}/ - alongside the article file"
- label: "imgs subdirectory"
description: "{article-dir}/imgs/ - images folder near article"
```
**Q6: Quick Mode**
```yaml
header: "Quick"
question: "Enable quick mode by default?"
@@ -380,7 +401,7 @@ options:
description: "Skip confirmation, use auto-selection"
```
**Q6: Save Location**
**Q7: Save Location**
```yaml
header: "Save"
question: "Where to save preferences?"
@@ -395,7 +416,7 @@ options:
Full setup details: `references/config/first-time-setup.md`
**EXTEND.md Supports**: Watermark | Preferred type | Preferred style | Preferred text | Preferred mood | Default aspect ratio | Quick mode | Custom style definitions | Language preference
**EXTEND.md Supports**: Watermark | Preferred type | Preferred style | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Custom style definitions | Language preference
Schema: `references/config/preferences-schema.md`
@@ -417,6 +438,30 @@ Read source content, save it if needed, and perform analysis.
- Note user's input language (from conversation)
- Compare with language preference in EXTEND.md
**1.5 Determine Output Directory** ⚠️
**MUST ask user when `default_output_dir` is not set in EXTEND.md and input is a file path.**
| Input | Behavior |
|-------|----------|
| File path + preference set | Use configured `default_output_dir` |
| File path + **no preference** | ⚠️ **MUST** ask with AskUserQuestion ↓ |
| Pasted content | `cover-image/{topic-slug}/` (always independent) |
`default_output_dir` preference values:
| Preference Value | Output Path |
|------------------|-------------|
| `same-dir` | `{article-dir}/` |
| `imgs-subdir` | `{article-dir}/imgs/` |
| `independent` | `cover-image/{topic-slug}/` |
**AskUserQuestion** (when no preference, file path input only):
- `{article-dir}/imgs/` - Images subdirectory near article
- `{article-dir}/` - Same directory as article
- `cover-image/{topic-slug}/` - Independent directory
- Save as default - Remember this choice for future runs
### Step 2: Confirm Options ⚠️
**Purpose**: Validate all 4 dimensions + aspect ratio.
+18 -1
View File
@@ -56,8 +56,25 @@ npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "
### Article (文章)
Before posting, ask user to choose a theme using AskUserQuestion:
| Theme | Description |
|-------|-------------|
| `default` | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 (by @brzhang) |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 (by @okooo5km) |
Default: `default`. If user has already specified a theme, skip the question.
**Workflow**:
1. Generate HTML preview and print the full `htmlPath` from JSON output so user can click to preview:
```bash
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme grace
npx -y bun ${SKILL_DIR}/scripts/md-to-wechat.ts article.md --theme <chosen-theme>
```
2. Post to WeChat:
```bash
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme <chosen-theme>
```
## Detailed References
@@ -53,7 +53,7 @@ Regular paragraph with **bold** and *italic*.
## Image Handling
1. **Parse**: Images in markdown are replaced with `[[IMAGE_PLACEHOLDER_N]]`
1. **Parse**: Images in markdown are replaced with `WECHATIMGPH_N`
2. **Render**: HTML is generated with placeholders in text
3. **Paste**: HTML content is pasted into WeChat editor
4. **Replace**: For each placeholder:
@@ -81,7 +81,7 @@ Claude:
3. Opens Chrome, navigates to WeChat editor
4. Pastes HTML content
5. For each image:
- Selects [[IMAGE_PLACEHOLDER_1]]
- Selects WECHATIMGPH_1
- Scrolls into view
- Presses Backspace to delete
- Pastes image
@@ -156,7 +156,7 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
let imageCounter = 0;
const modifiedBody = body.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
const placeholder = `[[IMAGE_PLACEHOLDER_${++imageCounter}]]`;
const placeholder = `WECHATIMGPH_${++imageCounter}`;
images.push({ src, placeholder });
return placeholder;
});
@@ -224,7 +224,7 @@ Output JSON format:
"htmlPath": "/tmp/wechat-article-images/temp-article.html",
"contentImages": [
{
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
"placeholder": "WECHATIMGPH_1",
"localPath": "/tmp/wechat-image/img.png",
"originalPath": "imgs/image.png"
}
@@ -80,6 +80,30 @@ async function pasteInEditor(session: ChromeSession): Promise<void> {
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId: session.sessionId });
}
async function sendCopy(cdp?: CdpConnection, sessionId?: string): Promise<void> {
if (process.platform === 'darwin') {
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
} else if (process.platform === 'linux') {
spawnSync('xdotool', ['key', 'ctrl+c']);
} else if (cdp && sessionId) {
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
}
}
async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void> {
if (process.platform === 'darwin') {
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']);
} else if (process.platform === 'linux') {
spawnSync('xdotool', ['key', 'ctrl+v']);
} else if (cdp && sessionId) {
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
}
}
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Promise<void> {
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
const fileUrl = `file://${absolutePath}`;
@@ -110,23 +134,8 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr
}, { sessionId });
await sleep(300);
console.log('[wechat] Copying with CDP keyboard event...');
const modifiers = process.platform === 'darwin' ? 4 : 2;
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'c',
code: 'KeyC',
modifiers,
windowsVirtualKeyCode: 67
}, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'c',
code: 'KeyC',
modifiers,
windowsVirtualKeyCode: 67
}, { sessionId });
console.log('[wechat] Copying content...');
await sendCopy(cdp, sessionId);
await sleep(1000);
console.log('[wechat] Closing HTML tab...');
@@ -134,23 +143,8 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr
}
async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void> {
console.log('[wechat] Pasting with CDP keyboard event...');
const modifiers = process.platform === 'darwin' ? 4 : 2;
await session.cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86
}, { sessionId: session.sessionId });
await sleep(50);
await session.cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'v',
code: 'KeyV',
modifiers,
windowsVirtualKeyCode: 86
}, { sessionId: session.sessionId });
console.log('[wechat] Pasting content...');
await sendPaste(session.cdp, session.sessionId);
await sleep(1000);
}
@@ -67,7 +67,7 @@ Code blocks become blockquotes (X doesn't support code)
1. **Cover Image**: First image or `cover_image` from frontmatter
2. **Remote Images**: Automatically downloaded to temp directory
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format
3. **Placeholders**: Images in content use `XIMGPH_N` format
4. **Insertion**: Placeholders are found, selected, and replaced with actual images
## Markdown to HTML Script
@@ -92,7 +92,7 @@ JSON output:
"coverImage": "/path/to/cover.jpg",
"contentImages": [
{
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
"placeholder": "XIMGPH_1",
"localPath": "/tmp/x-article-images/img.png",
"blockIndex": 5
}
+5 -5
View File
@@ -280,7 +280,7 @@ export async function parseMarkdown(
let imageCounter = 0;
const { html, totalBlocks } = convertMarkdownToHtml(body, (src, alt) => {
const placeholder = `[[IMAGE_PLACEHOLDER_${++imageCounter}]]`;
const placeholder = `XIMGPH_${++imageCounter}`;
const currentBlockIndex = images.length; // Will be set properly after HTML generation
images.push({ src, alt, blockIndex: -1 }); // blockIndex set later
@@ -292,7 +292,7 @@ export async function parseMarkdown(
let blockIdx = 0;
for (const line of htmlLines) {
for (let i = 0; i < images.length; i++) {
const placeholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`;
const placeholder = `XIMGPH_${i + 1}`;
if (line.includes(placeholder)) {
images[i]!.blockIndex = blockIdx;
}
@@ -312,7 +312,7 @@ export async function parseMarkdown(
// First image becomes cover if no cover specified
if (isFirstImage && !coverImagePath) {
coverImagePath = localPath;
coverPlaceholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`;
coverPlaceholder = `XIMGPH_${i + 1}`;
isFirstImage = false;
// Don't add to contentImages, it's the cover
continue;
@@ -320,7 +320,7 @@ export async function parseMarkdown(
isFirstImage = false;
contentImages.push({
placeholder: `[[IMAGE_PLACEHOLDER_${i + 1}]]`,
placeholder: `XIMGPH_${i + 1}`,
localPath,
originalPath: img.src,
blockIndex: img.blockIndex,
@@ -331,7 +331,7 @@ export async function parseMarkdown(
let finalHtml = html;
if (coverPlaceholder) {
// Remove the placeholder and its containing <p> tag
finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder.replace(/[[\]]/g, '\\$&')}</p>\\n?`, 'g'), '');
finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder}</p>\\n?`, 'g'), '');
}
// Resolve cover image path