deepseek-computer-use 0.1.0 → 0.1.1
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 +23 -8
- package/lib/index.js +3 -0
- package/native/Driver.cs +75 -21
- package/native/Overlay.cs +85 -29
- package/native/Program.cs +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# deepseek-computer-use
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/deepseek-computer-use)
|
|
4
|
+
|
|
3
5
|
> 社区插件,**非 DeepSeek 官方出品**。官方包在 `@deepseek-ai/*` scope 下。
|
|
4
6
|
|
|
5
7
|
给 DeepSeek Harness 的 **Windows 桌面控制(Computer Use)** 插件。
|
|
@@ -19,13 +21,11 @@ Windows 原生三件套:**UI Automation 观测 + SendInput 注入 + Windows.Gr
|
|
|
19
21
|
UI Automation、WGC)。
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
|
|
23
|
-
npm install --save deepseek-computer-use
|
|
24
|
-
|
|
25
|
-
# 现在还只能用 GitHub 源
|
|
26
|
-
npm install --save github:STARDUSTLC666/deepseek-computer-use
|
|
24
|
+
npm install deepseek-computer-use
|
|
27
25
|
```
|
|
28
26
|
|
|
27
|
+
[npmjs.com/package/deepseek-computer-use](https://www.npmjs.com/package/deepseek-computer-use)
|
|
28
|
+
|
|
29
29
|
然后在本机 profile 的 `cordis.patch.yml` 里启用(详见本文件末尾的配置示例):
|
|
30
30
|
|
|
31
31
|
```yaml
|
|
@@ -111,7 +111,17 @@ Codex 用两层覆盖层告诉用户"AI 正在动你的电脑",这里做的是
|
|
|
111
111
|
**Cursor Overlay — 虚拟光标 + 系统光标抑制**
|
|
112
112
|
- 覆盖层存在期间常驻一个带描边和投影的白色指针,启动时落在真实指针位置,
|
|
113
113
|
之后 0.35 缓动滑向每个动作点;背后一层极淡的强调色光晕
|
|
114
|
-
-
|
|
114
|
+
- 默认抑制真实系统光标(`SetSystemCursor` 换成透明图标),五层恢复保障:
|
|
115
|
+
1. 正常退出 / Esc / 超时 → 退出钩子里 `SPI_SETCURSORS`
|
|
116
|
+
2. **独立守卫进程**(`--cursor-guard <ownerPid>`)盯着覆盖层,覆盖层被强杀后
|
|
117
|
+
它负责还原——和 Codex 的 `system-cursor-manager` 同一思路
|
|
118
|
+
3. helper 的 **500ms 看门狗**:标记里的 PID 失效就还原,不需要任何请求
|
|
119
|
+
4. helper **启动即自检**:上次会话残留的标记会被清掉
|
|
120
|
+
5. 插件 dispose 时发 `restore_cursor` RPC
|
|
121
|
+
- 抑制期间 `%LOCALAPPDATA%\dsh-computer-use\cursor-hidden.flag` 记录自己的 PID,
|
|
122
|
+
守卫只清理属于自己 owner 的标记,不会误伤新一代覆盖层
|
|
123
|
+
- Windows 在休眠恢复 / 换分辨率后会重置光标,覆盖层每约 1.8 秒**重新抑制**一次
|
|
124
|
+
- 完全不想抑制:`DSH_CU_KEEP_CURSOR=1`
|
|
115
125
|
helper 强杀覆盖层时也会兜底恢复一次
|
|
116
126
|
|
|
117
127
|
**DPI 关键点**:覆盖层进程必须 Per-Monitor-V2 感知,否则在 150% 缩放屏上
|
|
@@ -238,7 +248,7 @@ node test/integration.mjs
|
|
|
238
248
|
|
|
239
249
|
固定装置是一个独立的 WinForms 可执行文件(`test/bin/dsh-cu-fixture.exe`,一个文本框 +
|
|
240
250
|
一个名为 "Delete all" 的按钮;特意不用 shell 托管——shell 窗口本身被硬拒绝)。覆盖:
|
|
241
|
-
|
|
251
|
+
16 个工具注册、截图数组与 origin/zIndex、`accessibility:null` 语义、`set_value`、
|
|
242
252
|
`type_text`、危险按钮触发审批、`dryRun` 不落地、`denyApps` 拒绝、Win 键硬拒绝、
|
|
243
253
|
`scroll` 契约。
|
|
244
254
|
|
|
@@ -252,7 +262,7 @@ MIT。第三方商用前请自行做安全评审:这个插件能让模型操
|
|
|
252
262
|
| 文件 | 用途 | 大小 |
|
|
253
263
|
|---|---|---|
|
|
254
264
|
| `dsh-computer-use-helper.exe` | UIA / SendInput / 抓屏调度 / 音频调度 | ~50KB |
|
|
255
|
-
| `dsh-cu-overlay.exe` |
|
|
265
|
+
| `dsh-cu-overlay.exe` | 呼吸光边 + 虚拟光标覆盖层 | ~30KB |
|
|
256
266
|
| `capture/` | WGC 抓屏(.NET Framework 4.8 + SharpDX) | 0.68MB |
|
|
257
267
|
| `audio/` | WASAPI 环回录音(NAudio) | 0.60MB |
|
|
258
268
|
| **合计** | | **~1.4MB** |
|
|
@@ -289,6 +299,9 @@ marker 文件按 **会话 + turn** 命名,避免旧会话的 Esc 标记误伤
|
|
|
289
299
|
audio: false # 开启 WASAPI 环回录音(start/stop_audio_recording)
|
|
290
300
|
redactApps: [] # 这些应用的截图永不出机器,例如 ["1password", "银行"]
|
|
291
301
|
defaultIncludeScreenshot: true
|
|
302
|
+
timeoutMs: 10000 # 单次 RPC 超时(launch_app 用 launchTimeoutMs)
|
|
303
|
+
helperPath: null # 覆盖 helper 路径(仅信任场景)
|
|
304
|
+
riskyPattern: '' # 自定义「危险动作」正则,命中走审批
|
|
292
305
|
toolPrefix: '' # 例如 cu_ 可避免和其他插件的 click 重名
|
|
293
306
|
```
|
|
294
307
|
|
|
@@ -297,6 +310,8 @@ marker 文件按 **会话 + turn** 命名,避免旧会话的 Esc 标记误伤
|
|
|
297
310
|
- 只支持 Windows;驱动层是接口(`IPlatformDriver`),macOS / Linux 可照着补。
|
|
298
311
|
- Codex window2 没有的接口(`paste` / `clipboard_*` / `key_down` / `move`)这里也没有。
|
|
299
312
|
- `App.useCount` 没有数据来源,按可选字段留空,不伪造。
|
|
313
|
+
- `redactApps` 在**插件层**执行:直接调用 helper 的 RPC 不受它约束,别把 helper 单独暴露给非受信调用方。
|
|
314
|
+
- 抢焦点类工具(如 ProcessLasso)会让 `type_text` 返回 `focus_failed`,这是设计行为——宁可不输入,也不打到别的窗口。
|
|
300
315
|
|
|
301
316
|
## License
|
|
302
317
|
|
package/lib/index.js
CHANGED
|
@@ -143,6 +143,9 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
143
143
|
|
|
144
144
|
if (typeof ctx.on === 'function') {
|
|
145
145
|
ctx.on('dispose', () => {
|
|
146
|
+
// The overlay hides the system cursor while it is up; make sure teardown puts it
|
|
147
|
+
// back even if the overlay process was killed instead of exiting cleanly.
|
|
148
|
+
try { bridge?.request('restore_cursor', {}, { timeoutMs: 2000, skipTurnTransition: false }); } catch { /* best effort */ }
|
|
146
149
|
try { bridge?.dispose(); } catch { /* already gone */ }
|
|
147
150
|
bridge = null;
|
|
148
151
|
for (const dispose of disposers) {
|
package/native/Driver.cs
CHANGED
|
@@ -19,6 +19,8 @@ public interface IPlatformDriver : IDisposable
|
|
|
19
19
|
string PlatformName { get; }
|
|
20
20
|
Dictionary<string, object> Ping(Dictionary<string, object> p);
|
|
21
21
|
Dictionary<string, object> Hello(Dictionary<string, object> p);
|
|
22
|
+
Dictionary<string, object> RestoreCursor(Dictionary<string, object> p);
|
|
23
|
+
void Maintenance();
|
|
22
24
|
Dictionary<string, object> Close(Dictionary<string, object> p);
|
|
23
25
|
Dictionary<string, object> ListWindows(Dictionary<string, object> p);
|
|
24
26
|
Dictionary<string, object> ListApps(Dictionary<string, object> p);
|
|
@@ -182,14 +184,12 @@ public class WindowsDriver : IPlatformDriver
|
|
|
182
184
|
// border and a virtual cursor; this is the DSH equivalent, and Esc is the kill
|
|
183
185
|
// switch: the overlay drops a cancel file that every input path checks first.
|
|
184
186
|
Process _overlay;
|
|
187
|
+
System.Threading.Timer _cursorWatchdog;
|
|
185
188
|
DateTime _overlayUntil = DateTime.MinValue;
|
|
186
189
|
string _overlayCancelFile;
|
|
187
|
-
string _overlayImage = "";
|
|
188
|
-
string _overlayCrop = "";
|
|
189
190
|
string _overlayText = "DSH 正在使用您的电脑";
|
|
190
191
|
string _overlayHint = "Esc 取消";
|
|
191
192
|
string _overlayAccent = "#3f7fd8";
|
|
192
|
-
string _overlayCorner = "br";
|
|
193
193
|
string _cursorFile; // the overlay polls this to draw the virtual cursor
|
|
194
194
|
/// Turn token that the user interrupted with Esc. Codex persists the same
|
|
195
195
|
/// fact as a marker file per conversation+turn; here the token travels with
|
|
@@ -213,6 +213,7 @@ public class WindowsDriver : IPlatformDriver
|
|
|
213
213
|
|
|
214
214
|
public WindowsDriver()
|
|
215
215
|
{
|
|
216
|
+
Maintenance(); // startup repair: a previous session may have died with cursors hidden
|
|
216
217
|
try { SetThreadDpiAwarenessContext(new IntPtr(-4)); } catch { }
|
|
217
218
|
try { SetProcessDPIAware(); } catch { }
|
|
218
219
|
}
|
|
@@ -340,18 +341,12 @@ public class WindowsDriver : IPlatformDriver
|
|
|
340
341
|
|
|
341
342
|
public Dictionary<string, object> OverlayShow(Dictionary<string, object> p)
|
|
342
343
|
{
|
|
343
|
-
string image = Json.GetString(p, "image");
|
|
344
|
-
if (!string.IsNullOrEmpty(image)) _overlayImage = image;
|
|
345
|
-
string crop = Json.GetString(p, "crop");
|
|
346
|
-
if (!string.IsNullOrEmpty(crop)) _overlayCrop = crop;
|
|
347
344
|
string text = Json.GetString(p, "text");
|
|
348
345
|
if (!string.IsNullOrEmpty(text)) _overlayText = text;
|
|
349
346
|
string hint = Json.GetString(p, "hint");
|
|
350
347
|
if (!string.IsNullOrEmpty(hint)) _overlayHint = hint;
|
|
351
348
|
string accent = Json.GetString(p, "accent");
|
|
352
349
|
if (!string.IsNullOrEmpty(accent)) _overlayAccent = accent;
|
|
353
|
-
string corner = Json.GetString(p, "corner");
|
|
354
|
-
if (!string.IsNullOrEmpty(corner)) _overlayCorner = corner;
|
|
355
350
|
EnsureOverlay(Json.GetLong(p, "ttlMs", 20000));
|
|
356
351
|
Dictionary<string, object> r = new Dictionary<string, object>();
|
|
357
352
|
r["visible"] = true;
|
|
@@ -391,15 +386,22 @@ public class WindowsDriver : IPlatformDriver
|
|
|
391
386
|
args.Append(" --text \"").Append(_overlayText).Append('"');
|
|
392
387
|
args.Append(" --hint \"").Append(_overlayHint).Append('"');
|
|
393
388
|
args.Append(" --accent \"").Append(_overlayAccent).Append('"');
|
|
394
|
-
args.Append(" --corner ").Append(_overlayCorner);
|
|
395
389
|
if (_cursorFile != null) args.Append(" --cursor-file \"").Append(_cursorFile).Append('"');
|
|
396
|
-
|
|
390
|
+
// Suppression is the default (the virtual cursor stands in for the real one);
|
|
391
|
+
// DSH_CU_KEEP_CURSOR=1 keeps the real pointer visible instead.
|
|
392
|
+
if (Environment.GetEnvironmentVariable("DSH_CU_KEEP_CURSOR") == "1") args.Append(" --no-suppress-cursor");
|
|
397
393
|
try
|
|
398
394
|
{
|
|
399
395
|
ProcessStartInfo psi = new ProcessStartInfo(exe, args.ToString());
|
|
400
396
|
psi.UseShellExecute = false;
|
|
401
397
|
_overlay = Process.Start(psi);
|
|
402
398
|
_overlayUntil = DateTime.UtcNow.AddMilliseconds(ttlMs);
|
|
399
|
+
// Codex runs a dedicated system-cursor-manager process that restores the
|
|
400
|
+
// pointer the moment its parent dies. This timer gives the same guarantee
|
|
401
|
+
// without a second exe: if the overlay vanishes with cursors hidden, the
|
|
402
|
+
// pointer comes back within half a second, with no request needed.
|
|
403
|
+
if (_cursorWatchdog == null)
|
|
404
|
+
_cursorWatchdog = new System.Threading.Timer(delegate { try { Maintenance(); } catch { } }, null, 500, 500);
|
|
403
405
|
}
|
|
404
406
|
catch { _overlay = null; }
|
|
405
407
|
}
|
|
@@ -410,7 +412,8 @@ public class WindowsDriver : IPlatformDriver
|
|
|
410
412
|
// Kill() skips the overlay's exit handlers, which is where it would restore the
|
|
411
413
|
// system cursors. Restore them from here so a terminated overlay can never leave
|
|
412
414
|
// the user without a pointer.
|
|
413
|
-
|
|
415
|
+
RestoreCursors();
|
|
416
|
+
try { File.Delete(OverlayCursorFlagPath()); } catch { }
|
|
414
417
|
try { if (_overlayCancelFile != null && File.Exists(_overlayCancelFile)) File.Delete(_overlayCancelFile); } catch { }
|
|
415
418
|
_overlay = null;
|
|
416
419
|
_overlayUntil = DateTime.MinValue;
|
|
@@ -609,6 +612,43 @@ public class WindowsDriver : IPlatformDriver
|
|
|
609
612
|
return d;
|
|
610
613
|
}
|
|
611
614
|
|
|
615
|
+
/// Run cheaply before every request: if a previous overlay died while it was hiding
|
|
616
|
+
/// the system cursors, put them back. A blank desktop cursor is worse than a stray
|
|
617
|
+
/// visible one, so this never assumes the overlay cleaned up after itself.
|
|
618
|
+
public void Maintenance()
|
|
619
|
+
{
|
|
620
|
+
try
|
|
621
|
+
{
|
|
622
|
+
string flag = OverlayCursorFlagPath();
|
|
623
|
+
if (!File.Exists(flag)) return;
|
|
624
|
+
int pid;
|
|
625
|
+
if (!int.TryParse(File.ReadAllText(flag).Trim(), out pid)) { RestoreCursors(); return; }
|
|
626
|
+
bool alive;
|
|
627
|
+
try { Process.GetProcessById(pid); alive = true; } catch { alive = false; }
|
|
628
|
+
if (!alive) { RestoreCursors(); try { File.Delete(flag); } catch { } }
|
|
629
|
+
}
|
|
630
|
+
catch { }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
static string OverlayCursorFlagPath()
|
|
634
|
+
{
|
|
635
|
+
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-computer-use", "cursor-hidden.flag");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
static void RestoreCursors()
|
|
639
|
+
{
|
|
640
|
+
try { SystemParametersInfo(0x0057, 0, IntPtr.Zero, 0); } catch { }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
public Dictionary<string, object> RestoreCursor(Dictionary<string, object> p)
|
|
644
|
+
{
|
|
645
|
+
RestoreCursors();
|
|
646
|
+
try { File.Delete(OverlayCursorFlagPath()); } catch { }
|
|
647
|
+
Dictionary<string, object> r = new Dictionary<string, object>();
|
|
648
|
+
r["restored"] = true;
|
|
649
|
+
return r;
|
|
650
|
+
}
|
|
651
|
+
|
|
612
652
|
public Dictionary<string, object> Hello(Dictionary<string, object> p)
|
|
613
653
|
{
|
|
614
654
|
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
@@ -811,8 +851,13 @@ public class WindowsDriver : IPlatformDriver
|
|
|
811
851
|
for (int i = 0; i < args.Count; i++)
|
|
812
852
|
{
|
|
813
853
|
if (i > 0) argLine.Append(' ');
|
|
814
|
-
string a = Convert.ToString(args[i], System.Globalization.CultureInfo.InvariantCulture);
|
|
815
|
-
|
|
854
|
+
string a = Convert.ToString(args[i], System.Globalization.CultureInfo.InvariantCulture) ?? "";
|
|
855
|
+
// A quote or newline would let a caller smuggle extra arguments past the
|
|
856
|
+
// single quoting rule below, so refuse instead of guessing.
|
|
857
|
+
if (a.IndexOf('"') >= 0 || a.IndexOf('\n') >= 0 || a.IndexOf('\r') >= 0)
|
|
858
|
+
throw new ToolError("bad_request", "launch_app argument " + i + " contains a quote or newline; pass a single plain argument");
|
|
859
|
+
if (a.Length == 0 || a.IndexOf(' ') >= 0 || a.IndexOf('\t') >= 0)
|
|
860
|
+
argLine.Append('"').Append(a).Append('"');
|
|
816
861
|
else argLine.Append(a);
|
|
817
862
|
}
|
|
818
863
|
psi.Arguments = argLine.ToString();
|
|
@@ -1642,13 +1687,16 @@ public class WindowsDriver : IPlatformDriver
|
|
|
1642
1687
|
EnsureOverlay(8000);
|
|
1643
1688
|
IntPtr h = ResolveWindow(p);
|
|
1644
1689
|
AssertAppApproved(h);
|
|
1645
|
-
|
|
1690
|
+
// Validation first: an invalid call must not activate a window or move the pointer.
|
|
1646
1691
|
double scrollY = Json.GetDouble(p, "scrollY", 0);
|
|
1647
1692
|
double scrollX = Json.GetDouble(p, "scrollX", 0);
|
|
1648
1693
|
double x = Json.GetDouble(p, "x", double.NaN);
|
|
1649
1694
|
double y = Json.GetDouble(p, "y", double.NaN);
|
|
1650
1695
|
if (double.IsNaN(x) || double.IsNaN(y))
|
|
1651
1696
|
throw new ToolError("bad_request", "scroll requires finite 'x' and 'y' (the point to scroll from, in window/screenshot coordinates); click a pane first if it needs focus");
|
|
1697
|
+
if (Math.Abs(scrollX) < 0.0001 && Math.Abs(scrollY) < 0.0001)
|
|
1698
|
+
throw new ToolError("bad_request", "scroll requires a non-zero scrollX or scrollY");
|
|
1699
|
+
Activate(h);
|
|
1652
1700
|
int sx, sy;
|
|
1653
1701
|
string scrollShot = Json.GetString(p, "screenshotId");
|
|
1654
1702
|
int originLeft, originTop;
|
|
@@ -1685,6 +1733,12 @@ public class WindowsDriver : IPlatformDriver
|
|
|
1685
1733
|
EnsureOverlay(8000);
|
|
1686
1734
|
IntPtr h = ResolveWindow(p);
|
|
1687
1735
|
AssertAppApproved(h);
|
|
1736
|
+
double fromX = Json.GetDouble(p, "from_x", double.NaN);
|
|
1737
|
+
double fromY = Json.GetDouble(p, "from_y", double.NaN);
|
|
1738
|
+
double toX = Json.GetDouble(p, "to_x", double.NaN);
|
|
1739
|
+
double toY = Json.GetDouble(p, "to_y", double.NaN);
|
|
1740
|
+
if (double.IsNaN(fromX) || double.IsNaN(fromY) || double.IsNaN(toX) || double.IsNaN(toY))
|
|
1741
|
+
throw new ToolError("bad_request", "drag requires finite from_x/from_y/to_x/to_y (window-relative pixels)");
|
|
1688
1742
|
Activate(h);
|
|
1689
1743
|
int originX, originY;
|
|
1690
1744
|
double dragScaleX = 1.0, dragScaleY = 1.0;
|
|
@@ -1692,8 +1746,8 @@ public class WindowsDriver : IPlatformDriver
|
|
|
1692
1746
|
if (!string.IsNullOrEmpty(dragShot))
|
|
1693
1747
|
{
|
|
1694
1748
|
ShotRegion region = ResolveShot(h, dragShot);
|
|
1695
|
-
AssertPointInsideShot(
|
|
1696
|
-
AssertPointInsideShot(
|
|
1749
|
+
AssertPointInsideShot(fromX, fromY, region, dragShot, "drag start");
|
|
1750
|
+
AssertPointInsideShot(toX, toY, region, dragShot, "drag end");
|
|
1697
1751
|
originX = region.OriginX; originY = region.OriginY;
|
|
1698
1752
|
dragScaleX = region.ScaleX; dragScaleY = region.ScaleY;
|
|
1699
1753
|
}
|
|
@@ -1702,10 +1756,10 @@ public class WindowsDriver : IPlatformDriver
|
|
|
1702
1756
|
RECT b = BoundsOf(h);
|
|
1703
1757
|
originX = b.Left; originY = b.Top;
|
|
1704
1758
|
}
|
|
1705
|
-
int fx = originX + (int)Math.Round(
|
|
1706
|
-
int fy = originY + (int)Math.Round(
|
|
1707
|
-
int tx = originX + (int)Math.Round(
|
|
1708
|
-
int ty = originY + (int)Math.Round(
|
|
1759
|
+
int fx = originX + (int)Math.Round(fromX * dragScaleX);
|
|
1760
|
+
int fy = originY + (int)Math.Round(fromY * dragScaleY);
|
|
1761
|
+
int tx = originX + (int)Math.Round(toX * dragScaleX);
|
|
1762
|
+
int ty = originY + (int)Math.Round(toY * dragScaleY);
|
|
1709
1763
|
|
|
1710
1764
|
MarkCursor(fx, fy);
|
|
1711
1765
|
SetCursorPos(fx, fy);
|
package/native/Overlay.cs
CHANGED
|
@@ -52,9 +52,6 @@ public static class Native
|
|
|
52
52
|
public const int ULW_ALPHA = 2;
|
|
53
53
|
public const uint SPI_SETCURSORS = 0x0057;
|
|
54
54
|
|
|
55
|
-
public static bool LastLayerOk = true;
|
|
56
|
-
public static int LastLayerError = 0;
|
|
57
|
-
|
|
58
55
|
public static void ApplyLayered(Form form, Bitmap surface)
|
|
59
56
|
{
|
|
60
57
|
IntPtr screenDc = GetDC(IntPtr.Zero);
|
|
@@ -67,8 +64,7 @@ public static class Native
|
|
|
67
64
|
POINT src = new POINT { X = 0, Y = 0 };
|
|
68
65
|
SIZE size = new SIZE { CX = surface.Width, CY = surface.Height };
|
|
69
66
|
BLENDFUNCTION blend = new BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = 1 };
|
|
70
|
-
|
|
71
|
-
LastLayerError = LastLayerOk ? 0 : Marshal.GetLastWin32Error();
|
|
67
|
+
UpdateLayeredWindow(form.Handle, screenDc, ref dst, ref size, memDc, ref src, 0, ref blend, ULW_ALPHA);
|
|
72
68
|
}
|
|
73
69
|
finally
|
|
74
70
|
{
|
|
@@ -84,19 +80,48 @@ public static class Native
|
|
|
84
80
|
try { SetWindowPos(form.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); } catch { }
|
|
85
81
|
}
|
|
86
82
|
|
|
83
|
+
/// Path of the "cursors are hidden" flag. It carries our PID so a long-lived
|
|
84
|
+
/// supervisor can tell "the overlay is still hiding them" from "the overlay died
|
|
85
|
+
/// and left the desktop without a pointer".
|
|
86
|
+
public static string CursorFlagPath()
|
|
87
|
+
{
|
|
88
|
+
string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-computer-use");
|
|
89
|
+
return Path.Combine(dir, "cursor-hidden.flag");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static IntPtr _blankCursor;
|
|
93
|
+
|
|
87
94
|
public static void SuppressSystemCursors(bool suppress)
|
|
88
95
|
{
|
|
89
96
|
try
|
|
90
97
|
{
|
|
91
|
-
if (!suppress)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
if (!suppress)
|
|
99
|
+
{
|
|
100
|
+
SystemParametersInfo(SPI_SETCURSORS, 0, IntPtr.Zero, 0);
|
|
101
|
+
try { File.Delete(CursorFlagPath()); } catch { }
|
|
102
|
+
if (_blankCursor != IntPtr.Zero) { try { DestroyCursor(_blankCursor); } catch { } _blankCursor = IntPtr.Zero; }
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Cache the blank cursor: Windows resets the cursors on resume, display
|
|
106
|
+
// change and some logins, so the controller calls this again periodically
|
|
107
|
+
// (Codex calls the same "system cursor re-suppression").
|
|
108
|
+
if (_blankCursor == IntPtr.Zero)
|
|
109
|
+
{
|
|
110
|
+
byte[] andMask = new byte[128];
|
|
111
|
+
byte[] xorMask = new byte[128];
|
|
112
|
+
for (int i = 0; i < andMask.Length; i++) andMask[i] = 0xFF;
|
|
113
|
+
_blankCursor = CreateCursor(IntPtr.Zero, 0, 0, 32, 32, andMask, xorMask);
|
|
114
|
+
}
|
|
115
|
+
IntPtr blank = _blankCursor;
|
|
96
116
|
if (blank == IntPtr.Zero) return;
|
|
97
117
|
uint[] ids = new uint[] { 32512, 32513, 32514, 32515, 32516, 32631, 32640, 32641, 32642, 32643, 32644, 32645, 32646, 32648, 32649, 32650, 32651, 32671 };
|
|
98
118
|
foreach (uint id in ids) SetSystemCursor(CopyIcon(blank), id);
|
|
99
|
-
|
|
119
|
+
try
|
|
120
|
+
{
|
|
121
|
+
Directory.CreateDirectory(Path.GetDirectoryName(CursorFlagPath()));
|
|
122
|
+
File.WriteAllText(CursorFlagPath(), System.Diagnostics.Process.GetCurrentProcess().Id.ToString());
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
100
125
|
}
|
|
101
126
|
catch { }
|
|
102
127
|
}
|
|
@@ -204,19 +229,6 @@ public sealed class CursorForm : Form
|
|
|
204
229
|
g.DrawPolygon(outline, arrow);
|
|
205
230
|
}
|
|
206
231
|
}
|
|
207
|
-
string dump = Environment.GetEnvironmentVariable("DSH_CU_DUMP");
|
|
208
|
-
if (!string.IsNullOrEmpty(dump))
|
|
209
|
-
{
|
|
210
|
-
try
|
|
211
|
-
{
|
|
212
|
-
surface.Save(dump, ImageFormat.Png);
|
|
213
|
-
System.IO.File.AppendAllText(dump + ".log",
|
|
214
|
-
DateTime.Now.ToString("HH:mm:ss.fff") + " visible=" + Visible + " handle=" + Handle.ToInt64() +
|
|
215
|
-
" bounds=" + Left + "," + Top + " " + Width + "x" + Height +
|
|
216
|
-
" layerOk=" + Native.LastLayerOk + " err=" + Native.LastLayerError + "\n");
|
|
217
|
-
}
|
|
218
|
-
catch { }
|
|
219
|
-
}
|
|
220
232
|
if (!Visible) Show();
|
|
221
233
|
Native.ApplyLayered(this, surface);
|
|
222
234
|
}
|
|
@@ -391,7 +403,7 @@ public sealed class StripForm : Form
|
|
|
391
403
|
// halo: widest + faintest first, then tighten
|
|
392
404
|
for (int i = 3; i >= 1; i--)
|
|
393
405
|
{
|
|
394
|
-
float width = i *
|
|
406
|
+
float width = i * 3.2f;
|
|
395
407
|
int alpha = Math.Max(6, glowAlpha / (i + 1));
|
|
396
408
|
using (Pen halo = new Pen(Color.FromArgb(alpha, accent), width))
|
|
397
409
|
{
|
|
@@ -410,7 +422,7 @@ public sealed class StripForm : Form
|
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
// core hairline
|
|
413
|
-
using (Pen core = new Pen(Color.FromArgb(coreAlpha, accent),
|
|
425
|
+
using (Pen core = new Pen(Color.FromArgb(coreAlpha, accent), 2f))
|
|
414
426
|
{
|
|
415
427
|
core.Alignment = PenAlignment.Center;
|
|
416
428
|
if (vertical)
|
|
@@ -514,7 +526,7 @@ internal sealed class Controller : ApplicationContext
|
|
|
514
526
|
delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
|
|
515
527
|
|
|
516
528
|
const int Radius = 18; // corner radius
|
|
517
|
-
const int Glow =
|
|
529
|
+
const int Glow = 16; // band the halo fades inside
|
|
518
530
|
|
|
519
531
|
public Controller(int ttlMs)
|
|
520
532
|
{
|
|
@@ -550,8 +562,8 @@ internal sealed class Controller : ApplicationContext
|
|
|
550
562
|
Rectangle screen = SystemInformation.VirtualScreen;
|
|
551
563
|
// slow 3.2s breath: the frame gets a little brighter and a touch wider
|
|
552
564
|
double phase = (Math.Sin(_tick / 26.0 - 1.57) + 1.0) / 2.0;
|
|
553
|
-
int coreAlpha =
|
|
554
|
-
int glowAlpha =
|
|
565
|
+
int coreAlpha = 200 + (int)Math.Round(55 * phase); // clearly visible, still a hairline
|
|
566
|
+
int glowAlpha = 42 + (int)Math.Round(26 * phase);
|
|
555
567
|
int thickness = Glow;
|
|
556
568
|
|
|
557
569
|
int x0 = screen.Left, y0 = screen.Top, w = screen.Width, h = screen.Height;
|
|
@@ -602,6 +614,10 @@ internal sealed class Controller : ApplicationContext
|
|
|
602
614
|
}
|
|
603
615
|
catch { }
|
|
604
616
|
|
|
617
|
+
// Windows restores the real cursors on resume / display change; put the blank
|
|
618
|
+
// ones back while the indicator is up (Codex: "schedule system cursor re-suppression").
|
|
619
|
+
if (Config.SuppressCursor && _tick % 40 == 0) Native.SuppressSystemCursors(true);
|
|
620
|
+
|
|
605
621
|
if (!_cursor.Live)
|
|
606
622
|
{
|
|
607
623
|
PointNative p;
|
|
@@ -641,6 +657,36 @@ internal static class Program
|
|
|
641
657
|
try { SetProcessDpiAwarenessContext(new IntPtr(-4)); } catch { }
|
|
642
658
|
try { SetProcessDPIAware(); } catch { }
|
|
643
659
|
|
|
660
|
+
// Guard mode: wait for the owner, then restore the cursors. This must run before
|
|
661
|
+
// anything else - a guard that reached the suppression block would hide the
|
|
662
|
+
// cursors again and spawn another guard, and the chain would never stop.
|
|
663
|
+
int guardIndex = Array.IndexOf(args, "--cursor-guard");
|
|
664
|
+
if (guardIndex >= 0 && args.Length > guardIndex + 1)
|
|
665
|
+
{
|
|
666
|
+
int ownerPid;
|
|
667
|
+
if (int.TryParse(args[guardIndex + 1], out ownerPid) && ownerPid != System.Diagnostics.Process.GetCurrentProcess().Id)
|
|
668
|
+
{
|
|
669
|
+
try
|
|
670
|
+
{
|
|
671
|
+
System.Diagnostics.Process owner = System.Diagnostics.Process.GetProcessById(ownerPid);
|
|
672
|
+
owner.WaitForExit(600000); // hard cap: a guard never lives forever
|
|
673
|
+
}
|
|
674
|
+
catch { /* owner already gone */ }
|
|
675
|
+
// Only clean up our own owner's suppression: a newer overlay may have
|
|
676
|
+
// taken over the cursors in the meantime, and restoring them here would
|
|
677
|
+
// make the real pointer reappear under a live indicator.
|
|
678
|
+
bool ours = false;
|
|
679
|
+
try
|
|
680
|
+
{
|
|
681
|
+
string flagPath = Native.CursorFlagPath();
|
|
682
|
+
ours = File.Exists(flagPath) && File.ReadAllText(flagPath).Trim() == ownerPid.ToString();
|
|
683
|
+
}
|
|
684
|
+
catch { }
|
|
685
|
+
if (ours) Native.SuppressSystemCursors(false);
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
644
690
|
int ttlMs = 20000;
|
|
645
691
|
for (int i = 0; i < args.Length - 1; i++)
|
|
646
692
|
{
|
|
@@ -660,6 +706,16 @@ internal static class Program
|
|
|
660
706
|
{
|
|
661
707
|
Native.SuppressSystemCursors(true);
|
|
662
708
|
AppDomain.CurrentDomain.ProcessExit += (s, e) => Native.SuppressSystemCursors(false);
|
|
709
|
+
try
|
|
710
|
+
{
|
|
711
|
+
System.Diagnostics.ProcessStartInfo guard = new System.Diagnostics.ProcessStartInfo(
|
|
712
|
+
System.Reflection.Assembly.GetExecutingAssembly().Location,
|
|
713
|
+
"--cursor-guard " + System.Diagnostics.Process.GetCurrentProcess().Id);
|
|
714
|
+
guard.UseShellExecute = false;
|
|
715
|
+
guard.CreateNoWindow = true;
|
|
716
|
+
System.Diagnostics.Process.Start(guard);
|
|
717
|
+
}
|
|
718
|
+
catch { /* the helper watchdog still covers us */ }
|
|
663
719
|
}
|
|
664
720
|
|
|
665
721
|
Application.EnableVisualStyles();
|
package/native/Program.cs
CHANGED
|
@@ -89,6 +89,7 @@ public static class Program
|
|
|
89
89
|
// Codex ships the budget with every call (x-oai-cua-request-budget-ms) and the
|
|
90
90
|
// helper honours it; here it bounds capture waits and marks overruns.
|
|
91
91
|
Budget.Begin(Json.GetLong(p, "__budgetMs", 0));
|
|
92
|
+
try { if (_driver != null) _driver.Maintenance(); } catch { }
|
|
92
93
|
object result = Dispatch(method, p);
|
|
93
94
|
return Ok(id, result);
|
|
94
95
|
}
|
|
@@ -109,6 +110,7 @@ public static class Program
|
|
|
109
110
|
{
|
|
110
111
|
case "ping": return _driver.Ping(p);
|
|
111
112
|
case "hello": return _driver.Hello(p);
|
|
113
|
+
case "restore_cursor": return _driver.RestoreCursor(p);
|
|
112
114
|
case "close": return _driver.Close(p);
|
|
113
115
|
case "list_windows": return _driver.ListWindows(p);
|
|
114
116
|
case "list_apps": return _driver.ListApps(p);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepseek-computer-use",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Windows Computer Use for DeepSeek Harness: UIA observation, SendInput actions, Windows.Graphics.Capture — modeled on Codex's window2 API. Community plugin, not an official DeepSeek package.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|