dsh-plugin-live-shot 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TangerineSplendor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,88 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # DSH Shot
2
+
3
+ [![DSH Market 已收录](https://raw.githubusercontent.com/2BingLing/dsh-market/master/assets/readme/badge-listed-zh.svg)](https://dsh.market/)
4
+
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.
6
+
7
+ Windows 实时桌面截图插件:全局热键、窗口识别、选区调整、标注,以及直接插入 DSH 输入框。
8
+
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 的相机/插图安装缺陷已在此前版本修复,该旧版已下架。**
12
+
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.
14
+
15
+ ## Install / 安装
16
+
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.
18
+
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.
20
+
21
+ **npm, for users who already have Node.js:**
22
+
23
+ ```powershell
24
+ npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.5 dsh-shot install
25
+ ```
26
+
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.
28
+
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.
30
+
31
+ 安装无需管理员权限。ZIP 安装不需要额外 Node 或网络下载;npm 方式需要 Node 和网络获取插件包。辅助运行时均从你本机的 DSH 复制,首次约需数百 MB 空间,不修改 DSH 程序目录。DSH 升级后重新运行安装器。
32
+
33
+ ## Use / 使用
34
+
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.
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.
37
+ - Rectangle, ellipse, arrow, pen, mosaic, text, undo and PNG save are available.
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.
39
+ - **隐藏 DSH** remembers whether to hide DSH during capture. **Shift+camera** inverts that choice for one capture. Completion or cancellation restores hidden windows.
40
+ - **⚙** changes the global shortcut. A conflict or save failure preserves the previous binding.
41
+
42
+ ## Known issue in this beta / 本版已知问题
43
+
44
+ 游戏预框选阶段 Esc 可能无效,暂用右键退出;松手后的编辑器 Esc 已获本机反馈正常。⚙ 中新增手动启用的「管理员游戏兼容」试验入口,需要本人确认 Windows 提示,仅本次辅助进程运行有效。**完整插件实测仍未解决该 Esc 问题,不能将此入口视为已修复或保证有效。**普通截图和安装不请求管理员权限。编辑后移动/调整选区仍使用 Electron,游戏焦点行为尚待进一步验证。
45
+
46
+ 本机用户已确认 B 站视频框选期间恢复可见、游戏原生拖选可用;这不是所有游戏或所有机器的兼容性保证。
47
+
48
+ ## Restart acceptance / 重启后三步验收
49
+
50
+ 1. Use the camera and the global hotkey; select over a moving clock or video and confirm the desktop stays live.
51
+ 2. Select a window, adjust the region and add annotations; enable **隐藏 DSH**, cancel and confirm DSH returns.
52
+ 3. Verify Enter inserts, Ctrl+C copies only, and saved hotkey/hide preferences survive another complete restart.
53
+
54
+ ## Uninstall / 卸载
55
+
56
+ For the explicit installer, run `uninstall.bat`, or:
57
+
58
+ ```powershell
59
+ npx --yes --package=dsh-plugin-live-shot@0.7.0-beta.5 dsh-shot uninstall
60
+ ```
61
+
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.
63
+
64
+ Full path options and troubleshooting: [portable_readme.md](portable_readme.md).
65
+
66
+ ## Limits and local access
67
+
68
+ One monitor per selection; no cross-monitor stitching or scrolling capture. Window detection covers top-level windows. Protected video and UAC secure desktops follow Windows restrictions. Capture occurs after hiding the overlay and waiting for composition, rather than promising the exact video frame at mouse release.
69
+
70
+ The plugin registers a global hotkey, reads pointer/window geometry, captures the region you select, and writes the clipboard or a PNG only when requested. It uses an authenticated random-token loopback connection between its local processes. The dedicated Electron helper uses `--no-sandbox` and a local page with context isolation and a network-blocking CSP; it must not load remote pages. The plugin does not upload screenshots or read account keys.
71
+
72
+ Composer delivery uses DSH's current paste/drop DOM behavior; version changes may require adaptation. Report DSH/Windows versions, monitor scaling, RDP use and the error message in [Issues](https://github.com/TangerineSplendor/dsh-screenshot/issues). Do not attach account settings or private screenshots.
73
+
74
+ ## Development
75
+
76
+ No npm dependencies or build step. Runtime files and machine-specific configuration are excluded from public artifacts. On Windows with Node:
77
+
78
+ ```powershell
79
+ node --experimental-vm-modules tests/host_test.mjs
80
+ node tests/selection_settings_test.mjs
81
+ node tests/client_install_test.mjs
82
+ node tests/worker_test.mjs
83
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build_portable.ps1
84
+ ```
85
+
86
+ The worker test captures a small region locally without writing it to disk. Integration tests require an installed DSH and should use isolated profiles.
87
+
88
+ MIT license for this plugin. DSH/Electron binaries are not included; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). This is an independent community plugin.
@@ -0,0 +1,7 @@
1
+ # Third-party runtime notice
2
+
3
+ This test archive contains plugin source and installation scripts. It does not include a DSH or Electron binary distribution.
4
+
5
+ During installation the scripts copy runtime files from the recipient's existing DeepSeek Harness installation into a dedicated local plugin data directory. The original installation is not modified. Available `LICENSE.electron.txt` and `LICENSES.chromium.html` files are copied with that runtime. Other notices in the original DSH installation remain applicable to its components.
6
+
7
+ This package uses the Windows PowerShell, .NET, Win32 and DWM facilities already present on the recipient's computer.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require('node:child_process');
3
+ const path = require('node:path');
4
+ const [command, ...args] = process.argv.slice(2);
5
+ if (!command || command === '--help' || command === '-h') {
6
+ console.log('DSH Shot for Windows x64\nUsage: dsh-shot install [PowerShell installer options]\n dsh-shot uninstall [PowerShell uninstaller options]\nAfter installation or removal, quit DSH completely and reopen it.');
7
+ } else if (!['install', 'uninstall'].includes(command)) {
8
+ console.error('Unknown command. Use dsh-shot --help.'); process.exitCode = 1;
9
+ } else if (process.platform !== 'win32' || process.arch !== 'x64') {
10
+ console.error('DSH Shot requires Windows x64.'); process.exitCode = 1;
11
+ } else {
12
+ const child = spawn('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-File',
13
+ path.join(__dirname, '..', command + '.ps1'), ...args], { stdio: 'inherit', windowsHide: true });
14
+ child.on('error', error => { console.error(error.message); process.exitCode = 1; });
15
+ child.on('close', code => { process.exitCode = code ?? 1; });
16
+ }
@@ -0,0 +1,144 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ Add-Type -AssemblyName System.Drawing
3
+ Add-Type -TypeDefinition @"
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Runtime.InteropServices;
7
+ using System.Threading;
8
+ public static class ShotNative {
9
+ [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; }
10
+ public class WindowInfo { public int x, y, width, height; public long handle; public uint pid; }
11
+ [StructLayout(LayoutKind.Sequential)] public struct Point { public int X; public int Y; }
12
+ [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr value);
13
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
14
+ [DllImport("user32.dll")] public static extern bool GetCursorPos(out Point point);
15
+ [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int key);
16
+ [DllImport("dwmapi.dll")] public static extern int DwmFlush();
17
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command);
18
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr window);
19
+ [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr window);
20
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr window);
21
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr window);
22
+ [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr window, int command);
23
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr window, out uint pid);
24
+ public delegate bool EnumCallback(IntPtr window, IntPtr data);
25
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumCallback callback, IntPtr data);
26
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr window, out Rect rect);
27
+ [DllImport("dwmapi.dll", EntryPoint="DwmGetWindowAttribute")] public static extern int GetFrame(IntPtr window, int attribute, out Rect rect, int size);
28
+ [DllImport("dwmapi.dll", EntryPoint="DwmGetWindowAttribute")] public static extern int GetCloaked(IntPtr window, int attribute, out int value, int size);
29
+ public static WindowInfo WindowAt(int x, int y, uint excludePid) {
30
+ WindowInfo found = null;
31
+ EnumWindows(delegate(IntPtr window, IntPtr data) {
32
+ uint pid; GetWindowThreadProcessId(window, out pid);
33
+ int cloaked = 0; GetCloaked(window, 14, out cloaked, 4);
34
+ if (pid == excludePid || !IsWindowVisible(window) || IsIconic(window) || cloaked != 0) return true;
35
+ Rect rect;
36
+ if (GetFrame(window, 9, out rect, Marshal.SizeOf(typeof(Rect))) != 0 && !GetWindowRect(window, out rect)) return true;
37
+ if (rect.Right - rect.Left < 2 || rect.Bottom - rect.Top < 2) return true;
38
+ if (x >= rect.Left && x < rect.Right && y >= rect.Top && y < rect.Bottom) {
39
+ found = new WindowInfo { x=rect.Left, y=rect.Top, width=rect.Right-rect.Left, height=rect.Bottom-rect.Top, handle=window.ToInt64(), pid=pid };
40
+ return false;
41
+ }
42
+ return true;
43
+ }, IntPtr.Zero);
44
+ return found;
45
+ }
46
+ private static Dictionary<long, uint> hidden = new Dictionary<long, uint>();
47
+ public static long[] HideProcessWindows(uint pid) {
48
+ List<long> handles = new List<long>();
49
+ EnumWindows(delegate(IntPtr window, IntPtr data) {
50
+ uint owner;
51
+ GetWindowThreadProcessId(window, out owner);
52
+ // Minimized windows already expose the desktop; leave them untouched.
53
+ if (owner == pid && IsWindowVisible(window) && !IsIconic(window)) {
54
+ long handle = window.ToInt64(); hidden[handle] = pid; handles.Add(handle);
55
+ ShowWindowAsync(window, 0);
56
+ }
57
+ return true;
58
+ }, IntPtr.Zero);
59
+ Thread.Sleep(140);
60
+ DwmFlush();
61
+ foreach (long handle in handles) {
62
+ if (IsWindowVisible(new IntPtr(handle))) {
63
+ RestoreAll();
64
+ throw new InvalidOperationException("DSH window could not be hidden");
65
+ }
66
+ }
67
+ return handles.ToArray();
68
+ }
69
+ public static void RestoreWindows(long[] handles, uint pid) {
70
+ foreach (long handle in handles) {
71
+ IntPtr window = new IntPtr(handle); uint owner;
72
+ GetWindowThreadProcessId(window, out owner);
73
+ if (IsWindow(window) && owner == pid) ShowWindowAsync(window, 8);
74
+ hidden.Remove(handle);
75
+ }
76
+ Thread.Sleep(50);
77
+ }
78
+ public static void RestoreAll() {
79
+ foreach (KeyValuePair<long, uint> item in hidden) {
80
+ IntPtr window = new IntPtr(item.Key); uint owner;
81
+ GetWindowThreadProcessId(window, out owner);
82
+ if (IsWindow(window) && owner == item.Value) ShowWindowAsync(window, 8);
83
+ }
84
+ hidden.Clear();
85
+ Thread.Sleep(50);
86
+ }
87
+ }
88
+ "@
89
+ try { [ShotNative]::SetThreadDpiAwarenessContext([IntPtr](-4)) | Out-Null }
90
+ catch { [ShotNative]::SetProcessDPIAware() | Out-Null }
91
+ [Console]::Out.WriteLine('READY')
92
+ [Console]::Out.Flush()
93
+ try {
94
+ while ($null -ne ($line = [Console]::In.ReadLine())) {
95
+ if ($line -eq 'quit') { break }
96
+ $bitmap = $null
97
+ $graphics = $null
98
+ $stream = $null
99
+ try {
100
+ $cmd = $line | ConvertFrom-Json
101
+ if ($cmd.op -eq 'windowAt') {
102
+ $result = [ShotNative]::WindowAt([int]$cmd.x, [int]$cmd.y, [uint32]$cmd.excludePid)
103
+ } elseif ($cmd.op -eq 'hide') {
104
+ if ([int]$cmd.pid -le 0) { throw 'DSH shell process is unavailable' }
105
+ $handles = [ShotNative]::HideProcessWindows([uint32]$cmd.pid)
106
+ $result = @{ windows=@($handles | ForEach-Object { $_.ToString() }); pid=[int]$cmd.pid }
107
+ } elseif ($cmd.op -eq 'restore') {
108
+ [ShotNative]::RestoreWindows([long[]]@($cmd.windows), [uint32]$cmd.pid)
109
+ $result = @{ restored=$true }
110
+ } elseif ($cmd.op -eq 'focus') {
111
+ $target = [System.Diagnostics.Process]::GetProcessById([int]$cmd.pid)
112
+ $window = $target.MainWindowHandle
113
+ if ($window -ne [IntPtr]::Zero) {
114
+ if ([ShotNative]::IsIconic($window)) { [ShotNative]::ShowWindow($window, 9) | Out-Null }
115
+ else { [ShotNative]::ShowWindow($window, 8) | Out-Null }
116
+ [ShotNative]::SetForegroundWindow($window) | Out-Null
117
+ }
118
+ $result = @{ focused=($window -ne [IntPtr]::Zero) }
119
+ } elseif ($cmd.op -eq 'pointer') {
120
+ $point = New-Object ShotNative+Point
121
+ [ShotNative]::GetCursorPos([ref]$point) | Out-Null
122
+ $result = @{ x=$point.X; y=$point.Y; down=(([ShotNative]::GetAsyncKeyState(1) -band 0x8000) -ne 0) }
123
+ } elseif ($cmd.op -eq 'region') {
124
+ $w = [int]$cmd.width; $h = [int]$cmd.height
125
+ if ($w -lt 1 -or $h -lt 1 -or ([long]$w * $h) -gt 100000000) { throw 'Invalid region dimensions' }
126
+ [ShotNative]::DwmFlush() | Out-Null
127
+ $bitmap = New-Object System.Drawing.Bitmap $w, $h
128
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
129
+ $graphics.CopyFromScreen([int]$cmd.x, [int]$cmd.y, 0, 0, $bitmap.Size)
130
+ $stream = New-Object System.IO.MemoryStream
131
+ $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
132
+ $result = @{ png=[Convert]::ToBase64String($stream.ToArray()); width=$w; height=$h }
133
+ } else { throw 'Unknown operation' }
134
+ [Console]::Out.WriteLine((@{ ok=$true; result=$result } | ConvertTo-Json -Compress))
135
+ } catch {
136
+ [Console]::Out.WriteLine((@{ ok=$false; error=$_.Exception.Message } | ConvertTo-Json -Compress))
137
+ } finally {
138
+ if ($graphics) { $graphics.Dispose() }
139
+ if ($stream) { $stream.Dispose() }
140
+ if ($bitmap) { $bitmap.Dispose() }
141
+ }
142
+ [Console]::Out.Flush()
143
+ }
144
+ } finally { [ShotNative]::RestoreAll() }
package/client.js ADDED
@@ -0,0 +1,147 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-plugin-live-shot',
3
+ factory(require) {
4
+ const React = require('react'), h = React.createElement;
5
+ const client = sessionStorage.getItem('dsh-shot-client') || crypto.randomUUID();
6
+ sessionStorage.setItem('dsh-shot-client', client);
7
+ let running = false, stopped = false, retryAt = 0, errorShown = '', delivered = new Set();
8
+ let hidePreference = false, preferenceLoaded = false;
9
+ let hotkeyPreference = 'Ctrl+Shift+X', shortcutProblem = '', hotkeyRegistered = false;
10
+ let gameCompatibilityReady = false, gameCompatibilityEnabling = false;
11
+ const preferenceState = () => ({ value: hidePreference, ready: preferenceLoaded,
12
+ hotkey: hotkeyPreference, problem: shortcutProblem, registered: hotkeyRegistered,
13
+ gameCompatibilityReady, gameCompatibilityEnabling });
14
+ const preferenceListeners = new Set();
15
+ function toast(message) {
16
+ let el = document.getElementById('dsh-shot-toast');
17
+ if (!el) {
18
+ el = document.createElement('div'); el.id = 'dsh-shot-toast';
19
+ el.style.cssText = 'position:fixed;bottom:85px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#222;color:white;padding:9px 16px;border-radius:8px;font:13px system-ui;pointer-events:none';
20
+ document.body.appendChild(el);
21
+ }
22
+ el.textContent = message; el.hidden = false;
23
+ clearTimeout(toast.timer); toast.timer = setTimeout(() => { el.hidden = true; }, 4500);
24
+ }
25
+ function deliver(file) {
26
+ const input = document.querySelector('[data-composer-input="true"]');
27
+ if (!input) return false;
28
+ input.focus();
29
+ const data = new DataTransfer(); data.items.add(file);
30
+ const paste = new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true });
31
+ if (!input.dispatchEvent(paste)) return true;
32
+ return !input.dispatchEvent(new DragEvent('drop', { dataTransfer: data, bubbles: true, cancelable: true }));
33
+ }
34
+ async function post(op, extra = {}) {
35
+ const response = await fetch('/api/dsh-shot/control', { method: 'POST', cache: 'no-store',
36
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify({ op, client, focused: document.hasFocus(), ...extra }) });
37
+ const raw = await response.text();
38
+ let data;
39
+ try { data = JSON.parse(raw); }
40
+ catch (_) { throw new Error(response.status === 404 ? '截图 Host 接口未加载,请完整退出 DSH 后重开' : '截图服务返回异常(HTTP ' + response.status + ')'); }
41
+ if (!response.ok) throw new Error(data.error || '截图服务不可用');
42
+ if (typeof data.hideDsh === 'boolean') {
43
+ hidePreference = data.hideDsh; preferenceLoaded = true;
44
+ hotkeyPreference = data.hotkey || hotkeyPreference; shortcutProblem = data.hotkeyError || ''; hotkeyRegistered = !!data.registered;
45
+ gameCompatibilityReady = data.gameCompatibilityReady === true;
46
+ gameCompatibilityEnabling = data.gameCompatibilityEnabling === true;
47
+ for (const listener of preferenceListeners) listener(preferenceState());
48
+ }
49
+ return data;
50
+ }
51
+ async function poll() {
52
+ if (running || stopped) return;
53
+ running = true;
54
+ try {
55
+ const data = await post('poll', { skipImage: Date.now() < retryAt });
56
+ if (data.error && errorShown !== data.error) { errorShown = data.error; toast(data.error); }
57
+ if (data.hotkeyError && errorShown !== data.hotkeyError) { errorShown = data.hotkeyError; toast(data.hotkeyError); }
58
+ if (data.result && Date.now() >= retryAt) {
59
+ const item = data.result;
60
+ if (!delivered.has(item.id)) {
61
+ const bytes = Uint8Array.from(atob(item.png), c => c.charCodeAt(0));
62
+ if (!deliver(new File([bytes], 'shot-' + Date.now() + '.png', { type: 'image/png' }))) {
63
+ retryAt = Date.now() + 5000;
64
+ toast('未找到可接收图片的输入框;图片已保留,打开对话后自动重试');
65
+ await post('failure'); return;
66
+ }
67
+ delivered.add(item.id);
68
+ if (delivered.size > 100) delivered.delete(delivered.values().next().value);
69
+ toast('截图已插入输入框');
70
+ }
71
+ await post('ack', { id: item.id });
72
+ }
73
+ } catch (e) { /* A restarting Host is retried by the next poll. */ }
74
+ finally { running = false; }
75
+ }
76
+ function ShotButton() {
77
+ const [preference, setPreference] = React.useState(preferenceState());
78
+ const [saving, setSaving] = React.useState(false);
79
+ const [settingsOpen, setSettingsOpen] = React.useState(false);
80
+ const [shortcutDraft, setShortcutDraft] = React.useState(hotkeyPreference);
81
+ const [settingsError, setSettingsError] = React.useState('');
82
+ React.useEffect(() => {
83
+ preferenceListeners.add(setPreference);
84
+ setPreference(preferenceState());
85
+ return () => preferenceListeners.delete(setPreference);
86
+ }, []);
87
+ const changePreference = async event => {
88
+ const value = event.target.checked; setSaving(true);
89
+ try { await post('settings', { hideDsh: value }); }
90
+ catch (error) { toast(error.message); }
91
+ finally { setSaving(false); }
92
+ };
93
+ const saveShortcut = async () => {
94
+ setSaving(true); setSettingsError('');
95
+ try { const data = await post('settings', { hotkey: shortcutDraft }); setSettingsOpen(false); toast('截图快捷键:' + data.hotkey); }
96
+ catch (error) { setSettingsError(error.message); }
97
+ finally { setSaving(false); }
98
+ };
99
+ const enableGameCompatibility = async () => {
100
+ setSettingsError('');
101
+ try { await post('gameCompatibility'); toast('请确认 Windows 提示,启用后即可返回游戏截图'); }
102
+ catch (error) { setSettingsError(error.message); }
103
+ };
104
+ return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, position: 'relative' } },
105
+ h('button', { type: 'button', title: '实时桌面截图(全局 ' + preference.hotkey + ')\nShift+点击:本次反向切换隐藏 DSH',
106
+ 'aria-label': '截图', onClick: event => {
107
+ void post('start', event.shiftKey ? { hideDsh: !hidePreference } : {}).catch(e => toast(e.message));
108
+ },
109
+ style: { border: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', width: 28, height: 28 } },
110
+ h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7 },
111
+ h('path', { d: 'M3 7h4l2-3h6l2 3h4v13H3z' }), h('circle', { cx: 12, cy: 13, r: 4 }))),
112
+ h('label', { title: '截图前隐藏 DSH,完成或取消后恢复;全局热键也使用此设置',
113
+ style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 11, whiteSpace: 'nowrap', cursor: 'pointer' } },
114
+ h('input', { type: 'checkbox', checked: preference.value, disabled: saving || !preference.ready,
115
+ onChange: changePreference, 'aria-label': '截图时隐藏 DSH', style: { margin: 0, accentColor: '#07a953' } }), '隐藏 DSH'),
116
+ h('button', { type: 'button', 'aria-label': '截图设置', title: '设置截图快捷键',
117
+ disabled: !preference.ready, style: { border: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', padding: '2px 5px' },
118
+ onClick: () => { setSettingsOpen(!settingsOpen); setShortcutDraft(preference.hotkey); setSettingsError(''); } }, '⚙'),
119
+ settingsOpen && h('div', { role: 'dialog', 'aria-label': '截图快捷键设置',
120
+ style: { position: 'absolute', bottom: 'calc(100% + 8px)', left: 0, width: 285, zIndex: 1000,
121
+ background: '#fff', color: '#222', border: '1px solid #ddd', borderRadius: 8, padding: 12, boxShadow: '0 4px 20px #0003', font: '13px system-ui' } },
122
+ h('label', { style: { display: 'block' } }, '全局截图快捷键',
123
+ h('input', { type: 'text', value: shortcutDraft, 'aria-label': '全局截图快捷键', placeholder: 'Ctrl+Alt+A',
124
+ style: { display: 'block', boxSizing: 'border-box', width: '100%', marginTop: 6, padding: 6, color: '#222', background: '#fff', border: '1px solid #aaa', borderRadius: 4 },
125
+ onChange: event => setShortcutDraft(event.target.value),
126
+ onKeyDown: event => { if (event.key === 'Enter') { event.preventDefault(); void saveShortcut(); } if (event.key === 'Escape') setSettingsOpen(false); } })),
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 提示;仅本次运行有效。')),
133
+ (settingsError || preference.problem) && h('div', { role: 'alert', style: { color: '#b22', marginBottom: 6 } }, settingsError || preference.problem),
134
+ h('div', { style: { display: 'flex', gap: 6 } },
135
+ h('button', { type: 'button', disabled: saving, onClick: () => { void saveShortcut(); } }, '保存'),
136
+ h('button', { type: 'button', disabled: saving, onClick: () => setShortcutDraft('Ctrl+Shift+X') }, '恢复默认'),
137
+ h('button', { type: 'button', onClick: () => setSettingsOpen(false) }, '取消'))));
138
+ }
139
+ return { inject: ['slots'], apply(ctx) {
140
+ ctx.slots.inject('conversation.input.left', () => ctx.slots.register({ name: 'conversation.input.left', id: 'dsh-shot', order: 20 }, ShotButton));
141
+ ctx.effect(() => {
142
+ stopped = false; void poll(); const timer = setInterval(() => { void poll(); }, 700);
143
+ return () => { stopped = true; clearInterval(timer); document.getElementById('dsh-shot-toast')?.remove(); };
144
+ });
145
+ } };
146
+ },
147
+ });
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-shot
3
+ name: 'dsh-plugin-live-shot'