Skill: 16miku/wechat-auto-publishing (version 3.0.0)

--- name: wechat-auto-publishing-complete description: Use this skill to fully reproduce and operate a local end-to-end WeChat Official Account publishing workflow: prepare the environment, validate dependencies, configure non-sensitive placeholders for credentials, gather source material, draft articles, prepare cover and body images, assemble a WeChat-ready Markdown package, publish to draft, optionally submit for formal publication, poll status, archive outputs, and attach scheduling or alerting. Use whenever the user wants a complete reproducible公众号自动发文 skill with environment setup, templates, runbooks, and execution scaffolding, while keeping all secrets and personal account details outside the skill package. Key real-world findings: freepublish does not always behave like manual platform publishing for homepage visibility, production mode should often default to draft-only, image files must be validated by real format rather than extension alone, and multi-account deployments should use isolated directories. --- # WeChat Auto Publishing Complete Use this skill to reproduce, document, and operate a complete local WeChat Official Account auto-publishing workflow without embedding any secrets, private account details, or personal identifiers in the skill package. This skill is intentionally broader than a minimal workflow note. It includes the operational context needed to reproduce the workflow on a fresh machine, while still keeping all sensitive values external. ## Core outcome The desired end state is a reusable local workflow that can do the following: 1. prepare the environment on a new machine 2. gather the day’s source material 3. determine the article angle 4. draft the article in a target style 5. prepare `cover.png`, `image1.jpg`, and `image2.jpg` 6. assemble a publishable Markdown package 7. publish to the WeChat draft box 8. optionally complete formal publication 9. archive outputs and execution results 10. optionally attach scheduling and alerting ## Required safety rule Never store real private values in this skill package. Do not include: - real `WECHAT_APP_ID` - real `WECHAT_APP_SECRET` - real `GOOGLE_API_KEY` - any real cookies, tokens, session secrets, API keys, or private IDs - user-specific公众号 identifiers unless the user explicitly asks for them to be hard-coded - any private filesystem paths that reveal personal context unless rewritten as placeholders When documenting configuration, only include: - variable names - placeholder values - lookup order - validation methods - safe example file structures ## Skill structure Read the bundled references depending on what the user is trying to accomplish: - `references/environment-and-config.md` — use when preparing a new machine or validating prerequisites - `references/source-gathering.md` — use when collecting the day’s source pool and market angle - `references/writing-style.md` — use when drafting and formatting the article - `references/image-strategy.md` — use when preparing cover and body images, including gallery mode - `references/publishing.md` — use when publishing to draft/final and archiving outputs - `references/scheduling-and-alerting.md` — use when attaching cron, wrappers, logs, and alerts - `references/security-boundary.md` — use when checking what the skill must and must not contain - `templates/article-template.md` — use as the default publishable article skeleton - `templates/env.example.txt` — use as the safe non-secret environment placeholder file - `templates/run.sh` — use as a starting point for a local orchestrator script - `templates/run.production-example.sh` — use as a more production-ready orchestrator template with gallery, publish mode, and time-slot support - `templates/cron.example.txt` — use as a starting point for scheduling - `templates/publish-result.example.json` — use as a result artifact template - `templates/gallery-config.example.txt` — use as a local gallery config example - `templates/cover-image-extend.example.md` — use as a safe cover-image preference example - `templates/image-gen-extend.example.md` — use as a safe image-gen preference example - `templates/workspace-tree.txt` — use as a recommended directory layout - `runbook.md` — use as the operator-facing execution checklist ## Standard execution flow ### Step 1: Prepare environment Before doing any content work, verify that the environment is reproducible on the target machine. Confirm: - runtime dependencies exist - the publishing script dependency chain exists - non-sensitive config placeholders are in place - secrets will be supplied outside the skill package - external prerequisites such as WeChat IP allowlisting are understood Read `references/environment-and-config.md`. ### Step 2: Gather source material Collect raw source material, filter it down, and compress the market angle. Read `references/source-gathering.md`. ### Step 3: Draft the article Draft an article in the target style using the publishable Markdown conventions from this skill. Read `references/writing-style.md` and start from `templates/article-template.md` when useful. ### Step 4: Prepare images Prepare: - `cover.png` - `image1.jpg` - `image2.jpg` Choose image sources explicitly: - user-provided assets - local gallery mode - generated images Read `references/image-strategy.md`. ### Step 5: Assemble the package Make sure the article package is self-contained and publishable: - `article.md` - `cover.png` - `image1.jpg` - `image2.jpg` Use relative paths and UTF-8 encoding. ### Step 6: Publish to draft Use the local publishing pathway to send the article to WeChat draft. Do not treat publishing as successful unless the draft step returns a meaningful success result, such as a valid `media_id`. If `baoyu-post-to-wechat` 因依赖问题（如 `simple-xml-to-json` 兼容性错误）无法运行，使用备用脚本： ```bash node templates/publish.mjs ``` 备用脚本 `publish.mjs` 支持完整流程：获取 token -> 上传封面 -> 上传内图 -> Markdown 转 HTML -> 创建草稿 -> 正式发布 -> 归档结果。 Read `references/publishing.md`. ### Step 7: Optionally complete formal publication If the workflow includes final publication, submit, poll, and capture the final URL. 备用脚本 `templates/publish.mjs` 已内置正式发布功能（freepublish），如果使用备用脚本完成 Step 6，正式发布会自动执行，无需额外操作。 ### Step 8: Archive results Save result artifacts, logs, identifiers, and remaining gallery state where relevant. ### Step 9: Add scheduling and alerting If the user wants timed operation, attach a wrapper script and scheduler entry. Read `references/scheduling-and-alerting.md`. ## Important real-world publishing boundary ### `freepublish` is not always operationally equivalent to manual publishing in the MP backend In practice, a workflow may successfully: - submit draft publication - obtain `publish_id` - obtain `article_id` - obtain `article_url` but still not behave the same way as manually publishing the draft inside the WeChat Official Account admin console, especially for homepage visibility expectations. Therefore, the skill must distinguish between: - **technical publication success** - **platform backend success** - **operational visibility success** ## Recommended production modes ### Mode A: `draft_only` (recommended production default) ```text auto content generation → auto image preparation → auto draft submission → human publishes in MP backend ``` Use this mode when homepage visibility and platform-consistent display matter more than full automation. ### Mode B: `full_publish` (testing / exploratory mode) ```text auto content generation → auto draft submission → auto freepublish submit → poll result → archive article_url ``` Use this mode only when the operator explicitly accepts that API publication may not be equivalent to backend manual publication. ## Multi-account deployment principle When operating multiple public accounts, prefer: ```text one account = one working directory = one .env = one title history = one cron entry ``` This avoids credential confusion, title-history pollution, log mixing, and accidental cross-account publishing. ## Image validity principle Do not trust file extension alone. Before publish, validate that images are: - present - readable - non-empty - in a real supported format (PNG/JPEG/WebP if supported by the target chain) If a file is named `cover.png` but is actually HEIF/HEVC, the WeChat API may reject it. ## Writing quality principle Do not directly inject scraped source titles into the body as-is. Avoid: - raw news-title insertion - truncated title fragments ending with `...` - media-style prefixes such as `报道：` in final body text Prefer: - title cleaning - summary transformation - author-tone rewriting ## Workflow principles ### Keep the package explicit Prefer explicit files and paths over hidden assumptions. ### Keep secrets external Configuration structure belongs inside the skill. Real credentials do not. ### Keep failure handling visible If a step fails, preserve enough output and log context for a human operator to understand what happened. ### Keep image behavior configurable Do not hard-code a single image source assumption. The workflow should make it clear whether the body images come from a local gallery, generated assets, or user-supplied files. ## Reproduction checklist A fresh-machine reproduction should be able to answer yes to all of these: - runtime tools are installed - publishing dependency chain is installed - safe config placeholders are present - operator knows where to put real secrets outside the skill package - source gathering pathway is defined - article template is available - image strategy is defined - publishing checks are defined - result archive format is defined - scheduling examples exist if automation is needed ## Final rule This skill is allowed to be operationally complete, but it must remain secret-free. Every reusable process detail can go into the skill; every real credential and personal account detail must stay outside it.

