rssany 0.3.2 → 0.3.4

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 (113) 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/statics/README.md +7 -7
  6. package/app/webui/build/200.html +36 -36
  7. package/app/webui/build/_app/immutable/assets/10.CmGYYZFR.css +1 -0
  8. package/app/webui/build/_app/immutable/assets/11.Dkz3VS_N.css +1 -0
  9. package/app/webui/build/_app/immutable/assets/14.BCCBoMGj.css +1 -0
  10. package/app/webui/build/_app/immutable/assets/6.Cm_jpHOq.css +1 -0
  11. package/app/webui/build/_app/immutable/assets/7.CJ3BjogD.css +1 -0
  12. package/app/webui/build/_app/immutable/assets/9.CATKVZ-n.css +1 -0
  13. package/app/webui/build/_app/immutable/assets/{SourcesList.D5Lso0bo.css → SourcesList.ke66uOSi.css} +1 -1
  14. package/app/webui/build/_app/immutable/assets/chevron-down.CV-KWLNP.css +1 -0
  15. package/app/webui/build/_app/immutable/chunks/{CGCMIfh3.js → 4TuV_psf.js} +1 -1
  16. package/app/webui/build/_app/immutable/chunks/{DAdOEnFb.js → B0czyjwj.js} +1 -1
  17. package/app/webui/build/_app/immutable/chunks/{CFwxUBGi.js → B553hBXT.js} +1 -1
  18. package/app/webui/build/_app/immutable/chunks/B8StT3Do.js +6 -0
  19. package/app/webui/build/_app/immutable/chunks/BI_ale1m.js +1 -0
  20. package/app/webui/build/_app/immutable/chunks/BK0ygNWX.js +2 -0
  21. package/app/webui/build/_app/immutable/chunks/{Brun6sCr.js → BZY5aksi.js} +2 -2
  22. package/app/webui/build/_app/immutable/chunks/{C8umpVpB.js → BnqaikL8.js} +1 -1
  23. package/app/webui/build/_app/immutable/chunks/BsQ08Wq_.js +1 -0
  24. package/app/webui/build/_app/immutable/chunks/C9wTDiHH.js +1 -0
  25. package/app/webui/build/_app/immutable/chunks/{B-CeeY89.js → CAKuIoAf.js} +1 -1
  26. package/app/webui/build/_app/immutable/chunks/CEWi_rGa.js +1 -0
  27. package/app/webui/build/_app/immutable/chunks/CIsxbQ9B.js +1 -0
  28. package/app/webui/build/_app/immutable/chunks/{ChUctqXA.js → Cc7aBSsN.js} +1 -1
  29. package/app/webui/build/_app/immutable/chunks/{DXDBlEGf.js → Czo-EcYU.js} +1 -1
  30. package/app/webui/build/_app/immutable/chunks/{BAJAS8BI.js → D8G961Hm.js} +1 -1
  31. package/app/webui/build/_app/immutable/chunks/{CS53ooo0.js → DIeahUKq.js} +1 -1
  32. package/app/webui/build/_app/immutable/chunks/DO5OXNYS.js +1 -0
  33. package/app/webui/build/_app/immutable/chunks/{Dr67pd7v.js → DhZZHWov.js} +1 -1
  34. package/app/webui/build/_app/immutable/chunks/{Dyvi1wBH.js → DptdhtA1.js} +1 -1
  35. package/app/webui/build/_app/immutable/chunks/{ClknbeNl.js → FDS7fbwH.js} +1 -1
  36. package/app/webui/build/_app/immutable/chunks/{DCEayuDt.js → IhDlsCxD.js} +1 -1
  37. package/app/webui/build/_app/immutable/chunks/Nd0ktDhd.js +1 -0
  38. package/app/webui/build/_app/immutable/chunks/{D6kzEN_P.js → SvdgnirT.js} +1 -1
  39. package/app/webui/build/_app/immutable/chunks/WW6La7Nt.js +2 -0
  40. package/app/webui/build/_app/immutable/chunks/{DsxvjlCC.js → pd_p3yYy.js} +5 -5
  41. package/app/webui/build/_app/immutable/chunks/qwlQuQWi.js +36 -0
  42. package/app/webui/build/_app/immutable/chunks/rNwPv4DZ.js +1 -0
  43. package/app/webui/build/_app/immutable/entry/app.CsaNbZ85.js +2 -0
  44. package/app/webui/build/_app/immutable/entry/start.BZt_oICN.js +1 -0
  45. package/app/webui/build/_app/immutable/nodes/{0.CQDkqUeN.js → 0.zuqEMBQT.js} +3 -3
  46. package/app/webui/build/_app/immutable/nodes/1.DzLFxso0.js +1 -0
  47. package/app/webui/build/_app/immutable/nodes/10.BC-1FXeC.js +1 -0
  48. package/app/webui/build/_app/immutable/nodes/11.CePwEHqu.js +3 -0
  49. package/app/webui/build/_app/immutable/nodes/12.eeQ9B6bg.js +1 -0
  50. package/app/webui/build/_app/immutable/nodes/13.BLKI9iZo.js +1 -0
  51. package/app/webui/build/_app/immutable/nodes/14.T9l5Rh19.js +1 -0
  52. package/app/webui/build/_app/immutable/nodes/15.39GHtAg1.js +1 -0
  53. package/app/webui/build/_app/immutable/nodes/{16.p6WfP435.js → 16.BO-9BBQ5.js} +2 -2
  54. package/app/webui/build/_app/immutable/nodes/17.BWRgFCUw.js +1 -0
  55. package/app/webui/build/_app/immutable/nodes/2.BOYqXdCa.js +1 -0
  56. package/app/webui/build/_app/immutable/nodes/3.CJw5Nx2o.js +1 -0
  57. package/app/webui/build/_app/immutable/nodes/5.9zgwFV6I.js +2 -0
  58. package/app/webui/build/_app/immutable/nodes/6.trFT-trT.js +2 -0
  59. package/app/webui/build/_app/immutable/nodes/7.DCqiDq6O.js +1 -0
  60. package/app/webui/build/_app/immutable/nodes/8.DG_T-wWN.js +1 -0
  61. package/app/webui/build/_app/immutable/nodes/9.FiPTezQy.js +1 -0
  62. package/app/webui/build/_app/version.json +1 -1
  63. package/bin/rssany.js +55 -3
  64. package/dist/index.js +314 -96
  65. package/dist/index.js.map +1 -1
  66. package/package.json +107 -103
  67. package/scripts/postinstall.mjs +44 -0
  68. package/scripts/reset.mjs +137 -135
  69. package/scripts/user-dir.mjs +52 -0
  70. package/app/webui/build/_app/immutable/assets/10.Dj8_pmut.css +0 -1
  71. package/app/webui/build/_app/immutable/assets/13.Qu_tY6H9.css +0 -1
  72. package/app/webui/build/_app/immutable/assets/5.B-dPiwB7.css +0 -1
  73. package/app/webui/build/_app/immutable/assets/6.B27N7pdA.css +0 -1
  74. package/app/webui/build/_app/immutable/assets/8.Cgji2b15.css +0 -1
  75. package/app/webui/build/_app/immutable/assets/9.BsCIAvn3.css +0 -1
  76. package/app/webui/build/_app/immutable/chunks/6prdYIKP.js +0 -1
  77. package/app/webui/build/_app/immutable/chunks/B2cyTHdf.js +0 -2
  78. package/app/webui/build/_app/immutable/chunks/B6WG2Sd3.js +0 -1
  79. package/app/webui/build/_app/immutable/chunks/BA4Gucnq.js +0 -1
  80. package/app/webui/build/_app/immutable/chunks/BXTsojX2.js +0 -36
  81. package/app/webui/build/_app/immutable/chunks/BkD3yAYe.js +0 -1
  82. package/app/webui/build/_app/immutable/chunks/C4uF_YIK.js +0 -1
  83. package/app/webui/build/_app/immutable/chunks/CBY2biv-.js +0 -1
  84. package/app/webui/build/_app/immutable/chunks/DAV9bzjw.js +0 -1
  85. package/app/webui/build/_app/immutable/chunks/DL3Q5sfb.js +0 -1
  86. package/app/webui/build/_app/immutable/chunks/DVa8Y-mQ.js +0 -1
  87. package/app/webui/build/_app/immutable/chunks/DoRPmqLn.js +0 -2
  88. package/app/webui/build/_app/immutable/chunks/vtBo8kBV.js +0 -1
  89. package/app/webui/build/_app/immutable/entry/app.dq7-6soi.js +0 -2
  90. package/app/webui/build/_app/immutable/entry/start.BnoTfBrB.js +0 -1
  91. package/app/webui/build/_app/immutable/nodes/1.BUa24rXB.js +0 -1
  92. package/app/webui/build/_app/immutable/nodes/10.MZgVhpGF.js +0 -1
  93. package/app/webui/build/_app/immutable/nodes/11.B39IbrZ0.js +0 -1
  94. package/app/webui/build/_app/immutable/nodes/12.B5B81dLQ.js +0 -1
  95. package/app/webui/build/_app/immutable/nodes/13.DWWcH27k.js +0 -6
  96. package/app/webui/build/_app/immutable/nodes/14.Db6eOPqq.js +0 -1
  97. package/app/webui/build/_app/immutable/nodes/15.B2jiP2VJ.js +0 -1
  98. package/app/webui/build/_app/immutable/nodes/2.AJd2163d.js +0 -1
  99. package/app/webui/build/_app/immutable/nodes/3.BZQeL-vz.js +0 -1
  100. package/app/webui/build/_app/immutable/nodes/4.BT_N8pCh.js +0 -2
  101. package/app/webui/build/_app/immutable/nodes/5.Bo_ftyqW.js +0 -2
  102. package/app/webui/build/_app/immutable/nodes/6.vOowdQUr.js +0 -1
  103. package/app/webui/build/_app/immutable/nodes/7.BfVbBKZu.js +0 -1
  104. package/app/webui/build/_app/immutable/nodes/8.BslYG5f2.js +0 -1
  105. package/app/webui/build/_app/immutable/nodes/9.DAgT-df2.js +0 -1
  106. /package/app/webui/build/_app/immutable/assets/{11.qYZMiTb0.css → 12.qYZMiTb0.css} +0 -0
  107. /package/app/webui/build/_app/immutable/assets/{12.DfJcfUWl.css → 13.DfJcfUWl.css} +0 -0
  108. /package/app/webui/build/_app/immutable/assets/{14.DfMfOrS3.css → 15.DfMfOrS3.css} +0 -0
  109. /package/app/webui/build/_app/immutable/assets/{15.nNGjXhCQ.css → 17.nNGjXhCQ.css} +0 -0
  110. /package/app/webui/build/_app/immutable/assets/{4.Di6rvlY-.css → 5.Di6rvlY-.css} +0 -0
  111. /package/app/webui/build/_app/immutable/assets/{7.CrNxmd8B.css → 8.CrNxmd8B.css} +0 -0
  112. /package/app/webui/build/_app/immutable/nodes/{17.BtYZF6FM.js → 18.BtYZF6FM.js} +0 -0
  113. /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,30 +1504,39 @@ 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(() => {
1525
+ });
1526
+ }
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(() => {
1310
1538
  });
