dsh-desktop-windows 0.1.8 → 0.2.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/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.8",
4
+ "version": "0.2.0",
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
@@ -41,6 +41,8 @@ let tray = null // 系统托盘
41
41
  let isQuitting = false // 用户从托盘选择退出时置 true,放行窗口关闭
42
42
  let setupWindow = null // 首次环境引导窗口
43
43
  let sessionsWin = null // 会话管理窗口
44
+ let shotWindow = null // 选区截图窗口
45
+ let pendingShot = null // 全屏截图(nativeImage),选区确认后裁剪
44
46
 
45
47
  /* ------------------------------------------------------------------ *
46
48
  * 工具:日志
@@ -399,6 +401,81 @@ function openSessionsWindow() {
399
401
  ipcMain.handle('sessions:remove', (_e, id) => deleteSession(id))
400
402
  }
401
403
 
404
+ /* ------------------------------------------------------------------ *
405
+ * 选区截图窗口
406
+ * ------------------------------------------------------------------ */
407
+
408
+ function openShotWindow() {
409
+ if (shotWindow && !shotWindow.isDestroyed()) {
410
+ shotWindow.focus()
411
+ return
412
+ }
413
+ shotWindow = new BrowserWindow({
414
+ fullscreen: true,
415
+ frame: false,
416
+ resizable: false,
417
+ alwaysOnTop: true,
418
+ skipTaskbar: true,
419
+ webPreferences: {
420
+ contextIsolation: true,
421
+ nodeIntegration: false,
422
+ sandbox: true,
423
+ preload: path.join(__dirname, 'screenshot-preload.js'),
424
+ },
425
+ })
426
+ shotWindow.setAlwaysOnTop(true, 'screen-saver')
427
+ shotWindow.loadFile(path.join(__dirname, 'screenshot.html'))
428
+ shotWindow.once('ready-to-show', () => {
429
+ shotWindow.show()
430
+ shotWindow.focus()
431
+ })
432
+ shotWindow.webContents.once('did-finish-load', () => {
433
+ if (pendingShot && !shotWindow.isDestroyed()) {
434
+ shotWindow.webContents.send('screenshot:data', pendingShot.toDataURL())
435
+ }
436
+ })
437
+ shotWindow.on('closed', () => {
438
+ shotWindow = null
439
+ })
440
+ }
441
+
442
+ /** 选区确认:裁剪 → 剪贴板 → 恢复主窗口并粘贴。 */
443
+ function onShotDone(rect) {
444
+ const image = pendingShot
445
+ pendingShot = null
446
+ if (shotWindow && !shotWindow.isDestroyed()) shotWindow.destroy()
447
+ showMainWindow() // 恢复显示聊天窗口
448
+ if (!image || !rect) return
449
+ const sf = screen.getPrimaryDisplay().scaleFactor || 1
450
+ const r = {
451
+ x: Math.round(rect.x * sf),
452
+ y: Math.round(rect.y * sf),
453
+ width: Math.round(rect.w * sf),
454
+ height: Math.round(rect.h * sf),
455
+ }
456
+ if (r.width <= 0 || r.height <= 0) return
457
+ const cropped = image.crop(r)
458
+ clipboard.writeImage(cropped)
459
+ if (mainWindow && !mainWindow.isDestroyed()) {
460
+ mainWindow.show()
461
+ mainWindow.focus()
462
+ mainWindow.webContents
463
+ .executeJavaScript(
464
+ `(() => { const el = document.querySelector('textarea, [contenteditable="true"]'); if (el) { el.focus(); return true } return false })()`
465
+ )
466
+ .then((focused) => {
467
+ if (focused) mainWindow.webContents.paste()
468
+ })
469
+ .catch(() => {})
470
+ }
471
+ }
472
+
473
+ function onShotCancel() {
474
+ pendingShot = null
475
+ if (shotWindow && !shotWindow.isDestroyed()) shotWindow.destroy()
476
+ showMainWindow() // 恢复显示聊天窗口
477
+ }
478
+
402
479
  /* ------------------------------------------------------------------ *
403
480
  * 系统托盘
404
481
  * ------------------------------------------------------------------ */
