Files
blog/write-server/src/lib/rssapi.ts
T

223 lines
6.5 KiB
TypeScript
Raw Normal View History

2026-06-21 20:42:44 +08:00
import { RSS_API_BASE, RSS_API_TOKEN, WECHAT_APP_ID, WECHAT_APP_SECRET } from "./config";
async function rssFetch<T>(path: string, options?: RequestInit): Promise<T> {
const url = `${RSS_API_BASE}${path}`;
const res = await fetch(url, options);
if (!res.ok) throw new Error(`RSS API error: ${res.status}`);
return res.json();
}
function withToken(params?: URLSearchParams): string {
const p = params || new URLSearchParams();
if (RSS_API_TOKEN) p.set("token", RSS_API_TOKEN);
return p.toString();
}
// ---- Types ----
export interface Feed {
url: string;
feedTitle: string;
siteUrl?: string;
error?: string;
}
export interface FeedHealth {
url: string;
status: number;
ok: boolean;
latency?: number;
error?: string;
}
export interface Link {
name: string;
url: string;
description?: string;
image?: string;
hidden?: boolean;
rss?: string;
addedAt?: string;
}
export interface LinkHealth {
url: string;
ok: boolean;
status?: number;
latency?: number;
error?: string;
}
export interface Article {
title: string;
link: string;
pubDate: string;
feedName: string;
siteUrl: string;
favicon: string;
author: string;
}
export interface HealthResult {
total: number;
alive: number;
dead: number;
results: FeedHealth[];
}
// ---- Feeds API ----
export async function getFeeds(): Promise<Feed[]> {
const data = await rssFetch<{ feeds: Feed[] }>("/api/feeds");
return data.feeds || [];
}
export async function addFeed(url: string, feedTitle: string): Promise<void> {
const qs = withToken();
await rssFetch(`/api/feeds?${qs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, feedTitle }),
});
}
export async function updateFeed(oldUrl: string, newUrl: string, feedTitle: string): Promise<void> {
const qs = withToken();
const body: Record<string, string> = { url: newUrl, feedTitle };
if (oldUrl && oldUrl !== newUrl) body.oldUrl = oldUrl;
await rssFetch(`/api/feeds?${qs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
export async function deleteFeed(url: string): Promise<void> {
const qs = withToken(new URLSearchParams({ url, token: RSS_API_TOKEN }));
await rssFetch(`/api/feeds?${qs}`, { method: "DELETE" });
}
export async function getFeedsHealth(): Promise<HealthResult> {
return rssFetch("/api/health");
}
// ---- Articles API ----
export async function getArticles(limit = 10): Promise<Article[]> {
const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`);
return data.articles || [];
}
// ---- Links API ----
export async function getLinks(all = false): Promise<Link[]> {
const p = new URLSearchParams();
if (all) p.set("all", "1");
const data = await rssFetch<{ links: Link[] }>(`/api/links?${p.toString()}`);
return data.links || [];
}
export async function addLink(link: { name: string; url: string; description?: string; hidden?: boolean }): Promise<void> {
const qs = withToken();
await rssFetch(`/api/links?${qs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(link),
});
}
export async function updateLink(oldUrl: string, data: { name?: string; url?: string; description?: string; hidden?: boolean }): Promise<void> {
const qs = withToken();
const body: Record<string, unknown> = { ...data };
if (data.url && data.url !== oldUrl) body.oldUrl = oldUrl;
await rssFetch(`/api/links?${qs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
export async function deleteLink(url: string): Promise<void> {
const qs = withToken(new URLSearchParams({ url, token: RSS_API_TOKEN }));
await rssFetch(`/api/links?${qs}`, { method: "DELETE" });
}
// ---- SCF Proxy ----
const SCF_PROXY = process.env.SCF_PROXY || "https://scfapi.usj.cc";
export async function checkLinkHealth(targetUrl: string): Promise<LinkHealth> {
const start = Date.now();
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 12000);
const res = await fetch(targetUrl, {
method: "HEAD",
signal: controller.signal,
headers: { "User-Agent": "Mozilla/5.0 (compatible; BlogAdmin/1.0)" },
});
clearTimeout(timer);
return { url: targetUrl, ok: res.ok || res.status < 500, status: res.status, latency: Date.now() - start };
} catch (err: unknown) {
return { url: targetUrl, ok: false, error: err instanceof Error ? err.message : "timeout", latency: Date.now() - start };
}
}
export async function checkLinkViaProxy(targetUrl: string): Promise<LinkHealth> {
const start = Date.now();
try {
const res = await fetch(SCF_PROXY, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: targetUrl, timeout: 12000 }),
});
const data = await res.json();
return {
url: targetUrl,
ok: data.ok && (data.status < 500),
status: data.status,
latency: Date.now() - start,
error: data.ok ? undefined : data.error,
};
} catch (err: unknown) {
return { url: targetUrl, ok: false, error: err instanceof Error ? err.message : "proxy error", latency: Date.now() - start };
}
}
// ---- WeChat Material API ----
export interface WechatArticle {
title: string;
thumb_media_url?: string;
thumb_media_id?: string;
author?: string;
digest?: string;
show_cover_pic?: number;
content: string;
content_source_url?: string;
content_image_urls?: { original_url: string }[];
need_open_comment?: number;
only_fans_can_comment?: number;
}
export async function getWechatStatus(): Promise<{ configured: boolean; appId?: string }> {
const data = await rssFetch<{ configured: boolean; appId?: string }>("/api/wechat-material");
return data;
}
export async function syncWechat(articles: Record<string, unknown>[]): Promise<{ ok: boolean; media_id?: string; message?: string }> {
const qs = withToken();
const body: Record<string, unknown> = { articles };
if (WECHAT_APP_ID) body.appId = WECHAT_APP_ID;
if (WECHAT_APP_SECRET) body.appSecret = WECHAT_APP_SECRET;
const res = await fetch(`${RSS_API_BASE}/api/wechat-material?${qs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok || !data.ok) {
throw new Error(data.error || data.message || `WeChat API error: ${res.status}`);
}
return data;
}