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