@@ -552,30 +629,34 @@ if (!gotLock) {
552
629
  ipcMain.on('dsh-desktop:reload', () => {
553
630
  if (mainWindow) mainWindow.webContents.reload()
554
631
  })
555
- // 截图:截当前屏幕写剪贴板聚焦聊天输入框并粘贴
632
+ // 选区截图:隐藏主窗口截全屏选区窗口 → 裁剪 → 剪贴板 → 粘贴
556
633
  ipcMain.handle('dsh-desktop:capture', async () => {
557
634
  try {
635
+ // 先隐藏主窗口,避免截到聊天框本身
636
+ if (mainWindow && !mainWindow.isDestroyed()) mainWindow.hide()
637
+ await new Promise((r) => setTimeout(r, 250))
558
638
  const display = screen.getPrimaryDisplay()
559
- const size = display.size
639
+ const sf = display.scaleFactor || 1
560
640
  const sources = await desktopCapturer.getSources({
561
641
  types: ['screen'],
562
- thumbnailSize: { width: size.width, height: size.height },
642
+ thumbnailSize: {
643
+ width: Math.round(display.size.width * sf),
644
+ height: Math.round(display.size.height * sf),
645
+ },
563
646
  })
564
- if (!sources.length) return { ok: false, error: '未找到屏幕源' }
565
- const image = sources[0].thumbnail
566
- if (image.isEmpty()) return { ok: false, error: '截图为空' }
567
- clipboard.writeImage(image)
568
- // 聚焦聊天输入框并触发粘贴
569
- if (mainWindow && !mainWindow.isDestroyed()) {
570
- const focused = await mainWindow.webContents
571
- .executeJavaScript(
572
- `(() => { const el = document.querySelector('textarea, [contenteditable="true"]'); if (el) { el.focus(); return true } return false })()`
573
- )
574
- .catch(() => false)
575
- if (focused) mainWindow.webContents.paste()
647
+ if (!sources.length) {
648
+ showMainWindow()
649
+ return { ok: false, error: '未找到屏幕源' }
650
+ }
651
+ pendingShot = sources[0].thumbnail
652
+ if (pendingShot.isEmpty()) {
653
+ showMainWindow()
654
+ return { ok: false, error: '截图为空' }
576
655
  }
656
+ openShotWindow()
577
657
  return { ok: true }
578
658
  } catch (e) {
659
+ showMainWindow()
579
660
  return { ok: false, error: String((e && e.message) || e) }
580
661
  }
581
662
  })
@@ -585,6 +666,9 @@ if (!gotLock) {
585
666
  dialog.showMessageBox(mainWindow, { type: 'info', message: String(msg), buttons: ['好'] })
586
667
  }
587
668
  })
669
+ // 选区截图结果
670
+ ipcMain.on('screenshot:done', (_e, rect) => onShotDone(rect))
671
+ ipcMain.on('screenshot:cancel', () => onShotCancel())
588
672
  if (external) log('reusing external dsh service on port ' + port)
589
673
  })
