dsh-bridge-gateway 0.2.0 → 0.2.2

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
@@ -2,6 +2,62 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
4
 
5
+ ## [0.2.2] - 2026-10-11
6
+
7
+ ### 修复:旧内核浏览器打开后工作区永远加载不了
8
+
9
+ 远程浏览器(统信 UOS、360 浏览器,内核均为 Chromium 108)访问时外壳能渲染出来,但工作区
10
+ 一直停在「选择一个工作区开始」;DevTools 报 `Promise.withResolvers is not a function` 与
11
+ `AbortSignal.any is not a function`,并伴随 `connection lost, retry #N` 无限重连。
12
+
13
+ 根因是 DSH `0.2.0-rc.2` 的浏览器端直接使用了三个较新的 Web API,旧内核上没有:
14
+
15
+ | API | 要求版本 | 调用点 |
16
+ |---|---|---|
17
+ | `Promise.withResolvers` | Chrome/Edge ≥ 119、Firefox ≥ 121、Safari ≥ 17.4 | index.html 末尾的启动门 `(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers())`;`dsh-api-workspace-files` 客户端的工作区文件流 |
18
+ | `AbortSignal.any` | Chrome/Edge ≥ 116、Firefox ≥ 124、Safari ≥ 17.4 | `dsh-api-gateway` 的 `RemoteStream.read`、`dsh-api-session-controller`、`dsh-client-ui-workspace`、`dsh-client-ui-sidebar-files` 等 |
19
+ | `Array.prototype.toSorted` | Chrome/Edge ≥ 110 | `dsh-client-ui-model-selection` / `-schedule` / `-settings-models` |
20
+
21
+ 缺前两个的后果是启动门永不 resolve、远程流层直接抛错 —— 于是插件全部注册成功,工作区却
22
+ 永远加载不出来。
23
+
24
+ 修复走官方 `webserver/index-inject` 机制(与既有的 `crypto.randomUUID` polyfill 同一处),
25
+ 向 index.html 的 `<head>` 注入一段兜底脚本;该位置早于 `<body>` 末尾的启动门,所以一定先执行。
26
+ 三个 polyfill 全部带 `typeof` 守卫且幂等,现代浏览器上原样跳过、**绝不覆盖原生实现**;
27
+ `AbortSignal.any` 对齐原生语义:空数组不 abort、已 abort 的成员立即短路并沿用其 reason、
28
+ 组合信号 abort 后摘掉监听器(避免在 `lifetime` / `generation` 这类长命 signal 上堆积)。
29
+
30
+ 新增 `test/legacy-compat.test.mjs` 第 3 节共 10 条断言,在 `node:vm` 沙箱里剥离这三个 API 后
31
+ 验证注入脚本本身,其中两条是反向断言:原生齐全时三个函数引用必须原封不动、重复执行必须是 no-op。
32
+
33
+ > 附带说明:同一现场出现的 `404` / `net::ERR_HTTP_RESPONSE_CODE_FAILURE` 与本次故障无关 ——
34
+ > 那是 DevTools 打开时去取不存在的 source map(`/assets/index-*.js.map`)以及
35
+ > `/api/changes.summary`(deliverables 的 RPC)返回 404;工作区在零 4xx 的情况下照样加载失败。
36
+
37
+ ## [0.2.1] - 2026-09-25
38
+
39
+ ### 兼容性核查:DSH 0.1.7 无需改动
40
+
41
+ 对 `0.1.7-rc.2` 逐项复核后确认本插件不需要适配:宿主侧注入的 `connection`、`webServer`、
42
+ `sessions`、`agents`、`approval`、`workspaceRegistry`、`sessionPersistence`、`loader`
43
+ 八个服务全部仍在;客户端注入的 `slots` / `connection` / `workspaces` / `sessions` 与注册的
44
+ `settings.section` 槽位也仍在;`dsh-llm` 的 `createUserMessage`、`@deepseek-ai/cordis` 的
45
+ `Service` 导出未变。`0.1.5` 线继续兼容(peer 范围 `^4.0.1` / `>=0.1.0-rc.6` 两条线都满足)。
46
+
47
+ ### 修复:HTML 响应强制 no-store(远程端"改了页面不变")
48
+
49
+ DSH 的 index HTML **不带 cache-control**,浏览器按启发式缓存它;而该 HTML 里钉着各客户端模块的
50
+ `rev`,模块 bundle 又是 `immutable` 长缓存 —— 于是 host 重启/重建后换了新 rev,旧标签页与
51
+ **普通刷新**仍然指向旧 rev 的旧代码,表现为"改动永远不生效"(远程直连场景尤其明显:
52
+ 页面只重连、不重新取 index)。
53
+
54
+ 透明反向代理分支对 `content-type: text/html` 的响应统一加
55
+ `Cache-Control: no-store, must-revalidate`,并删除 `etag` / `last-modified`;
56
+ 非 HTML(含带 rev 的模块 bundle)保持原样,缓存策略不变。
57
+
58
+ 注意:这条只对**新取到的响应**生效;浏览器里已经存下的旧 index 需要一次硬刷新
59
+ (Cmd+Shift+R)或带新查询串的地址来替换。
60
+
5
61
  ## [0.2.0] - 2026-09-19
