dsh-desktop-windows 0.1.3 → 0.1.4

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/README.en.md CHANGED
@@ -16,6 +16,7 @@ Pure shell — it **does not modify dsh itself**. The server reuses your globall
16
16
 
17
17
  - 🪟 **Standalone window**: native desktop window loading the Harness Web UI
18
18
  - 🍱 **System tray**: closing the window minimizes to the tray; right-click to show or quit
19
+ - 🧭 **Auto environment setup**: on first launch it detects Node.js / dsh and guides a one-click install — friendly to non-technical users
19
20
  - 🧠 **Smart reuse**: reuses an already-running dsh service instead of starting a duplicate
20
21
  - 🧹 **Clean shutdown**: only kills the dsh process tree it started — never an external service
21
22
  - 🔒 **Single instance**: prevents double-launch conflicts
package/README.md CHANGED
@@ -18,6 +18,7 @@ DeepSeek Harness 的 **Electron 桌面壳**:双击图标,一个独立窗口
18
18
 
19
19
  - 🪟 **独立窗口**:原生桌面窗口加载 Harness Web UI,可最小化到任务栏
20
20
  - 🍱 **系统托盘**:关窗口最小化到托盘常驻,右键菜单可随时显示/退出
21
+ - 🧭 **环境自动引导**:首次启动自动检测 Node.js / dsh,缺失时引导一键安装,小白也能搞定
21
22
  - 🧠 **智能复用**:检测到已有的 dsh 服务就直接复用,不重复启动
22
23
  - 🧹 **干净退出**:关窗口只杀自己启动的 dsh 进程,绝不误杀外部服务
23
24
  - 🔒 **单实例锁**:防止双开冲突
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-desktop-windows",
3
3
  "productName": "DeepSeek Harness Desktop",
4
- "version": "0.1.3",
4
+ "version": "0.1.4",
5
5
  "description": "DeepSeek Harness 桌面版(Windows)· Electron desktop shell for DeepSeek Harness (dsh web) — 双击即用,无需终端 / double-click to run the harness in a standalone window",
6
6
  "main": "src/main.js",
7
7
  "license": "MIT",
package/src/main.js CHANGED
@@ -39,6 +39,7 @@ let mainWindow = null
39
39
  let ownedDsh = null // 本壳启动的 dsh 子进程;null 表示复用了外部已有服务
40
40
  let tray = null // 系统托盘
41
41
  let isQuitting = false // 用户从托盘选择退出时置 true,放行窗口关闭
42
+ let setupWindow = null // 首次环境引导窗口
42
43
 
