dsh-spectacles 0.0.0-stage → 1.0.0
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/CHANGELOG.md +85 -0
- package/LICENSE +202 -0
- package/README.md +140 -2
- package/README.zh.md +141 -0
- package/client.js +392 -0
- package/cordis.patch.yml +3 -0
- package/index.js +745 -0
- package/package.json +60 -3
- package/scripts/look-pick.ps1 +83 -0
- package/scripts/look-region.ps1 +205 -0
- package/scripts/look.ps1 +133 -0
- package/scripts/look2.ps1 +214 -0
- package/scripts/read-text.ps1 +198 -0
- package/scripts/selftest.ps1 +152 -0
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-spectacles",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Point at one window and let the agent read it clearly. Window scoped screen reading for DeepSeek Harness: look (numbers first), read_text (OCR), see (crop + zoom handed to the host vision).",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"os": [
|
|
9
|
+
"win32"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./client": "./client.js",
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"client.js",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"scripts/",
|
|
22
|
+
"README.md",
|
|
23
|
+
"README.zh.md",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"deepseek",
|
|
29
|
+
"dsh",
|
|
30
|
+
"plugin",
|
|
31
|
+
"vision",
|
|
32
|
+
"look",
|
|
33
|
+
"spectacles",
|
|
34
|
+
"screenshot",
|
|
35
|
+
"ocr",
|
|
36
|
+
"window-capture"
|
|
37
|
+
],
|
|
38
|
+
"dsh": {
|
|
39
|
+
"manifestVersion": 1,
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"inject": []
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/yanming119/dsh-spectacles.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/yanming119/dsh-spectacles",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/yanming119/dsh-spectacles/issues"
|
|
58
|
+
},
|
|
59
|
+
"author": "yanming119",
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
6
63
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[int]$TimeoutSec = 30,
|
|
3
|
+
[string]$Out = ""
|
|
4
|
+
)
|
|
5
|
+
$DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
|
|
6
|
+
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
7
|
+
if (-not $Out) { $Out = Join-Path $DataDir 'picked.json' }
|
|
8
|
+
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
Add-Type -TypeDefinition @"
|
|
12
|
+
using System;
|
|
13
|
+
using System.Text;
|
|
14
|
+
using System.Runtime.InteropServices;
|
|
15
|
+
|
|
16
|
+
public class Pk3 {
|
|
17
|
+
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
|
|
18
|
+
[DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
|
|
19
|
+
[DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint flags);
|
|
20
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
21
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
22
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
23
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
24
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
25
|
+
|
|
26
|
+
public struct POINT { public int X, Y; }
|
|
27
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
28
|
+
}
|
|
29
|
+
"@
|
|
30
|
+
|
|
31
|
+
function Get-Title($h) {
|
|
32
|
+
$sb = New-Object System.Text.StringBuilder 512
|
|
33
|
+
[Pk3]::GetWindowText($h, $sb, 512) | Out-Null
|
|
34
|
+
return $sb.ToString()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Emit($obj) {
|
|
38
|
+
$json = $obj | ConvertTo-Json -Compress
|
|
39
|
+
[System.IO.File]::WriteAllText($Out, $json, (New-Object System.Text.UTF8Encoding($false)))
|
|
40
|
+
Write-Output $json
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$start = [Pk3]::GetForegroundWindow()
|
|
44
|
+
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
|
45
|
+
|
|
46
|
+
while ((Get-Date) -lt $deadline) {
|
|
47
|
+
$fg = [Pk3]::GetForegroundWindow()
|
|
48
|
+
if ($fg -ne [IntPtr]::Zero -and $fg -ne $start -and [Pk3]::IsWindowVisible($fg) -and (Get-Title $fg).Length -gt 0) {
|
|
49
|
+
# 真正判据:鼠标此刻就在这个窗口上——说明是用户点它,不是别的窗口自己冒出来。
|
|
50
|
+
$pt = New-Object Pk3+POINT
|
|
51
|
+
[Pk3]::GetCursorPos([ref]$pt) | Out-Null
|
|
52
|
+
$under = [Pk3]::WindowFromPoint($pt)
|
|
53
|
+
$root = if ($under -ne [IntPtr]::Zero) { [Pk3]::GetAncestor($under, 2) } else { [IntPtr]::Zero }
|
|
54
|
+
if ($root -eq $fg) {
|
|
55
|
+
# 别把宿主自己(DeepSeek Harness 的窗口)当成目标——它自己会冒到前台
|
|
56
|
+
$procId = 0
|
|
57
|
+
[Pk3]::GetWindowThreadProcessId($fg, [ref]$procId) | Out-Null
|
|
58
|
+
$pname = ''
|
|
59
|
+
try { $pname = (Get-Process -Id ([int]$procId) -ErrorAction Stop).ProcessName } catch { $pname = '' }
|
|
60
|
+
if ($pname -like '*Harness*' -or (Get-Title $fg) -like '*DeepSeek Harness*') {
|
|
61
|
+
Start-Sleep -Milliseconds 60
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
$r = New-Object Pk3+RECT
|
|
65
|
+
[Pk3]::GetWindowRect($fg, [ref]$r) | Out-Null
|
|
66
|
+
$procId = 0
|
|
67
|
+
[Pk3]::GetWindowThreadProcessId($fg, [ref]$procId) | Out-Null
|
|
68
|
+
Emit ([ordered]@{
|
|
69
|
+
ok = $true
|
|
70
|
+
when = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
|
|
71
|
+
hwnd = [int]$fg
|
|
72
|
+
title = (Get-Title $fg)
|
|
73
|
+
pid = [int]$procId
|
|
74
|
+
rect = @([int]$r.Left, [int]$r.Top, [int]($r.Right - $r.Left), [int]($r.Bottom - $r.Top))
|
|
75
|
+
})
|
|
76
|
+
exit 0
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
Start-Sleep -Milliseconds 60
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Emit ([ordered]@{ ok = $false; error = 'timeout: no click' })
|
|
83
|
+
exit 1
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[int]$TimeoutSec = 60,
|
|
3
|
+
[string]$Out = ""
|
|
4
|
+
)
|
|
5
|
+
$DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
|
|
6
|
+
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
7
|
+
if (-not $Out) { $Out = Join-Path $DataDir 'region.json' }
|
|
8
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
12
|
+
Add-Type -AssemblyName System.Drawing
|
|
13
|
+
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @"
|
|
14
|
+
using System;
|
|
15
|
+
using System.Text;
|
|
16
|
+
using System.Runtime.InteropServices;
|
|
17
|
+
|
|
18
|
+
public class Rg {
|
|
19
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
20
|
+
[DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
|
|
21
|
+
[DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint flags);
|
|
22
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
23
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
24
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
25
|
+
|
|
26
|
+
public struct POINT { public int X, Y; }
|
|
27
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
28
|
+
}
|
|
29
|
+
"@
|
|
30
|
+
[void][Rg]::SetProcessDPIAware()
|
|
31
|
+
|
|
32
|
+
$vs = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
|
33
|
+
|
|
34
|
+
# 先冻一帧屏:遮罩下面是这张定格,选框里露出原图,框外压暗 —— 不动就能选得准
|
|
35
|
+
$bg = New-Object System.Drawing.Bitmap $vs.Width, $vs.Height
|
|
36
|
+
$g0 = [System.Drawing.Graphics]::FromImage($bg)
|
|
37
|
+
$g0.CopyFromScreen($vs.Left, $vs.Top, 0, 0, (New-Object System.Drawing.Size $vs.Width, $vs.Height))
|
|
38
|
+
$g0.Dispose()
|
|
39
|
+
|
|
40
|
+
$script:bg = $bg
|
|
41
|
+
$script:sel = $null
|
|
42
|
+
$script:dragging = $false
|
|
43
|
+
$script:done = $false
|
|
44
|
+
$script:cancelled = $false
|
|
45
|
+
$script:start = $null
|
|
46
|
+
$script:dim = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(120, 0, 0, 0))
|
|
47
|
+
$script:pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 0, 205, 120)), 2
|
|
48
|
+
$script:penDot = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 255, 255, 255)), 1
|
|
49
|
+
$script:font = New-Object System.Drawing.Font 'Microsoft YaHei UI', 12, ([System.Drawing.FontStyle]::Bold)
|
|
50
|
+
$script:brText = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::White)
|
|
51
|
+
$script:brBox = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(215, 20, 24, 30))
|
|
52
|
+
|
|
53
|
+
$form = New-Object System.Windows.Forms.Form
|
|
54
|
+
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
|
|
55
|
+
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
|
|
56
|
+
$form.Bounds = New-Object System.Drawing.Rectangle $vs.Left, $vs.Top, $vs.Width, $vs.Height
|
|
57
|
+
$form.TopMost = $true
|
|
58
|
+
$form.ShowInTaskbar = $false
|
|
59
|
+
$form.Cursor = [System.Windows.Forms.Cursors]::Cross
|
|
60
|
+
$form.KeyPreview = $true
|
|
61
|
+
$form.BackColor = [System.Drawing.Color]::Black
|
|
62
|
+
try { $form.GetType().GetProperty('DoubleBuffered', 'Instance,NonPublic').SetValue($form, $true) } catch {}
|
|
63
|
+
|
|
64
|
+
function Draw-Tag($g, $text, $x, $y) {
|
|
65
|
+
$sz = $g.MeasureString($text, $script:font)
|
|
66
|
+
$w = [int]$sz.Width + 16; $h = [int]$sz.Height + 8
|
|
67
|
+
$bx = [Math]::Max(0, [Math]::Min($x, $script:bg.Width - $w))
|
|
68
|
+
$by = [Math]::Max(0, [Math]::Min($y, $script:bg.Height - $h))
|
|
69
|
+
$g.FillRectangle($script:brBox, $bx, $by, $w, $h)
|
|
70
|
+
$g.DrawString($text, $script:font, $script:brText, ($bx + 8), ($by + 4))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$form.Add_Paint({
|
|
74
|
+
param($s, $e)
|
|
75
|
+
$g = $e.Graphics
|
|
76
|
+
$g.DrawImageUnscaled($script:bg, 0, 0)
|
|
77
|
+
$r = $script:sel
|
|
78
|
+
if ($r -and $r.Width -gt 0 -and $r.Height -gt 0) {
|
|
79
|
+
$W = $script:bg.Width; $H = $script:bg.Height
|
|
80
|
+
$g.FillRectangle($script:dim, 0, 0, $W, [Math]::Max(0, $r.Top))
|
|
81
|
+
$g.FillRectangle($script:dim, 0, $r.Bottom, $W, [Math]::Max(0, $H - $r.Bottom))
|
|
82
|
+
$g.FillRectangle($script:dim, 0, $r.Top, [Math]::Max(0, $r.Left), $r.Height)
|
|
83
|
+
$g.FillRectangle($script:dim, $r.Right, $r.Top, [Math]::Max(0, $W - $r.Right), $r.Height)
|
|
84
|
+
$g.DrawRectangle($script:pen, $r)
|
|
85
|
+
Draw-Tag $g ("{0} x {1}" -f $r.Width, $r.Height) ($r.Left + 6) ($r.Top + 6)
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
$g.FillRectangle($script:dim, 0, 0, $script:bg.Width, $script:bg.Height)
|
|
89
|
+
Draw-Tag $g '拖一个框;Esc 取消' 40 40
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
$form.Add_MouseDown({
|
|
94
|
+
param($s, $e)
|
|
95
|
+
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
96
|
+
$script:start = $e.Location
|
|
97
|
+
$script:sel = New-Object System.Drawing.Rectangle $e.X, $e.Y, 0, 0
|
|
98
|
+
$script:dragging = $true
|
|
99
|
+
$s.Invalidate()
|
|
100
|
+
}
|
|
101
|
+
elseif ($e.Button -eq [System.Windows.Forms.MouseButtons]::Right) {
|
|
102
|
+
$script:cancelled = $true
|
|
103
|
+
$s.Close()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
$form.Add_MouseMove({
|
|
108
|
+
param($s, $e)
|
|
109
|
+
if (-not $script:dragging) { return }
|
|
110
|
+
$x1 = [Math]::Min($script:start.X, $e.X); $y1 = [Math]::Min($script:start.Y, $e.Y)
|
|
111
|
+
$x2 = [Math]::Max($script:start.X, $e.X); $y2 = [Math]::Max($script:start.Y, $e.Y)
|
|
112
|
+
$script:sel = New-Object System.Drawing.Rectangle $x1, $y1, ($x2 - $x1), ($y2 - $y1)
|
|
113
|
+
$s.Invalidate()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
$closeTimer = New-Object System.Windows.Forms.Timer
|
|
117
|
+
$closeTimer.Interval = 160
|
|
118
|
+
$closeTimer.Add_Tick({ $closeTimer.Stop(); $form.Close() })
|
|
119
|
+
|
|
120
|
+
$form.Add_MouseUp({
|
|
121
|
+
param($s, $e)
|
|
122
|
+
if (-not $script:dragging) { return }
|
|
123
|
+
$script:dragging = $false
|
|
124
|
+
if ($script:sel -and $script:sel.Width -ge 8 -and $script:sel.Height -ge 8) {
|
|
125
|
+
$script:done = $true
|
|
126
|
+
# 先把遮罩藏掉(人立刻看见框定住了),窗口晚 160ms 再关:
|
|
127
|
+
# 松手这一下要是当场关窗,抬键消息会漏给下面的窗口,有的程序会当成一次点击。
|
|
128
|
+
$s.Visible = $false
|
|
129
|
+
$closeTimer.Start()
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
$form.Add_KeyDown({
|
|
134
|
+
param($s, $e)
|
|
135
|
+
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
|
|
136
|
+
$script:cancelled = $true
|
|
137
|
+
$s.Close()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
$form.Add_Shown({ $form.Activate() })
|
|
142
|
+
|
|
143
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
144
|
+
$timer.Interval = [Math]::Max(1000, $TimeoutSec * 1000)
|
|
145
|
+
$timer.Add_Tick({ $script:cancelled = $true; $form.Close() })
|
|
146
|
+
$timer.Start()
|
|
147
|
+
|
|
148
|
+
[System.Windows.Forms.Application]::Run($form)
|
|
149
|
+
$timer.Stop()
|
|
150
|
+
|
|
151
|
+
if (-not $script:done -or -not $script:sel) {
|
|
152
|
+
@{ ok = $false; error = 'cancelled' } | ConvertTo-Json -Compress
|
|
153
|
+
exit 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# 等遮罩真的消失,不然 WindowFromPoint 会点到我们自己
|
|
157
|
+
Start-Sleep -Milliseconds 90
|
|
158
|
+
|
|
159
|
+
$sel = $script:sel
|
|
160
|
+
$sx = $vs.Left + $sel.X
|
|
161
|
+
$sy = $vs.Top + $sel.Y
|
|
162
|
+
|
|
163
|
+
$pt = New-Object Rg+POINT
|
|
164
|
+
$pt.X = $sx + [int]($sel.Width / 2)
|
|
165
|
+
$pt.Y = $sy + [int]($sel.Height / 2)
|
|
166
|
+
$h = [Rg]::WindowFromPoint($pt)
|
|
167
|
+
if ($h -eq [IntPtr]::Zero) { @{ ok = $false; error = 'no window under the box' } | ConvertTo-Json -Compress; exit 1 }
|
|
168
|
+
$root = [Rg]::GetAncestor($h, 2)
|
|
169
|
+
if ($root -eq [IntPtr]::Zero) { $root = $h }
|
|
170
|
+
|
|
171
|
+
$sb = New-Object System.Text.StringBuilder 512
|
|
172
|
+
[Rg]::GetWindowText($root, $sb, 512) | Out-Null
|
|
173
|
+
if ($sb.Length -eq 0) {
|
|
174
|
+
@{ ok = $false; error = '那个位置没有窗口(是桌面空白处),换个地方再框' } | ConvertTo-Json -Compress
|
|
175
|
+
exit 1
|
|
176
|
+
}
|
|
177
|
+
$wr = New-Object Rg+RECT
|
|
178
|
+
[Rg]::GetWindowRect($root, [ref]$wr) | Out-Null
|
|
179
|
+
$procId = 0
|
|
180
|
+
[Rg]::GetWindowThreadProcessId($root, [ref]$procId) | Out-Null
|
|
181
|
+
|
|
182
|
+
# 框可能越出窗口:裁到窗口里面,换算成相对窗口左上角
|
|
183
|
+
$ix = [Math]::Max($sx, $wr.Left); $iy = [Math]::Max($sy, $wr.Top)
|
|
184
|
+
$ir = [Math]::Min($sx + $sel.Width, $wr.Right); $ib = [Math]::Min($sy + $sel.Height, $wr.Bottom)
|
|
185
|
+
$cw = [Math]::Max(1, $ir - $ix)
|
|
186
|
+
$ch = [Math]::Max(1, $ib - $iy)
|
|
187
|
+
$cx = $ix - $wr.Left
|
|
188
|
+
$cy = $iy - $wr.Top
|
|
189
|
+
|
|
190
|
+
$res = [ordered]@{
|
|
191
|
+
ok = $true
|
|
192
|
+
when = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')
|
|
193
|
+
hwnd = [int64]$root
|
|
194
|
+
title = $sb.ToString()
|
|
195
|
+
pid = $procId
|
|
196
|
+
rect = @($wr.Left, $wr.Top, ($wr.Right - $wr.Left), ($wr.Bottom - $wr.Top))
|
|
197
|
+
screen = @($sx, $sy, $sel.Width, $sel.Height)
|
|
198
|
+
crop = @($cx, $cy, $cw, $ch)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$dir = Split-Path $Out -Parent
|
|
202
|
+
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
|
203
|
+
[System.IO.File]::WriteAllText($Out, ($res | ConvertTo-Json -Compress), (New-Object System.Text.UTF8Encoding($false)))
|
|
204
|
+
|
|
205
|
+
$res | ConvertTo-Json -Compress
|
package/scripts/look.ps1
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$Crop = "",
|
|
3
|
+
[double]$Scale = 1.0,
|
|
4
|
+
[switch]$Image,
|
|
5
|
+
[string]$Out = "",
|
|
6
|
+
[string]$SaveDir = "",
|
|
7
|
+
[int]$TimeoutMs = 6000
|
|
8
|
+
)
|
|
9
|
+
$DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
|
|
10
|
+
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
11
|
+
if (-not $SaveDir) { $SaveDir = $DataDir }
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
Add-Type -AssemblyName System.Drawing
|
|
15
|
+
|
|
16
|
+
# fullscreen capture defaults to GDI: fast (~200ms) and needs no external binary.
|
|
17
|
+
# set DSH_LOOK_FFMPEG only to force the ffmpeg/DXGI path (slower, but sees some hardware overlays).
|
|
18
|
+
$ffmpeg = $env:DSH_LOOK_FFMPEG
|
|
19
|
+
if ($ffmpeg -and -not (Test-Path $ffmpeg)) { $ffmpeg = '' }
|
|
20
|
+
|
|
21
|
+
if (-not (Test-Path $SaveDir)) { New-Item -ItemType Directory -Path $SaveDir -Force | Out-Null }
|
|
22
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
|
|
23
|
+
$tmp = Join-Path $SaveDir "raw-$stamp.png"
|
|
24
|
+
$stateFile = Join-Path $SaveDir 'last-grid.txt'
|
|
25
|
+
|
|
26
|
+
$sw = [Diagnostics.Stopwatch]::StartNew()
|
|
27
|
+
$source = 'gdi-copyfromscreen'
|
|
28
|
+
if ($ffmpeg) {
|
|
29
|
+
$source = 'dxgi-ddagrabb'
|
|
30
|
+
& $ffmpeg -hide_banner -loglevel error -f lavfi -i "ddagrab=0:framerate=1" -vf "hwdownload,format=bgra" -frames:v 1 -y $tmp 2>$null | Out-Null
|
|
31
|
+
} else {
|
|
32
|
+
# GDI screen copy (DPI-aware, no external binary)
|
|
33
|
+
if (-not ('YM.Dpi' -as [type])) {
|
|
34
|
+
Add-Type -Namespace YM -Name Dpi -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern int GetSystemMetrics(int n);'
|
|
35
|
+
}
|
|
36
|
+
[void][YM.Dpi]::SetProcessDPIAware()
|
|
37
|
+
$sw0 = [YM.Dpi]::GetSystemMetrics(0); $sh0 = [YM.Dpi]::GetSystemMetrics(1)
|
|
38
|
+
$shot = New-Object System.Drawing.Bitmap $sw0, $sh0
|
|
39
|
+
$g0 = [System.Drawing.Graphics]::FromImage($shot)
|
|
40
|
+
$g0.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size $sw0, $sh0))
|
|
41
|
+
$g0.Dispose()
|
|
42
|
+
$shot.Save($tmp, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
43
|
+
$shot.Dispose()
|
|
44
|
+
}
|
|
45
|
+
$sw.Stop()
|
|
46
|
+
if (-not (Test-Path $tmp)) { Write-Output '{"ok":false,"error":"capture failed"}'; exit 1 }
|
|
47
|
+
|
|
48
|
+
$src = [System.Drawing.Bitmap]::FromFile($tmp)
|
|
49
|
+
$screenW = $src.Width
|
|
50
|
+
$screenH = $src.Height
|
|
51
|
+
|
|
52
|
+
$x = 0; $y = 0; $w = $screenW; $h = $screenH
|
|
53
|
+
if ($Crop) {
|
|
54
|
+
$p = $Crop -split ','
|
|
55
|
+
if ($p.Length -eq 4) {
|
|
56
|
+
$x = [int]$p[0]; $y = [int]$p[1]; $w = [int]$p[2]; $h = [int]$p[3]
|
|
57
|
+
if ($x -lt 0) { $x = 0 }
|
|
58
|
+
if ($y -lt 0) { $y = 0 }
|
|
59
|
+
if (($x + $w) -gt $screenW) { $w = $screenW - $x }
|
|
60
|
+
if (($y + $h) -gt $screenH) { $h = $screenH - $y }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function New-StatBitmap($bmp, $tw, $th) {
|
|
65
|
+
$small = New-Object System.Drawing.Bitmap $tw, $th
|
|
66
|
+
$g = [System.Drawing.Graphics]::FromImage($small)
|
|
67
|
+
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBilinear
|
|
68
|
+
$g.DrawImage($bmp, (New-Object System.Drawing.Rectangle 0, 0, $tw, $th), (New-Object System.Drawing.Rectangle $x, $y, $w, $h), [System.Drawing.GraphicsUnit]::Pixel)
|
|
69
|
+
$g.Dispose()
|
|
70
|
+
return $small
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$stat = New-StatBitmap $src 64 36
|
|
74
|
+
$gw = $stat.Width; $gh = $stat.Height
|
|
75
|
+
$gray = New-Object 'int[]' ($gw * $gh)
|
|
76
|
+
$sumR = 0; $sumG = 0; $sumB = 0
|
|
77
|
+
for ($yy = 0; $yy -lt $gh; $yy++) {
|
|
78
|
+
for ($xx = 0; $xx -lt $gw; $xx++) {
|
|
79
|
+
$c = $stat.GetPixel($xx, $yy)
|
|
80
|
+
$sumR += $c.R; $sumG += $c.G; $sumB += $c.B
|
|
81
|
+
$gray[$yy * $gw + $xx] = [int](($c.R * 0.299) + ($c.G * 0.587) + ($c.B * 0.114))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
$n = $gw * $gh
|
|
85
|
+
$avgR = [int]($sumR / $n); $avgG = [int]($sumG / $n); $avgB = [int]($sumB / $n)
|
|
86
|
+
|
|
87
|
+
$edges = 0
|
|
88
|
+
for ($yy = 1; $yy -lt $gh - 1; $yy++) {
|
|
89
|
+
for ($xx = 1; $xx -lt $gw - 1; $xx++) {
|
|
90
|
+
$i = $yy * $gw + $xx
|
|
91
|
+
$dx = [Math]::Abs($gray[$i + 1] - $gray[$i - 1])
|
|
92
|
+
$dy = [Math]::Abs($gray[$i + $gw] - $gray[$i - $gw])
|
|
93
|
+
if (($dx + $dy) -gt 60) { $edges++ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
$edgeDensity = [Math]::Round($edges / $n, 4)
|
|
97
|
+
|
|
98
|
+
$cur = ($gray | ForEach-Object { if ($_ -lt 10) { '00' } elseif ($_ -lt 100) { '0' + $_ } else { "$_" } }) -join ''
|
|
99
|
+
$changed = $null
|
|
100
|
+
if (Test-Path $stateFile) { $prev = Get-Content $stateFile -Raw; if ($prev.Trim() -eq $cur) { $changed = $false } else { $changed = $true } }
|
|
101
|
+
Set-Content -Path $stateFile -Value $cur -NoNewline
|
|
102
|
+
|
|
103
|
+
$stat.Dispose()
|
|
104
|
+
|
|
105
|
+
$pngPath = ''
|
|
106
|
+
if ($Image -or $Out) {
|
|
107
|
+
$target = New-Object System.Drawing.Bitmap ([int]($w * $Scale)), ([int]($h * $Scale))
|
|
108
|
+
$g2 = [System.Drawing.Graphics]::FromImage($target)
|
|
109
|
+
$g2.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
110
|
+
$g2.DrawImage($src, (New-Object System.Drawing.Rectangle 0, 0, $target.Width, $target.Height), (New-Object System.Drawing.Rectangle $x, $y, $w, $h), [System.Drawing.GraphicsUnit]::Pixel)
|
|
111
|
+
$g2.Dispose()
|
|
112
|
+
if ($Out) { $pngPath = $Out } else { $pngPath = Join-Path $SaveDir "look-$stamp.png" }
|
|
113
|
+
$target.Save($pngPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
114
|
+
$target.Dispose()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$src.Dispose()
|
|
118
|
+
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
119
|
+
|
|
120
|
+
$obj = [ordered]@{
|
|
121
|
+
ok = $true
|
|
122
|
+
when = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
|
|
123
|
+
source = $source
|
|
124
|
+
screen = @{ w = $screenW; h = $screenH }
|
|
125
|
+
region = @{ x = $x; y = $y; w = $w; h = $h }
|
|
126
|
+
captureMs = $sw.ElapsedMilliseconds
|
|
127
|
+
avgColor = ('#{0:X2}{1:X2}{2:X2}' -f $avgR, $avgG, $avgB)
|
|
128
|
+
edgeDensity = $edgeDensity
|
|
129
|
+
changed = $changed
|
|
130
|
+
grid = "$gw" + 'x' + "$gh"
|
|
131
|
+
png = $pngPath
|
|
132
|
+
}
|
|
133
|
+
$obj | ConvertTo-Json -Compress -Depth 4
|