dsh-zen-remote 2.3.10 → 2.4.0

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.
Files changed (244) hide show
  1. package/CHANGELOG.md +44 -72
  2. package/README.md +94 -84
  3. package/README.zh-CN.md +90 -81
  4. package/bin/zen-remote.mjs +5 -0
  5. package/docs/interface.md +6 -6
  6. package/docs/remote-access.en.md +6 -4
  7. package/docs/remote-access.md +6 -4
  8. package/dsh-push.mjs +96 -10
  9. package/lib/adapters/dsh-better-sidebar/index.js +7 -3
  10. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  11. package/lib/admin-routes.js +10 -2
  12. package/lib/admin-routes.js.map +1 -1
  13. package/lib/cli/args.js +251 -0
  14. package/lib/cli/args.js.map +1 -0
  15. package/lib/cli/bootstrap.js +449 -0
  16. package/lib/cli/bootstrap.js.map +1 -0
  17. package/lib/cli/command-setup.js +484 -0
  18. package/lib/cli/command-setup.js.map +1 -0
  19. package/lib/cli/commands-client.js +359 -0
  20. package/lib/cli/commands-client.js.map +1 -0
  21. package/lib/cli/commands-host.js +1108 -0
  22. package/lib/cli/commands-host.js.map +1 -0
  23. package/lib/cli/commands-status.js +279 -0
  24. package/lib/cli/commands-status.js.map +1 -0
  25. package/lib/cli/config-writer.js +334 -0
  26. package/lib/cli/config-writer.js.map +1 -0
  27. package/lib/cli/context.js +10 -0
  28. package/lib/cli/context.js.map +1 -0
  29. package/lib/cli/i18n.js +620 -0
  30. package/lib/cli/i18n.js.map +1 -0
  31. package/lib/cli/ipc-client.js +160 -0
  32. package/lib/cli/ipc-client.js.map +1 -0
  33. package/lib/cli/ipc-server.js +464 -0
  34. package/lib/cli/ipc-server.js.map +1 -0
  35. package/lib/cli/main.js +245 -0
  36. package/lib/cli/main.js.map +1 -0
  37. package/lib/cli/qr-terminal.js +48 -0
  38. package/lib/cli/qr-terminal.js.map +1 -0
  39. package/lib/client-data/qr.js +286 -0
  40. package/lib/client-data/qr.js.map +1 -0
  41. package/lib/client-routes.js +308 -25
  42. package/lib/client-routes.js.map +1 -1
  43. package/lib/client.js +1013 -229
  44. package/lib/config.js +50 -12
  45. package/lib/config.js.map +1 -1
  46. package/lib/debug-report.js +28 -1
  47. package/lib/debug-report.js.map +1 -1
  48. package/lib/fetch-route-intercept.js +334 -26
  49. package/lib/fetch-route-intercept.js.map +1 -1
  50. package/lib/index.js +573 -366
  51. package/lib/index.js.map +1 -1
  52. package/lib/intercept.js +1833 -280
  53. package/lib/intercept.js.map +1 -1
  54. package/lib/lan-gate-server.cjs +36 -10
  55. package/lib/merge-streams.js +103 -11
  56. package/lib/merge-streams.js.map +1 -1
  57. package/lib/plugin-route-intercept.js +93 -63
  58. package/lib/plugin-route-intercept.js.map +1 -1
  59. package/lib/relay-access.js +8 -1
  60. package/lib/relay-access.js.map +1 -1
  61. package/lib/relay-client.js +133 -4
  62. package/lib/relay-client.js.map +1 -1
  63. package/lib/relay-pool.js +275 -0
  64. package/lib/relay-pool.js.map +1 -0
  65. package/lib/relay-server.js +269 -17
  66. package/lib/relay-server.js.map +1 -1
  67. package/lib/restart-fields.js +6 -4
  68. package/lib/restart-fields.js.map +1 -1
  69. package/lib/share-store.js +65 -27
  70. package/lib/share-store.js.map +1 -1
  71. package/lib/state-store.js +400 -0
  72. package/lib/state-store.js.map +1 -0
  73. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
  74. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  75. package/lib/types/admin-routes.d.ts.map +1 -1
  76. package/lib/types/cli/args.d.ts +78 -0
  77. package/lib/types/cli/args.d.ts.map +1 -0
  78. package/lib/types/cli/bootstrap.d.ts +170 -0
  79. package/lib/types/cli/bootstrap.d.ts.map +1 -0
  80. package/lib/types/cli/command-setup.d.ts +72 -0
  81. package/lib/types/cli/command-setup.d.ts.map +1 -0
  82. package/lib/types/cli/commands-client.d.ts +36 -0
  83. package/lib/types/cli/commands-client.d.ts.map +1 -0
  84. package/lib/types/cli/commands-host.d.ts +82 -0
  85. package/lib/types/cli/commands-host.d.ts.map +1 -0
  86. package/lib/types/cli/commands-status.d.ts +46 -0
  87. package/lib/types/cli/commands-status.d.ts.map +1 -0
  88. package/lib/types/cli/config-writer.d.ts +92 -0
  89. package/lib/types/cli/config-writer.d.ts.map +1 -0
  90. package/lib/types/cli/context.d.ts +39 -0
  91. package/lib/types/cli/context.d.ts.map +1 -0
  92. package/lib/types/cli/i18n.d.ts +32 -0
  93. package/lib/types/cli/i18n.d.ts.map +1 -0
  94. package/lib/types/cli/ipc-client.d.ts +83 -0
  95. package/lib/types/cli/ipc-client.d.ts.map +1 -0
  96. package/lib/types/cli/ipc-server.d.ts +113 -0
  97. package/lib/types/cli/ipc-server.d.ts.map +1 -0
  98. package/lib/types/cli/main.d.ts +57 -0
  99. package/lib/types/cli/main.d.ts.map +1 -0
  100. package/lib/types/cli/qr-terminal.d.ts +17 -0
  101. package/lib/types/cli/qr-terminal.d.ts.map +1 -0
  102. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
  103. package/lib/types/client/current-session-report.d.ts +4 -0
  104. package/lib/types/client/current-session-report.d.ts.map +1 -1
  105. package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
  106. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  107. package/lib/types/client/locales.d.ts +47 -3
  108. package/lib/types/client/locales.d.ts.map +1 -1
  109. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  110. package/lib/types/client/settings/settings-css.d.ts +1 -1
  111. package/lib/types/client/settings/settings-css.d.ts.map +1 -1
  112. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  113. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  114. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
  115. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
  116. package/lib/types/client/wizard/wizard-css.d.ts +1 -1
  117. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
  118. package/lib/types/client-data/model-group-side.d.ts +23 -2
  119. package/lib/types/client-data/model-group-side.d.ts.map +1 -1
  120. package/lib/types/client-data/remote-status.d.ts +47 -1
  121. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  122. package/lib/types/client-data/settings-form.d.ts +85 -1
  123. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  124. package/lib/types/client-data/wizard.d.ts +56 -1
  125. package/lib/types/client-data/wizard.d.ts.map +1 -1
  126. package/lib/types/client-routes.d.ts +25 -1
  127. package/lib/types/client-routes.d.ts.map +1 -1
  128. package/lib/types/config.d.ts +40 -14
  129. package/lib/types/config.d.ts.map +1 -1
  130. package/lib/types/debug-report.d.ts.map +1 -1
  131. package/lib/types/fetch-route-intercept.d.ts +66 -8
  132. package/lib/types/fetch-route-intercept.d.ts.map +1 -1
  133. package/lib/types/index.d.ts +9 -2
  134. package/lib/types/index.d.ts.map +1 -1
  135. package/lib/types/intercept.d.ts +72 -10
  136. package/lib/types/intercept.d.ts.map +1 -1
  137. package/lib/types/merge-streams.d.ts +79 -1
  138. package/lib/types/merge-streams.d.ts.map +1 -1
  139. package/lib/types/plugin-route-intercept.d.ts +25 -9
  140. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  141. package/lib/types/relay-access.d.ts +3 -1
  142. package/lib/types/relay-access.d.ts.map +1 -1
  143. package/lib/types/relay-client.d.ts +23 -2
  144. package/lib/types/relay-client.d.ts.map +1 -1
  145. package/lib/types/relay-pool.d.ts +148 -0
  146. package/lib/types/relay-pool.d.ts.map +1 -0
  147. package/lib/types/relay-server.d.ts +30 -3
  148. package/lib/types/relay-server.d.ts.map +1 -1
  149. package/lib/types/restart-fields.d.ts +5 -5
  150. package/lib/types/restart-fields.d.ts.map +1 -1
  151. package/lib/types/share-store.d.ts.map +1 -1
  152. package/lib/types/state-store.d.ts +179 -0
  153. package/lib/types/state-store.d.ts.map +1 -0
  154. package/lib/types/upload-limits.d.ts +15 -0
  155. package/lib/types/upload-limits.d.ts.map +1 -1
  156. package/lib/types/virtual-id.d.ts +21 -0
  157. package/lib/types/virtual-id.d.ts.map +1 -1
  158. package/lib/types/wizard/probe.d.ts.map +1 -1
  159. package/lib/types/wizard/proxy-snippets.d.ts +3 -0
  160. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  161. package/lib/types/wizard/routes.d.ts +1 -0
  162. package/lib/types/wizard/routes.d.ts.map +1 -1
  163. package/lib/types/wizard/status.d.ts +9 -0
  164. package/lib/types/wizard/status.d.ts.map +1 -1
  165. package/lib/types/wizard/verify.d.ts +3 -0
  166. package/lib/types/wizard/verify.d.ts.map +1 -1
  167. package/lib/upload-limits.js +15 -0
  168. package/lib/upload-limits.js.map +1 -1
  169. package/lib/virtual-id.js +60 -0
  170. package/lib/virtual-id.js.map +1 -1
  171. package/lib/wizard/probe.js +46 -1
  172. package/lib/wizard/probe.js.map +1 -1
  173. package/lib/wizard/proxy-snippets.js +14 -1
  174. package/lib/wizard/proxy-snippets.js.map +1 -1
  175. package/lib/wizard/routes.js +23 -4
  176. package/lib/wizard/routes.js.map +1 -1
  177. package/lib/wizard/status.js +21 -0
  178. package/lib/wizard/status.js.map +1 -1
  179. package/lib/wizard/verify.js +31 -8
  180. package/lib/wizard/verify.js.map +1 -1
  181. package/package.json +8 -4
  182. package/pwa/icons/badge-96.png +0 -0
  183. package/pwa/icons/badge.svg +1 -0
  184. package/pwa/manifest.json +2 -1
  185. package/pwa/sw.js +3 -1
  186. package/scripts/check-debug-report.mjs +41 -1
  187. package/scripts/check-settings-form.mjs +14 -3
  188. package/scripts/check-settings-view.mjs +247 -0
  189. package/scripts/check-wizard-ui.mjs +37 -0
  190. package/src/adapters/dsh-better-sidebar/index.ts +7 -3
  191. package/src/admin-routes.ts +10 -2
  192. package/src/cli/args.ts +289 -0
  193. package/src/cli/bootstrap.ts +569 -0
  194. package/src/cli/command-setup.ts +540 -0
  195. package/src/cli/commands-client.ts +369 -0
  196. package/src/cli/commands-host.ts +1127 -0
  197. package/src/cli/commands-status.ts +296 -0
  198. package/src/cli/config-writer.ts +354 -0
  199. package/src/cli/context.ts +38 -0
  200. package/src/cli/i18n.ts +645 -0
  201. package/src/cli/ipc-client.ts +216 -0
  202. package/src/cli/ipc-server.ts +531 -0
  203. package/src/cli/main.ts +285 -0
  204. package/src/cli/qr-terminal.ts +49 -0
  205. package/src/client/RemoteStatusIcon.tsx +4 -1
  206. package/src/client/current-session-report.ts +28 -4
  207. package/src/client/effects/model-group-side.ts +19 -5
  208. package/src/client/effects/remote-session.ts +26 -6
  209. package/src/client/locales.ts +96 -6
  210. package/src/client/settings/SettingsSection.tsx +287 -92
  211. package/src/client/settings/settings-css.ts +18 -0
  212. package/src/client/styles/remote-session.css.ts +15 -1
  213. package/src/client/wizard/GatewayWizard.tsx +470 -109
  214. package/src/client/wizard/cards/ProxyCard.tsx +2 -1
  215. package/src/client/wizard/wizard-css.ts +23 -0
  216. package/src/client-data/model-group-side.ts +37 -10
  217. package/src/client-data/remote-status.ts +112 -17
  218. package/src/client-data/settings-form.ts +164 -8
  219. package/src/client-data/wizard.ts +112 -2
  220. package/src/client-routes.ts +334 -26
  221. package/src/config.ts +62 -16
  222. package/src/debug-report.ts +27 -1
  223. package/src/fetch-route-intercept.ts +381 -32
  224. package/src/index.ts +575 -370
  225. package/src/intercept.ts +1866 -286
  226. package/src/merge-streams.ts +155 -10
  227. package/src/plugin-route-intercept.ts +120 -71
  228. package/src/relay-access.ts +14 -3
  229. package/src/relay-client.ts +163 -5
  230. package/src/relay-pool.ts +387 -0
  231. package/src/relay-server.ts +312 -16
  232. package/src/restart-fields.ts +6 -4
  233. package/src/share-store.ts +59 -26
  234. package/src/state-store.ts +428 -0
  235. package/src/upload-limits.ts +16 -0
  236. package/src/virtual-id.ts +57 -0
  237. package/src/wizard/probe.ts +54 -2
  238. package/src/wizard/proxy-snippets.ts +17 -2
  239. package/src/wizard/routes.ts +24 -5
  240. package/src/wizard/status.ts +28 -0
  241. package/src/wizard/verify.ts +33 -11
  242. package/AGENTS.md +0 -185
  243. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
  244. package/docs/adr/0002-relay-emulates-websocket.md +0 -8