43
44
  /* ------------------------------------------------------------------ *
44
45
  * 工具:日志
@@ -138,6 +139,106 @@ async function shutdownOwnedDsh() {
138
139
  }
139
140
  }
140
141
 
142
+ /* ------------------------------------------------------------------ *
143
+ * 环境检测与首次引导
144
+ * ------------------------------------------------------------------ */
145
+
146
+ /** 执行一条命令并收集输出(Windows 下经 shell 运行,参数均为常量)。 */
147
+ function runCmd(cmd, args) {
148
+ return new Promise((resolve) => {
149
+ const full = args.length ? `"${cmd}" ${args.join(' ')}` : `"${cmd}"`
150
+ const child = spawn(full, { shell: true, windowsHide: true })
151
+ let out = ''
152
+ child.stdout.on('data', (d) => (out += d.toString()))
153
+ child.stderr.on('data', (d) => (out += d.toString()))
154
+ child.on('error', () => resolve(null))
155
+ child.on('close', (code) => resolve({ code, out: out.trim() }))
156
+ })
157
+ }
158
+
159
+ /** 检测 Node.js 与 dsh 是否可用。 */
160
+ async function checkEnvironment() {
161
+ const node = await runCmd('node', ['--version'])
162
+ const dshBin = resolveDshBin()
163
+ const dsh = await runCmd(dshBin, ['--version'])
164
+ return {
165
+ node:
166
+ node && node.code === 0
167
+ ? { ok: true, version: node.out.replace(/^v/i, '') }
168
+ : { ok: false, error: '未检测到 Node.js,请先安装(≥ 22)' },
169
+ dsh:
170
+ dsh && dsh.code === 0
171
+ ? { ok: true, version: (dsh.out.split('\n')[0] || 'dsh').trim() }
172
+ : { ok: false, error: `未检测到 dsh(${dshBin}),点击右侧按钮自动安装` },
173
+ }
174
+ }
175
+
176
+ /** 自动安装 dsh(npm 全局安装),输出流式回传引导窗口。 */
177
+ function installDsh() {
178
+ return new Promise((resolve) => {
179
+ const child = spawn('npm.cmd', ['i', '-g', '@deepseek-ai/dsh'], { shell: true, windowsHide: true })
180
+ const send = (line) => {
181
+ if (setupWindow && !setupWindow.isDestroyed()) {
182
+ setupWindow.webContents.send('setup:install-log', line)
183
+ }
184
+ }
185
+ child.stdout.on('data', (d) => send(d.toString().trim()))
186
+ child.stderr.on('data', (d) => send(d.toString().trim()))
187
+ child.on('error', (e) => resolve({ ok: false, error: e.message }))
188
+ child.on('close', (code) =>
189
+ resolve(code === 0 ? { ok: true } : { ok: false, error: `npm 退出码 ${code}` })
190
+ )
191
+ })
192
+ }
193
+
194
+ /** 打开首次环境引导窗口;resolve('ready')=环境就绪,resolve('closed')=用户关闭。 */
195
+ function startSetupFlow() {
196
+ return new Promise((resolve) => {
197
+ setupWindow = new BrowserWindow({
198
+ width: 580,
199
+ height: 660,
200
+ resizable: false,
201
+ maximizable: false,
202
+ minimizable: false,
203
+ title: 'DeepSeek Harness 环境设置',
204
+ autoHideMenuBar: true,
205
+ icon: path.join(__dirname, '..', 'assets', 'icon.ico'),
206
+ webPreferences: {
207
+ contextIsolation: true,
208
+ nodeIntegration: false,
209
+ sandbox: true,
210
+ preload: path.join(__dirname, 'setup-preload.js'),
211
+ },
212
+ })
213
+ setupWindow.setMenuBarVisibility(false)
214
+ setupWindow.loadFile(path.join(__dirname, 'setup.html'))
215
+ setupWindow.on('closed', () => {
216
+ setupWindow = null
217
+ resolve('closed')
218
+ })
219
+
220
+ ipcMain.removeHandler('setup:check')
221
+ ipcMain.removeHandler('setup:install-dsh')
222
+ ipcMain.removeHandler('setup:open-node')
223
+ ipcMain.removeHandler('setup:open-settings')
224
+ ipcMain.removeHandler('setup:finish')
225
+
226
+ ipcMain.handle('setup:check', () => checkEnvironment())
227
+ ipcMain.handle('setup:install-dsh', () => installDsh())
228
+ ipcMain.handle('setup:open-node', () => shell.openExternal('https://nodejs.org'))
229
+ ipcMain.handle('setup:open-settings', () => shell.openExternal('https://platform.deepseek.com'))
230
+ ipcMain.handle('setup:finish', async () => {
231
+ const env = await checkEnvironment()
232
+ if (env.node.ok && env.dsh.ok) {
233
+ if (setupWindow && !setupWindow.isDestroyed()) setupWindow.destroy()
234
+ resolve('ready')
235
+ return { ok: true }
236
+ }
237
+ return { ok: false, env }
238
+ })
239
+ })
240
+ }
241
+
141
242
  /* ------------------------------------------------------------------ *
142
243
  * 窗口
143
244
  * ------------------------------------------------------------------ */
@@ -294,6 +395,22 @@ if (!gotLock) {
294
395
  })
295
396
 
