dsh-desktop-windows 0.1.5 → 0.1.6

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.5",
4
+ "version": "0.1.6",
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
@@ -40,6 +40,7 @@ let ownedDsh = null // 本壳启动的 dsh 子进程;null 表示复用了外
40
40
  let tray = null // 系统托盘
41
41
  let isQuitting = false // 用户从托盘选择退出时置 true,放行窗口关闭
42
42
  let setupWindow = null // 首次环境引导窗口
43
+ let sessionsWin = null // 会话管理窗口
43
44
 
44
45
  /* ------------------------------------------------------------------ *
45
46
  * 工具:日志
@@ -139,6 +140,76 @@ async function shutdownOwnedDsh() {
139
140
  }
140
141
  }
141
142
 
143
+ /* ------------------------------------------------------------------ *
144
+ * 会话管理(读取/删除 ~/.dsh/sessions 下的会话)
145
+ * ------------------------------------------------------------------ */
146
+
147
+ function sessionsRoot() {
148
+ return path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh', 'sessions')
149
+ }
150
+
151
+ /** 将工作区目录名(--C-Users-...-- 编码)还原为可读路径。 */
152
+ function decodeWorkspaceName(name) {
153
+ let s = name
154
+ if (s.startsWith('--')) s = s.slice(2)
155
+ if (s.endsWith('--')) s = s.slice(0, -2)
156
+ s = s.replace(/-/g, '\\')
157
+ s = s.replace(/^C\\/, 'C:\\')
158
+ return s
159
+ }
160
+
161
+ /** 列出所有会话。 */
162
+ function listSessions() {
163
+ const root = sessionsRoot()
164
+ const list = []
165
+ if (!fs.existsSync(root)) return list
166
+ const workspaces = fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory())
167
+ for (const ws of workspaces) {
168
+ const wsPath = path.join(root, ws.name)
169
+ const sDirs = fs.readdirSync(wsPath, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith('session-'))
170
+ for (const s of sDirs) {
171
+ const sPath = path.join(wsPath, s.name)
172
+ const stat = fs.statSync(sPath)
173
+ let sizeBytes = 0
174
+ const walk = (dir) => {
175
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
176
+ const fp = path.join(dir, f.name)
177
+ if (f.isDirectory()) walk(fp)
178
+ else sizeBytes += fs.statSync(fp).size
179
+ }
180
+ }
181
+ try { walk(sPath) } catch { /* ignore */ }
182
+ list.push({
183
+ id: s.name,
184
+ workspace: decodeWorkspaceName(ws.name),
185
+ modified: stat.mtimeMs,
186
+ sizeKB: Math.round(sizeBytes / 1024),
187
+ })
188
+ }
189
+ }
190
+ return list.sort((a, b) => b.modified - a.modified)
191
+ }
192
+
193
+ /** 删除指定会话(按 id 定位目录并递归删除)。 */
194
+ function deleteSession(id) {
195
+ const root = sessionsRoot()
196
+ if (!fs.existsSync(root) || !/^session-[0-9a-f-]+$/i.test(id)) return { ok: false, error: '非法会话 ID' }
197
+ const workspaces = fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory())
198
+ for (const ws of workspaces) {
199
+ const target = path.join(root, ws.name, id)
200
+ if (fs.existsSync(target)) {
201
+ try {
202
+ fs.rmSync(target, { recursive: true, force: true })
203
+ log(`deleted session ${id} (${ws.name})`)
204
+ return { ok: true }
205
+ } catch (e) {
206
+ return { ok: false, error: e.message }
207
+ }
208
+ }
209
+ }
210
+ return { ok: false, error: '未找到该会话' }
211
+ }
212
+
142
213
  /* ------------------------------------------------------------------ *
143
214
  * 环境检测与首次引导
144
215
  * ------------------------------------------------------------------ */
@@ -297,6 +368,37 @@ function createWindow(url) {
297
368
  })
298
369
  }
299
370
 
371
+ /** 打开会话管理窗口。 */
372
+ function openSessionsWindow() {
373
+ if (sessionsWin && !sessionsWin.isDestroyed()) {
374
+ sessionsWin.focus()
375
+ return
376
+ }
377
+ sessionsWin = new BrowserWindow({
378
+ width: 620,
379
+ height: 680,
380
+ title: '会话管理 — DeepSeek Harness Desktop',
381
+ autoHideMenuBar: true,
382
+ icon: path.join(__dirname, '..', 'assets', 'icon.ico'),
383
+ webPreferences: {
384
+ contextIsolation: true,
385
+ nodeIntegration: false,
386
+ sandbox: true,
387
+ preload: path.join(__dirname, 'sessions-preload.js'),
388
+ },
389
+ })
390
+ sessionsWin.setMenuBarVisibility(false)
391
+ sessionsWin.loadFile(path.join(__dirname, 'sessions.html'))
392
+ sessionsWin.on('closed', () => {
393
+ sessionsWin = null
394
+ })
395
+
396
+ ipcMain.removeHandler('sessions:list')
397
+ ipcMain.removeHandler('sessions:remove')
398
+ ipcMain.handle('sessions:list', () => listSessions())
399
+ ipcMain.handle('sessions:remove', (_e, id) => deleteSession(id))
400
+ }
401
+
300
402
  /* ------------------------------------------------------------------ *
301
403
  * 系统托盘
302
404
  * ------------------------------------------------------------------ */
