openClaw实现微信公众号自动抓取学习并发布

6 阅读16分钟

1.抓取其他公众号

---
name: "wechat-article-downloader"
description: "Downloads WeChat official account articles via HTTP API with QR code login. Invoke when user wants to download articles from a WeChat public account, or needs to scrape WeChat articles."
---

# WeChat Article Downloader Skill

This skill provides a complete workflow to download articles from WeChat official accounts through an HTTP API.

## Prerequisites

1. The FastAPI server must be running locally on port 8000
2. User must have a WeChat account with MP (公众号平台) access
3. Python 3 with `requests` library installed

## Workflow

### Step 1: Start the Server (if not running)

```bash
python3 -m uvicorn main:app --host 0.0.0.0 --port 8000
```

Or use the provided script:
```bash
bash start_server.sh
```

### Step 2: Check Existing Login Status (Recommended)

**First, try to load credentials from local cache** to avoid re-scanning QR code:

```python
#!/usr/bin/env python3
import os
import json
import base64

AUTH_CACHE_FILE = ".auth_cache"

def load_credentials():
    """从本地缓存加载登录凭证"""
    if not os.path.exists(AUTH_CACHE_FILE):
        return None, None
    
    try:
        with open(AUTH_CACHE_FILE, "r") as f:
            encoded_str = f.read()
        
        json_str = base64.b64decode(encoded_str).decode('utf-8')
        data = json.loads(json_str)
        
        # 检查是否过期(24小时)
        if __import__('time').time() - data.get("timestamp", 0) > 86400:
            print("⚠️  登录凭证已过期(超过24小时),需要重新登录")
            return None, None
        
        print("✅ 找到有效的登录凭证,无需重新扫码!")
        return data.get("cookie"), data.get("token")
    except Exception as e:
        print(f"加载凭证失败: {e}")
        return None, None

# 尝试加载已有凭证
cookie, token = load_credentials()

if cookie and token:
    print("可以直接下载文章,跳过登录步骤")
else:
    print("需要重新登录,请执行 Step 3")
```

### Step 3: Get Login QR Code (If no valid credentials)

If no valid credentials found, get a QR code:

```bash
curl -s -X POST http://localhost:8000/api/login/init \
  -H "Content-Type: application/json" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print('Session ID:', d['session_id'])
open('qr_login.png', 'wb').write(__import__('base64').b64decode(d['qr_code']))
print('QR code saved to qr_login.png')
"
```

**Action**: Display the QR code to the user and instruct them to scan with WeChat.

**Important**: Wait for user to confirm they have scanned the QR code before proceeding.

### Step 4: Check Login Status (User confirms login)

Once user confirms they have logged in, check the status:

```bash
curl -s http://localhost:8000/api/login/status/{session_id} | python3 -m json.tool
```

Response statuses:
- `waiting` - Still waiting for scan
- `success` - Login successful, returns cookie and token
- `expired` - QR code expired (need to restart from Step 3)
- `error` - Login error

**Note**: Do NOT poll continuously. Wait for user to confirm login, then check once.

### Step 5: Save Credentials to Local Cache

After successful login, save credentials for future use:

```python
import json
import base64
import time

def save_credentials(cookie, token):
    """保存登录凭证到本地缓存"""
    try:
        data = {
            "cookie": cookie,
            "token": token,
            "timestamp": time.time()
        }
        json_str = json.dumps(data)
        encoded_str = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
        
        with open(".auth_cache", "w") as f:
            f.write(encoded_str)
        print("✅ 登录凭证已保存到 .auth_cache")
        return True
    except Exception as e:
        print(f"保存凭证失败: {e}")
        return False

# 保存凭证
save_credentials(cookie, token)
```

### Step 6: One-Click Download Articles

Create a Python script to download articles (recommended over curl for handling large base64 data).
**Note**: Downloads are saved as HTML format by default.

