mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-09 20:51:22 +00:00
Fix WeChat image upload fallback and WebP clipboard
This commit is contained in:
@@ -12,3 +12,9 @@ test("macOS image clipboard copy avoids Swift AppKit JIT", () => {
|
|||||||
assert.match(clipboardScript, /copyImageMacWithOsascript/);
|
assert.match(clipboardScript, /copyImageMacWithOsascript/);
|
||||||
assert.doesNotMatch(clipboardScript, /await runCommand\('swift', \[swiftPath, 'image', imagePath\]\)/);
|
assert.doesNotMatch(clipboardScript, /await runCommand\('swift', \[swiftPath, 'image', imagePath\]\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("macOS image clipboard copy converts WebP to PNG before AppleScript", () => {
|
||||||
|
assert.match(clipboardScript, /convertWebpMacToPng/);
|
||||||
|
assert.match(clipboardScript, /path\.extname\(imagePath\)\.toLowerCase\(\) === '\.webp'/);
|
||||||
|
assert.match(clipboardScript, /await copyImageMacWithOsascript\(pngPath\)/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import decodeWebp, { init as initWebpDecode } from '@jsquash/webp/decode.js';
|
||||||
|
import { Jimp, JimpMime } from 'jimp';
|
||||||
|
|
||||||
const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
||||||
|
|
||||||
@@ -205,6 +208,35 @@ function getAppleScriptImageType(imagePath: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let webpDecoderReady: Promise<void> | undefined;
|
||||||
|
|
||||||
|
async function ensureWebpDecoder(): Promise<void> {
|
||||||
|
if (!webpDecoderReady) {
|
||||||
|
webpDecoderReady = (async () => {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const wasmPath = path.resolve(__dirname, 'node_modules/@jsquash/webp/codec/dec/webp_dec.wasm');
|
||||||
|
const wasmModule = await WebAssembly.compile(await readFile(wasmPath));
|
||||||
|
await initWebpDecode(wasmModule, {});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
await webpDecoderReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertWebpMacToPng(webpPath: string, tempDir: string): Promise<string> {
|
||||||
|
await ensureWebpDecoder();
|
||||||
|
const decoded = await decodeWebp(await readFile(webpPath));
|
||||||
|
const image = new Jimp({
|
||||||
|
data: Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength),
|
||||||
|
width: decoded.width,
|
||||||
|
height: decoded.height,
|
||||||
|
});
|
||||||
|
const pngPath = path.join(tempDir, `${path.basename(webpPath, path.extname(webpPath))}.png`);
|
||||||
|
await writeFile(pngPath, await image.getBuffer(JimpMime.png));
|
||||||
|
return pngPath;
|
||||||
|
}
|
||||||
|
|
||||||
async function copyImageMacWithOsascript(imagePath: string): Promise<void> {
|
async function copyImageMacWithOsascript(imagePath: string): Promise<void> {
|
||||||
const imageType = getAppleScriptImageType(imagePath);
|
const imageType = getAppleScriptImageType(imagePath);
|
||||||
const escapedPath = escapeAppleScriptString(imagePath);
|
const escapedPath = escapeAppleScriptString(imagePath);
|
||||||
@@ -215,6 +247,14 @@ async function copyImageMacWithOsascript(imagePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyImageMac(imagePath: string): Promise<void> {
|
async function copyImageMac(imagePath: string): Promise<void> {
|
||||||
|
if (path.extname(imagePath).toLowerCase() === '.webp') {
|
||||||
|
await withTempDir('copy-to-clipboard-', async (tempDir) => {
|
||||||
|
const pngPath = await convertWebpMacToPng(imagePath, tempDir);
|
||||||
|
await copyImageMacWithOsascript(pngPath);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await copyImageMacWithOsascript(imagePath);
|
await copyImageMacWithOsascript(imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ test("browser article inserts inline images through WeChat local upload instead
|
|||||||
assert.doesNotMatch(articleScript, /await copyImageToClipboard\(img\.localPath\)/);
|
assert.doesNotMatch(articleScript, /await copyImageToClipboard\(img\.localPath\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("browser article uploads original images before fallback processing", () => {
|
||||||
|
const rawUploadIndex = articleScript.indexOf("await uploadImagePathThroughFileInput(session, absolutePath, beforeCount)");
|
||||||
|
const fallbackIndex = articleScript.indexOf("const fallback = await prepareFallbackWechatBodyImageUpload(absolutePath)");
|
||||||
|
|
||||||
|
assert.notEqual(rawUploadIndex, -1);
|
||||||
|
assert.notEqual(fallbackIndex, -1);
|
||||||
|
assert.ok(rawUploadIndex < fallbackIndex);
|
||||||
|
assert.match(articleScript, /prepareWechatBodyImageUpload/);
|
||||||
|
assert.match(articleScript, /Raw image upload failed, retrying with fallback processing/);
|
||||||
|
});
|
||||||
|
|
||||||
test("browser article waits for a saved draft appmsgid before reporting success", () => {
|
test("browser article waits for a saved draft appmsgid before reporting success", () => {
|
||||||
assert.match(articleScript, /waitForDraftSaved/);
|
assert.match(articleScript, /waitForDraftSaved/);
|
||||||
assert.match(articleScript, /appmsgid/);
|
assert.match(articleScript, /appmsgid/);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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';
|
const BODY_EDITOR_SELECTOR = '.rich_media_content .ProseMirror';
|
||||||
@@ -651,11 +653,32 @@ async function waitForBodyImageCount(session: ChromeSession, minimumCount: numbe
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadImageThroughFileInput(session: ChromeSession, imagePath: string): Promise<void> {
|
function inferImageContentType(imagePath: string): string {
|
||||||
const absolutePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(process.cwd(), imagePath);
|
const ext = path.extname(imagePath).toLowerCase();
|
||||||
if (!fs.existsSync(absolutePath)) throw new Error(`Image file not found: ${absolutePath}`);
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const beforeCount = await getBodyImageCount(session);
|
async function uploadImagePathThroughFileInput(
|
||||||
|
session: ChromeSession,
|
||||||
|
absolutePath: string,
|
||||||
|
beforeCount: number,
|
||||||
|
): Promise<void> {
|
||||||
const documentNode = await session.cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {
|
const documentNode = await session.cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {
|
||||||
depth: -1,
|
depth: -1,
|
||||||
pierce: true,
|
pierce: true,
|
||||||
@@ -679,6 +702,72 @@ async function uploadImageThroughFileInput(session: ChromeSession, imagePath: st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
interface DraftSaveStatus {
|
||||||
appmsgid: string;
|
appmsgid: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user