dsh-plugin-live-shot 0.7.0-beta.4 → 0.7.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,30 @@
1
1
  # DSH Shot
2
2
 
3
+ [![DSH Market 已收录](https://raw.githubusercontent.com/2BingLing/dsh-market/master/assets/readme/badge-listed-zh.svg)](https://dsh.market/)
4
+
3
5
  Windows screenshot and annotation plugin for DeepSeek Harness. Select the **live desktop**, capture only the selected region after releasing the mouse, then insert the image directly into the conversation draft.
4
6
 
5
7
  Windows 实时桌面截图插件:全局热键、窗口识别、选区调整、标注,以及直接插入 DSH 输入框。
6
8
 
7
- [Windows ZIP / Releases](https://github.com/TangerineSplendor/dsh-screenshot/releases) · [npm](https://www.npmjs.com/package/dsh-plugin-live-shot) · [Market submission](https://github.com/2BingLing/dsh-market/issues/283)
9
+ [Download Windows ZIP 0.7.0-beta.5](https://github.com/TangerineSplendor/dsh-screenshot/releases/download/v0.7.0-beta.5/dsh-shot-0.7.0-beta.5-windows-x64.zip) · [Release notes](https://github.com/TangerineSplendor/dsh-screenshot/releases/tag/v0.7.0-beta.5) · [DSH Market 已确认收录](https://github.com/2BingLing/dsh-market/issues/283#issuecomment-6379257821)
10
+
11
+ **当前测试版为 `0.7.0-beta.5`。此版改用 Windows 原生预框选,改善本机视频/游戏框选可见性;游戏预框选 Esc 取消仍有已知问题,暂用右键退出。旧 beta.2 的相机/插图安装缺陷已在此前版本修复,该旧版已下架。**
8
12
 
9
- **Beta: 0.7.0-beta.4. Windows 10/11 x64, official DSH desktop only.** Tested on the developer's machine with isolated profiles; compatibility on other machines still needs feedback. macOS, Linux, browser-only DSH and remote Hosts are unsupported.
13
+ **Beta: 0.7.0-beta.5. Windows 10/11 x64, official DSH desktop only.** Tested on the developer's machine with isolated profiles; compatibility on other machines still needs feedback. macOS, Linux, browser-only DSH and remote Hosts are unsupported.
10
14
 
11
15
  ## Install / 安装
12
16
 
13
17
  Install and open the official Windows DSH desktop app at least once. Keep DSH running during setup so the installer can find its executable.
14
18
 
15
- **ZIP, no additional Node.js required:** download the Windows ZIP from [Releases](https://github.com/TangerineSplendor/dsh-screenshot/releases), extract it completely and run `install.bat`. If prompted, select your installed `DeepSeek Harness.exe`. Quit DSH completely from its tray menu and reopen it.
19
+ **ZIP, no additional Node.js required:** download [dsh-shot-0.7.0-beta.5-windows-x64.zip](https://github.com/TangerineSplendor/dsh-screenshot/releases/download/v0.7.0-beta.5/dsh-shot-0.7.0-beta.5-windows-x64.zip), extract it completely and run `install.bat`. The installer must print `Installed 0.7.0-beta.5`. If prompted, select your installed `DeepSeek Harness.exe`. Quit DSH completely from its tray menu and reopen it. Do not reuse a beta.2 ZIP or its extracted installer.
16
20
 
17
21
  **npm, for users who already have Node.js:**
18
22
 
19
23
  ```powershell
20
- npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.4 dsh-shot install
24
+ npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.5 dsh-shot install
21
25
  ```
22
26
 
23
- Use `dsh-shot install -DshExe "C:\Apps\DeepSeek Harness\DeepSeek Harness.exe"` if automatic detection fails. `-ProfileDir`, `-DshHome` and `-DataDir` support non-default installations. Then fully quit and reopen DSH.
27
+ Alternatively, download the Release TGZ and use `npx --yes --package=./dsh-plugin-live-shot-0.7.0-beta.5.tgz dsh-shot install` from its folder. Use `dsh-shot install -DshExe "C:\Apps\DeepSeek Harness\DeepSeek Harness.exe"` if automatic detection fails. `-ProfileDir`, `-DshHome` and `-DataDir` support non-default installations. Then fully quit and reopen DSH.
24
28
 
25
29
  ZIP, npm and standard bundle installations all use `dsh-plugin-live-shot` as the installed directory, Loader entry, manifest name and Client module ID. The installer migrates the old `@local/dsh-shot` entry while preserving its settings and custom data directory. Beta.2 ZIP installations had a name mismatch that prevented the camera and image delivery from loading; upgrade with this installer and completely restart DSH. Standard bundle installations prepare the local runtime on first startup; if automatic discovery fails, use the explicit installer instead.
26
30
 
@@ -31,10 +35,16 @@ ZIP, npm and standard bundle installations all use `dsh-plugin-live-shot` as the
31
35
  - Click the camera in the composer, or press **Ctrl+Shift+X** from any program. The desktop remains live while selecting; no full-screen preview is captured.
32
36
  - Hover a window and click to select it, or drag a custom region. After release, move the selection with **↔** or resize its eight handles. Releasing a changed selection captures the new region.
33
37
  - Rectangle, ellipse, arrow, pen, mosaic, text, undo and PNG save are available.
34
- - **Enter / double-click / Done** inserts into the active DSH composer. **Ctrl+C** copies only. **Esc / right-click** cancels.
38
+ - **Enter / double-click / Done** inserts into the active DSH composer. **Ctrl+C** copies only. **Right-click** cancels. Esc works in the editor; preselection Esc in games remains a known issue.
35
39
  - **隐藏 DSH** remembers whether to hide DSH during capture. **Shift+camera** inverts that choice for one capture. Completion or cancellation restores hidden windows.
36
40
  - **⚙** changes the global shortcut. A conflict or save failure preserves the previous binding.
37
41
 
42
+ ## Known issue in this beta / 本版已知问题
43
+
44
+ 游戏预框选阶段 Esc 可能无效,暂用右键退出;松手后的编辑器 Esc 已获本机反馈正常。⚙ 中新增手动启用的「管理员游戏兼容」试验入口,需要本人确认 Windows 提示,仅本次辅助进程运行有效。**完整插件实测仍未解决该 Esc 问题,不能将此入口视为已修复或保证有效。**普通截图和安装不请求管理员权限。编辑后移动/调整选区仍使用 Electron,游戏焦点行为尚待进一步验证。
45
+
46
+ 本机用户已确认 B 站视频框选期间恢复可见、游戏原生拖选可用;这不是所有游戏或所有机器的兼容性保证。
47
+
38
48
  ## Restart acceptance / 重启后三步验收
39
49
 
40
50
  1. Use the camera and the global hotkey; select over a moving clock or video and confirm the desktop stays live.
@@ -46,7 +56,7 @@ ZIP, npm and standard bundle installations all use `dsh-plugin-live-shot` as the
46
56
  For the explicit installer, run `uninstall.bat`, or:
47
57
 
48
58
  ```powershell
49
- npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.4 dsh-shot uninstall
59
+ npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.5 dsh-shot uninstall
50
60
  ```
51
61
 
52
62
  Then completely restart DSH. Default removal retains settings and local runtime. The optional `-RemoveData` also deletes the dedicated data directory, including images saved in its `captures` folder. For a non-default profile, pass the same `-ProfileDir` used to install. If installed as a standard DSH bundle, remove it through that bundle manager instead.
package/client.js CHANGED
@@ -7,8 +7,10 @@ window.__ModuleLoader__.load({
7
7
  let running = false, stopped = false, retryAt = 0, errorShown = '', delivered = new Set();
8
8
  let hidePreference = false, preferenceLoaded = false;
9
9
  let hotkeyPreference = 'Ctrl+Shift+X', shortcutProblem = '', hotkeyRegistered = false;
10
+ let gameCompatibilityReady = false, gameCompatibilityEnabling = false;
10
11
  const preferenceState = () => ({ value: hidePreference, ready: preferenceLoaded,
11
- hotkey: hotkeyPreference, problem: shortcutProblem, registered: hotkeyRegistered });
12
+ hotkey: hotkeyPreference, problem: shortcutProblem, registered: hotkeyRegistered,
13
+ gameCompatibilityReady, gameCompatibilityEnabling });
12
14
  const preferenceListeners = new Set();
13
15
  function toast(message) {
14
16
  let el = document.getElementById('dsh-shot-toast');
@@ -40,6 +42,8 @@ window.__ModuleLoader__.load({
40
42
  if (typeof data.hideDsh === 'boolean') {
41
43
  hidePreference = data.hideDsh; preferenceLoaded = true;
42
44
  hotkeyPreference = data.hotkey || hotkeyPreference; shortcutProblem = data.hotkeyError || ''; hotkeyRegistered = !!data.registered;
45
+ gameCompatibilityReady = data.gameCompatibilityReady === true;
46
+ gameCompatibilityEnabling = data.gameCompatibilityEnabling === true;
43
47
  for (const listener of preferenceListeners) listener(preferenceState());
44
48
  }
45
49
  return data;
@@ -92,6 +96,11 @@ window.__ModuleLoader__.load({
92
96
  catch (error) { setSettingsError(error.message); }
93
97
  finally { setSaving(false); }
94
98
  };
99
+ const enableGameCompatibility = async () => {
100
+ setSettingsError('');
101
+ try { await post('gameCompatibility'); toast('请确认 Windows 提示,启用后即可返回游戏截图'); }
102
+ catch (error) { setSettingsError(error.message); }
103
+ };
95
104
  return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, position: 'relative' } },
96
105
  h('button', { type: 'button', title: '实时桌面截图(全局 ' + preference.hotkey + ')\nShift+点击:本次反向切换隐藏 DSH',
97
106
  'aria-label': '截图', onClick: event => {
@@ -116,6 +125,11 @@ window.__ModuleLoader__.load({
116
125
  onChange: event => setShortcutDraft(event.target.value),
117
126
  onKeyDown: event => { if (event.key === 'Enter') { event.preventDefault(); void saveShortcut(); } if (event.key === 'Escape') setSettingsOpen(false); } })),
118
127
  h('div', { style: { fontSize: 11, color: '#666', margin: '6px 0' } }, '例如 Ctrl+Alt+A、Ctrl+Shift+X、F8'),
128
+ h('div', { style: { borderTop: '1px solid #ddd', paddingTop: 8, marginBottom: 8 } },
129
+ h('button', { type: 'button', disabled: preference.gameCompatibilityReady || preference.gameCompatibilityEnabling,
130
+ onClick: () => { void enableGameCompatibility(); } },
131
+ preference.gameCompatibilityReady ? '游戏兼容已启用' : preference.gameCompatibilityEnabling ? '等待 Windows 确认…' : '启用管理员游戏兼容'),
132
+ h('div', { style: { fontSize: 11, color: '#666', marginTop: 5 } }, '用于游戏中 Esc 无法取消选框的情况。需确认 Windows 提示;仅本次运行有效。')),
119
133
  (settingsError || preference.problem) && h('div', { role: 'alert', style: { color: '#b22', marginBottom: 6 } }, settingsError || preference.problem),
120
134
  h('div', { style: { display: 'flex', gap: 6 } },
121
135
  h('button', { type: 'button', disabled: saving, onClick: () => { void saveShortcut(); } }, '保存'),
@@ -13,8 +13,10 @@ export function apply(ctx) {
13
13
  const require = createRequire(import.meta.url);
14
14
  const { app, BrowserWindow, screen, globalShortcut, ipcMain, clipboard, nativeImage, dialog, Notification } = require('electron');
15
15
  const worker = ctx.capture || new CaptureWorker();
16
+ const nativeSelection = ctx.nativeSelection === true;
16
17
  const channel = 'dsh-shot-' + randomUUID();
17
18
  let hotkey = DEFAULT_HOTKEY, hotkeyError = '';
19
+ let gameCompatibilityReady = false, gameCompatibilityEnabling = false;
18
20
  let win, active = false, phase = 'idle', display, startPoint, selection, poll, disposed = false;
19
21
  let registered = false, lastError = '', result = null, owner = null, readyMessage = null;
20
22
  let hideDsh = false, hiddenSession = null, opening = false, restoring = Promise.resolve(), settingsWrite = Promise.resolve();
@@ -27,7 +29,9 @@ export function apply(ctx) {
27
29
  const send = data => { if (win && !win.isDestroyed()) win.webContents.send(channel, data); };
28
30
  const clamp = p => ({ x: Math.max(0, Math.min(display.bounds.width, p.x)), y: Math.max(0, Math.min(display.bounds.height, p.y)) });
29
31
  function end() {
32
+ const cancelPicker = nativeSelection && phase === 'selecting';
30
33
  active = false; phase = 'idle'; clearInterval(poll); poll = null;
34
+ if (cancelPicker) void worker.run({ op: 'cancelPick' }).catch(() => {});
31
35
  generation++;
32
36
  if (readyMessage) { clearTimeout(readyMessage.timer); readyMessage.reject(new Error('Screenshot cancelled')); readyMessage = null; }
33
37
  if (win && !win.isDestroyed()) win.hide();
@@ -55,6 +59,9 @@ export function apply(ctx) {
55
59
  webPreferences: { preload: fileURLToPath(new URL('./overlay_preload.cjs', import.meta.url)),
56
60
  contextIsolation: true, nodeIntegration: false, sandbox: true,
57
61
  backgroundThrottling: false, additionalArguments: ['--shot-channel=' + channel] } });
62
+ // CSS alpha alone does not mark the native HWND as translucent for other
63
+ // applications' occlusion tracking. Preserve mouse input with native alpha.
64
+ if (process.platform === 'win32') win.setOpacity(0.99);
58
65
  win.setAlwaysOnTop(true, 'screen-saver');
59
66
  win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
60
67
  win.webContents.on('will-navigate', (e, url) => { if (!url.startsWith('file:')) e.preventDefault(); });
@@ -98,7 +105,20 @@ export function apply(ctx) {
98
105
  await present({ type: 'reset', width: display.bounds.width, height: display.bounds.height });
99
106
  if (!active) return;
100
107
  phase = 'selecting';
101
- win.show(); win.focus();
108
+ if (nativeSelection) {
109
+ const session = generation;
110
+ const bounds = screen.dipToScreenRect(win, display.bounds);
111
+ void (async () => {
112
+ const picked = await worker.run({ op: 'pick', ...bounds });
113
+ if (!active || disposed || generation !== session) return;
114
+ if (!picked.selected) { await end(); return; }
115
+ const physical = intersect(picked, bounds);
116
+ if (!physical) throw new Error('Selected region is outside the display');
117
+ const dip = screen.screenToDipRect(win, physical);
118
+ selection = { x: dip.x - display.bounds.x, y: dip.y - display.bounds.y, width: dip.width, height: dip.height };
119
+ await captureSelection(false, physical);
120
+ })().catch(error => { if (active && generation === session) report(error); });
121
+ } else { win.show(); win.focus(); }
102
122
  } catch (error) { await end(); throw error; }
103
123
  finally { opening = false; }
104
124
  }
@@ -161,14 +181,14 @@ export function apply(ctx) {
161
181
  }
162
182
  await captureSelection(preserve);
163
183
  }
164
- async function captureSelection(preserve) {
184
+ async function captureSelection(preserve, physicalRect) {
165
185
  const session = generation;
166
186
  phase = 'capturing';
167
187
  // Convert a rectangle on ONE monitor; multiplying a virtual-desktop union
168
188
  // by a single scaleFactor is incorrect with mixed-DPI monitors.
169
189
  const global = { x: display.bounds.x + selection.x, y: display.bounds.y + selection.y,
170
190
  width: selection.width, height: selection.height };
171
- const rect = screen.dipToScreenRect(win, global);
191
+ const rect = physicalRect || screen.dipToScreenRect(win, global);
172
192
  win.hide();
173
193
  await new Promise(resolve => setTimeout(resolve, 70));
174
194
  if (!active || disposed || session !== generation) return;
@@ -259,12 +279,27 @@ export function apply(ctx) {
259
279
  });
260
280
  settingsWrite = save.catch(() => {}); await save;
261
281
  }
282
+ if (body.op === 'gameCompatibility') {
283
+ await initial;
284
+ if (!nativeSelection) throw new Error('当前截图服务不支持游戏兼容,请重新启动 DSH');
285
+ if (active || opening) throw new Error('请先结束当前截图');
286
+ if (!gameCompatibilityEnabling && !gameCompatibilityReady) {
287
+ gameCompatibilityEnabling = true; lastError = '';
288
+ void worker.run({ op: 'enableAdminEscape' }).then(status => { gameCompatibilityReady = status.ready === true; })
289
+ .catch(error => { lastError = error.message; })
290
+ .finally(() => { gameCompatibilityEnabling = false; });
291
+ }
292
+ }
293
+ if (body.op === 'poll' && nativeSelection && !gameCompatibilityEnabling) {
294
+ const status = await worker.run({ op: 'adminEscapeStatus' });
295
+ gameCompatibilityReady = status.ready === true;
296
+ }
262
297
  if (body.op === 'start') await begin(body.client, body.hideDsh);
263
298
  if (body.op === 'cancel' && owner === body.client) await end();
264
299
  if (body.op === 'ack' && result?.id === body.id && (!result.owner || result.owner === body.client)) result = null;
265
300
  if (body.op === 'failure') lastError = 'Composer insertion failed; screenshot retained for retry.';
266
301
  if (result && (!result.owner || (!clients.has(result.owner) && body.focused))) result.owner = body.client;
267
- return Response.json({ registered, phase, hideDsh, dshHidden: !!hiddenSession?.windows.length, hotkey, hotkeyError, error: lastError,
302
+ return Response.json({ registered, phase, hideDsh, gameCompatibilityReady, gameCompatibilityEnabling, dshHidden: !!hiddenSession?.windows.length, hotkey, hotkeyError, error: lastError,
268
303
  result: result?.owner === body.client && body.op === 'poll' && !body.skipImage ? { id: result.id, png: result.png } : null });
269
304
  } catch (e) { return Response.json({ error: e.message }, { status: 400 }); }
270
305
  } }), 'dsh-shot: control');
@@ -0,0 +1,63 @@
1
+ using System;
2
+ using System.Runtime.InteropServices;
3
+ using System.Windows.Forms;
4
+
5
+ // The elevated process can only request cancellation of its original picker.
6
+ // It accepts no commands, files, capture requests, or keyboard text.
7
+ public sealed class AdminEscapeBridge : ApplicationContext {
8
+ private delegate IntPtr KeyboardProc(int code,IntPtr message,IntPtr data);
9
+ [DllImport("user32.dll")] private static extern bool IsWindow(IntPtr window);
10
+ [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr window,out uint pid);
11
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] private static extern IntPtr GetProp(IntPtr window,string name);
12
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] private static extern bool SetProp(IntPtr window,string name,IntPtr value);
13
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] private static extern IntPtr RemoveProp(IntPtr window,string name);
14
+ [DllImport("user32.dll")] private static extern bool PostMessage(IntPtr window,uint message,IntPtr param,IntPtr data);
15
+ [DllImport("user32.dll")] private static extern IntPtr SetWindowsHookEx(int kind,KeyboardProc callback,IntPtr module,uint thread);
16
+ [DllImport("user32.dll")] private static extern bool UnhookWindowsHookEx(IntPtr hook);
17
+ [DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hook,int code,IntPtr message,IntPtr data);
18
+ [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string name);
19
+ private readonly IntPtr target,nonce;
20
+ private readonly uint owner;
21
+ private readonly Timer timer=new Timer();
22
+ private readonly KeyboardProc callback;
23
+ private readonly DateTime started=DateTime.UtcNow;
24
+ private IntPtr hook;
25
+ private bool active;
26
+ private bool ValidTarget() {
27
+ uint actual;
28
+ return IsWindow(target)&&GetWindowThreadProcessId(target,out actual)!=0&&actual==owner&&GetProp(target,"DshShotEscapeOwner")==nonce;
29
+ }
30
+ private AdminEscapeBridge(int pid,long window,int value) {
31
+ owner=(uint)pid;target=new IntPtr(window);nonce=new IntPtr(value);
32
+ if(pid<=0||window==0||value<=0||!ValidTarget())throw new InvalidOperationException("Invalid picker owner");
33
+ if(GetProp(target,"DshShotEscapeAdmin")==nonce)throw new InvalidOperationException("Escape bridge already active");
34
+ callback=delegate(int code,IntPtr message,IntPtr data) {
35
+ if(active&&code>=0&&Marshal.ReadInt32(data)==27) {
36
+ int kind=message.ToInt32();
37
+ if((kind==0x100||kind==0x104)&&PostMessage(target,0x8039,nonce,IntPtr.Zero))return new IntPtr(1);
38
+ }
39
+ return CallNextHookEx(hook,code,message,data);
40
+ };
41
+ if(!SetProp(target,"DshShotEscapeAdmin",nonce))throw new InvalidOperationException("Could not acknowledge picker owner");
42
+ timer.Interval=25;
43
+ timer.Tick+=delegate {
44
+ if(!ValidTarget()||(DateTime.UtcNow-started).TotalHours>=12){ExitThread();return;}
45
+ active=GetProp(target,"DshShotEscapeActive")==nonce;
46
+ if(active&&hook==IntPtr.Zero)hook=SetWindowsHookEx(13,callback,GetModuleHandle(null),0);
47
+ if(!active&&hook!=IntPtr.Zero){UnhookWindowsHookEx(hook);hook=IntPtr.Zero;}
48
+ if(hook!=IntPtr.Zero)SetProp(target,"DshShotEscapeHook",nonce);
49
+ else RemoveProp(target,"DshShotEscapeHook");
50
+ };
51
+ timer.Start();
52
+ }
53
+ protected override void ExitThreadCore() {
54
+ timer.Stop();timer.Dispose();active=false;
55
+ if(hook!=IntPtr.Zero){UnhookWindowsHookEx(hook);hook=IntPtr.Zero;}
56
+ if(ValidTarget()){RemoveProp(target,"DshShotEscapeAdmin");RemoveProp(target,"DshShotEscapeHook");}
57
+ base.ExitThreadCore();
58
+ }
59
+ public static void Run(int pid,long window,int nonce) {
60
+ if(!new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent()).IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator))throw new InvalidOperationException("Administrator confirmation required");
61
+ Application.Run(new AdminEscapeBridge(pid,window,nonce));
62
+ }
63
+ }
@@ -0,0 +1,6 @@
1
+ #Requires -Version 5.1
2
+ param([int]$OwnerPid,[long]$OwnerWindow,[int]$Nonce)
3
+ $ErrorActionPreference = 'Stop'
4
+ Add-Type -AssemblyName System.Windows.Forms
5
+ Add-Type -Path (Join-Path $PSScriptRoot 'escape_bridge.cs') -ReferencedAssemblies System.Windows.Forms
6
+ [AdminEscapeBridge]::Run($OwnerPid,$OwnerWindow,$Nonce)
package/helper_main.cjs CHANGED
@@ -32,9 +32,21 @@ else {
32
32
  }
33
33
  app.on('window-all-closed', () => {});
34
34
  app.on('before-quit', event => { if (!stopping) { event.preventDefault(); void stop(); } });
35
- import(pathToFileURL(path.join(__dirname, 'electron_controller.js')).href).then(module => {
35
+ Promise.all([
36
+ import(pathToFileURL(path.join(__dirname, 'electron_controller.js')).href),
37
+ import(pathToFileURL(path.join(__dirname, 'picker.js')).href),
38
+ ]).then(([module, { NativePicker }]) => {
39
+ const picker = new NativePicker();
40
+ cleanups.push(() => picker.stop());
36
41
  module.apply({
37
- capture: { start: () => native({ op: 'warm' }), run: command => native(command), stop: () => {} },
42
+ nativeSelection: true,
43
+ capture: {
44
+ start: () => Promise.all([native({ op: 'warm' }), picker.start()]),
45
+ run: command => ['enableAdminEscape','adminEscapeStatus'].includes(command.op) ? picker.run(command)
46
+ : command.op === 'pick' ? picker.run({ ...command, op: 'select' })
47
+ : command.op === 'cancelPick' ? picker.run({ op: 'cancel' }) : native(command),
48
+ stop: () => {},
49
+ },
38
50
  connection: { fetch: { register: config => { route = config.fetch; return () => {}; } } },
39
51
  effect: fn => { const cleanup = fn(); if (typeof cleanup === 'function') cleanups.push(cleanup); },
40
52
  });
@@ -0,0 +1,286 @@
1
+ using System;
2
+ using System.Drawing;
3
+ using System.Drawing.Drawing2D;
4
+ using System.Drawing.Imaging;
5
+ using System.Runtime.InteropServices;
6
+ using System.Windows.Forms;
7
+ using System.Collections.Generic;
8
+ using System.Threading;
9
+ using System.Web.Script.Serialization;
10
+
11
+ public static class PickerApi {
12
+ public delegate IntPtr KeyboardProc(int code,IntPtr message,IntPtr data);
13
+ [DllImport("user32.dll")] public static extern IntPtr SetWindowsHookEx(int kind,KeyboardProc callback,IntPtr module,uint thread);
14
+ [DllImport("user32.dll")] public static extern bool UnhookWindowsHookEx(IntPtr hook);
15
+ [DllImport("user32.dll")] public static extern IntPtr CallNextHookEx(IntPtr hook,int code,IntPtr message,IntPtr data);
16
+ [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string name);
17
+ [StructLayout(LayoutKind.Sequential)] public struct RawDevice { public ushort page,usage; public uint flags; public IntPtr target; }
18
+ [StructLayout(LayoutKind.Sequential)] public struct RawHeader { public uint type,size; public IntPtr device,param; }
19
+ [DllImport("user32.dll",SetLastError=true)] public static extern bool RegisterRawInputDevices(RawDevice[] devices,uint count,uint size);
20
+ [DllImport("user32.dll")] public static extern uint GetRawInputData(IntPtr input,uint command,IntPtr data,ref uint size,uint headerSize);
21
+ [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left,Top,Right,Bottom; }
22
+ public delegate bool EnumProc(IntPtr handle,IntPtr data);
23
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc callback,IntPtr data);
24
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr handle);
25
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr handle);
26
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr handle,out uint pid);
27
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr handle,out Rect rect);
28
+ [DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr handle,int attribute,out Rect rect,int size);
29
+ [DllImport("dwmapi.dll",EntryPoint="DwmGetWindowAttribute")] public static extern int GetCloaked(IntPtr handle,int attribute,out int cloaked,int size);
30
+ public static Rectangle WindowAt(Point point,Rectangle area) {
31
+ Rectangle result=Rectangle.Empty;
32
+ uint self=(uint)System.Diagnostics.Process.GetCurrentProcess().Id;
33
+ EnumWindows(delegate(IntPtr handle,IntPtr data){
34
+ uint pid;GetWindowThreadProcessId(handle,out pid);int cloaked;
35
+ GetCloaked(handle,14,out cloaked,4);
36
+ if(pid==self||!IsWindowVisible(handle)||IsIconic(handle)||cloaked!=0)return true;
37
+ Rect r;if(DwmGetWindowAttribute(handle,9,out r,16)!=0&&!GetWindowRect(handle,out r))return true;
38
+ Rectangle rect=Rectangle.FromLTRB(r.Left,r.Top,r.Right,r.Bottom);
39
+ if(rect.Width<2||rect.Height<2||!rect.Contains(point))return true;
40
+ result=Rectangle.Intersect(rect,area);return false;
41
+ },IntPtr.Zero);
42
+ return result;
43
+ }
44
+ [StructLayout(LayoutKind.Sequential)] public struct P { public int X,Y; public P(int x,int y){X=x;Y=y;} }
45
+ [StructLayout(LayoutKind.Sequential)] public struct S { public int X,Y; public S(int x,int y){X=x;Y=y;} }
46
+ [StructLayout(LayoutKind.Sequential,Pack=1)] public struct Blend { public byte op,flags,alpha,format; }
47
+ [StructLayout(LayoutKind.Sequential)] public struct Info { public uint size; public int width,height; public ushort planes,bits; public uint compression,imageSize; public int xp,yp; public uint used,important,color; }
48
+ [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr value);
49
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
50
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
51
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern IntPtr GetProp(IntPtr window,string name);
52
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern bool SetProp(IntPtr window,string name,IntPtr value);
53
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern IntPtr RemoveProp(IntPtr window,string name);
54
+ [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int key);
55
+ [DllImport("user32.dll")] public static extern bool RegisterHotKey(IntPtr handle,int id,uint mods,uint key);
56
+ [DllImport("user32.dll")] public static extern bool UnregisterHotKey(IntPtr handle,int id);
57
+ [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr handle);
58
+ [DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr handle,IntPtr dc);
59
+ [DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr dc);
60
+ [DllImport("gdi32.dll")] public static extern IntPtr CreateDIBSection(IntPtr dc,ref Info info,uint usage,out IntPtr bits,IntPtr section,uint offset);
61
+ [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr dc,IntPtr value);
62
+ [DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr value);
63
+ [DllImport("gdi32.dll")] public static extern bool DeleteDC(IntPtr dc);
64
+ [DllImport("user32.dll",SetLastError=true)] public static extern bool UpdateLayeredWindow(IntPtr handle,IntPtr screen,ref P position,ref S size,IntPtr source,ref P origin,uint key,ref Blend blend,uint flags);
65
+ }
66
+
67
+ public class NativeSelectionWindow : Form {
68
+ private readonly Rectangle area;
69
+ private readonly IntPtr foreground;
70
+ private readonly Action<Dictionary<string,object>> done;
71
+ private Bitmap bitmap;
72
+ private Graphics graphics;
73
+ private IntPtr dc,dib,old;
74
+ private readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
75
+ private Point start,current;
76
+ private bool dragging,dirty=true,esc;
77
+ private bool finished;
78
+ private Rectangle hover;
79
+ private DateTime hoverTime=DateTime.MinValue;
80
+ private IntPtr keyboardHook;
81
+ private bool rawKeyboard;
82
+ private PickerApi.KeyboardProc keyboardCallback;
83
+ private Dictionary<string,object> outcome;
84
+ private string cancelReason="cancelled";
85
+ public string SourceWindow { get { return foreground.ToInt64().ToString(); } }
86
+ public bool EscapeHotkey { get { return esc; } }
87
+ public bool EscapeHook { get { return keyboardHook!=IntPtr.Zero; } }
88
+ public bool EscapeRawInput { get { return rawKeyboard; } }
89
+ private readonly DateTime opened=DateTime.UtcNow;
90
+ public NativeSelectionWindow(Rectangle bounds,Action<Dictionary<string,object>> closed) {
91
+ done=closed; foreground=PickerApi.GetForegroundWindow();
92
+ area=bounds;
93
+ FormBorderStyle=FormBorderStyle.None; ShowInTaskbar=false; TopMost=true;
94
+ AutoScaleMode=AutoScaleMode.None;
95
+ StartPosition=FormStartPosition.Manual; Bounds=area; Cursor=Cursors.Cross;
96
+ Text="DSH native selection";
97
+ timer.Interval=33;
98
+ timer.Tick+=delegate {
99
+ if ((DateTime.UtcNow-opened).TotalSeconds>120) { cancelReason="timeout";Finish(false); return; }
100
+ if ((PickerApi.GetAsyncKeyState(27)&0x8000)!=0 || (PickerApi.GetAsyncKeyState(2)&0x8000)!=0) { cancelReason="cancel-key";Finish(false); return; }
101
+ if (foreground!=IntPtr.Zero && PickerApi.GetForegroundWindow()!=foreground) { cancelReason="foreground-changed";Finish(false); return; }
102
+ if (dirty) Present();
103
+ };
104
+ }
105
+ protected override bool ShowWithoutActivation { get { return true; } }
106
+ protected override CreateParams CreateParams { get { var p=base.CreateParams; p.ExStyle|=0x80000|0x8000000|0x80; return p; } }
107
+ protected override void WndProc(ref Message message) {
108
+ if(message.Msg==0xff&&rawKeyboard) {
109
+ uint size=0,header=(uint)Marshal.SizeOf(typeof(PickerApi.RawHeader));
110
+ PickerApi.GetRawInputData(message.LParam,0x10000003,IntPtr.Zero,ref size,header);
111
+ if(size>=header+16&&size<=1024) {
112
+ IntPtr data=Marshal.AllocHGlobal((int)size);
113
+ try {
114
+ uint read=PickerApi.GetRawInputData(message.LParam,0x10000003,data,ref size,header);
115
+ if(read!=uint.MaxValue&&read>=header+16&&Marshal.ReadInt32(data)==1&&Marshal.ReadInt16(data,(int)header+6)==27&&(Marshal.ReadInt16(data,(int)header+2)&1)==0) {
116
+ cancelReason="escape-raw-input";BeginInvoke((Action)delegate{Finish(false);});
117
+ }
118
+ } finally { Marshal.FreeHGlobal(data); }
119
+ }
120
+ }
121
+ if (message.Msg==0x21) { message.Result=new IntPtr(3); return; }
122
+ if (message.Msg==0x312) { cancelReason="escape-hotkey";Finish(false); return; }
123
+ base.WndProc(ref message);
124
+ }
125
+ protected override void OnHandleCreated(EventArgs e) {
126
+ base.OnHandleCreated(e);
127
+ var screen=PickerApi.GetDC(IntPtr.Zero);
128
+ dc=PickerApi.CreateCompatibleDC(screen);
129
+ var info=new PickerApi.Info { size=40,width=area.Width,height=-area.Height,planes=1,bits=32 };
130
+ IntPtr pixels; dib=PickerApi.CreateDIBSection(screen,ref info,0,out pixels,IntPtr.Zero,0);
131
+ PickerApi.ReleaseDC(IntPtr.Zero,screen);
132
+ if (dib==IntPtr.Zero) throw new InvalidOperationException("CreateDIBSection failed");
133
+ old=PickerApi.SelectObject(dc,dib);
134
+ bitmap=new Bitmap(area.Width,area.Height,area.Width*4,PixelFormat.Format32bppPArgb,pixels);
135
+ graphics=Graphics.FromImage(bitmap); graphics.CompositingMode=CompositingMode.SourceCopy;
136
+ esc=PickerApi.RegisterHotKey(Handle,2,0,27); Present();
137
+ }
138
+ protected override void OnShown(EventArgs e) {
139
+ base.OnShown(e);
140
+ // Only Escape is handled. Other keys are neither logged nor modified.
141
+ // Keep callback work short; close the window on the message loop instead.
142
+ keyboardCallback=delegate(int code,IntPtr message,IntPtr data) {
143
+ if(code>=0&&Marshal.ReadInt32(data)==27) {
144
+ if(message.ToInt32()==0x100||message.ToInt32()==0x104)BeginInvoke((Action)delegate{cancelReason="escape-hook";Finish(false);});
145
+ return new IntPtr(1);
146
+ }
147
+ return PickerApi.CallNextHookEx(keyboardHook,code,message,data);
148
+ };
149
+ keyboardHook=PickerApi.SetWindowsHookEx(13,keyboardCallback,PickerApi.GetModuleHandle(null),0);
150
+ rawKeyboard=PickerApi.RegisterRawInputDevices(new[]{new PickerApi.RawDevice { page=1,usage=6,flags=0x100,target=Handle }},1,(uint)Marshal.SizeOf(typeof(PickerApi.RawDevice)));
151
+ timer.Start();
152
+ }
153
+ protected override void OnPaintBackground(PaintEventArgs e) {}
154
+ protected override void OnPaint(PaintEventArgs e) {}
155
+ protected override void OnMouseDown(MouseEventArgs e) {
156
+ if(e.Button==MouseButtons.Right){Finish(false);return;}
157
+ if(e.Button!=MouseButtons.Left)return;
158
+ hover=PickerApi.WindowAt(Cursor.Position,area);
159
+ start=current=e.Location; dragging=true; Capture=true; dirty=true;
160
+ }
161
+ protected override void OnMouseMove(MouseEventArgs e) {
162
+ if(dragging){current=e.Location;dirty=true;}
163
+ else if((DateTime.UtcNow-hoverTime).TotalMilliseconds>=90){hoverTime=DateTime.UtcNow;hover=PickerApi.WindowAt(Cursor.Position,area);dirty=true;}
164
+ }
165
+ protected override void OnMouseUp(MouseEventArgs e) {
166
+ if(e.Button!=MouseButtons.Left||!dragging)return;
167
+ current=e.Location; Capture=false; Finish(true);
168
+ }
169
+ private Rectangle RegionBox() {
170
+ int x=Math.Max(0,Math.Min(area.Width,Math.Min(start.X,current.X)));
171
+ int y=Math.Max(0,Math.Min(area.Height,Math.Min(start.Y,current.Y)));
172
+ int right=Math.Max(0,Math.Min(area.Width,Math.Max(start.X,current.X)));
173
+ int bottom=Math.Max(0,Math.Min(area.Height,Math.Max(start.Y,current.Y)));
174
+ return new Rectangle(x,y,right-x,bottom-y);
175
+ }
176
+ private void Present() {
177
+ if(graphics==null)return;
178
+ graphics.Clear(Color.FromArgb(1,0,0,0));
179
+ if(!dragging&&!hover.IsEmpty)using(var pen=new Pen(Color.FromArgb(255,10,132,255),1))graphics.DrawRectangle(pen,hover.X-area.X,hover.Y-area.Y,hover.Width,hover.Height);
180
+ if(dragging) {
181
+ Rectangle r=RegionBox();
182
+ using(var brush=new SolidBrush(Color.FromArgb(20,0,0,0))) {
183
+ graphics.FillRectangle(brush,0,0,area.Width,r.Top);
184
+ graphics.FillRectangle(brush,0,r.Bottom,area.Width,area.Height-r.Bottom);
185
+ graphics.FillRectangle(brush,0,r.Top,r.Left,r.Height);
186
+ graphics.FillRectangle(brush,r.Right,r.Top,area.Width-r.Right,r.Height);
187
+ }
188
+ if(r.Width>0&&r.Height>0)using(var pen=new Pen(Color.FromArgb(255,7,193,96),2))graphics.DrawRectangle(pen,r);
189
+ }
190
+ graphics.Flush();
191
+ var position=new PickerApi.P(area.X,area.Y); var size=new PickerApi.S(area.Width,area.Height); var origin=new PickerApi.P(0,0);
192
+ var blend=new PickerApi.Blend { alpha=255,format=1 };
193
+ if(!PickerApi.UpdateLayeredWindow(Handle,IntPtr.Zero,ref position,ref size,dc,ref origin,0,ref blend,2))throw new InvalidOperationException("UpdateLayeredWindow failed: "+Marshal.GetLastWin32Error());
194
+ dirty=false;
195
+ }
196
+ private void Finish(bool selected) {
197
+ if(finished)return;finished=true;
198
+ timer.Stop(); Rectangle r=RegionBox();
199
+ if(selected&&Math.Abs(start.X-current.X)<3&&Math.Abs(start.Y-current.Y)<3&&!hover.IsEmpty)r=new Rectangle(hover.X-area.X,hover.Y-area.Y,hover.Width,hover.Height);
200
+ outcome=new Dictionary<string,object>{{"selected",selected&&r.Width>=2&&r.Height>=2},{"reason",selected?"release":cancelReason},{"sourceWindow",SourceWindow},{"foregroundWindow",PickerApi.GetForegroundWindow().ToInt64().ToString()}};
201
+ if(selected&&r.Width>=2&&r.Height>=2){outcome["x"]=area.X+r.X;outcome["y"]=area.Y+r.Y;outcome["width"]=r.Width;outcome["height"]=r.Height;}
202
+ Close();
203
+ }
204
+ public void CancelSelection(){cancelReason="parent-cancel";Finish(false);}
205
+ public void CancelEscape(){cancelReason="escape-admin-bridge";Finish(false);}
206
+ protected override void OnFormClosed(FormClosedEventArgs e) {
207
+ timer.Stop(); timer.Dispose(); if(esc)PickerApi.UnregisterHotKey(Handle,2);
208
+ if(keyboardHook!=IntPtr.Zero){PickerApi.UnhookWindowsHookEx(keyboardHook);keyboardHook=IntPtr.Zero;}
209
+ if(rawKeyboard){PickerApi.RegisterRawInputDevices(new[]{new PickerApi.RawDevice { page=1,usage=6,flags=1,target=IntPtr.Zero }},1,(uint)Marshal.SizeOf(typeof(PickerApi.RawDevice)));rawKeyboard=false;}
210
+ if(graphics!=null)graphics.Dispose(); if(bitmap!=null)bitmap.Dispose();
211
+ if(dc!=IntPtr.Zero){PickerApi.SelectObject(dc,old);PickerApi.DeleteObject(dib);PickerApi.DeleteDC(dc);}
212
+ base.OnFormClosed(e); done(outcome??new Dictionary<string,object>{{"selected",false}});
213
+ }
214
+ }
215
+
216
+ public class NativeSelectionServer : ApplicationContext {
217
+ private class EscapeControl : Control {
218
+ public Action Escape;
219
+ public IntPtr Nonce;
220
+ protected override void WndProc(ref Message message){if(message.Msg==0x8039&&message.WParam==Nonce){if(Escape!=null)Escape();return;}base.WndProc(ref message);}
221
+ }
222
+ private readonly EscapeControl bridge=new EscapeControl();
223
+ private readonly string scriptRoot;
224
+ private readonly IntPtr nonce=new IntPtr((Guid.NewGuid().GetHashCode()&0x3fffffff)+1);
225
+ private System.Windows.Forms.Timer adminWait;
226
+ private readonly JavaScriptSerializer json=new JavaScriptSerializer();
227
+ private NativeSelectionWindow picker;
228
+ private bool closing;
229
+ private void Reply(string id,object result,string error) {
230
+ try{Console.WriteLine(json.Serialize(new Dictionary<string,object>{{"id",id},{"ok",error==null},{"result",result},{"error",error}}));Console.Out.Flush();}catch(System.IO.IOException){}
231
+ }
232
+ private bool AdminReady { get { return PickerApi.GetProp(bridge.Handle,"DshShotEscapeAdmin")==nonce; } }
233
+ public NativeSelectionServer(string root) {
234
+ scriptRoot=root;
235
+ IntPtr handle=bridge.Handle;
236
+ bridge.Nonce=nonce;bridge.Escape=delegate { if(picker!=null)picker.CancelEscape(); };
237
+ if(!PickerApi.SetProp(handle,"DshShotEscapeOwner",nonce))throw new InvalidOperationException("Could not initialize picker owner");
238
+ var input=new Thread(delegate(){
239
+ string line;
240
+ try{while((line=Console.ReadLine())!=null){string command=line;bridge.BeginInvoke((Action)delegate{Command(command);});}}
241
+ catch(Exception){ }
242
+ try{bridge.BeginInvoke((Action)delegate{ExitThread();});}catch(InvalidOperationException){}
243
+ });
244
+ input.IsBackground=true;input.Start();Console.WriteLine("READY");Console.Out.Flush();
245
+ }
246
+ private void Command(string line) {
247
+ string id=null;
248
+ try{
249
+ var cmd=json.Deserialize<Dictionary<string,object>>(line);id=Convert.ToString(cmd["id"]);string op=Convert.ToString(cmd["op"]);
250
+ if(op=="quit"){ExitThread();return;}
251
+ if(op=="adminEscapeStatus"){Reply(id,new { ready=AdminReady },null);return;}
252
+ if(op=="enableAdminEscape"){
253
+ if(AdminReady){Reply(id,new { ready=true },null);return;}
254
+ if(picker!=null||adminWait!=null)throw new InvalidOperationException("Finish selection before enabling game compatibility");
255
+ string script=System.IO.Path.Combine(scriptRoot,"escape_bridge.ps1");
256
+ var launch=new System.Diagnostics.ProcessStartInfo { FileName=System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),"WindowsPowerShell\\v1.0\\powershell.exe"),Arguments="-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \""+script+"\" -OwnerPid "+System.Diagnostics.Process.GetCurrentProcess().Id+" -OwnerWindow "+handleValue()+" -Nonce "+nonce.ToInt64(),UseShellExecute=true,Verb="runas",WindowStyle=System.Diagnostics.ProcessWindowStyle.Hidden };
257
+ using(var process=System.Diagnostics.Process.Start(launch)){}
258
+ DateTime deadline=DateTime.UtcNow.AddSeconds(15);string adminRequest=id;
259
+ adminWait=new System.Windows.Forms.Timer();adminWait.Interval=50;
260
+ adminWait.Tick+=delegate {
261
+ if(!AdminReady&&DateTime.UtcNow<deadline)return;
262
+ adminWait.Stop();adminWait.Dispose();adminWait=null;
263
+ Reply(adminRequest,new { ready=AdminReady },AdminReady?null:"Game compatibility helper did not start");
264
+ };
265
+ adminWait.Start();return;
266
+ }
267
+ if(op=="cancel"){if(picker!=null)picker.CancelSelection();Reply(id,new { cancelled=true },null);return;}
268
+ if(op=="status"){Reply(id,new { active=picker!=null, visible=picker!=null&&PickerApi.IsWindowVisible(picker.Handle), handle=picker==null?"0":picker.Handle.ToInt64().ToString(), sourceWindow=picker==null?"0":picker.SourceWindow, foregroundWindow=PickerApi.GetForegroundWindow().ToInt64().ToString(), escapeHook=picker!=null&&picker.EscapeHook, escapeHotkey=picker!=null&&picker.EscapeHotkey, escapeRawInput=picker!=null&&picker.EscapeRawInput, adminEscapeReady=AdminReady, adminEscapeHook=PickerApi.GetProp(bridge.Handle,"DshShotEscapeHook")==nonce },null);return;}
269
+ if(op!="select")throw new InvalidOperationException("Unknown native selection operation");
270
+ if(picker!=null)throw new InvalidOperationException("Native selection already active");
271
+ int x=Convert.ToInt32(cmd["x"]),y=Convert.ToInt32(cmd["y"]),w=Convert.ToInt32(cmd["width"]),h=Convert.ToInt32(cmd["height"]);
272
+ if(w<2||h<2||(long)w*h>100000000)throw new InvalidOperationException("Invalid native selection bounds");
273
+ string request=id;
274
+ picker=new NativeSelectionWindow(new Rectangle(x,y,w,h),delegate(Dictionary<string,object> result){PickerApi.RemoveProp(bridge.Handle,"DshShotEscapeActive");picker=null;Reply(request,result,null);});
275
+ PickerApi.SetProp(bridge.Handle,"DshShotEscapeActive",nonce);
276
+ picker.Show();
277
+ }catch(Exception error){Reply(id,null,error.Message);}
278
+ }
279
+ private string handleValue(){return bridge.Handle.ToInt64().ToString();}
280
+ protected override void ExitThreadCore(){if(closing)return;closing=true;if(adminWait!=null){adminWait.Stop();adminWait.Dispose();}if(picker!=null)picker.CancelSelection();PickerApi.RemoveProp(bridge.Handle,"DshShotEscapeOwner");bridge.Dispose();base.ExitThreadCore();}
281
+ public static void Run(string root){
282
+ try{PickerApi.SetThreadDpiAwarenessContext(new IntPtr(-4));}catch{PickerApi.SetProcessDPIAware();}
283
+ Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
284
+ Application.EnableVisualStyles();Application.Run(new NativeSelectionServer(root));
285
+ }
286
+ }
@@ -0,0 +1,5 @@
1
+ #Requires -Version 5.1
2
+ $ErrorActionPreference = 'Stop'
3
+ Add-Type -AssemblyName System.Drawing,System.Windows.Forms,System.Web.Extensions
4
+ Add-Type -Path (Join-Path $PSScriptRoot 'native_picker.cs') -ReferencedAssemblies System.Drawing,System.Windows.Forms,System.Web.Extensions
5
+ [NativeSelectionServer]::Run($PSScriptRoot)
package/package.json CHANGED
@@ -1,16 +1,37 @@
1
1
  {
2
2
  "name": "dsh-plugin-live-shot",
3
- "version": "0.7.0-beta.4",
3
+ "version": "0.7.0-beta.5",
4
4
  "license": "MIT",
5
5
  "author": "TangerineSplendor",
6
- "repository": { "type": "git", "url": "git+https://github.com/TangerineSplendor/dsh-screenshot.git" },
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/TangerineSplendor/dsh-screenshot.git"
9
+ },
7
10
  "homepage": "https://github.com/TangerineSplendor/dsh-screenshot#readme",