```python
#!/usr/bin/env python3
import json
import base64
import requests
import os

# 首先尝试加载已有凭证
def load_credentials():
    """从本地缓存加载登录凭证"""
    if not os.path.exists(".auth_cache"):
        return None, None
    
    try:
        with open(".auth_cache", "r") as f:
            encoded_str = f.read()
        
        json_str = base64.b64decode(encoded_str).decode('utf-8')
        data = json.loads(json_str)
        
        # 检查是否过期(24小时)
        if __import__('time').time() - data.get("timestamp", 0) > 86400:
            return None, None
        
        return data.get("cookie"), data.get("token")
    except:
        return None, None

# 加载凭证
cookie, token = load_credentials()

if not cookie or not token:
    print("❌ 没有找到有效的登录凭证,请先登录")
    exit(1)

# 下载文章(默认HTML格式)
url = "http://localhost:8000/api/download/one-click"
data = {
    "account_name": "公众号名称",
    "cookie": cookie,
    "token": token,
    "date_range": "last_month",  # last_week, last_month, last_3months, all
    "format": "html"  # 默认HTML格式,无需修改
}

print("正在下载公众号文章...")
response = requests.post(url, json=data)
result = response.json()

print(f"\n下载结果: {result.get('message', '未知')}")
print(f"找到文章数: {result.get('total_found', 0)}")
print(f"成功下载: {result.get('downloaded', 0)}")
print(f"文件名: {result.get('file_name', '无')}")

if 'file_data' in result:
    file_data = base64.b64decode(result['file_data'])
    filename = result.get('file_name', 'articles.zip')
    with open(filename, 'wb') as f:
        f.write(file_data)
    print(f"\n✅ 文件已保存: {filename}")
else:
    print("\n❌ 无文件数据")
```

Parameters:
- `account_name` (required) - WeChat official account name
- `cookie` (required) - Cookie from successful login
- `token` (required) - Token from successful login
- `date_range` (optional) - `last_week` (default), `last_month`, `last_3months`, `all`
- `format` (optional) - Fixed to `html` (no need to specify)

### Step 7: Extract ZIP File

The downloaded file is always a ZIP archive containing all articles. Extract it:

```bash
# Extract to a directory
unzip -o {file_name}.zip -d {account_name}_articles

# List extracted files (HTML format)
ls -la {account_name}_articles/HTML/
```

Articles are saved in the `HTML/` subdirectory as HTML files.

## Common Issues & Solutions

### 1. QR Code Display Issues

**Problem**: Terminal may not properly display or save QR code from curl output.

**Solution**: Use Python to extract and save the QR code image:
```bash
curl -s -X POST http://localhost:8000/api/login/init \
  -H "Content-Type: application/json" | python3 -c "
import sys, json, base64
d = json.load(sys.stdin)
open('qr_login.png', 'wb').write(base64.b64decode(d['qr_code']))
print('Session ID:', d['session_id'])
"
open qr_login.png
```

### 2. Base64 Data Truncation in Terminal

**Problem**: Large base64 data (ZIP files) gets truncated when displayed in terminal or processed with shell commands.

**Solution**: Always use Python script to handle file download and decoding, not shell commands.

### 3. QR Code Expired

**Problem**: QR code expires after a few minutes if not scanned.

**Solution**: If status returns `expired`, restart from Step 3 to get a new QR code.

### 4. Chinese Filename Encoding Issues

**Problem**: Chinese characters in filenames may display incorrectly in terminal.

**Solution**: Use `ls` command to view files - the actual filenames are correct, just terminal display issue.

### 5. Credentials Expired

**Problem**: Saved credentials expire after 24 hours.

**Solution**: The script will automatically detect expiration and prompt for re-login. Delete `.auth_cache` file to force re-login.

## Rate Limiting

WeChat has strict rate limiting:
- If you see `Rate limit detected`, wait 60 seconds automatically
- If limit persists, wait 1-24 hours before retrying
- Recommendation: Add delays between requests (5-10 seconds)

## Error Handling

Common errors and solutions:

