diff --git a/skills/baoyu-post-to-wechat/scripts/md-to-wechat.ts b/skills/baoyu-post-to-wechat/scripts/md-to-wechat.ts index b4ecdd2..690b5d0 100644 --- a/skills/baoyu-post-to-wechat/scripts/md-to-wechat.ts +++ b/skills/baoyu-post-to-wechat/scripts/md-to-wechat.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { mkdir, writeFile } from 'node:fs/promises'; import os from 'node:os'; import { createHash } from 'node:crypto'; +import { fileURLToPath } from 'node:url'; import https from 'node:https'; import http from 'node:http'; import { spawnSync } from 'node:child_process'; @@ -100,10 +101,7 @@ function parseFrontmatter(content: string): { frontmatter: Record 0) { const key = line.slice(0, colonIdx).trim(); - let value = line.slice(colonIdx + 1).trim(); - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } + const value = line.slice(colonIdx + 1).trim(); frontmatter[key] = value; } } @@ -111,42 +109,21 @@ function parseFrontmatter(content: string): { frontmatter: Record { - const content = fs.readFileSync(markdownPath, 'utf-8'); +export async function convertMarkdown(markdownPath: string, theme = 'default'): Promise { const baseDir = path.dirname(markdownPath); - const tempDir = options?.tempDir ?? path.join(os.tmpdir(), 'wechat-article-images'); - const theme = options?.theme ?? 'default'; - - await mkdir(tempDir, { recursive: true }); + const content = fs.readFileSync(markdownPath, 'utf-8'); const { frontmatter, body } = parseFrontmatter(content); - let title = options?.title ?? frontmatter.title ?? ''; - if (!title) { - const h1Match = body.match(/^#\s+(.+)$/m); - if (h1Match) title = h1Match[1]!; - } - - const author = frontmatter.author ?? ''; - let summary = frontmatter.summary ?? frontmatter.description ?? ''; + let title = frontmatter.title || path.basename(markdownPath, path.extname(markdownPath)); + const author = frontmatter.author || ''; + let summary = frontmatter.description || frontmatter.summary || ''; if (!summary) { - const lines = body.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - if (trimmed.startsWith('#')) continue; - if (trimmed.startsWith('![')) continue; - if (trimmed.startsWith('>')) continue; - if (trimmed.startsWith('-') || trimmed.startsWith('*')) continue; - if (/^\d+\./.test(trimmed)) continue; - - const cleanText = trimmed - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1') + const paragraphs = body.split('\n\n').filter(p => p.trim() && !p.startsWith('#')); + for (const para of paragraphs) { + const cleanText = para + .replace(/[#*`_\[\]]/g, '') .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') .replace(/`([^`]+)`/g, '$1'); @@ -168,13 +145,19 @@ async function parseMarkdownForWechat( const modifiedMarkdown = `---\n${Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n${modifiedBody}`; + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-article-images-')); const tempMdPath = path.join(tempDir, 'temp-article.md'); await writeFile(tempMdPath, modifiedMarkdown, 'utf-8'); - const scriptDir = path.dirname(new URL(import.meta.url).pathname); - const renderScript = path.join(scriptDir, 'md', 'render.ts'); + // 使用 fileURLToPath 来正确处理 Windows 路径 + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const renderScript = path.join(__dirname, 'md', 'render.ts'); console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}`); + console.error(`[md-to-wechat] Script dir: ${__dirname}`); + console.error(`[md-to-wechat] Render script: ${renderScript}`); + const result = spawnSync('npx', ['-y', 'bun', renderScript, tempMdPath, '--theme', theme], { stdio: ['inherit', 'pipe', 'pipe'], cwd: baseDir, @@ -213,7 +196,7 @@ function printUsage(): never { console.log(`Convert Markdown to WeChat-ready HTML with image placeholders Usage: - npx -y bun md-to-wechat.ts [options] + npx -y bun md-to-wechat-fixed.ts [options] Options: --title Override title @@ -227,15 +210,15 @@ Output JSON format: "contentImages": [ { "placeholder": "[[IMAGE_PLACEHOLDER_1]]", - "localPath": "/tmp/wechat-article-images/img.png", + "localPath": "/tmp/wechat-image/img.png", "originalPath": "imgs/image.png" } ] } Example: - 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-fixed.ts article.md + npx -y bun md-to-wechat-fixed.ts article.md --theme grace `); process.exit(0); } @@ -247,22 +230,21 @@ async function main(): Promise<void> { } let markdownPath: string | undefined; - let title: string | undefined; - let theme: string | undefined; + let theme = 'default'; for (let i = 0; i < args.length; i++) { const arg = args[i]!; if (arg === '--title' && args[i + 1]) { - title = args[++i]; + args[i + 1]; // skip value } else if (arg === '--theme' && args[i + 1]) { - theme = args[++i]; - } else if (!arg.startsWith('-')) { + theme = args[++i]!; + } else if (!arg.startsWith('--')) { markdownPath = arg; } } if (!markdownPath) { - console.error('Error: Markdown file path required'); + console.error('Error: Markdown file path is required'); process.exit(1); } @@ -271,7 +253,7 @@ async function main(): Promise<void> { process.exit(1); } - const result = await parseMarkdownForWechat(markdownPath, { title, theme }); + const result = await convertMarkdown(markdownPath, theme); console.log(JSON.stringify(result, null, 2)); } diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts index 2c866d5..2f992cc 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import process from 'node:process'; +import { fileURLToPath } from 'node:url'; import { launchChrome, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts'; const WECHAT_URL = 'https://mp.weixin.qq.com/'; @@ -65,8 +66,9 @@ async function clickMenuByText(session: ChromeSession, text: string): Promise<vo } async function copyImageToClipboard(imagePath: string): Promise<void> { - const scriptDir = path.dirname(new URL(import.meta.url).pathname); - const copyScript = path.join(scriptDir, './copy-to-clipboard.ts'); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const copyScript = path.join(__dirname, './copy-to-clipboard.ts'); const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' }); if (result.status !== 0) throw new Error(`Failed to copy image: ${imagePath}`); } @@ -108,30 +110,56 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr }, { sessionId }); await sleep(300); - console.log('[wechat] Copying with system Cmd+C...'); - if (process.platform === 'darwin') { - spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']); - } else { - spawnSync('xdotool', ['key', 'ctrl+c']); - } + console.log('[wechat] Copying with CDP keyboard event...'); + // 使用 CDP 发送 Ctrl+C (更可靠,不依赖系统工具) + const modifiers = process.platform === 'darwin' ? 4 : 2; // 4=Cmd, 2=Ctrl + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key: 'c', + code: 'KeyC', + modifiers, + windowsVirtualKeyCode: 67 + }, { sessionId }); + await sleep(50); + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'c', + code: 'KeyC', + modifiers, + windowsVirtualKeyCode: 67 + }, { sessionId }); await sleep(1000); console.log('[wechat] Closing HTML tab...'); await cdp.send('Target.closeTarget', { targetId }); } -async function pasteFromClipboardInEditor(): Promise<void> { - if (process.platform === 'darwin') { - spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']); - } else { - spawnSync('xdotool', ['key', 'ctrl+v']); - } +async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void> { + console.log('[wechat] Pasting with CDP keyboard event...'); + // 使用 CDP 发送 Ctrl+V (更可靠,不依赖系统工具) + const modifiers = process.platform === 'darwin' ? 4 : 2; // 4=Cmd, 2=Ctrl + await session.cdp.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key: 'v', + code: 'KeyV', + modifiers, + windowsVirtualKeyCode: 86 + }, { sessionId: session.sessionId }); + await sleep(50); + await session.cdp.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'v', + code: 'KeyV', + modifiers, + windowsVirtualKeyCode: 86 + }, { sessionId: session.sessionId }); await sleep(1000); } async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: string): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> { - const scriptDir = path.dirname(new URL(import.meta.url).pathname); - const mdToWechatScript = path.join(scriptDir, 'md-to-wechat.ts'); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const mdToWechatScript = path.join(__dirname, 'md-to-wechat-fixed.ts'); const args = ['-y', 'bun', mdToWechatScript, markdownPath]; if (theme) args.push('--theme', theme); @@ -295,6 +323,11 @@ export async function postArticle(options: ArticleOptions): Promise<void> { console.log('[wechat] Clicking on editor...'); await clickElement(session, '.ProseMirror'); + await sleep(1000); + + // 再次点击确保焦点 + console.log('[wechat] Ensuring editor focus...'); + await clickElement(session, '.ProseMirror'); await sleep(500); if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) { @@ -302,7 +335,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> { await copyHtmlFromBrowser(cdp, effectiveHtmlFile); await sleep(500); console.log('[wechat] Pasting into editor...'); - await pasteFromClipboardInEditor(); + await pasteFromClipboardInEditor(session); await sleep(3000); if (contentImages.length > 0) { @@ -328,7 +361,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> { await sleep(200); console.log('[wechat] Pasting image...'); - await pasteFromClipboardInEditor(); + await pasteFromClipboardInEditor(session); await sleep(3000); } console.log('[wechat] All images inserted.'); @@ -376,7 +409,7 @@ function printUsage(): never { console.log(`Post article to WeChat Official Account Usage: - npx -y bun wechat-article.ts [options] + npx -y bun wechat-article-fixed.ts [options] Options: --title <text> Article title (auto-extracted from markdown) @@ -391,10 +424,10 @@ Options: --profile <dir> Chrome profile directory Examples: - 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 --title "标题" --content "内容" --image img.png - npx -y bun wechat-article.ts --title "标题" --html article.html --submit + npx -y bun wechat-article-fixed.ts --markdown article.md + npx -y bun wechat-article-fixed.ts --markdown article.md --theme grace --submit + npx -y bun wechat-article-fixed.ts --title "标题" --content "内容" --image img.png + npx -y bun wechat-article-fixed.ts --title "标题" --html article.html --submit Markdown mode: Images in markdown are converted to placeholders. After pasting HTML,