1539
+ throw err;
1311
1540
  }
1312
1541
  }
1313
1542
  async function fetchHtml(url, config = {}) {
@@ -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) },
@@ -4307,23 +4541,7 @@ function registerAuthRoutes(app) {
4307
4541
  if (!authFlow) return c.json({ ok: false, message: "该站点无需登录" }, 400);
4308
4542
  const { loginUrl } = authFlow;
4309
4543
  const proxy = await resolveProxyForSite(site);
4310
- void launchBrowser({ headless: false, cacheDir: CACHE_DIR, proxy: resolveProxy({ proxy }) }).then(async (browser) => {
4311
- try {
4312
- const page = await browser.newPage();
4313
- await applyProxyAuthToPage(page, { proxy });
4314
- 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";
4315
- await page.setUserAgent(realUserAgent);
4316
- await page.setViewport({ width: 1366, height: 960 });
4317
- await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
4318
- page.once("close", () => {
4319
- void browser.close().catch(() => {
4320
- });
4321
- });
4322
- } catch {
4323
- await browser.close().catch(() => {
4324
- });
4325
- }
4326
- }).catch(() => {
4544
+ void openBrowserPage(loginUrl, CACHE_DIR, { proxy: resolveProxy({ proxy }) }).catch(() => {
4327
4545
  });
4328
4546
  return c.json({ ok: true, message: "已打开登录页面" });
4329
4547
  });