rssany 0.3.2 → 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 (114) 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/BKm6QCwp.js +1 -0
  22. package/app/webui/build/_app/immutable/chunks/BT6b4LcZ.js +36 -0
  23. package/app/webui/build/_app/immutable/chunks/{Brun6sCr.js → BZY5aksi.js} +2 -2
  24. package/app/webui/build/_app/immutable/chunks/{C8umpVpB.js → BnqaikL8.js} +1 -1
  25. package/app/webui/build/_app/immutable/chunks/BsQ08Wq_.js +1 -0
  26. package/app/webui/build/_app/immutable/chunks/C9wTDiHH.js +1 -0
  27. package/app/webui/build/_app/immutable/chunks/{B-CeeY89.js → CAKuIoAf.js} +1 -1
  28. package/app/webui/build/_app/immutable/chunks/CEWi_rGa.js +1 -0
  29. package/app/webui/build/_app/immutable/chunks/{ChUctqXA.js → Cc7aBSsN.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/Dg_D3pjF.js +1 -0
  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/{DXDBlEGf.js → GeNMTUn1.js} +1 -1
  37. package/app/webui/build/_app/immutable/chunks/{DCEayuDt.js → IhDlsCxD.js} +1 -1
  38. package/app/webui/build/_app/immutable/chunks/Nd0ktDhd.js +1 -0
  39. package/app/webui/build/_app/immutable/chunks/{D6kzEN_P.js → SvdgnirT.js} +1 -1
  40. package/app/webui/build/_app/immutable/chunks/WW6La7Nt.js +2 -0
  41. package/app/webui/build/_app/immutable/chunks/{DsxvjlCC.js → pd_p3yYy.js} +5 -5
  42. package/app/webui/build/_app/immutable/chunks/rNwPv4DZ.js +1 -0
  43. package/app/webui/build/_app/immutable/entry/app.BKLBG-4w.js +2 -0
  44. package/app/webui/build/_app/immutable/entry/start.D-X6pVtx.js +1 -0
  45. package/app/webui/build/_app/immutable/nodes/{0.CQDkqUeN.js → 0.CJDC_3s9.js} +3 -3
  46. package/app/webui/build/_app/immutable/nodes/1.DsKocFSb.js +1 -0
  47. package/app/webui/build/_app/immutable/nodes/10.BeejAn8z.js +1 -0
  48. package/app/webui/build/_app/immutable/nodes/11.D--uwkk0.js +3 -0
  49. package/app/webui/build/_app/immutable/nodes/12.BLyQ6rUu.js +1 -0
  50. package/app/webui/build/_app/immutable/nodes/13.Cl0WQK13.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.DHfwIlBx.js +1 -0
  53. package/app/webui/build/_app/immutable/nodes/{16.p6WfP435.js → 16.BKDfR-KV.js} +2 -2
  54. package/app/webui/build/_app/immutable/nodes/17.DofB8HQB.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.B9ucbp_W.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.Bs32Ieii.js +2 -0
  59. package/app/webui/build/_app/immutable/nodes/7.Cigxrk0v.js +1 -0
  60. package/app/webui/build/_app/immutable/nodes/8.pG10rCF0.js +1 -0
  61. package/app/webui/build/_app/immutable/nodes/9.Bzqb3xHY.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/Dr67pd7v.js +0 -1
  89. package/app/webui/build/_app/immutable/chunks/vtBo8kBV.js +0 -1
  90. package/app/webui/build/_app/immutable/entry/app.dq7-6soi.js +0 -2
  91. package/app/webui/build/_app/immutable/entry/start.BnoTfBrB.js +0 -1
  92. package/app/webui/build/_app/immutable/nodes/1.BUa24rXB.js +0 -1
  93. package/app/webui/build/_app/immutable/nodes/10.MZgVhpGF.js +0 -1
  94. package/app/webui/build/_app/immutable/nodes/11.B39IbrZ0.js +0 -1
  95. package/app/webui/build/_app/immutable/nodes/12.B5B81dLQ.js +0 -1
  96. package/app/webui/build/_app/immutable/nodes/13.DWWcH27k.js +0 -6
  97. package/app/webui/build/_app/immutable/nodes/14.Db6eOPqq.js +0 -1
  98. package/app/webui/build/_app/immutable/nodes/15.B2jiP2VJ.js +0 -1
  99. package/app/webui/build/_app/immutable/nodes/2.AJd2163d.js +0 -1
  100. package/app/webui/build/_app/immutable/nodes/3.BZQeL-vz.js +0 -1
  101. package/app/webui/build/_app/immutable/nodes/4.BT_N8pCh.js +0 -2
  102. package/app/webui/build/_app/immutable/nodes/5.Bo_ftyqW.js +0 -2
  103. package/app/webui/build/_app/immutable/nodes/6.vOowdQUr.js +0 -1
  104. package/app/webui/build/_app/immutable/nodes/7.BfVbBKZu.js +0 -1
  105. package/app/webui/build/_app/immutable/nodes/8.BslYG5f2.js +0 -1
  106. package/app/webui/build/_app/immutable/nodes/9.DAgT-df2.js +0 -1
  107. /package/app/webui/build/_app/immutable/assets/{11.qYZMiTb0.css → 12.qYZMiTb0.css} +0 -0
  108. /package/app/webui/build/_app/immutable/assets/{12.DfJcfUWl.css → 13.DfJcfUWl.css} +0 -0
  109. /package/app/webui/build/_app/immutable/assets/{14.DfMfOrS3.css → 15.DfMfOrS3.css} +0 -0
  110. /package/app/webui/build/_app/immutable/assets/{15.nNGjXhCQ.css → 17.nNGjXhCQ.css} +0 -0
  111. /package/app/webui/build/_app/immutable/assets/{4.Di6rvlY-.css → 5.Di6rvlY-.css} +0 -0
  112. /package/app/webui/build/_app/immutable/assets/{7.CrNxmd8B.css → 8.CrNxmd8B.css} +0 -0
  113. /package/app/webui/build/_app/immutable/nodes/{17.BtYZF6FM.js → 18.BtYZF6FM.js} +0 -0
  114. /package/app/webui/build/_app/immutable/nodes/{18.BIzqhTqv.js → 4.BIzqhTqv.js} +0 -0
package/.env.example CHANGED
@@ -1,52 +1,52 @@
1
- # ─── 服务基础 ────────────────────────────────────────────────────────────────
2
- PORT=18473
3
- NODE_ENV=production
4
-
5
- # 前端地址,OAuth 回调后重定向至此
6
- APP_URL=https://rssany.com
7
-
8
- # ─── 认证(必填)────────────────────────────────────────────────────────────
9
- # JWT 签名密钥,至少 32 位随机字符串,例如:openssl rand -base64 32
10
- JWT_SECRET=change-me-to-a-random-32-char-secret
11
-
12
- # ─── Google OAuth(可选)────────────────────────────────────────────────────
13
- # 在 https://console.cloud.google.com/apis/credentials 创建 OAuth 2.0 客户端
14
- # 授权回调 URI 填:https://your-domain.com/api/auth/google
15
- GOOGLE_CLIENT_ID=
16
- GOOGLE_CLIENT_SECRET=
17
-
18
- # ─── GitHub OAuth(可选)────────────────────────────────────────────────────
19
- # 在 https://github.com/settings/applications/new 创建 OAuth App
20
- # Authorization callback URL 填:https://your-domain.com/api/auth/github
21
- GITHUB_CLIENT_ID=
22
- GITHUB_CLIENT_SECRET=
23
-
24
- # ─── 邮件发送(全部放 env,无需在 config.json 写 email 节)─────────────────────
25
- # SMTP 配置
26
- SMTP_HOST=smtp.example.com
27
- SMTP_PORT=465
28
- SMTP_SECURE=true
29
- SMTP_USER=your@example.com
30
- SMTP_PASS=your-smtp-password
31
-
32
-
33
- # Resend API Key(driver=resend 时使用;https://resend.com/api-keys)
34
- RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
35
-
36
- # ─── LLM(解析 / Pipeline 等;也可在管理后台「设置 → LLM」写入 .rssany/config.json)──
37
- OPENAI_API_KEY=sk-...
38
- OPENAI_BASE_URL=https://api.openai.com/v1
39
- OPENAI_MODEL=gpt-4o-mini
40
-
41
-
42
- # ─── 用户数据目录(可选)──────────────────────────────────────────────────────
43
- # 默认:用户主目录下的 .rssany/(Windows:%USERPROFILE%\.rssany)
44
- # RSSANY_USER_DIR=/path/to/custom
45
-
46
- # ─── 数据库(本地 SQLite)────────────────────────────────────────────────────
47
- # 主库路径:<用户数据>/.rssany/data/rssany.db(启动时自动建表);可选:
48
- # RSSANY_DB_JOURNAL=delete 在 Windows 热重载场景下降低 WAL 损坏风险
49
-
50
- # ─── 缓存目录(可选)────────────────────────────────────────────────────────
51
- # 默认:<用户数据>/.rssany/cache;可单独覆盖:
52
- # CACHE_DIR=/path/to/custom/cache
1
+ # ─── 服务基础 ────────────────────────────────────────────────────────────────
2
+ PORT=18473
3
+ NODE_ENV=production
4
+
5
+ # 前端地址,OAuth 回调后重定向至此
6
+ APP_URL=https://rssany.com
7
+
8
+ # ─── 认证(必填)────────────────────────────────────────────────────────────
9
+ # JWT 签名密钥,至少 32 位随机字符串,例如:openssl rand -base64 32
10
+ JWT_SECRET=change-me-to-a-random-32-char-secret
11
+
12
+ # ─── Google OAuth(可选)────────────────────────────────────────────────────
13
+ # 在 https://console.cloud.google.com/apis/credentials 创建 OAuth 2.0 客户端
14
+ # 授权回调 URI 填:https://your-domain.com/api/auth/google
15
+ GOOGLE_CLIENT_ID=
16
+ GOOGLE_CLIENT_SECRET=
17
+
18
+ # ─── GitHub OAuth(可选)────────────────────────────────────────────────────
19
+ # 在 https://github.com/settings/applications/new 创建 OAuth App
20
+ # Authorization callback URL 填:https://your-domain.com/api/auth/github
21
+ GITHUB_CLIENT_ID=
22
+ GITHUB_CLIENT_SECRET=
23
+
24
+ # ─── 邮件发送(全部放 env,无需在 config.json 写 email 节)─────────────────────
25
+ # SMTP 配置
26
+ SMTP_HOST=smtp.example.com
27
+ SMTP_PORT=465
28
+ SMTP_SECURE=true
29
+ SMTP_USER=your@example.com
30
+ SMTP_PASS=your-smtp-password
31
+
32
+
33
+ # Resend API Key(driver=resend 时使用;https://resend.com/api-keys)
34
+ RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
35
+
36
+ # ─── LLM(解析 / Pipeline 等;也可在管理后台「设置 → LLM」写入 .rssany/config.json)──
37
+ OPENAI_API_KEY=sk-...
38
+ OPENAI_BASE_URL=https://api.openai.com/v1
39
+ OPENAI_MODEL=gpt-4o-mini
40
+
41
+
42
+ # ─── 用户数据目录(可选)──────────────────────────────────────────────────────
43
+ # 默认:用户主目录下的 .rssany/(Windows:%USERPROFILE%\.rssany)
44
+ # RSSANY_USER_DIR=/path/to/custom
45
+
46
+ # ─── 数据库(本地 SQLite)────────────────────────────────────────────────────
47
+ # 主库路径:<用户数据>/.rssany/data/rssany.db(启动时自动建表);可选:
48
+ # RSSANY_DB_JOURNAL=delete 在 Windows 热重载场景下降低 WAL 损坏风险
49
+
50
+ # ─── 缓存目录(可选)────────────────────────────────────────────────────────
51
+ # 默认:<用户数据>/.rssany/cache;可单独覆盖:
52
+ # CACHE_DIR=/path/to/custom/cache
package/README.md CHANGED
@@ -1,69 +1,78 @@
1
- # RSSAny - 把任何信息变成RSS订阅
2
-
3
-
4
- > 按信源抓取网页 / RSS / 邮件等,解析、补全文、打标签与翻译后**入库**,再按需生成 **RSS / Atom / JSON Feed** 与 JSON API。
5
-
6
- **RSSAny** 是一套自托管的订阅管线:列表 URL → **抓取与解析**(规则 / LLM)→ **正文提取**(自定义 / Readability / LLM)→ **upsert 去重** → 固定 **pipeline**(打标签、翻译等)→ 对外提供 `**/rss`** 等输出。
7
-
8
- ## 界面预览
9
-
10
- ![RSSAny Web 界面截图](images/ScreenShot_2026-04-10_153307_558.png)
11
-
12
- ---
13
-
14
- ## 功能概览
15
-
16
- - **统一订阅**:在 `.rssany/sources.json` 中配置网站列表、标准 RSS、IMAP 邮件等,由调度器按 `refresh` 策略拉取。
17
- - **可插拔信源**:**Site / Source** 插件(`.rssany.js` / `.rssany.ts`),见 **[插件配置说明](./docs/plugins.md)**。
18
- - **正文与解析**:在信源 `fetchItems`(及需要的 `ctx.extractItem` 等)内完成;入库后跑 pipeline。
19
- - **固定 pipeline**:`app/pipeline/` 中打标签、翻译等,由 `.rssany/config.json` 的 `pipeline.steps` 开关(**不是**用户目录下的 pipeline 插件)。
20
- - **LLM 辅助**:解析、提取、标签、翻译等可按配置走 OpenAI 兼容接口。
21
- - **站点登录**:需登录的站点通过 Puppeteer 管理 Cookie(与产品用户账号无关)。
22
- - **可选远端投递**:若 `config.json` 中 `**deliver.url`** 非空,在写库与 pipeline 完成后将条目以 `**{ sourceRef, items }**` JSON **POST** 到该 URL(由 `app/deliver/post.ts` 发送);留空则仅本地消费。
23
- - **Web 界面**:SvelteKit 构建产物由后端托管;**Feeds** 等需 **邮箱校验**;`**/admin`** 需 `**users.role === 'admin'**`(可从 `**/me**` 进入)。
24
-
25
- ---
26
-
27
- ## 技术栈(摘要)
28
-
29
-
30
- | 层级 | 说明 |
31
- | --- | ------------------------------------------------------------ |
32
- | 运行时 | Node.js **20–23**(见 `package.json` `engines`) |
33
- | 后端 | Hono、`tsx` 开发入口 |
34
- | 数据 | **SQLite**(Node.js 内置 `node:sqlite`,Node.js 20+),默认 **`~/.rssany/data/rssany.db`**(Windows:`%USERPROFILE%\.rssany\data\rssany.db`) |
35
-
36
- ---
37
-
38
- ## 快速开始
39
-
40
- 日常使用只需 **Node.js 20.x–23.x**(与 `package.json` 的 `engines` 一致):
41
-
42
- ### 全局安装(推荐)
43
-
44
- ```bash
1
+ # RSSAny - 把任何信息变成RSS订阅
2
+
3
+
4
+ > 按信源抓取网页 / RSS / 邮件等,解析、补全文、打标签与翻译后**入库**,再按需生成 **RSS / Atom / JSON Feed** 与 JSON API。
5
+
6
+ **RSSAny** 是一套自托管的订阅管线:列表 URL → **抓取与解析**(规则 / LLM)→ **正文提取**(自定义 / Readability / LLM)→ **upsert 去重** → 固定 **pipeline**(打标签、翻译等)→ 对外提供 `**/rss`** 等输出。
7
+
8
+ ## 界面预览
9
+
10
+ ![RSSAny Web 界面截图](images/ScreenShot_2026-04-10_153307_558.png)
11
+
12
+ ---
13
+
14
+ ## 功能概览
15
+
16
+ - **统一订阅**:在 `.rssany/sources.json` 中配置网站列表、标准 RSS、IMAP 邮件等,由调度器按 `refresh` 策略拉取。
17
+ - **可插拔信源**:**Site / Source** 插件(`.rssany.js` / `.rssany.ts`),见 **[插件配置说明](./docs/plugins.md)**。
18
+ - **正文与解析**:在信源 `fetchItems`(及需要的 `ctx.extractItem` 等)内完成;入库后跑 pipeline。
19
+ - **固定 pipeline**:`app/pipeline/` 中打标签、翻译等,由 `.rssany/config.json` 的 `pipeline.steps` 开关(**不是**用户目录下的 pipeline 插件)。
20
+ - **LLM 辅助**:解析、提取、标签、翻译等可按配置走 OpenAI 兼容接口。
21
+ - **站点登录**:需登录的站点通过 Puppeteer 管理 Cookie(与产品用户账号无关)。
22
+ - **可选远端投递**:若 `config.json` 中 `**deliver.url`** 非空,在写库与 pipeline 完成后将条目以 `**{ sourceRef, items }**` JSON **POST** 到该 URL(由 `app/deliver/post.ts` 发送);留空则仅本地消费。
23
+ - **Web 界面**:SvelteKit 构建产物由后端托管;**Feeds** 等需 **邮箱校验**;`**/admin`** 需 `**users.role === 'admin'**`(可从 `**/me**` 进入)。
24
+
25
+ ---
26
+
27
+ ## 技术栈(摘要)
28
+
29
+
30
+ | 层级 | 说明 |
31
+ | --- | ------------------------------------------------------------ |
32
+ | 运行时 | Node.js **20–23**(见 `package.json` `engines`) |
33
+ | 后端 | Hono、`tsx` 开发入口 |
34
+ | 数据 | **SQLite**(Node.js 内置 `node:sqlite`,Node.js 20+),全局安装默认 **`{npm prefix}/var/rssany/data/rssany.db`** |
35
+
36
+ ## 快速开始
37
+
38
+ 日常使用只需 **Node.js 20.x–23.x**(与 `package.json` 的 `engines` 一致):
39
+
40
+ ### 全局安装(推荐)
41
+
42
+ ```bash
45
43
  npm install -g rssany # 与 npm i -g rssany 相同
46
44
  rssany start
47
45
  ```
48
46
 
47
+ **macOS / Linux(系统自带 Node)** 若 `npm install -g` 报 `EACCES`,先一次性配置 npm 使用用户目录(仍是 npm 命令,无需 sudo):
48
+
49
+ ```bash
50
+ npm config set prefix "$HOME/.local"
51
+ export PATH="$HOME/.local/bin:$PATH" # 可写入 ~/.zshrc 或 ~/.bashrc
52
+ npm install -g rssany
53
+ ```
54
+
55
+ 使用 **nvm / fnm** 安装的 Node 通常可直接 `npm install -g rssany`,无需上述配置。
56
+
49
57
  安装包内已包含构建好的后端与 Web 界面;用 **`rssany start`** 后台启动并直接返回访问地址(默认 **`http://127.0.0.1:18473/`**,端口可在**运行命令时当前目录**下的 `.env` 里设置 `PORT`);用 **`rssany stop`** 关闭后台服务并输出执行状态。
50
-
51
- - **数据目录**:首次运行会在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany\`)自动从包内 **`init/`** 生成 `sources.json`、`config.json` 等(已存在则不会覆盖)。
58
+
59
+ - **数据目录**:全局安装时落在 **npm prefix 下的 `var/rssany/`**(例如 `~/.local/var/rssany/` nvm 的 `.../node/v22.x/var/rssany/`),与 `lib/node_modules/rssany` 同级,升级 npm 包不会覆盖配置与数据库。源码开发时使用仓库内 **`.rssany/`**。仍可用 **`RSSANY_USER_DIR`** 覆盖。
60
+ - **从 `~/.rssany` 迁移**:若新目录尚不存在且旧目录有数据,首次启动会自动迁移。
52
61
  - **可选配置**:在启动 `rssany start` 时的**当前目录**放置 `.env`(可参考仓库里的 `.env.example`),用于 JWT、OAuth、SMTP、LLM(如 `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `OPENAI_MODEL`)等。
