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 +1 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/main.js +117 -0
- package/src/setup-preload.js +22 -0
- package/src/setup.html +185 -0
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.
|
|
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>
|