path: README.md
content: # 微信公众号自动发文完整 Skill

这是一个用于**微信公众号自动发文工作流**的完整 Skill，目标是把“环境准备 → 资讯整理 → 写稿 → 图片准备 → 草稿发布 → 正式发布 → 结果归档 → 定时调度”整理为一套**可复现、可交付、可扩展**的本地工作流。

本 Skill 适用于以下场景：
- 搭建微信公众号自动发文流程
- 将现有公众号运营流程沉淀为标准 Skill
- 在新机器或新环境中复现自动发文链路
- 为定时发布、草稿审核、正式发布和归档提供统一操作规范

---

## 功能概览

本 Skill 覆盖以下核心能力：

### 1. 环境准备与复现
- 新机器初始化与依赖检查
- 运行时环境说明（Python / Node / npm / npx / bun）
- 本地工作目录与推荐结构说明
- 安全配置与占位模板
- 多公众号独立目录结构建议

### 2. 内容准备
- 资讯收集与筛选流程
- 市场角度整理
- 公众号文章写稿规范
- Markdown 成稿约定
- 避免将抓取标题原文直接拼入正文

### 3. 图片处理
- 封面图 `cover.png` 生成策略
- 正文首图 `image1.jpg` / 尾图 `image2.jpg` 处理策略
- 本地图库模式
- 用户提供图片模式
- 自动生成图片模式
- 图片失败回退与质检规则
- 图片真实格式校验与标准化要求（防止 HEIF/HEIC 冒充 png/jpg）

### 4. 公众号发布
- 草稿发布流程说明
- 正式发布流程说明（freepublish 轮询机制）
- 明确说明：`freepublish` 与平台后台手动发布效果不完全等价
- 推荐生产模式：自动入草稿箱 + 人工在平台手动发布
- 备用发布脚本 `publish.mjs`（纯 Node.js，零依赖，解决 Bun 兼容性问题）
- 发布成功判断标准（技术成功 / 平台成功 / 运营可见性成功三层区分）
- 发布结果归档规范
- 常见故障排查（IP 白名单、依赖兼容性、AI 生图 404/401、图片格式错误）

### 5. 自动化与运维
- `run.sh` 模板
- 更贴近实战的 `run.production-example.sh`
- `cron` 示例（含多账号、时区、双模式）
- 失败告警建议
- 运行手册与交接说明
- `draft_only` / `full_publish` 双模式说明

### 6. 安全边界
- Skill 本身不包含真实敏感配置
- 不包含真实 `WECHAT_APP_ID`
- 不包含真实 `WECHAT_APP_SECRET`
- 不包含真实 `GOOGLE_API_KEY`
- 不包含 Token / Cookie / 私人账号信息

---

## 目录结构

```text
wechat-auto-publishing-complete/
├─ SKILL.md
├─ runbook.md
├─ references/
│  ├─ environment-and-config.md
│  ├─ source-gathering.md
│  ├─ writing-style.md
│  ├─ image-strategy.md
│  ├─ publishing.md
│  ├─ scheduling-and-alerting.md
│  └─ security-boundary.md
└─ templates/
   ├─ article-template.md
   ├─ env.example.txt
   ├─ publish.mjs              ← 纯 Node.js 零依赖备用发布脚本
   ├─ run.sh
   ├─ run.production-example.sh ← 更贴近生产环境的脚本模板
   ├─ cron.example.txt
   ├─ publish-result.example.json
   ├─ gallery-config.example.txt
   ├─ cover-image-extend.example.md
   ├─ image-gen-extend.example.md
   └─ workspace-tree.txt
```

---

## 设计原则

本 Skill 的设计目标是：

- **完整性**：覆盖从准备环境到正式发布的完整链路
- **可复现**：适合在新机器、新项目目录中重新搭建
- **可交接**：适合作为团队或个人工作流沉淀后的标准交付物
- **可扩展**：后续可以继续补充脚本、图片策略、发布策略、调度策略
- **安全性**：严格区分“流程说明”和“真实秘密”，避免将私人敏感信息写入 Skill

---

## 使用方式

推荐按以下顺序使用本 Skill：

1. 阅读 `SKILL.md` 了解整体流程
2. 阅读 `references/environment-and-config.md` 准备运行环境
3. 使用 `templates/env.example.txt`、`templates/workspace-tree.txt` 搭建本地目录
4. 按 `references/source-gathering.md` 和 `references/writing-style.md` 准备文章内容
5. 按 `references/image-strategy.md` 准备封面图与正文图片
6. 按 `references/publishing.md` 完成草稿发布与正式发布判断
7. 按 `runbook.md` 做每日执行或交接
8. 如需定时运行，参考 `templates/run.sh`、`templates/run.production-example.sh` 与 `templates/cron.example.txt`

---

## 环境要求

建议运行环境至少具备：

- `python3`（如使用 uv 管理 Python，`python` 命令亦可）
- `node`
- `npm`
- `npx`
- `bun`（Bun 1.3.x 在 Windows 上可能存在兼容性问题，遇到时可用 Node.js 替代）

如果要完整复现工作流，还建议本地具备对应的内容收集、图片处理和公众号发布能力。

> 注意：AI 生图（Google API）可能需要代理，微信 API 必须直连。两个阶段需分别处理代理设置。

---

## 配置说明

本仓库**只提供安全占位模板**，不包含任何真实账号或敏感值。

示例：

```env
WECHAT_APP_ID=fill_in_valid_value_in_target_environment
WECHAT_APP_SECRET=fill_in_valid_value_in_target_environment
GOOGLE_BASE_URL=fill_in_valid_value_in_target_environment
GOOGLE_API_KEY=fill_in_valid_value_in_target_environment
```

请在你自己的目标环境中填写真实值，不要直接提交到仓库。

---

## 推荐适用对象

这个 Skill 适合：

- 想把微信公众号运营流程标准化的人
- 想把公众号写稿/发稿沉淀为可复用工具的人
- 想在本地实现半自动或全自动发文的人
- 想把现有工作流整理成 ClawHub / GitHub 可分发 Skill 的人

---

## 安全声明

本 Skill 只包含：
- 流程说明
- 参考文档
- 模板文件
- 安全占位示例

不包含：
- 真实凭证
- 私人配置
- 私有账号信息
- 真实生产环境秘密

请在使用前自行完成本地配置，并确保遵守目标平台的使用规则与安全要求。

---

## 后续可扩展方向

后续可以继续扩展：
- 更完整的执行脚本
- 更自动化的发布编排
- 更丰富的文章模板
- 更细粒度的图片素材策略
- 更完整的日志与告警体系
- 更适合团队协作的工作流版本


path: runbook.md
content: # WeChat Auto Publishing Runbook

Use this runbook when operating or handing off the workflow.

## 1. Fresh machine checklist

- [ ] `python3`, `node`, `npm`, `npx`, `bun` are installed
- [ ] the required local publishing skill/tool exists
- [ ] the Markdown dependency chain is installed
- [ ] `<project-dir>/.baoyu-skills/.env` exists in the target environment
- [ ] real secrets are supplied outside the skill package
- [ ] WeChat allowlist prerequisites are satisfied
- [ ] image-service access is available if image generation is enabled

## 2. Daily execution checklist

- [ ] gather and filter source material
- [ ] produce the market-angle summary
- [ ] draft the article from the template
- [ ] prepare `cover.png`
- [ ] prepare `image1.jpg` and `image2.jpg` if enabled
- [ ] verify frontmatter and file paths
- [ ] 发布前确认代理设置（生图需要代理，发布需要直连）
- [ ] publish to draft
- [ ] record `media_id`
- [ ] optionally publish formally
- [ ] 发布后检查 `output/full_publish_result.json` 中的 `publish_status` 是否为 0
- [ ] archive outputs