1. **"未找到公众号"** - Check the account name spelling
2. **"该公众号在指定时间范围内没有文章"** - Try a different date range
3. **Rate limit errors** - Wait and retry with delays
4. **"二维码已过期"** - Generate a new QR code
5. **"登录凭证已过期"** - Re-login to refresh credentials

## Complete Example Script (With Auto Login Check)

```python
#!/usr/bin/env python3
"""
微信公众号文章下载完整示例(带自动登录态检查)
"""
import json
import base64
import requests
import os
import subprocess
import time

SERVER_URL = "http://localhost:8000"
AUTH_CACHE_FILE = ".auth_cache"

def load_credentials():
    """从本地缓存加载登录凭证"""
    if not os.path.exists(AUTH_CACHE_FILE):
        return None, None
    
    try:
        with open(AUTH_CACHE_FILE, "r") as f:
            encoded_str = f.read()
        
        json_str = base64.b64decode(encoded_str).decode('utf-8')
        data = json.loads(json_str)
        
        # 检查是否过期(24小时)
        if time.time() - data.get("timestamp", 0) > 86400:
            print("⚠️  登录凭证已过期(超过24小时)")
            return None, None
        
        print("✅ 找到有效的登录凭证!")
        return data.get("cookie"), data.get("token")
    except Exception as e:
        print(f"加载凭证失败: {e}")
        return None, None

def save_credentials(cookie, token):
    """保存登录凭证到本地缓存"""
    try:
        data = {
            "cookie": cookie,
            "token": token,
            "timestamp": time.time()
        }
        json_str = json.dumps(data)
        encoded_str = base64.b64encode(json_str.encode('utf-8')).decode('utf-8')
        
        with open(AUTH_CACHE_FILE, "w") as f:
            f.write(encoded_str)
        print("✅ 登录凭证已保存")
        return True
    except Exception as e:
        print(f"保存凭证失败: {e}")
        return False

def get_qr_code():
    """获取登录二维码"""
    url = f"{SERVER_URL}/api/login/init"
    response = requests.post(url)
    data = response.json()
    
    # 保存二维码图片
    qr_data = base64.b64decode(data['qr_code'])
    with open('qr_login.png', 'wb') as f:
        f.write(qr_data)
    
    # 打开二维码图片
    subprocess.run(['open', 'qr_login.png'])
    
    return data['session_id']

def check_login(session_id):
    """检查登录状态"""
    url = f"{SERVER_URL}/api/login/status/{session_id}"
    response = requests.get(url)
    data = response.json()
    
    if data['status'] == 'success':
        return data['cookie'], data['token']
    elif data['status'] == 'expired':
        raise Exception("二维码已过期,请重新获取")
    else:
        return None, None

def download_articles(account_name, cookie, token, date_range='last_month'):
    """下载文章(默认HTML格式)"""
    url = f"{SERVER_URL}/api/download/one-click"
    data = {
        "account_name": account_name,
        "cookie": cookie,
        "token": token,
        "date_range": date_range,
        "format": "html"  # 固定使用HTML格式
    }
    
    print(f"正在下载 {account_name} 的文章...")
    response = requests.post(url, json=data)
    result = response.json()
    
    print(f"\n下载结果: {result.get('message', '未知')}")
    print(f"找到文章数: {result.get('total_found', 0)}")
    print(f"成功下载: {result.get('downloaded', 0)}")
    
    if 'file_data' in result:
        file_data = base64.b64decode(result['file_data'])
        filename = result.get('file_name', f'{account_name}_articles.zip')
        
        with open(filename, 'wb') as f:
            f.write(file_data)
        
        print(f"\n✅ 文件已保存: {filename}")
        return filename
    else:
        raise Exception("下载失败:无文件数据")

def extract_articles(zip_file, account_name):
    """解压文章"""
    import zipfile
    
    extract_dir = f"{account_name}_articles"
    os.makedirs(extract_dir, exist_ok=True)
    
    with zipfile.ZipFile(zip_file, 'r') as zip_ref:
        zip_ref.extractall(extract_dir)
    
    print(f"\n✅ 已解压到: {extract_dir}/")
    
    # 列出文件(HTML格式)
    html_dir = os.path.join(extract_dir, 'HTML')
    if os.path.exists(html_dir):
        files = sorted(os.listdir(html_dir))
        print(f"\n共 {len(files)} 篇文章:")
        for f in files:
            print(f"  - {f}")
    
    return extract_dir

def main():
    # 1. 尝试加载已有凭证
    print("=== 步骤1: 检查登录状态 ===")
    cookie, token = load_credentials()
    
    # 如果没有有效凭证,进行登录流程
    if not cookie or not token:
        print("没有找到有效凭证,需要登录")
        print("\n=== 步骤2: 获取登录二维码 ===")
        session_id = get_qr_code()
        print(f"Session ID: {session_id}")
        print("\n请使用微信扫描 qr_login.png 中的二维码")
        input("\n登录完成后按 Enter 键继续...")
        
        # 检查登录状态
        print("\n=== 步骤3: 检查登录状态 ===")
        cookie, token = check_login(session_id)
        if not cookie:
            print("登录失败,请重试")
            return
        print("✅ 登录成功!")
        
        # 保存凭证
        save_credentials(cookie, token)
    
    # 2. 输入公众号名称
    account_name = input("\n请输入公众号名称: ").strip()
    date_range = input("时间范围 (last_week/last_month/last_3months/all) [默认: last_month]: ").strip() or "last_month"
    
    # 3. 下载文章(默认HTML格式)
    print(f"\n=== 步骤4: 下载 {account_name} 的文章 ===")
    print("下载格式: HTML")
    try:
        zip_file = download_articles(account_name, cookie, token, date_range)
        
        # 4. 解压文章
        print(f"\n=== 步骤5: 解压文章 ===")
        extract_articles(zip_file, account_name)
        
        print("\n🎉 全部完成!")
    except Exception as e:
        print(f"\n❌ 错误: {e}")

if __name__ == "__main__":
    main()
```