296
397
  app.whenReady().then(async () => {
398
+ // 0) 环境检查:Node / dsh 缺失时先走引导窗口
399
+ let env = await checkEnvironment()
400
+ if (!env.node.ok || !env.dsh.ok) {
401
+ const result = await startSetupFlow()
402
+ if (result !== 'ready') {
403
+ // 用户关闭引导窗口 → 退出
404
+ app.quit()
405
+ return
406
+ }
407
+ env = await checkEnvironment()
408
+ if (!env.node.ok || !env.dsh.ok) {
409
+ app.quit()
410
+ return
411
+ }
412
+ }
413
+
297
414
  // 1) 已有服务(可能是用户手动起的 dsh web)→ 直接复用
298
415
  let port = resolvePort()
299
416
  let external = await probe(port)
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ // 引导窗口(setup.html)的 preload:通过 contextBridge 暴露安全 API
4
+ const { contextBridge, ipcRenderer } = require('electron')
5
+
6
+ contextBridge.exposeInMainWorld('setupAPI', {
7
+ // 返回环境检测结果 { node: {ok,version,error}, dsh: {ok,version,error} }
8
+ check: () => ipcRenderer.invoke('setup:check'),
9
+ // 自动安装 dsh,返回 { ok, error? }
10
+ installDsh: () => ipcRenderer.invoke('setup:install-dsh'),
11
+ // 安装过程的输出行(事件流)
12
+ onInstallLog: (cb) => {
13
+ const listener = (_event, line) => cb(line)
14
+ ipcRenderer.on('setup:install-log', listener)
15
+ },
16
+ // 打开 Node.js 下载页
17
+ openNode: () => ipcRenderer.invoke('setup:open-node'),
18
+ // 打开 DeepSeek 开放平台(API Key 指引)
19
+ openSettings: () => ipcRenderer.invoke('setup:open-settings'),
20
+ // 环境就绪,关闭引导窗口继续
21
+ finish: () => ipcRenderer.invoke('setup:finish'),
22
+ })
package/src/setup.html ADDED
@@ -0,0 +1,185 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>DeepSeek Harness 环境设置</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body {
9
+ font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
10
+ background: #0f172a;
11
+ color: #e2e8f0;
12
+ min-height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 24px;
17
+ }
18
+ .card {
19
+ background: #1e293b;
20
+ border-radius: 16px;
21
+ padding: 32px;
22
+ width: 520px;
23
+ box-shadow: 0 8px 30px rgba(0,0,0,.4);
24
+ }
25
+ .logo { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
26
+ .logo img { width: 40px; height: 40px; }
27
+ .logo h1 { font-size: 20px; font-weight: 600; }
28
+ .subtitle { color: #94a3b8; font-size: 13px; margin-bottom: 24px; line-height: 1.6; }
29
+ .item {
30
+ background: #0f172a;
31
+ border: 1px solid #334155;
32
+ border-radius: 10px;
33
+ padding: 14px 16px;
34
+ margin-bottom: 12px;
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 12px;
38
+ }
39
+ .item .icon { font-size: 20px; width: 28px; text-align: center; }
40
+ .item .info { flex: 1; }
41
+ .item .name { font-weight: 600; font-size: 14px; }
42
+ .item .desc { font-size: 12px; color: #94a3b8; margin-top: 2px; }
43
+ .ok { color: #4ade80; }
44
+ .missing { color: #f87171; }
45
+ .btn {
46
+ border: none; border-radius: 8px; padding: 8px 14px;
47
+ font-size: 13px; cursor: pointer; font-weight: 600;
48
+ background: #2563eb; color: #fff; transition: background .2s;
49
+ white-space: nowrap;
50
+ }
51
+ .btn:hover { background: #1d4ed8; }
52
+ .btn.secondary { background: #334155; }
53
+ .btn.secondary:hover { background: #475569; }
54
+ .btn:disabled { opacity: .5; cursor: not-allowed; }
55
+ .log {
56
+ background: #020617; border-radius: 8px; padding: 10px 12px;
57
+ font-family: Consolas, monospace; font-size: 11px; color: #7dd3fc;
58
+ max-height: 120px; overflow-y: auto; margin-bottom: 12px; display: none;
59
+ white-space: pre-wrap; word-break: break-all;
60
+ }
61
+ .footer { margin-top: 8px; display: flex; justify-content: flex-end; gap: 10px; }
62
+ .hint { font-size: 12px; color: #64748b; margin-top: 14px; line-height: 1.6; }
63
+ .ready-banner { display: none; background: #052e16; border: 1px solid #16a34a; color: #86efac; border-radius: 8px; padding: 10px 14px; font-size: 13px; margin-bottom: 12px; }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="card">
68
+ <div class="logo">
69
+ <img src="../assets/whale-256.png" alt="DeepSeek Harness">
70
+ <h1>DeepSeek Harness 环境设置</h1>
71
+ </div>
72
+ <div class="subtitle">首次使用前,请确保以下环境已就绪。缺失的项目可以点击按钮一键修复。</div>
73
+
74
+ <div class="ready-banner" id="readyBanner">✅ 环境已就绪,点击下方按钮开始使用!</div>
75
+
76
+ <div id="nodeItem" class="item">
77
+ <div class="icon" id="nodeIcon">⏳</div>
78
+ <div class="info">
79
+ <div class="name">Node.js(≥ 22)</div>
80
+ <div class="desc" id="nodeDesc">检测中…</div>
81
+ </div>
82
+ <button class="btn secondary" id="nodeBtn" style="display:none">下载 Node.js</button>
83
+ </div>
84
+
85
+ <div id="dshItem" class="item">
86
+ <div class="icon" id="dshIcon">⏳</div>
87
+ <div class="info">
88
+ <div class="name">dsh(DeepSeek Harness 命令行)</div>
89
+ <div class="desc" id="dshDesc">检测中…</div>
90
+ </div>
91
+ <button class="btn" id="dshBtn" style="display:none">自动安装</button>
92
+ </div>
93
+
94
+ <div class="item">
95
+ <div class="icon">🔑</div>
96
+ <div class="info">
97
+ <div class="name">DeepSeek API Key</div>
98
+ <div class="desc">启动后到「设置 → 模型」填入你的 API Key(platform.deepseek.com 获取)</div>
99
+ </div>
100
+ <button class="btn secondary" id="keyBtn">查看指引</button>
101
+ </div>
102
+
103
+ <div class="log" id="log"></div>
104
+
105
+ <div class="footer">
106
+ <button class="btn" id="startBtn" disabled>开始使用</button>
107
+ </div>
108
+ <div class="hint">小提示:也可以打开系统终端手动安装 —— <code>npm i -g @deepseek-ai/dsh</code></div>
109
+ </div>
110
+
111
+ <script>
112
+ const $ = (id) => document.getElementById(id)
113
+
114
+ function setItem(name, ok, desc) {
115
+ const icon = $(name + 'Icon')
116
+ const item = $(name + 'Item')
117
+ if (ok) {
118
+ icon.textContent = '✅'; icon.className = 'icon ok'
119
+ item.style.borderColor = '#166534'
120
+ } else {
121
+ icon.textContent = '❌'; icon.className = 'icon missing'
122
+ item.style.borderColor = '#7f1d1d'
123
+ }
124
+ $(name + 'Desc').textContent = desc
125
+ }
126
+
127
+ function updateReady() {
128
+ const nodeOk = window.__env && window.__env.node && window.__env.node.ok
129
+ const dshOk = window.__env && window.__env.dsh && window.__env.dsh.ok
130
+ const ready = !!(nodeOk && dshOk)
131
+ $('startBtn').disabled = !ready
132
+ $('readyBanner').style.display = ready ? 'block' : 'none'
133
+ return ready
134
+ }
135
+
136
+ async function runCheck() {
137
+ const env = await window.setupAPI.check()
138
+ window.__env = env
139
+ if (env.node && env.node.ok) {
140
+ setItem('node', true, '已安装:Node.js ' + env.node.version)
141
+ $('nodeBtn').style.display = 'none'
142
+ } else {
143
+ setItem('node', false, env.node ? env.node.error : '未检测到 Node.js')
144
+ $('nodeBtn').style.display = 'inline-block'
145
+ }
146
+ if (env.dsh && env.dsh.ok) {
147
+ setItem('dsh', true, '已安装:' + env.dsh.version)
148
+ $('dshBtn').style.display = 'none'
149
+ } else {
150
+ setItem('dsh', false, env.dsh ? env.dsh.error : '未检测到 dsh')
151
+ $('dshBtn').style.display = 'inline-block'
152
+ }
153
+ updateReady()
154
+ }
155
+
156
+ $('nodeBtn').onclick = () => window.setupAPI.openNode()
157
+ $('keyBtn').onclick = () => window.setupAPI.openSettings()
158
+ $('startBtn').onclick = () => window.setupAPI.finish()
159
+
160
+ $('dshBtn').onclick = async () => {
161
+ const btn = $('dshBtn')
162
+ btn.disabled = true
163
+ btn.textContent = '安装中…'
164
+ const log = $('log')
165
+ log.style.display = 'block'
166
+ log.textContent = ''
167
+ window.setupAPI.onInstallLog((line) => {
168
+ log.textContent += line + '\n'
169
+ log.scrollTop = log.scrollHeight
170
+ })
171
+ const result = await window.setupAPI.installDsh()
172
+ if (result && result.ok) {
173
+ log.textContent += '\n✅ dsh 安装成功!'
174
+ await runCheck()
175
+ } else {
176
+ log.textContent += '\n❌ 安装失败:' + (result ? result.error : '未知错误') + '\n可尝试手动运行:npm i -g @deepseek-ai/dsh'
177
+ btn.disabled = false
178
+ btn.textContent = '重试安装'
179
+ }
180
+ }
181
+
182
+ runCheck()
183
+ </script>
184
+ </body>
185
+ </html>