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.
- package/.env.example +1 -1
- package/README.md +32 -50
- package/app/plugins/builtin/rss.rssany.js +107 -24
- package/app/plugins/site.rssany.js +1 -1
- package/dist/index.js +1003 -271
- package/dist/index.js.map +1 -1
- package/{config.examples.json → init/config.json} +7 -1
- package/init/sources.json +353 -0
- package/package.json +4 -6
- package/statics/401.html +1 -1
- package/statics/README.md +1 -1
- package/webui/build/200.html +16 -18
- package/webui/build/_app/immutable/assets/0.DjU2hdCQ.css +1 -0
- package/webui/build/_app/immutable/assets/10.Dj8_pmut.css +1 -0
- package/webui/build/_app/immutable/assets/11.qYZMiTb0.css +1 -0
- package/webui/build/_app/immutable/assets/12.Ct59LCqW.css +1 -0
- package/webui/build/_app/immutable/assets/13.BhO9zvFi.css +1 -0
- package/webui/build/_app/immutable/assets/14.CujIhjQK.css +1 -0
- package/webui/build/_app/immutable/assets/15.nNGjXhCQ.css +1 -0
- package/webui/build/_app/immutable/assets/16.PP9XLDf7.css +1 -0
- package/webui/build/_app/immutable/assets/4.9wPHhVwv.css +1 -0
- package/webui/build/_app/immutable/assets/6.DSJfjJwx.css +1 -0
- package/webui/build/_app/immutable/assets/7.CrNxmd8B.css +1 -0
- package/webui/build/_app/immutable/assets/8.Ba5_jYIY.css +1 -0
- package/webui/build/_app/immutable/assets/{9.BZheTlzZ.css → 9.m-LCx_kl.css} +1 -1
- package/webui/build/_app/immutable/assets/BackToParentRoute.DGk-X5ow.css +1 -0
- package/webui/build/_app/immutable/assets/SourcesList.yTBBi3_m.css +1 -0
- package/webui/build/_app/immutable/assets/homeFeedPanelStore.BopJZtHu.css +1 -0
- package/webui/build/_app/immutable/chunks/{V2-VOe88.js → B-OsL1Ct.js} +1 -1
- package/webui/build/_app/immutable/chunks/B2Q1a1-H.js +2 -0
- package/webui/build/_app/immutable/chunks/BK3WtZwv.js +1 -0
- package/webui/build/_app/immutable/chunks/BQqoDzLx.js +1 -0
- package/webui/build/_app/immutable/chunks/BbWUOQ_m.js +1 -0
- package/webui/build/_app/immutable/chunks/Bp63qm3L.js +1 -0
- package/webui/build/_app/immutable/chunks/{CZDFXKiF.js → C85CNwD2.js} +1 -1
- package/webui/build/_app/immutable/chunks/CVzlFH44.js +1 -0
- package/webui/build/_app/immutable/chunks/CWNeClHp.js +6 -0
- package/webui/build/_app/immutable/chunks/CdMsRjxJ.js +1 -0
- package/webui/build/_app/immutable/chunks/Cihqbfi5.js +1 -0
- package/webui/build/_app/immutable/chunks/CllQAdvt.js +1 -0
- package/webui/build/_app/immutable/chunks/CtijX1u3.js +31 -0
- package/webui/build/_app/immutable/chunks/D5GvRCv7.js +1 -0
- package/webui/build/_app/immutable/chunks/DEDI7Ecm.js +1 -0
- package/webui/build/_app/immutable/chunks/DFuhmi31.js +1 -0
- package/webui/build/_app/immutable/chunks/DMWEh-Ek.js +2 -0
- package/webui/build/_app/immutable/chunks/DjNLq3TF.js +1 -0
- package/webui/build/_app/immutable/chunks/Dt2CddFe.js +1 -0
- package/webui/build/_app/immutable/chunks/Dv1VCsiB.js +41 -0
- package/webui/build/_app/immutable/chunks/Dw782Tjs.js +1 -0
- package/webui/build/_app/immutable/chunks/Xy_fhzQq.js +1 -0
- package/webui/build/_app/immutable/chunks/lk5LaiqA.js +1 -0
- package/webui/build/_app/immutable/chunks/mW5RwvnK.js +13 -0
- package/webui/build/_app/immutable/chunks/{CtHRh_pJ.js → tB7QMF3U.js} +1 -1
- package/webui/build/_app/immutable/chunks/xtNWTdbD.js +1 -0
- package/webui/build/_app/immutable/entry/app.BcD2eSsQ.js +2 -0
- package/webui/build/_app/immutable/entry/start.CbkdJdz1.js +1 -0
- package/webui/build/_app/immutable/nodes/0.DSUDmOx2.js +11 -0
- package/webui/build/_app/immutable/nodes/1.DU9aYGAb.js +1 -0
- package/webui/build/_app/immutable/nodes/10.Db6vw7Ih.js +1 -0
- package/webui/build/_app/immutable/nodes/11.BaAcorz3.js +1 -0
- package/webui/build/_app/immutable/nodes/12.Cg8AeCSH.js +1 -0
- package/webui/build/_app/immutable/nodes/13.nT3SOzEB.js +1 -0
- package/webui/build/_app/immutable/nodes/14.DqT4pcrQ.js +1 -0
- package/webui/build/_app/immutable/nodes/15.CCLbjxnH.js +1 -0
- package/webui/build/_app/immutable/nodes/{12.CMcby_lY.js → 16.DiigpVdP.js} +15 -15
- package/webui/build/_app/immutable/nodes/2.BYWOpaxy.js +1 -0
- package/webui/build/_app/immutable/nodes/3.DEcYOQc-.js +1 -0
- package/webui/build/_app/immutable/nodes/4.DTSxpKm7.js +2 -0
- package/webui/build/_app/immutable/nodes/5.CvM1TkLG.js +1 -0
- package/webui/build/_app/immutable/nodes/6.Dscr6LkS.js +1 -0
- package/webui/build/_app/immutable/nodes/7.Bp60MobD.js +1 -0
- package/webui/build/_app/immutable/nodes/8.DwSg0MHh.js +1 -0
- package/webui/build/_app/immutable/nodes/9.BeYOUjxR.js +1 -0
- package/webui/build/_app/version.json +1 -1
- package/sources.example.json +0 -562
- package/webui/build/_app/immutable/assets/0.BUAXpTm6.css +0 -1
- package/webui/build/_app/immutable/assets/10.I1OuCLrU.css +0 -1
- package/webui/build/_app/immutable/assets/11.CrO9xaki.css +0 -1
- package/webui/build/_app/immutable/assets/12.BEi6fInA.css +0 -1
- package/webui/build/_app/immutable/assets/14.Ctlgn1LZ.css +0 -1
- package/webui/build/_app/immutable/assets/2.eJ80XOGm.css +0 -1
- package/webui/build/_app/immutable/assets/4.B8-jYAVj.css +0 -1
- package/webui/build/_app/immutable/assets/6.Drn-0DON.css +0 -1
- package/webui/build/_app/immutable/assets/7.ms2diq_q.css +0 -1
- package/webui/build/_app/immutable/assets/8.DKymkjjs.css +0 -1
- package/webui/build/_app/immutable/assets/SourcesList.BhtYlRsQ.css +0 -1
- package/webui/build/_app/immutable/chunks/BUngiKFg.js +0 -1
- package/webui/build/_app/immutable/chunks/Bt0fzibd.js +0 -1
- package/webui/build/_app/immutable/chunks/BxHqDcpw.js +0 -1
- package/webui/build/_app/immutable/chunks/ByQRbEUX.js +0 -1
- package/webui/build/_app/immutable/chunks/C12mHcUp.js +0 -6
- package/webui/build/_app/immutable/chunks/C1kQ4pHy.js +0 -1
- package/webui/build/_app/immutable/chunks/C74gbb4Q.js +0 -1
- package/webui/build/_app/immutable/chunks/CAtemnMo.js +0 -1
- package/webui/build/_app/immutable/chunks/CVjCNJia.js +0 -1
- package/webui/build/_app/immutable/chunks/CjQQ9_Q2.js +0 -2
- package/webui/build/_app/immutable/chunks/D-6mYMI1.js +0 -1
- package/webui/build/_app/immutable/chunks/D1Gs8-g3.js +0 -1
- package/webui/build/_app/immutable/chunks/D9dRVKgL.js +0 -1
- package/webui/build/_app/immutable/chunks/DCEY1XiC.js +0 -1
- package/webui/build/_app/immutable/chunks/DI-t-G_K.js +0 -2
- package/webui/build/_app/immutable/chunks/DTUxjyWL.js +0 -1
- package/webui/build/_app/immutable/chunks/DWJZOHke.js +0 -1
- package/webui/build/_app/immutable/chunks/Dgs6d7X5.js +0 -1
- package/webui/build/_app/immutable/chunks/DjpPK99f.js +0 -71
- package/webui/build/_app/immutable/chunks/DjzVVxpy.js +0 -1
- package/webui/build/_app/immutable/chunks/DvtNA-3X.js +0 -1
- package/webui/build/_app/immutable/chunks/LQVMBmDN.js +0 -1
- package/webui/build/_app/immutable/chunks/Qw0Qgx6J.js +0 -1
- package/webui/build/_app/immutable/chunks/bohabpgg.js +0 -1
- package/webui/build/_app/immutable/chunks/c-YfbAB_.js +0 -8
- package/webui/build/_app/immutable/chunks/tpTQfoNn.js +0 -1
- package/webui/build/_app/immutable/entry/app.Cra5Zsz4.js +0 -2
- package/webui/build/_app/immutable/entry/start.ToY0Qh0_.js +0 -1
- package/webui/build/_app/immutable/nodes/0.D2-xzG_8.js +0 -11
- package/webui/build/_app/immutable/nodes/1.CFixzRR6.js +0 -1
- package/webui/build/_app/immutable/nodes/10.ayxWydPr.js +0 -1
- package/webui/build/_app/immutable/nodes/11.B0JS3E2j.js +0 -1
- package/webui/build/_app/immutable/nodes/13.DRpZV72T.js +0 -1
- package/webui/build/_app/immutable/nodes/14.DVeJW6bd.js +0 -1
- package/webui/build/_app/immutable/nodes/2.DIZ4IPNm.js +0 -1
- package/webui/build/_app/immutable/nodes/3.BFSNf0FK.js +0 -1
- package/webui/build/_app/immutable/nodes/4.BSsIjejE.js +0 -2
- package/webui/build/_app/immutable/nodes/5.COxRT9Oe.js +0 -1
- package/webui/build/_app/immutable/nodes/6.CBgQ4YzB.js +0 -1
- package/webui/build/_app/immutable/nodes/7.BbzWOL0V.js +0 -6
- package/webui/build/_app/immutable/nodes/8.C8120200.js +0 -1
- package/webui/build/_app/immutable/nodes/9.BH_BGQQ4.js +0 -1
- /package/webui/build/_app/immutable/nodes/{15.BtYZF6FM.js → 17.BtYZF6FM.js} +0 -0
- /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
|
+