6
62
 
7
63
  ### 破坏性变更(移除 dsh-mobile 协议舱;访问配置 Tab 重构;新增 Tailscale 隧道)
package/README.md CHANGED
@@ -55,11 +55,18 @@ Upstream's tunnels either go through Cloudflare or require your own server. This
55
55
  Upstream iterates quickly and this repository keeps up. Ported fixes are listed in [CHANGELOG](./CHANGELOG.md), notably:
56
56
 
57
57
  - DSH 0.1.5 adaptation: real agent-preset mounting, already-persisted session misdetection, platform config lost on restart, `/rename` durability;
58
+ - DSH 0.1.7 compatibility check: no change needed (the injected host services `connection` / `webServer` / `sessions` / `agents` / `approval` / `workspaceRegistry` / `sessionPersistence` / `loader`, the `settings.section` slot, and `createUserMessage` from `dsh-llm` all still exist);
58
59
  - Tunnel keepalive: WebSocket Ping/Pong answered at the frame layer, cloudflared crash self-healing and autoupdate disabled;
59
60
  - Reliability: Telegram proxy broken on Node >= 24, login session persistence.
60
61
 
61
62
  > Both sides evolve, so **feature coverage may lead on either side**. When in doubt, check upstream's README to see which side owns a given capability.
62
63
 
64
+ **5. Fixed: old-kernel browsers could not load the workspace (still present upstream)**
65
+
66
+ DSH's browser client calls `Promise.withResolvers` (Chrome/Edge ≥ 119) and `AbortSignal.any` (Chrome/Edge ≥ 116) directly. On an older kernel — UOS and 360 Browser, both Chromium 108 — the boot gate never resolves and the remote-stream layer throws, so the shell renders but the workspace stays stuck on "pick a workspace" while the console loops on `connection lost`.
67
+
68
+ This repository injects a small shim through the official `webserver/index-inject` mechanism (the same place as the existing `crypto.randomUUID` polyfill), covering `Promise.withResolvers`, `AbortSignal.any` and `Array.prototype.toSorted`. Every shim is `typeof`-guarded and idempotent, so a modern browser skips it untouched and never loses its native implementation.
69
+
63
70
  ### Naming
64
71
 
65
72
  The plugin was renamed from `dsh-bridge` to **`dsh-bridge-gateway`** (reflecting the added gateway capability). Existing users need no manual work — the data directory, configuration, passwords, certificates, and login state all migrate automatically:
@@ -77,6 +84,7 @@ The plugin was renamed from `dsh-bridge` to **`dsh-bridge-gateway`** (reflecting
77
84
  - **LAN access.** Scan a QR code from the remote-access panel on the same Wi-Fi and you are in.
78
85
  - **Public tunnels.** Cloudflare (temporary or fixed hostname) or a self-built tunnel, selectable per need alongside the direct gateway and Tailscale.
79
86
  - **Remote workspace.** Browse the host's directory tree and pick workspace conversations from the phone (via DSH's official browse picker).
