dsh-desktop-windows 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ReachGa0
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,86 @@
1
+ # dsh-desktop
2
+
3
+ [English](README.en.md) | [中文](README.md)
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Electron](https://img.shields.io/badge/Electron-43-blue)](https://www.electronjs.org/)
7
+ [![dsh](https://img.shields.io/badge/dsh-0.1.0--rc.6-purple)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![Platform](https://img.shields.io/badge/Platform-Windows-0078D6)]()
9
+ [![Build](https://github.com/ReachGa0/dsh-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/ReachGa0/dsh-desktop/actions)
10
+
11
+ An **Electron desktop shell** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): double-click an icon and get a standalone window running `dsh web` — no terminal, no manual commands, no browser tab juggling.
12
+
13
+ Pure shell — it **does not modify dsh itself**. The server reuses your globally installed `@deepseek-ai/dsh`.
14
+
15
+ ## ✨ Features
16
+
17
+ - 🪟 **Standalone window**: native desktop window loading the Harness Web UI
18
+ - 🍱 **System tray**: closing the window minimizes to the tray; right-click to show or quit
19
+ - 🧠 **Smart reuse**: reuses an already-running dsh service instead of starting a duplicate
20
+ - 🧹 **Clean shutdown**: only kills the dsh process tree it started — never an external service
21
+ - 🔒 **Single instance**: prevents double-launch conflicts
22
+ - 🎨 **Hardened**: `contextIsolation` + `sandbox` + no Node integration; external links open in your system browser
23
+ - 📦 **One-click installer**: `npm run dist` produces an NSIS setup
24
+
25
+ ## How it works
26
+
27
+ 1. On startup, probes whether a dsh service is already listening on `127.0.0.1:3080`;
28
+ 2. If not, spawns `dsh web --port 3080` automatically (logs to `%APPDATA%/DeepSeek Harness Desktop/dsh.log`);
29
+ 3. Once ready, opens an Electron window loading the Web UI;
30
+ 4. When the window closes, kills only the dsh process tree it started (an externally started service is never touched).
31
+
32
+ ## Prerequisites
33
+
34
+ - Node.js ≥ 22 (v24 verified)
35
+ - Globally installed dsh: `npm i -g @deepseek-ai/dsh` (0.1.0-rc.6 verified)
36
+ - To override the dsh binary path, set the `DSH_BIN` environment variable (e.g. point it at a source build of `apps/cli/src/bin.ts`)
37
+
38
+ ## Install & run
39
+
40
+ ```sh
41
+ cd dsh-desktop
42
+ npm install
43
+ npm start
44
+ ```
45
+
46
+ > If the Electron binary download is slow or fails (common on some networks), use a mirror:
47
+ > ```sh
48
+ > set "ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/"
49
+ > npm install
50
+ > ```
51
+
52
+ **Use a different port** (when 3080 is taken):
53
+
54
+ ```sh
55
+ npm start -- --port 3081
56
+ ```
57
+
58
+ On first launch: go to **Settings → Models**, enter your DeepSeek API Key, then pick a workspace.
59
+
60
+ ## Screenshots
61
+
62
+ > Coming soon — feel free to submit a screenshot via PR or Issue.
63
+
64
+ ## Package an installer
65
+
66
+ ```sh
67
+ npm run dist
68
+ ```
69
+
70
+ Output lands in `release/`: `dsh-desktop-0.1.0-setup.exe` (NSIS installer).
71
+
72
+ Custom icon: drop an `assets/icon.ico` (≥256×256) and add `"icon": "assets/icon.ico"` under `build.win` in `package.json`.
73
+
74
+ ## Troubleshooting
75
+
76
+ - **Launch fails / error dialog**: check `%APPDATA%/DeepSeek Harness Desktop/dsh.log`; make sure `dsh --version` works.
77
+ - **Port already in use**: run `npm start -- --port <new-port>` instead.
78
+ - **After upgrading dsh**: just re-run `npm i -g @deepseek-ai/dsh` — the shell needs no changes.
79
+
80
+ ## Contributing
81
+
82
+ Issues and PRs are welcome! The project is tiny — all the code lives in `src/`.
83
+
84
+ ## License
85
+
86
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # dsh-desktop
2
+
3
+ [中文](README.md) | [English](README.en.md)
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Electron](https://img.shields.io/badge/Electron-43-blue)](https://www.electronjs.org/)
7
+ [![dsh](https://img.shields.io/badge/dsh-0.1.0--rc.6-purple)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![Platform](https://img.shields.io/badge/Platform-Windows-0078D6)]()
9
+ [![Build](https://github.com/ReachGa0/dsh-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/ReachGa0/dsh-desktop/actions)
10
+
11
+ DeepSeek Harness 的 **Electron 桌面壳**:双击图标,一个独立窗口运行 `dsh web` —— 不用开终端、不用敲命令、不用切浏览器。
12
+
13
+ 纯壳,**不改动 dsh 本身**。服务端复用全局安装的 `@deepseek-ai/dsh`。
14
+
15
+ ## ✨ 特性
16
+
17
+ - 🪟 **独立窗口**:原生桌面窗口加载 Harness Web UI,可最小化到任务栏
18
+ - 🍱 **系统托盘**:关窗口最小化到托盘常驻,右键菜单可随时显示/退出
19
+ - 🧠 **智能复用**:检测到已有的 dsh 服务就直接复用,不重复启动
20
+ - 🧹 **干净退出**:关窗口只杀自己启动的 dsh 进程,绝不误杀外部服务
21
+ - 🔒 **单实例锁**:防止双开冲突
22
+ - 🎨 **安全加固**:`contextIsolation` + `sandbox` + 禁用 Node 集成,外部链接走系统浏览器
23
+ - 📦 **一键安装包**:`npm run dist` 生成 NSIS 安装程序
24
+
25
+ ## 原理
26
+
27
+ 1. 启动时探测 `127.0.0.1:3080` 是否已有 dsh 服务;
28
+ 2. 没有 → 自动执行 `dsh web --port 3080`(日志在 `%APPDATA%/DeepSeek Harness Desktop/dsh.log`);
29
+ 3. 就绪后打开 Electron 窗口加载 Web UI;
30
+ 4. 关窗口 → 自动杀掉由本壳启动的 dsh 进程树(手动起的外部服务不会被误杀)。
31
+
32
+ ## 前提
33
+
34
+ - Node.js ≥ 22(v24 验证通过)
35
+ - 全局安装 dsh:`npm i -g @deepseek-ai/dsh`(0.1.0-rc.6 验证通过)
36
+ - 如需覆盖 dsh 路径,设置环境变量 `DSH_BIN`(例如指向源码构建的 `apps/cli/src/bin.ts`)
37
+
38
+ ## 安装与运行
39
+
40
+ ```sh
41
+ cd Desktop\deepseek_work\dsh-desktop
42
+ npm install
43
+ npm start
44
+ ```
45
+
46
+ > 国内网络下 Electron 二进制下载可能很慢或失败,用镜像:
47
+ > ```sh
48
+ > set "ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/"
49
+ > npm install
50
+ > ```
51
+
52
+ **换端口**(3080 被占用时):
53
+
54
+ ```sh
55
+ npm start -- --port 3081
56
+ ```
57
+
58
+ 首次打开后:**设置 → 模型 → 填入 DeepSeek API Key**,然后选择工作区即可使用。
59
+
60
+ ## 截图
61
+
62
+ > 敬请期待 —— 欢迎提交你的界面截图(PR 或 Issue 均可)。
63
+
64
+ ## 打包成安装程序
65
+
66
+ ```sh
67
+ npm run dist
68
+ ```
69
+
70
+ 产物在 `release/` 下:`dsh-desktop-0.1.0-setup.exe`(NSIS 安装包)。
71
+
72
+ 自定义图标:放一个 `assets/icon.ico`(≥256×256),并在 `package.json` 的 `build.win` 里加 `"icon": "assets/icon.ico"`。
73
+
74
+ ## 常见问题
75
+
76
+ - **启动失败弹窗**:看 `%APPDATA%/DeepSeek Harness Desktop/dsh.log`;确认 `dsh --version` 可用。
77
+ - **端口被占**:用 `npm start -- --port <新端口>` 换一个端口即可。
78
+ - **改了 dsh 版本**:重新 `npm i -g @deepseek-ai/dsh` 即可,壳无需改动。
79
+
80
+ ## 贡献
81
+
82
+ 欢迎提 Issue 和 PR!项目很小,代码都在 `src/` 里,容易上手。
83
+
84
+ ## License
85
+
86
+ [MIT](LICENSE)
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "dsh-desktop-windows",
3
+ "productName": "DeepSeek Harness Desktop",
4
+ "version": "0.1.2",
5
+ "description": "Electron desktop shell for DeepSeek Harness (dsh web) on Windows — double-click to run the harness in a standalone window, no terminal needed",
6
+ "main": "src/main.js",
7
+ "license": "MIT",
8
+ "author": "ReachGa0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ReachGa0/dsh-desktop.git"
12
+ },
13
+ "keywords": [
14
+ "deepseek",
15
+ "deepseek-harness",
16
+ "dsh",
17
+ "dsh-plugin",
18
+ "electron",
19
+ "desktop",
20
+ "windows"
21
+ ],
22
+ "files": [
23
+ "src",
24
+ "assets",
25
+ "README.md",
26
+ "README.en.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "scripts": {
33
+ "start": "electron .",
34
+ "dist": "electron-builder --win nsis",
35
+ "dist:dir": "electron-builder --dir"
36
+ },
37
+ "devDependencies": {
38
+ "electron": "^43.4.0",
39
+ "electron-builder": "^26.15.3"
40
+ },
41
+ "build": {
42
+ "appId": "com.dsh.desktop",
43
+ "productName": "DeepSeek Harness Desktop",
44
+ "directories": {
45
+ "output": "release"
46
+ },
47
+ "files": [
48
+ "src/**/*",
49
+ "assets/**/*",
50
+ "package.json"
51
+ ],
52
+ "win": {
53
+ "target": [
54
+ "nsis"
55
+ ],
56
+ "artifactName": "dsh-desktop-${version}-setup.${ext}",
57
+ "icon": "assets/icon.ico"
58
+ },
59
+ "nsis": {
60
+ "oneClick": false,
61
+ "allowToChangeInstallationDirectory": true,
62
+ "createDesktopShortcut": true,
63
+ "shortcutName": "DeepSeek Harness"
64
+ }
65
+ }
66
+ }
package/src/main.js ADDED
@@ -0,0 +1,343 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * dsh-desktop — Electron 壳,包装 DeepSeek Harness Web UI。
5
+ *
6
+ * 职责:
7
+ * 1. 探测 127.0.0.1:3080 是否已有 dsh 服务在跑;
8
+ * 2. 没有则启动全局 `dsh web --port 3080`(日志写入 userData/dsh.log);
9
+ * 3. 等服务就绪后打开 BrowserWindow 加载 Web UI;
10
+ * 4. 窗口退出时,只杀掉由本壳启动的 dsh 进程树;
11
+ * 5. 单实例锁,防止双开。
12
+ *
13
+ * 不修改 dsh 任何代码;服务端用全局安装的 dsh(可用 DSH_BIN 环境变量覆盖路径)。
14
+ */
15
+
16
+ const { app, BrowserWindow, dialog, shell, Tray, Menu, nativeImage, ipcMain } = require('electron')
17
+ const { spawn, execFile } = require('node:child_process')
18
+ const http = require('node:http')
19
+ const fs = require('node:fs')
20
+ const path = require('node:path')
21
+
22
+ const DEFAULT_PORT = 3080
23
+ const START_TIMEOUT_MS = 90_000
24
+ const POLL_INTERVAL_MS = 500
25
+ const LOG_PREFIX = '[dsh-desktop]'
26
+
27
+ /** 解析端口:优先命令行 --port <n>,否则用默认值。 */
28
+ function resolvePort() {
29
+ const idx = process.argv.indexOf('--port')
30
+ if (idx !== -1 && process.argv[idx + 1]) {
31
+ const n = Number(process.argv[idx + 1])
32
+ if (Number.isInteger(n) && n > 0 && n < 65536) return n
33
+ console.warn(LOG_PREFIX, `invalid --port value "${process.argv[idx + 1]}", fallback to ${DEFAULT_PORT}`)
34
+ }
35
+ return DEFAULT_PORT
36
+ }
37
+
38
+ let mainWindow = null
39
+ let ownedDsh = null // 本壳启动的 dsh 子进程;null 表示复用了外部已有服务
40
+ let tray = null // 系统托盘
41
+ let isQuitting = false // 用户从托盘选择退出时置 true,放行窗口关闭
42
+
43
+ /* ------------------------------------------------------------------ *
44
+ * 工具:日志
45
+ * ------------------------------------------------------------------ */
46
+
47
+ function log(...args) {
48
+ console.log(LOG_PREFIX, ...args)
49
+ }
50
+
51
+ /* ------------------------------------------------------------------ *
52
+ * 工具:探测 / 等待 HTTP 服务
53
+ * ------------------------------------------------------------------ */
54
+
55
+ function probe(port, timeoutMs = 1500) {
56
+ return new Promise((resolve) => {
57
+ const req = http.get(
58
+ { host: '127.0.0.1', port, path: '/', timeout: timeoutMs },
59
+ (res) => {
60
+ res.resume()
61
+ resolve(true)
62
+ }
63
+ )
64
+ req.on('timeout', () => req.destroy())
65
+ req.on('error', () => resolve(false))
66
+ })
67
+ }
68
+
69
+ /** 轮询直到服务就绪或超时。 */
70
+ async function waitForServer(port, timeoutMs) {
71
+ const deadline = Date.now() + timeoutMs
72
+ while (Date.now() < deadline) {
73
+ if (await probe(port)) return true
74
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
75
+ }
76
+ return false
77
+ }
78
+
79
+ /* ------------------------------------------------------------------ *
80
+ * dsh 服务进程管理
81
+ * ------------------------------------------------------------------ */
82
+
83
+ /** 解析 dsh 可执行文件:优先 DSH_BIN,否则走 PATH(Windows 下是 dsh.cmd)。 */
84
+ function resolveDshBin() {
85
+ if (process.env.DSH_BIN) return process.env.DSH_BIN
86
+ // 全局 npm 安装的常见位置
87
+ if (process.env.APPDATA) {
88
+ const global = path.join(process.env.APPDATA, 'npm', 'dsh.cmd')
89
+ if (fs.existsSync(global)) return global
90
+ }
91
+ return 'dsh.cmd'
92
+ }
93
+
94
+ /** 启动 dsh web,日志追加到 userData/dsh.log。返回子进程。 */
95
+ function startDsh(port) {
96
+ const bin = resolveDshBin()
97
+ const logFile = path.join(app.getPath('userData'), 'dsh.log')
98
+ const out = fs.openSync(logFile, 'a')
99
+ // shell:true 时把参数拼进命令字符串(参数均为常量),避免 DEP0190 警告
100
+ const cmd = `"${bin}" web --port ${port}`
101
+ const child = spawn(cmd, {
102
+ shell: true,
103
+ windowsHide: true,
104
+ stdio: ['ignore', out, out],
105
+ env: { ...process.env },
106
+ })
107
+ log(`spawned dsh: ${cmd} (pid ${child.pid})`)
108
+ log(`dsh log: ${logFile}`)
109
+ child.on('exit', (code, signal) => {
110
+ log(`dsh exited: code=${code} signal=${signal}`)
111
+ if (ownedDsh === child) ownedDsh = null
112
+ })
113
+ child.on('error', (err) => {
114
+ log(`dsh spawn error: ${err.message}`)
115
+ })
116
+ return child
117
+ }
118
+
119
+ /** 杀掉进程树(Windows 用 taskkill /T 覆盖子孙进程),带超时兜底。 */
120
+ function killProcessTree(pid) {
121
+ return new Promise((resolve) => {
122
+ execFile(
123
+ 'taskkill',
124
+ ['/pid', String(pid), '/T', '/F'],
125
+ { windowsHide: true },
126
+ () => resolve() // 无论成败都继续退出流程
127
+ )
128
+ setTimeout(resolve, 2000)
129
+ })
130
+ }
131
+
132
+ async function shutdownOwnedDsh() {
133
+ if (ownedDsh && ownedDsh.pid) {
134
+ const pid = ownedDsh.pid
135
+ ownedDsh = null
136
+ await killProcessTree(pid)
137
+ log(`killed owned dsh pid ${pid}`)
138
+ }
139
+ }
140
+
141
+ /* ------------------------------------------------------------------ *
142
+ * 窗口
143
+ * ------------------------------------------------------------------ */
144
+
145
+ function createWindow(url) {
146
+ mainWindow = new BrowserWindow({
147
+ width: 1280,
148
+ height: 840,
149
+ minWidth: 960,
150
+ minHeight: 600,
151
+ title: 'DeepSeek Harness',
152
+ autoHideMenuBar: true,
153
+ icon: path.join(__dirname, '..', 'assets', 'icon.ico'),
154
+ webPreferences: {
155
+ contextIsolation: true,
156
+ nodeIntegration: false,
157
+ sandbox: true,
158
+ preload: path.join(__dirname, 'preload.js'),
159
+ },
160
+ })
161
+
162
+ mainWindow.loadURL(url)
163
+ mainWindow.on('closed', () => {
164
+ mainWindow = null
165
+ })
166
+ // F5 刷新(Ctrl+R 由应用菜单 accelerator 提供)
167
+ mainWindow.webContents.on('before-input-event', (event, input) => {
168
+ if (input.type === 'keyDown' && input.key === 'F5') {
169
+ event.preventDefault()
170
+ mainWindow.webContents.reload()
171
+ }
172
+ })
173
+ // 关窗口 → 最小化到托盘(不退出应用);从托盘菜单选"退出"才真正退出
174
+ mainWindow.on('close', (event) => {
175
+ if (!isQuitting) {
176
+ event.preventDefault()
177
+ mainWindow.hide()
178
+ log('window hidden to tray')
179
+ }
180
+ })
181
+
182
+ // 外部链接(非本机)用系统浏览器打开,不在壳内导航
183
+ mainWindow.webContents.setWindowOpenHandler(({ url: target }) => {
184
+ if (target.startsWith('http://127.0.0.1') || target.startsWith('http://localhost')) {
185
+ return { action: 'allow' }
186
+ }
187
+ shell.openExternal(target)
188
+ return { action: 'deny' }
189
+ })
190
+
191
+ mainWindow.webContents.on('will-navigate', (event, target) => {
192
+ if (!target.startsWith(url)) {
193
+ event.preventDefault()
194
+ shell.openExternal(target)
195
+ }
196
+ })
197
+ }
198
+
199
+ /* ------------------------------------------------------------------ *
200
+ * 系统托盘
201
+ * ------------------------------------------------------------------ */
202
+
203
+ function createTray() {
204
+ const iconPath = path.join(__dirname, '..', 'assets', 'tray.png')
205
+ const icon = nativeImage.createFromPath(iconPath)
206
+ tray = new Tray(icon)
207
+ tray.setToolTip('DeepSeek Harness')
208
+ tray.setContextMenu(
209
+ Menu.buildFromTemplate([
210
+ { label: '显示主窗口', click: showMainWindow },
211
+ { type: 'separator' },
212
+ {
213
+ label: '退出',
214
+ click: () => {
215
+ isQuitting = true
216
+ app.quit()
217
+ },
218
+ },
219
+ ])
220
+ )
221
+ tray.on('double-click', showMainWindow)
222
+ }
223
+
224
+ function showMainWindow() {
225
+ if (!mainWindow) return
226
+ mainWindow.show()
227
+ mainWindow.focus()
228
+ }
229
+
230
+ /* ------------------------------------------------------------------ *
231
+ * 应用菜单(autoHideMenuBar 下按 Alt 显示;快捷键始终生效)
232
+ * ------------------------------------------------------------------ */
233
+
234
+ function createApplicationMenu() {
235
+ const template = [
236
+ {
237
+ label: '文件',
238
+ submenu: [
239
+ {
240
+ label: '退出',
241
+ accelerator: 'Alt+F4',
242
+ click: () => {
243
+ isQuitting = true
244
+ app.quit()
245
+ },
246
+ },
247
+ ],
248
+ },
249
+ {
250
+ label: '视图',
251
+ submenu: [
252
+ {
253
+ label: '重新加载',
254
+ accelerator: 'CmdOrCtrl+R',
255
+ click: () => {
256
+ if (mainWindow) mainWindow.webContents.reload()
257
+ },
258
+ },
259
+ {
260
+ label: '强制重新加载(清缓存)',
261
+ accelerator: 'CmdOrCtrl+Shift+R',
262
+ click: () => {
263
+ if (mainWindow) mainWindow.webContents.reloadIgnoringCache()
264
+ },
265
+ },
266
+ { type: 'separator' },
267
+ { role: 'togglefullscreen', label: '全屏' },
268
+ {
269
+ label: '开发者工具',
270
+ accelerator: 'F12',
271
+ click: () => {
272
+ if (mainWindow) mainWindow.webContents.toggleDevTools()
273
+ },
274
+ },
275
+ ],
276
+ },
277
+ ]
278
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template))
279
+ }
280
+
281
+ /* ------------------------------------------------------------------ *
282
+ * 应用生命周期
283
+ * ------------------------------------------------------------------ */
284
+
285
+ const gotLock = app.requestSingleInstanceLock()
286
+ if (!gotLock) {
287
+ app.quit()
288
+ } else {
289
+ app.on('second-instance', () => {
290
+ if (mainWindow) {
291
+ if (mainWindow.isMinimized()) mainWindow.restore()
292
+ mainWindow.focus()
293
+ }
294
+ })
295
+
296
+ app.whenReady().then(async () => {
297
+ // 1) 已有服务(可能是用户手动起的 dsh web)→ 直接复用
298
+ let port = resolvePort()
299
+ let external = await probe(port)
300
+ if (!external) {
301
+ // 2) 没有 → 自己启动,等就绪
302
+ ownedDsh = startDsh(port)
303
+ const ready = await waitForServer(port, START_TIMEOUT_MS)
304
+ if (!ready) {
305
+ const logFile = path.join(app.getPath('userData'), 'dsh.log')
306
+ dialog.showErrorBox(
307
+ 'DeepSeek Harness 启动失败',
308
+ `dsh web 在 ${START_TIMEOUT_MS / 1000}s 内没有就绪。\n\n` +
309
+ `日志文件:${logFile}\n\n` +
310
+ '请确认已全局安装 dsh(npm i -g @deepseek-ai/dsh),或用 DSH_BIN 指定路径。\n' +
311
+ `若端口 ${port} 被其他程序占用,可换端口启动:dsh-desktop --port <新端口>`
312
+ )
313
+ await shutdownOwnedDsh()
314
+ app.quit()
315
+ return
316
+ }
317
+ }
318
+
319
+ createWindow(`http://127.0.0.1:${port}`)
320
+ createTray()
321
+ createApplicationMenu()
322
+ // preload 注入的悬浮刷新按钮 → 重新加载页面
323
+ ipcMain.on('dsh-desktop:reload', () => {
324
+ if (mainWindow) mainWindow.webContents.reload()
325
+ })
326
+ if (external) log('reusing external dsh service on port ' + port)
327
+ })
328
+
329
+ // 托盘常驻:窗口关闭不退出应用;只有用户从托盘选"退出"才真正退出
330
+ app.on('window-all-closed', () => {
331
+ if (isQuitting) {
332
+ shutdownOwnedDsh().finally(() => app.quit())
333
+ }
334
+ })
335
+
336
+ app.on('before-quit', (event) => {
337
+ // 防止退出竞态:先杀掉子进程再放行
338
+ if (ownedDsh) {
339
+ event.preventDefault()
340
+ shutdownOwnedDsh().finally(() => app.quit())
341
+ }
342
+ })
343
+ }
package/src/preload.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ // 最小 preload:为将来扩展保留的桥接点 + 注入悬浮刷新按钮。
4
+ // 页面(dsh Web UI)直接走 HTTP/WebSocket 与本地服务通信,无需 IPC。
5
+
6
+ const { ipcRenderer } = require('electron')
7
+
8
+ // 注入一个悬浮"刷新"按钮(右下角,半透明,鼠标悬停变清晰)
9
+ window.addEventListener('DOMContentLoaded', () => {
10
+ setTimeout(() => {
11
+ if (!document.body) return
12
+ const btn = document.createElement('button')
13
+ btn.textContent = '⟳'
14
+ btn.title = '刷新页面(等价 F5)'
15
+ btn.style.cssText = `
16
+ position: fixed;
17
+ right: 14px;
18
+ bottom: 14px;
19
+ width: 34px;
20
+ height: 34px;
21
+ border-radius: 10px;
22
+ border: none;
23
+ background: rgba(30, 41, 59, 0.72);
24
+ color: #fff;
25
+ font-size: 18px;
26
+ line-height: 1;
27
+ cursor: pointer;
28
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
29
+ opacity: 0.45;
30
+ transition: opacity 0.2s;
31
+ z-index: 2147483647;
32
+ `
33
+ btn.onmouseenter = () => (btn.style.opacity = '1')
34
+ btn.onmouseleave = () => (btn.style.opacity = '0.45')
35
+ btn.onclick = () => ipcRenderer.send('dsh-desktop:reload')
36
+ document.body.appendChild(btn)
37
+ }, 1500)
38
+ })