## 3. Failure checklist

If publish fails:
- [ ] preserve logs
- [ ] preserve the article package
- [ ] do not consume gallery images if success was not confirmed
- [ ] record the failed step and error summary
- [ ] send an alert if automation is enabled

## 4. 常见故障排查

### 微信 API 报 40164（IP 不在白名单）

以错误信息中返回的 IP 为准添加白名单，不要依赖 `curl ifconfig.me` 的结果（代理环境下出口 IP 可能不同）。

### baoyu-post-to-wechat 报 SyntaxError（simple-xml-to-json）

Bun 与 `simple-xml-to-json` 包存在兼容性问题。解决方案：
- 使用 `templates/publish.mjs` 备用脚本：`node templates/publish.mjs`
- 或升级 Bun 到最新版本后重试

### AI 生图返回 404 或 401

检查 `.baoyu-skills/baoyu-image-gen/EXTEND.md` 或 `.baoyu-skills/baoyu-cover-image/EXTEND.md` 中的模型名是否被代理 API 支持。不同代理支持的模型列表可能不同。AI 生图失败时优先使用备用图片继续发布流程，不要让图片生成失败阻断草稿创建。

### 图片上传报 40113（unsupported file type hint）

检查封面/正文图片的真实格式是否与扩展名一致。`.png` 文件名可能实际包含 HEIF 数据，需要重新编码为标准 PNG/JPEG。

### `freepublish` 成功但文章未按预期显示在主页

这属于 `freepublish` 行为边界，不一定是脚本 bug。建议生产环境使用 `draft_only` 模式，人工在公众号后台手动发布。

## 5. Recommended daily operating modes

### Production mode (recommended)

1. Gather sources
2. Generate article
3. Prepare images
4. Publish to draft
5. Open MP backend
6. Review and manually publish from draft

### Fully automated experimental mode

1. Gather sources
2. Generate article
3. Prepare images
4. Publish to draft
5. Auto-submit `freepublish`
6. Poll result
7. Archive `article_url`

> Warning: successful API publication does not always guarantee the same homepage visibility behavior as manual backend publishing.

## 6. Multi-account practice

Use isolated working directories per account.

Example:
- `/root/wechat-auto`
- `/root/wechat-auto-niugushashou`

Keep isolated:
- `.baoyu-skills/.env`
- `title_history.txt`
- logs
- cron entry

## 7. Distribution checklist

Before sharing the skill package:
- [ ] verify no real secrets are embedded
- [ ] verify no private account identifiers are exposed unintentionally
- [ ] verify example files only contain placeholders
- [ ] verify the package is self-explanatory for a new machine setup


path: templates/article-template.md
content: ---
title: 文章标题
author: 作者名
summary: 一句话摘要
cover: ./cover.png
---

![image](./image1.jpg)

开头第一句直接抛情绪。

第二句直接说结论。

第三句继续强化读者体感。

## 1、第一个小标题

这里写第一部分正文。

每段尽量保持短句和留白。

## 2、第二个小标题

这里写第二部分正文。

继续保持口语化和轻量感。

## 3、我的操作？

这里写当前态度、节奏或仓位判断。

## 4、明日观察重点：

（1）观察点一

（2）观察点二

（3）观察点三

![image](./image2.jpg)

各位顺手点个赞，

转发一下，

让机器人识别一下，

不然阅读量太低了！


path: templates/cover-image-extend.example.md
content: ---
version: 3
watermark:
  enabled: false
  content: ""
  position: bottom-right
  opacity: 0.7
preferred_type: conceptual
preferred_palette: cool
preferred_rendering: digital
preferred_text: none
preferred_mood: balanced
default_aspect: 2.35:1
default_output_dir: same-dir
quick_mode: true
language: zh
custom_palettes: []
---


path: templates/cron.example.txt
content: # Example cron entries
# Replace placeholders before use.

# Single account, basic schedule
0 8,17 * * * cd <project-dir> && /bin/bash ./run.sh >> ./output/cron.log 2>&1

# Production-recommended draft-only mode
0 8,17 * * * TZ=Asia/Shanghai WECHAT_AUTO_PUBLISH_MODE=draft_only /root/wechat-auto/run.sh >> /root/wechat-auto/cron.log 2>&1

# Multi-account full_publish example (only if operator explicitly accepts API publication behavior differences)
0 8,17 * * * TZ=Asia/Shanghai WECHAT_AUTO_PUBLISH_MODE=full_publish /root/wechat-auto/run.sh >> /root/wechat-auto/cron.log 2>&1
0 7,16 * * * TZ=Asia/Shanghai WECHAT_AUTO_PUBLISH_MODE=full_publish /root/wechat-auto-niugushashou/run.sh >> /root/wechat-auto-niugushashou/cron.log 2>&1


path: templates/env.example.txt
content: # 微信公众号 AppID
WECHAT_APP_ID=fill_in_valid_value_in_target_environment
# 微信公众号 AppSecret（泄露可导致公众号被完全控制，请妥善保管）
WECHAT_APP_SECRET=fill_in_valid_value_in_target_environment
# Google API 代理地址（如使用第三方代理访问 Gemini 等 Google 服务）
GOOGLE_BASE_URL=fill_in_valid_value_in_target_environment
# Google API Key（通过代理访问时同样属于敏感信息，不要硬编码）
GOOGLE_API_KEY=fill_in_valid_value_in_target_environment


path: templates/gallery-config.example.txt
content: gallery_root = <gallery-root>
gallery_source_dir = <gallery-root>/unused
gallery_used_dir = <gallery-root>/used
gallery_bad_dir = <gallery-root>/bad

gallery_enabled = true
gallery_strategy = random
gallery_pick_count = 2
gallery_consume_mode = move_to_used
gallery_low_stock_threshold = 20
gallery_allowed_ext = .jpg,.jpeg,.png,.webp


path: templates/image-gen-extend.example.md
content: ---
version: 1
default_provider: google
default_quality: 2k
default_aspect_ratio: 2.35:1
default_image_size: null
default_model:
  google: gemini-3.1-flash-image-preview
  openai: null
  dashscope: null
  replicate: null
---


path: templates/publish-result.example.json
content: success: yes
timestamp: 2026-04-05T15:47:43.363Z
media_id: example_media_id
title: 文章标题示例
summary: 一句话摘要示例
author: 作者名
thumb_media_id: example_thumb_media_id
publish_id: 2247483950
article_id: example_article_id
article_url: http://mp.weixin.qq.com/s?__biz=xxx
publish_status: 0

path: templates/publish.mjs
content: #!/usr/bin/env node

/**
 * 备用发布脚本 - 微信公众号文章发布
 *
 * 当 baoyu-post-to-wechat 因 jimp / simple-xml-to-json 等依赖
 * 在 Windows + Bun 环境下出现兼容性问题无法运行时，可使用本脚本替代。
 *
 * 特点：纯 Node.js 实现，零第三方依赖。
 *
 * 用法：
 *   1. 将本文件复制到工作目录（与 article.md、cover.png 同级）
 *   2. 确保 .baoyu-skills/.env 中配置了 WECHAT_APP_ID 和 WECHAT_APP_SECRET
 *   3. 运行: node publish.mjs
 */

import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

// --- 基础配置 ---
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// 清除代理环境变量，确保直连微信 API
delete process.env.http_proxy;
delete process.env.https_proxy;
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.all_proxy;
delete process.env.ALL_PROXY;

const WX_API = 'https://api.weixin.qq.com';

// --- 工具函数 ---