87
+ - **Legacy browser support.** The gateway injects `Promise.withResolvers` / `AbortSignal.any` / `Array.prototype.toSorted` polyfills into the page, so Chromium 108-class kernels (UOS, 360 Browser) load the workspace too. Modern browsers skip them untouched.
80
88
  - **Security.** Password gate + QR-based password-free token + admin unlock/lock for sensitive config + rate limiting against brute force, plus persisted login state (no re-login after a host restart). Client-claimed `isLocalhost` is never trusted.
81
89
  - **Visitor management.** See live who is currently connected (LAN / direct gateway / tunnel, with the real source IP behind a tunnel) plus an IP blacklist. **Viewing is open to every visitor**; disconnecting and blacklisting require admin rights.
82
90
  - **IM bots.** WeChat / QQ / Feishu / Telegram bots connect through their own gateway links — no public IP needed. Per-platform workspace / agent preset / model selection is configurable. Guides live in [docs/](./docs).
@@ -99,7 +107,8 @@ The panel has 5 tabs: **Access** / **Visitors** / **IM Bots** / **Security** / *
99
107
  ## Requirements
100
108
 
101
109
  - Node `^22.19.0 || >=24.0.0`.
102
- - DSH with a web profile (built and tested against `0.1.5-rc.2`; adapted for the 0.1.5 RPC/webServer entry points).
110
+ - DSH with a web profile (built and tested against `0.1.7-rc.2`, and still compatible with the `0.1.5` line: the host services, client slots and settings entry points it uses exist in both, so no extra adaptation is needed).
111
+ - A modern browser for the web UI: Chrome/Edge ≥ 119, Safari ≥ 17.4 or Firefox ≥ 121. Older kernels are kept working by the injected polyfills described above (Chromium 108-class UOS / 360 Browser included).
103
112
 
104
113
  ## Install
105
114
 