## API Endpoints Reference

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/login/init` | POST | Get login QR code |
| `/api/login/status/{session_id}` | GET | Check login status |
| `/api/login/cancel/{session_id}` | POST | Cancel login |
| `/api/download/one-click` | POST | Search, list, and download articles |
| `/api/health` | GET | Health check |

## When to Use This Skill

Invoke this skill when:
1. User wants to download articles from a WeChat official account
2. User asks to scrape WeChat public account content
3. User needs to batch download WeChat articles
4. User mentions "微信公众号文章下载"

## Important Notes

1. **QR Code Login Required**: User must scan QR code with WeChat mobile app
2. **Credential Caching**: Login credentials are cached locally for 24 hours to avoid repeated scanning
3. **Wait for User Confirmation**: Do not poll login status continuously; wait for user to confirm
4. **Use Python for Downloads**: Shell commands may truncate large base64 data
5. **Always Extract ZIP**: Downloaded files are always ZIP archives
6. **Rate Limits**: WeChat has strict rate limiting, be patient
7. **Cookie Expiration**: Login session expires after 24 hours (cached credentials also expire after 24 hours)
8. **File Format**: Articles are downloaded in HTML format by default (no format selection needed)

2.学习

---
name: "article-style-learner"
description: "Analyzes articles from a directory to learn writing style, caches the style as a prompt file, then generates new articles. Invoke when user wants to learn article writing style and create similar content."
---

# Article Style Learner

This skill analyzes articles from a specified directory to learn writing patterns, style, tone, and techniques. It caches the learned style as a prompt file for reuse, then generates new articles in markdown format based on user-specified topics.

## Workflow

### Step 1: Check for Cached Style Prompt

Before learning, check if a cached style prompt exists at:
- `.trae/skills/article-style-learner/style-prompt.md`

**If user says "根据skill生成文章" or similar (without requesting to re-learn):**
1. Read the cached `style-prompt.md` file
2. Skip to Step 4: Generate Article

**If user says "重新学习" or provides a new directory:**
1. Proceed to Step 2: Learn Article Style

### Step 2: Learn Article Style

When invoked with a directory path, scan the directory containing articles and analyze:

1. **Title Patterns**
   - Use of numbers, punctuation, emotional words
   - Title structure (topic + hook, question format, etc.)
   - Length and keyword placement

2. **Opening Patterns**
   - Hook techniques (questions, data, stories, direct statements)
   - Transition methods
   - Note: Do NOT include specific public account promotion text

3. **Body Structure**
   - Paragraph length and rhythm
   - Use of headings and subheadings
   - Data presentation style
   - Quote and source citation patterns
   - Transition phrases between sections

4. **Language Characteristics**
   - Tone (formal/casual, authoritative/friendly)
   - Sentence structure (short/long, simple/complex)
   - Use of rhetorical devices (metaphors, contrast, emphasis)
   - Special formatting (bold, quotes, brackets)

5. **Closing Patterns**
   - Call-to-action style
   - Summary techniques
   - Follow-up content prompts

### Step 3: Generate and Cache Style Prompt

Create a comprehensive style prompt and save it to:
`.trae/skills/article-style-learner/style-prompt.md`

The prompt file should follow this format:

```markdown
# Article Style Prompt