53
- - **重置全部本地数据**(结束占用 `PORT` 的进程并删除用户目录,慎用):执行 **`rssany reset`**;在含 `.env` 的目录下运行可读取 `PORT` / `RSSANY_USER_DIR`,或事先在环境里导出这些变量。
54
-
62
+ - **重置全部本地数据**(结束占用 `PORT` 的进程并删除用户目录,慎用):执行 **`rssany reset`**;在含 `.env` 的目录下运行可读取 `PORT` / `RSSANY_USER_DIR`,或事先在环境里导出这些变量。
63
+
55
64
  CLI 名为 **`rssany`**;裸 `rssany` 只显示用法,不再直接进入服务运行状态。
56
-
57
- ### 从源码运行(开发 / 贡献)
58
-
65
+
66
+ ### 从源码运行(开发 / 贡献)
67
+
59
68
  需要 **npm**:
60
-
61
- ```bash
69
+
70
+ ```bash
62
71
  npm install
63
72
  npm run webui:install
64
- cp .env.example .env # 按需修改
65
- ```
66
-
73
+ cp .env.example .env # 按需修改
74
+ ```
75
+
67
76
  **开发**(单一后端地址;前端静态构建自动 watch):
68
77
 
69
78
  ```bash
@@ -73,97 +82,97 @@ npm run dev
73
82
  该命令会先启动 `webui` 静态构建 watch,等首轮 HTML 构建完成后再启动后端服务;浏览器只访问后端地址(默认 `http://127.0.0.1:3999/`),不再单独启动前端开发服务器。
