dsh-desktop-windows 0.2.2 → 0.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-desktop-windows",
3
3
  "productName": "DeepSeek Harness Desktop",
4
- "version": "0.2.2",
4
+ "version": "0.2.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
@@ -18,6 +18,7 @@ const { spawn, execFile } = require('node:child_process')
18
18
  const http = require('node:http')
19
19
  const fs = require('node:fs')
20
20
  const path = require('node:path')
21
+ const os = require('node:os')
21
22
 
22
23
  const DEFAULT_PORT = 3080
23
24
  const START_TIMEOUT_MS = 90_000
@@ -52,6 +53,15 @@ function log(...args) {
52
53
  console.log(LOG_PREFIX, ...args)
53
54
  }
54
55
 
56
+ /** 截图流程日志(追加写入 ~/.dsh/logs/screenshot.log,便于诊断)。 */
57
+ function shotLog(...args) {
58
+ try {
59
+ const line = `[${new Date().toISOString()}] ${args.join(' ')}`
60
+ console.log('[shot]', ...args)
61
+ fs.appendFileSync(path.join(os.homedir(), '.dsh', 'logs', 'screenshot.log'), line + '\n')
62
+ } catch { /* 忽略日志错误 */ }
63
+ }
64
+
55
65
  /* ------------------------------------------------------------------ *
56
66
  * 工具:探测 / 等待 HTTP 服务
57
67
  * ------------------------------------------------------------------ */