/** 读取 .env 文件，返回键值对 */
function loadEnv(envPath) {
  const content = readFileSync(envPath, 'utf-8');
  const env = {};
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eqIdx = trimmed.indexOf('=');
    if (eqIdx === -1) continue;
    const key = trimmed.slice(0, eqIdx).trim();
    let val = trimmed.slice(eqIdx + 1).trim();
    // 去除引号
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    env[key] = val;
  }
  return env;
}

/** 解析 frontmatter（--- 包裹的 YAML 头部） */
function parseFrontmatter(md) {
  const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
  if (!match) return { meta: {}, body: md };
  const meta = {};
  for (const line of match[1].split('\n')) {
    const idx = line.indexOf(':');
    if (idx === -1) continue;
    const key = line.slice(0, idx).trim();
    let val = line.slice(idx + 1).trim();
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    meta[key] = val;
  }
  return { meta, body: match[2] };
}

/** 简易 Markdown -> HTML 转换 */
function markdownToHtml(md) {
  let html = md;

  // 图片 ![alt](src)
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;height:auto;" />');

  // 标题 h1-h6（h2 加样式增强视觉区分）
  html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
  html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
  html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
  html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
  html = html.replace(/^##\s+(.+)$/gm, '<h2 style="margin-top:1.5em;margin-bottom:0.5em;font-size:18px;font-weight:bold;border-left:4px solid #1e90ff;padding-left:10px;">$1</h2>');
  html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');

  // 加粗 **text**
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');

  // 斜体 *text*
  html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');

  // 无序列表 - item
  html = html.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>');
  html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');

  // 行内代码 `code`
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>');

  // 链接 [text](url)
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');

  // 段落处理：非空行包裹为 <p>，已有标签的行跳过，段落间加间距
  const lines = html.split('\n');
  const result = [];
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) {
      result.push('');
    } else if (/^<(h[1-6]|ul|ol|li|img|blockquote|pre|code|hr|div|table|p)/.test(trimmed)) {
      result.push(trimmed);
    } else {
      result.push(`<p style="margin-bottom:1em;line-height:1.8;">${trimmed}</p>`);
    }
  }

  return result.filter(l => l !== '').join('\n');
}

/** 手动构建 multipart/form-data */
function buildMultipart(fields, files) {
  const boundary = '----NodeFormBoundary' + Math.random().toString(36).slice(2);
  const parts = [];

  for (const { name, value } of (fields || [])) {
    parts.push(
      `--${boundary}\r\n` +
      `Content-Disposition: form-data; name="${name}"\r\n\r\n` +
      `${value}\r\n`
    );
  }

  for (const { name, filename, contentType, data } of (files || [])) {
    const header =
      `--${boundary}\r\n` +
      `Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n` +
      `Content-Type: ${contentType}\r\n\r\n`;
    parts.push({ header, data });
  }

  // 组装为 Buffer
  const buffers = [];
  for (const part of parts) {
    if (typeof part === 'string') {
      buffers.push(Buffer.from(part, 'utf-8'));
    } else {
      buffers.push(Buffer.from(part.header, 'utf-8'));
      buffers.push(Buffer.isBuffer(part.data) ? part.data : Buffer.from(part.data));
      buffers.push(Buffer.from('\r\n', 'utf-8'));
    }
  }
  buffers.push(Buffer.from(`--${boundary}--\r\n`, 'utf-8'));

  return {
    body: Buffer.concat(buffers),
    contentType: `multipart/form-data; boundary=${boundary}`,
  };
}

/** 带错误处理的 fetch 封装 */
async function wxFetch(url, options = {}) {
  console.log(`  -> ${options.method || 'GET'} ${url.split('?')[0]}`);
  const resp = await fetch(url, options);
  const data = await resp.json();
  if (data.errcode && data.errcode !== 0) {
    throw new Error(`微信 API 错误: ${data.errcode} - ${data.errmsg}`);
  }
  return data;
}

// --- 核心业务函数 ---

/** 步骤1: 获取 access_token */
async function getAccessToken(appId, appSecret) {
  console.log('\n[1/6] 获取 access_token...');
  const url = `${WX_API}/cgi-bin/token?grant_type=client_credential&appid=${appId}&[REDACTED_SECRET]`;
  const data = await wxFetch(url);
  console.log(`  access_token: ${data.access_token.slice(0, 20)}...`);
  return data.access_token;
}

/** 步骤2: 上传封面图为永久素材，返回 thumb_media_id */
async function uploadCoverMaterial(token, coverPath) {
  console.log('\n[2/6] 上传封面图为永久素材...');
  const fileData = readFileSync(coverPath);
  const ext = coverPath.split('.').pop().toLowerCase();
  const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif' };
  const contentType = mimeMap[ext] || 'image/png';

  const { body, contentType: ct } = buildMultipart(
    [{ name: 'type', value: 'image' }],
    [{ name: 'media', filename: `cover.${ext}`, contentType, data: fileData }]
  );

  const url = `${WX_API}/cgi-bin/material/add_material?access_token=${token}&type=image`;
  const data = await wxFetch(url, {
    method: 'POST',
    headers: { 'Content-Type': ct },
    body,
  });

  console.log(`  thumb_media_id: ${data.media_id}`);
  return data.media_id;
}

/** 步骤3: 上传文章内图片到微信（临时图片，返回微信 URL） */
async function uploadContentImage(token, imagePath) {
  console.log(`  上传图片: ${imagePath}`);
  const fileData = readFileSync(imagePath);
  const filename = imagePath.split(/[/\\]/).pop();
  const ext = filename.split('.').pop().toLowerCase();
  const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif' };
  const contentType = mimeMap[ext] || 'image/jpeg';

  const { body, contentType: ct } = buildMultipart(
    [],
    [{ name: 'media', filename, contentType, data: fileData }]
  );

  const url = `${WX_API}/cgi-bin/media/uploadimg?access_token=${token}`;
  const data = await wxFetch(url, {
    method: 'POST',
    headers: { 'Content-Type': ct },
    body,
  });

  console.log(`  微信 URL: ${data.url}`);
  return data.url;
}

/** 步骤4: 处理文章内容，上传图片并替换路径 */
async function processArticle(token) {
  console.log('\n[3/6] 读取并解析 article.md...');
  const mdContent = readFileSync(join(__dirname, 'article.md'), 'utf-8');
  const { meta, body } = parseFrontmatter(mdContent);
  console.log(`  标题: ${meta.title}`);
  console.log(`  摘要: ${meta.summary}`);

  console.log('\n[4/6] 上传文章内图片...');
  // 找出所有本地图片引用
  const imgRegex = /!\[([^\]]*)\]\(\.\/([^)]+)\)/g;
  const localImages = new Set();
  let match;
  while ((match = imgRegex.exec(body)) !== null) {
    localImages.add(match[2]);
  }

  // 上传每张图片，建立映射
  const imageUrlMap = {};
  for (const imgFile of localImages) {
    const imgPath = join(__dirname, imgFile);
    if (!existsSync(imgPath)) {
      console.log(`  警告: 图片不存在 ${imgFile}，跳过`);
      continue;
    }
    const wxUrl = await uploadContentImage(token, imgPath);
    imageUrlMap[`./${imgFile}`] = wxUrl;
  }

  console.log('\n[5/6] 转换 Markdown 为 HTML...');
  // 先替换图片路径再转 HTML
  let processedBody = body;
  for (const [local, wxUrl] of Object.entries(imageUrlMap)) {
    processedBody = processedBody.split(local).join(wxUrl);
  }

  const htmlContent = markdownToHtml(processedBody);
  console.log(`  HTML 长度: ${htmlContent.length} 字符`);

  return { meta, htmlContent };
}

/** 步骤5: 创建草稿 */
async function createDraft(token, thumbMediaId, title, content, digest, author) {
  console.log('\n[6/7] 创建草稿...');
  const url = `${WX_API}/cgi-bin/draft/add?access_token=${token}`;

  const article = {
    title,
    author: author || '',
    digest: digest || '',
    content,
    thumb_media_id: thumbMediaId,
    need_open_comment: 0,
    only_fans_can_comment: 0,
  };

  const data = await wxFetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ articles: [article] }),
  });

  console.log(`  草稿 media_id: ${data.media_id}`);
  return data;
}

