mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-09 20:51:22 +00:00
Fix WeChat browser article publishing
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const clipboardScript = fs.readFileSync(path.join(__dirname, "copy-to-clipboard.ts"), "utf8");
|
||||
|
||||
test("macOS image clipboard copy avoids Swift AppKit JIT", () => {
|
||||
assert.match(clipboardScript, /copyImageMacWithOsascript/);
|
||||
assert.doesNotMatch(clipboardScript, /await runCommand\('swift', \[swiftPath, 'image', imagePath\]\)/);
|
||||
});
|
||||
@@ -186,12 +186,36 @@ default:
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeAppleScriptString(value: string): string {
|
||||
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function getAppleScriptImageType(imagePath: string): string {
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.png':
|
||||
return '«class PNGf»';
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
return 'JPEG picture';
|
||||
case '.gif':
|
||||
return 'GIF picture';
|
||||
default:
|
||||
throw new Error(`macOS clipboard image copy supports PNG, JPEG, and GIF via AppleScript; convert ${ext || 'this file'} to PNG first.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyImageMacWithOsascript(imagePath: string): Promise<void> {
|
||||
const imageType = getAppleScriptImageType(imagePath);
|
||||
const escapedPath = escapeAppleScriptString(imagePath);
|
||||
await runCommand('osascript', [
|
||||
'-e',
|
||||
`set the clipboard to (read (POSIX file "${escapedPath}") as ${imageType})`,
|
||||
]);
|
||||
}
|
||||
|
||||
async function copyImageMac(imagePath: string): Promise<void> {
|
||||
await withTempDir('copy-to-clipboard-', async (tempDir) => {
|
||||
const swiftPath = path.join(tempDir, 'clipboard.swift');
|
||||
await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');
|
||||
await runCommand('swift', [swiftPath, 'image', imagePath]);
|
||||
});
|
||||
await copyImageMacWithOsascript(imagePath);
|
||||
}
|
||||
|
||||
async function copyHtmlMac(htmlFilePath: string): Promise<void> {
|
||||
@@ -377,4 +401,3 @@ await main().catch((err) => {
|
||||
console.error(`Error: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const articleScript = fs.readFileSync(path.join(__dirname, "wechat-article.ts"), "utf8");
|
||||
|
||||
test("browser article paste uses CDP-targeted paste instead of global macOS keystrokes", () => {
|
||||
assert.equal(
|
||||
articleScript.includes('tell application "System Events" to keystroke "v" using command down'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("browser article publishing verifies the title before saving drafts", () => {
|
||||
assert.match(articleScript, /verifyTitleUnchangedBeforeSave/);
|
||||
assert.match(articleScript, /Title was modified during paste/);
|
||||
});
|
||||
|
||||
test("browser article body insertion does not paste HTML through the active form field", () => {
|
||||
assert.match(articleScript, /insertHtmlIntoEditorFromFile/);
|
||||
assert.doesNotMatch(articleScript, /await copyHtmlFromBrowser\(cdp, effectiveHtmlFile, contentImages\)/);
|
||||
});
|
||||
|
||||
test("browser article body operations target the body ProseMirror, not the title ProseMirror", () => {
|
||||
assert.match(articleScript, /BODY_EDITOR_SELECTOR = '\.rich_media_content \.ProseMirror'/);
|
||||
assert.doesNotMatch(articleScript, /clickElement\(session, '\\.ProseMirror'\)/);
|
||||
});
|
||||
|
||||
test("browser article reuses the selected account Chrome profile before launching", () => {
|
||||
assert.match(articleScript, /await findExistingChromeDebugPort\(profileDir\)/);
|
||||
});
|
||||
|
||||
test("browser article inserts inline images through WeChat local upload instead of clipboard paste", () => {
|
||||
assert.match(articleScript, /uploadImageThroughFileInput/);
|
||||
assert.match(articleScript, /DOM\.setFileInputFiles/);
|
||||
assert.doesNotMatch(articleScript, /await copyImageToClipboard\(img\.localPath\)/);
|
||||
});
|
||||
|
||||
test("browser article waits for a saved draft appmsgid before reporting success", () => {
|
||||
assert.match(articleScript, /waitForDraftSaved/);
|
||||
assert.match(articleScript, /appmsgid/);
|
||||
assert.doesNotMatch(articleScript, /Waiting for save confirmation/);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageS
|
||||
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
||||
|
||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||
const BODY_EDITOR_SELECTOR = '.rich_media_content .ProseMirror';
|
||||
|
||||
interface ImageInfo {
|
||||
placeholder: string;
|
||||
@@ -196,27 +197,27 @@ async function pasteInEditor(session: ChromeSession): Promise<void> {
|
||||
}
|
||||
|
||||
async function sendCopy(cdp?: CdpConnection, sessionId?: string): Promise<void> {
|
||||
if (process.platform === 'darwin') {
|
||||
if (cdp && sessionId) {
|
||||
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
||||
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 });
|
||||
} else if (process.platform === 'darwin') {
|
||||
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
|
||||
} else if (process.platform === 'linux') {
|
||||
spawnSync('xdotool', ['key', 'ctrl+c']);
|
||||
} else if (cdp && sessionId) {
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void> {
|
||||
if (process.platform === 'darwin') {
|
||||
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']);
|
||||
} else if (process.platform === 'linux') {
|
||||
spawnSync('xdotool', ['key', 'ctrl+v']);
|
||||
} else if (cdp && sessionId) {
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
if (!cdp || !sessionId) {
|
||||
throw new Error('Targeted paste requires a Chrome DevTools session');
|
||||
}
|
||||
|
||||
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
}
|
||||
|
||||
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, contentImages: ImageInfo[] = []): Promise<void> {
|
||||
@@ -294,6 +295,82 @@ async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void>
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
async function insertHtmlIntoEditorFromFile(
|
||||
session: ChromeSession,
|
||||
htmlFilePath: string,
|
||||
contentImages: ImageInfo[] = [],
|
||||
): Promise<void> {
|
||||
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
|
||||
const html = fs.readFileSync(absolutePath, 'utf8');
|
||||
const replacements = contentImages.map(img => ({ placeholder: img.placeholder, localPath: img.localPath }));
|
||||
|
||||
const result = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(function() {
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return JSON.stringify({ ok: false, reason: 'editor-missing' });
|
||||
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = ${JSON.stringify(html)};
|
||||
const replacements = ${JSON.stringify(replacements)};
|
||||
|
||||
for (const img of Array.from(template.content.querySelectorAll('img'))) {
|
||||
const src = img.getAttribute('src') || '';
|
||||
const localPath = img.getAttribute('data-local-path') || '';
|
||||
const replacement = replacements.find((item) => item.placeholder === src || item.localPath === localPath);
|
||||
if (replacement) {
|
||||
img.replaceWith(document.createTextNode(replacement.placeholder));
|
||||
}
|
||||
}
|
||||
|
||||
const output = template.content.querySelector('#output');
|
||||
const wrapper = document.createElement('div');
|
||||
if (output) {
|
||||
wrapper.innerHTML = output.innerHTML;
|
||||
} else {
|
||||
wrapper.appendChild(template.content.cloneNode(true));
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(editor);
|
||||
range.deleteContents();
|
||||
range.collapse(true);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
const inserted = document.execCommand('insertHTML', false, wrapper.innerHTML);
|
||||
editor.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
inputType: 'insertHTML',
|
||||
data: wrapper.innerText || ''
|
||||
}));
|
||||
|
||||
return JSON.stringify({
|
||||
ok: inserted || (editor.innerText || '').trim().length > 0,
|
||||
textLength: (editor.innerText || '').trim().length
|
||||
});
|
||||
})()
|
||||
`,
|
||||
returnByValue: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
const parsed = JSON.parse(result.result.value || '{}') as { ok?: boolean; reason?: string; textLength?: number };
|
||||
if (!parsed.ok) {
|
||||
throw new Error(`Failed to insert HTML into body editor${parsed.reason ? `: ${parsed.reason}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyTitleUnchangedBeforeSave(session: ChromeSession, expectedTitle: string): Promise<void> {
|
||||
if (!expectedTitle) return;
|
||||
|
||||
const actualTitle = await evaluate<string>(session, `document.querySelector('#title')?.value || ''`);
|
||||
if (actualTitle !== expectedTitle) {
|
||||
throw new Error(`Title was modified during paste. Expected: "${expectedTitle}", got: "${actualTitle}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareEditorPasteTarget(
|
||||
session: ChromeSession,
|
||||
context: string,
|
||||
@@ -303,19 +380,24 @@ async function prepareEditorPasteTarget(
|
||||
await sleep(100);
|
||||
|
||||
if (options.clickEditor) {
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
const ready = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const active = document.activeElement;
|
||||
const selection = window.getSelection();
|
||||
const selectionInEditor = !!selection && selection.rangeCount > 0 && !!selection.anchorNode && editor.contains(selection.anchorNode);
|
||||
const focusInEditor = !!active && (active === editor || editor.contains(active));
|
||||
const activeIsUnsafeInput = !!active && (
|
||||
active.matches?.('#title, #author, #js_description') ||
|
||||
((active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') && !editor.contains(active))
|
||||
);
|
||||
if (activeIsUnsafeInput) return false;
|
||||
if (selectionInEditor || focusInEditor) return true;
|
||||
|
||||
if (${JSON.stringify(Boolean(options.clickEditor))}) {
|
||||
@@ -438,7 +520,7 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
||||
const result = await session.cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const placeholder = ${JSON.stringify(placeholder)};
|
||||
@@ -456,6 +538,7 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
||||
// Exact match if next char is not a digit
|
||||
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
editor.focus();
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(node, idx);
|
||||
@@ -486,7 +569,7 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
|
||||
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
||||
const removed = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const sel = window.getSelection();
|
||||
@@ -548,6 +631,101 @@ async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<b
|
||||
return removed;
|
||||
}
|
||||
|
||||
async function getBodyImageCount(session: ChromeSession): Promise<number> {
|
||||
return await evaluate<number>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return 0;
|
||||
return Array.from(editor.querySelectorAll('img')).filter((img) => !img.classList.contains('ProseMirror-separator')).length;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async function waitForBodyImageCount(session: ChromeSession, minimumCount: number, timeoutMs = 45_000): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const count = await getBodyImageCount(session);
|
||||
if (count >= minimumCount) return true;
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function uploadImageThroughFileInput(session: ChromeSession, imagePath: string): Promise<void> {
|
||||
const absolutePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(process.cwd(), imagePath);
|
||||
if (!fs.existsSync(absolutePath)) throw new Error(`Image file not found: ${absolutePath}`);
|
||||
|
||||
const beforeCount = await getBodyImageCount(session);
|
||||
const documentNode = await session.cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {
|
||||
depth: -1,
|
||||
pierce: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
const inputNode = await session.cdp.send<{ nodeId: number }>('DOM.querySelector', {
|
||||
nodeId: documentNode.root.nodeId,
|
||||
selector: 'input[type="file"][accept*="image"]',
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
if (!inputNode.nodeId) throw new Error('WeChat local image upload input not found');
|
||||
|
||||
await session.cdp.send('DOM.setFileInputFiles', {
|
||||
nodeId: inputNode.nodeId,
|
||||
files: [absolutePath],
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
const inserted = await waitForBodyImageCount(session, beforeCount + 1);
|
||||
if (!inserted) {
|
||||
const afterCount = await getBodyImageCount(session);
|
||||
throw new Error(`Image upload did not insert into editor: ${path.basename(absolutePath)} (${beforeCount} -> ${afterCount})`);
|
||||
}
|
||||
}
|
||||
|
||||
interface DraftSaveStatus {
|
||||
appmsgid: string;
|
||||
isLoading: boolean;
|
||||
submitText: string;
|
||||
url: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
async function getDraftSaveStatus(session: ChromeSession): Promise<DraftSaveStatus> {
|
||||
const raw = await evaluate<string>(session, `
|
||||
(function() {
|
||||
const submit = document.querySelector('#js_submit');
|
||||
const button = submit?.querySelector('button');
|
||||
const url = location.href;
|
||||
const appmsgid = new URL(url).searchParams.get('appmsgid') || '';
|
||||
const messages = Array.from(document.querySelectorAll('.weui-desktop-toast, .weui-desktop-toptips, .js_tips'))
|
||||
.map((el) => (el.innerText || el.textContent || '').trim())
|
||||
.filter(Boolean);
|
||||
return JSON.stringify({
|
||||
appmsgid,
|
||||
isLoading: !!submit?.classList.contains('btn_loading') || !!button?.disabled,
|
||||
submitText: (submit?.innerText || '').trim(),
|
||||
url,
|
||||
messages
|
||||
});
|
||||
})()
|
||||
`);
|
||||
return JSON.parse(raw || '{}') as DraftSaveStatus;
|
||||
}
|
||||
|
||||
async function waitForDraftSaved(session: ChromeSession, timeoutMs = 60_000): Promise<string> {
|
||||
const start = Date.now();
|
||||
let lastStatus: DraftSaveStatus | null = null;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
lastStatus = await getDraftSaveStatus(session);
|
||||
if (lastStatus.appmsgid && !lastStatus.isLoading) return lastStatus.appmsgid;
|
||||
|
||||
const relevantFailure = lastStatus.messages.find((message) => /保存.*失败|草稿.*失败|save.*fail/i.test(message));
|
||||
if (relevantFailure) throw new Error(`Draft save failed: ${relevantFailure}`);
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
throw new Error(`Draft save did not complete${lastStatus ? `: ${JSON.stringify(lastStatus)}` : ''}`);
|
||||
}
|
||||
|
||||
export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
const { title, content, htmlFile, markdownFile, theme, color, citeStatus = true, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
||||
let { contentImages = [] } = options;
|
||||
@@ -591,7 +769,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
let chrome: ReturnType<typeof import('node:child_process').spawn> | null = null;
|
||||
|
||||
// Try connecting to existing Chrome: explicit port > auto-detect > launch new
|
||||
const portToTry = cdpPort ?? await findExistingChromeDebugPort();
|
||||
const portToTry = cdpPort ?? await findExistingChromeDebugPort(profileDir);
|
||||
if (portToTry) {
|
||||
const existing = await tryConnectExisting(portToTry);
|
||||
if (existing) {
|
||||
@@ -680,7 +858,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
console.log('[wechat] Waiting for editor to load...');
|
||||
const editorLoaded = await waitForElement(session, '#title', 30_000);
|
||||
if (!editorLoaded) throw new Error('Editor did not load (#title not found)');
|
||||
await waitForElement(session, '.ProseMirror', 15_000);
|
||||
await waitForElement(session, BODY_EDITOR_SELECTOR, 15_000);
|
||||
await sleep(2000);
|
||||
|
||||
if (effectiveTitle) {
|
||||
@@ -705,25 +883,23 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
}
|
||||
|
||||
console.log('[wechat] Clicking on editor...');
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(1000);
|
||||
|
||||
console.log('[wechat] Ensuring editor focus...');
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(500);
|
||||
|
||||
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
||||
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
|
||||
await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
|
||||
await sleep(500);
|
||||
console.log(`[wechat] Inserting HTML content from: ${effectiveHtmlFile}`);
|
||||
await prepareEditorPasteTarget(session, 'body content paste', { clickEditor: true });
|
||||
console.log('[wechat] Pasting into editor...');
|
||||
await pasteFromClipboardInEditor(session);
|
||||
await insertHtmlIntoEditorFromFile(session, effectiveHtmlFile, contentImages);
|
||||
await sleep(3000);
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
|
||||
const editorHasContent = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
const text = editor.innerText?.trim() || '';
|
||||
return text.length > 0;
|
||||
@@ -749,18 +925,15 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
console.log(`[wechat] Copying image: ${path.basename(img.localPath)}`);
|
||||
await copyImageToClipboard(img.localPath);
|
||||
await sleep(300);
|
||||
|
||||
console.log('[wechat] Deleting placeholder with Backspace...');
|
||||
await pressDeleteKey(session);
|
||||
await sleep(200);
|
||||
|
||||
console.log('[wechat] Pasting image...');
|
||||
await prepareEditorPasteTarget(session, 'inline image paste');
|
||||
await pasteFromClipboardInEditor(session);
|
||||
await sleep(3000);
|
||||
console.log(`[wechat] Uploading image: ${path.basename(img.localPath)}`);
|
||||
await prepareEditorPasteTarget(session, 'inline image upload');
|
||||
await uploadImageThroughFileInput(session, img.localPath);
|
||||
await sleep(1000);
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
await removeExtraEmptyLineAfterImage(session);
|
||||
}
|
||||
console.log('[wechat] All images inserted.');
|
||||
@@ -768,12 +941,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
} else if (content) {
|
||||
for (const img of images) {
|
||||
if (fs.existsSync(img)) {
|
||||
console.log(`[wechat] Pasting image: ${img}`);
|
||||
await copyImageToClipboard(img);
|
||||
await sleep(500);
|
||||
await prepareEditorPasteTarget(session, 'leading image paste');
|
||||
await pasteInEditor(session);
|
||||
await sleep(2000);
|
||||
console.log(`[wechat] Uploading image: ${img}`);
|
||||
await prepareEditorPasteTarget(session, 'leading image upload');
|
||||
await uploadImageThroughFileInput(session, img);
|
||||
await sleep(1000);
|
||||
await removeExtraEmptyLineAfterImage(session);
|
||||
}
|
||||
}
|
||||
@@ -785,7 +956,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
const editorHasContent = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
const text = editor.innerText?.trim() || '';
|
||||
return text.length > 0;
|
||||
@@ -822,17 +993,12 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
|
||||
console.log('[wechat] Saving as draft...');
|
||||
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
||||
await sleep(3000);
|
||||
|
||||
const saved = await evaluate<boolean>(session, `!!document.querySelector('.weui-desktop-toast')`);
|
||||
if (saved) {
|
||||
console.log('[wechat] Draft saved successfully!');
|
||||
} else {
|
||||
console.log('[wechat] Waiting for save confirmation...');
|
||||
await sleep(5000);
|
||||
}
|
||||
const appmsgid = await waitForDraftSaved(session);
|
||||
console.log(`[wechat] Draft saved successfully! appmsgid: ${appmsgid}`);
|
||||
|
||||
console.log('[wechat] Done. Browser window left open.');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user