8
- "bugs": { "url": "https://github.com/TangerineSplendor/dsh-screenshot/issues" },
9
- "keywords": ["dsh-plugin", "deepseek-harness", "screenshot", "windows", "annotation"],
10
- "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/", "tag": "beta" },
11
- "bin": { "dsh-shot": "./bin/dsh-shot.cjs" },
12
- "os": ["win32"],
13
- "cpu": ["x64"],
11
+ "bugs": {
12
+ "url": "https://github.com/TangerineSplendor/dsh-screenshot/issues"
13
+ },
14
+ "keywords": [
15
+ "dsh-plugin",
16
+ "deepseek-harness",
17
+ "screenshot",
18
+ "windows",
19
+ "annotation"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/",
24
+ "tag": "beta"
25
+ },
26
+ "bin": {
27
+ "dsh-shot": "./bin/dsh-shot.cjs"
28
+ },
29
+ "os": [
30
+ "win32"
31
+ ],
32
+ "cpu": [
33
+ "x64"
34
+ ],
14
35
  "type": "module",
15
36
  "description": "Screenshot editor that lands the crop straight in the composer draft.",
16
37
  "exports": {
@@ -31,6 +52,11 @@
31
52
  "runtime_paths.cjs",
32
53
  "client.js",
33
54
  "worker.js",
55
+ "picker.js",
56
+ "native_picker.cs",
57
+ "native_picker.ps1",
58
+ "escape_bridge.cs",
59
+ "escape_bridge.ps1",
34
60
  "capture_worker.ps1",
35
61
  "overlay_preload.cjs",
36
62
  "overlay.html",
package/picker.js ADDED
@@ -0,0 +1,76 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { randomUUID } from 'node:crypto';
4
+ import readline from 'node:readline';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import runtimePaths from './runtime_paths.cjs';
8
+
9
+ export class NativePicker {
10
+ constructor() { this.child = null; this.pending = new Map(); }
11
+ record(update) {
12
+ this.diagnostics = { ...this.diagnostics, ...update, updatedAt: new Date().toISOString() };
13
+ try { fs.writeFileSync(path.join(runtimePaths().dataDir, 'native-selection-status.json'), JSON.stringify(this.diagnostics)); } catch (_) {}
14
+ }
15
+ start() {
16
+ if (this.child) return this.ready;
17
+ const temp = runtimePaths().tempDir;
18
+ fs.mkdirSync(temp, { recursive: true });
19
+ const child = this.child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('./native_picker.ps1', import.meta.url))], {
20
+ env: { ...process.env, TEMP: temp, TMP: temp }, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
21
+ });
22
+ let stderr = '';
23
+ child.stderr.on('data', data => { stderr = (stderr + data).slice(-2000); this.record({ stderr }); });
24
+ this.ready = new Promise((resolve, reject) => {
25
+ const timer = setTimeout(() => { reject(new Error('Native selection startup timed out')); this.stop(); }, 15000);
26
+ this.rejectStart = error => { clearTimeout(timer); reject(error); };
27
+ readline.createInterface({ input: child.stdout }).on('line', line => {
28
+ if (line === 'READY') { clearTimeout(timer); this.record({ state: 'ready' }); resolve(); return; }
29
+ try {
30
+ const message = JSON.parse(line), task = this.pending.get(message.id);
31
+ if (!task) return;
32
+ this.pending.delete(message.id); clearTimeout(task.timer);
33
+ message.ok ? task.resolve(message.result) : task.reject(new Error(message.error));
34
+ } catch (error) { this.fail(child, error); }
35
+ });
36
+ });
37
+ child.on('error', error => this.fail(child, error));
38
+ child.stdin.on('error', error => this.fail(child, error));
39
+ child.on('exit', () => this.fail(child, new Error('Native selection exited: ' + stderr)));
40
+ return this.ready;
41
+ }
42
+ fail(child, error) {
43
+ if (this.child !== child) return;
44
+ this.rejectStart?.(error); this.child = null;
45
+ for (const task of this.pending.values()) { clearTimeout(task.timer); task.reject(error); }
46
+ this.pending.clear();
47
+ if (child.exitCode === null) child.kill();
48
+ }
49
+ async run(command) {
50
+ await this.start();
51
+ if (command.op === 'select') this.record({ state: 'selecting', bounds: command, result: null });
52
+ return new Promise((resolve, reject) => {
53
+ const id = randomUUID();
54
+ this.pending.set(id, { resolve: result => { if (command.op === 'select') this.record({ state: result.selected ? 'selected' : 'cancelled', result }); resolve(result); }, reject, timer: setTimeout(() => {
55
+ this.pending.delete(id); reject(new Error('Native selection timed out')); this.stop();
56
+ }, command.op === 'enableAdminEscape' ? 120000 : command.op === 'select' ? 130000 : 12000) });
57
+ this.child.stdin.write(JSON.stringify({ ...command, id }) + '\n');
58
+ if (command.op === 'select') {
59
+ const child = this.child;
60
+ const timer = setTimeout(() => { if (this.child === child) void this.run({ op: 'status' }).then(status => this.record({ status })).catch(() => {}); }, 100);
61
+ timer.unref();
62
+ }
63
+ });
64
+ }
65
+ stop() {
66
+ const child = this.child;
67
+ if (!child) return;
68
+ this.child = null;
69
+ this.rejectStart?.(new Error('Native selection stopped'));
70
+ for (const task of this.pending.values()) { clearTimeout(task.timer); task.reject(new Error('Native selection stopped')); }
71
+ this.pending.clear();
72
+ child.stdin.end(JSON.stringify({ id: randomUUID(), op: 'quit' }) + '\n');
73
+ const timer = setTimeout(() => child.kill(), 1500); timer.unref();
74
+ child.once('exit', () => clearTimeout(timer));
75
+ }
76
+ }
@@ -1,6 +1,6 @@
1
1
  # DSH Shot — Windows 可移植测试包
