rssany 0.1.1 → 0.1.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 (130) hide show
  1. package/.env.example +1 -1
  2. package/README.md +32 -50
  3. package/app/plugins/builtin/rss.rssany.js +107 -24
  4. package/app/plugins/site.rssany.js +1 -1
  5. package/dist/index.js +1003 -271
  6. package/dist/index.js.map +1 -1
  7. package/{config.examples.json → init/config.json} +7 -1
  8. package/init/sources.json +353 -0
  9. package/package.json +4 -6
  10. package/statics/401.html +1 -1
  11. package/statics/README.md +1 -1
  12. package/webui/build/200.html +16 -18
  13. package/webui/build/_app/immutable/assets/0.DjU2hdCQ.css +1 -0
  14. package/webui/build/_app/immutable/assets/10.Dj8_pmut.css +1 -0
  15. package/webui/build/_app/immutable/assets/11.qYZMiTb0.css +1 -0
  16. package/webui/build/_app/immutable/assets/12.Ct59LCqW.css +1 -0
  17. package/webui/build/_app/immutable/assets/13.BhO9zvFi.css +1 -0
  18. package/webui/build/_app/immutable/assets/14.CujIhjQK.css +1 -0
  19. package/webui/build/_app/immutable/assets/15.nNGjXhCQ.css +1 -0
  20. package/webui/build/_app/immutable/assets/16.PP9XLDf7.css +1 -0
  21. package/webui/build/_app/immutable/assets/4.9wPHhVwv.css +1 -0
  22. package/webui/build/_app/immutable/assets/6.DSJfjJwx.css +1 -0
  23. package/webui/build/_app/immutable/assets/7.CrNxmd8B.css +1 -0
  24. package/webui/build/_app/immutable/assets/8.Ba5_jYIY.css +1 -0
  25. package/webui/build/_app/immutable/assets/{9.BZheTlzZ.css → 9.m-LCx_kl.css} +1 -1
  26. package/webui/build/_app/immutable/assets/BackToParentRoute.DGk-X5ow.css +1 -0
  27. package/webui/build/_app/immutable/assets/SourcesList.yTBBi3_m.css +1 -0
  28. package/webui/build/_app/immutable/assets/homeFeedPanelStore.BopJZtHu.css +1 -0
  29. package/webui/build/_app/immutable/chunks/{V2-VOe88.js → B-OsL1Ct.js} +1 -1
  30. package/webui/build/_app/immutable/chunks/B2Q1a1-H.js +2 -0
  31. package/webui/build/_app/immutable/chunks/BK3WtZwv.js +1 -0
  32. package/webui/build/_app/immutable/chunks/BQqoDzLx.js +1 -0
  33. package/webui/build/_app/immutable/chunks/BbWUOQ_m.js +1 -0
  34. package/webui/build/_app/immutable/chunks/Bp63qm3L.js +1 -0
  35. package/webui/build/_app/immutable/chunks/{CZDFXKiF.js → C85CNwD2.js} +1 -1
  36. package/webui/build/_app/immutable/chunks/CVzlFH44.js +1 -0
  37. package/webui/build/_app/immutable/chunks/CWNeClHp.js +6 -0
  38. package/webui/build/_app/immutable/chunks/CdMsRjxJ.js +1 -0
  39. package/webui/build/_app/immutable/chunks/Cihqbfi5.js +1 -0
  40. package/webui/build/_app/immutable/chunks/CllQAdvt.js +1 -0
  41. package/webui/build/_app/immutable/chunks/CtijX1u3.js +31 -0
  42. package/webui/build/_app/immutable/chunks/D5GvRCv7.js +1 -0
  43. package/webui/build/_app/immutable/chunks/DEDI7Ecm.js +1 -0
  44. package/webui/build/_app/immutable/chunks/DFuhmi31.js +1 -0
  45. package/webui/build/_app/immutable/chunks/DMWEh-Ek.js +2 -0
  46. package/webui/build/_app/immutable/chunks/DjNLq3TF.js +1 -0
  47. package/webui/build/_app/immutable/chunks/Dt2CddFe.js +1 -0
  48. package/webui/build/_app/immutable/chunks/Dv1VCsiB.js +41 -0
  49. package/webui/build/_app/immutable/chunks/Dw782Tjs.js +1 -0
  50. package/webui/build/_app/immutable/chunks/Xy_fhzQq.js +1 -0
  51. package/webui/build/_app/immutable/chunks/lk5LaiqA.js +1 -0
  52. package/webui/build/_app/immutable/chunks/mW5RwvnK.js +13 -0
  53. package/webui/build/_app/immutable/chunks/{CtHRh_pJ.js → tB7QMF3U.js} +1 -1
  54. package/webui/build/_app/immutable/chunks/xtNWTdbD.js +1 -0
  55. package/webui/build/_app/immutable/entry/app.BcD2eSsQ.js +2 -0
  56. package/webui/build/_app/immutable/entry/start.CbkdJdz1.js +1 -0
  57. package/webui/build/_app/immutable/nodes/0.DSUDmOx2.js +11 -0
  58. package/webui/build/_app/immutable/nodes/1.DU9aYGAb.js +1 -0
  59. package/webui/build/_app/immutable/nodes/10.Db6vw7Ih.js +1 -0
  60. package/webui/build/_app/immutable/nodes/11.BaAcorz3.js +1 -0
  61. package/webui/build/_app/immutable/nodes/12.Cg8AeCSH.js +1 -0
  62. package/webui/build/_app/immutable/nodes/13.nT3SOzEB.js +1 -0
  63. package/webui/build/_app/immutable/nodes/14.DqT4pcrQ.js +1 -0
  64. package/webui/build/_app/immutable/nodes/15.CCLbjxnH.js +1 -0
  65. package/webui/build/_app/immutable/nodes/{12.CMcby_lY.js → 16.DiigpVdP.js} +15 -15
  66. package/webui/build/_app/immutable/nodes/2.BYWOpaxy.js +1 -0
  67. package/webui/build/_app/immutable/nodes/3.DEcYOQc-.js +1 -0
  68. package/webui/build/_app/immutable/nodes/4.DTSxpKm7.js +2 -0
  69. package/webui/build/_app/immutable/nodes/5.CvM1TkLG.js +1 -0
  70. package/webui/build/_app/immutable/nodes/6.Dscr6LkS.js +1 -0
  71. package/webui/build/_app/immutable/nodes/7.Bp60MobD.js +1 -0
  72. package/webui/build/_app/immutable/nodes/8.DwSg0MHh.js +1 -0
  73. package/webui/build/_app/immutable/nodes/9.BeYOUjxR.js +1 -0
  74. package/webui/build/_app/version.json +1 -1
  75. package/sources.example.json +0 -562
  76. package/webui/build/_app/immutable/assets/0.BUAXpTm6.css +0 -1
  77. package/webui/build/_app/immutable/assets/10.I1OuCLrU.css +0 -1
  78. package/webui/build/_app/immutable/assets/11.CrO9xaki.css +0 -1
  79. package/webui/build/_app/immutable/assets/12.BEi6fInA.css +0 -1
  80. package/webui/build/_app/immutable/assets/14.Ctlgn1LZ.css +0 -1
  81. package/webui/build/_app/immutable/assets/2.eJ80XOGm.css +0 -1
  82. package/webui/build/_app/immutable/assets/4.B8-jYAVj.css +0 -1
  83. package/webui/build/_app/immutable/assets/6.Drn-0DON.css +0 -1
  84. package/webui/build/_app/immutable/assets/7.ms2diq_q.css +0 -1
  85. package/webui/build/_app/immutable/assets/8.DKymkjjs.css +0 -1
  86. package/webui/build/_app/immutable/assets/SourcesList.BhtYlRsQ.css +0 -1
  87. package/webui/build/_app/immutable/chunks/BUngiKFg.js +0 -1
  88. package/webui/build/_app/immutable/chunks/Bt0fzibd.js +0 -1
  89. package/webui/build/_app/immutable/chunks/BxHqDcpw.js +0 -1
  90. package/webui/build/_app/immutable/chunks/ByQRbEUX.js +0 -1
  91. package/webui/build/_app/immutable/chunks/C12mHcUp.js +0 -6
  92. package/webui/build/_app/immutable/chunks/C1kQ4pHy.js +0 -1
  93. package/webui/build/_app/immutable/chunks/C74gbb4Q.js +0 -1
  94. package/webui/build/_app/immutable/chunks/CAtemnMo.js +0 -1
  95. package/webui/build/_app/immutable/chunks/CVjCNJia.js +0 -1
  96. package/webui/build/_app/immutable/chunks/CjQQ9_Q2.js +0 -2
  97. package/webui/build/_app/immutable/chunks/D-6mYMI1.js +0 -1
  98. package/webui/build/_app/immutable/chunks/D1Gs8-g3.js +0 -1
  99. package/webui/build/_app/immutable/chunks/D9dRVKgL.js +0 -1
  100. package/webui/build/_app/immutable/chunks/DCEY1XiC.js +0 -1
  101. package/webui/build/_app/immutable/chunks/DI-t-G_K.js +0 -2
  102. package/webui/build/_app/immutable/chunks/DTUxjyWL.js +0 -1
  103. package/webui/build/_app/immutable/chunks/DWJZOHke.js +0 -1
  104. package/webui/build/_app/immutable/chunks/Dgs6d7X5.js +0 -1
  105. package/webui/build/_app/immutable/chunks/DjpPK99f.js +0 -71
  106. package/webui/build/_app/immutable/chunks/DjzVVxpy.js +0 -1
  107. package/webui/build/_app/immutable/chunks/DvtNA-3X.js +0 -1
  108. package/webui/build/_app/immutable/chunks/LQVMBmDN.js +0 -1
  109. package/webui/build/_app/immutable/chunks/Qw0Qgx6J.js +0 -1
  110. package/webui/build/_app/immutable/chunks/bohabpgg.js +0 -1
  111. package/webui/build/_app/immutable/chunks/c-YfbAB_.js +0 -8
  112. package/webui/build/_app/immutable/chunks/tpTQfoNn.js +0 -1
  113. package/webui/build/_app/immutable/entry/app.Cra5Zsz4.js +0 -2
  114. package/webui/build/_app/immutable/entry/start.ToY0Qh0_.js +0 -1
  115. package/webui/build/_app/immutable/nodes/0.D2-xzG_8.js +0 -11
  116. package/webui/build/_app/immutable/nodes/1.CFixzRR6.js +0 -1
  117. package/webui/build/_app/immutable/nodes/10.ayxWydPr.js +0 -1
  118. package/webui/build/_app/immutable/nodes/11.B0JS3E2j.js +0 -1
  119. package/webui/build/_app/immutable/nodes/13.DRpZV72T.js +0 -1
  120. package/webui/build/_app/immutable/nodes/14.DVeJW6bd.js +0 -1
  121. package/webui/build/_app/immutable/nodes/2.DIZ4IPNm.js +0 -1
  122. package/webui/build/_app/immutable/nodes/3.BFSNf0FK.js +0 -1
  123. package/webui/build/_app/immutable/nodes/4.BSsIjejE.js +0 -2
  124. package/webui/build/_app/immutable/nodes/5.COxRT9Oe.js +0 -1
  125. package/webui/build/_app/immutable/nodes/6.CBgQ4YzB.js +0 -1
  126. package/webui/build/_app/immutable/nodes/7.BbzWOL0V.js +0 -6
  127. package/webui/build/_app/immutable/nodes/8.C8120200.js +0 -1
  128. package/webui/build/_app/immutable/nodes/9.BH_BGQQ4.js +0 -1
  129. /package/webui/build/_app/immutable/nodes/{15.BtYZF6FM.js → 17.BtYZF6FM.js} +0 -0
  130. /package/webui/build/_app/immutable/nodes/{16.Ba_qJjp6.js → 18.Ba_qJjp6.js} +0 -0
