mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Merge pull request #154 from zhangga/codex/fix-wechat-browser-article-publish
Fix WeChat browser article publishing reliability
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> {
|
async function copyImageMac(imagePath: string): Promise<void> {
|
||||||
await withTempDir('copy-to-clipboard-', async (tempDir) => {
|
await copyImageMacWithOsascript(imagePath);
|
||||||
const swiftPath = path.join(tempDir, 'clipboard.swift');
|
|
||||||
await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');
|
|
||||||
await runCommand('swift', [swiftPath, 'image', imagePath]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyHtmlMac(htmlFilePath: string): Promise<void> {
|
async function copyHtmlMac(htmlFilePath: string): Promise<void> {
|
||||||
@@ -377,4 +401,3 @@ await main().catch((err) => {
|
|||||||
console.error(`Error: ${message}`);
|
console.error(`Error: ${message}`);
|
||||||
process.exit(1);
|
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';
|
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
||||||
|
|
||||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||||
|
const BODY_EDITOR_SELECTOR = '.rich_media_content .ProseMirror';
|
||||||
|
|
||||||
interface ImageInfo {
|
interface ImageInfo {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
@@ -196,27 +197,27 @@ async function pasteInEditor(session: ChromeSession): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendCopy(cdp?: CdpConnection, sessionId?: string): 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']);
|
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
|
||||||
} else if (process.platform === 'linux') {
|
} else if (process.platform === 'linux') {
|
||||||
spawnSync('xdotool', ['key', 'ctrl+c']);
|
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> {
|
async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void> {
|
||||||
if (process.platform === 'darwin') {
|
if (!cdp || !sessionId) {
|
||||||
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']);
|
throw new Error('Targeted paste requires a Chrome DevTools session');
|
||||||
} 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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
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);
|
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(
|
async function prepareEditorPasteTarget(
|
||||||
session: ChromeSession,
|
session: ChromeSession,
|
||||||
context: string,
|
context: string,
|
||||||
@@ -303,19 +380,24 @@ async function prepareEditorPasteTarget(
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
|
||||||
if (options.clickEditor) {
|
if (options.clickEditor) {
|
||||||
await clickElement(session, '.ProseMirror');
|
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ready = await evaluate<boolean>(session, `
|
const ready = await evaluate<boolean>(session, `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
const selectionInEditor = !!selection && selection.rangeCount > 0 && !!selection.anchorNode && editor.contains(selection.anchorNode);
|
const selectionInEditor = !!selection && selection.rangeCount > 0 && !!selection.anchorNode && editor.contains(selection.anchorNode);
|
||||||
const focusInEditor = !!active && (active === editor || editor.contains(active));
|
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 (selectionInEditor || focusInEditor) return true;
|
||||||
|
|
||||||
if (${JSON.stringify(Boolean(options.clickEditor))}) {
|
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', {
|
const result = await session.cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
const placeholder = ${JSON.stringify(placeholder)};
|
const placeholder = ${JSON.stringify(placeholder)};
|
||||||
@@ -456,6 +538,7 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
|||||||
// Exact match if next char is not a digit
|
// Exact match if next char is not a digit
|
||||||
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
@@ -486,7 +569,7 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
|
|||||||
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
||||||
const removed = await evaluate<boolean>(session, `
|
const removed = await evaluate<boolean>(session, `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
@@ -548,6 +631,101 @@ async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<b
|
|||||||
return removed;
|
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> {
|
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;
|
const { title, content, htmlFile, markdownFile, theme, color, citeStatus = true, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
||||||
let { contentImages = [] } = 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;
|
let chrome: ReturnType<typeof import('node:child_process').spawn> | null = null;
|
||||||
|
|
||||||
// Try connecting to existing Chrome: explicit port > auto-detect > launch new
|
// Try connecting to existing Chrome: explicit port > auto-detect > launch new
|
||||||
const portToTry = cdpPort ?? await findExistingChromeDebugPort();
|
const portToTry = cdpPort ?? await findExistingChromeDebugPort(profileDir);
|
||||||
if (portToTry) {
|
if (portToTry) {
|
||||||
const existing = await tryConnectExisting(portToTry);
|
const existing = await tryConnectExisting(portToTry);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -680,7 +858,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
console.log('[wechat] Waiting for editor to load...');
|
console.log('[wechat] Waiting for editor to load...');
|
||||||
const editorLoaded = await waitForElement(session, '#title', 30_000);
|
const editorLoaded = await waitForElement(session, '#title', 30_000);
|
||||||
if (!editorLoaded) throw new Error('Editor did not load (#title not found)');
|
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);
|
await sleep(2000);
|
||||||
|
|
||||||
if (effectiveTitle) {
|
if (effectiveTitle) {
|
||||||
@@ -705,25 +883,23 @@ 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, BODY_EDITOR_SELECTOR);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
console.log('[wechat] Ensuring editor focus...');
|
console.log('[wechat] Ensuring editor focus...');
|
||||||
await clickElement(session, '.ProseMirror');
|
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
||||||
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
|
console.log(`[wechat] Inserting HTML content from: ${effectiveHtmlFile}`);
|
||||||
await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
|
|
||||||
await sleep(500);
|
|
||||||
await prepareEditorPasteTarget(session, 'body content paste', { clickEditor: true });
|
await prepareEditorPasteTarget(session, 'body content paste', { clickEditor: true });
|
||||||
console.log('[wechat] Pasting into editor...');
|
await insertHtmlIntoEditorFromFile(session, effectiveHtmlFile, contentImages);
|
||||||
await pasteFromClipboardInEditor(session);
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||||
|
|
||||||
const editorHasContent = await evaluate<boolean>(session, `
|
const editorHasContent = await evaluate<boolean>(session, `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
const text = editor.innerText?.trim() || '';
|
const text = editor.innerText?.trim() || '';
|
||||||
return text.length > 0;
|
return text.length > 0;
|
||||||
@@ -749,18 +925,15 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
await sleep(500);
|
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...');
|
console.log('[wechat] Deleting placeholder with Backspace...');
|
||||||
await pressDeleteKey(session);
|
await pressDeleteKey(session);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
|
||||||
console.log('[wechat] Pasting image...');
|
console.log(`[wechat] Uploading image: ${path.basename(img.localPath)}`);
|
||||||
await prepareEditorPasteTarget(session, 'inline image paste');
|
await prepareEditorPasteTarget(session, 'inline image upload');
|
||||||
await pasteFromClipboardInEditor(session);
|
await uploadImageThroughFileInput(session, img.localPath);
|
||||||
await sleep(3000);
|
await sleep(1000);
|
||||||
|
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||||
await removeExtraEmptyLineAfterImage(session);
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
console.log('[wechat] All images inserted.');
|
console.log('[wechat] All images inserted.');
|
||||||
@@ -768,12 +941,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
} else if (content) {
|
} else if (content) {
|
||||||
for (const img of images) {
|
for (const img of images) {
|
||||||
if (fs.existsSync(img)) {
|
if (fs.existsSync(img)) {
|
||||||
console.log(`[wechat] Pasting image: ${img}`);
|
console.log(`[wechat] Uploading image: ${img}`);
|
||||||
await copyImageToClipboard(img);
|
await prepareEditorPasteTarget(session, 'leading image upload');
|
||||||
await sleep(500);
|
await uploadImageThroughFileInput(session, img);
|
||||||
await prepareEditorPasteTarget(session, 'leading image paste');
|
await sleep(1000);
|
||||||
await pasteInEditor(session);
|
|
||||||
await sleep(2000);
|
|
||||||
await removeExtraEmptyLineAfterImage(session);
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,7 +956,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
const editorHasContent = await evaluate<boolean>(session, `
|
const editorHasContent = await evaluate<boolean>(session, `
|
||||||
(function() {
|
(function() {
|
||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
const text = editor.innerText?.trim() || '';
|
const text = editor.innerText?.trim() || '';
|
||||||
return text.length > 0;
|
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...');
|
console.log('[wechat] Saving as draft...');
|
||||||
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
||||||
await sleep(3000);
|
const appmsgid = await waitForDraftSaved(session);
|
||||||
|
console.log(`[wechat] Draft saved successfully! appmsgid: ${appmsgid}`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[wechat] Done. Browser window left open.');
|
console.log('[wechat] Done. Browser window left open.');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user