dsh-rss 0.3.2 → 0.4.1
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/README.en.md +109 -100
- package/README.md +115 -109
- package/lib/config.d.ts +1 -0
- package/lib/index.d.ts +9 -1
- package/lib/index.js +13 -3
- package/lib/legacy-settings.d.ts +4 -0
- package/lib/legacy-settings.js +64 -0
- package/lib/network.js +4 -0
- package/lib/settings.d.ts +30 -13
- package/lib/settings.js +13 -0
- package/package.json +4 -3
package/README.en.md
CHANGED
|
@@ -1,100 +1,109 @@
|
|
|
1
|
-
# dsh-rss
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1
|
+
# dsh-rss
|
|
2
|
+
|
|
3
|
+
## 0.4.1 update (2026-09-27)
|
|
4
|
+
|
|
5
|
+
Explains feed resolution failures caused by proxy Fake-IP DNS and suggests using a real DNS address while keeping reserved-address protection enabled.
|
|
6
|
+
|
|
7
|
+
Validation host: Harness 0.1.7-rc.2 built from official sources, retaining the local tool-scheduler fix. Build and automated checks pass; interactive coverage and external-service limits are recorded in this release round.
|
|
8
|
+
|
|
9
|
+
   
|
|
10
|
+
|
|
11
|
+
[](https://awesome-dsh-plugin.com)
|
|
12
|
+
|
|
13
|
+
DSH (DeepSeek Harness) plugin for RSS/Atom subscriptions: manage feeds, fetch and parse RSS 0.9x / 1.0 / 2.0 and Atom, with OPML bulk import/export, exposing nine model-facing tools.
|
|
14
|
+
|
|
15
|
+
## Compatibility
|
|
16
|
+
|
|
17
|
+
Version 0.4.0 supports official-source Harness **0.1.7-alpha.2** (2026-09-23, with a local `Symbol.for` tool-scheduler fix). 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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web add dsh-rss
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## Uninstall
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
dsh plugin --profile web remove dsh-rss
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Override the plugin row in your profile's `cordis.patch.yml` (the plugin also loads with all defaults when absent):
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- id: rss
|
|
46
|
+
name: 'dsh-rss'
|
|
47
|
+
config:
|
|
48
|
+
# proxyUrl: http://127.0.0.1:7890 # enable when a feed needs a special proxy
|
|
49
|
+
timeoutMs: 15000 # fetch timeout in ms (default 15000)
|
|
50
|
+
# maxBodyBytes: 5242880 # response size cap (default 5MB, guards oversized responses)
|
|
51
|
+
# userAgent: 'dsh-rss/0.2.0' # custom fetch UA
|
|
52
|
+
# feedsYaml: | # optional: pre-seed subscriptions (or use the rss_add tool)
|
|
53
|
+
# - url: https://example.com/feed.xml
|
|
54
|
+
# name: My feed
|
|
55
|
+
# category: tech
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
| Tool | Purpose | Key parameters |
|
|
61
|
+
| :-- | :-- | :-- |
|
|
62
|
+
| `rss_list` | List subscribed feeds | none |
|
|
63
|
+
| `rss_add` | Add a subscription (fetches and validates the URL first) | `url` required; `name`/`category` optional |
|
|
64
|
+
| `rss_remove` | Remove a subscription | `url` or `name`, at least one |
|
|
65
|
+
| `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 |
|
|
66
|
+
| `rss_check` | Validate that a URL is a parseable feed | `url` required |
|
|
67
|
+
| `rss_opml_export` | Export subscriptions as OPML 2.0 text (optionally write a file) | `path` optional |
|
|
68
|
+
| `rss_opml_import` | Bulk-import subscriptions from OPML 2.0 text | `opml` required |
|
|
69
|
+
|
|
70
|
+
| `rss_search` | Search across feeds | `query` required; optional feed, date and limit filters |
|
|
71
|
+
| `rss_health` | Check configuration, feeds and cursors offline | none |
|
|
72
|
+
|
|
73
|
+
### Examples
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
rss_add { url: https://example.com/feed.xml, name: my-feed }
|
|
77
|
+
rss_fetch { name: my-feed, limit: 10 }
|
|
78
|
+
rss_check { url: https://example.com/feed.xml }
|
|
79
|
+
rss_opml_export { path: subscriptions.opml }
|
|
80
|
+
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Subscriptions
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
## Proxy
|
|
88
|
+
|
|
89
|
+
Most feeds are reachable directly; a few require a special proxy from your network. When you hit a `fetch failed` error suggesting a proxy, set `proxyUrl` to your local proxy address (e.g. `http://127.0.0.1:7890`) and restart. The proxy only routes this plugin's fetch requests and does not affect other plugins in the same process.
|
|
90
|
+
|
|
91
|
+
## Parsing capabilities
|
|
92
|
+
|
|
93
|
+
- RSS 2.0 / RSS 1.0 (RDF) / Atom, normalized to one output shape
|
|
94
|
+
- Entity decoding, CDATA, `content:encoded`, `dc:creator` and other common fields
|
|
95
|
+
- RFC 822 / ISO 8601 dates normalized to ISO 8601 UTC (`pubDate`); the raw text stays in `pubDateRaw`
|
|
96
|
+
- Relative links resolved against the feed URL
|
|
97
|
+
- 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)
|
|
98
|
+
- Safety: no DTD / external entity parsing; 5MB body cap; configurable fetch timeout
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm install
|
|
104
|
+
pnpm test # build + offline tests with explicit fetch and DNS fixtures
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,109 +1,115 @@
|
|
|
1
|
-
[English](README.en.md)
|
|
2
|
-
|
|
3
|
-
# dsh-rss
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
| `
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
[English](README.en.md)
|
|
2
|
+
|
|
3
|
+
# dsh-rss
|
|
4
|
+
|
|
5
|
+
## 0.4.1 更新(2026-09-27)
|
|
6
|
+
|
|
7
|
+
识别代理 Fake-IP 造成的订阅源解析问题,提示使用真实 DNS 地址;继续拒绝保留地址,无需放开内网访问。
|
|
8
|
+
|
|
9
|
+
验证宿主:官方源码构建的 Harness 0.1.7-rc.2(保留本地工具调度器修复)。构建与自动测试通过;实际操作和外部服务限制见本轮验收记录。
|
|
10
|
+
|
|
11
|
+
> **agent 的资讯雷达**:订阅管理 + RSS/Atom 抓取解析。
|
|
12
|
+
|
|
13
|
+
   
|
|
14
|
+
|
|
15
|
+
[](https://awesome-dsh-plugin.com)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DSH(DeepSeek Harness)的 RSS/Atom 订阅工具插件:管理订阅源,抓取并解析 RSS 0.9x / 1.0 / 2.0 与 Atom,支持 OPML 批量导入导出,给模型提供九个可直接调用的工具。
|
|
19
|
+
|
|
20
|
+
## 兼容性
|
|
21
|
+
|
|
22
|
+
0.4.0 适配官方源码构建的 Harness **0.1.7-alpha.2**(2026-09-23,含本地工具调度器 `Symbol.for` 修复)。18 个插件同载、工具契约、订阅读写与重启持久化通过隔离验证;真实外部订阅源需要正常网络连接。Node 要求为 22.19 及以上的 22.x,或 24 及以上。
|
|
23
|
+
|
|
24
|
+
抓取接收 Harness 的取消信号,取消 DNS、网络读取或跨源搜索时保留原始取消原因。默认预检域名解析结果并拒绝回环、私网和链路本地地址;每次重定向都重新校验。可信内网源可显式配置 `allowPrivateNetwork: true`。
|
|
25
|
+
|
|
26
|
+
`rss_opml_export.path` 必须相对本次调用的会话工作区,父目录须已存在;工具拒绝绝对路径、目录穿越和指向工作区外的链接,并返回实际写入文件的绝对路径。文件写入默认通过 Harness 审批门,`opmlWriteApproval: false` 可关闭该门;省略 `path` 时仅返回 OPML 文本。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
dsh plugin --profile web add dsh-rss
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
安装后重启 Web 服务即可。直接对助手说「订阅这个 RSS 地址,命名为技术资讯」或「列出我的订阅」即可使用,日常增删订阅不需要编辑配置文件。
|
|
35
|
+
|
|
36
|
+
## 卸载
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
dsh plugin --profile web remove dsh-rss
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## 配置
|
|
46
|
+
|
|
47
|
+
在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时插件也能加载,只是全部用默认值):
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- id: rss
|
|
51
|
+
name: 'dsh-rss'
|
|
52
|
+
config:
|
|
53
|
+
# proxyUrl: http://127.0.0.1:7890 # 部分订阅源需要特殊代理(梯子)才能访问时启用
|
|
54
|
+
timeoutMs: 15000 # 抓取超时(毫秒,默认 15000)
|
|
55
|
+
# maxBodyBytes: 5242880 # 订阅源体积上限(默认 5MB,防超大响应)
|
|
56
|
+
# userAgent: 'dsh-rss/0.2.0' # 自定义抓取 UA
|
|
57
|
+
# feedsYaml: | # 可选:预置订阅列表(也可用 rss_add 工具添加)
|
|
58
|
+
# - url: https://example.com/feed.xml
|
|
59
|
+
# name: 示例订阅
|
|
60
|
+
# category: 技术
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 工具一览
|
|
64
|
+
|
|
65
|
+
| 工具 | 作用 | 关键参数 |
|
|
66
|
+
| :-- | :-- | :-- |
|
|
67
|
+
| `rss_list` | 列出已订阅源 | 无 |
|
|
68
|
+
| `rss_add` | 添加订阅(先抓取校验地址) | `url` 必填;`name`/`category` 可选 |
|
|
69
|
+
| `rss_remove` | 删除订阅 | `url` 或 `name` 至少一个 |
|
|
70
|
+
| `rss_fetch` | 抓取解析订阅源,返回源信息与条目(含正文 content) | `url` 或 `name` 至少一个;`limit` 1-100 默认 20 |
|
|
71
|
+
| `rss_check` | 校验地址是否为可解析的订阅源 | `url` 必填 |
|
|
72
|
+
| `rss_opml_export` | 导出订阅列表为 OPML 2.0 文本(可写入文件) | `path` 可选 |
|
|
73
|
+
| `rss_opml_import` | 从 OPML 2.0 文本批量导入订阅源 | `opml` 必填 |
|
|
74
|
+
| `rss_search` | 跨订阅源关键词搜索条目(标题/摘要/正文/作者) | `query` 必填;`name`/`url`/`since`/`limit` 可选 |
|
|
75
|
+
| `rss_health` | 插件自检(配置/订阅/游标/解析器,离线) | 无 |
|
|
76
|
+
|
|
77
|
+
### 示例
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
rss_add { url: https://example.com/feed.xml, name: 我的订阅 }
|
|
81
|
+
rss_fetch { name: 我的订阅, limit: 10 }
|
|
82
|
+
rss_check { url: https://example.com/feed.xml }
|
|
83
|
+
rss_opml_export { path: subscriptions.opml }
|
|
84
|
+
rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 订阅存储
|
|
88
|
+
|
|
89
|
+
Harness 0.1.7 把订阅列表和读取游标保存在当前 profile 的 `rss` 配置行;增删后立即生效,重启仍保留。默认安装会自动导入旧 `settings.yaml` 或 `settings.yaml.imported` 的 `dsh-rss` 数据一次,保留原文件,且不覆盖已有 profile 值。旧版宿主继续使用原 settings 存储。同名订阅请用 URL 区分。
|
|
90
|
+
|
|
91
|
+
## 特殊代理(梯子)
|
|
92
|
+
|
|
93
|
+
大部分订阅源可以直接访问;少数订阅源需要特殊代理(梯子)才能连通。遇到 `抓取失败` 且提示需要代理时,把 `proxyUrl` 配成你的本地代理地址(如 `http://127.0.0.1:7890`)并重启即可。代理只作用于本插件的抓取请求,不影响同进程其他插件。
|
|
94
|
+
|
|
95
|
+
## 解析能力
|
|
96
|
+
|
|
97
|
+
- RSS 2.0 / RSS 1.0(RDF)/ Atom 三种格式,统一归一化输出
|
|
98
|
+
- 实体解码、CDATA、`content:encoded`、`dc:creator`、`itunes` 等常见字段
|
|
99
|
+
- RFC 822 / ISO 8601 日期统一转 ISO 8601 UTC(`pubDate`),原始文本保留在 `pubDateRaw`
|
|
100
|
+
- 相对链接按订阅源地址解析成绝对链接
|
|
101
|
+
- 摘要去 HTML 标签并截断到 500 字符;RSS `content:encoded` / Atom `content` 作为 `content` 字段保留(去标签后最多 20000 字符)
|
|
102
|
+
- 安全:不解析 DTD/外部实体,内容只做文本抽取;响应体积上限 5MB;抓取超时可配
|
|
103
|
+
|
|
104
|
+
## 开发
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pnpm install
|
|
108
|
+
pnpm test # 构建 + 离线测试(fetch 与 DNS 均使用夹具)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
发布前门禁:危险模式扫描、manifest 自检、`pnpm audit --prod`、全量测试,以及全新 profile 的真实启动冒烟测试。
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/lib/config.d.ts
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -28,10 +28,18 @@ type RssPreExecuteListener = (exec: RssPendingToolExecution, next: () => Promise
|
|
|
28
28
|
/** 插件所需的最小 ctx 面(社区插件不依赖宿主内部类型)。 */
|
|
29
29
|
export interface RssPluginContext {
|
|
30
30
|
settings: {
|
|
31
|
-
register(ns: string, schema: unknown, options?: {
|
|
31
|
+
register?(ns: string, schema: unknown, options?: {
|
|
32
32
|
base?: Record<string, unknown>;
|
|
33
33
|
applies?: string;
|
|
34
34
|
}): RssSettingsScope;
|
|
35
|
+
update?(ns: string, patch: Record<string, unknown>): Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
fiber?: {
|
|
38
|
+
entry?: {
|
|
39
|
+
options: {
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
35
43
|
};
|
|
36
44
|
tools: {
|
|
37
45
|
register(definition: RssToolDefinition): () => void;
|
package/lib/index.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* @module dsh-rss
|
|
8
8
|
*/
|
|
9
9
|
import { resolveConfig } from './config.js';
|
|
10
|
-
import { RSS_SETTINGS_NAMESPACE, RssSettingsSchema } from './settings.js';
|
|
10
|
+
import { RSS_SETTINGS_NAMESPACE, RssSettingsSchema, liveConfig } from './settings.js';
|
|
11
11
|
import { buildRssTools } from './tools.js';
|
|
12
|
+
import { installLegacySettingsImport } from './legacy-settings.js';
|
|
12
13
|
/** cordis 服务注入:apply 里要用 ctx.settings 与 ctx.tools,必须显式声明,否则宿主会抛 cannot get property without inject。 */
|
|
13
14
|
export const name = 'rss';
|
|
14
15
|
export const inject = ['settings', 'tools'];
|
|
@@ -18,11 +19,20 @@ export const inject = ['settings', 'tools'];
|
|
|
18
19
|
* @param config - 插件配置(可缺省)。
|
|
19
20
|
*/
|
|
20
21
|
export function apply(ctx, config) {
|
|
22
|
+
config = liveConfig(config ?? {});
|
|
21
23
|
const cfg = resolveConfig(config);
|
|
22
|
-
|
|
24
|
+
installLegacySettingsImport(ctx, config, RSS_SETTINGS_NAMESPACE, 'rss', ['feedsYaml', 'cursorsJson']);
|
|
25
|
+
const settingsScope = typeof ctx.settings.register === 'function' ? ctx.settings.register(RSS_SETTINGS_NAMESPACE, RssSettingsSchema, {
|
|
23
26
|
base: { feedsYaml: cfg.feedsYaml },
|
|
24
27
|
applies: 'live',
|
|
25
|
-
})
|
|
28
|
+
}) : {
|
|
29
|
+
get: () => ({ feedsYaml: config?.feedsYaml ?? '', cursorsJson: config?.cursorsJson ?? '' }),
|
|
30
|
+
update: async (patch) => {
|
|
31
|
+
if (!ctx.settings.update)
|
|
32
|
+
throw new Error('当前宿主不支持保存 RSS 配置');
|
|
33
|
+
await ctx.settings.update(ctx.fiber?.entry?.options.id ?? 'rss', patch);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
26
36
|
const disposers = [];
|
|
27
37
|
for (const definition of buildRssTools(cfg, settingsScope)) {
|
|
28
38
|
disposers.push(ctx.tools.register(definition));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** No backup is edited or deleted; the completion marker lives beside imported values. */
|
|
2
|
+
export declare function importLegacySettings(ctx: any, config: Record<string, any>, namespace: string, entryId: string, fields: readonly string[], home?: string): Promise<boolean>;
|
|
3
|
+
/** Defer persistence until the entry and settings provider are active. */
|
|
4
|
+
export declare function installLegacySettingsImport(ctx: any, config: Record<string, any>, namespace: string, entryId: string, fields: readonly string[]): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Import only this plugin's retired settings section, preserving profile overrides. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
function plain(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function missing(legacy, current) {
|
|
10
|
+
const patch = {};
|
|
11
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
12
|
+
if (current[key] === undefined)
|
|
13
|
+
patch[key] = value;
|
|
14
|
+
else if (plain(value) && plain(current[key])) {
|
|
15
|
+
const nested = missing(value, current[key]);
|
|
16
|
+
if (Object.keys(nested).length)
|
|
17
|
+
patch[key] = nested;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return patch;
|
|
21
|
+
}
|
|
22
|
+
/** No backup is edited or deleted; the completion marker lives beside imported values. */
|
|
23
|
+
export async function importLegacySettings(ctx, config, namespace, entryId, fields, home) {
|
|
24
|
+
if (typeof ctx.settings.register === 'function' || config.legacySettingsImported === true)
|
|
25
|
+
return false;
|
|
26
|
+
// Old namespaces were global. Do not copy credentials into unrelated custom instances.
|
|
27
|
+
if (ctx.fiber?.entry?.options.id !== entryId)
|
|
28
|
+
return false;
|
|
29
|
+
let base = home ?? (process.env.DSH_HOME?.trim() || join(homedir(), '.dsh'));
|
|
30
|
+
if (base === '~')
|
|
31
|
+
base = homedir();
|
|
32
|
+
else if (base.startsWith('~/') || base.startsWith('~\\'))
|
|
33
|
+
base = join(homedir(), base.slice(2));
|
|
34
|
+
let document;
|
|
35
|
+
for (const file of ['settings.yaml.imported', 'settings.yaml']) {
|
|
36
|
+
try {
|
|
37
|
+
document = parse(readFileSync(resolve(base, file), 'utf8'));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== 'ENOENT')
|
|
42
|
+
throw new Error('Cannot read legacy settings; original file preserved');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!plain(document) || !plain(document[namespace]))
|
|
46
|
+
return false;
|
|
47
|
+
const section = Object.fromEntries(fields.filter(key => Object.hasOwn(document[namespace], key)).map(key => [key, document[namespace][key]]));
|
|
48
|
+
if (Object.keys(section).length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
await ctx.settings.update(entryId, { ...missing(section, { ...config }), legacySettingsImported: true });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** Defer persistence until the entry and settings provider are active. */
|
|
54
|
+
export function installLegacySettingsImport(ctx, config, namespace, entryId, fields) {
|
|
55
|
+
if (typeof ctx.settings.register === 'function' || typeof ctx.inject !== 'function')
|
|
56
|
+
return;
|
|
57
|
+
ctx.inject(['appReady'], (readyCtx) => {
|
|
58
|
+
readyCtx.effect(() => readyCtx.appReady.onReady(() => {
|
|
59
|
+
void importLegacySettings(ctx, config, namespace, entryId, fields).catch(() => {
|
|
60
|
+
ctx.logger?.warn?.(namespace + ': legacy settings could not be imported; the original settings file is preserved.');
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
}
|
package/lib/network.js
CHANGED
|
@@ -126,6 +126,10 @@ async function assertPublicUrl(url, cfg, lookupImpl, signal) {
|
|
|
126
126
|
throw new Error('无法安全解析订阅源域名 ' + hostname + ':DNS 未返回地址。');
|
|
127
127
|
const blocked = addresses.find((entry) => isBlockedNetworkAddress(entry.address));
|
|
128
128
|
if (blocked !== undefined) {
|
|
129
|
+
const v4 = ipv4Bytes(blocked.address);
|
|
130
|
+
if (v4 !== null && v4[0] === 198 && (v4[1] === 18 || v4[1] === 19)) {
|
|
131
|
+
throw new Error('订阅源域名 ' + hostname + ' 被解析为保留地址 ' + blocked.address + ',可能是代理软件的 Fake-IP,已拒绝访问。请在代理 DNS 设置中让该域名返回真实 IP(如加入 fake-ip-filter),然后重试。无需开启内网访问。');
|
|
132
|
+
}
|
|
129
133
|
throw new Error('出于安全原因,域名 ' + hostname + ' 解析到了非公网地址 ' + blocked.address + ',已拒绝访问。若确需访问可信内网源,请显式配置 allowPrivateNetwork: true。');
|
|
130
134
|
}
|
|
131
135
|
}
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import modern from '@deepseek-ai/schemastery';
|
|
2
|
+
export declare const Config: modern<Schemastery.ObjectS<NoInfer<{
|
|
3
|
+
feedsYaml: modern<string, string, "volatile">;
|
|
4
|
+
cursorsJson: modern<string, string, "volatile">;
|
|
5
|
+
proxyUrl: modern<string, string, "plain">;
|
|
6
|
+
timeoutMs: modern<number, number, "plain">;
|
|
7
|
+
maxBodyBytes: modern<number, number, "plain">;
|
|
8
|
+
userAgent: modern<string, string, "plain">;
|
|
9
|
+
allowPrivateNetwork: modern<boolean, boolean, "plain">;
|
|
10
|
+
opmlWriteApproval: modern<boolean, boolean, "plain">;
|
|
11
|
+
legacySettingsImported: modern<boolean, boolean, "volatile">;
|
|
12
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
13
|
+
feedsYaml: modern<string, string, "volatile">;
|
|
14
|
+
cursorsJson: modern<string, string, "volatile">;
|
|
15
|
+
proxyUrl: modern<string, string, "plain">;
|
|
16
|
+
timeoutMs: modern<number, number, "plain">;
|
|
17
|
+
maxBodyBytes: modern<number, number, "plain">;
|
|
18
|
+
userAgent: modern<string, string, "plain">;
|
|
19
|
+
allowPrivateNetwork: modern<boolean, boolean, "plain">;
|
|
20
|
+
opmlWriteApproval: modern<boolean, boolean, "plain">;
|
|
21
|
+
legacySettingsImported: modern<boolean, boolean, "volatile">;
|
|
22
|
+
}>>, "plain">;
|
|
23
|
+
export declare function liveConfig<T extends object>(config: T): T;
|
|
7
24
|
/** 本插件拥有的 settings 文档命名空间。 */
|
|
8
25
|
export declare const RSS_SETTINGS_NAMESPACE = "dsh-rss";
|
|
9
26
|
/** settings 页形状:目前只有订阅列表(YAML 文本)。 */
|
|
10
|
-
export declare const RssSettingsSchema:
|
|
11
|
-
feedsYaml:
|
|
12
|
-
cursorsJson:
|
|
13
|
-
}
|
|
14
|
-
feedsYaml:
|
|
15
|
-
cursorsJson:
|
|
16
|
-
}
|
|
27
|
+
export declare const RssSettingsSchema: modern<Schemastery.ObjectS<NoInfer<{
|
|
28
|
+
feedsYaml: modern<string, string, Mode extends "volatile-defined" | "volatile" ? "volatile-defined" : "defined">;
|
|
29
|
+
cursorsJson: modern<string, string, Mode extends "volatile-defined" | "volatile" ? "volatile-defined" : "defined">;
|
|
30
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
31
|
+
feedsYaml: modern<string, string, Mode extends "volatile-defined" | "volatile" ? "volatile-defined" : "defined">;
|
|
32
|
+
cursorsJson: modern<string, string, Mode extends "volatile-defined" | "volatile" ? "volatile-defined" : "defined">;
|
|
33
|
+
}>>, "plain">;
|
|
17
34
|
/** settings 解析后的值。 */
|
|
18
35
|
export interface RssSettingsValue {
|
|
19
36
|
feedsYaml: string;
|
package/lib/settings.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* @module dsh-rss/settings
|
|
5
5
|
*/
|
|
6
6
|
import z from 'schemastery';
|
|
7
|
+
import modern from '@deepseek-ai/schemastery';
|
|
8
|
+
export const Config = modern.object({
|
|
9
|
+
feedsYaml: modern.string().volatile(), cursorsJson: modern.string().volatile(),
|
|
10
|
+
proxyUrl: modern.string(), timeoutMs: modern.number(), maxBodyBytes: modern.number(),
|
|
11
|
+
userAgent: modern.string(), allowPrivateNetwork: modern.boolean(), opmlWriteApproval: modern.boolean(),
|
|
12
|
+
legacySettingsImported: modern.boolean().volatile(),
|
|
13
|
+
});
|
|
14
|
+
export function liveConfig(config) {
|
|
15
|
+
return new Proxy(config, { get(target, key, receiver) {
|
|
16
|
+
const value = Reflect.get(target, key, receiver);
|
|
17
|
+
return value !== null && typeof value === 'object' && typeof value.get === 'function' ? value.get() : value;
|
|
18
|
+
} });
|
|
19
|
+
}
|
|
7
20
|
/** 本插件拥有的 settings 文档命名空间。 */
|
|
8
21
|
export const RSS_SETTINGS_NAMESPACE = 'dsh-rss';
|
|
9
22
|
/** settings 页形状:目前只有订阅列表(YAML 文本)。 */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rss",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH RSS
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "DSH RSS 插件:订阅源管理、OPML 导入导出、抓取解析 RSS/Atom 并保留正文。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"fast-xml-parser": "^5.0.0",
|
|
47
47
|
"schemastery": "^3.18.0",
|
|
48
48
|
"undici": "^8.10.0",
|
|
49
|
-
"yaml": "^2.9.0"
|
|
49
|
+
"yaml": "^2.9.0",
|
|
50
|
+
"@deepseek-ai/schemastery": "^3.18.3"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@types/node": "^24.0.0",
|