Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post articles and image-text content to WeChat Official Account via API or Chrome CDP, with markdown-to-WeChat HTML conversion.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/wechat-api.ts
1import fs from "node:fs";2import path from "node:path";3import { spawnSync } from "node:child_process";4import { fileURLToPath } from "node:url";5import { loadWechatExtendConfig, resolveAccount, loadCredentials } from "./wechat-extend-config.ts";6import {7type WechatUploadAsset,8prepareWechatBodyImageUpload,9needsWechatBodyImageProcessing,10detectImageFormatFromBuffer,11} from "./wechat-image-processor.ts";1213interface AccessTokenResponse {14access_token?: string;15errcode?: number;16errmsg?: string;17}1819interface UploadResponse {20media_id: string;21url: string;22errcode?: number;23errmsg?: string;24}2526interface PublishResponse {27media_id?: string;28errcode?: number;29errmsg?: string;30}3132interface ImageInfo {33placeholder: string;34localPath: string;35originalPath: string;36}3738interface MarkdownRenderResult {39title: string;40author: string;41summary: string;42htmlPath: string;43contentImages: ImageInfo[];44}4546type ArticleType = "news" | "newspic";4748interface ArticleOptions {49title: string;50author?: string;51digest?: string;52content: string;53thumbMediaId: string;54articleType: ArticleType;55imageMediaIds?: string[];56needOpenComment?: number;57onlyFansCanComment?: number;58}5960const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";61const UPLOAD_BODY_IMG_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg";62const UPLOAD_MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";63const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";6465async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {66const url = `${TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;67const res = await fetch(url);68if (!res.ok) {69throw new Error(`Failed to fetch access token: ${res.status}`);70}71const data = await res.json() as AccessTokenResponse;72if (data.errcode) {73throw new Error(`Access token error ${data.errcode}: ${data.errmsg}`);74}75if (!data.access_token) {76throw new Error("No access_token in response");77}78return data.access_token;79}8081function toHttpsUrl(url: string | undefined): string {82if (!url) return "";83return url.startsWith("http://") ? url.replace(/^http:\/\//i, "https://") : url;84}8586async function loadUploadAsset(87imagePath: string,88baseDir?: string,89): Promise<WechatUploadAsset> {90let fileBuffer: Buffer;91let filename: string;92let contentType: string;93let fileSize = 0;94let fileExt = "";9596if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {97const response = await fetch(imagePath);98if (!response.ok) {99throw new Error(`Failed to download image: ${imagePath}`);100}101const buffer = await response.arrayBuffer();102if (buffer.byteLength === 0) {103throw new Error(`Remote image is empty: ${imagePath}`);104}105fileBuffer = Buffer.from(buffer);106fileSize = buffer.byteLength;107const urlPath = imagePath.split("?")[0];108filename = path.basename(urlPath) || "image.jpg";109fileExt = path.extname(filename).toLowerCase();110contentType = response.headers.get("content-type") || "image/jpeg";111} else {112const resolvedPath = path.isAbsolute(imagePath)113? imagePath114: path.resolve(baseDir || process.cwd(), imagePath);115116if (!fs.existsSync(resolvedPath)) {117throw new Error(`Image not found: ${resolvedPath}`);118}119const stats = fs.statSync(resolvedPath);120if (stats.size === 0) {121throw new Error(`Local image is empty: ${resolvedPath}`);122}123fileSize = stats.size;124fileBuffer = fs.readFileSync(resolvedPath);125filename = path.basename(resolvedPath);126fileExt = path.extname(filename).toLowerCase();127const mimeTypes: Record<string, string> = {128".jpg": "image/jpeg",129".jpeg": "image/jpeg",130".png": "image/png",131".gif": "image/gif",132".webp": "image/webp",133".bmp": "image/bmp",134".tiff": "image/tiff",135".tif": "image/tiff",136".svg": "image/svg+xml",137".ico": "image/x-icon",138};139contentType = mimeTypes[fileExt] || "image/jpeg";140}141142// Detect actual format from magic bytes to fix extension/content-type mismatches143// (e.g. CDNs serving WebP for URLs with .png extension)144const detected = detectImageFormatFromBuffer(fileBuffer);145if (detected && detected.contentType !== contentType) {146console.error(`[wechat-api] Format mismatch: ${filename} declared as ${contentType}, actual ${detected.contentType}`);147contentType = detected.contentType;148fileExt = detected.fileExt;149filename = `${path.basename(filename, path.extname(filename))}${detected.fileExt}`;150}151152return {153buffer: fileBuffer,154filename,155contentType,156fileExt,157fileSize,158};159}160161async function uploadImage(162imagePath: string,163accessToken: string,164baseDir?: string,165uploadType: "body" | "material" = "body"166): Promise<UploadResponse> {167const asset = await loadUploadAsset(imagePath, baseDir);168let uploadAsset = asset;169170if (uploadType === "body" && needsWechatBodyImageProcessing(asset)) {171const prepared = await prepareWechatBodyImageUpload(asset);172uploadAsset = {173...asset,174buffer: prepared.buffer,175filename: prepared.filename,176contentType: prepared.contentType,177fileExt: path.extname(prepared.filename).toLowerCase(),178fileSize: prepared.buffer.length,179};180const note = prepared.processingNotes.join(", ");181console.error(`[wechat-api] Processed ${asset.filename} for body upload: ${note}`);182}183184const result = await uploadToWechat(185uploadAsset.buffer,186uploadAsset.filename,187uploadAsset.contentType,188accessToken,189uploadType,190);191192// media/uploadimg 接口只返回 URL,material/add_material 返回 media_id193if (uploadType === "body") {194return {195url: toHttpsUrl(result.url),196media_id: "",197} as UploadResponse;198} else {199result.url = toHttpsUrl(result.url);200return result;201}202}203204// 实际的微信上传函数205async function uploadToWechat(206fileBuffer: Buffer,207filename: string,208contentType: string,209accessToken: string,210uploadType: "body" | "material"211): Promise<UploadResponse> {212const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;213const header = [214`--${boundary}`,215`Content-Disposition: form-data; name="media"; filename="${filename}"`,216`Content-Type: ${contentType}`,217"",218"",219].join("\r\n");220const footer = `\r\n--${boundary}--\r\n`;221222const headerBuffer = Buffer.from(header, "utf-8");223const footerBuffer = Buffer.from(footer, "utf-8");224const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]);225226const uploadUrl = uploadType === "body" ? UPLOAD_BODY_IMG_URL : UPLOAD_MATERIAL_URL;227const url = `${uploadUrl}?type=image&access_token=${accessToken}`;228const res = await fetch(url, {229method: "POST",230headers: {231"Content-Type": `multipart/form-data; boundary=${boundary}`,232},233body,234});235236const data = await res.json() as UploadResponse;237if (data.errcode && data.errcode !== 0) {238throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`);239}240241return data;242}243244async function uploadImagesInHtml(245html: string,246accessToken: string,247baseDir: string,248contentImages: ImageInfo[] = [],249articleType: ArticleType = "news",250collectNewsCoverFallback: boolean = false,251): Promise<{ html: string; firstCoverMediaId: string; imageMediaIds: string[] }> {252const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;253const matches = [...html.matchAll(imgRegex)];254255if (matches.length === 0 && contentImages.length === 0) {256return { html, firstCoverMediaId: "", imageMediaIds: [] };257}258259let firstCoverMediaId = "";260let updatedHtml = html;261const imageMediaIds: string[] = [];262const uploadedBySource = new Map<string, UploadResponse>();263264for (const match of matches) {265const [fullTag, src] = match;266if (!src) continue;267268if (src.startsWith("https://mmbiz.qpic.cn")) {269if (collectNewsCoverFallback && !firstCoverMediaId) {270try {271const coverResp = await uploadImage(src, accessToken, baseDir, "material");272firstCoverMediaId = coverResp.media_id;273} catch (err) {274console.error(`[wechat-api] Failed to reuse existing WeChat image as cover: ${src}`, err);275}276}277continue;278}279280const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);281const imagePath = localPathMatch ? localPathMatch[1]! : src;282283console.error(`[wechat-api] Uploading body image: ${imagePath}`);284try {285let resp = uploadedBySource.get(imagePath);286if (!resp) {287// 正文图片使用 media/uploadimg 接口获取 URL288resp = await uploadImage(imagePath, accessToken, baseDir, "body");289uploadedBySource.set(imagePath, resp);290}291const newTag = fullTag292.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)293.replace(/\sdata-local-path=["'][^"']+["']/, "");294updatedHtml = updatedHtml.replace(fullTag, newTag);295const shouldUploadMaterial = articleType === "newspic" || (collectNewsCoverFallback && !firstCoverMediaId);296if (shouldUploadMaterial) {297let materialResp = uploadedBySource.get(`${imagePath}:material`);298if (!materialResp) {299materialResp = await uploadImage(imagePath, accessToken, baseDir, "material");300uploadedBySource.set(`${imagePath}:material`, materialResp);301}302if (articleType === "newspic" && materialResp.media_id) {303imageMediaIds.push(materialResp.media_id);304}305if (collectNewsCoverFallback && !firstCoverMediaId && materialResp.media_id) {306firstCoverMediaId = materialResp.media_id;307}308}309} catch (err) {310console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);311}312}313314for (const image of contentImages) {315if (!updatedHtml.includes(image.placeholder)) continue;316317const imagePath = image.localPath || image.originalPath;318console.error(`[wechat-api] Uploading body image: ${imagePath}`);319320try {321let resp = uploadedBySource.get(imagePath);322if (!resp) {323// 正文图片使用 media/uploadimg 接口获取 URL324resp = await uploadImage(imagePath, accessToken, baseDir, "body");325uploadedBySource.set(imagePath, resp);326}327328const replacementTag = `<img src="${resp.url}" style="display: block; width: 100%; margin: 1.5em auto;">`;329updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag);330const shouldUploadMaterial = articleType === "newspic" || (collectNewsCoverFallback && !firstCoverMediaId);331if (shouldUploadMaterial) {332let materialResp = uploadedBySource.get(`${imagePath}:material`);333if (!materialResp) {334materialResp = await uploadImage(imagePath, accessToken, baseDir, "material");335uploadedBySource.set(`${imagePath}:material`, materialResp);336}337if (articleType === "newspic" && materialResp.media_id) {338imageMediaIds.push(materialResp.media_id);339}340if (collectNewsCoverFallback && !firstCoverMediaId && materialResp.media_id) {341firstCoverMediaId = materialResp.media_id;342}343}344} catch (err) {345console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err);346}347}348349return { html: updatedHtml, firstCoverMediaId, imageMediaIds };350}351352async function publishToDraft(353options: ArticleOptions,354accessToken: string355): Promise<PublishResponse> {356const url = `${DRAFT_URL}?access_token=${accessToken}`;357358let article: Record<string, unknown>;359360const noc = options.needOpenComment ?? 1;361const ofcc = options.onlyFansCanComment ?? 0;362363if (options.articleType === "newspic") {364if (!options.imageMediaIds || options.imageMediaIds.length === 0) {365throw new Error("newspic requires at least one image");366}367article = {368article_type: "newspic",369title: options.title,370content: options.content,371need_open_comment: noc,372only_fans_can_comment: ofcc,373image_info: {374image_list: options.imageMediaIds.map(id => ({ image_media_id: id })),375},376};377if (options.author) article.author = options.author;378} else {379article = {380article_type: "news",381title: options.title,382content: options.content,383thumb_media_id: options.thumbMediaId,384need_open_comment: noc,385only_fans_can_comment: ofcc,386};387if (options.author) article.author = options.author;388if (options.digest) article.digest = options.digest;389}390391const res = await fetch(url, {392method: "POST",393headers: {394"Content-Type": "application/json",395},396body: JSON.stringify({ articles: [article] }),397});398399const data = await res.json() as PublishResponse;400if (data.errcode && data.errcode !== 0) {401throw new Error(`Publish failed ${data.errcode}: ${data.errmsg}`);402}403404return data;405}406407function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {408const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);409if (!match) return { frontmatter: {}, body: content };410411const frontmatter: Record<string, string> = {};412const lines = match[1]!.split("\n");413for (const line of lines) {414const colonIdx = line.indexOf(":");415if (colonIdx > 0) {416const key = line.slice(0, colonIdx).trim();417let value = line.slice(colonIdx + 1).trim();418if ((value.startsWith('"') && value.endsWith('"')) ||419(value.startsWith("'") && value.endsWith("'"))) {420value = value.slice(1, -1);421}422frontmatter[key] = value;423}424}425426return { frontmatter, body: match[2]! };427}428429function renderMarkdownWithPlaceholders(430markdownPath: string,431theme: string = "default",432color?: string,433citeStatus: boolean = true,434title?: string,435): MarkdownRenderResult {436const __filename = fileURLToPath(import.meta.url);437const __dirname = path.dirname(__filename);438const mdToWechatScript = path.join(__dirname, "md-to-wechat.ts");439const baseDir = path.dirname(markdownPath);440441const args = ["-y", "bun", mdToWechatScript, markdownPath];442if (title) args.push("--title", title);443if (theme) args.push("--theme", theme);444if (color) args.push("--color", color);445if (!citeStatus) args.push("--no-cite");446447console.error(`[wechat-api] Rendering markdown with placeholders via md-to-wechat: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);448const result = spawnSync("npx", args, {449stdio: ["inherit", "pipe", "pipe"],450cwd: baseDir,451});452453if (result.status !== 0) {454const stderr = result.stderr?.toString() || "";455throw new Error(`Markdown placeholder render failed: ${stderr}`);456}457458const stdout = result.stdout?.toString() || "";459return JSON.parse(stdout) as MarkdownRenderResult;460}461462function replaceAllPlaceholders(html: string, placeholder: string, replacement: string): string {463const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");464return html.replace(new RegExp(escapedPlaceholder + "(?!\\d)", "g"), replacement);465}466467function extractHtmlContent(htmlPath: string): string {468const html = fs.readFileSync(htmlPath, "utf-8");469const match = html.match(/<div id="output">([\s\S]*?)<\/div>\s*<\/body>/);470if (match) {471return match[1]!.trim();472}473const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);474return bodyMatch ? bodyMatch[1]!.trim() : html;475}476477function printUsage(): never {478console.log(`Publish article to WeChat Official Account draft using API479480Usage:481npx -y bun wechat-api.ts <file> [options]482483Arguments:484file Markdown (.md) or HTML (.html) file485486Options:487--type <type> Article type: news (文章, default) or newspic (图文)488--title <title> Override title489--author <name> Author name (max 16 chars)490--summary <text> Article summary/digest (max 128 chars)491--theme <name> Theme name for markdown (default, grace, simple, modern). Default: default492--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)493--cover <path> Cover image path (local or URL)494--account <alias> Select account by alias (for multi-account setups)495--no-cite Disable bottom citations for ordinary external links in markdown mode496--dry-run Parse and render only, don't publish497--help Show this help498499Frontmatter Fields (markdown):500title Article title501author Author name502digest/summary Article summary503coverImage/featureImage/cover/image Cover image path504505Comments:506Comments are enabled by default, open to all users.507508Environment Variables:509WECHAT_APP_ID WeChat App ID510WECHAT_APP_SECRET WeChat App Secret511512Config File Locations (in priority order):5131. Environment variables5142. <cwd>/.baoyu-skills/.env5153. ~/.baoyu-skills/.env516517Example:518npx -y bun wechat-api.ts article.md519npx -y bun wechat-api.ts article.md --theme grace --cover cover.png520npx -y bun wechat-api.ts article.md --author "Author Name" --summary "Brief intro"521npx -y bun wechat-api.ts article.html --title "My Article"522npx -y bun wechat-api.ts images/ --type newspic --title "Photo Album"523npx -y bun wechat-api.ts article.md --dry-run524npx -y bun wechat-api.ts article.md --no-cite525`);526process.exit(0);527}528529interface CliArgs {530filePath: string;531isHtml: boolean;532articleType: ArticleType;533title?: string;534author?: string;535summary?: string;536theme: string;537color?: string;538cover?: string;539account?: string;540citeStatus: boolean;541dryRun: boolean;542}543544function parseArgs(argv: string[]): CliArgs {545if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {546printUsage();547}548549const args: CliArgs = {550filePath: "",551isHtml: false,552articleType: "news",553theme: "default",554citeStatus: true,555dryRun: false,556};557558for (let i = 0; i < argv.length; i++) {559const arg = argv[i]!;560if (arg === "--type" && argv[i + 1]) {561const t = argv[++i]!.toLowerCase();562if (t === "news" || t === "newspic") {563args.articleType = t;564}565} else if (arg === "--title" && argv[i + 1]) {566args.title = argv[++i];567} else if (arg === "--author" && argv[i + 1]) {568args.author = argv[++i];569} else if (arg === "--summary" && argv[i + 1]) {570args.summary = argv[++i];571} else if (arg === "--theme" && argv[i + 1]) {572args.theme = argv[++i]!;573} else if (arg === "--color" && argv[i + 1]) {574args.color = argv[++i];575} else if (arg === "--cover" && argv[i + 1]) {576args.cover = argv[++i];577} else if (arg === "--account" && argv[i + 1]) {578args.account = argv[++i];579} else if (arg === "--cite") {580args.citeStatus = true;581} else if (arg === "--no-cite") {582args.citeStatus = false;583} else if (arg === "--dry-run") {584args.dryRun = true;585} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) {586i++;587} else if (!arg.startsWith("-")) {588args.filePath = arg;589}590}591592if (!args.filePath) {593console.error("Error: File path required");594process.exit(1);595}596597args.isHtml = args.filePath.toLowerCase().endsWith(".html");598599return args;600}601602function extractHtmlTitle(html: string): string {603const titleMatch = html.match(/<title>([^<]+)<\/title>/i);604if (titleMatch) return titleMatch[1]!;605const h1Match = html.match(/<h1[^>]*>([^<]+)<\/h1>/i);606if (h1Match) return h1Match[1]!.replace(/<[^>]+>/g, "").trim();607return "";608}609610async function main(): Promise<void> {611const args = parseArgs(process.argv.slice(2));612613const filePath = path.resolve(args.filePath);614if (!fs.existsSync(filePath)) {615console.error(`Error: File not found: ${filePath}`);616process.exit(1);617}618619const baseDir = path.dirname(filePath);620let title = args.title || "";621let author = args.author || "";622let digest = args.summary || "";623let htmlPath: string;624let htmlContent: string;625let frontmatter: Record<string, string> = {};626let contentImages: ImageInfo[] = [];627628if (args.isHtml) {629htmlPath = filePath;630htmlContent = extractHtmlContent(htmlPath);631const mdPath = filePath.replace(/\.html$/i, ".md");632if (fs.existsSync(mdPath)) {633const mdContent = fs.readFileSync(mdPath, "utf-8");634const parsed = parseFrontmatter(mdContent);635frontmatter = parsed.frontmatter;636if (!title && frontmatter.title) title = frontmatter.title;637if (!author) author = frontmatter.author || "";638if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";639}640if (!title) {641title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));642}643console.error(`[wechat-api] Using HTML file: ${htmlPath}`);644} else {645const content = fs.readFileSync(filePath, "utf-8");646const parsed = parseFrontmatter(content);647frontmatter = parsed.frontmatter;648const body = parsed.body;649650title = title || frontmatter.title || "";651if (!title) {652const h1Match = body.match(/^#\s+(.+)$/m);653if (h1Match) title = h1Match[1]!;654}655if (!author) author = frontmatter.author || "";656if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";657658console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}, citeStatus: ${args.citeStatus}`);659const rendered = renderMarkdownWithPlaceholders(filePath, args.theme, args.color, args.citeStatus, args.title);660htmlPath = rendered.htmlPath;661contentImages = rendered.contentImages;662if (!title) title = rendered.title;663if (!author) author = rendered.author;664if (!digest) digest = rendered.summary;665console.error(`[wechat-api] HTML generated: ${htmlPath}`);666console.error(`[wechat-api] Placeholder images: ${contentImages.length}`);667htmlContent = extractHtmlContent(htmlPath);668}669670if (!title) {671console.error("Error: No title found. Provide via --title, frontmatter, or <title> tag.");672process.exit(1);673}674675if (digest && digest.length > 120) {676const truncated = digest.slice(0, 117);677const lastPunct = Math.max(truncated.lastIndexOf("。"), truncated.lastIndexOf(","), truncated.lastIndexOf(";"), truncated.lastIndexOf("、"));678digest = lastPunct > 80 ? truncated.slice(0, lastPunct + 1) : truncated + "...";679console.error(`[wechat-api] Digest truncated to ${digest.length} chars`);680}681682console.error(`[wechat-api] Title: ${title}`);683if (author) console.error(`[wechat-api] Author: ${author}`);684if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);685console.error(`[wechat-api] Type: ${args.articleType}`);686687const extConfig = loadWechatExtendConfig();688const resolved = resolveAccount(extConfig, args.account);689if (resolved.name) console.error(`[wechat-api] Account: ${resolved.name} (${resolved.alias})`);690691if (!author && resolved.default_author) author = resolved.default_author;692693if (args.dryRun) {694console.log(JSON.stringify({695articleType: args.articleType,696title,697author: author || undefined,698digest: digest || undefined,699htmlPath,700contentLength: htmlContent.length,701placeholderImageCount: contentImages.length || undefined,702account: resolved.alias || undefined,703}, null, 2));704return;705}706707const creds = loadCredentials(resolved);708for (const skippedSource of creds.skippedSources) {709console.error(`[wechat-api] Skipped incomplete credential source: ${skippedSource}`);710}711console.error(`[wechat-api] Credentials source: ${creds.source}`);712console.error("[wechat-api] Fetching access token...");713const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);714715const rawCoverPath = args.cover ||716frontmatter.coverImage ||717frontmatter.featureImage ||718frontmatter.cover ||719frontmatter.image;720const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover721? path.resolve(process.cwd(), rawCoverPath)722: rawCoverPath;723const needNewsCoverFallback = args.articleType === "news" && !coverPath;724725console.error("[wechat-api] Uploading body images...");726const { html: processedHtml, firstCoverMediaId, imageMediaIds } = await uploadImagesInHtml(727htmlContent,728accessToken,729baseDir,730contentImages,731args.articleType,732needNewsCoverFallback,733);734htmlContent = processedHtml;735736let thumbMediaId = "";737738if (coverPath) {739console.error(`[wechat-api] Uploading cover: ${coverPath}`);740// 封面图片使用 material/add_material 接口741const coverResp = await uploadImage(coverPath, accessToken, baseDir, "material");742thumbMediaId = coverResp.media_id;743console.error(`[wechat-api] Cover uploaded successfully, media_id: ${thumbMediaId}`);744} else if (firstCoverMediaId && args.articleType === "news") {745// news 类型没有封面时,使用第一张正文图的 media_id 作为封面(兜底逻辑)746thumbMediaId = firstCoverMediaId;747console.error(`[wechat-api] Using first body image as cover (fallback), media_id: ${thumbMediaId}`);748}749750if (args.articleType === "news" && !thumbMediaId) {751console.error("Error: No cover image. Provide via --cover, frontmatter.coverImage, or include an image in content.");752process.exit(1);753}754755if (args.articleType === "newspic" && imageMediaIds.length === 0) {756console.error("Error: newspic requires at least one image in content.");757process.exit(1);758}759760console.error("[wechat-api] Publishing to draft...");761const result = await publishToDraft({762title,763author: author || undefined,764digest: digest || undefined,765content: htmlContent,766thumbMediaId,767articleType: args.articleType,768imageMediaIds: args.articleType === "newspic" ? imageMediaIds : undefined,769needOpenComment: resolved.need_open_comment,770onlyFansCanComment: resolved.only_fans_can_comment,771}, accessToken);772773console.log(JSON.stringify({774success: true,775media_id: result.media_id,776title,777articleType: args.articleType,778}, null, 2));779780console.error(`[wechat-api] Published successfully! media_id: ${result.media_id}`);781}782783await main().catch((err) => {784console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);785process.exit(1);786});787