dsh-helper 0.16.7 → 0.16.8

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
@@ -8,6 +8,36 @@
8
8
  > (`package-lock.json` 根节点跟齐)与本文件顶部段落,两者连同提交一起走;发布只补 annotated tag,
9
9
  > 不再用 `npm version` 升版。
10
10
 
11
+ ## [0.16.8] — 未发布
12
+
13
+ **桌面端的重启按钮不再骗人** —— 原先在 DSH Desktop 里它显示为可点,一点就报「重启失败:untrusted」。
14
+
15
+ ### 修复
16
+
17
+ - **根因**:桌面窗口跑在 `dsh-app://app`,请求由 Electron 主进程的 `forwardWebRequest` 转发给宿主,
18
+ 而它转发前会删掉 `origin` / `host` / `cookie` / `sec-fetch-site`。重启闸 `trustedRestartRequest`
19
+ 要求 `Origin` 存在且与 `Host` 同源,于是桌面端恒为 false;而状态接口走的是松一档的
20
+ `trustedLocalRequest`(放行无 Origin),按钮因此显示成"可点" —— 两者不一致,成了
21
+ 「看起来能用、一点就 403」。
22
+ - 新增 `detectedDesktopHost()`(判据 `DSH_CLIENT_VERSION`,Electron 主进程只发给桌面端宿主;
23
+ 刻意不吃 `ELECTRON_RUN_AS_NODE` —— 那是「Electron 当 Node 用」的通用标记,会误伤别的 Electron 应用):
24
+ 桌面端 `/restart/status` 返回 `allowed: false, reason: 'desktop'`,按钮置灰并提示
25
+ 「桌面端宿主由 DeepSeek Harness App 管理:退出 App 后重新打开即可」;`POST /restart` 补上同一档 403,
26
+ 且**排在来源闸之前**,免得用户继续看到没头没尾的 untrusted。
27
+ - 与上游 dshmarket 口径一致:它在桌面端把重启标成 `supported: false` / `managedBy: 'desktop-host'`。
28
+ 桌面端也确实不该自己重启 —— 宿主一退出,Electron 主进程立刻 `fail("dsh desktop host stopped")`
29
+ 进 fatal 恢复;插件另起的替代进程还拿不到主进程持有的 hostCookie(窗口会 401)。
30
+ - 冒烟:新增 4 条桌面端断言(含「只认 `DSH_CLIENT_VERSION`」与「非桌面端的无 Origin 请求仍被来源闸拒掉」
31
+ 两条边界,后者保证这一档没把来源闸整体顶掉),host-smoke 114 → 118 项;`docs/DEVELOPMENT.md`
32
+ 自检表的项数同步。
33
+
34
+ ### 备注
35
+
36
+ - **桌面端其余功能不受影响**:`/config`、`/send-file`、`/restart/status` 走的都是放行无 Origin 的那道闸,
37
+ 一直是正常的;桌面端只有「重启」这一项走不通。
38
+ - 桌面端要重启宿主,用 App 自己的方式:退出 App 后重新打开(开发模式下 App 菜单里另有
39
+ 「重启应用与 Host」,正式版没有这一项)。
40
+
11
41
  ## [0.16.7] — 未发布
12
42
 
13
43
  **README 精简** —— 357 行 → 55 行。
@@ -53,7 +53,7 @@ npm run build # tools/build-client.mjs → lib/client.js(esbuild)
53
53
  | --- | --- | --- |
54
54
  | `npm run typecheck` | `tsc -p tsconfig.json`:只检查标了 `// @ts-check` 的自研 Host 文件(`lib/schedule*.js`),不碰构建产物 | 见下 |
55
55
  | `npm run smoke` | 模拟 `__ModuleLoader__` + 假 React/DOM 跑客户端逻辑:打标、未读判定、cron 模型往返、样式表结构、布局度量、remote 方法集合、提示音、界面外观覆盖规则、设置卡片分组与投递加固两项的呈现、apply 分阶段降级 | 81 项 |