## Title Formula
[Extracted title patterns with examples]

## Opening Style
[How articles typically start - hook techniques, greeting style]

## Body Structure
[Typical article organization - paragraph length, heading usage, section transitions]

## Language Characteristics
[Tone, sentence structure, rhetorical devices, special formatting]

## Data Presentation
[How numbers, statistics, and quotes are presented]

## Closing Pattern
[How articles typically end - summary style, call-to-action]

## Writing Guidelines
[Specific instructions for generating articles in this style]
```

### Step 4: Generate New Article

Based on the cached style prompt and user's topic request:

1. Read the `style-prompt.md` file
2. Apply the title formula to create an engaging headline
3. Use the opening style with appropriate hook
4. Structure body content following learned patterns
5. Match the tone and language style
6. Close using the established pattern

## Usage Instructions

### First Time / Re-learn

When user wants to learn from articles:

1. **Ask for directory path**: "请提供存放文章的目录路径"
2. **Scan and analyze**: Read all article files in the directory
3. **Generate style prompt**: Create and save `style-prompt.md`
4. **Present style profile**: Show the learned style characteristics
5. **Ask for topic**: "请告诉我你想写什么主题的文章?"
6. **Generate article**: Create markdown article following learned style

### Subsequent Uses

When user says "根据skill生成文章" or similar:

1. **Check for cached prompt**: Read `style-prompt.md`
2. **Ask for topic**: "请告诉我你想写什么主题的文章?"
3. **Generate article**: Create markdown article using cached style

## Example Output Format

```markdown
# [Generated Title Following Learned Pattern]

[Opening following learned style]

## [Subheading]

[Body content in learned style...]

## [Subheading]

[Body content...]

