dsh-rss 0.6.1 → 0.6.3

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
@@ -1,5 +1,15 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.6.3 (2026-10-17)
4
+
5
+ - 修复英文窄窗口中 RSS 标题截断;设置导航改为顶部换行排列,给订阅列表保留可读宽度。样式仅在 RSS 面板打开时生效。
6
+ - 在官方 DSH 0.2.1-alpha.5 中检查来源目录、分类、OPML 预览导入与导出、编辑订阅和中英文布局。
7
+
8
+ ## 0.6.2 (2026-10-15)
9
+
10
+ - 加固直连、HTTP CONNECT 与 TLS 握手的套接字错误隔离,抓取结束会销毁本次请求仍持有的连接,避免错误在请求结束后逃逸。
11
+ - 更新 Undici 至 8.12.1;新增独立进程的直连失败、代理 TLS 失败和超时取消回归检查。
12
+
3
13
  [返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
4
14
 
5
15
  [历史英文记录](docs/CHANGELOG.en.md)
package/README.en.md CHANGED
@@ -8,6 +8,11 @@ Subscribe to and read RSS or Atom feeds, and manage updates from DSH.
8
8
 
9
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
+ Feedback and contributions are welcome: report [issues](https://github.com/STARDUSTLC666/dsh-rss/issues) or submit [pull requests](https://github.com/STARDUSTLC666/dsh-rss/pulls).
12
+
13
+
14
+ See the [0.6.3 validation record](docs/validation-0.6.3.md) for DSH `0.2.1-alpha.5` checks and the narrow-window layout fix.
15
+
11
16
  ## What it does
12
17
 
13
18
  - Add feeds, read articles and fetch updates.
package/README.md CHANGED
@@ -8,6 +8,11 @@
8
8
 
9
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
+ 欢迎使用,遇到问题或有改进建议,请提交 [issues](https://github.com/STARDUSTLC666/dsh-rss/issues) 和 [PR](https://github.com/STARDUSTLC666/dsh-rss/pulls)。
12
+
13
+
14
+ 本轮 DSH `0.2.1-alpha.5` 操作与窄窗口修复见 [0.6.3 验收记录](docs/validation-0.6.3.md)。
15
+
11
16
  ## 功能
12
17
 
13
18
  - 添加订阅、查看文章和抓取更新。
@@ -1,5 +1,7 @@
1
1
  # dsh-rss 验证记录
2
2
 
3
+ [0.6.3 · DSH 0.2.1-alpha.5 与窄窗口验收](validation-0.6.3.md)。
4
+
3
5
  [0.6.0 推荐订阅源验收](validation-0.6.0.md)。
4
6
 
5
7
  [0.5.2 中英文界面验收](validation-language-2026-10-03.md)。
@@ -0,0 +1,10 @@
1
+ # 0.6.3 · DSH alpha.5 网页验收
2
+
3
+ 宿主为公开 npm `@deepseek-ai/dsh@0.2.1-alpha.5`,Windows Node 24,系统 Chrome 无头浏览器,使用隔离 profile。
4
+
5
+ - 119 项自动测试通过。
6
+ - 实际打开推荐来源目录并搜索,预览嵌套 OPML 分类后明确导入;编辑名称和导出文件均读取同一份订阅配置。导入不会自动抓取全部来源。
7
+ - 英文标题曾在 390px 窗口的双列设置布局中被截断;修复后 RSS 所在设置页改用顶部换行导航,正文占用完整可用列,标题、按钮和下拉框可读。关闭 RSS 面板后该导航样式不影响其他设置页。
8
+ - 中文与英文 UI 操作通过,原始来源名称和用户分类保持原文;错误与空列表有明确提示。
9
+
10
+ 本轮不重测真实外部网站,也不声称没有 VPN 的网络验收通过;网络可用性仍由用户的实际连接决定。原生桌面窗口未操作。
package/lib/client.js CHANGED
@@ -50,14 +50,14 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
50
50
  }
51
51
 
52
52
  const CSS = `
53
- .dshr{color:var(--dsw-alias-label-primary,#202737);font-size:14px;line-height:1.55;max-width:920px;min-width:0}
53
+ .dshr{color:var(--dsw-alias-label-primary,#202737);font-size:14px;line-height:1.55;max-width:920px;min-width:0;overflow-wrap:anywhere}
54
54
  .dshr *,.dshr-dialog *{box-sizing:border-box}.dshr h2{font-size:22px;margin:0 0 6px}.dshr p{margin:0 0 12px}
55
55
  .dshr-muted{color:var(--dsw-alias-label-tertiary,#657083);font-size:13px}.dshr-toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
56
56
  .dshr button,.dshr-dialog button{font:inherit;min-height:38px;padding:7px 12px;border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:9px;color:inherit;background:var(--dsw-alias-bg-layer-1,#fff);cursor:pointer}
57
57
  .dshr button:disabled,.dshr-dialog button:disabled{opacity:.5;cursor:not-allowed}.dshr button:hover:not(:disabled),.dshr-dialog button:hover:not(:disabled){border-color:#5685d4}
58
58
  .dshr .primary,.dshr-dialog .primary{background:#366fd2;color:#fff;border-color:#366fd2}.dshr-dialog .danger{background:#b93838;border-color:#b93838;color:#fff}
59
59
  .dshr button:focus-visible,.dshr-dialog button:focus-visible,.dshr input:focus-visible,.dshr select:focus-visible,.dshr-dialog input:focus-visible,.dshr-dialog textarea:focus-visible{outline:2px solid #5685d4;outline-offset:2px}
60
- .dshr-filters{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.dshr-filters label:first-child{flex:1 1 220px}.dshr label,.dshr-dialog label{display:flex;flex-direction:column;gap:5px;font-size:13px}
60
+ .dshr-filters{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.dshr-filters label:first-child{flex:1 1 220px}.dshr label,.dshr-dialog label{display:flex;flex-direction:column;gap:5px;font-size:13px;min-width:0;max-width:100%}
61
61
  .dshr input,.dshr select,.dshr-dialog input,.dshr-dialog textarea{font:inherit;font-size:16px;width:100%;min-width:0;min-height:40px;border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit}
62
62
  .dshr-list{display:grid;gap:10px;margin:0;padding:0;list-style:none}.dshr-card{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:12px;padding:14px;display:flex;flex-wrap:wrap;gap:12px}
63
63
  .dshr-info{flex:1 1 240px;min-width:0}.dshr-info h3{font-size:15px;margin:0 0 4px;overflow-wrap:anywhere}.dshr-url{font-size:12px;overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary,#657083)}
@@ -72,6 +72,13 @@ window.__ModuleLoader__.load({ id: 'dsh-rss', factory: require => {
72
72
  .dshr-catalog{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;list-style:none;padding:0;margin:14px 0}.dshr-source{border:1px solid var(--dsw-alias-border-l2,#d9dee7);border-radius:12px;padding:14px;min-width:0;align-self:start}.dshr-source h4{font-size:16px;margin:0 0 6px;overflow-wrap:anywhere}.dshr-source p{margin:8px 0}.dshr-source .dshr-actions{margin-top:12px}.dshr-source.is-selected{border-color:#5685d4}.dshr-source details{margin-top:8px;font-size:12px}.dshr-source summary{cursor:pointer}.dshr-source .dshr-preview{margin-top:12px;font-size:13px}
73
73
  @media(max-width:640px){.dshr-catalog{grid-template-columns:1fr}.dshr-dialog .dshr-filters label{flex-basis:100%}}
74
74
  @media(max-width:520px){.dshr-dialog{padding:16px}.dshr-actions{width:100%}.dshr-filters label{width:100%}.dshr-toolbar button{flex:1 1 auto}.dshr-card{padding:12px}}
75
+ @media(max-width:640px){
76
+ [role=dialog]:has(.dshr){width:100%!important;max-width:100%!important;flex-direction:column!important}
77
+ [role=dialog]:has(.dshr)>nav+div{min-width:0;width:100%}
78
+ [role=dialog]:has(.dshr)>nav+div>div:has(.dshr){padding:12px!important;min-width:0}
79
+ [role=dialog]:has(.dshr)>nav{width:100%!important;flex-basis:auto!important;border-right:0;max-height:170px;overflow:auto}
80
+ [role=dialog]:has(.dshr)>nav>div:has(>button){display:flex;flex-direction:row!important;flex-wrap:wrap;gap:4px}
81
+ }
75
82
  `;
76
83
  async function request(action, args, signal) {
77
84
  const response = await fetch(new URL(ROUTE, document.baseURI), action ? {
@@ -1,14 +1,39 @@
1
1
  /**
2
2
  * 插件级 HTTP 代理:给 RSS 抓取请求一个走指定代理的 fetch,不影响同进程其他插件。
3
3
  */
4
- import { Agent, Client, ProxyAgent, fetch as undiciFetch } from 'undici';
4
+ import { Agent, Client, ProxyAgent, buildConnector, fetch as undiciFetch } from 'undici';
5
5
  import { isIP } from 'node:net';
6
6
  /** One request/redirect hop owns its dispatcher until its body has been read. */
7
7
  export function createFeedTransport(url, proxyUrl, addresses) {
8
8
  const hostname = url.hostname.replace(/^\[|\]$/g, '');
9
9
  const servername = isIP(hostname) === 0 ? hostname : undefined;
10
+ const sockets = new Set();
11
+ const onLateError = () => { };
12
+ function watch(socket) {
13
+ // CONNECT detaches the HTTP client's listeners before handing over its socket.
14
+ // Reattach after handover as well as immediately after socket construction.
15
+ if (!socket.listeners('error').includes(onLateError))
16
+ socket.on('error', onLateError);
17
+ if (!sockets.has(socket)) {
18
+ sockets.add(socket);
19
+ socket.once('close', () => sockets.delete(socket));
20
+ }
21
+ return socket;
22
+ }
23
+ function guarded(connect) {
24
+ return (options, callback) => {
25
+ const socket = connect(options, (error, connected) => {
26
+ if (error !== null)
27
+ callback(error, null);
28
+ else
29
+ callback(null, watch(connected));
30
+ });
31
+ if (socket !== undefined)
32
+ watch(socket);
33
+ };
34
+ }
10
35
  const dispatcher = proxyUrl === ''
11
- ? new Agent({ connect: addresses === undefined ? {} : {
36
+ ? new Agent({ connect: guarded(buildConnector(addresses === undefined ? {} : {
12
37
  lookup: (name, options, callback) => {
13
38
  if (name !== hostname) {
14
39
  callback(new Error('Unexpected RSS connection hostname'), '', 4);
@@ -24,23 +49,47 @@ export function createFeedTransport(url, proxyUrl, addresses) {
24
49
  else
25
50
  callback(null, candidates[0].address, candidates[0].family);
26
51
  },
27
- } })
52
+ })) })
28
53
  : new ProxyAgent({ uri: proxyUrl, proxyTunnel: true, requestTls: { servername },
54
+ clientFactory: (origin, rawOptions) => {
55
+ const options = rawOptions;
56
+ const connect = typeof options.connect === 'function' ? options.connect : buildConnector(options.connect ?? {});
57
+ return new Client(origin, { ...options, connect: guarded(connect) });
58
+ },
29
59
  factory: (origin, options) => {
30
60
  const delegate = options.connect;
31
- if (addresses === undefined || typeof delegate !== 'function')
32
- return new Client(origin, options);
61
+ if (typeof delegate !== 'function')
62
+ throw new Error('RSS proxy did not supply a tunnel connector');
63
+ const endpoint = guarded(buildConnector({ servername }));
33
64
  return new Client(origin, { ...options, connect: (socketOptions, callback) => {
34
- const address = addresses[0].address;
65
+ const address = addresses?.[0]?.address ?? hostname;
35
66
  const port = socketOptions.port || (url.protocol === 'https:' ? '443' : '80');
36
67
  const host = (isIP(address) === 6 ? '[' + address + ']' : address) + ':' + port;
37
- delegate({ ...socketOptions, hostname: address, host, port, servername }, callback);
68
+ // Ask ProxyAgent for a raw CONNECT tunnel, then own the TLS socket too.
69
+ // A callback around the final TLS socket alone misses handshake errors.
70
+ delegate({ ...socketOptions, protocol: 'http:', hostname: address, host, port, servername }, (error, tunnel) => {
71
+ if (error !== null) {
72
+ callback(error, null);
73
+ return;
74
+ }
75
+ watch(tunnel);
76
+ if (url.protocol !== 'https:') {
77
+ callback(null, tunnel);
78
+ return;
79
+ }
80
+ endpoint({ ...socketOptions, hostname: address, host, port, servername, httpSocket: tunnel }, callback);
81
+ });
38
82
  } });
39
83
  },
40
84
  });
41
85
  return {
42
86
  fetch: async (input, init) => await undiciFetch(input, { ...init, redirect: 'manual', dispatcher }),
43
- dispose: async () => { await dispatcher.destroy(); },
87
+ dispose: async () => {
88
+ for (const socket of sockets)
89
+ socket.destroy();
90
+ await dispatcher.destroy();
91
+ sockets.clear();
92
+ },
44
93
  };
45
94
  }
46
95
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rss",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "订阅和阅读 RSS / Atom,在 DSH 中查看与管理更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -55,7 +55,7 @@
55
55
  "@deepseek-ai/schemastery": "^3.18.3",
56
56
  "fast-xml-parser": "^5.0.0",
57
57
  "schemastery": "^3.18.0",
58
- "undici": "^8.11.2",
58
+ "undici": "^8.12.1",
59
59
  "yaml": "^2.9.0"
60
60
  },
61
61
  "devDependencies": {