/** 步骤6: 正式发布 */
async function freePublish(token, mediaId) {
  console.log('\n[7/7] 提交正式发布...');
  const url = `${WX_API}/cgi-bin/freepublish/submit?access_token=${token}`;
  const data = await wxFetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ media_id: mediaId }),
  });
  const publishId = data.publish_id;
  console.log(`  publish_id: ${publishId}`);

  // 轮询发布状态
  console.log('  轮询发布状态...');
  for (let i = 0; i < 30; i++) {
    await new Promise(r => setTimeout(r, 3000));
    const statusUrl = `${WX_API}/cgi-bin/freepublish/get?access_token=${token}`;
    const statusData = await wxFetch(statusUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ publish_id: publishId }),
    });
    const status = statusData.publish_status;
    console.log(`  第${i + 1}次查询: publish_status=${status}`);
    if (status === 0) {
      // 发布成功
      const articleId = statusData.article_id;
      const articleUrl = statusData.article_detail?.item?.[0]?.article_url || '';
      console.log(`  发布成功! article_id: ${articleId}`);
      if (articleUrl) console.log(`  文章 URL: ${articleUrl}`);
      return { publish_id: publishId, article_id: articleId, article_url: articleUrl, publish_status: 0 };
    } else if (status === 2) {
      // 原创审核中
      const articleId = statusData.article_id;
      console.log(`  原创审核中，article_id: ${articleId}`);
      return { publish_id: publishId, article_id: articleId, article_url: '', publish_status: 2 };
    } else if (status === 1) {
      // 发布中，继续轮询
      continue;
    } else {
      console.log(`  发布失败，status: ${status}`);
      return { publish_id: publishId, publish_status: status };
    }
  }
  console.log('  轮询超时，请手动检查发布状态');
  return { publish_id: publishId, publish_status: -1 };
}

// --- 主流程 ---

async function main() {
  console.log('=== 微信公众号文章发布脚本 ===\n');

  // 加载环境变量
  const envPath = join(__dirname, '.baoyu-skills', '.env');
  if (!existsSync(envPath)) {
    console.error(`错误: 找不到 .env 文件: ${envPath}`);
    process.exit(1);
  }
  const env = loadEnv(envPath);
  const appId = env.WECHAT_APP_ID;
  const appSecret = env.WECHAT_APP_SECRET;

  if (!appId || !appSecret) {
    console.error('错误: .env 中缺少 WECHAT_APP_ID 或 WECHAT_APP_SECRET');
    process.exit(1);
  }
  console.log(`AppID: ${appId.slice(0, 6)}...`);

  try {
    // 1. 获取 token
    const token = await getAccessToken(appId, appSecret);

    // 2. 上传封面图
    const coverPath = join(__dirname, 'cover.png');
    const thumbMediaId = await uploadCoverMaterial(token, coverPath);

    // 3-5. 处理文章并上传图片
    const { meta, htmlContent } = await processArticle(token);

    // 6. 创建草稿
    const result = await createDraft(
      token,
      thumbMediaId,
      meta.title || '未命名文章',
      htmlContent,
      meta.summary || '',
      meta.author || ''
    );

    // 7. 正式发布
    const publishResult = await freePublish(token, result.media_id);

    // 保存结果
    const outputDir = join(__dirname, 'output');
    if (!existsSync(outputDir)) {
      mkdirSync(outputDir, { recursive: true });
    }

    const fullResult = {
      success: true,
      timestamp: new Date().toISOString(),
      media_id: result.media_id,
      title: meta.title,
      summary: meta.summary,
      author: meta.author,
      thumb_media_id: thumbMediaId,
      publish_id: publishResult.publish_id,
      article_id: publishResult.article_id || '',
      article_url: publishResult.article_url || '',
      publish_status: publishResult.publish_status,
    };

    const outputPath = join(outputDir, 'full_publish_result.json');
    writeFileSync(outputPath, JSON.stringify(fullResult, null, 2), 'utf-8');

    console.log('\n=== 发布完成 ===');
    console.log(`结果已保存到: ${outputPath}`);
    console.log(JSON.stringify(fullResult, null, 2));

  } catch (err) {
    console.error('\n发布失败:', err.message);

    // 即使失败也保存错误信息
    const outputDir = join(__dirname, 'output');
    if (!existsSync(outputDir)) {
      mkdirSync(outputDir, { recursive: true });
    }
    const errorResult = {
      success: false,
      timestamp: new Date().toISOString(),
      error: err.message,
    };
    const outputPath = join(outputDir, 'full_publish_result.json');
    writeFileSync(outputPath, JSON.stringify(errorResult, null, 2), 'utf-8');
    console.error(`错误信息已保存到: ${outputPath}`);
    process.exit(1);
  }
}

main();


path: templates/run.production-example.sh
content: #!/usr/bin/env bash
set -euo pipefail

TZ="Asia/Shanghai"
export TZ
TODAY="$(date +%F)"
RUN_SLOT="$(date +%H%M%S)"
WORKDIR="/tmp/wechat_daily_${TODAY}_${RUN_SLOT}"
ARTICLE="$WORKDIR/article.md"
FINANCE_JSON="$WORKDIR/finance.json"
TITLE_HISTORY_FILE="<project-dir>/title_history.txt"
PUBLISH_RESULT_JSON="$WORKDIR/publish_result.json"
FULL_PUBLISH_RESULT_JSON="$WORKDIR/full_publish_result.json"
GALLERY_ROOT="/tmp/wechat-gallery"
UNUSED_DIR="$GALLERY_ROOT/unused"
USED_DIR="$GALLERY_ROOT/used"
BAD_DIR="$GALLERY_ROOT/bad"
LOW_STOCK_THRESHOLD=20
PUBLISH_MODE="${WECHAT_AUTO_PUBLISH_MODE:-draft_only}"

mkdir -p "$WORKDIR" "$UNUSED_DIR" "$USED_DIR" "$BAD_DIR" "$(dirname "$TITLE_HISTORY_FILE")"
export WECHAT_AUTO_TITLE_HISTORY="$TITLE_HISTORY_FILE"

echo "[wechat-auto] WORKDIR=$WORKDIR"
echo "[wechat-auto] PUBLISH_MODE=$PUBLISH_MODE"

# 1) gather source material
# 2) generate article.md
# 3) select 2 gallery images
# 4) validate real image formats before publish
# 5) publish to draft
# 6) if full_publish, run freepublish submit/get
# 7) only mutate gallery state on success


path: templates/run.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Safe minimal template only. Replace placeholders in the target environment.
PROJECT_DIR="<project-dir>"
OUTPUT_DIR="$PROJECT_DIR/output"
ARTICLE_PATH="$PROJECT_DIR/article.md"
PUBLISHER_SCRIPT="<path-to-publisher-skill>/scripts/wechat-api.ts"
PUBLISH_MODE="${WECHAT_AUTO_PUBLISH_MODE:-draft_only}"

mkdir -p "$OUTPUT_DIR"
cd "$PROJECT_DIR"

echo "[run.sh] starting workflow at $(date -Iseconds)"
echo "[run.sh] publish_mode=$PUBLISH_MODE"

echo "[run.sh] validate package files"
test -f "$ARTICLE_PATH"
test -f "$PROJECT_DIR/cover.png"

# Body images are optional depending on workflow mode
if [ -f "$PROJECT_DIR/image1.jpg" ]; then echo "image1 present"; fi
if [ -f "$PROJECT_DIR/image2.jpg" ]; then echo "image2 present"; fi

# Example placeholders for a real workflow:
# 1) gather source material
# 2) draft article.md
# 3) prepare cover.png, image1.jpg, image2.jpg
# 4) publish to draft
# 5) if full_publish, run freepublish submit/get
# 6) archive results