[Closing following learned pattern]
```

## Important Notes

- **CRITICAL**: NEVER include specific public account names or promotion text in generated articles
- Preserve the unique voice and tone of the source articles
- Maintain consistency in formatting and structure
- Adapt content to the new topic while keeping style elements
- Generate pure markdown format output
- The cached `style-prompt.md` allows quick article generation without re-learning

3.发布

---
name: wechat-publisher
description: "一键发布 Markdown 到微信公众号草稿箱。基于 wenyan-cli,支持多主题、代码高亮、图片自动上传。"
metadata:
  {
    "openclaw":
      {
        "emoji": "📱",
      },
  }
---

# wechat-publisher

**一键发布 Markdown 文章到微信公众号草稿箱**

基于 [wenyan-cli](https://github.com/caol64/wenyan-cli) 封装的 OpenClaw skill。

## 功能

- ✅ Markdown 自动转换为微信公众号格式
- ✅ 自动上传图片到微信图床
- ✅ 一键推送到草稿箱
- ✅ 多主题支持(代码高亮、Mac 风格代码块)
- ✅ 支持本地和网络图片

## 快速开始

### 1. 安装 wenyan-cli

**wenyan-cli 需要全局安装:**

```bash
npm install -g @wenyan-md/cli
```

**验证安装:**
```bash
wenyan --help
```

> **注意:** publish.sh 脚本会自动检测并安装 wenyan-cli(如果未安装)

### 2. 配置 API 凭证

API 凭证已保存在 `/Users/leebot/.openclaw/workspace/TOOLS.md`

确保环境变量已设置:
```bash
export WECHAT_APP_ID=your_wechat_app_id
export WECHAT_APP_SECRET=your_wechat_app_secret
```

**重要:** 确保你的 IP 已添加到微信公众号后台的白名单!

配置方法:https://yuzhi.tech/docs/wenyan/upload

### 3. 准备 Markdown 文件

文件顶部**必须**包含完整的 frontmatter(wenyan 强制要求):

```markdown
---
title: 文章标题(必填!)
cover: https://example.com/cover.jpg  # 封面图(必填!)
---

# 正文开始

你的内容...
```

**⚠️ 关键发现(实测):**
- `title` 和 `cover` **都是必填字段**!
- 缺少任何一个都会报错:"未能找到文章封面"
- 虽然文档说"正文有图可省略cover",但实际测试必须提供 cover
- 所有图片(本地/网络)都会自动上传到微信图床

**推荐封面图来源:**
```markdown
# 方案1: 相对路径(推荐,便于分享)
cover: ./assets/default-cover.jpg

# 方案2: 绝对路径
cover: /Users/bruce/photos/cover.jpg

# 方案3: 网络图片
cover: https://your-cdn.com/image.jpg
```

**💡 提示:** 使用相对路径时,从 Markdown 文件所在目录开始计算。

### 4. 发布文章

**方式 1: 使用 publish.sh 脚本**
```bash
cd /Users/leebot/.openclaw/workspace/wechat-publisher
./scripts/publish.sh /path/to/article.md
```

**方式 2: 直接使用 wenyan-cli**
```bash
wenyan publish -f article.md -t lapis -h solarized-light
```

**方式 3: 在 OpenClaw 中使用**
```
"帮我发布这篇文章到微信公众号" + 附带 Markdown 文件路径
```

## 主题选项

wenyan-cli 支持多种主题:

**内置主题:**
- `default` - 默认主题
- `lapis` - 青金石(推荐)
- `phycat` - 物理猫
- 更多主题见:https://github.com/caol64/wenyan-core/tree/main/src/assets/themes

**代码高亮主题:**
- `atom-one-dark` / `atom-one-light`
- `dracula`
- `github-dark` / `github`
- `monokai`
- `solarized-dark` / `solarized-light` (推荐)
- `xcode`

**使用示例:**
```bash
# 使用 lapis 主题 + solarized-light 代码高亮
wenyan publish -f article.md -t lapis -h solarized-light

# 使用 phycat 主题 + GitHub 代码高亮
wenyan publish -f article.md -t phycat -h github

# 关闭 Mac 风格代码块
wenyan publish -f article.md -t lapis --no-mac-style

# 关闭链接转脚注
wenyan publish -f article.md -t lapis --no-footnote
```

## 自定义主题

### 临时使用自定义主题
```bash
wenyan publish -f article.md -c /path/to/custom-theme.css
```

### 安装自定义主题(永久)
```bash
# 从本地文件安装
wenyan theme --add --name my-theme --path /path/to/theme.css

# 从网络安装
wenyan theme --add --name my-theme --path https://example.com/theme.css

# 使用已安装的主题
wenyan publish -f article.md -t my-theme

