rssany 0.3.1 → 0.3.3

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.
Files changed (119) hide show
  1. package/.env.example +52 -52
  2. package/README.md +156 -147
  3. package/app/plugins/builtin/email.rssany.js +84 -84
  4. package/app/plugins/builtin/rss.rssany.js +164 -164
  5. package/app/plugins/builtin/xiaohongshu.rssany.js +59 -2
  6. package/app/statics/README.md +7 -7
  7. package/app/webui/build/200.html +36 -36
  8. package/app/webui/build/_app/immutable/assets/0.BLOTwIuF.css +1 -0
  9. package/app/webui/build/_app/immutable/assets/10.CmGYYZFR.css +1 -0
  10. package/app/webui/build/_app/immutable/assets/11.Dkz3VS_N.css +1 -0
  11. package/app/webui/build/_app/immutable/assets/14.BCCBoMGj.css +1 -0
  12. package/app/webui/build/_app/immutable/assets/6.Cm_jpHOq.css +1 -0
  13. package/app/webui/build/_app/immutable/assets/7.CJ3BjogD.css +1 -0
  14. package/app/webui/build/_app/immutable/assets/9.CATKVZ-n.css +1 -0
  15. package/app/webui/build/_app/immutable/assets/{SourcesList.D5Lso0bo.css → SourcesList.ke66uOSi.css} +1 -1
  16. package/app/webui/build/_app/immutable/assets/chevron-down.CV-KWLNP.css +1 -0
  17. package/app/webui/build/_app/immutable/chunks/{CGCMIfh3.js → 4TuV_psf.js} +1 -1
  18. package/app/webui/build/_app/immutable/chunks/{DAdOEnFb.js → B0czyjwj.js} +1 -1
  19. package/app/webui/build/_app/immutable/chunks/{CFwxUBGi.js → B553hBXT.js} +1 -1
  20. package/app/webui/build/_app/immutable/chunks/B8StT3Do.js +6 -0
  21. package/app/webui/build/_app/immutable/chunks/BI_ale1m.js +1 -0
  22. package/app/webui/build/_app/immutable/chunks/BK0ygNWX.js +2 -0
  23. package/app/webui/build/_app/immutable/chunks/BKm6QCwp.js +1 -0
  24. package/app/webui/build/_app/immutable/chunks/BT6b4LcZ.js +36 -0
  25. package/app/webui/build/_app/immutable/chunks/BZY5aksi.js +36 -0
  26. package/app/webui/build/_app/immutable/chunks/{C8umpVpB.js → BnqaikL8.js} +1 -1
  27. package/app/webui/build/_app/immutable/chunks/BsQ08Wq_.js +1 -0
  28. package/app/webui/build/_app/immutable/chunks/C9wTDiHH.js +1 -0
  29. package/app/webui/build/_app/immutable/chunks/{B-CeeY89.js → CAKuIoAf.js} +1 -1
  30. package/app/webui/build/_app/immutable/chunks/CEWi_rGa.js +1 -0
  31. package/app/webui/build/_app/immutable/chunks/{ChUctqXA.js → Cc7aBSsN.js} +1 -1
  32. package/app/webui/build/_app/immutable/chunks/{BAJAS8BI.js → D8G961Hm.js} +1 -1
  33. package/app/webui/build/_app/immutable/chunks/{CS53ooo0.js → DIeahUKq.js} +1 -1
  34. package/app/webui/build/_app/immutable/chunks/DO5OXNYS.js +1 -0
  35. package/app/webui/build/_app/immutable/chunks/Dg_D3pjF.js +1 -0
  36. package/app/webui/build/_app/immutable/chunks/{Dyvi1wBH.js → DptdhtA1.js} +1 -1
  37. package/app/webui/build/_app/immutable/chunks/{ClknbeNl.js → FDS7fbwH.js} +1 -1
  38. package/app/webui/build/_app/immutable/chunks/{CqYSO3Dx.js → GeNMTUn1.js} +1 -1
  39. package/app/webui/build/_app/immutable/chunks/{DCEayuDt.js → IhDlsCxD.js} +1 -1
  40. package/app/webui/build/_app/immutable/chunks/Nd0ktDhd.js +1 -0
  41. package/app/webui/build/_app/immutable/chunks/{D6kzEN_P.js → SvdgnirT.js} +1 -1
  42. package/app/webui/build/_app/immutable/chunks/WW6La7Nt.js +2 -0
  43. package/app/webui/build/_app/immutable/chunks/{DsxvjlCC.js → pd_p3yYy.js} +5 -5
  44. package/app/webui/build/_app/immutable/chunks/rNwPv4DZ.js +1 -0
  45. package/app/webui/build/_app/immutable/entry/app.BKLBG-4w.js +2 -0
  46. package/app/webui/build/_app/immutable/entry/start.D-X6pVtx.js +1 -0
  47. package/app/webui/build/_app/immutable/nodes/{0.DK_mcVDm.js → 0.CJDC_3s9.js} +3 -3
  48. package/app/webui/build/_app/immutable/nodes/1.DsKocFSb.js +1 -0
  49. package/app/webui/build/_app/immutable/nodes/10.BeejAn8z.js +1 -0
  50. package/app/webui/build/_app/immutable/nodes/11.D--uwkk0.js +3 -0
  51. package/app/webui/build/_app/immutable/nodes/12.BLyQ6rUu.js +1 -0
  52. package/app/webui/build/_app/immutable/nodes/13.Cl0WQK13.js +1 -0
  53. package/app/webui/build/_app/immutable/nodes/14.T9l5Rh19.js +1 -0
  54. package/app/webui/build/_app/immutable/nodes/15.DHfwIlBx.js +1 -0
  55. package/app/webui/build/_app/immutable/nodes/{16.zfSe93Ab.js → 16.BKDfR-KV.js} +2 -2
  56. package/app/webui/build/_app/immutable/nodes/17.DofB8HQB.js +1 -0
  57. package/app/webui/build/_app/immutable/nodes/2.BOYqXdCa.js +1 -0
  58. package/app/webui/build/_app/immutable/nodes/3.B9ucbp_W.js +1 -0
  59. package/app/webui/build/_app/immutable/nodes/5.9zgwFV6I.js +2 -0
  60. package/app/webui/build/_app/immutable/nodes/6.Bs32Ieii.js +2 -0
  61. package/app/webui/build/_app/immutable/nodes/7.Cigxrk0v.js +1 -0
  62. package/app/webui/build/_app/immutable/nodes/8.pG10rCF0.js +1 -0
  63. package/app/webui/build/_app/immutable/nodes/9.Bzqb3xHY.js +1 -0
  64. package/app/webui/build/_app/version.json +1 -1
  65. package/bin/rssany.js +55 -3
  66. package/dist/index.js +361 -99
  67. package/dist/index.js.map +1 -1
  68. package/package.json +107 -103
  69. package/scripts/dev.mjs +5 -1
  70. package/scripts/postinstall.mjs +44 -0
  71. package/scripts/reset.mjs +137 -135
  72. package/scripts/user-dir.mjs +52 -0
  73. package/app/webui/build/_app/immutable/assets/0.DsKls1SN.css +0 -1
  74. package/app/webui/build/_app/immutable/assets/10.Dj8_pmut.css +0 -1
  75. package/app/webui/build/_app/immutable/assets/13.Qu_tY6H9.css +0 -1
  76. package/app/webui/build/_app/immutable/assets/5.B-dPiwB7.css +0 -1
  77. package/app/webui/build/_app/immutable/assets/6.B27N7pdA.css +0 -1
  78. package/app/webui/build/_app/immutable/assets/8.Cgji2b15.css +0 -1
  79. package/app/webui/build/_app/immutable/assets/9.BsCIAvn3.css +0 -1
  80. package/app/webui/build/_app/immutable/chunks/6prdYIKP.js +0 -1
  81. package/app/webui/build/_app/immutable/chunks/B2cyTHdf.js +0 -2
  82. package/app/webui/build/_app/immutable/chunks/B6WG2Sd3.js +0 -1
  83. package/app/webui/build/_app/immutable/chunks/BA4Gucnq.js +0 -1
  84. package/app/webui/build/_app/immutable/chunks/BkD3yAYe.js +0 -1
  85. package/app/webui/build/_app/immutable/chunks/C4uF_YIK.js +0 -1
  86. package/app/webui/build/_app/immutable/chunks/CBY2biv-.js +0 -1
  87. package/app/webui/build/_app/immutable/chunks/CVW0ymE1.js +0 -1
  88. package/app/webui/build/_app/immutable/chunks/DJ2e04vK.js +0 -36
  89. package/app/webui/build/_app/immutable/chunks/DL3Q5sfb.js +0 -1
  90. package/app/webui/build/_app/immutable/chunks/DVa8Y-mQ.js +0 -1
  91. package/app/webui/build/_app/immutable/chunks/DkamXS6W.js +0 -36
  92. package/app/webui/build/_app/immutable/chunks/DoRPmqLn.js +0 -2
  93. package/app/webui/build/_app/immutable/chunks/_qj9U-za.js +0 -1
  94. package/app/webui/build/_app/immutable/chunks/vtBo8kBV.js +0 -1
  95. package/app/webui/build/_app/immutable/entry/app.RFfWi3_i.js +0 -2
  96. package/app/webui/build/_app/immutable/entry/start.DU_kyeGS.js +0 -1
  97. package/app/webui/build/_app/immutable/nodes/1.0PRrU2uQ.js +0 -1
  98. package/app/webui/build/_app/immutable/nodes/10.CsxzlUER.js +0 -1
  99. package/app/webui/build/_app/immutable/nodes/11.D-PkhIRW.js +0 -1
  100. package/app/webui/build/_app/immutable/nodes/12.GGf-JLUY.js +0 -1
  101. package/app/webui/build/_app/immutable/nodes/13.DWWcH27k.js +0 -6
  102. package/app/webui/build/_app/immutable/nodes/14.COwSLwDN.js +0 -1
  103. package/app/webui/build/_app/immutable/nodes/15.nDN_AHrs.js +0 -1
  104. package/app/webui/build/_app/immutable/nodes/2.AJd2163d.js +0 -1
  105. package/app/webui/build/_app/immutable/nodes/3.CEVEHuaH.js +0 -1
  106. package/app/webui/build/_app/immutable/nodes/4.BT_N8pCh.js +0 -2
  107. package/app/webui/build/_app/immutable/nodes/5.BZScQ2CH.js +0 -2
  108. package/app/webui/build/_app/immutable/nodes/6.CkFk8X--.js +0 -1
  109. package/app/webui/build/_app/immutable/nodes/7.CuQJk7te.js +0 -1
  110. package/app/webui/build/_app/immutable/nodes/8.DIavWJnU.js +0 -1
  111. package/app/webui/build/_app/immutable/nodes/9.Db30M8x0.js +0 -1
  112. /package/app/webui/build/_app/immutable/assets/{11.qYZMiTb0.css → 12.qYZMiTb0.css} +0 -0
  113. /package/app/webui/build/_app/immutable/assets/{12.DfJcfUWl.css → 13.DfJcfUWl.css} +0 -0
  114. /package/app/webui/build/_app/immutable/assets/{14.DfMfOrS3.css → 15.DfMfOrS3.css} +0 -0
  115. /package/app/webui/build/_app/immutable/assets/{15.nNGjXhCQ.css → 17.nNGjXhCQ.css} +0 -0
  116. /package/app/webui/build/_app/immutable/assets/{4.Di6rvlY-.css → 5.Di6rvlY-.css} +0 -0
  117. /package/app/webui/build/_app/immutable/assets/{7.CrNxmd8B.css → 8.CrNxmd8B.css} +0 -0
  118. /package/app/webui/build/_app/immutable/nodes/{17.BtYZF6FM.js → 18.BtYZF6FM.js} +0 -0
  119. /package/app/webui/build/_app/immutable/nodes/{18.BIzqhTqv.js → 4.BIzqhTqv.js} +0 -0