74
83
 
75
84
  或分步:一个终端运行 `npm run webui:watch`,另一个终端运行 `npm run dev:backend`。
76
-
85
+
77
86
  **生产**(本仓库):`npm run webui:build && npm start`。
78
-
87
+
79
88
  **重置本地数据**(与全局安装的 `rssany reset` 逻辑相同):`npm run reset`。
80
-
81
- 发布到 npm 时 `prepublishOnly` 会执行 `build:all`(后端 `vite build` + `webui:build`)。
82
-
83
- ---
84
-
85
- ## 数据流(简图)
86
-
87
- ```
88
- sources.json / 信源插件
89
- → 调度器触发 fetchItems
90
- → upsertItems
91
- → pipeline(每条一次)
92
- → [可选] deliver.url POST(出站,非入站 API)
93
- ```
94
-
95
- 消费侧:**RSS/XML**、`**/api/*`**、Web UI。
96
-
97
- ---
98
-
99
- ## 常用 HTTP 能力
100
-
101
- ### RSS 输出
102
-
103
- - **按条件从库中生成**:支持 `search`、`tags`、`lng`、`limit` 等查询参数;可用 `subscribed=1` 限定为 `sources.json` 中出现的 ref。
104
- - **按 URL 即时抓取**:`GET /rss/https://example.com/...`(具体行为以路由实现为准)。
105
-
106
- ---
107
-
108
- ## 配置
109
-
110
- **信源插件(Site / Source)**:目录约定、`listUrlPattern` / `pattern`、`fetchItems`、与 `sources.json` 的关系等,见 **[docs/plugins.md](./docs/plugins.md)**。
111
-
112
- ### Pipeline(固定代码)
113
-
114
- `**app/pipeline/**`,通过 `**.rssany/config.json**` 配置步骤,例如:
115
-
116
- ```json
117
- {
118
- "pipeline": {
119
- "steps": [
120
- { "id": "tagger", "enabled": true },
121
- { "id": "translator", "enabled": false }
122
- ]
123
- },
124
- "deliver": {
125
- "url": ""
126
- }
127
- }
128
- ```
129
-
130
- `deliver.url` 非空时会对处理完成的条目向该 URL 发起出站 POST;留空则不投递。
131
-
132
- ### `sources.json` 片段示例
133
-
134
- ```json
135
- {
136
- "sources": [
137
- { "ref": "https://example.com/feed.xml", "label": "Example", "refresh": "1h" }
138
- ]
139
- }
140
- ```
141
-
142
- 合法 `refresh` 取值包括:`10min`、`30min`、`1h`、`6h`、`12h`、`1day`(默认)、`3day`、`7day`。
143
-
144
- ---
145
-
146
- ## 仓库目录(摘要)
147
-
148
- ```
149
- ├── app/ # 后端:路由、feeder、scraper、pipeline、db、auth…
150
- │ └── plugins/builtin/ # 内置信源 *.rssany.js
151
- ├── docs/ # 用户文档(如 plugins.md)
152
- └── webui/ # SvelteKit 前端
153
-
154
- ~/.rssany/ # 运行时用户数据(首次启动创建;或 RSSANY_USER_DIR)
155
- ├── sources.json
156
- ├── config.json
157
- ├── tags.json
158
- ├── data/rssany.db # SQLite 主库
159
- ├── cache/
160
- └── plugins/ # 用户插件覆盖内置
161
- ```
162
-
163
- 更细的模块说明见 **[AGENTS.md](./AGENTS.md)**(与代码迭代同步,若有出入以代码为准)。
164
-
165
- ---
166
-
167
- ## 许可证
168
-
89
+
90
+ 发布到 npm 时 `prepublishOnly` 会执行 `build:all`(后端 `vite build` + `webui:build`)。
91
+
92
+ ---
93
+
94
+ ## 数据流(简图)
95
+
96
+ ```
97
+ sources.json / 信源插件
98
+ → 调度器触发 fetchItems
99
+ → upsertItems
100
+ → pipeline(每条一次)
101
+ → [可选] deliver.url POST(出站,非入站 API)
102
+ ```
103
+
104
+ 消费侧:**RSS/XML**、`**/api/*`**、Web UI。
105
+
106
+ ---
107
+
108
+ ## 常用 HTTP 能力
109
+
110
+ ### RSS 输出
111
+
112
+ - **按条件从库中生成**:支持 `search`、`tags`、`lng`、`limit` 等查询参数;可用 `subscribed=1` 限定为 `sources.json` 中出现的 ref。
113
+ - **按 URL 即时抓取**:`GET /rss/https://example.com/...`(具体行为以路由实现为准)。
114
+
115
+ ---
116
+
117
+ ## 配置
118
+
119
+ **信源插件(Site / Source)**:目录约定、`listUrlPattern` / `pattern`、`fetchItems`、与 `sources.json` 的关系等,见 **[docs/plugins.md](./docs/plugins.md)**。
120
+
121
+ ### Pipeline(固定代码)
122
+
123
+ `**app/pipeline/**`,通过 `**.rssany/config.json**` 配置步骤,例如:
124
+
125
+ ```json
126
+ {
127
+ "pipeline": {
128
+ "steps": [
129
+ { "id": "tagger", "enabled": true },
130
+ { "id": "translator", "enabled": false }
131
+ ]
132
+ },
133
+ "deliver": {
134
+ "url": ""
135
+ }
136
+ }
137
+ ```
138
+
139
+ `deliver.url` 非空时会对处理完成的条目向该 URL 发起出站 POST;留空则不投递。
140
+
141
+ ### `sources.json` 片段示例
142
+
143
+ ```json
144
+ {
145
+ "sources": [
146
+ { "ref": "https://example.com/feed.xml", "label": "Example", "refresh": "1h" }
147
+ ]
148
+ }
149
+ ```
150
+
151
+ 合法 `refresh` 取值包括:`10min`、`30min`、`1h`、`6h`、`12h`、`1day`(默认)、`3day`、`7day`。
152
+
153
+ ---
154
+
155
+ ## 仓库目录(摘要)
156
+
157
+ ```
158
+ ├── app/ # 后端:路由、feeder、scraper、pipeline、db、auth…
159
+ │ └── plugins/builtin/ # 内置信源 *.rssany.js
160
+ ├── docs/ # 用户文档(如 plugins.md)
161
+ └── webui/ # SvelteKit 前端
162
+
163
+ ~/.rssany/ # 运行时用户数据(首次启动创建;或 RSSANY_USER_DIR)
164
+ ├── sources.json
165
+ ├── config.json
166
+ ├── tags.json
167
+ ├── data/rssany.db # SQLite 主库
168
+ ├── cache/
169
+ └── plugins/ # 用户插件覆盖内置
170
+ ```
171
+
172
+ 更细的模块说明见 **[AGENTS.md](./AGENTS.md)**(与代码迭代同步,若有出入以代码为准)。
173
+
174
+ ---
175
+
176
+ ## 许可证
177
+
169
178
  MIT
