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 +21 -0
- package/README.md +77 -0
- package/cordis.patch.yml +20 -0
- package/lib/config.d.ts +28 -0
- package/lib/config.js +41 -0
- package/lib/feeds.d.ts +30 -0
- package/lib/feeds.js +118 -0
- package/lib/index.d.ts +39 -0
- package/lib/index.js +50 -0
- package/lib/parser.d.ts +42 -0
- package/lib/parser.js +294 -0
- package/lib/proxy-fetch.d.ts +5 -0
- package/lib/proxy-fetch.js +14 -0
- package/lib/settings.d.ts +18 -0
- package/lib/settings.js +12 -0
- package/lib/tools.d.ts +46 -0
- package/lib/tools.js +405 -0
- package/package.json +65 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 五个面向模型的 RSS 工具:list / add / remove / fetch / check。
|
|
3
|
+
* 直接调用 ctx.tools.register 注册【编译好的 JSON Schema】参数与 canonical 输出。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-rss/tools
|
|
6
|
+
*/
|
|
7
|
+
import { addFeed, findFeedsByName, parseFeedsYaml, removeFeed, serializeFeeds } from './feeds.js';
|
|
8
|
+
import { parseFeed } from './parser.js';
|
|
9
|
+
import { createProxyFetch } from './proxy-fetch.js';
|
|
10
|
+
const TIMEOUT_MS = 30000;
|
|
11
|
+
const PROXY_HINT = '。若该订阅源需要特殊代理(梯子)才能访问,请在 cordis.patch.yml 里给 dsh-rss 配置 proxyUrl(如 http://127.0.0.1:7890)后重启。';
|
|
12
|
+
/**
|
|
13
|
+
* 编译作者 DSL 为原始 JSON Schema(正是 defineTool 存为 definition.parameters 的值)。
|
|
14
|
+
* 原生线路会原样下发该值,原始 DSL 会被模型 API 拒绝(schema must be a JSON Schema)。
|
|
15
|
+
*/
|
|
16
|
+
function compileParameters(spec) {
|
|
17
|
+
const properties = {};
|
|
18
|
+
const required = [];
|
|
19
|
+
for (const [key, prop] of Object.entries(spec)) {
|
|
20
|
+
if (prop?.required === true)
|
|
21
|
+
required.push(key);
|
|
22
|
+
const node = {};
|
|
23
|
+
if (typeof prop?.type === 'string')
|
|
24
|
+
node.type = prop.type;
|
|
25
|
+
if (typeof prop?.description === 'string')
|
|
26
|
+
node.description = prop.description;
|
|
27
|
+
if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
|
|
28
|
+
const items = { type: 'string' };
|
|
29
|
+
if (prop.items.type === 'object')
|
|
30
|
+
items.additionalProperties = true;
|
|
31
|
+
node.items = items;
|
|
32
|
+
}
|
|
33
|
+
properties[key] = node;
|
|
34
|
+
}
|
|
35
|
+
return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
|
|
36
|
+
}
|
|
37
|
+
function asRecord(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
39
|
+
}
|
|
40
|
+
function optionalString(args, key) {
|
|
41
|
+
const value = args[key];
|
|
42
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
43
|
+
}
|
|
44
|
+
function requiredString(args, key, label) {
|
|
45
|
+
const value = optionalString(args, key);
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function clampedInteger(args, key, fallback, min, max) {
|
|
52
|
+
const value = args[key];
|
|
53
|
+
if (typeof value !== 'number' || !Number.isInteger(value))
|
|
54
|
+
return fallback;
|
|
55
|
+
return Math.min(max, Math.max(min, value));
|
|
56
|
+
}
|
|
57
|
+
function assertHttpUrl(url) {
|
|
58
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
59
|
+
throw new Error('订阅源地址必须是 http(s):// 开头的完整地址,例如 https://example.com/feed.xml。');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** 构造默认 fetch:配置了 proxyUrl 时走插件级代理。 */
|
|
63
|
+
function makeFetch(cfg) {
|
|
64
|
+
if (cfg.proxyUrl === '')
|
|
65
|
+
return globalThis.fetch;
|
|
66
|
+
return createProxyFetch(cfg.proxyUrl);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 抓取订阅源 XML:校验地址、限时、限制体积、剥 BOM。
|
|
70
|
+
*/
|
|
71
|
+
async function fetchFeedXml(url, cfg, fetchImpl) {
|
|
72
|
+
assertHttpUrl(url);
|
|
73
|
+
const fetcher = fetchImpl ?? makeFetch(cfg);
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetcher(url, {
|
|
77
|
+
headers: {
|
|
78
|
+
'user-agent': cfg.userAgent,
|
|
79
|
+
accept: 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*',
|
|
80
|
+
},
|
|
81
|
+
redirect: 'follow',
|
|
82
|
+
signal: AbortSignal.timeout(cfg.timeoutMs),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error('抓取失败:' + (error instanceof Error ? error.message : String(error)) + PROXY_HINT);
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error('抓取失败:服务器返回 HTTP ' + response.status + '。');
|
|
90
|
+
}
|
|
91
|
+
let bytes;
|
|
92
|
+
try {
|
|
93
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new Error('读取订阅源内容失败:' + (error instanceof Error ? error.message : String(error)));
|
|
97
|
+
}
|
|
98
|
+
if (bytes.length > cfg.maxBodyBytes) {
|
|
99
|
+
throw new Error('订阅源内容超过 ' + cfg.maxBodyBytes + ' 字节上限,已停止读取。');
|
|
100
|
+
}
|
|
101
|
+
let xml = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
|
|
102
|
+
xml = xml.replace(/^\uFEFF/, '');
|
|
103
|
+
return { xml, finalUrl: response.url || url };
|
|
104
|
+
}
|
|
105
|
+
// ---------- 输出 JSON Schema ----------
|
|
106
|
+
const feedItemSchema = {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
url: { type: 'string' },
|
|
110
|
+
name: { type: 'string' },
|
|
111
|
+
category: { type: 'string' },
|
|
112
|
+
},
|
|
113
|
+
additionalProperties: true,
|
|
114
|
+
};
|
|
115
|
+
const entrySchema = {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
title: { type: 'string' },
|
|
119
|
+
link: { type: 'string' },
|
|
120
|
+
guid: { type: 'string' },
|
|
121
|
+
pubDate: { type: 'string' },
|
|
122
|
+
pubDateRaw: { type: 'string' },
|
|
123
|
+
author: { type: 'string' },
|
|
124
|
+
summary: { type: 'string' },
|
|
125
|
+
categories: { type: 'array', items: { type: 'string' } },
|
|
126
|
+
enclosure: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
url: { type: 'string' },
|
|
130
|
+
type: { type: 'string' },
|
|
131
|
+
length: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
additionalProperties: true,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
additionalProperties: true,
|
|
137
|
+
};
|
|
138
|
+
const feedMetaSchema = {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
url: { type: 'string' },
|
|
142
|
+
title: { type: 'string' },
|
|
143
|
+
link: { type: 'string' },
|
|
144
|
+
description: { type: 'string' },
|
|
145
|
+
language: { type: 'string' },
|
|
146
|
+
lastBuildDate: { type: 'string' },
|
|
147
|
+
generator: { type: 'string' },
|
|
148
|
+
feedType: { type: 'string' },
|
|
149
|
+
entryCount: { type: 'integer' },
|
|
150
|
+
},
|
|
151
|
+
additionalProperties: true,
|
|
152
|
+
};
|
|
153
|
+
const listSchema = {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
count: { type: 'integer' },
|
|
157
|
+
feeds: { type: 'array', items: feedItemSchema },
|
|
158
|
+
},
|
|
159
|
+
additionalProperties: true,
|
|
160
|
+
};
|
|
161
|
+
const addSchema = {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
added: feedItemSchema,
|
|
165
|
+
existed: { type: 'boolean' },
|
|
166
|
+
count: { type: 'integer' },
|
|
167
|
+
feedTitle: { type: 'string' },
|
|
168
|
+
feedType: { type: 'string' },
|
|
169
|
+
},
|
|
170
|
+
additionalProperties: true,
|
|
171
|
+
};
|
|
172
|
+
const removeSchema = {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
removed: { type: 'array', items: feedItemSchema },
|
|
176
|
+
count: { type: 'integer' },
|
|
177
|
+
},
|
|
178
|
+
additionalProperties: true,
|
|
179
|
+
};
|
|
180
|
+
const fetchSchema = {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
url: { type: 'string' },
|
|
184
|
+
feed: feedMetaSchema,
|
|
185
|
+
entries: { type: 'array', items: entrySchema },
|
|
186
|
+
truncated: { type: 'boolean' },
|
|
187
|
+
},
|
|
188
|
+
additionalProperties: true,
|
|
189
|
+
};
|
|
190
|
+
const checkSchema = {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
ok: { type: 'boolean' },
|
|
194
|
+
url: { type: 'string' },
|
|
195
|
+
title: { type: 'string' },
|
|
196
|
+
feedType: { type: 'string' },
|
|
197
|
+
entryCount: { type: 'integer' },
|
|
198
|
+
description: { type: 'string' },
|
|
199
|
+
},
|
|
200
|
+
additionalProperties: true,
|
|
201
|
+
};
|
|
202
|
+
// ---------- render 帮助函数 ----------
|
|
203
|
+
function feedLabel(feed) {
|
|
204
|
+
const rec = asRecord(feed);
|
|
205
|
+
const url = typeof rec.url === 'string' ? rec.url : '';
|
|
206
|
+
const name = typeof rec.name === 'string' && rec.name !== '' ? rec.name : '';
|
|
207
|
+
const category = typeof rec.category === 'string' && rec.category !== '' ? ',分类:' + rec.category : '';
|
|
208
|
+
return (name !== '' ? name + '(' + url + ')' : url) + category;
|
|
209
|
+
}
|
|
210
|
+
function renderList(_args, value) {
|
|
211
|
+
const rec = asRecord(value);
|
|
212
|
+
const feeds = Array.isArray(rec.feeds) ? rec.feeds : [];
|
|
213
|
+
const lines = ['共订阅 ' + feeds.length + ' 个订阅源:'];
|
|
214
|
+
for (const feed of feeds)
|
|
215
|
+
lines.push('- ' + feedLabel(feed));
|
|
216
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
217
|
+
}
|
|
218
|
+
function renderAdd(_args, value) {
|
|
219
|
+
const rec = asRecord(value);
|
|
220
|
+
const existed = rec.existed === true;
|
|
221
|
+
const text = existed
|
|
222
|
+
? '该订阅已存在,已更新其信息:' + feedLabel(rec.added) + '。当前共 ' + String(rec.count) + ' 个订阅源。'
|
|
223
|
+
: '已添加订阅:' + feedLabel(rec.added) + '。当前共 ' + String(rec.count) + ' 个订阅源。';
|
|
224
|
+
return [{ type: 'text', text }];
|
|
225
|
+
}
|
|
226
|
+
function renderRemove(_args, value) {
|
|
227
|
+
const rec = asRecord(value);
|
|
228
|
+
const removed = Array.isArray(rec.removed) ? rec.removed : [];
|
|
229
|
+
const lines = ['已移除 ' + removed.length + ' 个订阅:'];
|
|
230
|
+
for (const feed of removed)
|
|
231
|
+
lines.push('- ' + feedLabel(feed));
|
|
232
|
+
lines.push('剩余 ' + String(rec.count) + ' 个订阅源。');
|
|
233
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
234
|
+
}
|
|
235
|
+
function renderFetch(_args, value) {
|
|
236
|
+
const rec = asRecord(value);
|
|
237
|
+
const feed = asRecord(rec.feed);
|
|
238
|
+
const entries = Array.isArray(rec.entries) ? rec.entries : [];
|
|
239
|
+
const title = typeof feed.title === 'string' && feed.title !== '' ? feed.title : (typeof rec.url === 'string' ? rec.url : '');
|
|
240
|
+
const feedType = typeof feed.feedType === 'string' ? feed.feedType : '';
|
|
241
|
+
const lines = ['订阅源 ' + title + '(' + feedType + '):共 ' + String(feed.entryCount) + ' 条,本次返回 ' + entries.length + ' 条。'];
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const item = asRecord(entry);
|
|
244
|
+
const entryTitle = typeof item.title === 'string' && item.title !== '' ? item.title : '(无标题)';
|
|
245
|
+
const pubDate = typeof item.pubDate === 'string' && item.pubDate !== '' ? '(' + item.pubDate + ')' : '';
|
|
246
|
+
const link = typeof item.link === 'string' && item.link !== '' ? ' ' + item.link : '';
|
|
247
|
+
lines.push('- ' + entryTitle + pubDate + link);
|
|
248
|
+
}
|
|
249
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
250
|
+
}
|
|
251
|
+
function renderCheck(_args, value) {
|
|
252
|
+
const rec = asRecord(value);
|
|
253
|
+
const title = typeof rec.title === 'string' && rec.title !== '' ? rec.title : (typeof rec.url === 'string' ? rec.url : '');
|
|
254
|
+
const text = '订阅源可用:' + title + '(' + String(rec.feedType) + '),共 ' + String(rec.entryCount) + ' 条。';
|
|
255
|
+
return [{ type: 'text', text }];
|
|
256
|
+
}
|
|
257
|
+
// ---------- 工具构建 ----------
|
|
258
|
+
/**
|
|
259
|
+
* 构建五个工具定义。每个 execute 惰性读取配置与订阅,错误时抛出中文指引。
|
|
260
|
+
* @param config - 已解析配置。
|
|
261
|
+
* @param settingsScope - dsh-rss settings scope(get/update)。
|
|
262
|
+
* @param fetchImpl - 可选注入的 fetch(测试用),缺省按 proxyUrl 构造。
|
|
263
|
+
*/
|
|
264
|
+
export function buildRssTools(config, settingsScope, fetchImpl) {
|
|
265
|
+
const cfg = config;
|
|
266
|
+
const getFeeds = () => {
|
|
267
|
+
const value = settingsScope.get();
|
|
268
|
+
const yamlText = value !== null && typeof value === 'object' && typeof value.feedsYaml === 'string' ? value.feedsYaml : '';
|
|
269
|
+
return parseFeedsYaml(yamlText);
|
|
270
|
+
};
|
|
271
|
+
const persistFeeds = async (feeds) => {
|
|
272
|
+
try {
|
|
273
|
+
await settingsScope.update({ feedsYaml: serializeFeeds(feeds) });
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
throw new Error('写入订阅配置失败(可能已被其他会话修改,请重试):' + (error instanceof Error ? error.message : String(error)));
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const rssList = {
|
|
280
|
+
name: 'rss_list',
|
|
281
|
+
description: '列出已订阅的 RSS/Atom 源(存在 settings 的 dsh-rss.feedsYaml 里,可用 rss_add / rss_remove 管理)。返回每个订阅的 url、name、category 与总数。',
|
|
282
|
+
parameters: compileParameters({}),
|
|
283
|
+
output: {
|
|
284
|
+
schema: listSchema,
|
|
285
|
+
render: renderList,
|
|
286
|
+
},
|
|
287
|
+
async execute(rawArgs) {
|
|
288
|
+
const feeds = getFeeds();
|
|
289
|
+
return { count: feeds.length, feeds };
|
|
290
|
+
},
|
|
291
|
+
timeoutMs: TIMEOUT_MS,
|
|
292
|
+
};
|
|
293
|
+
const rssAdd = {
|
|
294
|
+
name: 'rss_add',
|
|
295
|
+
description: '添加一个 RSS/Atom 订阅源(会先抓取校验该地址是否可用)。url 必填;name 缺省时用订阅源自身的标题;category 可选。同一 url 重复添加时只更新 name/category。添加结果持久化到 settings,重启后仍在。',
|
|
296
|
+
parameters: compileParameters({
|
|
297
|
+
url: { type: 'string', required: true, description: '订阅源地址(http(s):// 开头,必填)。' },
|
|
298
|
+
name: { type: 'string', description: '显示名(可选,缺省用订阅源标题)。' },
|
|
299
|
+
category: { type: 'string', description: '分类标签(可选)。' },
|
|
300
|
+
}),
|
|
301
|
+
output: {
|
|
302
|
+
schema: addSchema,
|
|
303
|
+
render: renderAdd,
|
|
304
|
+
},
|
|
305
|
+
async execute(rawArgs) {
|
|
306
|
+
const args = asRecord(rawArgs);
|
|
307
|
+
const url = requiredString(args, 'url', '订阅源地址');
|
|
308
|
+
assertHttpUrl(url);
|
|
309
|
+
const name = optionalString(args, 'name') ?? '';
|
|
310
|
+
const category = optionalString(args, 'category') ?? '';
|
|
311
|
+
const { xml, finalUrl } = await fetchFeedXml(url, cfg, fetchImpl);
|
|
312
|
+
const parsed = parseFeed(xml, finalUrl);
|
|
313
|
+
const effectiveName = name !== '' ? name : parsed.feed.title;
|
|
314
|
+
const { feeds, added, existed } = addFeed(getFeeds(), url, effectiveName, category);
|
|
315
|
+
await persistFeeds(feeds);
|
|
316
|
+
return { added, existed, count: feeds.length, feedTitle: parsed.feed.title, feedType: parsed.feed.feedType };
|
|
317
|
+
},
|
|
318
|
+
timeoutMs: TIMEOUT_MS,
|
|
319
|
+
};
|
|
320
|
+
const rssRemove = {
|
|
321
|
+
name: 'rss_remove',
|
|
322
|
+
description: '删除已订阅的 RSS/Atom 源。url 与 name 至少给一个:url 精确匹配(不区分大小写),name 匹配所有同名订阅。删除结果持久化到 settings。',
|
|
323
|
+
parameters: compileParameters({
|
|
324
|
+
url: { type: 'string', description: '要删除的订阅源地址(可选,与 name 至少给一个)。' },
|
|
325
|
+
name: { type: 'string', description: '要删除的订阅显示名(可选,与 url 至少给一个;同名全部删除)。' },
|
|
326
|
+
}),
|
|
327
|
+
output: {
|
|
328
|
+
schema: removeSchema,
|
|
329
|
+
render: renderRemove,
|
|
330
|
+
},
|
|
331
|
+
async execute(rawArgs) {
|
|
332
|
+
const args = asRecord(rawArgs);
|
|
333
|
+
const { feeds, removed } = removeFeed(getFeeds(), optionalString(args, 'url'), optionalString(args, 'name'));
|
|
334
|
+
await persistFeeds(feeds);
|
|
335
|
+
return { removed, count: feeds.length };
|
|
336
|
+
},
|
|
337
|
+
timeoutMs: TIMEOUT_MS,
|
|
338
|
+
};
|
|
339
|
+
const rssFetch = {
|
|
340
|
+
name: 'rss_fetch',
|
|
341
|
+
description: '抓取并解析一个 RSS/Atom 订阅源,返回源信息与最新条目(标题、链接、guid、发布时间、作者、摘要、分类、附件)。url 与 name 至少给一个:name 查已订阅源(同名多个时需改用 url),url 直接抓取任意地址。limit 控制返回条数(1-100,默认 20),超过部分以 truncated=true 提示。',
|
|
342
|
+
parameters: compileParameters({
|
|
343
|
+
url: { type: 'string', description: '订阅源地址(可选,与 name 至少给一个)。' },
|
|
344
|
+
name: { type: 'string', description: '已订阅源的名字(可选,可用 rss_list 查看)。' },
|
|
345
|
+
limit: { type: 'integer', description: '返回条数,1-100,默认 20。' },
|
|
346
|
+
}),
|
|
347
|
+
output: {
|
|
348
|
+
schema: fetchSchema,
|
|
349
|
+
render: renderFetch,
|
|
350
|
+
},
|
|
351
|
+
async execute(rawArgs) {
|
|
352
|
+
const args = asRecord(rawArgs);
|
|
353
|
+
let url = optionalString(args, 'url');
|
|
354
|
+
const name = optionalString(args, 'name');
|
|
355
|
+
if (url === undefined) {
|
|
356
|
+
if (name === undefined) {
|
|
357
|
+
throw new Error('rss_fetch 需要 url 参数(订阅源地址),或 name 参数(已订阅源的名字)。');
|
|
358
|
+
}
|
|
359
|
+
const matches = findFeedsByName(getFeeds(), name);
|
|
360
|
+
if (matches.length === 0) {
|
|
361
|
+
throw new Error('没有找到名为 ' + name + ' 的订阅。可用 rss_list 查看已订阅源,或直接给 url 参数。');
|
|
362
|
+
}
|
|
363
|
+
if (matches.length > 1) {
|
|
364
|
+
throw new Error('有 ' + matches.length + ' 个同名订阅,请改用 url 参数指定。');
|
|
365
|
+
}
|
|
366
|
+
url = matches[0].url;
|
|
367
|
+
}
|
|
368
|
+
assertHttpUrl(url);
|
|
369
|
+
const limit = clampedInteger(args, 'limit', 20, 1, 100);
|
|
370
|
+
const { xml, finalUrl } = await fetchFeedXml(url, cfg, fetchImpl);
|
|
371
|
+
const parsed = parseFeed(xml, finalUrl);
|
|
372
|
+
const entries = parsed.entries.slice(0, limit);
|
|
373
|
+
return { url: finalUrl, feed: parsed.feed, entries, truncated: parsed.entries.length > limit };
|
|
374
|
+
},
|
|
375
|
+
timeoutMs: TIMEOUT_MS,
|
|
376
|
+
};
|
|
377
|
+
const rssCheck = {
|
|
378
|
+
name: 'rss_check',
|
|
379
|
+
description: '校验一个地址是否为可解析的 RSS/Atom 订阅源(不修改订阅列表)。返回 ok、源标题、类型与条目数,供添加订阅前检查。',
|
|
380
|
+
parameters: compileParameters({
|
|
381
|
+
url: { type: 'string', required: true, description: '要校验的订阅源地址(必填)。' },
|
|
382
|
+
}),
|
|
383
|
+
output: {
|
|
384
|
+
schema: checkSchema,
|
|
385
|
+
render: renderCheck,
|
|
386
|
+
},
|
|
387
|
+
async execute(rawArgs) {
|
|
388
|
+
const args = asRecord(rawArgs);
|
|
389
|
+
const url = requiredString(args, 'url', '订阅源地址');
|
|
390
|
+
assertHttpUrl(url);
|
|
391
|
+
const { xml, finalUrl } = await fetchFeedXml(url, cfg, fetchImpl);
|
|
392
|
+
const parsed = parseFeed(xml, finalUrl);
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
url: finalUrl,
|
|
396
|
+
title: parsed.feed.title,
|
|
397
|
+
feedType: parsed.feed.feedType,
|
|
398
|
+
entryCount: parsed.entries.length,
|
|
399
|
+
description: parsed.feed.description,
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
timeoutMs: TIMEOUT_MS,
|
|
403
|
+
};
|
|
404
|
+
return [rssList, rssAdd, rssRemove, rssFetch, rssCheck];
|
|
405
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-rss",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH RSS 订阅工具插件:管理订阅源并抓取解析 RSS/Atom(rss_list / rss_add / rss_remove / rss_fetch / rss_check)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"prepare": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
26
|
+
"prepublishOnly": "pnpm run build"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"dsh",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"plugin",
|
|
37
|
+
"rss",
|
|
38
|
+
"atom",
|
|
39
|
+
"feed"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"fast-xml-parser": "^5.0.0",
|
|
47
|
+
"schemastery": "^3.18.0",
|
|
48
|
+
"undici": "^8.10.0",
|
|
49
|
+
"yaml": "^2.9.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^24.0.0",
|
|
53
|
+
"typescript": "^5.6.0"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/STARDUSTLC666/dsh-rss"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/STARDUSTLC666/dsh-rss/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-rss#readme",
|
|
63
|
+
"author": "stardustlc",
|
|
64
|
+
"packageManager": "pnpm@11.7.0"
|
|
65
|
+
}
|