dsh-rss 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stardustlc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # dsh-rss
2
+
3
+ DSH(DeepSeek Harness)的 RSS/Atom 订阅工具插件:管理订阅源,抓取并解析 RSS 0.9x / 1.0 / 2.0 与 Atom,给模型提供五个可直接调用的工具。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ dsh plugin --profile web add dsh-rss
9
+ ```
10
+
11
+ 安装后重启 Web 服务即可。
12
+
13
+ ## 配置
14
+
15
+ 在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时插件也能加载,只是全部用默认值):
16
+
17
+ ```yaml
18
+ - id: rss
19
+ name: 'dsh-rss'
20
+ config:
21
+ # proxyUrl: http://127.0.0.1:7890 # 部分订阅源需要特殊代理(梯子)才能访问时启用
22
+ timeoutMs: 15000 # 抓取超时(毫秒,默认 15000)
23
+ # maxBodyBytes: 5242880 # 订阅源体积上限(默认 5MB,防超大响应)
24
+ # userAgent: 'dsh-rss/0.1.0' # 自定义抓取 UA
25
+ # feedsYaml: | # 可选:预置订阅列表(也可用 rss_add 工具添加)
26
+ # - url: https://example.com/feed.xml
27
+ # name: 示例订阅
28
+ # category: 技术
29
+ ```
30
+
31
+ ## 工具一览
32
+
33
+ | 工具 | 作用 | 关键参数 |
34
+ | :-- | :-- | :-- |
35
+ | `rss_list` | 列出已订阅源 | 无 |
36
+ | `rss_add` | 添加订阅(先抓取校验地址) | `url` 必填;`name`/`category` 可选 |
37
+ | `rss_remove` | 删除订阅 | `url` 或 `name` 至少一个 |
38
+ | `rss_fetch` | 抓取解析订阅源,返回源信息与条目 | `url` 或 `name` 至少一个;`limit` 1-100 默认 20 |
39
+ | `rss_check` | 校验地址是否为可解析的订阅源 | `url` 必填 |
40
+
41
+ ### 示例
42
+
43
+ ```text
44
+ rss_add { url: https://example.com/feed.xml, name: 我的订阅 }
45
+ rss_fetch { name: 我的订阅, limit: 10 }
46
+ rss_check { url: https://example.com/feed.xml }
47
+ ```
48
+
49
+ ## 订阅存储
50
+
51
+ 订阅列表保存在 settings 的 `dsh-rss` 命名空间(`feedsYaml` 字段)里:`rss_add` / `rss_remove` 会自动读写并持久化,重启后仍在;也可以在配置里用 `feedsYaml` 预置初始订阅。同名订阅请用 `url` 区分。
52
+
53
+ ## 特殊代理(梯子)
54
+
55
+ 大部分订阅源可以直接访问;少数订阅源需要特殊代理(梯子)才能连通。遇到 `抓取失败` 且提示需要代理时,把 `proxyUrl` 配成你的本地代理地址(如 `http://127.0.0.1:7890`)并重启即可。代理只作用于本插件的抓取请求,不影响同进程其他插件。
56
+
57
+ ## 解析能力
58
+
59
+ - RSS 2.0 / RSS 1.0(RDF)/ Atom 三种格式,统一归一化输出
60
+ - 实体解码、CDATA、`content:encoded`、`dc:creator`、`itunes` 等常见字段
61
+ - RFC 822 / ISO 8601 日期统一转 ISO 8601 UTC(`pubDate`),原始文本保留在 `pubDateRaw`
62
+ - 相对链接按订阅源地址解析成绝对链接
63
+ - 摘要去 HTML 标签并截断到 500 字符;正文全文读取在后续版本提供
64
+ - 安全:不解析 DTD/外部实体,内容只做文本抽取;响应体积上限 5MB;抓取超时可配
65
+
66
+ ## 开发
67
+
68
+ ```bash
69
+ pnpm install
70
+ pnpm test # 构建 + 47 个测试
71
+ ```
72
+
73
+ 发布前门禁:危险模式扫描、manifest 自检、`pnpm audit --prod`、全量测试,以及全新 profile 的真实启动冒烟测试。
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,20 @@
1
+ # dsh-rss 组合包补丁:把 RSS 订阅工具插件行插入 profile。
2
+ # 本补丁按 id 覆盖;用户应在自己 profile 的 cordis.patch.yml 里覆盖本行的整个
3
+ # config(见 README 的配置示例)。配置缺失时插件仍会加载,但每个工具在调用时
4
+ # 会抛出带中文指引的错误,提示补全配置后重启。
5
+ #
6
+ # 示例(用户 profile 的 cordis.patch.yml):
7
+ # - id: rss
8
+ # name: 'dsh-rss'
9
+ # config:
10
+ # # proxyUrl: http://127.0.0.1:7890 # 部分订阅源需要特殊代理(梯子)时启用
11
+ # timeoutMs: 15000 # 抓取超时(毫秒)
12
+ # # feedsYaml: | # 可选:预置订阅列表(也可用 rss_add 工具添加)
13
+ # # - url: https://example.com/feed.xml
14
+ # # name: 示例订阅
15
+ #
16
+ - insert:
17
+ - id: rss
18
+ name: 'dsh-rss'
19
+ config:
20
+ timeoutMs: 15000
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-rss 配置解析:把 cordis.patch.yml 里的行配置解析成强类型配置。
3
+ * 缺失字段给默认值;非法字段抛出带中文指引的错误。
4
+ *
5
+ * @module dsh-rss/config
6
+ */
7
+ /** 插件行配置(cordis.patch.yml 里的 config 段,可缺省)。 */
8
+ export interface RssConfig {
9
+ proxyUrl?: string;
10
+ timeoutMs?: number;
11
+ maxBodyBytes?: number;
12
+ userAgent?: string;
13
+ feedsYaml?: string;
14
+ }
15
+ /** 解析后的配置:所有字段都有值。 */
16
+ export interface ResolvedRssConfig {
17
+ proxyUrl: string;
18
+ timeoutMs: number;
19
+ maxBodyBytes: number;
20
+ userAgent: string;
21
+ feedsYaml: string;
22
+ }
23
+ /**
24
+ * 解析并校验配置。
25
+ * @param config - 插件行配置(可能为 undefined/null)。
26
+ * @throws 配置值非法时抛出中文错误。
27
+ */
28
+ export declare function resolveConfig(config: RssConfig | undefined | null): ResolvedRssConfig;
package/lib/config.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * dsh-rss 配置解析:把 cordis.patch.yml 里的行配置解析成强类型配置。
3
+ * 缺失字段给默认值;非法字段抛出带中文指引的错误。
4
+ *
5
+ * @module dsh-rss/config
6
+ */
7
+ const DEFAULT_TIMEOUT_MS = 15000;
8
+ const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
9
+ const DEFAULT_USER_AGENT = 'dsh-rss/0.1.0 (DeepSeek Harness RSS plugin)';
10
+ function optionalString(value) {
11
+ return typeof value === 'string' ? value : undefined;
12
+ }
13
+ /**
14
+ * 解析并校验配置。
15
+ * @param config - 插件行配置(可能为 undefined/null)。
16
+ * @throws 配置值非法时抛出中文错误。
17
+ */
18
+ export function resolveConfig(config) {
19
+ const cfg = config ?? {};
20
+ const proxyUrl = (optionalString(cfg.proxyUrl) ?? '').trim();
21
+ if (proxyUrl !== '' && !/^https?:\/\//i.test(proxyUrl)) {
22
+ throw new Error('proxyUrl 必须是 http(s):// 开头的地址,例如 http://127.0.0.1:7890。');
23
+ }
24
+ let timeoutMs = DEFAULT_TIMEOUT_MS;
25
+ if (cfg.timeoutMs !== undefined) {
26
+ if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0) {
27
+ throw new Error('timeoutMs 必须是大于 0 的数字(毫秒),例如 15000。');
28
+ }
29
+ timeoutMs = Math.min(120000, Math.max(1000, Math.round(cfg.timeoutMs)));
30
+ }
31
+ let maxBodyBytes = DEFAULT_MAX_BODY_BYTES;
32
+ if (cfg.maxBodyBytes !== undefined) {
33
+ if (typeof cfg.maxBodyBytes !== 'number' || !Number.isFinite(cfg.maxBodyBytes) || cfg.maxBodyBytes <= 0) {
34
+ throw new Error('maxBodyBytes 必须是大于 0 的数字(字节),例如 5242880。');
35
+ }
36
+ maxBodyBytes = Math.min(50 * 1024 * 1024, Math.max(1024 * 1024, Math.round(cfg.maxBodyBytes)));
37
+ }
38
+ const userAgent = optionalString(cfg.userAgent) ?? DEFAULT_USER_AGENT;
39
+ const feedsYaml = optionalString(cfg.feedsYaml) ?? '';
40
+ return { proxyUrl, timeoutMs, maxBodyBytes, userAgent, feedsYaml };
41
+ }
package/lib/feeds.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /** 单个订阅。 */
2
+ export interface Feed {
3
+ url: string;
4
+ name: string;
5
+ category: string;
6
+ }
7
+ /**
8
+ * 解析 feedsYaml 为订阅数组。空文本返回空数组;非法 YAML / 缺 url 抛中文错误。
9
+ */
10
+ export declare function parseFeedsYaml(text: string): Feed[];
11
+ /** 订阅数组序列化回 YAML 文本(带使用说明头)。 */
12
+ export declare function serializeFeeds(feeds: Feed[]): string;
13
+ /**
14
+ * 添加订阅:按 url(不区分大小写)去重;已存在时合并更新 name/category。
15
+ */
16
+ export declare function addFeed(feeds: Feed[], url: string, name: string, category: string): {
17
+ feeds: Feed[];
18
+ added: Feed;
19
+ existed: boolean;
20
+ };
21
+ /**
22
+ * 删除订阅:url 精确匹配(不区分大小写),或 name 匹配所有同名项(不区分大小写)。
23
+ * 两者都缺或没有匹配项时抛中文错误。
24
+ */
25
+ export declare function removeFeed(feeds: Feed[], url?: string, name?: string): {
26
+ feeds: Feed[];
27
+ removed: Feed[];
28
+ };
29
+ /** 按名字找订阅(不区分大小写)。 */
30
+ export declare function findFeedsByName(feeds: Feed[], name: string): Feed[];
package/lib/feeds.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 订阅列表存取:feedsYaml(settings 里的 YAML 文本)与 Feed 对象数组互转。
3
+ *
4
+ * @module dsh-rss/feeds
5
+ */
6
+ import YAML from 'yaml';
7
+ const FEEDS_HEADER = [
8
+ '# dsh-rss 订阅源列表',
9
+ '# 每行一个订阅:url 必填,name(显示名)与 category(分类)可选。',
10
+ '# 也可以直接写字符串:- https://example.com/feed.xml',
11
+ '',
12
+ ];
13
+ /**
14
+ * 解析 feedsYaml 为订阅数组。空文本返回空数组;非法 YAML / 缺 url 抛中文错误。
15
+ */
16
+ export function parseFeedsYaml(text) {
17
+ const trimmed = (text ?? '').trim();
18
+ if (trimmed === '')
19
+ return [];
20
+ let raw;
21
+ try {
22
+ raw = YAML.parse(trimmed);
23
+ }
24
+ catch (error) {
25
+ throw new Error('订阅配置(feedsYaml)不是合法的 YAML:' + (error instanceof Error ? error.message : String(error)) + '。请在设置页修正 dsh-rss 的订阅列表。');
26
+ }
27
+ if (raw === null || raw === undefined)
28
+ return [];
29
+ if (!Array.isArray(raw)) {
30
+ throw new Error('订阅配置(feedsYaml)必须是一个列表(每行 - url: ...)。请在设置页修正 dsh-rss 的订阅列表。');
31
+ }
32
+ const feeds = [];
33
+ for (let index = 0; index < raw.length; index++) {
34
+ const item = raw[index];
35
+ let url = '';
36
+ let name = '';
37
+ let category = '';
38
+ if (typeof item === 'string') {
39
+ url = item.trim();
40
+ }
41
+ else if (typeof item === 'object' && item !== null) {
42
+ const rec = item;
43
+ url = typeof rec.url === 'string' ? rec.url.trim() : '';
44
+ name = typeof rec.name === 'string' ? rec.name.trim() : '';
45
+ category = typeof rec.category === 'string' ? rec.category.trim() : '';
46
+ }
47
+ if (url === '') {
48
+ throw new Error('订阅配置(feedsYaml)第 ' + (index + 1) + ' 项缺少 url 字段。请在设置页修正 dsh-rss 的订阅列表。');
49
+ }
50
+ feeds.push({ url, name, category });
51
+ }
52
+ return feeds;
53
+ }
54
+ /** 订阅数组序列化回 YAML 文本(带使用说明头)。 */
55
+ export function serializeFeeds(feeds) {
56
+ const rows = [];
57
+ for (const feed of feeds) {
58
+ const rec = { url: feed.url };
59
+ if (feed.name !== '')
60
+ rec.name = feed.name;
61
+ if (feed.category !== '')
62
+ rec.category = feed.category;
63
+ rows.push(rec);
64
+ }
65
+ const body = YAML.stringify(rows).trimEnd();
66
+ return FEEDS_HEADER.join('\n') + body + '\n';
67
+ }
68
+ function sameUrl(a, b) {
69
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
70
+ }
71
+ /**
72
+ * 添加订阅:按 url(不区分大小写)去重;已存在时合并更新 name/category。
73
+ */
74
+ export function addFeed(feeds, url, name, category) {
75
+ const cleanUrl = url.trim();
76
+ const cleanName = name.trim();
77
+ const cleanCategory = category.trim();
78
+ const existing = feeds.find((feed) => sameUrl(feed.url, cleanUrl));
79
+ if (existing) {
80
+ const merged = {
81
+ url: existing.url,
82
+ name: cleanName !== '' ? cleanName : existing.name,
83
+ category: cleanCategory !== '' ? cleanCategory : existing.category,
84
+ };
85
+ const next = feeds.map((feed) => feed === existing ? merged : feed);
86
+ return { feeds: next, added: merged, existed: true };
87
+ }
88
+ const added = { url: cleanUrl, name: cleanName, category: cleanCategory };
89
+ return { feeds: [...feeds, added], added, existed: false };
90
+ }
91
+ /**
92
+ * 删除订阅:url 精确匹配(不区分大小写),或 name 匹配所有同名项(不区分大小写)。
93
+ * 两者都缺或没有匹配项时抛中文错误。
94
+ */
95
+ export function removeFeed(feeds, url, name) {
96
+ const cleanUrl = (url ?? '').trim();
97
+ const cleanName = (name ?? '').trim();
98
+ if (cleanUrl === '' && cleanName === '') {
99
+ throw new Error('删除订阅需要提供 url 或 name 参数(至少一个)。');
100
+ }
101
+ const removed = [];
102
+ const next = feeds.filter((feed) => {
103
+ const match = (cleanUrl !== '' && sameUrl(feed.url, cleanUrl)) || (cleanName !== '' && feed.name.toLowerCase() === cleanName.toLowerCase());
104
+ if (match)
105
+ removed.push(feed);
106
+ return !match;
107
+ });
108
+ if (removed.length === 0) {
109
+ const hint = cleanUrl !== '' ? '(url=' + cleanUrl + ')' : '(name=' + cleanName + ')';
110
+ throw new Error('没有找到匹配的订阅' + hint + '。可用 rss_list 查看已订阅源。');
111
+ }
112
+ return { feeds: next, removed };
113
+ }
114
+ /** 按名字找订阅(不区分大小写)。 */
115
+ export function findFeedsByName(feeds, name) {
116
+ const needle = name.trim().toLowerCase();
117
+ return feeds.filter((feed) => feed.name.trim().toLowerCase() === needle);
118
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * dsh-rss —— RSS/Atom 订阅工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):把五个面向模型的工具(rss_list / rss_add / rss_remove /
5
+ * rss_fetch / rss_check)注册进宿主进程的工具注册表,并把订阅列表接入 settings
6
+ * 命名空间(dsh-rss.feedsYaml)。配置缺失时插件照常加载,工具在 execute 时才抛出
7
+ * 带中文指引的错误。
8
+ *
9
+ * @module dsh-rss
10
+ */
11
+ import { type RssConfig } from './config.js';
12
+ import { type RssSettingsScope, type RssToolDefinition } from './tools.js';
13
+ /** cordis 服务注入:apply 里要用 ctx.settings 与 ctx.tools,必须显式声明,否则宿主会抛 cannot get property without inject。 */
14
+ export declare const inject: string[];
15
+ /** 插件所需的最小 ctx 面(社区插件不依赖宿主内部类型)。 */
16
+ export interface RssPluginContext {
17
+ settings: {
18
+ register(ns: string, schema: unknown, options?: {
19
+ base?: Record<string, unknown>;
20
+ applies?: string;
21
+ }): RssSettingsScope;
22
+ };
23
+ tools: {
24
+ register(definition: RssToolDefinition): () => void;
25
+ };
26
+ on?(event: string, listener: () => void): () => void;
27
+ }
28
+ /**
29
+ * 插件入口:解析配置、注册 settings 命名空间与五个 RSS 工具。
30
+ * @param ctx - 宿主上下文(至少含 settings.register 与 tools.register)。
31
+ * @param config - 插件配置(可缺省)。
32
+ */
33
+ export declare function apply(ctx: RssPluginContext, config?: RssConfig | null): void;
34
+ export * from './config.js';
35
+ export * from './feeds.js';
36
+ export * from './parser.js';
37
+ export * from './proxy-fetch.js';
38
+ export * from './settings.js';
39
+ export * from './tools.js';
package/lib/index.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * dsh-rss —— RSS/Atom 订阅工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):把五个面向模型的工具(rss_list / rss_add / rss_remove /
5
+ * rss_fetch / rss_check)注册进宿主进程的工具注册表,并把订阅列表接入 settings
6
+ * 命名空间(dsh-rss.feedsYaml)。配置缺失时插件照常加载,工具在 execute 时才抛出
7
+ * 带中文指引的错误。
8
+ *
9
+ * @module dsh-rss
10
+ */
11
+ import { resolveConfig } from './config.js';
12
+ import { RSS_SETTINGS_NAMESPACE, RssSettingsSchema } from './settings.js';
13
+ import { buildRssTools } from './tools.js';
14
+ /** cordis 服务注入:apply 里要用 ctx.settings 与 ctx.tools,必须显式声明,否则宿主会抛 cannot get property without inject。 */
15
+ export const inject = ['settings', 'tools'];
16
+ /**
17
+ * 插件入口:解析配置、注册 settings 命名空间与五个 RSS 工具。
18
+ * @param ctx - 宿主上下文(至少含 settings.register 与 tools.register)。
19
+ * @param config - 插件配置(可缺省)。
20
+ */
21
+ export function apply(ctx, config) {
22
+ let cfg;
23
+ try {
24
+ cfg = resolveConfig(config);
25
+ }
26
+ catch (error) {
27
+ console.warn('[dsh-rss] ' + (error instanceof Error ? error.message : String(error)));
28
+ cfg = resolveConfig(null);
29
+ }
30
+ const settingsScope = ctx.settings.register(RSS_SETTINGS_NAMESPACE, RssSettingsSchema, {
31
+ base: { feedsYaml: cfg.feedsYaml },
32
+ applies: 'live',
33
+ });
34
+ const disposers = [];
35
+ for (const definition of buildRssTools(cfg, settingsScope)) {
36
+ disposers.push(ctx.tools.register(definition));
37
+ }
38
+ if (typeof ctx.on === 'function') {
39
+ ctx.on('dispose', () => {
40
+ for (const dispose of disposers)
41
+ dispose();
42
+ });
43
+ }
44
+ }
45
+ export * from './config.js';
46
+ export * from './feeds.js';
47
+ export * from './parser.js';
48
+ export * from './proxy-fetch.js';
49
+ export * from './settings.js';
50
+ export * from './tools.js';
@@ -0,0 +1,42 @@
1
+ /** 条目附件(播客等)。 */
2
+ export interface RssEnclosure {
3
+ url: string;
4
+ type: string;
5
+ length: string;
6
+ }
7
+ /** 归一化后的条目。所有字符串字段都有值,categories 恒为数组,enclosure 缺省时省略。 */
8
+ export interface RssEntry {
9
+ title: string;
10
+ link: string;
11
+ guid: string;
12
+ pubDate: string;
13
+ pubDateRaw: string;
14
+ author: string;
15
+ summary: string;
16
+ categories: string[];
17
+ enclosure?: RssEnclosure;
18
+ }
19
+ /** 订阅源元信息。 */
20
+ export interface RssFeedMeta {
21
+ url: string;
22
+ title: string;
23
+ link: string;
24
+ description: string;
25
+ language: string;
26
+ lastBuildDate: string;
27
+ generator: string;
28
+ feedType: 'rss' | 'atom' | 'rdf';
29
+ entryCount: number;
30
+ }
31
+ /** 解析结果。 */
32
+ export interface ParsedFeed {
33
+ feed: RssFeedMeta;
34
+ entries: RssEntry[];
35
+ }
36
+ /**
37
+ * 解析 XML 文本为归一化结构。
38
+ * @param xmlText - 订阅源 XML 原文。
39
+ * @param url - 订阅源地址(用于解析相对链接)。
40
+ * @throws 内容为空 / XML 非法 / 缺少 feed 根元素时抛中文错误。
41
+ */
42
+ export declare function parseFeed(xmlText: string, url: string): ParsedFeed;