package/.env.example CHANGED
@@ -33,7 +33,7 @@ SMTP_PASS=your-smtp-password
33
33
  # Resend API Key(driver=resend 时使用;https://resend.com/api-keys)
34
34
  RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
35
35
 
36
- # ─── LLM(研究报告模式用)──────────────────────────────────────────────────
36
+ # ─── LLM(解析 / Pipeline 等;也可在管理后台「设置 → LLM」写入 .rssany/config.json)──
37
37
  OPENAI_API_KEY=sk-...
38
38
  OPENAI_BASE_URL=https://api.openai.com/v1
39
39
  OPENAI_MODEL=gpt-4o-mini
package/README.md CHANGED
@@ -5,18 +5,21 @@
5
5
 
6
6
  **RSSAny** 是一套自托管的订阅管线:列表 URL → **抓取与解析**(规则 / LLM)→ **正文提取**(自定义 / Readability / LLM)→ **upsert 去重** → 固定 **pipeline**(打标签、翻译等)→ 对外提供 `**/rss`** 等输出。
7
7
 
8
+ ## 界面预览
9
+
10
+ ![RSSAny Web 界面截图](images/ScreenShot_2026-04-10_153307_558.png)
11
+
8
12
  ---
9
13
 
10
14
  ## 功能概览
