109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
// rss-robot-cf —— 主入口
|
||||
|
|
// 路由表 + Cron scheduled。迁自 EdgeOne 的 api/*.js 各端点 + check-feeds.js 抓取逻辑。
|
|||
|
|
|
|||
|
|
import type { Env } from './types';
|
|||
|
|
import { runCron } from './lib/rss/cron';
|
|||
|
|
|
|||
|
|
/** 每小时抓取的源数(66 源 ÷ 22 ≈ 3 小时跑完一轮) */
|
|||
|
|
const RSS_BATCH = 22;
|
|||
|
|
import * as FeedsRoute from './routes/rss/feeds';
|
|||
|
|
import * as LinksRoute from './routes/rss/links';
|
|||
|
|
import { results, articles } from './routes/rss/results';
|
|||
|
|
import { auth, health } from './routes/rss/misc';
|
|||
|
|
import { favicon, update, proxy, cronRun, notifyMail, proxyHealth, deployStatusGet, deployStatusPost, deployNotify } from './routes/rss/tools';
|
|||
|
|
import { linkApply, linkApplyList, linkApplyReview } from './routes/rss/links';
|
|||
|
|
import * as WechatRoute from './routes/rss/wechat';
|
|||
|
|
import { randomImage, artalkCommenter } from './routes/rss/media';
|
|||
|
|
import * as GreetingRoute from './routes/rss/greeting';
|
|||
|
|
import { respond } from './lib/rss/util';
|
|||
|
|
|
|||
|
|
const CORS = {
|
|||
|
|
'Access-Control-Allow-Origin': '*',
|
|||
|
|
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|||
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const RssApi = {
|
|||
|
|
// 每小时轮转抓取:每次抓 RSS_BATCH 个源,游标存在 KV(rss_cursor),
|
|||
|
|
// 三四个小时覆盖全部 66 个源。免费版单次调用限 50 subrequest,
|
|||
|
|
// 所以批量不能大;连续失败的源会进入退避期被跳过(见 cron.ts)。
|
|||
|
|
async scheduled(_controller: ScheduledController, env: Env, _ctx: ExecutionContext): Promise<void> {
|
|||
|
|
await runCron(env, { limit: RSS_BATCH, rotate: true });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
|
|||
|
|
const url = new URL(request.url);
|
|||
|
|
const path = url.pathname;
|
|||
|
|
|
|||
|
|
// CORS 预检(所有端点)
|
|||
|
|
if (request.method === 'OPTIONS') {
|
|||
|
|
return new Response(null, { status: 204, headers: CORS });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 微信验证文件(MP_verify_xxx.txt)
|
|||
|
|
if (path.startsWith('/MP_verify_') && path.endsWith('.txt')) {
|
|||
|
|
const code = path.replace('/MP_verify_', '').replace('.txt', '');
|
|||
|
|
return new Response(code, { status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 健康检查
|
|||
|
|
if (path === '/api/health') return health(request, env);
|
|||
|
|
|
|||
|
|
// 管理页(/feed 订阅源管理)与首页登录门
|
|||
|
|
if (path === '/feed') {
|
|||
|
|
const resp = await env.ASSETS!.fetch(new URL('/feed.html', request.url));
|
|||
|
|
if (resp.ok) return new Response(resp.body, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
|||
|
|
return respond({ error: 'feed.html not found' }, 404);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 路由分发
|
|||
|
|
try {
|
|||
|
|
switch (path) {
|
|||
|
|
case '/api/feeds':
|
|||
|
|
return FeedsRoute.onRequest(request, env);
|
|||
|
|
case '/api/links':
|
|||
|
|
return LinksRoute.onRequest(request, env);
|
|||
|
|
case '/api/results':
|
|||
|
|
return results(request, env);
|
|||
|
|
case '/api/articles':
|
|||
|
|
return articles(request, env);
|
|||
|
|
case '/api/auth':
|
|||
|
|
return auth(request, env);
|
|||
|
|
case '/api/favicon':
|
|||
|
|
return favicon(request, env);
|
|||
|
|
case '/api/update':
|
|||
|
|
return update(request, env);
|
|||
|
|
case '/api/cron/run':
|
|||
|
|
return cronRun(request, env);
|
|||
|
|
case '/api/deploy-status':
|
|||
|
|
return request.method === 'POST' ? deployStatusPost(request, env) : deployStatusGet(request, env);
|
|||
|
|
case '/api/deploy-notify':
|
|||
|
|
return deployNotify(request, env);
|
|||
|
|
case '/api/link-apply':
|
|||
|
|
return linkApply(request, env);
|
|||
|
|
case '/api/link-apply/list':
|
|||
|
|
return linkApplyList(request, env);
|
|||
|
|
case '/api/link-apply/review':
|
|||
|
|
return linkApplyReview(request, env);
|
|||
|
|
case '/api/proxy':
|
|||
|
|
return proxy(request, env);
|
|||
|
|
case '/api/notify-mail':
|
|||
|
|
return notifyMail(request, env);
|
|||
|
|
case '/api/proxy-health':
|
|||
|
|
return proxyHealth(request, env);
|
|||
|
|
case '/api/wechat-material':
|
|||
|
|
return WechatRoute.onRequest(request, env);
|
|||
|
|
case '/api/random-image':
|
|||
|
|
return randomImage(request, env);
|
|||
|
|
case '/api/artalk/commenter':
|
|||
|
|
return artalkCommenter(request, env);
|
|||
|
|
case '/api/ai/greeting':
|
|||
|
|
return GreetingRoute.onRequest(request, env);
|
|||
|
|
default:
|
|||
|
|
return respond({ error: 'not found', path }, 404);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
return respond({ error: (err as Error).message }, 500);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
};
|