@wenbin_wb/dsh-bridge 2.12.0 → 2.12.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,27 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v2.12.1] - 2026-10-03
8
+
9
+ > 本版为**桌面版(Electron)兼容修复**版。排查依据为一手来源:官方同仓库 `apps/desktop-host/src/index.ts`、`apps/desktop/README.zh.md` 及官方 `evaluatePluginCompatibility` 实测。核心功能(面板、IM、隧道、存储)在桌面端与 Web 版跑同一套宿主与前端,确认兼容;以下两处是"Web 正常、桌面走错路"的分支,已修。
10
+
11
+ ### 🐞 修复
12
+
13
+ - **面板"重启 DSH"在桌面端拦截**:桌面宿主由 Electron 壳经 IPC 管理生死,插件若按 Web/CLI 假设派生 `restart-helper` 重拉进程,会产生壳外孤儿(抢占 19387 端口、触发壳的恢复对话框)。现 `restartDsh` 开头检测桌面环境(`process.versions.electron` 或主模块路径 `desktop-host`,fail-open),直接拒绝并指引走应用菜单重启,绝不派生任何子进程。
14
+ - **插件自升级 profile 参数化**:`upgradePlugin` 原来硬编码 `dsh plugin --profile web add`,在桌面端(profile 名为 `desktop`)会把包装进宿主不加载的 profile,造成"显示成功实际没升"。现按宿主形态推导(桌面 `desktop`、其余 `web`),传入值白名单只认 `desktop`/`web`(防 shell 注入)。
15
+ - **桌面端一键升级 DSH 文案**:`_probeDshUpgrade` 在桌面端仍正确返回不可升级(应用包内文件不能用 npm 动),但 reason 换成桌面 relevant 的指引(走应用菜单"检查更新")。
16
+
17
+ ### 🧪 测试
18
+
19
+ - 新增 `test/desktop-host-compat.test.mjs`(11 项:electron 标记判定、`desktop-host` 路径分段判定、fail-open 脏输入、profile 推导、桌面重启零派生、非桌面重启不受影响、默认 web/显式 desktop/注入 profile 白名单,以及"测试文件名含子串不误判"的回归)。全量 **395 项测试 100% 通过**。
20
+
21
+ ### 📝 升级说明
22
+
23
+ - 无破坏性变更;Web/CLI 用户行为与 v2.12.0 完全一致(桌面检测 fail-open,判不准时按原路走)。
24
+ - 桌面端实机验证仍未做(本机无桌面版安装包):P0/P1 修复基于官方源码的静态结论 + 单测,`process.versions.electron` 在 RunAsNode 下的存在性为 Electron 官方行为。建议在 macOS/Windows 桌面端实测"面板重启被拒"与"插件升级进 desktop profile"两条路径后再关闭该跟踪。
25
+
26
+ ---
27
+
7
28
  ## [v2.12.0] - 2026-10-03
8
29
 
9
30
  > 本版为**安全加固 + 首次启用引导 + 发布包瘦身**版。依据外部安全审计(针对 v2.10.13)逐条核实后修复;按「不破坏现有用户」原则,**默认监听地址保持 `0.0.0.0` 不变**,改用引导提示降低「开箱敞开」风险。
package/lib/index.js CHANGED
@@ -317,6 +317,49 @@ export function detectSupervisor({ env = process.env, cgroup = '' } = {}) {
317
317
  return { kind: 'self', reason: '未检出托管器(无 DSH_DAEMON/PM2_HOME,也不在 systemd 单元内)' };
318
318
  }
319
319
 