11
15
 
12
16
  - **统一订阅**:在 `.rssany/sources.json` 中配置网站列表、标准 RSS、IMAP 邮件等,由调度器按 `refresh` 策略拉取。
13
- - **可插拔信源**:`app/plugins/builtin/` 与 `.rssany/plugins/` 中的 **Site** 插件(`.rssany.js` / `.rssany.ts`),自定义列表解析与详情规则。
17
+ - **可插拔信源**:**Site / Source** 插件(`.rssany.js` / `.rssany.ts`),见 **[插件配置说明](./docs/plugins.md)**。
14
18
  - **正文与解析**:在信源 `fetchItems`(及需要的 `ctx.extractItem` 等)内完成;入库后跑 pipeline。
15
19
  - **固定 pipeline**:`app/pipeline/` 中打标签、翻译等,由 `.rssany/config.json` 的 `pipeline.steps` 开关(**不是**用户目录下的 pipeline 插件)。
16
20
  - **LLM 辅助**:解析、提取、标签、翻译等可按配置走 OpenAI 兼容接口。
17
21
  - **站点登录**:需登录的站点通过 Puppeteer 管理 Cookie(与产品用户账号无关)。
18
22
  - **可选远端投递**:若 `config.json` 中 `**deliver.url`** 非空,在写库与 pipeline 完成后将条目以 `**{ sourceRef, items }**` JSON **POST** 到该 URL(由 `app/deliver/post.ts` 发送);留空则仅本地消费。
