rssany 0.1.0 → 0.1.2
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 +10 -11
- package/{plugins/sources → app/plugins/builtin}/email.rssany.js +92 -96
- package/app/plugins/builtin/rss.rssany.js +164 -0
- package/{plugins/templates → app/plugins}/site.rssany.js +6 -7
- package/dist/index.js +1200 -807
- 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 +6 -7
- 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.C6Q_nuW9.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/BXCWEhUd.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/CVzlFH44.js +1 -0
- package/webui/build/_app/immutable/chunks/CWNeClHp.js +6 -0
- package/webui/build/_app/immutable/chunks/Cihqbfi5.js +1 -0
- package/webui/build/_app/immutable/chunks/CkUAV0m0.js +41 -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/{Cg3zih_x.js → DcAshVxe.js} +1 -1
- 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/Dw782Tjs.js +1 -0
- package/webui/build/_app/immutable/chunks/EIZIMsXK.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.DdgnooOk.js +2 -0
- package/webui/build/_app/immutable/entry/start.DhJaJZhR.js +1 -0
- package/webui/build/_app/immutable/nodes/0.BE05Cuc4.js +11 -0
- package/webui/build/_app/immutable/nodes/1.5DFDaT4c.js +1 -0
- package/webui/build/_app/immutable/nodes/10.OVK4i9XE.js +1 -0
- package/webui/build/_app/immutable/nodes/11.Dhn_rO4A.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.B_KpJLxn.js +1 -0
- package/webui/build/_app/immutable/nodes/15.RaWaA-0I.js +1 -0
- package/webui/build/_app/immutable/nodes/{12.D9g8GCjm.js → 16.DSUgqolV.js} +15 -15
- package/webui/build/_app/immutable/nodes/2.BYWOpaxy.js +1 -0
- package/webui/build/_app/immutable/nodes/3.wQvGs9w-.js +1 -0
- package/webui/build/_app/immutable/nodes/4.DTSxpKm7.js +2 -0
- package/webui/build/_app/immutable/nodes/5.CCtn90c0.js +1 -0
- package/webui/build/_app/immutable/nodes/6.C2_mjW1u.js +1 -0
- package/webui/build/_app/immutable/nodes/7.Dwz6W7A1.js +1 -0
- package/webui/build/_app/immutable/nodes/8.DzkEw6rx.js +1 -0
- package/webui/build/_app/immutable/nodes/9.DtlXEwe1.js +1 -0
- package/webui/build/_app/version.json +1 -1
- package/plugins/sources/rss.rssany.js +0 -83
- 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/CkS2JMkE.js +0 -1
- 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/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.4I2fqDIL.js +0 -2
- package/webui/build/_app/immutable/entry/start.CrgdT2Qb.js +0 -1
- package/webui/build/_app/immutable/nodes/0.gA9sQtoM.js +0 -11
- package/webui/build/_app/immutable/nodes/1.Bybh7btp.js +0 -1
- package/webui/build/_app/immutable/nodes/10.DEkJCZ6X.js +0 -1
- package/webui/build/_app/immutable/nodes/11.CDNNJqlQ.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,13 +5,17 @@
|
|
|
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
|
-
- **可插拔信源**:`plugins/
|
|
14
|
-
-
|
|
17
|
+
- **可插拔信源**:`app/plugins/builtin/` 与 `.rssany/plugins/` 中的 **Site** 插件(`.rssany.js` / `.rssany.ts`),自定义列表解析与详情规则。
|
|
18
|
+
- **正文与解析**:在信源 `fetchItems`(及需要的 `ctx.extractItem` 等)内完成;入库后跑 pipeline。
|
|
15
19
|
- **固定 pipeline**:`app/pipeline/` 中打标签、翻译等,由 `.rssany/config.json` 的 `pipeline.steps` 开关(**不是**用户目录下的 pipeline 插件)。
|
|
16
20
|
- **LLM 辅助**:解析、提取、标签、翻译等可按配置走 OpenAI 兼容接口。
|
|
17
21
|
- **站点登录**:需登录的站点通过 Puppeteer 管理 Cookie(与产品用户账号无关)。
|
|
@@ -56,7 +60,7 @@ pnpm run webui:install
|
|
|
56
60
|
```bash
|
|
57
61
|
cp .env.example .env
|
|
58
62
|
```
|
|
59
|
-
2. 信源与全局配置:首次启动会在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany
|
|
63
|
+
2. 信源与全局配置:首次启动会在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany\`)下自动从包内 **`init/`** 目录中的默认数据复制生成 `sources.json`、`config.json`(若已存在则不会覆盖)。也可手动复制仓库里的 `init/sources.json`、`init/config.json`。
|
|
60
64
|
3. (可选)LLM:在 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL` 等。
|
|
61
65
|
|
|
62
66
|
### 运行
|
|
@@ -99,7 +103,7 @@ rssany
|
|
|
99
103
|
|
|
100
104
|
重置数据(结束 `PORT` 监听进程并删除用户目录):**`rssany reset`**(与仓库内 **`pnpm reset`** 相同逻辑;可在含 `.env` 的目录下执行以读取 `PORT` / `RSSANY_USER_DIR`)。
|
|
101
105
|
|
|
102
|
-
用户数据在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany`),与工作目录无关。可选环境变量 **`RSSANY_USER_DIR`** 可指定其它路径。等价于 `node node_modules/rssany/dist/index.js`;CLI 名称为 `rssany`。内置 `plugins/`、`statics/`、`webui/build` 随包安装路径解析。
|
|
106
|
+
用户数据在 **`~/.rssany/`**(Windows:`%USERPROFILE%\.rssany`),与工作目录无关。可选环境变量 **`RSSANY_USER_DIR`** 可指定其它路径。等价于 `node node_modules/rssany/dist/index.js`;CLI 名称为 `rssany`。内置 `app/plugins/builtin/`、`statics/`、`webui/build` 随包安装路径解析。
|
|
103
107
|
|
|
104
108
|
---
|
|
105
109
|
|
|
@@ -109,7 +113,6 @@ rssany
|
|
|
109
113
|
sources.json / Site 插件
|
|
110
114
|
→ 调度器触发 fetchItems
|
|
111
115
|
→ upsertItems
|
|
112
|
-
→ [可选] enrich 队列
|
|
113
116
|
→ pipeline(每条一次)
|
|
114
117
|
→ [可选] deliver.url POST(出站,非入站 API)
|
|
115
118
|
```
|
|
@@ -131,11 +134,7 @@ sources.json / Site 插件
|
|
|
131
134
|
|
|
132
135
|
### 信源插件(Site)
|
|
133
136
|
|
|
134
|
-
放置于 `**plugins/
|
|
135
|
-
|
|
136
|
-
### Enrich 插件
|
|
137
|
-
|
|
138
|
-
`**plugins/enrich/**`、`**.rssany/plugins/enrich/**`,按 enrich 管线加载。
|
|
137
|
+
放置于 `**app/plugins/builtin/**` 或 `**.rssany/plugins/**`(扁平),用户插件可与内置插件同 `id` 覆盖。最小约定包括 `id`、`listUrlPattern` 等(详见 `app/scraper/sources/web/site.ts`)。
|
|
139
138
|
|
|
140
139
|
### Pipeline(固定代码)
|
|
141
140
|
|
|
@@ -175,7 +174,7 @@ sources.json / Site 插件
|
|
|
175
174
|
|
|
176
175
|
```
|
|
177
176
|
├── app/ # 后端:路由、feeder、scraper、pipeline、mcp、db、auth…
|
|
178
|
-
|
|
177
|
+
│ └── plugins/builtin/ # 内置信源 *.rssany.js
|
|
179
178
|
└── webui/ # SvelteKit 前端
|
|
180
179
|
|
|
181
180
|
~/.rssany/ # 运行时用户数据(首次启动创建;或 RSSANY_USER_DIR)
|
|
@@ -1,96 +1,92 @@
|
|
|
1
|
-
// 内置 IMAP 邮件插件:匹配 imap://、imaps:// 协议 URL
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
logger:
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
return items.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime());
|
|
95
|
-
},
|
|
96
|
-
};
|
|
1
|
+
// 内置 IMAP 邮件插件:匹配 imap://、imaps:// 协议 URL
|
|
2
|
+
|
|
3
|
+
function parseImapUrl(sourceId) {
|
|
4
|
+
const url = new URL(sourceId);
|
|
5
|
+
const host = url.hostname;
|
|
6
|
+
const port = url.port ? parseInt(url.port, 10) : 993;
|
|
7
|
+
const secure = url.protocol === "imaps:" || port === 993;
|
|
8
|
+
const user = decodeURIComponent(url.username);
|
|
9
|
+
const pass = decodeURIComponent(url.password);
|
|
10
|
+
const folder = decodeURIComponent(url.pathname.slice(1)) || "INBOX";
|
|
11
|
+
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "30", 10));
|
|
12
|
+
return { host, port, secure, user, pass, folder, limit };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeGuid(messageId, uid, host, createHash) {
|
|
16
|
+
const raw = messageId ?? `${uid}@${host}`;
|
|
17
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
id: "__email__",
|
|
22
|
+
pattern: /^imaps?:\/\//,
|
|
23
|
+
priority: 0,
|
|
24
|
+
refreshInterval: "30min",
|
|
25
|
+
async 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
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// 内置 RSS/Atom/JSON Feed:通过浏览器(Puppeteer)拉取 Feed URL,再用 rss-parser 解析;
|
|
2
|
+
// 与站点插件一致走 Chrome,便于应对需浏览器环境或代理的场景;XML 使用 HTTP 响应原文(useHttpResponseBody)。
|
|
3
|
+
|
|
4
|
+
const UA = "RssAny/1.0 (+https://github.com/joohw/rssany)";
|
|
5
|
+
|
|
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
|
+
}
|
|
62
|
+
}
|
|
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,
|
|
91
|
+
});
|
|
92
|
+
return html;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default {
|
|
96
|
+
id: "__rss__",
|
|
97
|
+
pattern: /^https?:\/\//,
|
|
98
|
+
match: looksLikeFeed,
|
|
99
|
+
priority: 20,
|
|
100
|
+
refreshInterval: "1h",
|
|
101
|
+
async fetchItems(sourceId, ctx) {
|
|
102
|
+
const { deps } = 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);
|
|
119
|
+
return (feed.items ?? []).map((item) => {
|
|
120
|
+
const link = item.link ?? item.guid ?? sourceId;
|
|
121
|
+
const guid = item.guid ?? deps.createHash("sha256").update(link).digest("hex");
|
|
122
|
+
const pubDate =
|
|
123
|
+
item.pubDate != null
|
|
124
|
+
? new Date(item.pubDate)
|
|
125
|
+
: item.isoDate != null
|
|
126
|
+
? new Date(item.isoDate)
|
|
127
|
+
: new Date();
|
|
128
|
+
const authorRaw =
|
|
129
|
+
typeof item.creator === "string" ? item.creator : typeof item.author === "string" ? item.author : undefined;
|
|
130
|
+
const author = authorRaw ? [authorRaw] : undefined;
|
|
131
|
+
const summary =
|
|
132
|
+
typeof item.summary === "string" ? item.summary : typeof item.contentSnippet === "string" ? item.contentSnippet : undefined;
|
|
133
|
+
const content =
|
|
134
|
+
typeof item.content === "string" ? item.content : typeof item["content:encoded"] === "string" ? item["content:encoded"] : undefined;
|
|
135
|
+
const imageUrl = extractItemImageUrl(item);
|
|
136
|
+
const base = {
|
|
137
|
+
guid,
|
|
138
|
+
title: item.title ?? "",
|
|
139
|
+
link,
|
|
140
|
+
pubDate,
|
|
141
|
+
author,
|
|
142
|
+
summary,
|
|
143
|
+
content,
|
|
144
|
+
};
|
|
145
|
+
if (!imageUrl) return base;
|
|
146
|
+
return { ...base, imageUrl, cover_img: imageUrl };
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function looksLikeFeed(url) {
|
|
152
|
+
const lower = url.toLowerCase();
|
|
153
|
+
return (
|
|
154
|
+
lower.includes("/feed") ||
|
|
155
|
+
lower.includes("/rss") ||
|
|
156
|
+
lower.includes("/atom") ||
|
|
157
|
+
lower.endsWith(".xml") ||
|
|
158
|
+
lower.endsWith(".rss") ||
|
|
159
|
+
lower.endsWith(".atom") ||
|
|
160
|
+
lower.includes("format=rss") ||
|
|
161
|
+
lower.includes("format=atom") ||
|
|
162
|
+
lower.includes("output=rss")
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Site 插件模板(管理页「添加插件」会复制到 `.rssany/plugins/
|
|
2
|
+
* Site 插件模板(管理页「添加插件」会复制到 `.rssany/plugins/{id}.rssany.js`)
|
|
3
3
|
* 修改 `id` 后请与文件名保持一致。
|
|
4
4
|
*
|
|
5
5
|
* 接口说明:app/scraper/sources/web/site.ts
|
|
@@ -7,20 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
id: "__PLUGIN_ID__",
|
|
10
|
-
listUrlPattern:
|
|
10
|
+
listUrlPattern: __LIST_URL_PATTERN__,
|
|
11
11
|
refreshInterval: "1day",
|
|
12
12
|
|
|
13
|
-
/** sourceId 与订阅里 ref 一致;ctx 含 fetchHtml、extractItem
|
|
13
|
+
/** sourceId 与订阅里 ref 一致;ctx 含 fetchHtml、extractItem、deps(parseHtml 等) */
|
|
14
14
|
async fetchItems(sourceId, ctx) {
|
|
15
15
|
const { html, finalUrl } = await ctx.fetchHtml(sourceId, {
|
|
16
16
|
waitMs: 2000,
|
|
17
17
|
purify: true,
|
|
18
18
|
});
|
|
19
|
-
|
|
19
|
+
const root = ctx.deps.parseHtml(html);
|
|
20
|
+
void root;
|
|
20
21
|
void finalUrl;
|
|
21
|
-
// TODO:
|
|
22
|
+
// TODO: 用 ctx.deps.parseHtml 解析列表页,产出 { title, link, summary?, pubDate? } 等 FeedItem
|
|
22
23
|
return [];
|
|
23
24
|
},
|
|
24
|
-
|
|
25
|
-
// enrichItem: async (item, ctx) => ctx.extractItem(item),
|
|
26
25
|
};
|