# Example draft publishing command:
# npx -y bun "$PUBLISHER_SCRIPT" "$ARTICLE_PATH" --theme default | tee "$OUTPUT_DIR/publish.log"

# Production recommendation:
# default to draft_only unless the operator explicitly chooses full_publish.

echo "[run.sh] workflow template finished"


path: templates/workspace-tree.txt
content: <project-dir>/
├─ .baoyu-skills/
│  ├─ .env
│  ├─ baoyu-image-gen/
│  │  └─ EXTEND.md
│  └─ baoyu-cover-image/
│     └─ EXTEND.md
├─ article.md
├─ cover.png
├─ image1.jpg
├─ image2.jpg
├─ output/
│  ├─ full_publish_result.json
│  ├─ publish_log.jsonl
│  └─ cron.log
└─ run.sh


path: references/environment-and-config.md
content: # Environment and Configuration

Use this reference when reproducing the workflow on a new machine or validating an existing machine.

## Goal

Make the workflow reproducible without storing any private credentials in the skill package.

## Required runtime

Confirm these are installed and callable:

```bash
python3 --version
node --version
npm -v
npx -v
bun --version
```

Minimum expected tools:
- `python3`
- `node`
- `npm`
- `npx`
- `bun`

### Python 通过 uv 管理的情况

如果用户使用 uv 管理 Python，`python3` 命令可能不存在。检查方式：

```bash
# 检查 uv 管理的 Python 版本
uv python list --installed

# 或直接用 python（不带 3）
python --version
```

如果 `python3` 不可用但 `python` 可用，在脚本中使用 `python` 替代即可。

### Bun 版本兼容性

Bun 1.3.x 在 Windows 上与某些 npm 包（如 `simple-xml-to-json`）存在兼容性问题，可能导致运行时报 `SyntaxError`。

建议：
- 保持 Bun 为最新版本（`bun upgrade`）
- 如果遇到兼容性问题，使用 Node.js 作为备用运行时（`node` 替代 `bun`）

## Required local capabilities

Prefer these local skills or equivalent tooling if available:
- `news-aggregator-skill`
- `baoyu-post-to-wechat`
- `baoyu-image-gen`
- `baoyu-cover-image`
- `investment-advisor`
- `wechat-toolkit`
- optionally `equity-research`, `market-environment-analysis`, `marcus-investment-skill`

## Safe config model

Keep real secrets outside the skill package.

Use placeholders like these in examples only:

```env
WECHAT_APP_ID=fill_in_valid_value_in_target_environment
WECHAT_APP_SECRET=fill_in_valid_value_in_target_environment
GOOGLE_BASE_URL=https://api.ikuncode.cc/
GOOGLE_API_KEY=fill_in_valid_value_in_target_environment
```

## Recommended config placement

Preferred order:
1. process environment
2. `<project-dir>/.baoyu-skills/.env`
3. `~/.baoyu-skills/.env`

## 代理环境处理

如果本地开启了 Clash/V2Ray 等代理工具，需要注意代理变量对不同 API 的影响：

- AI 图片生成（Google API）：可能需要代理才能访问
- 微信 API：必须直连，不能走代理

建议在脚本开头统一处理代理变量：

```bash
# 清除代理环境变量（微信 API 需要直连）
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY

# 如果需要代理访问 Google API，在生图阶段单独设置：
# export https_proxy=http://127.0.0.1:7890
```

在生图和发布两个阶段分别处理代理设置，避免代理干扰微信 API 调用。

## Publishing dependency chain

If the Markdown rendering chain is not ready, install it on the target machine:

```bash
cd /root/clawd/skills/baoyu-post-to-wechat/scripts/md
npm install
```

Adjust the path if the publishing skill lives elsewhere in the target environment.

## baoyu-skills 安装

baoyu-skills 是发布流程依赖的核心工具集。

- 仓库地址：https://github.com/jimliu/baoyu-skills
- 安装方式：

```bash
git clone https://github.com/jimliu/baoyu-skills.git
cd baoyu-skills
npm install    # monorepo 结构，主目录安装即可
```

- 如果 `npm install` 后使用 `bun` 运行报错，尝试用 `bun install` 重新安装依赖：

```bash
cd baoyu-skills
bun install
```

## External prerequisites

An operator must manually ensure:
- the server egress IP is in the WeChat allowlist
- the WeChat API credentials are valid in the target environment
- the image service key is valid if image generation is enabled
- the machine can reach WeChat APIs and the configured image service

## Recommended workspace layout

Use a stable project directory in production rather than relying on `/tmp`:

```text
<project-dir>/
├─ .baoyu-skills/
│  ├─ .env
│  ├─ baoyu-image-gen/
│  │  └─ EXTEND.md
│  └─ baoyu-cover-image/
│     └─ EXTEND.md
├─ article.md
├─ cover.png
├─ image1.jpg
├─ image2.jpg
├─ output/
└─ run.sh
```

## Reproduction rule

If the workflow is being moved to another host, reproduce:
- the runtime
- the skill/tool layout
- the non-secret config structure
- the dependency installation steps
- the file naming conventions
- the publish success criteria

## Multi-account deployment

For multiple official accounts, use isolated working directories.

Example:

```text
/root/wechat-auto-a/
/root/wechat-auto-b/
```

Each directory should own its own:
- `.baoyu-skills/.env`
- `article.md` / article generation scripts
- `run.sh`
- `title_history.txt`
- `cron.log`
- `output/`

### Why this matters

This avoids:
- credential confusion
- title-history pollution
- log mixing
- accidental cross-account publishing


path: references/image-strategy.md
content: # Image Strategy

Use this reference when preparing `cover.png`, `image1.jpg`, and `image2.jpg`.

## Output contract

Use these filenames consistently:
- `cover.png`
- `image1.jpg`
- `image2.jpg`

## Cover image

Prefer a dedicated cover-generation path that outputs `cover.png`.

Recommended default characteristics:
- conceptual style
- cool palette
- digital rendering
- little or no text baked into the generated image
- aspect suited for WeChat cover use

The article frontmatter should point to:

```yaml
cover: ./cover.png
```

## Body image sources

Support these sources explicitly:
1. user-provided images
2. local gallery images
3. generated images

The operator may choose the preferred order, but the workflow should make the source explicit.

## Local gallery mode

Recommended gallery layout:

```text
<gallery-root>/
├─ unused/
├─ used/
└─ bad/
```

### Rules

- choose exactly 2 images from `unused/`
- do not select the same image twice in the same article
- only allow supported image formats
- do not mutate gallery state until publish success
- if publish fails, keep selected images in `unused/`
- if fewer than 2 valid images remain, stop the gallery branch and report low stock

### Suggested config model

```text
gallery_enabled = true
gallery_strategy = random
gallery_pick_count = 2
gallery_consume_mode = move_to_used
gallery_low_stock_threshold = 20
gallery_allowed_ext = .jpg,.jpeg,.png,.webp
```

## Generation fallback

If image generation fails, prefer one of these fallback paths if configured:
- local gallery
- user-provided images
- publish without body images only if that is an explicit workflow option

## AI 生图实践经验

### Google Gemini 模型选择

- EXTEND.md 中配置的默认模型为 `gemini-3.1-flash-image-preview`
- 如果使用代理 API（如 `api.ikuncode.cc`），需确认代理支持的模型列表
- 不同模型名称格式不同，错误的模型名会返回 404 错误
- 建议在 EXTEND.md 中明确记录当前使用的模型名，方便排查

### 代理与直连

AI 生图和微信发布对网络环境的要求不同：

- AI 生图调用 Google API 时可能需要代理才能访问
- 生成图片后上传到微信时必须直连（微信 API 不能走代理）
- 建议在生图和发布两个阶段分别处理代理设置：