56
- | `npm run host-smoke` | host 侧纯函数与真实引擎:图片嗅探、投递方式、错误翻译、投递加固(口令 / 目录白名单)、重启安全闸、平台适配 spawn、文案表、来源闸,以及用真实调度引擎跑的定时任务用例(执行窗口并发变更、迁移诊断自动合并 / 仍搁浅、合并幂等性 = 删任务后提示消失 / migration 字段穿过 persist、损坏文件留存、改名迁移与幂等) | 114 项 |
56
+ | `npm run host-smoke` | host 侧纯函数与真实引擎:图片嗅探、投递方式、错误翻译、投递加固(口令 / 目录白名单)、重启安全闸、平台适配 spawn、文案表、来源闸,以及用真实调度引擎跑的定时任务用例(执行窗口并发变更、迁移诊断自动合并 / 仍搁浅、合并幂等性 = 删任务后提示消失 / migration 字段穿过 persist、损坏文件留存、改名迁移与幂等) | 118 项 |
57
57
  | `npm run render-smoke` | 客户端组件真实渲染断言(配置卡片、定时任务面板、界面外观组、标题栏刷新按钮) | 10 项 |
58
58
  | `npm run preview` | 把呼吸灯样式抽成预览页,调动画参数用 | — |
59
59
  | `npm run verify` | `typecheck` + `build` + 上面三个冒烟 | — |