2
2
 
3
- 版本:0.7.0-beta.4。适用于 **Windows 10/11 x64 的官方 DeepSeek Harness 桌面版**。需要先安装并打开过一次 DSH。macOS、Linux、纯浏览器、远程服务器 Host 和其他桌面壳未验证。
3
+ 版本:0.7.0-beta.5。适用于 **Windows 10/11 x64 的官方 DeepSeek Harness 桌面版**。需要先安装并打开过一次 DSH。macOS、Linux、纯浏览器、远程服务器 Host 和其他桌面壳未验证。
4
4
 
5
5
  ## 安装
6
6
 
@@ -18,7 +18,7 @@
18
18
  - 悬停高亮窗口,单击选择整个窗口;也可以自由拖动框选。
19
19
  - 松手后用 **↔** 移动选区,或拖动八个手柄缩放;释放后重新抓取真实范围。
20
20
  - 支持矩形、椭圆、箭头、画笔、马赛克、文字、撤销和 PNG 保存。
21
- - **Enter / 双击 / 完成**:图片进入当前 DSH 输入框。**Ctrl+C**:仅复制图片。**Esc / 右键**:退出。
21
+ - **Enter / 双击 / 完成**:图片进入当前 DSH 输入框。**Ctrl+C**:仅复制图片。**右键**:退出。游戏预框选 Esc 仍可能无效;松手后的编辑器可用 Esc。
22
22
  - 相机旁的 **隐藏 DSH** 会记住选择;Shift+点击相机仅本次反向切换。结束截图后恢复窗口。
23
23
  - 相机旁 **⚙** 可以修改全局热键。冲突或保存失败时保留原设置。
24
24
 
@@ -52,6 +52,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 -DshExe "C
52
52
 
53
53
  ## 已知限制与反馈
54
54
 
55
+ - 游戏预框选阶段 Esc 取消仍未解决,暂用右键退出。⚙ 内「启用管理员游戏兼容」为试验入口,需本人确认 Windows 提示,仅本次运行有效;完整插件实测仍未解决此问题,不保证有效。普通截图/安装不请求管理员权限。
56
+ - 预框选现使用 Windows 原生非激活窗口,松手后才抓选区;移动/调整选区仍经 Electron,在游戏中的焦点行为尚待验证。
57
+
55
58
  - 当前一次框选限于一个显示器;窗口识别只识别顶层窗口。
56
59
  - 松手后有隐藏覆盖层和等待合成的短暂间隔;视频截图不是对释放时刻的逐帧保证。
57
60
  - 仅在开发者本机及不同路径下验证过,本包用于收集其他 Windows / DSH 版本的兼容性反馈。