package/README.zh.md CHANGED
@@ -68,12 +68,27 @@ DeepSeek Harness(DSH)远程访问插件。以 [wenbin-wb/dsh-bridge](https:/
68
68
  上游迭代很快,本仓库持续跟进。已移植的修复见 [CHANGELOG](./CHANGELOG.md),主要包括:
69
69
 
70
70
  - DSH 0.1.5 适配:agent preset 真实挂载、会话已存在误判、平台配置重启即丢、`/rename` 持久化;
71
+ - DSH 0.1.7 兼容性核查:无需改动(宿主注入的 `connection` / `webServer` / `sessions` / `agents` /
72
+ `approval` / `workspaceRegistry` / `sessionPersistence` / `loader` 服务、`settings.section` 槽位、
73
+ `dsh-llm` 的 `createUserMessage` 均仍存在);
71
74
  - 隧道保活:WebSocket Ping/Pong 帧层应答、cloudflared 崩溃自愈与禁用 autoupdate;
72
75
  - 可靠性:Telegram 代理在 Node ≥ 24 下失效、登录 Session 持久化。
73
76
 
74
77
  > 由于两边都在演进,**功能上可能各有先后**。遇到问题时建议对照上游 README 确认某项能力
75
78
  > 归属哪一侧。
76
79
 
80
+ **五、修正:旧内核浏览器打开后工作区永远加载不了(上游至今保留)**
81
+
82
+ DSH 的浏览器端直接调用了 `Promise.withResolvers`(Chrome/Edge ≥ 119)与 `AbortSignal.any`
83
+ (Chrome/Edge ≥ 116)。旧内核上(统信 UOS、360 浏览器,内核均为 Chromium 108)启动门永不
84
+ resolve、远程流层直接抛错 —— 于是外壳渲染得出来,工作区却一直停在「选择一个工作区」,
85
+ 控制台刷 `connection lost` 重连。
86
+
87
+ 本仓库经官方 `webserver/index-inject` 机制(与既有的 `crypto.randomUUID` polyfill 同一处)
88
+ 注入一段兜底脚本,覆盖 `Promise.withResolvers`、`AbortSignal.any` 与
89
+ `Array.prototype.toSorted`。每段都带 `typeof` 守卫且幂等,现代浏览器上原样跳过,
90
+ **绝不覆盖原生实现**。
91
+
77
92
  ### 命名说明
78
93
 
79
94
  插件名由 `dsh-bridge` 改为 **`dsh-bridge-gateway`**(体现「网关」这一新增能力)。
@@ -96,6 +111,9 @@ DeepSeek Harness(DSH)远程访问插件。以 [wenbin-wb/dsh-bridge](https:/
96
111
  - **局域网访问。** 同一 Wi-Fi 下用手机扫远程访问面板里的二维码即可进入。
97
112
  - **公网隧道。** Cloudflare(临时/固定域名)或自建隧道,与直连网关、Tailscale 按需选用。
98
113
  - **远程工作区。** 手机端网页目录树直接浏览并选择工作区对话(走 DSH 官方 browse 选择器)。
114
+ - **旧内核浏览器支持。** 网关向页面注入 `Promise.withResolvers` / `AbortSignal.any` /
115
+ `Array.prototype.toSorted` 三段 polyfill,Chromium 108 档内核(统信 UOS、360 浏览器)
116
+ 也能正常加载工作区;现代浏览器上原样跳过。
99
117
  - **安全认证。** 密码门禁 + 扫码免密 Token + 敏感配置的管理员解锁/锁定 + 防暴力破解限频、
100
118
  登录态持久化(宿主重启不必重新登录);永不信任客户端自称的 `isLocalhost`。
101
119
  - **访客管理。** 实时查看「谁正连着本机」(局域网 / 直连 / 隧道,隧道场景显示真实来源 IP),
@@ -123,7 +141,10 @@ DeepSeek Harness(DSH)远程访问插件。以 [wenbin-wb/dsh-bridge](https:/
123
141
  ## 要求
124
142
 
125
143
  - Node `^22.19.0 || >=24.0.0`。
126
- - DSH web profile(构建与测试基于 `0.1.5-rc.2`,已适配 0.1.5 的 RPC/webServer 入口)。
144
+ - DSH web profile(构建与测试基于 `0.1.7-rc.2`,同时兼容 `0.1.5` 线:宿主服务、客户端槽位与
145
+ 设置接口在两个版本上都存在,无需额外适配)。
146
+ - 网页端建议使用现代浏览器:Chrome/Edge ≥ 119、Safari ≥ 17.4 或 Firefox ≥ 121。
147
+ 旧内核由上文注入的 polyfill 兜底(含 Chromium 108 档的统信 UOS / 360 浏览器)。
127
148
 
128
149
  ## 安装
129
150
 
@@ -11637,6 +11637,12 @@ var require_follow_redirects = __commonJS({
11637
11637
  RedirectableRequest.prototype._sanitizeOptions = function(options) {
11638
11638
  if (!options.headers) {
11639
11639
  options.headers = {};
11640
+ } else if (Array.isArray(options.headers)) {
11641
+ var headerList = options.headers;
11642
+ options.headers = {};
11643
+ for (var i = 0; i < headerList.length; i += 2) {
11644
+ options.headers[headerList[i]] = headerList[i + 1];
11645
+ }
11640
11646
  }
11641
11647
  if (!isArray(options.sensitiveHeaders)) {
11642
11648
  options.sensitiveHeaders = [];
@@ -11911,7 +11917,7 @@ var require_follow_redirects = __commonJS({
11911
11917
  return URL2 && value instanceof URL2;
11912
11918
  }
11913
11919
  function escapeRegex(regex) {
11914
- return regex.replace(/[\]\\/()*+?.$]/g, "\\$&");
11920
+ return regex.replace(/[[\]\\/()*+?.^$|]/g, "\\$&");
11915
11921
  }
11916
11922
  module.exports = wrap({ http, https });
11917
11923
  module.exports.wrap = wrap;
@@ -22776,11 +22782,23 @@ var require_websocket = __commonJS({
22776
22782
  this._isServer = false;
22777
22783
  this._redirects = 0;
22778
22784
  if (protocols === void 0) {
22779
- protocols = [];
22785
+ if (!options || options.protocols === void 0) {
22786
+ protocols = [];
22787
+ } else if (Array.isArray(options.protocols)) {
22788
+ protocols = options.protocols;
22789
+ } else {
22790
+ protocols = [options.protocols];
22791
+ }
22780
22792
  } else if (!Array.isArray(protocols)) {
22781
22793
  if (typeof protocols === "object" && protocols !== null) {
22782
22794
  options = protocols;
22783
- protocols = [];
22795
+ if (options.protocols === void 0) {
22796
+ protocols = [];
22797
+ } else if (Array.isArray(options.protocols)) {
22798
+ protocols = options.protocols;
22799
+ } else {
22800
+ protocols = [options.protocols];
22801
+ }
22784
22802
  } else {
22785
22803
  protocols = [protocols];
22786
22804
  }
@@ -22977,7 +22995,6 @@ var require_websocket = __commonJS({
22977
22995
  }
22978
22996
  return;
22979
22997
  }
22980
- this._readyState = _WebSocket.CLOSING;
22981
22998
  this._sender.close(code, data, !this._isServer, (err) => {
22982
22999
  if (err) return;
22983
23000
  this._closeFrameSent = true;
@@ -22985,6 +23002,7 @@ var require_websocket = __commonJS({
22985
23002
  this._socket.end();
22986
23003
  }
22987
23004
  });
23005
+ this._readyState = _WebSocket.CLOSING;
22988
23006
  setCloseTimer(this);
22989
23007
  }
22990
23008
  /**
@@ -23209,6 +23227,7 @@ var require_websocket = __commonJS({
23209
23227
  socketPath: void 0,
23210
23228
  hostname: void 0,
23211
23229
  protocol: void 0,
23230
+ protocols: void 0,
23212
23231
  timeout: void 0,
23213
23232
  method: "GET",
23214
23233
  host: void 0,
package/lib/index.js CHANGED
@@ -57,6 +57,77 @@ const inject = ['connection', 'webServer', 'sessions', 'agents', 'approval', 'wo
57
57
  const DIRECTORY_PICKER_BACKEND = '@deepseek-ai/dsh-host-directory-picker-browse';
58
58
  const DIRECTORY_PICKER_SURFACE = '@deepseek-ai/dsh-client-ui-directory-picker-browse';
59
59
 
60
+ // ---------------------------------------------------------------------------
61
+ // 旧内核兜底 polyfill(由 apply() 经 webserver/index-inject 注入 index.html 的 <head>)
62
+ // ---------------------------------------------------------------------------
63
+ // 为什么需要:DSH 0.2.0-rc.2 的浏览器端直接用了三个较新的 Web API,旧内核上一进
64
+ // 页面就抛异常,表现为「外壳出得来、工作区永远加载不了」。2026-10-11 在统信 UOS /
65
+ // 360 浏览器(内核均为 Chrome 108)上实测复现,DevTools 报
66
+ // `Promise.withResolvers is not a function` 与 `AbortSignal.any is not a function`,
67
+ // 后者抛在 dsh-api-gateway 的 RemoteStream.read 里,表现为 connection lost 无限重连。
68
+ //
69
+ // Promise.withResolvers Chrome/Edge <119、Firefox <121、Safari <17.4
70
+ // 调用点:index.html 末尾的启动门
71
+ // (globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()
72
+ // 以及 @deepseek-ai/dsh-api-workspace-files 的客户端(工作区文件流启动)。
73
+ // AbortSignal.any Chrome/Edge <116、Firefox <124、Safari <17.4
74
+ // 调用点:@deepseek-ai/dsh-api-gateway、dsh-api-session-controller、
75
+ // dsh-client-ui-workspace、dsh-client-ui-sidebar-files 等。
76
+ // Array.prototype.toSorted Chrome/Edge <110
77
+ // 调用点:dsh-client-ui-model-selection / -schedule / -settings-models。
78
+ //
79
+ // 注入位置在 <head>,早于 <body> 末尾的启动门,因此一定先于它们执行。
80
+ // 全部带 typeof 守卫且幂等:现代内核上原样跳过,绝不覆盖原生实现。
81
+ const LEGACY_BROWSER_POLYFILL = `
82
+ !function () {
83
+ try {
84
+ if (typeof Promise.withResolvers !== 'function') {
85
+ Promise.withResolvers = function () {
86
+ var resolve, reject;
87
+ var promise = new Promise(function (res, rej) { resolve = res; reject = rej; });
88
+ return { promise: promise, resolve: resolve, reject: reject };
89
+ };
90
+ }
91
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.any !== 'function') {
92
+ AbortSignal.any = function (signals) {
93
+ if (signals == null || typeof signals[Symbol.iterator] !== 'function') {
94
+ throw new TypeError('AbortSignal.any: argument is not iterable');
95
+ }
96
+ var list = [], it = signals[Symbol.iterator](), step;
97
+ while (!(step = it.next()).done) list.push(step.value);
98
+ for (var i = 0; i < list.length; i++) {
99
+ if (list[i] === null || typeof list[i] !== 'object' || typeof list[i].aborted !== 'boolean') {
100
+ throw new TypeError('AbortSignal.any: member is not an AbortSignal');
101
+ }
102
+ }
103
+ var ctrl = new AbortController();
104
+ for (var j = 0; j < list.length; j++) {
105
+ if (list[j].aborted) { ctrl.abort(list[j].reason); return ctrl.signal; }
106
+ }
107
+ var onAbort = function (e) { ctrl.abort(e.target.reason); };
108
+ for (var k = 0; k < list.length; k++) list[k].addEventListener('abort', onAbort, { once: true });
109
+ // 组合信号 abort 后摘掉监听:lifetime / generation 这类长命 signal 上不能堆监听器
110
+ ctrl.signal.addEventListener('abort', function () {
111
+ for (var m = 0; m < list.length; m++) list[m].removeEventListener('abort', onAbort);
112
+ }, { once: true });
113
+ return ctrl.signal;
114
+ };
115
+ }
116
+ if (typeof Array.prototype.toSorted !== 'function') {
117
+ Object.defineProperty(Array.prototype, 'toSorted', {
118
+ value: function (compareFn) {
119
+ 'use strict';
120
+ if (this === null || this === undefined) throw new TypeError('Array.prototype.toSorted called on null or undefined');
121
+ if (compareFn !== undefined && typeof compareFn !== 'function') throw new TypeError('The comparison function must be either a function or undefined');
122
+ return Array.prototype.slice.call(this).sort(compareFn);
123
+ },
124
+ writable: true, enumerable: false, configurable: true,
125
+ });
126
+ }
127
+ } catch (e) { void e; }
128
+ }();
129
+ `;
130
+
60
131
  // 从 package.json 动态读取版本号,发版只需改 package.json 一处
61
132
  const PACKAGE_JSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
62
133
  const VERSION = PACKAGE_JSON.version ?? '0.0.0';
@@ -558,7 +629,18 @@ class ProxyServer {
558
629
  const proxyReq = httpRequest(
559
630
  { host: '127.0.0.1', port: this.targetPort, method: req.method, path: req.url, headers, agent: false },
560
631
  (proxyRes) => {
561
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
632
+ // DSH 的 index HTML 不带 cache-control,浏览器会**启发式缓存**它;而它里面钉着
633
+ // 各客户端模块的 rev,于是 host 重启换了新 rev 之后,旧标签页/普通刷新仍指向旧 rev
634
+ // 的 immutable bundle —— 表现就是"改了但页面永远不变"(远程端尤其明显)。
635
+ // 这里对 HTML 响应强制 no-store:每次刷新都重新取 index。bundle 本身仍按 rev 长缓存。
636
+ const outHeaders = { ...proxyRes.headers };
637
+ const contentType = String(outHeaders['content-type'] ?? '');
638
+ if (contentType.includes('text/html')) {
639
+ outHeaders['cache-control'] = 'no-store, must-revalidate';
640
+ delete outHeaders.etag;
641
+ delete outHeaders['last-modified'];
642
+ }
643
+ res.writeHead(proxyRes.statusCode ?? 502, outHeaders);
562
644
  proxyRes.pipe(res);
563
645
  res.on('close', () => proxyRes.destroy());
564
646
  proxyRes.on('error', () => res.destroy());
@@ -2074,6 +2156,14 @@ function apply(ctx, config = {}) {
2074
2156
  text:
2075
2157
  "!function(){try{if(self.crypto&&!self.crypto.randomUUID){self.crypto.randomUUID=function(){var b=new Uint8Array(16);self.crypto.getRandomValues(b);b[6]=b[6]&15|64;b[8]=b[8]&63|128;var h=\"\";for(var i=0;i<16;i++){var x=b[i].toString(16);h+=(x.length<2?\"0\":\"\")+x;if(i===3||i===5||i===7||i===9)h+=\"-\";}return h;}}}catch(e){}}();",
2076
2158
  },
2159
+ {
2160
+ // 旧内核兜底:Promise.withResolvers / AbortSignal.any / Array.prototype.toSorted。
2161
+ // 缺前两个的表现是「外壳出来、工作区永远加载不了」,取舍与调用点见文件顶部
2162
+ // LEGACY_BROWSER_POLYFILL 的注释块。
2163
+ kind: 'script',
2164
+ placement: 'head',
2165
+ text: LEGACY_BROWSER_POLYFILL,
2166
+ },
2077
2167
  );
2078
2168
  });
2079
2169
 
@@ -2600,4 +2690,4 @@ function apply(ctx, config = {}) {
2600
2690
  }, 'dsh-bridge: stop wechat, qq, feishu, telegram, proxy, auth and tunnels');
2601
2691
  }
2602
2692
 
2603
- export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4 };
2693
+ export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4, LEGACY_BROWSER_POLYFILL };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-bridge-gateway",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "以 dsh-bridge 为根基的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / Tailscale 隧道 / 自建隧道 / 公网直连网关 / IM Bot,全部在 Web 面板中配置。",
5
- "releaseNotes": "【dsh-bridge-gateway 0.2.0】\n• 🆕 Tailscale 隧道卡片:网关自动探测 `tailscale serve` 地址(只读,不代改配置),支持一键填入、手动编辑与二维码扫码直连\n• 🗂️ 访问配置 Tab:原「局域网」「公网访问」两块合并,直连网关 / Cloudflare / Tailscale / 自建隧道 / 局域网五张卡片,默认只展开直连网关\n• 👥 访客管理 Tab:远程连接监控与持久黑名单从公网 Tab 独立出来\n• 📱 移除 dsh-mobile 协议舱(/ws/mobile 设备配对)及其 Linux 一键部署 CLI(init / setup / status / remove)\n• 🎨 卡片支持折叠,折叠时保留状态标签与一行关键信息\n• 🐛 修复 Tab 数量超出容器宽度导致「运维监控」在桌面端不可见(Tab 合并后恢复)",
5
+ "releaseNotes": "【dsh-bridge-gateway 0.2.2】\n• 🐛 修复旧内核浏览器(统信 UOS / 360 浏览器等 Chromium 108)打开后工作区永远加载不了:经官方 `webserver/index-inject` 注入 `Promise.withResolvers` / `AbortSignal.any` / `Array.prototype.toSorted` 三个 polyfill,全部带 `typeof` 守卫且幂等,现代浏览器上原样跳过、不覆盖原生实现\n• ✅ 新增 10 条兼容性断言(含「不得覆盖原生实现」「幂等」两条反向断言),全套 216 项测试通过",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "exports": {