# 删除主题
wenyan theme --rm my-theme
```

### 列出所有主题
```bash
wenyan theme -l
```

## 工作流程

1. **准备内容** - 用 Markdown 写作
2. **运行脚本** - 一键发布到草稿箱
3. **审核发布** - 到公众号后台审核并发布

## Markdown 格式要求

### 必需的 Frontmatter

**⚠️ 关键(实测结果):wenyan-cli 强制要求完整的 frontmatter!**

```markdown
---
title: 文章标题(必填!)
cover: 封面图片URL或路径(必填!)
---
```

**示例 1:相对路径(推荐)**
```markdown
---
title: 我的技术文章
cover: ./assets/cover.jpg
---

# 正文...
```

**示例 2:绝对路径**
```markdown
---
title: 我的技术文章
cover: /Users/bruce/photos/cover.jpg
---

# 正文...
```

**示例 3:网络图片**
```markdown
---
title: 我的技术文章
cover: https://example.com/cover.jpg
---

# 正文...
```

**❌ 错误示例(会报错):**

```markdown
# 只有 title,没有 cover
---
title: 我的文章
---

错误信息:未能找到文章封面
```

```markdown
# 完全没有 frontmatter
# 我的文章

错误信息:未能找到文章封面
```

**💡 重要发现:**
- 虽然 wenyan 官方文档说"正文有图片可省略cover"
- 但**实际测试必须提供 cover**,否则报错
- title 和 cover **缺一不可**

### 图片支持
- ✅ 本地路径:`![](./images/photo.jpg)`
- ✅ 绝对路径:`![](/Users/bruce/photo.jpg)`
- ✅ 网络图片:`![](https://example.com/photo.jpg)`

所有图片会自动上传到微信图床!

### 代码块
````markdown
```python
def hello():
    print("Hello, WeChat!")
```

会自动添加代码高亮和 Mac 风格装饰。

故障排查

1. 上传失败:IP 不在白名单

错误信息: ip not in whitelist

解决方法:

  1. 获取你的公网 IP:curl ifconfig.me
  2. 登录微信公众号后台:mp.weixin.qq.com/
  3. 开发 → 基本配置 → IP 白名单 → 添加你的 IP

2. wenyan-cli 未安装

错误信息: wenyan: command not found

解决方法:

npm install -g @wenyan-md/cli

3. 环境变量未设置

错误信息: WECHAT_APP_ID is required

解决方法:

export WECHAT_APP_ID=your_wechat_app_id
export WECHAT_APP_SECRET=your_wechat_app_secret

或在 ~/.zshrc / ~/.bashrc 中永久添加。

4. Frontmatter 缺失

错误信息: title is required in frontmatter

解决方法: 在 Markdown 文件顶部添加:

---
title: 你的文章标题
---

参考资料

更新日志

2026-02-05 - v1.0.0

  • ✅ 初始版本
  • ✅ 基于 wenyan-cli 封装
  • ✅ 支持一键发布到草稿箱
  • ✅ 多主题支持
  • ✅ 自动图片上传

License

Apache License 2.0 (继承自 wenyan-cli)

- publish.sh
```python
#!/usr/bin/env bash
# wechat-publisher: 发布 Markdown 到微信公众号草稿箱
# Usage: ./publish.sh <markdown-file> [theme] [highlight]

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 默认配置
DEFAULT_THEME="lapis"
DEFAULT_HIGHLIGHT="solarized-light"
TOOLS_MD="$HOME/.openclaw/workspace/TOOLS.md"

# 检查 wenyan-cli 是否安装
check_wenyan() {
    if ! command -v wenyan &> /dev/null; then
        echo -e "${RED}❌ wenyan-cli 未安装!${NC}"
        echo -e "${YELLOW}正在安装 wenyan-cli...${NC}"
        npm install -g @wenyan-md/cli
        if [ $? -eq 0 ]; then
            echo -e "${GREEN}✅ wenyan-cli 安装成功!${NC}"
        else
            echo -e "${RED}❌ 安装失败!请手动运行: npm install -g @wenyan-md/cli${NC}"
            exit 1
        fi
    fi
}

# 从 TOOLS.md 读取环境变量
load_credentials() {
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        if [ -f "$TOOLS_MD" ]; then
            echo -e "${YELLOW}📖 从 TOOLS.md 读取凭证...${NC}"
            export WECHAT_APP_ID=$(grep "export WECHAT_APP_ID=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_ID=//' | tr -d ' ')
            export WECHAT_APP_SECRET=$(grep "export WECHAT_APP_SECRET=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_SECRET=//' | tr -d ' ')
        fi
    fi
}

# 检查环境变量
check_env() {
    load_credentials
    
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        echo -e "${RED}❌ 环境变量未设置!${NC}"
        echo -e "${YELLOW}请在 TOOLS.md 中添加微信公众号凭证:${NC}"
        echo ""
        echo "  ## 🔐 WeChat Official Account (微信公众号)"
        echo "  "
        echo "  export WECHAT_APP_ID=your_app_id"
        echo "  export WECHAT_APP_SECRET=your_app_secret"
        echo ""
        echo -e "${YELLOW}或者手动设置环境变量:${NC}"
        echo "  export WECHAT_APP_ID=your_app_id"
        echo "  export WECHAT_APP_SECRET=your_app_secret"
        echo ""
        echo -e "${YELLOW}或者运行:${NC}"
        echo "  source ./scripts/setup.sh"
        exit 1
    fi
}

# 检查文件是否存在
check_file() {
    local file="$1"
    if [ ! -f "$file" ]; then
        echo -e "${RED}❌ 文件不存在: $file${NC}"
        exit 1
    fi
}

# 发布函数
publish() {
    local file="$1"
    local theme="${2:-$DEFAULT_THEME}"
    local highlight="${3:-$DEFAULT_HIGHLIGHT}"
    
    echo -e "${GREEN}📝 准备发布文章...${NC}"
    echo "  文件: $file"
    echo "  主题: $theme"
    echo "  代码高亮: $highlight"
    echo ""
    
    # 执行发布
    wenyan publish -f "$file" -t "$theme" -h "$highlight"
    
    if [ $? -eq 0 ]; then
        echo ""
        echo -e "${GREEN}✅ 发布成功!${NC}"
        echo -e "${YELLOW}📱 请前往微信公众号后台草稿箱查看:${NC}"
        echo "  https://mp.weixin.qq.com/"
    else
        echo ""
        echo -e "${RED}❌ 发布失败!${NC}"
        echo -e "${YELLOW}💡 常见问题:${NC}"
        echo "  1. IP 未在白名单 → 添加到公众号后台"
        echo "  2. Frontmatter 缺失 → 文件顶部添加 title + cover"
        echo "  3. API 凭证错误 → 检查 TOOLS.md 中的凭证"
        echo "  4. 封面尺寸错误 → 需要 1080×864 像素"
        exit 1
    fi
}

# 显示帮助
show_help() {
    echo "Usage: $0 <markdown-file> [theme] [highlight]"
    echo ""
    echo "Examples:"
    echo "  $0 article.md"
    echo "  $0 article.md lapis"
    echo "  $0 article.md lapis solarized-light"
    echo ""
    echo "Available themes:"
    echo "  default, lapis, phycat, ..."
    echo "  Run 'wenyan theme -l' to see all themes"
    echo ""
    echo "Available highlights:"
    echo "  atom-one-dark, atom-one-light, dracula, github-dark, github,"
    echo "  monokai, solarized-dark, solarized-light, xcode"
}

# 主函数
main() {
    # 检查参数
    if [ $# -eq 0 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
        show_help
        exit 0
    fi
    
    local file="$1"
    local theme="$2"
    local highlight="$3"
    
    # 执行检查
    check_wenyan
    check_env
    check_file "$file"
    
    # 发布文章
    publish "$file" "$theme" "$highlight"
}

# 运行
main "$@"