@@ -1,92 +1,92 @@
1
- // 内置 IMAP 邮件插件:匹配 imap://、imaps:// 协议 URL
1
+ // 内置 IMAP 邮件插件:匹配 imap://、imaps:// 协议 URL
2
2
  export const id = "__email__";
3
3
  export const name = "Email";
4
4
  export const pattern = /^imaps?:\/\//;
5
5
  export const priority = 0;
6
6
  export const refreshInterval = "30min";
7
7
 
8
- function parseImapUrl(sourceId) {
9
- const url = new URL(sourceId);
10
- const host = url.hostname;
11
- const port = url.port ? parseInt(url.port, 10) : 993;
12
- const secure = url.protocol === "imaps:" || port === 993;
13
- const user = decodeURIComponent(url.username);
14
- const pass = decodeURIComponent(url.password);
15
- const folder = decodeURIComponent(url.pathname.slice(1)) || "INBOX";
16
- const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "30", 10));
17
- return { host, port, secure, user, pass, folder, limit };
18
- }
19
-
20
- function makeGuid(messageId, uid, host, createHash) {
21
- const raw = messageId ?? `${uid}@${host}`;
22
- return createHash("sha256").update(raw).digest("hex");
23
- }
24
-
25
- export async function fetchItems(sourceId, ctx) {
26
- const { deps } = ctx;
27
- const { host, port, secure, user, pass, folder, limit } = parseImapUrl(sourceId);
28
- const client = new deps.ImapFlow({
29
- host,
30
- port,
31
- secure,
32
- auth: { user, pass },
33
- logger: false,
34
- });
35
-
36
- client.on("error", (err) => {
37
- deps.logger.error("source", "IMAP 连接异常", { err: err?.message, host, folder });
38
- });
39
-
40
- const items = [];
41
- let connected = false;
42
- try {
43
- await client.connect();
44
- connected = true;
45
- const lock = await client.getMailboxLock(folder);
46
- try {
47
- const mailbox = client.mailbox;
48
- if (mailbox === false) return [];
49
- const total = mailbox.exists ?? 0;
50
- if (total === 0) return [];
51
- const start = Math.max(1, total - limit + 1);
52
- for await (const msg of client.fetch(`${start}:*`, { source: true, envelope: true })) {
53
- try {
54
- if (msg.source === undefined || msg.envelope === undefined) continue;
55
- const parsed = await deps.simpleParser(msg.source);
56
- const envelope = msg.envelope;
57
- const guid = makeGuid(envelope.messageId, msg.uid, host, deps.createHash);
58
- const title = parsed.subject ?? envelope.subject ?? "(无主题)";
59
- const fromAddr = envelope.from?.[0];
60
- const authorRaw = fromAddr?.name || fromAddr?.address || undefined;
61
- const author = authorRaw ? [authorRaw] : undefined;
62
- const pubDate = parsed.date ?? envelope.date ?? new Date();
63
- const link = `imap://${host}/${encodeURIComponent(folder)}#${msg.uid}`;
64
- const htmlBody = typeof parsed.html === "string" ? parsed.html : undefined;
65
- const textBody = typeof parsed.text === "string" ? parsed.text : undefined;
66
- const content = htmlBody ?? (textBody ? `<pre>${textBody}</pre>` : undefined);
67
- const summary = textBody?.slice(0, 300) || undefined;
68
- items.push({ guid, title, link, pubDate, author, summary, content });
69
- } catch (err) {
70
- deps.logger.warn("source", "解析单封邮件失败", { err: err?.message });
71
- }
72
- }
73
- } finally {
74
- lock.release();
75
- }
76
- } catch (err) {
77
- deps.logger.warn("source", "拉取 IMAP 邮件失败", { err: err?.message, host, folder });
78
- return [];
79
- } finally {
80
- if (connected && client.usable) {
81
- try {
82
- await client.logout();
83
- } catch (err) {
84
- deps.logger.warn("source", "IMAP 退出连接失败", { err: err?.message, host, folder });
85
- }
86
- } else {
87
- client.close();
88
- }
89
- }
90
- return items.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime());
8
+ function parseImapUrl(sourceId) {
9
+ const url = new URL(sourceId);
10
+ const host = url.hostname;
11
+ const port = url.port ? parseInt(url.port, 10) : 993;
12
+ const secure = url.protocol === "imaps:" || port === 993;
13
+ const user = decodeURIComponent(url.username);
14
+ const pass = decodeURIComponent(url.password);
15
+ const folder = decodeURIComponent(url.pathname.slice(1)) || "INBOX";
16
+ const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "30", 10));
17
+ return { host, port, secure, user, pass, folder, limit };
18
+ }
19
+
20
+ function makeGuid(messageId, uid, host, createHash) {
21
+ const raw = messageId ?? `${uid}@${host}`;
22
+ return createHash("sha256").update(raw).digest("hex");
23
+ }
24
+
25
+ export async function fetchItems(sourceId, ctx) {
26
+ const { deps } = ctx;
27
+ const { host, port, secure, user, pass, folder, limit } = parseImapUrl(sourceId);
28
+ const client = new deps.ImapFlow({
29
+ host,
30
+ port,
31
+ secure,
32
+ auth: { user, pass },
33
+ logger: false,
34
+ });
35
+
36
+ client.on("error", (err) => {
37
+ deps.logger.error("source", "IMAP 连接异常", { err: err?.message, host, folder });
38
+ });
39
+
40
+ const items = [];
41
+ let connected = false;
42
+ try {
43
+ await client.connect();
44
+ connected = true;
45
+ const lock = await client.getMailboxLock(folder);
46
+ try {
47
+ const mailbox = client.mailbox;
48
+ if (mailbox === false) return [];
49
+ const total = mailbox.exists ?? 0;
50
+ if (total === 0) return [];
51
+ const start = Math.max(1, total - limit + 1);
52
+ for await (const msg of client.fetch(`${start}:*`, { source: true, envelope: true })) {
53
+ try {
54
+ if (msg.source === undefined || msg.envelope === undefined) continue;
55
+ const parsed = await deps.simpleParser(msg.source);
56
+ const envelope = msg.envelope;
57
+ const guid = makeGuid(envelope.messageId, msg.uid, host, deps.createHash);
58
+ const title = parsed.subject ?? envelope.subject ?? "(无主题)";
59
+ const fromAddr = envelope.from?.[0];
60
+ const authorRaw = fromAddr?.name || fromAddr?.address || undefined;
61
+ const author = authorRaw ? [authorRaw] : undefined;
62
+ const pubDate = parsed.date ?? envelope.date ?? new Date();
63
+ const link = `imap://${host}/${encodeURIComponent(folder)}#${msg.uid}`;
64
+ const htmlBody = typeof parsed.html === "string" ? parsed.html : undefined;
65
+ const textBody = typeof parsed.text === "string" ? parsed.text : undefined;
66
+ const content = htmlBody ?? (textBody ? `<pre>${textBody}</pre>` : undefined);
67
+ const summary = textBody?.slice(0, 300) || undefined;
68
+ items.push({ guid, title, link, pubDate, author, summary, content });
69
+ } catch (err) {
70
+ deps.logger.warn("source", "解析单封邮件失败", { err: err?.message });
71
+ }
72
+ }
73
+ } finally {
74
+ lock.release();
75
+ }
76
+ } catch (err) {
77
+ deps.logger.warn("source", "拉取 IMAP 邮件失败", { err: err?.message, host, folder });
78
+ return [];
79
+ } finally {
80
+ if (connected && client.usable) {
81
+ try {
82
+ await client.logout();
83
+ } catch (err) {
84
+ deps.logger.warn("source", "IMAP 退出连接失败", { err: err?.message, host, folder });
85
+ }
86
+ } else {
87
+ client.close();
88
+ }
89
+ }
90
+ return items.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime());
91
91
  }
92
92