dsh-codex-style-computer-use 1.2.3 → 1.3.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 +27 -14
- package/README.zh.md +28 -14
- package/icon.svg +29 -0
- package/lib/batch.js +21 -5
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/continuation.js +14 -0
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/compile-worker.ps1 +5 -1
- package/lib/core/index.js +170 -28
- package/lib/core/native-runtime.js +6 -1
- package/lib/core/winmd/Windows.winmd +0 -0
- package/lib/core/worker.cs +1116 -369
- package/lib/cycle-lifecycle.js +66 -0
- package/lib/cycle.js +99 -9
- package/lib/index.js +68 -618
- package/lib/policy.js +24 -3
- package/lib/skill-gate.js +1 -1
- package/lib/terminal-cleanup.js +57 -0
- package/lib/tools.js +58 -6
- package/mcp/src/execution.js +22 -0
- package/mcp/src/index.js +36 -13
- package/package.json +4 -2
- package/scripts/render-promo.py +81 -0
- package/skills/computer-use/SKILL.md +23 -3
- package/skills/computer-use/references/lifecycle.md +23 -12
- package/skills/computer-use/references/zero-uia-apps.md +54 -0
package/lib/core/worker.cs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Safety model: the worker is a faithful actuator. Policy (approvals,
|
|
14
14
|
// blacklists, masking) lives in the DSH plugin / MCP gate layers. Worker-side
|
|
15
15
|
// guards: exact physical Ctrl+Esc for manual pause, event-driven human ownership,
|
|
16
|
-
// and accepted-input cleanup. Physical
|
|
16
|
+
// and accepted-input cleanup. Physical takeover during actuation yields until three seconds quiet.
|
|
17
17
|
|
|
18
18
|
using System;
|
|
19
19
|
using System.Collections.Generic;
|
|
@@ -24,23 +24,26 @@ using System.Drawing.Imaging;
|
|
|
24
24
|
using System.IO;
|
|
25
25
|
using System.Linq;
|
|
26
26
|
using System.Runtime.InteropServices;
|
|
27
|
+
using System.Runtime.InteropServices.WindowsRuntime; // AsBuffer: byte[] -> WinRT IBuffer
|
|
27
28
|
using System.Text;
|
|
28
29
|
using System.Threading;
|
|
29
30
|
using System.Web.Script.Serialization;
|
|
30
31
|
using System.Windows.Automation;
|
|
31
32
|
using System.Windows.Forms; // Clipboard (watch out: bare Point stays our own struct)
|
|
33
|
+
// Inbox OCR (zero external dependencies): the union Windows.winmd ships in lib/core/winmd.
|
|
34
|
+
using Windows.Media.Ocr;
|
|
35
|
+
using Windows.Graphics.Imaging;
|
|
32
36
|
|
|
33
37
|
namespace DshComputerUse.Worker
|
|
34
38
|
{
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// and no physical event returns control. Ordinary input never creates a STOP file.
|
|
39
|
+
// A live cycle observes physical holds, but only actual actuation can start a handoff.
|
|
40
|
+
// Once yielded, three quiet seconds with no held input returns control to the agent.
|
|
38
41
|
public sealed class HumanInputState
|
|
39
42
|
{
|
|
40
43
|
public const int SustainedMs = 2000, IdleMs = 3000;
|
|
41
44
|
readonly object sync = new object();
|
|
42
45
|
readonly HashSet<int> held = new HashSet<int>();
|
|
43
|
-
bool enabled, seen;
|
|
46
|
+
bool enabled, acting, seen;
|
|
44
47
|
string phase = "idle", lastKind = "none";
|
|
45
48
|
long first, last, revision, burst;
|
|
46
49
|
public string Phase { get { lock (sync) { return phase; } } }
|
|
@@ -67,20 +70,38 @@ namespace DshComputerUse.Worker
|
|
|
67
70
|
{
|
|
68
71
|
if (enabled == on) return;
|
|
69
72
|
enabled = on;
|
|
70
|
-
if (!on) { phase = "idle";
|
|
71
|
-
|
|
73
|
+
if (!on) { phase = "idle"; acting = false; }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
public void SetActing(bool on, long now)
|
|
77
|
+
{
|
|
78
|
+
lock (sync)
|
|
79
|
+
{
|
|
80
|
+
acting = on;
|
|
81
|
+
// Do not replay earlier ordinary activity. A key/button still physically
|
|
82
|
+
// held does need ownership before we can inject a conflicting chord.
|
|
83
|
+
if (on && enabled && phase == "idle" && held.Count > 0)
|
|
84
|
+
{ last = now; revision++; Start(now); }
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
|
|
75
|
-
public void Record(int key, bool down, bool up, long now)
|
|
88
|
+
public void Record(int key, bool down, bool up, long now) { Record(key, down, up, now, false); }
|
|
89
|
+
/// A DELIBERATE pointer move (the 16-px tolerance in PointerHandoffTolerance has already
|
|
90
|
+
/// filtered jitter and agent motion) yields even BETWEEN actuations. Human spec
|
|
91
|
+
/// 2026-11-04: while a cycle is live, the human reaching for the mouse is a takeover no
|
|
92
|
+
/// matter whether an injection happens to be in flight at that instant — the old gate
|
|
93
|
+
/// (`idle && !acting`) only lit the yield during the few dozen milliseconds of each
|
|
94
|
+
/// click, so an interruption in the gaps showed no light at all.
|
|
95
|
+
public void Record(int key, bool down, bool up, long now, bool deliberate)
|
|
76
96
|
{
|
|
77
97
|
lock (sync)
|
|
78
98
|
{
|
|
79
99
|
bool continued = held.Count > 0 || (seen && now - last < IdleMs);
|
|
80
100
|
lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
|
|
81
101
|
if (key != 0) { if (down) held.Add(key); if (up) held.Remove(key); }
|
|
82
|
-
seen = true; last = now;
|
|
83
|
-
if (!enabled) return;
|
|
102
|
+
seen = true; last = now;
|
|
103
|
+
if (!enabled || (phase == "idle" && !acting && !deliberate)) return;
|
|
104
|
+
revision++;
|
|
84
105
|
if (phase == "idle" || !continued) Start(now);
|
|
85
106
|
if (now - first >= SustainedMs) phase = "waiting";
|
|
86
107
|
}
|
|
@@ -103,6 +124,68 @@ namespace DshComputerUse.Worker
|
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
// The UAC secure desktop (and the lock screen) is the ONE surface automation must never
|
|
128
|
+
// reach — Windows enforces that itself. The honest addition (human request 2026-11-04)
|
|
129
|
+
// is to NOTICE the switch, refuse to dispatch while it stands, tell the human exactly
|
|
130
|
+
// what to do, and auto-continue the moment the normal desktop is back. Detection is an
|
|
131
|
+
// OpenInputDesktop probe: while the secure desktop is up, an unprivileged thread cannot
|
|
132
|
+
// open the input desktop at all. Transitions are confirmed over consecutive misses so a
|
|
133
|
+
// single hiccup never pauses dispatch, and one success clears the state immediately.
|
|
134
|
+
public sealed class SecureDesktopWatch
|
|
135
|
+
{
|
|
136
|
+
public const int ConfirmMisses = 3;
|
|
137
|
+
readonly object sync = new object();
|
|
138
|
+
bool active; int misses;
|
|
139
|
+
public bool Active { get { lock (sync) { return active; } } }
|
|
140
|
+
/// Returns true only on a STATE TRANSITION; the caller audits and notifies those alone.
|
|
141
|
+
public bool Observe(bool reachable)
|
|
142
|
+
{
|
|
143
|
+
lock (sync)
|
|
144
|
+
{
|
|
145
|
+
if (reachable)
|
|
146
|
+
{
|
|
147
|
+
misses = 0;
|
|
148
|
+
if (!active) return false;
|
|
149
|
+
active = false;
|
|
150
|
+
return true; // secured -> normal: dispatch may resume
|
|
151
|
+
}
|
|
152
|
+
if (active) return false;
|
|
153
|
+
if (++misses < ConfirmMisses) return false;
|
|
154
|
+
active = true;
|
|
155
|
+
return true; // normal -> secured
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Sum only physical displacement, rebasing every injected cursor event. Agent
|
|
161
|
+
// motion cannot turn a 1-pixel hand tremor into a takeover. Back-and-forth jitter
|
|
162
|
+
// cancels rather than accumulating path length; a deliberate 16-pixel move yields.
|
|
163
|
+
public sealed class PointerHandoffTolerance
|
|
164
|
+
{
|
|
165
|
+
public const int DistancePx = 16, BurstGapMs = 250;
|
|
166
|
+
readonly object sync = new object();
|
|
167
|
+
bool seen;
|
|
168
|
+
int x, y;
|
|
169
|
+
long dx, dy, lastPhysical;
|
|
170
|
+
public void Reset(int px, int py)
|
|
171
|
+
{ lock (sync) { seen = true; x = px; y = py; dx = dy = 0; lastPhysical = 0; } }
|
|
172
|
+
public bool Record(int px, int py, bool physical, bool acting, bool yielded, long now)
|
|
173
|
+
{
|
|
174
|
+
lock (sync)
|
|
175
|
+
{
|
|
176
|
+
long mx = seen ? (long)px - x : 0, my = seen ? (long)py - y : 0;
|
|
177
|
+
x = px; y = py; seen = true;
|
|
178
|
+
if (!physical) return false;
|
|
179
|
+
if (!acting && !yielded) { dx = dy = 0; lastPhysical = now; return false; }
|
|
180
|
+
if (mx == 0 && my == 0) return false;
|
|
181
|
+
if (yielded) return true;
|
|
182
|
+
if (now - lastPhysical > BurstGapMs) dx = dy = 0;
|
|
183
|
+
lastPhysical = now; dx += mx; dy += my;
|
|
184
|
+
return dx * dx + dy * dy >= DistancePx * DistancePx;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
106
189
|
// A temporary pause is an explicit, single-use claim. No clock grants permission.
|
|
107
190
|
public sealed class RecoveryPermit
|
|
108
191
|
{
|
|
@@ -177,7 +260,12 @@ namespace DshComputerUse.Worker
|
|
|
177
260
|
if (!active || paused || exited) return null;
|
|
178
261
|
lock (sync)
|
|
179
262
|
{
|
|
180
|
-
if (hwnd == 0 || pid == 0)
|
|
263
|
+
if (hwnd == 0 || pid == 0)
|
|
264
|
+
{
|
|
265
|
+
const string missing = "this worker has no confirmed target for the current cycle";
|
|
266
|
+
if (host || foreground == 0 || process == 0) return missing;
|
|
267
|
+
return missing + ". Observed foreground hwnd=" + foreground + ", pid=" + process;
|
|
268
|
+
}
|
|
181
269
|
if (!targetExists) return "the activated target window no longer exists";
|
|
182
270
|
if (host) return "the agent host is in front of the target window";
|
|
183
271
|
if (hwnd != foreground || pid != process) return "the foreground no longer matches the activated target";
|
|
@@ -389,6 +477,13 @@ namespace DshComputerUse.Worker
|
|
|
389
477
|
public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);
|
|
390
478
|
[DllImport("user32.dll")]
|
|
391
479
|
public static extern uint SendNotifyMessage(IntPtr hWnd, uint msg, IntPtr w, IntPtr l);
|
|
480
|
+
// Secure-desktop probe: while the UAC prompt (or the lock screen) is up, an unprivileged
|
|
481
|
+
// thread CANNOT open the input desktop — that refusal is the only reliable, permission-free
|
|
482
|
+
// witness that the machine switched to the surface automation must never touch.
|
|
483
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
484
|
+
public static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
|
|
485
|
+
[DllImport("user32.dll")]
|
|
486
|
+
public static extern bool CloseDesktop(IntPtr h);
|
|
392
487
|
|
|
393
488
|
// ---- low-level keyboard hook: the human's emergency brake ----
|
|
394
489
|
public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
|
|
@@ -415,7 +510,7 @@ namespace DshComputerUse.Worker
|
|
|
415
510
|
[StructLayout(LayoutKind.Sequential)]
|
|
416
511
|
public struct KBDLLHOOKSTRUCT { public uint vkCode, scanCode, flags, time; public IntPtr dwExtraInfo; }
|
|
417
512
|
[StructLayout(LayoutKind.Sequential)]
|
|
418
|
-
public struct NativeMsg { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; }
|
|
513
|
+
public struct NativeMsg { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; public uint lPrivate; }
|
|
419
514
|
public const int WH_KEYBOARD_LL = 13, WM_KEYDOWN_ = 0x0100, WM_SYSKEYDOWN_ = 0x0104,
|
|
420
515
|
WM_KEYUP_ = 0x0101, WM_SYSKEYUP_ = 0x0105;
|
|
421
516
|
// KBDLLHOOKSTRUCT.flags bit 4: the key did NOT come from the physical keyboard — some
|
|
@@ -455,7 +550,6 @@ namespace DshComputerUse.Worker
|
|
|
455
550
|
[StructLayout(LayoutKind.Sequential)] struct SIZEL { public int cx, cy; }
|
|
456
551
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
|
457
552
|
struct BLENDFUNCTION { public byte BlendOp, BlendFlags, SourceConstantAlpha, AlphaFormat; }
|
|
458
|
-
[StructLayout(LayoutKind.Sequential)] struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam, lParam; public uint time; public int ptX, ptY; }
|
|
459
553
|
|
|
460
554
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
461
555
|
static extern IntPtr CreateWindowEx(int exStyle, string cls, string name, int style,
|
|
@@ -465,49 +559,77 @@ namespace DshComputerUse.Worker
|
|
|
465
559
|
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
466
560
|
[DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr h);
|
|
467
561
|
[DllImport("user32.dll")] static extern int ReleaseDC(IntPtr h, IntPtr dc);
|
|
468
|
-
[DllImport("user32.dll")] static extern bool PeekMessage(out
|
|
562
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool PeekMessage(out Native.NativeMsg m, IntPtr h, uint min, uint max, uint remove);
|
|
563
|
+
[DllImport("user32.dll")] static extern bool ValidateRect(IntPtr h, IntPtr rect);
|
|
469
564
|
[DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr dc);
|
|
470
565
|
[DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr dc);
|
|
471
566
|
[DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr dc, IntPtr o);
|
|
472
567
|
[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr o);
|
|
473
568
|
[DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr dc, ref Native.BITMAPINFO bmi, uint usage, out IntPtr bits, IntPtr section, uint offset);
|
|
474
|
-
[DllImport("user32.dll")] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
|
|
569
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
|
|
475
570
|
IntPtr src, ref POINTL pptSrc, int colorKey, ref BLENDFUNCTION blend, int flags);
|
|
476
571
|
[DllImport("user32.dll")] static extern int GetSystemMetrics(int i);
|
|
572
|
+
[DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
|
|
477
573
|
|
|
478
574
|
static Thread _thread;
|
|
479
|
-
static volatile
|
|
480
|
-
static volatile
|
|
575
|
+
static volatile bool _acting;
|
|
576
|
+
static volatile bool _preparing;
|
|
577
|
+
static volatile int _preparationStart;
|
|
578
|
+
static int _preparationSerial, _presentedPreparation, _completedPreparation;
|
|
579
|
+
static volatile string _rendererStage = "not-started", _rendererFault;
|
|
580
|
+
static volatile int _lastLoopTick, _lastPresentTick, _lastFailedError;
|
|
581
|
+
static int _frames, _presentAttempts, _presentSuccesses, _lastAttemptWant, _lastAttemptSerial;
|
|
582
|
+
static int _lastAttemptedPreparationSerial = -1, _lastFailedSerial = -1, _lastFailedWant, _rebuiltSerial = -1;
|
|
583
|
+
public static Dictionary<string, object> Health()
|
|
584
|
+
{
|
|
585
|
+
var thread = _thread;
|
|
586
|
+
int now = Environment.TickCount;
|
|
587
|
+
return Program.Dict("workerPid", System.Diagnostics.Process.GetCurrentProcess().Id,
|
|
588
|
+
"threadAlive", thread != null && thread.IsAlive, "stage", _rendererStage, "fault", _rendererFault,
|
|
589
|
+
"want", Want, "preparationSerial", Volatile.Read(ref _preparationSerial), "presentedSerial", Volatile.Read(ref _presentedPreparation),
|
|
590
|
+
"completedSerial", Volatile.Read(ref _completedPreparation),
|
|
591
|
+
"loopAgeMs", Volatile.Read(ref _frames) == 0 ? (object)null : unchecked((uint)(now - _lastLoopTick)),
|
|
592
|
+
"presentAttempts", Volatile.Read(ref _presentAttempts), "lastAttemptWant", Volatile.Read(ref _lastAttemptWant),
|
|
593
|
+
"lastAttemptSerial", Volatile.Read(ref _lastAttemptSerial), "presentSuccesses", Volatile.Read(ref _presentSuccesses),
|
|
594
|
+
"lastAttemptedPreparationSerial", Volatile.Read(ref _lastAttemptedPreparationSerial),
|
|
595
|
+
"lastSuccessAgeMs", Volatile.Read(ref _presentSuccesses) == 0 ? (object)null : unchecked((uint)(now - _lastPresentTick)),
|
|
596
|
+
"lastFailedSerial", Volatile.Read(ref _lastFailedSerial), "lastFailedWant", Volatile.Read(ref _lastFailedWant),
|
|
597
|
+
"lastWin32Error", Volatile.Read(ref _lastFailedSerial) < 0 ? (object)null : _lastFailedError,
|
|
598
|
+
"rebuiltSerial", Volatile.Read(ref _rebuiltSerial));
|
|
599
|
+
}
|
|
600
|
+
public const int PreparationMs = 1200; // human spec 2026-11-04: shortened from 1500 after review
|
|
481
601
|
static volatile bool _enabled = true;
|
|
482
602
|
static volatile string _label = "AI 正在操控此电脑 · 监听中";
|
|
483
603
|
|
|
484
|
-
///
|
|
604
|
+
/// Legacy indicator requests announce monitoring; only real input can paint deep blue.
|
|
485
605
|
public static void Touch(int holdMs)
|
|
486
606
|
{
|
|
487
607
|
if (!_enabled) return;
|
|
488
|
-
|
|
489
|
-
_lastTouchMs = Environment.TickCount;
|
|
608
|
+
_thinking = true;
|
|
490
609
|
EnsureStarted();
|
|
491
610
|
}
|
|
492
|
-
public static void Off() {
|
|
611
|
+
public static void Off() { _acting = false; _preparing = false; _thinking = false; }
|
|
612
|
+
public static bool Acting { get { return _acting; } }
|
|
613
|
+
public static bool Preparing { get { return _preparing; } }
|
|
614
|
+
public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
|
|
615
|
+
public static bool PreparationComplete { get { return _preparing && Volatile.Read(ref _completedPreparation) == Volatile.Read(ref _preparationSerial); } }
|
|
616
|
+
public static void ActuationBegin() { _acting = true; EnsureStarted(); }
|
|
617
|
+
public static void ActuationEnd() { _acting = false; }
|
|
618
|
+
public static void Prepare(bool on)
|
|
619
|
+
{
|
|
620
|
+
if (!on) { _preparing = false; return; }
|
|
621
|
+
_preparationStart = 0;
|
|
622
|
+
Interlocked.Increment(ref _preparationSerial);
|
|
623
|
+
_preparing = true; _enabled = true; EnsureStarted();
|
|
624
|
+
}
|
|
493
625
|
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// deep-blue ACTING border had already expired at its fixed 1200 ms hold WHILE SendInput steps
|
|
497
|
-
// were still being injected — pixels went blue at 1737 ms, then cyan/green at 2153/3438 ms with
|
|
498
|
-
// moveStillPending:true on all eight frames. Nothing here lengthens the hold: the hold is
|
|
499
|
-
// still 1200 ms. What changes is WHERE IT IS ANCHORED (the last real injection, not the moment
|
|
500
|
-
// the op began) and that an injection SEQUENCE in flight is a state the light can read.
|
|
626
|
+
// Nested injection sequences stay observable across drag/key/typing gaps.
|
|
627
|
+
// The enclosing actual action scope controls ownership and the deep-blue cue.
|
|
501
628
|
static int _injecting = 0; // refcount; Interlocked supplies the barriers
|
|
502
629
|
public static void InputBegin() { Interlocked.Increment(ref _injecting); }
|
|
503
630
|
public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
|
|
504
631
|
public static bool Injecting { get { return _injecting > 0; } }
|
|
505
|
-
|
|
506
|
-
/// SendInput goes through, so a multi-step op (type/drag/scroll/key/animated move) stays blue
|
|
507
|
-
/// for as long as its events keep coming, and still fades on the same short tail once they
|
|
508
|
-
/// stop. The old behaviour anchored on the op's START, so any op longer than the hold went
|
|
509
|
-
/// cyan mid-flight — the "not a long think, the input is still running" report.
|
|
510
|
-
public static void KeepActing() { _lastTouchMs = Environment.TickCount; }
|
|
632
|
+
public static void KeepActing() { /* Actual operation scope owns the cue, with no trailing hold. */ }
|
|
511
633
|
|
|
512
634
|
// Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
|
|
513
635
|
// during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
|
|
@@ -515,7 +637,7 @@ namespace DshComputerUse.Worker
|
|
|
515
637
|
static volatile bool _killNow = false;
|
|
516
638
|
public static void Kill()
|
|
517
639
|
{
|
|
518
|
-
|
|
640
|
+
_acting = false; _preparing = false; _thinking = false; _flashStart = 0;
|
|
519
641
|
_killNow = true;
|
|
520
642
|
}
|
|
521
643
|
|
|
@@ -567,7 +689,7 @@ namespace DshComputerUse.Worker
|
|
|
567
689
|
public static int DeriveWant(bool flashOn, int committed)
|
|
568
690
|
{
|
|
569
691
|
if (Panic.Exited) return 0;
|
|
570
|
-
return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (flashOn ? 2 : committed)));
|
|
692
|
+
return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting ? 1 : (flashOn ? 2 : committed)))));
|
|
571
693
|
}
|
|
572
694
|
/// The title states the same fact as the light, derived the same way (an ended session must
|
|
573
695
|
/// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
|
|
@@ -575,7 +697,7 @@ namespace DshComputerUse.Worker
|
|
|
575
697
|
{
|
|
576
698
|
if (Panic.Exited) return "AI 已停止操控 · 未监听";
|
|
577
699
|
return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
|
|
578
|
-
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : label);
|
|
700
|
+
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · 1.2 秒准备" : label);
|
|
579
701
|
}
|
|
580
702
|
static volatile string _shownTitle = null;
|
|
581
703
|
// What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
|
|
@@ -589,14 +711,9 @@ namespace DshComputerUse.Worker
|
|
|
589
711
|
static extern bool SetWindowText(IntPtr h, string text);
|
|
590
712
|
public static void Stopped(bool on)
|
|
591
713
|
{
|
|
714
|
+
// Only the window owner sets the title (in Run). A physical brake must
|
|
715
|
+
// never wait for WM_SETTEXT to be serviced by the overlay thread.
|
|
592
716
|
_stoppedMode = on;
|
|
593
|
-
// The overlay's WINDOW TITLE is part of the text channel: renaming it means any
|
|
594
|
-
// window list — the agent's own computer_windows, or a human's Task Manager —
|
|
595
|
-
// states plainly that the session is stopped, with no screenshot required.
|
|
596
|
-
if (_hwnd != IntPtr.Zero)
|
|
597
|
-
{
|
|
598
|
-
try { SetWindowText(_hwnd, on ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : _label); } catch { }
|
|
599
|
-
}
|
|
600
717
|
}
|
|
601
718
|
|
|
602
719
|
/// Mark "the agent just captured the screen": a yellow border flashes around the
|
|
@@ -663,37 +780,90 @@ namespace DshComputerUse.Worker
|
|
|
663
780
|
|
|
664
781
|
static void ThreadProc()
|
|
665
782
|
{
|
|
666
|
-
try { Run(); }
|
|
783
|
+
try { Run(); }
|
|
784
|
+
catch (Exception ex)
|
|
785
|
+
{
|
|
786
|
+
_rendererFault = _rendererStage + ": " + ex.Message;
|
|
787
|
+
Program.AuditRecord("GLOW-RENDER-ERROR", _rendererFault);
|
|
788
|
+
}
|
|
789
|
+
finally
|
|
790
|
+
{
|
|
791
|
+
_rendererStage = "stopped";
|
|
792
|
+
lock (typeof(Glow)) { _thread = null; }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
static void PumpOverlayMessages(IntPtr hwnd)
|
|
797
|
+
{
|
|
798
|
+
_rendererStage = "message-pump";
|
|
799
|
+
const uint WM_PAINT = 0x000F;
|
|
800
|
+
Native.NativeMsg msg;
|
|
801
|
+
for (int i = 0; i < 32 && PeekMessage(out msg, hwnd, 0, 0, PM_REMOVE); i++)
|
|
802
|
+
{
|
|
803
|
+
// UpdateLayeredWindow provides all pixels. WM_PAINT stays pending
|
|
804
|
+
// until its update region is validated, even with PM_REMOVE.
|
|
805
|
+
if (msg.message == WM_PAINT) ValidateRect(msg.hwnd, IntPtr.Zero);
|
|
806
|
+
else Native.DispatchMessage(ref msg);
|
|
807
|
+
}
|
|
667
808
|
}
|
|
668
809
|
|
|
669
810
|
static void Run()
|
|
670
811
|
{
|
|
812
|
+
while (RenderOverlay()) { } // Rebuild at most once per unpresented preparation.
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
static bool RenderOverlay()
|
|
816
|
+
{
|
|
817
|
+
_rendererFault = null;
|
|
818
|
+
_rendererStage = "display-metrics";
|
|
671
819
|
int vx = GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
|
|
672
820
|
int vy = GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
|
|
673
821
|
int vw = GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
|
|
674
822
|
int vh = GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
|
|
675
|
-
if (vw <= 0 || vh <= 0)
|
|
823
|
+
if (vw <= 0 || vh <= 0) throw new InvalidOperationException("empty virtual desktop");
|
|
676
824
|
|
|
825
|
+
_rendererStage = "create-window";
|
|
677
826
|
IntPtr hwnd = CreateWindowEx(
|
|
678
827
|
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST,
|
|
679
828
|
"STATIC", _label, WS_POPUP, vx, vy, vw, vh, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
|
680
|
-
if (hwnd == IntPtr.Zero)
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
829
|
+
if (hwnd == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
830
|
+
IntPtr screenDc = IntPtr.Zero, memDc = IntPtr.Zero, dib = IntPtr.Zero, oldBmp = IntPtr.Zero;
|
|
831
|
+
try
|
|
832
|
+
{
|
|
833
|
+
TryExcludeOverlayFromCapture(hwnd);
|
|
834
|
+
_hwnd = hwnd;
|
|
835
|
+
_rendererStage = "create-surface";
|
|
836
|
+
screenDc = GetDC(IntPtr.Zero);
|
|
837
|
+
if (screenDc == IntPtr.Zero) throw new InvalidOperationException("GetDC failed");
|
|
838
|
+
memDc = CreateCompatibleDC(screenDc);
|
|
839
|
+
if (memDc == IntPtr.Zero) throw new InvalidOperationException("CreateCompatibleDC failed");
|
|
840
|
+
Native.BITMAPINFO bmi = new Native.BITMAPINFO();
|
|
841
|
+
bmi.bmiHeader.biSize = (uint)Marshal.SizeOf(typeof(Native.BITMAPINFOHEADER));
|
|
842
|
+
bmi.bmiHeader.biWidth = vw;
|
|
843
|
+
bmi.bmiHeader.biHeight = -vh; // top-down
|
|
844
|
+
bmi.bmiHeader.biPlanes = 1;
|
|
845
|
+
bmi.bmiHeader.biBitCount = 32;
|
|
846
|
+
IntPtr bits;
|
|
847
|
+
dib = CreateDIBSection(screenDc, ref bmi, 0, out bits, IntPtr.Zero, 0);
|
|
848
|
+
if (dib == IntPtr.Zero) throw new InvalidOperationException("CreateDIBSection failed");
|
|
849
|
+
oldBmp = SelectObject(memDc, dib);
|
|
850
|
+
if (oldBmp == IntPtr.Zero || oldBmp == new IntPtr(-1)) throw new InvalidOperationException("SelectObject failed");
|
|
851
|
+
_rendererStage = "rendering";
|
|
852
|
+
return RenderFrames(hwnd, screenDc, memDc, bits, vx, vy, vw, vh);
|
|
853
|
+
}
|
|
854
|
+
finally
|
|
855
|
+
{
|
|
856
|
+
_hwnd = IntPtr.Zero; _shownTitle = null;
|
|
857
|
+
if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) SelectObject(memDc, oldBmp);
|
|
858
|
+
if (dib != IntPtr.Zero) DeleteObject(dib);
|
|
859
|
+
if (memDc != IntPtr.Zero) DeleteDC(memDc);
|
|
860
|
+
if (screenDc != IntPtr.Zero) ReleaseDC(IntPtr.Zero, screenDc);
|
|
861
|
+
DestroyWindow(hwnd);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
696
864
|
|
|
865
|
+
static bool RenderFrames(IntPtr hwnd, IntPtr screenDc, IntPtr memDc, IntPtr bits, int vx, int vy, int vw, int vh)
|
|
866
|
+
{
|
|
697
867
|
POINTL dst = new POINTL(); dst.x = vx; dst.y = vy;
|
|
698
868
|
POINTL src = new POINTL(); src.x = 0; src.y = 0;
|
|
699
869
|
SIZEL size = new SIZEL(); size.cx = vw; size.cy = vh;
|
|
@@ -701,48 +871,32 @@ namespace DshComputerUse.Worker
|
|
|
701
871
|
// ---- state machine with dwell hysteresis (no strobing) ----
|
|
702
872
|
int curBitmap = 0; // 0 = none, 1 = blue border, 2 = yellow flash
|
|
703
873
|
int paintedFlash = 0;
|
|
874
|
+
int lastPreparationPaint = 0;
|
|
704
875
|
int alpha = 0;
|
|
705
876
|
bool visible = false;
|
|
706
|
-
int committed = 0;
|
|
707
|
-
int pending = -1;
|
|
708
|
-
int pendingSince = 0;
|
|
877
|
+
int committed = 0;
|
|
709
878
|
while (true)
|
|
710
879
|
{
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
880
|
+
_lastLoopTick = Environment.TickCount;
|
|
881
|
+
_rendererStage = "rendering";
|
|
882
|
+
Interlocked.Increment(ref _frames);
|
|
883
|
+
// Ownership and deep blue use the same actual action scope. There is no
|
|
884
|
+
// delayed onset or lingering tail during reading, thinking or preparation.
|
|
885
|
+
bool acting = _enabled && Acting;
|
|
715
886
|
bool thinking = ThinkingActive;
|
|
716
887
|
bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
|
|
717
888
|
|
|
718
|
-
|
|
719
|
-
// The flash is an OVERRIDE, never a state that lingers.
|
|
720
|
-
//
|
|
721
|
-
// 2026-09-13: the flash used to write `committed = 2`, so once it ended the dwell
|
|
722
|
-
// hysteresis (600 ms) held the yellow bitmap on screen — with the border snapped to
|
|
723
|
-
// full alpha that read as "the yellow frame is permanently lit", and the human lost
|
|
724
|
-
// the ability to tell WHICH step took a screenshot. Now yellow shows only while the
|
|
725
|
-
// flash is actually running (<=400 ms) and the border drops straight back to deep
|
|
726
|
-
// blue when acting, or pale breathing when thinking.
|
|
727
|
-
if (!flashOn)
|
|
728
|
-
{
|
|
729
|
-
if (live != committed)
|
|
730
|
-
{
|
|
731
|
-
// require a state to hold for 600 ms before switching: kills strobe
|
|
732
|
-
if (pending != live) { pending = live; pendingSince = Environment.TickCount; }
|
|
733
|
-
else if (unchecked(Environment.TickCount - pendingSince) >= 600) { committed = live; pending = -1; }
|
|
734
|
-
}
|
|
735
|
-
else { pending = -1; }
|
|
736
|
-
}
|
|
889
|
+
committed = acting ? 1 : (thinking ? 3 : 0);
|
|
737
890
|
|
|
738
891
|
// ONE derivation (Glow.DeriveWant): the exit flag outranks ask/stop/flash/committed,
|
|
739
892
|
// so no later state change on another thread can paint a light on an ended session.
|
|
893
|
+
int preparationSerial = Volatile.Read(ref _preparationSerial);
|
|
740
894
|
int want = DeriveWant(flashOn, committed);
|
|
741
895
|
_want = want; // publish it: the state the light is showing, as a fact
|
|
742
896
|
|
|
743
897
|
// Keep the overlay's WINDOW TITLE in sync with the state, from the render loop.
|
|
744
|
-
//
|
|
745
|
-
//
|
|
898
|
+
// When the title was only written by Stopped(), a process that INHERITED a stop
|
|
899
|
+
// before this thread created the window (SyncFromFile at startup)
|
|
746
900
|
// showed a red border with the title "AI 正在操控此电脑". The title is the text
|
|
747
901
|
// channel the agent and the human both read, so a lying title is worse than none.
|
|
748
902
|
// It is derived from the same flags (Glow.DeriveTitle) and a hidden window still has
|
|
@@ -760,14 +914,14 @@ namespace DshComputerUse.Worker
|
|
|
760
914
|
_killNow = false;
|
|
761
915
|
alpha = 0;
|
|
762
916
|
if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
|
|
763
|
-
curBitmap = 0; committed = 0;
|
|
764
|
-
|
|
917
|
+
curBitmap = 0; committed = 0; paintedFlash = 0;
|
|
918
|
+
PumpOverlayMessages(hwnd);
|
|
765
919
|
Thread.Sleep(12);
|
|
766
920
|
continue;
|
|
767
921
|
}
|
|
768
922
|
// want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
|
|
769
923
|
// 4 emergency-stopped (RED, steady — the session is dead until re-armed)
|
|
770
|
-
int needBitmap = want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
924
|
+
int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
771
925
|
// A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
|
|
772
926
|
//
|
|
773
927
|
// The present below used to be gated on `diff != 0` alone, while the flash snaps
|
|
@@ -778,7 +932,15 @@ namespace DshComputerUse.Worker
|
|
|
778
932
|
// RED frame when the border happened to be at full alpha already — whether the
|
|
779
933
|
// brake was visible was luck. Present when EITHER the bitmap or the alpha changed.
|
|
780
934
|
bool repaint = false;
|
|
781
|
-
|
|
935
|
+
int preparationElapsed = -1;
|
|
936
|
+
if (want == 7 && (curBitmap != 6 || !PreparationVisible || unchecked(Environment.TickCount - lastPreparationPaint) >= 50))
|
|
937
|
+
{
|
|
938
|
+
lastPreparationPaint = Environment.TickCount;
|
|
939
|
+
preparationElapsed = PreparationVisible ? unchecked(lastPreparationPaint - _preparationStart) : 0;
|
|
940
|
+
PaintPreparation(bits, vw, vh, preparationElapsed);
|
|
941
|
+
curBitmap = 6; repaint = true;
|
|
942
|
+
}
|
|
943
|
+
else if (needBitmap == 2 && paintedFlash != _flashStart) { PaintFlash(bits, vw, vh); paintedFlash = _flashStart; curBitmap = 2; alpha = 204; repaint = true; /* snap ON at 80%: a flash is an event, not a fade */ }
|
|
782
944
|
else if (needBitmap == 5 && curBitmap != 5) { PaintHumanYield(bits, vw, vh); curBitmap = 5; repaint = true; }
|
|
783
945
|
else if (needBitmap == 4 && curBitmap != 4) { PaintStop(bits, vw, vh); curBitmap = 4; repaint = true; }
|
|
784
946
|
else if (needBitmap == 3 && curBitmap != 3) { PaintBlue(bits, vw, vh, true); curBitmap = 3; repaint = true; }
|
|
@@ -790,7 +952,7 @@ namespace DshComputerUse.Worker
|
|
|
790
952
|
int target;
|
|
791
953
|
bool flashFrame = false;
|
|
792
954
|
bool breathFrame = false; // breathing states are ABSOLUTE, never eased
|
|
793
|
-
if (want == 0) target = 0;
|
|
955
|
+
if (want == 0) { target = 0; breathFrame = true; }
|
|
794
956
|
else if (want == 2)
|
|
795
957
|
{
|
|
796
958
|
// The capture cue runs its OWN envelope (wind-up, burst, short fade): an absolute
|
|
@@ -798,7 +960,8 @@ namespace DshComputerUse.Worker
|
|
|
798
960
|
target = FlashAlpha(unchecked(Environment.TickCount - _flashStart));
|
|
799
961
|
flashFrame = true;
|
|
800
962
|
}
|
|
801
|
-
else if (want == 1) target = FULL;
|
|
963
|
+
else if (want == 1) { target = FULL; breathFrame = true; }
|
|
964
|
+
else if (want == 7) { target = 255; breathFrame = true; }
|
|
802
965
|
else if (want == 4) target = FULL; // steady red, no breathing
|
|
803
966
|
else if (want == 6)
|
|
804
967
|
{
|
|
@@ -808,22 +971,23 @@ namespace DshComputerUse.Worker
|
|
|
808
971
|
else if (want == 5)
|
|
809
972
|
{
|
|
810
973
|
// ASK: red like a stop, but at TWICE the cyan breath rate (1250 ms vs 2500 ms)
|
|
811
|
-
// and
|
|
974
|
+
// and a full swing — so the pulse is unmistakable at a glance. Human spec
|
|
975
|
+
// 2026-11-04, raised the same day after review: the first floor (25) still
|
|
976
|
+
// read as too dim at the trough; the whole band moved up to 50..140.
|
|
812
977
|
double askPhase = (Environment.TickCount % 1250) / 1250.0;
|
|
813
|
-
target =
|
|
978
|
+
target = 95 + (int)(Math.Sin(askPhase * 2 * Math.PI) * 45);
|
|
814
979
|
breathFrame = true;
|
|
815
980
|
}
|
|
816
981
|
else
|
|
817
982
|
{
|
|
818
983
|
// Faint blue "thinking", breathing so a still screen still shows life.
|
|
819
984
|
// 2.5 s cycle = 20% faster than the original 3.0 s.
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
// point: the ring INHALES and goes dark, then comes back.
|
|
985
|
+
// Human spec 2026-11-04, raised the same day after review: the first floor
|
|
986
|
+
// (25) still read as "going dark" at the trough; the whole band moved up to
|
|
987
|
+
// 50..140 — the halo stays clearly visible at its dimmest while the breathing
|
|
988
|
+
// contrast (swing 90) is preserved.
|
|
825
989
|
double phase = (Environment.TickCount % 2500) / 2500.0;
|
|
826
|
-
target =
|
|
990
|
+
target = 95 + (int)(Math.Sin(phase * 2 * Math.PI) * 45);
|
|
827
991
|
breathFrame = true;
|
|
828
992
|
}
|
|
829
993
|
|
|
@@ -860,156 +1024,225 @@ namespace DshComputerUse.Worker
|
|
|
860
1024
|
SetWindowPos(hwnd, new IntPtr(-1), vx, vy, vw, vh, 0x0004 | 0x0010 | 0x0040);
|
|
861
1025
|
visible = true;
|
|
862
1026
|
}
|
|
863
|
-
|
|
1027
|
+
_rendererStage = "presenting";
|
|
1028
|
+
Volatile.Write(ref _lastAttemptWant, want);
|
|
1029
|
+
Volatile.Write(ref _lastAttemptSerial, preparationSerial);
|
|
1030
|
+
if (want == 7) Volatile.Write(ref _lastAttemptedPreparationSerial, preparationSerial);
|
|
1031
|
+
Interlocked.Increment(ref _presentAttempts);
|
|
1032
|
+
bool presented = UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA);
|
|
1033
|
+
int nativeError = presented ? 0 : Marshal.GetLastWin32Error();
|
|
1034
|
+
if (presented)
|
|
1035
|
+
{
|
|
1036
|
+
_lastPresentTick = Environment.TickCount;
|
|
1037
|
+
Interlocked.Increment(ref _presentSuccesses);
|
|
1038
|
+
}
|
|
1039
|
+
else
|
|
1040
|
+
{
|
|
1041
|
+
bool firstFailure = Volatile.Read(ref _lastFailedSerial) != preparationSerial;
|
|
1042
|
+
_lastFailedError = nativeError;
|
|
1043
|
+
Volatile.Write(ref _lastFailedWant, want);
|
|
1044
|
+
Volatile.Write(ref _lastFailedSerial, preparationSerial);
|
|
1045
|
+
if (firstFailure) Program.AuditRecord("GLOW-PRESENT-FAILED", "serial=" + preparationSerial + "; want=" + want + "; win32=" + nativeError);
|
|
1046
|
+
if (want == 7 && !PreparationVisible && Volatile.Read(ref _rebuiltSerial) != preparationSerial)
|
|
1047
|
+
{
|
|
1048
|
+
Volatile.Write(ref _rebuiltSerial, preparationSerial);
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (presented && want == 7 && !PreparationVisible)
|
|
1053
|
+
{
|
|
1054
|
+
_preparationStart = Environment.TickCount;
|
|
1055
|
+
Volatile.Write(ref _presentedPreparation, preparationSerial);
|
|
1056
|
+
}
|
|
1057
|
+
if (presented && want == 7 && preparationElapsed >= PreparationMs)
|
|
1058
|
+
Volatile.Write(ref _completedPreparation, preparationSerial);
|
|
864
1059
|
}
|
|
865
1060
|
else if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
|
|
866
1061
|
}
|
|
867
|
-
|
|
868
|
-
while (PeekMessage(out m, hwnd, 0, 0, PM_REMOVE)) { }
|
|
1062
|
+
PumpOverlayMessages(hwnd);
|
|
869
1063
|
Thread.Sleep(16);
|
|
870
1064
|
}
|
|
871
1065
|
}
|
|
872
1066
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1067
|
+
// Thin, square screen-edge rails leave the application itself completely clear.
|
|
1068
|
+
// Each pixel belongs to exactly one inset ring: corners meet without gaps or
|
|
1069
|
+
// overlapping hot spots. Human request 2026-11-04, doubled the same day after
|
|
1070
|
+
// review: the bloom must span 1.6 cm of physical screen for every colour, not
|
|
1071
|
+
// the old fixed eight pixels (which read as a hairline on a 2560x1600 panel at
|
|
1072
|
+
// 175%), so the ring count below is derived from the overlay window's own DPI:
|
|
1073
|
+
// ~60 rings at 96 dpi, ~106 at 168.
|
|
1074
|
+
static int EdgeRailThickness()
|
|
1075
|
+
{
|
|
1076
|
+
// Offscreen painting (visual fixtures) has no window; GetDpiForWindow then
|
|
1077
|
+
// fails and the 96-dpi fallback keeps the fixture geometry deterministic.
|
|
1078
|
+
uint dpi = 96;
|
|
1079
|
+
IntPtr window = _hwnd;
|
|
1080
|
+
if (window != IntPtr.Zero)
|
|
1081
|
+
{
|
|
1082
|
+
uint live = Native.GetDpiForWindow(window);
|
|
1083
|
+
if (live > 0) dpi = live;
|
|
1084
|
+
}
|
|
1085
|
+
int rings = (int)Math.Round(1.6 / 2.54 * dpi); // 1.6 cm in this panel's pixels
|
|
1086
|
+
return Math.Max(8, Math.Min(rings, 192));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// The fixed eight-ring ramp was {244,216,94,53,30,16,8,3}: two bright core
|
|
1090
|
+
// rings, then a falloff to near zero. Same profile here, stretched over the
|
|
1091
|
+
// DPI-derived width: ring 0-1 keep their core alpha, rings 2..n-1 decay
|
|
1092
|
+
// geometrically from 94 down to ~3 at the innermost ring.
|
|
1093
|
+
static int EdgeRailAlpha(int ring, int rings)
|
|
886
1094
|
{
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
int r = band + 10;
|
|
901
|
-
int peak = intense ? 240 : 168;
|
|
902
|
-
System.Drawing.Color col = intense
|
|
903
|
-
? System.Drawing.Color.FromArgb(255, 46, 132, 255) // deep, saturated blue
|
|
904
|
-
: System.Drawing.Color.FromArgb(255, 34, 226, 240); // cyan halo (thinking)
|
|
905
|
-
const int N = 60;
|
|
906
|
-
float step = (float)band / N;
|
|
907
|
-
for (int i = 0; i < N; i++)
|
|
1095
|
+
if (ring == 0) return 244;
|
|
1096
|
+
if (ring == 1) return 216;
|
|
1097
|
+
return (int)Math.Round(94 * Math.Pow(3.0 / 94.0, (double)(ring - 2) / (rings - 3)));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
static void PaintEdgeRail(System.Drawing.Graphics g, Rectangle bounds, Color tint)
|
|
1101
|
+
{
|
|
1102
|
+
int rings = EdgeRailThickness();
|
|
1103
|
+
var smoothing = g.SmoothingMode;
|
|
1104
|
+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
|
|
1105
|
+
try
|
|
1106
|
+
{
|
|
1107
|
+
for (int i = 0; i < rings; i++)
|
|
908
1108
|
{
|
|
909
|
-
|
|
910
|
-
int
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1109
|
+
int alpha = EdgeRailAlpha(i, rings);
|
|
1110
|
+
int x = bounds.Left + i, y = bounds.Top + i;
|
|
1111
|
+
int width = bounds.Width - 2 * i, height = bounds.Height - 2 * i;
|
|
1112
|
+
if (width <= 0 || height <= 0) break;
|
|
1113
|
+
Color start = Color.FromArgb(alpha, Math.Min(255, tint.R + 18), Math.Min(255, tint.G + 14), Math.Min(255, tint.B + 8));
|
|
1114
|
+
Color end = Color.FromArgb(alpha, tint.R, tint.G, tint.B);
|
|
1115
|
+
using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(
|
|
1116
|
+
new RectangleF(bounds.X, bounds.Y, Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)), start, end, 32f))
|
|
915
1117
|
{
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1118
|
+
g.FillRectangle(brush, x, y, width, 1);
|
|
1119
|
+
if (height > 1) g.FillRectangle(brush, x, y + height - 1, width, 1);
|
|
1120
|
+
if (height > 2)
|
|
1121
|
+
{
|
|
1122
|
+
g.FillRectangle(brush, x, y + 1, 1, height - 2);
|
|
1123
|
+
if (width > 1) g.FillRectangle(brush, x + width - 1, y + 1, 1, height - 2);
|
|
1124
|
+
}
|
|
920
1125
|
}
|
|
921
1126
|
}
|
|
922
|
-
|
|
1127
|
+
}
|
|
1128
|
+
finally { g.SmoothingMode = smoothing; }
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
static Rectangle OverlayDisplay(int w, int h)
|
|
1132
|
+
{
|
|
1133
|
+
var display = new Rectangle(0, 0, w, h);
|
|
1134
|
+
if (w == Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN) && h == Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN))
|
|
1135
|
+
{
|
|
1136
|
+
display = Screen.PrimaryScreen.Bounds;
|
|
1137
|
+
display.Offset(-Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), -Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN));
|
|
1138
|
+
}
|
|
1139
|
+
return display;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// WDA_EXCLUDEFROMCAPTURE must not be sent on older Windows, where its fallback
|
|
1143
|
+
// can black out the covered image. Unsupported APIs simply leave the overlay visible.
|
|
1144
|
+
static void TryExcludeOverlayFromCapture(IntPtr hwnd)
|
|
1145
|
+
{
|
|
1146
|
+
try
|
|
1147
|
+
{
|
|
1148
|
+
object build = Microsoft.Win32.Registry.GetValue(
|
|
1149
|
+
@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentBuildNumber", null);
|
|
1150
|
+
int number;
|
|
1151
|
+
if (int.TryParse(Convert.ToString(build), out number) && number >= 19041)
|
|
1152
|
+
SetWindowDisplayAffinity(hwnd, 0x00000011);
|
|
1153
|
+
}
|
|
1154
|
+
catch { /* display preference only; never changes control or capture behavior */ }
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
static void PaintBlue(IntPtr bits, int w, int h, bool intense)
|
|
1158
|
+
{
|
|
1159
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1160
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), intense
|
|
1161
|
+
? Color.FromArgb(49, 116, 248) : Color.FromArgb(36, 206, 216));
|
|
923
1162
|
});
|
|
924
1163
|
}
|
|
925
1164
|
|
|
926
|
-
/// Semi-transparent badge in the top-left corner. The human must be able to SEE that a
|
|
927
|
-
/// brake exists (and whether it is currently on) — a brake nobody can see feels exactly
|
|
928
|
-
/// like no brake at all.
|
|
929
1165
|
public static int HumanYieldAlpha(int tick)
|
|
930
|
-
{ return
|
|
1166
|
+
{ return 90 + (int)Math.Round(35 * (1 - Math.Cos((unchecked((uint)tick) % 2800) * 2 * Math.PI / 2800.0))); }
|
|
931
1167
|
static void PaintHumanYield(IntPtr bits, int w, int h)
|
|
932
1168
|
{
|
|
933
1169
|
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
float inset = (float)i * band / 48;
|
|
938
|
-
int alpha = (int)(175 * Math.Pow(1 - i / 48.0, 1.8));
|
|
939
|
-
using (var pen = new Pen(Color.FromArgb(alpha, 255, 231, 151), band / 48f + 1.2f))
|
|
940
|
-
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
941
|
-
{ AddRound(shape, inset, inset, w - 2 * inset, h - 2 * inset, Math.Max(1, band + 8 - inset)); g.DrawPath(pen, shape); }
|
|
942
|
-
}
|
|
943
|
-
PaintBadge(g, "已让出键鼠 · 静默 3 秒后继续 · Ctrl+Esc 暂停", 175, false);
|
|
1170
|
+
Color tint = Color.FromArgb(238, 190, 102);
|
|
1171
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
1172
|
+
PaintStatusChip(g, w, h, "已让出键鼠 · 静默 3 秒后接续", tint);
|
|
944
1173
|
});
|
|
945
1174
|
}
|
|
946
1175
|
|
|
947
|
-
static void
|
|
1176
|
+
static void PaintPreparation(IntPtr bits, int w, int h, int elapsed)
|
|
948
1177
|
{
|
|
949
|
-
|
|
1178
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1179
|
+
Color tint = Color.FromArgb(166, 139, 250);
|
|
1180
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
1181
|
+
Rectangle display = OverlayDisplay(w, h);
|
|
1182
|
+
float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
|
|
1183
|
+
float bw = Math.Min(408 * scale, display.Width - 24), bh = 64 * scale;
|
|
1184
|
+
float x = display.Left + (display.Width - bw) / 2, y = display.Top + 10 * scale;
|
|
1185
|
+
float progress = Math.Max(0f, Math.Min(1f, elapsed / (float)PreparationMs));
|
|
1186
|
+
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
1187
|
+
using (var fill = new System.Drawing.Drawing2D.LinearGradientBrush(new RectangleF(x, y, bw, bh),
|
|
1188
|
+
Color.FromArgb(224, 23, 27, 39), Color.FromArgb(224, 35, 31, 53), 0f))
|
|
1189
|
+
using (var outline = new Pen(Color.FromArgb(110, tint), 1f))
|
|
1190
|
+
using (var title = new Font("Microsoft YaHei UI", 12.5f * scale, FontStyle.Bold))
|
|
1191
|
+
using (var detail = new Font("Microsoft YaHei UI", 9f * scale))
|
|
1192
|
+
using (var white = new SolidBrush(Color.FromArgb(255, 246, 247, 253)))
|
|
1193
|
+
using (var soft = new SolidBrush(Color.FromArgb(238, 183, 188, 207)))
|
|
1194
|
+
using (var ring = new Pen(Color.FromArgb(70, tint), 2 * scale))
|
|
1195
|
+
using (var accent = new Pen(Color.FromArgb(245, tint), 2 * scale))
|
|
1196
|
+
using (var track = new SolidBrush(Color.FromArgb(52, tint)))
|
|
1197
|
+
using (var bar = new System.Drawing.Drawing2D.LinearGradientBrush(
|
|
1198
|
+
new RectangleF(x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale),
|
|
1199
|
+
Color.FromArgb(220, 167, 139, 250), Color.FromArgb(235, 96, 165, 250), 0f))
|
|
1200
|
+
{
|
|
1201
|
+
AddRound(shape, x, y, bw, bh, 14 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
|
|
1202
|
+
g.DrawEllipse(ring, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale);
|
|
1203
|
+
if (progress > 0) g.DrawArc(accent, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale, -90, 360 * progress);
|
|
1204
|
+
g.DrawString("即将开始操作,请暂停键鼠", title, white, x + 48 * scale, y + 9 * scale);
|
|
1205
|
+
g.DrawString("Ctrl+Esc 暂停", detail, soft, x + 48 * scale, y + 34 * scale);
|
|
1206
|
+
string remaining = (Math.Max(0, PreparationMs - elapsed) / 1000.0).ToString("0.0") + " 秒后开始";
|
|
1207
|
+
SizeF size = g.MeasureString(remaining, detail);
|
|
1208
|
+
g.DrawString(remaining, detail, soft, x + bw - 16 * scale - size.Width, y + 34 * scale);
|
|
1209
|
+
g.FillRectangle(track, x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale);
|
|
1210
|
+
if (progress > 0) g.FillRectangle(bar, x + 48 * scale, y + 56 * scale, (bw - 64 * scale) * progress, 2 * scale);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
950
1213
|
}
|
|
951
1214
|
|
|
952
|
-
static void
|
|
1215
|
+
static void PaintStatusChip(System.Drawing.Graphics g, int w, int h, string text, Color tint)
|
|
953
1216
|
{
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
int o = Math.Max(40, Math.Min(255, opacity));
|
|
958
|
-
using (System.Drawing.Font f = new System.Drawing.Font("Microsoft YaHei", 11.25f, System.Drawing.FontStyle.Bold))
|
|
1217
|
+
Rectangle display = OverlayDisplay(w, h);
|
|
1218
|
+
float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
|
|
1219
|
+
using (var font = new Font("Microsoft YaHei UI", 9f * scale))
|
|
959
1220
|
{
|
|
960
|
-
|
|
961
|
-
float bw =
|
|
962
|
-
float x =
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
g.
|
|
972
|
-
|
|
973
|
-
g.DrawPath(pen, p);
|
|
974
|
-
p.Dispose();
|
|
975
|
-
using (System.Drawing.SolidBrush tb = new System.Drawing.SolidBrush(
|
|
976
|
-
red ? System.Drawing.Color.FromArgb((int)(o * 0.70), 255, 226, 226)
|
|
977
|
-
: System.Drawing.Color.FromArgb((int)(o * 0.62), 236, 245, 255)))
|
|
978
|
-
g.DrawString(txt, f, tb, x + 12f, y + 6f);
|
|
1221
|
+
SizeF measured = g.MeasureString(text, font);
|
|
1222
|
+
float bw = Math.Min(measured.Width + 38 * scale, display.Width - 24), bh = 28 * scale;
|
|
1223
|
+
float x = display.Left + (display.Width - bw) / 2, y = display.Top + 8 * scale;
|
|
1224
|
+
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
1225
|
+
using (var fill = new SolidBrush(Color.FromArgb(222, 22, 26, 36)))
|
|
1226
|
+
using (var outline = new Pen(Color.FromArgb(90, tint), 1f))
|
|
1227
|
+
using (var accent = new SolidBrush(Color.FromArgb(255, tint)))
|
|
1228
|
+
using (var ink = new SolidBrush(Color.FromArgb(250, 235, 239, 248)))
|
|
1229
|
+
{
|
|
1230
|
+
AddRound(shape, x, y, bw, bh, 12 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
|
|
1231
|
+
g.FillEllipse(accent, x + 12 * scale, y + 11 * scale, 6 * scale, 6 * scale);
|
|
1232
|
+
g.DrawString(text, font, ink, x + 24 * scale, y + (bh - measured.Height) / 2);
|
|
1233
|
+
}
|
|
979
1234
|
}
|
|
980
1235
|
}
|
|
981
1236
|
|
|
982
|
-
/// The stopped state: a RED border plus a red badge, steady (no breathing) — the
|
|
983
|
-
/// computer-use session is dead until a human re-arms it.
|
|
984
1237
|
static void PaintStop(IntPtr bits, int w, int h)
|
|
985
1238
|
{
|
|
986
|
-
WithGraphics(bits, w, h, delegate
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const int N = 60;
|
|
991
|
-
float step = (float)band / N;
|
|
992
|
-
for (int i = 0; i < N; i++)
|
|
993
|
-
{
|
|
994
|
-
double t = (double)i / N;
|
|
995
|
-
int a = (int)Math.Round(248 * Math.Pow(1 - t, 2.0));
|
|
996
|
-
if (a <= 0) continue;
|
|
997
|
-
float inset = i * step;
|
|
998
|
-
using (System.Drawing.Pen pen = new System.Drawing.Pen(
|
|
999
|
-
System.Drawing.Color.FromArgb(a, 238, 66, 74), step + 1.2f))
|
|
1000
|
-
{
|
|
1001
|
-
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
1002
|
-
AddRound(path, inset, inset, w - inset * 2, h - inset * 2, Math.Max(1, r - inset));
|
|
1003
|
-
g.DrawPath(pen, path);
|
|
1004
|
-
path.Dispose();
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
PaintBadge(g, "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出", 252, true);
|
|
1239
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1240
|
+
Color tint = Color.FromArgb(240, 84, 102);
|
|
1241
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
1242
|
+
PaintStatusChip(g, w, h, "操作已暂停 · Ctrl+Alt+R 继续", tint);
|
|
1008
1243
|
});
|
|
1009
1244
|
}
|
|
1010
1245
|
|
|
1011
|
-
/// Yellow "a screenshot was just taken" rectangle around the captured region —
|
|
1012
|
-
/// same stepped-gradient treatment, no strokes, no hard edges.
|
|
1013
1246
|
static void PaintFlash(IntPtr bits, int w, int h)
|
|
1014
1247
|
{
|
|
1015
1248
|
int fx = _fx, fy = _fy, fw = _fw, fh = _fh;
|
|
@@ -1019,32 +1252,8 @@ namespace DshComputerUse.Worker
|
|
|
1019
1252
|
if (fx + fw > w) fw = w - fx;
|
|
1020
1253
|
if (fy + fh > h) fh = h - fy;
|
|
1021
1254
|
if (fw < 8 || fh < 8) { fx = 0; fy = 0; fw = w; fh = h; }
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
{
|
|
1025
|
-
// 2026-09-12 (human): the capture cue on its own, 40% narrower again — 32 -> 19.
|
|
1026
|
-
// The corner radius scales with it (13 -> 8) so the strip still reads as a thin
|
|
1027
|
-
// glow instead of a fat rounded rectangle. Width only: the alpha profile is
|
|
1028
|
-
// untouched, so the cue keeps its brightness.
|
|
1029
|
-
int band = 19;
|
|
1030
|
-
int r = 8;
|
|
1031
|
-
const int N = 40;
|
|
1032
|
-
float step = (float)band / N;
|
|
1033
|
-
for (int i = 0; i < N; i++)
|
|
1034
|
-
{
|
|
1035
|
-
double t = (double)i / N;
|
|
1036
|
-
int a = (int)Math.Round(200 * Math.Pow(1 - t, 1.9));
|
|
1037
|
-
if (a <= 0) continue;
|
|
1038
|
-
float inset = i * step;
|
|
1039
|
-
using (System.Drawing.Pen pen = new System.Drawing.Pen(
|
|
1040
|
-
System.Drawing.Color.FromArgb(a, 255, 186, 20), step + 1.2f))
|
|
1041
|
-
{
|
|
1042
|
-
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
1043
|
-
AddRound(path, fx + inset, fy + inset, fw - inset * 2, fh - inset * 2, Math.Max(1, r - inset));
|
|
1044
|
-
g.DrawPath(pen, path);
|
|
1045
|
-
path.Dispose();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1255
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1256
|
+
PaintEdgeRail(g, new Rectangle(fx, fy, fw, fh), Color.FromArgb(247, 193, 62));
|
|
1048
1257
|
});
|
|
1049
1258
|
}
|
|
1050
1259
|
|
|
@@ -1056,6 +1265,7 @@ namespace DshComputerUse.Worker
|
|
|
1056
1265
|
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
|
|
1057
1266
|
{
|
|
1058
1267
|
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
1268
|
+
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
|
|
1059
1269
|
g.Clear(System.Drawing.Color.Transparent);
|
|
1060
1270
|
draw(g);
|
|
1061
1271
|
}
|
|
@@ -1232,10 +1442,12 @@ namespace DshComputerUse.Worker
|
|
|
1232
1442
|
// separate statements, so a Q between the check and the write re-lit a dead cycle.
|
|
1233
1443
|
lock (SyncLock)
|
|
1234
1444
|
{
|
|
1445
|
+
Program.InvalidateActiveInput();
|
|
1235
1446
|
_temporary = null;
|
|
1236
1447
|
_engaged = false;
|
|
1237
1448
|
_stopped = false;
|
|
1238
1449
|
CycleLit = false; // R2: the cycle is dead and must not be revived by anyone
|
|
1450
|
+
_controlsReleased = true;
|
|
1239
1451
|
Disarm(); // ... and its monitors die WITH it, not at the next turn
|
|
1240
1452
|
try { Glow.Kill(); } catch { }
|
|
1241
1453
|
try { Glow.Ask(false); } catch { }
|
|
@@ -1258,6 +1470,7 @@ namespace DshComputerUse.Worker
|
|
|
1258
1470
|
{
|
|
1259
1471
|
if (_exited) return false;
|
|
1260
1472
|
if (!CycleLit) Program.ResetKickoffFocus();
|
|
1473
|
+
_controlsReleased = false;
|
|
1261
1474
|
CycleLit = true; // an agent cycle is live: cyan is on (R1)
|
|
1262
1475
|
Arm(); // "Computer use 一旦启用,立即挂打断监听"
|
|
1263
1476
|
// A BRAKE OUTRANKS "THINKING" (cold-start bug, 2026-09-13): if a stop is already
|
|
@@ -1281,6 +1494,19 @@ namespace DshComputerUse.Worker
|
|
|
1281
1494
|
return true;
|
|
1282
1495
|
}
|
|
1283
1496
|
}
|
|
1497
|
+
public static bool TryEndCycle()
|
|
1498
|
+
{
|
|
1499
|
+
SyncExitedFromFile(); SyncFromFile();
|
|
1500
|
+
lock (SyncLock)
|
|
1501
|
+
{
|
|
1502
|
+
if (_engaged || _stopped || PauseRequested || ExitRequested) return false;
|
|
1503
|
+
_controlsReleased = true;
|
|
1504
|
+
CycleLit = false;
|
|
1505
|
+
Disarm(); Glow.Kill();
|
|
1506
|
+
Program.ReleaseOwnedInputs(); Program.ReleaseDesktopOwnership();
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1284
1510
|
|
|
1285
1511
|
/// Was the record written after this process started? Only then is it news rather than an
|
|
1286
1512
|
/// inherited fact. (The record carries its own `at=` line precisely so this is answerable
|
|
@@ -1355,7 +1581,7 @@ namespace DshComputerUse.Worker
|
|
|
1355
1581
|
}
|
|
1356
1582
|
// OUTSIDE the lock: Notify writes to stdout, and a full pipe must never hold the lock the
|
|
1357
1583
|
// exit and the cycle-opener share.
|
|
1358
|
-
if (adopted) ReleaseStuck();
|
|
1584
|
+
if (adopted) { ReleaseStuck(); Program.ReleaseDesktopOwnership(); }
|
|
1359
1585
|
if (announce) Notify("exit", ExitSummary());
|
|
1360
1586
|
}
|
|
1361
1587
|
|
|
@@ -1451,6 +1677,9 @@ namespace DshComputerUse.Worker
|
|
|
1451
1677
|
static IntPtr _hook = IntPtr.Zero;
|
|
1452
1678
|
static Native.LowLevelKeyboardProc _proc; // keep alive: the GC must not collect it
|
|
1453
1679
|
static IntPtr _mouseHook = IntPtr.Zero;
|
|
1680
|
+
static readonly uint CurrentPid = (uint)Process.GetCurrentProcess().Id;
|
|
1681
|
+
static readonly ManualResetEvent _hooksInitialized = new ManualResetEvent(false);
|
|
1682
|
+
static volatile bool _hooksReady;
|
|
1454
1683
|
static Native.LowLevelMouseProc _mouseProc; // same: the GC must not collect it
|
|
1455
1684
|
static Thread _thread;
|
|
1456
1685
|
|
|
@@ -1720,9 +1949,14 @@ namespace DshComputerUse.Worker
|
|
|
1720
1949
|
_proc = HookCallback;
|
|
1721
1950
|
IntPtr hmod = Native.GetModuleHandle(null);
|
|
1722
1951
|
_hook = Native.SetWindowsHookEx(Native.WH_KEYBOARD_LL, _proc, hmod, 0);
|
|
1952
|
+
int keyboardError = _hook == IntPtr.Zero ? Marshal.GetLastWin32Error() : 0;
|
|
1723
1953
|
// Same thread, same message loop: a second low-level hook for the wheel.
|
|
1724
1954
|
_mouseProc = MouseHookCallback;
|
|
1725
1955
|
_mouseHook = Native.SetWindowsHookEx(Native.WH_MOUSE_LL, _mouseProc, hmod, 0);
|
|
1956
|
+
int mouseError = _mouseHook == IntPtr.Zero ? Marshal.GetLastWin32Error() : 0;
|
|
1957
|
+
_hooksReady = _hook != IntPtr.Zero && _mouseHook != IntPtr.Zero;
|
|
1958
|
+
_hooksInitialized.Set();
|
|
1959
|
+
if (!_hooksReady) Program.AuditRecord("CONTROL-HOOK-ERROR", "keyboard=" + keyboardError + "; mouse=" + mouseError);
|
|
1726
1960
|
Native.NativeMsg msg;
|
|
1727
1961
|
while (Native.GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
|
|
1728
1962
|
{
|
|
@@ -1730,13 +1964,33 @@ namespace DshComputerUse.Worker
|
|
|
1730
1964
|
Native.DispatchMessage(ref msg);
|
|
1731
1965
|
}
|
|
1732
1966
|
}
|
|
1733
|
-
catch
|
|
1967
|
+
catch (Exception ex) { Program.AuditRecord("CONTROL-HOOK-ERROR", ex.GetType().Name); }
|
|
1968
|
+
finally { _hooksReady = false; _hooksInitialized.Set(); }
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
public static void RequireControlHooks()
|
|
1972
|
+
{
|
|
1973
|
+
if (!_hooksInitialized.WaitOne(1500) || !_hooksReady)
|
|
1974
|
+
{
|
|
1975
|
+
var error = new Exception("CONTROL_HOOK_UNAVAILABLE: physical input monitoring did not start or has stopped; no input was dispatched.");
|
|
1976
|
+
error.Data["code"] = "CONTROL_HOOK_UNAVAILABLE";
|
|
1977
|
+
error.Data["outcome"] = "not-dispatched";
|
|
1978
|
+
throw error;
|
|
1979
|
+
}
|
|
1734
1980
|
}
|
|
1735
1981
|
|
|
1736
1982
|
// Hook callbacks only record ownership and enqueue control work. Serial execution
|
|
1737
1983
|
// preserves Ctrl+Esc / resume / exit ordering without blocking the OS hook on I/O.
|
|
1738
|
-
static int _pendingPauses;
|
|
1984
|
+
static int _pendingPauses, _pendingExits;
|
|
1739
1985
|
public static bool PauseRequested { get { return Volatile.Read(ref _pendingPauses) > 0; } }
|
|
1986
|
+
public static bool ExitRequested { get { return Volatile.Read(ref _pendingExits) > 0; } }
|
|
1987
|
+
public static bool FinishOneShotControls()
|
|
1988
|
+
{
|
|
1989
|
+
if (!PauseRequested && !ExitRequested) return true;
|
|
1990
|
+
// A one-shot process must give the recognised chord a chance to persist
|
|
1991
|
+
// STOP/EXITED before Main returns and its background threads disappear.
|
|
1992
|
+
return SpinWait.SpinUntil(delegate { return !PauseRequested && !ExitRequested; }, 5000);
|
|
1993
|
+
}
|
|
1740
1994
|
static readonly ConcurrentQueue<Action> ControlQueue = new ConcurrentQueue<Action>();
|
|
1741
1995
|
static int _controlWorkQueued;
|
|
1742
1996
|
internal static void QueueControl(Action action)
|
|
@@ -1751,6 +2005,31 @@ namespace DshComputerUse.Worker
|
|
|
1751
2005
|
} while (!ControlQueue.IsEmpty && Interlocked.CompareExchange(ref _controlWorkQueued, 1, 0) == 0);
|
|
1752
2006
|
});
|
|
1753
2007
|
}
|
|
2008
|
+
static void QueueDesktopControl(Action action, Action finished = null)
|
|
2009
|
+
{
|
|
2010
|
+
QueueControl(delegate {
|
|
2011
|
+
// Serialize with graceful release: a queued physical hotkey may not
|
|
2012
|
+
// reopen a completed cycle or steer an exit into its continuing chat.
|
|
2013
|
+
try
|
|
2014
|
+
{
|
|
2015
|
+
lock (SyncLock)
|
|
2016
|
+
{
|
|
2017
|
+
if (!ControlListenersEnabled || !Program.TryClaimDesktop("physical control"))
|
|
2018
|
+
{ Program.AuditRecord("CONTROL-SKIPPED", ControlListenersEnabled ? "desktop owned by another worker" : "cycle released"); return; }
|
|
2019
|
+
try { action(); }
|
|
2020
|
+
finally { if (!CycleLit) Program.ReleaseDesktopOwnership(); }
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
catch (Exception ex) { Program.AuditRecord("CONTROL-ERROR", ex.GetType().Name); }
|
|
2024
|
+
finally { if (finished != null) finished(); }
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
static void QueueExit(Action exit)
|
|
2029
|
+
{
|
|
2030
|
+
Interlocked.Increment(ref _pendingExits);
|
|
2031
|
+
QueueDesktopControl(exit, delegate { Interlocked.Decrement(ref _pendingExits); });
|
|
2032
|
+
}
|
|
1754
2033
|
public static string ControlHotkey(uint vk, bool down, bool human, bool ctrl, bool alt, bool shift, bool win, bool live)
|
|
1755
2034
|
{
|
|
1756
2035
|
if (!down || !human) return "";
|
|
@@ -1760,8 +2039,7 @@ namespace DshComputerUse.Worker
|
|
|
1760
2039
|
return "";
|
|
1761
2040
|
}
|
|
1762
2041
|
static bool _suppressPauseEsc;
|
|
1763
|
-
static
|
|
1764
|
-
static POINT _physicalPoint;
|
|
2042
|
+
static readonly PointerHandoffTolerance PointerMotion = new PointerHandoffTolerance();
|
|
1765
2043
|
public static bool PhysicalEvent(IntPtr extra, uint flags, uint injectedMask)
|
|
1766
2044
|
{ return extra != OwnMagic && (flags & injectedMask) == 0; }
|
|
1767
2045
|
|
|
@@ -1773,15 +2051,18 @@ namespace DshComputerUse.Worker
|
|
|
1773
2051
|
{
|
|
1774
2052
|
var m = (Native.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Native.MSLLHOOKSTRUCT));
|
|
1775
2053
|
if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
|
|
1776
|
-
|
|
2054
|
+
bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
|
|
2055
|
+
int message = wParam.ToInt32();
|
|
2056
|
+
// Significant idle motion invalidates observations, but does not yield.
|
|
2057
|
+
bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
|
|
2058
|
+
true, Handoff.Active, HumanClock.ElapsedMilliseconds);
|
|
2059
|
+
if (physical)
|
|
1777
2060
|
{
|
|
1778
|
-
int
|
|
1779
|
-
bool down = false, up = false, activity = true;
|
|
2061
|
+
int key = 0;
|
|
2062
|
+
bool down = false, up = false, activity = true, deliberate = false;
|
|
1780
2063
|
switch (message)
|
|
1781
2064
|
{
|
|
1782
|
-
case 0x200:
|
|
1783
|
-
activity = !_havePhysicalPoint || m.pt.X != _physicalPoint.X || m.pt.Y != _physicalPoint.Y;
|
|
1784
|
-
_physicalPoint = m.pt; _havePhysicalPoint = true; break;
|
|
2065
|
+
case 0x200: activity = movement; deliberate = movement; break;
|
|
1785
2066
|
case 0x201: key = 1; down = true; break;
|
|
1786
2067
|
case 0x202: key = 1; up = true; break;
|
|
1787
2068
|
case 0x204: key = 2; down = true; break;
|
|
@@ -1793,7 +2074,7 @@ namespace DshComputerUse.Worker
|
|
|
1793
2074
|
case 0x20A: case 0x20E: activity = unchecked((short)(m.mouseData >> 16)) != 0; break;
|
|
1794
2075
|
default: activity = false; break;
|
|
1795
2076
|
}
|
|
1796
|
-
if (activity) RecordHumanInput(key, down, up);
|
|
2077
|
+
if (activity) RecordHumanInput(key, down, up, deliberate);
|
|
1797
2078
|
}
|
|
1798
2079
|
}
|
|
1799
2080
|
}
|
|
@@ -1820,28 +2101,26 @@ namespace DshComputerUse.Worker
|
|
|
1820
2101
|
{ if (up) _suppressPauseEsc = false; return new IntPtr(1); }
|
|
1821
2102
|
bool ctrl = _humanCtrl && (Native.GetAsyncKeyState(Native.VK_CONTROL) & 0x8000) != 0;
|
|
1822
2103
|
bool alt = _humanAlt && (Native.GetAsyncKeyState(Native.VK_MENU) & 0x8000) != 0;
|
|
1823
|
-
string hotkey = ControlHotkey(k.vkCode, down, human, ctrl, alt,
|
|
1824
|
-
Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited);
|
|
2104
|
+
string hotkey = ControlListenersEnabled ? ControlHotkey(k.vkCode, down, human, ctrl, alt,
|
|
2105
|
+
Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited) : "";
|
|
1825
2106
|
if (hotkey == "pause")
|
|
1826
2107
|
{
|
|
1827
2108
|
_suppressPauseEsc = true;
|
|
1828
|
-
Interlocked.Increment(ref _pendingPauses);
|
|
1829
2109
|
// The hook marks ownership immediately; do not perform SendInput or
|
|
1830
2110
|
// filesystem work on this OS hook thread.
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
});
|
|
2111
|
+
var evidence = ChordEvidence(k, ctrl, alt);
|
|
2112
|
+
Interlocked.Increment(ref _pendingPauses);
|
|
2113
|
+
QueueDesktopControl(delegate { Engage("Ctrl+Esc chord detected", false, "hook", evidence()); },
|
|
2114
|
+
delegate { Interlocked.Decrement(ref _pendingPauses); });
|
|
1836
2115
|
return new IntPtr(1); // Ctrl+Esc must not also open the Windows Start menu.
|
|
1837
2116
|
}
|
|
1838
2117
|
if (hotkey == "exit")
|
|
1839
2118
|
{
|
|
1840
|
-
|
|
1841
|
-
|
|
2119
|
+
var evidence = ChordEvidence(k, ctrl, alt);
|
|
2120
|
+
QueueExit(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence()); });
|
|
1842
2121
|
}
|
|
1843
2122
|
else if (hotkey == "resume")
|
|
1844
|
-
|
|
2123
|
+
QueueDesktopControl(delegate { Clear(); });
|
|
1845
2124
|
}
|
|
1846
2125
|
}
|
|
1847
2126
|
}
|
|
@@ -1886,6 +2165,7 @@ namespace DshComputerUse.Worker
|
|
|
1886
2165
|
// SendInputChecked (which now refuses after an exit): releasing what is stuck is exactly
|
|
1887
2166
|
// the cleanup that must still be allowed.
|
|
1888
2167
|
ReleaseStuck();
|
|
2168
|
+
Program.ReleaseDesktopOwnership();
|
|
1889
2169
|
Program.AuditRecord("EXIT", _exitSource + "\t" + why + "\t" + _exitEvidence);
|
|
1890
2170
|
Notify("exit", ExitSummary());
|
|
1891
2171
|
}
|
|
@@ -1893,7 +2173,7 @@ namespace DshComputerUse.Worker
|
|
|
1893
2173
|
/// The chord as it was SEEN: the fields someone can re-check, including whose Ctrl/Alt the
|
|
1894
2174
|
/// hook recorded and which window was in front. Deliberately verbose — this line is the
|
|
1895
2175
|
/// difference between "the human pressed it" and "we cannot know who pressed it".
|
|
1896
|
-
static string ChordEvidence(Native.KBDLLHOOKSTRUCT k, bool ctrl, bool alt)
|
|
2176
|
+
static Func<string> ChordEvidence(Native.KBDLLHOOKSTRUCT k, bool ctrl, bool alt)
|
|
1897
2177
|
{
|
|
1898
2178
|
StringBuilder sb = new StringBuilder();
|
|
1899
2179
|
sb.Append("vk=0x").Append(k.vkCode.ToString("X2"));
|
|
@@ -1907,31 +2187,37 @@ namespace DshComputerUse.Worker
|
|
|
1907
2187
|
// later whether "the human pressed it" was ever measurable — and note that OURSELVES
|
|
1908
2188
|
// (extra=OUR_MAGIC) is a different, stronger answer than "some injector".
|
|
1909
2189
|
sb.Append(" injected=").Append((k.flags & Native.LLKHF_INJECTED) != 0 ? "1" : "0");
|
|
1910
|
-
|
|
2190
|
+
IntPtr fg = Native.GetForegroundWindow();
|
|
2191
|
+
uint fgPid;
|
|
2192
|
+
Native.GetWindowThreadProcessId(fg, out fgPid);
|
|
2193
|
+
sb.Append(" fgPid=").Append(fgPid);
|
|
2194
|
+
// Capture the foreground identity on the hook thread. Enrich it on the
|
|
2195
|
+
// control queue without sending a message to our own overlay window.
|
|
2196
|
+
return delegate
|
|
1911
2197
|
{
|
|
1912
|
-
|
|
1913
|
-
uint fgPid;
|
|
1914
|
-
Native.GetWindowThreadProcessId(fg, out fgPid);
|
|
1915
|
-
sb.Append(" fgPid=").Append(fgPid);
|
|
1916
|
-
try { sb.Append(" fg=").Append(Process.GetProcessById((int)fgPid).ProcessName); } catch { }
|
|
1917
|
-
StringBuilder t = new StringBuilder(256);
|
|
1918
|
-
if (Native.GetWindowText(fg, t, 256) > 0)
|
|
2198
|
+
try
|
|
1919
2199
|
{
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2200
|
+
using (var proc = Process.GetProcessById((int)fgPid)) sb.Append(" fg=").Append(proc.ProcessName);
|
|
2201
|
+
uint currentPid;
|
|
2202
|
+
Native.GetWindowThreadProcessId(fg, out currentPid);
|
|
2203
|
+
StringBuilder t = new StringBuilder(256);
|
|
2204
|
+
if (currentPid == fgPid && fgPid != CurrentPid && Native.GetWindowText(fg, t, 256) > 0)
|
|
2205
|
+
{
|
|
2206
|
+
string title = t.ToString();
|
|
2207
|
+
if (title.Length > 60) title = title.Substring(0, 60);
|
|
2208
|
+
sb.Append(" fgTitle=\"").Append(title.Replace("\t", " ")).Append('"');
|
|
2209
|
+
}
|
|
1923
2210
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2211
|
+
catch { }
|
|
2212
|
+
return sb.ToString();
|
|
2213
|
+
};
|
|
1927
2214
|
}
|
|
1928
2215
|
|
|
1929
2216
|
/// Polling fallback for the human's combos.
|
|
1930
2217
|
///
|
|
1931
2218
|
/// The low-level hook is the primary path, but it only reports what Windows hands it: a
|
|
1932
|
-
/// missed keydown would
|
|
1933
|
-
///
|
|
1934
|
-
/// missed when polled every 40 ms — so Q always exits and R always releases, hook or no hook.
|
|
2219
|
+
/// missed keydown would otherwise have no fallback. Poll the press bits as an additional
|
|
2220
|
+
/// witness for Q/R while the hook has confirmed human-held modifiers.
|
|
1935
2221
|
static void PollThread()
|
|
1936
2222
|
{
|
|
1937
2223
|
bool prevQ = false, prevR = false;
|
|
@@ -1963,21 +2249,19 @@ namespace DshComputerUse.Worker
|
|
|
1963
2249
|
bool r = (Native.GetAsyncKeyState(Native.VK_R) & 1) != 0;
|
|
1964
2250
|
bool agentQuiet = _agentActAt == 0 || unchecked(Environment.TickCount - _agentActAt) > 400;
|
|
1965
2251
|
if (warm < 3) warm++;
|
|
1966
|
-
else if (_humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
|
|
2252
|
+
else if (ControlListenersEnabled && _humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
|
|
1967
2253
|
{
|
|
1968
2254
|
// R FIRST: Ctrl+Alt+R is the human's explicit re-arm, and a stale Q bit must
|
|
1969
2255
|
// never beat it — pressing the key that brings computer use BACK used to be
|
|
1970
2256
|
// able to END it instead.
|
|
1971
|
-
if (r && !prevR)
|
|
2257
|
+
if (r && !prevR) QueueDesktopControl(delegate { Clear(); });
|
|
1972
2258
|
else if (q && !prevQ)
|
|
1973
2259
|
{
|
|
1974
2260
|
Native.KBDLLHOOKSTRUCT kk = new Native.KBDLLHOOKSTRUCT();
|
|
1975
2261
|
kk.vkCode = Native.VK_Q;
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
ChordEvidence(kk, asyncCtrl, asyncAlt);
|
|
1980
|
-
QueueControl(delegate { Exit("Ctrl+Alt+Q chord detected", "poll", evidence); });
|
|
2262
|
+
var evidence = ChordEvidence(kk, asyncCtrl, asyncAlt);
|
|
2263
|
+
QueueExit(delegate { Exit("Ctrl+Alt+Q chord detected", "poll",
|
|
2264
|
+
"q-latch + hook-tracked modifiers + measured async key state; " + evidence()); });
|
|
1981
2265
|
}
|
|
1982
2266
|
}
|
|
1983
2267
|
prevQ = q; prevR = r;
|
|
@@ -1993,6 +2277,8 @@ namespace DshComputerUse.Worker
|
|
|
1993
2277
|
// durable manual brake and never creates a STOP record.
|
|
1994
2278
|
static int _agentActAt;
|
|
1995
2279
|
static volatile bool _armed = false;
|
|
2280
|
+
static volatile bool _controlsReleased = true;
|
|
2281
|
+
public static bool ControlListenersEnabled { get { return !_controlsReleased; } }
|
|
1996
2282
|
public static volatile bool CycleLit;
|
|
1997
2283
|
public static bool Armed { get { return _armed; } }
|
|
1998
2284
|
public static bool Exited { get { return _exited; } }
|
|
@@ -2000,27 +2286,77 @@ namespace DshComputerUse.Worker
|
|
|
2000
2286
|
static readonly HumanInputState Handoff = new HumanInputState();
|
|
2001
2287
|
static readonly object HandoffPublishLock = new object();
|
|
2002
2288
|
static string _publishedHandoff = "idle";
|
|
2289
|
+
static long _physicalInputEpoch, _publishedInputEpoch;
|
|
2003
2290
|
static int _handoffWorkQueued;
|
|
2004
2291
|
public static bool HumanActive { get { return Handoff.Active; } }
|
|
2005
2292
|
public static long HumanRevision { get { return Handoff.Revision; } }
|
|
2006
2293
|
public static bool HumanHolds(int vk) { return Handoff.Held(vk); }
|
|
2007
|
-
public static
|
|
2294
|
+
public static long InputEpoch { get { return Interlocked.Read(ref _physicalInputEpoch); } }
|
|
2295
|
+
public static object CooperationState()
|
|
2296
|
+
{
|
|
2297
|
+
var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
|
|
2298
|
+
state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
|
|
2299
|
+
return state;
|
|
2300
|
+
}
|
|
2008
2301
|
static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
|
|
2302
|
+
// ---------- secure-desktop (UAC / lock screen) dispatch gate, human request 2026-11-04 ----------
|
|
2303
|
+
static readonly SecureDesktopWatch SecureWatch = new SecureDesktopWatch();
|
|
2304
|
+
public static bool SecureDesktopActive { get { return SecureWatch.Active; } }
|
|
2305
|
+
static bool InputDesktopReachable()
|
|
2306
|
+
{
|
|
2307
|
+
IntPtr desktop = Native.OpenInputDesktop(0, false, 0x0001); // DESKTOP_READOBJECT
|
|
2308
|
+
if (desktop != IntPtr.Zero) { Native.CloseDesktop(desktop); return true; }
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
/// Polled from the monitor thread. Audits and notifies TRANSITIONS only.
|
|
2312
|
+
static void PollSecureDesktop()
|
|
2313
|
+
{
|
|
2314
|
+
if (!SecureWatch.Observe(InputDesktopReachable())) return;
|
|
2315
|
+
bool active = SecureWatch.Active;
|
|
2316
|
+
Program.AuditRecord(active ? "SECURE-DESKTOP-ENGAGED" : "SECURE-DESKTOP-CLEARED",
|
|
2317
|
+
active
|
|
2318
|
+
? "input desktop unreachable (UAC prompt or lock screen); input dispatch paused until it returns"
|
|
2319
|
+
: "input desktop reachable again; dispatch may continue");
|
|
2320
|
+
Notify("secure", active ? "active" : "cleared");
|
|
2321
|
+
}
|
|
2322
|
+
/// Input ops refuse to dispatch while the secure desktop stands: no injection can reach
|
|
2323
|
+
/// it anyway, and a clean refusal plus a notified human beats a blind timeout mid-cycle.
|
|
2324
|
+
public static void RequireNormalDesktop()
|
|
2325
|
+
{
|
|
2326
|
+
if (!SecureWatch.Active) return;
|
|
2327
|
+
var error = new Exception("SECURE_DESKTOP_ACTIVE: the Windows secure desktop (a UAC prompt or the lock screen) is up; no input was dispatched. Ask the human to answer the prompt; dispatch resumes automatically once the normal desktop returns.");
|
|
2328
|
+
error.Data["code"] = "SECURE_DESKTOP_ACTIVE";
|
|
2329
|
+
error.Data["outcome"] = "not-dispatched";
|
|
2330
|
+
throw error;
|
|
2331
|
+
}
|
|
2009
2332
|
public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
|
|
2010
2333
|
public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
|
|
2011
2334
|
public static void AgentActed() { _agentActAt = Environment.TickCount; }
|
|
2335
|
+
public static void BeginActuation()
|
|
2336
|
+
{
|
|
2337
|
+
POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
|
|
2338
|
+
Glow.ActuationBegin();
|
|
2339
|
+
Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
|
|
2340
|
+
}
|
|
2341
|
+
public static void EndActuation()
|
|
2342
|
+
{
|
|
2343
|
+
Handoff.SetActing(false, HumanClock.ElapsedMilliseconds);
|
|
2344
|
+
Glow.ActuationEnd();
|
|
2345
|
+
}
|
|
2012
2346
|
static bool IsBareModifier(uint vk)
|
|
2013
2347
|
{
|
|
2014
2348
|
return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
|
|
2015
2349
|
vk == 0xA0 || vk == 0xA1 || vk == 0xA2 || vk == 0xA3 ||
|
|
2016
2350
|
vk == 0xA4 || vk == 0xA5 || vk == 0x5B || vk == 0x5C;
|
|
2017
2351
|
}
|
|
2018
|
-
public static void RecordHumanInput(int key, bool down, bool up)
|
|
2352
|
+
public static void RecordHumanInput(int key, bool down, bool up) { RecordHumanInput(key, down, up, false); }
|
|
2353
|
+
public static void RecordHumanInput(int key, bool down, bool up, bool deliberate)
|
|
2019
2354
|
{
|
|
2355
|
+
Interlocked.Increment(ref _physicalInputEpoch);
|
|
2020
2356
|
long now = HumanClock.ElapsedMilliseconds;
|
|
2021
2357
|
bool was = Handoff.Active;
|
|
2022
2358
|
Handoff.Enable(_armed && CycleLit && !_exited, now);
|
|
2023
|
-
Handoff.Record(key, down, up, now);
|
|
2359
|
+
Handoff.Record(key, down, up, now, deliberate);
|
|
2024
2360
|
if (!was && Handoff.Active && Interlocked.CompareExchange(ref _handoffWorkQueued, 1, 0) == 0)
|
|
2025
2361
|
ThreadPool.QueueUserWorkItem(delegate {
|
|
2026
2362
|
try { Program.ReleaseOwnedInputs(); PublishHandoff(); }
|
|
@@ -2032,16 +2368,22 @@ namespace DshComputerUse.Worker
|
|
|
2032
2368
|
lock (HandoffPublishLock)
|
|
2033
2369
|
{
|
|
2034
2370
|
string phase = Handoff.Phase;
|
|
2035
|
-
|
|
2371
|
+
long epoch = InputEpoch;
|
|
2372
|
+
if (phase == _publishedHandoff && epoch == _publishedInputEpoch) return;
|
|
2373
|
+
bool changed = phase != _publishedHandoff;
|
|
2036
2374
|
_publishedHandoff = phase;
|
|
2375
|
+
_publishedInputEpoch = epoch;
|
|
2376
|
+
if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
|
|
2037
2377
|
if (phase != "idle") Glow.HumanYield();
|
|
2038
|
-
Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
|
|
2378
|
+
if (changed) Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
|
|
2039
2379
|
Notify("handoff", phase);
|
|
2040
2380
|
}
|
|
2041
2381
|
}
|
|
2042
2382
|
static void MonitorHuman()
|
|
2043
2383
|
{
|
|
2384
|
+
if (!ControlListenersEnabled) return;
|
|
2044
2385
|
SyncExitedFromFile();
|
|
2386
|
+
PollSecureDesktop();
|
|
2045
2387
|
long now = HumanClock.ElapsedMilliseconds;
|
|
2046
2388
|
Handoff.Enable(_armed && CycleLit && !_exited, now);
|
|
2047
2389
|
Handoff.Tick(now);
|
|
@@ -2371,7 +2713,7 @@ namespace DshComputerUse.Worker
|
|
|
2371
2713
|
static readonly HashSet<string> ReadOnlyOps = new HashSet<string> {
|
|
2372
2714
|
"selftest", "cursor", "panic", "resume", "recover", "echoargs", "ping", "screens",
|
|
2373
2715
|
"windows", "uia", "uiaFromPoint", "uiaFocused", "waitForIdle", "clipRead",
|
|
2374
|
-
"capture", "annotate", "waitStable", "waitChange", "focus", "frameSig", "probe",
|
|
2716
|
+
"capture", "ocr", "annotate", "waitStable", "waitChange", "focus", "frameSig", "probe",
|
|
2375
2717
|
// diagnostics that must be able to ANSWER while a brake is up: they send no input and
|
|
2376
2718
|
// light nothing, which is exactly when someone needs to inspect the release decision
|
|
2377
2719
|
"keyClass", "stuckPlan",
|
|
@@ -2459,6 +2801,7 @@ namespace DshComputerUse.Worker
|
|
|
2459
2801
|
try { Glow.Stopped(false); } catch { }
|
|
2460
2802
|
// Re-arming re-baselines the human monitor: the travel that tripped the brake must not
|
|
2461
2803
|
// still be sitting in the accumulator, or the very next poll would brake again.
|
|
2804
|
+
if (CycleLit && !_exited) Glow.Thinking(0);
|
|
2462
2805
|
_agentActAt = 0;
|
|
2463
2806
|
}
|
|
2464
2807
|
if (was) Notify("resume", "");
|
|
@@ -2648,12 +2991,16 @@ namespace DshComputerUse.Worker
|
|
|
2648
2991
|
bool finished;
|
|
2649
2992
|
ClipboardLease(IClipboardAccess access, List<ClipboardBytes> previous, uint written)
|
|
2650
2993
|
{ this.access = access; this.previous = previous; this.written = written; }
|
|
2994
|
+
/// Returns null when the previous clipboard cannot be preserved (private formats, odd
|
|
2995
|
+
/// sizes). The caller then downgrades to a plain overwrite-paste with a warning rather
|
|
2996
|
+
/// than refusing — see Snapshot's incident note (2026-11-04).
|
|
2651
2997
|
public static ClipboardLease Begin(IClipboardAccess access, string text)
|
|
2652
2998
|
{
|
|
2653
2999
|
using (var session = access.Open())
|
|
2654
3000
|
{
|
|
2655
3001
|
if (session.Sequence == 0) throw new Exception("Clipboard sequence unavailable; clipboard was not changed.");
|
|
2656
|
-
var previous = session.Snapshot(); //
|
|
3002
|
+
var previous = session.Snapshot(); // null = cannot preserve: caller downgrades, we do not clear anything here
|
|
3003
|
+
if (previous == null) return null;
|
|
2657
3004
|
try
|
|
2658
3005
|
{
|
|
2659
3006
|
session.Replace(new List<ClipboardBytes> {
|
|
@@ -2692,8 +3039,9 @@ namespace DshComputerUse.Worker
|
|
|
2692
3039
|
}
|
|
2693
3040
|
}
|
|
2694
3041
|
|
|
2695
|
-
// Copy only explicitly supported HGLOBAL formats. Unknown/private or GDI formats
|
|
2696
|
-
//
|
|
3042
|
+
// Copy only explicitly supported HGLOBAL formats. Unknown/private or GDI formats cannot be
|
|
3043
|
+
// snapshotted — Snapshot returns null for them and the caller downgrades to
|
|
3044
|
+
// paste-without-restore with a warning (incident 2026-11-04) instead of losing the paste.
|
|
2697
3045
|
public sealed class NativeClipboardAccess : IClipboardAccess, IDisposable
|
|
2698
3046
|
{
|
|
2699
3047
|
readonly NativeWindow window = new NativeWindow();
|
|
@@ -2717,14 +3065,6 @@ namespace DshComputerUse.Worker
|
|
|
2717
3065
|
[DllImport("kernel32.dll")] static extern UIntPtr GlobalSize(IntPtr memory);
|
|
2718
3066
|
[DllImport("kernel32.dll")] static extern IntPtr GlobalFree(IntPtr memory);
|
|
2719
3067
|
[DllImport("kernel32.dll")] static extern void SetLastError(uint error);
|
|
2720
|
-
static Exception Unavailable(string reason)
|
|
2721
|
-
{
|
|
2722
|
-
var error = new Exception("CLIPBOARD_PRESERVATION_UNAVAILABLE: " + reason +
|
|
2723
|
-
"; clipboard unchanged. Use mode:unicode if suitable for the target editor.");
|
|
2724
|
-
error.Data["code"] = "CLIPBOARD_PRESERVATION_UNAVAILABLE";
|
|
2725
|
-
error.Data["outcome"] = "not-dispatched";
|
|
2726
|
-
return error;
|
|
2727
|
-
}
|
|
2728
3068
|
public static bool Supported(uint format, string name)
|
|
2729
3069
|
{
|
|
2730
3070
|
if (format == 1 || format == 7 || format == 8 || format == 13 || format == 15 || format == 16 || format == 17) return true;
|
|
@@ -2746,6 +3086,13 @@ namespace DshComputerUse.Worker
|
|
|
2746
3086
|
}
|
|
2747
3087
|
public uint Sequence { get { return GetClipboardSequenceNumber(); } }
|
|
2748
3088
|
public void Dispose() { if (opened) { CloseClipboard(); opened = false; } }
|
|
3089
|
+
/// Returns null when the current clipboard cannot be fully snapshotted (private or GDI
|
|
3090
|
+
/// formats, too many/oversized entries, lock failures). NULL MEANS "cannot preserve",
|
|
3091
|
+
/// not "refuse to work" (human incident 2026-11-04): a WeChat-session clipboard carried
|
|
3092
|
+
/// private format 49161, paste was refused, the JS auto-fallback re-typed 37 CJK chars
|
|
3093
|
+
/// as unicode keystrokes and a garbled message actually went out. Losing an opaque
|
|
3094
|
+
/// clipboard item is a recoverable local cost; a mis-typed external message is not —
|
|
3095
|
+
/// so the caller downgrades to paste-without-restore instead of refusing.
|
|
2749
3096
|
public List<ClipboardBytes> Snapshot()
|
|
2750
3097
|
{
|
|
2751
3098
|
var formats = new List<uint>();
|
|
@@ -2756,27 +3103,26 @@ namespace DshComputerUse.Worker
|
|
|
2756
3103
|
format = EnumClipboardFormats(format);
|
|
2757
3104
|
if (format == 0)
|
|
2758
3105
|
{
|
|
2759
|
-
if (Marshal.GetLastWin32Error() != 0)
|
|
3106
|
+
if (Marshal.GetLastWin32Error() != 0) return null;
|
|
2760
3107
|
break;
|
|
2761
3108
|
}
|
|
2762
3109
|
var name = new StringBuilder(256);
|
|
2763
3110
|
if (format >= 0xC000) GetClipboardFormatName(format, name, name.Capacity);
|
|
2764
|
-
if (!Supported(format, name.ToString()))
|
|
3111
|
+
if (!Supported(format, name.ToString())) return null;
|
|
2765
3112
|
formats.Add(format);
|
|
2766
|
-
if (formats.Count > 64)
|
|
3113
|
+
if (formats.Count > 64) return null;
|
|
2767
3114
|
}
|
|
2768
3115
|
var result = new List<ClipboardBytes>();
|
|
2769
3116
|
long total = 0;
|
|
2770
3117
|
foreach (uint id in formats)
|
|
2771
3118
|
{
|
|
2772
3119
|
IntPtr handle = GetClipboardData(id);
|
|
2773
|
-
if (handle == IntPtr.Zero)
|
|
3120
|
+
if (handle == IntPtr.Zero) return null;
|
|
2774
3121
|
ulong size = GlobalSize(handle).ToUInt64();
|
|
2775
|
-
if (size == 0 || size > 32 * 1024 * 1024 || total + (long)size > 32 * 1024 * 1024)
|
|
2776
|
-
throw Unavailable("clipboard snapshot exceeds supported size");
|
|
3122
|
+
if (size == 0 || size > 32 * 1024 * 1024 || total + (long)size > 32 * 1024 * 1024) return null;
|
|
2777
3123
|
total += (long)size;
|
|
2778
3124
|
IntPtr data = GlobalLock(handle);
|
|
2779
|
-
if (data == IntPtr.Zero)
|
|
3125
|
+
if (data == IntPtr.Zero) return null;
|
|
2780
3126
|
try
|
|
2781
3127
|
{
|
|
2782
3128
|
var bytes = new byte[(int)size]; Marshal.Copy(data, bytes, 0, bytes.Length);
|
|
@@ -2822,13 +3168,13 @@ namespace DshComputerUse.Worker
|
|
|
2822
3168
|
static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
|
|
2823
3169
|
static readonly DateTime Epoch = new DateTime(1970, 1, 1);
|
|
2824
3170
|
|
|
2825
|
-
// Position correction is diagnostic only.
|
|
2826
|
-
//
|
|
3171
|
+
// Position correction is diagnostic only. Deliberate physical takeover during
|
|
3172
|
+
// actual input yields; incidental mouse jitter and idle use do not pause the task.
|
|
2827
3173
|
static int _positionCorrections;
|
|
2828
3174
|
static string _dpiNote = "unset";
|
|
2829
3175
|
static readonly HashSet<string> ObsOps = new HashSet<string>
|
|
2830
3176
|
{
|
|
2831
|
-
"ping", "echoargs", "screens", "cursor", "capture", "annotate",
|
|
3177
|
+
"ping", "echoargs", "screens", "cursor", "capture", "ocr", "annotate",
|
|
2832
3178
|
"windows", "uia", "uiaFromPoint", "uiaFocused", "waitForIdle", "clipRead",
|
|
2833
3179
|
"selftest", "indicator", "waitStable", "waitChange", "focus", "frameSig", "probe"
|
|
2834
3180
|
};
|
|
@@ -2841,16 +3187,44 @@ namespace DshComputerUse.Worker
|
|
|
2841
3187
|
"activate", "windowOp", "calibrate", "selectRange", "uiaAct"
|
|
2842
3188
|
};
|
|
2843
3189
|
static readonly object InputLock = new object();
|
|
3190
|
+
static readonly DesktopOwnershipLease DesktopLease = new DesktopOwnershipLease(DesktopOwnershipLease.DefaultPath());
|
|
3191
|
+
static bool _releaseDesktopPending;
|
|
3192
|
+
public static bool DesktopOwned { get { return DesktopLease.Owned; } }
|
|
3193
|
+
public static bool TryClaimDesktop(string owner)
|
|
3194
|
+
{ string occupiedBy; return DesktopLease.TryAcquire(owner, out occupiedBy); }
|
|
3195
|
+
static void EnsureDesktopOwnership(string op)
|
|
3196
|
+
{
|
|
3197
|
+
if (op == "ping" || op == "echoargs" || op == "keyClass" || op == "stuckPlan" || op == "probe") return;
|
|
3198
|
+
string occupiedBy;
|
|
3199
|
+
if (!DesktopLease.TryAcquire("native cycle: " + op, out occupiedBy))
|
|
3200
|
+
{
|
|
3201
|
+
var error = InputInterrupted("MACHINE_OWNED", "Another agent owns this desktop's computer-use cycle (" + occupiedBy + "). This operation was not dispatched; keep the task pending until that cycle releases control.");
|
|
3202
|
+
error.Data["outcome"] = "not-dispatched"; throw error;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
public static void ReleaseDesktopOwnership()
|
|
3206
|
+
{
|
|
3207
|
+
lock (InputLock)
|
|
3208
|
+
{
|
|
3209
|
+
_releaseDesktopPending = true;
|
|
3210
|
+
if (!_operationLive && OwnedUps.Count == 0) { DesktopLease.Dispose(); _releaseDesktopPending = false; }
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
2844
3213
|
static readonly Dictionary<string, INPUT> OwnedUps = new Dictionary<string, INPUT>();
|
|
2845
3214
|
static readonly ConcurrentDictionary<int, bool> CancelledRequests = new ConcurrentDictionary<int, bool>();
|
|
2846
3215
|
static long _cancelGeneration;
|
|
3216
|
+
public static void InvalidateActiveInput() { Interlocked.Increment(ref _cancelGeneration); }
|
|
2847
3217
|
static long _operationCancelGeneration, _operationHumanRevision;
|
|
2848
3218
|
static volatile bool _operationLive, _operationDispatched;
|
|
3219
|
+
static bool _operationActing;
|
|
3220
|
+
static string _lastAnnouncedSeries;
|
|
3221
|
+
static long _announcedHumanRevision, _announcedInputEpoch, _preparedInputEpoch;
|
|
2849
3222
|
static volatile int _activeRequestId, _lastCompletedRequestId;
|
|
2850
3223
|
// The seam is internal to the compiled program; no tool can replace it.
|
|
2851
3224
|
internal static Func<INPUT[], uint> InputSender = delegate(INPUT[] inputs) {
|
|
2852
3225
|
return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
|
|
2853
3226
|
};
|
|
3227
|
+
internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
|
|
2854
3228
|
|
|
2855
3229
|
static Exception InputInterrupted(string code, string message)
|
|
2856
3230
|
{
|
|
@@ -2861,16 +3235,89 @@ namespace DshComputerUse.Worker
|
|
|
2861
3235
|
}
|
|
2862
3236
|
public static void CheckHumanInput()
|
|
2863
3237
|
{
|
|
3238
|
+
if (Panic.ExitRequested)
|
|
3239
|
+
throw InputInterrupted("CYCLE-ENDED", "Ctrl+Alt+Q requested exit; no further input will be sent for this operation.");
|
|
2864
3240
|
if (Panic.PauseRequested)
|
|
2865
3241
|
throw InputInterrupted("ABORTED", "Ctrl+Esc requested a manual pause; wait for manual recovery.");
|
|
2866
3242
|
if (_operationLive && _operationCancelGeneration != Interlocked.Read(ref _cancelGeneration))
|
|
2867
3243
|
throw InputInterrupted("INPUT_CANCELLED", "the owning tool was cancelled; no further input will be sent.");
|
|
2868
3244
|
if (Panic.HumanActive || (_operationLive && _operationHumanRevision != Panic.HumanRevision))
|
|
2869
|
-
throw InputInterrupted("HUMAN_YIELD", "
|
|
3245
|
+
throw InputInterrupted("HUMAN_YIELD", "Temporary interruption only: the user currently owns input and the original task remains active. Wait locally until control returns after three quiet seconds, then immediately obtain fresh target evidence and continue the remaining authorized task. Do not abandon, cancel or declare completion; never replay a partial write.");
|
|
2870
3246
|
}
|
|
2871
3247
|
static void MarkMutation()
|
|
2872
3248
|
{
|
|
2873
|
-
Panic.Check("actuation"); CheckHumanInput(); _operationDispatched = true;
|
|
3249
|
+
Panic.Check("actuation"); BeginActuation(); CheckHumanInput(); _operationDispatched = true;
|
|
3250
|
+
}
|
|
3251
|
+
static void BeginActuation()
|
|
3252
|
+
{
|
|
3253
|
+
if (_operationActing) return;
|
|
3254
|
+
if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
|
|
3255
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop before input began. No action was dispatched. Obtain fresh target evidence and continue the original task; this is a temporary checkpoint, not cancellation.");
|
|
3256
|
+
_operationActing = true;
|
|
3257
|
+
Panic.BeginActuation();
|
|
3258
|
+
CheckHumanInput();
|
|
3259
|
+
if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
|
|
3260
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The desktop changed at the input boundary. No action was dispatched. Read the current target and continue the pending task.");
|
|
3261
|
+
}
|
|
3262
|
+
static void EndActuationForReadback()
|
|
3263
|
+
{
|
|
3264
|
+
lock (InputLock)
|
|
3265
|
+
{
|
|
3266
|
+
// Nested selection/chord helpers may still own a modifier or button.
|
|
3267
|
+
// Keep their sequence protected until every accepted down is released.
|
|
3268
|
+
if (OwnedUps.Count != 0) return;
|
|
3269
|
+
_operationActing = false;
|
|
3270
|
+
Panic.EndActuation();
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
static void PrepareActuation(string op, Dictionary<string, object> request)
|
|
3274
|
+
{
|
|
3275
|
+
object raw;
|
|
3276
|
+
var args = request.TryGetValue("args", out raw) ? raw as Dictionary<string, object> : null;
|
|
3277
|
+
string series = args == null ? "" : GetS(args, "__inputSeries", "");
|
|
3278
|
+
if (!string.IsNullOrEmpty(series) && series == _lastAnnouncedSeries &&
|
|
3279
|
+
_announcedHumanRevision == Panic.HumanRevision)
|
|
3280
|
+
{
|
|
3281
|
+
if (_announcedInputEpoch != Panic.InputEpoch)
|
|
3282
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop between batch steps. Obtain fresh target evidence and continue the remaining task without replaying completed input.");
|
|
3283
|
+
_preparedInputEpoch = Panic.InputEpoch; return;
|
|
3284
|
+
}
|
|
3285
|
+
Panic.Check(op); CheckHumanInput();
|
|
3286
|
+
long inputEpoch = Panic.InputEpoch;
|
|
3287
|
+
IntPtr foreground = PreparationForeground();
|
|
3288
|
+
Glow.Prepare(true);
|
|
3289
|
+
try
|
|
3290
|
+
{
|
|
3291
|
+
var startup = Stopwatch.StartNew();
|
|
3292
|
+
while (!Glow.PreparationVisible)
|
|
3293
|
+
{
|
|
3294
|
+
Panic.Check(op); CheckHumanInput();
|
|
3295
|
+
if (startup.ElapsedMilliseconds >= 5000) throw PreparationUnavailable("first-frame");
|
|
3296
|
+
Thread.Sleep(10);
|
|
3297
|
+
}
|
|
3298
|
+
var timer = Stopwatch.StartNew();
|
|
3299
|
+
while (timer.ElapsedMilliseconds < Glow.PreparationMs || !Glow.PreparationComplete)
|
|
3300
|
+
{
|
|
3301
|
+
Panic.Check(op); CheckHumanInput();
|
|
3302
|
+
if (timer.ElapsedMilliseconds >= 5000) throw PreparationUnavailable("countdown-frame");
|
|
3303
|
+
int remaining = Glow.PreparationMs - (int)timer.ElapsedMilliseconds;
|
|
3304
|
+
Thread.Sleep(remaining > 0 ? Math.Min(25, remaining) : 10);
|
|
3305
|
+
}
|
|
3306
|
+
Panic.Check(op); CheckHumanInput();
|
|
3307
|
+
if (inputEpoch != Panic.InputEpoch || foreground != PreparationForeground())
|
|
3308
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop during the preparation cue. No action was dispatched. Preserve the task, obtain fresh target evidence, and continue the authorized task immediately; this is not cancellation or abandonment.");
|
|
3309
|
+
_lastAnnouncedSeries = series; _announcedHumanRevision = Panic.HumanRevision;
|
|
3310
|
+
_announcedInputEpoch = _preparedInputEpoch = inputEpoch;
|
|
3311
|
+
}
|
|
3312
|
+
finally { Glow.Prepare(false); }
|
|
3313
|
+
}
|
|
3314
|
+
static Exception PreparationUnavailable(string phase)
|
|
3315
|
+
{
|
|
3316
|
+
var health = Glow.Health();
|
|
3317
|
+
health["failurePhase"] = phase;
|
|
3318
|
+
var error = InputInterrupted("PREPARATION_UNAVAILABLE", "The preparation notice could not be displayed through completion; no input was dispatched. glowHealth=" + Json.Serialize(health));
|
|
3319
|
+
error.Data["glowHealth"] = health;
|
|
3320
|
+
return error;
|
|
2874
3321
|
}
|
|
2875
3322
|
public static bool CancelInput(int requestId)
|
|
2876
3323
|
{
|
|
@@ -2912,6 +3359,7 @@ namespace DshComputerUse.Worker
|
|
|
2912
3359
|
// cross-process STOP may hold SyncLock while releasing owned input.
|
|
2913
3360
|
if (Panic.Engaged || Panic.Stopped || Panic.Exited)
|
|
2914
3361
|
throw InputInterrupted("ABORTED", "manual pause or exit owns the desktop; no further input will be sent.");
|
|
3362
|
+
BeginActuation();
|
|
2915
3363
|
if (inputs.Length == 1)
|
|
2916
3364
|
{
|
|
2917
3365
|
bool isUp; INPUT release; int physical;
|
|
@@ -3106,6 +3554,10 @@ namespace DshComputerUse.Worker
|
|
|
3106
3554
|
else if (kind == "resume")
|
|
3107
3555
|
hint = "Re-armed: a human pressed Ctrl+Alt+R (the worker notifies `resume` from Clear() " +
|
|
3108
3556
|
"alone) — working may continue from the break point.";
|
|
3557
|
+
else if (kind == "secure")
|
|
3558
|
+
hint = why == "active"
|
|
3559
|
+
? "The Windows secure desktop is up (a UAC prompt or the lock screen). Input dispatch is PAUSED — nothing you send will be dispatched until it closes. Tell the human a system dialog needs their answer; dispatch resumes automatically the moment the normal desktop returns, then simply retry the step."
|
|
3560
|
+
: "The normal desktop is back. Input dispatch has resumed automatically — retry the interrupted step and continue the task.";
|
|
3109
3561
|
else
|
|
3110
3562
|
hint = "worker state change \"" + kind + "\": " + why;
|
|
3111
3563
|
WriteLine(Json.Serialize(Dict(
|
|
@@ -3136,6 +3588,11 @@ namespace DshComputerUse.Worker
|
|
|
3136
3588
|
Dictionary<string, object> resp;
|
|
3137
3589
|
try { resp = Handle(oneshot); }
|
|
3138
3590
|
catch (Exception ex) { resp = Err(ex); }
|
|
3591
|
+
if (!Panic.FinishOneShotControls())
|
|
3592
|
+
{
|
|
3593
|
+
Program.AuditRecord("CONTROL-DRAIN-TIMEOUT", "one-shot control finalization exceeded 5s");
|
|
3594
|
+
resp = Err("CONTROL-DRAIN-TIMEOUT: physical brake finalization could not be confirmed; verify STOP/EXITED before continuing.");
|
|
3595
|
+
}
|
|
3139
3596
|
AuditActuation(oneshot, resp);
|
|
3140
3597
|
WriteLine(Json.Serialize(resp));
|
|
3141
3598
|
return 0;
|
|
@@ -3218,20 +3675,30 @@ namespace DshComputerUse.Worker
|
|
|
3218
3675
|
{
|
|
3219
3676
|
var request = Json.Deserialize<Dictionary<string, object>>(line);
|
|
3220
3677
|
string op = GetS(request, "op", "");
|
|
3221
|
-
|
|
3678
|
+
object rawArgs;
|
|
3679
|
+
var args = request.TryGetValue("args", out rawArgs) ? rawArgs as Dictionary<string, object> : null;
|
|
3680
|
+
bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
|
|
3681
|
+
(op == "focus" && args != null && GetB(args, "refocus", false));
|
|
3682
|
+
bool dryInput = input && args != null && GetB(args, "dryRun", false);
|
|
3683
|
+
// Only the coordinate click probe has a native read-only implementation.
|
|
3684
|
+
// No other input may use the flag to skip the controls and then dispatch.
|
|
3685
|
+
if (dryInput && (op != "click" || !args.ContainsKey("x") || !args.ContainsKey("y")))
|
|
3686
|
+
return Err("UNSUPPORTED_DRY_RUN: native input simulation requires a click with x and y; no input was dispatched.");
|
|
3687
|
+
EnsureDesktopOwnership(op);
|
|
3222
3688
|
int requestId = GetI(request, "id", 0);
|
|
3223
3689
|
bool ignored;
|
|
3224
3690
|
_activeRequestId = requestId;
|
|
3225
3691
|
_operationHumanRevision = Panic.HumanRevision;
|
|
3226
3692
|
_operationCancelGeneration = Interlocked.Read(ref _cancelGeneration);
|
|
3227
3693
|
_operationDispatched = false; _operationLive = input;
|
|
3694
|
+
bool scopedCycle = false;
|
|
3228
3695
|
try
|
|
3229
3696
|
{
|
|
3230
3697
|
if (CancelledRequests.TryRemove(requestId, out ignored))
|
|
3231
3698
|
throw InputInterrupted("INPUT_CANCELLED", "cancelled before dispatch.");
|
|
3232
3699
|
if (input) CheckHumanInput();
|
|
3233
3700
|
string pauseId = Panic.TemporaryPauseId;
|
|
3234
|
-
Dictionary<string, object> result = HandleCore(line);
|
|
3701
|
+
Dictionary<string, object> result = HandleCore(line, input, dryInput, ref scopedCycle);
|
|
3235
3702
|
if (input) CheckHumanInput();
|
|
3236
3703
|
if (!string.IsNullOrEmpty(pauseId) && GetB(result, "ok", false) &&
|
|
3237
3704
|
(op == "windows" || op == "uia" || op == "uiaFromPoint" || op == "uiaFocused" || op == "capture"))
|
|
@@ -3240,14 +3707,20 @@ namespace DshComputerUse.Worker
|
|
|
3240
3707
|
}
|
|
3241
3708
|
finally
|
|
3242
3709
|
{
|
|
3243
|
-
if (input)
|
|
3710
|
+
if (input)
|
|
3711
|
+
{
|
|
3712
|
+
try { ReleaseOwnedInputs(); }
|
|
3713
|
+
finally { _operationActing = false; Panic.EndActuation(); Glow.Prepare(false); }
|
|
3714
|
+
}
|
|
3244
3715
|
_operationLive = false; _activeRequestId = 0;
|
|
3716
|
+
if (scopedCycle) Panic.TryEndCycle();
|
|
3717
|
+
if (_releaseDesktopPending) ReleaseDesktopOwnership();
|
|
3245
3718
|
_lastCompletedRequestId = Math.Max(_lastCompletedRequestId, requestId);
|
|
3246
3719
|
CancelledRequests.TryRemove(requestId, out ignored);
|
|
3247
3720
|
}
|
|
3248
3721
|
}
|
|
3249
3722
|
|
|
3250
|
-
static Dictionary<string, object> HandleCore(string line)
|
|
3723
|
+
static Dictionary<string, object> HandleCore(string line, bool input, bool dryInput, ref bool scopedCycle)
|
|
3251
3724
|
{
|
|
3252
3725
|
Dictionary<string, object> req;
|
|
3253
3726
|
try { req = Json.Deserialize<Dictionary<string, object>>(line); }
|
|
@@ -3257,6 +3730,7 @@ namespace DshComputerUse.Worker
|
|
|
3257
3730
|
Dictionary<string, object> a = (req.TryGetValue("args", out av) && av is Dictionary<string, object>) ? (Dictionary<string, object>)av : new Dictionary<string, object>();
|
|
3258
3731
|
bool cycleOp = GetB(a, "__cycle", false) && op != null &&
|
|
3259
3732
|
op != "abort" && op != "ping" && op != "echoargs" &&
|
|
3733
|
+
op != "probe" && op != "keyClass" && op != "stuckPlan" &&
|
|
3260
3734
|
op != "indicator" && op != "calm" && op != "panic" && op != "resume" && op != "recover" &&
|
|
3261
3735
|
// ... AND THE CYCLE HAS TO BE ALIVE TO BE LIT (2026-09-13). "Lit by any call" is how
|
|
3262
3736
|
// a DEAD cycle came back as a cyan breathing border: after Ctrl+Alt+Q ended the
|
|
@@ -3279,7 +3753,22 @@ namespace DshComputerUse.Worker
|
|
|
3279
3753
|
// human re-arms it (CheckOp's allowlist keeps the health probe and re-arm alive).
|
|
3280
3754
|
// Emergency stop gates EVERY op that could drive the machine; a DRY RUN drives nothing
|
|
3281
3755
|
// and is therefore treated as the observation it really is.
|
|
3282
|
-
Panic.CheckOp(
|
|
3756
|
+
Panic.CheckOp(dryInput ? "probe" : op);
|
|
3757
|
+
|
|
3758
|
+
if (input && !dryInput)
|
|
3759
|
+
{
|
|
3760
|
+
// Direct native input (including --op) has no JS cycle signature.
|
|
3761
|
+
// Arm physical controls before the purple cue and release this scope
|
|
3762
|
+
// when the request ends. A human pause or exit retains its own latch.
|
|
3763
|
+
if (!Panic.CycleLit)
|
|
3764
|
+
{
|
|
3765
|
+
scopedCycle = Panic.TryLightCycle(0);
|
|
3766
|
+
if (!scopedCycle) Panic.Check(op);
|
|
3767
|
+
}
|
|
3768
|
+
Panic.RequireControlHooks();
|
|
3769
|
+
Panic.RequireNormalDesktop();
|
|
3770
|
+
PrepareActuation(op, req);
|
|
3771
|
+
}
|
|
3283
3772
|
|
|
3284
3773
|
switch (op)
|
|
3285
3774
|
{
|
|
@@ -3291,7 +3780,7 @@ namespace DshComputerUse.Worker
|
|
|
3291
3780
|
case "beginAsk": return Ok(BeginAskOp(a));
|
|
3292
3781
|
case "endAsk": return Ok(EndAskOp(a));
|
|
3293
3782
|
case "exit": return Ok(ExitOp(a));
|
|
3294
|
-
case "ping": return Ok(Dict("pong", true, "cooperation", Panic.CooperationState()));
|
|
3783
|
+
case "ping": return Ok(Dict("pong", true, "cooperation", Panic.CooperationState(), "glowHealth", Glow.Health()));
|
|
3295
3784
|
// TEST SEAM (D, 2026-09-13). A fixture may not re-implement the rule it measures, so
|
|
3296
3785
|
// this op drives the PRODUCTION transition (Panic.ClassifyAndTrackKey — the same
|
|
3297
3786
|
// function the low-level hook calls) with explicit evidence: ours / injected / vk.
|
|
@@ -3357,6 +3846,7 @@ namespace DshComputerUse.Worker
|
|
|
3357
3846
|
case "screens": return Ok(VirtualScreenDict());
|
|
3358
3847
|
case "cursor": POINT cp; Native.GetCursorPos(out cp); return Ok(PtDict(cp.X, cp.Y));
|
|
3359
3848
|
case "capture": return Ok(Capture(a));
|
|
3849
|
+
case "ocr": return Ok(Ocr(a));
|
|
3360
3850
|
case "annotate": return Ok(Annotate(a));
|
|
3361
3851
|
case "move": FailsafeCheck(a); return Ok(Move(a));
|
|
3362
3852
|
case "click": FailsafeCheck(a); return Ok(Click(a));
|
|
@@ -3378,13 +3868,15 @@ namespace DshComputerUse.Worker
|
|
|
3378
3868
|
case "probe":
|
|
3379
3869
|
{
|
|
3380
3870
|
string what = GetS(a, "what", "point");
|
|
3381
|
-
Dictionary<string, object> pr = what == "focus" ? ProbeFocus()
|
|
3871
|
+
Dictionary<string, object> pr = what == "glow" ? Dict() : what == "focus" ? ProbeFocus()
|
|
3382
3872
|
: (what == "window" ? WinBrief(FindHwnd(a)) : ProbePoint(GetI(a, "x", 0), GetI(a, "y", 0)));
|
|
3873
|
+
pr["glowHealth"] = Glow.Health();
|
|
3383
3874
|
// The state machine as a FACT, in the one op that exists to answer "what is
|
|
3384
3875
|
// actually true right now". Read-only, so asking never changes the answer.
|
|
3385
3876
|
pr["cycle"] = Dict(
|
|
3386
3877
|
"cycleLit", Panic.CycleLit,
|
|
3387
3878
|
"armed", Panic.Armed,
|
|
3879
|
+
"secureDesktop", Panic.SecureDesktopActive,
|
|
3388
3880
|
"engaged", Panic.Engaged,
|
|
3389
3881
|
"stopped", Panic.Stopped,
|
|
3390
3882
|
"exited", Panic.Exited,
|
|
@@ -3399,8 +3891,9 @@ namespace DshComputerUse.Worker
|
|
|
3399
3891
|
pr["glow"] = Dict(
|
|
3400
3892
|
"want", Glow.Want,
|
|
3401
3893
|
// Is a real injection sequence in flight RIGHT NOW? The ACTING light is driven
|
|
3402
|
-
// by
|
|
3894
|
+
// by the actual action scope, so both are reported as observable facts.
|
|
3403
3895
|
"injecting", Glow.Injecting,
|
|
3896
|
+
"acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
|
|
3404
3897
|
"title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
|
|
3405
3898
|
// EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
|
|
3406
3899
|
// nothing held is the observable form of "no ghost right-click / Alt menu".
|
|
@@ -3417,9 +3910,9 @@ namespace DshComputerUse.Worker
|
|
|
3417
3910
|
// like the emergency brake. Called when the agent's turn ends, so a lingering blue
|
|
3418
3911
|
// border can never claim the machine is still being driven after control stopped.
|
|
3419
3912
|
case "calm":
|
|
3420
|
-
Panic.
|
|
3421
|
-
|
|
3422
|
-
|
|
3913
|
+
if (!Panic.TryEndCycle())
|
|
3914
|
+
return Ok(Dict("released", false, "reason", "A manual pause or question still owns this cycle; its brake and ownership were preserved."));
|
|
3915
|
+
return Ok(Dict("released", true, "indicator", "off", "hard", true));
|
|
3423
3916
|
case "focus": return Ok(FocusReport(a, GetB(a, "refocus", false)));
|
|
3424
3917
|
case "frameSig": return Ok(FrameSigOp(a));
|
|
3425
3918
|
// `source=agent` (not "unknown"): this op exists ONLY for the agent's own `stop`, and a
|
|
@@ -3465,6 +3958,90 @@ namespace DshComputerUse.Worker
|
|
|
3465
3958
|
|
|
3466
3959
|
static long NowMs() { return (long)(DateTime.UtcNow - Epoch).TotalMilliseconds; }
|
|
3467
3960
|
|
|
3961
|
+
// ---------- OCR: text WITH bounding boxes, straight from the worker ----------
|
|
3962
|
+
// Zero-UIA apps (WeChat incident 2026-11-04) burned one screenshot per decision because
|
|
3963
|
+
// finding "傳送" or a contact name meant reading pixels with model vision. Windows' inbox
|
|
3964
|
+
// OCR (Windows.Media.Ocr — ships with the OS, zero external dependencies) returns text
|
|
3965
|
+
// with per-word rectangles, so the model can locate controls BY NAME and spend vision
|
|
3966
|
+
// only where looks actually matter. Coordinates are ABSOLUTE screen pixels (region origin
|
|
3967
|
+
// added), ready for computer_click without further math.
|
|
3968
|
+
static OcrEngine _ocrEngine;
|
|
3969
|
+
static string _ocrLanguage;
|
|
3970
|
+
static Dictionary<string, object> Ocr(Dictionary<string, object> a)
|
|
3971
|
+
{
|
|
3972
|
+
ValidateKickoffCapture(a);
|
|
3973
|
+
if (_ocrEngine == null)
|
|
3974
|
+
{
|
|
3975
|
+
_ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages();
|
|
3976
|
+
if (_ocrEngine != null) _ocrLanguage = _ocrEngine.RecognizerLanguage.LanguageTag;
|
|
3977
|
+
}
|
|
3978
|
+
if (_ocrEngine == null)
|
|
3979
|
+
throw new Exception("OCR_UNAVAILABLE: no OCR recognizer language is installed on this machine. " +
|
|
3980
|
+
"Install one (Settings > Time & language > Language > add a language with 'Text recognition') " +
|
|
3981
|
+
"or read regions with computer_shot instead.");
|
|
3982
|
+
int vx = Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
|
|
3983
|
+
int vy = Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
|
|
3984
|
+
int vw = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
|
|
3985
|
+
int vh = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
|
|
3986
|
+
int x = GetI(a, "x", vx), y = GetI(a, "y", vy);
|
|
3987
|
+
int w = GetI(a, "width", vw), h = GetI(a, "height", vh);
|
|
3988
|
+
if (GetB(a, "clamp", true))
|
|
3989
|
+
{
|
|
3990
|
+
int x2 = Math.Max(x, vx), y2 = Math.Max(y, vy);
|
|
3991
|
+
int x3 = Math.Min(x + w, vx + vw), y3 = Math.Min(y + h, vy + vh);
|
|
3992
|
+
if (x3 <= x2 || y3 <= y2) throw new Exception("region outside virtual desktop");
|
|
3993
|
+
x = x2; y = y2; w = x3 - x2; h = y3 - y2;
|
|
3994
|
+
}
|
|
3995
|
+
// OCR cost scales with pixels; one decision never needs more than 16M of them.
|
|
3996
|
+
if ((long)w * h > 16L * 1024 * 1024)
|
|
3997
|
+
throw new Exception("OCR region exceeds 16M pixels (" + w + "x" + h + "); crop tighter.");
|
|
3998
|
+
byte[] bgra = new byte[w * h * 4];
|
|
3999
|
+
IntPtr screen = IntPtr.Zero, mem = IntPtr.Zero, dib = IntPtr.Zero, oldBmp = IntPtr.Zero;
|
|
4000
|
+
try
|
|
4001
|
+
{
|
|
4002
|
+
screen = Native.GetDC(IntPtr.Zero);
|
|
4003
|
+
mem = Native.CreateCompatibleDC(screen);
|
|
4004
|
+
Native.BITMAPINFO bmi = new Native.BITMAPINFO();
|
|
4005
|
+
bmi.bmiHeader.biSize = (uint)Marshal.SizeOf(typeof(Native.BITMAPINFOHEADER));
|
|
4006
|
+
bmi.bmiHeader.biWidth = w; bmi.bmiHeader.biHeight = -h; // top-down
|
|
4007
|
+
bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32;
|
|
4008
|
+
bmi.bmiHeader.biCompression = 0;
|
|
4009
|
+
IntPtr bits;
|
|
4010
|
+
dib = Native.CreateDIBSection(screen, ref bmi, 0, out bits, IntPtr.Zero, 0);
|
|
4011
|
+
if (dib == IntPtr.Zero) throw new Exception("OCR: CreateDIBSection failed");
|
|
4012
|
+
oldBmp = Native.SelectObject(mem, dib);
|
|
4013
|
+
Native.BitBlt(mem, 0, 0, w, h, screen, x, y, 0x00CC0020 | 0x40000000);
|
|
4014
|
+
Marshal.Copy(bits, bgra, 0, bgra.Length);
|
|
4015
|
+
}
|
|
4016
|
+
finally
|
|
4017
|
+
{
|
|
4018
|
+
if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) Native.SelectObject(mem, oldBmp);
|
|
4019
|
+
if (dib != IntPtr.Zero) Native.DeleteObject(dib);
|
|
4020
|
+
if (mem != IntPtr.Zero) Native.DeleteDC(mem);
|
|
4021
|
+
if (screen != IntPtr.Zero) Native.ReleaseDC(IntPtr.Zero, screen);
|
|
4022
|
+
}
|
|
4023
|
+
SoftwareBitmap bitmap = SoftwareBitmap.CreateCopyFromBuffer(
|
|
4024
|
+
WindowsRuntimeBufferExtensions.AsBuffer(bgra), BitmapPixelFormat.Bgra8, w, h);
|
|
4025
|
+
OcrResult result = _ocrEngine.RecognizeAsync(bitmap).AsTask().GetAwaiter().GetResult();
|
|
4026
|
+
List<object> lines = new List<object>();
|
|
4027
|
+
StringBuilder joined = new StringBuilder();
|
|
4028
|
+
foreach (OcrLine line in result.Lines)
|
|
4029
|
+
{
|
|
4030
|
+
List<object> words = new List<object>();
|
|
4031
|
+
foreach (OcrWord word in line.Words)
|
|
4032
|
+
{
|
|
4033
|
+
words.Add(Dict("text", word.Text,
|
|
4034
|
+
"x", x + (int)word.BoundingRect.X, "y", y + (int)word.BoundingRect.Y,
|
|
4035
|
+
"w", (int)word.BoundingRect.Width, "h", (int)word.BoundingRect.Height));
|
|
4036
|
+
}
|
|
4037
|
+
lines.Add(Dict("text", line.Text, "words", words));
|
|
4038
|
+
if (joined.Length > 0) joined.Append('\n');
|
|
4039
|
+
joined.Append(line.Text);
|
|
4040
|
+
}
|
|
4041
|
+
return Dict("lines", lines, "text", joined.ToString(), "language", _ocrLanguage,
|
|
4042
|
+
"region", new int[] { x, y, w, h });
|
|
4043
|
+
}
|
|
4044
|
+
|
|
3468
4045
|
static Dictionary<string, object> Capture(Dictionary<string, object> a)
|
|
3469
4046
|
{
|
|
3470
4047
|
ValidateKickoffCapture(a); // before even the pixel-stability sampler
|
|
@@ -3706,6 +4283,7 @@ namespace DshComputerUse.Worker
|
|
|
3706
4283
|
// ACTING light on it, and clear the mark in a finally so the blue can never outlive the
|
|
3707
4284
|
// input it is describing.
|
|
3708
4285
|
CheckHumanInput();
|
|
4286
|
+
BeginActuation();
|
|
3709
4287
|
Glow.InputBegin();
|
|
3710
4288
|
try
|
|
3711
4289
|
{
|
|
@@ -3767,7 +4345,8 @@ namespace DshComputerUse.Worker
|
|
|
3767
4345
|
string btn = GetS(a, "button", "left");
|
|
3768
4346
|
int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
|
|
3769
4347
|
int gap = GetI(a, "intervalMs", 60);
|
|
3770
|
-
|
|
4348
|
+
bool dryRun = GetB(a, "dryRun", false);
|
|
4349
|
+
if (!dryRun) AgentActing(a, "click");
|
|
3771
4350
|
CheckHumanInput();
|
|
3772
4351
|
// If the caller NAMED the window it wants to click in, put that window in front
|
|
3773
4352
|
// first: a click on a covered pixel is delivered to whatever is on top of it (the
|
|
@@ -3778,7 +4357,6 @@ namespace DshComputerUse.Worker
|
|
|
3778
4357
|
// would change the thing it measures. That bug hid a real occlusion for a whole test
|
|
3779
4358
|
// round (2026-09-13): every dry run reported "ok" because it had just activated the
|
|
3780
4359
|
// window it was asked about.
|
|
3781
|
-
bool dryRun = GetB(a, "dryRun", false);
|
|
3782
4360
|
// A caller-supplied window is re-asserted (raised, ALT tap, titlebar click) on the
|
|
3783
4361
|
// next line, so it has to be checked BEFORE that: the pixel check further down comes
|
|
3784
4362
|
// far too late to protect a window the worker was told to bring forward.
|
|
@@ -3921,6 +4499,7 @@ namespace DshComputerUse.Worker
|
|
|
3921
4499
|
SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
|
|
3922
4500
|
if (i < clicks - 1) Thread.Sleep(gap);
|
|
3923
4501
|
}
|
|
4502
|
+
EndActuationForReadback();
|
|
3924
4503
|
double changedPct = -1;
|
|
3925
4504
|
if (verify && before != null)
|
|
3926
4505
|
{
|
|
@@ -3940,8 +4519,10 @@ namespace DshComputerUse.Worker
|
|
|
3940
4519
|
{
|
|
3941
4520
|
clickOut["changedPct"] = changedPct;
|
|
3942
4521
|
if (changedPct < 0.15 && !a.ContainsKey("verify"))
|
|
3943
|
-
clickOut["warning"] = "nothing changed near the click point within ~240ms
|
|
3944
|
-
"
|
|
4522
|
+
clickOut["warning"] = "nothing changed near the click point within ~240ms. This is EXPECTED when the " +
|
|
4523
|
+
"click lands on an already-focused editor or a control already in the desired state (a caret blink " +
|
|
4524
|
+
"alone sits below the threshold — test report 2026-11-04); it can also mean a dead/disabled control " +
|
|
4525
|
+
"or a wrong pixel. Read focus/receiver above and only retry if they disagree with the intent.";
|
|
3945
4526
|
}
|
|
3946
4527
|
return clickOut;
|
|
3947
4528
|
}
|
|
@@ -4047,16 +4628,8 @@ namespace DshComputerUse.Worker
|
|
|
4047
4628
|
{
|
|
4048
4629
|
_lastRefocus = EnsureTargetForeground();
|
|
4049
4630
|
}
|
|
4050
|
-
//
|
|
4051
|
-
//
|
|
4052
|
-
// 1200 ms, not 30000 (2026-09-13). The hold is how long the DEEP BLUE "acting" border
|
|
4053
|
-
// stays after an input injection — and 30 s meant a 50 ms click painted half a minute of
|
|
4054
|
-
// "I am driving the machine", during which the cyan "thinking" state could never show
|
|
4055
|
-
// (the render loop shows acting in preference to thinking). The human watched the blue
|
|
4056
|
-
// light sit still through a whole page read and asked, correctly, why. Blue now means
|
|
4057
|
-
// "input, right now, plus a short tail"; cyan is what a reading agent looks like.
|
|
4058
|
-
int hold = GetI(a, "indicatorMs", 1200);
|
|
4059
|
-
Glow.Touch(hold);
|
|
4631
|
+
// Validation and focus readback do not own input. The first real mutation
|
|
4632
|
+
// or SendInput opens the actual scope and its deep-blue cue.
|
|
4060
4633
|
}
|
|
4061
4634
|
|
|
4062
4635
|
static Dictionary<string, object> Indicator(Dictionary<string, object> a)
|
|
@@ -4360,11 +4933,16 @@ namespace DshComputerUse.Worker
|
|
|
4360
4933
|
|
|
4361
4934
|
// Explicit window scope excludes unrelated desktop animation without loosening the
|
|
4362
4935
|
// change threshold. A moved/minimized/replaced window invalidates the observation.
|
|
4363
|
-
static Func<byte[]> WaitSampler(Dictionary<string, object> a, int step, out object scope
|
|
4936
|
+
static Func<byte[]> WaitSampler(Dictionary<string, object> a, int step, out object scope,
|
|
4937
|
+
out int rx, out int ry, out int rw, out int rh)
|
|
4364
4938
|
{
|
|
4365
4939
|
if (!a.ContainsKey("hwnd") || GetI(a, "hwnd", 0) == 0)
|
|
4366
4940
|
{
|
|
4367
|
-
|
|
4941
|
+
rx = Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN);
|
|
4942
|
+
ry = Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN);
|
|
4943
|
+
rw = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
|
|
4944
|
+
rh = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
|
|
4945
|
+
scope = Dict("kind", "virtual-desktop", "region", Dict("x", rx, "y", ry, "width", rw, "height", rh));
|
|
4368
4946
|
return delegate { return FrameSignature(step); };
|
|
4369
4947
|
}
|
|
4370
4948
|
IntPtr h = new IntPtr(GetI(a, "hwnd", 0));
|
|
@@ -4376,7 +4954,8 @@ namespace DshComputerUse.Worker
|
|
|
4376
4954
|
Rectangle visible = Rectangle.Intersect(virtualRect, Rectangle.FromLTRB(original.Left, original.Top, original.Right, original.Bottom));
|
|
4377
4955
|
if (visible.Width <= 16 || visible.Height <= 16)
|
|
4378
4956
|
throw new Exception("WAIT_TARGET_UNAVAILABLE: window has no usable visible screen region");
|
|
4379
|
-
|
|
4957
|
+
rx = visible.X; ry = visible.Y; rw = visible.Width; rh = visible.Height;
|
|
4958
|
+
scope = Dict("kind", "window", "hwnd", h.ToInt64(), "region", Dict("x", rx, "y", ry, "width", rw, "height", rh));
|
|
4380
4959
|
return delegate {
|
|
4381
4960
|
RECT current;
|
|
4382
4961
|
if (!Native.IsWindow(h) || Native.IsIconic(h) || PidOf(h) != pid || !Native.GetWindowRect(h, out current) ||
|
|
@@ -4386,6 +4965,29 @@ namespace DshComputerUse.Worker
|
|
|
4386
4965
|
};
|
|
4387
4966
|
}
|
|
4388
4967
|
|
|
4968
|
+
/// Bounding box (absolute screen px) of every signature cell that differs beyond the
|
|
4969
|
+
/// threshold — "where did it change", not just "did it" (test report 2026-11-04: a
|
|
4970
|
+
/// 0.344% diff cannot distinguish a sent message from a blinking caret without this).
|
|
4971
|
+
static int[] SigDiffBounds(byte[] a, byte[] b, int threshold, int cols, int step, int rx, int ry)
|
|
4972
|
+
{
|
|
4973
|
+
if (a == null || b == null || a.Length != b.Length || cols <= 0) return null;
|
|
4974
|
+
int minX = -1, minY = -1, maxX = -1, maxY = -1;
|
|
4975
|
+
for (int i = 0; i < a.Length; i++)
|
|
4976
|
+
{
|
|
4977
|
+
int d = a[i] - b[i];
|
|
4978
|
+
if (d < 0) d = -d;
|
|
4979
|
+
if (d <= threshold) continue;
|
|
4980
|
+
int c = i % cols, r = i / cols;
|
|
4981
|
+
if (minX < 0 || c < minX) minX = c;
|
|
4982
|
+
if (c > maxX) maxX = c;
|
|
4983
|
+
if (minY < 0 || r < minY) minY = r;
|
|
4984
|
+
if (r > maxY) maxY = r;
|
|
4985
|
+
}
|
|
4986
|
+
if (minX < 0) return null;
|
|
4987
|
+
return new int[] { rx + minX * step, ry + minY * step,
|
|
4988
|
+
(maxX - minX + 1) * step, (maxY - minY + 1) * step };
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4389
4991
|
static Dictionary<string, object> WaitStable(Dictionary<string, object> a)
|
|
4390
4992
|
{
|
|
4391
4993
|
int capMs = Math.Max(400, GetI(a, "timeoutMs", 8000));
|
|
@@ -4394,8 +4996,34 @@ namespace DshComputerUse.Worker
|
|
|
4394
4996
|
int step = Math.Max(4, Math.Min(64, GetI(a, "step", 8)));
|
|
4395
4997
|
double diffPct = GetD(a, "diffPct", 0.30);
|
|
4396
4998
|
int waited, estimate; double lastDiff;
|
|
4397
|
-
|
|
4999
|
+
int rx, ry, rw, rh;
|
|
5000
|
+
object scope; Func<byte[]> sample = WaitSampler(a, step, out scope, out rx, out ry, out rw, out rh);
|
|
4398
5001
|
bool ok = SettleSampleWait(capMs, stableMs, pollMs, diffPct, sample, out waited, out lastDiff, out estimate);
|
|
5002
|
+
if (!ok)
|
|
5003
|
+
{
|
|
5004
|
+
// CARET / BLINK DETECTION (test report 2026-11-04): a blinking caret is a ~1-2 Hz
|
|
5005
|
+
// periodic sub-percent change that never settles — Notepad burned the full 8 s cap
|
|
5006
|
+
// on lastDiffPct 0.388%. Probe one extra window (~2.4 s covers several blink
|
|
5007
|
+
// cycles): if EVERY consecutive diff is tiny but nonzero, the only motion is the
|
|
5008
|
+
// caret — call it stable and say exactly why.
|
|
5009
|
+
int probes = Math.Max(6, 2400 / pollMs);
|
|
5010
|
+
byte[] prev = sample();
|
|
5011
|
+
double max = 0; bool zero = false, large = false;
|
|
5012
|
+
for (int i = 0; i < probes; i++)
|
|
5013
|
+
{
|
|
5014
|
+
Thread.Sleep(pollMs);
|
|
5015
|
+
byte[] cur = sample();
|
|
5016
|
+
double d = SigDiffPct(prev, cur, 8);
|
|
5017
|
+
prev = cur;
|
|
5018
|
+
if (d == 0) zero = true;
|
|
5019
|
+
if (d > 1.0) large = true;
|
|
5020
|
+
if (d > max) max = d;
|
|
5021
|
+
}
|
|
5022
|
+
if (!large && !zero && max < 1.0)
|
|
5023
|
+
return Dict("stable", true, "waitedMs", waited, "lastDiffPct", Math.Round(max, 3),
|
|
5024
|
+
"estimateMs", estimate, "scope", scope, "caretSteady", true,
|
|
5025
|
+
"note", "the only sampled motion is a small periodic change consistent with a blinking caret (~1-2 Hz); treated as stable. lastDiffPct here is the LARGEST probe difference. This still does not verify task completion.");
|
|
5026
|
+
}
|
|
4399
5027
|
return Dict("stable", ok, "waitedMs", waited, "lastDiffPct", Math.Round(lastDiff, 3),
|
|
4400
5028
|
"estimateMs", estimate, "scope", scope,
|
|
4401
5029
|
"note", ok ? "sampled pixels settled; this does not verify task completion"
|
|
@@ -4408,10 +5036,12 @@ namespace DshComputerUse.Worker
|
|
|
4408
5036
|
int pollMs = Math.Max(100, GetI(a, "pollMs", 220));
|
|
4409
5037
|
int step = Math.Max(4, Math.Min(64, GetI(a, "step", 8)));
|
|
4410
5038
|
double diffPct = GetD(a, "diffPct", 0.30);
|
|
4411
|
-
|
|
5039
|
+
int rx, ry, rw, rh;
|
|
5040
|
+
object scope; Func<byte[]> sample = WaitSampler(a, step, out scope, out rx, out ry, out rw, out rh);
|
|
4412
5041
|
byte[] first = sample();
|
|
4413
5042
|
long t0 = NowMs();
|
|
4414
5043
|
int samples = 0;
|
|
5044
|
+
int cols = (rw - 1) / step + 1;
|
|
4415
5045
|
while (NowMs() - t0 < timeoutMs)
|
|
4416
5046
|
{
|
|
4417
5047
|
Thread.Sleep(pollMs);
|
|
@@ -4419,7 +5049,15 @@ namespace DshComputerUse.Worker
|
|
|
4419
5049
|
samples++;
|
|
4420
5050
|
double d = SigDiffPct(first, cur, 8);
|
|
4421
5051
|
if (d > diffPct)
|
|
4422
|
-
|
|
5052
|
+
{
|
|
5053
|
+
// WHERE it changed (test report 2026-11-04): 0.344% cannot tell a sent
|
|
5054
|
+
// message from a caret blink; the bounding box can — input area vs caret.
|
|
5055
|
+
int[] bounds = SigDiffBounds(first, cur, 8, cols, step, rx, ry);
|
|
5056
|
+
Dictionary<string, object> hit = Dict("changed", true, "afterMs", NowMs() - t0,
|
|
5057
|
+
"diffPct", Math.Round(d, 3), "samples", samples, "scope", scope);
|
|
5058
|
+
if (bounds != null) hit["changeBounds"] = Dict("x", bounds[0], "y", bounds[1], "width", bounds[2], "height", bounds[3]);
|
|
5059
|
+
return hit;
|
|
5060
|
+
}
|
|
4423
5061
|
}
|
|
4424
5062
|
return Dict("changed", false, "afterMs", NowMs() - t0, "samples", samples, "scope", scope,
|
|
4425
5063
|
"note", "no sampled pixel change exceeded the threshold; inspect task state before retrying a write");
|
|
@@ -4665,6 +5303,7 @@ namespace DshComputerUse.Worker
|
|
|
4665
5303
|
KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
|
|
4666
5304
|
delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
|
|
4667
5305
|
delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
|
|
5306
|
+
EndActuationForReadback();
|
|
4668
5307
|
Thread.Sleep(30);
|
|
4669
5308
|
Dictionary<string, object> keyOut = Dict("combo", combo, "focus", focus);
|
|
4670
5309
|
if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
|
|
@@ -4717,6 +5356,24 @@ namespace DshComputerUse.Worker
|
|
|
4717
5356
|
{
|
|
4718
5357
|
CheckHumanInput();
|
|
4719
5358
|
ClipboardLease lease = ClipboardLease.Begin(access, text);
|
|
5359
|
+
if (lease == null)
|
|
5360
|
+
{
|
|
5361
|
+
// UNPRESERVABLE CLIPBOARD, 2026-11-04: the previous clipboard holds
|
|
5362
|
+
// private/oversized content we cannot snapshot. Overwrite and paste
|
|
5363
|
+
// anyway — a lost opaque clipboard item is recoverable, while the old
|
|
5364
|
+
// refusal fed CJK text to the unicode fallback and an IME-garbled
|
|
5365
|
+
// message actually went out (WeChat, format 49161).
|
|
5366
|
+
MarkMutation();
|
|
5367
|
+
Clipboard.SetDataObject(text, true);
|
|
5368
|
+
Thread.Sleep(60);
|
|
5369
|
+
Key(Dict("combo", "ctrl+v", "holdMs", 40, "indicatorMs", 800));
|
|
5370
|
+
Dictionary<string, object> downgraded = Dict("mode", "paste", "chars", text.Length, "focus", focus,
|
|
5371
|
+
"clipboardRestore", "skipped-unpreservable-clipboard");
|
|
5372
|
+
downgraded["clipboardWarning"] = "The previous clipboard could not be preserved (private or oversized formats) and was overwritten by this paste. Restore it manually if it mattered.";
|
|
5373
|
+
if (typeWarn != null) downgraded["warning"] = typeWarn;
|
|
5374
|
+
if (_lastRefocus != null) downgraded["refocused"] = _lastRefocus;
|
|
5375
|
+
return downgraded;
|
|
5376
|
+
}
|
|
4720
5377
|
Exception inputError = null;
|
|
4721
5378
|
try
|
|
4722
5379
|
{
|
|
@@ -4820,12 +5477,18 @@ namespace DshComputerUse.Worker
|
|
|
4820
5477
|
|
|
4821
5478
|
static Dictionary<string, object> Windows(Dictionary<string, object> a)
|
|
4822
5479
|
{
|
|
5480
|
+
uint self = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
|
|
4823
5481
|
List<object> list = new List<object>();
|
|
4824
5482
|
Native.EnumWindows(delegate(IntPtr h, IntPtr l)
|
|
4825
5483
|
{
|
|
4826
5484
|
if (!Native.IsWindowVisible(h)) return true;
|
|
4827
5485
|
StringBuilder sb = new StringBuilder(512);
|
|
4828
5486
|
if (Native.GetWindowText(h, sb, 512) == 0) return true;
|
|
5487
|
+
// Hide this worker's own click-through indicator overlay (test report 2026-11-04):
|
|
5488
|
+
// it is a full-screen topmost layer, never a target, and listing it only invites
|
|
5489
|
+
// the model to "activate" the thing that must never take focus.
|
|
5490
|
+
uint pid; Native.GetWindowThreadProcessId(h, out pid);
|
|
5491
|
+
if (pid == self) return true;
|
|
4829
5492
|
list.Add(WinDict(h));
|
|
4830
5493
|
return true;
|
|
4831
5494
|
}, IntPtr.Zero);
|
|
@@ -4886,15 +5549,16 @@ namespace DshComputerUse.Worker
|
|
|
4886
5549
|
// The whole ladder now lives in RefocusWindow (shared with the keyboard focus
|
|
4887
5550
|
// guard, which must re-assert the same window before typing).
|
|
4888
5551
|
string how = RefocusWindow(h, GetB(a, "clickFallback", true));
|
|
4889
|
-
|
|
5552
|
+
uint confirmedPid = 0;
|
|
5553
|
+
if (how == null || !Native.IsWindow(h) || !IsForeground(h) || (confirmedPid = PidOf(h)) == 0) how = "failed";
|
|
4890
5554
|
else
|
|
4891
5555
|
{
|
|
4892
5556
|
SetSticky(h);
|
|
4893
|
-
_kickoffFocus.Confirm(h.ToInt64(),
|
|
5557
|
+
_kickoffFocus.Confirm(h.ToInt64(), confirmedPid);
|
|
4894
5558
|
}
|
|
4895
5559
|
CheckHumanInput();
|
|
4896
5560
|
return Dict("activated", how != "failed", "activatedVia", how,
|
|
4897
|
-
"hwnd", h.ToInt64(),
|
|
5561
|
+
"hwnd", h.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
|
|
4898
5562
|
"hint", how == "failed"
|
|
4899
5563
|
? "foreground lock refused every method; click the window (or its taskbar button) and retry"
|
|
4900
5564
|
: null);
|
|
@@ -4933,13 +5597,21 @@ namespace DshComputerUse.Worker
|
|
|
4933
5597
|
IntPtr fg = TopLevel(Native.GetForegroundWindow());
|
|
4934
5598
|
string problem = _kickoffFocus.CaptureProblem(Panic.CycleLit, Panic.Stopped, Panic.Exited,
|
|
4935
5599
|
target != IntPtr.Zero && Native.IsWindow(target), fg.ToInt64(), PidOf(fg), IsHostWindow(fg));
|
|
4936
|
-
if (problem != null)
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
"
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
5600
|
+
if (problem != null)
|
|
5601
|
+
{
|
|
5602
|
+
// NAME THE ACTUAL FOREGROUND (test report 2026-11-04): "identify the intended
|
|
5603
|
+
// window" without saying who holds the foreground forced an extra computer_state
|
|
5604
|
+
// round trip just to learn the one fact this refusal already measured.
|
|
5605
|
+
StringBuilder fgTitle = new StringBuilder(256);
|
|
5606
|
+
if (fg != IntPtr.Zero) Native.GetWindowText(fg, fgTitle, fgTitle.Capacity);
|
|
5607
|
+
throw new Exception(
|
|
5608
|
+
"KICKOFF-FOCUS-REQUIRED: " + problem + ". No screenshot was returned. " +
|
|
5609
|
+
"Foreground now: hwnd=" + fg.ToInt64() + ", pid=" + PidOf(fg) +
|
|
5610
|
+
", title=\"" + fgTitle.ToString() + "\". " +
|
|
5611
|
+
"If the reported foreground HWND and PID match the target already verified for this task, reactivate it. " +
|
|
5612
|
+
"Otherwise identify the intended window with computer_state {windows:true}. " +
|
|
5613
|
+
"Use computer_window {hwnd:...,op:'activate'} and require activated:true before retrying the screenshot.");
|
|
5614
|
+
}
|
|
4943
5615
|
}
|
|
4944
5616
|
|
|
4945
5617
|
static IntPtr TopLevel(IntPtr h)
|
|
@@ -5550,8 +6222,8 @@ namespace DshComputerUse.Worker
|
|
|
5550
6222
|
{
|
|
5551
6223
|
CheckHumanInput();
|
|
5552
6224
|
if (h == IntPtr.Zero || !Native.IsWindow(h)) return null;
|
|
5553
|
-
if (!Native.IsWindowVisible(h)) Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE);
|
|
5554
|
-
if (Native.IsIconic(h)) Native.ShowWindow(h, Native.SW_RESTORE);
|
|
6225
|
+
if (!Native.IsWindowVisible(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE); }
|
|
6226
|
+
if (Native.IsIconic(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_RESTORE); }
|
|
5555
6227
|
Thread.Sleep(40);
|
|
5556
6228
|
CheckHumanInput();
|
|
5557
6229
|
if (IsForeground(h)) return "already-foreground";
|
|
@@ -6018,8 +6690,7 @@ namespace DshComputerUse.Worker
|
|
|
6018
6690
|
", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
|
|
6019
6691
|
", index=" + index + ") among " + seen + " elements");
|
|
6020
6692
|
|
|
6021
|
-
//
|
|
6022
|
-
// fight, so the quiet gate is skipped on purpose.
|
|
6693
|
+
// A UIA mutation shares the same actual-action boundary and takeover checks.
|
|
6023
6694
|
// A selector WITHOUT hwnd searches the whole desktop, so there the element that
|
|
6024
6695
|
// actually matched decides the victim - and a host control is never driven through
|
|
6025
6696
|
// its automation pattern either.
|
|
@@ -6028,11 +6699,9 @@ namespace DshComputerUse.Worker
|
|
|
6028
6699
|
HostGuardPid(foundPid, "uiaAct");
|
|
6029
6700
|
Panic.Check("uiaAct");
|
|
6030
6701
|
CheckHumanInput();
|
|
6031
|
-
Glow.Touch(GetI(a, "indicatorMs", 1200)); // same reason: see AgentActing
|
|
6032
6702
|
|
|
6033
6703
|
object pat = null;
|
|
6034
6704
|
string via = null;
|
|
6035
|
-
MarkMutation();
|
|
6036
6705
|
switch (action)
|
|
6037
6706
|
{
|
|
6038
6707
|
case "invoke":
|
|
@@ -6064,6 +6733,7 @@ namespace DshComputerUse.Worker
|
|
|
6064
6733
|
default:
|
|
6065
6734
|
throw new Exception("action must be invoke|select|expand|collapse|toggle|setValue|focus|scrollIntoView");
|
|
6066
6735
|
}
|
|
6736
|
+
EndActuationForReadback();
|
|
6067
6737
|
if (via == null)
|
|
6068
6738
|
throw new Exception("element does not support '" + action + "': " + ElemDict(found)["name"]);
|
|
6069
6739
|
|
|
@@ -6356,6 +7026,7 @@ namespace DshComputerUse.Worker
|
|
|
6356
7026
|
static Dictionary<string, object> Err(Exception error)
|
|
6357
7027
|
{
|
|
6358
7028
|
var result = Err(error.Message);
|
|
7029
|
+
if (error.Data["glowHealth"] != null) result["glowHealth"] = error.Data["glowHealth"];
|
|
6359
7030
|
if (error.Data["code"] != null)
|
|
6360
7031
|
{
|
|
6361
7032
|
result["code"] = (string)error.Data["code"];
|
|
@@ -6373,3 +7044,79 @@ namespace DshComputerUse.Worker
|
|
|
6373
7044
|
static void WriteLine(string s) { lock (OutLock) { Console.Out.WriteLine(s); Console.Out.Flush(); } }
|
|
6374
7045
|
}
|
|
6375
7046
|
}
|
|
7047
|
+
|
|
7048
|
+
// Integrated verbatim into worker.cs by the native worker build. Tests compile this
|
|
7049
|
+
// production class without starting hooks, overlays or desktop input.
|
|
7050
|
+
namespace DshComputerUse.Worker
|
|
7051
|
+
{
|
|
7052
|
+
public sealed class DesktopOwnershipLease : System.IDisposable
|
|
7053
|
+
{
|
|
7054
|
+
readonly object sync = new object();
|
|
7055
|
+
readonly string path;
|
|
7056
|
+
System.IO.FileStream stream;
|
|
7057
|
+
public DesktopOwnershipLease(string file) { path = file; }
|
|
7058
|
+
public bool Owned { get { lock (sync) { return stream != null; } } }
|
|
7059
|
+
public static string DefaultPath()
|
|
7060
|
+
{
|
|
7061
|
+
string isolated = System.Environment.GetEnvironmentVariable("DSH_COMPUTER_USE_OWNER_FILE");
|
|
7062
|
+
if (!string.IsNullOrEmpty(isolated)) return isolated;
|
|
7063
|
+
return System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData),
|
|
7064
|
+
"dsh-computer-use", "desktop-" + System.Diagnostics.Process.GetCurrentProcess().SessionId + ".owner");
|
|
7065
|
+
}
|
|
7066
|
+
public bool TryAcquire(string owner, out string occupiedBy)
|
|
7067
|
+
{
|
|
7068
|
+
lock (sync)
|
|
7069
|
+
{
|
|
7070
|
+
occupiedBy = "";
|
|
7071
|
+
if (stream != null) return true;
|
|
7072
|
+
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)));
|
|
7073
|
+
System.IO.FileStream opened;
|
|
7074
|
+
try
|
|
7075
|
+
{
|
|
7076
|
+
// A persistent handle is the lock. Never delete the pathname, use a
|
|
7077
|
+
// timer, or steal based on PID: Windows releases it when the process dies.
|
|
7078
|
+
opened = new System.IO.FileStream(path, System.IO.FileMode.OpenOrCreate,
|
|
7079
|
+
System.IO.FileAccess.ReadWrite, System.IO.FileShare.Read);
|
|
7080
|
+
}
|
|
7081
|
+
catch (System.IO.IOException error)
|
|
7082
|
+
{
|
|
7083
|
+
int code = System.Runtime.InteropServices.Marshal.GetHRForException(error) & 0xFFFF;
|
|
7084
|
+
if (code != 32 && code != 33) throw;
|
|
7085
|
+
occupiedBy = ReadOwner();
|
|
7086
|
+
return false;
|
|
7087
|
+
}
|
|
7088
|
+
try
|
|
7089
|
+
{
|
|
7090
|
+
byte[] record = System.Text.Encoding.UTF8.GetBytes("pid=" + System.Diagnostics.Process.GetCurrentProcess().Id +
|
|
7091
|
+
"\nowner=" + (owner ?? "worker").Replace("\r", " ").Replace("\n", " ") + "\n");
|
|
7092
|
+
opened.SetLength(0); opened.Write(record, 0, record.Length); opened.Flush();
|
|
7093
|
+
stream = opened;
|
|
7094
|
+
return true;
|
|
7095
|
+
}
|
|
7096
|
+
catch { opened.Dispose(); throw; }
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
string ReadOwner()
|
|
7100
|
+
{
|
|
7101
|
+
try
|
|
7102
|
+
{
|
|
7103
|
+
using (var input = new System.IO.FileStream(path, System.IO.FileMode.Open,
|
|
7104
|
+
System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
|
|
7105
|
+
using (var reader = new System.IO.StreamReader(input))
|
|
7106
|
+
{
|
|
7107
|
+
char[] buffer = new char[512];
|
|
7108
|
+
return new string(buffer, 0, reader.Read(buffer, 0, buffer.Length)).Trim();
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
7111
|
+
catch { return "another live computer-use worker"; }
|
|
7112
|
+
}
|
|
7113
|
+
public void Dispose()
|
|
7114
|
+
{
|
|
7115
|
+
lock (sync)
|
|
7116
|
+
{
|
|
7117
|
+
if (stream == null) return;
|
|
7118
|
+
stream.Dispose(); stream = null;
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
}
|