```bash
# 生图阶段：开启代理
export https_proxy=http://127.0.0.1:7890

# 发布阶段：关闭代理
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY
```

### 图片尺寸建议

- 封面图推荐 2.35:1 宽屏比例（适合微信公众号封面展示）
- 正文配图推荐 3:2 比例
- 生成的图片通常 2-4MB，微信 API 上传限制为 10MB，一般不会超限

## Additional quality gate

Before inserting images into the article package, check:
- file exists
- file is readable
- image dimensions are valid
- file is not empty or obviously broken

### Real-format validation (important practical addition)

Do not trust extension alone. Before publish, validate that the actual file signature / MIME is compatible with the expected upload type.

Common bad case:
- file named `cover.png`
- actual content is HEIF / HEVC
- WeChat upload fails with `40113 unsupported file type hint`

### Recommended normalization rule

Before final upload, normalize images into standard formats when needed:
- cover → PNG or JPEG
- body images → JPEG preferred

If image provenance is uncertain (mobile export / user-provided / fallback gallery), normalization is strongly recommended.

## Failure grading

### Level 1
AI image generation fails, but fallback images are valid → continue.

### Level 2
Body images fail, but cover is valid → continue only if workflow explicitly allows missing body images.

### Level 3
Cover image invalid and no fallback available → block publication.

### Level 4
Cover exists but real format invalid → normalize/re-encode, then retry.


path: references/publishing.md
content: # Publishing

Use this reference when publishing the article package to WeChat draft and optional final publication.

## Safe config rule

Never store real credentials inside the skill package.

Use safe placeholders in examples only:

```env
WECHAT_APP_ID=fill_in_valid_value_in_target_environment
WECHAT_APP_SECRET=fill_in_valid_value_in_target_environment
GOOGLE_BASE_URL=https://api.ikuncode.cc/
GOOGLE_API_KEY=fill_in_valid_value_in_target_environment
```

## Publishing success should be split into 3 layers

### 1. Technical success

Examples:
- access token retrieved
- image upload succeeded
- draft submission succeeded
- `media_id` returned
- `publish_id` returned
- `article_url` returned

### 2. Platform success

Examples:
- draft visible in MP backend
- publish task visible in backend
- publish status reaches success state

### 3. Operational success

Examples:
- article appears where the operator expects in the official account frontend/homepage flow
- article behaves the same way as manual backend publishing

> Important: Technical success does not automatically imply operational success.

## Draft publishing success criteria

Treat draft publishing as successful only if the run returns a meaningful success signal such as:
- successful access token retrieval
- successful image upload
- successful cover upload
- successful draft submission
- returned `media_id`

If no `media_id` is returned, do not mark the draft step as successful.

## Formal publication success criteria

If final publication is enabled, capture:
- `publish_id`
- returned article identifiers if available
- publication status
- final article URL if available

## `freepublish` boundary

`freepublish_submit` is the official API used to submit a draft for publication. In practice, it can successfully return `publish_id`, `article_id`, and `article_url`, but this should not be documented as universally equivalent to manual publishing inside the WeChat MP backend.

### Recommended wording

- `draft/add` = save to draft
- `freepublish_submit` = submit draft for publication
- manual backend publish = platform UI publication behavior

### Recommended production default

Use `draft_only` as the default production mode when homepage visibility consistency matters.

### Recommended testing mode

Use `full_publish` only when the operator explicitly accepts API-publication behavior differences.

## Archive outputs

Save at least:
- title
- execution time
- media_id
- publish_id
- article URL
- success/failure state
- error summary if any
- gallery mutation result if gallery mode is enabled
- publication mode (`draft_only` / `full_publish`)
- whether result is considered technical success only or operationally accepted

Recommended outputs:
- `output/full_publish_result.json`
- `output/publish_log.jsonl`

## Packaging rule before publish

Before publishing, verify:
- `article.md` exists
- `cover.png` exists
- `image1.jpg` and `image2.jpg` exist if body images are enabled
- frontmatter contains `title`, `summary`, and `cover`
- file encoding is UTF-8
- relative file paths resolve correctly
- image real formats are valid for WeChat upload (do not trust extension alone)

## Fallback publish script

When `baoyu-post-to-wechat` fails due to jimp / simple-xml-to-json compatibility issues on Windows + Bun, use `templates/publish.mjs` as a fallback. It is a pure Node.js script with zero third-party dependencies.

Usage:
1. Copy `templates/publish.mjs` to the working directory (same level as `article.md` and `cover.png`)
2. Ensure `.baoyu-skills/.env` contains `WECHAT_APP_ID` and `WECHAT_APP_SECRET`
3. Run: `node publish.mjs`

## IP whitelist notes

- When using a proxy (e.g. Clash), the IP returned by `curl ifconfig.me` or httpbin may not be the real outbound IP
- When WeChat API returns error 40164, the error message includes the actual request IP — use that IP for the whitelist
- After saving the whitelist in the WeChat MP admin console, it may take 1-2 minutes to take effect

## Formal publish flow

`publish.mjs` supports the full publish cycle:
1. Call `freepublish/submit` with the draft `media_id` to submit for publication
2. Poll `freepublish/get` with the returned `publish_id` to check status
3. Poll interval: 3 seconds, max 30 attempts (90 seconds total)
4. `publish_status` values: 0 = success, 1 = publishing, 2 = original content review pending

## HTML rendering optimization

The built-in Markdown-to-HTML converter in `publish.mjs` applies WeChat-friendly styles:
- `h2` headings get a blue left border (`border-left: 4px solid #1e90ff`) with padding for visual emphasis
- Paragraphs get `margin-bottom: 1em` and `line-height: 1.8` for comfortable reading on mobile
- Images get `max-width: 100%` to prevent overflow on small screens

## Image upload validation note

A file extension alone is not enough. Examples of bad cases:
- filename ends with `.png`
- actual file content is HEIF / HEVC
- API rejects upload with `40113 unsupported file type hint`

Therefore, production workflows should include either:
- MIME / magic-byte inspection
- or image re-encoding into standard JPEG / PNG before publish


path: references/scheduling-and-alerting.md
content: # Scheduling and Alerting

Use this reference when attaching automation to the workflow.

## Goal

Allow the article workflow to run on a schedule while keeping failures visible.

## Scheduling modes

### Production recommendation: `draft_only`

Use cron with `draft_only` and let operators manually publish in MP backend. This is recommended when homepage visibility consistency matters.

### Experimental: `full_publish`

Use cron with `full_publish` only when the operator accepts that API publication may not match backend manual publication in homepage visibility.

## Wrapper script responsibilities

A local wrapper should:
- create a dated or timestamped workspace
- gather source material
- draft the article
- prepare images
- publish to draft
- optionally publish formally
- save result artifacts
- notify on failure

## Example schedule shapes

Typical schedules may include morning and afternoon publication windows.

## Logging

Keep at least:
- stdout/stderr log file
- structured publish result artifact
- optional append-only JSONL execution log

## Minimum alerts

Alert on:
- token retrieval failure
- draft publish failure
- formal submit failure
- poll timeout
- missing final article URL
- image preparation failure without fallback
- gallery underflow
- IP 白名单变更告警：公网 IP 可能因运营商变化而改变，建议定期检查出口 IP 是否与白名单一致
- 代理状态告警：如果依赖代理访问 Google API，代理不可用时 AI 生图会失败，需监控代理可达性

## publish_status 状态码说明

正式发布后轮询 `publish_status` 字段含义：
- `0`：发布成功
- `1`：发布中（需继续轮询）
- `2`：原创审核中（已提交，等待审核）
- 其他值：发布失败

## 轮询策略

正式发布后需要轮询获取最终发布结果：
- 轮询间隔：3 秒
- 最大轮询次数：30 次（共 90 秒）
- 超时处理：记录 `publish_id` 供后续手动查询，不视为发布失败
- 轮询接口：`GET https://api.weixin.qq.com/cgi-bin/freepublish/get?access_token=TOKEN`，POST body 包含 `publish_id`

