// 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 { await runCron(env, { limit: RSS_BATCH, rotate: true }); }, async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise { 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); } }, };