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.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 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 path from 'node:path';
|
||||
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']);
|
||||
|
||||
@@ -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> {
|
||||
const imageType = getAppleScriptImageType(imagePath);
|
||||
const escapedPath = escapeAppleScriptString(imagePath);
|
||||
@@ -215,6 +247,14 @@ async function copyImageMacWithOsascript(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,17 @@ test("browser article inserts inline images through WeChat local upload instead
|
||||
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", () => {
|
||||
assert.match(articleScript, /waitForDraftSaved/);
|
||||
assert.match(articleScript, /appmsgid/);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
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 { prepareWechatBodyImageUpload } from './wechat-image-processor.ts';
|
||||
|
||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||
const BODY_EDITOR_SELECTOR = '.rich_media_content .ProseMirror';
|
||||
@@ -651,11 +653,32 @@ async function waitForBodyImageCount(session: ChromeSession, minimumCount: numbe
|
||||
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}`);
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
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', {
|
||||
depth: -1,
|
||||
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 {
|
||||
appmsgid: string;
|
||||
isLoading: boolean;
|
||||
|
||||
Reference in New Issue
Block a user