dsh-island-overlay 0.2.1 → 0.2.3

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.md CHANGED
@@ -83,15 +83,16 @@ dsh plugin --profile desktop add dsh-island-overlay
83
83
  | 事件 | 显示 |
84
84
  |---|---|
85
85
  | `turn/start` | 思考(小鲸鱼 + 计时,2.5s 后缩成小图标) |
86
- | `approval/asked` | 等待审批(琥珀脉冲,常驻展开) |
86
+ | `approval/asked` | 等待审批(琥珀脉冲,常驻展开,计时接着走) |
87
87
  | `tool/call` = `ask_user_question` | 等待回答(同上) |
88
88
  | `turn/end` = `completed` | 完成(绿勾,3s 后消失) |
89
89
  | `turn/end` = `error` / `blocked` / `max-tokens` | 失败(红色抖动,6s 后消失) |
90
90
  | `turn/end` = `aborted` / `interrupted` / `forked` | 不显示,直接消失 |
91
91
 
92
92
  多个会话同时有事时只显示优先级最高的一个(审批/回答 > 失败 > 完成 > 思考),其余用 `+N` 角标;最多并列显示 3 个会话,每段各自计时。
93
+ 计时读的是**回合开始时间**,所以「思考 20 分钟 → 弹审批 → 继续思考」是一条连续的时间轴,不会归零。
93
94
 
94
- 交互:拖动可移动位置(记在 `~/.dsh/dsh-island-pos.json`);点一下把 DSH 拉到前台;
95
+ 交互:拖动可移动位置(自动切到「自定义位置」,记在 `~/.dsh/dsh-island-pos.json`);点一下把 DSH 拉到前台;
95
96
  鼠标划过展开全文;检测到前台是全屏程序(视频/游戏)时自动隐藏。
96
97
 
97
98
  ## 卸载
@@ -102,12 +103,102 @@ dsh plugin --profile desktop add dsh-island-overlay
102
103
 
103
104
  然后重启 DSH,再把文件夹和 `%LOCALAPPDATA%\dsh-island-runtime` 删掉。
104
105
 