package/README.zh-CN.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <h1 align="center">dsh-zen-remote</h1>
2
- <p align="center">一个 DeepSeek Harness 插件、两种角色。<b>主服务端</b>把 DSH 变成手机能安全访问的 PWA(手机界面、配对网关、锁屏推送),并把选定的会话共享给已配对的桌面端;<b>子客户端</b>跑在另一台 DSH 桌面端里,把服务端共享的会话直接摆进自己的侧边栏——完整历史、实时进度、发消息、审批、终端,全部在服务端执行。</p>
2
+ <p align="center">一个 DeepSeek Harness 插件、两种角色。<b>主服务端</b>把 DSH 变成手机能安全访问的 PWA(手机界面、配对网关、锁屏推送),并把选定的会话共享给已配对的桌面端;<b>子客户端</b>跑在另一台 DSH 桌面端里,可同时连接<b>最多 8 台主服务端</b>,把服务端共享的会话直接摆进自己的侧边栏——完整历史、实时进度、发消息、审批、终端,全部在服务端执行。</p>
3
3
 
4
4
  <p align="center">
5
5
  <a href="https://www.npmjs.com/package/dsh-zen-remote"><img src="https://img.shields.io/npm/v/dsh-zen-remote?style=flat-square&color=5B4CF0&logo=npm&label=npm" alt="npm version"></a>
@@ -21,7 +21,7 @@
21
21
  | ![会话列表主屏](assets/home.png) | ![会话页](assets/session.png) | ![会话信息卡](assets/info.png) |
22
22
 
23
23
  | composer 权限 sheet | 公网设备看到的配对页 |
24
- | --- | --- |
24
+ | --- | --- | --- |
25
25
  | ![composer 权限 sheet](assets/sheet.png) | ![配对页](assets/pairing.png) |
26
26
 
27
27
  > 截图为 390×844 手机视口、浅色主题;深浅主题均适配。配对页由网关绘制,按 DSH 官方风格、跟随系统浅色 / 深色。
@@ -32,8 +32,8 @@
32
32
 