package/dist/index.js CHANGED
@@ -5,14 +5,14 @@ import { serve } from "@hono/node-server";
5
5
  import { Hono } from "hono";
6
6
  import { cors } from "hono/cors";
7
7
  import { exec } from "node:child_process";
8
+ import { createHash } from "node:crypto";
8
9
  import { join, dirname, basename, resolve, sep, relative } from "node:path";
9
10
  import { promisify } from "node:util";
10
11
  import puppeteerCore from "puppeteer-core";
11
12
  import { parse, NodeType } from "node-html-parser";
12
13
  import { DatabaseSync } from "node:sqlite";
13
- import { mkdir, writeFile, copyFile, access, rename, readFile, readdir, stat, unlink } from "node:fs/promises";
14
+ import { mkdir, rename, writeFile, copyFile, access, readFile, readdir, stat, unlink } from "node:fs/promises";
14
15
  import { fileURLToPath, pathToFileURL } from "node:url";
15
- import { createHash } from "node:crypto";
16
16
  import { JSDOM } from "jsdom";
17
17
  import { Readability } from "@mozilla/readability";
18
18
  import OpenAI from "openai";
@@ -160,7 +160,7 @@ function markPipelineDrop(item) {
160
160
  function isPipelineDroppedItem(item) {
161
161
  return item.extra?.[PIPELINE_DROP_EXTRA_KEY] === true;
162
162
  }
163
- function canonicalHttpSourceRef(ref) {
163
+ function canonicalHttpSourceRef(ref, opts = {}) {
164
164
  const t = ref.trim();
165
165
  if (!t) return t;
166
166
  if (!/^https?:\/\//i.test(t)) return t.toLowerCase();
@@ -172,7 +172,7 @@ function canonicalHttpSourceRef(ref) {
172
172
  if (path.length > 1 && path.endsWith("/")) {
173
173
  path = path.slice(0, -1);
174
174
  }
175
- path = path.toLowerCase();
175
+ if (opts.lowerPathCase) path = path.toLowerCase();
176
176
  return `${protocol}//${host}${path}${u.search}${u.hash}`;
177
177
  } catch {
178
178
  return t.toLowerCase();
@@ -209,8 +209,48 @@ const httpSourceRef = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defin
209
209
  const __dir = dirname(fileURLToPath(import.meta.url));
210
210
  const base = basename(__dir);
211
211
  const PACKAGE_ROOT = base === "app" || base === "dist" ? join(__dir, "..") : __dir;
212
- const envUserDir = process.env.RSSANY_USER_DIR?.trim();
213
- const USER_DIR = envUserDir && envUserDir.length > 0 ? envUserDir : join(homedir(), ".rssany");
212
+ const LEGACY_HOME_DIR = ".rssany";
213
+ function resolveNpmPrefixFromPackageRoot(packageRoot) {
214
+ const normalized = packageRoot.replace(/\\/g, "/");
215
+ const libSuffix = "/lib/node_modules/rssany";
216
+ if (normalized.endsWith(libSuffix)) {
217
+ return packageRoot.slice(0, packageRoot.length - libSuffix.length);
218
+ }
219
+ const flatSuffix = "/node_modules/rssany";
220
+ if (normalized.endsWith(flatSuffix)) {
221
+ return packageRoot.slice(0, packageRoot.length - flatSuffix.length);
222
+ }
223
+ return null;
224
+ }
225
+ function isGlobalNpmInstall(packageRoot) {
226
+ const normalized = packageRoot.replace(/\\/g, "/");
227
+ if (normalized.endsWith("/lib/node_modules/rssany")) return true;
228
+ const globalPatterns = [
229
+ /\/npm\/node_modules\/rssany$/,
230
+ /\/\.local\/lib\/node_modules\/rssany$/,
231
+ /\/\.local\/node_modules\/rssany$/,
232
+ /\/\.npm-global\/lib\/node_modules\/rssany$/,
233
+ /\/\.nvm\/versions\/node\/[^/]+\/lib\/node_modules\/rssany$/,
234
+ /\/\.fnm\/node-versions\/[^/]+\/installation\/lib\/node_modules\/rssany$/
235
+ ];
236
+ return globalPatterns.some((pattern) => pattern.test(normalized));
237
+ }
238
+ function resolveDefaultUserDir(packageRoot) {
239
+ const env = process.env.RSSANY_USER_DIR?.trim();
240
+ if (env) return env;
241
+ const npmPrefix = resolveNpmPrefixFromPackageRoot(packageRoot);
242
+ if (npmPrefix && isGlobalNpmInstall(packageRoot)) {
243
+ return join(npmPrefix, "var", "rssany");
244
+ }
245
+ if (!packageRoot.replace(/\\/g, "/").includes("/node_modules/")) {
246
+ return join(packageRoot, ".rssany");
247
+ }
248
+ return join(homedir(), LEGACY_HOME_DIR);
249
+ }
250
+ function getLegacyHomeUserDir() {
251
+ return join(homedir(), LEGACY_HOME_DIR);
252
+ }
253
+ const USER_DIR = resolveDefaultUserDir(PACKAGE_ROOT);
214
254
  const DATA_DIR = join(USER_DIR, "data");
215
255
  const CACHE_DIR = process.env.CACHE_DIR ?? join(USER_DIR, "cache");
216
256
  join(USER_DIR, "sites.json");
@@ -279,7 +319,24 @@ async function ensureUserDirPackageJsonForPlugins() {
279
319
  });
280
320
  }
281
321
  }
322
+ async function migrateLegacyHomeUserDir() {
323
+ const legacy = getLegacyHomeUserDir();
324
+ if (USER_DIR === legacy) return;
325
+ if (await pathExists(USER_DIR)) return;
326
+ if (!await pathExists(legacy)) return;
327
+ try {
328
+ await rename(legacy, USER_DIR);
329
+ logger.info("config", "已从 ~/.rssany 迁移用户数据", { from: legacy, to: USER_DIR });
330
+ } catch (err) {
331
+ logger.warn("config", "从 ~/.rssany 迁移失败", {
332
+ from: legacy,
333
+ to: USER_DIR,
334
+ err: err instanceof Error ? err.message : String(err)
335
+ });
336
+ }
337
+ }
282
338
  async function initUserDir() {
339
+ await migrateLegacyHomeUserDir();
283
340
  await mkdir(USER_DIR, { recursive: true });
284
341
  await mkdir(DATA_DIR, { recursive: true });
285
342
  await mkdir(CACHE_DIR, { recursive: true });
@@ -1081,9 +1138,14 @@ function launchArgs(config) {
1081
1138
  }
1082
1139
  return base2;
1083
1140
  }
1084
- function getUserDataDir(cacheDir) {
1141
+ function proxyProfileName(proxy) {
1142
+ if (!proxy) return "main";
1143
+ const hash = createHash("sha1").update(proxy).digest("hex").slice(0, 12);
1144
+ return `main_proxy_${hash}`;
1145
+ }
1146
+ function getUserDataDir(cacheDir, proxy) {
1085
1147
  if (!cacheDir) return void 0;
1086
- return join(cacheDir, "browser_data", "main");
1148
+ return join(cacheDir, "browser_data", proxyProfileName(proxy));
1087
1149
  }
1088
1150
  function isAlreadyRunningError(e) {
1089
1151
  const msg = e instanceof Error ? e.message : String(e);
@@ -1174,13 +1236,141 @@ function isFrameDetachedError(e) {
1174
1236
  return /detached|Navigating frame was detached|Session closed/i.test(msg);
1175
1237
  }
1176
1238
  const sharedBrowsers = /* @__PURE__ */ new Map();
1239
+ const managedBrowsers = /* @__PURE__ */ new WeakSet();
1240
+ const infoPagePromises = /* @__PURE__ */ new WeakMap();
1241
+ const RSSANY_INFO_PAGE_PREFIX = "data:text/html;charset=utf-8,";
1242
+ function rssAnyInfoPageUrl() {
1243
+ const html = `<!doctype html>
1244
+ <html lang="zh-CN">
1245
+ <head>
1246
+ <meta charset="utf-8">
1247
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1248
+ <title>RssAny 浏览器</title>
1249
+ <style>
1250
+ :root {
1251
+ color-scheme: light dark;
1252
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1253
+ background: #f7f7f5;
1254
+ color: #202124;
1255
+ }
1256
+ body {
1257
+ margin: 0;
1258
+ min-height: 100vh;
1259
+ display: grid;
1260
+ place-items: center;
1261
+ padding: 32px;
1262
+ box-sizing: border-box;
1263
+ }
1264
+ main {
1265
+ max-width: 680px;
1266
+ border: 1px solid #d7d7d1;
1267
+ border-radius: 8px;
1268
+ background: #ffffff;
1269
+ padding: 28px 32px;
1270
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
1271
+ }
1272
+ h1 {
1273
+ margin: 0 0 12px;
1274
+ font-size: 24px;
1275
+ line-height: 1.25;
1276
+ }
1277
+ p {
1278
+ margin: 10px 0 0;
1279
+ font-size: 15px;
1280
+ line-height: 1.7;
1281
+ }
1282
+ code {
1283
+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
1284
+ font-size: 13px;
1285
+ background: #f0f0ec;
1286
+ padding: 2px 6px;
1287
+ border-radius: 4px;
1288
+ }
1289
+ @media (prefers-color-scheme: dark) {
1290
+ :root {
1291
+ background: #181a1b;
1292
+ color: #f1f1ed;
1293
+ }
1294
+ main {
1295
+ background: #202325;
1296
+ border-color: #3a3d3f;
1297
+ box-shadow: none;
1298
+ }
1299
+ code {
1300
+ background: #303335;
1301
+ }
1302
+ }
1303
+ </style>
1304
+ </head>
1305
+ <body>
1306
+ <main>
1307
+ <h1>这是 RssAny 创建的浏览器</h1>
1308
+ <p>RssAny 会复用这个浏览器执行订阅抓取、站点登录、页面解析和调试打开等任务。</p>
1309
+ <p>抓取任务会临时打开自己的标签页,完成后自动关闭。请保留此页面,以便区分 RssAny 管理的浏览器窗口。</p>
1310
+ <p>用户数据与 cookies 保存在 <code>.rssany/cache/browser_data</code> 对应的浏览器 profile 中。</p>
1311
+ </main>
1312
+ </body>
1313
+ </html>`;
1314
+ return `${RSSANY_INFO_PAGE_PREFIX}${encodeURIComponent(html)}`;
1315
+ }
1316
+ function isRssAnyInfoPage(page) {
1317
+ return page.url().startsWith(RSSANY_INFO_PAGE_PREFIX);
1318
+ }
1319
+ function isBlankPage(page) {
1320
+ const url = page.url();
1321
+ return url === "about:blank" || url === "" || url.startsWith("chrome://newtab");
1322
+ }
1323
+ async function cleanupExtraBlankPages(browser) {
1324
+ if (!isBrowserConnected(browser)) return;
1325
+ const pages = await browser.pages().catch(() => []);
1326
+ for (const page of pages) {
1327
+ if (page.isClosed() || isRssAnyInfoPage(page) || !isBlankPage(page)) continue;
1328
+ if (pages.length <= 1) continue;
1329
+ await page.close().catch(() => {
1330
+ });
1331
+ }
1332
+ }
1333
+ async function ensureRssAnyInfoPage(browser) {
1334
+ if (!isBrowserConnected(browser)) return;
1335
+ const current = infoPagePromises.get(browser);
1336
+ if (current) return current;
1337
+ const promise = (async () => {
1338
+ const pages = await browser.pages().catch(() => []);
1339
+ if (pages.some((page2) => !page2.isClosed() && isRssAnyInfoPage(page2))) {
1340
+ await cleanupExtraBlankPages(browser);
1341
+ return;
1342
+ }
1343
+ const page = await browser.newPage();
1344
+ await page.goto(rssAnyInfoPageUrl(), { waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
1345
+ });
1346
+ await cleanupExtraBlankPages(browser);
1347
+ })().finally(() => {
1348
+ infoPagePromises.delete(browser);
1349
+ });
1350
+ infoPagePromises.set(browser, promise);
1351
+ return promise;
1352
+ }
1353
+ function setupRssAnyBrowserLifecycle(browser, headless) {
1354
+ if (headless || managedBrowsers.has(browser)) return;
1355
+ managedBrowsers.add(browser);
1356
+ browser.on("targetcreated", () => {
1357
+ setTimeout(() => {
1358
+ cleanupExtraBlankPages(browser).catch(() => {
1359
+ });
1360
+ }, 2500);
1361
+ });
1362
+ browser.on("targetdestroyed", () => {
1363
+ setTimeout(() => {
1364
+ ensureRssAnyInfoPage(browser).catch(() => {
1365
+ });
1366
+ }, 300);
1367
+ });
1368
+ }
1177
1369
  function browserKey(config) {
1178
- const wantHeadless = config.headless !== false;
1179
1370
  const executablePath = config.chromeExecutablePath ?? process.env.CHROME_PATH ?? findChromeExecutable() ?? "";
1180
- const userDataDir = getUserDataDir(config.cacheDir);
1181
1371
  const proxy = resolveProxy(config) ?? "";
1372
+ const userDataDir = getUserDataDir(config.cacheDir, proxy);
1182
1373
  return JSON.stringify({
1183
- headless: wantHeadless,
1184
1374
  userDataDir: userDataDir ? resolve(userDataDir) : "",
1185
1375
  proxy,
1186
1376
  executablePath
@@ -1195,7 +1385,8 @@ async function launchBrowser(config) {
1195
1385
  if (!executablePath) {
1196
1386
  throw new Error("未找到 Chrome 可执行文件,请安装 Google Chrome 或设置 CHROME_PATH 环境变量");
1197
1387
  }
1198
- const userDataDir = getUserDataDir(config.cacheDir);
1388
+ const proxy = resolveProxy(config);
1389
+ const userDataDir = getUserDataDir(config.cacheDir, proxy);
1199
1390
  const maxRetries = 2;
1200
1391
  let lastErr;
1201
1392
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -1211,7 +1402,7 @@ async function launchBrowser(config) {
1211
1402
  }
1212
1403
  return await puppeteerCore.launch({
1213
1404
  headless: wantHeadless,
1214
- args: launchArgs({ proxy: config.proxy, headless: wantHeadless }),
1405
+ args: launchArgs({ proxy, headless: wantHeadless }),
1215
1406
  userDataDir,
1216
1407
  executablePath,
1217
1408
  ignoreDefaultArgs: ["--enable-automation"]
@@ -1224,7 +1415,7 @@ async function launchBrowser(config) {
1224
1415
  if (isAlreadyRunningError(e)) {
1225
1416
  const dir = userDataDir ?? "browser_data/main";
1226
1417
  throw new Error(
1227
- `Chrome 的 profile 目录已被占用(${dir})。通常是因为上次未正常退出或同时运行了多个本服务实例。请关闭占用该目录的 Chrome 进程后重试,或设置环境变量 CACHE_DIR 使用不同缓存目录。`
1418
+ `Chrome 的 profile 目录已被占用(${dir})。通常是因为上次未正常退出,或另一个服务实例正在使用同一个缓存目录。请关闭占用该目录的 Chrome 进程后重试,或设置环境变量 CACHE_DIR 使用不同缓存目录。`
1228
1419
  );
1229
1420
  }
1230
1421
  throw e;
@@ -1233,24 +1424,53 @@ async function launchBrowser(config) {
1233
1424
  throw lastErr;
1234
1425
  }
1235
1426
  async function getOrCreateBrowser(config) {
1236
- const key = browserKey(config);
1427
+ const normalizedConfig = { ...config, proxy: resolveProxy(config) };
1428
+ const key = browserKey(normalizedConfig);
1429
+ const wantHeadless = normalizedConfig.headless !== false;
1237
1430
  const current = sharedBrowsers.get(key);
1238
- if (isBrowserConnected(current?.browser)) {
1239
- return current.browser;
1240
- }
1241
1431
  if (current?.promise) {
1242
- return current.promise;
1432
+ const browser = await current.promise;
1433
+ if (isBrowserConnected(browser) && (wantHeadless || current.headless === false)) {
1434
+ if (!wantHeadless) {
1435
+ await ensureRssAnyInfoPage(browser);
1436
+ }
1437
+ return browser;
1438
+ }
1439
+ if (isBrowserConnected(browser)) {
1440
+ await browser.close().catch(() => {
1441
+ });
1442
+ }
1443
+ if (sharedBrowsers.get(key) === current) {
1444
+ sharedBrowsers.delete(key);
1445
+ }
1446
+ } else if (isBrowserConnected(current?.browser)) {
1447
+ if (wantHeadless || current.headless === false) {
1448
+ if (!wantHeadless) {
1449
+ await ensureRssAnyInfoPage(current.browser);
1450
+ }
1451
+ return current.browser;
1452
+ }
1453
+ await current.browser.close().catch(() => {
1454
+ });
1455
+ if (sharedBrowsers.get(key) === current) {
1456
+ sharedBrowsers.delete(key);
1457
+ }
1243
1458
  }
1244
1459
  const slot = {};
1245
- const promise = launchBrowser({ ...config, proxy: resolveProxy(config) }).then((browser) => {
1460
+ const promise = launchBrowser(normalizedConfig).then((browser) => {
1246
1461
  slot.browser = browser;
1247
1462
  slot.promise = void 0;
1463
+ slot.headless = wantHeadless;
1464
+ setupRssAnyBrowserLifecycle(browser, wantHeadless);
1248
1465
  browser.once("disconnected", () => {
1249
1466
  if (sharedBrowsers.get(key)?.browser === browser) {
1250
1467
  sharedBrowsers.delete(key);
1251
1468
  }
1252
1469
  });
1253
- return browser;
1470
+ if (wantHeadless) {
1471
+ return browser;
1472
+ }
1473
+ return ensureRssAnyInfoPage(browser).then(() => browser);
1254
1474
  }).catch((err) => {
1255
1475
  if (sharedBrowsers.get(key) === slot) {
1256
1476
  sharedBrowsers.delete(key);
@@ -1284,32 +1504,41 @@ async function preCheckAuth(authFlow, cacheDir, opts) {
1284
1504
  }
1285
1505
  async function ensureAuth(authFlow, cacheDir, opts) {
1286
1506
  const { checkAuth, loginUrl, loginTimeoutMs = 60 * 1e3, pollIntervalMs = 2e3 } = authFlow;
1287
- const browser = await launchBrowser({ headless: false, cacheDir, proxy: resolveProxy(opts) });
1507
+ const browser = await getOrCreateBrowser({ headless: false, cacheDir, proxy: resolveProxy(opts) });
1508
+ const page = await browser.newPage();
1288
1509
  try {
1289
- const page = await browser.newPage();
1290
- try {
1291
- await setupPage(page, false);
1292
- await applyProxyAuthToPage(page, opts);
1293
- await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
1294
- await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1295
- const authenticated = await checkAuth(page, page.url());
1296
- if (authenticated) return;
1297
- const startTime = Date.now();
1298
- while (Date.now() - startTime < loginTimeoutMs) {
1299
- await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
1300
- const authenticated2 = await checkAuth(page, page.url());
1301
- if (authenticated2) return;
1302
- }
1303
- throw new Error(`登录超时(${loginTimeoutMs}ms)`);
1304
- } finally {
1305
- await page.close().catch(() => {
1306
- });
1510
+ await setupPage(page, false);
1511
+ await applyProxyAuthToPage(page, opts);
1512
+ await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
1513
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
1514
+ const authenticated = await checkAuth(page, page.url());
1515
+ if (authenticated) return;
1516
+ const startTime = Date.now();
1517
+ while (Date.now() - startTime < loginTimeoutMs) {
1518
+ await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
1519
+ const authenticated2 = await checkAuth(page, page.url());
1520
+ if (authenticated2) return;
1307
1521
  }
1522
+ throw new Error(`登录超时(${loginTimeoutMs}ms)`);
1308
1523
  } finally {
1309
- await browser.close().catch(() => {
1524
+ await page.close().catch(() => {
1310
1525
  });
1311
1526
  }
1312
1527
  }
1528
+ async function openBrowserPage(url, cacheDir, opts) {
1529
+ const browser = await getOrCreateBrowser({ headless: false, cacheDir, proxy: resolveProxy(opts) });
1530
+ const page = await browser.newPage();
1531
+ try {
1532
+ await setupPage(page, false);
1533
+ await applyProxyAuthToPage(page, opts);
1534
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
1535
+ return page;
1536
+ } catch (err) {
1537
+ await page.close().catch(() => {
1538
+ });
1539
+ throw err;
1540
+ }
1541
+ }
1313
1542
  async function fetchHtml(url, config = {}) {
1314
1543
  const {
1315
1544
  timeoutMs,
@@ -2133,30 +2362,52 @@ async function initSources() {
2133
2362
  function resolveRef(src) {
2134
2363
  return src.ref ?? src.url ?? "";
2135
2364
  }
2136
- async function readGlobalProxyFromConfig() {
2137
- try {
2138
- const raw = await readFile(CONFIG_PATH, "utf-8");
2139
- const j = JSON.parse(raw);
2140
- if (typeof j.globalProxy === "string") {
2141
- const t = j.globalProxy.trim();
2142
- return t.length > 0 ? t : void 0;
2143
- }
2144
- } catch {
2365
+ function normalizeProxyList(raw) {
2366
+ if (!Array.isArray(raw)) return [];
2367
+ const seen = /* @__PURE__ */ new Set();
2368
+ const out = [];
2369
+ for (const v of raw) {
2370
+ if (typeof v !== "string") continue;
2371
+ const t = v.trim();
2372
+ if (!t || seen.has(t)) continue;
2373
+ seen.add(t);
2374
+ out.push(t);
2145
2375
  }
2146
- return void 0;
2376
+ return out;
2147
2377
  }
2148
- async function saveGlobalProxyToConfig(proxy) {
2149
- let root = {};
2378
+ async function readConfigRoot() {
2150
2379
  try {
2151
2380
  const raw = await readFile(CONFIG_PATH, "utf-8");
2152
- root = JSON.parse(raw);
2381
+ return JSON.parse(raw);
2153
2382
  } catch {
2383
+ return {};
2154
2384
  }
2155
- const t = proxy.trim();
2156
- if (t.length === 0) {
2385
+ }
2386
+ async function readProxySettingsFromConfig() {
2387
+ const root = await readConfigRoot();
2388
+ const globalProxy = typeof root.globalProxy === "string" ? root.globalProxy.trim() : "";
2389
+ return {
2390
+ globalProxy,
2391
+ proxyList: normalizeProxyList(root.proxyList)
2392
+ };
2393
+ }
2394
+ async function readGlobalProxyFromConfig() {
2395
+ const { globalProxy } = await readProxySettingsFromConfig();
2396
+ return globalProxy.length > 0 ? globalProxy : void 0;
2397
+ }
2398
+ async function saveProxySettingsToConfig(settings) {
2399
+ const root = await readConfigRoot();
2400
+ const globalProxy = settings.globalProxy.trim();
2401
+ const proxyList = normalizeProxyList(settings.proxyList);
2402
+ if (globalProxy) {
2403
+ root.globalProxy = globalProxy;
2404
+ } else {
2157
2405
  delete root.globalProxy;
2406
+ }
2407
+ if (proxyList.length > 0) {
2408
+ root.proxyList = proxyList;
2158
2409
  } else {
2159
- root.globalProxy = t;
2410
+ delete root.proxyList;
2160
2411
  }
2161
2412
  await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2) + "\n", "utf-8");
2162
2413
  }
@@ -3533,23 +3784,7 @@ function registerSourcesRoutes(app) {
3533
3784
  const source = getSource(url);
3534
3785
  const merged = await getEffectiveProxyForListUrl(url, source);
3535
3786
  const proxy = resolveProxy({ proxy: merged });
3536
- void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy }).then(async (browser) => {
3537
- try {
3538
- const page = await browser.newPage();
3539
- await applyProxyAuthToPage(page, { proxy: merged });
3540
- 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";
3541
- await page.setUserAgent(realUserAgent);
3542
- await page.setViewport({ width: 1366, height: 960 });
3543
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
3544
- page.once("close", () => {
3545
- void browser.close().catch(() => {
3546
- });
3547
- });
3548
- } catch {
3549
- await browser.close().catch(() => {
3550
- });
3551
- }
3552
- }).catch(() => {
3787
+ void openBrowserPage(url, CACHE_DIR, { proxy }).catch(() => {
3553
3788
  });
3554
3789
  return c.json({ ok: true, message: "已在爬虫浏览器中打开" });
3555
3790
  } catch {
@@ -3824,16 +4059,15 @@ function registerLlmRoutes(app) {
3824
4059
  }
3825
4060
  function registerProxySettingsRoutes(app) {
3826
4061
  app.get("/api/proxy", requireAdmin(), async (c) => {
3827
- const globalProxy = await readGlobalProxyFromConfig() ?? "";
3828
- return c.json({ globalProxy });
4062
+ return c.json(await readProxySettingsFromConfig());
3829
4063
  });
3830
4064
  app.put("/api/proxy", requireAdmin(), async (c) => {
3831
4065
  try {
3832
4066
  const body = await c.req.json().catch(() => ({}));
3833
4067
  const globalProxy = typeof body.globalProxy === "string" ? body.globalProxy : "";
3834
- await saveGlobalProxyToConfig(globalProxy);
3835
- const saved = await readGlobalProxyFromConfig() ?? "";
3836
- return c.json({ ok: true, globalProxy: saved });
4068
+ const proxyList = Array.isArray(body.proxyList) ? body.proxyList.filter((v) => typeof v === "string") : [];
4069
+ await saveProxySettingsToConfig({ globalProxy, proxyList });
4070
+ return c.json({ ok: true, ...await readProxySettingsFromConfig() });
3837
4071
  } catch (err) {
3838
4072
  return c.json(
3839
4073
  { ok: false, message: err instanceof Error ? err.message : String(err) },
@@ -3920,7 +4154,7 @@ const CACHE_KEY_PREFIX = "feed-favicon:v1:";
3920
4154
  const CACHE_MAX_AGE_SEC = 3 * 24 * 60 * 60;
3921
4155
  const CACHE_MAX_AGE_MS = CACHE_MAX_AGE_SEC * 1e3;
3922
4156
  const CACHE_CONTROL = `public, max-age=${CACHE_MAX_AGE_SEC}`;
3923
- const FETCH_TIMEOUT_MS = 6e3;
4157
+ const FETCH_TIMEOUT_MS$1 = 6e3;
3924
4158
  const MAX_ICON_BYTES = 2 * 1024 * 1024;
3925
4159
  const MAX_HTML_BYTES = 512 * 1024;
3926
4160
  const inflightByDomain = /* @__PURE__ */ new Map();
@@ -4008,7 +4242,7 @@ async function fetchHtmlPage(url) {
4008
4242
  Accept: "text/html,application/xhtml+xml;q=0.9,*/*;q=0.1",
4009
4243
  "User-Agent": "Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon"
4010
4244
  },
4011
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4245
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$1)
4012
4246
  });
4013
4247
  if (!upstream.ok) return null;
4014
4248
  const ab = await upstream.arrayBuffer();
@@ -4105,7 +4339,7 @@ async function fetchIconCandidate(url) {
4105
4339
  Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
4106
4340
  "User-Agent": "Mozilla/5.0 (compatible; RssAny/1.0; +https://github.com/rssany/rssany) favicon"
4107
4341
  },
4108
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4342
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$1)
4109
4343
  });
4110
4344
  } catch {
4111
4345
  return null;
@@ -4216,9 +4450,53 @@ function registerFeedFaviconRoutes(app) {
4216
4450
  });
4217
4451
  });
4218
4452
  }
4453
+ const MAX_BYTES = 5 * 1024 * 1024;
4454
+ const FETCH_TIMEOUT_MS = 12e3;
4455
+ function isAllowedImageUrl(raw) {
4456
+ try {
4457
+ const url = new URL(raw);
4458
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
4459
+ if (!url.hostname) return null;
4460
+ return url;
4461
+ } catch {
4462
+ return null;
4463
+ }
4464
+ }
4465
+ function registerCoverImgRoutes(app) {
4466
+ app.get("/api/cover-img", async (c) => {
4467
+ const raw = (c.req.query("url") ?? "").trim();
4468
+ if (!raw) return c.text("missing url", 400);
4469
+ const url = isAllowedImageUrl(raw);
4470
+ if (!url) return c.text("invalid url", 400);
4471
+ try {
4472
+ const res = await fetch(url.toString(), {
4473
+ redirect: "follow",
4474
+ headers: {
4475
+ Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
4476
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
4477
+ Referer: `${url.origin}/`
4478
+ },
4479
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4480
+ });
4481
+ if (!res.ok) return c.text("upstream error", 502);
4482
+ const buf = Buffer.from(await res.arrayBuffer());
4483
+ if (buf.length > MAX_BYTES) return c.text("too large", 413);
4484
+ const ct = (res.headers.get("content-type") ?? "application/octet-stream").split(";")[0].trim();
4485
+ return new Response(buf, {
4486
+ headers: {
4487
+ "Content-Type": ct,
4488
+ "Cache-Control": "public, max-age=86400"
4489
+ }
4490
+ });
4491
+ } catch {
4492
+ return c.text("fetch failed", 502);
4493
+ }
4494
+ });
4495
+ }
4219
4496
  function registerApiRoutes(app) {
4220
4497
  registerServerRoutes(app);
4221
4498
  registerFeedFaviconRoutes(app);
4499
+ registerCoverImgRoutes(app);
4222
4500
  registerRssApiRoutes(app);
4223
4501
  registerSchedulerRoutes(app);
4224
4502
  registerPluginsRoutes(app);
@@ -4263,23 +4541,7 @@ function registerAuthRoutes(app) {
4263
4541
  if (!authFlow) return c.json({ ok: false, message: "该站点无需登录" }, 400);
4264
4542
  const { loginUrl } = authFlow;
4265
4543
  const proxy = await resolveProxyForSite(site);
4266
- void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy: resolveProxy({ proxy }) }).then(async (browser) => {
4267
- try {
4268
- const page = await browser.newPage();
4269
- await applyProxyAuthToPage(page, { proxy });
4270
- 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";
4271
- await page.setUserAgent(realUserAgent);
4272
- await page.setViewport({ width: 1366, height: 960 });
4273
- await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
4274
- page.once("close", () => {
4275
- void browser.close().catch(() => {
4276
- });
4277
- });
4278
- } catch {
4279
- await browser.close().catch(() => {
4280
- });
4281
- }
4282
- }).catch(() => {
4544
+ void openBrowserPage(loginUrl, CACHE_DIR, { proxy: resolveProxy({ proxy }) }).catch(() => {
4283
4545
  });
4284
4546
  return c.json({ ok: true, message: "已打开登录页面" });
4285
4547
  });