320
+ /**
321
+ * 判断当前宿主是否跑在 DSH 桌面版(Electron)内(纯函数,便于单测)。
322
+ *
323
+ * 背景:桌面端宿主是 Electron 以 RunAsNode 模式拉起的同一套 @deepseek-ai/dsh,
324
+ * 进程生死由 Electron 壳经 IPC 管理(shutdown/quit-inspection),没有 systemd、
325
+ * DSH_DAEMON/PM2 这类托管器。插件若按 Web/CLI 版的假设自行重启/升级宿主,
326
+ * 会拉起脱离壳管理的孤儿进程(抢占 19387 端口、触发壳的恢复对话框)或把包
327
+ * 装进错误的 profile(桌面端 profile 名为 `desktop`,不是 `web`)。
328
+ *
329
+ * 判据(按可靠性排序,任一命中即判为桌面端):
330
+ * 1. `process.versions.electron` 存在 —— Electron(含 RunAsNode 模式)的官方
331
+ * 行为保证该字段存在,跨平台最可靠;
332
+ * 2. 主模块路径含 `desktop-host` —— 桌面宿主入口固定为 desktop-host 包。
333
+ *
334
+ * fail-open:判不准时一律按 Web/CLI 版走,绝不能把正常用户的重启/升级拦掉。
335
+ *
336
+ * @param {object} [opts]
337
+ * @param {object} [opts.versions] process.versions(默认 process.versions)
338
+ * @param {string} [opts.argv1] process.argv[1](默认 process.argv[1])
339
+ * @returns {boolean}
340
+ */
341
+ export function isDesktopHost({ versions = process.versions, argv1 = process.argv[1] } = {}) {
342
+ try {
343
+ if (versions && typeof versions.electron === 'string' && versions.electron.length > 0) return true;
344
+ } catch { /* 忽略,继续下一个判据 */ }
345
+ try {
346
+ // 必须按路径分段匹配(前后是分隔符或串 ends):避免用户目录名恰好包含
347
+ // desktop-host 子串(如本仓库的 test/desktop-host-compat.test.mjs)时误判。
348
+ // 生产路径形如 .../dsh-desktop-host/lib/index.js。
349
+ if (typeof argv1 === 'string' && /(?:^|[/\\])(?:dsh-)?desktop-host(?:[/\\]|$)/i.test(argv1)) return true;
350
+ } catch { /* 忽略,fail-open */ }
351
+ return false;
352
+ }
353
+
354
+ /**
355
+ * 当前宿主加载的 profile 名(纯函数,便于单测)。
356
+ * 桌面端为 `desktop`,其余(Web/CLI)为 `web`。供 `dsh plugin --profile <name> add`
357
+ * 类命令使用,避免把包装进宿主不加载的 profile。
358
+ */
359
+ export function currentProfileName(opts = {}) {
360
+ return isDesktopHost(opts) ? 'desktop' : 'web';
361
+ }
362
+
320
363
  class ProxyServer {
321
364
  constructor({ localPort, targetPort, authManager, logger, allowedOrigins }) {
322
365
  this.localPort = localPort;
@@ -1183,7 +1226,11 @@ class BridgeService {
1183
1226
  }
1184
1227
  return {
1185
1228
  upgradable: false,
1186
- reason: 'dsh 非标准 npm 全局安装(Electron/打包/源码/pnpm 等),无法自动升级;请按官方渠道手动更新',
1229
+ // 桌面端 dsh 装在 App 包内:走桌面应用内更新,不要用 npm 动它。
1230
+ // 桌面检测是 fail-open 的,判不准时仍是这条通用文案,不影响 Web/CLI 用户。
1231
+ reason: isDesktopHost()
1232
+ ? '桌面版 DSH 由应用内更新统一管理(一键升级仅支持 npm 全局安装的 CLI 版);请用桌面应用菜单的"检查更新"升级,勿用 npm 改动应用包内文件'
1233
+ : 'dsh 非标准 npm 全局安装(Electron/打包/源码/pnpm 等),无法自动升级;请按官方渠道手动更新',
1187
1234
  };
1188
1235
  } catch {
1189
1236
  return { upgradable: false, reason: '探测 dsh 安装形态失败,请按官方渠道手动更新' };
@@ -1244,7 +1291,9 @@ class BridgeService {
1244
1291
  }
1245
1292
 
1246
1293
  // 一键直接升级插件(执行 dsh / npx / npm 自动升级,使用安全的参数数组彻底杜绝 shell 注入)
1247
- async upgradePlugin({ version } = {}) {
1294
+ // opts.profile 供测试注入;默认按当前宿主推导(桌面端为 `desktop`,其余为 `web`),
1295
+ // 绝不能写死 --profile web——桌面端会把包装进宿主不加载的 profile,造成"显示成功实际没升"。
1296
+ async upgradePlugin({ version, spawnImpl = spawn, profile } = {}) {
1248
1297
  const targetVersion = version ? String(version).trim() : 'latest';
1249
1298
  // 严格 SemVer 白名单正则校验
1250
1299
  if (!/^(latest|\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?)$/.test(targetVersion)) {
@@ -1283,9 +1332,15 @@ class BridgeService {
1283
1332
  const siblingNpm = join(nodeDir, isWin ? 'npm.cmd' : 'npm');
1284
1333
  const siblingNpx = join(nodeDir, isWin ? 'npx.cmd' : 'npx');
1285
1334
 
1335
+ // 目标 profile:显式传入优先,否则按宿主形态推导(桌面端 `desktop`,其余 `web`)。
1336
+ // 桌面端 profile 名固定,见官方 desktop-host(runProfile profile: 'desktop')。
1337
+ // 安全:payload 里的 profile 不可信,白名单只认 desktop/web,其余一律按推导值处理,
1338
+ // 避免未校验字符串进入 shell:true 的 spawn 参数(参数拼接有注入风险)。
1339
+ const targetProfile = (profile === 'desktop' || profile === 'web') ? profile : currentProfileName();
1340
+
1286
1341
  const tasks = [
1287
- { cmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
1288
- { cmd: existsSync(siblingNpx) ? siblingNpx : 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
1342
+ { cmd: 'dsh', args: ['plugin', '--profile', targetProfile, 'add', pkgSpec] },
1343
+ { cmd: existsSync(siblingNpx) ? siblingNpx : 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', targetProfile, 'add', pkgSpec] },
1289
1344
  { cmd: existsSync(siblingNpm) ? siblingNpm : 'npm', args: ['install', pkgSpec] },
1290
1345
  ];
1291
1346
 
@@ -1296,7 +1351,7 @@ class BridgeService {
1296
1351
  const res = await new Promise((resolve, reject) => {
1297
1352
  let cp;
1298
1353
  try {
1299
- cp = spawn(task.cmd, task.args, {
1354
+ cp = spawnImpl(task.cmd, task.args, {
1300
1355
  windowsHide: true,
1301
1356
  shell: true,
1302
1357
  env: augmentedEnv,
@@ -1517,6 +1572,17 @@ class BridgeService {
1517
1572
  // 并把全过程写入 ~/.dsh/dsh-bridge/restart.log(失败不再无声)。
1518
1573
  // opts 仅供单测注入(spawnImpl / readCgroup / env / scheduleExit)。
1519
1574
  async restartDsh(opts = {}) {
1575
+ // 桌面端拦截(P0):桌面宿主由 Electron 壳经 IPC 管理生死,插件绝不能自行
1576
+ // 派生 helper 重拉进程——那会产生壳外孤儿(抢占 19387 端口、触发壳的恢复
1577
+ // 对话框)。直接拒绝,并指引用户走应用菜单重启。
1578
+ // isDesktopHost 是 fail-open 的:判不准时按 Web/CLI 版走,不拦正常用户。
1579
+ if (isDesktopHost({ versions: opts.processVersions, argv1: opts.argv1 })) {
1580
+ return {
1581
+ ok: false,
1582
+ error: '当前运行在 DSH 桌面版内,宿主由应用统一管理,插件不能自行重启。请用桌面应用菜单的重启/退出重进(有关闭确认保护正在跑的任务),不要用面板重启。',
1583
+ desktopManaged: true,
1584
+ };
1585
+ }
1520
1586
  const env = opts.env ?? process.env;
1521
1587
  const spawnImpl = opts.spawnImpl ?? spawn;
1522
1588
  const readCgroup = opts.readCgroup
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.12.0",
3
+ "version": "2.12.1",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
- "releaseNotes": "【v2.12.0】安全加固 + 首次启用引导 + 修复 IM 标题 + 重启打断保护 + 发布包瘦身 88%\n• 修复:IM /sessions 会话标题全为「新会话」——DSH 0.1.7 把 session_projcache 改为 per-record 目录,插件仍读旧单文件路径;现兼容两种布局\n• 安全:config.json 与微信 context token 落盘收紧为 0600(此前 0644 同机可读,含明文隧道凭据),存量文件启动时自动收敛\n• 安全:WebSocket 升级补齐 Origin 校验(防恶意页面借浏览器 Cookie 建连);Origin 缺失一律放行,不影响 IM 机器人等非浏览器客户端\n• 修复:局域网直连原生 3080 端口时 WebSocket 被误拒、实时通道静默失效\n• 新功能:首次启用引导——检测到「监听 0.0.0.0 但未开启访问认证」时提示开门禁,已配置用户不打扰\n• 新功能:重启打断保护——有任务在跑(含子代理、待审批)时弹二次确认并列出具体现场,避免静默打断\n• 修复:一键升级 DSH 显示成功但实际仍是旧版本——prefix 错位(装到 lib/lib/node_modules)改用 npm prefix -g;升级后核验 dsh --version,不匹配则报错而非报喜\n• 优化:文档配图转 WebP 并移出发布包,tarball 7.63MB → 0.87MB\n• 说明:默认监听地址保持 0.0.0.0 不变,不破坏现有部署;权限与校验改动重启 dsh 后生效",
5
+ "releaseNotes": "【v2.12.1】桌面版(Electron)兼容修复\n• 修复:面板\"重启 DSH\"在桌面端拦截——桌面宿主由应用壳管理,插件不再自行派生重启(避免壳外孤儿进程与端口冲突),指引走应用菜单重启\n• 修复:插件自升级 profile 参数化——桌面端装进 desktop profile,不再硬编码 web(避免\"显示成功实际没升\")\n• 优化:桌面端一键升级 DSH 的降级文案指向应用内更新",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "exports": {