33
33
  1. [安装](#1-安装)
34
34
  2. [快速上手(一体化配置与配对向导)](#2-快速上手一体化配置与配对向导)
35
- - [场景 A:配置主服务端并配对手机 / 电脑](#场景-a作为主服务端开放手机与另一台电脑连入)
36
- - [场景 B:配置另一台电脑作为桌面客户端](#场景-b作为桌面客户端连接远端服务端)
35
+ - [场景 A:作为主服务端(开放手机与另一台电脑连入)](#场景-a作为主服务端开放手机与另一台电脑连入)
36
+ - [场景 B:作为桌面客户端(连接最多 8 台远端服务端)](#场景-b作为桌面客户端连接最多-8-台远端服务端)
37
37
  3. [日常使用:会话共享与跨端协作](#3-日常使用会话共享与跨端协作)
38
38
  4. [锁屏推送:通知什么时候会响](#4-锁屏推送通知什么时候会响)
39
39
  5. [功能一览](#5-功能一览)
@@ -52,11 +52,44 @@
52
52
 
53
53
  ```sh
54
54
  dsh plugin --profile web add dsh-zen-remote
55
- # 然后重启 dsh web
55
+ # 然后重启 dsh web(或直接运行 npx dsh-zen-remote setup 一键完成安装、拉起与向导配置)
56
56
  ```
57
57
 
58
58
  > `dsh plugin` 必须带 `--profile` 指定目标 profile。桌面端的 `desktop` profile 由 App 独占管理,命令行会拒绝 `--profile desktop`,桌面端请直接使用上面的界面安装。
59
59
 
60
+ ### 无头服务器 CLI 一键向导(`dsh-zen-remote`)
61
+
62
+ 插件自带同包分发的命令行工具,远程 VPS 与无头环境(`dsh web`)无需打开浏览器插件页,在终端即可完成全部接入配置、三灯连通验证与扫码 / 配对码配对:
63
+
64
+ ```sh
65
+ npx dsh-zen-remote setup # 三步交互向导:选角色 → 选接入方式 → 三灯验证 + 终端二维码 / 桌面配对码
66
+ dsh-zen-remote status # 查看当前角色、网关与连接状态(--json 机器可读;--debug 导出脱敏诊断报告)
67
+ ```
68
+
69
+ <details>
70
+ <summary>展开查看完整 CLI 子命令清单(<code>host</code> / <code>client</code> / 非交互参数)</summary>
71
+
72
+ ```sh
73
+ # 主服务端命令(role 为 host 时可用)
74
+ dsh-zen-remote host device pair --device-role web # 生成手机/浏览器配对二维码与 8 位码(或 desktop-client)
75
+ dsh-zen-remote host device list # 列出全部已配对设备(支持 --json)
76
+ dsh-zen-remote host device revoke <id|--all> [--yes] # 吊销单台或全部已配对设备
77
+ dsh-zen-remote host ingress start --mode lan # 配置并启动公网入口(caddy | cloudflared | external | lan)
78
+ dsh-zen-remote host ingress stop [--yes] # 停止托管的 Caddy / Cloudflare Tunnel 进程
79
+ dsh-zen-remote host ingress verify [--no-wait] # 三灯连通自检并同时打印手机二维码与桌面客户端配对码
80
+ dsh-zen-remote host ingress snippet [--proxy nginx|caddy|lucky] # 打印反向代理配置片段
81
+
82
+ # 子客户端命令(role 为 client 时可用,最多同时连接 8 台主服务端)
83
+ dsh-zen-remote client pair http://192.168.1.2:3088 --code 12345678 [--name My-Mac]
84
+ dsh-zen-remote client list # 列出已配对的主服务端(最多 8 台)及在线状态
85
+ dsh-zen-remote client unpair <hostUrl|entryId|--all> [--yes] # 移除单台或全部已配对的主服务端
86
+ ```
87
+
88
+ - **自动预检与脚本化**:`setup` 会先自动扫描候选 profile 多选安装插件(自动跳过 `desktop`)并拉起 DSH 守护进程 / 应用,再进入三步向导;非交互脚本加 `--yes` 并附上参数(如 `setup --role host --ingress lan --no-wait --yes`)即可零提问跑完。
89
+ - **进程与权限安全**:CLI 通过本机 `0600` 权限的 IPC socket(`~/.dsh/zen-remote-cli.sock`)驱动插件进程,Caddy / Cloudflare Tunnel 始终由插件进程托管,CLI 绝不另起进程;支持 `--lang zh|en` 切换输出语言。
90
+
91
+ </details>
92
+
60
93
  <details>
61
94
  <summary>手动编辑 Profile / 本地开发(<code>link:</code> 安装)与升级、卸载说明</summary>
62
95
 
@@ -66,7 +99,7 @@ dsh plugin --profile web add dsh-zen-remote
66
99
  // ~/.dsh/profiles/desktop/package.json
67
100
  {
68
101
  "dependencies": {
69
- "dsh-zen-remote": "^2.3.10" // 本地开发换成 "link:/path/to/dsh-zen-remote"
102
+ "dsh-zen-remote": "^2.4.0" // 本地开发换成 "link:/path/to/dsh-zen-remote"
70
103
  }
71
104
  }
72
105
  ```
@@ -80,7 +113,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
80
113
  - **开着 HMR 升级插件**:先卸载再添加(`dsh plugin --profile <name> remove dsh-zen-remote` 后再 `add`),直接在运行中的旧版上覆盖安装不会被干净接管。
81
114
  - **DSH 0.2.1-alpha.3+ 模板重置**:`dsh --profile <name> --from-default-profile <模板>` 会用新副本覆盖自定义 profile,执行前请先备份 `~/.dsh/profiles/<name>`。
82
115
  - **从 1.x 升级**:2.0 起挂载层合并为 `dsh-zen-remote` 单行,主入口按角色自动加载网关与推送,请删掉 `cordis.patch.yml` 里旧的 `dsh-zen-remote-gateway` / `dsh-zen-remote-push` / `dsh-push.mjs` 行。原有 `~/.dsh/lan-gate.config.json` 与已配对设备全部自动保留,无需重配。
83
- - **彻底卸载**:在「插件」页点「卸载」后重启。若需连同配对与共享记录一并清空,删除 `~/.dsh/lan-gate-state.json`、`~/.dsh/lan-gate.config.json`、`~/.dsh/zen-remote-shares.json` 与 `~/.dsh/zen-remote-server.json`。
116
+ - **彻底卸载**:在「插件」页点「卸载」后重启。若需连同配对与共享记录一并清空,删除 `~/.dsh/lan-gate-state.json`、`~/.dsh/lan-gate.config.json` 与 `~/.dsh/zen-remote-state.json`(旧版本另有 `~/.dsh/zen-remote-shares.json` 与 `~/.dsh/zen-remote-server.json`——两者会在首次启动时自动迁入状态文件并被删除)。
84
117
 
85
118
  </details>
86
119
 
@@ -90,9 +123,9 @@ cd ~/.dsh/profiles/desktop && pnpm install
90
123
 
91
124
  同一个插件包含两种运行角色:
92
125
  - **作为服务端(主机,`role: host`)**:运行主力会话、启动本地安全网关与锁屏推送,允许手机浏览器/PWA 或另一台电脑连入。
93
- - **作为桌面客户端(从机,`role: client`)**:不启动网关也不抢端口,连接到远端服务端,把服务端共享的会话直接挂载到本机侧边栏。
126
+ - **作为桌面客户端(从机,`role: client`)**:不启动网关也不抢端口,可**同时连接最多 8 台远端主服务端**,把它们共享的会话直接挂载到本机侧边栏。
94
127
 
95
- 打开 DSH **「插件」页 → 点击 `dsh-zen-remote`**,页面顶部就是**「远程访问与设备配对」**向导卡片:
128
+ 打开 DSH **「插件」页 → 点击 `dsh-zen-remote`**,页面顶部就是**「远程访问与设备配对」**向导卡片(无头服务器在终端运行 `npx dsh-zen-remote setup` 即可走完同一套三步流程):
96
129
 
97
130
  ![插件详情页顶部的「远程访问与设备配对」状态总览卡片](docs/images/wizard-overview.png)
98
131
 
@@ -106,45 +139,41 @@ cd ~/.dsh/profiles/desktop && pnpm install
106
139
 
107
140
  ![配置向导第 1 步:选择本机角色与服务端接入方式](docs/images/wizard-step1-role-path.png)
108
141
 
109
- 根据你的网络环境四选一(切换接入方式时,向导会自动停止上一方式的托管进程并重置网关监听地址):
110
-
111
142
  | 接入方式 | 适合什么环境 | 向导自动为你做什么 |
112
143
  | --- | --- | --- |
113
- | **内置 Caddy(推荐)** | 有公网 IP(或已做路由器端口映射)+ 域名托管在 Cloudflare | 自动下载预编译的 `Caddy + caddy-dns/cloudflare`(中国大陆网络自动走镜像源加速),通过 DNS-01 自动申请并续期 HTTPS 证书;网关仅监听 `127.0.0.1:3088` |
114
- | **使用已有反向代理** | 机器上已跑着 Nginx、Caddy 或 Lucky | 自动生成带 WebSocket 与 `X-Forwarded-*` 头的现成配置片段,支持一键将反代源 IP 写入受信任代理列表(手配完整说明见 [docs/remote-access.md](docs/remote-access.md)) |
115
- | **Cloudflare Tunnel 内网穿透** | 没有公网 IP / 不想配路由器端口映射 + 域名托管在 Cloudflare | 自动下载 `cloudflared`(国内网络自动走镜像源加速),调用 Cloudflare API 自动创建或复用隧道、写入 DNS CNAME 记录并拉起隧道;网关仅监听 `127.0.0.1:3088` |
116
- | **局域网 / Tailscale 直连** | 手机或另一台电脑在同一 Wi-Fi / 局域网,或共用 Tailscale 虚拟局域网 | 无需域名与公网证书;自动识别本机局域网 / Tailscale IPv4 地址并监听 `0.0.0.0:3088`,同时网关内置**私网源 IP 铁闸**,直接拒绝任何公网 IP 的未代理明文直连(`403 public-direct-forbidden`) |
144
+ | **内置 Caddy(推荐)** | 有公网 IP(或已做端口映射)+ 域名在 Cloudflare | 自动下载预编译 `Caddy + caddy-dns/cloudflare`(国内网络镜像加速),通过 DNS-01 自动申请并续期证书(仅监听 `127.0.0.1:3088`) |
145
+ | **使用已有反向代理** | 已跑着 Nginx、Caddy 或 Lucky | 自动生成反代配置片段,支持一键将反代源 IP 写入受信任列表([手配指南](docs/remote-access.md)) |
146
+ | **Cloudflare Tunnel 内网穿透** | 无公网 IP / 不做端口映射 + 域名在 Cloudflare | 自动下载 `cloudflared`、创建或复用隧道、写入 DNS CNAME 并拉起隧道(仅监听 `127.0.0.1:3088`) |
147
+ | **局域网 / Tailscale 直连** | 同一 Wi-Fi / 局域网,或共用 Tailscale 虚拟网 | 自动识别本机局域网 / Tailscale IPv4 并监听 `0.0.0.0:3088`,内置**私网源 IP 铁闸**直接拒绝公网 IP 未代理直连(`403 public-direct-forbidden`) |
117
148
 
118
149
  #### 第 2 步:填写连接参数
119
- - **内置 Caddy / Cloudflare Tunnel**:填写公开访问域名(如 `dsh.example.com`)并粘贴 Cloudflare API 令牌(点击输入框旁的「如何获取 API 令牌?」可查看分步指引与权限表;域名尚未接入 Cloudflare 可点底部「域名尚未接入 Cloudflare?」查看迁入步骤)。
120
- - **使用已有反向代理**:填写反代对外提供的 `https://` 地址,点击「查看 Nginx / Caddy / Lucky 配置片段」复制到你的反代中。
121
- - **局域网 / Tailscale 直连**:向导会自动检测并预填 `http://<本机局域网或Tailscale IP>:3088`,确认即可。
150
+ - **内置 Caddy / Cloudflare Tunnel**:填写公开域名(如 `dsh.example.com`)并粘贴 Cloudflare API 令牌。
151
+ - **使用已有反向代理**:填写外部 `https://` 地址,复制生成的 Nginx / Caddy / Lucky 配置片段。
152
+ - **局域网 / Tailscale 直连**:直接确认向导自动检测并预填的 `http://<本机局域网或Tailscale IP>:3088`。
122
153
 
123
154
  #### 第 3 步:连通验证与扫码 / 配对码配对
124
155
 
125
156
  ![配置向导第 3 步:自动连通性检测、手机扫码配对与桌面客户端配对](docs/images/wizard-step3-verify-pair.png)
126
157
 
127
- 1. **启动入口服务 & 自动连通自检**:
128
- - 若选了「内置 Caddy」或「Cloudflare Tunnel」,先点顶部状态栏的**「启动 Caddy」**或**「创建并启动隧道」**。
129
- - 向导会自动探测「本地网关就绪」与「访问地址连通」(自动验证外部访问 `/` 返回 `401` 配对页且 `/lan-gate/admin` 返回 `403` 隔离)。
130
- 2. **配对手机 / 浏览器(Web 应用端)**:
131
- - 保持在「配对手机 / 浏览器(扫码)」标签页,点击**「生成配对二维码」**。
132
- - 用手机相机或浏览器扫码打开配对页,输入屏幕上的 8 位配对码即可进入 DSH;随后在手机浏览器菜单点**「添加到主屏幕」**安装为独立 PWA,并允许通知权限以接收锁屏推送。
133
- 3. **配对另一台电脑(桌面客户端)**:
134
- - 切换到**「配对另一台电脑(桌面客户端)」**标签页,点击**「生成桌面配对码」**,然后点击**「复制地址与配对码」**发给另一台电脑。
158
+ 1. **启动入口服务 & 自动连通自检**:点击顶部状态栏**「启动 Caddy」**或**「创建并启动隧道」**(如适用),向导自动探测「本地网关就绪」与「访问地址连通」。
159
+ 2. **配对手机 / 浏览器(Web 应用端)**:在「配对手机 / 浏览器(扫码)」标签页点**「生成配对二维码」**,手机扫码并输入 8 位配对码进入 DSH,随后点浏览器菜单**「添加到主屏幕」**安装为独立 PWA。
160
+ 3. **配对另一台电脑(桌面客户端)**:切到**「配对另一台电脑(桌面客户端)」**标签页,点**「生成桌面配对码」**并**「复制地址与配对码」**。
135
161
 
136
- > **日常快捷操作**:配置完成后,无需再进向导——直接在插件详情页顶部的总览卡片底部点**「配对手机 / 扫码」**、**「配对另一台电脑」**或**「管理设备」**(支持重命名设备、切换设备角色、单独或全部移除设备、发送测试推送)。
162
+ > **日常快捷操作**:配置完成后无需再进向导——直接在总览卡片底部点**「配对手机 / 扫码」**、**「配对另一台电脑」**或**「管理设备」**。
137
163
 
138
164
  ---
139
165
 
140
- ### 场景 B:作为桌面客户端(连接远端服务端)
166
+ ### 场景 B:作为桌面客户端(连接最多 8 台远端服务端)
141
167
 
142
- 在另一台装了 DSH 桌面端的电脑上:
168
+ 在另一台装了 DSH 桌面端的电脑上(或通过命令行 `dsh-zen-remote client pair <hostUrl> --code <8位码>`):
143
169
 
144
- 1. 打开「插件」页 → `dsh-zen-remote` → 点击**「开始配置向导」**。
170
+ 1. 打开「插件」页 → `dsh-zen-remote` → 点击**「开始配置向导」**(若已是客户端角色,可直接点**「+ 添加服务端」**)。
145
171
  2. **第 1 步**:选择**「作为桌面客户端(从机)」**,点击「下一步」。
146
172
  3. **第 2 步**:填写本机显示名称(如 `MacBook-Pro`)、主服务端的访问地址(公网必须为 `https://`,局域网 / Tailscale 可为 `http://192.168.x.x:3088` 或 `http://100.x.x.x:3088`)以及从主服务端复制来的 8 位桌面配对码,点击**「配对并连接」**。
147
173
  4. **第 3 步**:三盏状态灯(「本地配对凭证」「服务端连接」「会话与插件同步」)全绿即表示连接成功,左侧边栏会立即出现主服务端共享的工作区与会话分组。
174
+ 5. **多服务端管理(最多同时连接 8 台主服务端)**:
175
+ - 在向导第 3 步点**「+ 继续添加另一台」**、顶栏总览卡点**「+ 添加服务端」**,或打开**「管理服务端」**弹窗(及底层设置卡片)即可逐台查看在线状态、**立即重连**、**重新配对**、**移除**或**全部移除**(配满 `8 / 8` 台时会有显眼红字提醒上限,已有地址仍可覆盖重配)。
176
+ - 各台主服务端的远程分组严格按**配对添加顺序**合并在本地工作区之后;多台同名主服务端自动编号「名称-1」「名称-2」;模型选单、标题栏连接图标、只读横幅、插件桥与附件上传均按各会话所属 `serverId` 独立隔离。
148
177
 
149
178
  ---
150
179
 
@@ -153,7 +182,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
153
182
  | | **Web 应用端(手机 / 平板 / 浏览器)** | **桌面应用端(另一台 DSH 桌面客户端)** |
154
183
  | --- | --- | --- |
155
184
  | 可见范围 | 主服务端上的**全部**会话 | **仅**主服务端已开启「远程访问」的会话(服务端中继强制隔离) |
156
- | 配对凭证 | 浏览器长期 HttpOnly Cookie | 插件配置中的加密设备令牌(Bearer Token) |
185
+ | 配对凭证 | 浏览器长期 HttpOnly Cookie | 状态文件 `~/.dsh/zen-remote-state.json` 中的设备令牌(Bearer Token) |
157
186
 
158
187
  ### 如何开启或关闭单个会话的远程共享
159
188
 
@@ -164,7 +193,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
164
193
  ![主服务端在会话的「…」菜单里开启远程](docs/images/host-share-menu.png)
165
194
 
166
195
  2. **主服务端会话页标题栏**:点击标题右侧的地球图标(悬停可查看剩余闲置休眠时间;图标带绿点代表此刻有桌面客户端正在查看);
167
- 3. **手机端(Web 应用端)会话页**:打开右上角会话信息卡片,切换**「远程访问」**开关;
196
+ 3. **手机端(Web 应用端)会话页**:打开右上角 `ⓘ` 会话信息卡片,切换**「远程访问」**开关;
168
197
 
169
198
  <p align="center"><img src="docs/images/mobile-info-remote.png" alt="手机会话信息卡片里的「远程访问」开关" width="320"></p>
170
199
 
@@ -172,11 +201,11 @@ cd ~/.dsh/profiles/desktop && pnpm install
172
201
 
173
202
  ### 在子客户端上使用远程会话
174
203
 
175
- - **侧边栏分组**:服务端至少有一个会话开启远程后,子客户端侧边栏底部会出现**「服务端名 · 工作区名」**分组,点开即可像本地会话一样查看历史、流式对话、审批工具、使用终端、查看文件树与 Git diff、下载交付物或拖拽上传附件。
204
+ - **侧边栏分组**:服务端至少有一个会话开启远程后,子客户端侧边栏底部会出现**「服务端名 · 工作区名」**分组(最多同时接入 8 台主服务端,按配对顺序排列)。点开即可像本地会话一样查看历史、流式对话、审批工具、使用服务端终端、查看文件树与 Git diff、加载正文图片、下载交付物或拖拽/粘贴/选择文件上传。
176
205
 
177
206
  ![子客户端侧边栏里的「书房 · Desktop」分组,标题行是远程连接状态图标](docs/images/client-sidebar.png)
178
207
 
179
- - **关闭与自动休眠**:开启远程的会话若连续 **48 小时**(`idleHours` 可调)无任何回合活动,会自动休眠关闭远程(正在运行或等待审批/回答的会话绝不休眠)。当会话关闭远程、或客户端主动取消配对/被服务端移除时,侧边栏会自动将其归档隐藏;正在查看的会话页会展示「远程已关闭」横幅并禁用输入,重新开启后自动恢复。
208
+ - **关闭与自动休眠**:开启远程的会话若连续 **48 小时**(`idleHours` 可调)无任何回合活动,会自动休眠关闭远程(正在运行或等待审批/回答的会话绝不休眠)。当会话关闭远程、或客户端移除配对/被服务端吊销时,侧边栏会自动将其归档隐藏;正在查看的会话页会展示「远程已关闭」横幅并禁用输入,重新开启后自动恢复。
180
209
 
181
210
  ![远程被关闭后,会话页输入框上方的「远程已关闭」横幅](docs/images/client-remote-closed.png)
182
211
 
@@ -184,35 +213,22 @@ cd ~/.dsh/profiles/desktop && pnpm install
184
213
 
185
214
  ## 4. 锁屏推送:通知什么时候会响
186
215
 
187
- 默认只在**真正需要你介入**的时候响,分为两条独立通道(推送载荷全程 `aes128gcm` 端到端加密):
216
+ 默认只在**真正需要你介入**的时候响,分为两条独立通道(推送载荷全程 `aes128gcm` 端到端加密;可通过总开关 `pushEnabled` 一键停用全部通知与 `push_notify` 工具,或开启 `pushSessionName` 在通知正文首行显示 `来源会话:<会话名>`):
188
217
 
189
- 1. **系统自动触发(恒开,不受最小间隔压制)**:
190
- - **工具等待授权**:推送「DSH 等你授权」(附带工具名)。为配合自动审批插件,默认等待 `DSH_PUSH_APPROVAL_GRACE_MS`(5 秒),5 秒内已被自动审批处理的操作不会打扰你。
218
+ 1. **系统自动触发(`pushEnabled` 开启时恒开,不受最小间隔压制)**:
219
+ - **工具等待授权**:等待 `DSH_PUSH_APPROVAL_GRACE_MS`(默认 5 秒,让自动审批插件先行处理)后仍未决时,推送「DSH 等你授权」(附带工具名)。
191
220
  - **模型调用 `ask_user_question` 等待回答**:推送「DSH 等你回答」。
192
- - 子代理会话卡在授权或提问上时同样会推送。
193
221
  2. **模型主动调用(`push_notify` 工具)**:
194
- - 当你交代「做完告诉我」、或模型遇到阻塞必须你拍板时,模型会主动调用 `push_notify` 推送到手机锁屏。
195
- - 普通回合结束默认不推送(如需每轮结束都推,可在进阶配置中打开 `pushTurnEnd`);开启 `pushSummary` 可在通知正文中附带本轮最终回复摘要(120 字以内,不含思考过程)。
222
+ - 当你交代「做完告诉我」、或模型遇到阻塞必须你拍板时,模型会主动调用 `push_notify` 推送到手机锁屏。普通回合结束默认不推送(如需每轮结束都推,可打开 `pushTurnEnd`;开启 `pushSummary` 可附带 120 字以内的最终回复摘要)。
196
223
 
197
224
  ---
198
225
 
199
226
  ## 5. 功能一览
200
227
 
201
- - **移动端 PWA 界面(主服务端)**:
202
- - 会话列表主屏 + 独立会话页两级页面栈(支持左缘右滑返回、安卓返回手势接管、冷启动首帧骨架屏与内容哈希长缓存)
203
- - 主屏第三方插件入口 chips(按已装插件自动浮现,可自定义显隐)
204
- - 底部重排 composer(图标化按钮,权限与模型切换改为底部 Sheet,支持手机本地拍照/文件上传)
205
- - 会话信息卡(六格 token/耗时统计、导出日志、重命名、Fork、归档、一键导出 PNG 分享长图、远程访问开关)
206
- - **一体化配置与配对向导(2.3 新增)**:
207
- - 插件详情页顶部集成 2×2 实时状态总览与三步向导弹窗,统一管理服务端四路接入(内置 Caddy、已有反代、Cloudflare Tunnel、局域网/Tailscale 直连)与桌面客户端配对
208
- - 预编译 `Caddy + caddy-dns/cloudflare` 与 `cloudflared` 自动下载,内置中国大陆网络自动探测与镜像加速(`gh-proxy.com` / `ghfast.top`)、连接超时与断流自动切换源,并由 GitHub Actions 每日巡检跟随上游版本自动更新
209
- - 局域网 / Tailscale 模式自动探测本机内网 IP 自检,网关内置私网源 IP 铁闸拦截公网未代理明文直连
210
- - 独立「已配对设备管理」弹窗,以及客户端解配/吊销时自动归档清理侧边栏残留会话
211
- - **跨桌面端远程会话(桌面客户端)**:
212
- - 侧边栏远程工作区分组,支持完整对话流、审批/提问抢答同步、服务端模型目录同步、文件树与改动 diff、服务端远程终端
213
- - 交付物分段流式下载到本机、服务端 LibreOffice 渲染 Office 文档 PDF 预览、最大 500 MiB 非图片大文件流式上传与拖拽文件自动转上传;远程会话中回形针与粘贴的非图片文件同样自动转字节上传(不再留下读不到的本机路径引用)
214
- - **第三方插件桥(Plugin Bridge)**:
215
- - 协作插件登记桥接声明或通过内置适配器(如 `dsh-better-sidebar`),即可在子客户端的远程会话中直接读写主服务端的数据面板(整机数据需服务端主人在设置页逐插件授权)。详见 [插件桥文档](docs/plugin-bridge.md)。
228
+ - **移动端 PWA 界面(主服务端)**:会话列表主屏 + 独立会话页两级页面栈(左缘右滑返回、安卓返回手势接管、冷启动首帧骨架屏与内容哈希长缓存)、主屏插件入口 chips 自动发现、底部重排 composer(图标化按钮、底部权限/模型 Sheet、手机拍照/文件上传)、会话信息卡(六格统计含 `tok/s` 输出速度、导出日志、重命名、Fork、归档、一键导出 PNG 分享长图、远程访问开关)。
229
+ - **一体化配置向导与无头 CLI(`dsh-zen-remote`)**:插件详情页顶部 2×2 实时状态总览、三步向导弹窗,以及同包命令行工具(`setup` / `status` / `host` / `client`),统一覆盖服务端四路接入(内置 Caddy 自动证书、已有反代、Cloudflare Tunnel、局域网/Tailscale 直连 + 私网源 IP 铁闸)与客户端配对。
230
+ - **跨桌面端远程会话与多服务端连接池(桌面客户端)**:支持同时连接**最多 8 台主服务端**(按配对顺序合并分组、同名自动编号),提供完整历史与流式对话、审批/提问同步、按 `serverId` 隔离的服务端模型选单、文件树与改动 diff、双工服务端远程终端(`terminal/attach`)、正文内联图片转发、交付物分段下载、服务端 LibreOffice PDF 预览、最大 500 MiB 非图片大文件流式上传(拖拽、回形针选择、粘贴全覆盖)及一键导出脱敏诊断报告。
231
+ - **第三方插件桥(Plugin Bridge)**:协作插件登记桥接声明或通过内置适配器(如 `dsh-better-sidebar` `0.25.0`–`0.30.1`),即可在远程会话中直接读写主服务端的数据面板(整机数据需服务端主人逐插件授权)。详见 [插件桥文档](docs/plugin-bridge.md)。
216
232
 
217
233
  深度说明:[界面设计文档](docs/interface.md) · [公网接入与中继协议](docs/remote-access.md)
218
234
 
@@ -220,21 +236,21 @@ cd ~/.dsh/profiles/desktop && pnpm install
220
236
 
221
237
  ## 6. 进阶配置与字段参考
222
238
 
223
- 日常使用只需通过插件详情页顶部的**「远程访问与设备配对」向导**即可完成全部配置。若需微调底层参数,可在 `dsh-zen-remote` 插件详情页的**「包含的组件」**下再次点击 `dsh-zen-remote` 展开底层配置卡片。
224
-
225
- <details>
226
- <summary>查看底层配置卡片截图与全部字段参考表</summary>
227
-
228
- 每个字段按此顺序取第一个合法值:**环境变量 > 插件行设置 > `~/.dsh/lan-gate.config.json` > 默认值**。修改需要重启网关/推送的字段后插件会自动重载,无需重启 App。
239
+ 日常使用只需通过插件详情页顶部的**「远程访问与设备配对」向导**(或终端 `npx dsh-zen-remote setup`)即可完成全部配置。若需微调底层参数,可在 `dsh-zen-remote` 插件详情页的**「包含的组件」**下再次点击 `dsh-zen-remote` 展开底层配置卡片。
229
240
 
230
241
  | 主服务端角色与配对卡片 | 已开启远程的会话列表 | 子客户端配置与诊断卡片 |
231
242
  | --- | --- | --- |
232
243
  | ![主服务端角色](docs/images/host-settings-role.png) ![配对与设备](docs/images/host-pairing.png) | ![已开启远程的会话](docs/images/host-settings-shares.png) | ![子客户端设置](docs/images/client-settings.png) |
233
244
 
245
+ <details>
246
+ <summary>展开查看全部配置字段参考表与环境变量</summary>
247
+
248
+ 每个字段按此顺序取第一个合法值:**环境变量 > 插件行设置 > `~/.dsh/lan-gate.config.json` > 默认值**。修改需要重启网关/推送的字段后插件会自动重载,无需重启 App。
249
+
234
250
  | 字段 | 默认值 | 说明 |
235
251
  | --- | --- | --- |
236
252
  | `role` | `host` | `host` 运行网关 + 推送 + 共享;`client` 作为桌面客户端连接远端服务端 |
237
- | `serverName` | 电脑名 | **设备名称**(≤ 40 字),主服务端作侧边栏分组标题,子客户端作配对登记名,双向自动同步 |
253
+ | `serverName` | 电脑名 | **设备名称**(≤ 40 字),主服务端作侧边栏分组标题,子客户端作配对登记名;子客户端改名后向其已配对的各主服务端单向广播(服务端不再反写) |
238
254
  | `port` | `3088` | 本地网关端口;被占用时自动向上递增重试(最多 +20) |
239
255
  | `host` | `127.0.0.1` | 网关监听地址;本机反代/隧道保持 `127.0.0.1`,局域网/Tailscale 直连设为 `0.0.0.0` |
240
256
  | `targetPort` | (自动) | 本机 DSH Web UI 端口,留空自动探测宿主实际监听端口 |
@@ -242,6 +258,8 @@ cd ~/.dsh/profiles/desktop && pnpm install
242
258
  | `trustedProxies` | 空 | 逗号分隔的受信任反代 IP(反代不在本机时配置) |
243
259
  | `vapidSubject` | `mailto:admin@localhost` | Web Push 联系标识(**iOS 必须为真实邮箱或 `https://` 网址**,否则 Apple 拒收) |
244
260
  | `lang` | `auto` | 网关页面与通知语言:`auto`(跟随浏览器)、`zh`、`en` |
261
+ | `pushEnabled` | 开 | 会话通知总开关;关闭后停用全部自动推送,也不再注册 `push_notify` 工具与提示词段落 |
262
+ | `pushSessionName` | 关 | 推送正文第一行加「来源会话:<会话名>」(标题保持固定事件标题不变) |
245
263
  | `pushTurnEnd` | 关 | 普通回合结束时是否也发送推送(等授权/等回答的通知恒开,不受此项影响) |
246
264
  | `pushEvents` | `agent/turn-stopping` | 触发回合结束推送的事件名(逗号分隔) |
247
265
  | `pushDebounceMs` | `15000` | 两条自动推送之间的最小间隔毫秒数 |
@@ -250,13 +268,13 @@ cd ~/.dsh/profiles/desktop && pnpm install
250
268
  | `idleHours` | `48` | 远程共享会话的闲置自动休眠时长(小时,范围 `(0, 8760]`) |
251
269
  | `autoShareNewSessions` | 关 | 主服务端新建的会话是否自动开启远程共享 |
252
270
  | `pluginBridgeConsent` | `{}` | 插件桥整机数据授权表(插件名 → 布尔值) |
253
- | `serverUrl`(客户端) | 空 | 远端主服务端的网关地址(公网强制 `https://`,内网允许 `http://`) |
254
- | `deviceToken`(客户端) | 空 | 配对签发的客户端设备令牌(密钥字段,界面脱敏) |
271
+ | `serverUrl`(客户端) | 空 | 远端主服务端的网关地址(公网强制 `https://`,内网允许 `http://`)。旧版单服务端字段:已配对列表现存于 `~/.dsh/zen-remote-state.json`(最多 8 台),该行凭证会在首次启动时自动迁入 |
272
+ | `deviceToken`(客户端) | 空 | 配对签发的客户端设备令牌(密钥字段,界面脱敏)。旧版单服务端字段,与 `serverUrl` 一同自动迁入状态文件;新配对走设置卡片 / 向导,令牌从不回传浏览器 |
255
273
  | `turnFoldDesktop` | 关 | 是否在桌面端宽度也默认折叠回合中间过程 |
256
274
  | `keyboardLiftRatio` / `keyboardLiftMaxPx` / `keyboardSafetyPadPx` | `0.42` / `400` / `15` | 软键盘高度不可见环境下的输入框抬升校准参数 |
257
275
  | `maxUploadBytes` | 20 MB | 手机端单次附件上传大小上限 |
258
276
 
259
- 历史环境变量(优先级高于插件行):`LAN_GATE_PORT`、`LAN_GATE_HOST`、`LAN_GATE_TARGET_PORT`、`LAN_GATE_RATE_LIMIT`、`LAN_GATE_TRUSTED_PROXIES`、`LAN_GATE_VAPID_SUBJECT`、`LAN_GATE_LANG`、`DSH_PUSH_TURN_END`、`DSH_PUSH_EVENTS`、`DSH_PUSH_DEBOUNCE_MS`、`DSH_PUSH_SUMMARY`、`DSH_PUSH_TOOL`、`DSH_PUSH_LANG`、`DSH_PUSH_APPROVAL_GRACE_MS`。
277
+ 历史环境变量(优先级高于插件行):`LAN_GATE_PORT`、`LAN_GATE_HOST`、`LAN_GATE_TARGET_PORT`、`LAN_GATE_RATE_LIMIT`、`LAN_GATE_TRUSTED_PROXIES`、`LAN_GATE_VAPID_SUBJECT`、`LAN_GATE_LANG`、`DSH_PUSH_ENABLED`、`DSH_PUSH_SESSION_NAME`、`DSH_PUSH_TURN_END`、`DSH_PUSH_EVENTS`、`DSH_PUSH_DEBOUNCE_MS`、`DSH_PUSH_SUMMARY`、`DSH_PUSH_TOOL`、`DSH_PUSH_LANG`、`DSH_PUSH_APPROVAL_GRACE_MS`。
260
278
 
261
279
  </details>
262
280
 
@@ -264,23 +282,14 @@ cd ~/.dsh/profiles/desktop && pnpm install
264
282
 
265
283
  ## 7. 架构、安全模型与已知说明
266
284
 
267
- ### 安全模型与信任边界
268
-
269
- - **已配对的桌面客户端属于受信任设备**:远程会话中的终端以**服务端当前系统用户**权限运行,文件预览接口可读取服务端进程有权限访问的文件,且可在服务端任意工作区新建会话。
270
- - **未开启远程的会话严格隔离**:未共享会话的列表、历史、流式事件、审批/提问以及内嵌的 `dsh-session:` 引用均在服务端中继层直接过滤或拒绝,绝不仅是在前端隐藏。
271
- - **公网明文直连防护**:即便在局域网模式下将网关监听设为 `0.0.0.0`,网关底层的 `isPrivateOrLoopbackIp` 铁闸也只放行本机回环、RFC1918 局域网段与 Tailscale(`100.64.0.0/10` / `fd7a:115c:a1e0::/48`)的直连请求;任何来自公网 IP 的未代理直连一律返回 `403 public-direct-forbidden`。
272
- - **管理操作仅限服务端本机**:生成配对码、修改设备角色、配置向导、吊销设备均只允许在服务端本机操作。
273
- - **数据与隐私**:插件自身不向任何第三方收集或上报数据。配对状态存于 `~/.dsh/lan-gate-state.json`,共享表存于 `~/.dsh/zen-remote-shares.json`,托管的 Cloudflare 令牌以 `0600` 权限存于 `~/.dsh/zen-remote-ingress/`。
274
-
275
- ### 版本容错
276
-
277
- 1. **中继协议版本**:主服务端与子客户端的 `dsh-zen-remote` 版本需保持一致。
278
- 2. **DSH 宿主版本**:两端 DSH 版本可以不同。连接握手时自动比对接口组规范化指纹,存在差异的分组在侧边栏标注「(版本有差异)」,仅受影响的单面板降级,其余功能照常工作。
279
-
280
- ### 已知限制与常见问题
285
+ - **受信任客户端与严格会话隔离**:已配对的桌面客户端在远程会话中以服务端系统用户权限运行终端与文件预览,而未开启远程的会话在服务端中继层被严格过滤或拒绝;即便在局域网模式下监听 `0.0.0.0`,网关内置私网源 IP 铁闸也会直接拒绝公网 IP 的未代理直连(`403 public-direct-forbidden`),且配对、向导与设备管理操作仅限在服务端本机执行。
286
+ - **版本容错**:主服务端与子客户端需保持 `dsh-zen-remote` 版本一致;两端 DSH 宿主版本可以不同(握手时自动比对接口规范化指纹,存在差异时仅受影响的单面板降级,其余功能照常工作)。
281
287
 
282
288
  <details>
283
- <summary>展开查看已知限制、iOS PWA 适配与已适配第三方插件列表</summary>
289
+ <summary>展开查看状态文件与隐私说明、已知限制、iOS PWA 适配与已适配第三方插件列表</summary>
290
+
291
+ **状态文件与数据隐私**
292
+ - 插件自身不向任何第三方收集或上报数据。网关配对状态存于 `~/.dsh/lan-gate-state.json`;主进程三合一状态文件 `~/.dsh/zen-remote-state.json`(`0600`)统一保存本机 `serverId`、共享会话表 `sessions` 与子客户端已配对服务端列表 `servers`(含令牌,最多 8 台);托管的 Cloudflare 令牌以 `0600` 权限存于 `~/.dsh/zen-remote-ingress/`。
284
293
 
285
294
  **已知限制**
286
295
  - 同时打开多个 DSH 窗口时,子客户端对「当前正打开哪个会话」的判断可能存在偏差。
@@ -296,7 +305,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
296
305
 
297
306
  | 插件 | 移动端与远程适配内容 | 实测版本 |
298
307
  | --- | --- | --- |
299
- | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 移动端右栏全屏抽屉、左缘滑回与安全区避让;**插件桥**内置适配 `0.25.0` 至 `0.30.0` 八版(服务端授权后远程会话可直接读写服务端文件/Git/任务面板) | 0.19.1 / 适配器 0.25–0.30.0 |
308
+ | [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) | 移动端右栏全屏抽屉、左缘滑回与安全区避让;**插件桥**内置适配 `0.25.0` 至 `0.30.1` 九版(服务端授权后远程会话可直接读写服务端文件/Git/任务面板) | 0.19.1 / 适配器 0.25–0.30.1 |
300
309
  | [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams) | AgentTeams 活动浮层避让会话顶栏、子代理头部保留父会话一键返回 | 0.1.9 |
301
310
  | [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | 用量与余额入口收进手机主屏 chips 行 | 0.2.9 |
302
311
  | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | 定时任务入口收进手机主屏 chips 行 | 0.2.3 |
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../lib/cli/main.js'
3
+
4
+ const code = await runCli(process.argv.slice(2))
5
+ if (code !== 0) process.exitCode = code
package/docs/interface.md CHANGED
@@ -18,8 +18,8 @@ v1.0.0(fork 自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-
18
18
 
19
19
  设置区块挂在 DSH「插件」页的 `plugins.row.config` 槽位上(`src/client/settings/`),是 2.0 唯一的配置与管理界面:
20
20
 
21
- - **主服务端区块**:角色(角色卡里带两角色共用的**设备名称**字段,即 `serverName`:host 上是其他设备侧边栏分组标题里的名字,与子客户端双向自动同步)→ 网关与反代(host/port/可信反代/限流)→ 配对(先选设备角色、显示书面指引,再生成)→ 设备列表(名称、角色、最近在线、是否订阅推送;改名 / `set-role` / 吊销)→ 推送(各策略字段 + 测试推送)→ 远程共享(闲置时长、自动开启、已共享会话列表与一键全关);
22
- - **子客户端区块**:角色(设备名称同一字段,作为配对时注册到服务端的名字)→ 服务端地址与配对(配对按角色卡里的设备名称注册,名字非法时配对按钮禁用)→ 连接状态(已连接 / 离线 / 版本有差异 / 令牌已吊销,可解配)→ 诊断(接口指纹差异组、运行时不兼容记录);
21
+ - **主服务端区块**:角色(角色卡里带两角色共用的**设备名称**字段,即 `serverName`:host 上是其他设备侧边栏分组标题里的名字;子客户端侧改名后向其已配对的各主服务端单向广播,不再由服务端反写)→ 网关与反代(host/port/可信反代/限流)→ 配对(先选设备角色、显示书面指引,再生成)→ 设备列表(名称、角色、最近在线、是否订阅推送;改名 / `set-role` / 吊销)→ 推送(各策略字段 + 测试推送)→ 远程共享(闲置时长、自动开启、已共享会话列表与一键全关);
22
+ - **子客户端区块**:角色(设备名称同一字段,作为配对时注册到各服务端的名字)→ 服务端地址与配对(配对按角色卡里的设备名称注册,名字非法时配对按钮禁用)→ **已配对服务端列表**:卡片标题带「已配对 N / 8 台」徽标,逐台显示显示名(重名自动带 `-1`/`-2`)、地址与连接状态(已连接 / 离线倒计时 / 令牌已吊销 / 不兼容),行级操作「立即重连」(离线时)、「重新配对」(地址回填输入框)、「移除」,列表底部「全部移除」;**最多同时连接 8 台主服务端**,配满时红字标注上限并禁用新地址的配对按钮,输入已有地址仍可覆盖重配令牌 → 连接状态(汇总状态灯与 M / N 台在线明细)→ 诊断(全局拦截器状态 + 逐台服务端的接口指纹差异组、运行时不兼容记录与插件桥状态);
23
23
  - **经网关打开时只读**:手机或其它浏览器经网关打开设置页可以看,但生成配对码、改角色、吊销这些管理动作被拒绝(带 `x-zen-remote-via` 标记的请求一律拒绝 POST)——设备丢了也没法用它再配出新设备;
24
24
  - 角色判定读的是 configForms 行文档里的已保存角色;行文档答不上来时回退探测 `/_dsh/mobile-nav/client-config`(该路由两角色都注册,报的是合并后的生效角色)。
25
25
 
@@ -35,14 +35,14 @@ v1.0.0(fork 自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-
35
35
 
36
36
  ### 子客户端:远程部件
37
37
 
38
- - **远程分组**:侧边栏在本地工作区后面按「服务端名 · 工作区名」多出几组,默认收起,里面是服务端已开启远程的会话;服务端的工作区只有其中至少一个会话开启了远程才会成组——没动过的工作区不显示成空分组,已显示过的分组里会话全部关闭后空分组留到刷新页面(DSH 界面对移除过的工作区 id 不再接受,见根 README 已知限制);服务端离线时分组标注「(离线)」、输入禁用、后台退避重连;解配 / 吊销保留分组只换标注,这两种状态同样禁用输入、也不等自动重连(只能从设置页重新配对才恢复);仅当换到另一台服务端才整组移除;
38
+ - **远程分组**:侧边栏在本地工作区后面按「服务端名 · 工作区名」多出几组,默认收起,里面是服务端已开启远程的会话;服务端的工作区只有其中至少一个会话开启了远程才会成组——没动过的工作区不显示成空分组,已显示过的分组里会话全部关闭后空分组留到刷新页面(DSH 界面对移除过的工作区 id 不再接受,见根 README 已知限制);服务端离线时分组标注「(离线)」、输入禁用、后台退避重连;解配 / 吊销保留分组只换标注,这两种状态同样禁用输入、也不等自动重连(只能从设置页重新配对才恢复);仅当换到另一台服务端才整组移除;**多服务端(2.4)**:子客户端最多同时配对 8 台主服务端,各台的分组按**配对顺序**在本地工作区后依次拼接——哪台先连上不打乱顺序;多台服务端重名时显示名按配对顺序自动编号「名称-1」「名称-2」,改名不再重名后编号自动消失;在设置页移除某台服务端时,仅该台的分组与会话从侧边栏撤下,其余服务端的分组与正在运行的流不受任何中断;
39
39
  - **标题行连接图标**(同一槽位,order 26):每个远程会话的连接状态——在线 / 离线 / 版本有差异;点击可关闭该会话的远程(二次确认,文案写明「关闭后需要在服务端重新开启」);
40
40
  - **产物下载**:产出台的卡片(`deliverables.file.actions`,order 40)与文档预览头(`sidebar.right.tab.document.actions`,order 40)各多一个「下载到本机」。远程会话里官方只有「预览」和「在服务端机器上打开」,产物此前无法落到子客户端磁盘——这个按钮用中继已放行的 `workspaceFiles/readBytes` 按窗口(默认 2 MiB,宿主的 `maxBytes` 更小时按 `details.limit` 自动缩小)读回字节,再由浏览器落盘;卡片上的入口从卡片自己的预览按钮 `title`(宿主 `resolveWorkspacePath` 的绝对路径)取路径,预览头直接用 owner 给的 `absolutePath`。两个入口都只在虚拟 id 会话渲染,主服务端与本地会话逐像素 no-op。同一版起 Word/Excel/PPT 预览改由**服务端** LibreOffice 出 PDF(`officeToPdf/render` 登记进中继白名单,PDF 字节走 `INVOKE_BINARY_RESULT_METHODS` 标记),不再报「此功能暂不支持远程会话」。
41
41
  - **composer 横幅**(`conversation.input.dock`,order 15):四种状态——会话被休眠或服务端关闭时显示「远程已关闭」及原因;服务端离线显示「暂时只读,恢复连接后自动可用」;令牌被吊销、解除配对也显示横幅(这两者不会自己恢复,文案指向设置页重新配对)。横幅出现的同时**输入一律禁用**,禁用走宿主自己的 composer-block 契约(`ctx.conversation.blocks.set(sessionId, { reason })`,输入框、发送钮与模型控件一起禁掉,`reason` 成为占位文案),链接恢复或组件卸载时清除,不碰宿主 DOM;
42
- - 面板长尾:远程会话里带会话 id 的调用自动走中继;改动摘要 / diff 是例外路径——它们不走 typert 网关而是 plain-HTTP GET,浏览器侧的 fetch 包装把带虚拟 id 的 `/api/changes.*` 请求改写到子客户端路由 `/_dsh/zen-remote/client/http/<route>` 再经中继;离线时写一律本地拒绝,`terminal/follow` 也按写处理(它的附带会接管终端的输入控制权,离线放行等于悄悄抢走一条不在线连接的控制权,所以离线即拒绝);认不准数据来源的面板直接隐藏,而不是显示本机错误数据(清单见根 README「已知限制」)。
42
+ - 面板长尾:远程会话里带会话 id 的调用自动走中继;改动摘要 / diff 是例外路径——它们不走 typert 网关而是 plain-HTTP GET,浏览器侧的 fetch 包装把带虚拟 id 的 `/api/changes.*` 请求改写到子客户端路由 `/_dsh/zen-remote/client/http/<route>` 再经中继;离线时写一律本地拒绝,`terminal/follow` 与 DSH 0.2.2+ 的 `terminal/attach` 也按写处理(附带会接管终端的输入控制权,离线放行等于悄悄抢走一条不在线连接的控制权,所以离线即拒绝);`terminal/attach` 是双工流(TA-2):拦截器不半关闭宿主的上行收件箱,把它和 mux 上行窗口一起交给中继客户端的泵(`/v1/stream-uplink`,单飞行合批、严格保序),键盘输入与 resize 在远程终端上照常生效;认不准数据来源的面板直接隐藏,而不是显示本机错误数据(清单见根 README「已知限制」)。
43
43
  - **关闭的会话与断线恢复**(CP4 / CP4-client-fix2):服务端关闭某会话的远程(或它被闲置休眠扫掉)时,该会话**从侧边栏列表隐藏**——按「已归档」处理(宿主 `sessionVisible` 在默认筛选下不显示 archived 里的会话),打开「显示已归档」筛选仍能找到。唯一的例外是此刻正打开的会话页:页面停在原处——关闭不重试,已出过帧的会话流也不再报错,静默保持到页面自己关掉它(宿主把会话流的任何终止错误都渲染成对话区顶部的「历史加载失败:…」,而关闭原因只该由横幅表达);若之后服务端重新共享、又有对它的调用成功,关闭记录随之清掉,这条流改为**正常结束**——宿主把它当作载具恢复、带着新历史自动重开。零帧的会话流、其他面板的流以及对它的 typert 单次请求仍以错误结束,但文字统一为「远程已关闭」,不透出服务端的内部报错(改动摘要 / diff 走的 plain-HTTP 转发例外:那里仍是只带错误码的 403)。composer 横幅显示「远程已关闭」及原因,输入经 composer-block 禁用;当前会话绝不进 archived(否则宿主守卫 `clearArchivedCurrent` 会把正打开的页踢回首页,横幅就无从显示),用户切到别的会话后(当前会话信号来自宿主持久化的选择存储,逐帧读取、另有 1 秒轮询兜底)它才转为隐藏。服务端重新共享(id 回到某工作区的 `sessionIds`)即从 archived 移出、回到正常位置。服务端断线期间已打开的远程会话页保持原状:会话流与任务列表类面板在中继恢复后自动续上——已出过帧的流以干净 end 结束,宿主界面把它当作载具错误立即重开、拿到恢复后的新快照;零帧的原位重开,重开同样走退避(1 秒起翻倍、上限 30 秒)。「远程已关闭」与「暂时只读」是真判决,不在续上之列;文件树变更与终端面板断线后需重新打开。
44
44
 
45
- 这些部件的数据源是 `GET /_dsh/zen-remote/client/remote-status`,该路由**两种角色都注册**(host 上应答空的 `{state:'unpaired'}` 结论,桌面端 App 里切角色不用换代码路径);client 前缀下其余路由(配对代理、连接状态、立即重连、解配、plain-HTTP 转发)只在 client 角色挂载,host 角色对它们一律 404(最小暴露面)。
45
+ 这些部件的数据源是 `GET /_dsh/zen-remote/client/remote-status`,该路由**两种角色都注册**(host 上应答空的 `{state:'unpaired'}` 结论,桌面端 App 里切角色不用换代码路径);多服务端下该路由按 `serverId` 在 `servers` 字典里给出每台自己的结论(状态 / 版本差异 / 服务端名 / 已桥接插件),顶层字段保留首台服务端的结论以兼容旧页面,标题行图标与 composer 横幅都按当前打开会话的虚拟 id 取**所属那台**的结论。client 前缀下其余路由(claim 配对代理、连接状态、立即重连、移除/全部移除、unshare、plain-HTTP 转发)只在 client 角色挂载,host 角色对它们一律 404(最小暴露面)。
46
46
 
47
47
  ### 桌面端门之外生效的部件清单
48
48
 
@@ -391,7 +391,7 @@ PWA,再进来就是冷启动。插件自己的边缘手势也触发不了—
391
391
  ## 兼容插件
392
392
 
393
393
  - [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(0.19+:它的全部 tab 都在 DSH 0.1.5 官方右侧栏里,手机上宿主自己切全屏,本插件在会话页头部给它留入口位、左缘回退手势收它、文件树里点 @ 引用后自动收起——面板全屏盖住会话页,草稿变化看不见,收起本身就是点击反馈;≤ 0.18 自绘右栏的老版本:全宽抽屉 + 安全区 + 底部关闭 pill,只在 DSH < 0.1.5 上还会走到)——本机实测 **0.19.1**(老版规则实测 0.15.0)
394
- - dsh-better-sidebar 的**远程会话处理**(PB-12 起分两档):**0.25.0、0.26.0、0.26.1、0.28.0-alpha.0、0.28.0、0.28.1、0.28.2 与 0.30.0** 八版由 zen-remote 的内置适配器(`src/adapters/dsh-better-sidebar/`)**代理**——它的文件浏览、编辑器、git 变动、任务等界面在远程会话里读写的是主服务端的数据(整机数据,需主服务端主人在设置页同意;`open.external` 远程拒绝、`settings.*` 与 `/sidebar/bundle` 留本机、侧聊转发(子会话继承父会话共享);0.26 新方法已一并接入(`fs.readPage` 分页读、`sidechat.dequeue` 撤回排队消息、`sidechat.model`/`sidechat.models` 线程切模型、`/sidebar/write` 编辑器保存;0.28.1 再添 `git.history` 分页提交历史读;0.28.2 接口面不变,只把 detached 会话的 cwd 改为读存储 header(不再整份解码会话日志);`ws/sidechat` 侧聊推送也已桥接(PB-26b:升级 URL 不带参数、会话 id 在连接后的 JSON 帧里,客户端把 watch 帧里的虚拟 id 还原成真实 id 才转发,插件推送的 `threadId` 换算回虚拟 id 才给浏览器,未共享的线程 id 在两侧都会被拒、连接即断)))。**其他版本维持隐藏**:那些版本的界面都带 `{sessionId, cwd}` 语境调它自己的 `/sidebar/api/*` 接口,远程会话里这些请求落到子客户端本机——文件树会显示本机同名文件、编辑会真的写本机文件;远程会话(`html[data-zr-remote-session="1"]`)里按插件自己的挂载/渲染标记隐藏这些界面,选择器在 `src/client/styles/remote-session.css.ts`,未安装时一条都不命中。因为它接管了宿主的「文件」标签,未桥接的远程会话里暂时没有侧栏文件浏览;聊天或交付物里的文件链接(`dsh-resource://file/**`)会被它的编辑器注册抢先接住,展开一个空的右栏(根治要等 zen-remote 自己的远程文件树)。隐藏档的定位仍是**隐藏而非代理**:插件组件保持挂载并继续静默发只读请求,但所有写控件都在被隐藏的界面里,不会误写本机。PB-14 起显示规则按桥接状态生效:已桥接(0.25.0/0.26.0 且主服务端同意)时远程会话里**照常显示**(面板数据经适配器来自主服务端,隐藏规则全部让位);未桥接时维持隐藏,已打开的 tab 空态改为一行说明占位(better-sidebar 未在远程会话桥接——中英双语,随浏览器语言),空白工作台浮动按钮一并隐藏
394
+ - dsh-better-sidebar 的**远程会话处理**(PB-12 起分两档):**0.25.0、0.26.0、0.26.1、0.28.0-alpha.0、0.28.0、0.28.1、0.28.2、0.30.0 与 0.30.1** 九版由 zen-remote 的内置适配器(`src/adapters/dsh-better-sidebar/`)**代理**——它的文件浏览、编辑器、git 变动、任务等界面在远程会话里读写的是主服务端的数据(整机数据,需主服务端主人在设置页同意;`open.external` 远程拒绝、`settings.*` 与 `/sidebar/bundle` 留本机、侧聊转发(子会话继承父会话共享);0.26 新方法已一并接入(`fs.readPage` 分页读、`sidechat.dequeue` 撤回排队消息、`sidechat.model`/`sidechat.models` 线程切模型、`/sidebar/write` 编辑器保存;0.28.1 再添 `git.history` 分页提交历史读;0.28.2 接口面不变,只把 detached 会话的 cwd 改为读存储 header(不再整份解码会话日志);0.30.1 是 0.30.0 的 wire-identical 补丁版,同一接口面摘要直接登记;`ws/sidechat` 侧聊推送也已桥接(PB-26b:升级 URL 不带参数、会话 id 在连接后的 JSON 帧里,客户端把 watch 帧里的虚拟 id 还原成真实 id 才转发,插件推送的 `threadId` 换算回虚拟 id 才给浏览器,未共享的线程 id 在两侧都会被拒、连接即断)))。**其他版本维持隐藏**:那些版本的界面都带 `{sessionId, cwd}` 语境调它自己的 `/sidebar/api/*` 接口,远程会话里这些请求落到子客户端本机——文件树会显示本机同名文件、编辑会真的写本机文件;远程会话(`html[data-zr-remote-session="1"]`)里按插件自己的挂载/渲染标记隐藏这些界面,选择器在 `src/client/styles/remote-session.css.ts`,未安装时一条都不命中。因为它接管了宿主的「文件」标签,未桥接的远程会话里暂时没有侧栏文件浏览;聊天或交付物里的文件链接(`dsh-resource://file/**`)会被它的编辑器注册抢先接住,展开一个空的右栏(根治要等 zen-remote 自己的远程文件树)。隐藏档的定位仍是**隐藏而非代理**:插件组件保持挂载并继续静默发只读请求,但所有写控件都在被隐藏的界面里,不会误写本机。PB-14 起显示规则按桥接状态生效:已桥接(0.25.0/0.26.0 且主服务端同意)时远程会话里**照常显示**(面板数据经适配器来自主服务端,隐藏规则全部让位);未桥接时维持隐藏,已打开的 tab 空态改为一行说明占位(better-sidebar 未在远程会话桥接——中英双语,随浏览器语言),空白工作台浮动按钮一并隐藏
395
395
  - [@nanmicoder/dsh-agent-teams](https://github.com/NanmiCoder/dsh-agent-teams)(AgentTeams 活动浮层:手机端挪到会话头部下方、避开安全区,会话列表页隐藏;子代理会话头部保留可点的父会话面包屑用于切回)——本机实测 **0.1.9**
396
396
  - [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开;旧包名 `dsh-usage-stats` 已废弃,两个名字同时留在 profile 里会因重复的 `usage-stats` 行让 DSH 起不来)——本机实测 **0.2.9**
397
397
  - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
@@ -235,7 +235,7 @@ Since 2.0.0 pairing codes are minted from the dsh-zen-remote settings block in t
235
235
  1. With the proxy in place, open the DSH Plugins page **on the host machine** and expand the dsh-zen-remote settings block.
236
236
  2. Pick the device role (web app device / desktop app device) and click generate — an 8-character code, valid for 10 minutes, single-use. **The code is bound to the role**: a web-device code offered to the desktop channel is refused and vice versa; a wrong-channel code is neither consumed nor counted toward the lockout.
237
237
  3. Web app device: on the phone or another computer, open your proxy's HTTPS domain in a browser — you'll land on the pairing page. Enter the code (device name is optional).
238
- 4. Desktop app device: in the other computer's DSH desktop app, open the plugin settings block, switch the role to **client**, and enter the server URL plus the code. The URL must be `https://` unless it is a private-network host — LAN ranges (`192.168.x`, `10.x`, `172.16–31.x`), Tailscale/CGNAT (`100.64.x`), loopback, `localhost` / `*.local`, or IPv6 `::1` / `fc00::/7` / `fe80::/10`.
238
+ 4. Desktop app device: in the other computer's DSH desktop app, open the plugin settings block, switch the role to **client**, and enter the server URL plus the code. The URL must be `https://` unless it is a private-network host — LAN ranges (`192.168.x`, `10.x`, `172.16–31.x`), Tailscale/CGNAT (`100.64.x`), loopback, `localhost` / `*.local`, or IPv6 `::1` / `fc00::/7` / `fe80::/10`. One sub-client can stay paired with up to **8 main servers** at once: each successful pairing lands in the paired-server list, and the plugin settings block (or the plugin page's top-bar wizard) then adds, reconnects or removes servers one at a time. When several paired servers report the same name, the sidebar's remote groups are numbered `-1`, `-2` in pairing order — the suffixes disappear automatically once a rename removes the collision.
239
239
  5. On success a web device is dropped straight into the DSH Web UI (PWA-injected); identity is stored in a long-lived cookie, so switching Wi-Fi/IP never logs you out.
240
240
  6. On the phone, use the browser menu's "Add to Home Screen" to get a standalone app.
241
241
  7. The page will prompt you to enable "agent-done push" — grant notification permission and you'll get a system notification when the agent finishes, even from another app.
@@ -315,7 +315,8 @@ A desktop client has no DSH login cookie, and the relay deliberately does not fo
315
315
  | `/v1/handshake` | POST | Handshake: exchanges the relay protocol version, server id / display name, DSH version, and the interface fingerprint tables. A relay-protocol-version mismatch refuses the connection; the answer carries `serverName` (the server's display name) and `deviceName` (this device's name in the server's records) |
316
316
  | `/v1/device/name` | POST | Device rename: **handled by the gateway itself, never forwarded to DSH** — open to paired desktop app devices only, and it renames only the CALLING device's own record; body `{"name":"…"}` (1–40 chars). Renames reach the other side through the stream heartbeat (~15s) and the handshake refresh (~30s) |
317
317
  | `/v1/invoke` | POST | One DSH remote call on a shared session (single-shot, JSON in and out) |
318
- | `/v1/stream` | POST | Opens one DSH stream subscription. **NDJSON format**: one JSON value per line; `{"type":"ping"}` heartbeat lines (every 15s by default) keep reverse proxies from timing idle streams away, and each one carries the current `serverName` / `deviceName` (a server rename reaches every connected client within one heartbeat); when a share closes or a subscription loses its session the server writes `{"type":"error","error":{"code":"unshared"}}` and ends the response |
318
+ | `/v1/stream` | POST | Opens one DSH stream subscription. **NDJSON format**: one JSON value per line; `{"type":"ping"}` heartbeat lines (every 15s by default) keep reverse proxies from timing idle streams away, and each one carries the current `serverName` / `deviceName` (a server rename reaches every connected client within one heartbeat); when a share closes or a subscription loses its session the server writes `{"type":"error","error":{"code":"unshared"}}` and ends the response. **Duplex streams** (TA-1): when the method declares an uplink and the request body carries `uplink: true`, the server writes a `{"type":"uplink","streamId":"<32-hex>"}` handshake line FIRST, before any frame, and from then on the client may push frames up through `/v1/stream-uplink`; an older client that doesn't know the line ignores it, and a one-directional stream (no `uplink: true`) behaves byte-for-byte as before |
319
+ | `/v1/stream-uplink` | POST | The duplex stream's uplink channel (TA-1): body `{streamId, seq, items?, end?}` — `items` is the batch of uplink frames (several can share one round trip), `end: true` half-closes the uplink. The server resolves the live stream by `streamId` through four walls — device ownership (cross-device answers 404 `unknown-stream`), the session still shared (403 `not-shared`), a strictly increasing transport sequence (out of order 409 `out-of-order`), and unconsumed buffered bytes within 256 KiB (past it 413 `uplink-overflow` and the stream aborts). The client side is a **single-in-flight batching pump**: at most one uplink POST on the wire at a time per stream, frames read while it flies accumulate into the next batch — strictly ordered by construction (concurrent requests could arrive reordered and trip the host's `Terminal input sequence has a gap`), and fast typing costs the fewest round trips |
319
320
  | `/v1/event-result` | POST | The answer half for approval/question events (`{eventId, result}`); the server matches it against what it forwarded, per device, before handing it over |
320
321
  | `/v1/http` | POST | The plain-HTTP long tail of the changes summary / diff panels: the `{route, query}` body admits only `changes.summary` / `changes.diff` and forwards GETs only (the query string is this call's coordinate, never a request body to forward); the query is **normalized and rebuilt** along an allowlist — only `sessionId` (exactly once, share-checked) and `seq` / `index` (at most once each, decimal non-negative integers) survive, unknown parameters are dropped, and the synthetic URL is composed from the normalized result alone (a control-character parsing differential between the two URL parsers was once an authorization bypass) — then dispatched **in-process** through the host's shared `/api` fetch handler, no loopback HTTP |
321
322
  | `/v1/upload` | POST | The **non-image attachment** upload of a remote session: the request body is a raw byte stream (not JSON); the query string is normalized and rebuilt along an allowlist — only `sessionId` (exactly once, share-checked, 403 when not shared) and `name` (at most once) survive; the cap is 500 MiB, enforced twice (a `Content-Length` pre-check plus a counting pump over the stream), over-limit answers 413 (the refusal is written before the tail bytes are drained); at most 8 concurrent uploads per device (429 past that); then, like `/v1/http`, dispatched **in-process** through the host's shared `/api` handler — the staged receipt lands under the same session the later `session/prompt` resolves the attachment against |
@@ -332,10 +333,10 @@ The `/v1/http` route is a second allowlist, keyed by **route name** (not part of
332
333
  - The server executes everything through the DSH gateway service's public invoke/stream methods, under the operator identity — **it never replaces DSH internals**;
333
334
  - Subagent and fork sessions never enter the table; reachability walks the ancestor chain (a shared ancestor shares the family);
334
335
  - `dsh-session:` references are scanned too: DSH injects the referenced session's content along **messages** (the content text blocks of `session/prompt` / `subagents/prompt`), **queue edits** (`session/updateQueue`'s edit content) and **slash commands** (EVERY string of a `commands/execute` call — a command handler steers its raw input in as the next user message), with no access check of its own at injection time, so a call whose reference names an unshared session is refused server-side with 403; the @-mention candidates (`sessionReferenceResolver/candidates`) likewise only ever list shared sessions' rows;
335
- - Terminals are deliberate (a desktop client is a trusted device): what opens is a **server-side** PTY running as the server user, without the agent sandbox or approval restrictions; closing the session's remote access kills its terminal streams with it;
336
+ - Terminals are deliberate (a desktop client is a trusted device): what opens is a **server-side** PTY running as the server user, without the agent sandbox or approval restrictions; closing the session's remote access kills its terminal streams with it. DSH 0.2.2+'s `terminal/attach` is a duplex stream (TA-2): the downlink carries `snapshot` / `output` / `state` / `ack` frames while the uplink sends `write` / `resize` commands through `/v1/stream-uplink` — the interceptor does not half-close the host's uplink inbox, it hands the iterable and the mux's uplink window (shape-checked, garbage dropped) to the relay client's pump; the `terminal/follow|write|resize` trio of ≤ 0.2.1 stays registered, so both DSH generations remote-terminal fine;
336
337
  - `workspaceFiles/read` and friends are not contained to the session directory (DSH itself does not contain them) — anything the server process can read is readable, under the same trusted-device premise as terminals;
337
338
  - The plugin bridge's whole-machine data requires the server owner's per-plugin consent — the toggles in the settings page's Plugin bridge section, everything OFF by default; the loopback carries the operator's cookie, so a bridged plugin sees the same permissions the local browser sees. The bridge's boundary is therefore the DECLARED route set (exact equality, plus a `prefix: true` route's sub-paths under the PB-23b raw-byte tail red line) plus the share table — there is **no** parameter-level scanning;
338
- - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **the eight versions 0.25.0 through 0.30.0** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments). A prefix hit's tail passes the shape red line: `?`/`#`/`..` (encoded forms included) or whitespace answers 400 `bad-request` (PB-23b, the same rule as the cooperative segment). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the three WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push, `/sidebar/ws/sidechat` side-chat push) bridge over the byte tunnel — the first two carry the session id in their upgrade URL's query and their frames pass through untouched (the browser never reads the session id inside a push); `ws/sidechat`'s upgrade URL carries NO parameter at all — the ids ride the JSON text frames sent after the handshake (`{"op":"watch","threadIds":[…]}`), through the per-frame gate (PB-26b): the client rewrites every virtual id in a watch frame back to its real form before forwarding (an empty array = unsubscribe-all, passes verbatim), the server share-checks EVERY text frame before the plugin sees one (an unshared thread id never reaches the plugin — the connection dies), and the plugin's pushed `{"threadId":…}` top-level id is virtualized before it reaches the browser (better-sidebar's subscription comparison demands the exact virtual id); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
339
+ - **Adapters (better-sidebar)**: beyond cooperative declarations, zen-remote ships a built-in adapter for `dsh-better-sidebar` **the nine versions 0.25.0 through 0.30.1** (`src/adapters/` — non-cooperative third-party plugins are bridged by it, with prefix/path-segment route matching and session-id rewriting in JSON bodies and path segments; 0.30.1 is a wire-identical patch release of 0.30.0 and registers under the same digest `394de9f588d37092`). A prefix hit's tail passes the shape red line: `?`/`#`/`..` (encoded forms included) or whitespace answers 400 `bad-request` (PB-23b, the same rule as the cooperative segment). **Consenting to its whole-machine toggle hands the paired desktop client whole-disk file read/write on the server, git write operations (commit, push, revert, …), and model spend through commit-message generation** — the product decision is "both directions forward, no path fencing", and a server owner should treat that toggle as terminal-grade trust in the paired machine; `open.external` (launching processes on the host) refuses for remote sessions, `settings.*` and `/sidebar/bundle` stay on the sub-client's own machine; the three WebSockets (`/sidebar/ws/fs-watch` file-tree watch, `/sidebar/ws/agent-opens` open push, `/sidebar/ws/sidechat` side-chat push) bridge over the byte tunnel — the first two carry the session id in their upgrade URL's query and their frames pass through untouched (the browser never reads the session id inside a push); `ws/sidechat`'s upgrade URL carries NO parameter at all — the ids ride the JSON text frames sent after the handshake (`{"op":"watch","threadIds":[…]}`), through the per-frame gate (PB-26b): the client rewrites every virtual id in a watch frame back to its real form before forwarding (an empty array = unsubscribe-all, passes verbatim), the server share-checks EVERY text frame before the plugin sees one (an unshared thread id never reaches the plugin — the connection dies), and the plugin's pushed `{"threadId":…}` top-level id is virtualized before it reaches the browser (better-sidebar's subscription comparison demands the exact virtual id); side chat (`sidechat.*`) forwards — its child sessions carry `meta.parentSession`, so they inherit the parent's share through the ancestor chain, and the `childId` in `sidechat.start`'s answer is virtualized before it reaches the browser; 0.28's (alpha.0 and stable) `changes.ops` long-poll (`wait: true`, parks up to 25 s — past the relay's 15 s header wait) has `wait` stripped on the server, so a remote session's "this round's files" view polls on 0.26's 2.5 s cadence; `sidechat.models` shows the **sub-client's own** model catalog in the picker while the choice goes through `sidechat.model` and takes effect on the **server** — when the two machines' catalogs differ, what the server actually applies is authoritative;
339
340
  - Concurrent streams are budgeted per device (32), and so are invokes kept in flight at once — past the per-device cap the invoke answers 429 `too-many-invokes`. Error codes shaped `*/internal` (`gateway/internal` and friends) are returned as the bare code with no message — that kind of message quotes server-side internals. Event answers are matched against the subscription and device they were forwarded to. Approval/question forwarding is **first come, first served**: whichever of the server's own UI and the sub-client answers first wins; the later answer is refused by the gateway and the client syncs to "already handled". The relay **never answers on the client's behalf**: an unshared session's approval/question events are not forwarded at all, and with the server's own UI offline too, the approval simply keeps waiting — the gateway re-delivers still-pending events to the next `$events` subscriber. A wait, never a rejection (an abstention would fail every pending approval of the session at once and break the push → wake → approve flow). Notification-type events (EMIT frames: session-list summaries, account expirations and other server-wide state) of an unshared session are never forwarded.
340
341
 
341
342
 
@@ -436,6 +437,7 @@ The open body caps at 1 MiB (413 past it); each device holds at most 8 concurren
436
437
  - **A desktop client is a trusted device** — its terminal runs as the server user without the agent sandbox or approval restrictions, file previews are not contained to the session directory, and it can create sessions in any server workspace. "Only shared sessions are visible" constrains session data, not the machine; giving a pairing code to a computer you don't trust is handing it the machine.
437
438
  - DSH's own capability boundary — the gateway only forwards traffic to DSH safely; it can't and doesn't add security measures DSH itself doesn't have (DSH's own `/api` trust fence is DSH's concern).
438
439
  - The state file `~/.dsh/lan-gate-state.json` stores the VAPID private key and every device's token in plaintext — this file *is* full access to your gateway. Mind its file permissions on the host, and don't sync `~/.dsh` into a shared drive or an untrusted backup location.
440
+ - The client role keeps its own state file, `~/.dsh/zen-remote-state.json` (0600, atomic tmp+rename writes): the local server identity, the shared-session table and **the device tokens of every paired main server (up to 8)** all live in it — it is equivalent to full access to every paired server, and deserves the same care as the bullet above. The legacy scatter files `zen-remote-server.json` / `zen-remote-shares.json` migrate into it automatically on first start and are then deleted.
439
441
 
440
442
  ---
441
443
 
@@ -272,7 +272,7 @@ sudo cloudflared service install # 通了再装成常驻服务
272
272
  1. 反代配好后,在**这台机器本机**打开 DSH「插件」页,展开 dsh-zen-remote 的设置区块。
273
273
  2. 选设备角色(Web 应用端 / 桌面应用端),点生成,得到一个 8 位码,10 分钟内有效,只能用一次。**码与角色绑定**:Web 应用端的码拿到桌面端去用会被拒绝,反之亦然;拿错的码不被消耗、也不计入错码锁定。
274
274
  3. Web 应用端:手机或另一台电脑的浏览器打开你反代的 HTTPS 域名,在配对页输入配对码(可选填设备名)。
275
- 4. 桌面应用端:在另一台 DSH 桌面端的插件设置区块里把角色切成「子客户端」,填服务端地址与配对码。地址必须 https,只有内网/回环/Tailscale 段(`192.168.x`、`10.x`、`172.16–31.x`、`100.64.x`、`127.x`、`localhost`/`*.local`、IPv6 `::1`/`fc00::/7`/`fe80::/10`)才允许 http。
275
+ 4. 桌面应用端:在另一台 DSH 桌面端的插件设置区块里把角色切成「子客户端」,填服务端地址与配对码。地址必须 https,只有内网/回环/Tailscale 段(`192.168.x`、`10.x`、`172.16–31.x`、`100.64.x`、`127.x`、`localhost`/`*.local`、IPv6 `::1`/`fc00::/7`/`fe80::/10`)才允许 http。一台子客户端最多同时配对 **8 台主服务端**:配对成功的一台进入已配对列表,之后在插件设置区块(或插件页顶栏向导)里「添加服务端」逐台配对、重连或移除;多台服务端重名时,侧边栏的远程分组按配对顺序自动编号为「名称-1」「名称-2」,改名不再重名后编号自动消失。
276
276
  5. 配对成功后自动进入 DSH Web UI(已注入 PWA),身份保存在长期 Cookie 里,换 Wi-Fi/换 IP 都不会掉线。
277
277
  6. 手机上通过浏览器菜单「添加到主屏幕」,就能像原生 App 一样独立打开。
278
278
  7. 页面会提示开启「任务完成推送」,同意通知权限后,agent 干完活即使切到别的 App 也能收到系统通知。
@@ -352,7 +352,8 @@ sudo cloudflared service install # 通了再装成常驻服务
352
352
  | `/v1/handshake` | POST | 握手:交换中继协议版本、服务端 id / 显示名、DSH 版本号、接口指纹表。中继协议版本不一致即拒绝连接;回包带 `serverName`(服务端显示名)与 `deviceName`(本设备在服务端记录里的名字) |
353
353
  | `/v1/device/name` | POST | 设备改名:**网关自己处理、不转发给 DSH**——只对已配对的桌面应用端开放,只改调用者自己的设备记录;请求体 `{"name":"…"}`(1–40 字),改名经流心跳(约 15 秒)与握手刷新(约 30 秒)同步到对端 |
354
354
  | `/v1/invoke` | POST | 在已共享会话上执行一次 DSH 远程调用(单次,JSON 进出;`workspaceFiles/readBytes` 这类二进制结果以 `{"$zr-u8": <base64>}` 标记承载,子客户端还原成 `Uint8Array`——见 `src/invoke-binary.ts`) |
355
- | `/v1/stream` | POST | 打开一条 DSH 流式订阅。**NDJSON 格式**:响应体每行一条 JSON;`{"type":"ping"}` 心跳行(默认 15 秒一条)防止反代把闲置流掐掉,每条都带当下的 `serverName` / `deviceName`(服务端改名约 15 秒内到达所有客户端);共享关闭/订阅失效时补一行 `{"type":"error","error":{"code":"unshared"}}` 再结束 |
355
+ | `/v1/stream` | POST | 打开一条 DSH 流式订阅。**NDJSON 格式**:响应体每行一条 JSON;`{"type":"ping"}` 心跳行(默认 15 秒一条)防止反代把闲置流掐掉,每条都带当下的 `serverName` / `deviceName`(服务端改名约 15 秒内到达所有客户端);共享关闭/订阅失效时补一行 `{"type":"error","error":{"code":"unshared"}}` 再结束。**双工流**(TA-1):方法声明带 uplink 且请求体带 `uplink: true` 时,服务端先写出一行 `{"type":"uplink","streamId":"<32-hex>"}` 握手行再转发任何帧,此后客户端可经 `/v1/stream-uplink` 向这条流上行帧;不认识这行的旧客户端自动忽略,单向流(不带 `uplink: true`)行为一字不变 |
356
+ | `/v1/stream-uplink` | POST | 双工流的上行通道(TA-1):请求体 `{streamId, seq, items?, end?}`——`items` 是本次批量上行帧(一次网络往返可合批多条),`end: true` 半关闭上行;服务端按 `streamId` 找到活跃流后过四道闸——设备归属(跨设备 404 `unknown-stream`)、会话仍共享(403 `not-shared`)、传输序号严格递增(乱序 409 `out-of-order`)、未消费缓冲不超 256 KiB(超限 413 `uplink-overflow` 并中止该流)。客户端侧是**单飞行合批泵**:同一时刻最多一个上行 POST 在途,飞行期间读到的帧攒成本地批次,既天然严格保序(避免并发请求乱序触发宿主 `Terminal input sequence has a gap`)又把快速输入的往返次数降到最低 |
356
357
  | `/v1/event-result` | POST | 审批/提问这类「需要应答事件」的应答回传(`{eventId, result}`),服务端按转发记录核对归属后转交 |
357
358
  | `/v1/http` | POST | 改动摘要 / diff 面板的 plain-HTTP 长尾:请求体 `{route, query}` 只登记 `changes.summary` / `changes.diff` 两条、只转发 GET(查询串是这次调用的坐标,不是要转发的请求体);查询串按允许清单**规范化重建**——只保留 `sessionId`(恰好一次,过共享表)与 `seq` / `index`(至多一次、十进制非负整数),未知参数丢弃,合成 URL 只由规范化结果拼成(两次解析对控制字符的处理差异曾是越权读取口子)——随后**进程内**交给宿主 `/api` 共享 fetch handler,不走回环 HTTP |
358
359
  | `/v1/upload` | POST | 远程会话的**非图片附件**上传:请求体是原始字节流(不是 JSON);查询串按允许清单规范化重建——只保留 `sessionId`(恰好一次、过共享表,未共享 403)与 `name`(至多一次);上限 500 MiB,`Content-Length` 预检加流式计数双保险,超限 413(先回拒绝再排空尾部字节);每设备并发 8(超出 429);随后与 `/v1/http` 一样**进程内**交给宿主 `/api` 共享 handler——上传回执落进的会话与之后 `session/prompt` 解析附件引用的会话是同一个 |
@@ -369,10 +370,10 @@ sudo cloudflared service install # 通了再装成常驻服务
369
370
  - 中继执行时用的是 DSH 网关服务的公开调用方法,身份是操作者身份,**不在服务端替换任何 DSH 内部方法**;
370
371
  - 子智能体/分叉会话不单独落表,可达性按「祖先链上有已共享会话」判定;
371
372
  - `dsh-session:` 会话引用也会被扫描:DSH 会在**发消息**(`session/prompt` / `subagents/prompt` 的 content 文本块)、**改写排队消息**(`session/updateQueue` 的 edit 内容)与**斜杠命令**(`commands/execute` 参数里的全部字符串——命令处理器会把原始输入拼进下一条用户消息)这三条路径上注入被引用会话的内容,且注入时不再做权限检查,所以引用指向未共享会话的调用在服务端直接 403;@ 引用候选(`sessionReferenceResolver/candidates`)的答案也只放行已共享会话的行;
372
- - 终端是有意开放的(桌面应用端是可信设备):开的是**服务端**的 PTY,以服务端用户身份运行、不受智能体沙箱与审批限制;关闭会话的远程时终端流一并终止;
373
+ - 终端是有意开放的(桌面应用端是可信设备):开的是**服务端**的 PTY,以服务端用户身份运行、不受智能体沙箱与审批限制;关闭会话的远程时终端流一并终止。DSH 0.2.2+ 的 `terminal/attach` 是双工流(TA-2):下行是 `snapshot` / `output` / `state` / `ack` 帧,上行经 `/v1/stream-uplink` 送 `write` / `resize` 命令——拦截器不半关闭宿主的上行收件箱,而是把它和 mux 的上行窗口(形状校验,畸形丢弃)一起交给中继客户端的泵;≤ 0.2.1 的 `terminal/follow|write|resize` 三件套保留登记,两代 DSH 都能远程终端;
373
374
  - `workspaceFiles/read` 等文件预览接口不限制在会话目录内(DSH 自身就不限制),服务端进程可读的文件都能读——信任前提与终端相同;
374
375
  - 插件桥的整机数据需要主服务端主人逐插件同意——设置页「插件桥」里的开关,缺省全部关闭;回环执行带操作者 cookie,插件接口看到的权限与本机浏览器相同,因此桥接的边界就是声明登记的路径(exact 相等,`prefix: true` 路由含其子路径,前缀尾巴还要过 PB-23b 的原样字节形状红线)加会话共享表,**不做**参数级扫描;
375
- - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0 至 0.30.0 的八个版本**内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写)。前缀命中的尾巴过形状红线:`?`/`#`/`..`(含编码形态)或空白一律 400 `bad-request`(PB-23b,与协作段同一条)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;三条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送、`/sidebar/ws/sidechat` 侧聊推送)经字节隧道桥接——前两条的升级 URL 带会话 query,帧原样透传(浏览器不消费推送里的会话 id);`ws/sidechat` 的升级 URL 不带任何参数,会话 id 在连接建立后的 JSON 文本帧里(`{"op":"watch","threadIds":[…]}`),走逐帧闸门(PB-26b):客户端把 watch 帧里的虚拟 id 逐个还原成真实 id 再转发(空数组 = 全部退订,原样放行),服务端对每个文本帧先按共享表核对、通过才交给插件(未共享的线程 id 到不了插件,连接即断),插件推回的 `{"threadId":…}` 顶层 id 换算回虚拟 id 才给浏览器(better-sidebar 的订阅比对要求虚拟 id 精确匹配);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
376
+ - **适配器(better-sidebar)**:协作声明之外,zen-remote 为 `dsh-better-sidebar` **0.25.0 至 0.30.1 的九个版本**内置了一个适配器(`src/adapters/`,不配合的第三方插件由它代为桥接,路由按前缀/路径段匹配、会话 id 在 JSON 体与路径段里改写;0.30.1 是 0.30.0 的 wire-identical 补丁版,同一摘要 `394de9f588d37092` 直接登记)。前缀命中的尾巴过形状红线:`?`/`#`/`..`(含编码形态)或空白一律 400 `bad-request`(PB-23b,与协作段同一条)。**同意它的整机数据开关,等于让已配对的桌面应用端读写主服务端整盘文件、执行 git 写操作(提交、推送、回滚……),并通过提交信息生成消耗主服务端的模型额度**——产品上的决定就是「读写都接、不加路径围栏」,主服务端主人应当把它当成给对方一台本机终端级别的信任来开;`open.external`(在宿主机起进程)对远程会话拒绝,`settings.*` 与 `/sidebar/bundle` 留在子客户端本机;三条 WebSocket(`/sidebar/ws/fs-watch` 文件树监视、`/sidebar/ws/agent-opens` 打开推送、`/sidebar/ws/sidechat` 侧聊推送)经字节隧道桥接——前两条的升级 URL 带会话 query,帧原样透传(浏览器不消费推送里的会话 id);`ws/sidechat` 的升级 URL 不带任何参数,会话 id 在连接建立后的 JSON 文本帧里(`{"op":"watch","threadIds":[…]}`),走逐帧闸门(PB-26b):客户端把 watch 帧里的虚拟 id 逐个还原成真实 id 再转发(空数组 = 全部退订,原样放行),服务端对每个文本帧先按共享表核对、通过才交给插件(未共享的线程 id 到不了插件,连接即断),插件推回的 `{"threadId":…}` 顶层 id 换算回虚拟 id 才给浏览器(better-sidebar 的订阅比对要求虚拟 id 精确匹配);侧聊(`sidechat.*`)转发——侧聊子会话带 `meta.parentSession`,按祖先链自动继承父会话的共享,`sidechat.start` 响应里的 `childId` 会换算成虚拟 id 回给浏览器;0.28(alpha.0 与正式版)的 `changes.ops` 长轮询(`wait: true`,最长挂 25 秒,超过中继 15 秒的响应头等待)在服务端去掉 `wait`,远程会话的「本轮文件」退回 0.26 的 2.5 秒轮询;`sidechat.models` 的模型选择器显示的是**子客户端本机**的模型目录,而选择经 `sidechat.model` 作用在**服务端**——两机目录不一致时以服务端实际生效为准;
376
377
  - 每台设备并发流数有上限(32)、同时在途的 invoke 数也有上限(32,超出 429 `too-many-invokes`);`*/internal` 形状的错误码(`gateway/internal` 等)只回码不带消息——那类消息引用的是服务端内部细节。事件应答按「转发时的订阅 + 设备」核对归属。审批/提问的转发是**先到先得**:服务端自己的界面和子客户端谁先应答谁生效,后答的一方被网关拒绝、同步成「已处理」。中继**不代答**:未共享会话的审批/提问事件不会被转发,服务端界面也不在线时这个审批就一直等待——网关会把仍未应答的事件转交给下一个连上来的订阅,等,而不是替你拒绝(代答会把整批待审批一次性判死,砸掉「推送 → 唤醒 → 审批」链路)。未共享会话的**通知类**事件(EMIT 帧:会话列表摘要、账号到期之类的服务端全局状态)一律不转发;唯一的例外是已共享会话(含其子会话)的 `api-session/status`——它只描述单个会话的运行/空闲,逐帧按共享表判定,子客户端靠它在主机侧一轮开始或结束时同步运行状态徽标(断线期间的翻转由重连后补拉列表纠正)。
377
378
 
378
379
 
@@ -473,6 +474,7 @@ open 的体上限 1 MiB(超出 413);每设备并发隧道上限 8,超出
473
474
  - **桌面应用端是可信设备**——它的终端以服务端用户身份运行、不受智能体沙箱与审批限制,文件预览不限制在会话目录内,还能在服务端任意工作区新建会话。「只有已共享会话可见」约束的是会话数据,不是这台机器;把配对码给不值得信任的计算机等于把机器交给它。
474
475
  - DSH 自身的能力边界——网关只负责把流量安全地转发给 DSH,不会也不能给 DSH 本身加它没有的安全措施(比如 `/api` 自己的信任栅栏是 DSH 那边的事)。
475
476
  - 状态文件 `~/.dsh/lan-gate-state.json` 明文存着 VAPID 私钥和所有设备的令牌——这个文件本身就等于全部访问权限,注意宿主机上它的文件权限,别把 `~/.dsh` 目录同步进公共网盘或备份到不受信的地方。
477
+ - 子客户端角色另有状态文件 `~/.dsh/zen-remote-state.json`(0600,tmp+rename 原子写):本机服务端标识、共享会话表与**全部已配对主服务端(最多 8 台)的设备令牌**都在里面——它本身就等于对所有已配对主服务端的访问权限,安全注意与上一条相同。旧版散落的 `zen-remote-server.json` / `zen-remote-shares.json` 会在首次启动时自动迁入并删除。
476
478
 
477
479
  ---
478
480