590
674
 
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ // 选区截图窗口的 preload:把主进程的 IPC 桥接给页面(contextBridge 隔离)
4
+ const { contextBridge, ipcRenderer } = require('electron')
5
+
6
+ contextBridge.exposeInMainWorld('__screenshot', {
7
+ // 订阅截图数据(主进程发来的 dataURL)
8
+ onData: (cb) => {
9
+ ipcRenderer.on('screenshot:data', (_e, dataUrl) => cb(dataUrl))
10
+ },
11
+ // 提交选区(页面坐标)
12
+ done: (rect) => ipcRenderer.send('screenshot:done', rect),
13
+ // 取消
14
+ cancel: () => ipcRenderer.send('screenshot:cancel'),
15
+ })
@@ -0,0 +1,121 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>截图选区</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
8
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #000; cursor: crosshair; }
9
+ #stage { position: fixed; inset: 0; }
10
+ #stage img { width: 100%; height: 100%; object-fit: fill; display: block; }
11
+ #mask { position: fixed; inset: 0; pointer-events: none; }
12
+ #sel { position: fixed; border: 1.5px solid #2563eb; background: rgba(37, 99, 235, 0.12); display: none; pointer-events: none; }
13
+ #sel .sz { position: absolute; left: 0; top: -22px; background: #2563eb; color: #fff; font: 11px/18px sans-serif; padding: 0 6px; border-radius: 4px; white-space: nowrap; }
14
+ #toolbar {
15
+ position: fixed; display: none; align-items: center; gap: 8px;
16
+ background: rgba(15, 23, 42, 0.92); border: 1px solid #334155;
17
+ border-radius: 10px; padding: 8px 12px; z-index: 10;
18
+ box-shadow: 0 4px 20px rgba(0,0,0,.5);
19
+ }
20
+ #toolbar .tip { color: #94a3b8; font: 12px/1 sans-serif; margin-right: 4px; }
21
+ #toolbar button {
22
+ border: none; border-radius: 6px; padding: 6px 14px; cursor: pointer;
23
+ font: 600 12px/1 sans-serif;
24
+ }
25
+ #okBtn { background: #2563eb; color: #fff; }
26
+ #okBtn:hover { background: #1d4ed8; }
27
+ #cancelBtn { background: #334155; color: #e2e8f0; }
28
+ #cancelBtn:hover { background: #475569; }
29
+ #hint {
30
+ position: fixed; left: 50%; top: 20px; transform: translateX(-50%);
31
+ background: rgba(15, 23, 42, 0.85); color: #e2e8f0;
32
+ font: 13px/1 sans-serif; padding: 8px 16px; border-radius: 8px;
33
+ z-index: 5; pointer-events: none;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div id="stage"><img id="bg" alt=""></div>
39
+ <div id="mask"></div>
40
+ <div id="sel"><div class="sz"></div></div>
41
+ <div id="toolbar">
42
+ <span class="tip">拖动鼠标框选区域</span>
43
+ <button id="okBtn">✔ 确定</button>
44
+ <button id="cancelBtn">✕ 取消 (Esc)</button>
45
+ </div>
46
+ <div id="hint">拖动鼠标框选要截取的区域 · Esc 取消</div>
47
+
48
+ <script>
49
+ const bg = document.getElementById('bg')
50
+ const mask = document.getElementById('mask')
51
+ const sel = document.getElementById('sel')
52
+ const toolbar = document.getElementById('toolbar')
53
+ const hint = document.getElementById('hint')
54
+ const sz = sel.querySelector('.sz')
55
+
56
+ let startX = 0, startY = 0, drawing = false
57
+ let rect = null
58
+
59
+ // 接收主进程传来的截图 dataURL
60
+ window.__screenshot.onData((dataUrl) => {
61
+ bg.src = dataUrl
62
+ setTimeout(() => (hint.style.opacity = '0.9'), 100)
63
+ })
64
+
65
+ const updateMask = () => {
66
+ if (!rect) { mask.style.background = 'rgba(0,0,0,0.45)'; return }
67
+ const { x, y, w, h } = rect
68
+ mask.style.background =
69
+ `linear-gradient(rgba(0,0,0,0.45) ${y}px, transparent ${y}px ${y + h}px, rgba(0,0,0,0.45) ${y + h}px),` +
70
+ `linear-gradient(90deg, rgba(0,0,0,0.45) ${x}px, transparent ${x}px ${x + w}px, rgba(0,0,0,0.45) ${x + w}px)`
71
+ }
72
+
73
+ document.addEventListener('mousedown', (e) => {
74
+ if (e.button !== 0) return
75
+ startX = e.clientX; startY = e.clientY
76
+ drawing = true
77
+ rect = null
78
+ sel.style.display = 'none'
79
+ toolbar.style.display = 'none'
80
+ })
81
+
82
+ document.addEventListener('mousemove', (e) => {
83
+ if (!drawing) return
84
+ const x = Math.min(startX, e.clientX)
85
+ const y = Math.min(startY, e.clientY)
86
+ const w = Math.abs(e.clientX - startX)
87
+ const h = Math.abs(e.clientY - startY)
88
+ rect = { x, y, w, h }
89
+ sel.style.display = 'block'
90
+ sel.style.left = x + 'px'; sel.style.top = y + 'px'
91
+ sel.style.width = w + 'px'; sel.style.height = h + 'px'
92
+ sz.textContent = Math.round(w) + ' × ' + Math.round(h)
93
+ updateMask()
94
+ })
95
+
96
+ document.addEventListener('mouseup', () => {
97
+ if (!drawing) return
98
+ drawing = false
99
+ if (rect && rect.w > 5 && rect.h > 5) {
100
+ const t = document.getElementById('toolbar')
101
+ t.style.display = 'flex'
102
+ t.style.left = Math.min(rect.x + rect.w + 10, window.innerWidth - 220) + 'px'
103
+ t.style.top = (rect.y + rect.h + 12 > window.innerHeight - 60 ? rect.y - 56 : rect.y + rect.h + 12) + 'px'
104
+ hint.style.opacity = '0'
105
+ } else {
106
+ rect = null; sel.style.display = 'none'
107
+ }
108
+ })
109
+
110
+ document.getElementById('okBtn').onclick = () => {
111
+ if (rect) window.__screenshot.done(rect)
112
+ }
113
+ document.getElementById('cancelBtn').onclick = () => window.__screenshot.cancel()
114
+
115
+ document.addEventListener('keydown', (e) => {
116
+ if (e.key === 'Escape') window.__screenshot.cancel()
117
+ if (e.key === 'Enter' && rect) window.__screenshot.done(rect)
118
+ })
119
+ </script>
120
+ </body>
121
+ </html>