dsh-rss 0.5.1 → 0.6.0
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/CHANGELOG.md +41 -0
- package/README.en.md +45 -137
- package/README.md +45 -143
- package/docs/CHANGELOG.en.md +27 -0
- package/docs/USAGE.en.md +110 -0
- package/docs/USAGE.md +111 -0
- package/docs/VALIDATION.md +27 -0
- package/docs/validation-0.6.0.md +11 -0
- package/docs/validation-2026-10-02-fakeip.md +32 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/catalog.d.ts +29 -0
- package/lib/catalog.js +40 -0
- package/lib/client.js +78 -10
- package/lib/web.d.ts +30 -0
- package/lib/web.js +14 -1
- package/package.json +5 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
[返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
|
+
|
|
7
|
+
## 0.6.0 (2026-10-04)
|
|
8
|
+
|
|
9
|
+
- 新增推荐订阅目录:16 个网站原生 RSS/Atom,提供五类主题、关键词与内容语言筛选、近期文章预览、可用性检查和「已订阅」状态。浏览目录不会自动订阅或抓取全部来源。
|
|
10
|
+
- 保留「输入地址」和 OPML 导入入口;分类可选择或自定义,已有名称和分类原样保留。预览与订阅共用原来的网络保护与订阅存储。
|
|
11
|
+
- 本次真实网络读取通过 16 个来源;可用性仍取决于各用户网络。有效但暂时空的来源会明确解释;失败时保留输入。
|
|
12
|
+
- 验收范围见 [0.6.0 推荐源检查](docs/validation-0.6.0.md)。
|
|
13
|
+
|
|
14
|
+
## 0.5.2 (2026-10-03)
|
|
15
|
+
|
|
16
|
+
- 修复语言切换时菜单滞后一轮;订阅检查、OPML 跳过原因与后台错误引导支持英文,订阅名称和文章保留原文。
|
|
17
|
+
- 验收范围见 [中英文界面检查](docs/validation-language-2026-10-03.md)。
|
|
18
|
+
|
|
19
|
+
## 0.5.1 (2026-10-02)
|
|
20
|
+
|
|
21
|
+
修复 VPN 开启 Fake-IP 后外部订阅源被拦截的问题。Windows 默认沿用已启用的手动系统代理;检测到 `198.18.0.0/15` 时,通过 HTTPS DNS 查询真实公网地址,再固定连接地址并保留原域名和 TLS 证书校验。VPN 可以保持开启,无需修改代理软件配置或放开内网访问。抓取、跳转或读取结束后释放本次请求的连接。
|
|
22
|
+
|
|
23
|
+
106 项 Windows 测试通过;在 VPN 开启、原生 DNS 返回 Fake-IP 的环境中,少数派、Python Insider、NASA 和 GitHub Atom 的检查、添加、抓取、持久化增量去重与搜索通过正常插件调用验证,没有注入测试 DNS 或抓取结果。Python 旧订阅地址也仍可读取。桌面原生窗口的单独验收不包含在这些结果中。
|
|
24
|
+
|
|
25
|
+
## 0.5.0 (2026-10-02)
|
|
26
|
+
|
|
27
|
+
新增 **设置 → RSS 订阅** 面板:添加、编辑、移除、搜索、分类筛选、检查可用性与 OPML 导入导出。面板与对话工具共用原有订阅列表,不需要迁移数据。跟随 DSH 的中文/英文和外观设置。
|
|
28
|
+
|
|
29
|
+
OPML 导入先预览新增、重复和无效项,再确认保存。默认保留重复订阅的名称和分类;勾选更新时会展示变化。保存失败或预览过期时保留输入。移除订阅需要确认具体名称和地址。
|
|
30
|
+
|
|
31
|
+
同时修复损坏 XML 被部分导入、无效/含密码 URL 被接受,以及并发添加覆盖另一笔订阅的问题。检查记录只保留在本次 DSH 运行中,导入不会自动抓取全部订阅。
|
|
32
|
+
|
|
33
|
+
94 项 Windows 测试通过;在官方 Harness `0.2.0-rc.2` 中实际操作了添加、编辑、筛选、检查、导入、移除、Tab/Enter/Esc、双页面冲突和重启持久化。桌面原生窗口与窄屏交互本轮未完成验收,外部生产订阅未替代为本地模拟测试。
|
|
34
|
+
|
|
35
|
+
## 0.4.3 (2026-10-01)
|
|
36
|
+
|
|
37
|
+
修复订阅 URL 的大小写比较:添加、删除、OPML 导入与搜索统一归一化协议、域名和默认端口,保留路径和查询参数的大小写,避免合并或删除不同订阅源。保留此前的代理 Fake-IP DNS 提示和保留地址检查。
|
|
38
|
+
|
|
39
|
+
## 更早的改动
|
|
40
|
+
|
|
41
|
+
完整历史可查阅 [GitHub 提交记录](https://github.com/STARDUSTLC666/dsh-rss/commits/master)。
|
package/README.en.md
CHANGED
|
@@ -1,139 +1,47 @@
|
|
|
1
1
|
# dsh-rss
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## Subscription panel
|
|
50
|
-
|
|
51
|
-
Open **Settings → RSS subscriptions**. New URLs are fetched and checked before saving; the name and category are optional. Renaming or recategorizing an unchanged URL works without the remote feed being online.
|
|
52
|
-
|
|
53
|
-
Choose an OPML file (up to 1 MiB) or paste its text, review the preview, then confirm. Rejected URLs include a reason. If another page or tool changed the list, preview again without retyping. Export downloads OPML without changing subscriptions.
|
|
54
|
-
|
|
55
|
-
Cards show the most recent check/fetch time, article count or error during this run. Read-only configurations allow viewing, checking and exporting.
|
|
56
|
-
|
|
57
|
-
## Uninstall
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
dsh plugin --profile web remove dsh-rss
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
## Configuration
|
|
67
|
-
|
|
68
|
-
Override the plugin row in your profile's `cordis.patch.yml` (the plugin also loads with all defaults when absent):
|
|
69
|
-
|
|
70
|
-
```yaml
|
|
71
|
-
- id: rss
|
|
72
|
-
name: 'dsh-rss'
|
|
73
|
-
config:
|
|
74
|
-
# proxyUrl: http://127.0.0.1:7890 # optional override for the Windows system proxy
|
|
75
|
-
# useSystemProxy: true # use the enabled Windows manual proxy by default
|
|
76
|
-
# fakeIpDnsFallback: true # validate real public DNS answers when Fake-IP is detected
|
|
77
|
-
timeoutMs: 15000 # fetch timeout in ms (default 15000)
|
|
78
|
-
# maxBodyBytes: 5242880 # response size cap (default 5MB, guards oversized responses)
|
|
79
|
-
# userAgent: 'dsh-rss/0.2.0' # custom fetch UA
|
|
80
|
-
# feedsYaml: | # optional: pre-seed subscriptions (or use the rss_add tool)
|
|
81
|
-
# - url: https://example.com/feed.xml
|
|
82
|
-
# name: My feed
|
|
83
|
-
# category: tech
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Tools
|
|
87
|
-
|
|
88
|
-
| Tool | Purpose | Key parameters |
|
|
89
|
-
| :-- | :-- | :-- |
|
|
90
|
-
| `rss_list` | List subscribed feeds | none |
|
|
91
|
-
| `rss_add` | Add a subscription (fetches and validates the URL first) | `url` required; `name`/`category` optional |
|
|
92
|
-
| `rss_remove` | Remove a subscription | `url` or `name`, at least one |
|
|
93
|
-
| `rss_fetch` | Fetch and parse a feed, returning feed info and entries (with full `content`) | `url` or `name`, at least one; `limit` 1-100, default 20 |
|
|
94
|
-
| `rss_check` | Validate that a URL is a parseable feed | `url` required |
|
|
95
|
-
| `rss_opml_export` | Export subscriptions as OPML 2.0 text (optionally write a file) | `path` optional |
|
|
96
|
-
| `rss_opml_import` | Bulk-import subscriptions from OPML 2.0 text | `opml` required |
|
|
97
|
-
|
|
98
|
-
| `rss_search` | Search across feeds | `query` required; optional feed, date and limit filters |
|
|
99
|
-
| `rss_health` | Check configuration, feeds and cursors offline | none |
|
|
100
|
-
|
|
101
|
-
### Examples
|
|
102
|
-
|
|
103
|
-
```text
|
|
104
|
-
rss_add { url: https://example.com/feed.xml, name: my-feed }
|
|
105
|
-
rss_fetch { name: my-feed, limit: 10 }
|
|
106
|
-
rss_check { url: https://example.com/feed.xml }
|
|
107
|
-
rss_opml_export { path: subscriptions.opml }
|
|
108
|
-
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Subscriptions
|
|
112
|
-
|
|
113
|
-
On Harness 0.1.7, subscriptions and read cursors are stored in the current profile’s `rss` entry. Changes apply immediately and survive restart. Default installations automatically import the retired `dsh-rss` section from `settings.yaml` or `settings.yaml.imported` once, preserving the original file and existing profile values. Older hosts retain their original settings storage. Use a URL to distinguish feeds with identical names.
|
|
114
|
-
|
|
115
|
-
## Proxy
|
|
116
|
-
|
|
117
|
-
Windows uses its enabled manual proxy by default, honoring protocol selection and bypass entries. No PAC scripts are evaluated and OS settings are never modified. An explicit `proxyUrl` takes precedence; `useSystemProxy: false` disables automatic detection. Other platforms can still configure an HTTP(S) `proxyUrl`. These routes apply only to this plugin.
|
|
118
|
-
|
|
119
|
-
When native DNS returns VPN Fake-IP (`198.18.0.0/15`), only the feed hostname is sent to [Cloudflare HTTPS DNS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/), with Google HTTPS DNS as a service-failure fallback. No URL paths, feed content, credentials or subscription lists are sent. Answers must pass public-address checks, and actual connections are pinned to those addresses. Disable this behavior with `fakeIpDnsFallback: false`. Ordinary public DNS, private DNS and IP literals do not trigger it. Trusted internal feeds still require an explicit `allowPrivateNetwork: true`.
|
|
120
|
-
|
|
121
|
-
## Parsing capabilities
|
|
122
|
-
|
|
123
|
-
- RSS 2.0 / RSS 1.0 (RDF) / Atom, normalized to one output shape
|
|
124
|
-
- Entity decoding, CDATA, `content:encoded`, `dc:creator` and other common fields
|
|
125
|
-
- RFC 822 / ISO 8601 dates normalized to ISO 8601 UTC (`pubDate`); the raw text stays in `pubDateRaw`
|
|
126
|
-
- Relative links resolved against the feed URL
|
|
127
|
-
- Summaries stripped of HTML tags and truncated at 500 chars; RSS `content:encoded` / Atom `content` is preserved as a `content` field (tags stripped, up to 20000 chars)
|
|
128
|
-
- Safety: no DTD / external entity parsing; 5MB body cap; configurable fetch timeout
|
|
129
|
-
|
|
130
|
-
## Development
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
pnpm install
|
|
134
|
-
pnpm test # build + offline tests with explicit fetch and DNS fixtures
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## License
|
|
138
|
-
|
|
139
|
-
MIT
|
|
3
|
+
[中文](README.md)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Subscribe to and read RSS or Atom feeds, and manage updates from DSH.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss)
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Add feeds, read articles and fetch updates.
|
|
14
|
+
- Choose from five catalog categories, preview recent articles and subscribe, or enter a URL.
|
|
15
|
+
- Import or export OPML and retain article content.
|
|
16
|
+
- Use a feed panel and optional plugin-level proxy configuration.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
In DSH Desktop, install `dsh-rss` from the Plugins panel. If the bundled dsh command is available:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
dsh plugin --profile desktop add dsh-rss
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For the web version, replace `desktop` with `web`. Restart DSH after installation.
|
|
27
|
+
|
|
28
|
+
## Start using it
|
|
29
|
+
|
|
30
|
+
Open Settings → RSS subscriptions → Recommended feeds, choose a topic, preview articles and subscribe. You can also enter your own feed URL or ask to subscribe and summarize recent articles.
|
|
31
|
+
|
|
32
|
+
## Requirements and configuration
|
|
33
|
+
|
|
34
|
+
No account key is required. Feeds are accessed directly by default; a proxy depends on feed reachability from your network.
|
|
35
|
+
|
|
36
|
+
Detailed configuration, tool arguments and troubleshooting are in the [usage guide](docs/USAGE.en.md). For standalone development, follow the Node requirement in [package.json](package.json).
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
- [Usage and troubleshooting](docs/USAGE.en.md)
|
|
41
|
+
- [Changelog](CHANGELOG.md)
|
|
42
|
+
- [Validation scope and history](docs/VALIDATION.md)
|
|
43
|
+
- [Report a problem or suggest a feature](https://github.com/STARDUSTLC666/dsh-rss/issues)
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,145 +1,47 @@
|
|
|
1
|
-
[English](README.en.md)
|
|
2
|
-
|
|
3
1
|
# dsh-rss
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
安装后重启 Web 服务即可。直接对助手说「订阅这个 RSS 地址,命名为技术资讯」或「列出我的订阅」即可使用,日常增删订阅不需要编辑配置文件。
|
|
51
|
-
|
|
52
|
-
桌面版使用 `dsh plugin --profile desktop add dsh-rss`,安装或更新后重新打开 DSH。网页和桌面的共享设置页面均可显示订阅面板;已安装的旧版本请先通过插件管理器更新。
|
|
53
|
-
|
|
54
|
-
## 订阅面板
|
|
55
|
-
|
|
56
|
-
打开 **设置 → RSS 订阅**。添加时填写 RSS/Atom 地址,名称可留空,分类可选。新地址通过抓取检查后才会保存;编辑原地址的名称或分类不需要源网站在线。
|
|
57
|
-
|
|
58
|
-
导入可选择不超过 1 MiB 的 OPML 文件或粘贴文本。先检查预览,再点击确认;跳过的地址会说明原因。已有订阅发生变化时重新预览即可,原输入仍保留。导出通过浏览器下载 OPML,不会修改订阅。
|
|
59
|
-
|
|
60
|
-
列表卡片显示最近一次手动检查或工具抓取的时间和文章数,失败时显示具体原因。只读配置仍可查看、检查和导出。
|
|
61
|
-
|
|
62
|
-
## 卸载
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
dsh plugin --profile web remove dsh-rss
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## 配置
|
|
72
|
-
|
|
73
|
-
在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时插件也能加载,只是全部用默认值):
|
|
74
|
-
|
|
75
|
-
```yaml
|
|
76
|
-
- id: rss
|
|
77
|
-
name: 'dsh-rss'
|
|
78
|
-
config:
|
|
79
|
-
# proxyUrl: http://127.0.0.1:7890 # 可选;覆盖自动读取的 Windows 系统代理
|
|
80
|
-
# useSystemProxy: true # 默认读取 Windows 已启用的手动系统代理
|
|
81
|
-
# fakeIpDnsFallback: true # 默认在 Fake-IP 下查询并校验真实公网地址
|
|
82
|
-
timeoutMs: 15000 # 抓取超时(毫秒,默认 15000)
|
|
83
|
-
# maxBodyBytes: 5242880 # 订阅源体积上限(默认 5MB,防超大响应)
|
|
84
|
-
# userAgent: 'dsh-rss/0.2.0' # 自定义抓取 UA
|
|
85
|
-
# feedsYaml: | # 可选:预置订阅列表(也可用 rss_add 工具添加)
|
|
86
|
-
# - url: https://example.com/feed.xml
|
|
87
|
-
# name: 示例订阅
|
|
88
|
-
# category: 技术
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 工具一览
|
|
92
|
-
|
|
93
|
-
| 工具 | 作用 | 关键参数 |
|
|
94
|
-
| :-- | :-- | :-- |
|
|
95
|
-
| `rss_list` | 列出已订阅源 | 无 |
|
|
96
|
-
| `rss_add` | 添加订阅(先抓取校验地址) | `url` 必填;`name`/`category` 可选 |
|
|
97
|
-
| `rss_remove` | 删除订阅 | `url` 或 `name` 至少一个 |
|
|
98
|
-
| `rss_fetch` | 抓取解析订阅源,返回源信息与条目(含正文 content) | `url` 或 `name` 至少一个;`limit` 1-100 默认 20 |
|
|
99
|
-
| `rss_check` | 校验地址是否为可解析的订阅源 | `url` 必填 |
|
|
100
|
-
| `rss_opml_export` | 导出订阅列表为 OPML 2.0 文本(可写入文件) | `path` 可选 |
|
|
101
|
-
| `rss_opml_import` | 从 OPML 2.0 文本批量导入订阅源 | `opml` 必填 |
|
|
102
|
-
| `rss_search` | 跨订阅源关键词搜索条目(标题/摘要/正文/作者) | `query` 必填;`name`/`url`/`since`/`limit` 可选 |
|
|
103
|
-
| `rss_health` | 插件自检(配置/订阅/游标/解析器,离线) | 无 |
|
|
104
|
-
|
|
105
|
-
### 示例
|
|
106
|
-
|
|
107
|
-
```text
|
|
108
|
-
rss_add { url: https://example.com/feed.xml, name: 我的订阅 }
|
|
109
|
-
rss_fetch { name: 我的订阅, limit: 10 }
|
|
110
|
-
rss_check { url: https://example.com/feed.xml }
|
|
111
|
-
rss_opml_export { path: subscriptions.opml }
|
|
112
|
-
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## 订阅存储
|
|
116
|
-
|
|
117
|
-
Harness 0.1.7 把订阅列表和读取游标保存在当前 profile 的 `rss` 配置行;增删后立即生效,重启仍保留。默认安装会自动导入旧 `settings.yaml` 或 `settings.yaml.imported` 的 `dsh-rss` 数据一次,保留原文件,且不覆盖已有 profile 值。旧版宿主继续使用原 settings 存储。同名订阅请用 URL 区分。
|
|
118
|
-
|
|
119
|
-
## 代理与 VPN
|
|
120
|
-
|
|
121
|
-
Windows 上默认读取已启用的手动系统代理,尊重协议与绕过列表;不执行 PAC 脚本,也不修改系统设置。`proxyUrl` 显式配置优先;`useSystemProxy: false` 可关闭自动读取。其他平台仍可通过 `proxyUrl` 配置 HTTP(S) 代理。这些代理仅作用于本插件。
|
|
122
|
-
|
|
123
|
-
当系统 DNS 返回 VPN 的 Fake-IP(`198.18.0.0/15`)时,默认只向 [Cloudflare HTTPS DNS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/) 查询订阅源域名;服务不可用时尝试 Google HTTPS DNS。查询不会发送订阅源路径、正文、密钥或订阅列表,回答仍须通过公网地址检查;实际连接固定到检查过的地址。`fakeIpDnsFallback: false` 可关闭该兼容方式。普通公网 DNS、私网 DNS 和 IP 字面量不会触发此查询。可信内网订阅仍需单独显式配置 `allowPrivateNetwork: true`。
|
|
124
|
-
|
|
125
|
-
## 解析能力
|
|
126
|
-
|
|
127
|
-
- RSS 2.0 / RSS 1.0(RDF)/ Atom 三种格式,统一归一化输出
|
|
128
|
-
- 实体解码、CDATA、`content:encoded`、`dc:creator`、`itunes` 等常见字段
|
|
129
|
-
- RFC 822 / ISO 8601 日期统一转 ISO 8601 UTC(`pubDate`),原始文本保留在 `pubDateRaw`
|
|
130
|
-
- 相对链接按订阅源地址解析成绝对链接
|
|
131
|
-
- 摘要去 HTML 标签并截断到 500 字符;RSS `content:encoded` / Atom `content` 作为 `content` 字段保留(去标签后最多 20000 字符)
|
|
132
|
-
- 安全:不解析 DTD/外部实体,内容只做文本抽取;响应体积上限 5MB;抓取超时可配
|
|
133
|
-
|
|
134
|
-
## 开发
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
pnpm install
|
|
138
|
-
pnpm test # 构建 + 离线测试(fetch 与 DNS 均使用夹具)
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
发布前门禁:危险模式扫描、manifest 自检、`pnpm audit --prod`、全量测试,以及全新 profile 的真实启动冒烟测试。
|
|
142
|
-
|
|
143
|
-
## License
|
|
144
|
-
|
|
145
|
-
MIT
|
|
3
|
+
[English](README.en.md)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
订阅和阅读 RSS / Atom,在 DSH 中查看与管理更新。
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss)
|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- 添加订阅、查看文章和抓取更新。
|
|
14
|
+
- 从五类推荐目录选择来源,预览近期文章后订阅;也可输入地址或导入 OPML。
|
|
15
|
+
- 导入、导出 OPML,保留文章正文。
|
|
16
|
+
- 可视化订阅面板,支持插件级网络代理。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
桌面版可在「插件」面板按包名 `dsh-rss` 安装。已配置 dsh 命令时也可使用:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
dsh plugin --profile desktop add dsh-rss
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
网页版把命令中的 `desktop` 改为 `web`。安装后重启 DSH。
|
|
27
|
+
|
|
28
|
+
## 开始使用
|
|
29
|
+
|
|
30
|
+
打开「设置 → RSS 订阅 → 推荐订阅源」,选择感兴趣的分类,预览后点击订阅;也可以输入自己的地址,或说:“订阅这个 RSS 地址并总结最新文章。”
|
|
31
|
+
|
|
32
|
+
## 依赖与配置
|
|
33
|
+
|
|
34
|
+
不需要账号密钥。默认尝试直接连接订阅源;是否需要代理取决于订阅源和当前网络。
|
|
35
|
+
|
|
36
|
+
详细配置、工具参数与排错见[使用说明](docs/USAGE.md)。从源码独立开发时,Node 要求以 [package.json](package.json) 为准。
|
|
37
|
+
|
|
38
|
+
## 文档
|
|
39
|
+
|
|
40
|
+
- [使用与排错](docs/USAGE.md)
|
|
41
|
+
- [更新记录](CHANGELOG.md)
|
|
42
|
+
- [验证范围与历史记录](docs/VALIDATION.md)
|
|
43
|
+
- [问题反馈与功能建议](https://github.com/STARDUSTLC666/dsh-rss/issues)
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Historical release notes
|
|
2
|
+
|
|
3
|
+
[Current changelog](../CHANGELOG.md) · [Overview](../README.en.md)
|
|
4
|
+
|
|
5
|
+
These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
|
|
6
|
+
|
|
7
|
+
## 0.5.1 (2026-10-02)
|
|
8
|
+
|
|
9
|
+
Fixes public feeds being rejected when a VPN returns Fake-IP DNS addresses. Windows uses the enabled manual system proxy by default. For `198.18.0.0/15` answers, HTTPS DNS obtains real public addresses; connections are pinned to validated addresses while preserving the original hostname and TLS certificate verification. Keep the VPN enabled without changing its configuration or allowing private networks. Request-owned connections are released after redirects, errors or body reads.
|
|
10
|
+
|
|
11
|
+
All 106 Windows tests pass. With the VPN enabled and native DNS returning Fake-IP, normal plugin calls pass check/add/fetch/persisted incremental deduplication/search for SSPAI, Python Insider, NASA and GitHub Atom, with no injected DNS or fetch results. The legacy Python feed URL also remains readable. These results do not establish separate native desktop UI acceptance.
|
|
12
|
+
|
|
13
|
+
## 0.5.0 (2026-10-02)
|
|
14
|
+
|
|
15
|
+
Adds **Settings → RSS subscriptions** with add/edit/remove, search, category filters, feed checks and OPML import/export. The panel and conversation tools share the existing subscription list. No migration is needed. UI labels follow the host language and appearance settings.
|
|
16
|
+
|
|
17
|
+
Import previews new feeds, duplicate changes and rejected items before confirmation. Existing labels/categories are preserved by default; updates require an explicit checkbox. Failed saves and stale previews preserve your input. Removing a feed requires confirmation of its name and URL.
|
|
18
|
+
|
|
19
|
+
Fixes partial imports of malformed XML, acceptance of invalid URLs or embedded credentials, and lost subscriptions during concurrent additions. Check history lasts for the current DSH run; importing does not fetch every feed.
|
|
20
|
+
|
|
21
|
+
All 94 Windows tests pass. Actual browser interaction in official-source Harness `0.2.0-rc.2` covers add/edit/filter/check/import/remove, Tab/Enter/Escape, two-page conflicts and restart persistence. Native desktop and narrow-screen interaction remain unverified in this run. Synthetic feeds do not establish production-network compatibility.
|
|
22
|
+
|
|
23
|
+
## 0.4.3 (2026-10-01)
|
|
24
|
+
|
|
25
|
+
Fixes URL comparison for add, remove, OPML import and search. Schemes, hosts and default ports are normalized while case-sensitive paths and query parameters remain distinct, preventing unrelated subscriptions from being merged or removed. Existing Fake-IP DNS guidance and reserved-address checks remain available.
|
|
26
|
+
|
|
27
|
+
Validation host: Harness `0.2.0-rc.2` built from official sources (commit `639ed01539`) on 2026-10-01. All 81 Windows tests, the 18-plugin co-load and nine RSS tool contracts pass. Production external feeds still require separate network validation.
|
package/docs/USAGE.en.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# dsh-rss usage guide
|
|
2
|
+
|
|
3
|
+
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dsh plugin --profile web add dsh-rss
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Restart the web service after installing. Ask your assistant to subscribe to an RSS URL or list your subscriptions; ordinary subscription management needs no configuration-file editing.
|
|
12
|
+
|
|
13
|
+
For the desktop profile, use `dsh plugin --profile desktop add dsh-rss` and reopen DSH after installation or updates. The shared web settings surface provides the panel in both shells. Update older installed versions through the plugin manager first.
|
|
14
|
+
|
|
15
|
+
## Subscription panel
|
|
16
|
+
|
|
17
|
+
Open **Settings → RSS subscriptions** and choose Recommended feeds, Enter a URL, or Import OPML.
|
|
18
|
+
|
|
19
|
+
The catalog lists 16 publisher-hosted RSS/Atom feeds in Technology news, AI and research, Software updates, Reading and blogs, and Life and interests. Filter by source, keywords and content language. Cards link to the publisher and show the feed URL. Preview and check fetches only the selected source and shows up to five recent articles. Subscribe checks the feed before saving, and existing URLs show Subscribed. Browsing the catalog does not subscribe, fetch every site, or rename or recategorize existing subscriptions. Feeds such as arXiv can be valid but empty on non-publication days.
|
|
20
|
+
|
|
21
|
+
Enter a URL accepts your own RSS/Atom address; the name and category are optional. Pick an existing or suggested category, or type your own. New URLs are fetched and checked before saving. Renaming or recategorizing an unchanged URL works without the remote feed being online.
|
|
22
|
+
|
|
23
|
+
Choose an OPML file (up to 1 MiB) or paste its text, review the preview, then confirm. Rejected URLs include a reason. If another page or tool changed the list, preview again without retyping. Export downloads OPML without changing subscriptions.
|
|
24
|
+
|
|
25
|
+
Cards show the most recent check/fetch time, article count or error during this run. Read-only configurations allow viewing, checking and exporting.
|
|
26
|
+
|
|
27
|
+
Feeds come from publishers or an instance you choose, rather than being produced by the plugin. For sites without a native feed, paste a complete URL generated by your chosen [RSSHub instance](https://github.com/DIYgod/RSSHub). Catalog entries do not need RSSHub. An ordinary webpage URL is not a feed URL. Reachability depends on the publisher and your current network; the plugin does not require a VPN. Retry an unavailable source or choose another one. Failed saves preserve your input.
|
|
28
|
+
|
|
29
|
+
## Uninstall
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dsh plugin --profile web remove dsh-rss
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Override the plugin row in your profile's `cordis.patch.yml` (the plugin also loads with all defaults when absent):
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
- id: rss
|
|
43
|
+
name: 'dsh-rss'
|
|
44
|
+
config:
|
|
45
|
+
# proxyUrl: http://127.0.0.1:7890 # optional override for the Windows system proxy
|
|
46
|
+
# useSystemProxy: true # use the enabled Windows manual proxy by default
|
|
47
|
+
# fakeIpDnsFallback: true # validate real public DNS answers when Fake-IP is detected
|
|
48
|
+
timeoutMs: 15000 # fetch timeout in ms (default 15000)
|
|
49
|
+
# maxBodyBytes: 5242880 # response size cap (default 5MB, guards oversized responses)
|
|
50
|
+
# userAgent: 'dsh-rss/0.2.0' # custom fetch UA
|
|
51
|
+
# feedsYaml: | # optional: pre-seed subscriptions (or use the rss_add tool)
|
|
52
|
+
# - url: https://example.com/feed.xml
|
|
53
|
+
# name: My feed
|
|
54
|
+
# category: tech
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| Tool | Purpose | Key parameters |
|
|
60
|
+
| :-- | :-- | :-- |
|
|
61
|
+
| `rss_list` | List subscribed feeds | none |
|
|
62
|
+
| `rss_add` | Add a subscription (fetches and validates the URL first) | `url` required; `name`/`category` optional |
|
|
63
|
+
| `rss_remove` | Remove a subscription | `url` or `name`, at least one |
|
|
64
|
+
| `rss_fetch` | Fetch and parse a feed, returning feed info and entries (with full `content`) | `url` or `name`, at least one; `limit` 1-100, default 20 |
|
|
65
|
+
| `rss_check` | Validate that a URL is a parseable feed | `url` required |
|
|
66
|
+
| `rss_opml_export` | Export subscriptions as OPML 2.0 text (optionally write a file) | `path` optional |
|
|
67
|
+
| `rss_opml_import` | Bulk-import subscriptions from OPML 2.0 text | `opml` required |
|
|
68
|
+
|
|
69
|
+
| `rss_search` | Search across feeds | `query` required; optional feed, date and limit filters |
|
|
70
|
+
| `rss_health` | Check configuration, feeds and cursors offline | none |
|
|
71
|
+
|
|
72
|
+
### Examples
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
rss_add { url: https://example.com/feed.xml, name: my-feed }
|
|
76
|
+
rss_fetch { name: my-feed, limit: 10 }
|
|
77
|
+
rss_check { url: https://example.com/feed.xml }
|
|
78
|
+
rss_opml_export { path: subscriptions.opml }
|
|
79
|
+
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Subscriptions
|
|
83
|
+
|
|
84
|
+
On Harness 0.1.7, subscriptions and read cursors are stored in the current profile’s `rss` entry. Changes apply immediately and survive restart. Default installations automatically import the retired `dsh-rss` section from `settings.yaml` or `settings.yaml.imported` once, preserving the original file and existing profile values. Older hosts retain their original settings storage. Use a URL to distinguish feeds with identical names.
|
|
85
|
+
|
|
86
|
+
## Proxy
|
|
87
|
+
|
|
88
|
+
Windows uses its enabled manual proxy by default, honoring protocol selection and bypass entries. No PAC scripts are evaluated and OS settings are never modified. An explicit `proxyUrl` takes precedence; `useSystemProxy: false` disables automatic detection. Other platforms can still configure an HTTP(S) `proxyUrl`. These routes apply only to this plugin.
|
|
89
|
+
|
|
90
|
+
When native DNS returns VPN Fake-IP (`198.18.0.0/15`), only the feed hostname is sent to [Cloudflare HTTPS DNS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/), with Google HTTPS DNS as a service-failure fallback. No URL paths, feed content, credentials or subscription lists are sent. Answers must pass public-address checks, and actual connections are pinned to those addresses. Disable this behavior with `fakeIpDnsFallback: false`. Ordinary public DNS, private DNS and IP literals do not trigger it. Trusted internal feeds still require an explicit `allowPrivateNetwork: true`.
|
|
91
|
+
|
|
92
|
+
## Parsing capabilities
|
|
93
|
+
|
|
94
|
+
- RSS 2.0 / RSS 1.0 (RDF) / Atom, normalized to one output shape
|
|
95
|
+
- Entity decoding, CDATA, `content:encoded`, `dc:creator` and other common fields
|
|
96
|
+
- RFC 822 / ISO 8601 dates normalized to ISO 8601 UTC (`pubDate`); the raw text stays in `pubDateRaw`
|
|
97
|
+
- Relative links resolved against the feed URL
|
|
98
|
+
- Summaries stripped of HTML tags and truncated at 500 chars; RSS `content:encoded` / Atom `content` is preserved as a `content` field (tags stripped, up to 20000 chars)
|
|
99
|
+
- Safety: no DTD / external entity parsing; 5MB body cap; configurable fetch timeout
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm install
|
|
105
|
+
pnpm test # build + offline tests with explicit fetch and DNS fixtures
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# dsh-rss 使用说明
|
|
2
|
+
|
|
3
|
+
[返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dsh plugin --profile web add dsh-rss
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
安装后重启 Web 服务即可。直接对助手说「订阅这个 RSS 地址,命名为技术资讯」或「列出我的订阅」即可使用,日常增删订阅不需要编辑配置文件。
|
|
12
|
+
|
|
13
|
+
桌面版使用 `dsh plugin --profile desktop add dsh-rss`,安装或更新后重新打开 DSH。网页和桌面的共享设置页面均可显示订阅面板;已安装的旧版本请先通过插件管理器更新。
|
|
14
|
+
|
|
15
|
+
## 订阅面板
|
|
16
|
+
|
|
17
|
+
打开 **设置 → RSS 订阅**,提供「推荐订阅源」「输入地址」「导入 OPML」三个入口。
|
|
18
|
+
|
|
19
|
+
推荐目录收录 16 个网站原生 RSS/Atom,按科技资讯、AI 与科研、软件更新、阅读与博客、生活与兴趣分类。可按名称、关键词和内容语言筛选;每个来源可查看原网站与订阅地址。「预览并检查」只抓取所选来源,显示最多 5 篇近期文章;「订阅」通过抓取验证后才保存,已添加的地址显示「已订阅」。目录不会自动订阅、批量抓取或改动原有名称和分类。arXiv 等按发布日更新的源可能有效但没有当前条目。
|
|
20
|
+
|
|
21
|
+
选择「输入地址」填写自己的 RSS/Atom 地址,名称可留空。分类可选择目录提供的分类、现有分类,也可直接输入自定义分类。新地址通过抓取检查后才会保存;编辑原地址的名称或分类不需要源网站在线。
|
|
22
|
+
|
|
23
|
+
导入可选择不超过 1 MiB 的 OPML 文件或粘贴文本。先检查预览,再点击确认;跳过的地址会说明原因。已有订阅发生变化时重新预览即可,原输入仍保留。导出通过浏览器下载 OPML,不会修改订阅。
|
|
24
|
+
|
|
25
|
+
列表卡片显示最近一次手动检查或工具抓取的时间和文章数,失败时显示具体原因。只读配置仍可查看、检查和导出。
|
|
26
|
+
|
|
27
|
+
RSS 来自网站或用户选择的实例,不由插件自己生产。网站没有原生源时,可粘贴你选择的 [RSSHub 实例](https://github.com/DIYgod/RSSHub) 生成的完整地址;目录中的来源不依赖 RSSHub。不要把普通网页地址当作 RSS 地址。可用性取决于来源网站与用户当前网络,插件不要求使用 VPN;当前网络无法访问某个网站时,可重试或改选其他源。保存失败时不会清空输入。
|
|
28
|
+
|
|
29
|
+
## 卸载
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dsh plugin --profile web remove dsh-rss
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
36
|
+
|
|
37
|
+
## 配置
|
|
38
|
+
|
|
39
|
+
在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时插件也能加载,只是全部用默认值):
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
- id: rss
|
|
43
|
+
name: 'dsh-rss'
|
|
44
|
+
config:
|
|
45
|
+
# proxyUrl: http://127.0.0.1:7890 # 可选;覆盖自动读取的 Windows 系统代理
|
|
46
|
+
# useSystemProxy: true # 默认读取 Windows 已启用的手动系统代理
|
|
47
|
+
# fakeIpDnsFallback: true # 默认在 Fake-IP 下查询并校验真实公网地址
|
|
48
|
+
timeoutMs: 15000 # 抓取超时(毫秒,默认 15000)
|
|
49
|
+
# maxBodyBytes: 5242880 # 订阅源体积上限(默认 5MB,防超大响应)
|
|
50
|
+
# userAgent: 'dsh-rss/0.2.0' # 自定义抓取 UA
|
|
51
|
+
# feedsYaml: | # 可选:预置订阅列表(也可用 rss_add 工具添加)
|
|
52
|
+
# - url: https://example.com/feed.xml
|
|
53
|
+
# name: 示例订阅
|
|
54
|
+
# category: 技术
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 工具一览
|
|
58
|
+
|
|
59
|
+
| 工具 | 作用 | 关键参数 |
|
|
60
|
+
| :-- | :-- | :-- |
|
|
61
|
+
| `rss_list` | 列出已订阅源 | 无 |
|
|
62
|
+
| `rss_add` | 添加订阅(先抓取校验地址) | `url` 必填;`name`/`category` 可选 |
|
|
63
|
+
| `rss_remove` | 删除订阅 | `url` 或 `name` 至少一个 |
|
|
64
|
+
| `rss_fetch` | 抓取解析订阅源,返回源信息与条目(含正文 content) | `url` 或 `name` 至少一个;`limit` 1-100 默认 20 |
|
|
65
|
+
| `rss_check` | 校验地址是否为可解析的订阅源 | `url` 必填 |
|
|
66
|
+
| `rss_opml_export` | 导出订阅列表为 OPML 2.0 文本(可写入文件) | `path` 可选 |
|
|
67
|
+
| `rss_opml_import` | 从 OPML 2.0 文本批量导入订阅源 | `opml` 必填 |
|
|
68
|
+
| `rss_search` | 跨订阅源关键词搜索条目(标题/摘要/正文/作者) | `query` 必填;`name`/`url`/`since`/`limit` 可选 |
|
|
69
|
+
| `rss_health` | 插件自检(配置/订阅/游标/解析器,离线) | 无 |
|
|
70
|
+
|
|
71
|
+
### 示例
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
rss_add { url: https://example.com/feed.xml, name: 我的订阅 }
|
|
75
|
+
rss_fetch { name: 我的订阅, limit: 10 }
|
|
76
|
+
rss_check { url: https://example.com/feed.xml }
|
|
77
|
+
rss_opml_export { path: subscriptions.opml }
|
|
78
|
+
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 订阅存储
|
|
82
|
+
|
|
83
|
+
Harness 0.1.7 把订阅列表和读取游标保存在当前 profile 的 `rss` 配置行;增删后立即生效,重启仍保留。默认安装会自动导入旧 `settings.yaml` 或 `settings.yaml.imported` 的 `dsh-rss` 数据一次,保留原文件,且不覆盖已有 profile 值。旧版宿主继续使用原 settings 存储。同名订阅请用 URL 区分。
|
|
84
|
+
|
|
85
|
+
## 代理与 VPN
|
|
86
|
+
|
|
87
|
+
Windows 上默认读取已启用的手动系统代理,尊重协议与绕过列表;不执行 PAC 脚本,也不修改系统设置。`proxyUrl` 显式配置优先;`useSystemProxy: false` 可关闭自动读取。其他平台仍可通过 `proxyUrl` 配置 HTTP(S) 代理。这些代理仅作用于本插件。
|
|
88
|
+
|
|
89
|
+
当系统 DNS 返回 VPN 的 Fake-IP(`198.18.0.0/15`)时,默认只向 [Cloudflare HTTPS DNS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/) 查询订阅源域名;服务不可用时尝试 Google HTTPS DNS。查询不会发送订阅源路径、正文、密钥或订阅列表,回答仍须通过公网地址检查;实际连接固定到检查过的地址。`fakeIpDnsFallback: false` 可关闭该兼容方式。普通公网 DNS、私网 DNS 和 IP 字面量不会触发此查询。可信内网订阅仍需单独显式配置 `allowPrivateNetwork: true`。
|
|
90
|
+
|
|
91
|
+
## 解析能力
|
|
92
|
+
|
|
93
|
+
- RSS 2.0 / RSS 1.0(RDF)/ Atom 三种格式,统一归一化输出
|
|
94
|
+
- 实体解码、CDATA、`content:encoded`、`dc:creator`、`itunes` 等常见字段
|
|
95
|
+
- RFC 822 / ISO 8601 日期统一转 ISO 8601 UTC(`pubDate`),原始文本保留在 `pubDateRaw`
|
|
96
|
+
- 相对链接按订阅源地址解析成绝对链接
|
|
97
|
+
- 摘要去 HTML 标签并截断到 500 字符;RSS `content:encoded` / Atom `content` 作为 `content` 字段保留(去标签后最多 20000 字符)
|
|
98
|
+
- 安全:不解析 DTD/外部实体,内容只做文本抽取;响应体积上限 5MB;抓取超时可配
|
|
99
|
+
|
|
100
|
+
## 开发
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm install
|
|
104
|
+
pnpm test # 构建 + 离线测试(fetch 与 DNS 均使用夹具)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
发布前门禁:危险模式扫描、manifest 自检、`pnpm audit --prod`、全量测试,以及全新 profile 的真实启动冒烟测试。
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# dsh-rss 验证记录
|
|
2
|
+
|
|
3
|
+
[0.6.0 推荐订阅源验收](validation-0.6.0.md)。
|
|
4
|
+
|
|
5
|
+
[0.5.2 中英文界面验收](validation-language-2026-10-03.md)。
|
|
6
|
+
|
|
7
|
+
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
8
|
+
|
|
9
|
+
## 原中文记录
|
|
10
|
+
|
|
11
|
+
0.4.0 起适配官方源码构建的 Harness **0.1.7 及以上的声明式设置接口**;当前基线为 **0.2.0-rc.2**,18 个插件同载、工具契约、订阅读写与重启持久化在隔离环境通过验证;真实外部订阅源需要正常网络连接。Node 要求为 22.19 及以上的 22.x,或 24 及以上。
|
|
12
|
+
|
|
13
|
+
抓取接收 Harness 的取消信号,取消 DNS、网络读取或跨源搜索时保留原始取消原因。默认预检域名解析结果并拒绝回环、私网和链路本地地址;每次重定向都重新校验。可信内网源可显式配置 `allowPrivateNetwork: true`。
|
|
14
|
+
|
|
15
|
+
`rss_opml_export.path` 必须相对本次调用的会话工作区,父目录须已存在;工具拒绝绝对路径、目录穿越和指向工作区外的链接,并返回实际写入文件的绝对路径。文件写入默认通过 Harness 审批门,`opmlWriteApproval: false` 可关闭该门;省略 `path` 时仅返回 OPML 文本。
|
|
16
|
+
|
|
17
|
+
## Original English record
|
|
18
|
+
|
|
19
|
+
Version 0.4.0 supports the declarative settings interface introduced in Harness **0.1.7**. The current baseline is official-source **0.2.0-rc.2**. All 18 plugins load together; tool contracts, subscription updates and restart persistence pass in isolation. Live feeds require working network access. Requires Node 22.19 or later within 22.x, or 24 or later.
|
|
20
|
+
|
|
21
|
+
Fetches honor the Harness cancellation signal through DNS, response streaming, and cross-feed searches. Hostname preflight checks reject loopback, private, and link-local addresses by default, including redirect destinations. Set `allowPrivateNetwork: true` for trusted internal feeds.
|
|
22
|
+
|
|
23
|
+
`rss_opml_export.path` is relative to the calling session's workspace, with an existing parent directory. Absolute paths, traversal, and links outside the workspace are rejected; the result returns the absolute written path. File writes request Harness approval by default (`opmlWriteApproval: false` disables this gate). Omit `path` to return only OPML text.
|
|
24
|
+
|
|
25
|
+
## 0.4.3 原验证说明
|
|
26
|
+
|
|
27
|
+
验证宿主:官方源码构建的 Harness `0.2.0-rc.2`(commit `639ed01539`,2026-10-01)。81 项 Windows 插件测试、18 个插件共同加载与 9 个 RSS 工具的契约检查通过。真实外部订阅源仍需正常网络连接。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 0.6.0 推荐订阅源验收 · 2026-10-04
|
|
2
|
+
|
|
3
|
+
Windows / Node 24.16.0:110 项测试通过。新增回归验证浏览目录不联网或改写订阅、按规范化 URL 标记已订阅、推荐预览不写入订阅、请求地址锁定到选定的推荐源、最多返回五篇且仅允许无凭据的 HTTP(S) 文章链接,以及离线预览/订阅失败不写入数据。原有 OPML、取消、并发保存和网络保护回归均通过。
|
|
4
|
+
|
|
5
|
+
使用插件真实抓取与解析函数、原生 DNS、默认公网保护,读取下列 16 个发布方提供的 RSS/Atom 地址成功:IT之家、少数派、爱范儿、Hacker News、arXiv AI、Hugging Face、Google DeepMind、小众软件、DeepSeek Harness releases、Python Insider、Rust、Node.js、月光博客、Simon Willison、NASA、xkcd。arXiv 本次有效但条目数为零。另一个候选阮一峰返回 HTTP 403,未放入推荐目录。
|
|
6
|
+
|
|
7
|
+
网络验收保留用户当前 VPN/系统代理,未注入测试 DNS、网络响应或解析结果。成功表示本次环境可读取,不保证其他网络均可访问;插件无需强制使用 VPN。推荐源直接来自发布方,不依赖 RSSHub 实例。
|
|
8
|
+
|
|
9
|
+
官方源码构建的 DSH 0.2.0-rc.2(639ed01539)独立 profile 中实际操作了推荐入口、分类、IT之家真实文章预览、订阅与已订阅状态、无效网页地址失败后保留所有输入、自定义分类、英文切换及 arXiv 空源提示。390px 布局测量显示目录单列,文档宽度 390px,订阅对话框没有水平溢出。文章和用户分类仍为原文。
|
|
10
|
+
|
|
11
|
+
本轮未进行原生桌面窗口操作;网页和自动回归不表示每个原生桌面交互已验收。测试 profile 不使用或修改个人订阅与密钥。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# RSS 0.5.1 VPN / Fake-IP acceptance
|
|
2
|
+
|
|
3
|
+
Date: 2026-10-02. Host: official-source Harness `0.2.0-rc.2`, commit `639ed015397290b3745d163aafe02ffee4aa3f84`. Windows, Node 24.16.0, Undici 8.11.2.
|
|
4
|
+
|
|
5
|
+
## Reproduction and fix
|
|
6
|
+
|
|
7
|
+
Native Windows DNS returned `198.18.*` for public feed domains while FlClash remained enabled in Fake-IP mode. Version 0.5.0 rejected those answers before fetching. Version 0.5.1 uses the already-enabled manual Windows proxy and HTTPS DNS for Fake-IP answers. Feed connections are pinned to validated public addresses, with the original HTTP Host and TLS servername. Each redirect is separately validated. TLS verification and private-network protection remain enabled. Request-owned dispatchers are destroyed after completion or cancellation.
|
|
8
|
+
|
|
9
|
+
The VPN was not stopped or reconfigured. Its three process IDs and generated configuration SHA256 remained unchanged during acceptance. No user profile, API key or existing subscription was used or modified.
|
|
10
|
+
|
|
11
|
+
## Verification
|
|
12
|
+
|
|
13
|
+
- `pnpm test`: 106/106 on Windows. Includes real local CONNECT-proxy transport, preserved Host, no second socket DNS lookup, HTTPS DNS cancellation/body cap/redirect handling, private answers, opt-outs and Windows proxy parsing/bypass behavior.
|
|
14
|
+
- `pnpm audit --prod --registry https://registry.npmjs.org`: no known vulnerabilities.
|
|
15
|
+
- `node --check lib/client.js`: passed.
|
|
16
|
+
- Exact npm tarball installed into a fresh consumer: 34/34 relevant regressions passed. The tarball contains the compiled networking modules and client entry.
|
|
17
|
+
- Normal `buildRssTools` calls from that installation used production fetch/DNS without injection, the default 15-second timeout, no explicit `proxyUrl`, and `allowPrivateNetwork: false`.
|
|
18
|
+
|
|
19
|
+
| Real source | Format / entries | Check | Add / fetch / search | Incremental after tools rebuilt from disk |
|
|
20
|
+
| --- | --- | --- | --- | --- |
|
|
21
|
+
| `https://sspai.com/feed` | RSS / 10 | Pass | Pass | 10 first, 0 repeated |
|
|
22
|
+
| `https://blog.python.org/rss.xml` | RSS / 50 | Pass | Pass | 50 first, 0 repeated |
|
|
23
|
+
| `https://www.nasa.gov/feed/` | RSS / 10 | Pass | Pass | 10 first, 0 repeated |
|
|
24
|
+
| `https://github.com/deepseek-ai/deepseek-harness/releases.atom` | Atom / 10 | Pass | Pass | 10 first, 0 repeated |
|
|
25
|
+
|
|
26
|
+
The old Python URL `https://blog.python.org/feeds/posts/default?alt=rss` also passed. It returned a valid RSS feed without an HTTP redirect in the earlier URL diagnostic.
|
|
27
|
+
|
|
28
|
+
## Human browser operations
|
|
29
|
+
|
|
30
|
+
An isolated, keyless profile loaded the fresh tarball installation in the official host. Through Settings → RSS subscriptions, all four public feeds were entered and saved using the visible form. Every card displayed its real entry count and a passing check. Clicking the GitHub Atom check button completed successfully. Adding `http://127.0.0.1/private-feed` was rejected, retained the typed form values and left the four subscriptions intact. No console errors were recorded. Screenshots and DOM snapshots were retained in the local validation directory. The temporary browser tab and test host closed after acceptance.
|
|
31
|
+
|
|
32
|
+
This covers the shared browser settings surface and production networking. It does not constitute separate native desktop-window acceptance or live testing of other plugins.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 0.5.2:中英文界面验收
|
|
2
|
+
|
|
3
|
+
2026-10-03,Windows / Node 24.16.0。采用 E 盘独立 DSH_HOME、合成资料与无密钥 profile;用户资料、账号、密钥、桌面窗口与 VPN 保持原状。
|
|
4
|
+
|
|
5
|
+
- 自动测试:107 项全部通过。
|
|
6
|
+
- 可见 Web 操作:英文无效 OPML 显示可恢复说明;混合合法 / 非 HTTP 地址预览正确显示跳过原因,并只导入一条合成中文命名订阅。中文界面读取同一订阅,原名称保留;无效 OPML 中文提示保留。
|
|
7
|
+
- 宿主:官方 DSH 0.2.1-alpha.1 Web;官方 RC2 与 alpha SDK 同载 18 组件,110 工具、35 插件技能、19 输出样例注册与 schema 检查通过。无账号的健康结果不代表真实连接。
|
|
8
|
+
- 边界:导入本身不抓取文章。本轮语言补丁没有重复执行外部订阅网络矩阵,网络验收见之前版本记录。
|
|
9
|
+
|
|
10
|
+
中英文共用同一份资料;界面翻译不会改写标题、正文、路径、引用或项目文件。系统文件选择控件和文件对话框跟随 Windows / 浏览器语言。未知技术诊断与原始引擎日志保留原文;英文界面提供操作恢复引导。
|
|
11
|
+
|
|
12
|
+
此记录不代表原生桌面窗口内的新功能已逐项操作通过。发布包完整性、GitHub CI 与公开 npm 全新安装在发布时另行核对。
|
|
13
|
+
|
|
14
|
+
## English scope
|
|
15
|
+
|
|
16
|
+
The Chinese and English UI share the same data. UI translation preserves original content, paths and project files. Visible interactions were performed in an isolated official alpha Web profile; both RC2 and alpha SDK registration/schema checks passed. Native file controls use the OS/browser language, and original diagnostic logs are retained. Browser results do not certify native desktop interaction or real-service delivery.
|
package/lib/catalog.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Publisher-hosted feeds; browsing this list never contacts their websites. */
|
|
2
|
+
export declare const sourceCategories: readonly [{
|
|
3
|
+
readonly id: "technology";
|
|
4
|
+
readonly name: readonly ["科技资讯", "Technology news"];
|
|
5
|
+
}, {
|
|
6
|
+
readonly id: "research";
|
|
7
|
+
readonly name: readonly ["AI 与科研", "AI and research"];
|
|
8
|
+
}, {
|
|
9
|
+
readonly id: "software";
|
|
10
|
+
readonly name: readonly ["软件更新", "Software updates"];
|
|
11
|
+
}, {
|
|
12
|
+
readonly id: "reading";
|
|
13
|
+
readonly name: readonly ["阅读与博客", "Reading and blogs"];
|
|
14
|
+
}, {
|
|
15
|
+
readonly id: "interests";
|
|
16
|
+
readonly name: readonly ["生活与兴趣", "Life and interests"];
|
|
17
|
+
}];
|
|
18
|
+
export interface RecommendedSource {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
url: string;
|
|
22
|
+
website: string;
|
|
23
|
+
category: typeof sourceCategories[number]['id'];
|
|
24
|
+
language: 'zh' | 'en';
|
|
25
|
+
description: readonly [string, string];
|
|
26
|
+
}
|
|
27
|
+
export declare const recommendedSources: readonly RecommendedSource[];
|
|
28
|
+
/** Feed-provided links stay text-only or become ordinary HTTP(S) links. */
|
|
29
|
+
export declare function articleLink(value: string, base: string): string;
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Publisher-hosted feeds; browsing this list never contacts their websites. */
|
|
2
|
+
export const sourceCategories = [
|
|
3
|
+
{ id: 'technology', name: ['科技资讯', 'Technology news'] },
|
|
4
|
+
{ id: 'research', name: ['AI 与科研', 'AI and research'] },
|
|
5
|
+
{ id: 'software', name: ['软件更新', 'Software updates'] },
|
|
6
|
+
{ id: 'reading', name: ['阅读与博客', 'Reading and blogs'] },
|
|
7
|
+
{ id: 'interests', name: ['生活与兴趣', 'Life and interests'] },
|
|
8
|
+
];
|
|
9
|
+
// Descriptions are original catalog copy. Article content belongs to publishers.
|
|
10
|
+
// These are native RSS/Atom endpoints, without an RSSHub instance dependency.
|
|
11
|
+
export const recommendedSources = [
|
|
12
|
+
{ id: 'ithome', name: 'IT之家', url: 'https://www.ithome.com/rss/', website: 'https://www.ithome.com/', category: 'technology', language: 'zh', description: ['数码产品、科技行业与软件资讯。', 'Consumer devices, technology and software news.'] },
|
|
13
|
+
{ id: 'sspai', name: '少数派', url: 'https://sspai.com/feed', website: 'https://sspai.com/', category: 'technology', language: 'zh', description: ['数字生活、效率工具与使用经验。', 'Digital life, productivity tools and practical guides.'] },
|
|
14
|
+
{ id: 'ifanr', name: '爱范儿', url: 'https://www.ifanr.com/feed', website: 'https://www.ifanr.com/', category: 'technology', language: 'zh', description: ['科技产品、设计与数字生活观察。', 'Technology products, design and digital life.'] },
|
|
15
|
+
{ id: 'hacker-news', name: 'Hacker News', url: 'https://news.ycombinator.com/rss', website: 'https://news.ycombinator.com/', category: 'technology', language: 'en', description: ['技术社区的热门文章与讨论链接。', 'Popular articles and discussions from the technology community.'] },
|
|
16
|
+
{ id: 'arxiv-ai', name: 'arXiv · Artificial Intelligence', url: 'https://rss.arxiv.org/rss/cs.AI', website: 'https://arxiv.org/list/cs.AI/recent', category: 'research', language: 'en', description: ['人工智能论文更新;非发布日可能没有条目。', 'AI paper announcements; non-publication days can have no entries.'] },
|
|
17
|
+
{ id: 'hugging-face', name: 'Hugging Face Blog', url: 'https://huggingface.co/blog/feed.xml', website: 'https://huggingface.co/blog', category: 'research', language: 'en', description: ['开放模型、数据集与机器学习实践。', 'Open models, datasets and machine-learning practice.'] },
|
|
18
|
+
{ id: 'deepmind', name: 'Google DeepMind', url: 'https://deepmind.google/blog/rss.xml', website: 'https://deepmind.google/blog/', category: 'research', language: 'en', description: ['研究团队发布的 AI 研究和产品进展。', 'AI research and product updates from the research team.'] },
|
|
19
|
+
{ id: 'appinn', name: '小众软件', url: 'https://www.appinn.com/feed/', website: 'https://www.appinn.com/', category: 'software', language: 'zh', description: ['实用软件、工具推荐和更新介绍。', 'Useful applications, tools and software updates.'] },
|
|
20
|
+
{ id: 'harness', name: 'DeepSeek Harness releases', url: 'https://github.com/deepseek-ai/DeepSeek-Harness/releases.atom', website: 'https://github.com/deepseek-ai/DeepSeek-Harness/releases', category: 'software', language: 'en', description: ['DSH 官方 GitHub 版本发布记录。', 'Official DSH releases published on GitHub.'] },
|
|
21
|
+
{ id: 'python', name: 'Python Insider', url: 'https://blog.python.org/feeds/posts/default', website: 'https://blog.python.org/', category: 'software', language: 'en', description: ['Python 官方版本发布与项目消息。', 'Official Python releases and project news.'] },
|
|
22
|
+
{ id: 'rust', name: 'Rust Blog', url: 'https://blog.rust-lang.org/feed.xml', website: 'https://blog.rust-lang.org/', category: 'software', language: 'en', description: ['Rust 官方版本与语言生态进展。', 'Official Rust releases and ecosystem updates.'] },
|
|
23
|
+
{ id: 'nodejs', name: 'Node.js Blog', url: 'https://nodejs.org/en/feed/blog.xml', website: 'https://nodejs.org/en/blog', category: 'software', language: 'en', description: ['Node.js 官方版本、安全更新与项目消息。', 'Official Node.js releases, security updates and project news.'] },
|
|
24
|
+
{ id: 'moonlight', name: '月光博客', url: 'https://www.williamlong.info/rss.xml', website: 'https://www.williamlong.info/', category: 'reading', language: 'zh', description: ['互联网、技术应用与个人博客文章。', 'Personal writing about the internet and technology.'] },
|
|
25
|
+
{ id: 'simon-willison', name: 'Simon Willison', url: 'https://simonwillison.net/atom/everything/', website: 'https://simonwillison.net/', category: 'reading', language: 'en', description: ['开发实践、数据工具与 AI 使用笔记。', 'Development practice, data tools and AI notes.'] },
|
|
26
|
+
{ id: 'nasa', name: 'NASA', url: 'https://www.nasa.gov/feed/', website: 'https://www.nasa.gov/', category: 'interests', language: 'en', description: ['航天任务、科学发现与宇宙探索。', 'Space missions, science and exploration.'] },
|
|
27
|
+
{ id: 'xkcd', name: 'xkcd', url: 'https://xkcd.com/rss.xml', website: 'https://xkcd.com/', category: 'interests', language: 'en', description: ['关于科学、技术和日常生活的漫画更新。', 'Comics about science, technology and everyday life.'] },
|
|
28
|
+
];
|
|
29
|
+
/** Feed-provided links stay text-only or become ordinary HTTP(S) links. */
|
|
30
|
+
export function articleLink(value, base) {
|
|
31
|
+
if (!value || value.length > 4096)
|
|
32
|
+
return '';
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value, base);
|
|
35
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password ? url.href : '';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -8,11 +8,19 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
8
8
|
const listeners = new Set();
|
|
9
9
|
const words = {
|
|
10
10
|
title: ['RSS 订阅', 'RSS subscriptions'], intro: ['在这里管理订阅,也可以在对话里让 AI 读取和搜索文章。', 'Manage feeds here, then ask AI to read or search articles in a conversation.'],
|
|
11
|
+
recommended: ['推荐订阅源', 'Recommended feeds'], manual: ['输入地址', 'Enter a URL'],
|
|
12
|
+
catalogHint: ['先选感兴趣的分类,再预览或订阅。不会默认添加,也不会自动访问所有网站。', 'Choose a topic, then preview or subscribe. Feeds are not added by default and websites are not contacted automatically.'],
|
|
13
|
+
networkHint: ['可用性取决于你当前的网络,国内外网站可能不同。预览失败可以重试,或改选其他来源。', 'Availability depends on your current network and can differ by website. Retry a failed preview or choose another source.'],
|
|
14
|
+
sourceSearch: ['搜索来源或关键词', 'Search sources or keywords'], sourceLanguage: ['内容语言', 'Content language'], anyLanguage: ['全部语言', 'All languages'], chinese: ['中文', 'Chinese'], english: ['英文', 'English'],
|
|
15
|
+
native: ['网站原生 RSS / Atom', 'Publisher RSS / Atom'], website: ['来源网站', 'Source website'], previewArticles: ['预览并检查', 'Preview and check'], subscribe: ['订阅', 'Subscribe'], subscribed: ['已订阅', 'Subscribed'],
|
|
16
|
+
recentArticles: ['近期文章(最多 5 篇)', 'Recent articles (up to 5)'], noRecentArticles: ['订阅格式有效,但此时没有文章。有些来源只在发布日提供条目。', 'The feed is valid but currently empty. Some sources only include entries on publication days.'], noTitle: ['无标题', 'Untitled'],
|
|
17
|
+
customCategory: ['可选现有分类,也可填写自定义分类。已有分类原样保留。', 'Choose an existing category or type your own. Existing categories are preserved.'],
|
|
18
|
+
rsshubHint: ['其他网站:可在“输入地址”添加原生订阅,或粘贴你选择的 RSSHub 实例生成的地址。', 'For other sites, use “Enter a URL” with a native feed or a URL generated by your chosen RSSHub instance.'],
|
|
11
19
|
add: ['添加订阅', 'Add feed'], import: ['导入 OPML', 'Import OPML'], export: ['导出 OPML', 'Export OPML'], refresh: ['刷新列表', 'Refresh list'],
|
|
12
20
|
search: ['搜索名称或地址', 'Search name or URL'], category: ['分类', 'Category'], all: ['全部分类', 'All categories'], uncategorized: ['未分类', 'Uncategorized'],
|
|
13
21
|
name: ['显示名称', 'Display name'], nameHint: ['留空时使用订阅源的标题', 'Leave blank to use the feed title'], url: ['订阅源地址', 'Feed URL'], categoryHint: ['例如:科技、阅读', 'For example: Tech, Reading'],
|
|
14
22
|
save: ['保存订阅', 'Save feed'], saving: ['正在处理…', 'Working…'], cancel: ['取消', 'Cancel'], close: ['关闭', 'Close'], edit: ['编辑', 'Edit'], remove: ['移除', 'Remove'], check: ['检查可用性', 'Check feed'],
|
|
15
|
-
empty: ['还没有订阅', 'No subscriptions yet'], emptyHint: ['
|
|
23
|
+
empty: ['还没有订阅', 'No subscriptions yet'], emptyHint: ['从推荐目录选几个感兴趣的来源,也可以输入 RSS/Atom 地址或导入 OPML。', 'Choose sources from the recommended catalog, enter an RSS/Atom URL, or import OPML.'], noMatch: ['没有匹配项,试试清空筛选。', 'No matches. Try clearing the filters.'], clear: ['清空筛选', 'Clear filters'],
|
|
16
24
|
readonly: ['当前配置为只读。你可以查看、检查和导出订阅。', 'These settings are read-only. You can view, check and export feeds.'],
|
|
17
25
|
pending: ['本次运行尚未检查', 'Not checked during this run'], entries: ['篇文章', 'articles'], checked: ['检查通过', 'Check passed'], failed: ['检查失败', 'Check failed'],
|
|
18
26
|
statusHint: ['检查记录仅保留在本次 DSH 运行中;导入不会自动抓取所有订阅。', 'Check history lasts for this DSH run. Import does not fetch every feed.'],
|
|
@@ -27,6 +35,20 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
27
35
|
bodyLarge: ['文件超过 1 MiB,请拆分后导入。', 'File exceeds 1 MiB. Split it before importing.'], session: ['DSH 会话已失效,请重新打开 DSH 页面。', 'The DSH session has expired. Reopen the DSH page.'], fileError: ['无法读取文件,请重新选择或粘贴 OPML 文本。', 'Cannot read this file. Choose it again or paste OPML text.'],
|
|
28
36
|
};
|
|
29
37
|
const t = key => (words[key] || [key, key])[language === 'en' ? 1 : 0];
|
|
38
|
+
const localized = value => value?.[language === 'en' ? 1 : 0] || '';
|
|
39
|
+
|
|
40
|
+
function feedError(message, code) {
|
|
41
|
+
if (language !== 'en' || !/[\u3400-\u9fff]/.test(message || '')) return message;
|
|
42
|
+
if (['conflict', 'duplicate', 'readonly', 'bodyLarge', 'session'].includes(code)) return t(code);
|
|
43
|
+
if (/HTTP/.test(message)) return 'Feed request failed. Check the URL, connection and proxy settings, then retry.';
|
|
44
|
+
if (/YAML|feedsYaml/.test(message)) return 'The feed configuration is invalid. Fix the feedsYaml list in settings before retrying.';
|
|
45
|
+
if (/OPML|outline|xmlUrl/.test(message)) return 'The OPML content is invalid. Export it again with xmlUrl attributes on feed outlines.';
|
|
46
|
+
if (/地址|URL|url|http|https|协议/.test(message)) return 'Enter a valid HTTP or HTTPS feed URL. Local and private-network addresses are not allowed.';
|
|
47
|
+
if (/写入|只读/.test(message)) return 'Could not save the feed configuration. Check write access and reload the list before retrying; your input is preserved.';
|
|
48
|
+
if (/未找到|找不到|没有找到/.test(message)) return 'No matching feed was found. Refresh the subscription list.';
|
|
49
|
+
return 'The feed could not be read. Check the URL, network and proxy settings, then retry.';
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
const CSS = `
|
|
31
53
|
.dshr{color:var(--dsw-alias-label-primary,#202737);font-size:14px;line-height:1.55;max-width:920px;min-width:0}
|
|
32
54
|
.dshr *,.dshr-dialog *{box-sizing:border-box}.dshr h2{font-size:22px;margin:0 0 6px}.dshr p{margin:0 0 12px}
|
|
@@ -45,6 +67,10 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
45
67
|
.dshr-dialog{font:inherit;line-height:1.55;color:var(--dsw-alias-label-primary,#202737);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:14px;padding:22px;max-width:620px;width:calc(100% - 28px);max-height:calc(100dvh - 28px);overflow:auto;box-shadow:0 18px 70px #0003}
|
|
46
68
|
.dshr-dialog::backdrop{background:#14203366}.dshr-dialog h3{font-size:19px;margin:0 0 12px}.dshr-dialog form{display:grid;gap:14px}.dshr-dialog textarea{min-height:140px;resize:vertical}.dshr-dialog p{margin:0}.dshr-dialog .dshr-buttons{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:14px}
|
|
47
69
|
.dshr-dialog label.dshr-checkbox{display:flex;flex-direction:row;align-items:flex-start;gap:8px}.dshr-checkbox input{width:18px;height:18px;min-height:18px;margin-top:3px;flex-shrink:0}.dshr-preview{border-top:1px solid var(--dsw-alias-border-l2,#d9dee7);padding-top:12px;overflow-wrap:anywhere}.dshr-preview ul{margin:8px 0;padding-left:20px}.dshr-preview li{margin-bottom:5px}
|
|
70
|
+
.dshr-dialog.catalog{max-width:900px}.dshr-dialog select{font:inherit;min-height:40px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;width:100%}
|
|
71
|
+
.dshr-dialog select:focus-visible,.dshr-dialog a:focus-visible{outline:2px solid #5685d4;outline-offset:2px}.dshr-dialog a{color:var(--dsw-alias-accent-primary,#245cb5);overflow-wrap:anywhere}.dshr-dialog .dshr-filters{margin-top:14px}.dshr-dialog .dshr-filters label{flex:1 1 140px}
|
|
72
|
+
.dshr-catalog{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;list-style:none;padding:0;margin:14px 0}.dshr-source{border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:12px;padding:14px;min-width:0;align-self:start}.dshr-source h4{font-size:16px;margin:0 0 6px;overflow-wrap:anywhere}.dshr-source p{margin:8px 0}.dshr-source .dshr-actions{margin-top:12px}.dshr-source.is-selected{border-color:#5685d4}.dshr-source details{margin-top:8px;font-size:12px}.dshr-source summary{cursor:pointer}.dshr-source .dshr-preview{margin-top:12px;font-size:13px}
|
|
73
|
+
@media(max-width:640px){.dshr-catalog{grid-template-columns:1fr}.dshr-dialog .dshr-filters label{flex-basis:100%}}
|
|
48
74
|
@media(max-width:520px){.dshr-dialog{padding:16px}.dshr-actions{width:100%}.dshr-filters label{width:100%}.dshr-toolbar button{flex:1 1 auto}.dshr-card{padding:12px}}
|
|
49
75
|
`;
|
|
50
76
|
async function request(action, args, signal) {
|
|
@@ -54,23 +80,23 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
54
80
|
let value;
|
|
55
81
|
try { value = await response.json(); } catch { throw new Error(response.status === 401 ? t('session') : t('loadError')); }
|
|
56
82
|
if (!response.ok) {
|
|
57
|
-
const error = new Error(['conflict', 'duplicate', 'readonly'].includes(value.code) ? t(value.code) : value.error || t('loadError'));
|
|
83
|
+
const error = new Error(['conflict', 'duplicate', 'readonly'].includes(value.code) ? t(value.code) : feedError(value.error, value.code) || t('loadError'));
|
|
58
84
|
error.snapshot = value.snapshot;
|
|
59
85
|
throw error;
|
|
60
86
|
}
|
|
61
87
|
return value;
|
|
62
88
|
}
|
|
63
|
-
function Dialog({ title, busy, onClose, children }) {
|
|
89
|
+
function Dialog({ title, busy, onClose, children, wide }) {
|
|
64
90
|
const ref = useRef(null);
|
|
65
91
|
const id = useId();
|
|
66
92
|
useEffect(() => { ref.current.showModal(); return () => ref.current?.close(); }, []);
|
|
67
|
-
return h('dialog', { ref, className: 'dshr-dialog', 'aria-labelledby': id,
|
|
93
|
+
return h('dialog', { ref, className: 'dshr-dialog' + (wide ? ' catalog' : ''), 'aria-labelledby': id,
|
|
68
94
|
onKeyDownCapture: event => {
|
|
69
95
|
if (event.nativeEvent.isComposing) return;
|
|
70
96
|
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); if (!busy) onClose(); }
|
|
71
97
|
if (event.key === 'Tab') {
|
|
72
98
|
event.stopPropagation();
|
|
73
|
-
const targets = [...event.currentTarget.querySelectorAll('input:not([disabled]),textarea:not([disabled]),select:not([disabled]),button:not([disabled])')];
|
|
99
|
+
const targets = [...event.currentTarget.querySelectorAll('input:not([disabled]),textarea:not([disabled]),select:not([disabled]),button:not([disabled]),a[href],summary')].filter(node => node.getClientRects().length);
|
|
74
100
|
const first = targets[0], last = targets[targets.length - 1];
|
|
75
101
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
|
76
102
|
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
|
@@ -85,6 +111,9 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
85
111
|
const [error, setError] = useState(''), [notice, setNotice] = useState(''), [query, setQuery] = useState(''), [category, setCategory] = useState('*');
|
|
86
112
|
const [modal, setModal] = useState(null), [draft, setDraft] = useState({ url: '', name: '', category: '' });
|
|
87
113
|
const [opml, setOpml] = useState(''), [updateExisting, setUpdateExisting] = useState(false), [preview, setPreview] = useState(null);
|
|
114
|
+
const [catalogQuery, setCatalogQuery] = useState(''), [catalogCategory, setCatalogCategory] = useState('*'), [catalogLanguage, setCatalogLanguage] = useState('*');
|
|
115
|
+
const [sourcePreviews, setSourcePreviews] = useState({}), [selectedSource, setSelectedSource] = useState('');
|
|
116
|
+
const categoryListId = useId();
|
|
88
117
|
const controller = useRef(null), trigger = useRef(null);
|
|
89
118
|
useEffect(() => { listeners.add(setLanguage); return () => listeners.delete(setLanguage); }, []);
|
|
90
119
|
useEffect(() => { void run(null); return () => controller.current?.abort(); }, []);
|
|
@@ -106,6 +135,7 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
106
135
|
trigger.current = event.currentTarget; setError(''); setNotice(''); setPreview(null);
|
|
107
136
|
if (type === 'feed') setDraft(feed ? { url: feed.url, name: feed.name, category: feed.category } : { url: '', name: '', category: '' });
|
|
108
137
|
if (type === 'import') { setOpml(''); setUpdateExisting(false); }
|
|
138
|
+
if (type === 'catalog') { setCatalogQuery(''); setCatalogCategory('*'); setCatalogLanguage('*'); setSelectedSource(''); }
|
|
109
139
|
setModal({ type, feed });
|
|
110
140
|
}
|
|
111
141
|
async function chooseFile(event) {
|
|
@@ -117,6 +147,7 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
117
147
|
}
|
|
118
148
|
const field = (key, hint) => h('label', { key }, t(key), h('input', {
|
|
119
149
|
name: key, type: key === 'url' ? 'url' : 'text', value: draft[key], placeholder: hint, required: key === 'url', disabled: !!busy,
|
|
150
|
+
list: key === 'category' ? categoryListId : undefined,
|
|
120
151
|
autoFocus: key === 'url', onChange: event => setDraft({ ...draft, [key]: event.target.value }),
|
|
121
152
|
}));
|
|
122
153
|
const buttons = submit => h('div', { className: 'dshr-buttons' },
|
|
@@ -124,12 +155,18 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
124
155
|
const alert = error ? h('div', { role: 'alert', className: 'dshr-alert' }, error) : null;
|
|
125
156
|
const feeds = state?.feeds || [], writable = state?.writable === true;
|
|
126
157
|
const categories = [...new Set(feeds.map(feed => feed.category))].sort();
|
|
158
|
+
const catalog = state?.catalog || { categories: [], sources: [] };
|
|
159
|
+
const sourceCategory = source => localized(catalog.categories.find(category => category.id === source.category)?.name);
|
|
160
|
+
const sourceOptions = catalog.sources.filter(source => (catalogCategory === '*' || source.category === catalogCategory) &&
|
|
161
|
+
(catalogLanguage === '*' || source.language === catalogLanguage) &&
|
|
162
|
+
(source.name + ' ' + source.url + ' ' + (source.description || []).join(' ')).toLowerCase().includes(catalogQuery.trim().toLowerCase()));
|
|
127
163
|
const filtered = feeds.filter(feed => (category === '*' || feed.category === category) && (feed.name + ' ' + feed.url).toLowerCase().includes(query.trim().toLowerCase()));
|
|
128
164
|
return h('section', { className: 'dshr', 'aria-label': t('title') },
|
|
129
165
|
h('h2', null, t('title')), h('p', { className: 'dshr-muted' }, t('intro')),
|
|
130
166
|
state && !writable ? h('p', { className: 'dshr-notice' }, t('readonly')) : null,
|
|
131
167
|
h('div', { className: 'dshr-toolbar' },
|
|
132
|
-
h('button', { className: 'primary', disabled: !
|
|
168
|
+
h('button', { className: 'primary', disabled: !state || !!busy, onClick: event => open('catalog', null, event) }, t('recommended')),
|
|
169
|
+
h('button', { disabled: !writable || !!busy, onClick: event => open('feed', null, event) }, t('manual')),
|
|
133
170
|
h('button', { disabled: !writable || !!busy, onClick: event => open('import', null, event) }, t('import')),
|
|
134
171
|
h('button', { disabled: !state || !!busy, onClick: () => run('export', {}, value => {
|
|
135
172
|
const url = URL.createObjectURL(new Blob([value.opml], { type: 'text/x-opml;charset=utf-8' }));
|
|
@@ -151,17 +188,44 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
151
188
|
h('span', { className: 'dshr-tag' }, feed.category || t('uncategorized')),
|
|
152
189
|
h('div', { className: 'dshr-status' }, feed.check ? [
|
|
153
190
|
new Date(feed.check.checkedAt).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN'), ' · ',
|
|
154
|
-
feed.check.ok ? feed.check.entryCount + ' ' + t('entries') + ' · ' + t('checked') : t('failed') + ':' + feed.check.error,
|
|
191
|
+
feed.check.ok ? feed.check.entryCount + ' ' + t('entries') + ' · ' + t('checked') : t('failed') + (language === 'en' ? ': ' : ':') + feedError(feed.check.error),
|
|
155
192
|
] : t('pending'))),
|
|
156
193
|
h('div', { className: 'dshr-actions' },
|
|
157
194
|
h('button', { disabled: !!busy, onClick: () => run('check', { id: feed.id }, value => setNotice(t('checked') + ' · ' + value.check.entryCount + ' ' + t('entries'))) }, busy === feed.id ? t('saving') : t('check')),
|
|
158
195
|
h('button', { disabled: !writable || !!busy, onClick: event => open('feed', feed, event) }, t('edit')),
|
|
159
196
|
h('button', { disabled: !writable || !!busy, onClick: event => open('remove', feed, event) }, t('remove')))))),
|
|
160
197
|
feeds.length ? h('p', { className: 'dshr-muted', style: { marginTop: 12 } }, t('statusHint')) : null),
|
|
161
|
-
modal ? h(Dialog, { title: modal.type === 'import' ? t('import') : modal.type === 'remove' ? t('deleteTitle') : modal.feed ? t('edit') : t('add'), busy: !!busy, onClose: close },
|
|
198
|
+
modal ? h(Dialog, { title: modal.type === 'catalog' ? t('recommended') : modal.type === 'import' ? t('import') : modal.type === 'remove' ? t('deleteTitle') : modal.feed ? t('edit') : t('add'), wide: modal.type === 'catalog', busy: !!busy, onClose: close },
|
|
162
199
|
alert,
|
|
200
|
+
modal.type === 'catalog' ? h('div', null,
|
|
201
|
+
h('p', { className: 'dshr-muted' }, t('catalogHint')), h('p', { className: 'dshr-muted' }, t('networkHint')),
|
|
202
|
+
notice ? h('p', { role: 'status', className: 'dshr-notice' }, notice) : null,
|
|
203
|
+
h('div', { className: 'dshr-filters' },
|
|
204
|
+
h('label', null, t('sourceSearch'), h('input', { type: 'search', autoFocus: true, value: catalogQuery, disabled: !!busy, onChange: event => setCatalogQuery(event.target.value) })),
|
|
205
|
+
h('label', null, t('category'), h('select', { value: catalogCategory, disabled: !!busy, onChange: event => setCatalogCategory(event.target.value) }, h('option', { value: '*' }, t('all')), catalog.categories.map(category => h('option', { key: category.id, value: category.id }, localized(category.name))))),
|
|
206
|
+
h('label', null, t('sourceLanguage'), h('select', { value: catalogLanguage, disabled: !!busy, onChange: event => setCatalogLanguage(event.target.value) }, h('option', { value: '*' }, t('anyLanguage')), h('option', { value: 'zh' }, t('chinese')), h('option', { value: 'en' }, t('english'))))),
|
|
207
|
+
!sourceOptions.length ? h('p', { role: 'status' }, t('noMatch')) : null,
|
|
208
|
+
h('ul', { className: 'dshr-catalog' }, sourceOptions.map(source => {
|
|
209
|
+
const recent = sourcePreviews[source.id];
|
|
210
|
+
return h('li', { key: source.id, className: 'dshr-source' + (selectedSource === source.id ? ' is-selected' : ''), 'aria-label': source.name },
|
|
211
|
+
h('h4', null, source.name), h('span', { className: 'dshr-tag' }, sourceCategory(source)), ' ', h('span', { className: 'dshr-tag' }, t(source.language === 'zh' ? 'chinese' : 'english')),
|
|
212
|
+
h('p', null, localized(source.description)), h('a', { href: source.website, target: '_blank', rel: 'noopener noreferrer' }, t('website')),
|
|
213
|
+
h('details', null, h('summary', null, t('url')), h('p', { className: 'dshr-url' }, source.url), h('p', { className: 'dshr-muted' }, t('native'))),
|
|
214
|
+
source.check ? h('p', { className: 'dshr-status' }, source.check.ok ? t('checked') + ' · ' + source.check.entryCount + ' ' + t('entries') : t('failed') + ' · ' + feedError(source.check.error)) : null,
|
|
215
|
+
h('div', { className: 'dshr-actions' },
|
|
216
|
+
h('button', { type: 'button', disabled: !!busy, onClick: () => { setSelectedSource(source.id); void run('previewSource', { sourceId: source.id }, value => setSourcePreviews(old => ({ ...old, [source.id]: value.sourcePreview }))); } }, busy === 'previewSource' && selectedSource === source.id ? t('saving') : t('previewArticles')),
|
|
217
|
+
h('button', { type: 'button', className: source.subscribed ? '' : 'primary', disabled: !writable || !!busy || source.subscribed, onClick: () => { setSelectedSource(source.id); void run('add', { url: source.url, name: source.name, category: sourceCategory(source), revision: state.revision }, () => setNotice(t('saved') + ' · ' + source.name)); } }, source.subscribed ? t('subscribed') : t('subscribe'))),
|
|
218
|
+
recent ? h('div', { className: 'dshr-preview', role: 'status' }, h('strong', null, t('recentArticles')), h('p', { className: 'dshr-muted' }, new Date(recent.checkedAt).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN')),
|
|
219
|
+
!recent.entries.length ? h('p', null, t('noRecentArticles')) : h('ul', null, recent.entries.map((entry, index) => h('li', { key: index },
|
|
220
|
+
entry.link ? h('a', { href: entry.link, target: '_blank', rel: 'noopener noreferrer' }, entry.title || t('noTitle')) : entry.title || t('noTitle'),
|
|
221
|
+
entry.pubDate ? h('div', { className: 'dshr-muted' }, new Date(entry.pubDate).toLocaleDateString(language === 'en' ? 'en-US' : 'zh-CN')) : null)))) : null);
|
|
222
|
+
})),
|
|
223
|
+
h('p', { className: 'dshr-muted' }, t('rsshubHint')),
|
|
224
|
+
h('div', { className: 'dshr-buttons' }, h('button', { type: 'button', disabled: !!busy, onClick: close }, t('close')))) :
|
|
163
225
|
modal.type === 'feed' ? h('form', { onSubmit: event => { event.preventDefault(); void run(modal.feed ? 'edit' : 'add', { ...draft, id: modal.feed?.id, revision: state.revision }, () => { close(); setNotice(t('saved')); }); } },
|
|
164
226
|
field('url', 'https://example.com/feed.xml'), field('name', t('nameHint')), field('category', t('categoryHint')),
|
|
227
|
+
h('datalist', { id: categoryListId }, [...new Set([...categories, ...catalog.categories.map(category => localized(category.name))])].filter(Boolean).map(value => h('option', { key: value, value }))),
|
|
228
|
+
h('p', { className: 'dshr-muted' }, t('customCategory')),
|
|
165
229
|
error ? h('button', { type: 'button', disabled: !!busy, onClick: () => run(null) }, t('refresh')) : null,
|
|
166
230
|
buttons(h('button', { type: 'submit', className: 'primary', disabled: !!busy }, t(busy ? 'saving' : 'save')))) :
|
|
167
231
|
modal.type === 'remove' ? h('div', null, h('strong', null, modal.feed.name || modal.feed.url), h('p', { className: 'dshr-url' }, modal.feed.url), h('p', null, t('deleteHint')),
|
|
@@ -178,7 +242,7 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
178
242
|
(preview.duplicates || []).slice(0, 20).map((item, index) => h('li', { key: 'duplicate-' + index },
|
|
179
243
|
t('duplicates'), ':', item.before.name || item.url, ' · ', item.before.category || t('uncategorized'),
|
|
180
244
|
updateExisting ? ' → ' + (item.after.name || item.url) + ' · ' + (item.after.category || t('uncategorized')) : ' · ' + t('keep'))),
|
|
181
|
-
preview.skipped.slice(0, 20).map((item, index) => h('li', { key: index }, item.title, ' · ', item.url, ':', item.reason))),
|
|
245
|
+
preview.skipped.slice(0, 20).map((item, index) => h('li', { key: index }, item.title, ' · ', item.url, language === 'en' ? ': ' : ':', feedError(item.reason)))),
|
|
182
246
|
preview.added.length > 20 || preview.skipped.length > 20 || (preview.duplicates || []).length > 20 ? h('p', { className: 'dshr-muted' }, t('previewLimit')) : null) : null,
|
|
183
247
|
h('div', { className: 'dshr-buttons' }, h('button', { type: 'button', disabled: !!busy, onClick: close }, t('cancel')),
|
|
184
248
|
h('button', { type: 'submit', disabled: !!busy || !opml.trim() }, t(busy === 'preview' ? 'saving' : 'preview')),
|
|
@@ -196,7 +260,11 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
|
|
|
196
260
|
const style = document.createElement('style'); style.dataset.pluginCss = 'dsh-rss/client'; style.textContent = CSS; document.head.appendChild(style);
|
|
197
261
|
return () => style.remove();
|
|
198
262
|
}, 'dsh-rss: styles');
|
|
199
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-rss', order: 55, label: () =>
|
|
263
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-rss', order: 55, label: () => {
|
|
264
|
+
let lang = language;
|
|
265
|
+
try { const service = ctx.get ? ctx.get('locale') : ctx.locale; if (service?.getSnapshot) lang = String(service.getSnapshot().active || '').startsWith('en') ? 'en' : 'zh'; } catch {}
|
|
266
|
+
return words.title[lang === 'en' ? 1 : 0];
|
|
267
|
+
}, inject: () => ({}) }, Panel));
|
|
200
268
|
}
|
|
201
269
|
return { apply, inject: ['slots', 'locale'] };
|
|
202
270
|
} });
|
package/lib/web.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
3
3
|
import type { RssSettingsScope } from './tools.js';
|
|
4
4
|
import type { ResolvedRssConfig } from './config.js';
|
|
5
5
|
import { type FetchLike, type DnsLookupLike } from './network.js';
|
|
6
|
+
import { sourceCategories } from './catalog.js';
|
|
6
7
|
export declare const RSS_SETTINGS_ROUTE = "/_dsh/dsh-rss/subscriptions";
|
|
7
8
|
export declare function createRssSettingsBackend(config: ResolvedRssConfig, scope: RssSettingsScope, options?: {
|
|
8
9
|
writable?: () => boolean;
|
|
@@ -19,6 +20,35 @@ export declare function createRssSettingsBackend(config: ResolvedRssConfig, scop
|
|
|
19
20
|
name: string;
|
|
20
21
|
category: string;
|
|
21
22
|
}[];
|
|
23
|
+
catalog: {
|
|
24
|
+
categories: readonly [{
|
|
25
|
+
readonly id: "technology";
|
|
26
|
+
readonly name: readonly ["科技资讯", "Technology news"];
|
|
27
|
+
}, {
|
|
28
|
+
readonly id: "research";
|
|
29
|
+
readonly name: readonly ["AI 与科研", "AI and research"];
|
|
30
|
+
}, {
|
|
31
|
+
readonly id: "software";
|
|
32
|
+
readonly name: readonly ["软件更新", "Software updates"];
|
|
33
|
+
}, {
|
|
34
|
+
readonly id: "reading";
|
|
35
|
+
readonly name: readonly ["阅读与博客", "Reading and blogs"];
|
|
36
|
+
}, {
|
|
37
|
+
readonly id: "interests";
|
|
38
|
+
readonly name: readonly ["生活与兴趣", "Life and interests"];
|
|
39
|
+
}];
|
|
40
|
+
sources: {
|
|
41
|
+
subscribed: boolean;
|
|
42
|
+
check: import("./subscriptions.js").FeedCheck | null;
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
url: string;
|
|
46
|
+
website: string;
|
|
47
|
+
category: (typeof sourceCategories)[number]["id"];
|
|
48
|
+
language: "zh" | "en";
|
|
49
|
+
description: readonly [string, string];
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
22
52
|
};
|
|
23
53
|
handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
24
54
|
};
|
package/lib/web.js
CHANGED
|
@@ -2,6 +2,7 @@ import { sameFeedUrl } from './feeds.js';
|
|
|
2
2
|
import { assertHttpUrl } from './network.js';
|
|
3
3
|
import { buildOpml, parseOpml, importOpmlFeeds } from './opml.js';
|
|
4
4
|
import { feedId, subscriptionSnapshot, mutateSubscriptions, recentFeedChecks, readRssFeed, SubscriptionError } from './subscriptions.js';
|
|
5
|
+
import { recommendedSources, sourceCategories, articleLink } from './catalog.js';
|
|
5
6
|
export const RSS_SETTINGS_ROUTE = '/_dsh/dsh-rss/subscriptions';
|
|
6
7
|
// JSON quoting can double the OPML file size; limit the actual text separately.
|
|
7
8
|
const MAX_OPML = 1024 * 1024;
|
|
@@ -78,7 +79,9 @@ export function createRssSettingsBackend(config, scope, options = {}) {
|
|
|
78
79
|
const snapshot = () => {
|
|
79
80
|
const state = subscriptionSnapshot(scope);
|
|
80
81
|
const checks = recentFeedChecks(scope);
|
|
81
|
-
return { revision: state.revision, writable: writable(), feeds: state.feeds.map(feed => ({ ...feed, id: feedId(feed.url), check: checks.get(feed.url) ?? null }))
|
|
82
|
+
return { revision: state.revision, writable: writable(), feeds: state.feeds.map(feed => ({ ...feed, id: feedId(feed.url), check: checks.get(feed.url) ?? null })),
|
|
83
|
+
catalog: { categories: sourceCategories, sources: recommendedSources.map(source => ({ ...source,
|
|
84
|
+
subscribed: state.feeds.some(feed => sameFeedUrl(feed.url, source.url)), check: checks.get(source.url) ?? null })) } };
|
|
82
85
|
};
|
|
83
86
|
const find = (feeds, id) => {
|
|
84
87
|
const feed = feeds.find(feed => feedId(feed.url) === id);
|
|
@@ -108,6 +111,16 @@ export function createRssSettingsBackend(config, scope, options = {}) {
|
|
|
108
111
|
return { opml: buildOpml(subscriptionSnapshot(scope).feeds) };
|
|
109
112
|
if (action === 'preview')
|
|
110
113
|
return { preview: preview(args) };
|
|
114
|
+
if (action === 'previewSource') {
|
|
115
|
+
const source = recommendedSources.find(source => source.id === text(args, 'sourceId'));
|
|
116
|
+
if (!source)
|
|
117
|
+
throw new SubscriptionError('这个推荐源不存在,请刷新目录。');
|
|
118
|
+
const { parsed, finalUrl } = await check(source.url, signal);
|
|
119
|
+
return { sourcePreview: { sourceId: source.id, checkedAt: new Date().toISOString(), title: parsed.feed.title,
|
|
120
|
+
entryCount: parsed.entries.length, entries: parsed.entries.slice(0, 5).map(entry => ({
|
|
121
|
+
title: entry.title.slice(0, 400), link: articleLink(entry.link, finalUrl), pubDate: entry.pubDate.slice(0, 64),
|
|
122
|
+
})) } };
|
|
123
|
+
}
|
|
111
124
|
if (action === 'check') {
|
|
112
125
|
const url = text(args, 'id') ? find(subscriptionSnapshot(scope).feeds, text(args, 'id')).url : text(args, 'url');
|
|
113
126
|
const { parsed } = await check(url, signal);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rss",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "订阅和阅读 RSS / Atom,在 DSH 中查看与管理更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"lib",
|
|
19
19
|
"cordis.patch.yml",
|
|
20
20
|
"README.md",
|
|
21
|
-
"README.en.md"
|
|
21
|
+
"README.en.md",
|
|
22
|
+
"docs",
|
|
23
|
+
"CHANGELOG.md"
|
|
22
24
|
],
|
|
23
25
|
"scripts": {
|
|
24
26
|
"build": "tsc -p tsconfig.json",
|