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 CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.6.1 (2026-10-05)
8
+
9
+ - 增量抓取只消费本次返回的条目,剩余条目可继续分页;并发抓取串行合并游标,避免覆盖其他订阅进度。OPML 限制大小与嵌套深度。
10
+
7
11
  ## 0.6.0 (2026-10-04)
8
12
 
9
13
  - 新增推荐订阅目录:16 个网站原生 RSS/Atom,提供五类主题、关键词与内容语言筛选、近期文章预览、可用性检查和「已订阅」状态。浏览目录不会自动订阅或抓取全部来源。
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
- [![npm](https://img.shields.io/npm/v/dsh-rss)](https://www.npmjs.com/package/dsh-rss) [![downloads](https://img.shields.io/npm/dm/dsh-rss)](https://www.npmjs.com/package/dsh-rss)
9
+ [![npm](https://img.shields.io/npm/v/dsh-rss)](https://www.npmjs.com/package/dsh-rss) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-rss-downloads.svg)](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
- [![npm](https://img.shields.io/npm/v/dsh-rss)](https://www.npmjs.com/package/dsh-rss) [![downloads](https://img.shields.io/npm/dm/dsh-rss)](https://www.npmjs.com/package/dsh-rss)
9
+ [![npm](https://img.shields.io/npm/v/dsh-rss)](https://www.npmjs.com/package/dsh-rss) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-rss-downloads.svg)](https://www.npmjs.com/package/dsh-rss)
10
10
 
11
11
  ## 功能
12
12
 
@@ -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
@@ -2,6 +2,10 @@
2
2
 
3
3
  [返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
4
4
 
5
+ ## 本次改进
6
+
7
+ rss_fetch { incremental: true, limit: 20 } 可重复调用取得余下新条目;remaining 表示本次剩余数量。每源最多保留 5000 个去重 ID,不是永久文章归档。OPML 文本最多 1 MiB、嵌套最多 64 层。
8
+
5
9
  ## 安装
6
10
 
7
11
  ```bash
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 = 200;
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 cursors = parseCursors(readCursorsJson());
523
- const fresh = filterNewEntries(cursors[url], parsed.entries);
524
- cursors[url] = advanceCursor(cursors[url], parsed.entries, new Date().toISOString());
525
- await persistCursors(cursors);
526
- const entries = fresh.slice(0, limit);
527
- return { url: finalUrl, feed: parsed.feed, entries, truncated: fresh.length > limit, incremental: true, newCount: fresh.length };
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rss",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "订阅和阅读 RSS / Atom,在 DSH 中查看与管理更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",