19
- - **MCP**:条目检索等能力以 MCP 暴露,供 Cursor、Claude 等使用。
20
23
  - **Web 界面**:SvelteKit 构建产物由后端托管;**Feeds** 等需 **邮箱校验**;`**/admin`** 需 `**users.role === 'admin'**`(可从 `**/me**` 进入)。
21
24
 
22
25
  ---
@@ -38,82 +41,62 @@
38
41
 
39
42
  ## 快速开始
40
43
 
41
- ### 环境要求
42
-
43
- - Node.js **20.x–23.x**(与 `package.json` 的 `engines` 字段一致)
44
- - **pnpm**
44
+ 日常使用只需 **Node.js 20.x–23.x**(与 `package.json` 的 `engines` 一致):
45
45
 
46
- ### 安装依赖
46
+ ### 全局安装(推荐)
47
47
 
48
48
  ```bash
49
- pnpm install
50
- pnpm run webui:install
49
+ npm install -g rssany # 与 npm i -g rssany 相同
50
+ rssany
51
51
  ```
52
52
 
53
- ### 配置
53
+ 安装包内已包含构建好的后端与 Web 界面;启动后用浏览器打开终端里提示的地址(默认 **`http://127.0.0.1:18473/`**,端口可在**运行命令时当前目录**下的 `.env` 里设置 `PORT`)。
54
54
 
55
- 1. 复制环境变量示例并按需填写(JWT、OAuth、SMTP、LLM 等):
56
- ```bash
57
- cp .env.example .env
58
- ```
59
- 2. 信源与全局配置:首次启动会在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany\`)下自动从包内示例生成 `sources.json`、`config.json`(若已存在则不会覆盖)。也可手动复制仓库里的 `sources.example.json`、`config.examples.json`。
60
- 3. (可选)LLM:在 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL` 等。
61
-
62
- ### 运行
63
-
64
- **开发**(后端根路径托管 `webui` 构建产物,改前端需重新构建或 watch):
65
-
66
- ```bash
67
- # 推荐:API + 前端 watch(修改 Svelte 后自动写入构建目录,刷新浏览器即可)
68
- pnpm run dev:all
55
+ - **数据目录**:首次运行会在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany\`)自动从包内 **`init/`** 生成 `sources.json`、`config.json` 等(已存在则不会覆盖)。
56
+ - **可选配置**:在启动 `rssany` 时的**当前目录**放置 `.env`(可参考仓库里的 `.env.example`),用于 JWT、OAuth、SMTP、LLM(如 `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `OPENAI_MODEL`)等。
57
+ - **重置全部本地数据**(结束占用 `PORT` 的进程并删除用户目录,慎用):执行 **`rssany reset`**;在含 `.env` 的目录下运行可读取 `PORT` / `RSSANY_USER_DIR`,或事先在环境里导出这些变量。
69
58
 