105
- ## 排错
106
+ ## 常见问题
107
+
108
+ ### 安装
109
+
110
+ **`dsh` 命令找不到 / `'dsh' 不是内部或外部命令`**
111
+ DSH 不会把 `dsh` 加进 PATH,得用完整路径:
112
+
113
+ ```powershell
114
+ & "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add dsh-island-overlay
115
+ ```
116
+
117
+ 装在非默认目录(比如 `D:\`)时,把上面路径换成你的实际位置。
118
+
119
+ **`install.ps1` 报 `dsh.cmd not found`**
120
+ 脚本只在 `%LOCALAPPDATA%\Programs`、`Program Files`、`Program Files (x86)` 三个位置找 DSH。装在别处就手动指定:
121
+
122
+ ```powershell
123
+ powershell -ExecutionPolicy Bypass -File install.ps1 -Cli "D:\...\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
124
+ ```
125
+
126
+ **`无法加载文件 install.ps1,因为在此系统上禁止运行脚本`**
127
+ Windows 默认执行策略拦脚本。加 `-ExecutionPolicy Bypass`(上面的命令已经带了),不要改全局策略。
128
+
129
+ **需要自己装 pnpm / node 吗**
130
+ 不需要。DSH 自带一份 pnpm(`resources\runtime\primary-runtime\dependencies\pnpm\bin\pnpm.mjs`),
131
+ `dsh plugin` 内部用 DSH 自己的 Electron 跑它,跟系统的 node/pnpm 无关。系统里有别的 pnpm 也不会冲突。
132
+
133
+ **pnpm 报 `ERR_PNPM_...` / 网络超时**
134
+ `dsh plugin` 用的是你 npm 配置里的 registry。国内网络建议:
135
+
136
+ ```powershell
137
+ npm config get registry # 一般是 https://registry.npmmirror.com
138
+ ```
139
+
140
+ 本包在 npmmirror 已同步。若公司网络拦 npm,可临时指定源:
141
+ `dsh plugin --profile desktop add dsh-island-overlay --registry=https://registry.npmmirror.com`。
142
+
143
+ **装完没反应**
144
+ 装完必须**重启 DSH**(托盘右键退出,再打开)。热重载不会拉起浮层。
145
+ 确认 `~/.dsh/profiles/desktop/package.json` 的 `dsh.profile.bundles` 里有 `dsh-island-overlay`。
146
+
147
+ **已经用 `link:` 装过本地目录,又想换成 npm 版**
148
+ 先卸再装,否则 pnpm 会把 link 改写成 npm 版本、你本地的改动就不再生效:
149
+
150
+ ```powershell
151
+ & "<dsh.cmd 完整路径>" plugin --profile desktop remove dsh-island-overlay
152
+ & "<dsh.cmd 完整路径>" plugin --profile desktop add dsh-island-overlay
153
+ ```
154
+
155
+ 反过来(npm 版换成本地开发)把 `add dsh-island-overlay` 换成 `add link:<本目录绝对路径>`。
156
+
157
+ ### 运行时
158
+
159
+ **状态文件在变,但屏幕上看不到胶囊**
160
+ 先看日志,两个文件都在 `%USERPROFILE%\.dsh\`:
161
+
162
+ - `dsh-island-plugin.log`:`runtime prepared` + `spawn ...` 说明浮层被拉起过;
163
+ 只有 `skip: isElectron=false` 说明插件没跑在 DSH 主进程里。
164
+ - `dsh-island-overlay.log`:每次启动写 `start v0.2.x`,之后每 60 秒一条 `alive`。
165
+ 没有 `start` → 浮层根本没起来。
166
+
167
+ 手动起浮层验证:
168
+
169
+ ```powershell
170
+ & "$env:LOCALAPPDATA\dsh-island-runtime\DeepSeek Harness.exe" "<本目录>\overlay"
171
+ ```
172
+
173
+ 正常会弹出一条黑色胶囊(控制台里的 `disk_cache` 报错可以忽略)。
174
+
175
+ **浮层起不来,日志里连 `start` 都没有**
176
+ 多半是运行时镜像不完整。删掉 `%LOCALAPPDATA%\dsh-island-runtime` 再重启 DSH,插件会自动重建
177
+ (同盘用硬链接,不额外占空间)。镜像里必须有 `resources\default_app.asar`,缺它 Electron 就不认命令行给的 app 目录。
178
+
179
+ **跑一阵子自己消失了**
180
+ 已修(0.2.0+)。旧版在心跳短暂中断后不会恢复显示,看起来就是「消失了」。升级后:心跳断 30 秒先隐藏,
181
+ 恢复后自动重新显示,连续 5 分钟没恢复才退出进程。仍复现的话看 `dsh-island-overlay.log` 最后几行。
182
+
183
+ **点胶囊后 DSH 变成窗口化 / 反应慢**
184
+ 已修(0.2.0+)。旧版用 `SW_RESTORE` 会把最大化窗口还原;现在只在窗口最小化时才还原。
185
+ 慢是因为旧版每次点击都新起一个 PowerShell,现在复用常驻的 `watch.ps1`(约 150–250ms)。
186
+
187
+ **胶囊挡到浏览器标签页**
188
+ 鼠标事件是穿透的(只挡视觉,不挡点击)。想少挡:设置里把「迷你态」调小,或改用「顶部居中 → 右上角」,
189
+ 或开「仅 DSH 不在前台时显示」。拖动可以直接改位置(记在 `~/.dsh/dsh-island-pos.json`)。
190
+
191
+ **换了显示器 / 拔了副屏后胶囊不见了**
192
+ 位置会被夹回最近的屏幕工作区,理论上不会跑到屏幕外。真丢了就删 `~/.dsh/dsh-island-pos.json` 重启 DSH。
193
+
194
+ **改了 `overlay/index.html` 没生效**
195
+ 浮层是 DSH 启动时拉起的独立进程,不热重载。改完重启 DSH,或手动 `taskkill` 掉浮层进程(插件 3 秒内会重拉)。
196
+
197
+ **全屏看视频 / 打游戏时胶囊还在**
198
+ 设置里开「全屏应用时自动隐藏」。注意它只认「窗口铺满整块屏幕」,最大化窗口不算全屏。
199
+ 少数独占全屏的游戏连置顶窗口都能盖住,这是 Windows 的限制,插件绕不过去。
200
+
201
+ **多个 DSH 实例会不会叠出两个胶囊**
202
+ 不会,浮层有单实例锁,第二个实例直接退出。
203
+
106
204
 
107
- - **没反应**:确认 `~/.dsh/dsh-island-state.json` 有内容在变(3 秒刷新一次)。没有 → 插件没加载,
108
- 看 DSH 启动日志,或重新跑一次 `install.ps1`。
109
- - **有 JSON 没胶囊**:浮层没起来。手动试:
110
- `"C:\Users\<你>\AppData\Local\Programs\DeepSeek Harness\DeepSeek Harness.exe" <本目录>\overlay`
111
- 正常会弹出一条黑色胶囊,控制台有无关紧要的 disk_cache 报错。
112
- - **胶囊不置顶 / 盖住别人**:Windows 的置顶窗口在少数全屏独占程序(部分游戏)上会被盖住,这是系统限制。
113
- - **改了 `overlay/index.html` 要重启 DSH**:浮层是启动时拉起的,不热重载。
package/lib/index.js CHANGED
@@ -167,24 +167,27 @@ export default {
167
167
  }
168
168
 
169
169
  const name = titleOf(session)
170
+ // since = 这一回合的开始时间,只有 turn/start 能改它。
171
+ // 以前 approval/asked、approval/decided、tool/call 都把它重置成 now,
172
+ // 于是「思考 20 分钟 → 弹审批 → 继续思考」的计时会从 0 重新数。
170
173
  switch (type) {
171
174
  case 'turn/start':
172
175
  touch(sid, { state: 'thinking', detail: '', since: Date.now(), title: name })
173
176
  break
174
177
  case 'approval/asked':
175
- touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'), since: Date.now(), title: name })
178
+ touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'), title: name })
176
179
  break
177
180
  case 'approval/decided':
178
- if (items.has(sid)) touch(sid, { state: 'thinking', detail: '', since: Date.now(), title: name })
181
+ if (items.has(sid)) touch(sid, { state: 'thinking', detail: '', title: name })
179
182
  break
180
183
  case 'tool/call':
181
184
  if (String(data.name || '') === 'ask_user_question') {
182
- touch(sid, { state: 'ask', detail: '等待你的回答', since: Date.now(), title: name })
185
+ touch(sid, { state: 'ask', detail: '等待你的回答', title: name })
183
186
  }
184
187
  break
185
188
  case 'tool/result': {
186
189
  const cur = items.get(sid)
187
- if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '', since: Date.now(), title: name })
190
+ if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '', title: name })
188
191
  break
189
192
  }
190
193
  case 'turn/end': {
@@ -1,4 +1,5 @@
1
1
  # Bring the DSH main window to the foreground (click-through pill -> click).
2
+ # Fallback path only: the overlay normally sends "activate" over watch.ps1's stdin.
2
3
  $ErrorActionPreference = 'SilentlyContinue'
3
4
 
4
5
  Add-Type -TypeDefinition @"
@@ -7,6 +8,7 @@ using System.Runtime.InteropServices;
7
8
  public class IslandFg {
8
9
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
9
10
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
11
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
10
12
  [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, IntPtr extra);
11
13
  }
12
14
  "@
@@ -20,5 +22,6 @@ if (-not $p) { exit }
20
22
  # 先按一下 Alt 骗过前台锁定,再抢焦点
21
23
  [IslandFg]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
22
24
  [IslandFg]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
23
- [IslandFg]::ShowWindow($p.MainWindowHandle, 9) | Out-Null
25
+ # 只在真最小化时还原。SW_RESTORE 会把最大化窗口缩回普通窗口,就是用户报的「点了变成窗口化」
26
+ if ([IslandFg]::IsIconic($p.MainWindowHandle)) { [IslandFg]::ShowWindow($p.MainWindowHandle, 9) | Out-Null }
24
27
  [IslandFg]::SetForegroundWindow($p.MainWindowHandle) | Out-Null
@@ -150,7 +150,12 @@ function esc(s) {
150
150
 
151
151
  function fmtTime(sec) {
152
152
  sec = Math.max(0, Math.floor(sec))
153
- return sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm' + (sec % 60) + 's'
153
+ if (sec < 60) return sec + 's'
154
+ const m = Math.floor(sec / 60)
155
+ const s = sec % 60
156
+ if (m < 60) return s ? m + 'm' + s + 's' : m + 'm'
157
+ const h = Math.floor(m / 60)
158
+ return m % 60 ? h + 'h' + (m % 60) + 'm' : h + 'h'
154
159
  }
155
160
 
156
161
  function icon(state) {
@@ -202,8 +207,11 @@ function ordered() {
202
207
  .sort((a, b) => (RANK[b.state] || 0) - (RANK[a.state] || 0) || (a.since || 0) - (b.since || 0))
203
208
  }
204
209
 
210
+ const TIMED = { thinking: 1, approval: 1, ask: 1 }
211
+
205
212
  function segHtml(it, primary) {
206
- const meta = it.state === 'thinking'
213
+ // 计时都读 since(= 回合开始),所以「思考 → 审批 → 继续思考」是一条连续的时间轴
214
+ const meta = TIMED[it.state]
207
215
  ? '<span class="meta" data-since="' + (Number(it.since) || Date.now()) + '">' + fmtTime((Date.now() - (Number(it.since) || Date.now())) / 1000) + '</span>'
208
216
  : it.state === 'done' ? '<span class="meta">刚刚</span>' : ''
209
217
  // 只有主段展开标题,其余段只留图标 + 状态词,避免胶囊被撑爆
@@ -228,6 +236,9 @@ function render() {
228
236
  curState = ''
229
237
  clearTimeout(shrinkTimer)
230
238
  if (tick) { clearInterval(tick); tick = null }
239
+ // 悬停中被清空时必须把鼠标穿透还回去:窗口本身还在,ignoreMouseEvents=false
240
+ // 会让它在「看不见」的状态下继续吃掉顶部中间那块区域的点击。
241
+ if (hovered) { hovered = false; window.islandApi.hover(false) }
231
242
  return
232
243
  }
233
244
 
@@ -235,8 +246,9 @@ function render() {
235
246
  const e = cfg.effects || DEFAULTS.effects
236
247
  const shown = list.slice(0, MAXSEG)
237
248
  const rest = list.length - shown.length
238
- // cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items 没变也得重画
239
- const key = list.map(it => it.state + it.id + it.since).join(',') + '|' + rest + '|' + JSON.stringify(cfg)
249
+ // cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items 没变也得重画。
250
+ // title/detail 同样要进:会话改名、失败原因变化时 since 不变,漏了就不会重绘。
251
+ const key = list.map(it => it.state + it.id + it.since + '|' + (it.title || '') + '|' + (it.detail || '')).join(',') + '|' + rest + '|' + JSON.stringify(cfg)
240
252
  if (key !== curKey) {
241
253
  curKey = key
242
254
  curState = best.state
@@ -255,9 +267,9 @@ function render() {
255
267
  scheduleMini(hovered ? 0 : holdFor(best.state))
256
268
  }
257
269
 
258
- // 只要有思考中的段就跑秒表
259
- const anyThinking = list.some(it => it.state === 'thinking')
260
- if (anyThinking) {
270
+ // 只要有一段带计时(思考/审批/等待回答)就跑秒表
271
+ const anyTimed = list.some(it => TIMED[it.state])
272
+ if (anyTimed) {
261
273
  if (!tick) { paintTimer(); tick = setInterval(paintTimer, 1000) }
262
274
  } else if (tick) { clearInterval(tick); tick = null }
263
275
  }
package/overlay/main.js CHANGED
@@ -7,6 +7,7 @@ const { execFile, spawn } = require('child_process')
7
7
  const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
8
8
  const STATE_FILE = path.join(DSH_HOME, 'dsh-island-state.json')
9
9
  const POS_FILE = path.join(DSH_HOME, 'dsh-island-pos.json')
10
+ const CONFIG_FILE = path.join(DSH_HOME, 'dsh-island-config.json')
10
11
  const DEAD_MS = 30000
11
12
  const LOG_FILE = path.join(DSH_HOME, 'dsh-island-overlay.log')
12
13
 
@@ -57,6 +58,20 @@ function savePos() {
57
58
  } catch (err) {}
58
59
  }
59
60
 
61
+ // 拖动 = 切到「自定义位置」。宿主 watchFile 监听同一个配置文件,会立刻把新
62
+ // 配置下发回来(不需要浮层自己刷新界面)。写失败就算了,位置至少本次有效。
63
+ function persistPositionCustom() {
64
+ try {
65
+ let cur = {}
66
+ try { cur = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) || {} } catch (err) {}
67
+ if (cur.position === 'custom') return
68
+ cur.position = 'custom'
69
+ const tmp = CONFIG_FILE + '.tmp'
70
+ fs.writeFileSync(tmp, JSON.stringify(cur, null, 2), 'utf8')
71
+ fs.renameSync(tmp, CONFIG_FILE)
72
+ } catch (err) {}
73
+ }
74
+
60
75
  // 窗口比胶囊大一圈:缩放后的胶囊要放得下,还要留出呼吸/涟漪的溢出空间
61
76
  function winSize(s) {
62
77
  return { w: Math.round(BASE_W * s), h: Math.round(BASE_H * s) }
@@ -113,15 +128,27 @@ function create() {
113
128
  win.setIgnoreMouseEvents(true, { forward: true })
114
129
  win.loadFile(path.join(__dirname, 'index.html'))
115
130
  win.once('ready-to-show', () => win.showInactive())
131
+ // 首帧会丢:tick() 在页面还没加载完时就把状态 send 出去了(被丢弃),
132
+ // 而 last 已经记下,之后状态不再变化就一直不重发 —— 结果是浮层起来了却空白。
133
+ // 页面加载完清掉 last,让下一次 tick 无条件重发。
134
+ win.webContents.on('did-finish-load', () => { last = '' })
116
135
 
117
136
  // 诊断:之前浮层会莫名其妙消失且日志里什么都没留下,这里把每种死法都记下来
118
- win.webContents.on('render-process-gone', (_e, d) => log('render-process-gone: ' + JSON.stringify(d)))
137
+ let reloads = 0
138
+ win.webContents.on('render-process-gone', (_e, d) => {
139
+ log('render-process-gone: ' + JSON.stringify(d))
140
+ // 渲染进程挂了窗口会留在那但永远空白。重载一次;连续崩就退出,
141
+ // 让插件的 3 秒心跳重新拉起一个干净进程,而不是卡在空白窗口上。
142
+ if (reloads++ < 2) setTimeout(() => { try { win.reload() } catch (err) {} }, 1000)
143
+ else { log('quit: renderer keeps dying'); app.quit() }
144
+ })
119
145
  win.webContents.on('unresponsive', () => log('unresponsive'))
120
146
  win.webContents.on('responsive', () => log('responsive'))
121
147
  app.on('child-process-gone', (_e, d) => log('child-process-gone: ' + JSON.stringify(d)))
122
148
  win.on('hide', () => log('window hidden'))
123
149
  win.on('show', () => log('window shown'))
124
- win.on('closed', () => log('window closed'))
150
+ // 窗口被关掉但进程还在 = 不会再有任何界面;直接退出让宿主重新拉起
151
+ win.on('closed', () => { log('window closed'); app.quit() })
125
152
 
126
153
  // 别的置顶窗口偶尔会把我们压下去,定期重新抬一次
127
154
  setInterval(() => {
@@ -276,6 +303,9 @@ ipcMain.on('drag-end', () => {
276
303
  if (!dragOrigin) return
277
304
  dragOrigin = null
278
305
  savePos()
306
+ // 只写 pos.json 没用:place() 只在 position==='custom' 时才读它,
307
+ // 所以拖完重启又回到顶部居中。这里把配置切成 custom(宿主 watchFile 会接住)。
308
+ persistPositionCustom()
279
309
  })
280
310
 
281
311
  app.whenReady().then(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-island-overlay",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "exports": {