@wenbin_wb/dsh-bridge 2.10.11 → 2.10.12
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 +7 -6
- package/lib/index.js +38 -17
- package/lib/restart-helper.mjs +27 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## [v2.10.
|
|
7
|
+
## [v2.10.12] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
> v2.10.11 未正式发布到 npm,其内容已并入本版。
|
|
8
10
|
|
|
9
11
|
### 🐞 修复
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- **「重启 DSH」按钮在 systemd 托管下点了没用,DSH 再也没起来(只能手动重启),且不留任何日志**:桥原实现是不识别托管器的「自派生子进程 + `process.exit(0)`」。在 systemd 单元(如 `dsh-web.service`,用户在 user slice 下)里这必然失败:`Restart=on-failure` 不认干净退出的 code 0(systemd 不会拉起),而默认 `KillMode=control-group` 会在主进程退出时把刚派生的子进程一并杀掉;旧实现还把子进程 stdio 设成 `ignore`,所以连失败原因都看不到。现按托管方式分流:从 `/proc/self/cgroup` 取出**最内层** systemd 单元名 → 交给 `systemctl [--user] restart --no-block <unit>`(stop+start 由 systemd 负责,新进程仍在正确 cgroup 内,并尊重单元自身的 Restart 策略);`DSH_DAEMON` / `PM2_HOME` 仍走守护进程退出;无托管器时派生独立助手 `lib/restart-helper.mjs`,**等旧进程真正退出、端口释放后**才拉起新 DSH,并在最长 120s 内确认端口可连接,全过程写入 `~/.dsh/dsh-bridge/restart.log`(子进程输出另存 `restart-child.log`)。前端在宿主明确返回 `ok:false`(例如 `systemctl` 与助手都派生失败)时**直接报错**,不再假装「正在重连」让用户面对永远转圈。注意:systemd 托管的环境需先手动重启一次让本修复生效,之后按钮即可正常使用。
|
|
13
|
+
- **iOS 16 / 旧 Safari 打不开 DSH**(红屏 `Failed to load plugins … Can't find variable: Iterator`):代理注入 HTML 时补齐 `Promise.withResolvers` 与 `Iterator` 垫片(Safari 分别是 17.4 / 18.4 才有),只在缺失时安装,其它浏览器零改动。
|
|
14
|
+
- **鸿蒙(HuaweiBrowser / ArkWeb)文档预览一直显示「文件资源服务不可用。」**:该内核解析 `dsh-resource://` 地址取不到 hostname,新增 URL 兼容层兜底;引擎正常时不介入。
|
|
15
|
+
- **移动端顶栏压住全屏面板顶部 52px(#41)**:改为直接位移面板容器本身,并让移动端断点与宿主判据对齐(`≤767px`)。
|
|
16
|
+
- **「重启 DSH」在 systemd 托管下点了没用、DSH 不再自动起来**:改为识别 systemd 单元并交给 `systemctl restart`,按退出码判定成败;失败时保持运行并提示手动重启命令。
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
package/lib/index.js
CHANGED
|
@@ -177,7 +177,7 @@ const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 5
|
|
|
177
177
|
<circle cx="256" cy="80" r="16" fill="#4f6ef7"/>
|
|
178
178
|
</svg>`;
|
|
179
179
|
|
|
180
|
-
const
|
|
180
|
+
const HTML_HEAD_ASSETS = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
181
181
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
182
182
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
183
183
|
<meta name="apple-mobile-web-app-title" content="DSH">
|
|
@@ -187,9 +187,14 @@ const HTML_HEAD_INJECTIONS = `<meta name="viewport" content="width=device-width,
|
|
|
187
187
|
<link rel="apple-touch-icon" href="/__dsh_bridge__/pwa-icon.svg">
|
|
188
188
|
<style data-dsh-bridge-overscroll="1">html,body{overscroll-behavior-y:none;-webkit-overflow-scrolling:touch}</style>
|
|
189
189
|
<script data-dsh-bridge-polyfill="1">!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){}}();</script>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
`;
|
|
191
|
+
/** 可分别判重注入的三段:各自的 data 标记即去重键(旧版只按 marker 1 判重,
|
|
192
|
+
* 两层桥串联时会出现"上游有 1 没有 2/3 → 整段跳过"的漏注入)。 */
|
|
193
|
+
const HTML_INJECTION_PARTS = [
|
|
194
|
+
{ mark: 'data-dsh-bridge-polyfill="1"', html: HTML_HEAD_ASSETS },
|
|
195
|
+
{ mark: 'data-dsh-bridge-polyfill="2"', html: BROWSER_ABORT_SIGNAL_POLYFILL },
|
|
196
|
+
{ mark: 'data-dsh-bridge-polyfill="3"', html: BROWSER_PROMISE_ITERATOR_POLYFILL },
|
|
197
|
+
];
|
|
193
198
|
|
|
194
199
|
function isCompressed(headers) {
|
|
195
200
|
return /(^|,\s*)(gzip|br|deflate)(\s*,|$)/i.test(String(headers['content-encoding'] ?? ''));
|
|
@@ -274,10 +279,11 @@ function sanitizeProxyHeaders(headers, extra) {
|
|
|
274
279
|
*/
|
|
275
280
|
export function parseSystemdUnit(cgroup) {
|
|
276
281
|
const parts = String(cgroup || '').split('/').filter(Boolean);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
// 只认**最内层**组件:若最内层不是 .service(例如 .scope、或 docker/k8s 的容器目录),
|
|
283
|
+
// 说明本进程不是被某个 service 直接托管,绝不能向上游误取祖先单元
|
|
284
|
+
// (否则会把 docker.service / cron.service 之类重启掉)。
|
|
285
|
+
const innermost = parts[parts.length - 1] ?? '';
|
|
286
|
+
return innermost.endsWith('.service') ? innermost : '';
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
/**
|
|
@@ -508,8 +514,10 @@ class ProxyServer {
|
|
|
508
514
|
const outHeaders = { ...proxyRes.headers };
|
|
509
515
|
if (contentType.includes('text/html') && !isCompressed(proxyRes.headers)) {
|
|
510
516
|
let html = out.toString('utf8');
|
|
511
|
-
|
|
512
|
-
|
|
517
|
+
const missingParts = HTML_INJECTION_PARTS.filter((part) => !html.includes(part.mark));
|
|
518
|
+
if (missingParts.length > 0) {
|
|
519
|
+
const payload = missingParts.map((part) => part.html).join('\n');
|
|
520
|
+
html = html.replace(/<head[^>]*>/i, (m) => `${m}${payload}`);
|
|
513
521
|
}
|
|
514
522
|
out = Buffer.from(html, 'utf8');
|
|
515
523
|
} else if (isSessionProjection) {
|
|
@@ -1378,26 +1386,39 @@ class BridgeService {
|
|
|
1378
1386
|
|
|
1379
1387
|
if (plan.kind === 'systemd') {
|
|
1380
1388
|
const args = [...(plan.scope === 'user' ? ['--user'] : []), 'restart', '--no-block', plan.unit];
|
|
1381
|
-
|
|
1389
|
+
// 关键:必须以 **systemctl 的退出码** 判定成败,而不是"派生成功"。
|
|
1390
|
+
// `--no-block` 下 systemctl 只发请求就返回(实测往返 ~6ms),但单元名不存在会 rc=5、
|
|
1391
|
+
// 权限不足会 rc=1;旧写法只看 'spawn' 事件 → 必然失败也返回 ok:true 再 exit(0),
|
|
1392
|
+
// 而干净退出不满足 Restart=on-failure → DSH 直接下线且不自动拉起。
|
|
1393
|
+
const outcome = await new Promise((resolve) => {
|
|
1382
1394
|
let settled = false;
|
|
1383
1395
|
const settle = (value) => { if (!settled) { settled = true; resolve(value); } };
|
|
1384
1396
|
try {
|
|
1385
1397
|
const child = spawnImpl('systemctl', args, { detached: true, stdio: 'ignore', env });
|
|
1386
|
-
child.once?.('error', (err) => settle(err));
|
|
1387
|
-
child.once?.('
|
|
1398
|
+
child.once?.('error', (err) => settle({ error: err }));
|
|
1399
|
+
child.once?.('close', (code) => settle({ code }));
|
|
1388
1400
|
child.unref?.();
|
|
1389
1401
|
} catch (err) {
|
|
1390
|
-
settle(err);
|
|
1402
|
+
settle({ error: err });
|
|
1391
1403
|
}
|
|
1392
1404
|
});
|
|
1393
1405
|
|
|
1394
|
-
if (!
|
|
1395
|
-
writeLog(`已请求 systemd
|
|
1406
|
+
if (!outcome.error && (outcome.code === 0 || outcome.code === null || outcome.code === undefined)) {
|
|
1407
|
+
writeLog(`已请求 systemd 重启(systemctl 退出码 ${outcome.code ?? 'n/a'}):systemctl ${args.join(' ')}`);
|
|
1396
1408
|
scheduleExit(1500);
|
|
1397
1409
|
const scopeText = plan.scope === 'user' ? '用户会话' : '系统';
|
|
1398
1410
|
return { ok: true, message: `已请求 systemd 重启 ${plan.unit}(${scopeText}),由 systemd 负责停止与拉起…` };
|
|
1399
1411
|
}
|
|
1400
|
-
|
|
1412
|
+
|
|
1413
|
+
// 失败时**不退化为自派生**:systemd 下主进程干净退出既不会被拉起,
|
|
1414
|
+
// 默认 KillMode=control-group 还会把 detached 助手一起杀掉 —— 那等于"报着重启却永久下线"。
|
|
1415
|
+
// 因此保持现状(DSH 继续运行)并如实报错,由用户手动处理。
|
|
1416
|
+
const reason = outcome.error ? outcome.error.message : `systemctl 退出码 ${outcome.code}`;
|
|
1417
|
+
writeLog(`systemctl 重启失败(${reason}),为不让 DSH 掉线未做任何退出`);
|
|
1418
|
+
return {
|
|
1419
|
+
ok: false,
|
|
1420
|
+
error: `systemctl ${args.join(' ')} 失败:${reason}。DSH 仍在运行;请手动执行「systemctl ${plan.scope === 'user' ? '--user ' : ''}restart ${plan.unit}」或检查单元。`,
|
|
1421
|
+
};
|
|
1401
1422
|
} else if (plan.kind === 'daemon') {
|
|
1402
1423
|
writeLog(`交由守护进程拉起(${plan.reason})`);
|
|
1403
1424
|
scheduleExit(600);
|
package/lib/restart-helper.mjs
CHANGED
|
@@ -81,15 +81,33 @@ async function main() {
|
|
|
81
81
|
|
|
82
82
|
let stdio = 'ignore';
|
|
83
83
|
try { stdio = openSync(childLog, 'a'); } catch { /* 打不开就用 ignore */ }
|
|
84
|
-
|
|
85
|
-
try
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
|
|
85
|
+
// spawn 的失败是异步 'error' 事件(例如 execPath 不存在 → ENOENT),只 try/catch 抓不到;
|
|
86
|
+
// 不挂监听会变成未捕获异常,而且父进程给助手设的是 stdio:'ignore' → 失败细节全部丢失。
|
|
87
|
+
// 这里挂监听并把原因写进日志,且**确认 spawn 成功后才**记录"已派生"。
|
|
88
|
+
const spawned = await new Promise((resolve) => {
|
|
89
|
+
let settled = false;
|
|
90
|
+
const settle = (value) => { if (!settled) { settled = true; resolve(value); } };
|
|
91
|
+
let child;
|
|
92
|
+
try {
|
|
93
|
+
child = spawn(execPath, argv, { cwd, env: process.env, detached: true, stdio: ['ignore', stdio, stdio] });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
log(`派生新 dsh 失败(同步异常):${err?.message ?? err}`);
|
|
96
|
+
settle(null);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
child.once('error', (err) => {
|
|
100
|
+
log(`派生新 dsh 失败:${err?.message ?? err}`);
|
|
101
|
+
settle(null);
|
|
102
|
+
});
|
|
103
|
+
child.once('spawn', () => {
|
|
104
|
+
log(`已派生新 dsh:pid=${child.pid ?? '(未知)'}(输出见 ${childLog})`);
|
|
105
|
+
settle(child);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!spawned) process.exit(4);
|
|
110
|
+
spawned.unref();
|
|
93
111
|
|
|
94
112
|
const up = await waitUntil(() => portAccepts(port), readyTimeoutMs, `新 dsh 监听 ${port}`);
|
|
95
113
|
log(up
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.12",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
-
"releaseNotes": "【v2.10.
|
|
5
|
+
"releaseNotes": "【v2.10.12】(含未正式发布的 v2.10.11 内容)\n• 修复:iOS 16 / 旧 Safari 打不开 DSH(红屏 Failed to load plugins)——补齐 Promise.withResolvers 与 Iterator 垫片,只在缺失时安装\n• 修复:鸿蒙手机上文档预览一直显示「文件资源服务不可用。」——新增 dsh-resource 地址的 URL 兼容层\n• 修复:移动端顶栏压住全屏面板顶部 52px(#41),并把移动端断点与宿主对齐(≤767px)\n• 修复:「重启 DSH」在 systemd 托管下点了没用、DSH 不再自动起来——改为交给 systemctl 重启并按退出码判定成败\n• 说明:systemd 环境需手动重启一次 dsh(systemctl --user restart dsh-web.service)让本版生效",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|