mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
3b29f3c57c
Re-implements PR #156 (remote WeChat API publishing for IP-allowlist constraints) with a fully local code path: spawn a background `ssh -N -D` SOCKS5 dynamic forward, wrap a `SocksProxyAgent`, and thread it through the existing token/upload/draft flow. No Python helper, no SCP, no remote files, no `AppSecret` ever leaving the local process. Reusing `uploadImagesInHtml` (which replaces the matched `<img>` fullTag rather than substring-replacing `src`) naturally avoids the original PR's `html.replace(src, url)` HTML-replacement bug. Changes: - New `wechat-remote-publish.ts`: typed-whitelist SSH args, free-port allocation, SOCKS readiness polling, signal-handler-backed cleanup. - New `wechat-http.ts`: minimal `https.request`-backed HTTP helper so a `SocksProxyAgent` can be passed through (undici `fetch` ignores agent). - New `wechat-image-loader.ts`: extracts `loadUploadAsset` for reuse. - `wechat-api.ts`: thread optional `agent?` through token/upload/draft; add `--remote*` CLI flags; dispatch via `withSshTunnel` when remote mode is selected by flag or `default_publish_method: remote-api`. - `wechat-extend-config.ts`: add typed `remote_publish_*` keys with account-over-global fallback and value validation. - Docs: SKILL.md, references/multi-account.md, references/config/ first-time-setup.md, README.md, README.zh.md. - Tests: 17 new node:test cases covering config parsing, SSH arg whitelisting, free-port allocation, multipart assembly, and HTTP agent threading. Co-authored-by: Dame5211 <1079825614@qq.com>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import {
|
|
type WechatUploadAsset,
|
|
detectImageFormatFromBuffer,
|
|
} from "./wechat-image-processor.ts";
|
|
|
|
export type { WechatUploadAsset };
|
|
|
|
const MIME_TYPES_BY_EXT: Record<string, string> = {
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".gif": "image/gif",
|
|
".webp": "image/webp",
|
|
".bmp": "image/bmp",
|
|
".tiff": "image/tiff",
|
|
".tif": "image/tiff",
|
|
".svg": "image/svg+xml",
|
|
".ico": "image/x-icon",
|
|
};
|
|
|
|
export async function loadUploadAsset(
|
|
imagePath: string,
|
|
baseDir?: string,
|
|
): Promise<WechatUploadAsset> {
|
|
let fileBuffer: Buffer;
|
|
let filename: string;
|
|
let contentType: string;
|
|
let fileSize = 0;
|
|
let fileExt = "";
|
|
|
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
const response = await fetch(imagePath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to download image: ${imagePath}`);
|
|
}
|
|
const buffer = await response.arrayBuffer();
|
|
if (buffer.byteLength === 0) {
|
|
throw new Error(`Remote image is empty: ${imagePath}`);
|
|
}
|
|
fileBuffer = Buffer.from(buffer);
|
|
fileSize = buffer.byteLength;
|
|
const urlPath = imagePath.split("?")[0]!;
|
|
filename = path.basename(urlPath) || "image.jpg";
|
|
fileExt = path.extname(filename).toLowerCase();
|
|
contentType = response.headers.get("content-type") || "image/jpeg";
|
|
} else {
|
|
const resolvedPath = path.isAbsolute(imagePath)
|
|
? imagePath
|
|
: path.resolve(baseDir || process.cwd(), imagePath);
|
|
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
throw new Error(`Image not found: ${resolvedPath}`);
|
|
}
|
|
const stats = fs.statSync(resolvedPath);
|
|
if (stats.size === 0) {
|
|
throw new Error(`Local image is empty: ${resolvedPath}`);
|
|
}
|
|
fileSize = stats.size;
|
|
fileBuffer = fs.readFileSync(resolvedPath);
|
|
filename = path.basename(resolvedPath);
|
|
fileExt = path.extname(filename).toLowerCase();
|
|
contentType = MIME_TYPES_BY_EXT[fileExt] || "image/jpeg";
|
|
}
|
|
|
|
const detected = detectImageFormatFromBuffer(fileBuffer);
|
|
if (detected && detected.contentType !== contentType) {
|
|
console.error(`[wechat-api] Format mismatch: ${filename} declared as ${contentType}, actual ${detected.contentType}`);
|
|
contentType = detected.contentType;
|
|
fileExt = detected.fileExt;
|
|
filename = `${path.basename(filename, path.extname(filename))}${detected.fileExt}`;
|
|
}
|
|
|
|
return {
|
|
buffer: fileBuffer,
|
|
filename,
|
|
contentType,
|
|
fileExt,
|
|
fileSize,
|
|
};
|
|
}
|