From e79a42fd9471e020d8b984e98b8c1ca6bd4a83b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=AA=E4=B8=8D=E8=83=BD=E5=81=9C?= Date: Wed, 18 Mar 2026 17:23:48 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E6=AD=A3=E6=96=87=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BD=BF=E7=94=A8=20media/uploadimg=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 区分正文图片和封面图片的上传接口 - 正文图片使用 media/uploadimg (返回 URL) - 封面图片使用 material/add_material (返回 media_id) - 添加 uploadType 参数支持两种上传方式 - 优化错误提示,告知用户 news 类型需要封面图 --- .../scripts/wechat-api.ts | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index e1127ad..4802736 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -52,7 +52,8 @@ interface ArticleOptions { } const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"; -const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material"; +const UPLOAD_BODY_IMG_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg"; +const UPLOAD_MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material"; const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add"; @@ -75,7 +76,8 @@ async function fetchAccessToken(appId: string, appSecret: string): Promise { let fileBuffer: Buffer; let filename: string; @@ -133,7 +135,9 @@ async function uploadImage( const footerBuffer = Buffer.from(footer, "utf-8"); const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]); - const url = `${UPLOAD_URL}?access_token=${accessToken}&type=image`; + // 根据上传类型选择不同的接口 + const uploadUrl = uploadType === "body" ? UPLOAD_BODY_IMG_URL : UPLOAD_MATERIAL_URL; + const url = `${uploadUrl}?type=image&access_token=${accessToken}`; const res = await fetch(url, { method: "POST", headers: { @@ -147,11 +151,23 @@ async function uploadImage( throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`); } - if (data.url?.startsWith("http://")) { - data.url = data.url.replace(/^http:\/\//i, "https://"); + // media/uploadimg 接口只返回 URL,material/add_material 返回 media_id + if (uploadType === "body") { + // 正文图片上传,返回 URL + if (data.url?.startsWith("http://")) { + data.url = data.url.replace(/^http:\/\//i, "https://"); + } + return { + url: data.url, + media_id: "", + } as UploadResponse; + } else { + // 封面图片上传,返回 media_id + if (data.url?.startsWith("http://")) { + data.url = data.url.replace(/^http:\/\//i, "https://"); + } + return data; } - - return data; } async function uploadImagesInHtml( @@ -159,15 +175,15 @@ async function uploadImagesInHtml( accessToken: string, baseDir: string, contentImages: ImageInfo[] = [], -): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> { +): Promise<{ html: string; firstImageUrl: string; allMediaIds: string[] }> { const imgRegex = /]*\ssrc=["']([^"']+)["'][^>]*>/gi; const matches = [...html.matchAll(imgRegex)]; if (matches.length === 0 && contentImages.length === 0) { - return { html, firstMediaId: "", allMediaIds: [] }; + return { html, firstImageUrl: "", allMediaIds: [] }; } - let firstMediaId = ""; + let firstImageUrl = ""; let updatedHtml = html; const allMediaIds: string[] = []; const uploadedBySource = new Map(); @@ -177,8 +193,8 @@ async function uploadImagesInHtml( if (!src) continue; if (src.startsWith("https://mmbiz.qpic.cn")) { - if (!firstMediaId) { - firstMediaId = src; + if (!firstImageUrl) { + firstImageUrl = src; } continue; } @@ -186,20 +202,20 @@ async function uploadImagesInHtml( const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/); const imagePath = localPathMatch ? localPathMatch[1]! : src; - console.error(`[wechat-api] Uploading image: ${imagePath}`); + console.error(`[wechat-api] Uploading body image: ${imagePath}`); try { let resp = uploadedBySource.get(imagePath); if (!resp) { - resp = await uploadImage(imagePath, accessToken, baseDir); + // 正文图片使用 media/uploadimg 接口 + resp = await uploadImage(imagePath, accessToken, baseDir, "body"); uploadedBySource.set(imagePath, resp); } const newTag = fullTag .replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`) .replace(/\sdata-local-path=["'][^"']+["']/, ""); updatedHtml = updatedHtml.replace(fullTag, newTag); - allMediaIds.push(resp.media_id); - if (!firstMediaId) { - firstMediaId = resp.media_id; + if (!firstImageUrl) { + firstImageUrl = resp.url; } } catch (err) { console.error(`[wechat-api] Failed to upload ${imagePath}:`, err); @@ -210,27 +226,27 @@ async function uploadImagesInHtml( if (!updatedHtml.includes(image.placeholder)) continue; const imagePath = image.localPath || image.originalPath; - console.error(`[wechat-api] Uploading placeholder image: ${imagePath}`); + console.error(`[wechat-api] Uploading body image: ${imagePath}`); try { let resp = uploadedBySource.get(imagePath); if (!resp) { - resp = await uploadImage(imagePath, accessToken, baseDir); + // 正文图片使用 media/uploadimg 接口 + resp = await uploadImage(imagePath, accessToken, baseDir, "body"); uploadedBySource.set(imagePath, resp); } const replacementTag = ``; updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag); - allMediaIds.push(resp.media_id); - if (!firstMediaId) { - firstMediaId = resp.media_id; + if (!firstImageUrl) { + firstImageUrl = resp.url; } } catch (err) { console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err); } } - return { html: updatedHtml, firstMediaId, allMediaIds }; + return { html: updatedHtml, firstImageUrl, allMediaIds }; } async function publishToDraft( @@ -592,8 +608,8 @@ async function main(): Promise { console.error("[wechat-api] Fetching access token..."); const accessToken = await fetchAccessToken(creds.appId, creds.appSecret); - console.error("[wechat-api] Uploading images..."); - const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml( + console.error("[wechat-api] Uploading body images..."); + const { html: processedHtml, firstImageUrl, allMediaIds } = await uploadImagesInHtml( htmlContent, accessToken, baseDir, @@ -613,16 +629,17 @@ async function main(): Promise { if (coverPath) { console.error(`[wechat-api] Uploading cover: ${coverPath}`); - const coverResp = await uploadImage(coverPath, accessToken, baseDir); + // 封面图片使用 material/add_material 接口 + const coverResp = await uploadImage(coverPath, accessToken, baseDir, "material"); thumbMediaId = coverResp.media_id; - } else if (firstMediaId) { - if (firstMediaId.startsWith("https://")) { - console.error(`[wechat-api] Uploading first image as cover: ${firstMediaId}`); - const coverResp = await uploadImage(firstMediaId, accessToken, baseDir); - thumbMediaId = coverResp.media_id; - } else { - thumbMediaId = firstMediaId; - } + console.error(`[wechat-api] Cover uploaded successfully, media_id: ${thumbMediaId}`); + } else if (firstImageUrl && args.articleType === "news") { + // news 类型需要 thumb_media_id,正文图片的 URL 无法使用 + console.error(`[wechat-api] Warning: No cover image provided for news article.`); + console.error(`[wechat-api] The first body image URL is: ${firstImageUrl}`); + console.error(`[wechat-api] However, news articles require thumb_media_id (not URL).`); + console.error(`[wechat-api] Please provide --cover parameter or set coverImage in frontmatter.`); + process.exit(1); } if (args.articleType === "news" && !thumbMediaId) { From 747977416d984d4e23fcdd01a62af5ec4060cc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=AA=E4=B8=8D=E8=83=BD=E5=81=9C?= Date: Fri, 20 Mar 2026 13:57:59 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dnewspic=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0=E5=92=8Cnews?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=B0=81=E9=9D=A2=E5=85=9C=E5=BA=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uploadImagesInHtml 函数添加 articleType 参数支持 - 为 newspic 类型正文图片额外调用 material 接口收集 media_id - 返回 firstImageSource 用于 news 类型封面兜底逻辑 - 恢复 news 类型没有显式封面时使用第一张正文图作为封面的逻辑 --- .../scripts/wechat-api.ts | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index 4802736..7266460 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -175,17 +175,19 @@ async function uploadImagesInHtml( accessToken: string, baseDir: string, contentImages: ImageInfo[] = [], -): Promise<{ html: string; firstImageUrl: string; allMediaIds: string[] }> { + articleType: ArticleType = "news", +): Promise<{ html: string; firstImageUrl: string; firstImageSource: string; imageMediaIds: string[] }> { const imgRegex = /]*\ssrc=["']([^"']+)["'][^>]*>/gi; const matches = [...html.matchAll(imgRegex)]; if (matches.length === 0 && contentImages.length === 0) { - return { html, firstImageUrl: "", allMediaIds: [] }; + return { html, firstImageUrl: "", firstImageSource: "", imageMediaIds: [] }; } let firstImageUrl = ""; + let firstImageSource = ""; let updatedHtml = html; - const allMediaIds: string[] = []; + const imageMediaIds: string[] = []; const uploadedBySource = new Map(); for (const match of matches) { @@ -206,7 +208,7 @@ async function uploadImagesInHtml( try { let resp = uploadedBySource.get(imagePath); if (!resp) { - // 正文图片使用 media/uploadimg 接口 + // 正文图片使用 media/uploadimg 接口获取 URL resp = await uploadImage(imagePath, accessToken, baseDir, "body"); uploadedBySource.set(imagePath, resp); } @@ -217,6 +219,21 @@ async function uploadImagesInHtml( if (!firstImageUrl) { firstImageUrl = resp.url; } + + // 如果是 newspic 类型,额外调用 material 接口收集 media_id + if (articleType === "newspic") { + let materialResp = uploadedBySource.get(`${imagePath}:material`); + if (!materialResp) { + materialResp = await uploadImage(imagePath, accessToken, baseDir, "material"); + uploadedBySource.set(`${imagePath}:material`, materialResp); + } + if (materialResp.media_id) { + imageMediaIds.push(materialResp.media_id); + if (!firstImageSource) { + firstImageSource = materialResp.media_id; + } + } + } } catch (err) { console.error(`[wechat-api] Failed to upload ${imagePath}:`, err); } @@ -231,7 +248,7 @@ async function uploadImagesInHtml( try { let resp = uploadedBySource.get(imagePath); if (!resp) { - // 正文图片使用 media/uploadimg 接口 + // 正文图片使用 media/uploadimg 接口获取 URL resp = await uploadImage(imagePath, accessToken, baseDir, "body"); uploadedBySource.set(imagePath, resp); } @@ -241,12 +258,27 @@ async function uploadImagesInHtml( if (!firstImageUrl) { firstImageUrl = resp.url; } + + // 如果是 newspic 类型,额外调用 material 接口收集 media_id + if (articleType === "newspic") { + let materialResp = uploadedBySource.get(`${imagePath}:material`); + if (!materialResp) { + materialResp = await uploadImage(imagePath, accessToken, baseDir, "material"); + uploadedBySource.set(`${imagePath}:material`, materialResp); + } + if (materialResp.media_id) { + imageMediaIds.push(materialResp.media_id); + if (!firstImageSource) { + firstImageSource = materialResp.media_id; + } + } + } } catch (err) { console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err); } } - return { html: updatedHtml, firstImageUrl, allMediaIds }; + return { html: updatedHtml, firstImageUrl, firstImageSource, imageMediaIds }; } async function publishToDraft( @@ -609,11 +641,12 @@ async function main(): Promise { const accessToken = await fetchAccessToken(creds.appId, creds.appSecret); console.error("[wechat-api] Uploading body images..."); - const { html: processedHtml, firstImageUrl, allMediaIds } = await uploadImagesInHtml( + const { html: processedHtml, firstImageUrl, firstImageSource, imageMediaIds } = await uploadImagesInHtml( htmlContent, accessToken, baseDir, contentImages, + args.articleType, ); htmlContent = processedHtml; @@ -633,13 +666,10 @@ async function main(): Promise { const coverResp = await uploadImage(coverPath, accessToken, baseDir, "material"); thumbMediaId = coverResp.media_id; console.error(`[wechat-api] Cover uploaded successfully, media_id: ${thumbMediaId}`); - } else if (firstImageUrl && args.articleType === "news") { - // news 类型需要 thumb_media_id,正文图片的 URL 无法使用 - console.error(`[wechat-api] Warning: No cover image provided for news article.`); - console.error(`[wechat-api] The first body image URL is: ${firstImageUrl}`); - console.error(`[wechat-api] However, news articles require thumb_media_id (not URL).`); - console.error(`[wechat-api] Please provide --cover parameter or set coverImage in frontmatter.`); - process.exit(1); + } else if (firstImageSource && args.articleType === "news") { + // news 类型没有封面时,使用第一张正文图的 media_id 作为封面(兜底逻辑) + thumbMediaId = firstImageSource; + console.error(`[wechat-api] Using first body image as cover (fallback), media_id: ${thumbMediaId}`); } if (args.articleType === "news" && !thumbMediaId) { @@ -647,7 +677,7 @@ async function main(): Promise { process.exit(1); } - if (args.articleType === "newspic" && allMediaIds.length === 0) { + if (args.articleType === "newspic" && imageMediaIds.length === 0) { console.error("Error: newspic requires at least one image in content."); process.exit(1); } @@ -660,7 +690,7 @@ async function main(): Promise { content: htmlContent, thumbMediaId, articleType: args.articleType, - imageMediaIds: args.articleType === "newspic" ? allMediaIds : undefined, + imageMediaIds: args.articleType === "newspic" ? imageMediaIds : undefined, needOpenComment: resolved.need_open_comment, onlyFansCanComment: resolved.only_fans_can_comment, }, accessToken); From fc5ad4b7628f24f083e6ab98461f852b51828fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=AA=E4=B8=8D=E8=83=BD=E5=81=9C?= Date: Fri, 20 Mar 2026 14:25:48 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86media/uploadimg?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=9A=84=E6=96=87=E4=BB=B6=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=92=8C=E5=A4=A7=E5=B0=8F=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加文件大小和格式检查常量 - GIF图片或大于1MB的图片自动回退使用material接口 - 确保向后兼容现有工作流 --- .../scripts/wechat-api.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index 7266460..154bf66 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -73,6 +73,10 @@ async function fetchAccessToken(appId: string, appSecret: string): Promise = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", @@ -118,7 +127,14 @@ async function uploadImage( ".gif": "image/gif", ".webp": "image/webp", }; - contentType = mimeTypes[ext] || "image/jpeg"; + contentType = mimeTypes[fileExt] || "image/jpeg"; + } + + // media/uploadimg 接口只支持 JPG/PNG 且小于 1MB,如果是其他格式或文件过大,需要使用 material 接口 + const isGifOrLarge = fileExt === ".gif" || fileSize > BODY_IMG_MAX_SIZE; + if (uploadType === "body" && isGifOrLarge) { + console.error(`[wechat-api] Image ${filename} is GIF or larger than 1MB, using material API instead`); + uploadType = "material"; } const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`; From 79c289ca928f62c9a9728487a529eca92650a926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=AA=E4=B8=8D=E8=83=BD=E5=81=9C?= Date: Fri, 20 Mar 2026 18:18:01 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86WebP=E7=AD=89?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81=E7=9A=84=E6=A0=BC=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=9E=E9=80=80=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加WebP、BMP、TIFF等格式到不支持列表 - 对于需要回退到material接口的情况,同时调用两个接口获取URL和media_id - 提取uploadToWechat函数避免代码重复 --- .../scripts/wechat-api.ts | 81 +++++++++++++------ 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts index 154bf66..9aa3372 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-api.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-api.ts @@ -75,7 +75,7 @@ async function fetchAccessToken(appId: string, appSecret: string): Promise BODY_IMG_MAX_SIZE; - if (uploadType === "body" && isGifOrLarge) { - console.error(`[wechat-api] Image ${filename} is GIF or larger than 1MB, using material API instead`); - uploadType = "material"; + // 检查是否需要回退到 material 接口 + const isUnsupportedFormat = BODY_IMG_UNSUPPORTED_FORMATS.includes(fileExt); + const isTooLarge = fileSize > BODY_IMG_MAX_SIZE; + const needFallback = uploadType === "body" && (isUnsupportedFormat || isTooLarge); + + // 记录警告信息 + if (needFallback) { + const reason = isUnsupportedFormat ? `unsupported format (${fileExt})` : `too large (${(fileSize / 1024 / 1024).toFixed(2)}MB)`; + console.error(`[wechat-api] Image ${filename} is ${reason}, using material API for both URL and media_id`); } + // 如果需要回退到 material 接口,为了获取正文图片 URL,需要同时调用两个接口 + let bodyUrl = ""; + if (needFallback) { + // 先调用 material 接口获取 media_id(也返回 url) + const materialResult = await uploadToWechat(fileBuffer, filename, contentType, accessToken, "material"); + // 再调用 body 接口获取可以在正文中使用的 URL + const bodyResult = await uploadToWechat(fileBuffer, filename, contentType, accessToken, "body"); + bodyUrl = bodyResult.url || materialResult.url; + + if (materialResult.url?.startsWith("http://")) { + materialResult.url = materialResult.url.replace(/^http:\/\//i, "https://"); + } + return { + url: bodyUrl, + media_id: materialResult.media_id, + } as UploadResponse; + } + + // 正常情况:直接使用选定的接口上传 + const result = await uploadToWechat(fileBuffer, filename, contentType, accessToken, uploadType); + + // media/uploadimg 接口只返回 URL,material/add_material 返回 media_id + if (uploadType === "body") { + if (result.url?.startsWith("http://")) { + result.url = result.url.replace(/^http:\/\//i, "https://"); + } + return { + url: result.url, + media_id: "", + } as UploadResponse; + } else { + if (result.url?.startsWith("http://")) { + result.url = result.url.replace(/^http:\/\//i, "https://"); + } + return result; + } +} + +// 实际的微信上传函数 +async function uploadToWechat( + fileBuffer: Buffer, + filename: string, + contentType: string, + accessToken: string, + uploadType: "body" | "material" +): Promise { const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`; const header = [ `--${boundary}`, @@ -151,7 +201,6 @@ async function uploadImage( const footerBuffer = Buffer.from(footer, "utf-8"); const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]); - // 根据上传类型选择不同的接口 const uploadUrl = uploadType === "body" ? UPLOAD_BODY_IMG_URL : UPLOAD_MATERIAL_URL; const url = `${uploadUrl}?type=image&access_token=${accessToken}`; const res = await fetch(url, { @@ -167,23 +216,7 @@ async function uploadImage( throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`); } - // media/uploadimg 接口只返回 URL,material/add_material 返回 media_id - if (uploadType === "body") { - // 正文图片上传,返回 URL - if (data.url?.startsWith("http://")) { - data.url = data.url.replace(/^http:\/\//i, "https://"); - } - return { - url: data.url, - media_id: "", - } as UploadResponse; - } else { - // 封面图片上传,返回 media_id - if (data.url?.startsWith("http://")) { - data.url = data.url.replace(/^http:\/\//i, "https://"); - } - return data; - } + return data; } async function uploadImagesInHtml(