|
|
11
|
+
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
## 功能概览
|
|
11
15
|
|
|
12
16
|
- **统一订阅**:在 `.rssany/sources.json` 中配置网站列表、标准 RSS、IMAP 邮件等,由调度器按 `refresh` 策略拉取。
|
|
13
|
-
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
+
### 从源码运行(开发 / 贡献)
|
|
76
62
|
|
|
77
|
-
|
|
63
|
+
需要 **pnpm**:
|
|
78
64
|
|
|
79
65
|
```bash
|
|
80
|
-
pnpm
|
|
66
|
+
pnpm install
|
|
67
|
+
pnpm run webui:install
|
|
68
|
+
cp .env.example .env # 按需修改
|
|
81
69
|
```
|
|
82
70
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
**生产**:
|
|
71
|
+
**开发**(后端托管 `webui` 构建目录;改前端可 watch):
|
|
86
72
|
|
|
87
73
|
```bash
|
|
88
|
-
pnpm run
|
|
74
|
+
pnpm run dev:all
|
|
89
75
|
```
|
|
90
76
|
|
|
91
|
-
|
|
77
|
+
或分步:`pnpm run webui:build` 后 `pnpm dev`。
|
|
92
78
|
|
|
93
|
-
|
|
79
|
+
**仅调试 WebUI 热更新**(可选):`cd webui && pnpm dev`(Vite 代理到本机后端,见 `webui/vite.config.ts`)。
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
npm install -g rssany
|
|
97
|
-
rssany
|
|
98
|
-
```
|
|
81
|
+
**生产**(本仓库):`pnpm run webui:build && pnpm start`。
|
|
99
82
|
|
|
100
|
-
|
|
83
|
+
**重置本地数据**(与全局安装的 `rssany reset` 逻辑相同):`pnpm reset`。
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
发布到 npm 时 `prepublishOnly` 会执行 `build:all`(后端 `vite build` + `webui:build`)。
|
|
103
86
|
|
|
104
87
|
---
|
|
105
88
|
|
|
106
89
|
## 数据流(简图)
|
|
107
90
|
|
|
108
91
|
```
|
|
109
|
-
sources.json /
|
|
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
|
|
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
|
-
|
|
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、
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
};
|