package/lib/client.js CHANGED
@@ -23481,6 +23481,8 @@ function restartDisabledTitle(reason) {
23481
23481
  return "\u5BBF\u4E3B\u6B63\u88AB\u8C03\u8BD5\u5668\u9644\u7740\uFF0C\u4E0D\u80FD\u91CD\u542F";
23482
23482
  case "supervisor":
23483
23483
  return "\u5BBF\u4E3B\u7531 systemd \u7B49\u76D1\u7763\u5668\u7BA1\u7406\uFF0C\u91CD\u542F\u529F\u80FD\u5DF2\u7981\u7528";
23484
+ case "desktop":
23485
+ return "\u684C\u9762\u7AEF\u5BBF\u4E3B\u7531 DeepSeek Harness App \u7BA1\u7406\uFF1A\u9000\u51FA App \u540E\u91CD\u65B0\u6253\u5F00\u5373\u53EF";
23484
23486
  case "untrusted":
23485
23487
  return "\u8BF7\u6C42\u6765\u6E90\u4E0D\u662F\u540C\u6E90\u73AF\u56DE";
23486
23488
  default:
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import { IMAGE_EXTENSIONS, sniffImageKind } from '../src/host/image-sniff.js';
34
34
  import {
35
35
  detectedDebugger,
36
+ detectedDesktopHost,
36
37
  detectedSupervisor,
37
38
  scheduleRestart,
38
39
  servingPort,
@@ -713,8 +714,13 @@ async function handleRestartStatusRequest(request, response) {
713
714
  const restartEnabled = settings.restartEnabled !== false;
714
715
  const supervisor = detectedSupervisor();
715
716
  const debuggerAttached = detectedDebugger();
717
+ const desktopHost = detectedDesktopHost();
716
718
  let reason = null;
719
+ // 桌面端排在 debugger / supervisor 之前:它是"这一档从根本上不该点",
720
+ // 而调试器/监督器是"这台机器上的偶然状态"。桌面端打开宿主调试端口时两者会同时命中,
721
+ // 那时该告诉用户的是前者。
717
722
  if (!restartEnabled) reason = 'disabled-in-config';
723
+ else if (desktopHost !== null) reason = 'desktop';
718
724
  else if (debuggerAttached !== null) reason = 'debugger';
719
725
  else if (supervisor !== null) reason = 'supervisor';
720
726
  json(response, 200, {
@@ -722,14 +728,15 @@ async function handleRestartStatusRequest(request, response) {
722
728
  reason,
723
729
  supervisor,
724
730
  debugger: debuggerAttached,
731
+ desktopHost,
725
732
  restartEnabled,
726
733
  });
727
734
  }
728
735
 
729
736
  /**
730
737
  * POST /api/dsh-helper/restart
731
- * 过四道闸:restartEnabled / trustedRestartRequest / detectedDebugger / detectedSupervisor
732
- * 通过则 scheduleRestart(port),本进程 500ms 后自杀。
738
+ * 过五道闸:restartEnabled / detectedDesktopHost / trustedRestartRequest /
739
+ * detectedDebugger / detectedSupervisor,通过则 scheduleRestart(port),本进程 500ms 后自杀。
733
740
  */
734
741
  async function handleRestartRequest(request, response) {
735
742
  if (request.method !== 'POST') {
@@ -742,6 +749,13 @@ async function handleRestartRequest(request, response) {
742
749
  json(response, 403, { error: 'disabled-in-config', reason: 'disabled-in-config' });
743
750
  return;
744
751
  }
752
+ // 桌面端必须排在 trustedRestartRequest **之前**:Electron 的 forwardWebRequest 转发时
753
+ // 会剥掉 Origin,这一档在桌面端永远过不了来源闸,先判桌面端才能给出说得通的理由
754
+ // (否则用户看到的是没头没尾的 untrusted,而按钮又是亮的)。
755
+ if (detectedDesktopHost() !== null) {
756
+ json(response, 403, { error: 'desktop', reason: 'desktop' });
757
+ return;
758
+ }
745
759
  if (!trustedRestartRequest(request)) {
746
760
  json(response, 403, { error: 'untrusted', reason: 'untrusted' });
747
761
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-helper",
3
- "version": "0.16.7",
3
+ "version": "0.16.8",
4
4
  "type": "module",
5
5
  "description": "给 DeepSeek Harness 打补丁:在任意会话把文件主动投递到 IM(微信)、按 cron 跑定时任务、工作区活动呼吸灯(含折叠未读提示)、会话标题栏活跃指示、对话完成提示音,另有一键重启按钮。",
6
6
  "icon": "icon.svg",
@@ -7,10 +7,13 @@
7
7
  * - scheduleRestart 不接受 recovery 参数(不写 .recovery.json)
8
8
  * - restartHelperSource 不带 recovery 移交分支
9
9
  * - 删除 recoveryScriptPath(依赖 lib/recovery.js 产物,本插件无产物)
10
+ * - 新增 detectedDesktopHost:dshmarket 靠配置里的 desktopHost 标记达成同一件事,
11
+ * 本插件没有那份配置,改成从宿主环境自己认
10
12
  *
11
13
  * 安全闸逻辑一行不漏照搬:
12
14
  * - supervisor: INVOCATION_ID||JOURNAL_STREAM 且 (ppid===1 || comm==='systemd')
13
15
  * - debugger: inspector.url() || execArgv/NODE_OPTIONS 按 token 前缀匹配
16
+ * - desktop: DSH_CLIENT_VERSION 非空(Electron 主进程只发给桌面端宿主)
14
17
  * - 请求来源: trustedRestartRequest(独立文件)
15
18
  */
16
19
 
@@ -81,6 +84,35 @@ export function detectedSupervisor(
81
84
  return null;
82
85
  }
83
86
 
87
+ /**
88
+ * 桌面端宿主检测(DSH Desktop)。
89
+ *
90
+ * 判据取 `DSH_CLIENT_VERSION`:Electron 主进程在 spawn 宿主进程时注入它
91
+ * (app.asar/lib/main.js 里构造 DesktopHostProcess 的那处 env),`dsh web`、
92
+ * 内置 CLI、headless 都不会有。比 `ELECTRON_RUN_AS_NODE` 精确 —— 后者是所有
93
+ * "把 Electron 当 Node 用"的通用标记,别的 Electron 应用里跑 dsh 也会命中。
94
+ *
95
+ * 为什么桌面端要单独判一档(2026-10-06 实测定案):桌面窗口跑在 `dsh-app://app`,
96
+ * 请求由 Electron 主进程的 forwardWebRequest 转发给宿主,而它**转发前会删掉
97
+ * origin / host / cookie / sec-fetch-site**。于是重启闸(trustedRestartRequest
98
+ * 要求 Origin 存在且与 Host 同源)在桌面端恒为 false,用户点按钮只会拿到
99
+ * `403 untrusted`;而 status 走的是松一档的 trustedLocalRequest(放行无 Origin),
100
+ * 按钮反倒显示成"可点"——两者不一致,就成了"看起来能用、一点就报错"。
101
+ * 上游 dshmarket 在 routes.ts 里把桌面端的 restart 直接标成
102
+ * `supported: false` / `managedBy: 'desktop-host'`,本插件补上同一档。
103
+ *
104
+ * 桌面端也确实不该自己重启:宿主一退出,Electron 主进程立刻
105
+ * `fail("dsh desktop host stopped")` 进 fatal 恢复;插件另起的替代进程还拿不到
106
+ * 主进程持有的 hostCookie(窗口会 401)。正确做法是让用户从 App 侧退出后重开。
107
+ *
108
+ * @returns {'desktop' | null}
109
+ */
110
+ export function detectedDesktopHost(env = process.env) {
111
+ const version = env.DSH_CLIENT_VERSION;
112
+ if (typeof version !== 'string' || version.trim() === '') return null;
113
+ return 'desktop';
114
+ }
115
+
84
116
  /**
85
117
  * 检测器附着检测。按以下优先级:
86
118
  * 1. inspector.url() 已设置(覆盖 --inspect 启动 / inspector.open() / SIGUSR1 attach)