rssany 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/plugins/builtin/agi-eval-evaluation.rssany.js +188 -0
- package/app/plugins/builtin/amii-research-talent.rssany.js +73 -0
- package/app/plugins/builtin/anthropic-research.rssany.js +155 -0
- package/app/plugins/builtin/appen-resources.rssany.js +155 -0
- package/app/plugins/builtin/baai-wudao-paper-article.rssany.js +185 -0
- package/app/plugins/builtin/baaidata-csdn.rssany.js +242 -0
- package/app/plugins/builtin/baidu-research.rssany.js +222 -0
- package/app/plugins/builtin/brightdata-blog.rssany.js +301 -0
- package/app/plugins/builtin/bytedance-seed-research.rssany.js +231 -0
- package/app/plugins/builtin/five-radar.rssany.js +490 -0
- package/app/plugins/builtin/flageval-news.rssany.js +118 -0
- package/app/plugins/builtin/google-deepmind-research.rssany.js +223 -0
- package/app/plugins/builtin/google-research-datasets.rssany.js +171 -0
- package/app/plugins/builtin/google-research.rssany.js +220 -0
- package/app/plugins/builtin/google.rssany.js +187 -0
- package/app/plugins/builtin/hacker-news-newest.rssany.js +130 -0
- package/app/plugins/builtin/harvard-dataverse.rssany.js +166 -0
- package/app/plugins/builtin/huaweicloud-bbs-blogs.rssany.js +185 -0
- package/app/plugins/builtin/lingowhale.rssany.js +119 -0
- package/app/plugins/builtin/meituan-tech.rssany.js +130 -0
- package/app/plugins/builtin/meta-ai-publications.rssany.js +221 -0
- package/app/plugins/builtin/mila-quebec.rssany.js +199 -0
- package/app/plugins/builtin/mit-csail-research.rssany.js +208 -0
- package/app/plugins/builtin/moonshot.rssany.js +127 -0
- package/app/plugins/builtin/opendatalab-news.rssany.js +174 -0
- package/app/plugins/builtin/opendatalab.rssany.js +109 -0
- package/app/plugins/builtin/opendrivelab-autonomous-driving.rssany.js +114 -0
- package/app/plugins/builtin/opendrivelab-embodiedai.rssany.js +114 -0
- package/app/plugins/builtin/opendrivelab-publications.rssany.js +130 -0
- package/app/plugins/builtin/opendrivelab.rssany.js +333 -0
- package/app/plugins/builtin/paperswithcode.rssany.js +227 -0
- package/app/plugins/builtin/pjlab-adg-publications.rssany.js +202 -0
- package/app/plugins/builtin/rss.rssany.js +11 -1
- package/app/plugins/builtin/selectdataset.rssany.js +206 -0
- package/app/plugins/builtin/sensetime-tech-achievements.rssany.js +154 -0
- package/app/plugins/builtin/supervisely-blog.rssany.js +159 -0
- package/app/plugins/builtin/theinformation-briefings.rssany.js +136 -0
- package/app/plugins/builtin/uci-ml-repository.rssany.js +111 -0
- package/app/plugins/builtin/venturebeat.rssany.js +97 -0
- package/app/plugins/builtin/worldlabs.rssany.js +129 -0
- package/app/plugins/builtin/x.rssany.js +328 -0
- package/app/plugins/builtin/xiaohongshu.rssany.js +283 -0
- package/app/plugins/builtin/zhipu-research.rssany.js +334 -0
- package/dist/index.js +62 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/webui/build/200.html +6 -6
- package/webui/build/_app/immutable/assets/{0.DjU2hdCQ.css → 0.BB88QFoe.css} +1 -1
- package/webui/build/_app/immutable/assets/homeFeedPanelStore.CSvlNcpm.css +1 -0
- package/webui/build/_app/immutable/chunks/BwlaCkNX.js +36 -0
- package/webui/build/_app/immutable/chunks/C0J2-L94.js +1 -0
- package/webui/build/_app/immutable/chunks/CLOXMsDk.js +36 -0
- package/webui/build/_app/immutable/chunks/{C85CNwD2.js → DgceFEv5.js} +1 -1
- package/webui/build/_app/immutable/chunks/{CllQAdvt.js → SqCUd34O.js} +1 -1
- package/webui/build/_app/immutable/entry/{app.BcD2eSsQ.js → app.B8zBPipq.js} +2 -2
- package/webui/build/_app/immutable/entry/start.CxRCKeCl.js +1 -0
- package/webui/build/_app/immutable/nodes/0.ChLNE3xy.js +11 -0
- package/webui/build/_app/immutable/nodes/{1.DU9aYGAb.js → 1.1N74-4Io.js} +1 -1
- package/webui/build/_app/immutable/nodes/{10.Db6vw7Ih.js → 10.DY30t9Ib.js} +1 -1
- package/webui/build/_app/immutable/nodes/{11.BaAcorz3.js → 11.ITuxnukH.js} +1 -1
- package/webui/build/_app/immutable/nodes/12.qLzWqB1c.js +1 -0
- package/webui/build/_app/immutable/nodes/{14.DqT4pcrQ.js → 14.BHnIxbVM.js} +1 -1
- package/webui/build/_app/immutable/nodes/{15.CCLbjxnH.js → 15.CLjT9il3.js} +1 -1
- package/webui/build/_app/immutable/nodes/{16.DiigpVdP.js → 16.BD-mKCLN.js} +1 -1
- package/webui/build/_app/immutable/nodes/{3.DEcYOQc-.js → 3.Dt5o2Fmz.js} +1 -1
- package/webui/build/_app/immutable/nodes/{5.CvM1TkLG.js → 5.Dy3vSsIP.js} +1 -1
- package/webui/build/_app/immutable/nodes/{6.Dscr6LkS.js → 6.DvclsL6H.js} +1 -1
- package/webui/build/_app/immutable/nodes/{7.Bp60MobD.js → 7.D2nJy-Uz.js} +1 -1
- package/webui/build/_app/immutable/nodes/{8.DwSg0MHh.js → 8.C75mhrqs.js} +1 -1
- package/webui/build/_app/immutable/nodes/{9.BeYOUjxR.js → 9.Bp_QXw3w.js} +1 -1
- package/webui/build/_app/version.json +1 -1
- package/webui/build/_app/immutable/assets/homeFeedPanelStore.BopJZtHu.css +0 -1
- package/webui/build/_app/immutable/chunks/CdMsRjxJ.js +0 -1
- package/webui/build/_app/immutable/chunks/CtijX1u3.js +0 -31
- package/webui/build/_app/immutable/chunks/Dv1VCsiB.js +0 -41
- package/webui/build/_app/immutable/entry/start.CbkdJdz1.js +0 -1
- package/webui/build/_app/immutable/nodes/0.DSUDmOx2.js +0 -11
- package/webui/build/_app/immutable/nodes/12.Cg8AeCSH.js +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../app/scraper/sources/web/fetcher/purify.ts","../app/scraper/sources/web/fetcher/cdp.ts","../app/types/feedItem.ts","../app/utils/httpSourceRef.ts","../app/packageRoot.ts","../app/config/paths.ts","../app/db/index.ts","../app/core/logger/config.ts","../app/core/logger/index.ts","../app/scraper/sources/web/fetcher/browser.ts","../app/utils/refreshInterval.ts","../app/core/cacher/index.ts","../app/scraper/sources/web/extractor/extractor.ts","../app/core/llmConfig.ts","../app/core/llm.ts","../app/scraper/sources/web/parser/parser.ts","../app/scraper/sources/web/site.ts","../app/scraper/auth/errors.ts","../app/plugins/loader.ts","../app/scraper/sources/web/index.ts","../app/plugins/hostDeps.ts","../app/scraper/sources/context.ts","../app/scraper/sources/index.ts","../app/scraper/subscription/types.ts","../app/config/globalProxy.ts","../app/scraper/subscription/index.ts","../app/pipeline/qualityFilter.ts","../app/pipeline/tagger.ts","../app/pipeline/translator.ts","../app/pipeline/config.ts","../app/pipeline/index.ts","../app/feeder/rss.ts","../app/core/events/index.ts","../app/config/deliver.ts","../app/deliver/post.ts","../app/feeder/feeder.ts","../app/scheduler/index.ts","../app/scraper/scheduler/index.ts","../app/auth/middleware.ts","../app/router/routes/api/server.ts","../app/router/routes/api/rss.ts","../app/router/routes/api/scheduler.ts","../app/router/routes/api/plugins.ts","../app/router/routes/api/pipeline.ts","../app/router/routes/api/feed.ts","../app/router/routes/api/items.ts","../app/router/routes/api/logs.ts","../app/router/routes/api/admin.ts","../app/router/routes/api/sources.ts","../app/router/routes/api/topics.ts","../app/router/routes/api/deliver.ts","../app/config/llmSettings.ts","../app/router/routes/api/llm.ts","../app/router/routes/api/proxy.ts","../app/tasks/index.ts","../app/router/routes/api/tasks.ts","../app/router/routes/api/feed-favicon.ts","../app/router/routes/api/index.ts","../app/router/routes/auth.ts","../app/router/utils.ts","../app/router/routes/admin.ts","../app/router/routes/rss.ts","../app/router/webui.ts","../app/router/index.ts"],"sourcesContent":["// 基于 node-html-parser 的 HTML 净化:移除与 RSS 内容无关的标签、属性、注释\n\nimport { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement, Node } from \"node-html-parser\";\n\n\nconst TAGS_TO_REMOVE = [\n \"script\", \"style\", \"svg\", \"symbol\", \"link\", \"meta\",\n \"input\", \"embed\", \"button\", \"select\", \"textarea\",\n \"nav\", \"iframe\", \"noscript\", \"template\", \"object\", \"canvas\",\n];\n\n\nconst BASE64_IMG_PATTERN = /^data:image\\/[^;\"'\\s]+;base64,/i;\n\n\nfunction collectCommentNodes(node: Node, out: Node[]): void {\n if (node.nodeType === NodeType.COMMENT_NODE) {\n out.push(node);\n return;\n }\n if (\"childNodes\" in node && Array.isArray(node.childNodes)) {\n for (const child of node.childNodes) {\n collectCommentNodes(child, out);\n }\n }\n}\n\n\nfunction stripRssIrrelevantAttributes(root: HTMLElement): void {\n const toProcess: HTMLElement[] = [root];\n const all = root.querySelectorAll(\"*\");\n for (const el of all) {\n if (el.nodeType === NodeType.ELEMENT_NODE) toProcess.push(el as HTMLElement);\n }\n for (const elem of toProcess) {\n elem.removeAttribute(\"class\");\n elem.removeAttribute(\"style\");\n const src = elem.getAttribute(\"src\");\n if (src && BASE64_IMG_PATTERN.test(src)) {\n elem.removeAttribute(\"src\");\n }\n }\n}\n\n\n/** 使用 node-html-parser 解析并净化 HTML,剥离与 RSS 内容无关的部分(默认开启) */\nexport function applyPurify(html: string, purify: boolean | undefined): string {\n if (purify === false) return html;\n const root = parse(html, { comment: true });\n for (const tag of TAGS_TO_REMOVE) {\n const list = root.querySelectorAll(tag);\n for (const el of list) {\n el.remove();\n }\n }\n const commentNodes: Node[] = [];\n collectCommentNodes(root, commentNodes);\n for (const node of commentNodes) {\n node.remove();\n }\n stripRssIrrelevantAttributes(root);\n return root.toString();\n}\n","// Chrome DevTools Protocol (CDP) 控制模块:支持连接到手动启动的 Chrome,避免安装 Puppeteer 专属 Chrome\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { platform } from \"node:os\";\nimport { join } from \"node:path\";\nimport puppeteerCore, { type Browser, type ConnectOptions, type LaunchOptions } from \"puppeteer-core\";\n\n\n/** 按平台枚举所有可能的 Chrome 路径,优先返回第一个实际存在的;全部不存在则返回 null */\nexport function findChromeExecutable(): string | null {\n const platformName = platform();\n const paths: string[] = [];\n const envChrome = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;\n if (envChrome) paths.push(envChrome);\n if (platformName === \"darwin\") {\n paths.push(\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\"\n );\n } else if (platformName === \"linux\") {\n paths.push(\n \"/usr/bin/google-chrome\",\n \"/usr/bin/google-chrome-stable\",\n \"/usr/bin/chromium\",\n \"/usr/bin/chromium-browser\",\n \"/snap/bin/chromium\"\n );\n } else if (platformName === \"win32\") {\n const programFiles = process.env[\"ProgramFiles\"] || \"C:\\\\Program Files\";\n const programFilesX86 = process.env[\"ProgramFiles(x86)\"] || \"C:\\\\Program Files (x86)\";\n paths.push(\n join(programFiles, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n join(programFilesX86, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n join(programFiles, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\"),\n join(programFilesX86, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\")\n );\n }\n for (const p of paths) {\n try {\n if (existsSync(p)) return p;\n } catch {\n // 跳过\n }\n }\n return null;\n}\n\n\n/** CDP 连接配置 */\nexport interface CDPConfig {\n /** Chrome 可执行文件路径,不提供则自动查找 */\n executablePath?: string;\n /** CDP 端口,默认 9222 */\n port?: number;\n /** 是否使用已启动的 Chrome(不自动启动) */\n useExisting?: boolean;\n /** userDataDir,用于持久化 cookies 等 */\n userDataDir?: string;\n /** 是否无头模式 */\n headless?: boolean;\n /** 代理配置 */\n proxy?: string;\n /** 额外的 Chrome 启动参数 */\n args?: string[];\n}\n\n\n/** 启动 Chrome 进程并返回 CDP WebSocket URL */\nasync function launchChromeWithCDP(config: CDPConfig): Promise<{ process: ChildProcess; wsEndpoint: string }> {\n const port = config.port ?? 9222;\n const executablePath = config.executablePath || findChromeExecutable();\n if (!executablePath) {\n throw new Error(\"未找到 Chrome 可执行文件,请设置 CHROME_PATH 环境变量或提供 executablePath\");\n }\n const args: string[] = [\n `--remote-debugging-port=${port}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-blink-features=AutomationControlled\",\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-web-security\",\n \"--disable-features=IsolateOrigins,site-per-process\",\n \"--disable-site-isolation-trials\",\n \"--disable-infobars\",\n ];\n if (config.headless !== false) {\n args.push(\"--headless=new\");\n }\n if (config.userDataDir) {\n args.push(`--user-data-dir=${config.userDataDir}`);\n }\n if (config.proxy) {\n const u = new URL(config.proxy);\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\n args.push(`--proxy-server=${serverUrl}`);\n }\n if (config.args) {\n args.push(...config.args);\n }\n const process = spawn(executablePath, args, {\n stdio: [\"ignore\", \"ignore\", \"ignore\"],\n detached: false,\n });\n // 等待 CDP 端点可用\n const wsEndpoint = `ws://127.0.0.1:${port}/devtools/browser`;\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Chrome 启动超时,无法连接到 CDP 端口 ${port}`));\n }, 30000);\n const checkInterval = setInterval(async () => {\n try {\n const http = await import(\"node:http\");\n const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {\n if (res.statusCode === 200) {\n clearInterval(checkInterval);\n clearTimeout(timeout);\n resolve();\n }\n });\n req.on(\"error\", () => {\n // 继续等待\n });\n req.end();\n } catch {\n // 继续等待\n }\n }, 500);\n process.on(\"error\", (err) => {\n clearInterval(checkInterval);\n clearTimeout(timeout);\n reject(err);\n });\n });\n return { process, wsEndpoint };\n}\n\n\n/** 连接到已启动的 Chrome(通过 CDP 端口) */\nasync function connectToExistingChrome(port: number = 9222): Promise<string> {\n const wsEndpoint = `ws://127.0.0.1:${port}/devtools/browser`;\n try {\n const http = await import(\"node:http\");\n await new Promise<void>((resolve, reject) => {\n const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {\n if (res.statusCode === 200) {\n resolve();\n } else {\n reject(new Error(`无法连接到 Chrome CDP 端口 ${port}`));\n }\n });\n req.on(\"error\", (err) => {\n reject(new Error(`无法连接到 Chrome CDP 端口 ${port},请确保 Chrome 已启动并开启 --remote-debugging-port=${port}: ${err.message}`));\n });\n req.end();\n });\n return wsEndpoint;\n } catch (err) {\n throw err instanceof Error ? err : new Error(`无法连接到 Chrome CDP 端口 ${port},请确保 Chrome 已启动并开启 --remote-debugging-port=${port}`);\n }\n}\n\n\n/** 通过 CDP 连接到 Chrome 并返回 Browser 对象 */\nexport async function connectBrowser(config: CDPConfig): Promise<{ browser: Browser; cleanup?: () => Promise<void> }> {\n let wsEndpoint: string;\n let chromeProcess: ChildProcess | undefined;\n if (config.useExisting) {\n wsEndpoint = await connectToExistingChrome(config.port);\n } else {\n const result = await launchChromeWithCDP(config);\n wsEndpoint = result.wsEndpoint;\n chromeProcess = result.process;\n }\n const connectOptions: ConnectOptions = {\n browserWSEndpoint: wsEndpoint,\n };\n const browser = await puppeteerCore.connect(connectOptions);\n const cleanup = chromeProcess\n ? async () => {\n await browser.disconnect();\n chromeProcess?.kill();\n }\n : async () => {\n await browser.disconnect();\n };\n return { browser, cleanup };\n}\n\n\n/** 通过 CDP 启动 Chrome 并返回 Browser 对象(使用 executablePath) */\nexport async function launchBrowser(config: CDPConfig & { executablePath: string }): Promise<{ browser: Browser; cleanup: () => Promise<void> }> {\n const port = config.port ?? 9222;\n const args: string[] = [\n `--remote-debugging-port=${port}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-blink-features=AutomationControlled\",\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-web-security\",\n \"--disable-features=IsolateOrigins,site-per-process\",\n \"--disable-site-isolation-trials\",\n \"--disable-infobars\",\n ];\n if (config.headless !== false) {\n args.push(\"--headless=new\");\n }\n if (config.userDataDir) {\n args.push(`--user-data-dir=${config.userDataDir}`);\n }\n if (config.proxy) {\n const u = new URL(config.proxy);\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\n args.push(`--proxy-server=${serverUrl}`);\n }\n if (config.args) {\n args.push(...config.args);\n }\n const launchOptions: LaunchOptions = {\n executablePath: config.executablePath,\n headless: config.headless !== false,\n args,\n userDataDir: config.userDataDir,\n ignoreDefaultArgs: [\"--enable-automation\"],\n };\n const browser = await puppeteerCore.launch(launchOptions);\n const cleanup = async () => {\n await browser.close();\n };\n return { browser, cleanup };\n}\n\n\n/** 导出类型供外部使用 */\nexport type { Browser, Page } from \"puppeteer-core\";\n","/**\r\n * 系统内部统一的 Feed Item 定义\r\n * 插件 → Normalizer → RSS Generator\r\n * 自包含:携带 sourceRef 后,入库 / Signal 投递等无需再单独传 ref。\r\n */\r\n\r\n/** 单语种译文字段(key 为 BCP 47,如 zh-CN、en) */\r\nexport interface ItemTranslationFields {\r\n title?: string;\r\n summary?: string;\r\n content?: string;\r\n}\r\n\r\n/** 带可选 translations 的条目视图(FeedItem 或 DB 行 + translations 等) */\r\nexport interface ItemWithOptionalTranslations {\r\n title: string;\r\n summary?: string;\r\n content?: string;\r\n translations?: Record<string, ItemTranslationFields>;\r\n}\r\n\r\n/**\r\n * 根据 lng 取条目的「有效」标题/摘要/正文:有 translations[lng] 则优先用译文,否则用原文。\r\n * 路由层传 lng 时用此结果生成 RSS 或 API 响应。\r\n */\r\nexport function getEffectiveItemFields(\r\n item: ItemWithOptionalTranslations,\r\n lng?: string | null,\r\n): { title: string; summary: string; content: string } {\r\n const raw = lng && lng !== \"\" ? item.translations?.[lng] : undefined;\r\n const t = raw && typeof raw === \"object\" ? raw : undefined;\r\n return {\r\n title: (t?.title != null && t.title !== \"\" ? t.title : item.title) ?? \"\",\r\n summary: (t?.summary != null && t.summary !== \"\" ? t.summary : item.summary) ?? \"\",\r\n content: (t?.content != null && t.content !== \"\" ? t.content : item.content) ?? \"\",\r\n };\r\n}\r\n\r\n/** 将发布时间转为 ISO 字符串供入库/序列化;Invalid Date 返回 null(避免 toISOString 抛 RangeError) */\r\nexport function pubDateToIsoOrNull(pubDate: unknown): string | null {\r\n if (pubDate == null) return null;\r\n if (pubDate instanceof Date) {\r\n const ms = pubDate.getTime();\r\n return Number.isNaN(ms) ? null : pubDate.toISOString();\r\n }\r\n if (typeof pubDate === \"string\") {\r\n const s = pubDate.trim();\r\n return s || null;\r\n }\r\n return null;\r\n}\r\n\r\n/** 将 author 规范为 string[],兼容 string 输入(插件等) */\r\nexport function normalizeAuthor(author: string | string[] | null | undefined): string[] | undefined {\r\n if (author == null) return undefined;\r\n if (Array.isArray(author)) return author.filter((s) => typeof s === \"string\" && s.trim()).map((s) => s.trim());\r\n const s = String(author).trim();\r\n return s ? [s] : undefined;\r\n}\r\n\r\n/** 将 author 转为显示用字符串(逗号分隔) */\r\nexport function authorToDisplay(author: string | string[] | null | undefined): string {\r\n const arr = normalizeAuthor(author);\r\n return arr?.join(\", \") ?? \"\";\r\n}\r\n\r\nexport interface FeedItem {\r\n /** 全局唯一标识,link 或稳定 hash */\r\n guid: string;\r\n /** 标题 */\r\n title: string;\r\n /** 原文链接 */\r\n link: string;\r\n /** 发布时间 */\r\n pubDate: Date;\r\n /** 作者列表 */\r\n author?: string[];\r\n /** 简要描述(纯文本,适合 RSS description) */\r\n summary?: string;\r\n /** 详情正文(输出到 RSS description) */\r\n content?: string;\r\n /** 条目配图 URL,输出为 RSS 2.0 <enclosure type=\"image/...\"> */\r\n imageUrl?: string;\r\n /**\r\n * 封面图:支持 http(s) URL、data URL,或裸 base64(可规范为 data:image/jpeg;base64,...)。\r\n * Gateway JSON 字段名 `cover_img`。\r\n */\r\n cover_img?: string;\r\n /** RSS 来源分类(直接来自 feed 的 <category> 字段) */\r\n categories?: string[];\r\n /** 系统 / pipeline 生成的标签(从用户管理的标签库中匹配) */\r\n tags?: string[];\r\n /** 信源标识(列表页 URL 或 imap 等),入库与按 ref 筛选用;设后则 upsertItems 等无需再传 ref */\r\n sourceRef?: string;\r\n /**\r\n * 多语种译文。key 为 BCP 47(如 zh-CN、en),路由支持 lng 参数时可据此返回对应译文。\r\n * 由 pipeline(如 translator)写入。\r\n */\r\n translations?: Record<string, ItemTranslationFields>;\r\n /**\r\n * 扩展字段,给插件留后门。\r\n * 框架保留键:`_rssanyPipelineDrop` 为 true 表示 pipeline 质量过滤丢弃,feeder 会删库并移出 RSS。\r\n */\r\n extra?: Record<string, unknown>;\r\n }\r\n\r\n/** Pipeline 质量过滤等步骤标记的丢弃键(写入 item.extra) */\r\nexport const PIPELINE_DROP_EXTRA_KEY = \"_rssanyPipelineDrop\";\r\n\r\nexport function markPipelineDrop(item: FeedItem): FeedItem {\r\n item.extra = { ...item.extra, [PIPELINE_DROP_EXTRA_KEY]: true };\r\n return item;\r\n}\r\n\r\nexport function isPipelineDroppedItem(item: FeedItem): boolean {\r\n return item.extra?.[PIPELINE_DROP_EXTRA_KEY] === true;\r\n}","/**\r\n * 信源 ref 入库与查询统一键(规范化存储):\r\n * - http(s):scheme、host(含端口)小写;pathname 小写并去掉非根路径末尾 `/`;query、hash 保持原样(避免破坏带大小写的查询参数)。\r\n * - 非 http(s):trim 后全串小写。\r\n */\r\nexport function canonicalHttpSourceRef(ref: string): string {\r\n const t = ref.trim();\r\n if (!t) return t;\r\n if (!/^https?:\\/\\//i.test(t)) return t.toLowerCase();\r\n try {\r\n const u = new URL(t);\r\n const protocol = u.protocol.toLowerCase();\r\n const host = u.host.toLowerCase();\r\n let path = u.pathname;\r\n if (path.length > 1 && path.endsWith(\"/\")) {\r\n path = path.slice(0, -1);\r\n }\r\n path = path.toLowerCase();\r\n return `${protocol}//${host}${path}${u.search}${u.hash}`;\r\n } catch {\r\n return t.toLowerCase();\r\n }\r\n}\r\n\r\nfunction maxIso(a: string | null, b: string | null): string | null {\r\n if (!a) return b;\r\n if (!b) return a;\r\n return a >= b ? a : b;\r\n}\r\n\r\n/**\r\n * 将 GROUP BY source_url 的统计按规范化键合并(兼容迁移前旧数据或异常重复写法)。\r\n */\r\nexport function mergeSourceStatsRows(\r\n rows: { source_url: string; count: number; latest_at: string | null }[],\r\n): { source_url: string; count: number; latest_at: string | null }[] {\r\n const map = new Map<string, { count: number; latest_at: string | null }>();\r\n for (const row of rows) {\r\n const k = canonicalHttpSourceRef(row.source_url);\r\n const prev = map.get(k);\r\n if (!prev) {\r\n map.set(k, { count: row.count, latest_at: row.latest_at });\r\n } else {\r\n map.set(k, {\r\n count: prev.count + row.count,\r\n latest_at: maxIso(prev.latest_at, row.latest_at),\r\n });\r\n }\r\n }\r\n return [...map.entries()]\r\n .map(([source_url, v]) => ({ source_url, count: v.count, latest_at: v.latest_at }))\r\n .sort((a, b) => b.count - a.count);\r\n}\r\n","// 解析 npm 包根或仓库根(含 app/plugins/builtin/、statics/、webui/build/);开发时来自 app/,打包后为 dist/ 的上一级\r\n\r\nimport { basename, dirname, join } from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\nconst __dir = dirname(fileURLToPath(import.meta.url));\r\nconst base = basename(__dir);\r\n\r\nexport const PACKAGE_ROOT =\r\n base === \"app\" || base === \"dist\" ? join(__dir, \"..\") : __dir;\r\n","// 路径配置:集中管理所有运行时路径,区分项目文件与用户数据\r\n\r\nimport { mkdir, rename, access, copyFile } from \"node:fs/promises\";\r\nimport { homedir } from \"node:os\";\r\nimport { join } from \"node:path\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\nconst envUserDir = process.env.RSSANY_USER_DIR?.trim();\r\n\r\n/** 用户数据根目录:~/.rssany/(或 RSSANY_USER_DIR);不纳入版本管理 */\r\nexport const USER_DIR = envUserDir && envUserDir.length > 0 ? envUserDir : join(homedir(), \".rssany\");\r\n\r\n/** SQLite 数据库目录:.rssany/data/ */\r\nexport const DATA_DIR = join(USER_DIR, \"data\");\r\n\r\n/** 缓存目录:.rssany/cache/(fetched、extracted、feeds、domains、browser_data 等子目录);环境变量 CACHE_DIR 可覆盖 */\r\nexport const CACHE_DIR = process.env.CACHE_DIR ?? join(USER_DIR, \"cache\");\r\n\r\n/** 站点配置文件:.rssany/sites.json */\r\nexport const SITES_CONFIG_PATH = join(USER_DIR, \"sites.json\");\r\n\r\n/** 爬虫配置:.rssany/sources.json(扁平信源列表,供 scheduler 使用) */\r\nexport const SOURCES_CONFIG_PATH = join(USER_DIR, \"sources.json\");\r\n\r\n/** 系统标签配置:.rssany/tags.json(供 pipeline tagger 使用) */\r\nexport const TAGS_CONFIG_PATH = join(USER_DIR, \"tags.json\");\r\n\r\n/** 全局配置:.rssany/config.json(pipeline 等) */\r\nexport const CONFIG_PATH = join(USER_DIR, \"config.json\");\r\n\r\n/** @deprecated 仅用于迁移:若存在 .rssany/subscriptions.json 且无 sources.json 则迁移为 sources.json */\r\nconst LEGACY_SUBSCRIPTIONS_PATH = join(USER_DIR, \"subscriptions.json\");\r\n\r\n/** 内置信源插件目录:app/plugins/builtin/(随包发布 *.rssany.js) */\r\nexport const BUILTIN_PLUGINS_DIR = join(PACKAGE_ROOT, \"app/plugins/builtin\");\r\n\r\n/** 用户插件目录:.rssany/plugins/(扁平 *.rssany.js / *.rssany.ts) */\r\nexport const USER_PLUGINS_DIR = join(USER_DIR, \"plugins\");\r\n\r\n/** 管理页「添加插件」所用模板(非 Site,不参与加载) */\r\nexport const PLUGIN_SITE_TEMPLATE_PATH = join(PACKAGE_ROOT, \"app/plugins/site.rssany.js\");\r\n\r\nasync function pathExists(p: string): Promise<boolean> {\r\n try {\r\n await access(p);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nasync function migrateFile(from: string, to: string): Promise<void> {\r\n if (!(await pathExists(from))) return;\r\n if (await pathExists(to)) return;\r\n try {\r\n await rename(from, to);\r\n logger.info(\"config\", \"配置已迁移\", { from, to });\r\n } catch (err) {\r\n logger.warn(\"config\", \"配置迁移失败\", { from, to, err: err instanceof Error ? err.message : String(err) });\r\n }\r\n}\r\n\r\n/** 包内首次初始化用的默认数据(`init/`;发布包需列入 package.json `files`) */\r\nconst INIT_DATA_DIR = join(PACKAGE_ROOT, \"init\");\r\nconst EXAMPLE_SOURCES = join(INIT_DATA_DIR, \"sources.json\");\r\nconst EXAMPLE_CONFIG = join(INIT_DATA_DIR, \"config.json\");\r\n\r\n/**\r\n * 若用户目录尚无 `sources.json` / `config.json`,则从包内 `init/sources.json`、`init/config.json` 复制(不覆盖已有文件)。\r\n */\r\nasync function seedExampleConfigsIfMissing(): Promise<void> {\r\n if (!(await pathExists(SOURCES_CONFIG_PATH)) && (await pathExists(EXAMPLE_SOURCES))) {\r\n try {\r\n await copyFile(EXAMPLE_SOURCES, SOURCES_CONFIG_PATH);\r\n logger.info(\"config\", \"已写入默认信源示例\", { path: SOURCES_CONFIG_PATH });\r\n } catch (err) {\r\n logger.warn(\"config\", \"写入 sources 示例失败\", {\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n if (!(await pathExists(CONFIG_PATH)) && (await pathExists(EXAMPLE_CONFIG))) {\r\n try {\r\n await copyFile(EXAMPLE_CONFIG, CONFIG_PATH);\r\n logger.info(\"config\", \"已写入默认配置示例\", { path: CONFIG_PATH });\r\n } catch (err) {\r\n logger.warn(\"config\", \"写入 config 示例失败\", {\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n}\r\n\r\n/** 初始化用户数据目录;若缺少 sources.json / config.json 则从包内示例复制 */\r\nexport async function initUserDir(): Promise<void> {\r\n await mkdir(USER_DIR, { recursive: true });\r\n await mkdir(DATA_DIR, { recursive: true });\r\n await mkdir(CACHE_DIR, { recursive: true });\r\n await mkdir(USER_PLUGINS_DIR, { recursive: true });\r\n await seedExampleConfigsIfMissing();\r\n if (!(await pathExists(SOURCES_CONFIG_PATH)) && (await pathExists(LEGACY_SUBSCRIPTIONS_PATH))) {\r\n await migrateFile(LEGACY_SUBSCRIPTIONS_PATH, SOURCES_CONFIG_PATH);\r\n }\r\n}\r\n","// SQLite 主库:建表 schema、FTS、FeedItem CRUD 与运行日志库\r\n\r\nimport Database from \"better-sqlite3\";\r\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { existsSync, openSync, closeSync, writeSync, unlinkSync, readFileSync } from \"node:fs\";\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { normalizeAuthor, pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport { canonicalHttpSourceRef } from \"../utils/httpSourceRef.js\";\r\nimport type { LogEntry } from \"../core/logger/types.js\";\r\nimport { DATA_DIR, TAGS_CONFIG_PATH } from \"../config/paths.js\";\r\n\r\n/** 主库日志模式:默认 WAL;环境变量 RSSANY_DB_JOURNAL=delete 时使用 DELETE(利于 Windows 下多进程/拷贝场景) */\r\nconst MAIN_DB_JOURNAL = (process.env.RSSANY_DB_JOURNAL ?? \"wal\").toLowerCase() === \"delete\" ? \"DELETE\" : \"WAL\";\r\n\r\nlet _db: Database.Database | null = null;\r\n\r\n/** 懒初始化:并发调用 getDb() 时共用一个 Promise,避免重复 new Database() + initSchema;失败时清空以便重试。损坏时可能抛出 SQLITE_CORRUPT_VTAB / database disk image is malformed */\r\nlet _dbInit: Promise<Database.Database> | null = null;\r\n\r\n/** 单写者锁:串行化 updateItemContent/upsertItems 等写库,避免 WAL 下并发 transient 错误 */\r\nlet _writeLock: Promise<void> = Promise.resolve();\r\n\r\n/** 单进程锁文件路径:与 rssany.db 同目录,内容为 PID */\r\nconst MAIN_DB_LOCK_PATH = join(DATA_DIR, \"rssany.db.lock\");\r\n\r\n/** 将损坏类错误写到 stderr,便于排查多进程/tsx watch 等导致的 db 问题 */\r\nfunction logCorruptDiagnostic(operation: string, err: unknown): void {\r\n const code = (err as { code?: string })?.code;\r\n const msg = err instanceof Error ? err.message : String(err);\r\n const lines = [\r\n \"[rssany db] 数据库可能损坏或并发冲突\",\r\n ` 操作: ${operation}`,\r\n ` 错误: ${code ?? \"unknown\"} - ${msg}`,\r\n \" 常见原因:\",\r\n \" 1. 多进程同时打开同一库(例如 tsx --watch 与另一实例同时写)\",\r\n \" 2. 异常退出后 WAL 未正常 checkpoint\",\r\n \" 3. 磁盘/杀毒/同步盘导致文件不完整\",\r\n \" 建议:\",\r\n \" - 避免多实例同时写库;开发时慎用 --watch 与后台任务并行\",\r\n \" - 可尝试 RSSANY_DB_JOURNAL=delete 使用 DELETE 模式降低多文件依赖\",\r\n \" - 备份后删除 .rssany/data/rssany.db 及同目录 -wal、-shm、rssany.db.lock 再启动\",\r\n ];\r\n process.stderr.write(lines.join(\"\\n\") + \"\\n\");\r\n}\r\n\r\n/** 独占创建锁文件(wx);若已存在则读 PID,旧进程已死则删除后重试 */\r\nfunction acquireDbLock(dbDir: string): void {\r\n const lockPath = join(dbDir, \"rssany.db.lock\");\r\n const pid = process.pid;\r\n const tryCreate = (): void => {\r\n try {\r\n const fd = openSync(lockPath, \"wx\");\r\n writeSync(fd, String(pid), 0, \"utf8\");\r\n closeSync(fd);\r\n return;\r\n } catch (e: unknown) {\r\n const code = (e as NodeJS.ErrnoException)?.code;\r\n if (code !== \"EEXIST\") throw e;\r\n }\r\n if (!existsSync(lockPath)) {\r\n tryCreate();\r\n return;\r\n }\r\n let oldPid: number | null = null;\r\n try {\r\n const buf = readFileSync(lockPath, \"utf8\");\r\n const n = parseInt(buf.trim(), 10);\r\n if (!Number.isNaN(n)) oldPid = n;\r\n } catch {\r\n /* 读锁文件失败则重试创建 */\r\n }\r\n if (oldPid !== null && oldPid !== pid) {\r\n const stillRunning = ((): boolean => {\r\n try {\r\n process.kill(oldPid!, 0);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n })();\r\n if (stillRunning) {\r\n throw new Error(\r\n `数据库已被其他进程占用(PID ${oldPid})。请勿多开实例;若确认无其他进程,可删除 ${lockPath} 后重试(常见于 tsx --watch 未退出)`,\r\n );\r\n }\r\n }\r\n try {\r\n unlinkSync(lockPath);\r\n } catch {\r\n /* ignore */\r\n }\r\n tryCreate();\r\n };\r\n tryCreate();\r\n}\r\n\r\n/** 进程退出时尝试删除锁文件(尽力而为) */\r\nfunction releaseDbLock(): void {\r\n if (!existsSync(MAIN_DB_LOCK_PATH)) return;\r\n try {\r\n unlinkSync(MAIN_DB_LOCK_PATH);\r\n } catch {\r\n /* ignore */\r\n }\r\n}\r\n\r\nexport function withWriteLock<T>(fn: () => Promise<T>): Promise<T> {\r\n const prev = _writeLock;\r\n let resolveOut!: (v: T) => void;\r\n let rejectOut!: (e: unknown) => void;\r\n const out = new Promise<T>((res, rej) => {\r\n resolveOut = res;\r\n rejectOut = rej;\r\n });\r\n _writeLock = prev\r\n .then(() => fn())\r\n .then(\r\n (v) => {\r\n resolveOut(v);\r\n },\r\n (e: unknown) => {\r\n if (isCorruptError(e)) {\r\n logCorruptDiagnostic(\"withWriteLock 内 updateItemContent/upsertItems 等\", e);\r\n }\r\n rejectOut(e);\r\n throw e;\r\n },\r\n );\r\n return out;\r\n}\r\n\r\n/** 仅英文「日期当标题」的粗判(如 Jan 15, 2024),用于去重修复时跳过 */\r\nconst DATE_ONLY_TITLE_RE =\r\n /^(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\\b[\\s\\d,./-]*(?:st|nd|rd|th)?[\\s\\d,./-]*$/i;\r\n\r\nfunction normalizeText(text: string | null | undefined): string {\r\n return (text ?? \"\").replace(/\\s+/g, \" \").trim();\r\n}\r\n\r\nfunction isDateOnlyTitle(title: string | null | undefined): boolean {\r\n const normalized = normalizeText(title);\r\n if (!normalized) return false;\r\n return DATE_ONLY_TITLE_RE.test(normalized);\r\n}\r\n\r\nfunction toMs(input: string | null | undefined): number | null {\r\n if (!input) return null;\r\n const ms = Date.parse(input);\r\n return Number.isNaN(ms) ? null : ms;\r\n}\r\n\r\n/** 将 DB 中 author 列(JSON 字符串或单字符串)解析为 string[] */\r\nexport function parseAuthorFromDb(raw: string | null | undefined): string[] | undefined {\r\n if (!raw?.trim()) return undefined;\r\n try {\r\n const p = JSON.parse(raw) as unknown;\r\n if (Array.isArray(p)) return p.filter((s) => typeof s === \"string\").map((s) => String(s).trim()).filter(Boolean);\r\n return [String(p).trim()];\r\n } catch {\r\n return [raw.trim()];\r\n }\r\n}\r\n\r\n/** 将原始行转为 DbItem,并解析 author / tags / translations 等 JSON 字段 */\r\nfunction toDbItem(row: Record<string, unknown>): DbItem {\r\n const author = parseAuthorFromDb(row.author as string) ?? null;\r\n const parseJsonArr = (v: unknown): string[] | null => {\r\n try {\r\n return v ? (JSON.parse(v as string) as string[]) : null;\r\n } catch {\r\n return null;\r\n }\r\n };\r\n const tags = parseJsonArr(row.tags);\r\n let translations: Record<string, { title?: string; summary?: string; content?: string }> | null = null;\r\n try {\r\n if (row.translations && typeof row.translations === \"string\") {\r\n const p = JSON.parse(row.translations) as unknown;\r\n if (p && typeof p === \"object\") translations = p as Record<string, { title?: string; summary?: string; content?: string }>;\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n return { ...row, author, tags, translations } as DbItem;\r\n}\r\n\r\nfunction mapRowsToDbItems(rows: Record<string, unknown>[]): DbItem[] {\r\n return rows.map(toDbItem);\r\n}\r\n\r\n/** 判断是否 SQLite 库损坏或 FTS 虚拟表损坏 */\r\nfunction isCorruptError(err: unknown): boolean {\r\n const code = (err as { code?: string })?.code;\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return (\r\n code === \"SQLITE_CORRUPT\" ||\r\n code === \"SQLITE_CORRUPT_VTAB\" ||\r\n msg.includes(\"database disk image is malformed\")\r\n );\r\n}\r\n\r\n/** 获取主库连接,路径:.rssany/data/rssany.db */\r\nexport async function getDb(): Promise<Database.Database> {\r\n if (_db) return _db;\r\n if (_dbInit) return _dbInit;\r\n const dbPath = join(DATA_DIR, \"rssany.db\");\r\n _dbInit = (async (): Promise<Database.Database> => {\r\n await mkdir(DATA_DIR, { recursive: true });\r\n acquireDbLock(DATA_DIR);\r\n let db: Database.Database | null = null;\r\n try {\r\n db = new Database(dbPath);\r\n db.pragma(`journal_mode = ${MAIN_DB_JOURNAL}`);\r\n db.pragma(\"synchronous = NORMAL\");\r\n initSchema(db);\r\n _db = db;\r\n return db;\r\n } catch (err: unknown) {\r\n _dbInit = null;\r\n releaseDbLock();\r\n if (db) {\r\n try {\r\n db.close();\r\n } catch {\r\n /* ignore */\r\n }\r\n db = null;\r\n }\r\n if (isCorruptError(err)) {\r\n logCorruptDiagnostic(\"打开/初始化主库 (getDb)\", err);\r\n }\r\n throw err;\r\n }\r\n })();\r\n return _dbInit;\r\n}\r\n\r\n/** 执行 PRAGMA integrity_check;正常为单字 ok;否则为错误描述。亦可能因库损坏在 prepare 阶段抛错,由调用方 catch */\r\nexport async function runIntegrityCheck(): Promise<string> {\r\n const db = await getDb();\r\n try {\r\n const row = db.prepare(\"PRAGMA integrity_check\").get() as { integrity_check: string } | undefined;\r\n return row?.integrity_check ?? \"unknown\";\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return `integrity_check 执行失败: ${msg}`;\r\n }\r\n}\r\n\r\n/** 运行日志库路径:.rssany/data/logs.db(与主库分离,避免主库 WAL 与日志写放大相互影响) */\r\nconst LOGS_DB_PATH = join(DATA_DIR, \"logs.db\");\r\n\r\nlet _logsDb: Database.Database | null = null;\r\nlet _logsDbInit: Promise<Database.Database> | null = null;\r\n\r\nfunction initLogsSchema(db: Database.Database): void {\r\n db.exec(`\r\n CREATE TABLE IF NOT EXISTS logs (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n level TEXT NOT NULL,\r\n category TEXT NOT NULL,\r\n message TEXT NOT NULL,\r\n payload TEXT,\r\n source_url TEXT,\r\n created_at TEXT NOT NULL\r\n );\r\n CREATE INDEX IF NOT EXISTS idx_logs_level_created ON logs(level, created_at);\r\n CREATE INDEX IF NOT EXISTS idx_logs_source_created ON logs(source_url, created_at);\r\n `);\r\n}\r\n\r\n/** 获取运行日志库(独立 logs.db,journal 使用 WAL) */\r\nexport async function getLogsDb(): Promise<Database.Database> {\r\n if (_logsDb) return _logsDb;\r\n if (_logsDbInit) return _logsDbInit;\r\n _logsDbInit = (async (): Promise<Database.Database> => {\r\n await mkdir(DATA_DIR, { recursive: true });\r\n const db = new Database(LOGS_DB_PATH);\r\n db.pragma(\"journal_mode = WAL\");\r\n db.pragma(\"synchronous = NORMAL\");\r\n initLogsSchema(db);\r\n _logsDb = db;\r\n return db;\r\n })();\r\n return _logsDbInit;\r\n}\r\n\r\n/**\r\n * 主表 items + FTS5 全文索引;视图 items_fts_src 抽出 zh-CN 译文字段参与检索。\r\n * 主库文件旁可能产生 -wal/-shm;日志写入独立 logs.db。\r\n */\r\nfunction initSchema(db: Database.Database): void {\r\n db.exec(`\r\n CREATE TABLE IF NOT EXISTS items (\r\n id TEXT PRIMARY KEY,\r\n url TEXT UNIQUE NOT NULL,\r\n source_url TEXT NOT NULL,\r\n title TEXT,\r\n author TEXT,\r\n summary TEXT,\r\n content TEXT,\r\n image_url TEXT,\r\n tags TEXT,\r\n translations TEXT,\r\n pub_date TEXT,\r\n fetched_at TEXT NOT NULL,\r\n pushed_at TEXT\r\n );\r\n CREATE INDEX IF NOT EXISTS idx_items_source ON items(source_url);\r\n CREATE INDEX IF NOT EXISTS idx_items_fetched ON items(fetched_at);\r\n CREATE INDEX IF NOT EXISTS idx_items_pushed ON items(pushed_at);\r\n `);\r\n\r\n db.exec(`\r\n CREATE VIEW IF NOT EXISTS items_fts_src AS\r\n SELECT rowid, title, summary, content,\r\n json_extract(translations, '$.\"zh-CN\".title') AS title_zh,\r\n json_extract(translations, '$.\"zh-CN\".summary') AS summary_zh,\r\n json_extract(translations, '$.\"zh-CN\".content') AS content_zh\r\n FROM items;\r\n CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING fts5(\r\n title, summary, content, title_zh, summary_zh, content_zh,\r\n content='items_fts_src',\r\n content_rowid='rowid'\r\n );\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_insert AFTER INSERT ON items\r\n BEGIN\r\n INSERT INTO items_fts(rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n NEW.rowid, NEW.title, NEW.summary, NEW.content,\r\n json_extract(NEW.translations, '$.\"zh-CN\".title'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".summary'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_update AFTER UPDATE ON items\r\n BEGIN\r\n INSERT INTO items_fts(items_fts, rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n 'delete', OLD.rowid, OLD.title, OLD.summary, OLD.content,\r\n json_extract(OLD.translations, '$.\"zh-CN\".title'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".summary'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".content')\r\n );\r\n INSERT INTO items_fts(rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n NEW.rowid, NEW.title, NEW.summary, NEW.content,\r\n json_extract(NEW.translations, '$.\"zh-CN\".title'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".summary'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_delete AFTER DELETE ON items\r\n BEGIN\r\n INSERT INTO items_fts(items_fts, rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n 'delete', OLD.rowid, OLD.title, OLD.summary, OLD.content,\r\n json_extract(OLD.translations, '$.\"zh-CN\".title'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".summary'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n `);\r\n\r\n // 旧库迁移:若无 image_url 列则追加\r\n try {\r\n const info = db.prepare(\"PRAGMA table_info(items)\").all() as { name: string }[];\r\n if (info && !info.some((c) => c.name === \"image_url\")) {\r\n db.exec(\"ALTER TABLE items ADD COLUMN image_url TEXT\");\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n\r\n migrateItemsSourceUrlIfNeeded(db);\r\n}\r\n\r\n/** 将 items.source_url 一次性规范化为 canonicalHttpSourceRef(user_version 2:大小写 + 去尾 slash 等) */\r\nfunction migrateItemsSourceUrlIfNeeded(db: Database.Database): void {\r\n const v = db.pragma(\"user_version\", { simple: true }) as number;\r\n if (v >= 2) return;\r\n const rows = db.prepare(\"SELECT rowid, source_url FROM items\").all() as { rowid: number; source_url: string }[];\r\n const upd = db.prepare(\"UPDATE items SET source_url = @next WHERE rowid = @rowid\");\r\n const run = db.transaction(() => {\r\n for (const r of rows) {\r\n const next = canonicalHttpSourceRef(r.source_url);\r\n if (next !== r.source_url) {\r\n upd.run({ next, rowid: r.rowid });\r\n }\r\n }\r\n db.pragma(\"user_version = 2\");\r\n });\r\n run();\r\n}\r\n\r\n/**\r\n * 批量插入或忽略重复(按 id);新行计入 newIds。\r\n * source_url 取自首条 item.sourceRef 或 sourceUrlOverride,写入前经 canonicalHttpSourceRef 规范化。\r\n */\r\nexport async function upsertItems(items: FeedItem[], sourceUrlOverride?: string): Promise<{ newCount: number; newIds: Set<string> }> {\r\n if (items.length === 0) return { newCount: 0, newIds: new Set() };\r\n const raw = (sourceUrlOverride ?? items[0].sourceRef)?.trim();\r\n if (!raw) {\r\n throw new Error(\"upsertItems: 每条 item 须有 sourceRef,或传入 sourceUrlOverride\");\r\n }\r\n const sourceUrl = canonicalHttpSourceRef(raw);\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const stmt = db.prepare(`\r\n INSERT OR IGNORE INTO items (id, url, source_url, title, author, summary, image_url, tags, pub_date, fetched_at)\r\n VALUES (@id, @url, @sourceUrl, @title, @author, @summary, @imageUrl, @tags, @pubDate, @fetchedAt)\r\n `);\r\n const selectExistingStmt = db.prepare(`\r\n SELECT id, title, author, summary, image_url, pub_date, fetched_at\r\n FROM items\r\n WHERE id = @id\r\n `);\r\n const repairExistingStmt = db.prepare(`\r\n UPDATE items\r\n SET title = @title,\r\n author = @author,\r\n summary = @summary,\r\n image_url = @imageUrl,\r\n pub_date = @pubDate,\r\n fetched_at = @fetchedAt\r\n WHERE id = @id\r\n `);\r\n const now = new Date().toISOString();\r\n let newCount = 0;\r\n const newIds = new Set<string>();\r\n const run = db.transaction((rows: FeedItem[]) => {\r\n for (const item of rows) {\r\n const nextTitle = normalizeText(item.title) || null;\r\n const nextSummary = normalizeText(item.summary) || null;\r\n const nextAuthorArr = normalizeAuthor(item.author);\r\n const nextAuthor = nextAuthorArr?.length ? JSON.stringify(nextAuthorArr) : null;\r\n const nextPubDate = pubDateToIsoOrNull(item.pubDate);\r\n const nextTags = item.tags?.length ? JSON.stringify(item.tags) : null;\r\n const nextImageUrl = typeof item.imageUrl === \"string\" && item.imageUrl.trim() ? item.imageUrl.trim() : null;\r\n const info = stmt.run({\r\n id: item.guid,\r\n url: item.link,\r\n sourceUrl,\r\n title: nextTitle,\r\n author: nextAuthor,\r\n summary: nextSummary,\r\n imageUrl: nextImageUrl,\r\n tags: nextTags,\r\n pubDate: nextPubDate,\r\n fetchedAt: now,\r\n });\r\n newCount += info.changes;\r\n if (info.changes > 0) newIds.add(item.guid);\r\n\r\n if (info.changes > 0) continue;\r\n const existing = selectExistingStmt.get({ id: item.guid }) as\r\n | {\r\n title: string | null;\r\n author: string | null;\r\n summary: string | null;\r\n image_url: string | null;\r\n pub_date: string | null;\r\n fetched_at: string | null;\r\n }\r\n | undefined;\r\n if (!existing) continue;\r\n\r\n const shouldRepairTitle =\r\n !!nextTitle &&\r\n !isDateOnlyTitle(nextTitle) &&\r\n (isDateOnlyTitle(existing.title) || !normalizeText(existing.title));\r\n const shouldRepairSummary =\r\n !!nextSummary && normalizeText(existing.summary).length < nextSummary.length;\r\n const shouldRepairImageUrl = !!nextImageUrl && !existing.image_url?.trim();\r\n const existingAuthorArr = parseAuthorFromDb(existing.author);\r\n const shouldRepairAuthor = !!nextAuthorArr?.length && !existingAuthorArr?.length;\r\n\r\n const existingPubDateMs = toMs(existing.pub_date);\r\n const existingFetchedAtMs = toMs(existing.fetched_at);\r\n const nextPubDateMs = toMs(nextPubDate);\r\n const existingPubDateLooksFallback =\r\n existingPubDateMs != null &&\r\n existingFetchedAtMs != null &&\r\n Math.abs(existingPubDateMs - existingFetchedAtMs) <= 5 * 60 * 1000;\r\n const shouldRepairPubDate =\r\n nextPubDateMs != null &&\r\n (existingPubDateMs == null ||\r\n (existingPubDateLooksFallback && nextPubDateMs < existingPubDateMs - 24 * 60 * 60 * 1000));\r\n\r\n if (!(shouldRepairTitle || shouldRepairSummary || shouldRepairImageUrl || shouldRepairAuthor || shouldRepairPubDate)) {\r\n continue;\r\n }\r\n\r\n repairExistingStmt.run({\r\n id: item.guid,\r\n title: shouldRepairTitle ? nextTitle : existing.title,\r\n author: shouldRepairAuthor ? nextAuthor : (existing.author ?? null),\r\n summary: shouldRepairSummary ? nextSummary : existing.summary,\r\n imageUrl: shouldRepairImageUrl ? nextImageUrl : (existing.image_url ?? null),\r\n pubDate: shouldRepairPubDate ? nextPubDate : existing.pub_date,\r\n fetchedAt: now,\r\n });\r\n }\r\n });\r\n run(items);\r\n return { newCount, newIds };\r\n });\r\n}\r\n\r\n/** 查询已存在的 guid 集合(用于 pipeline 等判断「是否新行」) */\r\nexport async function getExistingIds(guids: string[]): Promise<Set<string>> {\r\n if (guids.length === 0) return new Set();\r\n const db = await getDb();\r\n const placeholders = guids.map(() => \"?\").join(\",\");\r\n const rows = db.prepare(`SELECT id FROM items WHERE id IN (${placeholders})`).all(...guids) as { id: string }[];\r\n return new Set(rows.map((r) => r.id));\r\n}\r\n\r\n/**\r\n * 按 guid 更新正文等字段;不覆盖已有非空 content(COALESCE),\r\n * image_url/author/pub_date 等同理;tags/translations 整列替换。\r\n */\r\nexport async function updateItemContent(item: FeedItem): Promise<void> {\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n db.prepare(`\r\n UPDATE items\r\n SET content = COALESCE(content, @content),\r\n image_url = COALESCE(@imageUrl, image_url),\r\n author = COALESCE(@author, author),\r\n pub_date = COALESCE(@pubDate, pub_date),\r\n tags = @tags,\r\n translations = COALESCE(@translations, translations)\r\n WHERE id = @id\r\n `).run({\r\n id: item.guid,\r\n content: item.content ?? null,\r\n imageUrl: typeof item.imageUrl === \"string\" && item.imageUrl.trim() ? item.imageUrl.trim() : null,\r\n author: (() => {\r\n const arr = normalizeAuthor(item.author);\r\n return arr?.length ? JSON.stringify(arr) : null;\r\n })(),\r\n pubDate: pubDateToIsoOrNull(item.pubDate),\r\n tags: item.tags?.length ? JSON.stringify(item.tags) : null,\r\n translations: item.translations && Object.keys(item.translations).length > 0 ? JSON.stringify(item.translations) : null,\r\n });\r\n });\r\n}\r\n\r\n/** 按信源 URL 列表分页拉取;可选 since/until(YYYY-MM-DD 或 ISO 字符串) */\r\nexport async function queryFeedItems(\r\n sourceUrls: string[],\r\n limit: number,\r\n offset: number,\r\n opts?: { since?: string; until?: string },\r\n): Promise<{ items: DbItem[]; hasMore: boolean }> {\r\n if (sourceUrls.length === 0) return { items: [], hasMore: false };\r\n const expanded = [...new Set(sourceUrls.map((u) => canonicalHttpSourceRef(u)).filter(Boolean))];\r\n if (expanded.length === 0) return { items: [], hasMore: false };\r\n const db = await getDb();\r\n const placeholders = expanded.map((_, i) => `@u${i}`).join(\", \");\r\n const conditions: string[] = [`source_url IN (${placeholders})`];\r\n const params: Record<string, unknown> = { lim: limit + 1, off: offset };\r\n expanded.forEach((url, i) => {\r\n params[`u${i}`] = url;\r\n });\r\n if (opts?.since) {\r\n conditions.push(\"COALESCE(pub_date, fetched_at) >= @since\");\r\n params.since = opts.since.length === 10 ? `${opts.since}T00:00:00.000Z` : opts.since;\r\n }\r\n if (opts?.until) {\r\n conditions.push(\"COALESCE(pub_date, fetched_at) < @until\");\r\n if (opts.until.length === 10) {\r\n const d = new Date(`${opts.until}T12:00:00Z`);\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n params.until = d.toISOString();\r\n } else {\r\n params.until = opts.until;\r\n }\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n ${where}\r\n ORDER BY COALESCE(pub_date, fetched_at) DESC\r\n LIMIT @lim OFFSET @off\r\n `)\r\n .all(params) as Record<string, unknown>[];\r\n const hasMore = rows.length > limit;\r\n const items = mapRowsToDbItems(hasMore ? rows.slice(0, limit) : rows);\r\n return { items, hasMore };\r\n}\r\n\r\n/** 按 id(guid)取单条,供 MCP/API 等 */\r\nexport async function getItemById(id: string): Promise<DbItem | null> {\r\n const db = await getDb();\r\n const row = db.prepare(\"SELECT * FROM items WHERE id = @id\").get({ id }) as Record<string, unknown> | undefined;\r\n return row ? toDbItem(row) : null;\r\n}\r\n\r\n/** 单信源最近条目;可选 since(增量同步 /api/feed 等) */\r\nexport async function queryItemsBySource(sourceUrl: string, limit = 50, since?: Date): Promise<DbItem[]> {\r\n const key = canonicalHttpSourceRef(sourceUrl);\r\n if (!key) return [];\r\n const db = await getDb();\r\n const sinceClause = since ? \"AND COALESCE(pub_date, fetched_at) >= @since\" : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE source_url = @sourceUrl ${sinceClause}\r\n ORDER BY COALESCE(pub_date, fetched_at) DESC\r\n LIMIT @limit\r\n `)\r\n .all({ sourceUrl: key, limit, since: since?.toISOString() ?? null }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 多条件查询:source_url / sourceUrls / author / 全文 q / tags / 时间范围 */\r\nexport async function queryItems(opts: {\r\n sourceUrl?: string;\r\n sourceUrls?: string[];\r\n author?: string;\r\n q?: string;\r\n tags?: string[];\r\n limit?: number;\r\n offset?: number;\r\n since?: Date;\r\n until?: Date;\r\n}): Promise<{ items: DbItem[]; total: number }> {\r\n const db = await getDb();\r\n const { sourceUrl, sourceUrls, author, q, tags: tagsFilter, limit = 20, offset = 0, since, until } = opts;\r\n const conditions: string[] = [];\r\n const params: Record<string, unknown> = { limit, offset };\r\n if (sourceUrl) {\r\n const key = canonicalHttpSourceRef(sourceUrl);\r\n if (!key) {\r\n return { items: [], total: 0 };\r\n }\r\n conditions.push(\"i.source_url = @sourceUrl\");\r\n params.sourceUrl = key;\r\n } else if (sourceUrls && sourceUrls.length > 0) {\r\n const expanded = [...new Set(sourceUrls.map((s) => canonicalHttpSourceRef(s)).filter(Boolean))];\r\n if (expanded.length === 0) {\r\n return { items: [], total: 0 };\r\n }\r\n const placeholders = expanded.map((_, i) => `@src${i}`).join(\", \");\r\n conditions.push(`i.source_url IN (${placeholders})`);\r\n expanded.forEach((s, i) => ((params as Record<string, unknown>)[`src${i}`] = s));\r\n }\r\n if (author && author.trim().length >= 2) {\r\n conditions.push(\"instr(i.author, @author) > 0\");\r\n params.author = author.trim();\r\n }\r\n if (q) {\r\n conditions.push(\"i.rowid IN (SELECT rowid FROM items_fts WHERE items_fts MATCH @q)\");\r\n params.q = q;\r\n }\r\n if (tagsFilter && tagsFilter.length > 0) {\r\n const trimmed = tagsFilter.filter((t) => typeof t === \"string\" && t.trim()).map((t) => (t as string).trim());\r\n if (trimmed.length > 0) {\r\n const tagConds = trimmed.map((_, i) => `LOWER(TRIM(json_each.value)) = LOWER(@tag${i})`).join(\" OR \");\r\n conditions.push(`i.tags IS NOT NULL AND EXISTS (SELECT 1 FROM json_each(i.tags) WHERE ${tagConds})`);\r\n trimmed.forEach((t, i) => {\r\n (params as Record<string, unknown>)[`tag${i}`] = t;\r\n });\r\n }\r\n }\r\n if (since) {\r\n conditions.push(\"COALESCE(i.pub_date, i.fetched_at) >= @since\");\r\n params.since = since.toISOString();\r\n }\r\n if (until) {\r\n conditions.push(\"COALESCE(i.pub_date, i.fetched_at) < @until\");\r\n params.until = until.toISOString();\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT i.id, i.url, i.source_url, i.title, i.author, i.summary, i.content, i.tags, i.translations, i.pub_date, i.fetched_at, i.pushed_at\r\n FROM items i ${where}\r\n ORDER BY COALESCE(i.pub_date, i.fetched_at) DESC\r\n LIMIT @limit OFFSET @offset\r\n `)\r\n .all(params) as Record<string, unknown>[];\r\n const { count } = db.prepare(`SELECT COUNT(*) as count FROM items i ${where}`).get(params) as { count: number };\r\n return { items: mapRowsToDbItems(rows), total: count };\r\n}\r\n\r\n/** 从所有条目的 tags JSON 中移除指定标签(大小写不敏感),返回更新行数 */\r\nexport async function removeTagFromAllItems(tag: string): Promise<number> {\r\n const trimmed = String(tag ?? \"\").trim();\r\n if (!trimmed) return 0;\r\n const targetLower = trimmed.toLowerCase();\r\n\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const rows = db.prepare(\"SELECT id, tags FROM items WHERE tags IS NOT NULL AND tags != ''\").all() as { id: string; tags: string }[];\r\n\r\n const updateStmt = db.prepare(\"UPDATE items SET tags = @tags WHERE id = @id\");\r\n let count = 0;\r\n const run = db.transaction(() => {\r\n for (const row of rows) {\r\n let itemTags: string[];\r\n try {\r\n itemTags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const filtered = itemTags.filter((t) => String(t).trim().toLowerCase() !== targetLower);\r\n if (filtered.length === itemTags.length) continue;\r\n const nextTags = filtered.length > 0 ? JSON.stringify(filtered) : null;\r\n updateStmt.run({ id: row.id, tags: nextTags });\r\n count += 1;\r\n }\r\n });\r\n run();\r\n return count;\r\n });\r\n}\r\n\r\n/** 标记已投递(如 OpenWebUI 等出站),写入 pushed_at */\r\nexport async function markPushed(ids: string[]): Promise<void> {\r\n if (ids.length === 0) return;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const now = new Date().toISOString();\r\n const stmt = db.prepare(\"UPDATE items SET pushed_at = @now WHERE id = @id\");\r\n const run = db.transaction((list: string[]) => {\r\n for (const id of list) stmt.run({ now, id });\r\n });\r\n run(ids);\r\n });\r\n}\r\n\r\n/** 按 id 删除条目;同步删除 FTS 行(content 模式依赖 items_fts_src,zh-CN 字段在触发器中已展开) */\r\nexport async function deleteItem(id: string): Promise<boolean> {\r\n if (!id?.trim()) return false;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const run = db.transaction(() => {\r\n const row = db.prepare(\"SELECT rowid FROM items WHERE id = @id\").get({ id: id.trim() }) as { rowid: number } | undefined;\r\n if (!row) return 0;\r\n db.prepare(\"DELETE FROM items_fts WHERE rowid = @rowid\").run({ rowid: row.rowid });\r\n const info = db.prepare(\"DELETE FROM items WHERE id = @id\").run({ id: id.trim() });\r\n return info.changes;\r\n });\r\n return run() > 0;\r\n });\r\n}\r\n\r\n/** 按 source_url 删除该信源下全部条目(ref 经 canonicalHttpSourceRef 与入库键一致) */\r\nexport async function deleteItemsBySourceUrl(sourceUrl: string): Promise<number> {\r\n if (!sourceUrl?.trim()) return 0;\r\n const key = canonicalHttpSourceRef(sourceUrl.trim());\r\n if (!key) return 0;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const info = db.prepare(\"DELETE FROM items WHERE source_url = @sourceUrl\").run({ sourceUrl: key });\r\n return info.changes;\r\n });\r\n}\r\n\r\n/** 待投递:未写 pushed_at 且已有 content */\r\nexport async function getPendingPushItems(limit = 100): Promise<DbItem[]> {\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE pushed_at IS NULL AND content IS NOT NULL\r\n ORDER BY fetched_at ASC\r\n LIMIT @limit\r\n `)\r\n .all({ limit }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 按本地日(YYYY-MM-DD)取当日抓取过的条目,按 fetched_at,最多 300 条 */\r\nexport async function getItemsForDate(date: string): Promise<DbItem[]> {\r\n const db = await getDb();\r\n const start = new Date(`${date}T00:00:00`).toISOString();\r\n const end = new Date(`${date}T23:59:59.999`).toISOString();\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE fetched_at >= @start AND fetched_at <= @end\r\n ORDER BY fetched_at DESC\r\n LIMIT 300\r\n `)\r\n .all({ start, end }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 各信源条目数与最新一条时间,用于管理页统计(按 canonical 合并,与订阅 ref 对齐) */\r\nexport async function getSourceStats(): Promise<{ source_url: string; count: number; latest_at: string | null }[]> {\r\n const { mergeSourceStatsRows } = await import(\"../utils/httpSourceRef.js\");\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\r\n \"SELECT source_url, COUNT(*) as count, MAX(COALESCE(pub_date, fetched_at)) as latest_at FROM items GROUP BY source_url ORDER BY count DESC\",\r\n )\r\n .all() as { source_url: string; count: number; latest_at: string | null }[];\r\n return mergeSourceStatsRows(rows);\r\n}\r\n\r\n/** 写入一条运行日志到 logs.db(payload 为 JSON 字符串) */\r\nexport async function insertLog(entry: LogEntry): Promise<void> {\r\n const db = await getLogsDb();\r\n db.prepare(`\r\n INSERT INTO logs (level, category, message, payload, source_url, created_at)\r\n VALUES (@level, @category, @message, @payload, NULL, @created_at)\r\n `).run({\r\n level: entry.level,\r\n category: entry.category,\r\n message: entry.message,\r\n payload: entry.payload != null ? JSON.stringify(entry.payload) : null,\r\n created_at: entry.created_at,\r\n });\r\n}\r\n\r\n/** 分页查询运行日志(logs.db) */\r\nexport async function queryLogs(opts: {\r\n level?: LogEntry[\"level\"];\r\n category?: LogEntry[\"category\"];\r\n limit?: number;\r\n offset?: number;\r\n since?: Date;\r\n}): Promise<{ items: DbLog[]; total: number }> {\r\n const db = await getLogsDb();\r\n const { level, category, limit = 50, offset = 0, since } = opts;\r\n const conditions: string[] = [];\r\n const params: Record<string, unknown> = { limit, offset };\r\n if (level) {\r\n conditions.push(\"level = @level\");\r\n params.level = level;\r\n }\r\n if (category) {\r\n conditions.push(\"INSTR(LOWER(category), LOWER(@categoryPattern)) > 0\");\r\n params.categoryPattern = category;\r\n }\r\n if (since) {\r\n conditions.push(\"created_at >= @since\");\r\n params.since = since.toISOString();\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT id, level, category, message, payload, created_at\r\n FROM logs ${where}\r\n ORDER BY created_at DESC\r\n LIMIT @limit OFFSET @offset\r\n `)\r\n .all(params) as DbLog[];\r\n const { count } = db.prepare(`SELECT COUNT(*) as count FROM logs ${where}`).get(params) as { count: number };\r\n return { items: rows, total: count };\r\n}\r\n\r\n/** 清空 logs 表(运行日志库) */\r\nexport async function clearAllLogs(): Promise<number> {\r\n const db = await getLogsDb();\r\n const r = db.prepare(\"DELETE FROM logs\").run();\r\n return r.changes;\r\n}\r\n\r\n/** 读取系统标签列表:.rssany/tags.json,供 pipeline tagger 等 */\r\nexport async function getSystemTags(): Promise<string[]> {\r\n try {\r\n const raw = await readFile(TAGS_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as { tags?: unknown[] };\r\n if (!Array.isArray(parsed?.tags)) return [];\r\n return parsed.tags\r\n .filter((t): t is string => typeof t === \"string\" && t.trim().length > 0)\r\n .map((t) => t.trim());\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** 保存系统标签到 .rssany/tags.json */\r\nexport async function saveSystemTagsToFile(tags: string[]): Promise<void> {\r\n const list = tags\r\n .filter((t) => typeof t === \"string\" && t.trim())\r\n .map((t) => t.trim());\r\n await writeFile(TAGS_CONFIG_PATH, JSON.stringify({ tags: list }, null, 2), \"utf-8\");\r\n}\r\n\r\n/** 系统标签使用统计:结合 tags.json 与库内条目的 tags 字段 */\r\nexport async function getSystemTagStats(): Promise<TagStat[]> {\r\n const systemTags = await getSystemTags();\r\n if (systemTags.length === 0) return [];\r\n\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\"SELECT tags, pub_date, fetched_at FROM items WHERE tags IS NOT NULL AND tags != ''\")\r\n .all() as { tags: string; pub_date: string | null; fetched_at: string }[];\r\n\r\n const now = Date.now();\r\n const tagMap = new Map<string, { count: number; hotness: number }>();\r\n for (const name of systemTags) {\r\n tagMap.set(name.toLowerCase(), { count: 0, hotness: 0 });\r\n }\r\n\r\n for (const row of rows) {\r\n let itemTags: string[];\r\n try {\r\n itemTags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const pubMs = row.pub_date ? Date.parse(row.pub_date) : null;\r\n const fetchedMs = Date.parse(row.fetched_at);\r\n const factor = recencyFactor(pubMs, fetchedMs, now);\r\n\r\n for (const t of itemTags) {\r\n const key = String(t).trim().toLowerCase();\r\n const entry = tagMap.get(key);\r\n if (entry) {\r\n entry.count += 1;\r\n entry.hotness += factor;\r\n }\r\n }\r\n }\r\n\r\n return systemTags.map((name) => {\r\n const entry = tagMap.get(name.toLowerCase()) ?? { count: 0, hotness: 0 };\r\n return {\r\n name,\r\n count: entry.count,\r\n hotness: Math.round(entry.hotness * 100) / 100,\r\n };\r\n });\r\n}\r\n\r\n/** 标签统计:条数 + 热度(时间衰减) */\r\nexport interface TagStat {\r\n name: string;\r\n count: number;\r\n hotness: number;\r\n period?: number;\r\n}\r\n\r\n/** 时间衰减因子:1 / (1 + days_ago/7),越新权重越高 */\r\nfunction recencyFactor(pubDateMs: number | null, fetchedAtMs: number, nowMs: number): number {\r\n const ref = pubDateMs ?? fetchedAtMs;\r\n const daysAgo = (nowMs - ref) / (24 * 60 * 60 * 1000);\r\n return 1 / (1 + Math.max(0, daysAgo) / 7);\r\n}\r\n\r\n/** 非系统标签中热度较高者,返回最多 5 个且 hotness > 20 */\r\nexport async function getSuggestedTags(): Promise<TagStat[]> {\r\n const systemTags = await getSystemTags();\r\n const systemLower = new Set(systemTags.map((t) => t.toLowerCase().trim()));\r\n\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\"SELECT tags, pub_date, fetched_at FROM items WHERE tags IS NOT NULL AND tags != ''\")\r\n .all() as { tags: string; pub_date: string | null; fetched_at: string }[];\r\n\r\n const tagMap = new Map<string, { name: string; count: number; hotness: number }>();\r\n const now = Date.now();\r\n\r\n for (const row of rows) {\r\n let tags: string[];\r\n try {\r\n tags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const pubMs = row.pub_date ? Date.parse(row.pub_date) : null;\r\n const fetchedMs = Date.parse(row.fetched_at);\r\n const factor = recencyFactor(pubMs, fetchedMs, now);\r\n\r\n for (const t of tags) {\r\n const trimmed = String(t).trim();\r\n if (!trimmed) continue;\r\n const key = trimmed.toLowerCase();\r\n if (systemLower.has(key)) continue;\r\n\r\n const existing = tagMap.get(key);\r\n if (existing) {\r\n existing.count += 1;\r\n existing.hotness += factor;\r\n } else {\r\n tagMap.set(key, { name: trimmed, count: 1, hotness: factor });\r\n }\r\n }\r\n }\r\n\r\n return Array.from(tagMap.values())\r\n .filter((s) => s.hotness > 20)\r\n .sort((a, b) => b.hotness - a.hotness)\r\n .slice(0, 5)\r\n .map((s) => ({ name: s.name, count: s.count, hotness: Math.round(s.hotness * 100) / 100 }));\r\n}\r\n\r\n/** 库中行结构(snake_case);与 FeedItem 对应,author/tags 等为解析后类型 */\r\nexport interface DbItem {\r\n id: string;\r\n url: string;\r\n source_url: string;\r\n title: string | null;\r\n author: string[] | null;\r\n summary: string | null;\r\n content: string | null;\r\n image_url: string | null;\r\n tags: string[] | null;\r\n translations: Record<string, { title?: string; summary?: string; content?: string }> | null;\r\n pub_date: string | null;\r\n fetched_at: string;\r\n pushed_at: string | null;\r\n}\r\n\r\n/** 运行日志表行 */\r\nexport interface DbLog {\r\n id: number;\r\n level: string;\r\n category: string;\r\n message: string;\r\n payload: string | null;\r\n created_at: string;\r\n}\r\n","// 日志配置:从环境变量读取,不依赖 config.json 以尽早可用\n\nimport type { LogLevel } from \"./types.js\";\n\nconst LEVEL_ORDER: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction parseLevel(s: string | undefined, fallback: LogLevel): LogLevel {\n if (!s) return fallback;\n const v = s.toLowerCase() as LogLevel;\n if (LEVEL_ORDER.includes(v)) return v;\n return fallback;\n}\n\n/** 当前控制台最低输出级别(默认 info,生产可设为 warn) */\nexport function getConsoleLevel(): LogLevel {\n return parseLevel(process.env.LOG_LEVEL, \"info\");\n}\n\n/** 是否将 error/warn 写入数据库(默认 true) */\nexport function getLogToDb(): boolean {\n const v = process.env.LOG_TO_DB;\n if (v === \"0\" || v === \"false\") return false;\n if (v === \"1\" || v === \"true\") return true;\n return true;\n}\n\n/** 落库的最低级别(默认 warn) */\nexport function getDbLevel(): LogLevel {\n return parseLevel(process.env.LOG_DB_LEVEL, \"warn\");\n}\n\nexport function levelOrder(l: LogLevel): number {\n return LEVEL_ORDER.indexOf(l);\n}\n\n/** 是否应输出到控制台 */\nexport function shouldLogToConsole(consoleLevel: LogLevel, entryLevel: LogLevel): boolean {\n return levelOrder(entryLevel) >= levelOrder(consoleLevel);\n}\n\n/** 是否应写入 DB */\nexport function shouldLogToDb(logToDb: boolean, dbLevel: LogLevel, entryLevel: LogLevel): boolean {\n return logToDb && levelOrder(entryLevel) >= levelOrder(dbLevel);\n}\n","// 统一日志:仅落库,不打印控制台;全体级别落库,由日志页/API 查看\n\nimport { insertLog } from \"../../db/index.js\";\nimport { getLogToDb } from \"./config.js\";\nimport type { LogCategory, LogEntry, LogLevel } from \"./types.js\";\n\nfunction now(): string {\n return new Date().toISOString();\n}\n\nfunction writeDb(entry: LogEntry): void {\n insertLog(entry).catch((err) => {\n // 落库失败只打一次 stderr,避免循环\n process.stderr.write(`[logger] 写入日志表失败: ${err instanceof Error ? err.message : String(err)}\\n`);\n });\n}\n\nfunction emit(level: LogLevel, category: LogCategory, message: string, meta?: Record<string, unknown>): void {\n const payload = meta && Object.keys(meta).length > 0 ? { ...meta } : undefined;\n const entry: LogEntry = {\n level,\n category,\n message,\n payload: payload && Object.keys(payload).length > 0 ? payload : undefined,\n created_at: now(),\n };\n\n if (getLogToDb()) {\n writeDb(entry);\n }\n}\n\n/** 统一 logger:仅落库,不输出控制台;全体级别落库 */\nexport const logger = {\n error(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"error\", category, message, meta);\n },\n warn(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"warn\", category, message, meta);\n },\n info(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"info\", category, message, meta);\n },\n debug(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"debug\", category, message, meta);\n },\n};\n","// 使用无头浏览器(Puppeteer)拉取页面,缓存逻辑在 cacher 中\r\n\r\nimport { exec } from \"node:child_process\";\r\nimport { platform } from \"node:os\";\r\nimport { join, resolve } from \"node:path\";\r\nimport { promisify } from \"node:util\";\r\nimport puppeteerCore, { type Browser, type Page } from \"puppeteer-core\";\r\nimport { applyPurify } from \"./purify.js\";\r\nimport { findChromeExecutable } from \"./cdp.js\";\r\nimport type { AuthFlow } from \"../../../auth/index.js\";\r\nimport type { RequestConfig, StructuredHtmlResult } from \"./types.js\";\r\nimport { logger } from \"../../../../core/logger/index.js\";\r\n\r\nconst execAsync = promisify(exec);\r\n\r\n/** 与 launchArgs / setViewport 一致;无头拉高便于长页与懒加载内容 */\r\nconst VIEWPORT_WIDTH = 1366;\r\nconst VIEWPORT_HEIGHT_HEADLESS = 5000;\r\nconst VIEWPORT_HEIGHT_HEADFUL = 1200;\r\n\r\n/** 解析代理:显式传入的 proxy,否则 HTTP_PROXY / HTTPS_PROXY */\r\nexport function resolveProxy(config?: { proxy?: string }): string | undefined {\r\n return config?.proxy ?? process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY;\r\n}\r\n\r\n/** 从代理字符串解析出 serverUrl 和可选账号密码;支持 http://user:pass@host:port */\r\nfunction parseProxy(proxy: string): { serverUrl: string; username?: string; password?: string } {\r\n const u = new URL(proxy);\r\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\r\n const username = u.username || undefined;\r\n const password = u.password || undefined;\r\n return { serverUrl, username, password };\r\n}\r\n\r\n/** 在 Page 上设置代理认证(与 preCheckAuth / fetchHtml 一致;需与 launchBrowser 的 proxy 同时使用) */\r\nexport async function applyProxyAuthToPage(page: Page, opts?: { proxy?: string }): Promise<void> {\r\n const proxy = resolveProxy(opts);\r\n if (!proxy) return;\r\n const { username, password } = parseProxy(proxy);\r\n if (username !== undefined || password !== undefined) {\r\n await page.authenticate({ username: username ?? \"\", password: password ?? \"\" });\r\n }\r\n}\r\n\r\n\r\n/** 构建 Puppeteer launch args */\r\nfunction launchArgs(config?: { proxy?: string; headless?: boolean }): string[] {\r\n const base = [\r\n \"--disable-blink-features=AutomationControlled\",\r\n \"--no-sandbox\",\r\n \"--disable-setuid-sandbox\",\r\n \"--disable-dev-shm-usage\",\r\n \"--disable-web-security\",\r\n \"--disable-features=IsolateOrigins,site-per-process\",\r\n \"--disable-site-isolation-trials\",\r\n \"--disable-infobars\",\r\n ];\r\n const height = config?.headless !== false ? VIEWPORT_HEIGHT_HEADLESS : VIEWPORT_HEIGHT_HEADFUL;\r\n base.push(`--window-size=${VIEWPORT_WIDTH},${height}`);\r\n const proxy = resolveProxy(config);\r\n if (proxy) {\r\n const { serverUrl } = parseProxy(proxy);\r\n base.push(`--proxy-server=${serverUrl}`);\r\n }\r\n return base;\r\n}\r\n\r\n\r\n/** 获取 userDataDir:统一使用 main 目录,所有站点共享同一浏览器 profile */\r\nfunction getUserDataDir(cacheDir?: string): string | undefined {\r\n if (!cacheDir) return undefined;\r\n return join(cacheDir, \"browser_data\", \"main\");\r\n}\r\n\r\n\r\n/** 是否为「userDataDir 已被占用」的报错(上次进程未正常退出或并发启动) */\r\nfunction isAlreadyRunningError(e: unknown): boolean {\r\n const msg = e instanceof Error ? e.message : String(e);\r\n return /already running/i.test(msg) && /userDataDir|user-data-dir|user data dir/i.test(msg);\r\n}\r\n\r\n\r\n/**\r\n * 强制结束占用指定 userDataDir 的 Chrome/Chromium 进程(上次未正常退出时残留)。\r\n * 仅在 darwin/linux 下执行;启动前调用以释放 profile 锁。\r\n */\r\nasync function killStaleChromeProcesses(absUserDataDir: string): Promise<void> {\r\n const plat = platform();\r\n if (plat !== \"darwin\" && plat !== \"linux\") {\r\n return;\r\n }\r\n try {\r\n // 匹配命令行中包含该 userDataDir 的进程(Chrome 使用 --user-data-dir=/path)\r\n const psCmd = plat === \"darwin\"\r\n ? `ps -eww -o pid= -o args= 2>/dev/null`\r\n : `ps -eo pid,args --no-headers 2>/dev/null`;\r\n const { stdout } = await execAsync(psCmd, { maxBuffer: 4 * 1024 * 1024 });\r\n const pids = new Set<number>();\r\n const lineRegex = /^\\s*(\\d+)\\s+/;\r\n for (const line of stdout.split(\"\\n\")) {\r\n if (!line.includes(absUserDataDir)) continue;\r\n const m = line.match(lineRegex);\r\n if (m) pids.add(parseInt(m[1], 10));\r\n }\r\n if (pids.size === 0) return;\r\n logger.info(\"scraper\", \"发现占用 browser_data 的 Chrome 进程,正在结束\", { pids: [...pids], userDataDir: absUserDataDir });\r\n for (const pid of pids) {\r\n try {\r\n process.kill(pid, \"SIGTERM\");\r\n } catch {\r\n // 进程可能已退出\r\n }\r\n }\r\n await new Promise((r) => setTimeout(r, 800));\r\n for (const pid of pids) {\r\n try {\r\n process.kill(pid, \"SIGKILL\");\r\n } catch {\r\n // ignore\r\n }\r\n }\r\n await new Promise((r) => setTimeout(r, 300));\r\n } catch (err) {\r\n logger.warn(\"scraper\", \"结束残留 Chrome 进程时出错\", { err: err instanceof Error ? err.message : String(err) });\r\n }\r\n}\r\n\r\n\r\n// 注入脚本隐藏自动化特征\r\nasync function stealthPage(page: Page): Promise<void> {\r\n await page.evaluateOnNewDocument(() => {\r\n /* global navigator, window, document */\r\n Object.defineProperty(navigator, \"webdriver\", { get: () => false });\r\n Object.defineProperty(navigator, \"plugins\", { get: () => [1, 2, 3, 4, 5] });\r\n Object.defineProperty(navigator, \"languages\", { get: () => [\"zh-CN\", \"zh\", \"en\"] });\r\n const originalQuery = window.navigator.permissions.query;\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n window.navigator.permissions.query = (parameters: any) =>\r\n parameters.name === \"notifications\"\r\n ? Promise.resolve({ state: Notification.permission } as PermissionStatus)\r\n : originalQuery(parameters);\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n (window as any).chrome = { runtime: {} };\r\n Object.defineProperty(Notification, \"permission\", { get: () => \"default\" });\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const nav = navigator as any;\r\n if (nav.getBattery) {\r\n nav.getBattery = () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 });\r\n }\r\n });\r\n await page.setExtraHTTPHeaders({\r\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\",\r\n \"Accept-Encoding\": \"gzip, deflate, br\",\r\n \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\",\r\n \"Cache-Control\": \"max-age=0\",\r\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"',\r\n \"Sec-Ch-Ua-Mobile\": \"?0\",\r\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\r\n \"Sec-Fetch-Dest\": \"document\",\r\n \"Sec-Fetch-Mode\": \"navigate\",\r\n \"Sec-Fetch-Site\": \"none\",\r\n \"Sec-Fetch-User\": \"?1\",\r\n \"Upgrade-Insecure-Requests\": \"1\",\r\n });\r\n}\r\n\r\n\r\nfunction headersToRecord(headers: Record<string, unknown>): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(headers)) {\r\n out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n\r\n/** 对新 Page 做通用初始化:UA、Viewport、stealth 脚本 */\r\nasync function setupPage(page: Page, headless = true): Promise<void> {\r\n const realUserAgent =\r\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\r\n await page.setUserAgent(realUserAgent);\r\n await page.setViewport({\r\n width: VIEWPORT_WIDTH,\r\n height: headless ? VIEWPORT_HEIGHT_HEADLESS : VIEWPORT_HEIGHT_HEADFUL,\r\n });\r\n await stealthPage(page);\r\n}\r\n\r\n\r\n// ─── 浏览器:单发模式 ─────────────────────────────────────────────────────────\r\n// 每次任务独立启动 Chrome,用毕在调用方 `browser.close()`;不保留全局单例,不跨请求复用 Tab。\r\n\r\n/** 是否为「frame 已分离」类错误(页面发生客户端导航/重定向导致主 frame 失效) */\r\nfunction isFrameDetachedError(e: unknown): boolean {\r\n const msg = e instanceof Error ? e.message : String(e);\r\n return /detached|Navigating frame was detached|Session closed/i.test(msg);\r\n}\r\n\r\n\r\n/**\r\n * 启动新的 Chrome 实例(不缓存、不复用)。调用方须在 `finally` 中 `await browser.close()`。\r\n */\r\nexport async function launchBrowser(config: {\r\n headless?: boolean;\r\n cacheDir?: string;\r\n proxy?: string;\r\n chromeExecutablePath?: string;\r\n}): Promise<Browser> {\r\n const wantHeadless = config.headless !== false;\r\n const executablePath = config.chromeExecutablePath ?? process.env.CHROME_PATH ?? findChromeExecutable();\r\n if (!executablePath) {\r\n throw new Error(\"未找到 Chrome 可执行文件,请安装 Google Chrome 或设置 CHROME_PATH 环境变量\");\r\n }\r\n const userDataDir = getUserDataDir(config.cacheDir);\r\n const maxRetries = 2;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n if (attempt === 0 && userDataDir) {\r\n const absUserDataDir = resolve(userDataDir);\r\n await killStaleChromeProcesses(absUserDataDir);\r\n }\r\n if (attempt > 0) {\r\n const waitMs = attempt * 2000;\r\n logger.info(\"scraper\", \"userDataDir 曾被占用,等待后重试\", { waitMs, attempt });\r\n await new Promise((r) => setTimeout(r, waitMs));\r\n }\r\n return await puppeteerCore.launch({\r\n headless: wantHeadless,\r\n args: launchArgs({ proxy: config.proxy, headless: wantHeadless }),\r\n userDataDir,\r\n executablePath,\r\n ignoreDefaultArgs: [\"--enable-automation\"],\r\n });\r\n } catch (e) {\r\n lastErr = e;\r\n if (attempt < maxRetries && isAlreadyRunningError(e)) {\r\n continue;\r\n }\r\n if (isAlreadyRunningError(e)) {\r\n const dir = userDataDir ?? \"browser_data/main\";\r\n throw new Error(\r\n `Chrome 的 profile 目录已被占用(${dir})。通常是因为上次未正常退出或同时运行了多个本服务实例。请关闭占用该目录的 Chrome 进程后重试,或设置环境变量 CACHE_DIR 使用不同缓存目录。`\r\n );\r\n }\r\n throw e;\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\n/** @deprecated 使用 `launchBrowser`;行为与单发启动一致,不再复用全局实例 */\r\nexport const getOrCreateBrowser = launchBrowser;\r\n\r\n\r\n// ─── 对外 API ─────────────────────────────────────────────────────────────────\r\n\r\n/** 预检认证:单发浏览器(新开 Tab)检查是否已登录;opts 与 fetchHtml 一致(代理、有头/无头) */\r\nexport async function preCheckAuth(\r\n authFlow: AuthFlow,\r\n cacheDir: string,\r\n opts?: { proxy?: string; headless?: boolean }\r\n): Promise<boolean> {\r\n const { checkAuth, loginUrl, domain } = authFlow;\r\n if (domain == null || !cacheDir) return true;\r\n const isHeadless = opts?.headless !== false;\r\n const browser = await launchBrowser({\r\n headless: isHeadless,\r\n cacheDir,\r\n proxy: resolveProxy(opts),\r\n });\r\n try {\r\n const page = await browser.newPage();\r\n try {\r\n await setupPage(page, isHeadless);\r\n await applyProxyAuthToPage(page, opts);\r\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\r\n await new Promise((resolve) => setTimeout(resolve, 3000));\r\n return await checkAuth(page, page.url());\r\n } finally {\r\n await page.close().catch(() => {});\r\n }\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n\r\n\r\n// 执行认证流程:单发有头浏览器打开登录页,等待用户完成登录后关闭进程\r\nexport async function ensureAuth(\r\n authFlow: AuthFlow,\r\n cacheDir: string,\r\n opts?: { proxy?: string }\r\n): Promise<void> {\r\n const { checkAuth, loginUrl, loginTimeoutMs = 60 * 1000, pollIntervalMs = 2000 } = authFlow;\r\n const browser = await launchBrowser({ headless: false, cacheDir, proxy: resolveProxy(opts) });\r\n try {\r\n const page = await browser.newPage();\r\n try {\r\n await setupPage(page, false);\r\n await applyProxyAuthToPage(page, opts);\r\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\r\n await new Promise((resolve) => setTimeout(resolve, 3000));\r\n const authenticated = await checkAuth(page, page.url());\r\n if (authenticated) return;\r\n const startTime = Date.now();\r\n while (Date.now() - startTime < loginTimeoutMs) {\r\n await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));\r\n const authenticated = await checkAuth(page, page.url());\r\n if (authenticated) return;\r\n }\r\n throw new Error(`登录超时(${loginTimeoutMs}ms)`);\r\n } finally {\r\n await page.close().catch(() => {});\r\n }\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n\r\n\r\n// 单发浏览器:本次任务内最多开两个 Tab(frame 分离时重试一次),结束后关闭 Chrome\r\n// 若发生「Navigating frame was detached」等 frame 分离错误(常见于 SPA 客户端跳转),会换新 Tab 重试一次,并用 domcontentloaded 尽快取 HTML\r\nexport async function fetchHtml(url: string, config: RequestConfig = {}): Promise<StructuredHtmlResult> {\r\n const {\r\n timeoutMs,\r\n headers,\r\n cookies,\r\n cacheDir,\r\n checkAuth,\r\n authFlow,\r\n purify,\r\n headless,\r\n waitAfterLoadMs,\r\n waitForSelector,\r\n waitForSelectorTimeoutMs,\r\n useHttpResponseBody,\r\n } = config;\r\n const isHeadless = headless !== false;\r\n const browser = await launchBrowser({\r\n headless: isHeadless,\r\n cacheDir,\r\n proxy: resolveProxy(config),\r\n chromeExecutablePath: config.chromeExecutablePath,\r\n });\r\n const navigationTimeout = timeoutMs ?? 60000;\r\n const maxAttempts = 2;\r\n let lastError: unknown;\r\n\r\n try {\r\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\r\n const page = await browser.newPage();\r\n const isRetry = attempt === 1;\r\n // 重试时用 domcontentloaded 尽快取 HTML,减少 SPA 客户端跳转在取 content 前发生的概率\r\n const waitUntil = isRetry ? \"domcontentloaded\" : \"load\";\r\n const extraWaitMs = isRetry ? Math.min(500, Math.max(0, waitAfterLoadMs ?? 2000)) : Math.max(0, waitAfterLoadMs ?? 2000);\r\n try {\r\n if (config.browserContext) {\r\n await config.browserContext(page.browserContext());\r\n }\r\n await setupPage(page, isHeadless);\r\n const extraHeaders: Record<string, string> = { \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\", ...(headers ?? {}) };\r\n if (cookies != null && cookies !== \"\") {\r\n extraHeaders.cookie = cookies;\r\n }\r\n await page.setExtraHTTPHeaders(extraHeaders);\r\n const proxy = resolveProxy(config);\r\n if (proxy) {\r\n const { username, password } = parseProxy(proxy);\r\n if (username !== undefined || password !== undefined) {\r\n await page.authenticate({ username: username ?? \"\", password: password ?? \"\" });\r\n }\r\n }\r\n if (timeoutMs != null) {\r\n await page.setDefaultNavigationTimeout(timeoutMs);\r\n }\r\n const response = await page.goto(url, { waitUntil, timeout: navigationTimeout });\r\n if (extraWaitMs > 0) {\r\n await new Promise((resolve) => setTimeout(resolve, extraWaitMs));\r\n }\r\n if (waitForSelector != null && waitForSelector !== \"\" && !isRetry) {\r\n const selectorTimeout = waitForSelectorTimeoutMs ?? 20000;\r\n await page.waitForSelector(waitForSelector, { timeout: selectorTimeout });\r\n }\r\n if (checkAuth != null || authFlow != null) {\r\n const authCheck = checkAuth ?? authFlow?.checkAuth;\r\n if (authCheck != null) {\r\n const ok = await authCheck(page, url);\r\n if (!ok) {\r\n throw new Error(\"checkAuth failed: 未通过认证检查,请先调用 ensureAuth 进行预处理登录\");\r\n }\r\n }\r\n }\r\n let rawBody: string;\r\n if (useHttpResponseBody === true && response != null) {\r\n try {\r\n rawBody = await response.text();\r\n } catch {\r\n rawBody = await page.content();\r\n }\r\n } else {\r\n rawBody = await page.content();\r\n }\r\n const finalUrl = response?.url() ?? page.url() ?? String(url);\r\n const status = response?.status() ?? 0;\r\n const statusText = response?.statusText() ?? \"\";\r\n const rawHeaders = response?.headers() ?? {};\r\n const normalizedHeaders = headersToRecord(rawHeaders);\r\n const body = applyPurify(rawBody, purify);\r\n await page.close().catch(() => {});\r\n return { finalUrl, status, statusText, headers: normalizedHeaders, body };\r\n } catch (e) {\r\n lastError = e;\r\n await page.close().catch(() => {});\r\n if (isRetry || !isFrameDetachedError(e)) {\r\n throw e;\r\n }\r\n logger.warn(\"scraper\", \"fetchHtml 因 frame 分离重试\", { url, attempt: attempt + 1, err: e instanceof Error ? e.message : String(e) });\r\n await new Promise((r) => setTimeout(r, 800));\r\n }\r\n }\r\n throw lastError;\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n","// RefreshInterval:类型定义 + interval ↔ cron ↔ 缓存 key 转换,供全项目共用\n// 调度与缓存 key 对齐:refresh 自动转 cron,cron 转 strategy 与 cacher.timeBucket 一致\n// - refreshIntervalToCron(interval) → 调度器使用的 cron\n// - cronToRefreshInterval(cron) → cacher.cacheKey 使用的 strategy\n// - cacher.timeBucket(strategy) → 缓存 key 的时间窗口前缀\n// 三者逻辑一致,保证调度触发时刻与缓存 key 时间窗口边界对齐\n\nimport type { CacheKeyStrategy } from \"../scraper/sources/web/fetcher/types.js\";\n\n\n/** 刷新间隔类型(有时间语义的缓存策略,排除 forever) */\nexport type RefreshInterval = Exclude<CacheKeyStrategy, \"forever\">;\n\n\n/** 合法的刷新间隔值列表(用于运行时校验) */\nexport const VALID_INTERVALS: RefreshInterval[] = [\"1min\", \"5min\", \"10min\", \"30min\", \"1h\", \"6h\", \"12h\", \"1day\", \"3day\", \"7day\"];\n\n\n/**\n * 从 cron 表达式推断缓存策略(RefreshInterval),供 cacheKey 使用\n * 与 refreshIntervalToCron 互为逆映射,保证调度时间与缓存 key 时间窗口一致\n * 格式:minute hour day month weekday(5 字段)或 second minute hour day month weekday(6 字段)\n */\nexport function cronToRefreshInterval(cronExpr: string): RefreshInterval {\n const parts = cronExpr.trim().split(/\\s+/);\n const is6Field = parts.length >= 6;\n const minute = parts[is6Field ? 1 : 0];\n const hour = parts[is6Field ? 2 : 1];\n const day = parts[is6Field ? 3 : 2];\n const weekday = parts[is6Field ? 5 : 4];\n\n // 分钟级:*/N 或 *\n const minMatch = minute.match(/^\\*\\/(\\d+)$/);\n if (minute === \"*\" || (minMatch && parseInt(minMatch[1], 10) <= 1)) return \"1min\";\n if (minMatch) {\n const n = parseInt(minMatch[1], 10);\n if (n <= 5) return \"5min\";\n if (n <= 10) return \"10min\";\n if (n <= 30) return \"30min\";\n }\n\n // 小时级:minute=0, hour=*/N 或 *\n if (minute === \"0\" || minute === \"00\") {\n const hourMatch = hour.match(/^\\*\\/(\\d+)$/);\n if (hour === \"*\" || (hourMatch && parseInt(hourMatch[1], 10) <= 1)) return \"1h\";\n if (hourMatch) {\n const n = parseInt(hourMatch[1], 10);\n if (n <= 2) return \"1h\";\n if (n <= 6) return \"6h\";\n if (n <= 12) return \"12h\";\n }\n // 每天固定时刻(如 0 9 * * *)\n if (day === \"*\" || day === \"?\") return \"1day\";\n // 每周固定时刻(如 0 0 * * 0)\n if (weekday !== \"*\" && weekday !== \"?\") return \"7day\";\n }\n\n return \"1h\";\n}\n\n\n/**\n * 将 RefreshInterval 转为 cron 表达式\n * 调度触发时刻与 cacher.timeBucket 的时间窗口边界一致,保证缓存 key 与执行频率对齐\n */\nexport function refreshIntervalToCron(interval: RefreshInterval): string {\n const map: Record<RefreshInterval, string> = {\n \"1min\": \"* * * * *\",\n \"5min\": \"*/5 * * * *\",\n \"10min\": \"*/10 * * * *\",\n \"30min\": \"*/30 * * * *\",\n \"1h\": \"0 * * * *\",\n \"6h\": \"0 */6 * * *\",\n \"12h\": \"0 0,12 * * *\",\n \"1day\": \"0 0 * * *\",\n \"3day\": \"0 0 */3 * *\",\n \"7day\": \"0 0 * * 0\",\n };\n return map[interval];\n}\n\n\n/** 将 RefreshInterval 转换为对应的毫秒数 */\nexport function refreshIntervalToMs(interval: RefreshInterval): number {\n const map: Record<RefreshInterval, number> = {\n \"1min\": 1 * 60 * 1000,\n \"5min\": 5 * 60 * 1000,\n \"10min\": 10 * 60 * 1000,\n \"30min\": 30 * 60 * 1000,\n \"1h\": 60 * 60 * 1000,\n \"6h\": 6 * 60 * 60 * 1000,\n \"12h\": 12 * 60 * 60 * 1000,\n \"1day\": 24 * 60 * 60 * 1000,\n \"3day\": 3 * 24 * 60 * 60 * 1000,\n \"7day\": 7 * 24 * 60 * 60 * 1000,\n };\n return map[interval];\n}\n","// 缓存管理:认证 profile 缓存\n\nimport { createHash } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CacheKeyStrategy } from \"../../scraper/sources/web/fetcher/types.js\";\nimport { cronToRefreshInterval } from \"../../utils/refreshInterval.js\";\n\n\nconst DOMAINS_SUBDIR = \"domains\";\n\n\n/** 认证配置档:可存 cookies、localStorage、sessionStorage 等,由 cacher 写入 cacheDir/domains/ */\nexport interface AuthProfile {\n cookies?: string;\n localStorage?: string;\n sessionStorage?: string;\n [key: string]: unknown;\n}\n\n\nfunction profileFileName(domain: string): string {\n return `${domain.replace(/[/\\\\]/g, \"_\")}.json`;\n}\n\n\nfunction urlHash(url: string): string {\n return createHash(\"sha256\").update(url).digest(\"hex\");\n}\n\n\n// 将时间映射到对应策略的时间窗口前缀(UTC)\n// 与 refreshIntervalToCron 的时间边界一致:如 1h 策略,窗口为整点,cron 为 \"0 * * * *\"\nfunction timeBucket(strategy: CacheKeyStrategy, now: Date): string {\n const y = now.getUTCFullYear();\n const mo = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n const d = String(now.getUTCDate()).padStart(2, \"0\");\n const h = now.getUTCHours();\n const hs = String(h).padStart(2, \"0\");\n const min = now.getUTCMinutes();\n if (strategy === \"1min\") return `${y}-${mo}-${d}T${hs}:${String(min).padStart(2, \"0\")}`;\n if (strategy === \"5min\") return `${y}-${mo}-${d}T${hs}:${String(Math.floor(min / 5) * 5).padStart(2, \"0\")}`;\n if (strategy === \"10min\") return `${y}-${mo}-${d}T${hs}:${String(Math.floor(min / 10) * 10).padStart(2, \"0\")}`;\n if (strategy === \"30min\") return `${y}-${mo}-${d}T${hs}:${min < 30 ? \"00\" : \"30\"}`;\n if (strategy === \"1h\") return `${y}-${mo}-${d}T${hs}`;\n if (strategy === \"6h\") return `${y}-${mo}-${d}T${String(Math.floor(h / 6) * 6).padStart(2, \"0\")}`;\n if (strategy === \"12h\") return `${y}-${mo}-${d}T${h < 12 ? \"00\" : \"12\"}`;\n if (strategy === \"1day\") return `${y}-${mo}-${d}`;\n if (strategy === \"3day\") {\n const epochDay = Math.floor(now.getTime() / 86400000);\n return `d${Math.floor(epochDay / 3) * 3}`;\n }\n if (strategy === \"7day\") {\n const adjusted = new Date(now);\n adjusted.setUTCHours(0, 0, 0, 0);\n adjusted.setUTCDate(adjusted.getUTCDate() + 3 - ((adjusted.getUTCDay() + 6) % 7));\n const week1 = new Date(Date.UTC(adjusted.getUTCFullYear(), 0, 4));\n const isoYear = adjusted.getUTCFullYear();\n const isoWeek = 1 + Math.round(((adjusted.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getUTCDay() + 6) % 7) / 7);\n return `${isoYear}-W${String(isoWeek).padStart(2, \"0\")}`;\n }\n return \"\";\n}\n\n\n// 根据 URL 与策略生成缓存 key:forever=仅 sha256(url);其余=时间窗口前缀-sha256(url)\nexport function cacheKey(url: string, strategy: CacheKeyStrategy = \"forever\", now: Date = new Date()): string {\n const hash = urlHash(url);\n if (strategy === \"forever\") return hash;\n return `${timeBucket(strategy, now)}-${hash}`;\n}\n\n\n/** 基于 cron 表达式生成缓存 key:策略由 cronToRefreshInterval 派生,与调度触发时刻对齐 */\nexport function cacheKeyFromCron(url: string, cron: string, now: Date = new Date()): string {\n return cacheKey(url, cronToRefreshInterval(cron), now);\n}\n\n\n// 从 cacheDir/domains/{domain}.json 读取认证配置(未来可扩展为 domains/{domain}/{profileName}.json)\nexport async function readProfile(cacheDir: string, domain: string): Promise<AuthProfile | null> {\n const dir = join(cacheDir, DOMAINS_SUBDIR);\n const filePath = join(dir, profileFileName(domain));\n try {\n const raw = await readFile(filePath, \"utf-8\");\n return JSON.parse(raw) as AuthProfile;\n } catch {\n return null;\n }\n}\n\n\n// 将认证配置写入 cacheDir/domains/{domain}.json\nexport async function writeProfile(cacheDir: string, domain: string, data: AuthProfile): Promise<void> {\n const dir = join(cacheDir, DOMAINS_SUBDIR);\n await mkdir(dir, { recursive: true });\n const filePath = join(dir, profileFileName(domain));\n await writeFile(filePath, JSON.stringify(data, null, 2), \"utf-8\");\n}\n","// 正文提取器:从详情页 HTML 提取单条正文(规则 / Readability),支持缓存\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { JSDOM } from \"jsdom\";\nimport { Readability } from \"@mozilla/readability\";\nimport { cacheKey as cacherCacheKey } from \"../../../../core/cacher/index.js\";\nimport { fetchHtml } from \"../fetcher/index.js\";\nimport type { RequestConfig } from \"../fetcher/types.js\";\nimport type { FeedItem } from \"../../../../types/feedItem.js\";\nimport { normalizeAuthor } from \"../../../../types/feedItem.js\";\nimport type { ExtractedResult, ExtractorConfig } from \"./types.js\";\nimport { logger } from \"../../../../core/logger/index.js\";\n\n\nconst EXTRACTED_SUBDIR = \"extracted\";\n\n\n/** 从缓存读取已提取结果 */\nasync function readCachedExtracted(cacheDir: string, key: string): Promise<ExtractedResult | null> {\n const filePath = join(cacheDir, EXTRACTED_SUBDIR, `${key}.json`);\n try {\n const raw = await readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw) as ExtractedResult & { cachedAt?: string };\n return {\n author: parsed.author,\n title: parsed.title,\n summary: parsed.summary,\n content: parsed.content ?? (parsed as { contentMarkdown?: string }).contentMarkdown ?? (parsed as { contentHtml?: string }).contentHtml,\n pubDate: parsed.pubDate,\n };\n } catch {\n return null;\n }\n}\n\n\n/** 将提取结果写入缓存 */\nasync function writeCachedExtracted(cacheDir: string, key: string, result: ExtractedResult): Promise<void> {\n const dir = join(cacheDir, EXTRACTED_SUBDIR);\n await mkdir(dir, { recursive: true });\n const filePath = join(dir, `${key}.json`);\n const cache = { ...result, cachedAt: new Date().toISOString() };\n await writeFile(filePath, JSON.stringify(cache, null, 2), \"utf-8\");\n}\n\n\n/** 解析 key:与 fetched 一致,使用 sha256(url) */\nfunction extractedCacheKey(url: string, config: ExtractorConfig): string {\n if (config.cacheKey != null && config.cacheKey !== \"\") return config.cacheKey;\n if (url) return cacherCacheKey(url, \"forever\");\n return \"\";\n}\n\n\n/** 使用 Readability 从详情页 HTML 提取正文 */\nfunction extractWithReadability(html: string, url: string): ExtractedResult {\n const dom = new JSDOM(html, { url });\n const reader = new Readability(dom.window.document);\n const article = reader.parse();\n if (!article) return {};\n const summary = article.excerpt && article.excerpt.length > 200 ? article.excerpt.slice(0, 200) + \"…\" : article.excerpt;\n return {\n author: article.byline || undefined,\n title: article.title || undefined,\n summary: summary || undefined,\n content: article.content || undefined,\n };\n}\n\n\n/** 从 HTML 提取正文(规则 / Readability),支持缓存 */\nexport async function extractHtml(html: string, config: ExtractorConfig = {}): Promise<ExtractedResult> {\n const { url = \"\", mode, customExtractor, cacheDir, useCache = true } = config;\n const key = extractedCacheKey(url, config);\n if (useCache !== false && cacheDir != null && cacheDir !== \"\" && key) {\n const cached = await readCachedExtracted(cacheDir, key);\n if (cached != null) return cached;\n }\n if (customExtractor != null) {\n const result = await Promise.resolve(customExtractor(html, url));\n if (cacheDir != null && cacheDir !== \"\" && key) {\n await writeCachedExtracted(cacheDir, key, result);\n }\n return result;\n }\n if (mode === \"readability\") {\n const result = extractWithReadability(html, url);\n if (cacheDir != null && cacheDir !== \"\" && key) {\n await writeCachedExtracted(cacheDir, key, result);\n }\n return result;\n }\n return {};\n}\n\n\n/** 根据 link 拉取 HTML 并提取正文(提取过程不缓存 fetch) */\nexport async function extractFromLink(\n link: string,\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<ExtractedResult> {\n const { cacheDir } = extractorConfig;\n const fetchOpts: RequestConfig = {\n cacheDir: fetchConfig.cacheDir ?? cacheDir,\n useCache: false,\n timeoutMs: fetchConfig.timeoutMs ?? 15_000,\n ...fetchConfig,\n };\n const res = await fetchHtml(link, fetchOpts);\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`fetch 失败: ${res.status} ${res.statusText} for ${link}`);\n }\n const finalUrl = res.finalUrl ?? link;\n return extractHtml(res.body, {\n ...extractorConfig,\n url: finalUrl,\n cacheKey: extractorConfig.cacheKey ?? (cacheDir ? cacherCacheKey(link, \"forever\") : undefined),\n });\n}\n\n\n/** 对单个 FeedItem 根据 link 拉取并提取正文,合并回 item(补充 author/title/summary/content/pubDate) */\nexport async function extractItem(\n item: FeedItem,\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<FeedItem> {\n const extracted = await extractFromLink(item.link, extractorConfig, fetchConfig);\n const pubDate =\n extracted.pubDate != null\n ? typeof extracted.pubDate === \"string\"\n ? new Date(extracted.pubDate)\n : extracted.pubDate\n : item.pubDate;\n return {\n ...item,\n author: normalizeAuthor(extracted.author ?? item.author),\n title: extracted.title ?? item.title,\n summary: extracted.summary ?? item.summary,\n content: extracted.content ?? item.content,\n pubDate,\n };\n}\n\n\n/** 对多个 FeedItem 依次提取正文并合并(可复用 fetcher 缓存) */\nexport async function extractItems(\n items: FeedItem[],\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<FeedItem[]> {\n const results: FeedItem[] = [];\n const onProgress = extractorConfig.onProgress;\n for (let i = 0; i < items.length; i++) {\n try {\n results.push(await extractItem(items[i], extractorConfig, fetchConfig));\n } catch (err) {\n logger.warn(\"scraper\", \"正文提取失败\", { item_url: items[i].link, err: err instanceof Error ? err.message : String(err) });\n results.push(items[i]);\n }\n onProgress?.(i + 1, items.length);\n }\n return results;\n}\n","// LLM 配置:config.json 的 llm 块优先,其次环境变量\r\n\r\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\r\nimport \"dotenv/config\";\r\nimport { CONFIG_PATH } from \"../config/paths.js\";\r\n\r\n/** LLM 配置(文件、环境变量或调用方传入) */\r\nexport interface LLMConfig {\r\n apiKey?: string;\r\n baseUrl?: string;\r\n model?: string;\r\n}\r\n\r\nconst DEFAULT_BASE_URL = \"https://api.openai.com/v1\";\r\nconst DEFAULT_MODEL = \"gpt-4o-mini\";\r\n\r\ninterface FileLlmCache {\r\n mtimeMs: number;\r\n llm: Partial<LLMConfig>;\r\n}\r\n\r\nlet fileCache: FileLlmCache | null = null;\r\n\r\n/** 配置或外部写入后调用,使下次 getLLMConfig 重新读盘 */\r\nexport function invalidateLLMConfigCache(): void {\r\n fileCache = null;\r\n}\r\n\r\nfunction readLlmFromFileSync(): Partial<LLMConfig> {\r\n if (!existsSync(CONFIG_PATH)) return {};\r\n try {\r\n const st = statSync(CONFIG_PATH);\r\n if (fileCache && fileCache.mtimeMs === st.mtimeMs) return fileCache.llm;\r\n const raw = readFileSync(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { llm?: unknown };\r\n const llmRaw = j?.llm;\r\n const llm: Partial<LLMConfig> = {};\r\n if (llmRaw && typeof llmRaw === \"object\") {\r\n const o = llmRaw as Record<string, unknown>;\r\n if (typeof o.apiKey === \"string\" && o.apiKey.length > 0) llm.apiKey = o.apiKey;\r\n if (typeof o.baseUrl === \"string\" && o.baseUrl.trim()) llm.baseUrl = o.baseUrl.trim();\r\n if (typeof o.model === \"string\" && o.model.trim()) llm.model = o.model.trim();\r\n }\r\n fileCache = { mtimeMs: st.mtimeMs, llm };\r\n return llm;\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/** 合并文件与环境变量:文件中的非空字段优先 */\r\nexport function getLLMConfig(): Required<Pick<LLMConfig, \"baseUrl\" | \"model\">> & Pick<LLMConfig, \"apiKey\"> {\r\n const file = readLlmFromFileSync();\r\n const apiKey = file.apiKey ?? process.env.OPENAI_API_KEY;\r\n const baseUrl = file.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_BASE_URL;\r\n const model = file.model ?? process.env.OPENAI_MODEL ?? DEFAULT_MODEL;\r\n return { apiKey, baseUrl, model };\r\n}\r\n","// LLM 统一调用:封装 OpenAI chat completion,供 parser/extractor / pipeline 复用\r\n\r\nimport OpenAI from \"openai\";\r\nimport type { ChatCompletion } from \"openai/resources/chat/completions\";\r\nimport { getLLMConfig } from \"./llmConfig.js\";\r\nimport type { LLMConfig } from \"./llmConfig.js\";\r\n\r\n/** 从非流式 chat completion 取出助手文本;content 为空时检查 refusal、finish_reason */\r\nfunction extractAssistantText(completion: ChatCompletion): string {\r\n const choice = completion.choices[0];\r\n if (!choice) throw new Error(\"LLM 返回无 choices\");\r\n const msg = choice.message;\r\n const raw = msg.content;\r\n if (typeof raw === \"string\") {\r\n const t = raw.trim();\r\n if (t.length > 0) return t;\r\n }\r\n // DeepSeek deepseek-reasoner:推理在 reasoning_content,答复在 content;总输出受 max_tokens 限制,可能仅有 reasoning_content\r\n const extra = msg as unknown as Record<string, unknown>;\r\n const rc = extra.reasoning_content;\r\n if (typeof rc === \"string\" && rc.trim().length > 0) {\r\n return rc.trim();\r\n }\r\n const refusal = msg.refusal;\r\n if (typeof refusal === \"string\" && refusal.trim()) {\r\n throw new Error(`模型拒绝: ${refusal.trim()}`);\r\n }\r\n const fr = choice.finish_reason;\r\n if (fr === \"tool_calls\") {\r\n throw new Error(\"LLM 返回了工具调用而非文本,请换一个模型或关闭工具调用\");\r\n }\r\n if (fr === \"content_filter\") {\r\n throw new Error(\"内容被内容策略过滤\");\r\n }\r\n if (fr === \"length\") {\r\n throw new Error(\r\n \"LLM 输出在 content / reasoning_content 均为空前已用尽\",\r\n );\r\n }\r\n throw new Error(`LLM 返回空内容 (finish_reason=${String(fr)})`);\r\n}\r\n\r\n/** 合并调用方配置与环境变量配置 */\r\nfunction mergeConfig(override?: Partial<LLMConfig> & { apiUrl?: string }): { apiKey: string; baseUrl: string; model: string } {\r\n const env = getLLMConfig();\r\n const apiKey = override?.apiKey ?? env.apiKey;\r\n const baseUrl = override?.apiUrl ?? override?.baseUrl ?? env.baseUrl;\r\n const model = override?.model ?? env.model;\r\n if (!apiKey) throw new Error(\"LLM API Key 未配置:请在管理后台「设置 → LLM」或环境变量 OPENAI_API_KEY 中设置\");\r\n return { apiKey, baseUrl, model };\r\n}\r\n\r\n/** 调用 LLM 获取 JSON 响应,供 parser/extractor 复用 */\r\nexport async function chatJson(\r\n prompt: string,\r\n config?: Partial<LLMConfig> & { apiUrl?: string },\r\n options?: { maxTokens?: number; debugLabel?: string },\r\n): Promise<Record<string, unknown>> {\r\n const { apiKey, baseUrl, model } = mergeConfig(config);\r\n const openai = new OpenAI({ apiKey, baseURL: baseUrl });\r\n const completion = await openai.chat.completions.create({\r\n model,\r\n messages: [{ role: \"user\", content: prompt }],\r\n max_tokens: options?.maxTokens ?? 8192,\r\n response_format: { type: \"json_object\" },\r\n });\r\n const content = extractAssistantText(completion);\r\n return JSON.parse(content) as Record<string, unknown>;\r\n}\r\n\r\n/** 调用 LLM 获取纯文本响应 */\r\nexport async function chatText(\r\n prompt: string,\r\n config?: Partial<LLMConfig> & { apiUrl?: string },\r\n options?: { maxTokens?: number; debugLabel?: string },\r\n): Promise<string> {\r\n const { apiKey, baseUrl, model } = mergeConfig(config);\r\n const openai = new OpenAI({ apiKey, baseURL: baseUrl });\r\n const completion = await openai.chat.completions.create({\r\n model,\r\n messages: [{ role: \"user\", content: prompt }],\r\n max_tokens: options?.maxTokens ?? 8192,\r\n });\r\n return extractAssistantText(completion);\r\n}\r\n\r\nexport { getLLMConfig } from \"./llmConfig.js\";\r\nexport type { LLMConfig } from \"./llmConfig.js\";\r\n","// HTML 列表解析器:专注于从列表页提取多个条目(支持自定义函数、LLM 两种模式)\r\n\r\nimport { createHash } from \"node:crypto\";\r\nimport { applyPurify } from \"../fetcher/purify.js\";\r\nimport { chatJson } from \"../../../../core/llm.js\";\r\nimport { getLLMConfig } from \"../../../../core/llmConfig.js\";\r\nimport type { FeedItem } from \"../../../../types/feedItem.js\";\r\nimport type { ParsedEntry } from \"./types.js\";\r\n\r\n\r\n// 使用 sha256 生成 guid\r\nfunction generateGuid(link: string): string {\r\n return createHash(\"sha256\").update(link).digest(\"hex\");\r\n}\r\n\r\n\r\n/** 解析结果结构(类似 RSS feed 结构) */\r\nexport interface ParsedListResult {\r\n /** 解析出的条目列表 */\r\n items: FeedItem[];\r\n /** 源 URL */\r\n url?: string;\r\n /** 解析模式 */\r\n mode?: ParserMode;\r\n}\r\n\r\n\r\n/** 自定义解析函数:输入 HTML 和 URL,返回 ParsedEntry 数组 */\r\nexport type CustomParserFn = (html: string, url: string) => Promise<ParsedEntry[]> | ParsedEntry[];\r\n\r\n\r\n/** LLM 解析配置 */\r\nexport interface LLMParserConfig {\r\n /** LLM API 端点,不传则从环境变量 OPENAI_BASE_URL 读取 */\r\n apiUrl?: string;\r\n /** API Key,不传则从环境变量 OPENAI_API_KEY 读取 */\r\n apiKey?: string;\r\n /** 模型名称,不传则从环境变量 OPENAI_MODEL 读取 */\r\n model?: string;\r\n /** 提示词模板,{html} 会被替换为 HTML 内容 */\r\n prompt?: string;\r\n}\r\n\r\n\r\n/** 解析模式 */\r\nexport type ParserMode = \"custom\" | \"llm\";\r\n\r\n\r\n/** Parser 配置 */\r\nexport interface ParserConfig {\r\n /** 源 URL,用于提取相对链接和发布日期 */\r\n url?: string;\r\n /** 解析模式:custom(自定义函数)、llm(LLM API),默认根据 customParser 或 llmConfig 自动判断 */\r\n mode?: ParserMode;\r\n /** 自定义解析函数,mode 为 \"custom\" 时必需 */\r\n customParser?: CustomParserFn;\r\n /** LLM 解析配置,mode 为 \"llm\" 时使用(可从环境变量读取) */\r\n llmConfig?: LLMParserConfig;\r\n /** 是否包含详细内容(content),默认 false(仅摘要) */\r\n includeContent?: boolean;\r\n /** 解析前是否净化 HTML(移除 script/style/nav 等无关标签),llm 模式默认 true,custom 模式不适用 */\r\n purify?: boolean;\r\n}\r\n\r\n\r\n// 使用 LLM 解析 HTML,返回 ParsedEntry 数组\r\nasync function parseWithLLM(html: string, url: string, config: LLMParserConfig): Promise<ParsedEntry[]> {\r\n const prompt =\r\n config.prompt ??\r\n `你是一个专业的 HTML 内容提取助手。请仔细分析以下 HTML 页面,提取所有可点击的内容条目(如文章、笔记、帖子、视频等)。\r\n\r\n要求:\r\n1. 返回 JSON 对象,格式为 {\"items\": [...]}\r\n2. 每个条目必须包含以下字段:\r\n - title: 标题(必填)\r\n - link: 完整链接(必填)。**必须从 HTML 的 href 原样提取**:若为绝对路径(以 / 开头)则直接使用;若为相对路径则补全为完整 URL。当前页: ${url}\r\n - description: 摘要或描述(200字内,必填)\r\n - author: 作者(可选)\r\n - published: 发布日期 ISO 字符串(可选)\r\n3. 如果页面是列表页,提取所有列表项\r\n4. 如果页面是详情页,将整个页面作为一个条目提取\r\n5. 如果页面是用户主页/个人资料页:\r\n - 优先尝试提取该用户发布的内容列表\r\n - 如果无法提取列表,至少提取用户基本信息作为一个条目(title 为用户名称或页面标题,description 为用户简介或页面描述,link 为当前 URL)\r\n6. 如果页面是单页应用(SPA),尝试从以下位置提取数据:\r\n - <title> 标签中的页面标题\r\n - <meta name=\"description\"> 或 <meta property=\"og:description\"> 中的描述\r\n - <meta property=\"og:title\"> 中的标题\r\n - <link rel=\"canonical\" href=\"...\"> 中的规范链接(优先使用)\r\n - <script> 标签中的 JSON 数据(搜索包含 \"title\", \"description\", \"user\", \"author\", \"items\", \"list\" 等关键词的 JSON)\r\n - 任何包含 href 属性的 <a> 标签,**link 必须使用 href 的原始值**(以 / 开头的路径表示从站根算起)\r\n - 任何包含文本内容的元素\r\n7. **重要**:即使页面看起来是空的 SPA,也必须至少返回一个条目:\r\n - 从 <title> 提取标题(如果没有则使用 URL)\r\n - 从 <meta> 标签提取描述(如果没有则使用 \"页面内容通过 JavaScript 动态加载\")\r\n - link 使用当前 URL: ${url}\r\n - 如果页面 URL 包含 \"user\" 或 \"profile\",可以推断这是一个用户主页,title 可以是 \"用户主页\",description 可以是页面 URL\r\n\r\n**强制要求**:绝对不能返回空数组 {\"items\": []},至少要返回一个包含页面基本信息的条目。\r\n\r\nHTML:\r\n${html}`;\r\n const parsed = await chatJson(\r\n prompt,\r\n { apiKey: config.apiKey, apiUrl: config.apiUrl, model: config.model },\r\n { debugLabel: \"parseWithLLM\" }\r\n );\r\n let entries: ParsedEntry[] = [];\r\n if (parsed.items && Array.isArray(parsed.items)) {\r\n entries = parsed.items as ParsedEntry[];\r\n } else if (parsed.entries && Array.isArray(parsed.entries)) {\r\n entries = parsed.entries as ParsedEntry[];\r\n } else if (Array.isArray(parsed)) {\r\n entries = parsed as ParsedEntry[];\r\n } else if (parsed && typeof parsed === \"object\") {\r\n entries = [parsed as unknown as ParsedEntry];\r\n }\r\n return entries.map((e) => {\r\n const raw = e.link || url;\r\n if (raw.startsWith(\"http\")) return { ...e, link: raw };\r\n const path = raw.startsWith(\"/\") ? raw : `/${raw}`;\r\n try {\r\n const base = new URL(url);\r\n return { ...e, link: new URL(path, base.origin).href };\r\n } catch {\r\n return { ...e, link: new URL(raw, url).href };\r\n }\r\n });\r\n}\r\n\r\n\r\n// 将 ParsedEntry 转为 FeedItem(不包含详细内容)\r\nfunction toFeedItem(entry: ParsedEntry, includeContent: boolean): FeedItem {\r\n return {\r\n guid: entry.guid ?? generateGuid(entry.link),\r\n title: entry.title,\r\n link: entry.link,\r\n pubDate: entry.published ? new Date(entry.published) : new Date(),\r\n author: entry.author ? [entry.author] : undefined,\r\n summary: entry.description,\r\n content: includeContent ? entry.content : undefined,\r\n imageUrl: entry.imageUrl,\r\n };\r\n}\r\n\r\n\r\n/** 解析 HTML 列表页,返回包含 items 的对象(类似 RSS feed 结构) */\r\nexport async function parseHtml(html: string, config: ParserConfig = {}): Promise<ParsedListResult> {\r\n const { url = \"\", mode, customParser, llmConfig, includeContent = false, purify } = config;\r\n let entries: ParsedEntry[] = [];\r\n const actualMode = mode ?? (llmConfig != null ? \"llm\" : customParser != null ? \"custom\" : \"llm\");\r\n if (actualMode === \"llm\") {\r\n if (llmConfig == null && !getLLMConfig().apiKey) {\r\n throw new Error('mode 为 \"llm\" 时必须提供 llmConfig,或在后台「设置 → LLM」/ OPENAI_API_KEY 中配置 Key');\r\n }\r\n const htmlForLLM = applyPurify(html, purify !== false);\r\n entries = await parseWithLLM(htmlForLLM, url, llmConfig ?? {});\r\n } else if (actualMode === \"custom\") {\r\n if (customParser == null) {\r\n throw new Error('mode 为 \"custom\" 时必须提供 customParser');\r\n }\r\n entries = await customParser(html, url);\r\n if (!Array.isArray(entries)) {\r\n entries = [entries];\r\n }\r\n } else {\r\n throw new Error(`不支持的解析模式: ${actualMode}`);\r\n }\r\n const items = entries.map((e) => toFeedItem(e, includeContent));\r\n return {\r\n items,\r\n url,\r\n mode: actualMode,\r\n };\r\n}\r\n","// 站点抽象接口:声明 URL 模式及 fetchItems 能力(WebSource 专用)\n\nimport type { PluginHostDeps } from \"../../../plugins/hostDeps.js\";\nimport type { AuthFlow, CheckAuthFn } from \"../../auth/index.js\";\nimport type { RefreshInterval } from \"../../../utils/refreshInterval.js\";\nimport type { FeedItem } from \"../../../types/feedItem.js\";\n\n\n/** 框架注入给插件的调用上下文,提供浏览器抓取工具与默认正文提取 */\nexport interface SiteContext {\n /** 缓存目录 */\n cacheDir?: string;\n /** 是否无头浏览器 */\n headless?: boolean;\n /** 代理地址 */\n proxy?: string;\n /** 与 SourceContext.deps 相同,宿主注入依赖,用户插件勿从 npm 直接 import */\n deps: PluginHostDeps;\n /**\n * 用浏览器抓取指定 URL,返回渲染后的 HTML。\n * 插件需要访问网页时调用此方法,框架负责浏览器管理与 cookie 注入。\n */\n fetchHtml(\n url: string,\n opts?: {\n waitMs?: number;\n purify?: boolean;\n waitForSelector?: string;\n waitForSelectorTimeoutMs?: number;\n /** 使用导航 HTTP 响应原文(适合 RSS/XML),见 RequestConfig.useHttpResponseBody */\n useHttpResponseBody?: boolean;\n }\n ): Promise<{ html: string; finalUrl: string; status: number }>;\n /**\n * 默认正文提取:拉取 item.link 的 HTML 后用 Readability 提取正文,合并回条目并返回。\n * 可在 fetchItems 内按需 await ctx.extractItem(单条) 使用。\n */\n extractItem(\n item: FeedItem,\n opts?: { cacheKey?: string }\n ): Promise<FeedItem>;\n}\n\n\n/** 站点抽象接口:声明该站点支持的 URL 模式及数据获取能力 */\nexport interface Site {\n /** 站点标识,如 \"xiaohongshu\"、\"lingowhale\" */\n readonly id: string;\n /** 列表页 URL 匹配模式,{placeholder} 匹配路径段 */\n readonly listUrlPattern: string | RegExp;\n /** 条目有效时间窗口;不填默认 1day */\n readonly refreshInterval?: RefreshInterval;\n /** 代理地址;不填则使用 env HTTP_PROXY */\n readonly proxy?: string;\n /**\n * 核心能力:给定 sourceId,返回条目列表。\n * 插件自行决定如何获取数据(调用 ctx.fetchHtml、直接 fetch API 等均可)。\n */\n fetchItems(sourceId: string, ctx: SiteContext): Promise<FeedItem[]>;\n /** 认证:检查是否已登录 */\n checkAuth?: CheckAuthFn | null;\n /** 认证:登录页 URL */\n loginUrl?: string | null;\n /** 认证:域名,cookies 保存在 domains/{domain}.json */\n domain?: string | null;\n /** 认证:等待登录超时毫秒,默认 300000 */\n loginTimeoutMs?: number | null;\n /** 认证:轮询 checkAuth 间隔毫秒,默认 2000 */\n pollIntervalMs?: number | null;\n}\n\n\n/** 将字符串 URL 模式转为正则:{xxx} → [^/]+,末尾允许 ?query */\nfunction patternToRegex(pattern: string | RegExp): RegExp {\n if (pattern instanceof RegExp) return pattern;\n const pathOnly = pattern.split(\"?\")[0];\n const pl = \"<<<__PL__>>>\";\n const s = pathOnly\n .replace(/\\{[^}]*\\}/g, pl)\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (c) => \"\\\\\" + c)\n .replace(new RegExp(pl.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"g\"), \"[^/]+\");\n return new RegExp(\"^\" + s + \"(\\\\?.*)?$\");\n}\n\n\n/** 从 Site 扁平字段构建 AuthFlow,无需登录时返回 undefined */\nexport function toAuthFlow(site: Site): AuthFlow | undefined {\n if (!site.checkAuth || !site.loginUrl) return undefined;\n return {\n checkAuth: site.checkAuth,\n loginUrl: site.loginUrl,\n domain: site.domain ?? undefined,\n loginTimeoutMs: site.loginTimeoutMs ?? undefined,\n pollIntervalMs: site.pollIntervalMs ?? undefined,\n };\n}\n\n\n/** 判断 URL 是否匹配站点的 listUrlPattern */\nexport function matchesListUrl(site: Site, url: string): boolean {\n try {\n return patternToRegex(site.listUrlPattern).test(url);\n } catch {\n return false;\n }\n}\n\n\n/** 根据 listUrlPattern 自动计算 URL 匹配具体度(不匹配返回 -1) */\nexport function computeSpecificity(site: Site, url: string): number {\n if (!matchesListUrl(site, url)) return -1;\n const p = site.listUrlPattern;\n if (typeof p === \"string\") {\n const pathOnly = p.split(\"?\")[0];\n return 1000 + pathOnly.split(\"/\").filter(Boolean).length;\n }\n return p.source.length;\n}\n\n\n/** 根据 URL 查找匹配的站点实例,返回具体度最高的站点 */\nexport function getSiteByUrl(url: string, sites: Site[]): Site | undefined {\n const matched = sites\n .map((s) => ({ site: s, score: computeSpecificity(s, url) }))\n .filter((x) => x.score >= 0)\n .sort((a, b) => b.score - a.score);\n return matched[0]?.site;\n}\n","// HTTP 错误:router 捕获后返回对应状态码页面\n\nexport class AuthRequiredError extends Error {\n constructor(message = \"需要登录\") {\n super(message);\n this.name = \"AuthRequiredError\";\n }\n}\n\n\nexport class NotFoundError extends Error {\n constructor(message = \"未找到\") {\n super(message);\n this.name = \"NotFoundError\";\n }\n}\n","// 插件加载器:从 app/plugins/builtin/ 与 .rssany/plugins/ 加载 Site / Source 插件\r\n\r\nimport { readdir } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport { join } from \"node:path\";\r\nimport type { Site } from \"../scraper/sources/web/site.js\";\r\nimport type { Source } from \"../scraper/sources/types.js\";\r\nimport { BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR } from \"../config/paths.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\n\r\n/** LLM 帮助函数,由 feeder 注入到插件上下文 */\r\nexport interface PluginLlm {\r\n chatJson: (prompt: string, config?: Record<string, unknown>, options?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>>;\r\n chatText: (prompt: string, config?: Record<string, unknown>, options?: { maxTokens?: number; debugLabel?: string }) => Promise<string>;\r\n}\r\n\r\n/** DB 帮助函数,由 feeder 注入到插件上下文 */\r\nexport interface PluginDb {\r\n getSystemTags: () => Promise<string[]>;\r\n}\r\n\r\n/** 插件统一上下文,由 feeder 在执行前注入 llm / db */\r\nexport interface PluginContext {\r\n sourceUrl?: string;\r\n llm?: PluginLlm;\r\n db?: PluginDb;\r\n [key: string]: unknown;\r\n}\r\n\r\n\r\nconst PLUGIN_EXTENSIONS = [\".rssany.js\", \".rssany.ts\"];\r\n\r\n\r\n/** 判断对象是否为有效的 Site 实现 */\r\nfunction isValidSite(obj: unknown): obj is Site {\r\n if (obj == null || typeof obj !== \"object\") return false;\r\n const s = obj as Record<string, unknown>;\r\n return (\r\n typeof s.id === \"string\" &&\r\n (typeof s.listUrlPattern === \"string\" || s.listUrlPattern instanceof RegExp) &&\r\n typeof s.fetchItems === \"function\"\r\n );\r\n}\r\n\r\n/** 判断对象是否为有效的 Source 实现 */\r\nfunction isValidSource(obj: unknown): obj is Source {\r\n if (obj == null || typeof obj !== \"object\") return false;\r\n const s = obj as Record<string, unknown>;\r\n return (\r\n typeof s.id === \"string\" &&\r\n (typeof s.pattern === \"string\" || s.pattern instanceof RegExp) &&\r\n typeof s.fetchItems === \"function\" &&\r\n s.listUrlPattern === undefined\r\n );\r\n}\r\n\r\n/** 从单个目录加载 Site / Source 插件,并记录每个 Site / Source 的文件路径 */\r\nasync function loadSourcePluginsFromDir(\r\n dir: string,\r\n label: string,\r\n): Promise<{\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n}> {\r\n const siteEntries: Array<{ site: Site; filePath: string }> = [];\r\n const sources: Array<{ source: Source; filePath: string }> = [];\r\n let entries: { name: string; isFile: () => boolean }[];\r\n try {\r\n const raw = await readdir(dir, { withFileTypes: true, encoding: \"utf-8\" });\r\n entries = raw as { name: string; isFile: () => boolean }[];\r\n } catch {\r\n return { siteEntries, sources };\r\n }\r\n for (const e of entries) {\r\n const name = String(e.name);\r\n if (!e.isFile()) continue;\r\n if (!PLUGIN_EXTENSIONS.some((ext) => name.endsWith(ext))) continue;\r\n const filePath = join(dir, name);\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n const plugin = mod.default ?? mod;\r\n if (isValidSite(plugin)) {\r\n siteEntries.push({ site: plugin, filePath });\r\n } else if (isValidSource(plugin)) {\r\n sources.push({ source: plugin, filePath });\r\n } else {\r\n logger.warn(\"plugin\", \"插件未实现 Site 或 Source 接口,已跳过\", { label, name });\r\n }\r\n } catch (err) {\r\n logger.warn(\"plugin\", \"插件加载失败\", { label, name, err: err instanceof Error ? err.message : String(err) });\r\n }\r\n }\r\n return { siteEntries, sources };\r\n}\r\n\r\n\r\nasync function loadBuiltinAndUser(): Promise<{\r\n builtin: {\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n };\r\n user: {\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n };\r\n}> {\r\n const [builtin, user] = await Promise.all([\r\n loadSourcePluginsFromDir(BUILTIN_PLUGINS_DIR, \"builtin\"),\r\n loadSourcePluginsFromDir(USER_PLUGINS_DIR, \"user\"),\r\n ]);\r\n return { builtin, user };\r\n}\r\n\r\n\r\n/** Site / Source 插件 id → 当前生效的文件路径(用户覆盖内置后的路径) */\r\nconst pluginSitePaths = new Map<string, string>();\r\n\r\n/** 将 Source 插件路径写入 pathMap(不与 Site id 冲突;用户覆盖内置 Source) */\r\nfunction mergeSourcePluginPaths(\r\n siteIds: Set<string>,\r\n pathMap: Map<string, string>,\r\n builtinSources: Array<{ source: Source; filePath: string }>,\r\n userSources: Array<{ source: Source; filePath: string }>,\r\n): void {\r\n for (const { source, filePath } of builtinSources) {\r\n if (siteIds.has(source.id)) {\r\n logger.warn(\"plugin\", \"Source 插件 id 与 Site 插件冲突,已忽略 Source 路径\", { sourceId: source.id });\r\n continue;\r\n }\r\n pathMap.set(source.id, filePath);\r\n }\r\n for (const { source, filePath } of userSources) {\r\n if (siteIds.has(source.id)) {\r\n logger.warn(\"plugin\", \"Source 插件 id 与 Site 插件冲突,已忽略 Source 路径\", { sourceId: source.id });\r\n continue;\r\n }\r\n if (pathMap.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n pathMap.set(source.id, filePath);\r\n }\r\n}\r\n\r\n/** 根据插件 id 获取其源文件路径(用于详情页编辑,仅当前生效的插件有路径) */\r\nexport function getPluginFilePath(id: string): string | undefined {\r\n return pluginSitePaths.get(id);\r\n}\r\n\r\n/** 加载所有 Site 插件;用户插件可覆盖同 id 内置 */\r\nexport async function loadPlugins(): Promise<Site[]> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const merged = new Map<string, Site>();\r\n const pathMap = new Map<string, string>();\r\n for (const { site, filePath } of builtin.siteEntries) {\r\n merged.set(site.id, site);\r\n pathMap.set(site.id, filePath);\r\n }\r\n for (const { site, filePath } of user.siteEntries) {\r\n if (merged.has(site.id)) logger.info(\"plugin\", \"用户插件覆盖同名内置插件\", { pluginId: site.id });\r\n merged.set(site.id, site);\r\n pathMap.set(site.id, filePath);\r\n }\r\n mergeSourcePluginPaths(new Set(merged.keys()), pathMap, builtin.sources, user.sources);\r\n pluginSitePaths.clear();\r\n pathMap.forEach((path, id) => pluginSitePaths.set(id, path));\r\n return Array.from(merged.values());\r\n}\r\n\r\n\r\n/** 加载所有 Source 插件;用户插件可覆盖同 id */\r\nexport async function loadSourcePlugins(): Promise<Source[]> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const merged = new Map<string, Source>();\r\n for (const { source } of builtin.sources) merged.set(source.id, source);\r\n for (const { source } of user.sources) {\r\n if (merged.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n merged.set(source.id, source);\r\n }\r\n return Array.from(merged.values());\r\n}\r\n\r\n\r\n/** 加载 Site 与 Source:合并去重,供 initSources 使用;同时更新 pluginSitePaths */\r\nexport async function loadSiteAndSourcePlugins(): Promise<{ sites: Site[]; sources: Source[] }> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const siteMap = new Map<string, Site>();\r\n const pathMap = new Map<string, string>();\r\n for (const { site: s, filePath } of builtin.siteEntries) {\r\n siteMap.set(s.id, s);\r\n pathMap.set(s.id, filePath);\r\n }\r\n for (const { site: s, filePath } of user.siteEntries) {\r\n if (siteMap.has(s.id)) logger.info(\"plugin\", \"用户插件覆盖同名内置\", { pluginId: s.id });\r\n siteMap.set(s.id, s);\r\n pathMap.set(s.id, filePath);\r\n }\r\n mergeSourcePluginPaths(new Set(siteMap.keys()), pathMap, builtin.sources, user.sources);\r\n const sourceMap = new Map<string, Source>();\r\n for (const { source } of builtin.sources) sourceMap.set(source.id, source);\r\n for (const { source } of user.sources) {\r\n if (sourceMap.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n sourceMap.set(source.id, source);\r\n }\r\n pluginSitePaths.clear();\r\n pathMap.forEach((path, id) => pluginSitePaths.set(id, path));\r\n return { sites: Array.from(siteMap.values()), sources: Array.from(sourceMap.values()) };\r\n}\r\n","// 插件加载与包装:将 Site 插件包装为 Source 接口,注入 SiteContext 工具\n\nimport { fetchHtml as fetchHtmlFn, preCheckAuth } from \"./fetcher/index.js\";\nimport { extractHtml } from \"./extractor/index.js\";\nimport { parseHtml } from \"./parser/index.js\";\nimport { toAuthFlow, getSiteByUrl } from \"./site.js\";\nimport { AuthRequiredError } from \"../../auth/index.js\";\nimport type { Site, SiteContext } from \"./site.js\";\nimport type { Source, SourceContext } from \"../types.js\";\nimport type { FeedItem } from \"../../../types/feedItem.js\";\nimport { normalizeAuthor } from \"../../../types/feedItem.js\";\n\n\n/** 从 SourceContext + Site 构建注入了工具的 SiteContext */\nexport function buildSiteContext(site: Site, ctx: SourceContext): SiteContext {\n const proxy = ctx.proxy ?? site.proxy;\n const authFlow = toAuthFlow(site);\n return {\n cacheDir: ctx.cacheDir,\n headless: ctx.headless,\n proxy,\n deps: ctx.deps,\n async fetchHtml(url, opts) {\n const res = await fetchHtmlFn(url, {\n cacheDir: ctx.cacheDir,\n useCache: false,\n authFlow,\n headless: ctx.headless,\n proxy,\n waitAfterLoadMs: opts?.waitMs,\n purify: opts?.purify,\n waitForSelector: opts?.waitForSelector,\n waitForSelectorTimeoutMs: opts?.waitForSelectorTimeoutMs,\n useHttpResponseBody: opts?.useHttpResponseBody,\n });\n return { html: res.body, finalUrl: res.finalUrl ?? url, status: res.status };\n },\n async extractItem(item, opts) {\n const res = await fetchHtmlFn(item.link, {\n cacheDir: ctx.cacheDir,\n useCache: false,\n authFlow,\n headless: ctx.headless,\n proxy,\n });\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`默认正文提取失败: HTTP ${res.status} ${res.statusText} for ${item.link}`);\n }\n const extracted = await extractHtml(res.body, {\n url: res.finalUrl ?? item.link,\n cacheDir: ctx.cacheDir ?? undefined,\n mode: \"readability\",\n useCache: true,\n cacheKey: opts?.cacheKey,\n });\n const pubDate =\n extracted.pubDate != null\n ? typeof extracted.pubDate === \"string\"\n ? new Date(extracted.pubDate)\n : extracted.pubDate\n : item.pubDate;\n return {\n ...item,\n author: normalizeAuthor(extracted.author ?? item.author),\n title: extracted.title ?? item.title,\n summary: extracted.summary ?? item.summary,\n content: extracted.content ?? item.content,\n pubDate,\n };\n },\n };\n}\n\n\n/** 将 Site 插件包装为统一 Source 接口 */\nexport function createWebSource(site: Site): Source {\n const authFlow = toAuthFlow(site);\n return {\n id: site.id,\n pattern: site.listUrlPattern,\n priority: 50,\n refreshInterval: site.refreshInterval ?? undefined,\n proxy: site.proxy ?? undefined,\n preCheck: authFlow\n ? async (ctx: SourceContext) => {\n if (!ctx.cacheDir) return;\n const passed = await preCheckAuth(authFlow, ctx.cacheDir, {\n proxy: ctx.proxy,\n headless: ctx.headless,\n });\n if (!passed) throw new AuthRequiredError(`站点 ${site.id} 需要登录,请先执行 ensureAuth`);\n }\n : undefined,\n async fetchItems(sourceId: string, ctx: SourceContext): Promise<FeedItem[]> {\n return site.fetchItems(sourceId, buildSiteContext(site, ctx));\n },\n };\n}\n\n\n/** 通用 WebSource:兜底匹配所有 http/https URL,使用浏览器抓取 + LLM 解析 */\nexport const genericWebSource: Source = {\n id: \"generic\",\n pattern: /^https?:\\/\\//,\n priority: 200,\n async fetchItems(sourceId: string, ctx: SourceContext): Promise<FeedItem[]> {\n const res = await fetchHtmlFn(sourceId, {\n cacheDir: ctx.cacheDir,\n useCache: true,\n headless: ctx.headless,\n proxy: ctx.proxy,\n });\n // 304 Not Modified:浏览器用缓存,page.content() 仍会返回完整 HTML,视为成功\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`抓取失败: HTTP ${res.status} ${res.statusText}`);\n }\n const parsed = await parseHtml(res.body, {\n url: res.finalUrl ?? sourceId,\n });\n return parsed.items;\n },\n};\n\n\n/** 保存已加载的 Site 对象(供 auth 路由、调试路由直接访问) */\nconst loadedSites: Site[] = [];\n\n\n/** 更新已加载站点列表(由 sources/index.ts 调用) */\nexport function setLoadedSites(sites: Site[]): void {\n loadedSites.length = 0;\n loadedSites.push(...sites);\n}\n\n\n/** 根据 id 获取底层 Site(用于 auth 路由) */\nexport function getWebSite(id: string): Site | undefined {\n return loadedSites.find((s) => s.id === id);\n}\n\n\n/** 获取所有已加载的 Site(用于插件列表 API) */\nexport function getPluginSites(): Site[] {\n return loadedSites.filter((s) => s.id !== \"generic\");\n}\n\n\n/** 根据 URL 获取最具体匹配的 Site(用于调试路由) */\nexport function getBestSite(url: string): Site | undefined {\n return getSiteByUrl(url, loadedSites);\n}\n\n\nexport type { Site, SiteContext } from \"./site.js\";\nexport { toAuthFlow, computeSpecificity } from \"./site.js\";\nexport { loadPlugins } from \"../../../plugins/loader.js\";\n","// 宿主应用解析的常用依赖,注入到 SourceContext / SiteContext.deps\r\n// 用户目录下的 .rssany/plugins 内脚本无法解析应用 node_modules,须通过 ctx.deps 使用\r\n\r\nimport { parse, NodeType } from \"node-html-parser\";\r\nimport { createHash } from \"node:crypto\";\r\nimport RssParser from \"rss-parser\";\r\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\r\nimport { ImapFlow } from \"imapflow\";\r\nimport { simpleParser } from \"mailparser\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nexport const PLUGIN_HOST_DEPS = {\r\n parseHtml: parse,\r\n NodeType,\r\n createHash,\r\n RssParser,\r\n HttpsProxyAgent,\r\n ImapFlow,\r\n simpleParser,\r\n logger,\r\n} as const;\r\n\r\nexport type PluginHostDeps = typeof PLUGIN_HOST_DEPS;\r\n","import { PLUGIN_HOST_DEPS } from \"../../plugins/hostDeps.js\";\r\nimport type { SourceContext } from \"./types.js\";\r\nimport { fetchHtml as fetchHtmlFn } from \"./web/fetcher/index.js\";\r\n\r\n/** 构造带 deps 的信源上下文(抓取、preCheck、插件 fetchItems 均须使用) */\r\nexport function buildSourceContext(partial: {\r\n cacheDir?: string;\r\n headless?: boolean;\r\n proxy?: string;\r\n}): SourceContext {\r\n const { cacheDir, headless, proxy } = partial;\r\n return {\r\n ...partial,\r\n deps: PLUGIN_HOST_DEPS,\r\n async fetchHtml(url, opts) {\r\n const res = await fetchHtmlFn(url, {\r\n cacheDir,\r\n useCache: false,\r\n headless,\r\n proxy,\r\n waitAfterLoadMs: opts?.waitMs,\r\n purify: opts?.purify,\r\n waitForSelector: opts?.waitForSelector,\r\n waitForSelectorTimeoutMs: opts?.waitForSelectorTimeoutMs,\r\n useHttpResponseBody: opts?.useHttpResponseBody,\r\n });\r\n return { html: res.body, finalUrl: res.finalUrl ?? url, status: res.status };\r\n },\r\n };\r\n}\r\n","// 统一信源注册表:汇聚 WebSource 插件、Source 插件(RSS/Email)、genericWebSource,提供 getSource 查找入口\n\nimport { createWebSource, genericWebSource, setLoadedSites } from \"./web/index.js\";\nimport { loadSiteAndSourcePlugins } from \"../../plugins/loader.js\";\nimport type { Source } from \"./types.js\";\nimport { logger } from \"../../core/logger/index.js\";\nexport { buildSourceContext } from \"./context.js\";\n\n/** 所有已注册的信源(按 priority 排序后迭代匹配) */\nexport const registeredSources: Source[] = [];\n\n/** 将字符串 URL 模式转为正则:{placeholder} 匹配单个路径段,末尾允许 query */\nfunction sourcePatternToRegex(pattern: string | RegExp): RegExp {\n if (pattern instanceof RegExp) return pattern;\n const pathOnly = pattern.split(\"?\")[0];\n const pl = \"<<<__PL__>>>\";\n const escaped = pathOnly\n .replace(/\\{[^}]*\\}/g, pl)\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (c) => \"\\\\\" + c)\n .replace(new RegExp(pl.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"g\"), \"[^/]+\");\n return new RegExp(\"^\" + escaped + \"(\\\\?.*)?$\");\n}\n\n\n/** 根据 sourceId 查找匹配度最高的 Source;按 priority 升序,优先用 match 否则用 pattern */\nexport function getSource(sourceId: string): Source {\n for (const source of registeredSources) {\n const matches = source.match ? source.match(sourceId) : (() => {\n try {\n return sourcePatternToRegex(source.pattern).test(sourceId);\n } catch {\n return false;\n }\n })();\n if (matches) return source;\n }\n return genericWebSource;\n}\n\n\n/** 根据 id 精确查找 Source(用于内部调试) */\nexport function getSourceById(id: string): Source | undefined {\n return registeredSources.find((s) => s.id === id);\n}\n\n\n/** 初始化所有信源:加载 sources 插件、构建注册表(pipeline 为固定流程,见 app/pipeline/) */\nexport async function initSources(): Promise<void> {\n const siteResult = await loadSiteAndSourcePlugins();\n const { sites, sources: sourcePlugins } = siteResult;\n setLoadedSites(sites);\n registeredSources.length = 0;\n const webSources = sites.map((s) => createWebSource(s));\n const all: Source[] = [\n ...sourcePlugins,\n ...webSources,\n genericWebSource,\n ];\n all.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));\n registeredSources.push(...all);\n logger.info(\"scheduler\", \"信源已注册\", {\n total: registeredSources.length,\n siteCount: sites.length,\n sourcePluginCount: sourcePlugins.length,\n });\n}\n","// 订阅配置类型:一个订阅由多个信源组成,支持网页、RSS、邮件、API 等多种类型\n\nimport type { RefreshInterval } from \"../../utils/refreshInterval.js\";\n\n\n/** 信源类型枚举 */\nexport type SourceType = \"web\" | \"rss\" | \"email\";\n\n\n/**\n * 单个信源配置\n *\n * ref 格式示例:\n * web / rss → https://sspai.com/feed\n * → https://xiaohongshu.com/user/profile/xxx\n * email → imaps://user:password@imap.gmail.com:993/INBOX\n * → imap://user:password@imap.qq.com:143/INBOX\n * → imaps://me%40gmail.com:app-password@imap.gmail.com/INBOX\n * (用户名含 @ 时用 %40 编码;Gmail 需使用「应用专用密码」)\n */\nexport interface SubscriptionSource {\n /** 信源标识符:HTTP(S) URL、imaps?:// 连接串、或插件自定义协议(如 lingowhale://) */\n ref: string;\n /** 信源类型(省略时由 getSource 自动识别) */\n type?: SourceType;\n /** 显示名称,用于界面展示和报错信息(省略则显示 ref) */\n label?: string;\n /** 简短描述,用于界面展示信源用途或内容说明 */\n description?: string;\n /** 单源有效时间窗口覆盖:优先级高于 Source 声明;不填则使用 Source 声明 */\n refresh?: RefreshInterval;\n /** 单源 cron 表达式(如 \"0 9 * * *\" 每天 9:00);有值时优先于 refresh */\n cron?: string;\n /** 单源代理覆盖:优先级高于 Source.proxy;不填则使用 Source.proxy 或 env HTTP_PROXY */\n proxy?: string;\n /** 信源权重,用于排序与优先级控制;默认 0,值越大优先级越高 */\n weight?: number;\n}\n\n\n/** sources.json 标准格式:扁平信源列表,无频道/订阅层级 */\nexport interface SourcesFile {\n /** 所有要抓取的信源 */\n sources: SubscriptionSource[];\n}\n\n\n/** 从旧格式({ url })或新格式({ ref })中提取信源标识符,确保向后兼容 */\nexport function resolveRef(src: SubscriptionSource | { url?: string; ref?: string }): string {\n return (src as SubscriptionSource).ref ?? (src as { url?: string }).url ?? \"\";\n}\n","// 全局 HTTP(S) 代理:读写 .rssany/config.json 的 globalProxy;与单源代理合并见 subscription/getEffectiveProxyForListUrl\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\n\r\nexport async function readGlobalProxyFromConfig(): Promise<string | undefined> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { globalProxy?: unknown };\r\n if (typeof j.globalProxy === \"string\") {\r\n const t = j.globalProxy.trim();\r\n return t.length > 0 ? t : undefined;\r\n }\r\n } catch {\r\n /* 无文件或解析失败 */\r\n }\r\n return undefined;\r\n}\r\n\r\n/** 写入或清空(空串则删除键) */\r\nexport async function saveGlobalProxyToConfig(proxy: string): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n //\r\n }\r\n const t = proxy.trim();\r\n if (t.length === 0) {\r\n delete root.globalProxy;\r\n } else {\r\n root.globalProxy = t;\r\n }\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n}\r\n\r\n/** 插件 Site 声明的 proxy 优先于 config 全局代理 */\r\nexport async function resolveProxyForSite(site: { proxy?: string }): Promise<string | undefined> {\r\n const s = site.proxy?.trim();\r\n if (s) return s;\r\n return readGlobalProxyFromConfig();\r\n}\r\n","// 信源配置模块:读取 .rssany/sources.json(扁平信源列表,供 scheduler 使用)\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { SOURCES_CONFIG_PATH } from \"../../config/paths.js\";\r\nimport type { SubscriptionSource, SourcesFile } from \"./types.js\";\r\nimport { resolveRef } from \"./types.js\";\r\nimport type { Source } from \"../sources/types.js\";\r\nimport { readGlobalProxyFromConfig } from \"../../config/globalProxy.js\";\r\n\r\nexport type { SubscriptionSource, SourcesFile } from \"./types.js\";\r\nexport { resolveRef } from \"./types.js\";\r\n\r\n\r\n/** 从 .rssany/sources.json 加载;支持新格式 { sources: [] } 与旧格式 { [id]: { sources: [] } },统一扁平为 SubscriptionSource[] */\r\nasync function loadSourcesFile(): Promise<SubscriptionSource[]> {\r\n try {\r\n const raw = await readFile(SOURCES_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as unknown;\r\n if (!parsed || typeof parsed !== \"object\") return [];\r\n if (Array.isArray((parsed as SourcesFile).sources)) {\r\n return (parsed as SourcesFile).sources.filter((s) => resolveRef(s));\r\n }\r\n const entries = Object.values(parsed as Record<string, { sources?: SubscriptionSource[] }>);\r\n const list: SubscriptionSource[] = [];\r\n for (const entry of entries) {\r\n if (entry && Array.isArray(entry.sources)) {\r\n for (const s of entry.sources) {\r\n if (resolveRef(s)) list.push(s);\r\n }\r\n }\r\n }\r\n return list;\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n\r\n/** 获取所有信源(扁平列表),供 scheduler 使用 */\r\nexport async function getAllSources(): Promise<SubscriptionSource[]> {\r\n return loadSourcesFile();\r\n}\r\n\r\n/** 去重后的 ref 列表(与 sources.json 一致),供 Feed / 聚合查询使用 */\r\nexport async function getAllSubscriptionRefs(): Promise<string[]> {\r\n const list = await loadSourcesFile();\r\n const seen = new Set<string>();\r\n const out: string[] = [];\r\n for (const s of list) {\r\n const r = resolveRef(s);\r\n if (r && !seen.has(r)) {\r\n seen.add(r);\r\n out.push(r);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n\r\n/** 将扁平列表写回 sources.json(新格式);供 raw API 写入 */\r\nexport async function saveSourcesFile(sources: SubscriptionSource[]): Promise<void> {\r\n await writeFile(\r\n SOURCES_CONFIG_PATH,\r\n JSON.stringify({ sources }, null, 2) + \"\\n\",\r\n \"utf-8\"\r\n );\r\n}\r\n\r\n\r\n/**\r\n * 代理优先级:sources.json 单源 → 插件 Source.proxy → config.json globalProxy →(未写入则由 fetcher resolveProxy 使用 HTTP_PROXY)\r\n */\r\nexport async function getEffectiveProxyForListUrl(listUrl: string, source: Source): Promise<string | undefined> {\r\n const list = await getAllSources();\r\n const sub = list.find((s) => resolveRef(s) === listUrl);\r\n const fromSub = sub?.proxy?.trim();\r\n if (fromSub) return fromSub;\r\n const fromPlugin = source.proxy?.trim();\r\n if (fromPlugin) return fromPlugin;\r\n return readGlobalProxyFromConfig();\r\n}\r\n\r\n/** 读取 sources.json 原始内容(用于 GET /api/sources/raw);旧格式会转为新格式返回 */\r\nexport async function getSourcesRaw(): Promise<string> {\r\n try {\r\n const raw = await readFile(SOURCES_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as unknown;\r\n if (!parsed || typeof parsed !== \"object\") return JSON.stringify({ sources: [] }, null, 2);\r\n if (Array.isArray((parsed as SourcesFile).sources)) {\r\n return raw;\r\n }\r\n const list = await loadSourcesFile();\r\n return JSON.stringify({ sources: list }, null, 2);\r\n } catch {\r\n return JSON.stringify({ sources: [] }, null, 2);\r\n }\r\n}\r\n","/**\r\n * Pipeline 步骤:LLM 内容质量过滤\r\n *\r\n * 判定为低质量(无实质信息、垃圾占位、纯广告等)时标记条目为丢弃;\r\n * feeder 会从数据库删除该条并不再输出到 RSS。\r\n *\r\n * 需在 .rssany/config.json 的 pipeline.steps 中启用,且配置 OPENAI_API_KEY 等 LLM 环境变量。\r\n * 建议放在 tagger / translator 之前以省 token。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { markPipelineDrop } from \"../types/feedItem.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nconst MAX_BODY_CHARS = 4000;\r\n\r\nconst SYSTEM = `你是信息质量评估助手。根据标题、摘要与正文片段,判断该条是否值得保留在订阅信息流中。\r\n- 保留(keep: true):有实质信息、观点、技术内容、新闻价值,或对读者有参考意义。\r\n- 过滤(keep: false):明显垃圾广告、纯日期/站务占位、无意义一句话、乱码或空白、纯导航无内容、重复无信息量的模板句。\r\n- 只输出 JSON,格式:{\"keep\": true 或 false, \"reason\": \"一句话中文理由\"}`;\r\n\r\nexport interface QualityFilterContext {\r\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\r\n}\r\n\r\nfunction combinedForJudge(item: FeedItem): string {\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? \"\").trim();\r\n let body = (item.content ?? \"\").trim();\r\n if (body.length > MAX_BODY_CHARS) body = body.slice(0, MAX_BODY_CHARS) + \"\\n\\n[... 正文已截断 ...]\";\r\n const parts: string[] = [];\r\n if (title) parts.push(`标题:\\n${title}`);\r\n if (summary) parts.push(`摘要:\\n${summary}`);\r\n if (body) parts.push(`正文:\\n${body}`);\r\n return parts.join(\"\\n\\n---\\n\\n\");\r\n}\r\n\r\n/** 无 LLM 时跳过 */\r\nexport function qualityFilterMatch(_item: FeedItem, ctx: QualityFilterContext): boolean {\r\n return !!ctx.llm;\r\n}\r\n\r\nexport async function runQualityFilter(item: FeedItem, ctx: QualityFilterContext): Promise<FeedItem> {\r\n if (!ctx.llm) return item;\r\n\r\n const text = combinedForJudge(item);\r\n if (!text.trim()) return item;\r\n\r\n const prompt = `${SYSTEM}\\n\\n请评估以下条目:\\n\\n${text}`;\r\n\r\n let res: Record<string, unknown>;\r\n try {\r\n res = await ctx.llm.chatJson(prompt, undefined, { maxTokens: 256, debugLabel: \"qualityFilter\" });\r\n } catch {\r\n return item;\r\n }\r\n\r\n const isKeep = res.keep === true || res.keep === \"true\";\r\n const isDrop = res.keep === false || res.keep === \"false\";\r\n if (isKeep) return item;\r\n if (!isDrop) return item;\r\n\r\n const reason = typeof res.reason === \"string\" ? res.reason.trim() : \"\";\r\n logger.info(\"pipeline\", \"质量过滤移除条目\", { link: item.link, reason: reason || undefined });\r\n return markPipelineDrop(item);\r\n}\r\n","/**\n * Pipeline 步骤:LLM 自动打标签\n *\n * 从系统标签库(话题 tags 并集)中选取匹配的标签写入 item.tags。\n * 模型建议的 tag 若不在系统标签库中则忽略。\n *\n * 需要配置 OPENAI_API_KEY(或等效 LLM 环境变量)。\n */\n\nimport type { FeedItem } from \"../types/feedItem.js\";\n\nconst SYSTEM = `你是一个信息分类助手。根据文章标题和摘要,从候选标签库中选出最匹配的标签,最多 5 个。\n- 只能使用候选标签库中已有的标签,不要输出库中不存在的标签。\n- 如果没有合适的标签,输出空数组 {\"tags\": []},不要硬选不相关的标签。\n- 只输出 JSON,格式:{\"tags\": [\"tag1\", \"tag2\", ...]}`;\n\nexport interface TaggerContext {\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\n db?: { getSystemTags: () => Promise<string[]> };\n}\n\n/** 跳过已有 tags 的条目;无 LLM 则跳过 */\nexport function taggerMatch(item: FeedItem, ctx: TaggerContext): boolean {\n return !item.tags?.length && !!ctx.llm;\n}\n\nexport async function runTagger(item: FeedItem, ctx: TaggerContext): Promise<FeedItem> {\n if (!ctx.db) return item;\n const systemTags = await ctx.db.getSystemTags();\n if (!systemTags.length || !ctx.llm) return item;\n\n const candidateList = `候选标签库(只能从中选取):\\n${systemTags.join(\", \")}\\n\\n`;\n const prompt = `${SYSTEM}\\n\\n${candidateList}文章标题:${item.title ?? \"\"}\\n文章摘要:${item.summary ?? item.content?.slice(0, 300) ?? \"(无摘要)\"}`;\n\n let suggested: string[];\n try {\n const res = await ctx.llm.chatJson(prompt, undefined, { maxTokens: 256, debugLabel: \"tagger\" });\n suggested = Array.isArray(res.tags) ? res.tags.filter((t): t is string => typeof t === \"string\") : [];\n } catch {\n return item;\n }\n\n if (!suggested.length) return item;\n\n const set = new Set(systemTags.map((t) => t.toLowerCase()));\n const confirmed = suggested.filter((t) => set.has(t.toLowerCase()));\n\n if (confirmed.length) {\n item.tags = [...new Set([...(item.tags ?? []), ...confirmed])];\n }\n\n return item;\n}\n","/**\r\n * Pipeline 步骤:LLM 翻译为中文\r\n *\r\n * 将条目的 title、summary、content 翻译为中文,写入 item.translations[\"zh-CN\"]。\r\n * 路由支持 lng=zh-CN 时可据此返回译文。\r\n *\r\n * 需要配置 OPENAI_API_KEY(或等效 LLM 环境变量)。\r\n *\r\n * 粗判已为中文的条目不调 LLM(省 token);展示时 lng=zh-CN 无译文则回退原文。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\n\r\nconst ZH_CN = \"zh-CN\";\r\nconst MAX_CONTENT_CHARS = 6000;\r\n/** 用于语言检测的正文上限(与 runTranslator 截断策略大致一致) */\r\nconst DETECT_CONTENT_PREFIX = 2000;\r\n\r\nconst SYSTEM = `你是一个专业翻译助手。将用户提供的英文(或其他语言)内容翻译为简体中文。\r\n- 保持专业、准确、流畅。\r\n- 若原文已是中文,则保持原样或轻微润色。\r\n- 只输出 JSON,格式:{\"title\": \"译文标题\", \"summary\": \"译文摘要\", \"content\": \"译文正文\"}\r\n- 若某字段为空或用户未提供,对应输出空字符串 \"\"。`;\r\n\r\nexport interface TranslatorContext {\r\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\r\n}\r\n\r\nfunction combinedTextForDetection(item: FeedItem): string {\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? item.content?.slice(0, 500) ?? \"\").trim();\r\n const content = (item.content ?? \"\").trim().slice(0, DETECT_CONTENT_PREFIX);\r\n return `${title}\\n${summary}\\n${content}`;\r\n}\r\n\r\n/**\r\n * 粗判主体是否已是中文(汉字占比高),用于跳过 LLM。\r\n * 含日文假名 / 韩文谚文时不视为「已是中文」,避免误跳日文/韩文条目。\r\n */\r\nexport function isLikelyChineseContent(text: string): boolean {\r\n const t = text.trim();\r\n if (t.length < 8) return false;\r\n if (/[\\u3040-\\u309f\\u30a0-\\u30ff]/.test(t)) return false;\r\n if (/[\\uac00-\\ud7af]/.test(t)) return false;\r\n\r\n const cjk = (t.match(/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/g) ?? []).length;\r\n const latin = (t.match(/[A-Za-z]/g) ?? []).length;\r\n const letterish = cjk + latin;\r\n if (letterish < 12) return false;\r\n return cjk / letterish >= 0.55;\r\n}\r\n\r\n/** 跳过已有 zh-CN 译文、无 LLM、或粗判已为中文的条目 */\r\nexport function translatorMatch(item: FeedItem, ctx: TranslatorContext): boolean {\r\n const hasZh = item.translations?.[ZH_CN];\r\n if (hasZh || !ctx.llm) return false;\r\n if (isLikelyChineseContent(combinedTextForDetection(item))) return false;\r\n return true;\r\n}\r\n\r\nexport async function runTranslator(item: FeedItem, ctx: TranslatorContext): Promise<FeedItem> {\r\n if (!ctx.llm) return item;\r\n\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? item.content?.slice(0, 500) ?? \"\").trim();\r\n const content = (item.content ?? \"\").trim();\r\n const contentTruncated =\r\n content.length > MAX_CONTENT_CHARS ? content.slice(0, MAX_CONTENT_CHARS) + \"\\n\\n[... 内容已截断 ...]\" : content;\r\n\r\n if (!title && !summary && !content) return item;\r\n\r\n const parts: string[] = [];\r\n if (title) parts.push(`标题:\\n${title}`);\r\n if (summary) parts.push(`摘要:\\n${summary}`);\r\n if (contentTruncated) parts.push(`正文:\\n${contentTruncated}`);\r\n\r\n const prompt = `${SYSTEM}\\n\\n请翻译以下内容:\\n\\n${parts.join(\"\\n\\n---\\n\\n\")}`;\r\n\r\n let res: Record<string, unknown>;\r\n try {\r\n res = await ctx.llm.chatJson(prompt, undefined, {\r\n maxTokens: Math.min(8192, Math.ceil((title.length + summary.length + contentTruncated.length) * 1.5)),\r\n debugLabel: \"translator\",\r\n });\r\n } catch {\r\n return item;\r\n }\r\n\r\n const tTitle = typeof res.title === \"string\" ? res.title.trim() : \"\";\r\n const tSummary = typeof res.summary === \"string\" ? res.summary.trim() : \"\";\r\n const tContent = typeof res.content === \"string\" ? res.content.trim() : \"\";\r\n\r\n if (!tTitle && !tSummary && !tContent) return item;\r\n\r\n item.translations = item.translations ?? {};\r\n item.translations[ZH_CN] = {\r\n title: tTitle || undefined,\r\n summary: tSummary || undefined,\r\n content: tContent || undefined,\r\n };\r\n\r\n return item;\r\n}\r\n","/**\r\n * Pipeline 配置:从 .rssany/config.json 的 pipeline 块读取\r\n *\r\n * 格式:{ \"pipeline\": { \"steps\": [{ \"id\": \"qualityFilter\", \"enabled\": false }, ...] } }\r\n * - steps 数组顺序即执行顺序,enabled: false 的步骤跳过\r\n */\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"../config/paths.js\";\r\n\r\nexport interface PipelineStepConfig {\r\n id: string;\r\n enabled: boolean;\r\n}\r\n\r\nexport interface PipelineConfig {\r\n steps: PipelineStepConfig[];\r\n}\r\n\r\n/** 默认配置(入库前) */\r\nexport const DEFAULT_PIPELINE_STEPS: PipelineStepConfig[] = [\r\n { id: \"qualityFilter\", enabled: false },\r\n { id: \"tagger\", enabled: false },\r\n { id: \"translator\", enabled: false },\r\n];\r\n\r\n/** 可用步骤 id */\r\nexport const PIPELINE_STEP_IDS = [\"qualityFilter\", \"tagger\", \"translator\"] as const;\r\n\r\nfunction parseSteps(rawSteps: unknown[]): PipelineStepConfig[] {\r\n const steps: PipelineStepConfig[] = [];\r\n const seen = new Set<string>();\r\n for (const s of rawSteps) {\r\n if (s && typeof s === \"object\" && typeof (s as { id?: unknown }).id === \"string\") {\r\n const obj = s as { id: string; enabled?: unknown };\r\n const id = obj.id.trim();\r\n if (!id || seen.has(id)) continue;\r\n seen.add(id);\r\n const enabled = obj.enabled;\r\n steps.push({\r\n id,\r\n enabled: enabled !== false && enabled !== 0,\r\n });\r\n }\r\n }\r\n return steps;\r\n}\r\n\r\n/** 与默认步骤表对齐:保证 JSON 里出现的步骤 id 齐全、顺序与默认一致,已有项保留 enabled */\r\nfunction mergeWithDefaultSteps(userSteps: PipelineStepConfig[]): PipelineStepConfig[] {\r\n const map = new Map(userSteps.map((s) => [s.id, s]));\r\n return DEFAULT_PIPELINE_STEPS.map((def) => {\r\n const u = map.get(def.id);\r\n return { id: def.id, enabled: u ? u.enabled : def.enabled };\r\n });\r\n}\r\n\r\n/** 读取 pipeline 配置,缺失时返回默认;已存在的 config 会与默认步骤合并,使新步骤(如 qualityFilter)始终出现在列表中 */\r\nexport async function loadPipelineConfig(): Promise<PipelineConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as { pipeline?: { steps?: unknown[] } };\r\n const rawSteps = Array.isArray(parsed?.pipeline?.steps) ? parsed.pipeline.steps : [];\r\n const steps = mergeWithDefaultSteps(parseSteps(rawSteps));\r\n if (steps.length > 0) return { steps };\r\n } catch {\r\n // 文件不存在或解析失败\r\n }\r\n return { steps: [...DEFAULT_PIPELINE_STEPS] };\r\n}\r\n\r\n/** 保存 pipeline 配置到 config.json(合并其他块,不覆盖) */\r\nexport async function savePipelineConfig(config: PipelineConfig): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 文件不存在或解析失败,使用空对象\r\n }\r\n root.pipeline = { steps: config.steps };\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2), \"utf-8\");\r\n}\r\n","/**\r\n * Pipeline:入库前固定处理链(翻译、打标签等)\r\n *\r\n * 与 plugins 同级别,作为固定流程而非插件系统。\r\n * 步骤开关与排序由 .rssany/config.json 的 pipeline.steps 配置。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { qualityFilterMatch, runQualityFilter } from \"./qualityFilter.js\";\r\nimport { taggerMatch, runTagger } from \"./tagger.js\";\r\nimport { translatorMatch, runTranslator } from \"./translator.js\";\r\nimport { loadPipelineConfig } from \"./config.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nexport interface PipelineContext {\r\n sourceUrl?: string;\r\n llm?: {\r\n chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>>;\r\n chatText: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<string>;\r\n };\r\n db?: { getSystemTags: () => Promise<string[]> };\r\n}\r\n\r\n/** Pipeline 步骤注册表 */\r\nconst STEP_REGISTRY: Record<string, { match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }> = {\r\n qualityFilter: { match: qualityFilterMatch, run: runQualityFilter },\r\n tagger: { match: taggerMatch, run: runTagger },\r\n translator: { match: translatorMatch, run: runTranslator },\r\n};\r\n\r\n/** 根据配置解析出要执行的步骤(按配置顺序,仅启用且存在的) */\r\nasync function getResolvedSteps(): Promise<Array<{ id: string; match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }>> {\r\n const config = await loadPipelineConfig();\r\n const out: Array<{ id: string; match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }> = [];\r\n for (const { id, enabled } of config.steps) {\r\n if (!enabled) continue;\r\n const step = STEP_REGISTRY[id];\r\n if (!step) {\r\n logger.debug(\"pipeline\", \"未知步骤已跳过\", { id });\r\n continue;\r\n }\r\n out.push({ id, ...step });\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * 对单条条目执行 pipeline\r\n */\r\nexport async function runPipeline(\r\n item: FeedItem,\r\n ctx: PipelineContext\r\n): Promise<FeedItem> {\r\n const steps = await getResolvedSteps();\r\n let current = item;\r\n for (const step of steps) {\r\n if (!step.match(current, ctx)) continue;\r\n try {\r\n current = await step.run(current, ctx);\r\n } catch (err) {\r\n logger.warn(\"pipeline\", \"步骤执行失败\", {\r\n stepId: step.id,\r\n item_url: item.link,\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n return current;\r\n}\r\n\r\n/**\r\n * 对多条条目执行 pipeline\r\n */\r\nexport async function runPipelineBatch(\r\n items: FeedItem[],\r\n ctx: PipelineContext\r\n): Promise<FeedItem[]> {\r\n const out: FeedItem[] = [];\r\n for (let i = 0; i < items.length; i++) {\r\n out.push(await runPipeline(items[i], ctx));\r\n }\r\n return out;\r\n}\r\n","// 将频道 + 条目构建为 RSS 2.0 XML\n\nimport type { RssChannel, RssEntry } from \"./types.js\";\n\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n\nfunction isoToRfc822(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toUTCString();\n}\n\n\nfunction buildItem(entry: RssEntry): string {\n const title = escapeXml(entry.title ?? \"\");\n const link = escapeXml(entry.link ?? \"\");\n const descRaw = entry.description ?? \"\";\n const desc = descRaw.includes(\"<\") || descRaw.includes(\">\")\n ? `<![CDATA[${descRaw.replace(/\\]\\]>/g, \"]]]]><![CDATA[>\")}]]>`\n : escapeXml(descRaw);\n const pubDate = entry.published ? escapeXml(isoToRfc822(entry.published)) : \"\";\n const guid = entry.guid ?? entry.link ?? \"\";\n const guidEscaped = escapeXml(guid);\n let buf = ` <item>\\n <title>${title}</title>\\n <link>${link}</link>\\n <description>${desc}</description>\\n`;\n if (pubDate) buf += ` <pubDate>${pubDate}</pubDate>\\n`;\n if (entry.imageUrl?.trim()) {\n const encUrl = escapeXml(entry.imageUrl.trim());\n const encType = escapeXml(entry.imageType?.trim() || \"image/jpeg\");\n buf += ` <enclosure url=\"${encUrl}\" length=\"0\" type=\"${encType}\"/>\\n`;\n }\n buf += ` <guid isPermaLink=\"true\">${guidEscaped}</guid>\\n`;\n buf += ` </item>\\n`;\n return buf;\n}\n\n\nexport function buildRssXml(channel: RssChannel, entries: RssEntry[]): string {\n const title = escapeXml(channel.title);\n const link = escapeXml(channel.link);\n const desc = escapeXml(channel.description ?? \"\");\n const lang = escapeXml(channel.language ?? \"zh-CN\");\n const now = new Date().toUTCString();\n const items = entries.map(buildItem).join(\"\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${link}</link>\n <description>${desc}</description>\n <language>${lang}</language>\n <lastBuildDate>${now}</lastBuildDate>\n\n${items} </channel>\n</rss>`;\n}\n","// 事件总线:进程内单例 EventEmitter,供 db 层 emit、HTTP 层 subscribe\n\nimport { EventEmitter } from \"node:events\";\n\n\n/** feed:updated 事件的载荷 */\nexport interface FeedUpdatedEvent {\n sourceUrl: string;\n newCount: number;\n}\n\n\n/** 全局单例事件总线,setMaxListeners 避免 SSE 多连接时的警告 */\nexport const eventBus = new EventEmitter();\neventBus.setMaxListeners(200);\n\n\n/** 向事件总线广播新条目事件 */\nexport function emitFeedUpdated(payload: FeedUpdatedEvent): void {\n eventBus.emit(\"feed:updated\", payload);\n}\n\n\n/** 订阅 feed:updated 事件,返回取消订阅函数 */\nexport function onFeedUpdated(fn: (e: FeedUpdatedEvent) => void): () => void {\n eventBus.on(\"feed:updated\", fn);\n return () => eventBus.off(\"feed:updated\", fn);\n}\n","// 投递目标:.rssany/config.json 的 deliver.url / deliver.token;非空 url 时在写库(及 pipeline)之后额外 POST 到该 URL\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\n\r\nexport interface DeliverConfig {\r\n url: string;\r\n /** 与下游 Gateway(如 agidaily `data/token.txt`)一致:非空时请求头带 `Authorization: Bearer <token>` */\r\n token: string;\r\n}\r\n\r\nexport async function getDeliverConfig(): Promise<DeliverConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { deliver?: { url?: string; token?: string } };\r\n const u = j?.deliver?.url;\r\n const t = j?.deliver?.token;\r\n return {\r\n url: typeof u === \"string\" ? u.trim() : \"\",\r\n token: typeof t === \"string\" ? t.trim() : \"\",\r\n };\r\n } catch {\r\n return { url: \"\", token: \"\" };\r\n }\r\n}\r\n\r\n/** 非空表示启用投递(不影响是否写库) */\r\nexport async function getDeliverUrl(): Promise<string> {\r\n return (await getDeliverConfig()).url;\r\n}\r\n\r\nexport async function saveDeliverConfig(config: DeliverConfig): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 无文件则新建\r\n }\r\n const prev = root.deliver;\r\n const base =\r\n typeof prev === \"object\" && prev !== null && !Array.isArray(prev)\r\n ? { ...(prev as Record<string, unknown>) }\r\n : {};\r\n const url = config.url.trim();\r\n const token = config.token.trim();\r\n const next: Record<string, unknown> = { ...base, url };\r\n if (token) next.token = token;\r\n else delete next.token;\r\n root.deliver = next;\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n}\r\n","// 将条目 POST 到下游:JSON 体为 { sourceRef, items }\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nfunction feedItemsToPayload(items: FeedItem[]): unknown[] {\r\n return items.map((i) => ({\r\n guid: i.guid,\r\n title: i.title,\r\n link: i.link,\r\n pubDate: pubDateToIsoOrNull(i.pubDate) ?? new Date().toISOString(),\r\n author: i.author,\r\n summary: i.summary,\r\n content: i.content,\r\n tags: i.tags,\r\n sourceRef: i.sourceRef,\r\n translations: i.translations,\r\n }));\r\n}\r\n\r\nexport interface PostDeliverOptions {\r\n /** 非空时设置 `Authorization: Bearer <token>`(与 agidaily `POST /api/gateway/items` 一致) */\r\n bearerToken?: string;\r\n}\r\n\r\n/** POST { sourceRef, items } 到投递 URL;失败时打日志并抛出 */\r\nexport async function postDeliverItems(\r\n url: string,\r\n sourceRef: string,\r\n items: FeedItem[],\r\n options?: PostDeliverOptions,\r\n): Promise<void> {\r\n if (!url.trim() || items.length === 0) return;\r\n const body = JSON.stringify({ sourceRef, items: feedItemsToPayload(items) });\r\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\r\n const t = options?.bearerToken?.trim();\r\n if (t) headers.Authorization = `Bearer ${t}`;\r\n const res = await fetch(url.trim(), {\r\n method: \"POST\",\r\n headers,\r\n body,\r\n signal: AbortSignal.timeout(120_000),\r\n });\r\n if (!res.ok) {\r\n const text = await res.text().catch(() => \"\");\r\n throw new Error(`HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : \"\"}`);\r\n }\r\n}\r\n\r\nexport async function postDeliverItemsSafe(\r\n url: string,\r\n sourceRef: string,\r\n items: FeedItem[],\r\n options?: PostDeliverOptions,\r\n): Promise<void> {\r\n try {\r\n await postDeliverItems(url, sourceRef, items, options);\r\n } catch (err) {\r\n logger.warn(\"deliver\", \"投递失败\", {\r\n sourceRef,\r\n count: items.length,\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n}\r\n","// Feeder:根据 URL 生成 RSS,直接通过 Source 接口驱动,与具体信源解耦\r\n\r\nimport { cacheKey, cacheKeyFromCron } from \"../core/cacher/index.js\";\r\nimport { getSource } from \"../scraper/sources/index.js\";\r\nimport { runPipeline } from \"../pipeline/index.js\";\r\nimport { AuthRequiredError } from \"../scraper/auth/index.js\";\r\nimport { buildRssXml } from \"./rss.js\";\r\nimport type { RssChannel, RssEntry } from \"./types.js\";\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { normalizeAuthor, getEffectiveItemFields, isPipelineDroppedItem, pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport type { FeederConfig } from \"./types.js\";\r\nimport { buildSourceContext } from \"../scraper/sources/context.js\";\r\nimport { upsertItems, updateItemContent, getSystemTags, deleteItem } from \"../db/index.js\";\r\nimport { emitFeedUpdated } from \"../core/events/index.js\";\r\nimport { chatJson, chatText } from \"../core/llm.js\";\r\nimport type { PipelineContext } from \"../pipeline/index.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { getDeliverConfig } from \"../config/deliver.js\";\r\nimport { postDeliverItemsSafe } from \"../deliver/post.js\";\r\nimport { getEffectiveProxyForListUrl } from \"../scraper/subscription/index.js\";\r\nimport { canonicalHttpSourceRef } from \"../utils/httpSourceRef.js\";\r\n\r\n/** 主动拉取默认有头;仅显式 headless:true 时用无头;其余场景沿用 config.headless */\r\nfunction resolveHeadlessForFeeder(config: FeederConfig): boolean | undefined {\r\n if (config.force === true) {\r\n return config.headless === true ? true : false;\r\n }\r\n return config.headless;\r\n}\r\n\r\n/** 根据 listUrl + items 构建 RssChannel(与 generateAndCache 一致,用于缓存命中时实时生成 XML);lng 存在时设置 channel.language */\r\nfunction buildChannelFromItems(listUrl: string, items: FeedItem[], lng?: string | null): RssChannel {\r\n const channel: RssChannel = {\r\n title: items[0]?.author?.length ? `${items[0].author[0]} 的订阅` : \"RSS 订阅\",\r\n link: listUrl,\r\n description: `来自 ${listUrl} 的订阅`,\r\n };\r\n if (lng) channel.language = lng;\r\n return channel;\r\n}\r\n\r\n\r\n/** 根据条目生成 RssEntry:有 lng 且存在译文则用译文,否则用原文;有正文用 content,否则用 summary */\r\nfunction toRssEntry(item: FeedItem, lng?: string | null): RssEntry {\r\n const eff = getEffectiveItemFields(item, lng);\r\n const hasContent = eff.content != null && eff.content !== \"\";\r\n const desc = hasContent ? eff.content : eff.summary;\r\n return {\r\n title: eff.title,\r\n link: item.link,\r\n description: desc,\r\n guid: item.guid,\r\n published: pubDateToIsoOrNull(item.pubDate) ?? undefined,\r\n imageUrl: item.imageUrl,\r\n };\r\n}\r\n\r\n\r\n/** 同一 URL 的首次生成任务去重(仅在初始 fetch+parse 阶段有效) */\r\nconst generatingKeys = new Map<string, Promise<{ items: FeedItem[] }>>();\r\n\r\n\r\n/** Pipeline 上下文 */\r\nconst pipelineCtx: PipelineContext = {\r\n llm: { chatJson, chatText } as PipelineContext[\"llm\"],\r\n db: { getSystemTags },\r\n};\r\n\r\n/** 单条 pipeline */\r\nasync function runPipelineOnItem(item: FeedItem, ctx: { sourceUrl: string }): Promise<FeedItem> {\r\n return runPipeline(item, { ...pipelineCtx, ...ctx });\r\n}\r\n\r\n\r\n/** 执行生成流程:抓取 → 入库 → 对新条目跑 pipeline → 更新库 */\r\nasync function generateAndCache(\r\n listUrl: string,\r\n key: string,\r\n config: FeederConfig,\r\n proxy: string | undefined,\r\n): Promise<{ items: FeedItem[] }> {\r\n const { cacheDir = \"cache\" } = config;\r\n const headless = resolveHeadlessForFeeder(config);\r\n const source = getSource(listUrl);\r\n const ctx = buildSourceContext({ cacheDir, headless, proxy });\r\n let items: FeedItem[];\r\n try {\r\n items = await source.fetchItems(listUrl, ctx);\r\n } catch (err) {\r\n generatingKeys.delete(key);\r\n const message = err instanceof Error ? err.message : String(err);\r\n logger.error(\"scraper\", \"抓取失败\", { source_url: listUrl, err: message });\r\n throw err;\r\n }\r\n const sourceRefStored = canonicalHttpSourceRef(listUrl);\r\n items.forEach((i) => {\r\n i.sourceRef = sourceRefStored;\r\n i.author = normalizeAuthor(i.author);\r\n });\r\n generatingKeys.delete(key);\r\n logger.info(\"scraper\", \"抓取成功\", { source_url: listUrl, count: items.length });\r\n\r\n const { url: deliverUrl, token: deliverToken } = await getDeliverConfig();\r\n\r\n let newCount = 0;\r\n let newIds = new Set<string>();\r\n const upsertResult = await upsertItems(items).catch((err) => {\r\n logger.warn(\"db\", \"upsertItems 失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) });\r\n return { newCount: 0, newIds: new Set<string>() };\r\n });\r\n newCount = upsertResult.newCount;\r\n newIds = upsertResult.newIds;\r\n\r\n let pipelineDroppedNew = 0;\r\n const shouldRunPipelineRow = (guid: string) => newIds.has(guid);\r\n\r\n for (let i = 0; i < items.length; i++) {\r\n if (!shouldRunPipelineRow(items[i].guid)) continue;\r\n const processed = await runPipelineOnItem(items[i], { sourceUrl: sourceRefStored });\r\n items[i] = processed;\r\n if (isPipelineDroppedItem(processed)) {\r\n await deleteItem(processed.guid).catch((err) =>\r\n logger.warn(\"db\", \"质量过滤后删除条目失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) })\r\n );\r\n pipelineDroppedNew++;\r\n } else {\r\n updateItemContent(processed).catch((err) =>\r\n logger.warn(\"db\", \"updateItemContent 失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) })\r\n );\r\n }\r\n }\r\n if (newCount > 0) {\r\n emitFeedUpdated({ sourceUrl: sourceRefStored, newCount: newCount - pipelineDroppedNew });\r\n }\r\n const out = items.filter((i) => !isPipelineDroppedItem(i));\r\n if (deliverUrl && out.length > 0) {\r\n await postDeliverItemsSafe(deliverUrl, sourceRefStored, out, {\r\n bearerToken: deliverToken || undefined,\r\n });\r\n }\r\n return { items: out };\r\n}\r\n\r\n\r\n/** 根据 list URL 获取条目列表:按 cron 或 refresh 策略生成时间窗口 key 用于去重,每次均重新抓取 */\r\nexport async function getItems(listUrl: string, config: FeederConfig = {}): Promise<{ items: FeedItem[]; fromCache: boolean }> {\r\n const source = getSource(listUrl);\r\n const proxy = await getEffectiveProxyForListUrl(listUrl, source);\r\n const headless = resolveHeadlessForFeeder(config);\r\n const key = config.cron\r\n ? cacheKeyFromCron(listUrl, config.cron)\r\n : cacheKey(listUrl, config.refreshInterval ?? source.refreshInterval ?? \"1day\");\r\n if (source.preCheck != null) {\r\n try {\r\n await source.preCheck(\r\n buildSourceContext({\r\n cacheDir: config.cacheDir ?? \"cache\",\r\n headless,\r\n proxy,\r\n }),\r\n );\r\n } catch (err) {\r\n if (err instanceof AuthRequiredError) throw err;\r\n throw err;\r\n }\r\n }\r\n let task = config.force ? undefined : generatingKeys.get(key);\r\n if (!task) {\r\n task = generateAndCache(listUrl, key, config, proxy);\r\n if (!config.force) generatingKeys.set(key, task);\r\n }\r\n const { items } = await task;\r\n return { items, fromCache: false };\r\n}\r\n\r\n\r\n/** 将 FeedItem[] 转为 RSS 2.0 XML 字符串;可选 channelTitle/channelDesc 覆盖默认 */\r\nexport function feedItemsToRssXml(\r\n items: FeedItem[],\r\n listUrl: string,\r\n lng?: string | null,\r\n opts?: { channelTitle?: string; channelDesc?: string }\r\n): string {\r\n const channel = buildChannelFromItems(listUrl, items, lng);\r\n if (opts?.channelTitle) channel.title = opts.channelTitle;\r\n if (opts?.channelDesc) channel.description = opts.channelDesc;\r\n return buildRssXml(channel, items.map((it) => toRssEntry(it, lng)));\r\n}\r\n","// 通用调度器:cron 定时任务、重试与分组并发\r\n\r\nimport { CronExpressionParser } from \"cron-parser\";\r\nimport { schedule as cronSchedule, validate as cronValidate } from \"node-cron\";\r\n\r\n/** 校验 cron 表达式是否合法 */\r\nexport const validateCron = cronValidate;\r\n\r\n/** 调度任务:返回 Promise,失败时由调度器负责重试 */\r\nexport type ScheduledTask = () => Promise<void>;\r\n\r\n\r\n/** 调度选项 */\r\nexport interface ScheduleOptions {\r\n /** cron 表达式;不填或空表示一次性任务 */\r\n cron?: string;\r\n /** 失败时重试次数,默认 0 */\r\n retries?: number;\r\n /** 重试间隔(毫秒),默认 5000 */\r\n retryDelayMs?: number;\r\n /** 分组并发数,首次使用该分组时生效,默认 10 */\r\n concurrency?: number;\r\n /** 定时任务:注册后是否立即执行一次,默认 false */\r\n runNow?: boolean;\r\n /** 一次性任务:是否插队到队首,默认 false */\r\n priority?: boolean;\r\n /** 分组名(内部使用,由 schedule 注入) */\r\n group?: string;\r\n}\r\n\r\n\r\n/** 分组配置 */\r\nexport interface GroupConfig {\r\n /** 该组最大并发数 */\r\n concurrency: number;\r\n}\r\n\r\n\r\n/** 已注册任务 */\r\ninterface RegisteredTask {\r\n id: string;\r\n cronExpr: string;\r\n task: ScheduledTask;\r\n options: ScheduleOptions;\r\n stop: () => void;\r\n /** 上次触发时间(用于计算下次执行) */\r\n lastRunTime: number;\r\n}\r\n\r\n\r\n/** 分组队列项 */\r\ninterface QueuedItem {\r\n id: string;\r\n task: ScheduledTask | (() => Promise<unknown>);\r\n options: ScheduleOptions;\r\n resolve?: () => void;\r\n resolveValue?: (value: unknown) => void;\r\n rejectValue?: (err: unknown) => void;\r\n}\r\n\r\n\r\nconst tasks = new Map<string, RegisteredTask>();\r\nconst groups = new Map<string, { config: GroupConfig; running: number; queue: QueuedItem[]; completedCount: number }>();\r\nconst DEFAULT_RETRY_DELAY_MS = 5000;\r\nconst DEFAULT_GROUP_CONCURRENCY = 10;\r\n\r\n\r\nasync function runWithRetry(\r\n task: ScheduledTask,\r\n options: ScheduleOptions\r\n): Promise<void> {\r\n const retries = options.retries ?? 0;\r\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= retries; attempt++) {\r\n try {\r\n await task();\r\n return;\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < retries) {\r\n await new Promise((r) => setTimeout(r, retryDelayMs));\r\n }\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\nasync function runWithRetryAndResult<T>(\r\n task: () => Promise<T>,\r\n options: ScheduleOptions\r\n): Promise<T> {\r\n const retries = options.retries ?? 0;\r\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= retries; attempt++) {\r\n try {\r\n return await task();\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < retries) {\r\n await new Promise((r) => setTimeout(r, retryDelayMs));\r\n }\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\nfunction ensureGroup(group: string, concurrency: number): void {\r\n if (!groups.has(group)) {\r\n groups.set(group, { config: { concurrency }, running: 0, queue: [], completedCount: 0 });\r\n } else {\r\n const g = groups.get(group)!;\r\n g.config.concurrency = concurrency;\r\n if (g.completedCount === undefined) g.completedCount = 0;\r\n }\r\n}\r\n\r\n\r\nfunction enqueueAndProcess(\r\n group: string,\r\n id: string,\r\n task: ScheduledTask | (() => Promise<unknown>),\r\n options: ScheduleOptions,\r\n resolve?: () => void,\r\n priority?: boolean,\r\n resolveValue?: (value: unknown) => void,\r\n rejectValue?: (err: unknown) => void\r\n): void {\r\n const g = groups.get(group);\r\n if (!g) return;\r\n g.queue = g.queue.filter((it) => it.id !== id);\r\n const item: QueuedItem = { id, task, options, resolve, resolveValue, rejectValue };\r\n if (priority) {\r\n g.queue.unshift(item);\r\n } else {\r\n g.queue.push(item);\r\n }\r\n processGroupQueue(group);\r\n}\r\n\r\n\r\nfunction processGroupQueue(group: string): void {\r\n const g = groups.get(group);\r\n if (!g || g.running >= g.config.concurrency || g.queue.length === 0) return;\r\n const item = g.queue.shift()!;\r\n g.running += 1;\r\n const done = () => {\r\n g.running -= 1;\r\n g.completedCount = (g.completedCount ?? 0) + 1;\r\n processGroupQueue(group);\r\n };\r\n if (item.resolveValue != null || item.rejectValue != null) {\r\n runWithRetryAndResult(item.task as () => Promise<unknown>, item.options)\r\n .then((result) => {\r\n item.resolveValue?.(result);\r\n })\r\n .catch((err) => {\r\n item.rejectValue?.(err);\r\n })\r\n .finally(done);\r\n } else {\r\n runWithRetry(item.task as ScheduledTask, item.options)\r\n .catch(() => {})\r\n .finally(() => {\r\n item.resolve?.();\r\n done();\r\n });\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 调度任务:options.cron 有值时注册定时任务,否则一次性入队\r\n * @param group 分组名\r\n * @param id 任务唯一标识\r\n * @param task 异步任务函数\r\n * @param options cron 有值=定时任务,无值=一次性任务;可选 retries、retryDelayMs、concurrency、runNow、priority\r\n */\r\nexport function schedule(group: string, id: string, task: ScheduledTask, options: ScheduleOptions & { cron: string }): boolean;\r\nexport function schedule<T>(group: string, id: string, task: () => Promise<T>, options?: ScheduleOptions): Promise<T>;\r\nexport function schedule<T>(\r\n group: string,\r\n id: string,\r\n task: ScheduledTask | (() => Promise<T>),\r\n options: ScheduleOptions = {}\r\n): boolean | Promise<T> {\r\n const cronExpr = options.cron?.trim();\r\n ensureGroup(group, options.concurrency ?? groups.get(group)?.config.concurrency ?? DEFAULT_GROUP_CONCURRENCY);\r\n\r\n if (cronExpr && cronValidate(cronExpr)) {\r\n unschedule(id);\r\n const optsWithGroup = { ...options, group };\r\n const job = cronSchedule(cronExpr, () => {\r\n const reg = tasks.get(id);\r\n if (reg) reg.lastRunTime = Date.now();\r\n enqueueAndProcess(group, id, task as ScheduledTask, optsWithGroup);\r\n });\r\n tasks.set(id, {\r\n id,\r\n cronExpr,\r\n task: task as ScheduledTask,\r\n options: optsWithGroup,\r\n stop: () => job.stop(),\r\n lastRunTime: 0,\r\n });\r\n if (options.runNow) {\r\n runNow(id, true).catch(() => {});\r\n }\r\n return true;\r\n }\r\n\r\n return new Promise<T>((resolve, reject) => {\r\n enqueueAndProcess(\r\n group,\r\n id,\r\n task as () => Promise<unknown>,\r\n { ...options, group },\r\n undefined,\r\n options.priority ?? false,\r\n resolve as (v: unknown) => void,\r\n reject\r\n );\r\n });\r\n}\r\n\r\n\r\n/**\r\n * 取消任务\r\n */\r\nexport function unschedule(id: string): void {\r\n const reg = tasks.get(id);\r\n if (reg) {\r\n reg.stop();\r\n tasks.delete(id);\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 取消指定分组下的所有定时任务(不清理队列,用于 reschedule 前仅移除本组任务)\r\n */\r\nexport function unscheduleGroup(group: string): void {\r\n const ids = [...tasks.entries()].filter(([, reg]) => reg.options.group === group).map(([id]) => id);\r\n for (const id of ids) unschedule(id);\r\n}\r\n\r\n\r\n/**\r\n * 立即执行一次任务(不等待下次定时)\r\n * @param id 任务 id\r\n * @param priority 有分组时,true 表示插入队首优先执行,默认 false 插入队尾\r\n */\r\nexport function runNow(id: string, priority = false): Promise<void> {\r\n const reg = tasks.get(id);\r\n if (!reg) return Promise.resolve();\r\n reg.lastRunTime = Date.now();\r\n const group = reg.options.group;\r\n if (group) {\r\n return new Promise<void>((resolve) => {\r\n enqueueAndProcess(group, id, reg.task, reg.options, resolve, priority);\r\n });\r\n }\r\n return runWithRetry(reg.task, reg.options);\r\n}\r\n\r\n\r\n/**\r\n * 清空所有任务(含各分组队列中的待执行项)\r\n */\r\nexport function clearAll(): void {\r\n for (const [, reg] of tasks) {\r\n reg.stop();\r\n }\r\n tasks.clear();\r\n for (const g of groups.values()) {\r\n g.queue.length = 0;\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 获取已注册任务 id 列表\r\n */\r\nexport function getTaskIds(): string[] {\r\n return [...tasks.keys()];\r\n}\r\n\r\n\r\n/** 分组统计 */\r\nexport interface GroupStats {\r\n /** 正在执行的任务数 */\r\n running: number;\r\n /** 队列中等待的任务数 */\r\n queued: number;\r\n /** 该组最大并发数 */\r\n concurrency: number;\r\n /** 该组下已注册的定时任务数量 */\r\n scheduledCount: number;\r\n /** 已完成任务数(含定时任务每次执行 + 单次任务,进程启动后累计) */\r\n completedCount: number;\r\n /**\r\n * 下次任务时间(毫秒时间戳)\r\n * - 0:正在执行\r\n * - -1:无定时任务\r\n * - 其他:下次预计执行时间戳\r\n */\r\n nextRunTime: number;\r\n}\r\n\r\n\r\nfunction getNextRunForTask(reg: RegisteredTask): number {\r\n try {\r\n const base = reg.lastRunTime > 0 ? new Date(reg.lastRunTime) : new Date();\r\n const expr = CronExpressionParser.parse(reg.cronExpr, { currentDate: base });\r\n let next = expr.next().getTime();\r\n if (next <= Date.now()) {\r\n const retry = CronExpressionParser.parse(reg.cronExpr, { currentDate: new Date() });\r\n next = retry.next().getTime();\r\n }\r\n return next;\r\n } catch {\r\n return -1;\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 获取各分组的执行统计,用于管理页进度条等\r\n */\r\nexport function getGroupStats(): Record<string, GroupStats> {\r\n const result: Record<string, GroupStats> = {};\r\n for (const [name, g] of groups) {\r\n const groupTasks = [...tasks.values()].filter((t) => t.options.group === name);\r\n const scheduledCount = groupTasks.length;\r\n let nextRunTime: number;\r\n if (g.running > 0) {\r\n nextRunTime = 0;\r\n } else if (scheduledCount === 0) {\r\n nextRunTime = -1;\r\n } else {\r\n const times = groupTasks.map(getNextRunForTask).filter((t) => t > 0);\r\n nextRunTime = times.length > 0 ? Math.min(...times) : -1;\r\n }\r\n result[name] = {\r\n running: g.running,\r\n queued: g.queue.length,\r\n concurrency: g.config.concurrency,\r\n scheduledCount,\r\n completedCount: g.completedCount ?? 0,\r\n nextRunTime,\r\n };\r\n }\r\n return result;\r\n}\r\n\r\n\r\n","// 信源调度:根据 sources.json 中的信源 refresh 定时触发 getItems,使用通用调度器\r\n\r\nimport { watch } from \"node:fs\";\r\nimport { getAllSources } from \"../subscription/index.js\";\r\nimport { resolveRef } from \"../subscription/types.js\";\r\nimport { getItems } from \"../../feeder/index.js\";\r\nimport { SOURCES_CONFIG_PATH } from \"../../config/paths.js\";\r\nimport type { RefreshInterval } from \"../../utils/refreshInterval.js\";\r\nimport { refreshIntervalToCron } from \"../../utils/refreshInterval.js\";\r\nimport * as scheduler from \"../../scheduler/index.js\";\r\n\r\nconst DEFAULT_REFRESH: RefreshInterval = \"1day\";\r\nconst SOURCES_CONCURRENCY = 1;\r\n\r\nfunction createPullTask(ref: string, cacheDir: string, cronExpr: string): scheduler.ScheduledTask {\r\n return async () => {\r\n try {\r\n await getItems(ref, {\r\n cacheDir,\r\n cron: cronExpr,\r\n });\r\n } catch (err) {\r\n throw err;\r\n }\r\n };\r\n}\r\n\r\nexport const SOURCES_GROUP = \"sources\";\r\n\r\nasync function rescheduleSources(cacheDir: string, runNow: boolean): Promise<void> {\r\n scheduler.unscheduleGroup(SOURCES_GROUP);\r\n let sources: Awaited<ReturnType<typeof getAllSources>>;\r\n try {\r\n sources = await getAllSources();\r\n } catch {\r\n sources = [];\r\n }\r\n\r\n for (const src of sources) {\r\n const ref = resolveRef(src);\r\n if (!ref) continue;\r\n const cronExpr: string = src.cron\r\n ? src.cron\r\n : refreshIntervalToCron(src.refresh ?? DEFAULT_REFRESH);\r\n if (!scheduler.validateCron(cronExpr)) continue;\r\n scheduler.schedule(SOURCES_GROUP, ref, createPullTask(ref, cacheDir, cronExpr), {\r\n cron: cronExpr,\r\n retries: 2,\r\n retryDelayMs: 5000,\r\n concurrency: SOURCES_CONCURRENCY,\r\n runNow,\r\n });\r\n }\r\n}\r\n\r\nexport async function initScheduler(cacheDir: string): Promise<void> {\r\n await rescheduleSources(cacheDir, false);\r\n let debounceTimer: NodeJS.Timeout | null = null;\r\n try {\r\n const watcher = watch(SOURCES_CONFIG_PATH, () => {\r\n if (debounceTimer) clearTimeout(debounceTimer);\r\n debounceTimer = setTimeout(() => {\r\n rescheduleSources(cacheDir, false).catch(() => {});\r\n }, 500);\r\n });\r\n watcher.on(\"error\", () => {});\r\n } catch {\r\n /* sources.json 尚不存在 */\r\n }\r\n}\r\n","// 本地运行:管理 API 不做令牌校验(原 admin-token 逻辑已移除)\r\n\r\nimport type { Context, Next } from \"hono\";\r\n\r\n/** 占位中间件,与既有路由注册兼容,始终放行 */\r\nexport function requireAdmin() {\r\n return async (_c: Context, next: Next): Promise<Response | void> => {\r\n return next();\r\n };\r\n}\r\n","// /api/server-info\r\n\r\nimport { networkInterfaces } from \"node:os\";\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nconst PORT = Number(process.env.PORT) || 18473;\r\n\r\nexport function registerServerRoutes(app: Hono): void {\r\n app.get(\"/api/server-info\", requireAdmin(), (c) => {\r\n const lanIp = Object.values(networkInterfaces())\r\n .flat()\r\n .find((iface) => iface?.family === \"IPv4\" && !iface.internal)?.address;\r\n const lanUrl = lanIp ? `http://${lanIp}:${PORT}` : null;\r\n return c.json({ port: PORT, lanUrl });\r\n });\r\n}\r\n","// /api/rss(JSON,按 URL 抓取返回条目列表)\n\nimport { createHash } from \"node:crypto\";\nimport type { Hono } from \"hono\";\nimport { getItems } from \"../../../feeder/index.js\";\nimport { SOURCES_GROUP } from \"../../../scraper/scheduler/index.js\";\nimport * as scheduler from \"../../../scheduler/index.js\";\nimport { CACHE_DIR } from \"../../../config/paths.js\";\nimport { AuthRequiredError, NotFoundError } from \"../../../scraper/auth/index.js\";\nimport { getEffectiveItemFields, pubDateToIsoOrNull } from \"../../../types/feedItem.js\";\n\nexport function registerRssApiRoutes(app: Hono): void {\n app.get(\"/api/rss\", async (c) => {\n const url = c.req.query(\"url\");\n if (!url) return c.json({ error: \"url 参数缺失\" }, 400);\n const headlessParam = c.req.query(\"headless\");\n const headless = headlessParam === \"false\" || headlessParam === \"0\" ? false : undefined;\n const lng = c.req.query(\"lng\") ?? undefined;\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 20), 200);\n const offset = Number(c.req.query(\"offset\") ?? 0);\n try {\n const httpId = \"http-\" + createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\n const { items: allItems, fromCache } = await scheduler.schedule(\n SOURCES_GROUP,\n httpId,\n () => getItems(url, { cacheDir: CACHE_DIR, headless, lng })\n );\n const total = allItems.length;\n const pageItems = allItems.slice(offset, offset + limit);\n return c.json({\n fromCache,\n total,\n hasMore: offset + pageItems.length < total,\n items: pageItems.map((item) => {\n const { title, summary } = lng ? getEffectiveItemFields(item, lng) : { title: item.title, summary: item.summary ?? \"\" };\n return {\n guid: item.guid,\n title,\n link: item.link,\n summary,\n author: item.author,\n pubDate: pubDateToIsoOrNull(item.pubDate),\n };\n }),\n });\n } catch (err) {\n if (err instanceof AuthRequiredError) return c.json({ error: \"需要登录\", code: \"AUTH_REQUIRED\" }, 401);\n if (err instanceof NotFoundError) return c.json({ error: err.message, code: \"NOT_FOUND\" }, 404);\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);\n }\n });\n}\n","// /api/scheduler/stats\r\n\r\nimport type { Hono } from \"hono\";\r\nimport * as scheduler from \"../../../scheduler/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerSchedulerRoutes(app: Hono): void {\r\n app.get(\"/api/scheduler/stats\", requireAdmin(), (c) => {\r\n const stats = scheduler.getGroupStats();\r\n return c.json(stats);\r\n });\r\n}\r\n","// /api/plugins、POST /api/plugins(从模板新建)、/api/plugins/:id(读写字节码插件源文件)\r\n\r\nimport { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\r\nimport { join, resolve, sep } from \"node:path\";\r\nimport type { Hono } from \"hono\";\r\nimport { getPluginSites } from \"../../../scraper/sources/web/index.js\";\r\nimport { registeredSources } from \"../../../scraper/sources/index.js\";\r\nimport { getPluginFilePath } from \"../../../plugins/loader.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { initSources } from \"../../../scraper/sources/index.js\";\r\nimport { BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR, PLUGIN_SITE_TEMPLATE_PATH } from \"../../../config/paths.js\";\r\n\r\nconst SITE_TEMPLATE_FALLBACK = `/**\r\n * Site 插件模板(由 /plugins 页添加,位于 .rssany/plugins/)\r\n * HTML DOM 解析请用 ctx.deps.parseHtml,勿在插件内 import node_modules。\r\n */\r\nexport default {\r\n id: \"__PLUGIN_ID__\",\r\n listUrlPattern: __LIST_URL_PATTERN__,\r\n refreshInterval: \"1day\",\r\n\r\n async fetchItems(sourceId, ctx) {\r\n const { html, finalUrl } = await ctx.fetchHtml(sourceId, {\r\n waitMs: 2000,\r\n purify: true,\r\n });\r\n void ctx.deps.parseHtml(html);\r\n void finalUrl;\r\n return [];\r\n },\r\n};\r\n`;\r\n\r\nfunction isValidNewPluginId(id: string): boolean {\r\n return /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) && id !== \"generic\" && id !== \"new\";\r\n}\r\n\r\n/** 与模板中 `listUrlPattern: __LIST_URL_PATTERN__` 注入一致:非空、无换行、长度上限 */\r\nfunction isValidNewListUrlPattern(pattern: string): boolean {\r\n if (pattern.length === 0 || pattern.length > 2048) return false;\r\n if (/[\\r\\n]/.test(pattern)) return false;\r\n return true;\r\n}\r\n\r\nasync function fileExists(p: string): Promise<boolean> {\r\n try {\r\n await access(p);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nfunction isAllowedPluginPath(absPath: string): boolean {\r\n const f = resolve(absPath);\r\n for (const root of [BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR]) {\r\n const r = resolve(root);\r\n if (f === r || f.startsWith(r + sep)) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport function registerPluginsRoutes(app: Hono): void {\r\n /** 从模板在 .rssany/plugins/{id}.rssany.js 新建 Site 插件 */\r\n app.post(\"/api/plugins\", requireAdmin(), async (c) => {\r\n let body: { id?: string; listUrlPattern?: string };\r\n try {\r\n body = await c.req.json();\r\n } catch {\r\n return c.json({ error: \"无效 JSON\" }, 400);\r\n }\r\n const id = typeof body.id === \"string\" ? body.id.trim() : \"\";\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n if (!isValidNewPluginId(id)) {\r\n return c.json({ error: \"id 须为字母开头,仅含字母数字、下划线、连字符;不能为 generic 或 new\" }, 400);\r\n }\r\n const listUrlPatternRaw = typeof body.listUrlPattern === \"string\" ? body.listUrlPattern.trim() : \"\";\r\n if (!listUrlPatternRaw) {\r\n return c.json({ error: \"缺少支持的站点(listUrlPattern),例如 https://example.com/*\" }, 400);\r\n }\r\n if (!isValidNewListUrlPattern(listUrlPatternRaw)) {\r\n return c.json({ error: \"支持的站点须为非空字符串,不超过 2048 字符,且不能含换行\" }, 400);\r\n }\r\n await mkdir(USER_PLUGINS_DIR, { recursive: true });\r\n const outPath = join(USER_PLUGINS_DIR, `${id}.rssany.js`);\r\n if (await fileExists(outPath)) return c.json({ error: \"该 id 已存在同名文件\" }, 409);\r\n let tpl = SITE_TEMPLATE_FALLBACK;\r\n try {\r\n tpl = await readFile(PLUGIN_SITE_TEMPLATE_PATH, \"utf-8\");\r\n } catch {\r\n // 使用内置模板\r\n }\r\n const patternLiteral = JSON.stringify(listUrlPatternRaw);\r\n const content = tpl.replace(/__PLUGIN_ID__/g, id).replace(/__LIST_URL_PATTERN__/g, patternLiteral);\r\n if (!isAllowedPluginPath(outPath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n await writeFile(outPath, content, \"utf-8\");\r\n await initSources();\r\n return c.json({ ok: true, filePath: outPath, id });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n\r\n app.get(\"/api/plugins\", requireAdmin(), (c) => {\r\n const sites = getPluginSites().map((s) => ({\r\n kind: \"site\" as const,\r\n id: s.id,\r\n listUrlPattern: typeof s.listUrlPattern === \"string\" ? s.listUrlPattern : String(s.listUrlPattern),\r\n hasAuth: !!(s.checkAuth && s.loginUrl),\r\n }));\r\n const siteIds = new Set(sites.map((p) => p.id));\r\n const sources = registeredSources\r\n .filter((src) => src.id !== \"generic\" && !siteIds.has(src.id))\r\n .map((src) => ({\r\n kind: \"source\" as const,\r\n id: src.id,\r\n listUrlPattern: typeof src.pattern === \"string\" ? src.pattern : String(src.pattern),\r\n hasAuth: false,\r\n }));\r\n return c.json([...sites, ...sources]);\r\n });\r\n\r\n app.get(\"/api/plugins/:id\", requireAdmin(), async (c) => {\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n const filePath = getPluginFilePath(id);\r\n if (!filePath) return c.json({ error: \"未找到该插件或无可编辑文件\" }, 404);\r\n if (!isAllowedPluginPath(filePath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n const content = await readFile(filePath, \"utf-8\");\r\n return c.json({ id, filePath, content });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n\r\n app.put(\"/api/plugins/:id\", requireAdmin(), async (c) => {\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n let body: { content?: string };\r\n try {\r\n body = await c.req.json();\r\n } catch {\r\n return c.json({ error: \"无效 JSON\" }, 400);\r\n }\r\n if (typeof body.content !== \"string\") return c.json({ error: \"需要 content 字符串\" }, 400);\r\n const filePath = getPluginFilePath(id);\r\n if (!filePath) return c.json({ error: \"未找到该插件\" }, 404);\r\n if (!isAllowedPluginPath(filePath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n await writeFile(filePath, body.content, \"utf-8\");\r\n await initSources();\r\n return c.json({ ok: true });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n}\r\n","// /api/pipeline(步骤开关与排序)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport {\r\n loadPipelineConfig,\r\n savePipelineConfig,\r\n DEFAULT_PIPELINE_STEPS,\r\n PIPELINE_STEP_IDS,\r\n} from \"../../../pipeline/config.js\";\r\n\r\ntype StepInput = { id: string; enabled?: boolean };\r\n\r\nfunction parseSteps(rawSteps: unknown[]): Array<{ id: string; enabled: boolean }> {\r\n return rawSteps\r\n .filter((s) => s && typeof s === \"object\" && typeof (s as StepInput).id === \"string\")\r\n .map((s) => {\r\n const obj = s as StepInput;\r\n const e: unknown = obj.enabled;\r\n return {\r\n id: String(obj.id).trim(),\r\n enabled: e !== false && e !== 0,\r\n };\r\n })\r\n .filter((s) => s.id.length > 0);\r\n}\r\n\r\nfunction dedupeSteps<T extends { id: string }>(steps: T[]): T[] {\r\n const seen = new Set<string>();\r\n const out: T[] = [];\r\n for (const s of steps) {\r\n if (seen.has(s.id)) continue;\r\n seen.add(s.id);\r\n out.push(s);\r\n }\r\n return out;\r\n}\r\n\r\nexport function registerPipelineRoutes(app: Hono): void {\r\n app.get(\"/api/pipeline\", requireAdmin(), async (c) => {\r\n const config = await loadPipelineConfig();\r\n return c.json({\r\n steps: config.steps,\r\n availableIds: [...PIPELINE_STEP_IDS],\r\n defaults: DEFAULT_PIPELINE_STEPS,\r\n });\r\n });\r\n\r\n app.put(\"/api/pipeline\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ steps?: unknown[] }>();\r\n const rawSteps = Array.isArray(body?.steps) ? body.steps : [];\r\n const steps = dedupeSteps(parseSteps(rawSteps));\r\n await savePipelineConfig({ steps });\r\n return c.json({ ok: true, steps });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/feed、/api/events\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { streamSSE } from \"hono/streaming\";\r\nimport { onFeedUpdated } from \"../../../core/events/index.js\";\r\nimport { getAllSources, getAllSubscriptionRefs, resolveRef } from \"../../../scraper/subscription/index.js\";\r\nimport { getEffectiveItemFields, type ItemTranslationFields } from \"../../../types/feedItem.js\";\r\nimport { queryFeedItems } from \"../../../db/index.js\";\r\n\r\nexport function registerFeedRoutes(app: Hono): void {\r\n app.get(\"/api/feed\", async (c) => {\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 50), 200);\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n const refFilter = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? undefined;\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n const since = c.req.query(\"since\");\r\n const until = c.req.query(\"until\");\r\n\r\n const sources = await getAllSources();\r\n const refSet = new Set(sources.map((s) => resolveRef(s)).filter(Boolean));\r\n let sourceRefs: string[];\r\n if (refFilter) {\r\n sourceRefs = refSet.has(refFilter) ? [refFilter] : [];\r\n } else {\r\n sourceRefs = await getAllSubscriptionRefs();\r\n }\r\n\r\n const labelByRef = new Map(sources.map((s) => [resolveRef(s), s.label ?? resolveRef(s)] as const));\r\n const sourcesMeta = sources.map((s) => ({\r\n ref: resolveRef(s),\r\n label: s.label ?? resolveRef(s),\r\n }));\r\n\r\n const dateOpts = since || until ? { since: since ?? undefined, until: until ?? undefined } : undefined;\r\n const { items: dbItems, hasMore } = await queryFeedItems(sourceRefs, limit, offset, dateOpts);\r\n const items = dbItems.map((item) => {\r\n const refKey = item.source_url ?? \"\";\r\n const base = {\r\n ...item,\r\n sub_id: refKey,\r\n sub_title: labelByRef.get(refKey) ?? \"\",\r\n };\r\n if (!lng) return base;\r\n const view = {\r\n title: item.title ?? \"\",\r\n summary: item.summary ?? \"\",\r\n content: item.content ?? \"\",\r\n translations: (item as { translations?: Record<string, ItemTranslationFields> }).translations,\r\n };\r\n const eff = getEffectiveItemFields(view, lng);\r\n return { ...base, title: eff.title, summary: eff.summary, content: eff.content };\r\n });\r\n return c.json({ sources: sourcesMeta, items, hasMore });\r\n });\r\n\r\n app.get(\"/api/events\", (c) => {\r\n return streamSSE(c, async (stream) => {\r\n await stream.writeSSE({ data: JSON.stringify({ type: \"connected\" }) });\r\n const off = onFeedUpdated((e) => {\r\n stream.writeSSE({ data: JSON.stringify({ type: \"feed:updated\", sourceUrl: e.sourceUrl, newCount: e.newCount }) }).catch(() => {});\r\n });\r\n const heartbeat = setInterval(() => {\r\n stream.writeSSE({ data: \"\", event: \"ping\" }).catch(() => {});\r\n }, 25000);\r\n stream.onAbort(() => {\r\n off();\r\n clearInterval(heartbeat);\r\n });\r\n await new Promise<void>((resolve) => stream.onAbort(resolve));\r\n });\r\n });\r\n}\r\n","// /api/items、/api/items/pending-push、/api/items/mark-pushed、/api/items/by-source(须在 :id 之前注册)、/api/items/:id\r\n\r\n// /api/user/items(JWT 认证用户)\r\n\r\n\r\n\r\nimport type { Hono } from \"hono\";\r\n\r\nimport { getAllSubscriptionRefs } from \"../../../scraper/subscription/index.js\";\r\n\r\nimport { getEffectiveItemFields, type ItemTranslationFields } from \"../../../types/feedItem.js\";\r\n\r\nimport { queryItems, getPendingPushItems, markPushed, deleteItem, deleteItemsBySourceUrl } from \"../../../db/index.js\";\r\n\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\n\r\n\r\nfunction parseSubscribedFlag(v: string | undefined): boolean {\r\n\r\n return v === \"1\" || v === \"true\" || v === \"yes\";\r\n\r\n}\r\n\r\n\r\n\r\nexport function registerItemsRoutes(app: Hono): void {\r\n\r\n app.get(\"/api/items/pending-push\", async (c) => {\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 100), 500);\r\n\r\n const items = await getPendingPushItems(limit);\r\n\r\n return c.json({ items, count: items.length });\r\n\r\n });\r\n\r\n\r\n\r\n app.post(\"/api/items/mark-pushed\", async (c) => {\r\n\r\n try {\r\n\r\n const { ids } = await c.req.json<{ ids?: string[] }>();\r\n\r\n if (!Array.isArray(ids) || ids.length === 0) return c.json({ ok: false, message: \"ids 不能为空\" }, 400);\r\n\r\n await markPushed(ids);\r\n\r\n return c.json({ ok: true, count: ids.length });\r\n\r\n } catch (err) {\r\n\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n\r\n }\r\n\r\n });\r\n\r\n\r\n\r\n /** 清空指定信源(source_url)下所有已入库条目 — 必须早于 /api/items/:id,否则 \"by-source\" 会被当成 id */\r\n\r\n app.delete(\"/api/items/by-source\", requireAdmin(), async (c) => {\r\n\r\n const sourceUrl = (c.req.query(\"source_url\") ?? \"\").trim();\r\n\r\n if (!sourceUrl) return c.json({ ok: false, message: \"source_url 不能为空\" }, 400);\r\n\r\n const deleted = await deleteItemsBySourceUrl(sourceUrl);\r\n\r\n return c.json({ ok: true, deleted });\r\n\r\n });\r\n\r\n\r\n\r\n app.delete(\"/api/items/:id\", async (c) => {\r\n\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n\r\n if (!id) return c.json({ ok: false, message: \"id 不能为空\" }, 400);\r\n\r\n const deleted = await deleteItem(id);\r\n\r\n if (!deleted) return c.json({ ok: false, message: \"条目不存在或已删除\" }, 404);\r\n\r\n return c.json({ ok: true });\r\n\r\n });\r\n\r\n\r\n\r\n app.get(\"/api/items\", async (c) => {\r\n\r\n const ref = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? undefined;\r\n\r\n const subscribed = parseSubscribedFlag(c.req.query(\"subscribed\"));\r\n\r\n const author = c.req.query(\"author\") ?? undefined;\r\n\r\n const q = c.req.query(\"q\") ?? undefined;\r\n\r\n const tagsParam = c.req.query(\"tags\") ?? undefined;\r\n\r\n const tags = tagsParam ? tagsParam.split(\",\").map((t) => t.trim()).filter(Boolean) : undefined;\r\n\r\n // days=0 或不传 days 表示不按天数筛选;days=N 表示最近 N 天;since/until 仍可单独传\r\n\r\n const daysParam = c.req.query(\"days\");\r\n\r\n const sinceParam = c.req.query(\"since\") ?? undefined;\r\n\r\n const untilParam = c.req.query(\"until\") ?? undefined;\r\n\r\n let since: Date | undefined;\r\n\r\n let until: Date | undefined;\r\n\r\n const daysNum = daysParam !== undefined && daysParam !== \"\" ? Number(daysParam) : NaN;\r\n\r\n if (Number.isFinite(daysNum) && daysNum > 0) {\r\n\r\n const n = Math.max(1, Math.min(365, Math.floor(daysNum)));\r\n\r\n const now = new Date();\r\n\r\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\r\n\r\n const todayEnd = new Date(todayStart);\r\n\r\n todayEnd.setDate(todayEnd.getDate() + 1);\r\n\r\n since = new Date(todayStart);\r\n\r\n since.setDate(since.getDate() - (n - 1));\r\n\r\n until = todayEnd;\r\n\r\n } else {\r\n\r\n since = sinceParam ? new Date(sinceParam) : undefined;\r\n\r\n if (untilParam) {\r\n\r\n if (untilParam.length === 10) {\r\n\r\n const d = new Date(untilParam + \"T12:00:00Z\");\r\n\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n\r\n until = d;\r\n\r\n } else {\r\n\r\n until = new Date(untilParam);\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 200), 500);\r\n\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n\r\n\r\n let sourceUrls: string[] | undefined;\r\n\r\n let effectiveSourceUrl: string | undefined;\r\n\r\n if (ref) {\r\n\r\n effectiveSourceUrl = ref;\r\n\r\n sourceUrls = undefined;\r\n\r\n } else if (subscribed) {\r\n\r\n const refs = await getAllSubscriptionRefs();\r\n\r\n sourceUrls = refs.length > 0 ? refs : [];\r\n\r\n }\r\n\r\n\r\n\r\n if (!effectiveSourceUrl && sourceUrls?.length === 0) {\r\n\r\n return c.json({ items: [], total: 0, hasMore: false });\r\n\r\n }\r\n\r\n\r\n\r\n const result = await queryItems({\r\n\r\n sourceUrl: effectiveSourceUrl ?? (sourceUrls ? undefined : ref),\r\n\r\n sourceUrls,\r\n\r\n author,\r\n\r\n q,\r\n\r\n tags,\r\n\r\n since,\r\n\r\n until,\r\n\r\n limit,\r\n\r\n offset,\r\n\r\n });\r\n\r\n const items =\r\n\r\n lng && result.items.length > 0\r\n\r\n ? result.items.map((it) => {\r\n\r\n const view = {\r\n\r\n title: it.title ?? \"\",\r\n\r\n summary: it.summary ?? \"\",\r\n\r\n content: it.content ?? \"\",\r\n\r\n translations: (it as { translations?: Record<string, ItemTranslationFields> }).translations,\r\n\r\n };\r\n\r\n const eff = getEffectiveItemFields(view, lng);\r\n\r\n return { ...it, title: eff.title, summary: eff.summary, content: eff.content };\r\n\r\n })\r\n\r\n : result.items;\r\n\r\n const hasMore = offset + items.length < result.total;\r\n\r\n return c.json({ items, total: result.total, hasMore });\r\n\r\n });\r\n\r\n}\r\n\r\n","// /api/logs\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { clearAllLogs, queryLogs } from \"../../../db/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerLogsRoutes(app: Hono): void {\r\n app.delete(\"/api/logs\", requireAdmin(), async (c) => {\r\n const deleted = await clearAllLogs();\r\n return c.json({ ok: true, deleted });\r\n });\r\n\r\n app.get(\"/api/logs\", requireAdmin(), async (c) => {\r\n const levelParam = c.req.query(\"level\");\r\n const level = levelParam === \"error\" || levelParam === \"warn\" || levelParam === \"info\" || levelParam === \"debug\" ? levelParam : undefined;\r\n const categoryRaw = c.req.query(\"category\");\r\n const category = typeof categoryRaw === \"string\" && categoryRaw.trim() ? categoryRaw.trim() : undefined;\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 100), 200);\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n const sinceParam = c.req.query(\"since\");\r\n const since = sinceParam ? new Date(sinceParam) : undefined;\r\n const result = await queryLogs({ level, category, limit, offset, since });\r\n return c.json(result);\r\n });\r\n}\r\n","// /api/admin/verify, /api/admin/integrity-check\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { runIntegrityCheck } from \"../../../db/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerAdminApiRoutes(app: Hono): void {\r\n app.get(\"/api/admin/verify\", requireAdmin(), async (c) => {\r\n return c.json({ ok: true });\r\n });\r\n\r\n /** SQLite 主库 PRAGMA integrity_check */\r\n app.get(\"/api/admin/integrity-check\", requireAdmin(), async (c) => {\r\n try {\r\n const result = await runIntegrityCheck();\r\n const ok = result === \"ok\";\r\n return c.json({ ok, result });\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n return c.json({ ok: false, result: `error: ${message}` }, 500);\r\n }\r\n });\r\n}\r\n","// /api/sources/stats、/api/sources/raw、/api/sources/plugin-match(admin)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { getSourceStats } from \"../../../db/index.js\";\r\nimport { getSource } from \"../../../scraper/sources/index.js\";\r\nimport { getPluginSites } from \"../../../scraper/sources/web/index.js\";\r\nimport { getSourcesRaw, saveSourcesFile } from \"../../../scraper/subscription/index.js\";\r\nimport type { SourceType } from \"../../../scraper/subscription/types.js\";\r\nimport type { RefreshInterval } from \"../../../utils/refreshInterval.js\";\r\nimport { VALID_INTERVALS } from \"../../../utils/refreshInterval.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { canonicalHttpSourceRef } from \"../../../utils/httpSourceRef.js\";\r\n\r\nexport function registerSourcesRoutes(app: Hono): void {\r\n app.get(\"/api/sources/stats\", requireAdmin(), async (c) => {\r\n const stats = await getSourceStats();\r\n return c.json(stats);\r\n });\r\n\r\n app.post(\"/api/sources/plugin-match\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ refs?: string[] }>();\r\n const refs = Array.isArray(body?.refs) ? body.refs : [];\r\n const pluginIds = new Set(getPluginSites().map((s) => s.id));\r\n const result: Record<string, string | null> = {};\r\n for (const ref of refs) {\r\n const source = getSource(ref);\r\n result[ref] = pluginIds.has(source.id) ? source.id : null;\r\n }\r\n return c.json(result);\r\n } catch {\r\n return c.json({});\r\n }\r\n });\r\n\r\n app.get(\"/api/sources/raw\", requireAdmin(), async (c) => {\r\n try {\r\n const raw = await getSourcesRaw();\r\n return c.text(raw, 200, { \"Content-Type\": \"application/json; charset=utf-8\" });\r\n } catch {\r\n return c.text(JSON.stringify({ sources: [] }, null, 2), 200, { \"Content-Type\": \"application/json; charset=utf-8\" });\r\n }\r\n });\r\n\r\n app.put(\"/api/sources/raw\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ sources?: unknown[] }>();\r\n const list = Array.isArray(body?.sources) ? body.sources : [];\r\n const sources: { ref: string; type?: SourceType; label?: string; description?: string; refresh?: RefreshInterval; proxy?: string; weight?: number }[] = list\r\n .filter((s): s is Record<string, unknown> => s != null && typeof s === \"object\" && typeof (s as { ref?: unknown }).ref === \"string\")\r\n .map((s) => {\r\n const t = (s as { type?: string }).type;\r\n const type: SourceType | undefined =\r\n t === \"web\" || t === \"rss\" || t === \"email\" ? t : undefined;\r\n const r = (s as { refresh?: string }).refresh;\r\n const refresh: RefreshInterval | undefined =\r\n r && VALID_INTERVALS.includes(r as RefreshInterval) ? (r as RefreshInterval) : undefined;\r\n const w = (s as { weight?: unknown }).weight;\r\n const weight: number | undefined = typeof w === \"number\" ? w : undefined;\r\n return {\r\n ref: canonicalHttpSourceRef(String((s as { ref: string }).ref)),\r\n type,\r\n label: (s as { label?: string }).label,\r\n description: (s as { description?: string }).description,\r\n refresh,\r\n proxy: (s as { proxy?: string }).proxy,\r\n weight,\r\n };\r\n });\r\n await saveSourcesFile(sources);\r\n return c.json({ ok: true });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/tags — 系统标签(pipeline tagger);原 /api/topics 任务接口已迁至 /api/agent-tasks\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport {\r\n getSuggestedTags,\r\n getSystemTags,\r\n getSystemTagStats,\r\n saveSystemTagsToFile,\r\n removeTagFromAllItems,\r\n} from \"../../../db/index.js\";\r\n\r\nexport function registerTopicsRoutes(app: Hono): void {\r\n /** 系统标签:来自 .rssany/tags.json,供 pipeline tagger 使用 */\r\n app.get(\"/api/tags\", async (c) => {\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n });\r\n\r\n app.put(\"/api/tags\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ tags?: string[] }>();\r\n const list = Array.isArray(body?.tags) ? body.tags : [];\r\n await saveSystemTagsToFile(list);\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n ok: true,\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n\r\n /** 从所有条目的 tags 中移除指定标签 */\r\n app.post(\"/api/tags/remove-from-items\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ tag?: string }>();\r\n const tag = typeof body?.tag === \"string\" ? body.tag.trim() : \"\";\r\n if (!tag) return c.json({ ok: false, message: \"tag 参数缺失\" }, 400);\r\n const count = await removeTagFromAllItems(tag);\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n ok: true,\r\n removedCount: count,\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/deliver、/api/deliver/test — 配置投递 URL;非空则在正常写库后额外 POST 条目到该地址\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { getDeliverConfig, saveDeliverConfig } from \"../../../config/deliver.js\";\r\nimport { postDeliverItems } from \"../../../deliver/post.js\";\r\n\r\nexport function registerDeliverRoutes(app: Hono): void {\r\n app.get(\"/api/deliver\", requireAdmin(), async (c) => {\r\n const { url, token } = await getDeliverConfig();\r\n return c.json({ url, token });\r\n });\r\n\r\n app.put(\"/api/deliver\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ url?: string; token?: string }>();\r\n const url = typeof body?.url === \"string\" ? body.url.trim() : \"\";\r\n const token = typeof body?.token === \"string\" ? body.token.trim() : \"\";\r\n await saveDeliverConfig({ url, token });\r\n return c.json({ ok: true, url, token });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n\r\n app.post(\"/api/deliver/test\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ url?: string; token?: string }>();\r\n const url = typeof body?.url === \"string\" ? body.url.trim() : \"\";\r\n const token = typeof body?.token === \"string\" ? body.token.trim() : \"\";\r\n if (!url) return c.json({ ok: false, message: \"url 不能为空\" }, 400);\r\n const sample = {\r\n guid: \"deliver-test-\" + Date.now(),\r\n title: \"投递连通性测试\",\r\n link: \"https://example.com/rssany-deliver-test\",\r\n pubDate: new Date().toISOString(),\r\n summary: \"若下游收到此条,说明投递 URL 可用。\",\r\n };\r\n await postDeliverItems(\r\n url,\r\n \"rssany-deliver-test\",\r\n [\r\n {\r\n guid: sample.guid,\r\n title: sample.title,\r\n link: sample.link,\r\n pubDate: new Date(sample.pubDate),\r\n summary: sample.summary,\r\n sourceRef: \"rssany-deliver-test\",\r\n },\r\n ],\r\n { bearerToken: token || undefined },\r\n );\r\n return c.json({ ok: true });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// LLM:读写 .rssany/config.json 的 llm 块;与 OPENAI_* 环境变量合并见 core/llmConfig.ts\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\nimport { invalidateLLMConfigCache } from \"../core/llmConfig.js\";\r\n\r\nexport interface LlmFileConfig {\r\n apiKey?: string;\r\n baseUrl?: string;\r\n model?: string;\r\n}\r\n\r\nfunction trimOrUndef(s: unknown): string | undefined {\r\n if (typeof s !== \"string\") return undefined;\r\n const t = s.trim();\r\n return t.length > 0 ? t : undefined;\r\n}\r\n\r\n/** 读取 config.json 中的 llm 块(无则空) */\r\nexport async function readLlmFileConfig(): Promise<LlmFileConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { llm?: unknown };\r\n const llm = j?.llm;\r\n if (!llm || typeof llm !== \"object\") return {};\r\n const o = llm as Record<string, unknown>;\r\n return {\r\n apiKey: typeof o.apiKey === \"string\" ? o.apiKey : undefined,\r\n baseUrl: trimOrUndef(o.baseUrl),\r\n model: trimOrUndef(o.model),\r\n };\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\nexport interface SaveLlmSettingsInput {\r\n baseUrl: string;\r\n model: string;\r\n apiKey?: string;\r\n}\r\n\r\n/** 合并写入 config.json 的 llm 块,不覆盖其它顶层字段 */\r\nexport async function saveLlmSettings(input: SaveLlmSettingsInput): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 新建\r\n }\r\n const prev = await readLlmFileConfig();\r\n const next: Record<string, unknown> = {\r\n baseUrl: input.baseUrl.trim(),\r\n model: input.model.trim(),\r\n };\r\n\r\n const newKey = typeof input.apiKey === \"string\" && input.apiKey.length > 0 ? input.apiKey : undefined;\r\n if (newKey) {\r\n next.apiKey = newKey;\r\n } else if (prev.apiKey) {\r\n next.apiKey = prev.apiKey;\r\n }\r\n\r\n root.llm = next;\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n invalidateLLMConfigCache();\r\n}\r\n","// /api/llm — 读写 LLM 配置(config.json.llm);GET 不返回完整 Key\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { getLLMConfig } from \"../../../core/llmConfig.js\";\r\nimport { chatText } from \"../../../core/llm.js\";\r\nimport {readLlmFileConfig,saveLlmSettings} from \"../../../config/llmSettings.js\";\r\n\r\n\r\nexport function registerLlmRoutes(app: Hono): void {\r\n app.get(\"/api/llm\", requireAdmin(), async (c) => {\r\n const resolved = getLLMConfig();\r\n const file = await readLlmFileConfig();\r\n const hasApiKey = !!resolved.apiKey;\r\n const apiKeyInFile = !!(file.apiKey && file.apiKey.length > 0);\r\n return c.json({\r\n baseUrl: resolved.baseUrl,\r\n model: resolved.model,\r\n hasApiKey,\r\n apiKeyInFile,\r\n });\r\n });\r\n\r\n \r\n app.put(\"/api/llm\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{\r\n baseUrl?: unknown;\r\n model?: unknown;\r\n apiKey?: unknown;\r\n }>();\r\n const baseUrl = typeof body.baseUrl === \"string\" ? body.baseUrl : \"\";\r\n const model = typeof body.model === \"string\" ? body.model : \"\";\r\n const apiKey = typeof body.apiKey === \"string\" ? body.apiKey : undefined;\r\n await saveLlmSettings({\r\n baseUrl,\r\n model,\r\n ...(apiKey !== undefined ? { apiKey } : {}),\r\n });\r\n const resolved = getLLMConfig();\r\n const file = await readLlmFileConfig();\r\n return c.json({\r\n ok: true,\r\n baseUrl: resolved.baseUrl,\r\n model: resolved.model,\r\n hasApiKey: !!resolved.apiKey,\r\n apiKeyInFile: !!(file.apiKey && file.apiKey.length > 0),\r\n });\r\n } catch (err) {\r\n return c.json(\r\n { ok: false, message: err instanceof Error ? err.message : String(err) },\r\n 400,\r\n );\r\n }\r\n });\r\n\r\n app.post(\"/api/llm/test\", requireAdmin(), async (c) => {\r\n const t0 = Date.now();\r\n try {\r\n const cfg = getLLMConfig();\r\n if (!cfg.apiKey) {\r\n return c.json({ ok: false, message: \"未配置 API Key(请在界面或 OPENAI_API_KEY 中设置)\" }, 400);\r\n }\r\n const reply = await chatText(\"Reply with exactly the single word: ok\", undefined, {\r\n maxTokens: 32768,\r\n debugLabel: \"llmSettingsTest\",\r\n });\r\n return c.json({ ok: true, reply });\r\n } catch (err) {\r\n const ms = Date.now() - t0;\r\n const message = err instanceof Error ? err.message : String(err);\r\n console.error(\"[llm/test] fail\", { ms, message });\r\n return c.json({ ok: false, message }, 400);\r\n }\r\n });\r\n}\r\n","// GET/PUT /api/proxy — config.json 全局代理(admin)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { readGlobalProxyFromConfig, saveGlobalProxyToConfig } from \"../../../config/globalProxy.js\";\r\n\r\nexport function registerProxySettingsRoutes(app: Hono): void {\r\n app.get(\"/api/proxy\", requireAdmin(), async (c) => {\r\n const globalProxy = (await readGlobalProxyFromConfig()) ?? \"\";\r\n return c.json({ globalProxy });\r\n });\r\n\r\n app.put(\"/api/proxy\", requireAdmin(), async (c) => {\r\n try {\r\n const body = (await c.req.json().catch(() => ({}))) as { globalProxy?: unknown };\r\n const globalProxy = typeof body.globalProxy === \"string\" ? body.globalProxy : \"\";\r\n await saveGlobalProxyToConfig(globalProxy);\r\n const saved = (await readGlobalProxyFromConfig()) ?? \"\";\r\n return c.json({ ok: true, globalProxy: saved });\r\n } catch (err) {\r\n return c.json(\r\n { ok: false, message: err instanceof Error ? err.message : String(err) },\r\n 400,\r\n );\r\n }\r\n });\r\n}\r\n","// 后端任务:提交后立即返回 taskId,任务在后台执行,前端轮询状态\n\nconst tasks = new Map<string, { id: string; status: string; result?: unknown; error?: string; createdAt: number; updatedAt: number }>();\nlet idCounter = 0;\n\nfunction nextId(): string {\n idCounter += 1;\n return `t_${Date.now().toString(36)}_${idCounter}`;\n}\n\nexport function createTask(): string {\n const id = nextId();\n const now = Date.now();\n tasks.set(id, { id, status: \"pending\", createdAt: now, updatedAt: now });\n return id;\n}\n\nexport function getTask(id: string) {\n return tasks.get(id) ?? null;\n}\n\nexport function setTaskRunning(id: string): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"running\";\n t.updatedAt = Date.now();\n }\n}\n\nexport function setTaskDone<T>(id: string, result: T): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"done\";\n t.result = result as unknown;\n t.updatedAt = Date.now();\n }\n}\n\nexport function setTaskError(id: string, error: string): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"error\";\n t.error = error;\n t.updatedAt = Date.now();\n }\n}\n\n/** 清理 1 小时前的已完成/失败任务 */\nexport function pruneTasks(): void {\n const cutoff = Date.now() - 60 * 60 * 1000;\n for (const [id, t] of tasks) {\n if ((t.status === \"done\" || t.status === \"error\") && t.updatedAt < cutoff) {\n tasks.delete(id);\n }\n }\n}\n","// 任务 API:POST /api/tasks 提交拉取信源,GET /api/tasks/:id 轮询\r\n\r\nimport type { Hono } from \"hono\";\r\nimport * as taskStore from \"../../../tasks/index.js\";\r\nimport * as scheduler from \"../../../scheduler/index.js\";\r\nimport { CACHE_DIR } from \"../../../config/paths.js\";\r\nimport { getItems } from \"../../../feeder/index.js\";\r\nimport { SOURCES_GROUP } from \"../../../scraper/scheduler/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerTasksRoutes(app: Hono): void {\r\n app.get(\"/api/tasks/:id\", (c) => {\r\n const id = c.req.param(\"id\") ?? \"\";\r\n const task = taskStore.getTask(id);\r\n if (!task) return c.json({ error: \"任务不存在\" }, 404);\r\n return c.json(task);\r\n });\r\n\r\n app.post(\"/api/tasks\", requireAdmin(), async (c) => {\r\n try {\r\n const body = (await c.req.json().catch(() => ({}))) as { type?: string; ref?: string };\r\n const type = body.type ?? \"\";\r\n if (type === \"source-pull\") {\r\n const ref = typeof body.ref === \"string\" ? body.ref.trim() : \"\";\r\n if (!ref) return c.json({ error: \"ref 不能为空\" }, 400);\r\n const taskId = taskStore.createTask();\r\n scheduler.schedule(SOURCES_GROUP, taskId, async () => {\r\n taskStore.setTaskRunning(taskId);\r\n try {\r\n await getItems(ref, { cacheDir: CACHE_DIR, force: true });\r\n taskStore.setTaskDone(taskId, { ok: true });\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n taskStore.setTaskError(taskId, msg);\r\n throw err;\r\n }\r\n }, { priority: true }).catch(() => {});\r\n return c.json({ taskId });\r\n }\r\n return c.json({ error: `未知任务类型: ${type}` }, 400);\r\n } catch (err) {\r\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);\r\n }\r\n });\r\n}\r\n","// GET /api/feed-favicon?domain=example.com — 信源列表站点图标;磁盘缓存在 CACHE_DIR/feed-favicons。\r\n//\r\n// 合法 domain 时始终返回 200 + 可用图片:上游全部失败则回退为字母 SVG(按域名稳定着色)。\r\n// 磁盘与 HTTP 缓存均为 3 天(mtime / max-age),过期后重新抓取。\r\n//\r\n// 多路上游并行竞速(Promise.any)。默认不含 Google s2(国内访问差):设 FAVICON_INCLUDE_GOOGLE=1 可启用。\r\n// 境外聚合源(DDG / icon.horse / unavatar)在国内网络可能慢或失败;若仅需直连站点图标,设 FAVICON_THIRD_PARTY=0。\r\n// 解析首页 HTML 中 link[rel~=icon] 等(尊重 <base href>);设 FAVICON_SKIP_HTML=1 可关闭。\r\n// 同域名 in-flight 合并。\r\n\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, writeFile, mkdir, unlink, stat } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { parse } from \"node-html-parser\";\r\nimport type { Hono } from \"hono\";\r\nimport { CACHE_DIR } from \"../../../config/paths.js\";\r\n\r\nconst CACHE_SUBDIR = \"feed-favicons\";\r\nconst CACHE_KEY_PREFIX = \"feed-favicon:v1:\";\r\n/** 磁盘与 CDN/浏览器缓存:3 天 */\r\nconst CACHE_MAX_AGE_SEC = 3 * 24 * 60 * 60;\r\nconst CACHE_MAX_AGE_MS = CACHE_MAX_AGE_SEC * 1000;\r\nconst CACHE_CONTROL = `public, max-age=${CACHE_MAX_AGE_SEC}`;\r\n\r\nconst FETCH_TIMEOUT_MS = 6_000;\r\nconst MAX_ICON_BYTES = 2 * 1024 * 1024;\r\nconst MAX_HTML_BYTES = 512 * 1024;\r\n\r\nconst inflightByDomain = new Map<string, Promise<{ buf: Buffer; mime: string }>>();\r\n\r\nconst MAX_DOMAIN_LEN = 253;\r\n\r\nfunction isPlausibleHostname(s: string): boolean {\r\n if (s.length === 0 || s.length > MAX_DOMAIN_LEN) return false;\r\n return /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(s);\r\n}\r\n\r\nfunction cacheFilePath(domainKey: string): string {\r\n const h = createHash(\"sha256\").update(CACHE_KEY_PREFIX + domainKey.toLowerCase()).digest(\"hex\");\r\n return join(CACHE_DIR, CACHE_SUBDIR, h);\r\n}\r\n\r\n/** 直连目标站常见图标路径(不依赖境外 CDN,适合国内部署优先尝试) */\r\nfunction originFaviconUrls(domain: string): string[] {\r\n const d = domain.toLowerCase();\r\n const hosts: string[] = [`https://${d}`];\r\n if (d.startsWith(\"www.\")) {\r\n const bare = d.slice(4);\r\n if (bare) hosts.push(`https://${bare}`);\r\n } else {\r\n hosts.push(`https://www.${d}`);\r\n }\r\n const paths = [\"/favicon.ico\", \"/favicon.png\", \"/apple-touch-icon.png\"];\r\n const urls: string[] = [];\r\n for (const base of [...new Set(hosts)]) {\r\n for (const p of paths) {\r\n urls.push(`${base}${p}`);\r\n }\r\n }\r\n return urls;\r\n}\r\n\r\nfunction homepageUrlsForDomain(domain: string): string[] {\r\n const d = domain.toLowerCase();\r\n const urls = [`https://${d}/`];\r\n if (d.startsWith(\"www.\")) {\r\n const bare = d.slice(4);\r\n if (bare) urls.push(`https://${bare}/`);\r\n } else {\r\n urls.push(`https://www.${d}/`);\r\n }\r\n return [...new Set(urls)];\r\n}\r\n\r\nfunction isIconLinkRel(rel: string): boolean {\r\n const tokens = rel\r\n .toLowerCase()\r\n .trim()\r\n .split(/\\s+/)\r\n .filter(Boolean);\r\n if (tokens.some((x) => x === \"mask-icon\")) return true;\r\n if (tokens.some((x) => x === \"apple-touch-icon\" || x === \"apple-touch-icon-precomposed\")) return true;\r\n if (tokens.includes(\"shortcut\") && tokens.includes(\"icon\")) return true;\r\n return tokens.includes(\"icon\");\r\n}\r\n\r\n/** 从 HTML 提取 <link rel=\"icon\" …> 等 href,按文档顺序去重 */\r\nfunction parseLinkIconHrefs(html: string, pageUrl: string): string[] {\r\n const root = parse(html, { lowerCaseTagName: true });\r\n let base = pageUrl;\r\n const baseEl = root.querySelector(\"base[href]\");\r\n if (baseEl) {\r\n const bh = baseEl.getAttribute(\"href\")?.trim();\r\n if (bh) {\r\n try {\r\n base = new URL(bh, pageUrl).href;\r\n } catch {\r\n /* keep pageUrl */\r\n }\r\n }\r\n }\r\n const out: string[] = [];\r\n const seen = new Set<string>();\r\n for (const el of root.querySelectorAll(\"link[href]\")) {\r\n const rel = el.getAttribute(\"rel\") ?? \"\";\r\n if (!isIconLinkRel(rel)) continue;\r\n const href = el.getAttribute(\"href\")?.trim();\r\n if (!href || href.startsWith(\"data:\") || href.startsWith(\"blob:\")) continue;\r\n try {\r\n const abs = new URL(href, base).href;\r\n if ((abs.startsWith(\"http:\") || abs.startsWith(\"https:\")) && !seen.has(abs)) {\r\n seen.add(abs);\r\n out.push(abs);\r\n }\r\n } catch {\r\n /* skip */\r\n }\r\n }\r\n return out;\r\n}\r\n\r\nasync function fetchHtmlPage(url: string): Promise<string | null> {\r\n try {\r\n const upstream = await fetch(url, {\r\n redirect: \"follow\",\r\n headers: {\r\n Accept: \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.1\",\r\n \"User-Agent\": \"Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon\",\r\n },\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n });\r\n if (!upstream.ok) return null;\r\n const ab = await upstream.arrayBuffer();\r\n const buf = Buffer.from(ab);\r\n const slice = buf.subarray(0, Math.min(buf.length, MAX_HTML_BYTES));\r\n return slice.toString(\"utf-8\");\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 抓取首页 HTML,解析 link 图标;按候选首页顺序直到得到至少一条 href */\r\nasync function discoverIconUrlsFromHomepage(domain: string): Promise<string[]> {\r\n if (process.env.FAVICON_SKIP_HTML === \"1\" || process.env.FAVICON_SKIP_HTML === \"true\") {\r\n return [];\r\n }\r\n for (const pageUrl of homepageUrlsForDomain(domain)) {\r\n const html = await fetchHtmlPage(pageUrl);\r\n if (!html) continue;\r\n const hrefs = parseLinkIconHrefs(html, pageUrl);\r\n if (hrefs.length > 0) return hrefs;\r\n }\r\n return [];\r\n}\r\n\r\nfunction duckduckgoFaviconUrl(domain: string): string {\r\n return `https://icons.duckduckgo.com/ip3/${domain}.ico`;\r\n}\r\n\r\nfunction iconHorseUrl(domain: string): string {\r\n return `https://icon.horse/icon/${encodeURIComponent(domain)}`;\r\n}\r\n\r\nfunction unavatarUrl(domain: string): string {\r\n return `https://unavatar.io/${encodeURIComponent(domain)}`;\r\n}\r\n\r\nfunction googleFaviconUrl(domain: string): string {\r\n return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`;\r\n}\r\n\r\nfunction letterCharFromDomain(domain: string): string {\r\n const d = domain.toLowerCase().replace(/^www\\./, \"\");\r\n const m = d.match(/[a-z0-9]/);\r\n return m ? m[0]!.toUpperCase() : \"?\";\r\n}\r\n\r\nfunction hueFromDomain(domain: string): number {\r\n const h = createHash(\"sha256\").update(domain.toLowerCase()).digest();\r\n return ((h[0]! << 8) | h[1]!) % 360;\r\n}\r\n\r\nfunction escapeXmlText(s: string): string {\r\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\r\n}\r\n\r\n/** 上游全部失败时的稳定回退:圆角方块 + 首字母 */\r\nfunction letterAvatarSvg(domain: string): Buffer {\r\n const letter = escapeXmlText(letterCharFromDomain(domain));\r\n const hue = hueFromDomain(domain);\r\n const bg = `hsl(${hue} 42% 44%)`;\r\n const svg = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\">\r\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"${bg}\"/>\r\n <text x=\"32\" y=\"32\" dominant-baseline=\"central\" text-anchor=\"middle\" fill=\"#ffffff\" font-family=\"system-ui,Segoe UI,Helvetica,sans-serif\" font-size=\"28\" font-weight=\"600\">${letter}</text>\r\n</svg>`;\r\n return Buffer.from(svg.trim(), \"utf-8\");\r\n}\r\n\r\nfunction letterAvatarForDomain(domain: string): { buf: Buffer; mime: string } {\r\n return { buf: letterAvatarSvg(domain), mime: \"image/svg+xml\" };\r\n}\r\n\r\nfunction isEnoent(e: unknown): boolean {\r\n return typeof e === \"object\" && e !== null && (e as NodeJS.ErrnoException).code === \"ENOENT\";\r\n}\r\n\r\nfunction sniffImageMime(buf: Buffer): string | null {\r\n if (buf.length < 4) return null;\r\n if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return \"image/png\";\r\n if (buf.length >= 6 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return \"image/gif\";\r\n if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return \"image/jpeg\";\r\n if (\r\n buf.length >= 12 &&\r\n buf.subarray(0, 4).toString(\"ascii\") === \"RIFF\" &&\r\n buf.subarray(8, 12).toString(\"ascii\") === \"WEBP\"\r\n ) {\r\n return \"image/webp\";\r\n }\r\n if (buf.length >= 6 && buf.readUInt16LE(0) === 0 && (buf[2] === 1 || buf[2] === 2) && buf[3] === 0) {\r\n return \"image/x-icon\";\r\n }\r\n const head = buf.subarray(0, Math.min(256, buf.length)).toString(\"utf-8\").trimStart();\r\n if (head.startsWith(\"<svg\") || head.startsWith(\"<?xml\")) return \"image/svg+xml\";\r\n return null;\r\n}\r\n\r\nconst IMAGE_CT_PREFIX = \"image/\";\r\n\r\nfunction mimeFromFetch(ct: string | null): string | null {\r\n if (!ct) return null;\r\n const base = ct.split(\";\")[0].trim().toLowerCase();\r\n return base.startsWith(IMAGE_CT_PREFIX) ? base : null;\r\n}\r\n\r\nfunction resolveImageMime(buf: Buffer, ct: string | null): string | null {\r\n return sniffImageMime(buf) ?? mimeFromFetch(ct);\r\n}\r\n\r\nasync function fetchIconCandidate(url: string): Promise<{ buf: Buffer; ct: string | null } | null> {\r\n let upstream: Response;\r\n try {\r\n upstream = await fetch(url, {\r\n redirect: \"follow\",\r\n headers: {\r\n Accept: \"image/avif,image/webp,image/apng,image/*,*/*;q=0.8\",\r\n \"User-Agent\": \"Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon\",\r\n },\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n });\r\n } catch {\r\n return null;\r\n }\r\n if (!upstream.ok) return null;\r\n const ab = await upstream.arrayBuffer();\r\n const buf = Buffer.from(ab);\r\n if (buf.length === 0 || buf.length > MAX_ICON_BYTES) return null;\r\n return { buf, ct: upstream.headers.get(\"content-type\") };\r\n}\r\n\r\nfunction isValidIcon(got: { buf: Buffer; ct: string | null } | null): got is { buf: Buffer; ct: string | null } {\r\n if (!got) return false;\r\n const mime = resolveImageMime(got.buf, got.ct);\r\n return !!(mime && mime.startsWith(IMAGE_CT_PREFIX));\r\n}\r\n\r\nfunction upstreamFaviconUrls(domain: string, htmlIconUrls: string[]): string[] {\r\n const urls: string[] = [...originFaviconUrls(domain), ...htmlIconUrls];\r\n const thirdPartyOff =\r\n process.env.FAVICON_THIRD_PARTY === \"0\" || process.env.FAVICON_THIRD_PARTY === \"false\";\r\n if (!thirdPartyOff) {\r\n urls.push(duckduckgoFaviconUrl(domain), iconHorseUrl(domain), unavatarUrl(domain));\r\n }\r\n const includeGoogle =\r\n process.env.FAVICON_INCLUDE_GOOGLE === \"1\" || process.env.FAVICON_INCLUDE_GOOGLE === \"true\";\r\n if (includeGoogle) urls.push(googleFaviconUrl(domain));\r\n return urls;\r\n}\r\n\r\nasync function fetchFaviconFromNetwork(domain: string): Promise<{ buf: Buffer; mime: string }> {\r\n const htmlIconUrls = await discoverIconUrlsFromHomepage(domain);\r\n const urls = upstreamFaviconUrls(domain, htmlIconUrls);\r\n const tasks = urls.map(async (url) => {\r\n const got = await fetchIconCandidate(url);\r\n if (!isValidIcon(got)) {\r\n throw new Error(\"not-an-icon\");\r\n }\r\n const mime = resolveImageMime(got.buf, got.ct)!;\r\n return { buf: got.buf, mime };\r\n });\r\n try {\r\n return await Promise.any(tasks);\r\n } catch {\r\n return letterAvatarForDomain(domain);\r\n }\r\n}\r\n\r\nfunction fetchFaviconDeduped(domain: string): Promise<{ buf: Buffer; mime: string }> {\r\n let p = inflightByDomain.get(domain);\r\n if (p) return p;\r\n p = fetchFaviconFromNetwork(domain).finally(() => {\r\n if (inflightByDomain.get(domain) === p) inflightByDomain.delete(domain);\r\n });\r\n inflightByDomain.set(domain, p);\r\n return p;\r\n}\r\n\r\nexport function registerFeedFaviconRoutes(app: Hono): void {\r\n app.get(\"/api/feed-favicon\", async (c) => {\r\n const raw = (c.req.query(\"domain\") ?? \"\").trim();\r\n if (!raw || !isPlausibleHostname(raw)) {\r\n return new Response(null, { status: 400 });\r\n }\r\n const domain = raw.toLowerCase();\r\n const path = cacheFilePath(domain);\r\n\r\n let diskStale = false;\r\n try {\r\n const st = await stat(path);\r\n if (Date.now() - st.mtimeMs >= CACHE_MAX_AGE_MS) {\r\n diskStale = true;\r\n await unlink(path).catch(() => {});\r\n }\r\n } catch (e) {\r\n if (!isEnoent(e)) {\r\n return new Response(null, { status: 500 });\r\n }\r\n }\r\n\r\n if (!diskStale) {\r\n try {\r\n const cached = await readFile(path);\r\n const mime = resolveImageMime(cached, null);\r\n if (mime) {\r\n return new Response(new Uint8Array(cached), {\r\n status: 200,\r\n headers: {\r\n \"Content-Type\": mime,\r\n \"Cache-Control\": CACHE_CONTROL,\r\n },\r\n });\r\n }\r\n await unlink(path).catch(() => {});\r\n } catch (e) {\r\n if (!isEnoent(e)) {\r\n return new Response(null, { status: 500 });\r\n }\r\n }\r\n }\r\n\r\n const resolved = await fetchFaviconDeduped(domain);\r\n const { buf, mime } = resolved;\r\n\r\n try {\r\n await mkdir(join(CACHE_DIR, CACHE_SUBDIR), { recursive: true });\r\n await writeFile(path, buf);\r\n } catch {\r\n return new Response(null, { status: 500 });\r\n }\r\n\r\n return new Response(new Uint8Array(buf), {\r\n status: 200,\r\n headers: {\r\n \"Content-Type\": mime,\r\n \"Cache-Control\": CACHE_CONTROL,\r\n },\r\n });\r\n });\r\n}\r\n","// API 路由汇总:server、rss、items、feed、sources、scheduler、plugins、logs、admin、tags、tasks\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { registerServerRoutes } from \"./server.js\";\r\nimport { registerRssApiRoutes } from \"./rss.js\";\r\nimport { registerSchedulerRoutes } from \"./scheduler.js\";\r\nimport { registerPluginsRoutes } from \"./plugins.js\";\r\nimport { registerPipelineRoutes } from \"./pipeline.js\";\r\nimport { registerFeedRoutes } from \"./feed.js\";\r\nimport { registerItemsRoutes } from \"./items.js\";\r\nimport { registerLogsRoutes } from \"./logs.js\";\r\nimport { registerAdminApiRoutes } from \"./admin.js\";\r\nimport { registerSourcesRoutes } from \"./sources.js\";\r\nimport { registerTopicsRoutes } from \"./topics.js\";\r\nimport { registerDeliverRoutes } from \"./deliver.js\";\r\nimport { registerLlmRoutes } from \"./llm.js\";\r\nimport { registerProxySettingsRoutes } from \"./proxy.js\";\r\nimport { registerTasksRoutes } from \"./tasks.js\";\r\nimport { registerFeedFaviconRoutes } from \"./feed-favicon.js\";\r\n\r\nexport function registerApiRoutes(app: Hono): void {\r\n registerServerRoutes(app);\r\n registerFeedFaviconRoutes(app);\r\n registerRssApiRoutes(app);\r\n registerSchedulerRoutes(app);\r\n registerPluginsRoutes(app);\r\n registerPipelineRoutes(app);\r\n registerFeedRoutes(app);\r\n registerItemsRoutes(app);\r\n registerLogsRoutes(app);\r\n registerAdminApiRoutes(app);\r\n registerSourcesRoutes(app);\r\n registerTopicsRoutes(app);\r\n registerDeliverRoutes(app);\r\n registerLlmRoutes(app);\r\n registerProxySettingsRoutes(app);\r\n registerTasksRoutes(app);\r\n}\r\n","// 认证路由:登录检查、打开登录页、ensureAuth\n\nimport type { Hono } from \"hono\";\nimport { getWebSite, getBestSite, toAuthFlow } from \"../../scraper/sources/web/index.js\";\nimport { applyProxyAuthToPage, ensureAuth, preCheckAuth, launchBrowser, resolveProxy } from \"../../scraper/sources/web/fetcher/index.js\";\nimport { CACHE_DIR } from \"../../config/paths.js\";\nimport { resolveProxyForSite } from \"../../config/globalProxy.js\";\n\nexport function registerAuthRoutes(app: Hono): void {\n app.get(\"/auth/check\", async (c) => {\n const siteIdParam = c.req.query(\"siteId\");\n if (!siteIdParam) {\n return c.json({ ok: false, message: \"请提供 siteId\" }, 400);\n }\n const site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n try {\n const authenticated = await preCheckAuth(authFlow, CACHE_DIR, { proxy: await resolveProxyForSite(site) });\n return c.json({ ok: true, authenticated });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return c.json({ ok: false, message: `检查失败: ${msg}` }, 500);\n }\n });\n\n app.post(\"/auth/open\", async (c) => {\n const siteIdParam = c.req.query(\"siteId\");\n if (!siteIdParam) {\n return c.json({ ok: false, message: \"请提供 siteId\" }, 400);\n }\n const site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n const { loginUrl } = authFlow;\n const proxy = await resolveProxyForSite(site);\n void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy: resolveProxy({ proxy }) }).then(async (browser) => {\n try {\n const page = await browser.newPage();\n await applyProxyAuthToPage(page, { proxy });\n const realUserAgent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\n await page.setUserAgent(realUserAgent);\n await page.setViewport({ width: 1366, height: 960 });\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\n page.once(\"close\", () => {\n void browser.close().catch(() => {});\n });\n } catch {\n await browser.close().catch(() => {});\n }\n }).catch(() => {});\n return c.json({ ok: true, message: \"已打开登录页面\" });\n });\n\n app.post(\"/auth/ensure\", async (c) => {\n const urlParam = c.req.query(\"url\");\n const siteIdParam = c.req.query(\"siteId\");\n let site;\n if (urlParam) {\n const decoded = decodeURIComponent(urlParam);\n site = getBestSite(decoded);\n if (!site) return c.json({ ok: false, message: \"无匹配站点\" }, 404);\n } else if (siteIdParam) {\n site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n } else {\n return c.json({ ok: false, message: \"请提供 url 或 siteId\" }, 400);\n }\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n ensureAuth(authFlow, CACHE_DIR, { proxy: await resolveProxyForSite(site) }).then(() => {}).catch(() => {});\n return c.json({ ok: true, message: \"已打开登录窗口,请在弹出的浏览器中完成登录,完成后刷新订阅页面即可。\" });\n });\n}\n","// 路由层共享工具函数\r\n\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\nexport const STATICS_DIR = join(PACKAGE_ROOT, \"statics\");\r\n\r\n/** 从路径提取 URL(与 /rss/* 一致) */\r\nexport function parseUrlFromPath(path: string, prefix: string): string | null {\r\n const raw = path.slice(prefix.length) || \"\";\r\n const decoded = decodeURIComponent(raw.startsWith(\"/\") ? raw.slice(1) : raw);\r\n if (!decoded) return null;\r\n return decoded.startsWith(\"http\") ? decoded : `https://${decoded}`;\r\n}\r\n\r\n/** 读取静态 HTML(statics/ 目录,用于 401/404 错误页) */\r\nexport async function readStaticHtml(name: string, fallback: string): Promise<string> {\r\n try {\r\n return await readFile(join(STATICS_DIR, `${name}.html`), \"utf-8\");\r\n } catch {\r\n return fallback;\r\n }\r\n}\r\n\r\n/** HTML 转义,用于注入到页面中的不可信内容 */\r\nexport function escapeHtml(s: string): string {\r\n return s\r\n .replace(/&/g, \"&\")\r\n .replace(/</g, \"<\")\r\n .replace(/>/g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/'/g, \"'\");\r\n}\r\n","// Admin 路由:解析调试、正文提取调试\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { getSource } from \"../../scraper/sources/index.js\";\r\nimport { buildSourceContext } from \"../../scraper/sources/context.js\";\r\nimport { extractFromLink } from \"../../scraper/sources/web/extractor/index.js\";\r\nimport { CACHE_DIR } from \"../../config/paths.js\";\r\nimport { AuthRequiredError } from \"../../scraper/auth/index.js\";\r\nimport { parseUrlFromPath, readStaticHtml, escapeHtml } from \"../utils.js\";\r\nimport { requireAdmin } from \"../../auth/middleware.js\";\r\nimport { getEffectiveProxyForListUrl } from \"../../scraper/subscription/index.js\";\r\n\r\n/** 与 fetcher `resolveProxy` 一致:调试 query 优先 → sources.json / Source.proxy → 环境变量 */\r\nfunction effectiveProxyUsed(override: string | undefined, mergedFromSource: string | undefined): string | undefined {\r\n const o = override?.trim();\r\n if (o) return o;\r\n const s = mergedFromSource?.trim();\r\n if (s) return s;\r\n return process.env.HTTP_PROXY?.trim() || process.env.HTTPS_PROXY?.trim();\r\n}\r\n\r\nfunction redactProxyForLog(p: string | undefined): string | null {\r\n if (!p) return null;\r\n try {\r\n const u = new URL(p);\r\n if (u.username) u.username = \"***\";\r\n if (u.password) u.password = \"***\";\r\n return u.toString();\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function registerAdminRoutes(app: Hono): void {\r\n async function render401(listUrl: string): Promise<string> {\r\n const raw = await readStaticHtml(\"401\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>401</title></head><body><h1>401 需要登录</h1></body></html>\");\r\n return raw.replace(/\\{\\{listUrl\\}\\}/g, escapeHtml(listUrl));\r\n }\r\n\r\n /** Parse 与插件解耦:始终通过 getSource(url) 解析,无匹配插件时自动走 generic(浏览器抓取 + LLM 解析),与 /rss/* 行为一致 */\r\n app.get(\"/admin/parse/*\", requireAdmin(), async (c) => {\r\n const url = parseUrlFromPath(c.req.path, \"/admin/parse\");\r\n if (!url) return c.text(\"无效 URL,格式: /admin/parse/https://... 或 /admin/parse/example.com/...\", 400);\r\n try {\r\n // 调试默认有头浏览器;仅 headless=true|1 时使用无头\r\n const headlessParam = c.req.query(\"headless\");\r\n const headless = headlessParam === \"true\" || headlessParam === \"1\";\r\n const proxyOverride = c.req.query(\"proxy\")?.trim();\r\n const source = getSource(url);\r\n const fromSource = await getEffectiveProxyForListUrl(url, source);\r\n const ctx = buildSourceContext({\r\n cacheDir: CACHE_DIR,\r\n headless,\r\n proxy: proxyOverride || fromSource,\r\n });\r\n const items = await source.fetchItems(url, ctx);\r\n const mode = source.id === \"generic\" ? \"generic\" : \"plugin\";\r\n const effective = effectiveProxyUsed(proxyOverride, fromSource);\r\n return c.json({\r\n items,\r\n url,\r\n mode,\r\n pluginId: source.id,\r\n effectiveProxy: redactProxyForLog(effective),\r\n });\r\n } catch (err) {\r\n if (err instanceof AuthRequiredError) {\r\n const html = await render401(url);\r\n return c.html(html, 401);\r\n }\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return c.text(`解析失败: ${msg}`, 500);\r\n }\r\n });\r\n\r\n app.get(\"/admin/extractor/*\", requireAdmin(), async (c) => {\r\n const url = parseUrlFromPath(c.req.path, \"/admin/extractor\");\r\n if (!url) return c.text(\"无效 URL,格式: /admin/extractor/https://... 或 /admin/extractor/example.com/...\", 400);\r\n try {\r\n const headlessParam = c.req.query(\"headless\");\r\n const headless = headlessParam === \"true\" || headlessParam === \"1\";\r\n const proxyOverride = c.req.query(\"proxy\")?.trim();\r\n const source = getSource(url);\r\n const fromSource = await getEffectiveProxyForListUrl(url, source);\r\n const proxy = proxyOverride || fromSource;\r\n const result = await extractFromLink(url, {}, { timeoutMs: 60_000, headless, proxy });\r\n const effective = effectiveProxyUsed(proxyOverride, fromSource);\r\n return c.json({\r\n title: result.title ?? null,\r\n author: result.author ?? null,\r\n pubDate: result.pubDate ?? null,\r\n content: result.content ?? null,\r\n _extractor: \"readability\",\r\n effectiveProxy: redactProxyForLog(effective),\r\n });\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return c.text(`提取失败: ${msg}`, 500);\r\n }\r\n });\r\n}\r\n","// RSS 路由:/rss/* 生成 RSS XML\r\n\r\n\r\n\r\nimport { createHash } from \"node:crypto\";\r\n\r\nimport type { Hono } from \"hono\";\r\n\r\nimport { getItems, feedItemsToRssXml } from \"../../feeder/index.js\";\r\n\r\nimport { queryItems } from \"../../db/index.js\";\r\n\r\nimport { getAllSubscriptionRefs } from \"../../scraper/subscription/index.js\";\r\n\r\nimport { SOURCES_GROUP } from \"../../scraper/scheduler/index.js\";\r\n\r\nimport * as scheduler from \"../../scheduler/index.js\";\r\n\r\nimport { CACHE_DIR } from \"../../config/paths.js\";\r\n\r\nimport { AuthRequiredError, NotFoundError } from \"../../scraper/auth/index.js\";\r\n\r\nimport { parseUrlFromPath, readStaticHtml, escapeHtml } from \"../utils.js\";\r\n\r\n\r\n\r\nfunction parseSubscribedFlag(v: string | undefined): boolean {\r\n\r\n return v === \"1\" || v === \"true\" || v === \"yes\";\r\n\r\n}\r\n\r\n\r\n\r\nexport function registerRssRoutes(app: Hono): void {\r\n\r\n async function render401(listUrl: string): Promise<string> {\r\n\r\n const raw = await readStaticHtml(\"401\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>401</title></head><body><h1>401 需要登录</h1></body></html>\");\r\n\r\n return raw.replace(/\\{\\{listUrl\\}\\}/g, escapeHtml(listUrl));\r\n\r\n }\r\n\r\n\r\n\r\n /** 查询式 RSS:按 search、sourceUrl、subscribed、author、tags 过滤,返回匹配条目的 XML */\r\n\r\n app.get(\"/rss\", async (c) => {\r\n\r\n const search = c.req.query(\"search\") ?? c.req.query(\"q\") ?? undefined;\r\n\r\n const ref = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? c.req.query(\"sourceUrl\") ?? undefined;\r\n\r\n const subscribed = parseSubscribedFlag(c.req.query(\"subscribed\"));\r\n\r\n const author = c.req.query(\"author\") ?? undefined;\r\n\r\n const tagsParam = c.req.query(\"tags\") ?? undefined;\r\n\r\n const tags = tagsParam ? tagsParam.split(\",\").map((t) => t.trim()).filter(Boolean) : undefined;\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 50), 200);\r\n\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n\r\n const title = c.req.query(\"title\") ?? undefined;\r\n\r\n const daysParam = c.req.query(\"days\");\r\n\r\n const sinceParam = c.req.query(\"since\") ?? undefined;\r\n\r\n const untilParam = c.req.query(\"until\") ?? undefined;\r\n\r\n let since: Date | undefined;\r\n\r\n let until: Date | undefined;\r\n\r\n if (daysParam) {\r\n\r\n const n = Math.max(1, Math.min(365, Number(daysParam) || 1));\r\n\r\n const now = new Date();\r\n\r\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\r\n\r\n const todayEnd = new Date(todayStart);\r\n\r\n todayEnd.setDate(todayEnd.getDate() + 1);\r\n\r\n since = new Date(todayStart);\r\n\r\n since.setDate(since.getDate() - (n - 1));\r\n\r\n until = todayEnd;\r\n\r\n } else {\r\n\r\n since = sinceParam ? new Date(sinceParam) : undefined;\r\n\r\n if (untilParam) {\r\n\r\n if (untilParam.length === 10) {\r\n\r\n const d = new Date(untilParam + \"T12:00:00Z\");\r\n\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n\r\n until = d;\r\n\r\n } else {\r\n\r\n until = new Date(untilParam);\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n\r\n\r\n let sourceUrls: string[] | undefined;\r\n\r\n if (ref) {\r\n\r\n sourceUrls = undefined;\r\n\r\n } else if (subscribed) {\r\n\r\n sourceUrls = await getAllSubscriptionRefs();\r\n\r\n }\r\n\r\n\r\n\r\n if (sourceUrls?.length === 0) {\r\n\r\n const xml = feedItemsToRssXml([], new URL(c.req.url).href, lng, {\r\n\r\n channelTitle: title ?? \"RSS 订阅\",\r\n\r\n channelDesc: \"无匹配条目\",\r\n\r\n });\r\n\r\n return c.body(xml, 200, { \"Content-Type\": \"application/rss+xml; charset=utf-8\" });\r\n\r\n }\r\n\r\n\r\n\r\n const result = await queryItems({\r\n\r\n sourceUrl: sourceUrls ? undefined : ref,\r\n\r\n sourceUrls,\r\n\r\n author,\r\n\r\n q: search,\r\n\r\n tags,\r\n\r\n since,\r\n\r\n until,\r\n\r\n limit,\r\n\r\n offset,\r\n\r\n });\r\n\r\n const feedItems = result.items.map((dbItem) => ({\r\n\r\n guid: dbItem.id,\r\n\r\n title: dbItem.title ?? \"\",\r\n\r\n link: dbItem.url,\r\n\r\n pubDate: dbItem.pub_date ? new Date(dbItem.pub_date) : new Date(),\r\n\r\n author: dbItem.author ?? undefined,\r\n\r\n summary: dbItem.summary ?? undefined,\r\n\r\n content: dbItem.content ?? undefined,\r\n\r\n imageUrl: dbItem.image_url ?? undefined,\r\n\r\n tags: dbItem.tags ?? undefined,\r\n\r\n sourceRef: dbItem.source_url,\r\n\r\n translations: dbItem.translations ?? undefined,\r\n\r\n }));\r\n\r\n\r\n\r\n const rssUrl = new URL(c.req.url);\r\n\r\n const channelTitle = title ?? \"RSS 订阅\";\r\n\r\n const xml = feedItemsToRssXml(feedItems, rssUrl.href, lng, {\r\n\r\n channelTitle,\r\n\r\n channelDesc: `来自 ${rssUrl.href} 的订阅`,\r\n\r\n });\r\n\r\n return c.body(xml, 200, {\r\n\r\n \"Content-Type\": \"application/rss+xml; charset=utf-8\",\r\n\r\n });\r\n\r\n });\r\n\r\n\r\n\r\n /** 按 URL 抓取式 RSS:/rss/https://... 从信源实时抓取 */\r\n\r\n app.get(\"/rss/*\", async (c) => {\r\n\r\n const url = parseUrlFromPath(c.req.path, \"/rss\");\r\n\r\n if (!url) return c.text(\"无效 URL,格式: /rss/https://... 或 /rss/www.xiaohongshu.com/...\", 400);\r\n\r\n try {\r\n\r\n const headlessParam = c.req.query(\"headless\");\r\n\r\n const headless = headlessParam === \"false\" || headlessParam === \"0\" ? false : undefined;\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n const httpId = \"rss-\" + createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\r\n\r\n const { items } = await scheduler.schedule(\r\n\r\n SOURCES_GROUP,\r\n\r\n httpId,\r\n\r\n () => getItems(url, { cacheDir: CACHE_DIR, headless, lng }),\r\n\r\n );\r\n\r\n const xml = feedItemsToRssXml(items, url, lng);\r\n\r\n return c.body(xml, 200, {\r\n\r\n \"Content-Type\": \"application/rss+xml; charset=utf-8\",\r\n\r\n });\r\n\r\n } catch (err) {\r\n\r\n if (err instanceof AuthRequiredError) {\r\n\r\n const html = await render401(url);\r\n\r\n return c.html(html, 401);\r\n\r\n }\r\n\r\n if (err instanceof NotFoundError) {\r\n\r\n const html = await readStaticHtml(\"404\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>404</title></head><body><h1>404 未找到</h1></body></html>\");\r\n\r\n return c.html(html, 404);\r\n\r\n }\r\n\r\n const msg = err instanceof Error ? err.message : String(err);\r\n\r\n return c.text(`生成 RSS 失败: ${msg}`, 500);\r\n\r\n }\r\n\r\n });\r\n\r\n}\r\n\r\n","// 生产环境:在同一端口托管 SvelteKit 静态构建(adapter-static + 200.html SPA fallback)\r\n\r\nimport { existsSync } from \"node:fs\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { join, relative } from \"node:path\";\r\nimport { serveStatic } from \"@hono/node-server/serve-static\";\r\nimport type { Context, Hono } from \"hono\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\n/** 与 svelte.config.js 中 adapter-static 的 pages/assets 输出目录一致 */\r\nexport function getWebUiBuildDir(): string {\r\n const w = process.env.WEBUI_BUILD_DIR?.trim();\r\n if (w) {\r\n if (w.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(w)) return w;\r\n return join(process.cwd(), w);\r\n }\r\n return join(PACKAGE_ROOT, \"webui/build\");\r\n}\r\n\r\n/** 仅后端接口路径,不走静态/SPA;注意 /admin 为前端路由,仅 /admin/parse、/admin/extractor 为后端 */\r\nfunction isBackendOnlyPath(pathname: string): boolean {\r\n if (pathname.startsWith(\"/api\")) return true;\r\n if (pathname.startsWith(\"/rss\")) return true;\r\n if (pathname.startsWith(\"/auth\")) return true;\r\n if (pathname.startsWith(\"/admin/parse\") || pathname.startsWith(\"/admin/extractor\")) return true;\r\n return false;\r\n}\r\n\r\n/** 明显是静态资源路径但磁盘上无对应文件时,不应返回 200.html */\r\nfunction looksLikeStaticAsset(pathname: string): boolean {\r\n return /\\.[a-zA-Z0-9]{1,12}$/.test(pathname);\r\n}\r\n\r\n/**\r\n * 在已注册全部 API 路由之后调用。\r\n * `serveStatic` 的 root 需为相对 cwd 的路径(见 @hono/node-server/serve-static)。\r\n */\r\nexport function registerWebUiRoutes(app: Hono): void {\r\n const absRoot = getWebUiBuildDir();\r\n if (!existsSync(absRoot)) {\r\n console.warn(\r\n \"未找到 WebUI 构建目录,跳过根路径静态托管:\",\r\n absRoot,\r\n \"(构建前端:pnpm run webui:build)\",\r\n );\r\n return;\r\n }\r\n\r\n const relRoot = relative(process.cwd(), absRoot).replace(/\\\\/g, \"/\");\r\n const staticRoot =\r\n relRoot === \"\" || relRoot === \".\"\r\n ? \".\"\r\n : relRoot.startsWith(\".\") || relRoot.startsWith(\"/\") || /^[A-Za-z]:/.test(relRoot)\r\n ? relRoot\r\n : `./${relRoot}`;\r\n\r\n const staticMw = serveStatic({\r\n root: staticRoot,\r\n index: \"200.html\",\r\n });\r\n\r\n app.use(\"*\", async (c, next) => {\r\n if (isBackendOnlyPath(c.req.path)) return next();\r\n return staticMw(c, next);\r\n });\r\n\r\n const spaFallback = async (c: Context) => {\r\n const p = c.req.path;\r\n if (isBackendOnlyPath(p)) return c.notFound();\r\n if (looksLikeStaticAsset(p)) return c.notFound();\r\n try {\r\n const html = await readFile(join(absRoot, \"200.html\"), \"utf-8\");\r\n return c.html(html);\r\n } catch {\r\n return c.notFound();\r\n }\r\n };\r\n\r\n app.get(\"*\", spaFallback);\r\n}\r\n","// App 入口:Hono 服务,与 feeder 解耦;可替换为 Express 等\r\n\r\nimport \"dotenv/config\";\r\nimport { watch } from \"node:fs\";\r\nimport { networkInterfaces } from \"node:os\";\r\nimport { serve } from \"@hono/node-server\";\r\nimport { Hono } from \"hono\";\r\nimport { cors } from \"hono/cors\";\r\nimport { initSources as initSites } from \"../scraper/sources/index.js\";\r\nimport { initScheduler } from \"../scraper/scheduler/index.js\";\r\nimport { initUserDir, BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR, CACHE_DIR } from \"../config/paths.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { registerApiRoutes } from \"./routes/api/index.js\";\r\nimport { registerAuthRoutes } from \"./routes/auth.js\";\r\nimport { registerAdminRoutes } from \"./routes/admin.js\";\r\nimport { registerRssRoutes } from \"./routes/rss.js\";\r\nimport { registerWebUiRoutes } from \"./webui.js\";\r\n\r\nconst PORT = Number(process.env.PORT) || 18473;\r\nconst IS_DEV = process.env.NODE_ENV === \"development\" || process.argv.includes(\"--watch\");\r\nconst PLUGIN_WATCH_EXTS = [\".rssany.js\", \".rssany.ts\"];\r\n\r\nfunction createApp(): Hono {\r\n const app = new Hono();\r\n\r\n app.use(\r\n \"*\",\r\n cors({\r\n origin: \"*\",\r\n allowMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"PATCH\"],\r\n allowHeaders: [\"Content-Type\", \"Authorization\", \"X-Admin-Token\"],\r\n }),\r\n );\r\n\r\n registerApiRoutes(app);\r\n registerAuthRoutes(app);\r\n registerAdminRoutes(app);\r\n registerRssRoutes(app);\r\n registerWebUiRoutes(app);\r\n\r\n return app;\r\n}\r\n\r\nfunction watchPlugins(): void {\r\n let reloadTimer: NodeJS.Timeout | null = null;\r\n const debouncedReload = async () => {\r\n if (reloadTimer) clearTimeout(reloadTimer);\r\n reloadTimer = setTimeout(async () => {\r\n try {\r\n await initSites();\r\n } catch (err) {\r\n logger.error(\"plugin\", \"插件重新加载失败\", { err: err instanceof Error ? err.message : String(err) });\r\n }\r\n }, 300);\r\n };\r\n for (const dir of [BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR]) {\r\n const watcher = watch(dir, { recursive: true }, (eventType, filename) => {\r\n if (!filename || !PLUGIN_WATCH_EXTS.some((ext) => filename.endsWith(ext))) return;\r\n if (eventType === \"rename\" || eventType === \"change\") debouncedReload();\r\n });\r\n watcher.on(\"error\", (err) => {\r\n logger.warn(\"plugin\", \"插件目录监听错误\", { dir, err: err.message });\r\n });\r\n }\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n await initUserDir();\r\n await initSites();\r\n await initScheduler(CACHE_DIR);\r\n const app = createApp();\r\n const server = serve({ fetch: app.fetch, port: PORT, hostname: \"0.0.0.0\" });\r\n server.setMaxListeners(32);\r\n console.log(`服务已启动 http://127.0.0.1:${PORT}/(API + 静态前端,需先 pnpm run webui:build)`);\r\n const lanIp = Object.values(networkInterfaces()).flat().find((iface) => iface?.family === \"IPv4\" && !iface.internal)?.address;\r\n if (lanIp) console.log(`局域网访问 http://${lanIp}:${PORT}/`);\r\n if (IS_DEV) {\r\n watchPlugins();\r\n }\r\n}\r\nmain();\r\n"],"names":["s","now","mergeSourceStatsRows","base","resolve","authenticated","cacherCacheKey","fetchHtmlFn","SYSTEM","parseSteps","cronValidate","tasks","cronSchedule","runNow","scheduler.unscheduleGroup","scheduler.validateCron","scheduler.schedule","PORT","scheduler.getGroupStats","parseSubscribedFlag","taskStore.getTask","taskStore.createTask","taskStore.setTaskRunning","taskStore.setTaskDone","taskStore.setTaskError","mime","xml","initSites"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../app/scraper/sources/web/fetcher/purify.ts","../app/scraper/sources/web/fetcher/cdp.ts","../app/types/feedItem.ts","../app/utils/httpSourceRef.ts","../app/packageRoot.ts","../app/config/paths.ts","../app/db/index.ts","../app/core/logger/config.ts","../app/core/logger/index.ts","../app/scraper/sources/web/fetcher/browser.ts","../app/utils/refreshInterval.ts","../app/core/cacher/index.ts","../app/scraper/sources/web/extractor/extractor.ts","../app/core/llmConfig.ts","../app/core/llm.ts","../app/scraper/sources/web/parser/parser.ts","../app/scraper/sources/web/site.ts","../app/scraper/auth/errors.ts","../app/plugins/loader.ts","../app/scraper/sources/web/index.ts","../app/plugins/hostDeps.ts","../app/scraper/sources/context.ts","../app/scraper/sources/index.ts","../app/scraper/subscription/types.ts","../app/config/globalProxy.ts","../app/scraper/subscription/index.ts","../app/pipeline/qualityFilter.ts","../app/pipeline/tagger.ts","../app/pipeline/translator.ts","../app/pipeline/config.ts","../app/pipeline/index.ts","../app/feeder/rss.ts","../app/core/events/index.ts","../app/config/deliver.ts","../app/deliver/post.ts","../app/feeder/feeder.ts","../app/scheduler/index.ts","../app/scraper/scheduler/index.ts","../app/auth/middleware.ts","../app/router/routes/api/server.ts","../app/router/routes/api/rss.ts","../app/router/routes/api/scheduler.ts","../app/router/routes/api/plugins.ts","../app/router/routes/api/pipeline.ts","../app/router/routes/api/feed.ts","../app/router/routes/api/items.ts","../app/router/routes/api/logs.ts","../app/router/routes/api/admin.ts","../app/router/routes/api/sources.ts","../app/router/routes/api/topics.ts","../app/router/routes/api/deliver.ts","../app/config/llmSettings.ts","../app/router/routes/api/llm.ts","../app/router/routes/api/proxy.ts","../app/tasks/index.ts","../app/router/routes/api/tasks.ts","../app/router/routes/api/feed-favicon.ts","../app/router/routes/api/index.ts","../app/router/routes/auth.ts","../app/router/utils.ts","../app/router/routes/admin.ts","../app/router/routes/rss.ts","../app/router/webui.ts","../app/router/index.ts"],"sourcesContent":["// 基于 node-html-parser 的 HTML 净化:移除与 RSS 内容无关的标签、属性、注释\n\nimport { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement, Node } from \"node-html-parser\";\n\n\nconst TAGS_TO_REMOVE = [\n \"script\", \"style\", \"svg\", \"symbol\", \"link\", \"meta\",\n \"input\", \"embed\", \"button\", \"select\", \"textarea\",\n \"nav\", \"iframe\", \"noscript\", \"template\", \"object\", \"canvas\",\n];\n\n\nconst BASE64_IMG_PATTERN = /^data:image\\/[^;\"'\\s]+;base64,/i;\n\n\nfunction collectCommentNodes(node: Node, out: Node[]): void {\n if (node.nodeType === NodeType.COMMENT_NODE) {\n out.push(node);\n return;\n }\n if (\"childNodes\" in node && Array.isArray(node.childNodes)) {\n for (const child of node.childNodes) {\n collectCommentNodes(child, out);\n }\n }\n}\n\n\nfunction stripRssIrrelevantAttributes(root: HTMLElement): void {\n const toProcess: HTMLElement[] = [root];\n const all = root.querySelectorAll(\"*\");\n for (const el of all) {\n if (el.nodeType === NodeType.ELEMENT_NODE) toProcess.push(el as HTMLElement);\n }\n for (const elem of toProcess) {\n elem.removeAttribute(\"class\");\n elem.removeAttribute(\"style\");\n const src = elem.getAttribute(\"src\");\n if (src && BASE64_IMG_PATTERN.test(src)) {\n elem.removeAttribute(\"src\");\n }\n }\n}\n\n\n/** 使用 node-html-parser 解析并净化 HTML,剥离与 RSS 内容无关的部分(默认开启) */\nexport function applyPurify(html: string, purify: boolean | undefined): string {\n if (purify === false) return html;\n const root = parse(html, { comment: true });\n for (const tag of TAGS_TO_REMOVE) {\n const list = root.querySelectorAll(tag);\n for (const el of list) {\n el.remove();\n }\n }\n const commentNodes: Node[] = [];\n collectCommentNodes(root, commentNodes);\n for (const node of commentNodes) {\n node.remove();\n }\n stripRssIrrelevantAttributes(root);\n return root.toString();\n}\n","// Chrome DevTools Protocol (CDP) 控制模块:支持连接到手动启动的 Chrome,避免安装 Puppeteer 专属 Chrome\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { platform } from \"node:os\";\nimport { join } from \"node:path\";\nimport puppeteerCore, { type Browser, type ConnectOptions, type LaunchOptions } from \"puppeteer-core\";\n\n\n/** 按平台枚举所有可能的 Chrome 路径,优先返回第一个实际存在的;全部不存在则返回 null */\nexport function findChromeExecutable(): string | null {\n const platformName = platform();\n const paths: string[] = [];\n const envChrome = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;\n if (envChrome) paths.push(envChrome);\n if (platformName === \"darwin\") {\n paths.push(\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\"\n );\n } else if (platformName === \"linux\") {\n paths.push(\n \"/usr/bin/google-chrome\",\n \"/usr/bin/google-chrome-stable\",\n \"/usr/bin/chromium\",\n \"/usr/bin/chromium-browser\",\n \"/snap/bin/chromium\"\n );\n } else if (platformName === \"win32\") {\n const programFiles = process.env[\"ProgramFiles\"] || \"C:\\\\Program Files\";\n const programFilesX86 = process.env[\"ProgramFiles(x86)\"] || \"C:\\\\Program Files (x86)\";\n paths.push(\n join(programFiles, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n join(programFilesX86, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n join(programFiles, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\"),\n join(programFilesX86, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\")\n );\n }\n for (const p of paths) {\n try {\n if (existsSync(p)) return p;\n } catch {\n // 跳过\n }\n }\n return null;\n}\n\n\n/** CDP 连接配置 */\nexport interface CDPConfig {\n /** Chrome 可执行文件路径,不提供则自动查找 */\n executablePath?: string;\n /** CDP 端口,默认 9222 */\n port?: number;\n /** 是否使用已启动的 Chrome(不自动启动) */\n useExisting?: boolean;\n /** userDataDir,用于持久化 cookies 等 */\n userDataDir?: string;\n /** 是否无头模式 */\n headless?: boolean;\n /** 代理配置 */\n proxy?: string;\n /** 额外的 Chrome 启动参数 */\n args?: string[];\n}\n\n\n/** 启动 Chrome 进程并返回 CDP WebSocket URL */\nasync function launchChromeWithCDP(config: CDPConfig): Promise<{ process: ChildProcess; wsEndpoint: string }> {\n const port = config.port ?? 9222;\n const executablePath = config.executablePath || findChromeExecutable();\n if (!executablePath) {\n throw new Error(\"未找到 Chrome 可执行文件,请设置 CHROME_PATH 环境变量或提供 executablePath\");\n }\n const args: string[] = [\n `--remote-debugging-port=${port}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-blink-features=AutomationControlled\",\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-web-security\",\n \"--disable-features=IsolateOrigins,site-per-process\",\n \"--disable-site-isolation-trials\",\n \"--disable-infobars\",\n ];\n if (config.headless !== false) {\n args.push(\"--headless=new\");\n }\n if (config.userDataDir) {\n args.push(`--user-data-dir=${config.userDataDir}`);\n }\n if (config.proxy) {\n const u = new URL(config.proxy);\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\n args.push(`--proxy-server=${serverUrl}`);\n }\n if (config.args) {\n args.push(...config.args);\n }\n const process = spawn(executablePath, args, {\n stdio: [\"ignore\", \"ignore\", \"ignore\"],\n detached: false,\n });\n // 等待 CDP 端点可用\n const wsEndpoint = `ws://127.0.0.1:${port}/devtools/browser`;\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Chrome 启动超时,无法连接到 CDP 端口 ${port}`));\n }, 30000);\n const checkInterval = setInterval(async () => {\n try {\n const http = await import(\"node:http\");\n const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {\n if (res.statusCode === 200) {\n clearInterval(checkInterval);\n clearTimeout(timeout);\n resolve();\n }\n });\n req.on(\"error\", () => {\n // 继续等待\n });\n req.end();\n } catch {\n // 继续等待\n }\n }, 500);\n process.on(\"error\", (err) => {\n clearInterval(checkInterval);\n clearTimeout(timeout);\n reject(err);\n });\n });\n return { process, wsEndpoint };\n}\n\n\n/** 连接到已启动的 Chrome(通过 CDP 端口) */\nasync function connectToExistingChrome(port: number = 9222): Promise<string> {\n const wsEndpoint = `ws://127.0.0.1:${port}/devtools/browser`;\n try {\n const http = await import(\"node:http\");\n await new Promise<void>((resolve, reject) => {\n const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {\n if (res.statusCode === 200) {\n resolve();\n } else {\n reject(new Error(`无法连接到 Chrome CDP 端口 ${port}`));\n }\n });\n req.on(\"error\", (err) => {\n reject(new Error(`无法连接到 Chrome CDP 端口 ${port},请确保 Chrome 已启动并开启 --remote-debugging-port=${port}: ${err.message}`));\n });\n req.end();\n });\n return wsEndpoint;\n } catch (err) {\n throw err instanceof Error ? err : new Error(`无法连接到 Chrome CDP 端口 ${port},请确保 Chrome 已启动并开启 --remote-debugging-port=${port}`);\n }\n}\n\n\n/** 通过 CDP 连接到 Chrome 并返回 Browser 对象 */\nexport async function connectBrowser(config: CDPConfig): Promise<{ browser: Browser; cleanup?: () => Promise<void> }> {\n let wsEndpoint: string;\n let chromeProcess: ChildProcess | undefined;\n if (config.useExisting) {\n wsEndpoint = await connectToExistingChrome(config.port);\n } else {\n const result = await launchChromeWithCDP(config);\n wsEndpoint = result.wsEndpoint;\n chromeProcess = result.process;\n }\n const connectOptions: ConnectOptions = {\n browserWSEndpoint: wsEndpoint,\n };\n const browser = await puppeteerCore.connect(connectOptions);\n const cleanup = chromeProcess\n ? async () => {\n await browser.disconnect();\n chromeProcess?.kill();\n }\n : async () => {\n await browser.disconnect();\n };\n return { browser, cleanup };\n}\n\n\n/** 通过 CDP 启动 Chrome 并返回 Browser 对象(使用 executablePath) */\nexport async function launchBrowser(config: CDPConfig & { executablePath: string }): Promise<{ browser: Browser; cleanup: () => Promise<void> }> {\n const port = config.port ?? 9222;\n const args: string[] = [\n `--remote-debugging-port=${port}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-blink-features=AutomationControlled\",\n \"--no-sandbox\",\n \"--disable-setuid-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-web-security\",\n \"--disable-features=IsolateOrigins,site-per-process\",\n \"--disable-site-isolation-trials\",\n \"--disable-infobars\",\n ];\n if (config.headless !== false) {\n args.push(\"--headless=new\");\n }\n if (config.userDataDir) {\n args.push(`--user-data-dir=${config.userDataDir}`);\n }\n if (config.proxy) {\n const u = new URL(config.proxy);\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\n args.push(`--proxy-server=${serverUrl}`);\n }\n if (config.args) {\n args.push(...config.args);\n }\n const launchOptions: LaunchOptions = {\n executablePath: config.executablePath,\n headless: config.headless !== false,\n args,\n userDataDir: config.userDataDir,\n ignoreDefaultArgs: [\"--enable-automation\"],\n };\n const browser = await puppeteerCore.launch(launchOptions);\n const cleanup = async () => {\n await browser.close();\n };\n return { browser, cleanup };\n}\n\n\n/** 导出类型供外部使用 */\nexport type { Browser, Page } from \"puppeteer-core\";\n","/**\r\n * 系统内部统一的 Feed Item 定义\r\n * 插件 → Normalizer → RSS Generator\r\n * 自包含:携带 sourceRef 后,入库 / Signal 投递等无需再单独传 ref。\r\n */\r\n\r\n/** 单语种译文字段(key 为 BCP 47,如 zh-CN、en) */\r\nexport interface ItemTranslationFields {\r\n title?: string;\r\n summary?: string;\r\n content?: string;\r\n}\r\n\r\n/** 带可选 translations 的条目视图(FeedItem 或 DB 行 + translations 等) */\r\nexport interface ItemWithOptionalTranslations {\r\n title: string;\r\n summary?: string;\r\n content?: string;\r\n translations?: Record<string, ItemTranslationFields>;\r\n}\r\n\r\n/**\r\n * 根据 lng 取条目的「有效」标题/摘要/正文:有 translations[lng] 则优先用译文,否则用原文。\r\n * 路由层传 lng 时用此结果生成 RSS 或 API 响应。\r\n */\r\nexport function getEffectiveItemFields(\r\n item: ItemWithOptionalTranslations,\r\n lng?: string | null,\r\n): { title: string; summary: string; content: string } {\r\n const raw = lng && lng !== \"\" ? item.translations?.[lng] : undefined;\r\n const t = raw && typeof raw === \"object\" ? raw : undefined;\r\n return {\r\n title: (t?.title != null && t.title !== \"\" ? t.title : item.title) ?? \"\",\r\n summary: (t?.summary != null && t.summary !== \"\" ? t.summary : item.summary) ?? \"\",\r\n content: (t?.content != null && t.content !== \"\" ? t.content : item.content) ?? \"\",\r\n };\r\n}\r\n\r\n/** 将发布时间转为 ISO 字符串供入库/序列化;Invalid Date 返回 null(避免 toISOString 抛 RangeError) */\r\nexport function pubDateToIsoOrNull(pubDate: unknown): string | null {\r\n if (pubDate == null) return null;\r\n if (pubDate instanceof Date) {\r\n const ms = pubDate.getTime();\r\n return Number.isNaN(ms) ? null : pubDate.toISOString();\r\n }\r\n if (typeof pubDate === \"string\") {\r\n const s = pubDate.trim();\r\n return s || null;\r\n }\r\n return null;\r\n}\r\n\r\n/** 将 author 规范为 string[],兼容 string 输入(插件等) */\r\nexport function normalizeAuthor(author: string | string[] | null | undefined): string[] | undefined {\r\n if (author == null) return undefined;\r\n if (Array.isArray(author)) return author.filter((s) => typeof s === \"string\" && s.trim()).map((s) => s.trim());\r\n const s = String(author).trim();\r\n return s ? [s] : undefined;\r\n}\r\n\r\n/** 将 author 转为显示用字符串(逗号分隔) */\r\nexport function authorToDisplay(author: string | string[] | null | undefined): string {\r\n const arr = normalizeAuthor(author);\r\n return arr?.join(\", \") ?? \"\";\r\n}\r\n\r\nexport interface FeedItem {\r\n /** 全局唯一标识,link 或稳定 hash */\r\n guid: string;\r\n /** 标题 */\r\n title: string;\r\n /** 原文链接 */\r\n link: string;\r\n /** 发布时间 */\r\n pubDate: Date;\r\n /** 作者列表 */\r\n author?: string[];\r\n /** 简要描述(纯文本,适合 RSS description) */\r\n summary?: string;\r\n /** 详情正文(输出到 RSS description) */\r\n content?: string;\r\n /** 条目配图 URL,输出为 RSS 2.0 <enclosure type=\"image/...\"> */\r\n imageUrl?: string;\r\n /**\r\n * 封面图:支持 http(s) URL、data URL,或裸 base64(可规范为 data:image/jpeg;base64,...)。\r\n * Gateway JSON 字段名 `cover_img`。\r\n */\r\n cover_img?: string;\r\n /** RSS 来源分类(直接来自 feed 的 <category> 字段) */\r\n categories?: string[];\r\n /** 系统 / pipeline 生成的标签(从用户管理的标签库中匹配) */\r\n tags?: string[];\r\n /** 信源标识(列表页 URL 或 imap 等),入库与按 ref 筛选用;设后则 upsertItems 等无需再传 ref */\r\n sourceRef?: string;\r\n /**\r\n * 多语种译文。key 为 BCP 47(如 zh-CN、en),路由支持 lng 参数时可据此返回对应译文。\r\n * 由 pipeline(如 translator)写入。\r\n */\r\n translations?: Record<string, ItemTranslationFields>;\r\n /**\r\n * 扩展字段,给插件留后门。\r\n * 框架保留键:`_rssanyPipelineDrop` 为 true 表示 pipeline 质量过滤丢弃,feeder 会删库并移出 RSS。\r\n */\r\n extra?: Record<string, unknown>;\r\n }\r\n\r\n/** Pipeline 质量过滤等步骤标记的丢弃键(写入 item.extra) */\r\nexport const PIPELINE_DROP_EXTRA_KEY = \"_rssanyPipelineDrop\";\r\n\r\nexport function markPipelineDrop(item: FeedItem): FeedItem {\r\n item.extra = { ...item.extra, [PIPELINE_DROP_EXTRA_KEY]: true };\r\n return item;\r\n}\r\n\r\nexport function isPipelineDroppedItem(item: FeedItem): boolean {\r\n return item.extra?.[PIPELINE_DROP_EXTRA_KEY] === true;\r\n}","/**\r\n * 信源 ref 入库与查询统一键(规范化存储):\r\n * - http(s):scheme、host(含端口)小写;pathname 小写并去掉非根路径末尾 `/`;query、hash 保持原样(避免破坏带大小写的查询参数)。\r\n * - 非 http(s):trim 后全串小写。\r\n */\r\nexport function canonicalHttpSourceRef(ref: string): string {\r\n const t = ref.trim();\r\n if (!t) return t;\r\n if (!/^https?:\\/\\//i.test(t)) return t.toLowerCase();\r\n try {\r\n const u = new URL(t);\r\n const protocol = u.protocol.toLowerCase();\r\n const host = u.host.toLowerCase();\r\n let path = u.pathname;\r\n if (path.length > 1 && path.endsWith(\"/\")) {\r\n path = path.slice(0, -1);\r\n }\r\n path = path.toLowerCase();\r\n return `${protocol}//${host}${path}${u.search}${u.hash}`;\r\n } catch {\r\n return t.toLowerCase();\r\n }\r\n}\r\n\r\nfunction maxIso(a: string | null, b: string | null): string | null {\r\n if (!a) return b;\r\n if (!b) return a;\r\n return a >= b ? a : b;\r\n}\r\n\r\n/**\r\n * 将 GROUP BY source_url 的统计按规范化键合并(兼容迁移前旧数据或异常重复写法)。\r\n */\r\nexport function mergeSourceStatsRows(\r\n rows: { source_url: string; count: number; count_7d: number; latest_at: string | null }[],\r\n): { source_url: string; count: number; count_7d: number; latest_at: string | null }[] {\r\n const map = new Map<string, { count: number; count_7d: number; latest_at: string | null }>();\r\n for (const row of rows) {\r\n const k = canonicalHttpSourceRef(row.source_url);\r\n const prev = map.get(k);\r\n const count7 = row.count_7d ?? 0;\r\n if (!prev) {\r\n map.set(k, { count: row.count, count_7d: count7, latest_at: row.latest_at });\r\n } else {\r\n map.set(k, {\r\n count: prev.count + row.count,\r\n count_7d: prev.count_7d + count7,\r\n latest_at: maxIso(prev.latest_at, row.latest_at),\r\n });\r\n }\r\n }\r\n return [...map.entries()]\r\n .map(([source_url, v]) => ({ source_url, count: v.count, count_7d: v.count_7d, latest_at: v.latest_at }))\r\n .sort((a, b) => b.count - a.count);\r\n}\r\n","// 解析 npm 包根或仓库根(含 app/plugins/builtin/、statics/、webui/build/);开发时来自 app/,打包后为 dist/ 的上一级\r\n\r\nimport { basename, dirname, join } from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\nconst __dir = dirname(fileURLToPath(import.meta.url));\r\nconst base = basename(__dir);\r\n\r\nexport const PACKAGE_ROOT =\r\n base === \"app\" || base === \"dist\" ? join(__dir, \"..\") : __dir;\r\n","// 路径配置:集中管理所有运行时路径,区分项目文件与用户数据\r\n\r\nimport { mkdir, rename, access, copyFile, writeFile } from \"node:fs/promises\";\r\nimport { homedir } from \"node:os\";\r\nimport { join } from \"node:path\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\nconst envUserDir = process.env.RSSANY_USER_DIR?.trim();\r\n\r\n/** 用户数据根目录:~/.rssany/(或 RSSANY_USER_DIR);不纳入版本管理 */\r\nexport const USER_DIR = envUserDir && envUserDir.length > 0 ? envUserDir : join(homedir(), \".rssany\");\r\n\r\n/** SQLite 数据库目录:.rssany/data/ */\r\nexport const DATA_DIR = join(USER_DIR, \"data\");\r\n\r\n/** 缓存目录:.rssany/cache/(fetched、extracted、feeds、domains、browser_data 等子目录);环境变量 CACHE_DIR 可覆盖 */\r\nexport const CACHE_DIR = process.env.CACHE_DIR ?? join(USER_DIR, \"cache\");\r\n\r\n/** 站点配置文件:.rssany/sites.json */\r\nexport const SITES_CONFIG_PATH = join(USER_DIR, \"sites.json\");\r\n\r\n/** 爬虫配置:.rssany/sources.json(扁平信源列表,供 scheduler 使用) */\r\nexport const SOURCES_CONFIG_PATH = join(USER_DIR, \"sources.json\");\r\n\r\n/** 系统标签配置:.rssany/tags.json(供 pipeline tagger 使用) */\r\nexport const TAGS_CONFIG_PATH = join(USER_DIR, \"tags.json\");\r\n\r\n/** 全局配置:.rssany/config.json(pipeline 等) */\r\nexport const CONFIG_PATH = join(USER_DIR, \"config.json\");\r\n\r\n/** @deprecated 仅用于迁移:若存在 .rssany/subscriptions.json 且无 sources.json 则迁移为 sources.json */\r\nconst LEGACY_SUBSCRIPTIONS_PATH = join(USER_DIR, \"subscriptions.json\");\r\n\r\n/** 内置信源插件目录:app/plugins/builtin/(随包发布 *.rssany.js) */\r\nexport const BUILTIN_PLUGINS_DIR = join(PACKAGE_ROOT, \"app/plugins/builtin\");\r\n\r\n/** 用户插件目录:.rssany/plugins/(扁平 *.rssany.js / *.rssany.ts) */\r\nexport const USER_PLUGINS_DIR = join(USER_DIR, \"plugins\");\r\n\r\n/** 限定 .rssany 下动态 import 的模块类型,避免 Node 一直向上解析到用户主目录的 package.json 并触发 MODULE_TYPELESS_PACKAGE_JSON */\r\nconst USER_DIR_PACKAGE_JSON = join(USER_DIR, \"package.json\");\r\nconst USER_DIR_PACKAGE_JSON_MINIMAL = `${JSON.stringify({ type: \"module\", private: true, description: \"RssAny user data root; marks plugins as ESM for Node\" })}\\n`;\r\n\r\n/** 管理页「添加插件」所用模板(非 Site,不参与加载) */\r\nexport const PLUGIN_SITE_TEMPLATE_PATH = join(PACKAGE_ROOT, \"app/plugins/site.rssany.js\");\r\n\r\nasync function pathExists(p: string): Promise<boolean> {\r\n try {\r\n await access(p);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nasync function migrateFile(from: string, to: string): Promise<void> {\r\n if (!(await pathExists(from))) return;\r\n if (await pathExists(to)) return;\r\n try {\r\n await rename(from, to);\r\n logger.info(\"config\", \"配置已迁移\", { from, to });\r\n } catch (err) {\r\n logger.warn(\"config\", \"配置迁移失败\", { from, to, err: err instanceof Error ? err.message : String(err) });\r\n }\r\n}\r\n\r\n/** 包内首次初始化用的默认数据(`init/`;发布包需列入 package.json `files`) */\r\nconst INIT_DATA_DIR = join(PACKAGE_ROOT, \"init\");\r\nconst EXAMPLE_SOURCES = join(INIT_DATA_DIR, \"sources.json\");\r\nconst EXAMPLE_CONFIG = join(INIT_DATA_DIR, \"config.json\");\r\n\r\n/**\r\n * 若用户目录尚无 `sources.json` / `config.json`,则从包内 `init/sources.json`、`init/config.json` 复制(不覆盖已有文件)。\r\n */\r\nasync function seedExampleConfigsIfMissing(): Promise<void> {\r\n if (!(await pathExists(SOURCES_CONFIG_PATH)) && (await pathExists(EXAMPLE_SOURCES))) {\r\n try {\r\n await copyFile(EXAMPLE_SOURCES, SOURCES_CONFIG_PATH);\r\n logger.info(\"config\", \"已写入默认信源示例\", { path: SOURCES_CONFIG_PATH });\r\n } catch (err) {\r\n logger.warn(\"config\", \"写入 sources 示例失败\", {\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n if (!(await pathExists(CONFIG_PATH)) && (await pathExists(EXAMPLE_CONFIG))) {\r\n try {\r\n await copyFile(EXAMPLE_CONFIG, CONFIG_PATH);\r\n logger.info(\"config\", \"已写入默认配置示例\", { path: CONFIG_PATH });\r\n } catch (err) {\r\n logger.warn(\"config\", \"写入 config 示例失败\", {\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n}\r\n\r\n/** 若尚无文件则写入最小 package.json,使用户插件目录下的 *.rssany.js 被明确视为 ESM */\r\nasync function ensureUserDirPackageJsonForPlugins(): Promise<void> {\r\n if (await pathExists(USER_DIR_PACKAGE_JSON)) return;\r\n try {\r\n await writeFile(USER_DIR_PACKAGE_JSON, USER_DIR_PACKAGE_JSON_MINIMAL, \"utf-8\");\r\n logger.info(\"config\", \"已写入 .rssany/package.json(type: module,消除插件 ESM 歧义)\", { path: USER_DIR_PACKAGE_JSON });\r\n } catch (err) {\r\n logger.warn(\"config\", \"写入 .rssany/package.json 失败\", {\r\n path: USER_DIR_PACKAGE_JSON,\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n}\r\n\r\n/** 初始化用户数据目录;若缺少 sources.json / config.json 则从包内示例复制 */\r\nexport async function initUserDir(): Promise<void> {\r\n await mkdir(USER_DIR, { recursive: true });\r\n await mkdir(DATA_DIR, { recursive: true });\r\n await mkdir(CACHE_DIR, { recursive: true });\r\n await mkdir(USER_PLUGINS_DIR, { recursive: true });\r\n await ensureUserDirPackageJsonForPlugins();\r\n await seedExampleConfigsIfMissing();\r\n if (!(await pathExists(SOURCES_CONFIG_PATH)) && (await pathExists(LEGACY_SUBSCRIPTIONS_PATH))) {\r\n await migrateFile(LEGACY_SUBSCRIPTIONS_PATH, SOURCES_CONFIG_PATH);\r\n }\r\n}\r\n","// SQLite 主库:建表 schema、FTS、FeedItem CRUD 与运行日志库\r\n\r\nimport Database from \"better-sqlite3\";\r\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { existsSync, openSync, closeSync, writeSync, unlinkSync, readFileSync } from \"node:fs\";\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { normalizeAuthor, pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport { canonicalHttpSourceRef } from \"../utils/httpSourceRef.js\";\r\nimport type { LogEntry } from \"../core/logger/types.js\";\r\nimport { DATA_DIR, TAGS_CONFIG_PATH } from \"../config/paths.js\";\r\n\r\n/** 主库日志模式:默认 WAL;环境变量 RSSANY_DB_JOURNAL=delete 时使用 DELETE(利于 Windows 下多进程/拷贝场景) */\r\nconst MAIN_DB_JOURNAL = (process.env.RSSANY_DB_JOURNAL ?? \"wal\").toLowerCase() === \"delete\" ? \"DELETE\" : \"WAL\";\r\n\r\nlet _db: Database.Database | null = null;\r\n\r\n/** 懒初始化:并发调用 getDb() 时共用一个 Promise,避免重复 new Database() + initSchema;失败时清空以便重试。损坏时可能抛出 SQLITE_CORRUPT_VTAB / database disk image is malformed */\r\nlet _dbInit: Promise<Database.Database> | null = null;\r\n\r\n/** 单写者锁:串行化 updateItemContent/upsertItems 等写库,避免 WAL 下并发 transient 错误 */\r\nlet _writeLock: Promise<void> = Promise.resolve();\r\n\r\n/** 单进程锁文件路径:与 rssany.db 同目录,内容为 PID */\r\nconst MAIN_DB_LOCK_PATH = join(DATA_DIR, \"rssany.db.lock\");\r\n\r\n/** 将损坏类错误写到 stderr,便于排查多进程/tsx watch 等导致的 db 问题 */\r\nfunction logCorruptDiagnostic(operation: string, err: unknown): void {\r\n const code = (err as { code?: string })?.code;\r\n const msg = err instanceof Error ? err.message : String(err);\r\n const lines = [\r\n \"[rssany db] 数据库可能损坏或并发冲突\",\r\n ` 操作: ${operation}`,\r\n ` 错误: ${code ?? \"unknown\"} - ${msg}`,\r\n \" 常见原因:\",\r\n \" 1. 多进程同时打开同一库(例如 tsx --watch 与另一实例同时写)\",\r\n \" 2. 异常退出后 WAL 未正常 checkpoint\",\r\n \" 3. 磁盘/杀毒/同步盘导致文件不完整\",\r\n \" 建议:\",\r\n \" - 避免多实例同时写库;开发时慎用 --watch 与后台任务并行\",\r\n \" - 可尝试 RSSANY_DB_JOURNAL=delete 使用 DELETE 模式降低多文件依赖\",\r\n \" - 备份后删除 .rssany/data/rssany.db 及同目录 -wal、-shm、rssany.db.lock 再启动\",\r\n ];\r\n process.stderr.write(lines.join(\"\\n\") + \"\\n\");\r\n}\r\n\r\n/** 独占创建锁文件(wx);若已存在则读 PID,旧进程已死则删除后重试 */\r\nfunction acquireDbLock(dbDir: string): void {\r\n const lockPath = join(dbDir, \"rssany.db.lock\");\r\n const pid = process.pid;\r\n const tryCreate = (): void => {\r\n try {\r\n const fd = openSync(lockPath, \"wx\");\r\n writeSync(fd, String(pid), 0, \"utf8\");\r\n closeSync(fd);\r\n return;\r\n } catch (e: unknown) {\r\n const code = (e as NodeJS.ErrnoException)?.code;\r\n if (code !== \"EEXIST\") throw e;\r\n }\r\n if (!existsSync(lockPath)) {\r\n tryCreate();\r\n return;\r\n }\r\n let oldPid: number | null = null;\r\n try {\r\n const buf = readFileSync(lockPath, \"utf8\");\r\n const n = parseInt(buf.trim(), 10);\r\n if (!Number.isNaN(n)) oldPid = n;\r\n } catch {\r\n /* 读锁文件失败则重试创建 */\r\n }\r\n if (oldPid !== null && oldPid !== pid) {\r\n const stillRunning = ((): boolean => {\r\n try {\r\n process.kill(oldPid!, 0);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n })();\r\n if (stillRunning) {\r\n throw new Error(\r\n `数据库已被其他进程占用(PID ${oldPid})。请勿多开实例;若确认无其他进程,可删除 ${lockPath} 后重试(常见于 tsx --watch 未退出)`,\r\n );\r\n }\r\n }\r\n try {\r\n unlinkSync(lockPath);\r\n } catch {\r\n /* ignore */\r\n }\r\n tryCreate();\r\n };\r\n tryCreate();\r\n}\r\n\r\n/** 进程退出时尝试删除锁文件(尽力而为) */\r\nfunction releaseDbLock(): void {\r\n if (!existsSync(MAIN_DB_LOCK_PATH)) return;\r\n try {\r\n unlinkSync(MAIN_DB_LOCK_PATH);\r\n } catch {\r\n /* ignore */\r\n }\r\n}\r\n\r\nexport function withWriteLock<T>(fn: () => Promise<T>): Promise<T> {\r\n const prev = _writeLock;\r\n let resolveOut!: (v: T) => void;\r\n let rejectOut!: (e: unknown) => void;\r\n const out = new Promise<T>((res, rej) => {\r\n resolveOut = res;\r\n rejectOut = rej;\r\n });\r\n _writeLock = prev\r\n .then(() => fn())\r\n .then(\r\n (v) => {\r\n resolveOut(v);\r\n },\r\n (e: unknown) => {\r\n if (isCorruptError(e)) {\r\n logCorruptDiagnostic(\"withWriteLock 内 updateItemContent/upsertItems 等\", e);\r\n }\r\n rejectOut(e);\r\n throw e;\r\n },\r\n );\r\n return out;\r\n}\r\n\r\n/** 仅英文「日期当标题」的粗判(如 Jan 15, 2024),用于去重修复时跳过 */\r\nconst DATE_ONLY_TITLE_RE =\r\n /^(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\\b[\\s\\d,./-]*(?:st|nd|rd|th)?[\\s\\d,./-]*$/i;\r\n\r\nfunction normalizeText(text: string | null | undefined): string {\r\n return (text ?? \"\").replace(/\\s+/g, \" \").trim();\r\n}\r\n\r\nfunction isDateOnlyTitle(title: string | null | undefined): boolean {\r\n const normalized = normalizeText(title);\r\n if (!normalized) return false;\r\n return DATE_ONLY_TITLE_RE.test(normalized);\r\n}\r\n\r\nfunction toMs(input: string | null | undefined): number | null {\r\n if (!input) return null;\r\n const ms = Date.parse(input);\r\n return Number.isNaN(ms) ? null : ms;\r\n}\r\n\r\n/** 将 DB 中 author 列(JSON 字符串或单字符串)解析为 string[] */\r\nexport function parseAuthorFromDb(raw: string | null | undefined): string[] | undefined {\r\n if (!raw?.trim()) return undefined;\r\n try {\r\n const p = JSON.parse(raw) as unknown;\r\n if (Array.isArray(p)) return p.filter((s) => typeof s === \"string\").map((s) => String(s).trim()).filter(Boolean);\r\n return [String(p).trim()];\r\n } catch {\r\n return [raw.trim()];\r\n }\r\n}\r\n\r\n/** 将原始行转为 DbItem,并解析 author / tags / translations 等 JSON 字段 */\r\nfunction toDbItem(row: Record<string, unknown>): DbItem {\r\n const author = parseAuthorFromDb(row.author as string) ?? null;\r\n const parseJsonArr = (v: unknown): string[] | null => {\r\n try {\r\n return v ? (JSON.parse(v as string) as string[]) : null;\r\n } catch {\r\n return null;\r\n }\r\n };\r\n const tags = parseJsonArr(row.tags);\r\n let translations: Record<string, { title?: string; summary?: string; content?: string }> | null = null;\r\n try {\r\n if (row.translations && typeof row.translations === \"string\") {\r\n const p = JSON.parse(row.translations) as unknown;\r\n if (p && typeof p === \"object\") translations = p as Record<string, { title?: string; summary?: string; content?: string }>;\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n return { ...row, author, tags, translations } as DbItem;\r\n}\r\n\r\nfunction mapRowsToDbItems(rows: Record<string, unknown>[]): DbItem[] {\r\n return rows.map(toDbItem);\r\n}\r\n\r\n/** 判断是否 SQLite 库损坏或 FTS 虚拟表损坏 */\r\nfunction isCorruptError(err: unknown): boolean {\r\n const code = (err as { code?: string })?.code;\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return (\r\n code === \"SQLITE_CORRUPT\" ||\r\n code === \"SQLITE_CORRUPT_VTAB\" ||\r\n msg.includes(\"database disk image is malformed\")\r\n );\r\n}\r\n\r\n/** 获取主库连接,路径:.rssany/data/rssany.db */\r\nexport async function getDb(): Promise<Database.Database> {\r\n if (_db) return _db;\r\n if (_dbInit) return _dbInit;\r\n const dbPath = join(DATA_DIR, \"rssany.db\");\r\n _dbInit = (async (): Promise<Database.Database> => {\r\n await mkdir(DATA_DIR, { recursive: true });\r\n acquireDbLock(DATA_DIR);\r\n let db: Database.Database | null = null;\r\n try {\r\n db = new Database(dbPath);\r\n db.pragma(`journal_mode = ${MAIN_DB_JOURNAL}`);\r\n db.pragma(\"synchronous = NORMAL\");\r\n initSchema(db);\r\n _db = db;\r\n return db;\r\n } catch (err: unknown) {\r\n _dbInit = null;\r\n releaseDbLock();\r\n if (db) {\r\n try {\r\n db.close();\r\n } catch {\r\n /* ignore */\r\n }\r\n db = null;\r\n }\r\n if (isCorruptError(err)) {\r\n logCorruptDiagnostic(\"打开/初始化主库 (getDb)\", err);\r\n }\r\n throw err;\r\n }\r\n })();\r\n return _dbInit;\r\n}\r\n\r\n/** 执行 PRAGMA integrity_check;正常为单字 ok;否则为错误描述。亦可能因库损坏在 prepare 阶段抛错,由调用方 catch */\r\nexport async function runIntegrityCheck(): Promise<string> {\r\n const db = await getDb();\r\n try {\r\n const row = db.prepare(\"PRAGMA integrity_check\").get() as { integrity_check: string } | undefined;\r\n return row?.integrity_check ?? \"unknown\";\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return `integrity_check 执行失败: ${msg}`;\r\n }\r\n}\r\n\r\n/** 运行日志库路径:.rssany/data/logs.db(与主库分离,避免主库 WAL 与日志写放大相互影响) */\r\nconst LOGS_DB_PATH = join(DATA_DIR, \"logs.db\");\r\n\r\nlet _logsDb: Database.Database | null = null;\r\nlet _logsDbInit: Promise<Database.Database> | null = null;\r\n\r\nfunction initLogsSchema(db: Database.Database): void {\r\n db.exec(`\r\n CREATE TABLE IF NOT EXISTS logs (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n level TEXT NOT NULL,\r\n category TEXT NOT NULL,\r\n message TEXT NOT NULL,\r\n payload TEXT,\r\n source_url TEXT,\r\n created_at TEXT NOT NULL\r\n );\r\n CREATE INDEX IF NOT EXISTS idx_logs_level_created ON logs(level, created_at);\r\n CREATE INDEX IF NOT EXISTS idx_logs_source_created ON logs(source_url, created_at);\r\n `);\r\n}\r\n\r\n/** 获取运行日志库(独立 logs.db,journal 使用 WAL) */\r\nexport async function getLogsDb(): Promise<Database.Database> {\r\n if (_logsDb) return _logsDb;\r\n if (_logsDbInit) return _logsDbInit;\r\n _logsDbInit = (async (): Promise<Database.Database> => {\r\n await mkdir(DATA_DIR, { recursive: true });\r\n const db = new Database(LOGS_DB_PATH);\r\n db.pragma(\"journal_mode = WAL\");\r\n db.pragma(\"synchronous = NORMAL\");\r\n initLogsSchema(db);\r\n _logsDb = db;\r\n return db;\r\n })();\r\n return _logsDbInit;\r\n}\r\n\r\n/**\r\n * 主表 items + FTS5 全文索引;视图 items_fts_src 抽出 zh-CN 译文字段参与检索。\r\n * 主库文件旁可能产生 -wal/-shm;日志写入独立 logs.db。\r\n */\r\nfunction initSchema(db: Database.Database): void {\r\n db.exec(`\r\n CREATE TABLE IF NOT EXISTS items (\r\n id TEXT PRIMARY KEY,\r\n url TEXT UNIQUE NOT NULL,\r\n source_url TEXT NOT NULL,\r\n title TEXT,\r\n author TEXT,\r\n summary TEXT,\r\n content TEXT,\r\n image_url TEXT,\r\n tags TEXT,\r\n translations TEXT,\r\n pub_date TEXT,\r\n fetched_at TEXT NOT NULL,\r\n pushed_at TEXT\r\n );\r\n CREATE INDEX IF NOT EXISTS idx_items_source ON items(source_url);\r\n CREATE INDEX IF NOT EXISTS idx_items_fetched ON items(fetched_at);\r\n CREATE INDEX IF NOT EXISTS idx_items_pushed ON items(pushed_at);\r\n `);\r\n\r\n db.exec(`\r\n CREATE VIEW IF NOT EXISTS items_fts_src AS\r\n SELECT rowid, title, summary, content,\r\n json_extract(translations, '$.\"zh-CN\".title') AS title_zh,\r\n json_extract(translations, '$.\"zh-CN\".summary') AS summary_zh,\r\n json_extract(translations, '$.\"zh-CN\".content') AS content_zh\r\n FROM items;\r\n CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING fts5(\r\n title, summary, content, title_zh, summary_zh, content_zh,\r\n content='items_fts_src',\r\n content_rowid='rowid'\r\n );\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_insert AFTER INSERT ON items\r\n BEGIN\r\n INSERT INTO items_fts(rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n NEW.rowid, NEW.title, NEW.summary, NEW.content,\r\n json_extract(NEW.translations, '$.\"zh-CN\".title'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".summary'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_update AFTER UPDATE ON items\r\n BEGIN\r\n INSERT INTO items_fts(items_fts, rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n 'delete', OLD.rowid, OLD.title, OLD.summary, OLD.content,\r\n json_extract(OLD.translations, '$.\"zh-CN\".title'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".summary'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".content')\r\n );\r\n INSERT INTO items_fts(rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n NEW.rowid, NEW.title, NEW.summary, NEW.content,\r\n json_extract(NEW.translations, '$.\"zh-CN\".title'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".summary'),\r\n json_extract(NEW.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n CREATE TRIGGER IF NOT EXISTS items_fts_after_delete AFTER DELETE ON items\r\n BEGIN\r\n INSERT INTO items_fts(items_fts, rowid, title, summary, content, title_zh, summary_zh, content_zh)\r\n VALUES (\r\n 'delete', OLD.rowid, OLD.title, OLD.summary, OLD.content,\r\n json_extract(OLD.translations, '$.\"zh-CN\".title'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".summary'),\r\n json_extract(OLD.translations, '$.\"zh-CN\".content')\r\n );\r\n END;\r\n `);\r\n\r\n // 旧库迁移:若无 image_url 列则追加\r\n try {\r\n const info = db.prepare(\"PRAGMA table_info(items)\").all() as { name: string }[];\r\n if (info && !info.some((c) => c.name === \"image_url\")) {\r\n db.exec(\"ALTER TABLE items ADD COLUMN image_url TEXT\");\r\n }\r\n } catch {\r\n /* ignore */\r\n }\r\n\r\n migrateItemsSourceUrlIfNeeded(db);\r\n}\r\n\r\n/** 将 items.source_url 一次性规范化为 canonicalHttpSourceRef(user_version 2:大小写 + 去尾 slash 等) */\r\nfunction migrateItemsSourceUrlIfNeeded(db: Database.Database): void {\r\n const v = db.pragma(\"user_version\", { simple: true }) as number;\r\n if (v >= 2) return;\r\n const rows = db.prepare(\"SELECT rowid, source_url FROM items\").all() as { rowid: number; source_url: string }[];\r\n const upd = db.prepare(\"UPDATE items SET source_url = @next WHERE rowid = @rowid\");\r\n const run = db.transaction(() => {\r\n for (const r of rows) {\r\n const next = canonicalHttpSourceRef(r.source_url);\r\n if (next !== r.source_url) {\r\n upd.run({ next, rowid: r.rowid });\r\n }\r\n }\r\n db.pragma(\"user_version = 2\");\r\n });\r\n run();\r\n}\r\n\r\n/**\r\n * 批量插入或忽略重复(按 id);新行计入 newIds。\r\n * source_url 取自首条 item.sourceRef 或 sourceUrlOverride,写入前经 canonicalHttpSourceRef 规范化。\r\n */\r\nexport async function upsertItems(items: FeedItem[], sourceUrlOverride?: string): Promise<{ newCount: number; newIds: Set<string> }> {\r\n if (items.length === 0) return { newCount: 0, newIds: new Set() };\r\n const raw = (sourceUrlOverride ?? items[0].sourceRef)?.trim();\r\n if (!raw) {\r\n throw new Error(\"upsertItems: 每条 item 须有 sourceRef,或传入 sourceUrlOverride\");\r\n }\r\n const sourceUrl = canonicalHttpSourceRef(raw);\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const stmt = db.prepare(`\r\n INSERT OR IGNORE INTO items (id, url, source_url, title, author, summary, image_url, tags, pub_date, fetched_at)\r\n VALUES (@id, @url, @sourceUrl, @title, @author, @summary, @imageUrl, @tags, @pubDate, @fetchedAt)\r\n `);\r\n const selectExistingStmt = db.prepare(`\r\n SELECT id, title, author, summary, image_url, pub_date, fetched_at\r\n FROM items\r\n WHERE id = @id\r\n `);\r\n const repairExistingStmt = db.prepare(`\r\n UPDATE items\r\n SET title = @title,\r\n author = @author,\r\n summary = @summary,\r\n image_url = @imageUrl,\r\n pub_date = @pubDate,\r\n fetched_at = @fetchedAt\r\n WHERE id = @id\r\n `);\r\n const now = new Date().toISOString();\r\n let newCount = 0;\r\n const newIds = new Set<string>();\r\n const run = db.transaction((rows: FeedItem[]) => {\r\n for (const item of rows) {\r\n const nextTitle = normalizeText(item.title) || null;\r\n const nextSummary = normalizeText(item.summary) || null;\r\n const nextAuthorArr = normalizeAuthor(item.author);\r\n const nextAuthor = nextAuthorArr?.length ? JSON.stringify(nextAuthorArr) : null;\r\n const nextPubDate = pubDateToIsoOrNull(item.pubDate);\r\n const nextTags = item.tags?.length ? JSON.stringify(item.tags) : null;\r\n const nextImageUrl = typeof item.imageUrl === \"string\" && item.imageUrl.trim() ? item.imageUrl.trim() : null;\r\n const info = stmt.run({\r\n id: item.guid,\r\n url: item.link,\r\n sourceUrl,\r\n title: nextTitle,\r\n author: nextAuthor,\r\n summary: nextSummary,\r\n imageUrl: nextImageUrl,\r\n tags: nextTags,\r\n pubDate: nextPubDate,\r\n fetchedAt: now,\r\n });\r\n newCount += info.changes;\r\n if (info.changes > 0) newIds.add(item.guid);\r\n\r\n if (info.changes > 0) continue;\r\n const existing = selectExistingStmt.get({ id: item.guid }) as\r\n | {\r\n title: string | null;\r\n author: string | null;\r\n summary: string | null;\r\n image_url: string | null;\r\n pub_date: string | null;\r\n fetched_at: string | null;\r\n }\r\n | undefined;\r\n if (!existing) continue;\r\n\r\n const shouldRepairTitle =\r\n !!nextTitle &&\r\n !isDateOnlyTitle(nextTitle) &&\r\n (isDateOnlyTitle(existing.title) || !normalizeText(existing.title));\r\n const shouldRepairSummary =\r\n !!nextSummary && normalizeText(existing.summary).length < nextSummary.length;\r\n const shouldRepairImageUrl = !!nextImageUrl && !existing.image_url?.trim();\r\n const existingAuthorArr = parseAuthorFromDb(existing.author);\r\n const shouldRepairAuthor = !!nextAuthorArr?.length && !existingAuthorArr?.length;\r\n\r\n const existingPubDateMs = toMs(existing.pub_date);\r\n const existingFetchedAtMs = toMs(existing.fetched_at);\r\n const nextPubDateMs = toMs(nextPubDate);\r\n const existingPubDateLooksFallback =\r\n existingPubDateMs != null &&\r\n existingFetchedAtMs != null &&\r\n Math.abs(existingPubDateMs - existingFetchedAtMs) <= 5 * 60 * 1000;\r\n const shouldRepairPubDate =\r\n nextPubDateMs != null &&\r\n (existingPubDateMs == null ||\r\n (existingPubDateLooksFallback && nextPubDateMs < existingPubDateMs - 24 * 60 * 60 * 1000));\r\n\r\n if (!(shouldRepairTitle || shouldRepairSummary || shouldRepairImageUrl || shouldRepairAuthor || shouldRepairPubDate)) {\r\n continue;\r\n }\r\n\r\n repairExistingStmt.run({\r\n id: item.guid,\r\n title: shouldRepairTitle ? nextTitle : existing.title,\r\n author: shouldRepairAuthor ? nextAuthor : (existing.author ?? null),\r\n summary: shouldRepairSummary ? nextSummary : existing.summary,\r\n imageUrl: shouldRepairImageUrl ? nextImageUrl : (existing.image_url ?? null),\r\n pubDate: shouldRepairPubDate ? nextPubDate : existing.pub_date,\r\n fetchedAt: now,\r\n });\r\n }\r\n });\r\n run(items);\r\n return { newCount, newIds };\r\n });\r\n}\r\n\r\n/** 查询已存在的 guid 集合(用于 pipeline 等判断「是否新行」) */\r\nexport async function getExistingIds(guids: string[]): Promise<Set<string>> {\r\n if (guids.length === 0) return new Set();\r\n const db = await getDb();\r\n const placeholders = guids.map(() => \"?\").join(\",\");\r\n const rows = db.prepare(`SELECT id FROM items WHERE id IN (${placeholders})`).all(...guids) as { id: string }[];\r\n return new Set(rows.map((r) => r.id));\r\n}\r\n\r\n/**\r\n * 按 guid 更新正文等字段;不覆盖已有非空 content(COALESCE),\r\n * image_url/author/pub_date 等同理;tags/translations 整列替换。\r\n */\r\nexport async function updateItemContent(item: FeedItem): Promise<void> {\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n db.prepare(`\r\n UPDATE items\r\n SET content = COALESCE(content, @content),\r\n image_url = COALESCE(@imageUrl, image_url),\r\n author = COALESCE(@author, author),\r\n pub_date = COALESCE(@pubDate, pub_date),\r\n tags = @tags,\r\n translations = COALESCE(@translations, translations)\r\n WHERE id = @id\r\n `).run({\r\n id: item.guid,\r\n content: item.content ?? null,\r\n imageUrl: typeof item.imageUrl === \"string\" && item.imageUrl.trim() ? item.imageUrl.trim() : null,\r\n author: (() => {\r\n const arr = normalizeAuthor(item.author);\r\n return arr?.length ? JSON.stringify(arr) : null;\r\n })(),\r\n pubDate: pubDateToIsoOrNull(item.pubDate),\r\n tags: item.tags?.length ? JSON.stringify(item.tags) : null,\r\n translations: item.translations && Object.keys(item.translations).length > 0 ? JSON.stringify(item.translations) : null,\r\n });\r\n });\r\n}\r\n\r\n/** 按信源 URL 列表分页拉取;可选 since/until(YYYY-MM-DD 或 ISO 字符串) */\r\nexport async function queryFeedItems(\r\n sourceUrls: string[],\r\n limit: number,\r\n offset: number,\r\n opts?: { since?: string; until?: string },\r\n): Promise<{ items: DbItem[]; hasMore: boolean }> {\r\n if (sourceUrls.length === 0) return { items: [], hasMore: false };\r\n const expanded = [...new Set(sourceUrls.map((u) => canonicalHttpSourceRef(u)).filter(Boolean))];\r\n if (expanded.length === 0) return { items: [], hasMore: false };\r\n const db = await getDb();\r\n const placeholders = expanded.map((_, i) => `@u${i}`).join(\", \");\r\n const conditions: string[] = [`source_url IN (${placeholders})`];\r\n const params: Record<string, unknown> = { lim: limit + 1, off: offset };\r\n expanded.forEach((url, i) => {\r\n params[`u${i}`] = url;\r\n });\r\n if (opts?.since) {\r\n conditions.push(\"COALESCE(pub_date, fetched_at) >= @since\");\r\n params.since = opts.since.length === 10 ? `${opts.since}T00:00:00.000Z` : opts.since;\r\n }\r\n if (opts?.until) {\r\n conditions.push(\"COALESCE(pub_date, fetched_at) < @until\");\r\n if (opts.until.length === 10) {\r\n const d = new Date(`${opts.until}T12:00:00Z`);\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n params.until = d.toISOString();\r\n } else {\r\n params.until = opts.until;\r\n }\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n ${where}\r\n ORDER BY COALESCE(pub_date, fetched_at) DESC\r\n LIMIT @lim OFFSET @off\r\n `)\r\n .all(params) as Record<string, unknown>[];\r\n const hasMore = rows.length > limit;\r\n const items = mapRowsToDbItems(hasMore ? rows.slice(0, limit) : rows);\r\n return { items, hasMore };\r\n}\r\n\r\n/** 按 id(guid)取单条,供 MCP/API 等 */\r\nexport async function getItemById(id: string): Promise<DbItem | null> {\r\n const db = await getDb();\r\n const row = db.prepare(\"SELECT * FROM items WHERE id = @id\").get({ id }) as Record<string, unknown> | undefined;\r\n return row ? toDbItem(row) : null;\r\n}\r\n\r\n/** 单信源最近条目;可选 since(增量同步 /api/feed 等) */\r\nexport async function queryItemsBySource(sourceUrl: string, limit = 50, since?: Date): Promise<DbItem[]> {\r\n const key = canonicalHttpSourceRef(sourceUrl);\r\n if (!key) return [];\r\n const db = await getDb();\r\n const sinceClause = since ? \"AND COALESCE(pub_date, fetched_at) >= @since\" : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE source_url = @sourceUrl ${sinceClause}\r\n ORDER BY COALESCE(pub_date, fetched_at) DESC\r\n LIMIT @limit\r\n `)\r\n .all({ sourceUrl: key, limit, since: since?.toISOString() ?? null }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 多条件查询:source_url / sourceUrls / author / 全文 q / tags / 时间范围 */\r\nexport async function queryItems(opts: {\r\n sourceUrl?: string;\r\n sourceUrls?: string[];\r\n author?: string;\r\n q?: string;\r\n tags?: string[];\r\n limit?: number;\r\n offset?: number;\r\n since?: Date;\r\n until?: Date;\r\n}): Promise<{ items: DbItem[]; total: number }> {\r\n const db = await getDb();\r\n const { sourceUrl, sourceUrls, author, q, tags: tagsFilter, limit = 20, offset = 0, since, until } = opts;\r\n const conditions: string[] = [];\r\n const params: Record<string, unknown> = { limit, offset };\r\n if (sourceUrl) {\r\n const key = canonicalHttpSourceRef(sourceUrl);\r\n if (!key) {\r\n return { items: [], total: 0 };\r\n }\r\n conditions.push(\"i.source_url = @sourceUrl\");\r\n params.sourceUrl = key;\r\n } else if (sourceUrls && sourceUrls.length > 0) {\r\n const expanded = [...new Set(sourceUrls.map((s) => canonicalHttpSourceRef(s)).filter(Boolean))];\r\n if (expanded.length === 0) {\r\n return { items: [], total: 0 };\r\n }\r\n const placeholders = expanded.map((_, i) => `@src${i}`).join(\", \");\r\n conditions.push(`i.source_url IN (${placeholders})`);\r\n expanded.forEach((s, i) => ((params as Record<string, unknown>)[`src${i}`] = s));\r\n }\r\n if (author && author.trim().length >= 2) {\r\n conditions.push(\"instr(i.author, @author) > 0\");\r\n params.author = author.trim();\r\n }\r\n if (q) {\r\n conditions.push(\"i.rowid IN (SELECT rowid FROM items_fts WHERE items_fts MATCH @q)\");\r\n params.q = q;\r\n }\r\n if (tagsFilter && tagsFilter.length > 0) {\r\n const trimmed = tagsFilter.filter((t) => typeof t === \"string\" && t.trim()).map((t) => (t as string).trim());\r\n if (trimmed.length > 0) {\r\n const tagConds = trimmed.map((_, i) => `LOWER(TRIM(json_each.value)) = LOWER(@tag${i})`).join(\" OR \");\r\n conditions.push(`i.tags IS NOT NULL AND EXISTS (SELECT 1 FROM json_each(i.tags) WHERE ${tagConds})`);\r\n trimmed.forEach((t, i) => {\r\n (params as Record<string, unknown>)[`tag${i}`] = t;\r\n });\r\n }\r\n }\r\n if (since) {\r\n conditions.push(\"COALESCE(i.pub_date, i.fetched_at) >= @since\");\r\n params.since = since.toISOString();\r\n }\r\n if (until) {\r\n conditions.push(\"COALESCE(i.pub_date, i.fetched_at) < @until\");\r\n params.until = until.toISOString();\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT i.id, i.url, i.source_url, i.title, i.author, i.summary, i.content, i.tags, i.translations, i.pub_date, i.fetched_at, i.pushed_at\r\n FROM items i ${where}\r\n ORDER BY COALESCE(i.pub_date, i.fetched_at) DESC\r\n LIMIT @limit OFFSET @offset\r\n `)\r\n .all(params) as Record<string, unknown>[];\r\n const { count } = db.prepare(`SELECT COUNT(*) as count FROM items i ${where}`).get(params) as { count: number };\r\n return { items: mapRowsToDbItems(rows), total: count };\r\n}\r\n\r\n/** 从所有条目的 tags JSON 中移除指定标签(大小写不敏感),返回更新行数 */\r\nexport async function removeTagFromAllItems(tag: string): Promise<number> {\r\n const trimmed = String(tag ?? \"\").trim();\r\n if (!trimmed) return 0;\r\n const targetLower = trimmed.toLowerCase();\r\n\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const rows = db.prepare(\"SELECT id, tags FROM items WHERE tags IS NOT NULL AND tags != ''\").all() as { id: string; tags: string }[];\r\n\r\n const updateStmt = db.prepare(\"UPDATE items SET tags = @tags WHERE id = @id\");\r\n let count = 0;\r\n const run = db.transaction(() => {\r\n for (const row of rows) {\r\n let itemTags: string[];\r\n try {\r\n itemTags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const filtered = itemTags.filter((t) => String(t).trim().toLowerCase() !== targetLower);\r\n if (filtered.length === itemTags.length) continue;\r\n const nextTags = filtered.length > 0 ? JSON.stringify(filtered) : null;\r\n updateStmt.run({ id: row.id, tags: nextTags });\r\n count += 1;\r\n }\r\n });\r\n run();\r\n return count;\r\n });\r\n}\r\n\r\n/** 标记已投递(如 OpenWebUI 等出站),写入 pushed_at */\r\nexport async function markPushed(ids: string[]): Promise<void> {\r\n if (ids.length === 0) return;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const now = new Date().toISOString();\r\n const stmt = db.prepare(\"UPDATE items SET pushed_at = @now WHERE id = @id\");\r\n const run = db.transaction((list: string[]) => {\r\n for (const id of list) stmt.run({ now, id });\r\n });\r\n run(ids);\r\n });\r\n}\r\n\r\n/** 按 id 删除条目;同步删除 FTS 行(content 模式依赖 items_fts_src,zh-CN 字段在触发器中已展开) */\r\nexport async function deleteItem(id: string): Promise<boolean> {\r\n if (!id?.trim()) return false;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const run = db.transaction(() => {\r\n const row = db.prepare(\"SELECT rowid FROM items WHERE id = @id\").get({ id: id.trim() }) as { rowid: number } | undefined;\r\n if (!row) return 0;\r\n db.prepare(\"DELETE FROM items_fts WHERE rowid = @rowid\").run({ rowid: row.rowid });\r\n const info = db.prepare(\"DELETE FROM items WHERE id = @id\").run({ id: id.trim() });\r\n return info.changes;\r\n });\r\n return run() > 0;\r\n });\r\n}\r\n\r\n/** 按 source_url 删除该信源下全部条目(ref 经 canonicalHttpSourceRef 与入库键一致) */\r\nexport async function deleteItemsBySourceUrl(sourceUrl: string): Promise<number> {\r\n if (!sourceUrl?.trim()) return 0;\r\n const key = canonicalHttpSourceRef(sourceUrl.trim());\r\n if (!key) return 0;\r\n return withWriteLock(async () => {\r\n const db = await getDb();\r\n const info = db.prepare(\"DELETE FROM items WHERE source_url = @sourceUrl\").run({ sourceUrl: key });\r\n return info.changes;\r\n });\r\n}\r\n\r\n/** 待投递:未写 pushed_at 且已有 content */\r\nexport async function getPendingPushItems(limit = 100): Promise<DbItem[]> {\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE pushed_at IS NULL AND content IS NOT NULL\r\n ORDER BY fetched_at ASC\r\n LIMIT @limit\r\n `)\r\n .all({ limit }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 按本地日(YYYY-MM-DD)取当日抓取过的条目,按 fetched_at,最多 300 条 */\r\nexport async function getItemsForDate(date: string): Promise<DbItem[]> {\r\n const db = await getDb();\r\n const start = new Date(`${date}T00:00:00`).toISOString();\r\n const end = new Date(`${date}T23:59:59.999`).toISOString();\r\n const rows = db\r\n .prepare(`\r\n SELECT * FROM items\r\n WHERE fetched_at >= @start AND fetched_at <= @end\r\n ORDER BY fetched_at DESC\r\n LIMIT 300\r\n `)\r\n .all({ start, end }) as Record<string, unknown>[];\r\n return mapRowsToDbItems(rows);\r\n}\r\n\r\n/** 各信源条目数、近 7 天内有过拉取的条目数、最新一条时间,用于管理页统计(按 canonical 合并,与订阅 ref 对齐) */\r\nexport async function getSourceStats(): Promise<\r\n { source_url: string; count: number; count_7d: number; latest_at: string | null }[]\r\n> {\r\n const { mergeSourceStatsRows } = await import(\"../utils/httpSourceRef.js\");\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\r\n `SELECT source_url,\r\n COUNT(*) as count,\r\n SUM(CASE WHEN julianday(fetched_at) >= julianday('now', '-7 days') THEN 1 ELSE 0 END) as count_7d,\r\n MAX(COALESCE(pub_date, fetched_at)) as latest_at\r\n FROM items GROUP BY source_url ORDER BY count DESC`,\r\n )\r\n .all() as { source_url: string; count: number; count_7d: number; latest_at: string | null }[];\r\n return mergeSourceStatsRows(rows);\r\n}\r\n\r\n/** 写入一条运行日志到 logs.db(payload 为 JSON 字符串) */\r\nexport async function insertLog(entry: LogEntry): Promise<void> {\r\n const db = await getLogsDb();\r\n db.prepare(`\r\n INSERT INTO logs (level, category, message, payload, source_url, created_at)\r\n VALUES (@level, @category, @message, @payload, NULL, @created_at)\r\n `).run({\r\n level: entry.level,\r\n category: entry.category,\r\n message: entry.message,\r\n payload: entry.payload != null ? JSON.stringify(entry.payload) : null,\r\n created_at: entry.created_at,\r\n });\r\n}\r\n\r\n/** 分页查询运行日志(logs.db) */\r\nexport async function queryLogs(opts: {\r\n level?: LogEntry[\"level\"];\r\n category?: LogEntry[\"category\"];\r\n limit?: number;\r\n offset?: number;\r\n since?: Date;\r\n}): Promise<{ items: DbLog[]; total: number }> {\r\n const db = await getLogsDb();\r\n const { level, category, limit = 50, offset = 0, since } = opts;\r\n const conditions: string[] = [];\r\n const params: Record<string, unknown> = { limit, offset };\r\n if (level) {\r\n conditions.push(\"level = @level\");\r\n params.level = level;\r\n }\r\n if (category) {\r\n conditions.push(\"INSTR(LOWER(category), LOWER(@categoryPattern)) > 0\");\r\n params.categoryPattern = category;\r\n }\r\n if (since) {\r\n conditions.push(\"created_at >= @since\");\r\n params.since = since.toISOString();\r\n }\r\n const where = conditions.length ? `WHERE ${conditions.join(\" AND \")}` : \"\";\r\n const rows = db\r\n .prepare(`\r\n SELECT id, level, category, message, payload, created_at\r\n FROM logs ${where}\r\n ORDER BY created_at DESC\r\n LIMIT @limit OFFSET @offset\r\n `)\r\n .all(params) as DbLog[];\r\n const { count } = db.prepare(`SELECT COUNT(*) as count FROM logs ${where}`).get(params) as { count: number };\r\n return { items: rows, total: count };\r\n}\r\n\r\n/** 清空 logs 表(运行日志库) */\r\nexport async function clearAllLogs(): Promise<number> {\r\n const db = await getLogsDb();\r\n const r = db.prepare(\"DELETE FROM logs\").run();\r\n return r.changes;\r\n}\r\n\r\n/** 读取系统标签列表:.rssany/tags.json,供 pipeline tagger 等 */\r\nexport async function getSystemTags(): Promise<string[]> {\r\n try {\r\n const raw = await readFile(TAGS_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as { tags?: unknown[] };\r\n if (!Array.isArray(parsed?.tags)) return [];\r\n return parsed.tags\r\n .filter((t): t is string => typeof t === \"string\" && t.trim().length > 0)\r\n .map((t) => t.trim());\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n/** 保存系统标签到 .rssany/tags.json */\r\nexport async function saveSystemTagsToFile(tags: string[]): Promise<void> {\r\n const list = tags\r\n .filter((t) => typeof t === \"string\" && t.trim())\r\n .map((t) => t.trim());\r\n await writeFile(TAGS_CONFIG_PATH, JSON.stringify({ tags: list }, null, 2), \"utf-8\");\r\n}\r\n\r\n/** 系统标签使用统计:结合 tags.json 与库内条目的 tags 字段 */\r\nexport async function getSystemTagStats(): Promise<TagStat[]> {\r\n const systemTags = await getSystemTags();\r\n if (systemTags.length === 0) return [];\r\n\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\"SELECT tags, pub_date, fetched_at FROM items WHERE tags IS NOT NULL AND tags != ''\")\r\n .all() as { tags: string; pub_date: string | null; fetched_at: string }[];\r\n\r\n const now = Date.now();\r\n const tagMap = new Map<string, { count: number; hotness: number }>();\r\n for (const name of systemTags) {\r\n tagMap.set(name.toLowerCase(), { count: 0, hotness: 0 });\r\n }\r\n\r\n for (const row of rows) {\r\n let itemTags: string[];\r\n try {\r\n itemTags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const pubMs = row.pub_date ? Date.parse(row.pub_date) : null;\r\n const fetchedMs = Date.parse(row.fetched_at);\r\n const factor = recencyFactor(pubMs, fetchedMs, now);\r\n\r\n for (const t of itemTags) {\r\n const key = String(t).trim().toLowerCase();\r\n const entry = tagMap.get(key);\r\n if (entry) {\r\n entry.count += 1;\r\n entry.hotness += factor;\r\n }\r\n }\r\n }\r\n\r\n return systemTags.map((name) => {\r\n const entry = tagMap.get(name.toLowerCase()) ?? { count: 0, hotness: 0 };\r\n return {\r\n name,\r\n count: entry.count,\r\n hotness: Math.round(entry.hotness * 100) / 100,\r\n };\r\n });\r\n}\r\n\r\n/** 标签统计:条数 + 热度(时间衰减) */\r\nexport interface TagStat {\r\n name: string;\r\n count: number;\r\n hotness: number;\r\n period?: number;\r\n}\r\n\r\n/** 时间衰减因子:1 / (1 + days_ago/7),越新权重越高 */\r\nfunction recencyFactor(pubDateMs: number | null, fetchedAtMs: number, nowMs: number): number {\r\n const ref = pubDateMs ?? fetchedAtMs;\r\n const daysAgo = (nowMs - ref) / (24 * 60 * 60 * 1000);\r\n return 1 / (1 + Math.max(0, daysAgo) / 7);\r\n}\r\n\r\n/** 非系统标签中热度较高者,返回最多 5 个且 hotness > 20 */\r\nexport async function getSuggestedTags(): Promise<TagStat[]> {\r\n const systemTags = await getSystemTags();\r\n const systemLower = new Set(systemTags.map((t) => t.toLowerCase().trim()));\r\n\r\n const db = await getDb();\r\n const rows = db\r\n .prepare(\"SELECT tags, pub_date, fetched_at FROM items WHERE tags IS NOT NULL AND tags != ''\")\r\n .all() as { tags: string; pub_date: string | null; fetched_at: string }[];\r\n\r\n const tagMap = new Map<string, { name: string; count: number; hotness: number }>();\r\n const now = Date.now();\r\n\r\n for (const row of rows) {\r\n let tags: string[];\r\n try {\r\n tags = JSON.parse(row.tags) as string[];\r\n } catch {\r\n continue;\r\n }\r\n const pubMs = row.pub_date ? Date.parse(row.pub_date) : null;\r\n const fetchedMs = Date.parse(row.fetched_at);\r\n const factor = recencyFactor(pubMs, fetchedMs, now);\r\n\r\n for (const t of tags) {\r\n const trimmed = String(t).trim();\r\n if (!trimmed) continue;\r\n const key = trimmed.toLowerCase();\r\n if (systemLower.has(key)) continue;\r\n\r\n const existing = tagMap.get(key);\r\n if (existing) {\r\n existing.count += 1;\r\n existing.hotness += factor;\r\n } else {\r\n tagMap.set(key, { name: trimmed, count: 1, hotness: factor });\r\n }\r\n }\r\n }\r\n\r\n return Array.from(tagMap.values())\r\n .filter((s) => s.hotness > 20)\r\n .sort((a, b) => b.hotness - a.hotness)\r\n .slice(0, 5)\r\n .map((s) => ({ name: s.name, count: s.count, hotness: Math.round(s.hotness * 100) / 100 }));\r\n}\r\n\r\n/** 库中行结构(snake_case);与 FeedItem 对应,author/tags 等为解析后类型 */\r\nexport interface DbItem {\r\n id: string;\r\n url: string;\r\n source_url: string;\r\n title: string | null;\r\n author: string[] | null;\r\n summary: string | null;\r\n content: string | null;\r\n image_url: string | null;\r\n tags: string[] | null;\r\n translations: Record<string, { title?: string; summary?: string; content?: string }> | null;\r\n pub_date: string | null;\r\n fetched_at: string;\r\n pushed_at: string | null;\r\n}\r\n\r\n/** 运行日志表行 */\r\nexport interface DbLog {\r\n id: number;\r\n level: string;\r\n category: string;\r\n message: string;\r\n payload: string | null;\r\n created_at: string;\r\n}\r\n","// 日志配置:从环境变量读取,不依赖 config.json 以尽早可用\n\nimport type { LogLevel } from \"./types.js\";\n\nconst LEVEL_ORDER: LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\"];\n\nfunction parseLevel(s: string | undefined, fallback: LogLevel): LogLevel {\n if (!s) return fallback;\n const v = s.toLowerCase() as LogLevel;\n if (LEVEL_ORDER.includes(v)) return v;\n return fallback;\n}\n\n/** 当前控制台最低输出级别(默认 info,生产可设为 warn) */\nexport function getConsoleLevel(): LogLevel {\n return parseLevel(process.env.LOG_LEVEL, \"info\");\n}\n\n/** 是否将 error/warn 写入数据库(默认 true) */\nexport function getLogToDb(): boolean {\n const v = process.env.LOG_TO_DB;\n if (v === \"0\" || v === \"false\") return false;\n if (v === \"1\" || v === \"true\") return true;\n return true;\n}\n\n/** 落库的最低级别(默认 warn) */\nexport function getDbLevel(): LogLevel {\n return parseLevel(process.env.LOG_DB_LEVEL, \"warn\");\n}\n\nexport function levelOrder(l: LogLevel): number {\n return LEVEL_ORDER.indexOf(l);\n}\n\n/** 是否应输出到控制台 */\nexport function shouldLogToConsole(consoleLevel: LogLevel, entryLevel: LogLevel): boolean {\n return levelOrder(entryLevel) >= levelOrder(consoleLevel);\n}\n\n/** 是否应写入 DB */\nexport function shouldLogToDb(logToDb: boolean, dbLevel: LogLevel, entryLevel: LogLevel): boolean {\n return logToDb && levelOrder(entryLevel) >= levelOrder(dbLevel);\n}\n","// 统一日志:仅落库,不打印控制台;全体级别落库,由日志页/API 查看\n\nimport { insertLog } from \"../../db/index.js\";\nimport { getLogToDb } from \"./config.js\";\nimport type { LogCategory, LogEntry, LogLevel } from \"./types.js\";\n\nfunction now(): string {\n return new Date().toISOString();\n}\n\nfunction writeDb(entry: LogEntry): void {\n insertLog(entry).catch((err) => {\n // 落库失败只打一次 stderr,避免循环\n process.stderr.write(`[logger] 写入日志表失败: ${err instanceof Error ? err.message : String(err)}\\n`);\n });\n}\n\nfunction emit(level: LogLevel, category: LogCategory, message: string, meta?: Record<string, unknown>): void {\n const payload = meta && Object.keys(meta).length > 0 ? { ...meta } : undefined;\n const entry: LogEntry = {\n level,\n category,\n message,\n payload: payload && Object.keys(payload).length > 0 ? payload : undefined,\n created_at: now(),\n };\n\n if (getLogToDb()) {\n writeDb(entry);\n }\n}\n\n/** 统一 logger:仅落库,不输出控制台;全体级别落库 */\nexport const logger = {\n error(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"error\", category, message, meta);\n },\n warn(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"warn\", category, message, meta);\n },\n info(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"info\", category, message, meta);\n },\n debug(category: LogCategory, message: string, meta?: Record<string, unknown>) {\n emit(\"debug\", category, message, meta);\n },\n};\n","// 使用无头浏览器(Puppeteer)拉取页面,缓存逻辑在 cacher 中\r\n\r\nimport { exec } from \"node:child_process\";\r\nimport { platform } from \"node:os\";\r\nimport { join, resolve } from \"node:path\";\r\nimport { promisify } from \"node:util\";\r\nimport puppeteerCore, { type Browser, type Page } from \"puppeteer-core\";\r\nimport { applyPurify } from \"./purify.js\";\r\nimport { findChromeExecutable } from \"./cdp.js\";\r\nimport type { AuthFlow } from \"../../../auth/index.js\";\r\nimport type { RequestConfig, StructuredHtmlResult } from \"./types.js\";\r\nimport { logger } from \"../../../../core/logger/index.js\";\r\n\r\nconst execAsync = promisify(exec);\r\n\r\n/** 与 launchArgs / setViewport 一致;无头拉高便于长页与懒加载内容 */\r\nconst VIEWPORT_WIDTH = 1366;\r\nconst VIEWPORT_HEIGHT_HEADLESS = 5000;\r\nconst VIEWPORT_HEIGHT_HEADFUL = 1200;\r\n\r\n/** 解析代理:显式传入的 proxy,否则 HTTP_PROXY / HTTPS_PROXY */\r\nexport function resolveProxy(config?: { proxy?: string }): string | undefined {\r\n return config?.proxy ?? process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY;\r\n}\r\n\r\n/** 从代理字符串解析出 serverUrl 和可选账号密码;支持 http://user:pass@host:port */\r\nfunction parseProxy(proxy: string): { serverUrl: string; username?: string; password?: string } {\r\n const u = new URL(proxy);\r\n const serverUrl = u.port ? `${u.protocol}//${u.hostname}:${u.port}` : `${u.protocol}//${u.hostname}`;\r\n const username = u.username || undefined;\r\n const password = u.password || undefined;\r\n return { serverUrl, username, password };\r\n}\r\n\r\n/** 在 Page 上设置代理认证(与 preCheckAuth / fetchHtml 一致;需与 launchBrowser 的 proxy 同时使用) */\r\nexport async function applyProxyAuthToPage(page: Page, opts?: { proxy?: string }): Promise<void> {\r\n const proxy = resolveProxy(opts);\r\n if (!proxy) return;\r\n const { username, password } = parseProxy(proxy);\r\n if (username !== undefined || password !== undefined) {\r\n await page.authenticate({ username: username ?? \"\", password: password ?? \"\" });\r\n }\r\n}\r\n\r\n\r\n/** 构建 Puppeteer launch args */\r\nfunction launchArgs(config?: { proxy?: string; headless?: boolean }): string[] {\r\n const base = [\r\n \"--disable-blink-features=AutomationControlled\",\r\n \"--no-sandbox\",\r\n \"--disable-setuid-sandbox\",\r\n \"--disable-dev-shm-usage\",\r\n \"--disable-web-security\",\r\n \"--disable-features=IsolateOrigins,site-per-process\",\r\n \"--disable-site-isolation-trials\",\r\n \"--disable-infobars\",\r\n ];\r\n const height = config?.headless !== false ? VIEWPORT_HEIGHT_HEADLESS : VIEWPORT_HEIGHT_HEADFUL;\r\n base.push(`--window-size=${VIEWPORT_WIDTH},${height}`);\r\n const proxy = resolveProxy(config);\r\n if (proxy) {\r\n const { serverUrl } = parseProxy(proxy);\r\n base.push(`--proxy-server=${serverUrl}`);\r\n }\r\n return base;\r\n}\r\n\r\n\r\n/** 获取 userDataDir:统一使用 main 目录,所有站点共享同一浏览器 profile */\r\nfunction getUserDataDir(cacheDir?: string): string | undefined {\r\n if (!cacheDir) return undefined;\r\n return join(cacheDir, \"browser_data\", \"main\");\r\n}\r\n\r\n\r\n/** 是否为「userDataDir 已被占用」的报错(上次进程未正常退出或并发启动) */\r\nfunction isAlreadyRunningError(e: unknown): boolean {\r\n const msg = e instanceof Error ? e.message : String(e);\r\n return /already running/i.test(msg) && /userDataDir|user-data-dir|user data dir/i.test(msg);\r\n}\r\n\r\n\r\n/**\r\n * 强制结束占用指定 userDataDir 的 Chrome/Chromium 进程(上次未正常退出时残留)。\r\n * 仅在 darwin/linux 下执行;启动前调用以释放 profile 锁。\r\n */\r\nasync function killStaleChromeProcesses(absUserDataDir: string): Promise<void> {\r\n const plat = platform();\r\n if (plat !== \"darwin\" && plat !== \"linux\") {\r\n return;\r\n }\r\n try {\r\n // 匹配命令行中包含该 userDataDir 的进程(Chrome 使用 --user-data-dir=/path)\r\n const psCmd = plat === \"darwin\"\r\n ? `ps -eww -o pid= -o args= 2>/dev/null`\r\n : `ps -eo pid,args --no-headers 2>/dev/null`;\r\n const { stdout } = await execAsync(psCmd, { maxBuffer: 4 * 1024 * 1024 });\r\n const pids = new Set<number>();\r\n const lineRegex = /^\\s*(\\d+)\\s+/;\r\n for (const line of stdout.split(\"\\n\")) {\r\n if (!line.includes(absUserDataDir)) continue;\r\n const m = line.match(lineRegex);\r\n if (m) pids.add(parseInt(m[1], 10));\r\n }\r\n if (pids.size === 0) return;\r\n logger.info(\"scraper\", \"发现占用 browser_data 的 Chrome 进程,正在结束\", { pids: [...pids], userDataDir: absUserDataDir });\r\n for (const pid of pids) {\r\n try {\r\n process.kill(pid, \"SIGTERM\");\r\n } catch {\r\n // 进程可能已退出\r\n }\r\n }\r\n await new Promise((r) => setTimeout(r, 800));\r\n for (const pid of pids) {\r\n try {\r\n process.kill(pid, \"SIGKILL\");\r\n } catch {\r\n // ignore\r\n }\r\n }\r\n await new Promise((r) => setTimeout(r, 300));\r\n } catch (err) {\r\n logger.warn(\"scraper\", \"结束残留 Chrome 进程时出错\", { err: err instanceof Error ? err.message : String(err) });\r\n }\r\n}\r\n\r\n\r\n// 注入脚本隐藏自动化特征\r\nasync function stealthPage(page: Page): Promise<void> {\r\n await page.evaluateOnNewDocument(() => {\r\n /* global navigator, window, document */\r\n Object.defineProperty(navigator, \"webdriver\", { get: () => false });\r\n Object.defineProperty(navigator, \"plugins\", { get: () => [1, 2, 3, 4, 5] });\r\n Object.defineProperty(navigator, \"languages\", { get: () => [\"zh-CN\", \"zh\", \"en\"] });\r\n const originalQuery = window.navigator.permissions.query;\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n window.navigator.permissions.query = (parameters: any) =>\r\n parameters.name === \"notifications\"\r\n ? Promise.resolve({ state: Notification.permission } as PermissionStatus)\r\n : originalQuery(parameters);\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n (window as any).chrome = { runtime: {} };\r\n Object.defineProperty(Notification, \"permission\", { get: () => \"default\" });\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const nav = navigator as any;\r\n if (nav.getBattery) {\r\n nav.getBattery = () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 });\r\n }\r\n });\r\n await page.setExtraHTTPHeaders({\r\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\",\r\n \"Accept-Encoding\": \"gzip, deflate, br\",\r\n \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\",\r\n \"Cache-Control\": \"max-age=0\",\r\n \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"',\r\n \"Sec-Ch-Ua-Mobile\": \"?0\",\r\n \"Sec-Ch-Ua-Platform\": '\"macOS\"',\r\n \"Sec-Fetch-Dest\": \"document\",\r\n \"Sec-Fetch-Mode\": \"navigate\",\r\n \"Sec-Fetch-Site\": \"none\",\r\n \"Sec-Fetch-User\": \"?1\",\r\n \"Upgrade-Insecure-Requests\": \"1\",\r\n });\r\n}\r\n\r\n\r\nfunction headersToRecord(headers: Record<string, unknown>): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(headers)) {\r\n out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n\r\n/** 对新 Page 做通用初始化:UA、Viewport、stealth 脚本 */\r\nasync function setupPage(page: Page, headless = true): Promise<void> {\r\n const realUserAgent =\r\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\r\n await page.setUserAgent(realUserAgent);\r\n await page.setViewport({\r\n width: VIEWPORT_WIDTH,\r\n height: headless ? VIEWPORT_HEIGHT_HEADLESS : VIEWPORT_HEIGHT_HEADFUL,\r\n });\r\n await stealthPage(page);\r\n}\r\n\r\n\r\n// ─── 浏览器:单发模式 ─────────────────────────────────────────────────────────\r\n// 每次任务独立启动 Chrome,用毕在调用方 `browser.close()`;不保留全局单例,不跨请求复用 Tab。\r\n\r\n/** 是否为「frame 已分离」类错误(页面发生客户端导航/重定向导致主 frame 失效) */\r\nfunction isFrameDetachedError(e: unknown): boolean {\r\n const msg = e instanceof Error ? e.message : String(e);\r\n return /detached|Navigating frame was detached|Session closed/i.test(msg);\r\n}\r\n\r\n\r\n/**\r\n * 启动新的 Chrome 实例(不缓存、不复用)。调用方须在 `finally` 中 `await browser.close()`。\r\n */\r\nexport async function launchBrowser(config: {\r\n headless?: boolean;\r\n cacheDir?: string;\r\n proxy?: string;\r\n chromeExecutablePath?: string;\r\n}): Promise<Browser> {\r\n const wantHeadless = config.headless !== false;\r\n const executablePath = config.chromeExecutablePath ?? process.env.CHROME_PATH ?? findChromeExecutable();\r\n if (!executablePath) {\r\n throw new Error(\"未找到 Chrome 可执行文件,请安装 Google Chrome 或设置 CHROME_PATH 环境变量\");\r\n }\r\n const userDataDir = getUserDataDir(config.cacheDir);\r\n const maxRetries = 2;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n if (attempt === 0 && userDataDir) {\r\n const absUserDataDir = resolve(userDataDir);\r\n await killStaleChromeProcesses(absUserDataDir);\r\n }\r\n if (attempt > 0) {\r\n const waitMs = attempt * 2000;\r\n logger.info(\"scraper\", \"userDataDir 曾被占用,等待后重试\", { waitMs, attempt });\r\n await new Promise((r) => setTimeout(r, waitMs));\r\n }\r\n return await puppeteerCore.launch({\r\n headless: wantHeadless,\r\n args: launchArgs({ proxy: config.proxy, headless: wantHeadless }),\r\n userDataDir,\r\n executablePath,\r\n ignoreDefaultArgs: [\"--enable-automation\"],\r\n });\r\n } catch (e) {\r\n lastErr = e;\r\n if (attempt < maxRetries && isAlreadyRunningError(e)) {\r\n continue;\r\n }\r\n if (isAlreadyRunningError(e)) {\r\n const dir = userDataDir ?? \"browser_data/main\";\r\n throw new Error(\r\n `Chrome 的 profile 目录已被占用(${dir})。通常是因为上次未正常退出或同时运行了多个本服务实例。请关闭占用该目录的 Chrome 进程后重试,或设置环境变量 CACHE_DIR 使用不同缓存目录。`\r\n );\r\n }\r\n throw e;\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\n/** @deprecated 使用 `launchBrowser`;行为与单发启动一致,不再复用全局实例 */\r\nexport const getOrCreateBrowser = launchBrowser;\r\n\r\n\r\n// ─── 对外 API ─────────────────────────────────────────────────────────────────\r\n\r\n/** 预检认证:单发浏览器(新开 Tab)检查是否已登录;opts 与 fetchHtml 一致(代理、有头/无头) */\r\nexport async function preCheckAuth(\r\n authFlow: AuthFlow,\r\n cacheDir: string,\r\n opts?: { proxy?: string; headless?: boolean }\r\n): Promise<boolean> {\r\n const { checkAuth, loginUrl, domain } = authFlow;\r\n if (domain == null || !cacheDir) return true;\r\n const isHeadless = opts?.headless !== false;\r\n const browser = await launchBrowser({\r\n headless: isHeadless,\r\n cacheDir,\r\n proxy: resolveProxy(opts),\r\n });\r\n try {\r\n const page = await browser.newPage();\r\n try {\r\n await setupPage(page, isHeadless);\r\n await applyProxyAuthToPage(page, opts);\r\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\r\n await new Promise((resolve) => setTimeout(resolve, 3000));\r\n return await checkAuth(page, page.url());\r\n } finally {\r\n await page.close().catch(() => {});\r\n }\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n\r\n\r\n// 执行认证流程:单发有头浏览器打开登录页,等待用户完成登录后关闭进程\r\nexport async function ensureAuth(\r\n authFlow: AuthFlow,\r\n cacheDir: string,\r\n opts?: { proxy?: string }\r\n): Promise<void> {\r\n const { checkAuth, loginUrl, loginTimeoutMs = 60 * 1000, pollIntervalMs = 2000 } = authFlow;\r\n const browser = await launchBrowser({ headless: false, cacheDir, proxy: resolveProxy(opts) });\r\n try {\r\n const page = await browser.newPage();\r\n try {\r\n await setupPage(page, false);\r\n await applyProxyAuthToPage(page, opts);\r\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\r\n await new Promise((resolve) => setTimeout(resolve, 3000));\r\n const authenticated = await checkAuth(page, page.url());\r\n if (authenticated) return;\r\n const startTime = Date.now();\r\n while (Date.now() - startTime < loginTimeoutMs) {\r\n await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));\r\n const authenticated = await checkAuth(page, page.url());\r\n if (authenticated) return;\r\n }\r\n throw new Error(`登录超时(${loginTimeoutMs}ms)`);\r\n } finally {\r\n await page.close().catch(() => {});\r\n }\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n\r\n\r\n// 单发浏览器:本次任务内最多开两个 Tab(frame 分离时重试一次),结束后关闭 Chrome\r\n// 若发生「Navigating frame was detached」等 frame 分离错误(常见于 SPA 客户端跳转),会换新 Tab 重试一次,并用 domcontentloaded 尽快取 HTML\r\nexport async function fetchHtml(url: string, config: RequestConfig = {}): Promise<StructuredHtmlResult> {\r\n const {\r\n timeoutMs,\r\n headers,\r\n cookies,\r\n cacheDir,\r\n checkAuth,\r\n authFlow,\r\n purify,\r\n headless,\r\n waitAfterLoadMs,\r\n waitForSelector,\r\n waitForSelectorTimeoutMs,\r\n useHttpResponseBody,\r\n } = config;\r\n const isHeadless = headless !== false;\r\n const browser = await launchBrowser({\r\n headless: isHeadless,\r\n cacheDir,\r\n proxy: resolveProxy(config),\r\n chromeExecutablePath: config.chromeExecutablePath,\r\n });\r\n const navigationTimeout = timeoutMs ?? 60000;\r\n const maxAttempts = 2;\r\n let lastError: unknown;\r\n\r\n try {\r\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\r\n const page = await browser.newPage();\r\n const isRetry = attempt === 1;\r\n // 重试时用 domcontentloaded 尽快取 HTML,减少 SPA 客户端跳转在取 content 前发生的概率\r\n const waitUntil = isRetry ? \"domcontentloaded\" : \"load\";\r\n const extraWaitMs = isRetry ? Math.min(500, Math.max(0, waitAfterLoadMs ?? 2000)) : Math.max(0, waitAfterLoadMs ?? 2000);\r\n try {\r\n if (config.browserContext) {\r\n await config.browserContext(page.browserContext());\r\n }\r\n await setupPage(page, isHeadless);\r\n const extraHeaders: Record<string, string> = { \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\", ...(headers ?? {}) };\r\n if (cookies != null && cookies !== \"\") {\r\n extraHeaders.cookie = cookies;\r\n }\r\n await page.setExtraHTTPHeaders(extraHeaders);\r\n const proxy = resolveProxy(config);\r\n if (proxy) {\r\n const { username, password } = parseProxy(proxy);\r\n if (username !== undefined || password !== undefined) {\r\n await page.authenticate({ username: username ?? \"\", password: password ?? \"\" });\r\n }\r\n }\r\n if (timeoutMs != null) {\r\n await page.setDefaultNavigationTimeout(timeoutMs);\r\n }\r\n const response = await page.goto(url, { waitUntil, timeout: navigationTimeout });\r\n if (extraWaitMs > 0) {\r\n await new Promise((resolve) => setTimeout(resolve, extraWaitMs));\r\n }\r\n if (waitForSelector != null && waitForSelector !== \"\" && !isRetry) {\r\n const selectorTimeout = waitForSelectorTimeoutMs ?? 20000;\r\n await page.waitForSelector(waitForSelector, { timeout: selectorTimeout });\r\n }\r\n if (checkAuth != null || authFlow != null) {\r\n const authCheck = checkAuth ?? authFlow?.checkAuth;\r\n if (authCheck != null) {\r\n const ok = await authCheck(page, url);\r\n if (!ok) {\r\n throw new Error(\"checkAuth failed: 未通过认证检查,请先调用 ensureAuth 进行预处理登录\");\r\n }\r\n }\r\n }\r\n let rawBody: string;\r\n if (useHttpResponseBody === true && response != null) {\r\n try {\r\n rawBody = await response.text();\r\n } catch {\r\n rawBody = await page.content();\r\n }\r\n } else {\r\n rawBody = await page.content();\r\n }\r\n const finalUrl = response?.url() ?? page.url() ?? String(url);\r\n const status = response?.status() ?? 0;\r\n const statusText = response?.statusText() ?? \"\";\r\n const rawHeaders = response?.headers() ?? {};\r\n const normalizedHeaders = headersToRecord(rawHeaders);\r\n const body = applyPurify(rawBody, purify);\r\n await page.close().catch(() => {});\r\n return { finalUrl, status, statusText, headers: normalizedHeaders, body };\r\n } catch (e) {\r\n lastError = e;\r\n await page.close().catch(() => {});\r\n if (isRetry || !isFrameDetachedError(e)) {\r\n throw e;\r\n }\r\n logger.warn(\"scraper\", \"fetchHtml 因 frame 分离重试\", { url, attempt: attempt + 1, err: e instanceof Error ? e.message : String(e) });\r\n await new Promise((r) => setTimeout(r, 800));\r\n }\r\n }\r\n throw lastError;\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n}\r\n","// RefreshInterval:类型定义 + interval ↔ cron ↔ 缓存 key 转换,供全项目共用\n// 调度与缓存 key 对齐:refresh 自动转 cron,cron 转 strategy 与 cacher.timeBucket 一致\n// - refreshIntervalToCron(interval) → 调度器使用的 cron\n// - cronToRefreshInterval(cron) → cacher.cacheKey 使用的 strategy\n// - cacher.timeBucket(strategy) → 缓存 key 的时间窗口前缀\n// 三者逻辑一致,保证调度触发时刻与缓存 key 时间窗口边界对齐\n\nimport type { CacheKeyStrategy } from \"../scraper/sources/web/fetcher/types.js\";\n\n\n/** 刷新间隔类型(有时间语义的缓存策略,排除 forever) */\nexport type RefreshInterval = Exclude<CacheKeyStrategy, \"forever\">;\n\n\n/** 合法的刷新间隔值列表(用于运行时校验) */\nexport const VALID_INTERVALS: RefreshInterval[] = [\"1min\", \"5min\", \"10min\", \"30min\", \"1h\", \"6h\", \"12h\", \"1day\", \"3day\", \"7day\"];\n\n\n/**\n * 从 cron 表达式推断缓存策略(RefreshInterval),供 cacheKey 使用\n * 与 refreshIntervalToCron 互为逆映射,保证调度时间与缓存 key 时间窗口一致\n * 格式:minute hour day month weekday(5 字段)或 second minute hour day month weekday(6 字段)\n */\nexport function cronToRefreshInterval(cronExpr: string): RefreshInterval {\n const parts = cronExpr.trim().split(/\\s+/);\n const is6Field = parts.length >= 6;\n const minute = parts[is6Field ? 1 : 0];\n const hour = parts[is6Field ? 2 : 1];\n const day = parts[is6Field ? 3 : 2];\n const weekday = parts[is6Field ? 5 : 4];\n\n // 分钟级:*/N 或 *\n const minMatch = minute.match(/^\\*\\/(\\d+)$/);\n if (minute === \"*\" || (minMatch && parseInt(minMatch[1], 10) <= 1)) return \"1min\";\n if (minMatch) {\n const n = parseInt(minMatch[1], 10);\n if (n <= 5) return \"5min\";\n if (n <= 10) return \"10min\";\n if (n <= 30) return \"30min\";\n }\n\n // 小时级:minute=0, hour=*/N 或 *\n if (minute === \"0\" || minute === \"00\") {\n const hourMatch = hour.match(/^\\*\\/(\\d+)$/);\n if (hour === \"*\" || (hourMatch && parseInt(hourMatch[1], 10) <= 1)) return \"1h\";\n if (hourMatch) {\n const n = parseInt(hourMatch[1], 10);\n if (n <= 2) return \"1h\";\n if (n <= 6) return \"6h\";\n if (n <= 12) return \"12h\";\n }\n // 每天固定时刻(如 0 9 * * *)\n if (day === \"*\" || day === \"?\") return \"1day\";\n // 每周固定时刻(如 0 0 * * 0)\n if (weekday !== \"*\" && weekday !== \"?\") return \"7day\";\n }\n\n return \"1h\";\n}\n\n\n/**\n * 将 RefreshInterval 转为 cron 表达式\n * 调度触发时刻与 cacher.timeBucket 的时间窗口边界一致,保证缓存 key 与执行频率对齐\n */\nexport function refreshIntervalToCron(interval: RefreshInterval): string {\n const map: Record<RefreshInterval, string> = {\n \"1min\": \"* * * * *\",\n \"5min\": \"*/5 * * * *\",\n \"10min\": \"*/10 * * * *\",\n \"30min\": \"*/30 * * * *\",\n \"1h\": \"0 * * * *\",\n \"6h\": \"0 */6 * * *\",\n \"12h\": \"0 0,12 * * *\",\n \"1day\": \"0 0 * * *\",\n \"3day\": \"0 0 */3 * *\",\n \"7day\": \"0 0 * * 0\",\n };\n return map[interval];\n}\n\n\n/** 将 RefreshInterval 转换为对应的毫秒数 */\nexport function refreshIntervalToMs(interval: RefreshInterval): number {\n const map: Record<RefreshInterval, number> = {\n \"1min\": 1 * 60 * 1000,\n \"5min\": 5 * 60 * 1000,\n \"10min\": 10 * 60 * 1000,\n \"30min\": 30 * 60 * 1000,\n \"1h\": 60 * 60 * 1000,\n \"6h\": 6 * 60 * 60 * 1000,\n \"12h\": 12 * 60 * 60 * 1000,\n \"1day\": 24 * 60 * 60 * 1000,\n \"3day\": 3 * 24 * 60 * 60 * 1000,\n \"7day\": 7 * 24 * 60 * 60 * 1000,\n };\n return map[interval];\n}\n","// 缓存管理:认证 profile 缓存\n\nimport { createHash } from \"node:crypto\";\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { CacheKeyStrategy } from \"../../scraper/sources/web/fetcher/types.js\";\nimport { cronToRefreshInterval } from \"../../utils/refreshInterval.js\";\n\n\nconst DOMAINS_SUBDIR = \"domains\";\n\n\n/** 认证配置档:可存 cookies、localStorage、sessionStorage 等,由 cacher 写入 cacheDir/domains/ */\nexport interface AuthProfile {\n cookies?: string;\n localStorage?: string;\n sessionStorage?: string;\n [key: string]: unknown;\n}\n\n\nfunction profileFileName(domain: string): string {\n return `${domain.replace(/[/\\\\]/g, \"_\")}.json`;\n}\n\n\nfunction urlHash(url: string): string {\n return createHash(\"sha256\").update(url).digest(\"hex\");\n}\n\n\n// 将时间映射到对应策略的时间窗口前缀(UTC)\n// 与 refreshIntervalToCron 的时间边界一致:如 1h 策略,窗口为整点,cron 为 \"0 * * * *\"\nfunction timeBucket(strategy: CacheKeyStrategy, now: Date): string {\n const y = now.getUTCFullYear();\n const mo = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n const d = String(now.getUTCDate()).padStart(2, \"0\");\n const h = now.getUTCHours();\n const hs = String(h).padStart(2, \"0\");\n const min = now.getUTCMinutes();\n if (strategy === \"1min\") return `${y}-${mo}-${d}T${hs}:${String(min).padStart(2, \"0\")}`;\n if (strategy === \"5min\") return `${y}-${mo}-${d}T${hs}:${String(Math.floor(min / 5) * 5).padStart(2, \"0\")}`;\n if (strategy === \"10min\") return `${y}-${mo}-${d}T${hs}:${String(Math.floor(min / 10) * 10).padStart(2, \"0\")}`;\n if (strategy === \"30min\") return `${y}-${mo}-${d}T${hs}:${min < 30 ? \"00\" : \"30\"}`;\n if (strategy === \"1h\") return `${y}-${mo}-${d}T${hs}`;\n if (strategy === \"6h\") return `${y}-${mo}-${d}T${String(Math.floor(h / 6) * 6).padStart(2, \"0\")}`;\n if (strategy === \"12h\") return `${y}-${mo}-${d}T${h < 12 ? \"00\" : \"12\"}`;\n if (strategy === \"1day\") return `${y}-${mo}-${d}`;\n if (strategy === \"3day\") {\n const epochDay = Math.floor(now.getTime() / 86400000);\n return `d${Math.floor(epochDay / 3) * 3}`;\n }\n if (strategy === \"7day\") {\n const adjusted = new Date(now);\n adjusted.setUTCHours(0, 0, 0, 0);\n adjusted.setUTCDate(adjusted.getUTCDate() + 3 - ((adjusted.getUTCDay() + 6) % 7));\n const week1 = new Date(Date.UTC(adjusted.getUTCFullYear(), 0, 4));\n const isoYear = adjusted.getUTCFullYear();\n const isoWeek = 1 + Math.round(((adjusted.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getUTCDay() + 6) % 7) / 7);\n return `${isoYear}-W${String(isoWeek).padStart(2, \"0\")}`;\n }\n return \"\";\n}\n\n\n// 根据 URL 与策略生成缓存 key:forever=仅 sha256(url);其余=时间窗口前缀-sha256(url)\nexport function cacheKey(url: string, strategy: CacheKeyStrategy = \"forever\", now: Date = new Date()): string {\n const hash = urlHash(url);\n if (strategy === \"forever\") return hash;\n return `${timeBucket(strategy, now)}-${hash}`;\n}\n\n\n/** 基于 cron 表达式生成缓存 key:策略由 cronToRefreshInterval 派生,与调度触发时刻对齐 */\nexport function cacheKeyFromCron(url: string, cron: string, now: Date = new Date()): string {\n return cacheKey(url, cronToRefreshInterval(cron), now);\n}\n\n\n// 从 cacheDir/domains/{domain}.json 读取认证配置(未来可扩展为 domains/{domain}/{profileName}.json)\nexport async function readProfile(cacheDir: string, domain: string): Promise<AuthProfile | null> {\n const dir = join(cacheDir, DOMAINS_SUBDIR);\n const filePath = join(dir, profileFileName(domain));\n try {\n const raw = await readFile(filePath, \"utf-8\");\n return JSON.parse(raw) as AuthProfile;\n } catch {\n return null;\n }\n}\n\n\n// 将认证配置写入 cacheDir/domains/{domain}.json\nexport async function writeProfile(cacheDir: string, domain: string, data: AuthProfile): Promise<void> {\n const dir = join(cacheDir, DOMAINS_SUBDIR);\n await mkdir(dir, { recursive: true });\n const filePath = join(dir, profileFileName(domain));\n await writeFile(filePath, JSON.stringify(data, null, 2), \"utf-8\");\n}\n","// 正文提取器:从详情页 HTML 提取单条正文(规则 / Readability),支持缓存\n\nimport { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { JSDOM } from \"jsdom\";\nimport { Readability } from \"@mozilla/readability\";\nimport { cacheKey as cacherCacheKey } from \"../../../../core/cacher/index.js\";\nimport { fetchHtml } from \"../fetcher/index.js\";\nimport type { RequestConfig } from \"../fetcher/types.js\";\nimport type { FeedItem } from \"../../../../types/feedItem.js\";\nimport { normalizeAuthor } from \"../../../../types/feedItem.js\";\nimport type { ExtractedResult, ExtractorConfig } from \"./types.js\";\nimport { logger } from \"../../../../core/logger/index.js\";\n\n\nconst EXTRACTED_SUBDIR = \"extracted\";\n\n\n/** 从缓存读取已提取结果 */\nasync function readCachedExtracted(cacheDir: string, key: string): Promise<ExtractedResult | null> {\n const filePath = join(cacheDir, EXTRACTED_SUBDIR, `${key}.json`);\n try {\n const raw = await readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw) as ExtractedResult & { cachedAt?: string };\n return {\n author: parsed.author,\n title: parsed.title,\n summary: parsed.summary,\n content: parsed.content ?? (parsed as { contentMarkdown?: string }).contentMarkdown ?? (parsed as { contentHtml?: string }).contentHtml,\n pubDate: parsed.pubDate,\n };\n } catch {\n return null;\n }\n}\n\n\n/** 将提取结果写入缓存 */\nasync function writeCachedExtracted(cacheDir: string, key: string, result: ExtractedResult): Promise<void> {\n const dir = join(cacheDir, EXTRACTED_SUBDIR);\n await mkdir(dir, { recursive: true });\n const filePath = join(dir, `${key}.json`);\n const cache = { ...result, cachedAt: new Date().toISOString() };\n await writeFile(filePath, JSON.stringify(cache, null, 2), \"utf-8\");\n}\n\n\n/** 解析 key:与 fetched 一致,使用 sha256(url) */\nfunction extractedCacheKey(url: string, config: ExtractorConfig): string {\n if (config.cacheKey != null && config.cacheKey !== \"\") return config.cacheKey;\n if (url) return cacherCacheKey(url, \"forever\");\n return \"\";\n}\n\n\n/** 使用 Readability 从详情页 HTML 提取正文 */\nfunction extractWithReadability(html: string, url: string): ExtractedResult {\n const dom = new JSDOM(html, { url });\n const reader = new Readability(dom.window.document);\n const article = reader.parse();\n if (!article) return {};\n const summary = article.excerpt && article.excerpt.length > 200 ? article.excerpt.slice(0, 200) + \"…\" : article.excerpt;\n return {\n author: article.byline || undefined,\n title: article.title || undefined,\n summary: summary || undefined,\n content: article.content || undefined,\n };\n}\n\n\n/** 从 HTML 提取正文(规则 / Readability),支持缓存 */\nexport async function extractHtml(html: string, config: ExtractorConfig = {}): Promise<ExtractedResult> {\n const { url = \"\", mode, customExtractor, cacheDir, useCache = true } = config;\n const key = extractedCacheKey(url, config);\n if (useCache !== false && cacheDir != null && cacheDir !== \"\" && key) {\n const cached = await readCachedExtracted(cacheDir, key);\n if (cached != null) return cached;\n }\n if (customExtractor != null) {\n const result = await Promise.resolve(customExtractor(html, url));\n if (cacheDir != null && cacheDir !== \"\" && key) {\n await writeCachedExtracted(cacheDir, key, result);\n }\n return result;\n }\n if (mode === \"readability\") {\n const result = extractWithReadability(html, url);\n if (cacheDir != null && cacheDir !== \"\" && key) {\n await writeCachedExtracted(cacheDir, key, result);\n }\n return result;\n }\n return {};\n}\n\n\n/** 根据 link 拉取 HTML 并提取正文(提取过程不缓存 fetch) */\nexport async function extractFromLink(\n link: string,\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<ExtractedResult> {\n const { cacheDir } = extractorConfig;\n const fetchOpts: RequestConfig = {\n cacheDir: fetchConfig.cacheDir ?? cacheDir,\n useCache: false,\n timeoutMs: fetchConfig.timeoutMs ?? 15_000,\n ...fetchConfig,\n };\n const res = await fetchHtml(link, fetchOpts);\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`fetch 失败: ${res.status} ${res.statusText} for ${link}`);\n }\n const finalUrl = res.finalUrl ?? link;\n return extractHtml(res.body, {\n ...extractorConfig,\n url: finalUrl,\n cacheKey: extractorConfig.cacheKey ?? (cacheDir ? cacherCacheKey(link, \"forever\") : undefined),\n });\n}\n\n\n/** 对单个 FeedItem 根据 link 拉取并提取正文,合并回 item(补充 author/title/summary/content/pubDate) */\nexport async function extractItem(\n item: FeedItem,\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<FeedItem> {\n const extracted = await extractFromLink(item.link, extractorConfig, fetchConfig);\n const pubDate =\n extracted.pubDate != null\n ? typeof extracted.pubDate === \"string\"\n ? new Date(extracted.pubDate)\n : extracted.pubDate\n : item.pubDate;\n return {\n ...item,\n author: normalizeAuthor(extracted.author ?? item.author),\n title: extracted.title ?? item.title,\n summary: extracted.summary ?? item.summary,\n content: extracted.content ?? item.content,\n pubDate,\n };\n}\n\n\n/** 对多个 FeedItem 依次提取正文并合并(可复用 fetcher 缓存) */\nexport async function extractItems(\n items: FeedItem[],\n extractorConfig: ExtractorConfig = {},\n fetchConfig: RequestConfig = {}\n): Promise<FeedItem[]> {\n const results: FeedItem[] = [];\n const onProgress = extractorConfig.onProgress;\n for (let i = 0; i < items.length; i++) {\n try {\n results.push(await extractItem(items[i], extractorConfig, fetchConfig));\n } catch (err) {\n logger.warn(\"scraper\", \"正文提取失败\", { item_url: items[i].link, err: err instanceof Error ? err.message : String(err) });\n results.push(items[i]);\n }\n onProgress?.(i + 1, items.length);\n }\n return results;\n}\n","// LLM 配置:config.json 的 llm 块优先,其次环境变量\r\n\r\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\r\nimport \"dotenv/config\";\r\nimport { CONFIG_PATH } from \"../config/paths.js\";\r\n\r\n/** LLM 配置(文件、环境变量或调用方传入) */\r\nexport interface LLMConfig {\r\n apiKey?: string;\r\n baseUrl?: string;\r\n model?: string;\r\n}\r\n\r\nconst DEFAULT_BASE_URL = \"https://api.openai.com/v1\";\r\nconst DEFAULT_MODEL = \"gpt-4o-mini\";\r\n\r\ninterface FileLlmCache {\r\n mtimeMs: number;\r\n llm: Partial<LLMConfig>;\r\n}\r\n\r\nlet fileCache: FileLlmCache | null = null;\r\n\r\n/** 配置或外部写入后调用,使下次 getLLMConfig 重新读盘 */\r\nexport function invalidateLLMConfigCache(): void {\r\n fileCache = null;\r\n}\r\n\r\nfunction readLlmFromFileSync(): Partial<LLMConfig> {\r\n if (!existsSync(CONFIG_PATH)) return {};\r\n try {\r\n const st = statSync(CONFIG_PATH);\r\n if (fileCache && fileCache.mtimeMs === st.mtimeMs) return fileCache.llm;\r\n const raw = readFileSync(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { llm?: unknown };\r\n const llmRaw = j?.llm;\r\n const llm: Partial<LLMConfig> = {};\r\n if (llmRaw && typeof llmRaw === \"object\") {\r\n const o = llmRaw as Record<string, unknown>;\r\n if (typeof o.apiKey === \"string\" && o.apiKey.length > 0) llm.apiKey = o.apiKey;\r\n if (typeof o.baseUrl === \"string\" && o.baseUrl.trim()) llm.baseUrl = o.baseUrl.trim();\r\n if (typeof o.model === \"string\" && o.model.trim()) llm.model = o.model.trim();\r\n }\r\n fileCache = { mtimeMs: st.mtimeMs, llm };\r\n return llm;\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/** 合并文件与环境变量:文件中的非空字段优先 */\r\nexport function getLLMConfig(): Required<Pick<LLMConfig, \"baseUrl\" | \"model\">> & Pick<LLMConfig, \"apiKey\"> {\r\n const file = readLlmFromFileSync();\r\n const apiKey = file.apiKey ?? process.env.OPENAI_API_KEY;\r\n const baseUrl = file.baseUrl ?? process.env.OPENAI_BASE_URL ?? DEFAULT_BASE_URL;\r\n const model = file.model ?? process.env.OPENAI_MODEL ?? DEFAULT_MODEL;\r\n return { apiKey, baseUrl, model };\r\n}\r\n","// LLM 统一调用:封装 OpenAI chat completion,供 parser/extractor / pipeline 复用\r\n\r\nimport OpenAI from \"openai\";\r\nimport type { ChatCompletion } from \"openai/resources/chat/completions\";\r\nimport { getLLMConfig } from \"./llmConfig.js\";\r\nimport type { LLMConfig } from \"./llmConfig.js\";\r\n\r\n/** 从非流式 chat completion 取出助手文本;content 为空时检查 refusal、finish_reason */\r\nfunction extractAssistantText(completion: ChatCompletion): string {\r\n const choice = completion.choices[0];\r\n if (!choice) throw new Error(\"LLM 返回无 choices\");\r\n const msg = choice.message;\r\n const raw = msg.content;\r\n if (typeof raw === \"string\") {\r\n const t = raw.trim();\r\n if (t.length > 0) return t;\r\n }\r\n // DeepSeek deepseek-reasoner:推理在 reasoning_content,答复在 content;总输出受 max_tokens 限制,可能仅有 reasoning_content\r\n const extra = msg as unknown as Record<string, unknown>;\r\n const rc = extra.reasoning_content;\r\n if (typeof rc === \"string\" && rc.trim().length > 0) {\r\n return rc.trim();\r\n }\r\n const refusal = msg.refusal;\r\n if (typeof refusal === \"string\" && refusal.trim()) {\r\n throw new Error(`模型拒绝: ${refusal.trim()}`);\r\n }\r\n const fr = choice.finish_reason;\r\n if (fr === \"tool_calls\") {\r\n throw new Error(\"LLM 返回了工具调用而非文本,请换一个模型或关闭工具调用\");\r\n }\r\n if (fr === \"content_filter\") {\r\n throw new Error(\"内容被内容策略过滤\");\r\n }\r\n if (fr === \"length\") {\r\n throw new Error(\r\n \"LLM 输出在 content / reasoning_content 均为空前已用尽\",\r\n );\r\n }\r\n throw new Error(`LLM 返回空内容 (finish_reason=${String(fr)})`);\r\n}\r\n\r\n/** 合并调用方配置与环境变量配置 */\r\nfunction mergeConfig(override?: Partial<LLMConfig> & { apiUrl?: string }): { apiKey: string; baseUrl: string; model: string } {\r\n const env = getLLMConfig();\r\n const apiKey = override?.apiKey ?? env.apiKey;\r\n const baseUrl = override?.apiUrl ?? override?.baseUrl ?? env.baseUrl;\r\n const model = override?.model ?? env.model;\r\n if (!apiKey) throw new Error(\"LLM API Key 未配置:请在管理后台「设置 → LLM」或环境变量 OPENAI_API_KEY 中设置\");\r\n return { apiKey, baseUrl, model };\r\n}\r\n\r\n/** 调用 LLM 获取 JSON 响应,供 parser/extractor 复用 */\r\nexport async function chatJson(\r\n prompt: string,\r\n config?: Partial<LLMConfig> & { apiUrl?: string },\r\n options?: { maxTokens?: number; debugLabel?: string },\r\n): Promise<Record<string, unknown>> {\r\n const { apiKey, baseUrl, model } = mergeConfig(config);\r\n const openai = new OpenAI({ apiKey, baseURL: baseUrl });\r\n const completion = await openai.chat.completions.create({\r\n model,\r\n messages: [{ role: \"user\", content: prompt }],\r\n max_tokens: options?.maxTokens ?? 8192,\r\n response_format: { type: \"json_object\" },\r\n });\r\n const content = extractAssistantText(completion);\r\n return JSON.parse(content) as Record<string, unknown>;\r\n}\r\n\r\n/** 调用 LLM 获取纯文本响应 */\r\nexport async function chatText(\r\n prompt: string,\r\n config?: Partial<LLMConfig> & { apiUrl?: string },\r\n options?: { maxTokens?: number; debugLabel?: string },\r\n): Promise<string> {\r\n const { apiKey, baseUrl, model } = mergeConfig(config);\r\n const openai = new OpenAI({ apiKey, baseURL: baseUrl });\r\n const completion = await openai.chat.completions.create({\r\n model,\r\n messages: [{ role: \"user\", content: prompt }],\r\n max_tokens: options?.maxTokens ?? 8192,\r\n });\r\n return extractAssistantText(completion);\r\n}\r\n\r\nexport { getLLMConfig } from \"./llmConfig.js\";\r\nexport type { LLMConfig } from \"./llmConfig.js\";\r\n","// HTML 列表解析器:专注于从列表页提取多个条目(支持自定义函数、LLM 两种模式)\r\n\r\nimport { createHash } from \"node:crypto\";\r\nimport { applyPurify } from \"../fetcher/purify.js\";\r\nimport { chatJson } from \"../../../../core/llm.js\";\r\nimport { getLLMConfig } from \"../../../../core/llmConfig.js\";\r\nimport type { FeedItem } from \"../../../../types/feedItem.js\";\r\nimport type { ParsedEntry } from \"./types.js\";\r\n\r\n\r\n// 使用 sha256 生成 guid\r\nfunction generateGuid(link: string): string {\r\n return createHash(\"sha256\").update(link).digest(\"hex\");\r\n}\r\n\r\n\r\n/** 解析结果结构(类似 RSS feed 结构) */\r\nexport interface ParsedListResult {\r\n /** 解析出的条目列表 */\r\n items: FeedItem[];\r\n /** 源 URL */\r\n url?: string;\r\n /** 解析模式 */\r\n mode?: ParserMode;\r\n}\r\n\r\n\r\n/** 自定义解析函数:输入 HTML 和 URL,返回 ParsedEntry 数组 */\r\nexport type CustomParserFn = (html: string, url: string) => Promise<ParsedEntry[]> | ParsedEntry[];\r\n\r\n\r\n/** LLM 解析配置 */\r\nexport interface LLMParserConfig {\r\n /** LLM API 端点,不传则从环境变量 OPENAI_BASE_URL 读取 */\r\n apiUrl?: string;\r\n /** API Key,不传则从环境变量 OPENAI_API_KEY 读取 */\r\n apiKey?: string;\r\n /** 模型名称,不传则从环境变量 OPENAI_MODEL 读取 */\r\n model?: string;\r\n /** 提示词模板,{html} 会被替换为 HTML 内容 */\r\n prompt?: string;\r\n}\r\n\r\n\r\n/** 解析模式 */\r\nexport type ParserMode = \"custom\" | \"llm\";\r\n\r\n\r\n/** Parser 配置 */\r\nexport interface ParserConfig {\r\n /** 源 URL,用于提取相对链接和发布日期 */\r\n url?: string;\r\n /** 解析模式:custom(自定义函数)、llm(LLM API),默认根据 customParser 或 llmConfig 自动判断 */\r\n mode?: ParserMode;\r\n /** 自定义解析函数,mode 为 \"custom\" 时必需 */\r\n customParser?: CustomParserFn;\r\n /** LLM 解析配置,mode 为 \"llm\" 时使用(可从环境变量读取) */\r\n llmConfig?: LLMParserConfig;\r\n /** 是否包含详细内容(content),默认 false(仅摘要) */\r\n includeContent?: boolean;\r\n /** 解析前是否净化 HTML(移除 script/style/nav 等无关标签),llm 模式默认 true,custom 模式不适用 */\r\n purify?: boolean;\r\n}\r\n\r\n\r\n// 使用 LLM 解析 HTML,返回 ParsedEntry 数组\r\nasync function parseWithLLM(html: string, url: string, config: LLMParserConfig): Promise<ParsedEntry[]> {\r\n const prompt =\r\n config.prompt ??\r\n `你是一个专业的 HTML 内容提取助手。请仔细分析以下 HTML 页面,提取所有可点击的内容条目(如文章、笔记、帖子、视频等)。\r\n\r\n要求:\r\n1. 返回 JSON 对象,格式为 {\"items\": [...]}\r\n2. 每个条目必须包含以下字段:\r\n - title: 标题(必填)\r\n - link: 完整链接(必填)。**必须从 HTML 的 href 原样提取**:若为绝对路径(以 / 开头)则直接使用;若为相对路径则补全为完整 URL。当前页: ${url}\r\n - description: 摘要或描述(200字内,必填)\r\n - author: 作者(可选)\r\n - published: 发布日期 ISO 字符串(可选)\r\n3. 如果页面是列表页,提取所有列表项\r\n4. 如果页面是详情页,将整个页面作为一个条目提取\r\n5. 如果页面是用户主页/个人资料页:\r\n - 优先尝试提取该用户发布的内容列表\r\n - 如果无法提取列表,至少提取用户基本信息作为一个条目(title 为用户名称或页面标题,description 为用户简介或页面描述,link 为当前 URL)\r\n6. 如果页面是单页应用(SPA),尝试从以下位置提取数据:\r\n - <title> 标签中的页面标题\r\n - <meta name=\"description\"> 或 <meta property=\"og:description\"> 中的描述\r\n - <meta property=\"og:title\"> 中的标题\r\n - <link rel=\"canonical\" href=\"...\"> 中的规范链接(优先使用)\r\n - <script> 标签中的 JSON 数据(搜索包含 \"title\", \"description\", \"user\", \"author\", \"items\", \"list\" 等关键词的 JSON)\r\n - 任何包含 href 属性的 <a> 标签,**link 必须使用 href 的原始值**(以 / 开头的路径表示从站根算起)\r\n - 任何包含文本内容的元素\r\n7. **重要**:即使页面看起来是空的 SPA,也必须至少返回一个条目:\r\n - 从 <title> 提取标题(如果没有则使用 URL)\r\n - 从 <meta> 标签提取描述(如果没有则使用 \"页面内容通过 JavaScript 动态加载\")\r\n - link 使用当前 URL: ${url}\r\n - 如果页面 URL 包含 \"user\" 或 \"profile\",可以推断这是一个用户主页,title 可以是 \"用户主页\",description 可以是页面 URL\r\n\r\n**强制要求**:绝对不能返回空数组 {\"items\": []},至少要返回一个包含页面基本信息的条目。\r\n\r\nHTML:\r\n${html}`;\r\n const parsed = await chatJson(\r\n prompt,\r\n { apiKey: config.apiKey, apiUrl: config.apiUrl, model: config.model },\r\n { debugLabel: \"parseWithLLM\" }\r\n );\r\n let entries: ParsedEntry[] = [];\r\n if (parsed.items && Array.isArray(parsed.items)) {\r\n entries = parsed.items as ParsedEntry[];\r\n } else if (parsed.entries && Array.isArray(parsed.entries)) {\r\n entries = parsed.entries as ParsedEntry[];\r\n } else if (Array.isArray(parsed)) {\r\n entries = parsed as ParsedEntry[];\r\n } else if (parsed && typeof parsed === \"object\") {\r\n entries = [parsed as unknown as ParsedEntry];\r\n }\r\n return entries.map((e) => {\r\n const raw = e.link || url;\r\n if (raw.startsWith(\"http\")) return { ...e, link: raw };\r\n const path = raw.startsWith(\"/\") ? raw : `/${raw}`;\r\n try {\r\n const base = new URL(url);\r\n return { ...e, link: new URL(path, base.origin).href };\r\n } catch {\r\n return { ...e, link: new URL(raw, url).href };\r\n }\r\n });\r\n}\r\n\r\n\r\n// 将 ParsedEntry 转为 FeedItem(不包含详细内容)\r\nfunction toFeedItem(entry: ParsedEntry, includeContent: boolean): FeedItem {\r\n return {\r\n guid: entry.guid ?? generateGuid(entry.link),\r\n title: entry.title,\r\n link: entry.link,\r\n pubDate: entry.published ? new Date(entry.published) : new Date(),\r\n author: entry.author ? [entry.author] : undefined,\r\n summary: entry.description,\r\n content: includeContent ? entry.content : undefined,\r\n imageUrl: entry.imageUrl,\r\n };\r\n}\r\n\r\n\r\n/** 解析 HTML 列表页,返回包含 items 的对象(类似 RSS feed 结构) */\r\nexport async function parseHtml(html: string, config: ParserConfig = {}): Promise<ParsedListResult> {\r\n const { url = \"\", mode, customParser, llmConfig, includeContent = false, purify } = config;\r\n let entries: ParsedEntry[] = [];\r\n const actualMode = mode ?? (llmConfig != null ? \"llm\" : customParser != null ? \"custom\" : \"llm\");\r\n if (actualMode === \"llm\") {\r\n if (llmConfig == null && !getLLMConfig().apiKey) {\r\n throw new Error('mode 为 \"llm\" 时必须提供 llmConfig,或在后台「设置 → LLM」/ OPENAI_API_KEY 中配置 Key');\r\n }\r\n const htmlForLLM = applyPurify(html, purify !== false);\r\n entries = await parseWithLLM(htmlForLLM, url, llmConfig ?? {});\r\n } else if (actualMode === \"custom\") {\r\n if (customParser == null) {\r\n throw new Error('mode 为 \"custom\" 时必须提供 customParser');\r\n }\r\n entries = await customParser(html, url);\r\n if (!Array.isArray(entries)) {\r\n entries = [entries];\r\n }\r\n } else {\r\n throw new Error(`不支持的解析模式: ${actualMode}`);\r\n }\r\n const items = entries.map((e) => toFeedItem(e, includeContent));\r\n return {\r\n items,\r\n url,\r\n mode: actualMode,\r\n };\r\n}\r\n","// 站点抽象接口:声明 URL 模式及 fetchItems 能力(WebSource 专用)\n\nimport type { PluginHostDeps } from \"../../../plugins/hostDeps.js\";\nimport type { AuthFlow, CheckAuthFn } from \"../../auth/index.js\";\nimport type { RefreshInterval } from \"../../../utils/refreshInterval.js\";\nimport type { FeedItem } from \"../../../types/feedItem.js\";\n\n\n/** 框架注入给插件的调用上下文,提供浏览器抓取工具与默认正文提取 */\nexport interface SiteContext {\n /** 缓存目录 */\n cacheDir?: string;\n /** 是否无头浏览器 */\n headless?: boolean;\n /** 代理地址 */\n proxy?: string;\n /** 与 SourceContext.deps 相同,宿主注入依赖,用户插件勿从 npm 直接 import */\n deps: PluginHostDeps;\n /**\n * 用浏览器抓取指定 URL,返回渲染后的 HTML。\n * 插件需要访问网页时调用此方法,框架负责浏览器管理与 cookie 注入。\n */\n fetchHtml(\n url: string,\n opts?: {\n waitMs?: number;\n purify?: boolean;\n waitForSelector?: string;\n waitForSelectorTimeoutMs?: number;\n /** 使用导航 HTTP 响应原文(适合 RSS/XML),见 RequestConfig.useHttpResponseBody */\n useHttpResponseBody?: boolean;\n }\n ): Promise<{ html: string; finalUrl: string; status: number }>;\n /**\n * 默认正文提取:拉取 item.link 的 HTML 后用 Readability 提取正文,合并回条目并返回。\n * 可在 fetchItems 内按需 await ctx.extractItem(单条) 使用。\n */\n extractItem(\n item: FeedItem,\n opts?: { cacheKey?: string }\n ): Promise<FeedItem>;\n}\n\n\n/** 站点抽象接口:声明该站点支持的 URL 模式及数据获取能力 */\nexport interface Site {\n /** 站点标识,如 \"xiaohongshu\"、\"lingowhale\" */\n readonly id: string;\n /** 列表页 URL 匹配模式,{placeholder} 匹配路径段 */\n readonly listUrlPattern: string | RegExp;\n /** 条目有效时间窗口;不填默认 1day */\n readonly refreshInterval?: RefreshInterval;\n /** 代理地址;不填则使用 env HTTP_PROXY */\n readonly proxy?: string;\n /**\n * 核心能力:给定 sourceId,返回条目列表。\n * 插件自行决定如何获取数据(调用 ctx.fetchHtml、直接 fetch API 等均可)。\n */\n fetchItems(sourceId: string, ctx: SiteContext): Promise<FeedItem[]>;\n /** 认证:检查是否已登录 */\n checkAuth?: CheckAuthFn | null;\n /** 认证:登录页 URL */\n loginUrl?: string | null;\n /** 认证:域名,cookies 保存在 domains/{domain}.json */\n domain?: string | null;\n /** 认证:等待登录超时毫秒,默认 300000 */\n loginTimeoutMs?: number | null;\n /** 认证:轮询 checkAuth 间隔毫秒,默认 2000 */\n pollIntervalMs?: number | null;\n}\n\n\n/** 将字符串 URL 模式转为正则:{xxx} → [^/]+,末尾允许 ?query */\nfunction patternToRegex(pattern: string | RegExp): RegExp {\n if (pattern instanceof RegExp) return pattern;\n const pathOnly = pattern.split(\"?\")[0];\n const pl = \"<<<__PL__>>>\";\n const s = pathOnly\n .replace(/\\{[^}]*\\}/g, pl)\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (c) => \"\\\\\" + c)\n .replace(new RegExp(pl.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"g\"), \"[^/]+\");\n return new RegExp(\"^\" + s + \"(\\\\?.*)?$\");\n}\n\n\n/** 从 Site 扁平字段构建 AuthFlow,无需登录时返回 undefined */\nexport function toAuthFlow(site: Site): AuthFlow | undefined {\n if (!site.checkAuth || !site.loginUrl) return undefined;\n return {\n checkAuth: site.checkAuth,\n loginUrl: site.loginUrl,\n domain: site.domain ?? undefined,\n loginTimeoutMs: site.loginTimeoutMs ?? undefined,\n pollIntervalMs: site.pollIntervalMs ?? undefined,\n };\n}\n\n\n/** 判断 URL 是否匹配站点的 listUrlPattern */\nexport function matchesListUrl(site: Site, url: string): boolean {\n try {\n return patternToRegex(site.listUrlPattern).test(url);\n } catch {\n return false;\n }\n}\n\n\n/** 根据 listUrlPattern 自动计算 URL 匹配具体度(不匹配返回 -1) */\nexport function computeSpecificity(site: Site, url: string): number {\n if (!matchesListUrl(site, url)) return -1;\n const p = site.listUrlPattern;\n if (typeof p === \"string\") {\n const pathOnly = p.split(\"?\")[0];\n return 1000 + pathOnly.split(\"/\").filter(Boolean).length;\n }\n return p.source.length;\n}\n\n\n/** 根据 URL 查找匹配的站点实例,返回具体度最高的站点 */\nexport function getSiteByUrl(url: string, sites: Site[]): Site | undefined {\n const matched = sites\n .map((s) => ({ site: s, score: computeSpecificity(s, url) }))\n .filter((x) => x.score >= 0)\n .sort((a, b) => b.score - a.score);\n return matched[0]?.site;\n}\n","// HTTP 错误:router 捕获后返回对应状态码页面\n\nexport class AuthRequiredError extends Error {\n constructor(message = \"需要登录\") {\n super(message);\n this.name = \"AuthRequiredError\";\n }\n}\n\n\nexport class NotFoundError extends Error {\n constructor(message = \"未找到\") {\n super(message);\n this.name = \"NotFoundError\";\n }\n}\n","// 插件加载器:从 app/plugins/builtin/ 与 .rssany/plugins/ 加载 Site / Source 插件\r\n\r\nimport { readdir } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport { join } from \"node:path\";\r\nimport type { Site } from \"../scraper/sources/web/site.js\";\r\nimport type { Source } from \"../scraper/sources/types.js\";\r\nimport { BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR } from \"../config/paths.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\n\r\n/** LLM 帮助函数,由 feeder 注入到插件上下文 */\r\nexport interface PluginLlm {\r\n chatJson: (prompt: string, config?: Record<string, unknown>, options?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>>;\r\n chatText: (prompt: string, config?: Record<string, unknown>, options?: { maxTokens?: number; debugLabel?: string }) => Promise<string>;\r\n}\r\n\r\n/** DB 帮助函数,由 feeder 注入到插件上下文 */\r\nexport interface PluginDb {\r\n getSystemTags: () => Promise<string[]>;\r\n}\r\n\r\n/** 插件统一上下文,由 feeder 在执行前注入 llm / db */\r\nexport interface PluginContext {\r\n sourceUrl?: string;\r\n llm?: PluginLlm;\r\n db?: PluginDb;\r\n [key: string]: unknown;\r\n}\r\n\r\n\r\nconst PLUGIN_EXTENSIONS = [\".rssany.js\", \".rssany.ts\"];\r\n\r\n\r\n/** 判断对象是否为有效的 Site 实现 */\r\nfunction isValidSite(obj: unknown): obj is Site {\r\n if (obj == null || typeof obj !== \"object\") return false;\r\n const s = obj as Record<string, unknown>;\r\n return (\r\n typeof s.id === \"string\" &&\r\n (typeof s.listUrlPattern === \"string\" || s.listUrlPattern instanceof RegExp) &&\r\n typeof s.fetchItems === \"function\"\r\n );\r\n}\r\n\r\n/** 判断对象是否为有效的 Source 实现 */\r\nfunction isValidSource(obj: unknown): obj is Source {\r\n if (obj == null || typeof obj !== \"object\") return false;\r\n const s = obj as Record<string, unknown>;\r\n return (\r\n typeof s.id === \"string\" &&\r\n (typeof s.pattern === \"string\" || s.pattern instanceof RegExp) &&\r\n typeof s.fetchItems === \"function\" &&\r\n s.listUrlPattern === undefined\r\n );\r\n}\r\n\r\n/** 从单个目录加载 Site / Source 插件,并记录每个 Site / Source 的文件路径 */\r\nasync function loadSourcePluginsFromDir(\r\n dir: string,\r\n label: string,\r\n): Promise<{\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n}> {\r\n const siteEntries: Array<{ site: Site; filePath: string }> = [];\r\n const sources: Array<{ source: Source; filePath: string }> = [];\r\n let entries: { name: string; isFile: () => boolean }[];\r\n try {\r\n const raw = await readdir(dir, { withFileTypes: true, encoding: \"utf-8\" });\r\n entries = raw as { name: string; isFile: () => boolean }[];\r\n } catch {\r\n return { siteEntries, sources };\r\n }\r\n for (const e of entries) {\r\n const name = String(e.name);\r\n if (!e.isFile()) continue;\r\n if (!PLUGIN_EXTENSIONS.some((ext) => name.endsWith(ext))) continue;\r\n const filePath = join(dir, name);\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n const plugin = mod.default ?? mod;\r\n if (isValidSite(plugin)) {\r\n siteEntries.push({ site: plugin, filePath });\r\n } else if (isValidSource(plugin)) {\r\n sources.push({ source: plugin, filePath });\r\n } else {\r\n logger.warn(\"plugin\", \"插件未实现 Site 或 Source 接口,已跳过\", { label, name });\r\n }\r\n } catch (err) {\r\n logger.warn(\"plugin\", \"插件加载失败\", { label, name, err: err instanceof Error ? err.message : String(err) });\r\n }\r\n }\r\n return { siteEntries, sources };\r\n}\r\n\r\n\r\nasync function loadBuiltinAndUser(): Promise<{\r\n builtin: {\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n };\r\n user: {\r\n siteEntries: Array<{ site: Site; filePath: string }>;\r\n sources: Array<{ source: Source; filePath: string }>;\r\n };\r\n}> {\r\n const [builtin, user] = await Promise.all([\r\n loadSourcePluginsFromDir(BUILTIN_PLUGINS_DIR, \"builtin\"),\r\n loadSourcePluginsFromDir(USER_PLUGINS_DIR, \"user\"),\r\n ]);\r\n return { builtin, user };\r\n}\r\n\r\n\r\n/** Site / Source 插件 id → 当前生效的文件路径(用户覆盖内置后的路径) */\r\nconst pluginSitePaths = new Map<string, string>();\r\n\r\n/** 将 Source 插件路径写入 pathMap(不与 Site id 冲突;用户覆盖内置 Source) */\r\nfunction mergeSourcePluginPaths(\r\n siteIds: Set<string>,\r\n pathMap: Map<string, string>,\r\n builtinSources: Array<{ source: Source; filePath: string }>,\r\n userSources: Array<{ source: Source; filePath: string }>,\r\n): void {\r\n for (const { source, filePath } of builtinSources) {\r\n if (siteIds.has(source.id)) {\r\n logger.warn(\"plugin\", \"Source 插件 id 与 Site 插件冲突,已忽略 Source 路径\", { sourceId: source.id });\r\n continue;\r\n }\r\n pathMap.set(source.id, filePath);\r\n }\r\n for (const { source, filePath } of userSources) {\r\n if (siteIds.has(source.id)) {\r\n logger.warn(\"plugin\", \"Source 插件 id 与 Site 插件冲突,已忽略 Source 路径\", { sourceId: source.id });\r\n continue;\r\n }\r\n if (pathMap.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n pathMap.set(source.id, filePath);\r\n }\r\n}\r\n\r\n/** 根据插件 id 获取其源文件路径(用于详情页编辑,仅当前生效的插件有路径) */\r\nexport function getPluginFilePath(id: string): string | undefined {\r\n return pluginSitePaths.get(id);\r\n}\r\n\r\n/** 加载所有 Site 插件;用户插件可覆盖同 id 内置 */\r\nexport async function loadPlugins(): Promise<Site[]> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const merged = new Map<string, Site>();\r\n const pathMap = new Map<string, string>();\r\n for (const { site, filePath } of builtin.siteEntries) {\r\n merged.set(site.id, site);\r\n pathMap.set(site.id, filePath);\r\n }\r\n for (const { site, filePath } of user.siteEntries) {\r\n if (merged.has(site.id)) logger.info(\"plugin\", \"用户插件覆盖同名内置插件\", { pluginId: site.id });\r\n merged.set(site.id, site);\r\n pathMap.set(site.id, filePath);\r\n }\r\n mergeSourcePluginPaths(new Set(merged.keys()), pathMap, builtin.sources, user.sources);\r\n pluginSitePaths.clear();\r\n pathMap.forEach((path, id) => pluginSitePaths.set(id, path));\r\n return Array.from(merged.values());\r\n}\r\n\r\n\r\n/** 加载所有 Source 插件;用户插件可覆盖同 id */\r\nexport async function loadSourcePlugins(): Promise<Source[]> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const merged = new Map<string, Source>();\r\n for (const { source } of builtin.sources) merged.set(source.id, source);\r\n for (const { source } of user.sources) {\r\n if (merged.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n merged.set(source.id, source);\r\n }\r\n return Array.from(merged.values());\r\n}\r\n\r\n\r\n/** 加载 Site 与 Source:合并去重,供 initSources 使用;同时更新 pluginSitePaths */\r\nexport async function loadSiteAndSourcePlugins(): Promise<{ sites: Site[]; sources: Source[] }> {\r\n const { builtin, user } = await loadBuiltinAndUser();\r\n const siteMap = new Map<string, Site>();\r\n const pathMap = new Map<string, string>();\r\n for (const { site: s, filePath } of builtin.siteEntries) {\r\n siteMap.set(s.id, s);\r\n pathMap.set(s.id, filePath);\r\n }\r\n for (const { site: s, filePath } of user.siteEntries) {\r\n if (siteMap.has(s.id)) logger.info(\"plugin\", \"用户插件覆盖同名内置\", { pluginId: s.id });\r\n siteMap.set(s.id, s);\r\n pathMap.set(s.id, filePath);\r\n }\r\n mergeSourcePluginPaths(new Set(siteMap.keys()), pathMap, builtin.sources, user.sources);\r\n const sourceMap = new Map<string, Source>();\r\n for (const { source } of builtin.sources) sourceMap.set(source.id, source);\r\n for (const { source } of user.sources) {\r\n if (sourceMap.has(source.id)) logger.info(\"plugin\", \"用户 Source 插件覆盖同名内置\", { sourceId: source.id });\r\n sourceMap.set(source.id, source);\r\n }\r\n pluginSitePaths.clear();\r\n pathMap.forEach((path, id) => pluginSitePaths.set(id, path));\r\n return { sites: Array.from(siteMap.values()), sources: Array.from(sourceMap.values()) };\r\n}\r\n","// 插件加载与包装:将 Site 插件包装为 Source 接口,注入 SiteContext 工具\n\nimport { fetchHtml as fetchHtmlFn, preCheckAuth } from \"./fetcher/index.js\";\nimport { extractHtml } from \"./extractor/index.js\";\nimport { parseHtml } from \"./parser/index.js\";\nimport { toAuthFlow, getSiteByUrl } from \"./site.js\";\nimport { AuthRequiredError } from \"../../auth/index.js\";\nimport type { Site, SiteContext } from \"./site.js\";\nimport type { Source, SourceContext } from \"../types.js\";\nimport type { FeedItem } from \"../../../types/feedItem.js\";\nimport { normalizeAuthor } from \"../../../types/feedItem.js\";\n\n\n/** 从 SourceContext + Site 构建注入了工具的 SiteContext */\nexport function buildSiteContext(site: Site, ctx: SourceContext): SiteContext {\n const proxy = ctx.proxy ?? site.proxy;\n const authFlow = toAuthFlow(site);\n return {\n cacheDir: ctx.cacheDir,\n headless: ctx.headless,\n proxy,\n deps: ctx.deps,\n async fetchHtml(url, opts) {\n const res = await fetchHtmlFn(url, {\n cacheDir: ctx.cacheDir,\n useCache: false,\n authFlow,\n headless: ctx.headless,\n proxy,\n waitAfterLoadMs: opts?.waitMs,\n purify: opts?.purify,\n waitForSelector: opts?.waitForSelector,\n waitForSelectorTimeoutMs: opts?.waitForSelectorTimeoutMs,\n useHttpResponseBody: opts?.useHttpResponseBody,\n });\n return { html: res.body, finalUrl: res.finalUrl ?? url, status: res.status };\n },\n async extractItem(item, opts) {\n const res = await fetchHtmlFn(item.link, {\n cacheDir: ctx.cacheDir,\n useCache: false,\n authFlow,\n headless: ctx.headless,\n proxy,\n });\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`默认正文提取失败: HTTP ${res.status} ${res.statusText} for ${item.link}`);\n }\n const extracted = await extractHtml(res.body, {\n url: res.finalUrl ?? item.link,\n cacheDir: ctx.cacheDir ?? undefined,\n mode: \"readability\",\n useCache: true,\n cacheKey: opts?.cacheKey,\n });\n const pubDate =\n extracted.pubDate != null\n ? typeof extracted.pubDate === \"string\"\n ? new Date(extracted.pubDate)\n : extracted.pubDate\n : item.pubDate;\n return {\n ...item,\n author: normalizeAuthor(extracted.author ?? item.author),\n title: extracted.title ?? item.title,\n summary: extracted.summary ?? item.summary,\n content: extracted.content ?? item.content,\n pubDate,\n };\n },\n };\n}\n\n\n/** 将 Site 插件包装为统一 Source 接口 */\nexport function createWebSource(site: Site): Source {\n const authFlow = toAuthFlow(site);\n return {\n id: site.id,\n pattern: site.listUrlPattern,\n priority: 50,\n refreshInterval: site.refreshInterval ?? undefined,\n proxy: site.proxy ?? undefined,\n preCheck: authFlow\n ? async (ctx: SourceContext) => {\n if (!ctx.cacheDir) return;\n const passed = await preCheckAuth(authFlow, ctx.cacheDir, {\n proxy: ctx.proxy,\n headless: ctx.headless,\n });\n if (!passed) throw new AuthRequiredError(`站点 ${site.id} 需要登录,请先执行 ensureAuth`);\n }\n : undefined,\n async fetchItems(sourceId: string, ctx: SourceContext): Promise<FeedItem[]> {\n return site.fetchItems(sourceId, buildSiteContext(site, ctx));\n },\n };\n}\n\n\n/** 通用 WebSource:兜底匹配所有 http/https URL,使用浏览器抓取 + LLM 解析 */\nexport const genericWebSource: Source = {\n id: \"generic\",\n pattern: /^https?:\\/\\//,\n priority: 200,\n async fetchItems(sourceId: string, ctx: SourceContext): Promise<FeedItem[]> {\n const res = await fetchHtmlFn(sourceId, {\n cacheDir: ctx.cacheDir,\n useCache: true,\n headless: ctx.headless,\n proxy: ctx.proxy,\n });\n // 304 Not Modified:浏览器用缓存,page.content() 仍会返回完整 HTML,视为成功\n if (res.status !== 200 && res.status !== 304) {\n throw new Error(`抓取失败: HTTP ${res.status} ${res.statusText}`);\n }\n const parsed = await parseHtml(res.body, {\n url: res.finalUrl ?? sourceId,\n });\n return parsed.items;\n },\n};\n\n\n/** 保存已加载的 Site 对象(供 auth 路由、调试路由直接访问) */\nconst loadedSites: Site[] = [];\n\n\n/** 更新已加载站点列表(由 sources/index.ts 调用) */\nexport function setLoadedSites(sites: Site[]): void {\n loadedSites.length = 0;\n loadedSites.push(...sites);\n}\n\n\n/** 根据 id 获取底层 Site(用于 auth 路由) */\nexport function getWebSite(id: string): Site | undefined {\n return loadedSites.find((s) => s.id === id);\n}\n\n\n/** 获取所有已加载的 Site(用于插件列表 API) */\nexport function getPluginSites(): Site[] {\n return loadedSites.filter((s) => s.id !== \"generic\");\n}\n\n\n/** 根据 URL 获取最具体匹配的 Site(用于调试路由) */\nexport function getBestSite(url: string): Site | undefined {\n return getSiteByUrl(url, loadedSites);\n}\n\n\nexport type { Site, SiteContext } from \"./site.js\";\nexport { toAuthFlow, computeSpecificity } from \"./site.js\";\nexport { loadPlugins } from \"../../../plugins/loader.js\";\n","// 宿主应用解析的常用依赖,注入到 SourceContext / SiteContext.deps\r\n// 用户目录下的 .rssany/plugins 内脚本无法解析应用 node_modules,须通过 ctx.deps 使用\r\n\r\nimport { parse, NodeType } from \"node-html-parser\";\r\nimport { createHash } from \"node:crypto\";\r\nimport RssParser from \"rss-parser\";\r\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\r\nimport { ImapFlow } from \"imapflow\";\r\nimport { simpleParser } from \"mailparser\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nexport const PLUGIN_HOST_DEPS = {\r\n parseHtml: parse,\r\n NodeType,\r\n createHash,\r\n RssParser,\r\n HttpsProxyAgent,\r\n ImapFlow,\r\n simpleParser,\r\n logger,\r\n} as const;\r\n\r\nexport type PluginHostDeps = typeof PLUGIN_HOST_DEPS;\r\n","import { PLUGIN_HOST_DEPS } from \"../../plugins/hostDeps.js\";\r\nimport type { SourceContext } from \"./types.js\";\r\nimport { fetchHtml as fetchHtmlFn } from \"./web/fetcher/index.js\";\r\n\r\n/** 构造带 deps 的信源上下文(抓取、preCheck、插件 fetchItems 均须使用) */\r\nexport function buildSourceContext(partial: {\r\n cacheDir?: string;\r\n headless?: boolean;\r\n proxy?: string;\r\n}): SourceContext {\r\n const { cacheDir, headless, proxy } = partial;\r\n return {\r\n ...partial,\r\n deps: PLUGIN_HOST_DEPS,\r\n async fetchHtml(url, opts) {\r\n const res = await fetchHtmlFn(url, {\r\n cacheDir,\r\n useCache: false,\r\n headless,\r\n proxy,\r\n waitAfterLoadMs: opts?.waitMs,\r\n purify: opts?.purify,\r\n waitForSelector: opts?.waitForSelector,\r\n waitForSelectorTimeoutMs: opts?.waitForSelectorTimeoutMs,\r\n useHttpResponseBody: opts?.useHttpResponseBody,\r\n });\r\n return { html: res.body, finalUrl: res.finalUrl ?? url, status: res.status };\r\n },\r\n };\r\n}\r\n","// 统一信源注册表:汇聚 WebSource 插件、Source 插件(RSS/Email)、genericWebSource,提供 getSource 查找入口\n\nimport { createWebSource, genericWebSource, setLoadedSites } from \"./web/index.js\";\nimport { loadSiteAndSourcePlugins } from \"../../plugins/loader.js\";\nimport type { Source } from \"./types.js\";\nimport { logger } from \"../../core/logger/index.js\";\nexport { buildSourceContext } from \"./context.js\";\n\n/** 所有已注册的信源(按 priority 排序后迭代匹配) */\nexport const registeredSources: Source[] = [];\n\n/** 将字符串 URL 模式转为正则:{placeholder} 匹配单个路径段,末尾允许 query */\nfunction sourcePatternToRegex(pattern: string | RegExp): RegExp {\n if (pattern instanceof RegExp) return pattern;\n const pathOnly = pattern.split(\"?\")[0];\n const pl = \"<<<__PL__>>>\";\n const escaped = pathOnly\n .replace(/\\{[^}]*\\}/g, pl)\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (c) => \"\\\\\" + c)\n .replace(new RegExp(pl.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"g\"), \"[^/]+\");\n return new RegExp(\"^\" + escaped + \"(\\\\?.*)?$\");\n}\n\n\n/** 根据 sourceId 查找匹配度最高的 Source;按 priority 升序,优先用 match 否则用 pattern */\nexport function getSource(sourceId: string): Source {\n for (const source of registeredSources) {\n const matches = source.match ? source.match(sourceId) : (() => {\n try {\n return sourcePatternToRegex(source.pattern).test(sourceId);\n } catch {\n return false;\n }\n })();\n if (matches) return source;\n }\n return genericWebSource;\n}\n\n\n/** 根据 id 精确查找 Source(用于内部调试) */\nexport function getSourceById(id: string): Source | undefined {\n return registeredSources.find((s) => s.id === id);\n}\n\n\n/** 初始化所有信源:加载 sources 插件、构建注册表(pipeline 为固定流程,见 app/pipeline/) */\nexport async function initSources(): Promise<void> {\n const siteResult = await loadSiteAndSourcePlugins();\n const { sites, sources: sourcePlugins } = siteResult;\n setLoadedSites(sites);\n registeredSources.length = 0;\n const webSources = sites.map((s) => createWebSource(s));\n const all: Source[] = [\n ...sourcePlugins,\n ...webSources,\n genericWebSource,\n ];\n all.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));\n registeredSources.push(...all);\n logger.info(\"scheduler\", \"信源已注册\", {\n total: registeredSources.length,\n siteCount: sites.length,\n sourcePluginCount: sourcePlugins.length,\n });\n}\n","// 订阅配置类型:一个订阅由多个信源组成,支持网页、RSS、邮件、API 等多种类型\n\nimport type { RefreshInterval } from \"../../utils/refreshInterval.js\";\n\n\n/** 信源类型枚举 */\nexport type SourceType = \"web\" | \"rss\" | \"email\";\n\n\n/**\n * 单个信源配置\n *\n * ref 格式示例:\n * web / rss → https://sspai.com/feed\n * → https://xiaohongshu.com/user/profile/xxx\n * email → imaps://user:password@imap.gmail.com:993/INBOX\n * → imap://user:password@imap.qq.com:143/INBOX\n * → imaps://me%40gmail.com:app-password@imap.gmail.com/INBOX\n * (用户名含 @ 时用 %40 编码;Gmail 需使用「应用专用密码」)\n */\nexport interface SubscriptionSource {\n /** 信源标识符:HTTP(S) URL、imaps?:// 连接串、或插件自定义协议(如 lingowhale://) */\n ref: string;\n /** 信源类型(省略时由 getSource 自动识别) */\n type?: SourceType;\n /** 显示名称,用于界面展示和报错信息(省略则显示 ref) */\n label?: string;\n /** 简短描述,用于界面展示信源用途或内容说明 */\n description?: string;\n /** 单源有效时间窗口覆盖:优先级高于 Source 声明;不填则使用 Source 声明 */\n refresh?: RefreshInterval;\n /** 单源 cron 表达式(如 \"0 9 * * *\" 每天 9:00);有值时优先于 refresh */\n cron?: string;\n /** 单源代理覆盖:优先级高于 Source.proxy;不填则使用 Source.proxy 或 env HTTP_PROXY */\n proxy?: string;\n /** 信源权重,用于排序与优先级控制;默认 0,值越大优先级越高 */\n weight?: number;\n}\n\n\n/** sources.json 标准格式:扁平信源列表,无频道/订阅层级 */\nexport interface SourcesFile {\n /** 所有要抓取的信源 */\n sources: SubscriptionSource[];\n}\n\n\n/** 从旧格式({ url })或新格式({ ref })中提取信源标识符,确保向后兼容 */\nexport function resolveRef(src: SubscriptionSource | { url?: string; ref?: string }): string {\n return (src as SubscriptionSource).ref ?? (src as { url?: string }).url ?? \"\";\n}\n","// 全局 HTTP(S) 代理:读写 .rssany/config.json 的 globalProxy;与单源代理合并见 subscription/getEffectiveProxyForListUrl\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\n\r\nexport async function readGlobalProxyFromConfig(): Promise<string | undefined> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { globalProxy?: unknown };\r\n if (typeof j.globalProxy === \"string\") {\r\n const t = j.globalProxy.trim();\r\n return t.length > 0 ? t : undefined;\r\n }\r\n } catch {\r\n /* 无文件或解析失败 */\r\n }\r\n return undefined;\r\n}\r\n\r\n/** 写入或清空(空串则删除键) */\r\nexport async function saveGlobalProxyToConfig(proxy: string): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n //\r\n }\r\n const t = proxy.trim();\r\n if (t.length === 0) {\r\n delete root.globalProxy;\r\n } else {\r\n root.globalProxy = t;\r\n }\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n}\r\n\r\n/** 插件 Site 声明的 proxy 优先于 config 全局代理 */\r\nexport async function resolveProxyForSite(site: { proxy?: string }): Promise<string | undefined> {\r\n const s = site.proxy?.trim();\r\n if (s) return s;\r\n return readGlobalProxyFromConfig();\r\n}\r\n","// 信源配置模块:读取 .rssany/sources.json(扁平信源列表,供 scheduler 使用)\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { SOURCES_CONFIG_PATH } from \"../../config/paths.js\";\r\nimport type { SubscriptionSource, SourcesFile } from \"./types.js\";\r\nimport { resolveRef } from \"./types.js\";\r\nimport type { Source } from \"../sources/types.js\";\r\nimport { readGlobalProxyFromConfig } from \"../../config/globalProxy.js\";\r\n\r\nexport type { SubscriptionSource, SourcesFile } from \"./types.js\";\r\nexport { resolveRef } from \"./types.js\";\r\n\r\n\r\n/** 从 .rssany/sources.json 加载;支持新格式 { sources: [] } 与旧格式 { [id]: { sources: [] } },统一扁平为 SubscriptionSource[] */\r\nasync function loadSourcesFile(): Promise<SubscriptionSource[]> {\r\n try {\r\n const raw = await readFile(SOURCES_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as unknown;\r\n if (!parsed || typeof parsed !== \"object\") return [];\r\n if (Array.isArray((parsed as SourcesFile).sources)) {\r\n return (parsed as SourcesFile).sources.filter((s) => resolveRef(s));\r\n }\r\n const entries = Object.values(parsed as Record<string, { sources?: SubscriptionSource[] }>);\r\n const list: SubscriptionSource[] = [];\r\n for (const entry of entries) {\r\n if (entry && Array.isArray(entry.sources)) {\r\n for (const s of entry.sources) {\r\n if (resolveRef(s)) list.push(s);\r\n }\r\n }\r\n }\r\n return list;\r\n } catch {\r\n return [];\r\n }\r\n}\r\n\r\n\r\n/** 获取所有信源(扁平列表),供 scheduler 使用 */\r\nexport async function getAllSources(): Promise<SubscriptionSource[]> {\r\n return loadSourcesFile();\r\n}\r\n\r\n/** 去重后的 ref 列表(与 sources.json 一致),供 Feed / 聚合查询使用 */\r\nexport async function getAllSubscriptionRefs(): Promise<string[]> {\r\n const list = await loadSourcesFile();\r\n const seen = new Set<string>();\r\n const out: string[] = [];\r\n for (const s of list) {\r\n const r = resolveRef(s);\r\n if (r && !seen.has(r)) {\r\n seen.add(r);\r\n out.push(r);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n\r\n/** 将扁平列表写回 sources.json(新格式);供 raw API 写入 */\r\nexport async function saveSourcesFile(sources: SubscriptionSource[]): Promise<void> {\r\n await writeFile(\r\n SOURCES_CONFIG_PATH,\r\n JSON.stringify({ sources }, null, 2) + \"\\n\",\r\n \"utf-8\"\r\n );\r\n}\r\n\r\n\r\n/**\r\n * 代理优先级:sources.json 单源 → 插件 Source.proxy → config.json globalProxy →(未写入则由 fetcher resolveProxy 使用 HTTP_PROXY)\r\n */\r\nexport async function getEffectiveProxyForListUrl(listUrl: string, source: Source): Promise<string | undefined> {\r\n const list = await getAllSources();\r\n const sub = list.find((s) => resolveRef(s) === listUrl);\r\n const fromSub = sub?.proxy?.trim();\r\n if (fromSub) return fromSub;\r\n const fromPlugin = source.proxy?.trim();\r\n if (fromPlugin) return fromPlugin;\r\n return readGlobalProxyFromConfig();\r\n}\r\n\r\n/** 读取 sources.json 原始内容(用于 GET /api/sources/raw);旧格式会转为新格式返回 */\r\nexport async function getSourcesRaw(): Promise<string> {\r\n try {\r\n const raw = await readFile(SOURCES_CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as unknown;\r\n if (!parsed || typeof parsed !== \"object\") return JSON.stringify({ sources: [] }, null, 2);\r\n if (Array.isArray((parsed as SourcesFile).sources)) {\r\n return raw;\r\n }\r\n const list = await loadSourcesFile();\r\n return JSON.stringify({ sources: list }, null, 2);\r\n } catch {\r\n return JSON.stringify({ sources: [] }, null, 2);\r\n }\r\n}\r\n","/**\r\n * Pipeline 步骤:LLM 内容质量过滤\r\n *\r\n * 判定为低质量(无实质信息、垃圾占位、纯广告等)时标记条目为丢弃;\r\n * feeder 会从数据库删除该条并不再输出到 RSS。\r\n *\r\n * 需在 .rssany/config.json 的 pipeline.steps 中启用,且配置 OPENAI_API_KEY 等 LLM 环境变量。\r\n * 建议放在 tagger / translator 之前以省 token。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { markPipelineDrop } from \"../types/feedItem.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nconst MAX_BODY_CHARS = 4000;\r\n\r\nconst SYSTEM = `你是信息质量评估助手。根据标题、摘要与正文片段,判断该条是否值得保留在订阅信息流中。\r\n- 保留(keep: true):有实质信息、观点、技术内容、新闻价值,或对读者有参考意义。\r\n- 过滤(keep: false):明显垃圾广告、纯日期/站务占位、无意义一句话、乱码或空白、纯导航无内容、重复无信息量的模板句。\r\n- 只输出 JSON,格式:{\"keep\": true 或 false, \"reason\": \"一句话中文理由\"}`;\r\n\r\nexport interface QualityFilterContext {\r\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\r\n}\r\n\r\nfunction combinedForJudge(item: FeedItem): string {\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? \"\").trim();\r\n let body = (item.content ?? \"\").trim();\r\n if (body.length > MAX_BODY_CHARS) body = body.slice(0, MAX_BODY_CHARS) + \"\\n\\n[... 正文已截断 ...]\";\r\n const parts: string[] = [];\r\n if (title) parts.push(`标题:\\n${title}`);\r\n if (summary) parts.push(`摘要:\\n${summary}`);\r\n if (body) parts.push(`正文:\\n${body}`);\r\n return parts.join(\"\\n\\n---\\n\\n\");\r\n}\r\n\r\n/** 无 LLM 时跳过 */\r\nexport function qualityFilterMatch(_item: FeedItem, ctx: QualityFilterContext): boolean {\r\n return !!ctx.llm;\r\n}\r\n\r\nexport async function runQualityFilter(item: FeedItem, ctx: QualityFilterContext): Promise<FeedItem> {\r\n if (!ctx.llm) return item;\r\n\r\n const text = combinedForJudge(item);\r\n if (!text.trim()) return item;\r\n\r\n const prompt = `${SYSTEM}\\n\\n请评估以下条目:\\n\\n${text}`;\r\n\r\n let res: Record<string, unknown>;\r\n try {\r\n res = await ctx.llm.chatJson(prompt, undefined, { maxTokens: 256, debugLabel: \"qualityFilter\" });\r\n } catch {\r\n return item;\r\n }\r\n\r\n const isKeep = res.keep === true || res.keep === \"true\";\r\n const isDrop = res.keep === false || res.keep === \"false\";\r\n if (isKeep) return item;\r\n if (!isDrop) return item;\r\n\r\n const reason = typeof res.reason === \"string\" ? res.reason.trim() : \"\";\r\n logger.info(\"pipeline\", \"质量过滤移除条目\", { link: item.link, reason: reason || undefined });\r\n return markPipelineDrop(item);\r\n}\r\n","/**\n * Pipeline 步骤:LLM 自动打标签\n *\n * 从系统标签库(话题 tags 并集)中选取匹配的标签写入 item.tags。\n * 模型建议的 tag 若不在系统标签库中则忽略。\n *\n * 需要配置 OPENAI_API_KEY(或等效 LLM 环境变量)。\n */\n\nimport type { FeedItem } from \"../types/feedItem.js\";\n\nconst SYSTEM = `你是一个信息分类助手。根据文章标题和摘要,从候选标签库中选出最匹配的标签,最多 5 个。\n- 只能使用候选标签库中已有的标签,不要输出库中不存在的标签。\n- 如果没有合适的标签,输出空数组 {\"tags\": []},不要硬选不相关的标签。\n- 只输出 JSON,格式:{\"tags\": [\"tag1\", \"tag2\", ...]}`;\n\nexport interface TaggerContext {\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\n db?: { getSystemTags: () => Promise<string[]> };\n}\n\n/** 跳过已有 tags 的条目;无 LLM 则跳过 */\nexport function taggerMatch(item: FeedItem, ctx: TaggerContext): boolean {\n return !item.tags?.length && !!ctx.llm;\n}\n\nexport async function runTagger(item: FeedItem, ctx: TaggerContext): Promise<FeedItem> {\n if (!ctx.db) return item;\n const systemTags = await ctx.db.getSystemTags();\n if (!systemTags.length || !ctx.llm) return item;\n\n const candidateList = `候选标签库(只能从中选取):\\n${systemTags.join(\", \")}\\n\\n`;\n const prompt = `${SYSTEM}\\n\\n${candidateList}文章标题:${item.title ?? \"\"}\\n文章摘要:${item.summary ?? item.content?.slice(0, 300) ?? \"(无摘要)\"}`;\n\n let suggested: string[];\n try {\n const res = await ctx.llm.chatJson(prompt, undefined, { maxTokens: 256, debugLabel: \"tagger\" });\n suggested = Array.isArray(res.tags) ? res.tags.filter((t): t is string => typeof t === \"string\") : [];\n } catch {\n return item;\n }\n\n if (!suggested.length) return item;\n\n const set = new Set(systemTags.map((t) => t.toLowerCase()));\n const confirmed = suggested.filter((t) => set.has(t.toLowerCase()));\n\n if (confirmed.length) {\n item.tags = [...new Set([...(item.tags ?? []), ...confirmed])];\n }\n\n return item;\n}\n","/**\r\n * Pipeline 步骤:LLM 翻译为中文\r\n *\r\n * 将条目的 title、summary、content 翻译为中文,写入 item.translations[\"zh-CN\"]。\r\n * 路由支持 lng=zh-CN 时可据此返回译文。\r\n *\r\n * 需要配置 OPENAI_API_KEY(或等效 LLM 环境变量)。\r\n *\r\n * 粗判已为中文的条目不调 LLM(省 token);展示时 lng=zh-CN 无译文则回退原文。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\n\r\nconst ZH_CN = \"zh-CN\";\r\nconst MAX_CONTENT_CHARS = 6000;\r\n/** 用于语言检测的正文上限(与 runTranslator 截断策略大致一致) */\r\nconst DETECT_CONTENT_PREFIX = 2000;\r\n\r\nconst SYSTEM = `你是一个专业翻译助手。将用户提供的英文(或其他语言)内容翻译为简体中文。\r\n- 保持专业、准确、流畅。\r\n- 若原文已是中文,则保持原样或轻微润色。\r\n- 只输出 JSON,格式:{\"title\": \"译文标题\", \"summary\": \"译文摘要\", \"content\": \"译文正文\"}\r\n- 若某字段为空或用户未提供,对应输出空字符串 \"\"。`;\r\n\r\nexport interface TranslatorContext {\r\n llm?: { chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>> };\r\n}\r\n\r\nfunction combinedTextForDetection(item: FeedItem): string {\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? item.content?.slice(0, 500) ?? \"\").trim();\r\n const content = (item.content ?? \"\").trim().slice(0, DETECT_CONTENT_PREFIX);\r\n return `${title}\\n${summary}\\n${content}`;\r\n}\r\n\r\n/**\r\n * 粗判主体是否已是中文(汉字占比高),用于跳过 LLM。\r\n * 含日文假名 / 韩文谚文时不视为「已是中文」,避免误跳日文/韩文条目。\r\n */\r\nexport function isLikelyChineseContent(text: string): boolean {\r\n const t = text.trim();\r\n if (t.length < 8) return false;\r\n if (/[\\u3040-\\u309f\\u30a0-\\u30ff]/.test(t)) return false;\r\n if (/[\\uac00-\\ud7af]/.test(t)) return false;\r\n\r\n const cjk = (t.match(/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/g) ?? []).length;\r\n const latin = (t.match(/[A-Za-z]/g) ?? []).length;\r\n const letterish = cjk + latin;\r\n if (letterish < 12) return false;\r\n return cjk / letterish >= 0.55;\r\n}\r\n\r\n/** 跳过已有 zh-CN 译文、无 LLM、或粗判已为中文的条目 */\r\nexport function translatorMatch(item: FeedItem, ctx: TranslatorContext): boolean {\r\n const hasZh = item.translations?.[ZH_CN];\r\n if (hasZh || !ctx.llm) return false;\r\n if (isLikelyChineseContent(combinedTextForDetection(item))) return false;\r\n return true;\r\n}\r\n\r\nexport async function runTranslator(item: FeedItem, ctx: TranslatorContext): Promise<FeedItem> {\r\n if (!ctx.llm) return item;\r\n\r\n const title = (item.title ?? \"\").trim();\r\n const summary = (item.summary ?? item.content?.slice(0, 500) ?? \"\").trim();\r\n const content = (item.content ?? \"\").trim();\r\n const contentTruncated =\r\n content.length > MAX_CONTENT_CHARS ? content.slice(0, MAX_CONTENT_CHARS) + \"\\n\\n[... 内容已截断 ...]\" : content;\r\n\r\n if (!title && !summary && !content) return item;\r\n\r\n const parts: string[] = [];\r\n if (title) parts.push(`标题:\\n${title}`);\r\n if (summary) parts.push(`摘要:\\n${summary}`);\r\n if (contentTruncated) parts.push(`正文:\\n${contentTruncated}`);\r\n\r\n const prompt = `${SYSTEM}\\n\\n请翻译以下内容:\\n\\n${parts.join(\"\\n\\n---\\n\\n\")}`;\r\n\r\n let res: Record<string, unknown>;\r\n try {\r\n res = await ctx.llm.chatJson(prompt, undefined, {\r\n maxTokens: Math.min(8192, Math.ceil((title.length + summary.length + contentTruncated.length) * 1.5)),\r\n debugLabel: \"translator\",\r\n });\r\n } catch {\r\n return item;\r\n }\r\n\r\n const tTitle = typeof res.title === \"string\" ? res.title.trim() : \"\";\r\n const tSummary = typeof res.summary === \"string\" ? res.summary.trim() : \"\";\r\n const tContent = typeof res.content === \"string\" ? res.content.trim() : \"\";\r\n\r\n if (!tTitle && !tSummary && !tContent) return item;\r\n\r\n item.translations = item.translations ?? {};\r\n item.translations[ZH_CN] = {\r\n title: tTitle || undefined,\r\n summary: tSummary || undefined,\r\n content: tContent || undefined,\r\n };\r\n\r\n return item;\r\n}\r\n","/**\r\n * Pipeline 配置:从 .rssany/config.json 的 pipeline 块读取\r\n *\r\n * 格式:{ \"pipeline\": { \"steps\": [{ \"id\": \"qualityFilter\", \"enabled\": false }, ...] } }\r\n * - steps 数组顺序即执行顺序,enabled: false 的步骤跳过\r\n */\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"../config/paths.js\";\r\n\r\nexport interface PipelineStepConfig {\r\n id: string;\r\n enabled: boolean;\r\n}\r\n\r\nexport interface PipelineConfig {\r\n steps: PipelineStepConfig[];\r\n}\r\n\r\n/** 默认配置(入库前) */\r\nexport const DEFAULT_PIPELINE_STEPS: PipelineStepConfig[] = [\r\n { id: \"qualityFilter\", enabled: false },\r\n { id: \"tagger\", enabled: false },\r\n { id: \"translator\", enabled: false },\r\n];\r\n\r\n/** 可用步骤 id */\r\nexport const PIPELINE_STEP_IDS = [\"qualityFilter\", \"tagger\", \"translator\"] as const;\r\n\r\nfunction parseSteps(rawSteps: unknown[]): PipelineStepConfig[] {\r\n const steps: PipelineStepConfig[] = [];\r\n const seen = new Set<string>();\r\n for (const s of rawSteps) {\r\n if (s && typeof s === \"object\" && typeof (s as { id?: unknown }).id === \"string\") {\r\n const obj = s as { id: string; enabled?: unknown };\r\n const id = obj.id.trim();\r\n if (!id || seen.has(id)) continue;\r\n seen.add(id);\r\n const enabled = obj.enabled;\r\n steps.push({\r\n id,\r\n enabled: enabled !== false && enabled !== 0,\r\n });\r\n }\r\n }\r\n return steps;\r\n}\r\n\r\n/** 与默认步骤表对齐:保证 JSON 里出现的步骤 id 齐全、顺序与默认一致,已有项保留 enabled */\r\nfunction mergeWithDefaultSteps(userSteps: PipelineStepConfig[]): PipelineStepConfig[] {\r\n const map = new Map(userSteps.map((s) => [s.id, s]));\r\n return DEFAULT_PIPELINE_STEPS.map((def) => {\r\n const u = map.get(def.id);\r\n return { id: def.id, enabled: u ? u.enabled : def.enabled };\r\n });\r\n}\r\n\r\n/** 读取 pipeline 配置,缺失时返回默认;已存在的 config 会与默认步骤合并,使新步骤(如 qualityFilter)始终出现在列表中 */\r\nexport async function loadPipelineConfig(): Promise<PipelineConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const parsed = JSON.parse(raw) as { pipeline?: { steps?: unknown[] } };\r\n const rawSteps = Array.isArray(parsed?.pipeline?.steps) ? parsed.pipeline.steps : [];\r\n const steps = mergeWithDefaultSteps(parseSteps(rawSteps));\r\n if (steps.length > 0) return { steps };\r\n } catch {\r\n // 文件不存在或解析失败\r\n }\r\n return { steps: [...DEFAULT_PIPELINE_STEPS] };\r\n}\r\n\r\n/** 保存 pipeline 配置到 config.json(合并其他块,不覆盖) */\r\nexport async function savePipelineConfig(config: PipelineConfig): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 文件不存在或解析失败,使用空对象\r\n }\r\n root.pipeline = { steps: config.steps };\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2), \"utf-8\");\r\n}\r\n","/**\r\n * Pipeline:入库前固定处理链(翻译、打标签等)\r\n *\r\n * 与 plugins 同级别,作为固定流程而非插件系统。\r\n * 步骤开关与排序由 .rssany/config.json 的 pipeline.steps 配置。\r\n */\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { qualityFilterMatch, runQualityFilter } from \"./qualityFilter.js\";\r\nimport { taggerMatch, runTagger } from \"./tagger.js\";\r\nimport { translatorMatch, runTranslator } from \"./translator.js\";\r\nimport { loadPipelineConfig } from \"./config.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nexport interface PipelineContext {\r\n sourceUrl?: string;\r\n llm?: {\r\n chatJson: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<Record<string, unknown>>;\r\n chatText: (prompt: string, config?: unknown, opts?: { maxTokens?: number; debugLabel?: string }) => Promise<string>;\r\n };\r\n db?: { getSystemTags: () => Promise<string[]> };\r\n}\r\n\r\n/** Pipeline 步骤注册表 */\r\nconst STEP_REGISTRY: Record<string, { match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }> = {\r\n qualityFilter: { match: qualityFilterMatch, run: runQualityFilter },\r\n tagger: { match: taggerMatch, run: runTagger },\r\n translator: { match: translatorMatch, run: runTranslator },\r\n};\r\n\r\n/** 根据配置解析出要执行的步骤(按配置顺序,仅启用且存在的) */\r\nasync function getResolvedSteps(): Promise<Array<{ id: string; match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }>> {\r\n const config = await loadPipelineConfig();\r\n const out: Array<{ id: string; match: (item: FeedItem, ctx: PipelineContext) => boolean; run: (item: FeedItem, ctx: PipelineContext) => Promise<FeedItem> }> = [];\r\n for (const { id, enabled } of config.steps) {\r\n if (!enabled) continue;\r\n const step = STEP_REGISTRY[id];\r\n if (!step) {\r\n logger.debug(\"pipeline\", \"未知步骤已跳过\", { id });\r\n continue;\r\n }\r\n out.push({ id, ...step });\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * 对单条条目执行 pipeline\r\n */\r\nexport async function runPipeline(\r\n item: FeedItem,\r\n ctx: PipelineContext\r\n): Promise<FeedItem> {\r\n const steps = await getResolvedSteps();\r\n let current = item;\r\n for (const step of steps) {\r\n if (!step.match(current, ctx)) continue;\r\n try {\r\n current = await step.run(current, ctx);\r\n } catch (err) {\r\n logger.warn(\"pipeline\", \"步骤执行失败\", {\r\n stepId: step.id,\r\n item_url: item.link,\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n }\r\n return current;\r\n}\r\n\r\n/**\r\n * 对多条条目执行 pipeline\r\n */\r\nexport async function runPipelineBatch(\r\n items: FeedItem[],\r\n ctx: PipelineContext\r\n): Promise<FeedItem[]> {\r\n const out: FeedItem[] = [];\r\n for (let i = 0; i < items.length; i++) {\r\n out.push(await runPipeline(items[i], ctx));\r\n }\r\n return out;\r\n}\r\n","// 将频道 + 条目构建为 RSS 2.0 XML\n\nimport type { RssChannel, RssEntry } from \"./types.js\";\n\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n\nfunction isoToRfc822(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toUTCString();\n}\n\n\nfunction buildItem(entry: RssEntry): string {\n const title = escapeXml(entry.title ?? \"\");\n const link = escapeXml(entry.link ?? \"\");\n const descRaw = entry.description ?? \"\";\n const desc = descRaw.includes(\"<\") || descRaw.includes(\">\")\n ? `<![CDATA[${descRaw.replace(/\\]\\]>/g, \"]]]]><![CDATA[>\")}]]>`\n : escapeXml(descRaw);\n const pubDate = entry.published ? escapeXml(isoToRfc822(entry.published)) : \"\";\n const guid = entry.guid ?? entry.link ?? \"\";\n const guidEscaped = escapeXml(guid);\n let buf = ` <item>\\n <title>${title}</title>\\n <link>${link}</link>\\n <description>${desc}</description>\\n`;\n if (pubDate) buf += ` <pubDate>${pubDate}</pubDate>\\n`;\n if (entry.imageUrl?.trim()) {\n const encUrl = escapeXml(entry.imageUrl.trim());\n const encType = escapeXml(entry.imageType?.trim() || \"image/jpeg\");\n buf += ` <enclosure url=\"${encUrl}\" length=\"0\" type=\"${encType}\"/>\\n`;\n }\n buf += ` <guid isPermaLink=\"true\">${guidEscaped}</guid>\\n`;\n buf += ` </item>\\n`;\n return buf;\n}\n\n\nexport function buildRssXml(channel: RssChannel, entries: RssEntry[]): string {\n const title = escapeXml(channel.title);\n const link = escapeXml(channel.link);\n const desc = escapeXml(channel.description ?? \"\");\n const lang = escapeXml(channel.language ?? \"zh-CN\");\n const now = new Date().toUTCString();\n const items = entries.map(buildItem).join(\"\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${link}</link>\n <description>${desc}</description>\n <language>${lang}</language>\n <lastBuildDate>${now}</lastBuildDate>\n\n${items} </channel>\n</rss>`;\n}\n","// 事件总线:进程内单例 EventEmitter,供 db 层 emit、HTTP 层 subscribe\n\nimport { EventEmitter } from \"node:events\";\n\n\n/** feed:updated 事件的载荷 */\nexport interface FeedUpdatedEvent {\n sourceUrl: string;\n newCount: number;\n}\n\n\n/** 全局单例事件总线,setMaxListeners 避免 SSE 多连接时的警告 */\nexport const eventBus = new EventEmitter();\neventBus.setMaxListeners(200);\n\n\n/** 向事件总线广播新条目事件 */\nexport function emitFeedUpdated(payload: FeedUpdatedEvent): void {\n eventBus.emit(\"feed:updated\", payload);\n}\n\n\n/** 订阅 feed:updated 事件,返回取消订阅函数 */\nexport function onFeedUpdated(fn: (e: FeedUpdatedEvent) => void): () => void {\n eventBus.on(\"feed:updated\", fn);\n return () => eventBus.off(\"feed:updated\", fn);\n}\n","// 投递目标:.rssany/config.json 的 deliver.url / deliver.token;非空 url 时在写库(及 pipeline)之后额外 POST 到该 URL\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\n\r\nexport interface DeliverConfig {\r\n url: string;\r\n /** 与下游 Gateway(如 agidaily `data/token.txt`)一致:非空时请求头带 `Authorization: Bearer <token>` */\r\n token: string;\r\n}\r\n\r\nexport async function getDeliverConfig(): Promise<DeliverConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { deliver?: { url?: string; token?: string } };\r\n const u = j?.deliver?.url;\r\n const t = j?.deliver?.token;\r\n return {\r\n url: typeof u === \"string\" ? u.trim() : \"\",\r\n token: typeof t === \"string\" ? t.trim() : \"\",\r\n };\r\n } catch {\r\n return { url: \"\", token: \"\" };\r\n }\r\n}\r\n\r\n/** 非空表示启用投递(不影响是否写库) */\r\nexport async function getDeliverUrl(): Promise<string> {\r\n return (await getDeliverConfig()).url;\r\n}\r\n\r\nexport async function saveDeliverConfig(config: DeliverConfig): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 无文件则新建\r\n }\r\n const prev = root.deliver;\r\n const base =\r\n typeof prev === \"object\" && prev !== null && !Array.isArray(prev)\r\n ? { ...(prev as Record<string, unknown>) }\r\n : {};\r\n const url = config.url.trim();\r\n const token = config.token.trim();\r\n const next: Record<string, unknown> = { ...base, url };\r\n if (token) next.token = token;\r\n else delete next.token;\r\n root.deliver = next;\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n}\r\n","// 将条目 POST 到下游:JSON 体为 { sourceRef, items }\r\n\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\n\r\nfunction feedItemsToPayload(items: FeedItem[]): unknown[] {\r\n return items.map((i) => ({\r\n guid: i.guid,\r\n title: i.title,\r\n link: i.link,\r\n pubDate: pubDateToIsoOrNull(i.pubDate) ?? new Date().toISOString(),\r\n author: i.author,\r\n summary: i.summary,\r\n content: i.content,\r\n tags: i.tags,\r\n sourceRef: i.sourceRef,\r\n translations: i.translations,\r\n }));\r\n}\r\n\r\nexport interface PostDeliverOptions {\r\n /** 非空时设置 `Authorization: Bearer <token>`(与 agidaily `POST /api/gateway/items` 一致) */\r\n bearerToken?: string;\r\n}\r\n\r\n/** POST { sourceRef, items } 到投递 URL;失败时打日志并抛出 */\r\nexport async function postDeliverItems(\r\n url: string,\r\n sourceRef: string,\r\n items: FeedItem[],\r\n options?: PostDeliverOptions,\r\n): Promise<void> {\r\n if (!url.trim() || items.length === 0) return;\r\n const body = JSON.stringify({ sourceRef, items: feedItemsToPayload(items) });\r\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\r\n const t = options?.bearerToken?.trim();\r\n if (t) headers.Authorization = `Bearer ${t}`;\r\n const res = await fetch(url.trim(), {\r\n method: \"POST\",\r\n headers,\r\n body,\r\n signal: AbortSignal.timeout(120_000),\r\n });\r\n if (!res.ok) {\r\n const text = await res.text().catch(() => \"\");\r\n throw new Error(`HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : \"\"}`);\r\n }\r\n}\r\n\r\nexport async function postDeliverItemsSafe(\r\n url: string,\r\n sourceRef: string,\r\n items: FeedItem[],\r\n options?: PostDeliverOptions,\r\n): Promise<void> {\r\n try {\r\n await postDeliverItems(url, sourceRef, items, options);\r\n } catch (err) {\r\n logger.warn(\"deliver\", \"投递失败\", {\r\n sourceRef,\r\n count: items.length,\r\n err: err instanceof Error ? err.message : String(err),\r\n });\r\n }\r\n}\r\n","// Feeder:根据 URL 生成 RSS,直接通过 Source 接口驱动,与具体信源解耦\r\n\r\nimport { cacheKey, cacheKeyFromCron } from \"../core/cacher/index.js\";\r\nimport { getSource } from \"../scraper/sources/index.js\";\r\nimport { runPipeline } from \"../pipeline/index.js\";\r\nimport { AuthRequiredError } from \"../scraper/auth/index.js\";\r\nimport { buildRssXml } from \"./rss.js\";\r\nimport type { RssChannel, RssEntry } from \"./types.js\";\r\nimport type { FeedItem } from \"../types/feedItem.js\";\r\nimport { normalizeAuthor, getEffectiveItemFields, isPipelineDroppedItem, pubDateToIsoOrNull } from \"../types/feedItem.js\";\r\nimport type { FeederConfig } from \"./types.js\";\r\nimport { buildSourceContext } from \"../scraper/sources/context.js\";\r\nimport { upsertItems, updateItemContent, getSystemTags, deleteItem } from \"../db/index.js\";\r\nimport { emitFeedUpdated } from \"../core/events/index.js\";\r\nimport { chatJson, chatText } from \"../core/llm.js\";\r\nimport type { PipelineContext } from \"../pipeline/index.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { getDeliverConfig } from \"../config/deliver.js\";\r\nimport { postDeliverItemsSafe } from \"../deliver/post.js\";\r\nimport { getEffectiveProxyForListUrl } from \"../scraper/subscription/index.js\";\r\nimport { canonicalHttpSourceRef } from \"../utils/httpSourceRef.js\";\r\n\r\n/** 主动拉取默认有头;仅显式 headless:true 时用无头;其余场景沿用 config.headless */\r\nfunction resolveHeadlessForFeeder(config: FeederConfig): boolean | undefined {\r\n if (config.force === true) {\r\n return config.headless === true ? true : false;\r\n }\r\n return config.headless;\r\n}\r\n\r\n/** 根据 listUrl + items 构建 RssChannel(与 generateAndCache 一致,用于缓存命中时实时生成 XML);lng 存在时设置 channel.language */\r\nfunction buildChannelFromItems(listUrl: string, items: FeedItem[], lng?: string | null): RssChannel {\r\n const channel: RssChannel = {\r\n title: items[0]?.author?.length ? `${items[0].author[0]} 的订阅` : \"RSS 订阅\",\r\n link: listUrl,\r\n description: `来自 ${listUrl} 的订阅`,\r\n };\r\n if (lng) channel.language = lng;\r\n return channel;\r\n}\r\n\r\n\r\n/** 根据条目生成 RssEntry:有 lng 且存在译文则用译文,否则用原文;有正文用 content,否则用 summary */\r\nfunction toRssEntry(item: FeedItem, lng?: string | null): RssEntry {\r\n const eff = getEffectiveItemFields(item, lng);\r\n const hasContent = eff.content != null && eff.content !== \"\";\r\n const desc = hasContent ? eff.content : eff.summary;\r\n return {\r\n title: eff.title,\r\n link: item.link,\r\n description: desc,\r\n guid: item.guid,\r\n published: pubDateToIsoOrNull(item.pubDate) ?? undefined,\r\n imageUrl: item.imageUrl,\r\n };\r\n}\r\n\r\n\r\n/** 同一 URL 的首次生成任务去重(仅在初始 fetch+parse 阶段有效) */\r\nconst generatingKeys = new Map<string, Promise<{ items: FeedItem[] }>>();\r\n\r\n\r\n/** Pipeline 上下文 */\r\nconst pipelineCtx: PipelineContext = {\r\n llm: { chatJson, chatText } as PipelineContext[\"llm\"],\r\n db: { getSystemTags },\r\n};\r\n\r\n/** 单条 pipeline */\r\nasync function runPipelineOnItem(item: FeedItem, ctx: { sourceUrl: string }): Promise<FeedItem> {\r\n return runPipeline(item, { ...pipelineCtx, ...ctx });\r\n}\r\n\r\n\r\n/** 执行生成流程:抓取 → 入库 → 对新条目跑 pipeline → 更新库 */\r\nasync function generateAndCache(\r\n listUrl: string,\r\n key: string,\r\n config: FeederConfig,\r\n proxy: string | undefined,\r\n): Promise<{ items: FeedItem[] }> {\r\n const { cacheDir = \"cache\" } = config;\r\n const headless = resolveHeadlessForFeeder(config);\r\n const source = getSource(listUrl);\r\n const ctx = buildSourceContext({ cacheDir, headless, proxy });\r\n let items: FeedItem[];\r\n try {\r\n items = await source.fetchItems(listUrl, ctx);\r\n } catch (err) {\r\n generatingKeys.delete(key);\r\n const message = err instanceof Error ? err.message : String(err);\r\n logger.error(\"scraper\", \"抓取失败\", { source_url: listUrl, err: message });\r\n throw err;\r\n }\r\n const sourceRefStored = canonicalHttpSourceRef(listUrl);\r\n items.forEach((i) => {\r\n i.sourceRef = sourceRefStored;\r\n i.author = normalizeAuthor(i.author);\r\n });\r\n generatingKeys.delete(key);\r\n logger.info(\"scraper\", \"抓取成功\", { source_url: listUrl, count: items.length });\r\n\r\n const { url: deliverUrl, token: deliverToken } = await getDeliverConfig();\r\n\r\n let newCount = 0;\r\n let newIds = new Set<string>();\r\n const upsertResult = await upsertItems(items).catch((err) => {\r\n logger.warn(\"db\", \"upsertItems 失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) });\r\n return { newCount: 0, newIds: new Set<string>() };\r\n });\r\n newCount = upsertResult.newCount;\r\n newIds = upsertResult.newIds;\r\n\r\n let pipelineDroppedNew = 0;\r\n const shouldRunPipelineRow = (guid: string) => newIds.has(guid);\r\n\r\n for (let i = 0; i < items.length; i++) {\r\n if (!shouldRunPipelineRow(items[i].guid)) continue;\r\n const processed = await runPipelineOnItem(items[i], { sourceUrl: sourceRefStored });\r\n items[i] = processed;\r\n if (isPipelineDroppedItem(processed)) {\r\n await deleteItem(processed.guid).catch((err) =>\r\n logger.warn(\"db\", \"质量过滤后删除条目失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) })\r\n );\r\n pipelineDroppedNew++;\r\n } else {\r\n updateItemContent(processed).catch((err) =>\r\n logger.warn(\"db\", \"updateItemContent 失败\", { source_url: listUrl, err: err instanceof Error ? err.message : String(err) })\r\n );\r\n }\r\n }\r\n if (newCount > 0) {\r\n emitFeedUpdated({ sourceUrl: sourceRefStored, newCount: newCount - pipelineDroppedNew });\r\n }\r\n const out = items.filter((i) => !isPipelineDroppedItem(i));\r\n if (deliverUrl && out.length > 0) {\r\n await postDeliverItemsSafe(deliverUrl, sourceRefStored, out, {\r\n bearerToken: deliverToken || undefined,\r\n });\r\n }\r\n return { items: out };\r\n}\r\n\r\n\r\n/** 根据 list URL 获取条目列表:按 cron 或 refresh 策略生成时间窗口 key 用于去重,每次均重新抓取 */\r\nexport async function getItems(listUrl: string, config: FeederConfig = {}): Promise<{ items: FeedItem[]; fromCache: boolean }> {\r\n const source = getSource(listUrl);\r\n const proxy = await getEffectiveProxyForListUrl(listUrl, source);\r\n const headless = resolveHeadlessForFeeder(config);\r\n const key = config.cron\r\n ? cacheKeyFromCron(listUrl, config.cron)\r\n : cacheKey(listUrl, config.refreshInterval ?? source.refreshInterval ?? \"1day\");\r\n if (source.preCheck != null) {\r\n try {\r\n await source.preCheck(\r\n buildSourceContext({\r\n cacheDir: config.cacheDir ?? \"cache\",\r\n headless,\r\n proxy,\r\n }),\r\n );\r\n } catch (err) {\r\n if (err instanceof AuthRequiredError) throw err;\r\n throw err;\r\n }\r\n }\r\n let task = config.force ? undefined : generatingKeys.get(key);\r\n if (!task) {\r\n task = generateAndCache(listUrl, key, config, proxy);\r\n if (!config.force) generatingKeys.set(key, task);\r\n }\r\n const { items } = await task;\r\n return { items, fromCache: false };\r\n}\r\n\r\n\r\n/** 将 FeedItem[] 转为 RSS 2.0 XML 字符串;可选 channelTitle/channelDesc 覆盖默认 */\r\nexport function feedItemsToRssXml(\r\n items: FeedItem[],\r\n listUrl: string,\r\n lng?: string | null,\r\n opts?: { channelTitle?: string; channelDesc?: string }\r\n): string {\r\n const channel = buildChannelFromItems(listUrl, items, lng);\r\n if (opts?.channelTitle) channel.title = opts.channelTitle;\r\n if (opts?.channelDesc) channel.description = opts.channelDesc;\r\n return buildRssXml(channel, items.map((it) => toRssEntry(it, lng)));\r\n}\r\n","// 通用调度器:cron 定时任务、重试与分组并发\r\n\r\nimport { CronExpressionParser } from \"cron-parser\";\r\nimport { schedule as cronSchedule, validate as cronValidate } from \"node-cron\";\r\n\r\n/** 校验 cron 表达式是否合法 */\r\nexport const validateCron = cronValidate;\r\n\r\n/** 调度任务:返回 Promise,失败时由调度器负责重试 */\r\nexport type ScheduledTask = () => Promise<void>;\r\n\r\n\r\n/** 调度选项 */\r\nexport interface ScheduleOptions {\r\n /** cron 表达式;不填或空表示一次性任务 */\r\n cron?: string;\r\n /** 失败时重试次数,默认 0 */\r\n retries?: number;\r\n /** 重试间隔(毫秒),默认 5000 */\r\n retryDelayMs?: number;\r\n /** 分组并发数,首次使用该分组时生效,默认 10 */\r\n concurrency?: number;\r\n /** 定时任务:注册后是否立即执行一次,默认 false */\r\n runNow?: boolean;\r\n /** 一次性任务:是否插队到队首,默认 false */\r\n priority?: boolean;\r\n /** 分组名(内部使用,由 schedule 注入) */\r\n group?: string;\r\n}\r\n\r\n\r\n/** 分组配置 */\r\nexport interface GroupConfig {\r\n /** 该组最大并发数 */\r\n concurrency: number;\r\n}\r\n\r\n\r\n/** 已注册任务 */\r\ninterface RegisteredTask {\r\n id: string;\r\n cronExpr: string;\r\n task: ScheduledTask;\r\n options: ScheduleOptions;\r\n stop: () => void;\r\n /** 上次触发时间(用于计算下次执行) */\r\n lastRunTime: number;\r\n}\r\n\r\n\r\n/** 分组队列项 */\r\ninterface QueuedItem {\r\n id: string;\r\n task: ScheduledTask | (() => Promise<unknown>);\r\n options: ScheduleOptions;\r\n resolve?: () => void;\r\n resolveValue?: (value: unknown) => void;\r\n rejectValue?: (err: unknown) => void;\r\n}\r\n\r\n\r\nconst tasks = new Map<string, RegisteredTask>();\r\nconst groups = new Map<string, { config: GroupConfig; running: number; queue: QueuedItem[]; completedCount: number }>();\r\nconst DEFAULT_RETRY_DELAY_MS = 5000;\r\nconst DEFAULT_GROUP_CONCURRENCY = 10;\r\n\r\n\r\nasync function runWithRetry(\r\n task: ScheduledTask,\r\n options: ScheduleOptions\r\n): Promise<void> {\r\n const retries = options.retries ?? 0;\r\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= retries; attempt++) {\r\n try {\r\n await task();\r\n return;\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < retries) {\r\n await new Promise((r) => setTimeout(r, retryDelayMs));\r\n }\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\nasync function runWithRetryAndResult<T>(\r\n task: () => Promise<T>,\r\n options: ScheduleOptions\r\n): Promise<T> {\r\n const retries = options.retries ?? 0;\r\n const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt <= retries; attempt++) {\r\n try {\r\n return await task();\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < retries) {\r\n await new Promise((r) => setTimeout(r, retryDelayMs));\r\n }\r\n }\r\n }\r\n throw lastErr;\r\n}\r\n\r\n\r\nfunction ensureGroup(group: string, concurrency: number): void {\r\n if (!groups.has(group)) {\r\n groups.set(group, { config: { concurrency }, running: 0, queue: [], completedCount: 0 });\r\n } else {\r\n const g = groups.get(group)!;\r\n g.config.concurrency = concurrency;\r\n if (g.completedCount === undefined) g.completedCount = 0;\r\n }\r\n}\r\n\r\n\r\nfunction enqueueAndProcess(\r\n group: string,\r\n id: string,\r\n task: ScheduledTask | (() => Promise<unknown>),\r\n options: ScheduleOptions,\r\n resolve?: () => void,\r\n priority?: boolean,\r\n resolveValue?: (value: unknown) => void,\r\n rejectValue?: (err: unknown) => void\r\n): void {\r\n const g = groups.get(group);\r\n if (!g) return;\r\n g.queue = g.queue.filter((it) => it.id !== id);\r\n const item: QueuedItem = { id, task, options, resolve, resolveValue, rejectValue };\r\n if (priority) {\r\n g.queue.unshift(item);\r\n } else {\r\n g.queue.push(item);\r\n }\r\n processGroupQueue(group);\r\n}\r\n\r\n\r\nfunction processGroupQueue(group: string): void {\r\n const g = groups.get(group);\r\n if (!g || g.running >= g.config.concurrency || g.queue.length === 0) return;\r\n const item = g.queue.shift()!;\r\n g.running += 1;\r\n const done = () => {\r\n g.running -= 1;\r\n g.completedCount = (g.completedCount ?? 0) + 1;\r\n processGroupQueue(group);\r\n };\r\n if (item.resolveValue != null || item.rejectValue != null) {\r\n runWithRetryAndResult(item.task as () => Promise<unknown>, item.options)\r\n .then((result) => {\r\n item.resolveValue?.(result);\r\n })\r\n .catch((err) => {\r\n item.rejectValue?.(err);\r\n })\r\n .finally(done);\r\n } else {\r\n runWithRetry(item.task as ScheduledTask, item.options)\r\n .catch(() => {})\r\n .finally(() => {\r\n item.resolve?.();\r\n done();\r\n });\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 调度任务:options.cron 有值时注册定时任务,否则一次性入队\r\n * @param group 分组名\r\n * @param id 任务唯一标识\r\n * @param task 异步任务函数\r\n * @param options cron 有值=定时任务,无值=一次性任务;可选 retries、retryDelayMs、concurrency、runNow、priority\r\n */\r\nexport function schedule(group: string, id: string, task: ScheduledTask, options: ScheduleOptions & { cron: string }): boolean;\r\nexport function schedule<T>(group: string, id: string, task: () => Promise<T>, options?: ScheduleOptions): Promise<T>;\r\nexport function schedule<T>(\r\n group: string,\r\n id: string,\r\n task: ScheduledTask | (() => Promise<T>),\r\n options: ScheduleOptions = {}\r\n): boolean | Promise<T> {\r\n const cronExpr = options.cron?.trim();\r\n ensureGroup(group, options.concurrency ?? groups.get(group)?.config.concurrency ?? DEFAULT_GROUP_CONCURRENCY);\r\n\r\n if (cronExpr && cronValidate(cronExpr)) {\r\n unschedule(id);\r\n const optsWithGroup = { ...options, group };\r\n const job = cronSchedule(cronExpr, () => {\r\n const reg = tasks.get(id);\r\n if (reg) reg.lastRunTime = Date.now();\r\n enqueueAndProcess(group, id, task as ScheduledTask, optsWithGroup);\r\n });\r\n tasks.set(id, {\r\n id,\r\n cronExpr,\r\n task: task as ScheduledTask,\r\n options: optsWithGroup,\r\n stop: () => job.stop(),\r\n lastRunTime: 0,\r\n });\r\n if (options.runNow) {\r\n runNow(id, true).catch(() => {});\r\n }\r\n return true;\r\n }\r\n\r\n return new Promise<T>((resolve, reject) => {\r\n enqueueAndProcess(\r\n group,\r\n id,\r\n task as () => Promise<unknown>,\r\n { ...options, group },\r\n undefined,\r\n options.priority ?? false,\r\n resolve as (v: unknown) => void,\r\n reject\r\n );\r\n });\r\n}\r\n\r\n\r\n/**\r\n * 取消任务\r\n */\r\nexport function unschedule(id: string): void {\r\n const reg = tasks.get(id);\r\n if (reg) {\r\n reg.stop();\r\n tasks.delete(id);\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 取消指定分组下的所有定时任务(不清理队列,用于 reschedule 前仅移除本组任务)\r\n */\r\nexport function unscheduleGroup(group: string): void {\r\n const ids = [...tasks.entries()].filter(([, reg]) => reg.options.group === group).map(([id]) => id);\r\n for (const id of ids) unschedule(id);\r\n}\r\n\r\n\r\n/**\r\n * 立即执行一次任务(不等待下次定时)\r\n * @param id 任务 id\r\n * @param priority 有分组时,true 表示插入队首优先执行,默认 false 插入队尾\r\n */\r\nexport function runNow(id: string, priority = false): Promise<void> {\r\n const reg = tasks.get(id);\r\n if (!reg) return Promise.resolve();\r\n reg.lastRunTime = Date.now();\r\n const group = reg.options.group;\r\n if (group) {\r\n return new Promise<void>((resolve) => {\r\n enqueueAndProcess(group, id, reg.task, reg.options, resolve, priority);\r\n });\r\n }\r\n return runWithRetry(reg.task, reg.options);\r\n}\r\n\r\n\r\n/**\r\n * 清空所有任务(含各分组队列中的待执行项)\r\n */\r\nexport function clearAll(): void {\r\n for (const [, reg] of tasks) {\r\n reg.stop();\r\n }\r\n tasks.clear();\r\n for (const g of groups.values()) {\r\n g.queue.length = 0;\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 获取已注册任务 id 列表\r\n */\r\nexport function getTaskIds(): string[] {\r\n return [...tasks.keys()];\r\n}\r\n\r\n\r\n/** 分组统计 */\r\nexport interface GroupStats {\r\n /** 正在执行的任务数 */\r\n running: number;\r\n /** 队列中等待的任务数 */\r\n queued: number;\r\n /** 该组最大并发数 */\r\n concurrency: number;\r\n /** 该组下已注册的定时任务数量 */\r\n scheduledCount: number;\r\n /** 已完成任务数(含定时任务每次执行 + 单次任务,进程启动后累计) */\r\n completedCount: number;\r\n /**\r\n * 下次任务时间(毫秒时间戳)\r\n * - 0:正在执行\r\n * - -1:无定时任务\r\n * - 其他:下次预计执行时间戳\r\n */\r\n nextRunTime: number;\r\n}\r\n\r\n\r\nfunction getNextRunForTask(reg: RegisteredTask): number {\r\n try {\r\n const base = reg.lastRunTime > 0 ? new Date(reg.lastRunTime) : new Date();\r\n const expr = CronExpressionParser.parse(reg.cronExpr, { currentDate: base });\r\n let next = expr.next().getTime();\r\n if (next <= Date.now()) {\r\n const retry = CronExpressionParser.parse(reg.cronExpr, { currentDate: new Date() });\r\n next = retry.next().getTime();\r\n }\r\n return next;\r\n } catch {\r\n return -1;\r\n }\r\n}\r\n\r\n\r\n/**\r\n * 获取各分组的执行统计,用于管理页进度条等\r\n */\r\nexport function getGroupStats(): Record<string, GroupStats> {\r\n const result: Record<string, GroupStats> = {};\r\n for (const [name, g] of groups) {\r\n const groupTasks = [...tasks.values()].filter((t) => t.options.group === name);\r\n const scheduledCount = groupTasks.length;\r\n let nextRunTime: number;\r\n if (g.running > 0) {\r\n nextRunTime = 0;\r\n } else if (scheduledCount === 0) {\r\n nextRunTime = -1;\r\n } else {\r\n const times = groupTasks.map(getNextRunForTask).filter((t) => t > 0);\r\n nextRunTime = times.length > 0 ? Math.min(...times) : -1;\r\n }\r\n result[name] = {\r\n running: g.running,\r\n queued: g.queue.length,\r\n concurrency: g.config.concurrency,\r\n scheduledCount,\r\n completedCount: g.completedCount ?? 0,\r\n nextRunTime,\r\n };\r\n }\r\n return result;\r\n}\r\n\r\n\r\n","// 信源调度:根据 sources.json 中的信源 refresh 定时触发 getItems,使用通用调度器\r\n\r\nimport { watch } from \"node:fs\";\r\nimport { getAllSources } from \"../subscription/index.js\";\r\nimport { resolveRef } from \"../subscription/types.js\";\r\nimport { getItems } from \"../../feeder/index.js\";\r\nimport { SOURCES_CONFIG_PATH } from \"../../config/paths.js\";\r\nimport type { RefreshInterval } from \"../../utils/refreshInterval.js\";\r\nimport { refreshIntervalToCron } from \"../../utils/refreshInterval.js\";\r\nimport * as scheduler from \"../../scheduler/index.js\";\r\n\r\nconst DEFAULT_REFRESH: RefreshInterval = \"1day\";\r\nconst SOURCES_CONCURRENCY = 1;\r\n\r\nfunction createPullTask(ref: string, cacheDir: string, cronExpr: string): scheduler.ScheduledTask {\r\n return async () => {\r\n try {\r\n await getItems(ref, {\r\n cacheDir,\r\n cron: cronExpr,\r\n });\r\n } catch (err) {\r\n throw err;\r\n }\r\n };\r\n}\r\n\r\nexport const SOURCES_GROUP = \"sources\";\r\n\r\nasync function rescheduleSources(cacheDir: string, runNow: boolean): Promise<void> {\r\n scheduler.unscheduleGroup(SOURCES_GROUP);\r\n let sources: Awaited<ReturnType<typeof getAllSources>>;\r\n try {\r\n sources = await getAllSources();\r\n } catch {\r\n sources = [];\r\n }\r\n\r\n for (const src of sources) {\r\n const ref = resolveRef(src);\r\n if (!ref) continue;\r\n const cronExpr: string = src.cron\r\n ? src.cron\r\n : refreshIntervalToCron(src.refresh ?? DEFAULT_REFRESH);\r\n if (!scheduler.validateCron(cronExpr)) continue;\r\n scheduler.schedule(SOURCES_GROUP, ref, createPullTask(ref, cacheDir, cronExpr), {\r\n cron: cronExpr,\r\n retries: 2,\r\n retryDelayMs: 5000,\r\n concurrency: SOURCES_CONCURRENCY,\r\n runNow,\r\n });\r\n }\r\n}\r\n\r\nexport async function initScheduler(cacheDir: string): Promise<void> {\r\n await rescheduleSources(cacheDir, false);\r\n let debounceTimer: NodeJS.Timeout | null = null;\r\n try {\r\n const watcher = watch(SOURCES_CONFIG_PATH, () => {\r\n if (debounceTimer) clearTimeout(debounceTimer);\r\n debounceTimer = setTimeout(() => {\r\n rescheduleSources(cacheDir, false).catch(() => {});\r\n }, 500);\r\n });\r\n watcher.on(\"error\", () => {});\r\n } catch {\r\n /* sources.json 尚不存在 */\r\n }\r\n}\r\n","// 本地运行:管理 API 不做令牌校验(原 admin-token 逻辑已移除)\r\n\r\nimport type { Context, Next } from \"hono\";\r\n\r\n/** 占位中间件,与既有路由注册兼容,始终放行 */\r\nexport function requireAdmin() {\r\n return async (_c: Context, next: Next): Promise<Response | void> => {\r\n return next();\r\n };\r\n}\r\n","// /api/server-info\r\n\r\nimport { networkInterfaces } from \"node:os\";\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nconst PORT = Number(process.env.PORT) || 18473;\r\n\r\nexport function registerServerRoutes(app: Hono): void {\r\n app.get(\"/api/server-info\", requireAdmin(), (c) => {\r\n const lanIp = Object.values(networkInterfaces())\r\n .flat()\r\n .find((iface) => iface?.family === \"IPv4\" && !iface.internal)?.address;\r\n const lanUrl = lanIp ? `http://${lanIp}:${PORT}` : null;\r\n return c.json({ port: PORT, lanUrl });\r\n });\r\n}\r\n","// /api/rss(JSON,按 URL 抓取返回条目列表)\n\nimport { createHash } from \"node:crypto\";\nimport type { Hono } from \"hono\";\nimport { getItems } from \"../../../feeder/index.js\";\nimport { SOURCES_GROUP } from \"../../../scraper/scheduler/index.js\";\nimport * as scheduler from \"../../../scheduler/index.js\";\nimport { CACHE_DIR } from \"../../../config/paths.js\";\nimport { AuthRequiredError, NotFoundError } from \"../../../scraper/auth/index.js\";\nimport { getEffectiveItemFields, pubDateToIsoOrNull } from \"../../../types/feedItem.js\";\n\nexport function registerRssApiRoutes(app: Hono): void {\n app.get(\"/api/rss\", async (c) => {\n const url = c.req.query(\"url\");\n if (!url) return c.json({ error: \"url 参数缺失\" }, 400);\n const headlessParam = c.req.query(\"headless\");\n const headless = headlessParam === \"false\" || headlessParam === \"0\" ? false : undefined;\n const lng = c.req.query(\"lng\") ?? undefined;\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 20), 200);\n const offset = Number(c.req.query(\"offset\") ?? 0);\n try {\n const httpId = \"http-\" + createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\n const { items: allItems, fromCache } = await scheduler.schedule(\n SOURCES_GROUP,\n httpId,\n () => getItems(url, { cacheDir: CACHE_DIR, headless, lng })\n );\n const total = allItems.length;\n const pageItems = allItems.slice(offset, offset + limit);\n return c.json({\n fromCache,\n total,\n hasMore: offset + pageItems.length < total,\n items: pageItems.map((item) => {\n const { title, summary } = lng ? getEffectiveItemFields(item, lng) : { title: item.title, summary: item.summary ?? \"\" };\n return {\n guid: item.guid,\n title,\n link: item.link,\n summary,\n author: item.author,\n pubDate: pubDateToIsoOrNull(item.pubDate),\n };\n }),\n });\n } catch (err) {\n if (err instanceof AuthRequiredError) return c.json({ error: \"需要登录\", code: \"AUTH_REQUIRED\" }, 401);\n if (err instanceof NotFoundError) return c.json({ error: err.message, code: \"NOT_FOUND\" }, 404);\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);\n }\n });\n}\n","// /api/scheduler/stats\r\n\r\nimport type { Hono } from \"hono\";\r\nimport * as scheduler from \"../../../scheduler/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerSchedulerRoutes(app: Hono): void {\r\n app.get(\"/api/scheduler/stats\", requireAdmin(), (c) => {\r\n const stats = scheduler.getGroupStats();\r\n return c.json(stats);\r\n });\r\n}\r\n","// /api/plugins、POST /api/plugins(从模板新建)、/api/plugins/:id(读写字节码插件源文件)\r\n\r\nimport { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\r\nimport { join, resolve, sep } from \"node:path\";\r\nimport type { Hono } from \"hono\";\r\nimport { getPluginSites } from \"../../../scraper/sources/web/index.js\";\r\nimport { registeredSources } from \"../../../scraper/sources/index.js\";\r\nimport { getPluginFilePath } from \"../../../plugins/loader.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { initSources } from \"../../../scraper/sources/index.js\";\r\nimport { BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR, PLUGIN_SITE_TEMPLATE_PATH } from \"../../../config/paths.js\";\r\n\r\nconst SITE_TEMPLATE_FALLBACK = `/**\r\n * Site 插件模板(由 /plugins 页添加,位于 .rssany/plugins/)\r\n * HTML DOM 解析请用 ctx.deps.parseHtml,勿在插件内 import node_modules。\r\n */\r\nexport default {\r\n id: \"__PLUGIN_ID__\",\r\n listUrlPattern: __LIST_URL_PATTERN__,\r\n refreshInterval: \"1day\",\r\n\r\n async fetchItems(sourceId, ctx) {\r\n const { html, finalUrl } = await ctx.fetchHtml(sourceId, {\r\n waitMs: 2000,\r\n purify: true,\r\n });\r\n void ctx.deps.parseHtml(html);\r\n void finalUrl;\r\n return [];\r\n },\r\n};\r\n`;\r\n\r\nfunction isValidNewPluginId(id: string): boolean {\r\n return /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) && id !== \"generic\" && id !== \"new\";\r\n}\r\n\r\n/** 与模板中 `listUrlPattern: __LIST_URL_PATTERN__` 注入一致:非空、无换行、长度上限 */\r\nfunction isValidNewListUrlPattern(pattern: string): boolean {\r\n if (pattern.length === 0 || pattern.length > 2048) return false;\r\n if (/[\\r\\n]/.test(pattern)) return false;\r\n return true;\r\n}\r\n\r\nasync function fileExists(p: string): Promise<boolean> {\r\n try {\r\n await access(p);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nfunction isAllowedPluginPath(absPath: string): boolean {\r\n const f = resolve(absPath);\r\n for (const root of [BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR]) {\r\n const r = resolve(root);\r\n if (f === r || f.startsWith(r + sep)) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport function registerPluginsRoutes(app: Hono): void {\r\n /** 从模板在 .rssany/plugins/{id}.rssany.js 新建 Site 插件 */\r\n app.post(\"/api/plugins\", requireAdmin(), async (c) => {\r\n let body: { id?: string; listUrlPattern?: string };\r\n try {\r\n body = await c.req.json();\r\n } catch {\r\n return c.json({ error: \"无效 JSON\" }, 400);\r\n }\r\n const id = typeof body.id === \"string\" ? body.id.trim() : \"\";\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n if (!isValidNewPluginId(id)) {\r\n return c.json({ error: \"id 须为字母开头,仅含字母数字、下划线、连字符;不能为 generic 或 new\" }, 400);\r\n }\r\n const listUrlPatternRaw = typeof body.listUrlPattern === \"string\" ? body.listUrlPattern.trim() : \"\";\r\n if (!listUrlPatternRaw) {\r\n return c.json({ error: \"缺少支持的站点(listUrlPattern),例如 https://example.com/*\" }, 400);\r\n }\r\n if (!isValidNewListUrlPattern(listUrlPatternRaw)) {\r\n return c.json({ error: \"支持的站点须为非空字符串,不超过 2048 字符,且不能含换行\" }, 400);\r\n }\r\n await mkdir(USER_PLUGINS_DIR, { recursive: true });\r\n const outPath = join(USER_PLUGINS_DIR, `${id}.rssany.js`);\r\n if (await fileExists(outPath)) return c.json({ error: \"该 id 已存在同名文件\" }, 409);\r\n let tpl = SITE_TEMPLATE_FALLBACK;\r\n try {\r\n tpl = await readFile(PLUGIN_SITE_TEMPLATE_PATH, \"utf-8\");\r\n } catch {\r\n // 使用内置模板\r\n }\r\n const patternLiteral = JSON.stringify(listUrlPatternRaw);\r\n const content = tpl.replace(/__PLUGIN_ID__/g, id).replace(/__LIST_URL_PATTERN__/g, patternLiteral);\r\n if (!isAllowedPluginPath(outPath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n await writeFile(outPath, content, \"utf-8\");\r\n await initSources();\r\n return c.json({ ok: true, filePath: outPath, id });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n\r\n app.get(\"/api/plugins\", requireAdmin(), (c) => {\r\n const sites = getPluginSites().map((s) => ({\r\n kind: \"site\" as const,\r\n id: s.id,\r\n listUrlPattern: typeof s.listUrlPattern === \"string\" ? s.listUrlPattern : String(s.listUrlPattern),\r\n hasAuth: !!(s.checkAuth && s.loginUrl),\r\n }));\r\n const siteIds = new Set(sites.map((p) => p.id));\r\n const sources = registeredSources\r\n .filter((src) => src.id !== \"generic\" && !siteIds.has(src.id))\r\n .map((src) => ({\r\n kind: \"source\" as const,\r\n id: src.id,\r\n listUrlPattern: typeof src.pattern === \"string\" ? src.pattern : String(src.pattern),\r\n hasAuth: false,\r\n }));\r\n return c.json([...sites, ...sources]);\r\n });\r\n\r\n app.get(\"/api/plugins/:id\", requireAdmin(), async (c) => {\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n const filePath = getPluginFilePath(id);\r\n if (!filePath) return c.json({ error: \"未找到该插件或无可编辑文件\" }, 404);\r\n if (!isAllowedPluginPath(filePath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n const content = await readFile(filePath, \"utf-8\");\r\n return c.json({ id, filePath, content });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n\r\n app.put(\"/api/plugins/:id\", requireAdmin(), async (c) => {\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n if (!id) return c.json({ error: \"缺少 id\" }, 400);\r\n let body: { content?: string };\r\n try {\r\n body = await c.req.json();\r\n } catch {\r\n return c.json({ error: \"无效 JSON\" }, 400);\r\n }\r\n if (typeof body.content !== \"string\") return c.json({ error: \"需要 content 字符串\" }, 400);\r\n const filePath = getPluginFilePath(id);\r\n if (!filePath) return c.json({ error: \"未找到该插件\" }, 404);\r\n if (!isAllowedPluginPath(filePath)) return c.json({ error: \"路径不允许\" }, 403);\r\n try {\r\n await writeFile(filePath, body.content, \"utf-8\");\r\n await initSources();\r\n return c.json({ ok: true });\r\n } catch (e) {\r\n return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);\r\n }\r\n });\r\n}\r\n","// /api/pipeline(步骤开关与排序)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport {\r\n loadPipelineConfig,\r\n savePipelineConfig,\r\n DEFAULT_PIPELINE_STEPS,\r\n PIPELINE_STEP_IDS,\r\n} from \"../../../pipeline/config.js\";\r\n\r\ntype StepInput = { id: string; enabled?: boolean };\r\n\r\nfunction parseSteps(rawSteps: unknown[]): Array<{ id: string; enabled: boolean }> {\r\n return rawSteps\r\n .filter((s) => s && typeof s === \"object\" && typeof (s as StepInput).id === \"string\")\r\n .map((s) => {\r\n const obj = s as StepInput;\r\n const e: unknown = obj.enabled;\r\n return {\r\n id: String(obj.id).trim(),\r\n enabled: e !== false && e !== 0,\r\n };\r\n })\r\n .filter((s) => s.id.length > 0);\r\n}\r\n\r\nfunction dedupeSteps<T extends { id: string }>(steps: T[]): T[] {\r\n const seen = new Set<string>();\r\n const out: T[] = [];\r\n for (const s of steps) {\r\n if (seen.has(s.id)) continue;\r\n seen.add(s.id);\r\n out.push(s);\r\n }\r\n return out;\r\n}\r\n\r\nexport function registerPipelineRoutes(app: Hono): void {\r\n app.get(\"/api/pipeline\", requireAdmin(), async (c) => {\r\n const config = await loadPipelineConfig();\r\n return c.json({\r\n steps: config.steps,\r\n availableIds: [...PIPELINE_STEP_IDS],\r\n defaults: DEFAULT_PIPELINE_STEPS,\r\n });\r\n });\r\n\r\n app.put(\"/api/pipeline\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ steps?: unknown[] }>();\r\n const rawSteps = Array.isArray(body?.steps) ? body.steps : [];\r\n const steps = dedupeSteps(parseSteps(rawSteps));\r\n await savePipelineConfig({ steps });\r\n return c.json({ ok: true, steps });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/feed、/api/events\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { streamSSE } from \"hono/streaming\";\r\nimport { onFeedUpdated } from \"../../../core/events/index.js\";\r\nimport { getAllSources, getAllSubscriptionRefs, resolveRef } from \"../../../scraper/subscription/index.js\";\r\nimport { getEffectiveItemFields, type ItemTranslationFields } from \"../../../types/feedItem.js\";\r\nimport { queryFeedItems } from \"../../../db/index.js\";\r\n\r\nexport function registerFeedRoutes(app: Hono): void {\r\n app.get(\"/api/feed\", async (c) => {\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 50), 200);\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n const refFilter = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? undefined;\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n const since = c.req.query(\"since\");\r\n const until = c.req.query(\"until\");\r\n\r\n const sources = await getAllSources();\r\n const refSet = new Set(sources.map((s) => resolveRef(s)).filter(Boolean));\r\n let sourceRefs: string[];\r\n if (refFilter) {\r\n sourceRefs = refSet.has(refFilter) ? [refFilter] : [];\r\n } else {\r\n sourceRefs = await getAllSubscriptionRefs();\r\n }\r\n\r\n const labelByRef = new Map(sources.map((s) => [resolveRef(s), s.label ?? resolveRef(s)] as const));\r\n const sourcesMeta = sources.map((s) => ({\r\n ref: resolveRef(s),\r\n label: s.label ?? resolveRef(s),\r\n }));\r\n\r\n const dateOpts = since || until ? { since: since ?? undefined, until: until ?? undefined } : undefined;\r\n const { items: dbItems, hasMore } = await queryFeedItems(sourceRefs, limit, offset, dateOpts);\r\n const items = dbItems.map((item) => {\r\n const refKey = item.source_url ?? \"\";\r\n const base = {\r\n ...item,\r\n sub_id: refKey,\r\n sub_title: labelByRef.get(refKey) ?? \"\",\r\n };\r\n if (!lng) return base;\r\n const view = {\r\n title: item.title ?? \"\",\r\n summary: item.summary ?? \"\",\r\n content: item.content ?? \"\",\r\n translations: (item as { translations?: Record<string, ItemTranslationFields> }).translations,\r\n };\r\n const eff = getEffectiveItemFields(view, lng);\r\n return { ...base, title: eff.title, summary: eff.summary, content: eff.content };\r\n });\r\n return c.json({ sources: sourcesMeta, items, hasMore });\r\n });\r\n\r\n app.get(\"/api/events\", (c) => {\r\n return streamSSE(c, async (stream) => {\r\n await stream.writeSSE({ data: JSON.stringify({ type: \"connected\" }) });\r\n const off = onFeedUpdated((e) => {\r\n stream.writeSSE({ data: JSON.stringify({ type: \"feed:updated\", sourceUrl: e.sourceUrl, newCount: e.newCount }) }).catch(() => {});\r\n });\r\n const heartbeat = setInterval(() => {\r\n stream.writeSSE({ data: \"\", event: \"ping\" }).catch(() => {});\r\n }, 25000);\r\n stream.onAbort(() => {\r\n off();\r\n clearInterval(heartbeat);\r\n });\r\n await new Promise<void>((resolve) => stream.onAbort(resolve));\r\n });\r\n });\r\n}\r\n","// /api/items、/api/items/pending-push、/api/items/mark-pushed、/api/items/by-source(须在 :id 之前注册)、/api/items/:id\r\n\r\n// /api/user/items(JWT 认证用户)\r\n\r\n\r\n\r\nimport type { Hono } from \"hono\";\r\n\r\nimport { getAllSubscriptionRefs } from \"../../../scraper/subscription/index.js\";\r\n\r\nimport { getEffectiveItemFields, type ItemTranslationFields } from \"../../../types/feedItem.js\";\r\n\r\nimport { queryItems, getPendingPushItems, markPushed, deleteItem, deleteItemsBySourceUrl } from \"../../../db/index.js\";\r\n\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\n\r\n\r\nfunction parseSubscribedFlag(v: string | undefined): boolean {\r\n\r\n return v === \"1\" || v === \"true\" || v === \"yes\";\r\n\r\n}\r\n\r\n\r\n\r\nexport function registerItemsRoutes(app: Hono): void {\r\n\r\n app.get(\"/api/items/pending-push\", async (c) => {\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 100), 500);\r\n\r\n const items = await getPendingPushItems(limit);\r\n\r\n return c.json({ items, count: items.length });\r\n\r\n });\r\n\r\n\r\n\r\n app.post(\"/api/items/mark-pushed\", async (c) => {\r\n\r\n try {\r\n\r\n const { ids } = await c.req.json<{ ids?: string[] }>();\r\n\r\n if (!Array.isArray(ids) || ids.length === 0) return c.json({ ok: false, message: \"ids 不能为空\" }, 400);\r\n\r\n await markPushed(ids);\r\n\r\n return c.json({ ok: true, count: ids.length });\r\n\r\n } catch (err) {\r\n\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n\r\n }\r\n\r\n });\r\n\r\n\r\n\r\n /** 清空指定信源(source_url)下所有已入库条目 — 必须早于 /api/items/:id,否则 \"by-source\" 会被当成 id */\r\n\r\n app.delete(\"/api/items/by-source\", requireAdmin(), async (c) => {\r\n\r\n const sourceUrl = (c.req.query(\"source_url\") ?? \"\").trim();\r\n\r\n if (!sourceUrl) return c.json({ ok: false, message: \"source_url 不能为空\" }, 400);\r\n\r\n const deleted = await deleteItemsBySourceUrl(sourceUrl);\r\n\r\n return c.json({ ok: true, deleted });\r\n\r\n });\r\n\r\n\r\n\r\n app.delete(\"/api/items/:id\", async (c) => {\r\n\r\n const id = decodeURIComponent(c.req.param(\"id\") ?? \"\").trim();\r\n\r\n if (!id) return c.json({ ok: false, message: \"id 不能为空\" }, 400);\r\n\r\n const deleted = await deleteItem(id);\r\n\r\n if (!deleted) return c.json({ ok: false, message: \"条目不存在或已删除\" }, 404);\r\n\r\n return c.json({ ok: true });\r\n\r\n });\r\n\r\n\r\n\r\n app.get(\"/api/items\", async (c) => {\r\n\r\n const ref = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? undefined;\r\n\r\n const subscribed = parseSubscribedFlag(c.req.query(\"subscribed\"));\r\n\r\n const author = c.req.query(\"author\") ?? undefined;\r\n\r\n const q = c.req.query(\"q\") ?? undefined;\r\n\r\n const tagsParam = c.req.query(\"tags\") ?? undefined;\r\n\r\n const tags = tagsParam ? tagsParam.split(\",\").map((t) => t.trim()).filter(Boolean) : undefined;\r\n\r\n // days=0 或不传 days 表示不按天数筛选;days=N 表示最近 N 天;since/until 仍可单独传\r\n\r\n const daysParam = c.req.query(\"days\");\r\n\r\n const sinceParam = c.req.query(\"since\") ?? undefined;\r\n\r\n const untilParam = c.req.query(\"until\") ?? undefined;\r\n\r\n let since: Date | undefined;\r\n\r\n let until: Date | undefined;\r\n\r\n const daysNum = daysParam !== undefined && daysParam !== \"\" ? Number(daysParam) : NaN;\r\n\r\n if (Number.isFinite(daysNum) && daysNum > 0) {\r\n\r\n const n = Math.max(1, Math.min(365, Math.floor(daysNum)));\r\n\r\n const now = new Date();\r\n\r\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\r\n\r\n const todayEnd = new Date(todayStart);\r\n\r\n todayEnd.setDate(todayEnd.getDate() + 1);\r\n\r\n since = new Date(todayStart);\r\n\r\n since.setDate(since.getDate() - (n - 1));\r\n\r\n until = todayEnd;\r\n\r\n } else {\r\n\r\n since = sinceParam ? new Date(sinceParam) : undefined;\r\n\r\n if (untilParam) {\r\n\r\n if (untilParam.length === 10) {\r\n\r\n const d = new Date(untilParam + \"T12:00:00Z\");\r\n\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n\r\n until = d;\r\n\r\n } else {\r\n\r\n until = new Date(untilParam);\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 200), 500);\r\n\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n\r\n\r\n let sourceUrls: string[] | undefined;\r\n\r\n let effectiveSourceUrl: string | undefined;\r\n\r\n if (ref) {\r\n\r\n effectiveSourceUrl = ref;\r\n\r\n sourceUrls = undefined;\r\n\r\n } else if (subscribed) {\r\n\r\n const refs = await getAllSubscriptionRefs();\r\n\r\n sourceUrls = refs.length > 0 ? refs : [];\r\n\r\n }\r\n\r\n\r\n\r\n if (!effectiveSourceUrl && sourceUrls?.length === 0) {\r\n\r\n return c.json({ items: [], total: 0, hasMore: false });\r\n\r\n }\r\n\r\n\r\n\r\n const result = await queryItems({\r\n\r\n sourceUrl: effectiveSourceUrl ?? (sourceUrls ? undefined : ref),\r\n\r\n sourceUrls,\r\n\r\n author,\r\n\r\n q,\r\n\r\n tags,\r\n\r\n since,\r\n\r\n until,\r\n\r\n limit,\r\n\r\n offset,\r\n\r\n });\r\n\r\n const items =\r\n\r\n lng && result.items.length > 0\r\n\r\n ? result.items.map((it) => {\r\n\r\n const view = {\r\n\r\n title: it.title ?? \"\",\r\n\r\n summary: it.summary ?? \"\",\r\n\r\n content: it.content ?? \"\",\r\n\r\n translations: (it as { translations?: Record<string, ItemTranslationFields> }).translations,\r\n\r\n };\r\n\r\n const eff = getEffectiveItemFields(view, lng);\r\n\r\n return { ...it, title: eff.title, summary: eff.summary, content: eff.content };\r\n\r\n })\r\n\r\n : result.items;\r\n\r\n const hasMore = offset + items.length < result.total;\r\n\r\n return c.json({ items, total: result.total, hasMore });\r\n\r\n });\r\n\r\n}\r\n\r\n","// /api/logs\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { clearAllLogs, queryLogs } from \"../../../db/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerLogsRoutes(app: Hono): void {\r\n app.delete(\"/api/logs\", requireAdmin(), async (c) => {\r\n const deleted = await clearAllLogs();\r\n return c.json({ ok: true, deleted });\r\n });\r\n\r\n app.get(\"/api/logs\", requireAdmin(), async (c) => {\r\n const levelParam = c.req.query(\"level\");\r\n const level = levelParam === \"error\" || levelParam === \"warn\" || levelParam === \"info\" || levelParam === \"debug\" ? levelParam : undefined;\r\n const categoryRaw = c.req.query(\"category\");\r\n const category = typeof categoryRaw === \"string\" && categoryRaw.trim() ? categoryRaw.trim() : undefined;\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 100), 200);\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n const sinceParam = c.req.query(\"since\");\r\n const since = sinceParam ? new Date(sinceParam) : undefined;\r\n const result = await queryLogs({ level, category, limit, offset, since });\r\n return c.json(result);\r\n });\r\n}\r\n","// /api/admin/verify, /api/admin/integrity-check\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { runIntegrityCheck } from \"../../../db/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerAdminApiRoutes(app: Hono): void {\r\n app.get(\"/api/admin/verify\", requireAdmin(), async (c) => {\r\n return c.json({ ok: true });\r\n });\r\n\r\n /** SQLite 主库 PRAGMA integrity_check */\r\n app.get(\"/api/admin/integrity-check\", requireAdmin(), async (c) => {\r\n try {\r\n const result = await runIntegrityCheck();\r\n const ok = result === \"ok\";\r\n return c.json({ ok, result });\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n return c.json({ ok: false, result: `error: ${message}` }, 500);\r\n }\r\n });\r\n}\r\n","// /api/sources/stats、/api/sources/raw、/api/sources/plugin-match(admin)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { getSourceStats } from \"../../../db/index.js\";\r\nimport { getSource } from \"../../../scraper/sources/index.js\";\r\nimport { getPluginSites } from \"../../../scraper/sources/web/index.js\";\r\nimport { getSourcesRaw, saveSourcesFile, getEffectiveProxyForListUrl } from \"../../../scraper/subscription/index.js\";\r\nimport { launchBrowser, applyProxyAuthToPage, resolveProxy } from \"../../../scraper/sources/web/fetcher/index.js\";\r\nimport { CACHE_DIR } from \"../../../config/paths.js\";\r\nimport type { SourceType } from \"../../../scraper/subscription/types.js\";\r\nimport type { RefreshInterval } from \"../../../utils/refreshInterval.js\";\r\nimport { VALID_INTERVALS } from \"../../../utils/refreshInterval.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { canonicalHttpSourceRef } from \"../../../utils/httpSourceRef.js\";\r\n\r\nexport function registerSourcesRoutes(app: Hono): void {\r\n app.get(\"/api/sources/stats\", requireAdmin(), async (c) => {\r\n const stats = await getSourceStats();\r\n return c.json(stats);\r\n });\r\n\r\n app.post(\"/api/sources/plugin-match\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ refs?: string[] }>();\r\n const refs = Array.isArray(body?.refs) ? body.refs : [];\r\n const pluginIds = new Set(getPluginSites().map((s) => s.id));\r\n const result: Record<string, string | null> = {};\r\n for (const ref of refs) {\r\n const source = getSource(ref);\r\n result[ref] = pluginIds.has(source.id) ? source.id : null;\r\n }\r\n return c.json(result);\r\n } catch {\r\n return c.json({});\r\n }\r\n });\r\n\r\n /**\r\n * 在有头 Chrome 中打开 URL:与抓取共用 CACHE_DIR/browser_data、代理优先级与 /auth/open 一致。\r\n * 浏览器在本机服务端弹出,非用户默认浏览器。\r\n */\r\n app.post(\"/api/sources/open-browser\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ url?: string }>();\r\n const raw = typeof body?.url === \"string\" ? body.url.trim() : \"\";\r\n if (!raw) return c.json({ ok: false, message: \"缺少 url\" }, 400);\r\n const lower = raw.toLowerCase();\r\n if (!lower.startsWith(\"http://\") && !lower.startsWith(\"https://\")) {\r\n return c.json({ ok: false, message: \"仅支持 http(s) URL\" }, 400);\r\n }\r\n const url = raw;\r\n const source = getSource(url);\r\n const merged = await getEffectiveProxyForListUrl(url, source);\r\n const proxy = resolveProxy({ proxy: merged });\r\n void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy })\r\n .then(async (browser) => {\r\n try {\r\n const page = await browser.newPage();\r\n await applyProxyAuthToPage(page, { proxy: merged });\r\n const realUserAgent =\r\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\r\n await page.setUserAgent(realUserAgent);\r\n await page.setViewport({ width: 1366, height: 960 });\r\n await page.goto(url, { waitUntil: \"domcontentloaded\", timeout: 60_000 });\r\n page.once(\"close\", () => {\r\n void browser.close().catch(() => {});\r\n });\r\n } catch {\r\n await browser.close().catch(() => {});\r\n }\r\n })\r\n .catch(() => {});\r\n return c.json({ ok: true, message: \"已在爬虫浏览器中打开\" });\r\n } catch {\r\n return c.json({ ok: false, message: \"请求体无效\" }, 400);\r\n }\r\n });\r\n\r\n app.get(\"/api/sources/raw\", requireAdmin(), async (c) => {\r\n try {\r\n const raw = await getSourcesRaw();\r\n return c.text(raw, 200, { \"Content-Type\": \"application/json; charset=utf-8\" });\r\n } catch {\r\n return c.text(JSON.stringify({ sources: [] }, null, 2), 200, { \"Content-Type\": \"application/json; charset=utf-8\" });\r\n }\r\n });\r\n\r\n app.put(\"/api/sources/raw\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ sources?: unknown[] }>();\r\n const list = Array.isArray(body?.sources) ? body.sources : [];\r\n const sources: { ref: string; type?: SourceType; label?: string; description?: string; refresh?: RefreshInterval; proxy?: string; weight?: number }[] = list\r\n .filter((s): s is Record<string, unknown> => s != null && typeof s === \"object\" && typeof (s as { ref?: unknown }).ref === \"string\")\r\n .map((s) => {\r\n const t = (s as { type?: string }).type;\r\n const type: SourceType | undefined =\r\n t === \"web\" || t === \"rss\" || t === \"email\" ? t : undefined;\r\n const r = (s as { refresh?: string }).refresh;\r\n const refresh: RefreshInterval | undefined =\r\n r && VALID_INTERVALS.includes(r as RefreshInterval) ? (r as RefreshInterval) : undefined;\r\n const w = (s as { weight?: unknown }).weight;\r\n const weight: number | undefined = typeof w === \"number\" ? w : undefined;\r\n return {\r\n ref: canonicalHttpSourceRef(String((s as { ref: string }).ref)),\r\n type,\r\n label: (s as { label?: string }).label,\r\n description: (s as { description?: string }).description,\r\n refresh,\r\n proxy: (s as { proxy?: string }).proxy,\r\n weight,\r\n };\r\n });\r\n await saveSourcesFile(sources);\r\n return c.json({ ok: true });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/tags — 系统标签(pipeline tagger);原 /api/topics 任务接口已迁至 /api/agent-tasks\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport {\r\n getSuggestedTags,\r\n getSystemTags,\r\n getSystemTagStats,\r\n saveSystemTagsToFile,\r\n removeTagFromAllItems,\r\n} from \"../../../db/index.js\";\r\n\r\nexport function registerTopicsRoutes(app: Hono): void {\r\n /** 系统标签:来自 .rssany/tags.json,供 pipeline tagger 使用 */\r\n app.get(\"/api/tags\", async (c) => {\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n });\r\n\r\n app.put(\"/api/tags\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ tags?: string[] }>();\r\n const list = Array.isArray(body?.tags) ? body.tags : [];\r\n await saveSystemTagsToFile(list);\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n ok: true,\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n\r\n /** 从所有条目的 tags 中移除指定标签 */\r\n app.post(\"/api/tags/remove-from-items\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ tag?: string }>();\r\n const tag = typeof body?.tag === \"string\" ? body.tag.trim() : \"\";\r\n if (!tag) return c.json({ ok: false, message: \"tag 参数缺失\" }, 400);\r\n const count = await removeTagFromAllItems(tag);\r\n const [tags, stats, suggested] = await Promise.all([\r\n getSystemTags(),\r\n getSystemTagStats(),\r\n getSuggestedTags(),\r\n ]);\r\n return c.json({\r\n ok: true,\r\n removedCount: count,\r\n tags,\r\n stats: stats.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n suggestedTags: suggested.map((s) => ({ name: s.name, count: s.count, hotness: s.hotness })),\r\n });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// /api/deliver、/api/deliver/test — 配置投递 URL;非空则在正常写库后额外 POST 条目到该地址\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { getDeliverConfig, saveDeliverConfig } from \"../../../config/deliver.js\";\r\nimport { postDeliverItems } from \"../../../deliver/post.js\";\r\n\r\nexport function registerDeliverRoutes(app: Hono): void {\r\n app.get(\"/api/deliver\", requireAdmin(), async (c) => {\r\n const { url, token } = await getDeliverConfig();\r\n return c.json({ url, token });\r\n });\r\n\r\n app.put(\"/api/deliver\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ url?: string; token?: string }>();\r\n const url = typeof body?.url === \"string\" ? body.url.trim() : \"\";\r\n const token = typeof body?.token === \"string\" ? body.token.trim() : \"\";\r\n await saveDeliverConfig({ url, token });\r\n return c.json({ ok: true, url, token });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n\r\n app.post(\"/api/deliver/test\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{ url?: string; token?: string }>();\r\n const url = typeof body?.url === \"string\" ? body.url.trim() : \"\";\r\n const token = typeof body?.token === \"string\" ? body.token.trim() : \"\";\r\n if (!url) return c.json({ ok: false, message: \"url 不能为空\" }, 400);\r\n const sample = {\r\n guid: \"deliver-test-\" + Date.now(),\r\n title: \"投递连通性测试\",\r\n link: \"https://example.com/rssany-deliver-test\",\r\n pubDate: new Date().toISOString(),\r\n summary: \"若下游收到此条,说明投递 URL 可用。\",\r\n };\r\n await postDeliverItems(\r\n url,\r\n \"rssany-deliver-test\",\r\n [\r\n {\r\n guid: sample.guid,\r\n title: sample.title,\r\n link: sample.link,\r\n pubDate: new Date(sample.pubDate),\r\n summary: sample.summary,\r\n sourceRef: \"rssany-deliver-test\",\r\n },\r\n ],\r\n { bearerToken: token || undefined },\r\n );\r\n return c.json({ ok: true });\r\n } catch (err) {\r\n return c.json({ ok: false, message: err instanceof Error ? err.message : String(err) }, 400);\r\n }\r\n });\r\n}\r\n","// LLM:读写 .rssany/config.json 的 llm 块;与 OPENAI_* 环境变量合并见 core/llmConfig.ts\r\n\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { CONFIG_PATH } from \"./paths.js\";\r\nimport { invalidateLLMConfigCache } from \"../core/llmConfig.js\";\r\n\r\nexport interface LlmFileConfig {\r\n apiKey?: string;\r\n baseUrl?: string;\r\n model?: string;\r\n}\r\n\r\nfunction trimOrUndef(s: unknown): string | undefined {\r\n if (typeof s !== \"string\") return undefined;\r\n const t = s.trim();\r\n return t.length > 0 ? t : undefined;\r\n}\r\n\r\n/** 读取 config.json 中的 llm 块(无则空) */\r\nexport async function readLlmFileConfig(): Promise<LlmFileConfig> {\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n const j = JSON.parse(raw) as { llm?: unknown };\r\n const llm = j?.llm;\r\n if (!llm || typeof llm !== \"object\") return {};\r\n const o = llm as Record<string, unknown>;\r\n return {\r\n apiKey: typeof o.apiKey === \"string\" ? o.apiKey : undefined,\r\n baseUrl: trimOrUndef(o.baseUrl),\r\n model: trimOrUndef(o.model),\r\n };\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\nexport interface SaveLlmSettingsInput {\r\n baseUrl: string;\r\n model: string;\r\n apiKey?: string;\r\n}\r\n\r\n/** 合并写入 config.json 的 llm 块,不覆盖其它顶层字段 */\r\nexport async function saveLlmSettings(input: SaveLlmSettingsInput): Promise<void> {\r\n let root: Record<string, unknown> = {};\r\n try {\r\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\r\n root = JSON.parse(raw) as Record<string, unknown>;\r\n } catch {\r\n // 新建\r\n }\r\n const prev = await readLlmFileConfig();\r\n const next: Record<string, unknown> = {\r\n baseUrl: input.baseUrl.trim(),\r\n model: input.model.trim(),\r\n };\r\n\r\n const newKey = typeof input.apiKey === \"string\" && input.apiKey.length > 0 ? input.apiKey : undefined;\r\n if (newKey) {\r\n next.apiKey = newKey;\r\n } else if (prev.apiKey) {\r\n next.apiKey = prev.apiKey;\r\n }\r\n\r\n root.llm = next;\r\n await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + \"\\n\", \"utf-8\");\r\n invalidateLLMConfigCache();\r\n}\r\n","// /api/llm — 读写 LLM 配置(config.json.llm);GET 不返回完整 Key\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { getLLMConfig } from \"../../../core/llmConfig.js\";\r\nimport { chatText } from \"../../../core/llm.js\";\r\nimport {readLlmFileConfig,saveLlmSettings} from \"../../../config/llmSettings.js\";\r\n\r\n\r\nexport function registerLlmRoutes(app: Hono): void {\r\n app.get(\"/api/llm\", requireAdmin(), async (c) => {\r\n const resolved = getLLMConfig();\r\n const file = await readLlmFileConfig();\r\n const hasApiKey = !!resolved.apiKey;\r\n const apiKeyInFile = !!(file.apiKey && file.apiKey.length > 0);\r\n return c.json({\r\n baseUrl: resolved.baseUrl,\r\n model: resolved.model,\r\n hasApiKey,\r\n apiKeyInFile,\r\n });\r\n });\r\n\r\n \r\n app.put(\"/api/llm\", requireAdmin(), async (c) => {\r\n try {\r\n const body = await c.req.json<{\r\n baseUrl?: unknown;\r\n model?: unknown;\r\n apiKey?: unknown;\r\n }>();\r\n const baseUrl = typeof body.baseUrl === \"string\" ? body.baseUrl : \"\";\r\n const model = typeof body.model === \"string\" ? body.model : \"\";\r\n const apiKey = typeof body.apiKey === \"string\" ? body.apiKey : undefined;\r\n await saveLlmSettings({\r\n baseUrl,\r\n model,\r\n ...(apiKey !== undefined ? { apiKey } : {}),\r\n });\r\n const resolved = getLLMConfig();\r\n const file = await readLlmFileConfig();\r\n return c.json({\r\n ok: true,\r\n baseUrl: resolved.baseUrl,\r\n model: resolved.model,\r\n hasApiKey: !!resolved.apiKey,\r\n apiKeyInFile: !!(file.apiKey && file.apiKey.length > 0),\r\n });\r\n } catch (err) {\r\n return c.json(\r\n { ok: false, message: err instanceof Error ? err.message : String(err) },\r\n 400,\r\n );\r\n }\r\n });\r\n\r\n app.post(\"/api/llm/test\", requireAdmin(), async (c) => {\r\n const t0 = Date.now();\r\n try {\r\n const cfg = getLLMConfig();\r\n if (!cfg.apiKey) {\r\n return c.json({ ok: false, message: \"未配置 API Key(请在界面或 OPENAI_API_KEY 中设置)\" }, 400);\r\n }\r\n const reply = await chatText(\"Reply with exactly the single word: ok\", undefined, {\r\n maxTokens: 32768,\r\n debugLabel: \"llmSettingsTest\",\r\n });\r\n return c.json({ ok: true, reply });\r\n } catch (err) {\r\n const ms = Date.now() - t0;\r\n const message = err instanceof Error ? err.message : String(err);\r\n console.error(\"[llm/test] fail\", { ms, message });\r\n return c.json({ ok: false, message }, 400);\r\n }\r\n });\r\n}\r\n","// GET/PUT /api/proxy — config.json 全局代理(admin)\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\nimport { readGlobalProxyFromConfig, saveGlobalProxyToConfig } from \"../../../config/globalProxy.js\";\r\n\r\nexport function registerProxySettingsRoutes(app: Hono): void {\r\n app.get(\"/api/proxy\", requireAdmin(), async (c) => {\r\n const globalProxy = (await readGlobalProxyFromConfig()) ?? \"\";\r\n return c.json({ globalProxy });\r\n });\r\n\r\n app.put(\"/api/proxy\", requireAdmin(), async (c) => {\r\n try {\r\n const body = (await c.req.json().catch(() => ({}))) as { globalProxy?: unknown };\r\n const globalProxy = typeof body.globalProxy === \"string\" ? body.globalProxy : \"\";\r\n await saveGlobalProxyToConfig(globalProxy);\r\n const saved = (await readGlobalProxyFromConfig()) ?? \"\";\r\n return c.json({ ok: true, globalProxy: saved });\r\n } catch (err) {\r\n return c.json(\r\n { ok: false, message: err instanceof Error ? err.message : String(err) },\r\n 400,\r\n );\r\n }\r\n });\r\n}\r\n","// 后端任务:提交后立即返回 taskId,任务在后台执行,前端轮询状态\n\nconst tasks = new Map<string, { id: string; status: string; result?: unknown; error?: string; createdAt: number; updatedAt: number }>();\nlet idCounter = 0;\n\nfunction nextId(): string {\n idCounter += 1;\n return `t_${Date.now().toString(36)}_${idCounter}`;\n}\n\nexport function createTask(): string {\n const id = nextId();\n const now = Date.now();\n tasks.set(id, { id, status: \"pending\", createdAt: now, updatedAt: now });\n return id;\n}\n\nexport function getTask(id: string) {\n return tasks.get(id) ?? null;\n}\n\nexport function setTaskRunning(id: string): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"running\";\n t.updatedAt = Date.now();\n }\n}\n\nexport function setTaskDone<T>(id: string, result: T): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"done\";\n t.result = result as unknown;\n t.updatedAt = Date.now();\n }\n}\n\nexport function setTaskError(id: string, error: string): void {\n const t = tasks.get(id);\n if (t) {\n t.status = \"error\";\n t.error = error;\n t.updatedAt = Date.now();\n }\n}\n\n/** 清理 1 小时前的已完成/失败任务 */\nexport function pruneTasks(): void {\n const cutoff = Date.now() - 60 * 60 * 1000;\n for (const [id, t] of tasks) {\n if ((t.status === \"done\" || t.status === \"error\") && t.updatedAt < cutoff) {\n tasks.delete(id);\n }\n }\n}\n","// 任务 API:POST /api/tasks 提交拉取信源,GET /api/tasks/:id 轮询\r\n\r\nimport type { Hono } from \"hono\";\r\nimport * as taskStore from \"../../../tasks/index.js\";\r\nimport * as scheduler from \"../../../scheduler/index.js\";\r\nimport { CACHE_DIR } from \"../../../config/paths.js\";\r\nimport { getItems } from \"../../../feeder/index.js\";\r\nimport { SOURCES_GROUP } from \"../../../scraper/scheduler/index.js\";\r\nimport { requireAdmin } from \"../../../auth/middleware.js\";\r\n\r\nexport function registerTasksRoutes(app: Hono): void {\r\n app.get(\"/api/tasks/:id\", (c) => {\r\n const id = c.req.param(\"id\") ?? \"\";\r\n const task = taskStore.getTask(id);\r\n if (!task) return c.json({ error: \"任务不存在\" }, 404);\r\n return c.json(task);\r\n });\r\n\r\n app.post(\"/api/tasks\", requireAdmin(), async (c) => {\r\n try {\r\n const body = (await c.req.json().catch(() => ({}))) as { type?: string; ref?: string };\r\n const type = body.type ?? \"\";\r\n if (type === \"source-pull\") {\r\n const ref = typeof body.ref === \"string\" ? body.ref.trim() : \"\";\r\n if (!ref) return c.json({ error: \"ref 不能为空\" }, 400);\r\n const taskId = taskStore.createTask();\r\n scheduler.schedule(SOURCES_GROUP, taskId, async () => {\r\n taskStore.setTaskRunning(taskId);\r\n try {\r\n await getItems(ref, { cacheDir: CACHE_DIR, force: true });\r\n taskStore.setTaskDone(taskId, { ok: true });\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n taskStore.setTaskError(taskId, msg);\r\n throw err;\r\n }\r\n }, { priority: true }).catch(() => {});\r\n return c.json({ taskId });\r\n }\r\n return c.json({ error: `未知任务类型: ${type}` }, 400);\r\n } catch (err) {\r\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);\r\n }\r\n });\r\n}\r\n","// GET /api/feed-favicon?domain=example.com — 信源列表站点图标;磁盘缓存在 CACHE_DIR/feed-favicons。\r\n//\r\n// 合法 domain 时始终返回 200 + 可用图片:上游全部失败则回退为字母 SVG(按域名稳定着色)。\r\n// 磁盘与 HTTP 缓存均为 3 天(mtime / max-age),过期后重新抓取。\r\n//\r\n// 多路上游并行竞速(Promise.any)。默认不含 Google s2(国内访问差):设 FAVICON_INCLUDE_GOOGLE=1 可启用。\r\n// 境外聚合源(DDG / icon.horse / unavatar)在国内网络可能慢或失败;若仅需直连站点图标,设 FAVICON_THIRD_PARTY=0。\r\n// 解析首页 HTML 中 link[rel~=icon] 等(尊重 <base href>);设 FAVICON_SKIP_HTML=1 可关闭。\r\n// 同域名 in-flight 合并。\r\n\r\nimport { createHash } from \"node:crypto\";\r\nimport { readFile, writeFile, mkdir, unlink, stat } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { parse } from \"node-html-parser\";\r\nimport type { Hono } from \"hono\";\r\nimport { CACHE_DIR } from \"../../../config/paths.js\";\r\n\r\nconst CACHE_SUBDIR = \"feed-favicons\";\r\nconst CACHE_KEY_PREFIX = \"feed-favicon:v1:\";\r\n/** 磁盘与 CDN/浏览器缓存:3 天 */\r\nconst CACHE_MAX_AGE_SEC = 3 * 24 * 60 * 60;\r\nconst CACHE_MAX_AGE_MS = CACHE_MAX_AGE_SEC * 1000;\r\nconst CACHE_CONTROL = `public, max-age=${CACHE_MAX_AGE_SEC}`;\r\n\r\nconst FETCH_TIMEOUT_MS = 6_000;\r\nconst MAX_ICON_BYTES = 2 * 1024 * 1024;\r\nconst MAX_HTML_BYTES = 512 * 1024;\r\n\r\nconst inflightByDomain = new Map<string, Promise<{ buf: Buffer; mime: string }>>();\r\n\r\nconst MAX_DOMAIN_LEN = 253;\r\n\r\nfunction isPlausibleHostname(s: string): boolean {\r\n if (s.length === 0 || s.length > MAX_DOMAIN_LEN) return false;\r\n return /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(s);\r\n}\r\n\r\nfunction cacheFilePath(domainKey: string): string {\r\n const h = createHash(\"sha256\").update(CACHE_KEY_PREFIX + domainKey.toLowerCase()).digest(\"hex\");\r\n return join(CACHE_DIR, CACHE_SUBDIR, h);\r\n}\r\n\r\n/** 直连目标站常见图标路径(不依赖境外 CDN,适合国内部署优先尝试) */\r\nfunction originFaviconUrls(domain: string): string[] {\r\n const d = domain.toLowerCase();\r\n const hosts: string[] = [`https://${d}`];\r\n if (d.startsWith(\"www.\")) {\r\n const bare = d.slice(4);\r\n if (bare) hosts.push(`https://${bare}`);\r\n } else {\r\n hosts.push(`https://www.${d}`);\r\n }\r\n const paths = [\"/favicon.ico\", \"/favicon.png\", \"/apple-touch-icon.png\"];\r\n const urls: string[] = [];\r\n for (const base of [...new Set(hosts)]) {\r\n for (const p of paths) {\r\n urls.push(`${base}${p}`);\r\n }\r\n }\r\n return urls;\r\n}\r\n\r\nfunction homepageUrlsForDomain(domain: string): string[] {\r\n const d = domain.toLowerCase();\r\n const urls = [`https://${d}/`];\r\n if (d.startsWith(\"www.\")) {\r\n const bare = d.slice(4);\r\n if (bare) urls.push(`https://${bare}/`);\r\n } else {\r\n urls.push(`https://www.${d}/`);\r\n }\r\n return [...new Set(urls)];\r\n}\r\n\r\nfunction isIconLinkRel(rel: string): boolean {\r\n const tokens = rel\r\n .toLowerCase()\r\n .trim()\r\n .split(/\\s+/)\r\n .filter(Boolean);\r\n if (tokens.some((x) => x === \"mask-icon\")) return true;\r\n if (tokens.some((x) => x === \"apple-touch-icon\" || x === \"apple-touch-icon-precomposed\")) return true;\r\n if (tokens.includes(\"shortcut\") && tokens.includes(\"icon\")) return true;\r\n return tokens.includes(\"icon\");\r\n}\r\n\r\n/** 从 HTML 提取 <link rel=\"icon\" …> 等 href,按文档顺序去重 */\r\nfunction parseLinkIconHrefs(html: string, pageUrl: string): string[] {\r\n const root = parse(html, { lowerCaseTagName: true });\r\n let base = pageUrl;\r\n const baseEl = root.querySelector(\"base[href]\");\r\n if (baseEl) {\r\n const bh = baseEl.getAttribute(\"href\")?.trim();\r\n if (bh) {\r\n try {\r\n base = new URL(bh, pageUrl).href;\r\n } catch {\r\n /* keep pageUrl */\r\n }\r\n }\r\n }\r\n const out: string[] = [];\r\n const seen = new Set<string>();\r\n for (const el of root.querySelectorAll(\"link[href]\")) {\r\n const rel = el.getAttribute(\"rel\") ?? \"\";\r\n if (!isIconLinkRel(rel)) continue;\r\n const href = el.getAttribute(\"href\")?.trim();\r\n if (!href || href.startsWith(\"data:\") || href.startsWith(\"blob:\")) continue;\r\n try {\r\n const abs = new URL(href, base).href;\r\n if ((abs.startsWith(\"http:\") || abs.startsWith(\"https:\")) && !seen.has(abs)) {\r\n seen.add(abs);\r\n out.push(abs);\r\n }\r\n } catch {\r\n /* skip */\r\n }\r\n }\r\n return out;\r\n}\r\n\r\nasync function fetchHtmlPage(url: string): Promise<string | null> {\r\n try {\r\n const upstream = await fetch(url, {\r\n redirect: \"follow\",\r\n headers: {\r\n Accept: \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.1\",\r\n \"User-Agent\": \"Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon\",\r\n },\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n });\r\n if (!upstream.ok) return null;\r\n const ab = await upstream.arrayBuffer();\r\n const buf = Buffer.from(ab);\r\n const slice = buf.subarray(0, Math.min(buf.length, MAX_HTML_BYTES));\r\n return slice.toString(\"utf-8\");\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 抓取首页 HTML,解析 link 图标;按候选首页顺序直到得到至少一条 href */\r\nasync function discoverIconUrlsFromHomepage(domain: string): Promise<string[]> {\r\n if (process.env.FAVICON_SKIP_HTML === \"1\" || process.env.FAVICON_SKIP_HTML === \"true\") {\r\n return [];\r\n }\r\n for (const pageUrl of homepageUrlsForDomain(domain)) {\r\n const html = await fetchHtmlPage(pageUrl);\r\n if (!html) continue;\r\n const hrefs = parseLinkIconHrefs(html, pageUrl);\r\n if (hrefs.length > 0) return hrefs;\r\n }\r\n return [];\r\n}\r\n\r\nfunction duckduckgoFaviconUrl(domain: string): string {\r\n return `https://icons.duckduckgo.com/ip3/${domain}.ico`;\r\n}\r\n\r\nfunction iconHorseUrl(domain: string): string {\r\n return `https://icon.horse/icon/${encodeURIComponent(domain)}`;\r\n}\r\n\r\nfunction unavatarUrl(domain: string): string {\r\n return `https://unavatar.io/${encodeURIComponent(domain)}`;\r\n}\r\n\r\nfunction googleFaviconUrl(domain: string): string {\r\n return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`;\r\n}\r\n\r\nfunction letterCharFromDomain(domain: string): string {\r\n const d = domain.toLowerCase().replace(/^www\\./, \"\");\r\n const m = d.match(/[a-z0-9]/);\r\n return m ? m[0]!.toUpperCase() : \"?\";\r\n}\r\n\r\nfunction hueFromDomain(domain: string): number {\r\n const h = createHash(\"sha256\").update(domain.toLowerCase()).digest();\r\n return ((h[0]! << 8) | h[1]!) % 360;\r\n}\r\n\r\nfunction escapeXmlText(s: string): string {\r\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\r\n}\r\n\r\n/** 上游全部失败时的稳定回退:圆角方块 + 首字母 */\r\nfunction letterAvatarSvg(domain: string): Buffer {\r\n const letter = escapeXmlText(letterCharFromDomain(domain));\r\n const hue = hueFromDomain(domain);\r\n const bg = `hsl(${hue} 42% 44%)`;\r\n const svg = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\">\r\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"${bg}\"/>\r\n <text x=\"32\" y=\"32\" dominant-baseline=\"central\" text-anchor=\"middle\" fill=\"#ffffff\" font-family=\"system-ui,Segoe UI,Helvetica,sans-serif\" font-size=\"28\" font-weight=\"600\">${letter}</text>\r\n</svg>`;\r\n return Buffer.from(svg.trim(), \"utf-8\");\r\n}\r\n\r\nfunction letterAvatarForDomain(domain: string): { buf: Buffer; mime: string } {\r\n return { buf: letterAvatarSvg(domain), mime: \"image/svg+xml\" };\r\n}\r\n\r\nfunction isEnoent(e: unknown): boolean {\r\n return typeof e === \"object\" && e !== null && (e as NodeJS.ErrnoException).code === \"ENOENT\";\r\n}\r\n\r\nfunction sniffImageMime(buf: Buffer): string | null {\r\n if (buf.length < 4) return null;\r\n if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return \"image/png\";\r\n if (buf.length >= 6 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return \"image/gif\";\r\n if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return \"image/jpeg\";\r\n if (\r\n buf.length >= 12 &&\r\n buf.subarray(0, 4).toString(\"ascii\") === \"RIFF\" &&\r\n buf.subarray(8, 12).toString(\"ascii\") === \"WEBP\"\r\n ) {\r\n return \"image/webp\";\r\n }\r\n if (buf.length >= 6 && buf.readUInt16LE(0) === 0 && (buf[2] === 1 || buf[2] === 2) && buf[3] === 0) {\r\n return \"image/x-icon\";\r\n }\r\n const head = buf.subarray(0, Math.min(256, buf.length)).toString(\"utf-8\").trimStart();\r\n if (head.startsWith(\"<svg\") || head.startsWith(\"<?xml\")) return \"image/svg+xml\";\r\n return null;\r\n}\r\n\r\nconst IMAGE_CT_PREFIX = \"image/\";\r\n\r\nfunction mimeFromFetch(ct: string | null): string | null {\r\n if (!ct) return null;\r\n const base = ct.split(\";\")[0].trim().toLowerCase();\r\n return base.startsWith(IMAGE_CT_PREFIX) ? base : null;\r\n}\r\n\r\nfunction resolveImageMime(buf: Buffer, ct: string | null): string | null {\r\n return sniffImageMime(buf) ?? mimeFromFetch(ct);\r\n}\r\n\r\nasync function fetchIconCandidate(url: string): Promise<{ buf: Buffer; ct: string | null } | null> {\r\n let upstream: Response;\r\n try {\r\n upstream = await fetch(url, {\r\n redirect: \"follow\",\r\n headers: {\r\n Accept: \"image/avif,image/webp,image/apng,image/*,*/*;q=0.8\",\r\n \"User-Agent\": \"Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon\",\r\n },\r\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\r\n });\r\n } catch {\r\n return null;\r\n }\r\n if (!upstream.ok) return null;\r\n const ab = await upstream.arrayBuffer();\r\n const buf = Buffer.from(ab);\r\n if (buf.length === 0 || buf.length > MAX_ICON_BYTES) return null;\r\n return { buf, ct: upstream.headers.get(\"content-type\") };\r\n}\r\n\r\nfunction isValidIcon(got: { buf: Buffer; ct: string | null } | null): got is { buf: Buffer; ct: string | null } {\r\n if (!got) return false;\r\n const mime = resolveImageMime(got.buf, got.ct);\r\n return !!(mime && mime.startsWith(IMAGE_CT_PREFIX));\r\n}\r\n\r\nfunction upstreamFaviconUrls(domain: string, htmlIconUrls: string[]): string[] {\r\n const urls: string[] = [...originFaviconUrls(domain), ...htmlIconUrls];\r\n const thirdPartyOff =\r\n process.env.FAVICON_THIRD_PARTY === \"0\" || process.env.FAVICON_THIRD_PARTY === \"false\";\r\n if (!thirdPartyOff) {\r\n urls.push(duckduckgoFaviconUrl(domain), iconHorseUrl(domain), unavatarUrl(domain));\r\n }\r\n const includeGoogle =\r\n process.env.FAVICON_INCLUDE_GOOGLE === \"1\" || process.env.FAVICON_INCLUDE_GOOGLE === \"true\";\r\n if (includeGoogle) urls.push(googleFaviconUrl(domain));\r\n return urls;\r\n}\r\n\r\nasync function fetchFaviconFromNetwork(domain: string): Promise<{ buf: Buffer; mime: string }> {\r\n const htmlIconUrls = await discoverIconUrlsFromHomepage(domain);\r\n const urls = upstreamFaviconUrls(domain, htmlIconUrls);\r\n const tasks = urls.map(async (url) => {\r\n const got = await fetchIconCandidate(url);\r\n if (!isValidIcon(got)) {\r\n throw new Error(\"not-an-icon\");\r\n }\r\n const mime = resolveImageMime(got.buf, got.ct)!;\r\n return { buf: got.buf, mime };\r\n });\r\n try {\r\n return await Promise.any(tasks);\r\n } catch {\r\n return letterAvatarForDomain(domain);\r\n }\r\n}\r\n\r\nfunction fetchFaviconDeduped(domain: string): Promise<{ buf: Buffer; mime: string }> {\r\n let p = inflightByDomain.get(domain);\r\n if (p) return p;\r\n p = fetchFaviconFromNetwork(domain).finally(() => {\r\n if (inflightByDomain.get(domain) === p) inflightByDomain.delete(domain);\r\n });\r\n inflightByDomain.set(domain, p);\r\n return p;\r\n}\r\n\r\nexport function registerFeedFaviconRoutes(app: Hono): void {\r\n app.get(\"/api/feed-favicon\", async (c) => {\r\n const raw = (c.req.query(\"domain\") ?? \"\").trim();\r\n if (!raw || !isPlausibleHostname(raw)) {\r\n return new Response(null, { status: 400 });\r\n }\r\n const domain = raw.toLowerCase();\r\n const path = cacheFilePath(domain);\r\n\r\n let diskStale = false;\r\n try {\r\n const st = await stat(path);\r\n if (Date.now() - st.mtimeMs >= CACHE_MAX_AGE_MS) {\r\n diskStale = true;\r\n await unlink(path).catch(() => {});\r\n }\r\n } catch (e) {\r\n if (!isEnoent(e)) {\r\n return new Response(null, { status: 500 });\r\n }\r\n }\r\n\r\n if (!diskStale) {\r\n try {\r\n const cached = await readFile(path);\r\n const mime = resolveImageMime(cached, null);\r\n if (mime) {\r\n return new Response(new Uint8Array(cached), {\r\n status: 200,\r\n headers: {\r\n \"Content-Type\": mime,\r\n \"Cache-Control\": CACHE_CONTROL,\r\n },\r\n });\r\n }\r\n await unlink(path).catch(() => {});\r\n } catch (e) {\r\n if (!isEnoent(e)) {\r\n return new Response(null, { status: 500 });\r\n }\r\n }\r\n }\r\n\r\n const resolved = await fetchFaviconDeduped(domain);\r\n const { buf, mime } = resolved;\r\n\r\n try {\r\n await mkdir(join(CACHE_DIR, CACHE_SUBDIR), { recursive: true });\r\n await writeFile(path, buf);\r\n } catch {\r\n return new Response(null, { status: 500 });\r\n }\r\n\r\n return new Response(new Uint8Array(buf), {\r\n status: 200,\r\n headers: {\r\n \"Content-Type\": mime,\r\n \"Cache-Control\": CACHE_CONTROL,\r\n },\r\n });\r\n });\r\n}\r\n","// API 路由汇总:server、rss、items、feed、sources、scheduler、plugins、logs、admin、tags、tasks\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { registerServerRoutes } from \"./server.js\";\r\nimport { registerRssApiRoutes } from \"./rss.js\";\r\nimport { registerSchedulerRoutes } from \"./scheduler.js\";\r\nimport { registerPluginsRoutes } from \"./plugins.js\";\r\nimport { registerPipelineRoutes } from \"./pipeline.js\";\r\nimport { registerFeedRoutes } from \"./feed.js\";\r\nimport { registerItemsRoutes } from \"./items.js\";\r\nimport { registerLogsRoutes } from \"./logs.js\";\r\nimport { registerAdminApiRoutes } from \"./admin.js\";\r\nimport { registerSourcesRoutes } from \"./sources.js\";\r\nimport { registerTopicsRoutes } from \"./topics.js\";\r\nimport { registerDeliverRoutes } from \"./deliver.js\";\r\nimport { registerLlmRoutes } from \"./llm.js\";\r\nimport { registerProxySettingsRoutes } from \"./proxy.js\";\r\nimport { registerTasksRoutes } from \"./tasks.js\";\r\nimport { registerFeedFaviconRoutes } from \"./feed-favicon.js\";\r\n\r\nexport function registerApiRoutes(app: Hono): void {\r\n registerServerRoutes(app);\r\n registerFeedFaviconRoutes(app);\r\n registerRssApiRoutes(app);\r\n registerSchedulerRoutes(app);\r\n registerPluginsRoutes(app);\r\n registerPipelineRoutes(app);\r\n registerFeedRoutes(app);\r\n registerItemsRoutes(app);\r\n registerLogsRoutes(app);\r\n registerAdminApiRoutes(app);\r\n registerSourcesRoutes(app);\r\n registerTopicsRoutes(app);\r\n registerDeliverRoutes(app);\r\n registerLlmRoutes(app);\r\n registerProxySettingsRoutes(app);\r\n registerTasksRoutes(app);\r\n}\r\n","// 认证路由:登录检查、打开登录页、ensureAuth\n\nimport type { Hono } from \"hono\";\nimport { getWebSite, getBestSite, toAuthFlow } from \"../../scraper/sources/web/index.js\";\nimport { applyProxyAuthToPage, ensureAuth, preCheckAuth, launchBrowser, resolveProxy } from \"../../scraper/sources/web/fetcher/index.js\";\nimport { CACHE_DIR } from \"../../config/paths.js\";\nimport { resolveProxyForSite } from \"../../config/globalProxy.js\";\n\nexport function registerAuthRoutes(app: Hono): void {\n app.get(\"/auth/check\", async (c) => {\n const siteIdParam = c.req.query(\"siteId\");\n if (!siteIdParam) {\n return c.json({ ok: false, message: \"请提供 siteId\" }, 400);\n }\n const site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n try {\n const authenticated = await preCheckAuth(authFlow, CACHE_DIR, { proxy: await resolveProxyForSite(site) });\n return c.json({ ok: true, authenticated });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return c.json({ ok: false, message: `检查失败: ${msg}` }, 500);\n }\n });\n\n app.post(\"/auth/open\", async (c) => {\n const siteIdParam = c.req.query(\"siteId\");\n if (!siteIdParam) {\n return c.json({ ok: false, message: \"请提供 siteId\" }, 400);\n }\n const site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n const { loginUrl } = authFlow;\n const proxy = await resolveProxyForSite(site);\n void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy: resolveProxy({ proxy }) }).then(async (browser) => {\n try {\n const page = await browser.newPage();\n await applyProxyAuthToPage(page, { proxy });\n const realUserAgent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\n await page.setUserAgent(realUserAgent);\n await page.setViewport({ width: 1366, height: 960 });\n await page.goto(loginUrl, { waitUntil: \"domcontentloaded\", timeout: 60000 });\n page.once(\"close\", () => {\n void browser.close().catch(() => {});\n });\n } catch {\n await browser.close().catch(() => {});\n }\n }).catch(() => {});\n return c.json({ ok: true, message: \"已打开登录页面\" });\n });\n\n app.post(\"/auth/ensure\", async (c) => {\n const urlParam = c.req.query(\"url\");\n const siteIdParam = c.req.query(\"siteId\");\n let site;\n if (urlParam) {\n const decoded = decodeURIComponent(urlParam);\n site = getBestSite(decoded);\n if (!site) return c.json({ ok: false, message: \"无匹配站点\" }, 404);\n } else if (siteIdParam) {\n site = getWebSite(siteIdParam);\n if (!site) return c.json({ ok: false, message: \"无此站点\" }, 404);\n } else {\n return c.json({ ok: false, message: \"请提供 url 或 siteId\" }, 400);\n }\n const authFlow = toAuthFlow(site);\n if (!authFlow) return c.json({ ok: false, message: \"该站点无需登录\" }, 400);\n ensureAuth(authFlow, CACHE_DIR, { proxy: await resolveProxyForSite(site) }).then(() => {}).catch(() => {});\n return c.json({ ok: true, message: \"已打开登录窗口,请在弹出的浏览器中完成登录,完成后刷新订阅页面即可。\" });\n });\n}\n","// 路由层共享工具函数\r\n\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\nexport const STATICS_DIR = join(PACKAGE_ROOT, \"statics\");\r\n\r\n/** 从路径提取 URL(与 /rss/* 一致) */\r\nexport function parseUrlFromPath(path: string, prefix: string): string | null {\r\n const raw = path.slice(prefix.length) || \"\";\r\n const decoded = decodeURIComponent(raw.startsWith(\"/\") ? raw.slice(1) : raw);\r\n if (!decoded) return null;\r\n return decoded.startsWith(\"http\") ? decoded : `https://${decoded}`;\r\n}\r\n\r\n/** 读取静态 HTML(statics/ 目录,用于 401/404 错误页) */\r\nexport async function readStaticHtml(name: string, fallback: string): Promise<string> {\r\n try {\r\n return await readFile(join(STATICS_DIR, `${name}.html`), \"utf-8\");\r\n } catch {\r\n return fallback;\r\n }\r\n}\r\n\r\n/** HTML 转义,用于注入到页面中的不可信内容 */\r\nexport function escapeHtml(s: string): string {\r\n return s\r\n .replace(/&/g, \"&\")\r\n .replace(/</g, \"<\")\r\n .replace(/>/g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/'/g, \"'\");\r\n}\r\n","// Admin 路由:解析调试、正文提取调试\r\n\r\nimport type { Hono } from \"hono\";\r\nimport { getSource } from \"../../scraper/sources/index.js\";\r\nimport { buildSourceContext } from \"../../scraper/sources/context.js\";\r\nimport { extractFromLink } from \"../../scraper/sources/web/extractor/index.js\";\r\nimport { CACHE_DIR } from \"../../config/paths.js\";\r\nimport { AuthRequiredError } from \"../../scraper/auth/index.js\";\r\nimport { parseUrlFromPath, readStaticHtml, escapeHtml } from \"../utils.js\";\r\nimport { requireAdmin } from \"../../auth/middleware.js\";\r\nimport { getEffectiveProxyForListUrl } from \"../../scraper/subscription/index.js\";\r\n\r\n/** 与 fetcher `resolveProxy` 一致:调试 query 优先 → sources.json / Source.proxy → 环境变量 */\r\nfunction effectiveProxyUsed(override: string | undefined, mergedFromSource: string | undefined): string | undefined {\r\n const o = override?.trim();\r\n if (o) return o;\r\n const s = mergedFromSource?.trim();\r\n if (s) return s;\r\n return process.env.HTTP_PROXY?.trim() || process.env.HTTPS_PROXY?.trim();\r\n}\r\n\r\nfunction redactProxyForLog(p: string | undefined): string | null {\r\n if (!p) return null;\r\n try {\r\n const u = new URL(p);\r\n if (u.username) u.username = \"***\";\r\n if (u.password) u.password = \"***\";\r\n return u.toString();\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function registerAdminRoutes(app: Hono): void {\r\n async function render401(listUrl: string): Promise<string> {\r\n const raw = await readStaticHtml(\"401\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>401</title></head><body><h1>401 需要登录</h1></body></html>\");\r\n return raw.replace(/\\{\\{listUrl\\}\\}/g, escapeHtml(listUrl));\r\n }\r\n\r\n /** Parse 与插件解耦:始终通过 getSource(url) 解析,无匹配插件时自动走 generic(浏览器抓取 + LLM 解析),与 /rss/* 行为一致 */\r\n app.get(\"/admin/parse/*\", requireAdmin(), async (c) => {\r\n const url = parseUrlFromPath(c.req.path, \"/admin/parse\");\r\n if (!url) return c.text(\"无效 URL,格式: /admin/parse/https://... 或 /admin/parse/example.com/...\", 400);\r\n try {\r\n // 调试默认有头浏览器;仅 headless=true|1 时使用无头\r\n const headlessParam = c.req.query(\"headless\");\r\n const headless = headlessParam === \"true\" || headlessParam === \"1\";\r\n const proxyOverride = c.req.query(\"proxy\")?.trim();\r\n const source = getSource(url);\r\n const fromSource = await getEffectiveProxyForListUrl(url, source);\r\n const ctx = buildSourceContext({\r\n cacheDir: CACHE_DIR,\r\n headless,\r\n proxy: proxyOverride || fromSource,\r\n });\r\n const items = await source.fetchItems(url, ctx);\r\n const mode = source.id === \"generic\" ? \"generic\" : \"plugin\";\r\n const effective = effectiveProxyUsed(proxyOverride, fromSource);\r\n return c.json({\r\n items,\r\n url,\r\n mode,\r\n pluginId: source.id,\r\n effectiveProxy: redactProxyForLog(effective),\r\n });\r\n } catch (err) {\r\n if (err instanceof AuthRequiredError) {\r\n const html = await render401(url);\r\n return c.html(html, 401);\r\n }\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return c.text(`解析失败: ${msg}`, 500);\r\n }\r\n });\r\n\r\n app.get(\"/admin/extractor/*\", requireAdmin(), async (c) => {\r\n const url = parseUrlFromPath(c.req.path, \"/admin/extractor\");\r\n if (!url) return c.text(\"无效 URL,格式: /admin/extractor/https://... 或 /admin/extractor/example.com/...\", 400);\r\n try {\r\n const headlessParam = c.req.query(\"headless\");\r\n const headless = headlessParam === \"true\" || headlessParam === \"1\";\r\n const proxyOverride = c.req.query(\"proxy\")?.trim();\r\n const source = getSource(url);\r\n const fromSource = await getEffectiveProxyForListUrl(url, source);\r\n const proxy = proxyOverride || fromSource;\r\n const result = await extractFromLink(url, {}, { timeoutMs: 60_000, headless, proxy });\r\n const effective = effectiveProxyUsed(proxyOverride, fromSource);\r\n return c.json({\r\n title: result.title ?? null,\r\n author: result.author ?? null,\r\n pubDate: result.pubDate ?? null,\r\n content: result.content ?? null,\r\n _extractor: \"readability\",\r\n effectiveProxy: redactProxyForLog(effective),\r\n });\r\n } catch (err) {\r\n const msg = err instanceof Error ? err.message : String(err);\r\n return c.text(`提取失败: ${msg}`, 500);\r\n }\r\n });\r\n}\r\n","// RSS 路由:/rss/* 生成 RSS XML\r\n\r\n\r\n\r\nimport { createHash } from \"node:crypto\";\r\n\r\nimport type { Hono } from \"hono\";\r\n\r\nimport { getItems, feedItemsToRssXml } from \"../../feeder/index.js\";\r\n\r\nimport { queryItems } from \"../../db/index.js\";\r\n\r\nimport { getAllSubscriptionRefs } from \"../../scraper/subscription/index.js\";\r\n\r\nimport { SOURCES_GROUP } from \"../../scraper/scheduler/index.js\";\r\n\r\nimport * as scheduler from \"../../scheduler/index.js\";\r\n\r\nimport { CACHE_DIR } from \"../../config/paths.js\";\r\n\r\nimport { AuthRequiredError, NotFoundError } from \"../../scraper/auth/index.js\";\r\n\r\nimport { parseUrlFromPath, readStaticHtml, escapeHtml } from \"../utils.js\";\r\n\r\n\r\n\r\nfunction parseSubscribedFlag(v: string | undefined): boolean {\r\n\r\n return v === \"1\" || v === \"true\" || v === \"yes\";\r\n\r\n}\r\n\r\n\r\n\r\nexport function registerRssRoutes(app: Hono): void {\r\n\r\n async function render401(listUrl: string): Promise<string> {\r\n\r\n const raw = await readStaticHtml(\"401\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>401</title></head><body><h1>401 需要登录</h1></body></html>\");\r\n\r\n return raw.replace(/\\{\\{listUrl\\}\\}/g, escapeHtml(listUrl));\r\n\r\n }\r\n\r\n\r\n\r\n /** 查询式 RSS:按 search、sourceUrl、subscribed、author、tags 过滤,返回匹配条目的 XML */\r\n\r\n app.get(\"/rss\", async (c) => {\r\n\r\n const search = c.req.query(\"search\") ?? c.req.query(\"q\") ?? undefined;\r\n\r\n const ref = c.req.query(\"ref\") ?? c.req.query(\"source\") ?? c.req.query(\"sourceUrl\") ?? undefined;\r\n\r\n const subscribed = parseSubscribedFlag(c.req.query(\"subscribed\"));\r\n\r\n const author = c.req.query(\"author\") ?? undefined;\r\n\r\n const tagsParam = c.req.query(\"tags\") ?? undefined;\r\n\r\n const tags = tagsParam ? tagsParam.split(\",\").map((t) => t.trim()).filter(Boolean) : undefined;\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n const limit = Math.min(Number(c.req.query(\"limit\") ?? 50), 200);\r\n\r\n const offset = Number(c.req.query(\"offset\") ?? 0);\r\n\r\n const title = c.req.query(\"title\") ?? undefined;\r\n\r\n const daysParam = c.req.query(\"days\");\r\n\r\n const sinceParam = c.req.query(\"since\") ?? undefined;\r\n\r\n const untilParam = c.req.query(\"until\") ?? undefined;\r\n\r\n let since: Date | undefined;\r\n\r\n let until: Date | undefined;\r\n\r\n if (daysParam) {\r\n\r\n const n = Math.max(1, Math.min(365, Number(daysParam) || 1));\r\n\r\n const now = new Date();\r\n\r\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\r\n\r\n const todayEnd = new Date(todayStart);\r\n\r\n todayEnd.setDate(todayEnd.getDate() + 1);\r\n\r\n since = new Date(todayStart);\r\n\r\n since.setDate(since.getDate() - (n - 1));\r\n\r\n until = todayEnd;\r\n\r\n } else {\r\n\r\n since = sinceParam ? new Date(sinceParam) : undefined;\r\n\r\n if (untilParam) {\r\n\r\n if (untilParam.length === 10) {\r\n\r\n const d = new Date(untilParam + \"T12:00:00Z\");\r\n\r\n d.setUTCDate(d.getUTCDate() + 1);\r\n\r\n until = d;\r\n\r\n } else {\r\n\r\n until = new Date(untilParam);\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n\r\n\r\n let sourceUrls: string[] | undefined;\r\n\r\n if (ref) {\r\n\r\n sourceUrls = undefined;\r\n\r\n } else if (subscribed) {\r\n\r\n sourceUrls = await getAllSubscriptionRefs();\r\n\r\n }\r\n\r\n\r\n\r\n if (sourceUrls?.length === 0) {\r\n\r\n const xml = feedItemsToRssXml([], new URL(c.req.url).href, lng, {\r\n\r\n channelTitle: title ?? \"RSS 订阅\",\r\n\r\n channelDesc: \"无匹配条目\",\r\n\r\n });\r\n\r\n return c.body(xml, 200, { \"Content-Type\": \"application/rss+xml; charset=utf-8\" });\r\n\r\n }\r\n\r\n\r\n\r\n const result = await queryItems({\r\n\r\n sourceUrl: sourceUrls ? undefined : ref,\r\n\r\n sourceUrls,\r\n\r\n author,\r\n\r\n q: search,\r\n\r\n tags,\r\n\r\n since,\r\n\r\n until,\r\n\r\n limit,\r\n\r\n offset,\r\n\r\n });\r\n\r\n const feedItems = result.items.map((dbItem) => ({\r\n\r\n guid: dbItem.id,\r\n\r\n title: dbItem.title ?? \"\",\r\n\r\n link: dbItem.url,\r\n\r\n pubDate: dbItem.pub_date ? new Date(dbItem.pub_date) : new Date(),\r\n\r\n author: dbItem.author ?? undefined,\r\n\r\n summary: dbItem.summary ?? undefined,\r\n\r\n content: dbItem.content ?? undefined,\r\n\r\n imageUrl: dbItem.image_url ?? undefined,\r\n\r\n tags: dbItem.tags ?? undefined,\r\n\r\n sourceRef: dbItem.source_url,\r\n\r\n translations: dbItem.translations ?? undefined,\r\n\r\n }));\r\n\r\n\r\n\r\n const rssUrl = new URL(c.req.url);\r\n\r\n const channelTitle = title ?? \"RSS 订阅\";\r\n\r\n const xml = feedItemsToRssXml(feedItems, rssUrl.href, lng, {\r\n\r\n channelTitle,\r\n\r\n channelDesc: `来自 ${rssUrl.href} 的订阅`,\r\n\r\n });\r\n\r\n return c.body(xml, 200, {\r\n\r\n \"Content-Type\": \"application/rss+xml; charset=utf-8\",\r\n\r\n });\r\n\r\n });\r\n\r\n\r\n\r\n /** 按 URL 抓取式 RSS:/rss/https://... 从信源实时抓取 */\r\n\r\n app.get(\"/rss/*\", async (c) => {\r\n\r\n const url = parseUrlFromPath(c.req.path, \"/rss\");\r\n\r\n if (!url) return c.text(\"无效 URL,格式: /rss/https://... 或 /rss/www.xiaohongshu.com/...\", 400);\r\n\r\n try {\r\n\r\n const headlessParam = c.req.query(\"headless\");\r\n\r\n const headless = headlessParam === \"false\" || headlessParam === \"0\" ? false : undefined;\r\n\r\n const lng = c.req.query(\"lng\") ?? undefined;\r\n\r\n const httpId = \"rss-\" + createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\r\n\r\n const { items } = await scheduler.schedule(\r\n\r\n SOURCES_GROUP,\r\n\r\n httpId,\r\n\r\n () => getItems(url, { cacheDir: CACHE_DIR, headless, lng }),\r\n\r\n );\r\n\r\n const xml = feedItemsToRssXml(items, url, lng);\r\n\r\n return c.body(xml, 200, {\r\n\r\n \"Content-Type\": \"application/rss+xml; charset=utf-8\",\r\n\r\n });\r\n\r\n } catch (err) {\r\n\r\n if (err instanceof AuthRequiredError) {\r\n\r\n const html = await render401(url);\r\n\r\n return c.html(html, 401);\r\n\r\n }\r\n\r\n if (err instanceof NotFoundError) {\r\n\r\n const html = await readStaticHtml(\"404\", \"<!DOCTYPE html><html><head><meta charset=\\\"utf-8\\\"><title>404</title></head><body><h1>404 未找到</h1></body></html>\");\r\n\r\n return c.html(html, 404);\r\n\r\n }\r\n\r\n const msg = err instanceof Error ? err.message : String(err);\r\n\r\n return c.text(`生成 RSS 失败: ${msg}`, 500);\r\n\r\n }\r\n\r\n });\r\n\r\n}\r\n\r\n","// 生产环境:在同一端口托管 SvelteKit 静态构建(adapter-static + 200.html SPA fallback)\r\n\r\nimport { existsSync } from \"node:fs\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { join, relative } from \"node:path\";\r\nimport { serveStatic } from \"@hono/node-server/serve-static\";\r\nimport type { Context, Hono } from \"hono\";\r\nimport { PACKAGE_ROOT } from \"../packageRoot.js\";\r\n\r\n/** 与 svelte.config.js 中 adapter-static 的 pages/assets 输出目录一致 */\r\nexport function getWebUiBuildDir(): string {\r\n const w = process.env.WEBUI_BUILD_DIR?.trim();\r\n if (w) {\r\n if (w.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(w)) return w;\r\n return join(process.cwd(), w);\r\n }\r\n return join(PACKAGE_ROOT, \"webui/build\");\r\n}\r\n\r\n/** 仅后端接口路径,不走静态/SPA;注意 /admin 为前端路由,仅 /admin/parse、/admin/extractor 为后端 */\r\nfunction isBackendOnlyPath(pathname: string): boolean {\r\n if (pathname.startsWith(\"/api\")) return true;\r\n if (pathname.startsWith(\"/rss\")) return true;\r\n if (pathname.startsWith(\"/auth\")) return true;\r\n if (pathname.startsWith(\"/admin/parse\") || pathname.startsWith(\"/admin/extractor\")) return true;\r\n return false;\r\n}\r\n\r\n/** 明显是静态资源路径但磁盘上无对应文件时,不应返回 200.html */\r\nfunction looksLikeStaticAsset(pathname: string): boolean {\r\n return /\\.[a-zA-Z0-9]{1,12}$/.test(pathname);\r\n}\r\n\r\n/**\r\n * 在已注册全部 API 路由之后调用。\r\n * `serveStatic` 的 root 需为相对 cwd 的路径(见 @hono/node-server/serve-static)。\r\n */\r\nexport function registerWebUiRoutes(app: Hono): void {\r\n const absRoot = getWebUiBuildDir();\r\n if (!existsSync(absRoot)) {\r\n console.warn(\r\n \"未找到 WebUI 构建目录,跳过根路径静态托管:\",\r\n absRoot,\r\n \"(构建前端:pnpm run webui:build)\",\r\n );\r\n return;\r\n }\r\n\r\n const relRoot = relative(process.cwd(), absRoot).replace(/\\\\/g, \"/\");\r\n const staticRoot =\r\n relRoot === \"\" || relRoot === \".\"\r\n ? \".\"\r\n : relRoot.startsWith(\".\") || relRoot.startsWith(\"/\") || /^[A-Za-z]:/.test(relRoot)\r\n ? relRoot\r\n : `./${relRoot}`;\r\n\r\n const staticMw = serveStatic({\r\n root: staticRoot,\r\n index: \"200.html\",\r\n });\r\n\r\n app.use(\"*\", async (c, next) => {\r\n if (isBackendOnlyPath(c.req.path)) return next();\r\n return staticMw(c, next);\r\n });\r\n\r\n const spaFallback = async (c: Context) => {\r\n const p = c.req.path;\r\n if (isBackendOnlyPath(p)) return c.notFound();\r\n if (looksLikeStaticAsset(p)) return c.notFound();\r\n try {\r\n const html = await readFile(join(absRoot, \"200.html\"), \"utf-8\");\r\n return c.html(html);\r\n } catch {\r\n return c.notFound();\r\n }\r\n };\r\n\r\n app.get(\"*\", spaFallback);\r\n}\r\n","// App 入口:Hono 服务,与 feeder 解耦;可替换为 Express 等\r\n\r\nimport \"dotenv/config\";\r\nimport { watch } from \"node:fs\";\r\nimport { networkInterfaces } from \"node:os\";\r\nimport { serve } from \"@hono/node-server\";\r\nimport { Hono } from \"hono\";\r\nimport { cors } from \"hono/cors\";\r\nimport { initSources as initSites } from \"../scraper/sources/index.js\";\r\nimport { initScheduler } from \"../scraper/scheduler/index.js\";\r\nimport { initUserDir, BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR, CACHE_DIR } from \"../config/paths.js\";\r\nimport { logger } from \"../core/logger/index.js\";\r\nimport { registerApiRoutes } from \"./routes/api/index.js\";\r\nimport { registerAuthRoutes } from \"./routes/auth.js\";\r\nimport { registerAdminRoutes } from \"./routes/admin.js\";\r\nimport { registerRssRoutes } from \"./routes/rss.js\";\r\nimport { registerWebUiRoutes } from \"./webui.js\";\r\n\r\nconst PORT = Number(process.env.PORT) || 18473;\r\nconst IS_DEV = process.env.NODE_ENV === \"development\" || process.argv.includes(\"--watch\");\r\nconst PLUGIN_WATCH_EXTS = [\".rssany.js\", \".rssany.ts\"];\r\n\r\nfunction createApp(): Hono {\r\n const app = new Hono();\r\n\r\n app.use(\r\n \"*\",\r\n cors({\r\n origin: \"*\",\r\n allowMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"PATCH\"],\r\n allowHeaders: [\"Content-Type\", \"Authorization\", \"X-Admin-Token\"],\r\n }),\r\n );\r\n\r\n registerApiRoutes(app);\r\n registerAuthRoutes(app);\r\n registerAdminRoutes(app);\r\n registerRssRoutes(app);\r\n registerWebUiRoutes(app);\r\n\r\n return app;\r\n}\r\n\r\nfunction watchPlugins(): void {\r\n let reloadTimer: NodeJS.Timeout | null = null;\r\n const debouncedReload = async () => {\r\n if (reloadTimer) clearTimeout(reloadTimer);\r\n reloadTimer = setTimeout(async () => {\r\n try {\r\n await initSites();\r\n } catch (err) {\r\n logger.error(\"plugin\", \"插件重新加载失败\", { err: err instanceof Error ? err.message : String(err) });\r\n }\r\n }, 300);\r\n };\r\n for (const dir of [BUILTIN_PLUGINS_DIR, USER_PLUGINS_DIR]) {\r\n const watcher = watch(dir, { recursive: true }, (eventType, filename) => {\r\n if (!filename || !PLUGIN_WATCH_EXTS.some((ext) => filename.endsWith(ext))) return;\r\n if (eventType === \"rename\" || eventType === \"change\") debouncedReload();\r\n });\r\n watcher.on(\"error\", (err) => {\r\n logger.warn(\"plugin\", \"插件目录监听错误\", { dir, err: err.message });\r\n });\r\n }\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n await initUserDir();\r\n await initSites();\r\n await initScheduler(CACHE_DIR);\r\n const app = createApp();\r\n const server = serve({ fetch: app.fetch, port: PORT, hostname: \"0.0.0.0\" });\r\n server.setMaxListeners(32);\r\n console.log(`服务已启动 http://127.0.0.1:${PORT}/(API + 静态前端,需先 pnpm run webui:build)`);\r\n const lanIp = Object.values(networkInterfaces()).flat().find((iface) => iface?.family === \"IPv4\" && !iface.internal)?.address;\r\n if (lanIp) console.log(`局域网访问 http://${lanIp}:${PORT}/`);\r\n if (IS_DEV) {\r\n watchPlugins();\r\n }\r\n}\r\nmain();\r\n"],"names":["s","now","mergeSourceStatsRows","base","resolve","authenticated","cacherCacheKey","fetchHtmlFn","SYSTEM","parseSteps","cronValidate","tasks","cronSchedule","runNow","scheduler.unscheduleGroup","scheduler.validateCron","scheduler.schedule","PORT","scheduler.getGroupStats","parseSubscribedFlag","taskStore.getTask","taskStore.createTask","taskStore.setTaskRunning","taskStore.setTaskDone","taskStore.setTaskError","mime","xml","initSites"],"mappings":"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"}
|