Compare commits

...

5 Commits

Author SHA1 Message Date
Jim Liu 宝玉 6c0ae7a86a chore: release v1.31.1 2026-02-10 15:50:31 -06:00
Jim Liu 宝玉 52ade637af fix(baoyu-post-to-x): fix Chrome launch on macOS and cover image path resolution
Use open -na on macOS to avoid process blocking; resolve relative cover image
paths to absolute paths.
2026-02-10 15:46:14 -06:00
Jim Liu 宝玉 569beebdd6 feat(baoyu-post-to-wechat): adapt to new WeChat UI and fix digest/cover handling
Rename 图文 to 贴图 throughout; add ProseMirror editor support with old editor
fallback; add fallback file input selector; add upload progress monitoring;
improve save button detection with toast verification; truncate digest > 120
chars at punctuation boundary; fix cover image relative path resolution.
2026-02-10 15:46:09 -06:00
Jim Liu 宝玉 6cbf0f4e52 chore: release v1.31.0 2026-02-07 15:51:09 -06:00
Jim Liu 宝玉 5615e6150f feat(baoyu-post-to-wechat): add comment control, cover fallback chain, and first-time setup 2026-02-07 15:50:23 -06:00
11 changed files with 425 additions and 79 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.30.3"
"version": "1.31.1"
},
"plugins": [
{
+14
View File
@@ -2,6 +2,20 @@
English | [中文](./CHANGELOG.zh.md)
## 1.31.1 - 2026-02-10
### Features
- `baoyu-post-to-wechat`: adapt to new WeChat UI — rename 图文 to 贴图; add ProseMirror editor support with old editor fallback; add fallback file input selector; add upload progress monitoring; improve save button detection with toast verification.
### Fixes
- `baoyu-post-to-wechat`: truncate digest > 120 chars at punctuation boundary; fix cover image relative path resolution.
- `baoyu-post-to-x`: fix Chrome launch on macOS via `open -na`; fix cover image relative path resolution.
## 1.31.0 - 2026-02-07
### Features
- `baoyu-post-to-wechat`: add comment control settings (`need_open_comment`, `only_fans_can_comment`); add cover image fallback chain (CLI → frontmatter → `imgs/cover.png` → first inline image); add author resolution priority; add first-time setup flow with EXTEND.md preferences.
## 1.30.3 - 2026-02-06
### Refactor
+14
View File
@@ -2,6 +2,20 @@
[English](./CHANGELOG.md) | 中文
## 1.31.1 - 2026-02-10
### 新功能
- `baoyu-post-to-wechat`:适配微信新版 UI — 图文更名为贴图;新增 ProseMirror 编辑器支持(兼容旧版编辑器);新增备用文件上传选择器;新增上传进度监控;改进保存按钮检测并增加 toast 验证。
### 修复
- `baoyu-post-to-wechat`:摘要超过 120 字符时在标点处截断;修复封面图片相对路径解析。
- `baoyu-post-to-x`:修复 macOS 上 Chrome 启动问题(使用 `open -na`);修复封面图片相对路径解析。
## 1.31.0 - 2026-02-07
### 新功能
- `baoyu-post-to-wechat`:新增评论控制设置(`need_open_comment``only_fans_can_comment`);新增封面图片回退链(CLI → frontmatter → `imgs/cover.png` → 首张内联图片);新增作者优先级解析;新增首次使用引导流程和 EXTEND.md 偏好配置。
## 1.30.3 - 2026-02-06
### 重构
+4 -4
View File
@@ -519,12 +519,12 @@ Post content and articles to X (Twitter). Supports regular posts with images and
Post content to WeChat Official Account (微信公众号). Two modes available:
**Image-Text (图)** - Multiple images with short title/content:
**Image-Text (图)** - Multiple images with short title/content:
```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
```
**Article (文章)** - Full markdown/HTML with rich formatting:
+4 -4
View File
@@ -519,12 +519,12 @@ npx skills add jimliu/baoyu-skills
发布内容到微信公众号,支持两种模式:
**图模式** - 多图配短标题和正文:
**图模式** - 多图配短标题和正文:
```bash
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
/baoyu-post-to-wechat 图 --markdown article.md --images ./photos/
/baoyu-post-to-wechat 图 --markdown article.md --image img1.png --image img2.png --image img3.png
/baoyu-post-to-wechat 图 --title "标题" --content "内容" --image img1.png --submit
```
**文章模式** - 完整 markdown/HTML 富文本格式:
+63 -5
View File
@@ -1,6 +1,6 @@
---
name: baoyu-post-to-wechat
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (图文) with multiple images. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "图文/文章".
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
---
# Post to WeChat Official Account
@@ -44,10 +44,37 @@ test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && echo "user"
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Found │ Read, parse, apply settings │
├───────────┼───────────────────────────────────────────────────────────────────────────┤
│ Not found │ Use defaults
│ Not found │ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Save → Continue
└───────────┴───────────────────────────────────────────────────────────────────────────┘
**EXTEND.md Supports**: Default theme | Default publishing method (api/browser) | Default author | Chrome profile path
**EXTEND.md Supports**: Default theme | Default publishing method (api/browser) | Default author | Default open-comment switch | Default fans-only-comment switch | Chrome profile path
First-time setup: [references/config/first-time-setup.md](references/config/first-time-setup.md)
**Minimum supported keys** (case-insensitive, accept `1/0` or `true/false`):
| Key | Default | Mapping |
|-----|---------|---------|
| `default_author` | empty | Fallback for `author` when CLI/frontmatter not provided |
| `need_open_comment` | `1` | `articles[].need_open_comment` in `draft/add` request |
| `only_fans_can_comment` | `0` | `articles[].only_fans_can_comment` in `draft/add` request |
**Recommended EXTEND.md example**:
```md
default_theme: default
default_publish_method: api
default_author: 宝玉
need_open_comment: 1
only_fans_can_comment: 0
chrome_profile_path: /path/to/chrome/profile
```
**Value priority**:
1. CLI arguments
2. Frontmatter
3. EXTEND.md
4. Skill defaults
## Image-Text Posting (图文)
@@ -70,7 +97,7 @@ Publishing Progress:
- [ ] Step 1: Determine input type
- [ ] Step 2: Check markdown-to-html skill
- [ ] Step 3: Convert to HTML
- [ ] Step 4: Validate metadata (title, summary)
- [ ] Step 4: Validate metadata (title, summary, cover)
- [ ] Step 5: Select method and configure credentials
- [ ] Step 6: Publish to WeChat
- [ ] Step 7: Report completion
@@ -80,6 +107,13 @@ Publishing Progress:
Check and load EXTEND.md settings (see Preferences section above).
**CRITICAL**: If not found, complete first-time setup BEFORE any other steps or questions.
Resolve and store these defaults for later steps:
- `default_author`
- `need_open_comment` (default `1`)
- `only_fans_can_comment` (default `0`)
### Step 1: Determine Input Type
| Input Type | Detection | Action |
@@ -162,11 +196,19 @@ Check extracted metadata from Step 3 (or HTML meta tags if direct HTML input).
|-------|------------|
| Title | Prompt: "Enter title, or press Enter to auto-generate from content" |
| Summary | Prompt: "Enter summary, or press Enter to auto-generate (recommended for SEO)" |
| Author | Use fallback chain: CLI `--author` → frontmatter `author` → EXTEND.md `default_author` |
**Auto-Generation Logic**:
- **Title**: First H1/H2 heading, or first sentence
- **Summary**: First paragraph, truncated to 120 characters
**Cover Image Check** (required for `article_type=news`):
1. Use CLI `--cover` if provided.
2. Else use frontmatter (`featureImage`, `coverImage`, `cover`, `image`).
3. Else check article directory default path: `imgs/cover.png`.
4. Else fallback to first inline content image.
5. If still missing, stop and request a cover image before publishing.
### Step 5: Select Publishing Method and Configure
**Ask publishing method** (unless specified in EXTEND.md or CLI):
@@ -213,9 +255,20 @@ WECHAT_APP_SECRET=<user_input>
**API method**:
```bash
npx -y bun ${SKILL_DIR}/scripts/wechat-api.ts <html_file> [--title <title>] [--summary <summary>]
npx -y bun ${SKILL_DIR}/scripts/wechat-api.ts <html_file> [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>]
```
**`draft/add` payload rules**:
- Use endpoint: `POST https://api.weixin.qq.com/cgi-bin/draft/add?access_token=ACCESS_TOKEN`
- `article_type`: `news` (default) or `newspic`
- For `news`, include `thumb_media_id` (cover is required)
- Always resolve and send:
- `need_open_comment` (default `1`)
- `only_fans_can_comment` (default `0`)
- `author` resolution: CLI `--author` → frontmatter `author` → EXTEND.md `default_author`
If script parameters do not expose the two comment fields, still ensure final API request body includes resolved values.
**Browser method**:
```bash
@@ -237,6 +290,7 @@ Article:
• Title: [title]
• Summary: [summary]
• Images: [N] inline images
• Comments: [open/closed], [fans-only/all users]
Result:
✓ Draft saved to WeChat Official Account
@@ -289,6 +343,8 @@ Files created:
| Multiple images | ✓ (up to 9) | ✓ (inline) | ✓ (inline) |
| Themes | ✗ | ✓ | ✓ |
| Auto-generate metadata | ✗ | ✓ | ✓ |
| Default cover fallback (`imgs/cover.png`) | ✗ | ✓ | ✗ |
| Comment control (`need_open_comment`, `only_fans_can_comment`) | ✗ | ✓ | ✗ |
| Requires Chrome | ✓ | ✗ | ✓ |
| Requires API credentials | ✗ | ✓ | ✗ |
| Speed | Medium | Fast | Slow |
@@ -322,6 +378,8 @@ Files created:
| Not logged in (browser) | First run opens browser - scan QR to log in |
| Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` env var |
| Title/summary missing | Use auto-generation or provide manually |
| No cover image | Add frontmatter cover or place `imgs/cover.png` in article directory |
| Wrong comment defaults | Check `EXTEND.md` keys `need_open_comment` and `only_fans_can_comment` |
| Paste fails | Check system clipboard permissions |
## Extension Support
@@ -0,0 +1,146 @@
---
name: first-time-setup
description: First-time setup flow for baoyu-post-to-wechat preferences
---
# First-Time Setup
## Overview
When no EXTEND.md is found, guide user through preference setup.
**BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
- Ask about content or files to publish
- Ask about themes or publishing methods
- Proceed to content conversion or publishing
ONLY ask the questions in this setup flow, save EXTEND.md, then continue.
## Setup Flow
```
No EXTEND.md found
|
v
+---------------------+
| AskUserQuestion |
| (all questions) |
+---------------------+
|
v
+---------------------+
| Create EXTEND.md |
+---------------------+
|
v
Continue to Step 1
```
## Questions
**Language**: Use user's input language or saved language preference.
Use AskUserQuestion with ALL questions in ONE call:
### Question 1: Default Theme
```yaml
header: "Theme"
question: "Default theme for article conversion?"
options:
- label: "default (Recommended)"
description: "Classic layout - centered title with border, white-on-color H2"
- label: "grace"
description: "Elegant - text shadows, rounded cards, refined blockquotes"
- label: "simple"
description: "Minimal modern - asymmetric rounded corners, clean whitespace"
```
### Question 2: Default Publishing Method
```yaml
header: "Method"
question: "Default publishing method?"
options:
- label: "api (Recommended)"
description: "Fast, requires API credentials (AppID + AppSecret)"
- label: "browser"
description: "Slow, requires Chrome and login session"
```
### Question 3: Default Author
```yaml
header: "Author"
question: "Default author name for articles?"
options:
- label: "No default"
description: "Leave empty, specify per article"
```
Note: User will likely choose "Other" to type their author name.
### Question 4: Open Comments
```yaml
header: "Comments"
question: "Enable comments on articles by default?"
options:
- label: "Yes (Recommended)"
description: "Allow readers to comment on articles"
- label: "No"
description: "Disable comments by default"
```
### Question 5: Fans-Only Comments
```yaml
header: "Fans only"
question: "Restrict comments to followers only?"
options:
- label: "No (Recommended)"
description: "All readers can comment"
- label: "Yes"
description: "Only followers can comment"
```
### Question 6: Save Location
```yaml
header: "Save"
question: "Where to save preferences?"
options:
- label: "Project (Recommended)"
description: ".baoyu-skills/ (this project only)"
- label: "User"
description: "~/.baoyu-skills/ (all projects)"
```
## Save Locations
| Choice | Path | Scope |
|--------|------|-------|
| Project | `.baoyu-skills/baoyu-post-to-wechat/EXTEND.md` | Current project |
| User | `~/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md` | All projects |
## After Setup
1. Create directory if needed
2. Write EXTEND.md
3. Confirm: "Preferences saved to [path]"
4. Continue to Step 0 (load the saved preferences)
## EXTEND.md Template
```md
default_theme: [default/grace/simple]
default_publish_method: [api/browser]
default_author: [author name or empty]
need_open_comment: [1/0]
only_fans_can_comment: [1/0]
chrome_profile_path:
```
## Modifying Preferences Later
Users can edit EXTEND.md directly or delete it to trigger setup again.
@@ -1,7 +1,9 @@
# Image-Text Posting (图文发表)
# Image-Text Posting (贴图发表, formerly 图文)
Post image-text messages with multiple images to WeChat Official Account.
> **Note**: WeChat has renamed "图文" to "贴图" in the Official Account menu (as of 2026).
## Usage
```bash
@@ -517,6 +517,13 @@ async function main(): Promise<void> {
process.exit(1);
}
if (digest && digest.length > 120) {
const truncated = digest.slice(0, 117);
const lastPunct = Math.max(truncated.lastIndexOf("。"), truncated.lastIndexOf(""), truncated.lastIndexOf(""), truncated.lastIndexOf("、"));
digest = lastPunct > 80 ? truncated.slice(0, lastPunct + 1) : truncated + "...";
console.error(`[wechat-api] Digest truncated to ${digest.length} chars`);
}
console.error(`[wechat-api] Title: ${title}`);
if (author) console.error(`[wechat-api] Author: ${author}`);
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
@@ -547,11 +554,14 @@ async function main(): Promise<void> {
htmlContent = processedHtml;
let thumbMediaId = "";
const coverPath = args.cover ||
const rawCoverPath = args.cover ||
frontmatter.featureImage ||
frontmatter.coverImage ||
frontmatter.cover ||
frontmatter.image;
const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
? path.resolve(process.cwd(), rawCoverPath)
: rawCoverPath;
if (coverPath) {
console.error(`[wechat-api] Uploading cover: ${coverPath}`);
@@ -397,7 +397,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
await sleep(2000);
console.log('[wechat-browser] Looking for "图" menu...');
console.log('[wechat-browser] Looking for "图" menu...');
const menuResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
const menuItems = document.querySelectorAll('.new-creation__menu .new-creation__menu-item');
@@ -417,7 +417,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
const initialIds = new Set(initialTargets.targetInfos.map(t => t.targetId));
console.log(`[wechat-browser] Initial targets count: ${initialTargets.targetInfos.length}`);
console.log('[wechat-browser] Finding "图" menu position...');
console.log('[wechat-browser] Finding "图" menu position...');
const menuPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
(function() {
@@ -427,10 +427,10 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
const title = item.querySelector('.new-creation__menu-title');
const text = title?.textContent?.trim() || '';
console.log('Menu item text:', text);
if (text === '图文') {
if (text === '图文' || text === '贴图') {
item.scrollIntoView({ block: 'center' });
const rect = item.getBoundingClientRect();
console.log('Found 图rect:', JSON.stringify(rect));
console.log('Found 图,rect:', JSON.stringify(rect));
return JSON.stringify({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, width: rect.width, height: rect.height });
}
}
@@ -442,9 +442,9 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
console.log(`[wechat-browser] Menu position: ${menuPos.result.value}`);
const pos = menuPos.result.value !== 'null' ? JSON.parse(menuPos.result.value) : null;
if (!pos) throw new Error('图 menu not found or not visible');
if (!pos) throw new Error('图 menu not found or not visible');
console.log('[wechat-browser] Clicking "图" menu with mouse events...');
console.log('[wechat-browser] Clicking "图" menu with mouse events...');
await cdp.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: pos.x,
@@ -533,11 +533,22 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId });
const { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
// Try primary selector, then fallback to any multi-file image input
let { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
nodeId: root.nodeId,
selector: '.js_upload_btn_container input[type=file]',
}, { sessionId });
if (!nodeId) {
console.log('[wechat-browser] Primary file input not found, trying fallback selector...');
const fallback = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
nodeId: root.nodeId,
selector: 'input[type=file][multiple][accept*="image"]',
}, { sessionId });
nodeId = fallback.nodeId;
}
if (!nodeId) throw new Error('File input not found');
await cdp.send('DOM.setFileInputFiles', {
@@ -545,7 +556,30 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
files: absolutePaths,
}, { sessionId });
await sleep(1000);
// Dispatch change event to trigger the upload
await cdp.send('Runtime.evaluate', {
expression: `
const fileInput = document.querySelector('.js_upload_btn_container input[type=file]') || document.querySelector('input[type=file][multiple][accept*="image"]');
if (fileInput) fileInput.dispatchEvent(new Event('change', { bubbles: true }));
`,
}, { sessionId });
// Wait for images to upload
console.log('[wechat-browser] Waiting for images to upload...');
const targetCount = absolutePaths.length;
for (let i = 0; i < 30; i++) {
await sleep(2000);
const uploadCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
const thumbs = document.querySelectorAll('.weui-desktop-upload__thumb, .pic_item, [class*=upload_thumb]');
JSON.stringify({ uploaded: thumbs.length });
`,
returnByValue: true,
}, { sessionId });
const status = JSON.parse(uploadCheck.result.value);
console.log(`[wechat-browser] Upload progress: ${status.uploaded}/${targetCount}`);
if (status.uploaded >= targetCount) break;
}
console.log('[wechat-browser] Filling title...');
await cdp.send('Runtime.evaluate', {
@@ -561,73 +595,134 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
}, { sessionId });
await sleep(500);
console.log('[wechat-browser] Clicking on content editor...');
const editorPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
console.log('[wechat-browser] Filling content...');
// Try ProseMirror editor first (new WeChat UI), then fallback to old editor
const contentResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
(function() {
const editor = document.querySelector('.js_pmEditorArea');
if (editor) {
const rect = editor.getBoundingClientRect();
return JSON.stringify({ x: rect.x + 50, y: rect.y + 20 });
const contentHtml = ${JSON.stringify('<p>' + content.split('\n').filter(l => l.trim()).join('</p><p>') + '</p>')};
// New UI: ProseMirror contenteditable
const pm = document.querySelector('.ProseMirror[contenteditable=true]');
if (pm) {
pm.innerHTML = contentHtml;
pm.dispatchEvent(new Event('input', { bubbles: true }));
return 'ProseMirror: content set, length=' + pm.textContent.length;
}
return 'null';
// Old UI: .js_pmEditorArea
const oldEditor = document.querySelector('.js_pmEditorArea');
if (oldEditor) {
return JSON.stringify({ type: 'old', x: oldEditor.getBoundingClientRect().x + 50, y: oldEditor.getBoundingClientRect().y + 20 });
}
return 'editor_not_found';
})()
`,
returnByValue: true,
}, { sessionId });
if (editorPos.result.value === 'null') throw new Error('Content editor not found');
const editorClickPos = JSON.parse(editorPos.result.value);
const contentStatus = contentResult.result.value;
console.log(`[wechat-browser] Content result: ${contentStatus}`);
await cdp.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: editorClickPos.x,
y: editorClickPos.y,
button: 'left',
clickCount: 1,
}, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x: editorClickPos.x,
y: editorClickPos.y,
button: 'left',
clickCount: 1,
}, { sessionId });
await sleep(300);
console.log('[wechat-browser] Typing content with keyboard simulation...');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.length > 0) {
await cdp.send('Input.insertText', { text: line }, { sessionId });
}
if (i < lines.length - 1) {
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'Enter',
code: 'Enter',
windowsVirtualKeyCode: 13,
}, { sessionId });
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'Enter',
code: 'Enter',
windowsVirtualKeyCode: 13,
}, { sessionId });
}
await sleep(50);
if (contentStatus === 'editor_not_found') {
throw new Error('Content editor not found');
}
// Fallback: old editor uses keyboard simulation
if (contentStatus.startsWith('{')) {
const editorClickPos = JSON.parse(contentStatus);
if (editorClickPos.type === 'old') {
console.log('[wechat-browser] Using old editor with keyboard simulation...');
await cdp.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: editorClickPos.x,
y: editorClickPos.y,
button: 'left',
clickCount: 1,
}, { sessionId });
await sleep(50);
await cdp.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x: editorClickPos.x,
y: editorClickPos.y,
button: 'left',
clickCount: 1,
}, { sessionId });
await sleep(300);
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line!.length > 0) {
await cdp.send('Input.insertText', { text: line }, { sessionId });
}
if (i < lines.length - 1) {
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyDown',
key: 'Enter',
code: 'Enter',
windowsVirtualKeyCode: 13,
}, { sessionId });
await cdp.send('Input.dispatchKeyEvent', {
type: 'keyUp',
key: 'Enter',
code: 'Enter',
windowsVirtualKeyCode: 13,
}, { sessionId });
}
await sleep(50);
}
console.log('[wechat-browser] Content typed via keyboard.');
}
}
console.log('[wechat-browser] Content typed.');
await sleep(500);
if (submit) {
console.log('[wechat-browser] Saving as draft...');
await cdp.send('Runtime.evaluate', {
expression: `document.querySelector('#js_submit')?.click()`,
const submitResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
(function() {
// Try new UI: find button by text
const allBtns = document.querySelectorAll('button');
for (const btn of allBtns) {
const text = btn.textContent?.trim();
if (text === '保存为草稿') {
btn.click();
return 'clicked:保存为草稿';
}
}
// Fallback: old UI selector
const oldBtn = document.querySelector('#js_submit');
if (oldBtn) {
oldBtn.click();
return 'clicked:#js_submit';
}
// List available buttons for debugging
const btnTexts = [];
allBtns.forEach(b => {
const t = b.textContent?.trim();
if (t && t.length < 20) btnTexts.push(t);
});
return 'not_found:' + btnTexts.join(',');
})()
`,
returnByValue: true,
}, { sessionId });
console.log(`[wechat-browser] Submit result: ${submitResult.result.value}`);
await sleep(3000);
// Verify save success by checking for toast
const toastCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
expression: `
const toasts = document.querySelectorAll('.weui-desktop-toast, [class*=toast]');
const msgs = [];
toasts.forEach(t => { const text = t.textContent?.trim(); if (text) msgs.push(text); });
JSON.stringify(msgs);
`,
returnByValue: true,
}, { sessionId });
console.log(`[wechat-browser] Toast messages: ${toastCheck.result.value}`);
console.log('[wechat-browser] Draft saved!');
} else {
console.log('[wechat-browser] Article composed (preview mode). Add --submit to save as draft.');
@@ -641,7 +736,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
}
function printUsage(): never {
console.log(`Post image-text (图) to WeChat Official Account
console.log(`Post image-text (图) to WeChat Official Account
Usage:
npx -y bun wechat-browser.ts [options]
+10 -3
View File
@@ -109,7 +109,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
console.log(`[x-article] Reusing existing Chrome instance on port ${port}`);
} else {
console.log(`[x-article] Launching Chrome...`);
spawn(chromePath, [
const chromeArgs = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
@@ -117,7 +117,13 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
'--disable-blink-features=AutomationControlled',
'--start-maximized',
X_ARTICLES_URL,
], { stdio: 'ignore' });
];
if (process.platform === 'darwin') {
const appPath = chromePath.replace(/\/Contents\/MacOS\/Google Chrome$/, '');
spawn('open', ['-na', appPath, '--args', ...chromeArgs], { stdio: 'ignore' });
} else {
spawn(chromePath, chromeArgs, { stdio: 'ignore' });
}
}
let cdp: CdpConnection | null = null;
@@ -732,7 +738,8 @@ async function main(): Promise<void> {
if (arg === '--title' && args[i + 1]) {
title = args[++i];
} else if (arg === '--cover' && args[i + 1]) {
coverImage = args[++i];
const raw = args[++i]!;
coverImage = path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
} else if (arg === '--submit') {
submit = true;
} else if (arg === '--profile' && args[i + 1]) {