import { WECHAT_APP_ID, WECHAT_APP_SECRET } from "./config"; // ---- Token 管理 ---- interface TokenCache { token: string; expiresAt: number; } let tokenCache: TokenCache | null = null; async function getAccessToken(): Promise { if (tokenCache && Date.now() < tokenCache.expiresAt) { return tokenCache.token; } if (!WECHAT_APP_ID || !WECHAT_APP_SECRET) { throw new Error("微信 AppID 或 AppSecret 未配置"); } const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}`; const res = await fetch(url); const data = await res.json() as { access_token?: string; expires_in?: number; errcode?: number; errmsg?: string }; if (data.errcode) { throw new Error(`获取 access_token 失败: ${data.errmsg} (${data.errcode})`); } if (!data.access_token) { throw new Error("获取 access_token 失败: 返回为空"); } // 提前 5 分钟过期 tokenCache = { token: data.access_token, expiresAt: Date.now() + (data.expires_in! - 300) * 1000, }; return tokenCache.token; } // ---- 状态检查 ---- export async function getWechatStatus(): Promise<{ configured: boolean; appId?: string }> { return { configured: !!(WECHAT_APP_ID && WECHAT_APP_SECRET), appId: WECHAT_APP_ID ? WECHAT_APP_ID.slice(0, 6) + "****" : undefined, }; } // ---- 上传封面图片 ---- export async function uploadImage(imageUrl: string): Promise { const token = await getAccessToken(); // 下载图片 const imgRes = await fetch(imageUrl); if (!imgRes.ok) throw new Error(`下载图片失败: ${imgRes.status}`); const imgBuffer = await imgRes.arrayBuffer(); // 从 URL 推断文件名 const urlParts = imageUrl.split("/"); const filename = urlParts[urlParts.length - 1] || "cover.jpg"; // 构造 multipart/form-data const formData = new FormData(); const blob = new Blob([imgBuffer], { type: "image/jpeg" }); formData.append("media", blob, filename); const uploadUrl = `https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=image`; const uploadRes = await fetch(uploadUrl, { method: "POST", body: formData }); const uploadData = await uploadRes.json() as { media_id?: string; url?: string; errcode?: number; errmsg?: string }; if (uploadData.errcode) { throw new Error(`上传图片失败: ${uploadData.errmsg} (${uploadData.errcode})`); } return uploadData.media_id!; } // ---- 上传文章内图片(用于正文中引用的图片) ---- export async function uploadContentImage(imageUrl: string): Promise { const token = await getAccessToken(); const imgRes = await fetch(imageUrl); if (!imgRes.ok) throw new Error(`下载图片失败: ${imgRes.status}`); const imgBuffer = await imgRes.arrayBuffer(); const urlParts = imageUrl.split("/"); const filename = urlParts[urlParts.length - 1] || "image.jpg"; const formData = new FormData(); const blob = new Blob([imgBuffer], { type: "image/jpeg" }); formData.append("media", blob, filename); const uploadUrl = `https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`; const uploadRes = await fetch(uploadUrl, { method: "POST", body: formData }); const uploadData = await uploadRes.json() as { url?: string; errcode?: number; errmsg?: string }; if (uploadData.errcode) { throw new Error(`上传内容图片失败: ${uploadData.errmsg} (${uploadData.errcode})`); } return uploadData.url!; } // ---- 替换文章中的图片为微信 URL ---- async function replaceContentImages(html: string, imageUrls: string[]): Promise { let result = html; for (const url of imageUrls) { try { const wxUrl = await uploadContentImage(url); result = result.replaceAll(url, wxUrl); } catch (e) { console.warn(`[wechat] 跳过图片 ${url}:`, e instanceof Error ? e.message : String(e)); } } return result; } // ---- 创建草稿 ---- interface Article { title: string; author?: string; content: string; content_source_url?: string; thumb_media_id?: string; thumb_media_url?: string; show_cover_pic?: number; need_open_comment?: number; content_image_urls?: { original_url: string }[]; } interface DraftResult { ok: boolean; media_id?: string; message?: string; } export async function syncWechat(articles: Article[]): Promise { const token = await getAccessToken(); const processedArticles: Record[] = []; for (const article of articles) { let content = article.content; // 替换内容中的图片为微信 URL if (article.content_image_urls && article.content_image_urls.length > 0) { const urls = article.content_image_urls.map(u => u.original_url); content = await replaceContentImages(content, urls); } const processed: Record = { title: article.title, author: article.author || "", content: content, content_source_url: article.content_source_url || "", show_cover_pic: article.show_cover_pic ?? 1, need_open_comment: article.need_open_comment ?? 0, }; // 上传封面图 if (article.thumb_media_url) { try { const mediaId = await uploadImage(article.thumb_media_url); processed.thumb_media_id = mediaId; } catch (e) { console.warn("[wechat] 封面图上传失败:", e instanceof Error ? e.message : String(e)); } } else if (article.thumb_media_id) { processed.thumb_media_id = article.thumb_media_id; } processedArticles.push(processed); } // 创建草稿 const draftUrl = `https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${token}`; const draftRes = await fetch(draftUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ articles: processedArticles }), }); const draftData = await draftRes.json() as { media_id?: string; errcode?: number; errmsg?: string }; if (draftData.errcode) { return { ok: false, message: `创建草稿失败: ${draftData.errmsg} (${draftData.errcode})` }; } return { ok: true, media_id: draftData.media_id }; } // ---- 发布草稿 ---- export async function publishDraft(mediaId: string): Promise { const token = await getAccessToken(); const url = `https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=${token}`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ media_id: mediaId }), }); const data = await res.json() as { publish_id?: string; errcode?: number; errmsg?: string }; if (data.errcode) { return { ok: false, message: `发布失败: ${data.errmsg} (${data.errcode})` }; } return { ok: true, media_id: data.publish_id }; }