mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ff468a6a7 | |||
| c83d114a73 | |||
| e964100dd9 | |||
| 6c0ae7a86a | |||
| 52ade637af | |||
| 569beebdd6 | |||
| 6cbf0f4e52 | |||
| 5615e6150f | |||
| 7465f37dcf | |||
| 38a1b90db3 |
@@ -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.30.2"
|
"version": "1.31.2"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.31.2 - 2026-02-10
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix PowerShell clipboard copy failing on Windows due to `param()`/`-Path` not working with `-Command`.
|
||||||
|
- `baoyu-post-to-x`: fix PowerShell clipboard copy on Windows (same issue); fix `getScriptDir()` returning invalid path on Windows (`/C:/...` prefix).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- `baoyu-article-illustrator`: optimize SKILL.md from 197 to 150 lines (24% reduction); apply progressive disclosure pattern with concise overview and detailed references.
|
||||||
|
|
||||||
## 1.30.2 - 2026-02-06
|
## 1.30.2 - 2026-02-06
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.31.2 - 2026-02-10
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复 Windows 上 PowerShell 剪贴板复制失败的问题(`param()`/`-Path` 与 `-Command` 参数不兼容)。
|
||||||
|
- `baoyu-post-to-x`:修复 Windows 上 PowerShell 剪贴板复制(同上);修复 `getScriptDir()` 在 Windows 上返回无效路径(`/C:/...` 前缀)。
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-article-illustrator`:优化 SKILL.md 从 197 行精简至 150 行(减少 24%);采用渐进式披露模式,主文件提供简洁概览,详细内容通过引用文件提供。
|
||||||
|
|
||||||
## 1.30.2 - 2026-02-06
|
## 1.30.2 - 2026-02-06
|
||||||
|
|
||||||
### 重构
|
### 重构
|
||||||
|
|||||||
@@ -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:
|
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
|
```bash
|
||||||
/baoyu-post-to-wechat 图文 --markdown article.md --images ./photos/
|
/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 贴图 --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 贴图 --title "标题" --content "内容" --image img1.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
**Article (文章)** - Full markdown/HTML with rich formatting:
|
**Article (文章)** - Full markdown/HTML with rich formatting:
|
||||||
|
|||||||
+4
-4
@@ -519,12 +519,12 @@ npx skills add jimliu/baoyu-skills
|
|||||||
|
|
||||||
发布内容到微信公众号,支持两种模式:
|
发布内容到微信公众号,支持两种模式:
|
||||||
|
|
||||||
**图文模式** - 多图配短标题和正文:
|
**贴图模式** - 多图配短标题和正文:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/baoyu-post-to-wechat 图文 --markdown article.md --images ./photos/
|
/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 贴图 --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 贴图 --title "标题" --content "内容" --image img1.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
**文章模式** - 完整 markdown/HTML 富文本格式:
|
**文章模式** - 完整 markdown/HTML 富文本格式:
|
||||||
|
|||||||
@@ -11,55 +11,34 @@ Analyze articles, identify illustration positions, generate images with Type ×
|
|||||||
|
|
||||||
| Dimension | Controls | Examples |
|
| Dimension | Controls | Examples |
|
||||||
|-----------|----------|----------|
|
|-----------|----------|----------|
|
||||||
| **Type** | Information structure, layout | infographic, scene, flowchart, comparison, framework, timeline |
|
| **Type** | Information structure | infographic, scene, flowchart, comparison, framework, timeline |
|
||||||
| **Style** | Visual aesthetics, mood | notion, warm, minimal, blueprint, watercolor, elegant |
|
| **Style** | Visual aesthetics | notion, warm, minimal, blueprint, watercolor, elegant |
|
||||||
|
|
||||||
Type × Style can be freely combined. Example: `--type infographic --style blueprint`
|
Combine freely: `--type infographic --style blueprint`
|
||||||
|
|
||||||
## Illustration Purpose
|
## Types
|
||||||
|
|
||||||
Auto-detected during content analysis. Influences type/style recommendations.
|
|
||||||
|
|
||||||
| Purpose | Description | Best Types |
|
|
||||||
|---------|-------------|------------|
|
|
||||||
| **information** | Help understand abstract concepts | infographic, flowchart, comparison |
|
|
||||||
| **visualization** | Turn abstract ideas into concrete visuals | framework, comparison, infographic |
|
|
||||||
| **imagination** | Create atmosphere, spark imagination | scene, timeline |
|
|
||||||
|
|
||||||
## Type Gallery
|
|
||||||
|
|
||||||
| Type | Best For |
|
| Type | Best For |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `infographic` | Data, metrics, technical articles |
|
| `infographic` | Data, metrics, technical |
|
||||||
| `scene` | Narratives, personal stories, emotional content |
|
| `scene` | Narratives, emotional |
|
||||||
| `flowchart` | Tutorials, workflows, processes |
|
| `flowchart` | Processes, workflows |
|
||||||
| `comparison` | Side-by-side, before/after, options |
|
| `comparison` | Side-by-side, options |
|
||||||
| `framework` | Methodologies, models, architecture |
|
| `framework` | Models, architecture |
|
||||||
| `timeline` | History, progress, evolution |
|
| `timeline` | History, evolution |
|
||||||
|
|
||||||
## Styles
|
## Styles
|
||||||
|
|
||||||
See [references/styles.md](references/styles.md) for:
|
See [references/styles.md](references/styles.md) for Core Styles, full gallery, and Type × Style compatibility.
|
||||||
- **Core Styles**: Simplified tier for quick selection (vector, minimal-flat, sci-fi, hand-drawn, editorial, scene)
|
|
||||||
- **Style Gallery**: Full 20+ style options with descriptions
|
|
||||||
- **Auto Selection**: Content signals → Type/Style recommendations
|
|
||||||
- **Compatibility Matrix**: Type × Style combinations
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
Progress:
|
- [ ] Step 1: Pre-check (EXTEND.md, references, config)
|
||||||
- [ ] Step 1: Pre-check
|
- [ ] Step 2: Analyze content
|
||||||
- [ ] 1.5 Load preferences (EXTEND.md) ⛔ BLOCKING
|
- [ ] Step 3: Confirm settings (AskUserQuestion)
|
||||||
- [ ] 1.0 Reference images ⚠️ (if provided)
|
- [ ] Step 4: Generate outline
|
||||||
- [ ] 1.2-1.4 Config questions (1 AskUserQuestion, max 4 Qs)
|
- [ ] Step 5: Generate images
|
||||||
- [ ] Step 2: Setup & Analyze
|
|
||||||
- [ ] Step 3: Confirm Settings (1 AskUserQuestion, max 4 Qs)
|
|
||||||
- [ ] Q1: Type ⚠️
|
|
||||||
- [ ] Q2: Density ⚠️ MUST ASK
|
|
||||||
- [ ] Q3: Style ⚠️
|
|
||||||
- [ ] Step 4: Generate Outline
|
|
||||||
- [ ] Step 5: Generate Images
|
|
||||||
- [ ] Step 6: Finalize
|
- [ ] Step 6: Finalize
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -67,8 +46,6 @@ Progress:
|
|||||||
|
|
||||||
**1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING**
|
**1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING**
|
||||||
|
|
||||||
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other steps.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
||||||
@@ -76,122 +53,98 @@ test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
|||||||
|
|
||||||
| Result | Action |
|
| Result | Action |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| Found | Read, parse, display summary → Continue |
|
| Found | Read, parse, display summary |
|
||||||
| Not found | ⛔ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) |
|
| Not found | ⛔ Run [first-time-setup](references/config/first-time-setup.md) |
|
||||||
|
|
||||||
**Supports**: Watermark | Preferred type/style | Custom styles | Language | Output directory
|
|
||||||
|
|
||||||
**1.0-1.4**: Handle reference images, determine input type, ask config questions.
|
|
||||||
|
|
||||||
Full procedures: [references/workflow.md](references/workflow.md#step-1-pre-check)
|
Full procedures: [references/workflow.md](references/workflow.md#step-1-pre-check)
|
||||||
|
|
||||||
---
|
### Step 2: Analyze
|
||||||
|
|
||||||
### Step 2: Setup & Analyze
|
| Analysis | Output |
|
||||||
|
|----------|--------|
|
||||||
| Analysis | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| Content type | Technical / Tutorial / Methodology / Narrative |
|
| Content type | Technical / Tutorial / Methodology / Narrative |
|
||||||
| Illustration purpose | information / visualization / imagination |
|
| Purpose | information / visualization / imagination |
|
||||||
| Core arguments | 2-5 main points to visualize |
|
| Core arguments | 2-5 main points |
|
||||||
| Visual opportunities | Positions where illustrations add value |
|
| Positions | Where illustrations add value |
|
||||||
|
|
||||||
**CRITICAL**: If article uses metaphors, do NOT illustrate literally. Visualize the **underlying concept**.
|
**CRITICAL**: Metaphors → visualize underlying concept, NOT literal image.
|
||||||
|
|
||||||
Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--analyze)
|
Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--analyze)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 3: Confirm Settings ⚠️
|
### Step 3: Confirm Settings ⚠️
|
||||||
|
|
||||||
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
|
**ONE AskUserQuestion, max 4 Qs. Q1-Q3 REQUIRED.**
|
||||||
|
|
||||||
| Question | Options |
|
| Q | Options |
|
||||||
|----------|---------|
|
|---|---------|
|
||||||
| **Q1: Type** ⚠️ | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
| **Q1: Type** | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
||||||
| **Q2: Density** ⚠️ | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
| **Q2: Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
||||||
| **Q3: Style** ⚠️ | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, Other |
|
| **Q3: Style** | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, Other |
|
||||||
| **Q4: Language** | When article language ≠ EXTEND.md setting |
|
| Q4: Language | When article language ≠ EXTEND.md setting |
|
||||||
|
|
||||||
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
|
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 4: Generate Outline
|
### Step 4: Generate Outline
|
||||||
|
|
||||||
Save as `outline.md` with frontmatter (type, density, style, image_count, references) and illustration entries:
|
Save `outline.md` with frontmatter (type, density, style, image_count) and entries:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
## Illustration 1
|
## Illustration 1
|
||||||
**Position**: [section] / [paragraph]
|
**Position**: [section/paragraph]
|
||||||
**Purpose**: [why this helps]
|
**Purpose**: [why]
|
||||||
**Visual Content**: [what to show]
|
**Visual Content**: [what]
|
||||||
**Filename**: 01-infographic-concept-name.png
|
**Filename**: 01-infographic-concept-name.png
|
||||||
```
|
```
|
||||||
|
|
||||||
Full template: [references/workflow.md](references/workflow.md#step-4-generate-outline)
|
Full template: [references/workflow.md](references/workflow.md#step-4-generate-outline)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5: Generate Images
|
### Step 5: Generate Images
|
||||||
|
|
||||||
1. **Create Prompts**: Follow [references/prompt-construction.md](references/prompt-construction.md)
|
1. Create prompts per [references/prompt-construction.md](references/prompt-construction.md)
|
||||||
2. **Select Generation Skill**: Check available skills
|
2. Select generation skill from available skills
|
||||||
3. **Process References**: Handle `direct`/`style`/`palette` usage
|
3. Process references (`direct`/`style`/`palette`)
|
||||||
4. **Apply Watermark**: If enabled in EXTEND.md
|
4. Apply watermark if EXTEND.md enabled
|
||||||
5. **Generate**: Sequential, retry once on failure
|
5. Generate sequentially, retry once on failure
|
||||||
|
|
||||||
Full procedures: [references/workflow.md](references/workflow.md#step-5-generate-images)
|
Full procedures: [references/workflow.md](references/workflow.md#step-5-generate-images)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 6: Finalize
|
### Step 6: Finalize
|
||||||
|
|
||||||
**Update Article**: Insert `` after corresponding paragraphs.
|
Insert `` after paragraphs.
|
||||||
|
|
||||||
**Output Summary**:
|
|
||||||
```
|
```
|
||||||
Article Illustration Complete!
|
Article Illustration Complete!
|
||||||
Article: [path] | Type: [type] | Density: [level] | Style: [style]
|
Article: [path] | Type: [type] | Density: [level] | Style: [style]
|
||||||
Images: X/N generated
|
Images: X/N generated
|
||||||
Positions:
|
|
||||||
- 01-xxx.png → After "[Section]"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Output Directory
|
## Output Directory
|
||||||
|
|
||||||
```
|
```
|
||||||
illustrations/{topic-slug}/
|
illustrations/{topic-slug}/
|
||||||
├── source-{slug}.{ext}
|
├── source-{slug}.{ext}
|
||||||
├── references/ # Only if references provided
|
├── references/ # if provided
|
||||||
│ └── NN-ref-{slug}.png
|
|
||||||
├── outline.md
|
├── outline.md
|
||||||
├── prompts/
|
├── prompts/
|
||||||
│ └── illustration-{slug}.md
|
|
||||||
└── NN-{type}-{slug}.png
|
└── NN-{type}-{slug}.png
|
||||||
```
|
```
|
||||||
|
|
||||||
**Slug**: 2-4 word topic in kebab-case.
|
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
|
||||||
**Conflict**: Append `-YYYYMMDD-HHMMSS` if exists.
|
|
||||||
|
|
||||||
## Modification
|
## Modification
|
||||||
|
|
||||||
| Action | Steps |
|
| Action | Steps |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| **Edit** | Update prompt file FIRST → Regenerate → Update reference |
|
| Edit | Update prompt → Regenerate → Update reference |
|
||||||
| **Add** | Identify position → Create prompt → Generate → Update outline → Insert |
|
| Add | Position → Prompt → Generate → Update outline → Insert |
|
||||||
| **Delete** | Delete files → Remove reference → Update outline |
|
| Delete | Delete files → Remove reference → Update outline |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
| File | Content |
|
| File | Content |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| [references/workflow.md](references/workflow.md) | Detailed workflow procedures |
|
| [references/workflow.md](references/workflow.md) | Detailed procedures |
|
||||||
| [references/usage.md](references/usage.md) | Command syntax and options |
|
| [references/usage.md](references/usage.md) | Command syntax |
|
||||||
| [references/styles.md](references/styles.md) | Style gallery & compatibility |
|
| [references/styles.md](references/styles.md) | Style gallery |
|
||||||
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
|
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
|
||||||
| `references/styles/<style>.md` | Full style specifications |
|
| [references/config/first-time-setup.md](references/config/first-time-setup.md) | First-time setup |
|
||||||
| `references/config/preferences-schema.md` | EXTEND.md schema |
|
|
||||||
| `references/config/first-time-setup.md` | First-time setup flow |
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-post-to-wechat
|
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
|
# 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 │
|
│ 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 (图文)
|
## Image-Text Posting (图文)
|
||||||
|
|
||||||
@@ -70,7 +97,7 @@ Publishing Progress:
|
|||||||
- [ ] Step 1: Determine input type
|
- [ ] Step 1: Determine input type
|
||||||
- [ ] Step 2: Check markdown-to-html skill
|
- [ ] Step 2: Check markdown-to-html skill
|
||||||
- [ ] Step 3: Convert to HTML
|
- [ ] 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 5: Select method and configure credentials
|
||||||
- [ ] Step 6: Publish to WeChat
|
- [ ] Step 6: Publish to WeChat
|
||||||
- [ ] Step 7: Report completion
|
- [ ] Step 7: Report completion
|
||||||
@@ -80,6 +107,13 @@ Publishing Progress:
|
|||||||
|
|
||||||
Check and load EXTEND.md settings (see Preferences section above).
|
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
|
### Step 1: Determine Input Type
|
||||||
|
|
||||||
| Input Type | Detection | Action |
|
| 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" |
|
| 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)" |
|
| 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**:
|
**Auto-Generation Logic**:
|
||||||
- **Title**: First H1/H2 heading, or first sentence
|
- **Title**: First H1/H2 heading, or first sentence
|
||||||
- **Summary**: First paragraph, truncated to 120 characters
|
- **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
|
### Step 5: Select Publishing Method and Configure
|
||||||
|
|
||||||
**Ask publishing method** (unless specified in EXTEND.md or CLI):
|
**Ask publishing method** (unless specified in EXTEND.md or CLI):
|
||||||
@@ -213,9 +255,20 @@ WECHAT_APP_SECRET=<user_input>
|
|||||||
**API method**:
|
**API method**:
|
||||||
|
|
||||||
```bash
|
```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**:
|
**Browser method**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -237,6 +290,7 @@ Article:
|
|||||||
• Title: [title]
|
• Title: [title]
|
||||||
• Summary: [summary]
|
• Summary: [summary]
|
||||||
• Images: [N] inline images
|
• Images: [N] inline images
|
||||||
|
• Comments: [open/closed], [fans-only/all users]
|
||||||
|
|
||||||
Result:
|
Result:
|
||||||
✓ Draft saved to WeChat Official Account
|
✓ Draft saved to WeChat Official Account
|
||||||
@@ -289,6 +343,8 @@ Files created:
|
|||||||
| Multiple images | ✓ (up to 9) | ✓ (inline) | ✓ (inline) |
|
| Multiple images | ✓ (up to 9) | ✓ (inline) | ✓ (inline) |
|
||||||
| Themes | ✗ | ✓ | ✓ |
|
| Themes | ✗ | ✓ | ✓ |
|
||||||
| Auto-generate metadata | ✗ | ✓ | ✓ |
|
| Auto-generate metadata | ✗ | ✓ | ✓ |
|
||||||
|
| Default cover fallback (`imgs/cover.png`) | ✗ | ✓ | ✗ |
|
||||||
|
| Comment control (`need_open_comment`, `only_fans_can_comment`) | ✗ | ✓ | ✗ |
|
||||||
| Requires Chrome | ✓ | ✗ | ✓ |
|
| Requires Chrome | ✓ | ✗ | ✓ |
|
||||||
| Requires API credentials | ✗ | ✓ | ✗ |
|
| Requires API credentials | ✗ | ✓ | ✗ |
|
||||||
| Speed | Medium | Fast | Slow |
|
| Speed | Medium | Fast | Slow |
|
||||||
@@ -322,6 +378,8 @@ Files created:
|
|||||||
| Not logged in (browser) | First run opens browser - scan QR to log in |
|
| Not logged in (browser) | First run opens browser - scan QR to log in |
|
||||||
| Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` env var |
|
| Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` env var |
|
||||||
| Title/summary missing | Use auto-generation or provide manually |
|
| 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 |
|
| Paste fails | Check system clipboard permissions |
|
||||||
|
|
||||||
## Extension Support
|
## 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.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -228,25 +228,25 @@ async function copyHtmlLinux(htmlFilePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageWindows(imagePath: string): Promise<void> {
|
async function copyImageWindows(imagePath: string): Promise<void> {
|
||||||
|
const escaped = imagePath.replace(/'/g, "''");
|
||||||
const ps = [
|
const ps = [
|
||||||
'param([string]$Path)',
|
|
||||||
'Add-Type -AssemblyName System.Windows.Forms',
|
'Add-Type -AssemblyName System.Windows.Forms',
|
||||||
'Add-Type -AssemblyName System.Drawing',
|
'Add-Type -AssemblyName System.Drawing',
|
||||||
'$img = [System.Drawing.Image]::FromFile($Path)',
|
`$img = [System.Drawing.Image]::FromFile('${escaped}')`,
|
||||||
'[System.Windows.Forms.Clipboard]::SetImage($img)',
|
'[System.Windows.Forms.Clipboard]::SetImage($img)',
|
||||||
'$img.Dispose()',
|
'$img.Dispose()',
|
||||||
].join('; ');
|
].join('; ');
|
||||||
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps, '-Path', imagePath]);
|
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyHtmlWindows(htmlFilePath: string): Promise<void> {
|
async function copyHtmlWindows(htmlFilePath: string): Promise<void> {
|
||||||
|
const escaped = htmlFilePath.replace(/'/g, "''");
|
||||||
const ps = [
|
const ps = [
|
||||||
'param([string]$Path)',
|
|
||||||
'Add-Type -AssemblyName System.Windows.Forms',
|
'Add-Type -AssemblyName System.Windows.Forms',
|
||||||
'$html = Get-Content -Raw -LiteralPath $Path',
|
`$html = Get-Content -Raw -LiteralPath '${escaped}'`,
|
||||||
'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',
|
'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',
|
||||||
].join('; ');
|
].join('; ');
|
||||||
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps, '-Path', htmlFilePath]);
|
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageToClipboard(imagePathInput: string): Promise<void> {
|
async function copyImageToClipboard(imagePathInput: string): Promise<void> {
|
||||||
|
|||||||
@@ -517,6 +517,13 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
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}`);
|
console.error(`[wechat-api] Title: ${title}`);
|
||||||
if (author) console.error(`[wechat-api] Author: ${author}`);
|
if (author) console.error(`[wechat-api] Author: ${author}`);
|
||||||
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
|
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
|
||||||
@@ -547,11 +554,14 @@ async function main(): Promise<void> {
|
|||||||
htmlContent = processedHtml;
|
htmlContent = processedHtml;
|
||||||
|
|
||||||
let thumbMediaId = "";
|
let thumbMediaId = "";
|
||||||
const coverPath = args.cover ||
|
const rawCoverPath = args.cover ||
|
||||||
frontmatter.featureImage ||
|
frontmatter.featureImage ||
|
||||||
frontmatter.coverImage ||
|
frontmatter.coverImage ||
|
||||||
frontmatter.cover ||
|
frontmatter.cover ||
|
||||||
frontmatter.image;
|
frontmatter.image;
|
||||||
|
const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
|
||||||
|
? path.resolve(process.cwd(), rawCoverPath)
|
||||||
|
: rawCoverPath;
|
||||||
|
|
||||||
if (coverPath) {
|
if (coverPath) {
|
||||||
console.error(`[wechat-api] Uploading cover: ${coverPath}`);
|
console.error(`[wechat-api] Uploading cover: ${coverPath}`);
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
|
|
||||||
await sleep(2000);
|
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', {
|
const menuResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
const menuItems = document.querySelectorAll('.new-creation__menu .new-creation__menu-item');
|
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));
|
const initialIds = new Set(initialTargets.targetInfos.map(t => t.targetId));
|
||||||
console.log(`[wechat-browser] Initial targets count: ${initialTargets.targetInfos.length}`);
|
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', {
|
const menuPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
@@ -427,10 +427,10 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
const title = item.querySelector('.new-creation__menu-title');
|
const title = item.querySelector('.new-creation__menu-title');
|
||||||
const text = title?.textContent?.trim() || '';
|
const text = title?.textContent?.trim() || '';
|
||||||
console.log('Menu item text:', text);
|
console.log('Menu item text:', text);
|
||||||
if (text === '图文') {
|
if (text === '图文' || text === '贴图') {
|
||||||
item.scrollIntoView({ block: 'center' });
|
item.scrollIntoView({ block: 'center' });
|
||||||
const rect = item.getBoundingClientRect();
|
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 });
|
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}`);
|
console.log(`[wechat-browser] Menu position: ${menuPos.result.value}`);
|
||||||
|
|
||||||
const pos = menuPos.result.value !== 'null' ? JSON.parse(menuPos.result.value) : null;
|
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', {
|
await cdp.send('Input.dispatchMouseEvent', {
|
||||||
type: 'mousePressed',
|
type: 'mousePressed',
|
||||||
x: pos.x,
|
x: pos.x,
|
||||||
@@ -533,11 +533,22 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
|
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
|
||||||
|
|
||||||
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId });
|
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,
|
nodeId: root.nodeId,
|
||||||
selector: '.js_upload_btn_container input[type=file]',
|
selector: '.js_upload_btn_container input[type=file]',
|
||||||
}, { sessionId });
|
}, { 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');
|
if (!nodeId) throw new Error('File input not found');
|
||||||
|
|
||||||
await cdp.send('DOM.setFileInputFiles', {
|
await cdp.send('DOM.setFileInputFiles', {
|
||||||
@@ -545,7 +556,30 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
files: absolutePaths,
|
files: absolutePaths,
|
||||||
}, { sessionId });
|
}, { 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...');
|
console.log('[wechat-browser] Filling title...');
|
||||||
await cdp.send('Runtime.evaluate', {
|
await cdp.send('Runtime.evaluate', {
|
||||||
@@ -561,73 +595,134 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
console.log('[wechat-browser] Clicking on content editor...');
|
console.log('[wechat-browser] Filling content...');
|
||||||
const editorPos = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
// Try ProseMirror editor first (new WeChat UI), then fallback to old editor
|
||||||
|
const contentResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.js_pmEditorArea');
|
const contentHtml = ${JSON.stringify('<p>' + content.split('\n').filter(l => l.trim()).join('</p><p>') + '</p>')};
|
||||||
if (editor) {
|
|
||||||
const rect = editor.getBoundingClientRect();
|
// New UI: ProseMirror contenteditable
|
||||||
return JSON.stringify({ x: rect.x + 50, y: rect.y + 20 });
|
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,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
if (editorPos.result.value === 'null') throw new Error('Content editor not found');
|
const contentStatus = contentResult.result.value;
|
||||||
const editorClickPos = JSON.parse(editorPos.result.value);
|
console.log(`[wechat-browser] Content result: ${contentStatus}`);
|
||||||
|
|
||||||
await cdp.send('Input.dispatchMouseEvent', {
|
if (contentStatus === 'editor_not_found') {
|
||||||
type: 'mousePressed',
|
throw new Error('Content editor not found');
|
||||||
x: editorClickPos.x,
|
}
|
||||||
y: editorClickPos.y,
|
|
||||||
button: 'left',
|
// Fallback: old editor uses keyboard simulation
|
||||||
clickCount: 1,
|
if (contentStatus.startsWith('{')) {
|
||||||
}, { sessionId });
|
const editorClickPos = JSON.parse(contentStatus);
|
||||||
await sleep(50);
|
if (editorClickPos.type === 'old') {
|
||||||
await cdp.send('Input.dispatchMouseEvent', {
|
console.log('[wechat-browser] Using old editor with keyboard simulation...');
|
||||||
type: 'mouseReleased',
|
await cdp.send('Input.dispatchMouseEvent', {
|
||||||
x: editorClickPos.x,
|
type: 'mousePressed',
|
||||||
y: editorClickPos.y,
|
x: editorClickPos.x,
|
||||||
button: 'left',
|
y: editorClickPos.y,
|
||||||
clickCount: 1,
|
button: 'left',
|
||||||
}, { sessionId });
|
clickCount: 1,
|
||||||
await sleep(300);
|
}, { sessionId });
|
||||||
|
await sleep(50);
|
||||||
console.log('[wechat-browser] Typing content with keyboard simulation...');
|
await cdp.send('Input.dispatchMouseEvent', {
|
||||||
const lines = content.split('\n');
|
type: 'mouseReleased',
|
||||||
for (let i = 0; i < lines.length; i++) {
|
x: editorClickPos.x,
|
||||||
const line = lines[i];
|
y: editorClickPos.y,
|
||||||
if (line.length > 0) {
|
button: 'left',
|
||||||
await cdp.send('Input.insertText', { text: line }, { sessionId });
|
clickCount: 1,
|
||||||
}
|
}, { sessionId });
|
||||||
if (i < lines.length - 1) {
|
await sleep(300);
|
||||||
await cdp.send('Input.dispatchKeyEvent', {
|
|
||||||
type: 'keyDown',
|
const lines = content.split('\n');
|
||||||
key: 'Enter',
|
for (let i = 0; i < lines.length; i++) {
|
||||||
code: 'Enter',
|
const line = lines[i];
|
||||||
windowsVirtualKeyCode: 13,
|
if (line!.length > 0) {
|
||||||
}, { sessionId });
|
await cdp.send('Input.insertText', { text: line }, { sessionId });
|
||||||
await cdp.send('Input.dispatchKeyEvent', {
|
}
|
||||||
type: 'keyUp',
|
if (i < lines.length - 1) {
|
||||||
key: 'Enter',
|
await cdp.send('Input.dispatchKeyEvent', {
|
||||||
code: 'Enter',
|
type: 'keyDown',
|
||||||
windowsVirtualKeyCode: 13,
|
key: 'Enter',
|
||||||
}, { sessionId });
|
code: 'Enter',
|
||||||
}
|
windowsVirtualKeyCode: 13,
|
||||||
await sleep(50);
|
}, { 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);
|
await sleep(500);
|
||||||
|
|
||||||
if (submit) {
|
if (submit) {
|
||||||
console.log('[wechat-browser] Saving as draft...');
|
console.log('[wechat-browser] Saving as draft...');
|
||||||
await cdp.send('Runtime.evaluate', {
|
const submitResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `document.querySelector('#js_submit')?.click()`,
|
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 });
|
}, { sessionId });
|
||||||
|
console.log(`[wechat-browser] Submit result: ${submitResult.result.value}`);
|
||||||
await sleep(3000);
|
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!');
|
console.log('[wechat-browser] Draft saved!');
|
||||||
} else {
|
} else {
|
||||||
console.log('[wechat-browser] Article composed (preview mode). Add --submit to save as draft.');
|
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 {
|
function printUsage(): never {
|
||||||
console.log(`Post image-text (图文) to WeChat Official Account
|
console.log(`Post image-text (贴图) to WeChat Official Account
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
npx -y bun wechat-browser.ts [options]
|
npx -y bun wechat-browser.ts [options]
|
||||||
|
|||||||
@@ -228,25 +228,25 @@ async function copyHtmlLinux(htmlFilePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageWindows(imagePath: string): Promise<void> {
|
async function copyImageWindows(imagePath: string): Promise<void> {
|
||||||
|
const escaped = imagePath.replace(/'/g, "''");
|
||||||
const ps = [
|
const ps = [
|
||||||
'param([string]$Path)',
|
|
||||||
'Add-Type -AssemblyName System.Windows.Forms',
|
'Add-Type -AssemblyName System.Windows.Forms',
|
||||||
'Add-Type -AssemblyName System.Drawing',
|
'Add-Type -AssemblyName System.Drawing',
|
||||||
'$img = [System.Drawing.Image]::FromFile($Path)',
|
`$img = [System.Drawing.Image]::FromFile('${escaped}')`,
|
||||||
'[System.Windows.Forms.Clipboard]::SetImage($img)',
|
'[System.Windows.Forms.Clipboard]::SetImage($img)',
|
||||||
'$img.Dispose()',
|
'$img.Dispose()',
|
||||||
].join('; ');
|
].join('; ');
|
||||||
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps, '-Path', imagePath]);
|
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyHtmlWindows(htmlFilePath: string): Promise<void> {
|
async function copyHtmlWindows(htmlFilePath: string): Promise<void> {
|
||||||
|
const escaped = htmlFilePath.replace(/'/g, "''");
|
||||||
const ps = [
|
const ps = [
|
||||||
'param([string]$Path)',
|
|
||||||
'Add-Type -AssemblyName System.Windows.Forms',
|
'Add-Type -AssemblyName System.Windows.Forms',
|
||||||
'$html = Get-Content -Raw -LiteralPath $Path',
|
`$html = Get-Content -Raw -LiteralPath '${escaped}'`,
|
||||||
'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',
|
'[System.Windows.Forms.Clipboard]::SetText($html, [System.Windows.Forms.TextDataFormat]::Html)',
|
||||||
].join('; ');
|
].join('; ');
|
||||||
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps, '-Path', htmlFilePath]);
|
await runCommand('powershell.exe', ['-NoProfile', '-Sta', '-Command', ps]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageToClipboard(imagePathInput: string): Promise<void> {
|
async function copyImageToClipboard(imagePathInput: string): Promise<void> {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
console.log(`[x-article] Reusing existing Chrome instance on port ${port}`);
|
console.log(`[x-article] Reusing existing Chrome instance on port ${port}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[x-article] Launching Chrome...`);
|
console.log(`[x-article] Launching Chrome...`);
|
||||||
spawn(chromePath, [
|
const chromeArgs = [
|
||||||
`--remote-debugging-port=${port}`,
|
`--remote-debugging-port=${port}`,
|
||||||
`--user-data-dir=${profileDir}`,
|
`--user-data-dir=${profileDir}`,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
@@ -117,7 +117,13 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
'--disable-blink-features=AutomationControlled',
|
'--disable-blink-features=AutomationControlled',
|
||||||
'--start-maximized',
|
'--start-maximized',
|
||||||
X_ARTICLES_URL,
|
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;
|
let cdp: CdpConnection | null = null;
|
||||||
@@ -732,7 +738,8 @@ async function main(): Promise<void> {
|
|||||||
if (arg === '--title' && args[i + 1]) {
|
if (arg === '--title' && args[i + 1]) {
|
||||||
title = args[++i];
|
title = args[++i];
|
||||||
} else if (arg === '--cover' && args[i + 1]) {
|
} 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') {
|
} else if (arg === '--submit') {
|
||||||
submit = true;
|
submit = true;
|
||||||
} else if (arg === '--profile' && args[i + 1]) {
|
} else if (arg === '--profile' && args[i + 1]) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import net from 'node:net';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
export type PlatformCandidates = {
|
export type PlatformCandidates = {
|
||||||
darwin?: string[];
|
darwin?: string[];
|
||||||
@@ -216,7 +217,7 @@ export class CdpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getScriptDir(): string {
|
export function getScriptDir(): string {
|
||||||
return path.dirname(new URL(import.meta.url).pathname);
|
return path.dirname(fileURLToPath(import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
function runBunScript(scriptPath: string, args: string[]): boolean {
|
function runBunScript(scriptPath: string, args: string[]): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user