dsh-desktop-windows 0.1.7 → 0.1.9
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 +1 -1
- package/src/main.js +106 -1
- package/src/preload.js +54 -24
- package/src/screenshot-preload.js +15 -0
- package/src/screenshot.html +121 -0
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.9",
|
|
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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 不修改 dsh 任何代码;服务端用全局安装的 dsh(可用 DSH_BIN 环境变量覆盖路径)。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
const { app, BrowserWindow, dialog, shell, Tray, Menu, nativeImage, ipcMain } = require('electron')
|
|
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
19
|
const fs = require('node:fs')
|
|
@@ -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,79 @@ 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
|
+
if (!image || !rect) return
|
|
448
|
+
const sf = screen.getPrimaryDisplay().scaleFactor || 1
|
|
449
|
+
const r = {
|
|
450
|
+
x: Math.round(rect.x * sf),
|
|
451
|
+
y: Math.round(rect.y * sf),
|
|
452
|
+
width: Math.round(rect.w * sf),
|
|
453
|
+
height: Math.round(rect.h * sf),
|
|
454
|
+
}
|
|
455
|
+
if (r.width <= 0 || r.height <= 0) return
|
|
456
|
+
const cropped = image.crop(r)
|
|
457
|
+
clipboard.writeImage(cropped)
|
|
458
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
459
|
+
mainWindow.show()
|
|
460
|
+
mainWindow.focus()
|
|
461
|
+
mainWindow.webContents
|
|
462
|
+
.executeJavaScript(
|
|
463
|
+
`(() => { const el = document.querySelector('textarea, [contenteditable="true"]'); if (el) { el.focus(); return true } return false })()`
|
|
464
|
+
)
|
|
465
|
+
.then((focused) => {
|
|
466
|
+
if (focused) mainWindow.webContents.paste()
|
|
467
|
+
})
|
|
468
|
+
.catch(() => {})
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function onShotCancel() {
|
|
473
|
+
pendingShot = null
|
|
474
|
+
if (shotWindow && !shotWindow.isDestroyed()) shotWindow.destroy()
|
|
475
|
+
}
|
|
476
|
+
|
|
402
477
|
/* ------------------------------------------------------------------ *
|
|
403
478
|
* 系统托盘
|
|
404
479
|
* ------------------------------------------------------------------ */
|
|
@@ -552,6 +627,36 @@ if (!gotLock) {
|
|
|
552
627
|
ipcMain.on('dsh-desktop:reload', () => {
|
|
553
628
|
if (mainWindow) mainWindow.webContents.reload()
|
|
554
629
|
})
|
|
630
|
+
// 选区截图:截全屏 → 打开选区窗口 → 用户框选 → 裁剪 → 剪贴板 → 粘贴聊天框
|
|
631
|
+
ipcMain.handle('dsh-desktop:capture', async () => {
|
|
632
|
+
try {
|
|
633
|
+
const display = screen.getPrimaryDisplay()
|
|
634
|
+
const sf = display.scaleFactor || 1
|
|
635
|
+
const sources = await desktopCapturer.getSources({
|
|
636
|
+
types: ['screen'],
|
|
637
|
+
thumbnailSize: {
|
|
638
|
+
width: Math.round(display.size.width * sf),
|
|
639
|
+
height: Math.round(display.size.height * sf),
|
|
640
|
+
},
|
|
641
|
+
})
|
|
642
|
+
if (!sources.length) return { ok: false, error: '未找到屏幕源' }
|
|
643
|
+
pendingShot = sources[0].thumbnail
|
|
644
|
+
if (pendingShot.isEmpty()) return { ok: false, error: '截图为空' }
|
|
645
|
+
openShotWindow()
|
|
646
|
+
return { ok: true }
|
|
647
|
+
} catch (e) {
|
|
648
|
+
return { ok: false, error: String((e && e.message) || e) }
|
|
649
|
+
}
|
|
650
|
+
})
|
|
651
|
+
// 轻提示(截图失败等)
|
|
652
|
+
ipcMain.on('dsh-desktop:toast', (_e, msg) => {
|
|
653
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
654
|
+
dialog.showMessageBox(mainWindow, { type: 'info', message: String(msg), buttons: ['好'] })
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
// 选区截图结果
|
|
658
|
+
ipcMain.on('screenshot:done', (_e, rect) => onShotDone(rect))
|
|
659
|
+
ipcMain.on('screenshot:cancel', () => onShotCancel())
|
|
555
660
|
if (external) log('reusing external dsh service on port ' + port)
|
|
556
661
|
})
|
|
557
662
|
|
package/src/preload.js
CHANGED
|
@@ -1,38 +1,68 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// preload:注入底部工具条(截图 + 刷新)。
|
|
4
4
|
// 页面(dsh Web UI)直接走 HTTP/WebSocket 与本地服务通信,无需 IPC。
|
|
5
5
|
|
|
6
6
|
const { ipcRenderer } = require('electron')
|
|
7
7
|
|
|
8
|
-
// 注入一个悬浮"刷新"按钮(右下角,半透明,鼠标悬停变清晰)
|
|
9
8
|
window.addEventListener('DOMContentLoaded', () => {
|
|
10
9
|
setTimeout(() => {
|
|
11
10
|
if (!document.body) return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
|
|
12
|
+
// 底部工具条容器
|
|
13
|
+
const bar = document.createElement('div')
|
|
14
|
+
bar.style.cssText = `
|
|
16
15
|
position: fixed;
|
|
17
|
-
right:
|
|
18
|
-
bottom:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
border-radius: 10px;
|
|
22
|
-
border: none;
|
|
23
|
-
background: rgba(30, 41, 59, 0.72);
|
|
24
|
-
color: #fff;
|
|
25
|
-
font-size: 18px;
|
|
26
|
-
line-height: 1;
|
|
27
|
-
cursor: pointer;
|
|
28
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
29
|
-
opacity: 0.45;
|
|
30
|
-
transition: opacity 0.2s;
|
|
16
|
+
right: 12px;
|
|
17
|
+
bottom: 12px;
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 8px;
|
|
31
20
|
z-index: 2147483647;
|
|
32
21
|
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
|
|
23
|
+
const makeBtn = (text, title, onClick) => {
|
|
24
|
+
const b = document.createElement('button')
|
|
25
|
+
b.textContent = text
|
|
26
|
+
b.title = title
|
|
27
|
+
b.style.cssText = `
|
|
28
|
+
width: 34px;
|
|
29
|
+
height: 34px;
|
|
30
|
+
border-radius: 10px;
|
|
31
|
+
border: none;
|
|
32
|
+
background: rgba(30, 41, 59, 0.72);
|
|
33
|
+
color: #fff;
|
|
34
|
+
font-size: 17px;
|
|
35
|
+
line-height: 1;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
|
38
|
+
opacity: 0.45;
|
|
39
|
+
transition: opacity 0.2s;
|
|
40
|
+
`
|
|
41
|
+
b.onmouseenter = () => (b.style.opacity = '1')
|
|
42
|
+
b.onmouseleave = () => (b.style.opacity = '0.45')
|
|
43
|
+
b.onclick = onClick
|
|
44
|
+
return b
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 截图按钮:截屏 → 剪贴板 → 自动粘贴到聊天框
|
|
48
|
+
const shotBtn = makeBtn('📸', '截图并提问(截取当前屏幕,自动粘贴到聊天框)', () => {
|
|
49
|
+
shotBtn.style.opacity = '1'
|
|
50
|
+
shotBtn.textContent = '⏳'
|
|
51
|
+
ipcRenderer.invoke('dsh-desktop:capture').then((r) => {
|
|
52
|
+
shotBtn.textContent = '📸'
|
|
53
|
+
if (!r || !r.ok) {
|
|
54
|
+
ipcRenderer.send('dsh-desktop:toast', (r && r.error) || '截图失败')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// 刷新按钮
|
|
60
|
+
const reloadBtn = makeBtn('⟳', '刷新页面(等价 F5)', () => {
|
|
61
|
+
ipcRenderer.send('dsh-desktop:reload')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
bar.appendChild(shotBtn)
|
|
65
|
+
bar.appendChild(reloadBtn)
|
|
66
|
+
document.body.appendChild(bar)
|
|
37
67
|
}, 1500)
|
|
38
68
|
})
|
|
@@ -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>
|