feat(baoyu-post-to-wechat): default bottom citations for markdown input

Enable bottom citations by default for markdown articles. Add --no-cite
flag to all posting scripts (wechat-api, wechat-article, md-to-wechat)
to optionally disable. WeChat mp links stay inline.
This commit is contained in:
Jim Liu 宝玉
2026-03-06 23:38:23 -06:00
parent 8cea550627
commit 970f5e741a
5 changed files with 65 additions and 15 deletions
+8 -3
View File
@@ -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 (贴图, formerly 图文) 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. Markdown article workflows default to converting ordinary external links into bottom citations for WeChat-friendly output. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
--- ---
# Post to WeChat Official Account # Post to WeChat Official Account
@@ -252,10 +252,15 @@ WECHAT_APP_SECRET=<user_input>
**CRITICAL**: Publishing scripts handle markdown conversion internally. Do NOT pre-convert markdown to HTML — pass the original markdown file directly. This ensures the API method renders images as `<img>` tags (for API upload) while the browser method uses placeholders (for paste-and-replace workflow). **CRITICAL**: Publishing scripts handle markdown conversion internally. Do NOT pre-convert markdown to HTML — pass the original markdown file directly. This ensures the API method renders images as `<img>` tags (for API upload) while the browser method uses placeholders (for paste-and-replace workflow).
**Markdown citation default**:
- For markdown input, ordinary external links are converted to bottom citations by default.
- Use `--no-cite` only if the user explicitly wants to keep ordinary external links inline.
- Existing HTML input is left as-is; no extra citation conversion is applied.
**API method** (accepts `.md` or `.html`): **API method** (accepts `.md` or `.html`):
```bash ```bash
${BUN_X} ${SKILL_DIR}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>] ${BUN_X} ${SKILL_DIR}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>] [--no-cite]
``` ```
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md. **CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
@@ -274,7 +279,7 @@ If script parameters do not expose the two comment fields, still ensure final AP
**Browser method** (accepts `--markdown` or `--html`): **Browser method** (accepts `--markdown` or `--html`):
```bash ```bash
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>] ${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>] [--no-cite]
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --html <html_file> ${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --html <html_file>
``` ```
@@ -11,6 +11,9 @@ ${BUN_X} ./scripts/wechat-article.ts --markdown article.md
# With theme # With theme
${BUN_X} ./scripts/wechat-article.ts --markdown article.md --theme grace ${BUN_X} ./scripts/wechat-article.ts --markdown article.md --theme grace
# Disable bottom citations for ordinary external links
${BUN_X} ./scripts/wechat-article.ts --markdown article.md --no-cite
# With explicit options # With explicit options
${BUN_X} ./scripts/wechat-article.ts --markdown article.md --author "作者名" --summary "摘要" ${BUN_X} ./scripts/wechat-article.ts --markdown article.md --author "作者名" --summary "摘要"
``` ```
@@ -21,6 +24,7 @@ ${BUN_X} ./scripts/wechat-article.ts --markdown article.md --author "作者名"
|-----------|-------------| |-----------|-------------|
| `--markdown <path>` | Markdown file to convert and post | | `--markdown <path>` | Markdown file to convert and post |
| `--theme <name>` | Theme: default, grace, simple, modern | | `--theme <name>` | Theme: default, grace, simple, modern |
| `--no-cite` | Keep ordinary external links inline instead of converting them to bottom citations |
| `--title <text>` | Override title (auto-extracted from markdown) | | `--title <text>` | Override title (auto-extracted from markdown) |
| `--author <name>` | Author name | | `--author <name>` | Author name |
| `--summary <text>` | Article summary | | `--summary <text>` | Article summary |
@@ -51,6 +55,8 @@ Regular paragraph with **bold** and *italic*.
[Link text](https://example.com) [Link text](https://example.com)
``` ```
Markdown mode converts ordinary external links into bottom citations by default for WeChat-friendly output. Use `--no-cite` to disable that behavior.
## Image Handling ## Image Handling
1. **Parse**: Images in markdown are replaced with `WECHATIMGPH_N` 1. **Parse**: Images in markdown are replaced with `WECHATIMGPH_N`
@@ -112,10 +112,14 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
return { frontmatter, body: match[2]! }; return { frontmatter, body: match[2]! };
} }
export async function convertMarkdown(markdownPath: string, options?: { title?: string; theme?: string; color?: string }): Promise<ParsedResult> { export async function convertMarkdown(
markdownPath: string,
options?: { title?: string; theme?: string; color?: string; citeStatus?: boolean }
): Promise<ParsedResult> {
const baseDir = path.dirname(markdownPath); const baseDir = path.dirname(markdownPath);
const content = fs.readFileSync(markdownPath, 'utf-8'); const content = fs.readFileSync(markdownPath, 'utf-8');
const theme = options?.theme ?? 'default'; const theme = options?.theme ?? 'default';
const citeStatus = options?.citeStatus ?? true;
const { frontmatter, body } = parseFrontmatter(content); const { frontmatter, body } = parseFrontmatter(content);
@@ -185,8 +189,9 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const renderArgs = ['-y', 'bun', renderScript, tempMdPath, '--theme', theme]; const renderArgs = ['-y', 'bun', renderScript, tempMdPath, '--theme', theme];
if (options?.color) renderArgs.push('--color', options.color); if (options?.color) renderArgs.push('--color', options.color);
if (citeStatus) renderArgs.push('--cite');
console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}${options?.color ? `, color: ${options.color}` : ''}`); console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}${options?.color ? `, color: ${options.color}` : ''}, citeStatus: ${citeStatus}`);
const result = spawnSync('npx', renderArgs, { const result = spawnSync('npx', renderArgs, {
stdio: ['inherit', 'pipe', 'pipe'], stdio: ['inherit', 'pipe', 'pipe'],
@@ -232,6 +237,7 @@ Options:
--title <title> Override title --title <title> Override title
--theme <name> Theme name (default, grace, simple, modern) --theme <name> Theme name (default, grace, simple, modern)
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex) --color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--no-cite Disable bottom citations for ordinary external links
--help Show this help --help Show this help
Output JSON format: Output JSON format:
@@ -251,6 +257,7 @@ Example:
npx -y bun md-to-wechat.ts article.md npx -y bun md-to-wechat.ts article.md
npx -y bun md-to-wechat.ts article.md --theme grace npx -y bun md-to-wechat.ts article.md --theme grace
npx -y bun md-to-wechat.ts article.md --theme modern --color blue npx -y bun md-to-wechat.ts article.md --theme modern --color blue
npx -y bun md-to-wechat.ts article.md --no-cite
`); `);
process.exit(0); process.exit(0);
} }
@@ -265,6 +272,7 @@ async function main(): Promise<void> {
let title: string | undefined; let title: string | undefined;
let theme: string | undefined; let theme: string | undefined;
let color: string | undefined; let color: string | undefined;
let citeStatus = true;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]!; const arg = args[i]!;
@@ -274,6 +282,10 @@ async function main(): Promise<void> {
theme = args[++i]; theme = args[++i];
} else if (arg === '--color' && args[i + 1]) { } else if (arg === '--color' && args[i + 1]) {
color = args[++i]; color = args[++i];
} else if (arg === '--cite') {
citeStatus = true;
} else if (arg === '--no-cite') {
citeStatus = false;
} else if (!arg.startsWith('-')) { } else if (!arg.startsWith('-')) {
markdownPath = arg; markdownPath = arg;
} }
@@ -289,7 +301,7 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
const result = await convertMarkdown(markdownPath, { title, theme, color }); const result = await convertMarkdown(markdownPath, { title, theme, color, citeStatus });
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }
@@ -307,7 +307,12 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
return { frontmatter, body: match[2]! }; return { frontmatter, body: match[2]! };
} }
function renderMarkdownToHtml(markdownPath: string, theme: string = "default", color?: string): string { function renderMarkdownToHtml(
markdownPath: string,
theme: string = "default",
color?: string,
citeStatus: boolean = true
): string {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const renderScript = path.join(__dirname, "md", "render.ts"); const renderScript = path.join(__dirname, "md", "render.ts");
@@ -315,8 +320,9 @@ function renderMarkdownToHtml(markdownPath: string, theme: string = "default", c
const renderArgs = ["-y", "bun", renderScript, markdownPath, "--theme", theme]; const renderArgs = ["-y", "bun", renderScript, markdownPath, "--theme", theme];
if (color) renderArgs.push("--color", color); if (color) renderArgs.push("--color", color);
if (citeStatus) renderArgs.push("--cite");
console.error(`[wechat-api] Rendering markdown with theme: ${theme}${color ? `, color: ${color}` : ""}`); console.error(`[wechat-api] Rendering markdown with theme: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);
const result = spawnSync("npx", renderArgs, { const result = spawnSync("npx", renderArgs, {
stdio: ["inherit", "pipe", "pipe"], stdio: ["inherit", "pipe", "pipe"],
cwd: baseDir, cwd: baseDir,
@@ -362,6 +368,7 @@ Options:
--theme <name> Theme name for markdown (default, grace, simple, modern). Default: default --theme <name> Theme name for markdown (default, grace, simple, modern). Default: default
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex) --color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--cover <path> Cover image path (local or URL) --cover <path> Cover image path (local or URL)
--no-cite Disable bottom citations for ordinary external links in markdown mode
--dry-run Parse and render only, don't publish --dry-run Parse and render only, don't publish
--help Show this help --help Show this help
@@ -390,6 +397,7 @@ Example:
npx -y bun wechat-api.ts article.html --title "My Article" npx -y bun wechat-api.ts article.html --title "My Article"
npx -y bun wechat-api.ts images/ --type newspic --title "Photo Album" npx -y bun wechat-api.ts images/ --type newspic --title "Photo Album"
npx -y bun wechat-api.ts article.md --dry-run npx -y bun wechat-api.ts article.md --dry-run
npx -y bun wechat-api.ts article.md --no-cite
`); `);
process.exit(0); process.exit(0);
} }
@@ -404,6 +412,7 @@ interface CliArgs {
theme: string; theme: string;
color?: string; color?: string;
cover?: string; cover?: string;
citeStatus: boolean;
dryRun: boolean; dryRun: boolean;
} }
@@ -417,6 +426,7 @@ function parseArgs(argv: string[]): CliArgs {
isHtml: false, isHtml: false,
articleType: "news", articleType: "news",
theme: "default", theme: "default",
citeStatus: true,
dryRun: false, dryRun: false,
}; };
@@ -439,6 +449,10 @@ function parseArgs(argv: string[]): CliArgs {
args.color = argv[++i]; args.color = argv[++i];
} else if (arg === "--cover" && argv[i + 1]) { } else if (arg === "--cover" && argv[i + 1]) {
args.cover = argv[++i]; args.cover = argv[++i];
} else if (arg === "--cite") {
args.citeStatus = true;
} else if (arg === "--no-cite") {
args.citeStatus = false;
} else if (arg === "--dry-run") { } else if (arg === "--dry-run") {
args.dryRun = true; args.dryRun = true;
} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) { } else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) {
@@ -513,8 +527,8 @@ async function main(): Promise<void> {
if (!author) author = frontmatter.author || ""; if (!author) author = frontmatter.author || "";
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || ""; if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}`); console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}, citeStatus: ${args.citeStatus}`);
htmlPath = renderMarkdownToHtml(filePath, args.theme, args.color); htmlPath = renderMarkdownToHtml(filePath, args.theme, args.color, args.citeStatus);
console.error(`[wechat-api] HTML generated: ${htmlPath}`); console.error(`[wechat-api] HTML generated: ${htmlPath}`);
htmlContent = extractHtmlContent(htmlPath); htmlContent = extractHtmlContent(htmlPath);
} }
@@ -20,6 +20,7 @@ interface ArticleOptions {
markdownFile?: string; markdownFile?: string;
theme?: string; theme?: string;
color?: string; color?: string;
citeStatus?: boolean;
author?: string; author?: string;
summary?: string; summary?: string;
images?: string[]; images?: string[];
@@ -182,13 +183,19 @@ async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void>
await sleep(1000); await sleep(1000);
} }
async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: string, color?: string): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> { async function parseMarkdownWithPlaceholders(
markdownPath: string,
theme?: string,
color?: string,
citeStatus: boolean = true
): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const mdToWechatScript = path.join(__dirname, 'md-to-wechat.ts'); const mdToWechatScript = path.join(__dirname, 'md-to-wechat.ts');
const args = ['-y', 'bun', mdToWechatScript, markdownPath]; const args = ['-y', 'bun', mdToWechatScript, markdownPath];
if (theme) args.push('--theme', theme); if (theme) args.push('--theme', theme);
if (color) args.push('--color', color); if (color) args.push('--color', color);
if (!citeStatus) args.push('--no-cite');
const result = spawnSync('npx', args, { stdio: ['inherit', 'pipe', 'pipe'] }); const result = spawnSync('npx', args, { stdio: ['inherit', 'pipe', 'pipe'] });
if (result.status !== 0) { if (result.status !== 0) {
@@ -383,7 +390,7 @@ async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<b
} }
export async function postArticle(options: ArticleOptions): Promise<void> { export async function postArticle(options: ArticleOptions): Promise<void> {
const { title, content, htmlFile, markdownFile, theme, color, author, summary, images = [], submit = false, profileDir, cdpPort } = options; const { title, content, htmlFile, markdownFile, theme, color, citeStatus = true, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
let { contentImages = [] } = options; let { contentImages = [] } = options;
let effectiveTitle = title || ''; let effectiveTitle = title || '';
let effectiveAuthor = author || ''; let effectiveAuthor = author || '';
@@ -392,7 +399,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
if (markdownFile) { if (markdownFile) {
console.log(`[wechat] Parsing markdown: ${markdownFile}`); console.log(`[wechat] Parsing markdown: ${markdownFile}`);
const parsed = await parseMarkdownWithPlaceholders(markdownFile, theme, color); const parsed = await parseMarkdownWithPlaceholders(markdownFile, theme, color, citeStatus);
effectiveTitle = effectiveTitle || parsed.title; effectiveTitle = effectiveTitle || parsed.title;
effectiveAuthor = effectiveAuthor || parsed.author; effectiveAuthor = effectiveAuthor || parsed.author;
effectiveSummary = effectiveSummary || parsed.summary; effectiveSummary = effectiveSummary || parsed.summary;
@@ -677,6 +684,7 @@ Options:
--markdown <path> Markdown file to convert and post (recommended) --markdown <path> Markdown file to convert and post (recommended)
--theme <name> Theme for markdown (default, grace, simple, modern) --theme <name> Theme for markdown (default, grace, simple, modern)
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex) --color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--no-cite Disable bottom citations for ordinary external links in markdown mode
--author <name> Author name --author <name> Author name
--summary <text> Article summary --summary <text> Article summary
--image <path> Content image, can repeat (only with --content) --image <path> Content image, can repeat (only with --content)
@@ -687,13 +695,15 @@ Options:
Examples: Examples:
npx -y bun wechat-article.ts --markdown article.md npx -y bun wechat-article.ts --markdown article.md
npx -y bun wechat-article.ts --markdown article.md --theme grace --submit npx -y bun wechat-article.ts --markdown article.md --theme grace --submit
npx -y bun wechat-article.ts --markdown article.md --no-cite
npx -y bun wechat-article.ts --title "标题" --content "内容" --image img.png npx -y bun wechat-article.ts --title "标题" --content "内容" --image img.png
npx -y bun wechat-article.ts --title "标题" --html article.html --submit npx -y bun wechat-article.ts --title "标题" --html article.html --submit
Markdown mode: Markdown mode:
Images in markdown are converted to placeholders. After pasting HTML, Images in markdown are converted to placeholders. After pasting HTML,
each placeholder is selected, scrolled into view, deleted, and replaced each placeholder is selected, scrolled into view, deleted, and replaced
with the actual image via paste. with the actual image via paste. Ordinary external links are converted to
bottom citations by default.
`); `);
process.exit(0); process.exit(0);
} }
@@ -709,6 +719,7 @@ async function main(): Promise<void> {
let markdownFile: string | undefined; let markdownFile: string | undefined;
let theme: string | undefined; let theme: string | undefined;
let color: string | undefined; let color: string | undefined;
let citeStatus = true;
let author: string | undefined; let author: string | undefined;
let summary: string | undefined; let summary: string | undefined;
let submit = false; let submit = false;
@@ -723,6 +734,8 @@ async function main(): Promise<void> {
else if (arg === '--markdown' && args[i + 1]) markdownFile = args[++i]; else if (arg === '--markdown' && args[i + 1]) markdownFile = args[++i];
else if (arg === '--theme' && args[i + 1]) theme = args[++i]; else if (arg === '--theme' && args[i + 1]) theme = args[++i];
else if (arg === '--color' && args[i + 1]) color = args[++i]; else if (arg === '--color' && args[i + 1]) color = args[++i];
else if (arg === '--cite') citeStatus = true;
else if (arg === '--no-cite') citeStatus = false;
else if (arg === '--author' && args[i + 1]) author = args[++i]; else if (arg === '--author' && args[i + 1]) author = args[++i];
else if (arg === '--summary' && args[i + 1]) summary = args[++i]; else if (arg === '--summary' && args[i + 1]) summary = args[++i];
else if (arg === '--image' && args[i + 1]) images.push(args[++i]!); else if (arg === '--image' && args[i + 1]) images.push(args[++i]!);
@@ -734,7 +747,7 @@ async function main(): Promise<void> {
if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); } if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); }
if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); } if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); }
await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, color, author, summary, images, submit, profileDir, cdpPort }); await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, color, citeStatus, author, summary, images, submit, profileDir, cdpPort });
} }
await main().then(() => { await main().then(() => {