## 归档格式

`full_publish_result.json` 应包含以下完整字段：
- `publish_id`：发布任务 ID
- `article_id`：文章永久 ID
- `article_url`：文章访问链接
- `publish_status`：发布状态码（见上方状态码说明）
- `media_id`：草稿 media_id
- `title`：文章标题
- `timestamp`：发布时间戳

## Scheduler examples

See `templates/cron.example.txt` for a cron-style example and `templates/run.sh` for a wrapper starting point.

### Multi-account cron example

```cron
0 8,17 * * * TZ=Asia/Shanghai WECHAT_AUTO_PUBLISH_MODE=draft_only /root/wechat-auto/run.sh >> /root/wechat-auto/cron.log 2>&1
0 7,16 * * * TZ=Asia/Shanghai WECHAT_AUTO_PUBLISH_MODE=full_publish /root/wechat-auto-niugushashou/run.sh >> /root/wechat-auto-niugushashou/cron.log 2>&1
```

Archive and inspect logs separately per account.


path: references/security-boundary.md
content: # Security Boundary

Use this reference when checking whether the skill package is safe to distribute.

## Allowed inside the skill

The skill may include:
- workflow instructions
- placeholder configuration examples
- directory structure examples
- templates
- wrapper script templates with placeholder paths
- result artifact schemas
- references explaining lookup order and validation steps

## Forbidden inside the skill

The skill must not include:
- real secrets
- real account identifiers unless explicitly required by the user
- cookies, sessions, tokens, access keys, or private URLs
- personal workspace details that are not necessary for reuse
- embedded secret values in templates or shell scripts

## Safe example policy

Examples should use placeholders like:
- `fill_in_valid_value_in_target_environment`
- `<project-dir>`
- `<gallery-root>`
- `<path-to-publisher-skill>`

## Distribution check

Before packaging, inspect the files and confirm:
- no real secret-looking values are present
- no personal `.env` contents are embedded
- no private公众号 identifiers are exposed unintentionally
- no hard-coded credentials appear in scripts or templates

## 安全实践经验

### 代理 API Key 安全

如果使用第三方代理（如 `api.ikuncode.cc`）访问 Google API，代理的 API Key 也属于敏感信息，必须放在 `.env` 中，不能硬编码在脚本或模板里。代理 Key 泄露可能导致额度被盗用或服务被滥用。

### 微信 AppSecret 保护

AppSecret 泄露可导致公众号被完全控制（包括群发消息、修改配置等）。建议：
- `.env` 文件权限设为 `600`（仅所有者可读写）：`chmod 600 .env`
- 不要将 `.env` 提交到版本控制，确保 `.gitignore` 包含 `.env`
- 定期轮换 AppSecret

### 发布脚本安全

`templates/publish.mjs` 中的代理清除逻辑（`delete process.env.http_proxy` 等）是安全必要操作。微信 API 调用获取的 access_token 不应通过第三方代理传输，否则存在 token 被中间人截获的风险。发布脚本在调用微信接口前必须确保代理已清除。

### IP 白名单

微信公众号后台的 IP 白名单是重要的安全防线：
- 只添加必要的出口 IP，不要使用过宽的 CIDR 段
- 定期检查白名单中是否有多余或过期的 IP
- 公网 IP 可能因运营商调整而变化，变化后需及时更新白名单
- 新环境部署时，先确认出口 IP 再添加到白名单


path: references/source-gathering.md
content: # Source Gathering

Use this reference when collecting source material for the day’s WeChat article.

## Goal

Build a compact but high-signal source pool for article drafting.

## Recommended collection process

1. Gather 8-15 raw items
2. Narrow to 3-5 items worth writing about
3. Record for each kept item:
   - title
   - source
   - time
   - one-line summary
   - linked topic or sector

## What to gather

### Macro and market environment
- overseas market movement
- rates, bonds, and central-bank signals
- oil, gold, and major commodities
- geopolitics
- policy and regulatory changes

### Sector and theme signals
Typical candidates:
- energy
- chemicals
- technology
- AI infrastructure
- optical/network themes
- power-grid themes
- unexpected breakout themes

## Market-angle compression

Before drafting, compress the day into 3-5 observations, such as:
- whether the market is trend-driven or rotation-driven
- whether the setup is risk-on or risk-off
- what cross-market signals matter most
- what retail-reader emotional hook is strongest today

## Output expectation

The drafting step should receive:
- a filtered source list
- a short market judgment
- one or two primary directions worth expanding


path: references/writing-style.md
content: # Writing Style

Use this reference when drafting the WeChat article body.

## Goal

Produce an article that is readable inside WeChat, emotionally direct, and visually light.

## Style principles

- lead with emotion before analysis
- keep paragraphs short
- write from a retail-reader viewpoint when that matches the target account style
- avoid formal report language unless the user explicitly wants it
- include a clear tomorrow-watchlist section
- preserve a light interaction-oriented ending

## Title guidance

Prefer short, emotionally punchy titles when the account style supports it.

## Opening guidance

Start directly with an emotion, reaction, complaint, or clear conclusion. Avoid long warm-up paragraphs.

## Body structure

A common shape is:
1. opening emotional reaction
2. several numbered sections
3. current stance / current operation
4. tomorrow watchlist
5. interaction-oriented ending

## Publishable Markdown conventions

Use explicit frontmatter:

```markdown
---
title: 文章标题
author: 作者名
summary: 一句话摘要
cover: ./cover.png
---
```

Use standard Markdown image syntax for body images:

```markdown
![image](./image1.jpg)
```

For numbered subheads, use `##` heading with `、` separator for better visual distinction in WeChat:

```markdown
## 1、第一个小标题
```

This renders as a styled h2 heading with a blue left border in WeChat, providing clear visual separation between sections.

## Suggested publish-ready pattern

```markdown
---
title: 文章标题
author: 作者名
summary: 一句话摘要
cover: ./cover.png
---

![image](./image1.jpg)

开头第一句直接抛情绪。

## 1、第一个小标题

这里写正文。

## 2、第二个小标题

这里写正文。

## 3、我的操作？

这里写态度。

## 4、明日观察重点：

（1）观察点一

（2）观察点二

（3）观察点三

![image](./image2.jpg)

互动结尾。
```

## Avoid

Avoid these unless the user explicitly wants them:
- formal research-report phrasing
- oversized data dumps
- long theory-heavy paragraphs
- generic compliance-style endings

## WeChat formatting lessons learned

1. **标题格式**：推荐使用 `## 1、标题` 格式而非纯文本数字编号，在微信中渲染为带样式的二级标题（蓝色左边框），视觉区分度更好
2. **避免重复标题**：frontmatter 中已有 title 字段，正文不要再用 `# 标题` 重复，否则微信显示会出现两个标题
3. **段落间距**：每个话题/模块之间需要空行分隔，否则文字混在一起难以阅读
4. **作者名**：在 frontmatter 的 author 字段设置即可，不要在正文中重复写作者名

## Writing quality rule (from real workflow experience)

Do not directly paste scraped news titles into the article body.

### Avoid

- raw headline insertion
- truncated fragments ending with `...`
- media-report phrasing such as `报道：` left untouched
- title-like syntax being treated as final paragraph content

### Prefer

1. clean the source title
2. remove media-style prefixes
3. understand the event meaning
4. rewrite in the target author voice
5. ensure each paragraph reads like a complete paragraph, not a source-material placeholder

### Example

**Weak body sentence:**

`"48小时通牒"与"45天停火协议"挑动市场，周一油价冲高回落，黄金下跌、美股期货转涨，...`

**Better rewritten paragraph:**

`停火预期一出来，市场情绪立刻被带着跑，油价先冲高，随后又回落，黄金和风险资产也跟着来回波动。真正难的不是没有方向，而是消息反复，节奏特别容易做错。`


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)