added:增加Linux-server
This commit is contained in:
1 parent
712116d21d
commit
bd96aadb2f
90 files changed
+16784
-1
No files matched your search
@@ -0,0 +1,223 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user