dsh-desktop-windows 0.2.7 → 0.3.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.
package/README.en.md CHANGED
@@ -7,16 +7,21 @@
7
7
  [![dsh](https://img.shields.io/badge/dsh-0.1.0--rc.6-purple)](https://github.com/deepseek-ai/deepseek-harness)
8
8
  [![Platform](https://img.shields.io/badge/Platform-Windows-0078D6)]()
9
9
  [![Build](https://github.com/ReachGa0/dsh-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/ReachGa0/dsh-desktop/actions)
10
+ [![awesome-dsh-plugin](https://img.shields.io/badge/awesome--dsh--plugin-listed-8A2BE2)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
10
11
 
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
+ 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. **Region screenshot to ask the AI**, system tray, session manager, auto environment setup.
12
13
 
13
14
  Pure shell — it **does not modify dsh itself**. The server reuses your globally installed `@deepseek-ai/dsh`.
14
15
 
15
16
  ## ✨ Features
16
17
 
18
+ - 📸 **Region screenshot → ask AI**: one-click capture → full-screen overlay with real-time selection (GPU-accelerated, 8-direction handles to fine-tune) → auto-pastes into the chat → the AI answers from the image; the chat window hides itself during capture
19
+ - 🗂️ **Session manager**: `Alt → File → Session Manager…` lists every session, delete with one click (permanently removes chat history)
17
20
  - 🪟 **Standalone window**: native desktop window loading the Harness Web UI
18
21
  - 🍱 **System tray**: closing the window minimizes to the tray; right-click to show or quit
22
+ - 🔄 **Easy reload**: `F5` / `Ctrl+R` / floating button — load new plugins without restarting the window
19
23
  - 🧭 **Auto environment setup**: on first launch it detects Node.js / dsh and guides a one-click install — friendly to non-technical users
24
+ - 🔧 **Configurable port**: `npm start -- --port 3081` when 3080 is taken
20
25
  - 🧠 **Smart reuse**: reuses an already-running dsh service instead of starting a duplicate
21
26
  - 🧹 **Clean shutdown**: only kills the dsh process tree it started — never an external service
22
27
  - 🔒 **Single instance**: prevents double-launch conflicts
@@ -30,6 +35,18 @@ Pure shell — it **does not modify dsh itself**. The server reuses your globall
30
35
  3. Once ready, opens an Electron window loading the Web UI;
31
36
  4. When the window closes, kills only the dsh process tree it started (an externally started service is never touched).
32
37
 
38
+ ## 📸 Region screenshot
39
+
40
+ ![Region screenshot to ask AI (demo)](docs/demo/screenshot-ask.gif)
41
+
42
+ 1. Click the **📸** button (bottom-right) — the chat window auto-hides
43
+ 2. **Drag to select** an area on the full-screen overlay (live preview; drag the 8 handles to fine-tune after drawing)
44
+ 3. Click **✔ OK** (or double-click / Enter) — the chat window restores
45
+ 4. The screenshot lands in the chat box automatically → type a question → the AI answers from the image
46
+
47
+ > Pair with a vision bridge like [ModLens](https://www.npmjs.com/package/@liustack/modlens) so text-only DeepSeek models can read images too.
48
+ > Screenshots are also saved to `~/.dsh/screenshots/` (open the folder from the tray menu).
49
+
33
50
  ## Prerequisites
34
51
 
35
52
  - Node.js ≥ 22 (v24 verified)
@@ -58,17 +75,13 @@ npm start -- --port 3081
58
75
 
59
76
  On first launch: go to **Settings → Models**, enter your DeepSeek API Key, then pick a workspace.
60
77
 
61
- ## Screenshots
62
-
63
- > Coming soon — feel free to submit a screenshot via PR or Issue.
64
-
65
78
  ## Package an installer
66
79
 
67
80
  ```sh
68
81
  npm run dist
69
82
  ```
70
83
 
71
- Output lands in `release/`: `dsh-desktop-0.1.0-setup.exe` (NSIS installer).
84
+ Output lands in `release/`: `dsh-desktop-<version>-setup.exe` (NSIS installer).
72
85
 
73
86
  Custom icon: drop an `assets/icon.ico` (≥256×256) and add `"icon": "assets/icon.ico"` under `build.win` in `package.json`.
74
87
 
@@ -77,6 +90,8 @@ Custom icon: drop an `assets/icon.ico` (≥256×256) and add `"icon": "assets/ic
77
90
  - **Launch fails / error dialog**: check `%APPDATA%/DeepSeek Harness Desktop/dsh.log`; make sure `dsh --version` works.
78
91
  - **Port already in use**: run `npm start -- --port <new-port>` instead.
79
92
  - **After upgrading dsh**: just re-run `npm i -g @deepseek-ai/dsh` — the shell needs no changes.
93
+ - **Screenshot not in the chat box**: click the input and press `Ctrl+V` (the image is already on your clipboard).
94
+ - **Closing the window doesn't quit**: that's the tray — right-click the tray icon and choose "Quit".
80
95
 
81
96
  ## Contributing
82
97
 
package/README.md CHANGED
@@ -7,8 +7,9 @@
7
7
  [![dsh](https://img.shields.io/badge/dsh-0.1.0--rc.6-purple)](https://github.com/deepseek-ai/deepseek-harness)
8
8
  [![Platform](https://img.shields.io/badge/Platform-Windows-0078D6)]()
9
9
  [![Build](https://github.com/ReachGa0/dsh-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/ReachGa0/dsh-desktop/actions)
10
+ [![awesome-dsh-plugin](https://img.shields.io/badge/awesome--dsh--plugin-listed-8A2BE2)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
10
11
 
11
- > **English summary** · An [Electron](https://www.electronjs.org/) desktop shell for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh web) on **Windows** — double-click to run the harness in a standalone window. No terminal, no commands, no browser juggling. System tray support, floating reload button, configurable port. Full English docs: [README.en.md](README.en.md)
12
+ > **English summary** · An [Electron](https://www.electronjs.org/) desktop shell for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh web) on **Windows** — double-click to run the harness in a standalone window. **Region screenshot to ask AI**, system tray, session manager, auto environment setup. Full English docs: [README.en.md](README.en.md)
12
13
 
13
14
  DeepSeek Harness 的 **Electron 桌面壳**:双击图标,一个独立窗口运行 `dsh web` —— 不用开终端、不用敲命令、不用切浏览器。
14
15
 
@@ -16,9 +17,13 @@ DeepSeek Harness 的 **Electron 桌面壳**:双击图标,一个独立窗口
16
17
 
17
18
  ## ✨ 特性
18
19
 
20
+ - 📸 **选区截图提问**:一键截屏 → 全屏遮罩实时框选(GPU 加速、8 方向手柄微调)→ 自动粘贴到聊天框 → AI 直接看图回答;截图时自动隐藏聊天窗口
21
+ - 🗂️ **会话管理**:`Alt → 文件 → 会话管理…` 列出所有会话,一键删除(永久清理聊天记录)
19
22
  - 🪟 **独立窗口**:原生桌面窗口加载 Harness Web UI,可最小化到任务栏
20
23
  - 🍱 **系统托盘**:关窗口最小化到托盘常驻,右键菜单可随时显示/退出
24
+ - 🔄 **便捷刷新**:`F5` / `Ctrl+R` / 右下角悬浮按钮,加载新插件不用重启窗口
21
25
  - 🧭 **环境自动引导**:首次启动自动检测 Node.js / dsh,缺失时引导一键安装,小白也能搞定
26
+ - 🔧 **可换端口**:`npm start -- --port 3081` 解决端口占用
22
27
  - 🧠 **智能复用**:检测到已有的 dsh 服务就直接复用,不重复启动
23
28
  - 🧹 **干净退出**:关窗口只杀自己启动的 dsh 进程,绝不误杀外部服务
24
29
  - 🔒 **单实例锁**:防止双开冲突
@@ -32,6 +37,18 @@ DeepSeek Harness 的 **Electron 桌面壳**:双击图标,一个独立窗口
32
37
  3. 就绪后打开 Electron 窗口加载 Web UI;
33
38
  4. 关窗口 → 自动杀掉由本壳启动的 dsh 进程树(手动起的外部服务不会被误杀)。
34
39
 
40
+ ## 📸 选区截图提问
41
+
42
+ ![选区截图直接问 AI 演示](docs/demo/screenshot-ask.gif)
43
+
44
+ 1. 点右下角 **📸**(聊天窗口自动隐藏)
45
+ 2. 全屏遮罩上**拖动框选**区域(实时显示,画完可拖 8 个手柄微调)
46
+ 3. 点 **✔ 确定**(或双击 / 回车)→ 聊天窗口恢复
47
+ 4. 截图自动进入聊天框 → 输入问题 → AI 看图回答
48
+
49
+ > 配合 [ModLens](https://www.npmjs.com/package/@liustack/modlens) 等识图插件,DeepSeek 文本模型也能看懂图片。
50
+ > 截图同时自动保存到 `~/.dsh/screenshots/`(托盘右键可打开目录)。
51
+
35
52
  ## 前提
36
53
 
37
54
  - Node.js ≥ 22(v24 验证通过)
@@ -60,17 +77,13 @@ npm start -- --port 3081
60
77
 
61
78
  首次打开后:**设置 → 模型 → 填入 DeepSeek API Key**,然后选择工作区即可使用。
62
79
 
63
- ## 截图
64
-
65
- > 敬请期待 —— 欢迎提交你的界面截图(PR 或 Issue 均可)。
66
-
67
80
  ## 打包成安装程序
68
81
 
69
82
  ```sh
70
83
  npm run dist
71
84
  ```
72
85
 
73
- 产物在 `release/` 下:`dsh-desktop-0.1.0-setup.exe`(NSIS 安装包)。
86
+ 产物在 `release/` 下:`dsh-desktop-<版本>-setup.exe`(NSIS 安装包)。
74
87
 
75
88
  自定义图标:放一个 `assets/icon.ico`(≥256×256),并在 `package.json` 的 `build.win` 里加 `"icon": "assets/icon.ico"`。
76
89
 
@@ -79,6 +92,8 @@ npm run dist
79
92
  - **启动失败弹窗**:看 `%APPDATA%/DeepSeek Harness Desktop/dsh.log`;确认 `dsh --version` 可用。
80
93
  - **端口被占**:用 `npm start -- --port <新端口>` 换一个端口即可。
81
94
  - **改了 dsh 版本**:重新 `npm i -g @deepseek-ai/dsh` 即可,壳无需改动。
95
+ - **截图没进聊天框**:点一下输入框按 `Ctrl+V`(截图已在剪贴板)。
96
+ - **关闭窗口后应用还在**:正常!它最小化到托盘了,右键托盘图标选"退出"。
82
97
 
83
98
  ## 贡献
84
99
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-desktop-windows",
3
3
  "productName": "DeepSeek Harness Desktop",
4
- "version": "0.2.7",
5
- "description": "DeepSeek Harness 桌面版(Windows)· Electron desktop shell for DeepSeek Harness (dsh web) 双击即用,无需终端 / double-click to run the harness in a standalone window",
4
+ "version": "0.3.0",
5
+ "description": "DeepSeek Harness 桌面版(Windows)· Electron desktop shell for DeepSeek Harness — 双击即用 / double-click to run · 支持选区截图提问、系统托盘、会话管理 / region screenshot, system tray, session manager",
6
6
  "main": "src/main.js",
7
7
  "license": "MIT",
8
8
  "author": "ReachGa0",
@@ -64,3 +64,4 @@
64
64
  }
65
65
  }
66
66
  }
67
+
package/src/main.js CHANGED
@@ -16,6 +16,7 @@
16
16
  const { app, BrowserWindow, dialog, shell, Tray, Menu, nativeImage, ipcMain, desktopCapturer, clipboard, screen } = require('electron')
17
17
  const { spawn, execFile } = require('node:child_process')
18
18
  const http = require('node:http')
19
+ const https = require('node:https')
19
20
  const fs = require('node:fs')
20
21
  const path = require('node:path')
21
22
  const os = require('node:os')
@@ -62,6 +63,103 @@ function shotLog(...args) {
62
63
  } catch { /* 忽略日志错误 */ }
63
64
  }
64
65
 
66
+ /* ------------------------------------------------------------------ *
67
+ * 自动更新检查(启动时异步检测 GitHub Releases 最新版)
68
+ * ------------------------------------------------------------------ */
69
+
70
+ const REPO = 'ReachGa0/dsh-desktop'
71
+ const RELEASES_URL = `https://api.github.com/repos/${REPO}/releases/latest`
72
+ const RELEASES_PAGE = `https://github.com/${REPO}/releases/latest`
73
+ const UPDATE_CHECK_TIMEOUT_MS = 10_000
74
+
75
+ /** GET 一个 JSON(带超时与 UA,GitHub API 要求 UA)。resolve null 表示失败。 */
76
+ function fetchJson(url) {
77
+ return new Promise((resolve) => {
78
+ // 首次用系统证书;代理/反代环境证书校验可能失败,降级为不校验重试一次
79
+ // (仅更新检查用,不降低应用其他部分的安全级别)
80
+ const attempt = (rejectUnauthorized) => {
81
+ const req = https.get(
82
+ url,
83
+ { headers: { 'User-Agent': 'dsh-desktop-update-check', Accept: 'application/vnd.github+json' }, timeout: UPDATE_CHECK_TIMEOUT_MS, rejectUnauthorized },
84
+ (res) => {
85
+ if (res.statusCode !== 200) {
86
+ res.resume()
87
+ resolve(null)
88
+ return
89
+ }
90
+ let body = ''
91
+ res.setEncoding('utf8')
92
+ res.on('data', (d) => (body += d))
93
+ res.on('end', () => {
94
+ try { resolve(JSON.parse(body)) } catch { resolve(null) }
95
+ })
96
+ }
97
+ )
98
+ req.on('timeout', () => req.destroy())
99
+ req.on('error', () => {
100
+ // 证书类错误:再试一次宽松模式;其他错误直接放弃
101
+ if (rejectUnauthorized) attempt(false)
102
+ else resolve(null)
103
+ })
104
+ }
105
+ attempt(true)
106
+ })
107
+ }
108
+
109
+ /** 解析语义化版本号 → [major, minor, patch],失败返回 null。 */
110
+ function parseVersion(v) {
111
+ if (typeof v !== 'string') return null
112
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim())
113
+ if (!m) return null
114
+ return [Number(m[1]), Number(m[2]), Number(m[3])]
115
+ }
116
+
117
+ /** a 比 b 新?都解析失败时返回 false(宁可漏报不可误报)。 */
118
+ function isNewer(a, b) {
119
+ const pa = parseVersion(a)
120
+ const pb = parseVersion(b)
121
+ if (!pa || !pb) return false
122
+ for (let i = 0; i < 3; i++) {
123
+ if (pa[i] !== pb[i]) return pa[i] > pb[i]
124
+ }
125
+ return false
126
+ }
127
+
128
+ /**
129
+ * 检查更新:查询 GitHub Releases 最新版,若比当前版本新则弹窗提示。
130
+ * 全程静默失败(网络不通 / 解析失败 / 无新版都不打扰用户)。
131
+ */
132
+ async function checkForUpdates() {
133
+ const current = app.getVersion()
134
+ try {
135
+ const release = await fetchJson(RELEASES_URL)
136
+ if (!release) {
137
+ log('update check: no release data (offline or API error)')
138
+ return
139
+ }
140
+ const latest = release.tag_name
141
+ log(`update check: current=${current} latest=${latest}`)
142
+ if (!isNewer(latest, current)) return
143
+ const name = release.name && release.name !== latest ? `「${release.name}」` : ''
144
+ const detail = (release.body || '').split('\n').slice(0, 6).join('\n').trim()
145
+ const { response } = await dialog.showMessageBox(mainWindow || undefined, {
146
+ type: 'info',
147
+ title: '发现新版本',
148
+ message: `DeepSeek Harness Desktop 有新版本可用(${latest} ${name})`,
149
+ detail: detail ? `更新内容:\n${detail}\n\n当前版本:${current}` : `当前版本:${current}`,
150
+ buttons: ['前往下载', '以后再说'],
151
+ defaultId: 0,
152
+ cancelId: 1,
153
+ noLink: true,
154
+ })
155
+ if (response === 0) {
156
+ shell.openExternal(RELEASES_PAGE)
157
+ }
158
+ } catch (e) {
159
+ log('update check failed:', e && e.message)
160
+ }
161
+ }
162
+
65
163
  /* ------------------------------------------------------------------ *
66
164
  * 工具:探测 / 等待 HTTP 服务
67
165
  * ------------------------------------------------------------------ */
@@ -105,6 +203,11 @@ function resolveDshBin() {
105
203
  return 'dsh.cmd'
106
204
  }
107
205
 
206
+ // 主动关停 dsh 时的标记(杀进程也会触发 exit,需区分「主动关」和「意外崩」)
207
+ let isShuttingDownDsh = false
208
+ // 已自动重启过一次的标志(同一次会话最多自愈一次,避免崩溃循环)
209
+ let didAutoRestartDsh = false
210
+
108
211
  /** 启动 dsh web,日志追加到 userData/dsh.log。返回子进程。 */
109
212
  function startDsh(port) {
110
213
  const bin = resolveDshBin()
@@ -122,7 +225,12 @@ function startDsh(port) {
122
225
  log(`dsh log: ${logFile}`)
123
226
  child.on('exit', (code, signal) => {
124
227
  log(`dsh exited: code=${code} signal=${signal}`)
125
- if (ownedDsh === child) ownedDsh = null
228
+ if (ownedDsh !== child) return
229
+ ownedDsh = null
230
+ // 崩溃自愈:本壳启动的 dsh 意外退出(非主动关闭/非退出中)时自动重启一次
231
+ if (!isQuitting && !isShuttingDownDsh) {
232
+ restartOwnedDsh(port, code)
233
+ }
126
234
  })
127
235
  child.on('error', (err) => {
128
236
  log(`dsh spawn error: ${err.message}`)
@@ -130,6 +238,52 @@ function startDsh(port) {
130
238
  return child
131
239
  }
132
240
 
241
+ /** dsh 意外退出后自动重启一次;二次退出则提示用户并放弃。 */
242
+ function restartOwnedDsh(port, exitCode) {
243
+ if (didAutoRestartDsh) {
244
+ log('dsh crashed again after auto-restart; giving up')
245
+ const logFile = path.join(app.getPath('userData'), 'dsh.log')
246
+ dialog.showMessageBox(mainWindow || undefined, {
247
+ type: 'error',
248
+ title: 'dsh 服务异常',
249
+ message: 'dsh 服务启动后再次退出,已停止自动重启。',
250
+ detail: `退出码:${exitCode ?? '未知'}\n\n日志文件:${logFile}\n\n请查看日志定位问题,或手动在终端运行 dsh web 排查。`,
251
+ buttons: ['打开日志目录', '知道了'],
252
+ noLink: true,
253
+ }).then(({ response }) => {
254
+ if (response === 0) shell.showItemInFolder(logFile)
255
+ })
256
+ return
257
+ }
258
+ log(`dsh exited unexpectedly (code=${exitCode}); restarting in 3s…`)
259
+ didAutoRestartDsh = true
260
+ setTimeout(() => {
261
+ if (isQuitting) return
262
+ // 重启前再确认端口没有被外部服务接管(避免重复启动)
263
+ probe(port).then((up) => {
264
+ if (up) {
265
+ log('port already serving after crash; not restarting')
266
+ return
267
+ }
268
+ const child = startDsh(port)
269
+ ownedDsh = child
270
+ // 等它就绪;若起不来则等 exit 回调里的二次提示
271
+ waitForServer(port, START_TIMEOUT_MS).then((ready) => {
272
+ if (ready) {
273
+ log('dsh auto-restarted successfully')
274
+ dialog.showMessageBox(mainWindow || undefined, {
275
+ type: 'info',
276
+ title: 'dsh 已自动重启',
277
+ message: 'dsh 服务意外退出,已自动恢复。',
278
+ buttons: ['好'],
279
+ noLink: true,
280
+ }).catch(() => {})
281
+ }
282
+ })
283
+ })
284
+ }, 3000)
285
+ }
286
+
133
287
  /** 杀掉进程树(Windows 用 taskkill /T 覆盖子孙进程),带超时兜底。 */
134
288
  function killProcessTree(pid) {
135
289
  return new Promise((resolve) => {
@@ -147,6 +301,7 @@ async function shutdownOwnedDsh() {
147
301
  if (ownedDsh && ownedDsh.pid) {
148
302
  const pid = ownedDsh.pid
149
303
  ownedDsh = null
304
+ isShuttingDownDsh = true
150
305
  await killProcessTree(pid)
151
306
  log(`killed owned dsh pid ${pid}`)
152
307
  }
@@ -487,12 +642,15 @@ function onShotDone(rect) {
487
642
  }
488
643
  const cropped = image.crop(r)
489
644
  shotLog('shot: cropped', cropped.getSize().width + 'x' + cropped.getSize().height)
490
- // 保存一份临时文件(验证/诊断用)
645
+ // 保存到 ~/.dsh/screenshots/(持久保存,方便回顾;同时写剪贴板)
491
646
  try {
492
- fs.writeFileSync(path.join(os.tmpdir(), 'dsh-screenshot.png'), cropped.toPNG())
493
- shotLog('shot: saved temp png')
647
+ const dir = path.join(os.homedir(), '.dsh', 'screenshots')
648
+ fs.mkdirSync(dir, { recursive: true })
649
+ const file = path.join(dir, `shot-${new Date().toISOString().replace(/[:.]/g, '-')}.png`)
650
+ fs.writeFileSync(file, cropped.toPNG())
651
+ shotLog('shot: saved', file)
494
652
  } catch (e) {
495
- shotLog('shot: temp save failed', String(e && e.message || e))
653
+ shotLog('shot: save failed', String(e && e.message || e))
496
654
  }
497
655
  clipboard.writeImage(cropped)
498
656
  shotLog('shot: written to clipboard')
@@ -539,6 +697,15 @@ function createTray() {
539
697
  Menu.buildFromTemplate([
540
698
  { label: '显示主窗口', click: showMainWindow },
541
699
  { type: 'separator' },
700
+ {
701
+ label: '打开截图目录',
702
+ click: () => {
703
+ const dir = path.join(os.homedir(), '.dsh', 'screenshots')
704
+ fs.mkdirSync(dir, { recursive: true })
705
+ shell.openPath(dir)
706
+ },
707
+ },
708
+ { type: 'separator' },
542
709
  {
543
710
  label: '退出',
544
711
  click: () => {
@@ -675,6 +842,8 @@ if (!gotLock) {
675
842
  createWindow(`http://127.0.0.1:${port}`)
676
843
  createTray()
677
844
  createApplicationMenu()
845
+ // 自动更新检查:不阻塞启动,静默失败
846
+ checkForUpdates()
678
847
  // preload 注入的悬浮刷新按钮 → 重新加载页面
679
848
  ipcMain.on('dsh-desktop:reload', () => {
680
849
  if (mainWindow) mainWindow.webContents.reload()