|
|
@@ -1,12 +1,15 @@
|
|
|
|
import fs from 'node:fs';
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
|
|
|
import os from 'node:os';
|
|
|
|
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 { fileURLToPath } from 'node:url';
|
|
|
|
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, getAccountProfileDir, type ChromeSession, type CdpConnection } from './cdp.ts';
|
|
|
|
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, getAccountProfileDir, type ChromeSession, type CdpConnection } from './cdp.ts';
|
|
|
|
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
|
|
|
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
|
|
|
|
|
|
|
import { prepareWechatBodyImageUpload } from './wechat-image-processor.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 +199,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 +297,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 +382,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 +522,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 +540,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 +571,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 +633,188 @@ 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;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function inferImageContentType(imagePath: string): string {
|
|
|
|
|
|
|
|
const ext = path.extname(imagePath).toLowerCase();
|
|
|
|
|
|
|
|
switch (ext) {
|
|
|
|
|
|
|
|
case '.jpg':
|
|
|
|
|
|
|
|
case '.jpeg':
|
|
|
|
|
|
|
|
return 'image/jpeg';
|
|
|
|
|
|
|
|
case '.png':
|
|
|
|
|
|
|
|
return 'image/png';
|
|
|
|
|
|
|
|
case '.gif':
|
|
|
|
|
|
|
|
return 'image/gif';
|
|
|
|
|
|
|
|
case '.webp':
|
|
|
|
|
|
|
|
return 'image/webp';
|
|
|
|
|
|
|
|
case '.bmp':
|
|
|
|
|
|
|
|
return 'image/bmp';
|
|
|
|
|
|
|
|
case '.svg':
|
|
|
|
|
|
|
|
return 'image/svg+xml';
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
return 'application/octet-stream';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadImagePathThroughFileInput(
|
|
|
|
|
|
|
|
session: ChromeSession,
|
|
|
|
|
|
|
|
absolutePath: string,
|
|
|
|
|
|
|
|
beforeCount: number,
|
|
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
|
|
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 FallbackUploadImage {
|
|
|
|
|
|
|
|
uploadPath: string;
|
|
|
|
|
|
|
|
wasProcessed: boolean;
|
|
|
|
|
|
|
|
processingNotes: string[];
|
|
|
|
|
|
|
|
cleanup: () => void;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function prepareFallbackWechatBodyImageUpload(absolutePath: string): Promise<FallbackUploadImage> {
|
|
|
|
|
|
|
|
const buffer = fs.readFileSync(absolutePath);
|
|
|
|
|
|
|
|
const prepared = await prepareWechatBodyImageUpload({
|
|
|
|
|
|
|
|
buffer,
|
|
|
|
|
|
|
|
filename: path.basename(absolutePath),
|
|
|
|
|
|
|
|
contentType: inferImageContentType(absolutePath),
|
|
|
|
|
|
|
|
fileExt: path.extname(absolutePath).toLowerCase(),
|
|
|
|
|
|
|
|
fileSize: buffer.length,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!prepared.wasProcessed) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
uploadPath: absolutePath,
|
|
|
|
|
|
|
|
wasProcessed: false,
|
|
|
|
|
|
|
|
processingNotes: [],
|
|
|
|
|
|
|
|
cleanup: () => {},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-body-image-'));
|
|
|
|
|
|
|
|
const uploadPath = path.join(tempDir, prepared.filename);
|
|
|
|
|
|
|
|
fs.writeFileSync(uploadPath, prepared.buffer);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
uploadPath,
|
|
|
|
|
|
|
|
wasProcessed: true,
|
|
|
|
|
|
|
|
processingNotes: prepared.processingNotes,
|
|
|
|
|
|
|
|
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await uploadImagePathThroughFileInput(session, absolutePath, beforeCount);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
|
|
|
|
|
|
if (message.includes('local image upload input not found')) throw err;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const currentCount = await getBodyImageCount(session);
|
|
|
|
|
|
|
|
if (currentCount > beforeCount) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.warn(`[wechat] Raw image upload failed, retrying with fallback processing: ${message}`);
|
|
|
|
|
|
|
|
const fallback = await prepareFallbackWechatBodyImageUpload(absolutePath);
|
|
|
|
|
|
|
|
const notes = fallback.processingNotes.length > 0 ? ` (${fallback.processingNotes.join('; ')})` : '';
|
|
|
|
|
|
|
|
console.log(`[wechat] Retrying image upload with ${fallback.wasProcessed ? 'processed' : 'original'} file: ${path.basename(fallback.uploadPath)}${notes}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await uploadImagePathThroughFileInput(session, fallback.uploadPath, currentCount);
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
fallback.cleanup();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 +858,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 +947,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 +972,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 +1014,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 +1030,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 +1045,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 +1082,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 {
|
|
|
|