1301
This commit is contained in:
1 parent
f99ddec735
commit
8cd35ca127
9 files changed
+263
-45
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getWechatStatus, syncWechat } from "@/lib/rssapi";
|
||||
import { getWechatStatus, syncWechat } from "@/lib/wechat";
|
||||
import { marked } from "marked";
|
||||
|
||||
function resolveUrl(src: string, slug: string): string {
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function EditPostPage({
|
||||
const res = await fetch("/api/hugo");
|
||||
const data = await res.json();
|
||||
if (data.running) {
|
||||
window.open(`/preview/${post.frontMatter.slug}.html`, "_blank");
|
||||
window.open(`${location.protocol}//${location.hostname}:1313/${post.frontMatter.slug}.html`, "_blank");
|
||||
} else {
|
||||
alert("Hugo 预览启动失败");
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function HomePage() {
|
||||
const res = await fetch("/api/hugo");
|
||||
const data = await res.json();
|
||||
if (data.running) {
|
||||
window.open(`/preview/${slug}.html`, "_blank");
|
||||
window.open(`${location.protocol}//${location.hostname}:1313/${slug}.html`, "_blank");
|
||||
} else {
|
||||
alert("Hugo 预览启动失败,请稍后再试");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<DraftResult> {
|
||||
const token = await getAccessToken();
|
||||
|
||||
const processedArticles: Record<string, unknown>[] = [];
|
||||
|
||||
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<string, unknown> = {
|
||||
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<DraftResult> {
|
||||
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 };
|
||||
}
|
||||
Reference in new issue
Block a user