@@ -337,6 +439,11 @@ function createApplicationMenu() {
337
439
  {
338
440
  label: '文件',
339
441
  submenu: [
442
+ {
443
+ label: '会话管理…',
444
+ click: () => openSessionsWindow(),
445
+ },
446
+ { type: 'separator' },
340
447
  {
341
448
  label: '退出',
342
449
  accelerator: 'Alt+F4',
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ // 会话管理窗口的 preload:通过 contextBridge 暴露安全 API
4
+ const { contextBridge, ipcRenderer } = require('electron')
5
+
6
+ contextBridge.exposeInMainWorld('sessionsAPI', {
7
+ // 列出所有会话 [{ id, workspace, modified, sizeKB, current }]
8
+ list: () => ipcRenderer.invoke('sessions:list'),
9
+ // 删除指定会话,返回 { ok, error? }
10
+ remove: (id) => ipcRenderer.invoke('sessions:remove', id),
11
+ })
@@ -0,0 +1,110 @@
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; }
8
+ body {
9
+ font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
10
+ background: #0f172a; color: #e2e8f0;
11
+ min-height: 100vh; padding: 20px;
12
+ }
13
+ h1 { font-size: 18px; margin-bottom: 6px; }
14
+ .subtitle { color: #94a3b8; font-size: 13px; margin-bottom: 16px; line-height: 1.6; }
15
+ .warn { background: #450a0a; border: 1px solid #7f1d1d; color: #fca5a5; border-radius: 8px; padding: 10px 14px; font-size: 12px; margin-bottom: 16px; line-height: 1.6; }
16
+ .item {
17
+ background: #1e293b; border: 1px solid #334155; border-radius: 10px;
18
+ padding: 12px 14px; margin-bottom: 10px;
19
+ }
20
+ .item .head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
21
+ .item .title { font-weight: 600; font-size: 13px; word-break: break-all; }
22
+ .item .meta { font-size: 11px; color: #64748b; margin-top: 6px; line-height: 1.5; word-break: break-all; }
23
+ .item .current { display: inline-block; background: #14532d; color: #86efac; font-size: 10px; border-radius: 4px; padding: 2px 6px; margin-left: 6px; }
24
+ .btn {
25
+ border: none; border-radius: 8px; padding: 7px 14px;
26
+ font-size: 12px; cursor: pointer; font-weight: 600; white-space: nowrap;
27
+ background: #dc2626; color: #fff; transition: background .2s;
28
+ }
29
+ .btn:hover { background: #b91c1c; }
30
+ .btn:disabled { opacity: .4; cursor: not-allowed; }
31
+ .empty { text-align: center; color: #64748b; padding: 40px 0; font-size: 13px; }
32
+ .footer { margin-top: 14px; font-size: 12px; color: #64748b; line-height: 1.6; }
33
+ .count { color: #7dd3fc; }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <h1>🗂️ 会话管理</h1>
38
+ <div class="subtitle">这里列出了本机保存的所有会话。删除会话将<span style="color:#f87171">永久删除</span>该对话的聊天记录(不可恢复)。</div>
39
+ <div class="warn">⚠️ 请勿删除<b>当前正在使用的会话</b>(会导致当前对话异常)。删除后建议刷新主窗口(Ctrl+Shift+R)让列表更新。</div>
40
+ <div id="list"></div>
41
+ <div class="footer" id="footer"></div>
42
+
43
+ <script>
44
+ const $ = (id) => document.getElementById(id)
45
+ const listEl = $('list')
46
+ const footerEl = $('footer')
47
+
48
+ function fmtTime(ms) {
49
+ const d = new Date(ms)
50
+ const p = (n) => String(n).padStart(2, '0')
51
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
52
+ }
53
+
54
+ function fmtSize(kb) {
55
+ return kb >= 1024 ? (kb / 1024).toFixed(1) + ' MB' : kb + ' KB'
56
+ }
57
+
58
+ async function refresh() {
59
+ const sessions = await window.sessionsAPI.list()
60
+ footerEl.innerHTML = `共 <span class="count">${sessions.length}</span> 个会话`
61
+ if (sessions.length === 0) {
62
+ listEl.innerHTML = '<div class="empty">没有找到会话记录</div>'
63
+ return
64
+ }
65
+ listEl.innerHTML = ''
66
+ sessions.forEach((s) => {
67
+ const item = document.createElement('div')
68
+ item.className = 'item'
69
+ const head = document.createElement('div')
70
+ head.className = 'head'
71
+ const title = document.createElement('div')
72
+ title.className = 'title'
73
+ title.textContent = s.workspace
74
+ if (s.current) {
75
+ const tag = document.createElement('span')
76
+ tag.className = 'current'
77
+ tag.textContent = '当前'
78
+ title.appendChild(tag)
79
+ }
80
+ const delBtn = document.createElement('button')
81
+ delBtn.className = 'btn'
82
+ delBtn.textContent = '删除'
83
+ delBtn.onclick = async () => {
84
+ if (!confirm(`确定永久删除此会话?\n\n${s.id}\n\n此操作不可恢复!`)) return
85
+ delBtn.disabled = true
86
+ delBtn.textContent = '删除中…'
87
+ const r = await window.sessionsAPI.remove(s.id)
88
+ if (r && r.ok) {
89
+ item.remove()
90
+ } else {
91
+ alert('删除失败:' + (r ? r.error : '未知错误'))
92
+ delBtn.disabled = false
93
+ delBtn.textContent = '删除'
94
+ }
95
+ }
96
+ head.appendChild(title)
97
+ head.appendChild(delBtn)
98
+ const meta = document.createElement('div')
99
+ meta.className = 'meta'
100
+ meta.textContent = `ID: ${s.id} | 最后活跃: ${fmtTime(s.modified)} | 大小: ${fmtSize(s.sizeKB)}`
101
+ item.appendChild(head)
102
+ item.appendChild(meta)
103
+ listEl.appendChild(item)
104
+ })
105
+ }
106
+
107
+ refresh()
108
+ </script>
109
+ </body>
110
+ </html>