@@ -437,6 +447,7 @@ function openShotWindow() {
437
447
  shotWindow.webContents.once('did-finish-load', () => {
438
448
  if (pendingShot && !shotWindow.isDestroyed()) {
439
449
  shotWindow.webContents.send('screenshot:data', pendingShot.toDataURL())
450
+ shotLog('shot: data sent to selector window')
440
451
  }
441
452
  })
442
453
  shotWindow.on('closed', () => {
@@ -454,11 +465,15 @@ function onShotLoaded() {
454
465
 
455
466
  /** 选区确认:裁剪 → 剪贴板 → 恢复主窗口并粘贴。 */
456
467
  function onShotDone(rect) {
468
+ shotLog('shot: done called', rect ? JSON.stringify(rect) : 'null')
457
469
  const image = pendingShot
458
470
  pendingShot = null
459
471
  if (shotWindow && !shotWindow.isDestroyed()) shotWindow.destroy()
460
472
  showMainWindow() // 恢复显示聊天窗口
461
- if (!image || !rect) return
473
+ if (!image || !rect) {
474
+ shotLog('shot: done skipped (no image or rect)')
475
+ return
476
+ }
462
477
  const sf = screen.getPrimaryDisplay().scaleFactor || 1
463
478
  const r = {
464
479
  x: Math.round(rect.x * sf),
@@ -466,13 +481,21 @@ function onShotDone(rect) {
466
481
  width: Math.round(rect.w * sf),
467
482
  height: Math.round(rect.h * sf),
468
483
  }
469
- if (r.width <= 0 || r.height <= 0) return
484
+ if (r.width <= 0 || r.height <= 0) {
485
+ shotLog('shot: invalid crop rect after scale')
486
+ return
487
+ }
470
488
  const cropped = image.crop(r)
489
+ shotLog('shot: cropped', cropped.getSize().width + 'x' + cropped.getSize().height)
471
490
  // 保存一份临时文件(验证/诊断用)
472
491
  try {
473
492
  fs.writeFileSync(path.join(os.tmpdir(), 'dsh-screenshot.png'), cropped.toPNG())
474
- } catch { /* 非致命 */ }
493
+ shotLog('shot: saved temp png')
494
+ } catch (e) {
495
+ shotLog('shot: temp save failed', String(e && e.message || e))
496
+ }
475
497
  clipboard.writeImage(cropped)
498
+ shotLog('shot: written to clipboard')
476
499
  // 等主窗口就绪后尝试自动粘贴;若聚焦失败则提示手动 Ctrl+V
477
500
  if (mainWindow && !mainWindow.isDestroyed()) {
478
501
  setTimeout(async () => {
@@ -658,6 +681,7 @@ if (!gotLock) {
658
681
  })
659
682
  // 选区截图:隐藏主窗口 → 截全屏 → 选区窗口 → 裁剪 → 剪贴板 → 粘贴
660
683
  ipcMain.handle('dsh-desktop:capture', async () => {
684
+ shotLog('capture: requested')
661
685
  try {
662
686
  // 先隐藏主窗口,避免截到聊天框本身
663
687
  if (mainWindow && !mainWindow.isDestroyed()) mainWindow.hide()
@@ -673,17 +697,21 @@ if (!gotLock) {
673
697
  })
674
698
  if (!sources.length) {
675
699
  showMainWindow()
700
+ shotLog('capture: no screen source')
676
701
  return { ok: false, error: '未找到屏幕源' }
677
702
  }
678
703
  pendingShot = sources[0].thumbnail
679
704
  if (pendingShot.isEmpty()) {
680
705
  showMainWindow()
706
+ shotLog('capture: empty thumbnail')
681
707
  return { ok: false, error: '截图为空' }
682
708
  }
709
+ shotLog('capture: captured', pendingShot.getSize().width + 'x' + pendingShot.getSize().height)
683
710
  openShotWindow()
684
711
  return { ok: true }
685
712
  } catch (e) {
686
713
  showMainWindow()
714
+ shotLog('capture: error', String((e && e.message) || e))
687
715
  return { ok: false, error: String((e && e.message) || e) }
688
716
  }
689
717
  })
@@ -8,9 +8,22 @@
8
8
  html, body { width: 100%; height: 100%; overflow: hidden; background: #000; cursor: crosshair; }
9
9
  #stage { position: fixed; inset: 0; }
10
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; }
11
+ .mask { position: fixed; background: rgba(0, 0, 0, 0.45); pointer-events: none; }
12
+ #sel {
13
+ position: fixed; border: 1.5px solid #2563eb;
14
+ background: rgba(37, 99, 235, 0.10);
15
+ display: none; pointer-events: none; z-index: 3;
16
+ }
17
+ #sel .sz {
18
+ position: absolute; left: 0; top: -22px;
19
+ background: #2563eb; color: #fff;
20
+ font: 11px/18px sans-serif; padding: 0 6px; border-radius: 4px; white-space: nowrap;
21
+ }
22
+ .handle {
23
+ position: fixed; width: 9px; height: 9px; background: #2563eb;
24
+ border: 1px solid #fff; border-radius: 2px; display: none;
25
+ pointer-events: auto; z-index: 4;
26
+ }
14
27
  #toolbar {
15
28
  position: fixed; display: none; align-items: center; gap: 8px;
16
29
  background: rgba(15, 23, 42, 0.92); border: 1px solid #334155;
@@ -30,92 +43,179 @@
30
43
  position: fixed; left: 50%; top: 20px; transform: translateX(-50%);
31
44
  background: rgba(15, 23, 42, 0.85); color: #e2e8f0;
32
45
  font: 13px/1 sans-serif; padding: 8px 16px; border-radius: 8px;
33
- z-index: 5; pointer-events: none;
46
+ z-index: 5; pointer-events: none; transition: opacity .15s;
34
47
  }
35
48
  </style>
36
49
  </head>
37
50
  <body>
38
51
  <div id="stage"><img id="bg" alt=""></div>
39
- <div id="mask"></div>
52
+ <div id="maskTop" class="mask"></div>
53
+ <div id="maskBottom" class="mask"></div>
54
+ <div id="maskLeft" class="mask"></div>
55
+ <div id="maskRight" class="mask"></div>
40
56
  <div id="sel"><div class="sz"></div></div>
57
+ <div id="hN" class="handle" style="cursor:n-resize"></div>
58
+ <div id="hS" class="handle" style="cursor:s-resize"></div>
59
+ <div id="hE" class="handle" style="cursor:e-resize"></div>
60
+ <div id="hW" class="handle" style="cursor:w-resize"></div>
61
+ <div id="hNE" class="handle" style="cursor:ne-resize"></div>
62
+ <div id="hNW" class="handle" style="cursor:nw-resize"></div>
63
+ <div id="hSE" class="handle" style="cursor:se-resize"></div>
64
+ <div id="hSW" class="handle" style="cursor:sw-resize"></div>
41
65
  <div id="toolbar">
42
- <span class="tip">拖动鼠标框选区域</span>
66
+ <span class="tip" id="tip">拖动边框微调选区</span>
43
67
  <button id="okBtn">✔ 确定</button>
44
68
  <button id="cancelBtn">✕ 取消 (Esc)</button>
45
69
  </div>
46
- <div id="hint">拖动鼠标框选要截取的区域 · Esc 取消</div>
70
+ <div id="hint">拖动鼠标框选要截取的区域 · 画完后可拖拽边框微调 · Esc 取消</div>
47
71
 
48
72
  <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
73
+ var bg = document.getElementById('bg')
74
+ var sel = document.getElementById('sel')
75
+ var toolbar = document.getElementById('toolbar')
76
+ var hint = document.getElementById('hint')
77
+ var tip = document.getElementById('tip')
78
+ var sz = sel.querySelector('.sz')
79
+ var masks = {
80
+ top: document.getElementById('maskTop'),
81
+ bottom: document.getElementById('maskBottom'),
82
+ left: document.getElementById('maskLeft'),
83
+ right: document.getElementById('maskRight'),
84
+ }
85
+ var handles = {
86
+ n: document.getElementById('hN'), s: document.getElementById('hS'),
87
+ e: document.getElementById('hE'), w: document.getElementById('hW'),
88
+ ne: document.getElementById('hNE'), nw: document.getElementById('hNW'),
89
+ se: document.getElementById('hSE'), sw: document.getElementById('hSW'),
90
+ }
91
+ var W = window.innerWidth, H = window.innerHeight
92
+ var MIN = 8
93
+ var rect = null
94
+ var mode = 'draw' // draw | adjust
95
+ var drag = null
58
96
 
59
- // 接收主进程传来的截图 dataURL
60
- window.__screenshot.onData((dataUrl) => {
61
- bg.onload = () => {
97
+ window.__screenshot.onData(function (dataUrl) {
98
+ bg.onload = function () {
62
99
  window.__screenshot.loaded()
63
- setTimeout(() => (hint.style.opacity = '0.9'), 100)
100
+ setTimeout(function () { hint.style.opacity = '0.9' }, 100)
64
101
  }
65
102
  bg.src = dataUrl
66
103
  })
67
104
 
68
- const updateMask = () => {
69
- if (!rect) { mask.style.background = 'rgba(0,0,0,0.45)'; return }
70
- const { x, y, w, h } = rect
71
- mask.style.background =
72
- `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),` +
73
- `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)`
105
+ function clampRect(r) {
106
+ var x = Math.max(0, Math.min(W - MIN, r.x))
107
+ var y = Math.max(0, Math.min(H - MIN, r.y))
108
+ var w = Math.min(r.w, W - x)
109
+ var h = Math.min(r.h, H - y)
110
+ w = Math.max(MIN, w); h = Math.max(MIN, h)
111
+ return { x: x, y: y, w: w, h: h }
74
112
  }
75
113
 
76
- document.addEventListener('mousedown', (e) => {
77
- if (e.button !== 0) return
78
- startX = e.clientX; startY = e.clientY
79
- drawing = true
80
- rect = null
81
- sel.style.display = 'none'
82
- toolbar.style.display = 'none'
83
- })
84
-
85
- document.addEventListener('mousemove', (e) => {
86
- if (!drawing) return
87
- const x = Math.min(startX, e.clientX)
88
- const y = Math.min(startY, e.clientY)
89
- const w = Math.abs(e.clientX - startX)
90
- const h = Math.abs(e.clientY - startY)
91
- rect = { x, y, w, h }
114
+ function layout() {
115
+ if (!rect) return
116
+ var x = rect.x, y = rect.y, w = rect.w, h = rect.h
92
117
  sel.style.display = 'block'
93
118
  sel.style.left = x + 'px'; sel.style.top = y + 'px'
94
119
  sel.style.width = w + 'px'; sel.style.height = h + 'px'
95
120
  sz.textContent = Math.round(w) + ' × ' + Math.round(h)
96
- updateMask()
121
+ // 遮罩(4 块)
122
+ masks.top.style.cssText = 'left:0;top:0;width:' + W + 'px;height:' + y + 'px;display:block'
123
+ masks.bottom.style.cssText = 'left:0;top:' + (y + h) + 'px;width:' + W + 'px;height:' + (H - y - h) + 'px;display:block'
124
+ masks.left.style.cssText = 'left:0;top:' + y + 'px;width:' + x + 'px;height:' + h + 'px;display:block'
125
+ masks.right.style.cssText = 'left:' + (x + w) + 'px;top:' + y + 'px;width:' + (W - x - w) + 'px;height:' + h + 'px;display:block'
126
+ // 手柄
127
+ if (mode === 'adjust') {
128
+ var half = 4.5
129
+ handles.n.style.cssText = 'left:' + (x + w / 2 - half) + 'px;top:' + (y - half) + 'px;display:block'
130
+ handles.s.style.cssText = 'left:' + (x + w / 2 - half) + 'px;top:' + (y + h - half) + 'px;display:block'
131
+ handles.e.style.cssText = 'left:' + (x + w - half) + 'px;top:' + (y + h / 2 - half) + 'px;display:block'
132
+ handles.w.style.cssText = 'left:' + (x - half) + 'px;top:' + (y + h / 2 - half) + 'px;display:block'
133
+ handles.ne.style.cssText = 'left:' + (x + w - half) + 'px;top:' + (y - half) + 'px;display:block'
134
+ handles.nw.style.cssText = 'left:' + (x - half) + 'px;top:' + (y - half) + 'px;display:block'
135
+ handles.se.style.cssText = 'left:' + (x + w - half) + 'px;top:' + (y + h - half) + 'px;display:block'
136
+ handles.sw.style.cssText = 'left:' + (x - half) + 'px;top:' + (y + h - half) + 'px;display:block'
137
+ } else {
138
+ Object.keys(handles).forEach(function (k) { handles[k].style.display = 'none' })
139
+ }
140
+ }
141
+
142
+ function inside(e, r) {
143
+ return e.clientX >= r.x && e.clientX <= r.x + r.w && e.clientY >= r.y && e.clientY <= r.y + r.h
144
+ }
145
+
146
+ document.addEventListener('mousedown', function (e) {
147
+ if (e.button !== 0) return
148
+ if (mode === 'draw') {
149
+ drag = { type: 'draw', x0: e.clientX, y0: e.clientY }
150
+ } else {
151
+ // 调整模式:手柄 / 拖动移动 / 外部重画
152
+ var hKey = null
153
+ for (var k in handles) if (e.target === handles[k]) { hKey = k; break }
154
+ if (hKey) {
155
+ drag = { type: 'resize', dir: hKey, x0: e.clientX, y0: e.clientY, orig: { x: rect.x, y: rect.y, w: rect.w, h: rect.h } }
156
+ } else if (inside(e, rect)) {
157
+ drag = { type: 'move', x0: e.clientX, y0: e.clientY, orig: { x: rect.x, y: rect.y } }
158
+ } else {
159
+ // 点击选区外 → 重新画
160
+ mode = 'draw'
161
+ rect = null
162
+ sel.style.display = 'none'
163
+ toolbar.style.display = 'none'
164
+ Object.keys(handles).forEach(function (k) { handles[k].style.display = 'none' })
165
+ masks.top.style.display = masks.bottom.style.display = masks.left.style.display = masks.right.style.display = 'none'
166
+ hint.style.opacity = '0.9'
167
+ drag = { type: 'draw', x0: e.clientX, y0: e.clientY }
168
+ }
169
+ }
170
+ })
171
+
172
+ document.addEventListener('mousemove', function (e) {
173
+ if (!drag) return
174
+ var dx = e.clientX - drag.x0
175
+ var dy = e.clientY - drag.y0
176
+ if (drag.type === 'draw') {
177
+ rect = clampRect({ x: Math.min(drag.x0, e.clientX), y: Math.min(drag.y0, e.clientY), w: Math.abs(e.clientX - drag.x0), h: Math.abs(e.clientY - drag.y0) })
178
+ layout()
179
+ } else if (drag.type === 'move') {
180
+ rect = clampRect({ x: drag.orig.x + dx, y: drag.orig.y + dy, w: rect.w, h: rect.h })
181
+ layout()
182
+ } else if (drag.type === 'resize') {
183
+ var o = drag.orig
184
+ var r = { x: o.x, y: o.y, w: o.w, h: o.h }
185
+ if (drag.dir.indexOf('e') !== -1) r.w = o.w + dx
186
+ if (drag.dir.indexOf('s') !== -1) r.h = o.h + dy
187
+ if (drag.dir.indexOf('w') !== -1) { r.x = o.x + dx; r.w = o.w - dx }
188
+ if (drag.dir.indexOf('n') !== -1) { r.y = o.y + dy; r.h = o.h - dy }
189
+ rect = clampRect(r)
190
+ layout()
191
+ }
97
192
  })
98
193
 
99
- document.addEventListener('mouseup', () => {
100
- if (!drawing) return
101
- drawing = false
102
- if (rect && rect.w > 5 && rect.h > 5) {
103
- const t = document.getElementById('toolbar')
194
+ document.addEventListener('mouseup', function () {
195
+ if (!drag) return
196
+ drag = null
197
+ if (mode === 'draw' && rect && rect.w >= MIN && rect.h >= MIN) {
198
+ mode = 'adjust'
199
+ var t = document.getElementById('toolbar')
104
200
  t.style.display = 'flex'
105
- t.style.left = Math.min(rect.x + rect.w + 10, window.innerWidth - 220) + 'px'
106
- t.style.top = (rect.y + rect.h + 12 > window.innerHeight - 60 ? rect.y - 56 : rect.y + rect.h + 12) + 'px'
201
+ t.style.left = Math.min(rect.x + rect.w + 10, W - 230) + 'px'
202
+ t.style.top = (rect.y + rect.h + 14 > H - 60 ? rect.y - 58 : rect.y + rect.h + 14) + 'px'
203
+ tip.textContent = '可拖拽边框微调 · 点确定或双击完成'
107
204
  hint.style.opacity = '0'
108
- } else {
109
- rect = null; sel.style.display = 'none'
205
+ layout() // 显示手柄
110
206
  }
111
207
  })
112
208
 
113
- document.getElementById('okBtn').onclick = () => {
209
+ // 双击选区 直接提交
210
+ document.addEventListener('dblclick', function (e) {
211
+ if (rect && inside(e, rect)) window.__screenshot.done(rect)
212
+ })
213
+
214
+ document.getElementById('okBtn').onclick = function () {
114
215
  if (rect) window.__screenshot.done(rect)
115
216
  }
116
- document.getElementById('cancelBtn').onclick = () => window.__screenshot.cancel()
117
-
118
- document.addEventListener('keydown', (e) => {
217
+ document.getElementById('cancelBtn').onclick = function () { window.__screenshot.cancel() }
218
+ document.addEventListener('keydown', function (e) {
119
219
  if (e.key === 'Escape') window.__screenshot.cancel()
120
220
  if (e.key === 'Enter' && rect) window.__screenshot.done(rect)
121
221
  })