70
- # 或分步:先打一次前端再起后端
71
- pnpm run webui:build
72
- pnpm dev
73
- ```
59
+ 等价于在项目里执行 `node node_modules/rssany/dist/index.js`;CLI 名为 **`rssany`**。
74
60
 
75
- 默认监听 `**http://127.0.0.1:18473/**`(端口见 `.env.example` 中 `PORT`,避免与常见开发端口冲突)。
61
+ ### 从源码运行(开发 / 贡献)
76
62
 
77
- **重置本地数据**(结束占用 `PORT` 的监听进程,并删除 **`~/.rssany/`**,或 `RSSANY_USER_DIR` 所设目录):
63
+ 需要 **pnpm**:
78
64
 
79
65
  ```bash
80
- pnpm reset
66
+ pnpm install
67
+ pnpm run webui:install
68
+ cp .env.example .env # 按需修改
81
69
  ```
82
70
 
83
- **仅调试 WebUI 热更新**(可选):`cd webui && pnpm dev`(Vite 代理到本机后端,见 `webui/vite.config.ts`)。
84
-
85
- **生产**:
71
+ **开发**(后端托管 `webui` 构建目录;改前端可 watch):
86
72
 
87
73
  ```bash
88
- pnpm run webui:build && pnpm start
74
+ pnpm run dev:all
89
75
  ```
90
76
 
91
- ### npm 全局安装(或 `npx`)
77
+ 或分步:`pnpm run webui:build` `pnpm dev`。
92
78
 
93
- 发布包时 `prepublishOnly` 会执行 `build:all`(后端 `vite build` + `webui:build`)。安装后:
79
+ **仅调试 WebUI 热更新**(可选):`cd webui && pnpm dev`(Vite 代理到本机后端,见 `webui/vite.config.ts`)。
94
80
 
95
- ```bash
96
- npm install -g rssany
97
- rssany
98
- ```
81
+ **生产**(本仓库):`pnpm run webui:build && pnpm start`。
99
82
 
100
- 重置数据(结束 `PORT` 监听进程并删除用户目录):**`rssany reset`**(与仓库内 **`pnpm reset`** 相同逻辑;可在含 `.env` 的目录下执行以读取 `PORT` / `RSSANY_USER_DIR`)。
83
+ **重置本地数据**(与全局安装的 `rssany reset` 逻辑相同):`pnpm reset`。
101
84
 
102
- 用户数据在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany`),与工作目录无关。可选环境变量 **`RSSANY_USER_DIR`** 可指定其它路径。等价于 `node node_modules/rssany/dist/index.js`;CLI 名称为 `rssany`。内置 `app/plugins/builtin/`、`statics/`、`webui/build` 随包安装路径解析。
85
+ 发布到 npm `prepublishOnly` 会执行 `build:all`(后端 `vite build` + `webui:build`)。
103
86
 
104
87
  ---
105
88
 
106
89
  ## 数据流(简图)
107
90
 
108
91
  ```
109
- sources.json / Site 插件
92
+ sources.json / 信源插件
110
93
  → 调度器触发 fetchItems
111
94
  → upsertItems
112
95
  → pipeline(每条一次)
113
96
  → [可选] deliver.url POST(出站,非入站 API)
