import { RSS_API_BASE, RSS_API_TOKEN, WECHAT_APP_ID, WECHAT_APP_SECRET } from "./config"; async function rssFetch(path: string, options?: RequestInit): Promise { 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 { const data = await rssFetch<{ feeds: Feed[] }>("/api/feeds"); return data.feeds || []; } export async function addFeed(url: string, feedTitle: string): Promise { 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 { const qs = withToken(); const body: Record = { 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 { const qs = withToken(new URLSearchParams({ url, token: RSS_API_TOKEN })); await rssFetch(`/api/feeds?${qs}`, { method: "DELETE" }); } export async function getFeedsHealth(): Promise { return rssFetch("/api/health"); } // ---- Articles API ---- export async function getArticles(limit = 10): Promise { const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`); return data.articles || []; } // ---- Links API ---- export async function getLinks(all = false): Promise { 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 { 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 { const qs = withToken(); const body: Record = { ...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 { 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 { 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 { 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[]): Promise<{ ok: boolean; media_id?: string; message?: string }> { const qs = withToken(); const body: Record = { 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; }