dsh-rss 0.6.0 → 0.6.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/CHANGELOG.md +4 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/CHANGELOG.en.md +4 -0
- package/docs/USAGE.en.md +4 -0
- package/docs/USAGE.md +4 -0
- package/lib/opml.js +6 -2
- package/lib/tools.js +20 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Subscribe to and read RSS or Atom feeds, and manage updates from DSH.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss)
|
|
10
10
|
|
|
11
11
|
## What it does
|
|
12
12
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
订阅和阅读 RSS / Atom,在 DSH 中查看与管理更新。
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss) [](https://www.npmjs.com/package/dsh-rss)
|
|
10
10
|
|
|
11
11
|
## 功能
|
|
12
12
|
|
package/docs/CHANGELOG.en.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
|
|
6
6
|
|
|
7
|
+
## 0.6.1 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- Consume only returned entries during incremental fetches; remaining entries stay available. Serialize cursor updates to preserve concurrent progress. Bound OPML size and nesting.
|
|
10
|
+
|
|
7
11
|
## 0.5.1 (2026-10-02)
|
|
8
12
|
|
|
9
13
|
Fixes public feeds being rejected when a VPN returns Fake-IP DNS addresses. Windows uses the enabled manual system proxy by default. For `198.18.0.0/15` answers, HTTPS DNS obtains real public addresses; connections are pinned to validated addresses while preserving the original hostname and TLS certificate verification. Keep the VPN enabled without changing its configuration or allowing private networks. Request-owned connections are released after redirects, errors or body reads.
|
package/docs/USAGE.en.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
4
|
|
|
5
|
+
## Current improvements
|
|
6
|
+
|
|
7
|
+
Repeat rss_fetch with incremental=true and a limit to consume remaining new entries; remaining reports the pending count. Each feed retains up to 5000 deduplication IDs rather than a permanent article archive. OPML is limited to 1 MiB and 64 nesting levels.
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
```bash
|
package/docs/USAGE.md
CHANGED
package/lib/opml.js
CHANGED
|
@@ -43,6 +43,8 @@ export function parseOpml(text) {
|
|
|
43
43
|
const trimmed = (text ?? '').trim();
|
|
44
44
|
if (trimmed === '')
|
|
45
45
|
throw new Error('OPML 内容为空,请提供 OPML XML 文本。');
|
|
46
|
+
if (Buffer.byteLength(trimmed, 'utf8') > 1024 * 1024)
|
|
47
|
+
throw new Error('OPML 内容超过 1 MiB,请拆分后导入。');
|
|
46
48
|
const validation = XMLValidator.validate(trimmed);
|
|
47
49
|
if (validation !== true)
|
|
48
50
|
throw new Error('OPML 解析失败:第 ' + validation.err.line + ' 行,' + validation.err.msg);
|
|
@@ -60,7 +62,9 @@ export function parseOpml(text) {
|
|
|
60
62
|
const head = asRecord(opml.head);
|
|
61
63
|
const body = asRecord(opml.body);
|
|
62
64
|
const outlines = [];
|
|
63
|
-
const walk = (node, inheritedCategory) => {
|
|
65
|
+
const walk = (node, inheritedCategory, depth = 0) => {
|
|
66
|
+
if (depth > 64)
|
|
67
|
+
throw new Error('OPML 分类嵌套超过 64 层,请简化分类后导入。');
|
|
64
68
|
const rec = asRecord(node);
|
|
65
69
|
const xmlUrl = attr(rec, '@_xmlUrl') || attr(rec, '@_xmlurl');
|
|
66
70
|
const text = attr(rec, '@_text') || attr(rec, '@_title') || firstText(rec);
|
|
@@ -77,7 +81,7 @@ export function parseOpml(text) {
|
|
|
77
81
|
const children = Array.isArray(rawChildren) ? rawChildren : (rawChildren === undefined || rawChildren === null ? [] : [rawChildren]);
|
|
78
82
|
const nextCategory = text !== '' ? text : inheritedCategory;
|
|
79
83
|
for (const child of children)
|
|
80
|
-
walk(child, nextCategory);
|
|
84
|
+
walk(child, nextCategory, depth + 1);
|
|
81
85
|
};
|
|
82
86
|
const rawBodyOutlines = body.outline;
|
|
83
87
|
const bodyOutlines = Array.isArray(rawBodyOutlines) ? rawBodyOutlines : (rawBodyOutlines === undefined || rawBodyOutlines === null ? [] : [rawBodyOutlines]);
|
package/lib/tools.js
CHANGED
|
@@ -249,7 +249,8 @@ function renderOpmlImport(_args, value) {
|
|
|
249
249
|
}
|
|
250
250
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
251
251
|
}
|
|
252
|
-
const CURSOR_GUID_LIMIT =
|
|
252
|
+
const CURSOR_GUID_LIMIT = 5000;
|
|
253
|
+
const cursorWrites = new WeakMap();
|
|
253
254
|
/** 条目唯一 id:优先 guid,退回 link,再退回 标题|发布时间。 */
|
|
254
255
|
function entryId(entry) {
|
|
255
256
|
const rec = asRecord(entry);
|
|
@@ -519,12 +520,24 @@ export function buildRssTools(config, settingsScope, fetchImpl, lookupImpl) {
|
|
|
519
520
|
const entries = parsed.entries.slice(0, limit);
|
|
520
521
|
return { url: finalUrl, feed: parsed.feed, entries, truncated: parsed.entries.length > limit };
|
|
521
522
|
}
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
523
|
+
const requestedUrl = url;
|
|
524
|
+
const task = (cursorWrites.get(settingsScope) ?? Promise.resolve()).catch(() => { }).then(async () => {
|
|
525
|
+
exec?.signal.throwIfAborted();
|
|
526
|
+
const cursors = parseCursors(readCursorsJson());
|
|
527
|
+
const fresh = filterNewEntries(cursors[requestedUrl], parsed.entries);
|
|
528
|
+
const entries = fresh.slice(0, limit);
|
|
529
|
+
cursors[requestedUrl] = advanceCursor(cursors[requestedUrl], entries, new Date().toISOString());
|
|
530
|
+
await persistCursors(cursors);
|
|
531
|
+
return { url: finalUrl, feed: parsed.feed, entries, truncated: fresh.length > limit, incremental: true, newCount: fresh.length, remaining: fresh.length - entries.length };
|
|
532
|
+
});
|
|
533
|
+
cursorWrites.set(settingsScope, task);
|
|
534
|
+
try {
|
|
535
|
+
return await task;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
if (cursorWrites.get(settingsScope) === task)
|
|
539
|
+
cursorWrites.delete(settingsScope);
|
|
540
|
+
}
|
|
528
541
|
},
|
|
529
542
|
timeoutMs: cfg.timeoutMs,
|
|
530
543
|
};
|