114
97
  ```
115
98
 
116
- 消费侧:**RSS/XML**、`**/api/*`**、**MCP**、Web UI。
99
+ 消费侧:**RSS/XML**、`**/api/*`**、Web UI。
117
100
 
118
101
  ---
119
102
 
@@ -126,11 +109,9 @@ sources.json / Site 插件
126
109
 
127
110
  ---
128
111
 
129
- ## 插件与配置
130
-
131
- ### 信源插件(Site)
112
+ ## 配置
132
113
 
133
- 放置于 `**app/plugins/builtin/**` `**.rssany/plugins/**`(扁平),用户插件可与内置插件同 `id` 覆盖。最小约定包括 `id`、`listUrlPattern` 等(详见 `app/scraper/sources/web/site.ts`)。
114
+ **信源插件(Site / Source)**:目录约定、`listUrlPattern` / `pattern`、`fetchItems`、与 `sources.json` 的关系等,见 **[docs/plugins.md](./docs/plugins.md)**。
134
115
 
135
116
  ### Pipeline(固定代码)
136
117
 
@@ -169,8 +150,9 @@ sources.json / Site 插件
169
150
  ## 仓库目录(摘要)
170
151
 
171
152
  ```
172
- ├── app/ # 后端:路由、feeder、scraper、pipeline、mcp、db、auth…
153
+ ├── app/ # 后端:路由、feeder、scraper、pipeline、db、auth…
173
154
  │ └── plugins/builtin/ # 内置信源 *.rssany.js
155
+ ├── docs/ # 用户文档(如 plugins.md)
174
156
  └── webui/ # SvelteKit 前端
175
157
 
176
158
  ~/.rssany/ # 运行时用户数据(首次启动创建;或 RSSANY_USER_DIR)
@@ -1,30 +1,95 @@
1
- // 内置 RSS/Atom/JSON Feed 插件:匹配 *rss*、*atom*、*.xml 等标准 Feed URL
1
+ // 内置 RSS/Atom/JSON Feed:通过浏览器(Puppeteer)拉取 Feed URL,再用 rss-parser 解析;
2
+ // 与站点插件一致走 Chrome,便于应对需浏览器环境或代理的场景;XML 使用 HTTP 响应原文(useHttpResponseBody)。
2
3
 
3
4
  const UA = "RssAny/1.0 (+https://github.com/joohw/rssany)";
4
5
 
5
- async function fetchFeed(url, ctx) {
6
- const { deps } = ctx;
7
- const proxyToUse = ctx.proxy ?? process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY;
8
- if (proxyToUse) {
9
- const agent = new deps.HttpsProxyAgent(proxyToUse);
10
- const parserWithProxy = new deps.RssParser({
11
- timeout: 15_000,
12
- headers: {
13
- "User-Agent": UA,
14
- Accept: "application/rss+xml,application/atom+xml,application/json,application/xml,text/xml,*/*",
15
- },
16
- requestOptions: { agent },
17
- });
18
- return parserWithProxy.parseURL(url);
6
+ const IMAGE_TYPE_RE = /^image\//i;
7
+ const IMAGE_EXT_IN_PATH_RE = /\.(jpg|jpeg|png|gif|webp|avif|svg)(\?|#|$)/i;
8
+
9
+ function trimUrl(s) {
10
+ if (typeof s !== "string") return undefined;
11
+ const t = s.trim();
12
+ return t || undefined;
13
+ }
14
+
15
+ /** rss-parser 条目上尽量取出配图 URL(入库用 imageUrl,与 Gateway 的 cover_img 对齐)。 */
16
+ function extractItemImageUrl(item) {
17
+ const enc = item.enclosure;
18
+ if (enc && typeof enc.url === "string") {
19
+ const u = trimUrl(enc.url);
20
+ const t = typeof enc.type === "string" ? enc.type : "";
21
+ if (u && (IMAGE_TYPE_RE.test(t) || (!t && IMAGE_EXT_IN_PATH_RE.test(u)))) {
22
+ return u;
23
+ }
24
+ }
25
+
26
+ const itunesImg = item.itunes && typeof item.itunes.image === "string" ? item.itunes.image : undefined;
27
+ const fromItunes = trimUrl(itunesImg);
28
+ if (fromItunes) return fromItunes;
29
+
30
+ const thumbs = item.mediaThumbnail;
31
+ if (Array.isArray(thumbs) && thumbs[0]?.$) {
32
+ const u = trimUrl(thumbs[0].$.url ?? thumbs[0].$.href);
33
+ if (u) return u;
34
+ }
35
+
36
+ const mediaBlocks = item.mediaContent;
37
+ if (Array.isArray(mediaBlocks)) {
38
+ for (const block of mediaBlocks) {
39
+ const $ = block && block.$;
40
+ if (!$ || typeof $.url !== "string") continue;
41
+ const medium = $.medium;
42
+ const ctype = typeof $.type === "string" ? $.type : "";
43
+ if (medium === "image" || IMAGE_TYPE_RE.test(ctype)) {
44
+ const u = trimUrl($.url);
45
+ if (u) return u;
46
+ }
47
+ }
48
+ }
49
+
50
+ const atomLinks = item.atomLinks;
51
+ if (Array.isArray(atomLinks)) {
52
+ for (const l of atomLinks) {
53
+ const $ = l && l.$;
54
+ if (!$ || typeof $.href !== "string") continue;
55
+ const rel = String($.rel || "").toLowerCase();
56
+ const ctype = String($.type || "").toLowerCase();
57
+ if (rel === "enclosure" && ctype.startsWith("image/")) {
58
+ const u = trimUrl($.href);
59
+ if (u) return u;
60
+ }
61
+ }
19
62
  }
20
- const parser = new deps.RssParser({
21
- timeout: 15_000,
22
- headers: {
23
- "User-Agent": UA,
24
- Accept: "application/rss+xml,application/atom+xml,application/json,application/xml,text/xml,*/*",
25
- },
63
+
64
+ const fromHtml =
65
+ firstImgSrcFromHtml(item.content) ||
66
+ firstImgSrcFromHtml(item.summary) ||
67
+ firstImgSrcFromHtml(item["content:encoded"]) ||
68
+ firstImgSrcFromHtml(item.contentSnippet);
69
+ if (fromHtml && /^https?:\/\//i.test(fromHtml)) {
70
+ return fromHtml;
71
+ }
72
+
73
+ return undefined;
74
+ }
75
+
76
+ function firstImgSrcFromHtml(html) {
77
+ if (typeof html !== "string" || !html) return undefined;
78
+ const m = html.match(/<img[^>]+src\s*=\s*["']([^"']+)["']/i);
79
+ return m ? trimUrl(m[1]) : undefined;
80
+ }
81
+
82
+ async function fetchFeedXml(url, ctx) {
83
+ const fetchHtml = ctx.fetchHtml;
84
+ if (typeof fetchHtml !== "function") {
85
+ throw new Error("RSS 插件需要 ctx.fetchHtml(请通过 feeder / buildSourceContext 调用)");
86
+ }
87
+ const { html } = await fetchHtml(url, {
88
+ waitMs: 800,
89
+ purify: false,
90
+ useHttpResponseBody: true,
26
91
  });
27
- return parser.parseURL(url);
92
+ return html;
28
93
  }
29
94
 
30
95
  export default {
@@ -35,7 +100,22 @@ export default {
35
100
  refreshInterval: "1h",
36
101
  async fetchItems(sourceId, ctx) {
37
102
  const { deps } = ctx;
38
- const feed = await fetchFeed(sourceId, ctx);
103
+ const xml = await fetchFeedXml(sourceId, ctx);
104
+ const parser = new deps.RssParser({
105
+ timeout: 30_000,
106
+ headers: {
107
+ "User-Agent": UA,
108
+ Accept: "application/rss+xml,application/atom+xml,application/json,application/xml,text/xml,*/*",
109
+ },
110
+ customFields: {
111
+ item: [
112
+ ["media:thumbnail", "mediaThumbnail", { keepArray: true }],
113
+ ["media:content", "mediaContent", { keepArray: true }],
114
+ ["link", "atomLinks", { keepArray: true }],
115
+ ],
116
+ },
117
+ });
118
+ const feed = await parser.parseString(xml);
39
119
  return (feed.items ?? []).map((item) => {
40
120
  const link = item.link ?? item.guid ?? sourceId;
41
121
  const guid = item.guid ?? deps.createHash("sha256").update(link).digest("hex");
@@ -52,7 +132,8 @@ export default {
52
132
  typeof item.summary === "string" ? item.summary : typeof item.contentSnippet === "string" ? item.contentSnippet : undefined;
53
133
  const content =
54
134
  typeof item.content === "string" ? item.content : typeof item["content:encoded"] === "string" ? item["content:encoded"] : undefined;
55
- return {
135
+ const imageUrl = extractItemImageUrl(item);
136
+ const base = {
56
137
  guid,
57
138
  title: item.title ?? "",
58
139
  link,
@@ -61,6 +142,8 @@ export default {
61
142
  summary,
62
143
  content,
63
144
  };
145
+ if (!imageUrl) return base;
146
+ return { ...base, imageUrl, cover_img: imageUrl };
64
147
  });
65
148
  },
66
149
  };
@@ -7,7 +7,7 @@
7
7
 
8
8
  export default {
9
9
  id: "__PLUGIN_ID__",
10
- listUrlPattern: "https://example.com/{segment}",
10
+ listUrlPattern: __LIST_URL_PATTERN__,
11
11
  refreshInterval: "1day",
12
12
 
13
13
  /** sourceId 与订阅里 ref 一致;ctx 含 fetchHtml、extractItem、deps(parseHtml 等) */