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 CHANGED
@@ -1,100 +1,109 @@
1
- # dsh-rss
2
-
3
- ![npm](https://img.shields.io/npm/v/dsh-rss) ![downloads](https://img.shields.io/npm/dm/dsh-rss) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-rss) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-rss?style=social)
4
-
5
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
6
-
7
- 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 seven model-facing tools.
8
-
9
- ## Compatibility
10
-
11
- Verified with official `@deepseek-ai/dsh@0.1.5-rc.1` and Node `24.16.0` on 2026-09-11: all 18 components load alongside Modlens, with passing tool-schema, skill-registration and offline read-only invocation checks. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements match this Harness release: 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
12
-
13
- 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.
14
-
15
- `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.
16
-
17
- ## Installation
18
-
19
- ```bash
20
- dsh plugin --profile web add dsh-rss
21
- ```
22
-
23
- Restart the web service after installing.
24
-
25
- ## Uninstall
26
-
27
- ```bash
28
- dsh plugin --profile web remove dsh-rss
29
- ```
30
-
31
- Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
32
-
33
-
34
- ## Configuration
35
-
36
- Override the plugin row in your profile's `cordis.patch.yml` (the plugin also loads with all defaults when absent):
37
-
38
- ```yaml
39
- - id: rss
40
- name: 'dsh-rss'
41
- config:
42
- # proxyUrl: http://127.0.0.1:7890 # enable when a feed needs a special proxy
43
- timeoutMs: 15000 # fetch timeout in ms (default 15000)
44
- # maxBodyBytes: 5242880 # response size cap (default 5MB, guards oversized responses)
45
- # userAgent: 'dsh-rss/0.2.0' # custom fetch UA
46
- # feedsYaml: | # optional: pre-seed subscriptions (or use the rss_add tool)
47
- # - url: https://example.com/feed.xml
48
- # name: My feed
49
- # category: tech
50
- ```
51
-
52
- ## Tools
53
-
54
- | Tool | Purpose | Key parameters |
55
- | :-- | :-- | :-- |
56
- | `rss_list` | List subscribed feeds | none |
57
- | `rss_add` | Add a subscription (fetches and validates the URL first) | `url` required; `name`/`category` optional |
58
- | `rss_remove` | Remove a subscription | `url` or `name`, at least one |
59
- | `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 |
60
- | `rss_check` | Validate that a URL is a parseable feed | `url` required |
61
- | `rss_opml_export` | Export subscriptions as OPML 2.0 text (optionally write a file) | `path` optional |
62
- | `rss_opml_import` | Bulk-import subscriptions from OPML 2.0 text | `opml` required |
63
-
64
- ### Examples
65
-
66
- ```text
67
- rss_add { url: https://example.com/feed.xml, name: my-feed }
68
- rss_fetch { name: my-feed, limit: 10 }
69
- rss_check { url: https://example.com/feed.xml }
70
- rss_opml_export { path: subscriptions.opml }
71
- rss_opml_import { opml: "<?xml version=\"1.0\"?>..." }
72
- ```
73
-
74
- ## Subscriptions
75
-
76
- Subscriptions live in the settings namespace `dsh-rss` (the `feedsYaml` field): `rss_add` / `rss_remove` read and write it automatically and changes persist across restarts. You can also pre-seed subscriptions via the `feedsYaml` config field. Use `url` to distinguish feeds that share a name.
77
-
78
- ## Proxy
79
-
80
- 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.
81
-
82
- ## Parsing capabilities
83
-
84
- - RSS 2.0 / RSS 1.0 (RDF) / Atom, normalized to one output shape
85
- - Entity decoding, CDATA, `content:encoded`, `dc:creator` and other common fields
86
- - RFC 822 / ISO 8601 dates normalized to ISO 8601 UTC (`pubDate`); the raw text stays in `pubDateRaw`
87
- - Relative links resolved against the feed URL
88
- - 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)
89
- - Safety: no DTD / external entity parsing; 5MB body cap; configurable fetch timeout
90
-
91
- ## Development
92
-
93
- ```bash
94
- pnpm install
95
- pnpm test # build + offline tests with explicit fetch and DNS fixtures
96
- ```
97
-
98
- ## License
99
-
100
- MIT
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
+ ![npm](https://img.shields.io/npm/v/dsh-rss) ![downloads](https://img.shields.io/npm/dm/dsh-rss) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-rss) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-rss?style=social)
10
+
11
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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
- > **agent 的资讯雷达**:订阅管理 + RSS/Atom 抓取解析。
6
-
7
- ![npm version](https://img.shields.io/npm/v/dsh-rss?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-rss) ![license](https://img.shields.io/npm/l/dsh-rss) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-rss?style=social)
8
-
9
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
10
-
11
-
12
- DSH(DeepSeek Harness)的 RSS/Atom 订阅工具插件:管理订阅源,抓取并解析 RSS 0.9x / 1.0 / 2.0 与 Atom,支持 OPML 批量导入导出,给模型提供九个可直接调用的工具。
13
-
14
- ## 兼容性
15
-
16
- 已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
17
-
18
- 抓取接收 Harness 的取消信号,取消 DNS、网络读取或跨源搜索时保留原始取消原因。默认预检域名解析结果并拒绝回环、私网和链路本地地址;每次重定向都重新校验。可信内网源可显式配置 `allowPrivateNetwork: true`。
19
-
20
- `rss_opml_export.path` 必须相对本次调用的会话工作区,父目录须已存在;工具拒绝绝对路径、目录穿越和指向工作区外的链接,并返回实际写入文件的绝对路径。文件写入默认通过 Harness 审批门,`opmlWriteApproval: false` 可关闭该门;省略 `path` 时仅返回 OPML 文本。
21
-
22
- ## 安装
23
-
24
- ```bash
25
- dsh plugin --profile web add dsh-rss
26
- ```
27
-
28
- 安装后重启 Web 服务即可。
29
-
30
- ## 卸载
31
-
32
- ```bash
33
- dsh plugin --profile web remove dsh-rss
34
- ```
35
-
36
- 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
37
-
38
-
39
- ## 配置
40
-
41
- 在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时插件也能加载,只是全部用默认值):
42
-
43
- ```yaml
44
- - id: rss
45
- name: 'dsh-rss'
46
- config:
47
- # proxyUrl: http://127.0.0.1:7890 # 部分订阅源需要特殊代理(梯子)才能访问时启用
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
- 订阅列表保存在 settings 的 `dsh-rss` 命名空间(`feedsYaml` 字段)里:`rss_add` / `rss_remove` 会自动读写并持久化,重启后仍在;也可以在配置里用 `feedsYaml` 预置初始订阅。同名订阅请用 `url` 区分。
84
-
85
- ## 特殊代理(梯子)
86
-
87
- 大部分订阅源可以直接访问;少数订阅源需要特殊代理(梯子)才能连通。遇到 `抓取失败` 且提示需要代理时,把 `proxyUrl` 配成你的本地代理地址(如 `http://127.0.0.1:7890`)并重启即可。代理只作用于本插件的抓取请求,不影响同进程其他插件。
88
-
89
- ## 解析能力
90
-
91
- - RSS 2.0 / RSS 1.0(RDF)/ Atom 三种格式,统一归一化输出
92
- - 实体解码、CDATA、`content:encoded`、`dc:creator`、`itunes` 等常见字段
93
- - RFC 822 / ISO 8601 日期统一转 ISO 8601 UTC(`pubDate`),原始文本保留在 `pubDateRaw`
94
- - 相对链接按订阅源地址解析成绝对链接
95
- - 摘要去 HTML 标签并截断到 500 字符;RSS `content:encoded` / Atom `content` 作为 `content` 字段保留(去标签后最多 20000 字符)
96
- - 安全:不解析 DTD/外部实体,内容只做文本抽取;响应体积上限 5MB;抓取超时可配
97
-
98
- ## 开发
99
-
100
- ```bash
101
- pnpm install
102
- pnpm test # 构建 + 离线测试(fetch 与 DNS 均使用夹具)
103
- ```
104
-
105
- 发布前门禁:危险模式扫描、manifest 自检、`pnpm audit --prod`、全量测试,以及全新 profile 的真实启动冒烟测试。
106
-
107
- ## License
108
-
109
- MIT
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
+ ![npm version](https://img.shields.io/npm/v/dsh-rss?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-rss) ![license](https://img.shields.io/npm/l/dsh-rss) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-rss?style=social)
14
+
15
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](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
@@ -11,6 +11,7 @@ export interface RssConfig {
11
11
  maxBodyBytes?: number;
12
12
  userAgent?: string;
13
13
  feedsYaml?: string;
14
+ cursorsJson?: string;
14
15
  allowPrivateNetwork?: boolean;
15
16
  opmlWriteApproval?: boolean;
16
17
  }
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
- const settingsScope = ctx.settings.register(RSS_SETTINGS_NAMESPACE, RssSettingsSchema, {
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
- * settings 命名空间:订阅列表(feedsYaml)存在这里,供设置页与工具共同读写。
3
- *
4
- * @module dsh-rss/settings
5
- */
6
- import z from 'schemastery';
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: z<Schemastery.ObjectS<{
11
- feedsYaml: z<string, string>;
12
- cursorsJson: z<string, string>;
13
- }>, Schemastery.ObjectT<{
14
- feedsYaml: z<string, string>;
15
- cursorsJson: z<string, string>;
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.3.2",
4
- "description": "DSH RSS 订阅工具插件:管理订阅源、OPML 导入导出、抓取解析 RSS/Atom 并保留正文(rss_list / rss_add / rss_remove / rss_fetch / rss_check / rss_opml_export / rss_opml_import)",
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",