dsh-gui-control 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +187 -2
- package/assets/icon-180.png +0 -0
- package/assets/screenshot-bilibili.png +0 -0
- package/assets/screenshot-calc.png +0 -0
- package/assets/screenshot-sheet.png +0 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +8 -0
- package/lib/index.js +346 -0
- package/lib/scripts/DshGui.cs +267 -0
- package/lib/scripts/DshUia.cs +212 -0
- package/lib/scripts/gui-anchors.ps1 +146 -0
- package/lib/scripts/gui-daemon.ps1 +89 -0
- package/lib/scripts/gui-lib.ps1 +68 -0
- package/lib/scripts/gui-ocr.ps1 +71 -0
- package/lib/scripts/gui-ops.ps1 +915 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -3
- package/screenshots.json +5 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#requires -Version 5.1
|
|
2
|
+
# Persistent GUI-control daemon.
|
|
3
|
+
# Compiles the native helpers once, then serves ops over loopback TCP so each action
|
|
4
|
+
# costs ~5 ms instead of spawning PowerShell + recompiling C# (~700 ms).
|
|
5
|
+
# ASCII only.
|
|
6
|
+
param(
|
|
7
|
+
[int]$Port = 38471,
|
|
8
|
+
[string]$Root = ''
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = 'Stop'
|
|
12
|
+
if ([string]::IsNullOrEmpty($Root)) { $Root = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
13
|
+
$Root = (Resolve-Path $Root).Path
|
|
14
|
+
|
|
15
|
+
. (Join-Path $Root 'gui-ops.ps1')
|
|
16
|
+
|
|
17
|
+
$logPath = Join-Path $Root 'gui-daemon.log'
|
|
18
|
+
function Write-Log([string]$m) {
|
|
19
|
+
try { Add-Content -Path $logPath -Value ((Get-Date).ToString('MM-dd HH:mm:ss.fff') + ' ' + $m) -Encoding UTF8 } catch { }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
Import-DshGui $Root
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
Write-Log ('compile failed: ' + $_.Exception.Message)
|
|
27
|
+
throw
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# UIA is optional: some environments cannot load the accessibility assemblies.
|
|
31
|
+
try {
|
|
32
|
+
Import-DshUia $Root
|
|
33
|
+
Write-Log 'uia loaded'
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
Write-Log ('uia unavailable: ' + $_.Exception.Message)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
|
|
40
|
+
try { $listener.Server.SetSocketOption([System.Net.Sockets.SocketOptionLevel]::Socket, [System.Net.Sockets.SocketOptionName]::ReuseAddress, $true) } catch { }
|
|
41
|
+
$listener.Start()
|
|
42
|
+
Write-Log "listening on $Port (pid $PID)"
|
|
43
|
+
|
|
44
|
+
$running = $true
|
|
45
|
+
$served = 0
|
|
46
|
+
|
|
47
|
+
while ($running) {
|
|
48
|
+
$client = $listener.AcceptTcpClient()
|
|
49
|
+
$client.NoDelay = $true
|
|
50
|
+
$client.ReceiveTimeout = 120000
|
|
51
|
+
try {
|
|
52
|
+
$stream = $client.GetStream()
|
|
53
|
+
$reader = New-Object System.IO.StreamReader($stream, (New-Object System.Text.UTF8Encoding($false)))
|
|
54
|
+
$writer = New-Object System.IO.StreamWriter($stream, (New-Object System.Text.UTF8Encoding($false)))
|
|
55
|
+
$writer.AutoFlush = $true
|
|
56
|
+
while ($true) {
|
|
57
|
+
$line = $reader.ReadLine()
|
|
58
|
+
if ($null -eq $line) { break }
|
|
59
|
+
if ($line.Trim().Length -eq 0) { continue }
|
|
60
|
+
$op = $null
|
|
61
|
+
$resp = $null
|
|
62
|
+
try {
|
|
63
|
+
$op = $line | ConvertFrom-Json
|
|
64
|
+
$resp = Invoke-GuiOp $op
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
$resp = @{ ok = $false; error = $_.Exception.Message }
|
|
68
|
+
}
|
|
69
|
+
$served++
|
|
70
|
+
try {
|
|
71
|
+
$writer.WriteLine((ConvertTo-Json $resp -Compress -Depth 8))
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
Write-Log ('write failed: ' + $_.Exception.Message)
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
if ($null -ne $op -and $op.op -eq 'quit') { $running = $false; break }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
Write-Log ('connection error: ' + $_.Exception.Message)
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
try { $client.Close() } catch { }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Write-Log "stopped after $served ops"
|
|
89
|
+
$listener.Stop()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#requires -Version 5.1
|
|
2
|
+
# Client helpers for the persistent daemon. Dot-source this, then call G/GStart.
|
|
3
|
+
# ASCII only.
|
|
4
|
+
#
|
|
5
|
+
# . C:\path\to\gui-control\gui-lib.ps1
|
|
6
|
+
# G @{ op = 'screen' }
|
|
7
|
+
# G @{ op = 'click'; win = 'bilibili'; x = 120; y = 40 }
|
|
8
|
+
# G @{ op = 'findtext'; win = 'bilibili'; text = '第153话' }
|
|
9
|
+
|
|
10
|
+
$script:GuiRoot = $PSScriptRoot
|
|
11
|
+
$script:GuiPort = 38471
|
|
12
|
+
|
|
13
|
+
function Test-GuiDaemon {
|
|
14
|
+
try {
|
|
15
|
+
$c = New-Object System.Net.Sockets.TcpClient
|
|
16
|
+
$c.Connect('127.0.0.1', $script:GuiPort)
|
|
17
|
+
$c.Close()
|
|
18
|
+
return $true
|
|
19
|
+
}
|
|
20
|
+
catch { return $false }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Start-GuiDaemon {
|
|
24
|
+
if (Test-GuiDaemon) { return 'already running' }
|
|
25
|
+
$daemon = Join-Path $script:GuiRoot 'gui-daemon.ps1'
|
|
26
|
+
Start-Process -FilePath 'powershell.exe' `
|
|
27
|
+
-ArgumentList @('-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', $daemon, '-Port', $script:GuiPort) `
|
|
28
|
+
-WindowStyle Hidden | Out-Null
|
|
29
|
+
for ($i = 0; $i -lt 100; $i++) {
|
|
30
|
+
Start-Sleep -Milliseconds 100
|
|
31
|
+
if (Test-GuiDaemon) { return 'started' }
|
|
32
|
+
}
|
|
33
|
+
throw 'daemon did not come up (see gui-daemon.log)'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Stop-GuiDaemon {
|
|
37
|
+
try { [void](Send-GuiRaw @{ op = 'quit' }) } catch { }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Send-GuiRaw($op) {
|
|
41
|
+
$json = ($op | ConvertTo-Json -Compress -Depth 8)
|
|
42
|
+
$client = New-Object System.Net.Sockets.TcpClient
|
|
43
|
+
$client.NoDelay = $true
|
|
44
|
+
$client.Connect('127.0.0.1', $script:GuiPort)
|
|
45
|
+
try {
|
|
46
|
+
$stream = $client.GetStream()
|
|
47
|
+
$writer = New-Object System.IO.StreamWriter($stream, (New-Object System.Text.UTF8Encoding($false)))
|
|
48
|
+
$writer.AutoFlush = $true
|
|
49
|
+
$reader = New-Object System.IO.StreamReader($stream, (New-Object System.Text.UTF8Encoding($false)))
|
|
50
|
+
$writer.WriteLine($json)
|
|
51
|
+
$line = $reader.ReadLine()
|
|
52
|
+
if ($null -eq $line) { throw 'daemon closed the connection' }
|
|
53
|
+
return ($line | ConvertFrom-Json)
|
|
54
|
+
}
|
|
55
|
+
finally { $client.Close() }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Main entry: runs one op, throws on failure, returns the raw result payload.
|
|
59
|
+
function G($op) {
|
|
60
|
+
$r = Send-GuiRaw $op
|
|
61
|
+
if (-not $r.ok) { throw $r.error }
|
|
62
|
+
return $r.result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Batch: run an array of ops in ONE round-trip.
|
|
66
|
+
function GBatch([object[]]$ops) {
|
|
67
|
+
return (G @{ op = 'batch'; ops = $ops })
|
|
68
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#requires -Version 5.1
|
|
2
|
+
# Windows.Media.Ocr wrapper for PowerShell 5.1 (WinRT interop).
|
|
3
|
+
# Gives semantic targeting: find a control by its text instead of guessing pixels.
|
|
4
|
+
# ASCII only.
|
|
5
|
+
|
|
6
|
+
$script:OcrReady = $false
|
|
7
|
+
$script:OcrEngine = $null
|
|
8
|
+
$script:AsTask = $null
|
|
9
|
+
|
|
10
|
+
function Initialize-Ocr {
|
|
11
|
+
if ($script:OcrReady) { return }
|
|
12
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime -ErrorAction Stop
|
|
13
|
+
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
14
|
+
$null = [Windows.Storage.StorageFile, Windows.Foundation, ContentType = WindowsRuntime]
|
|
15
|
+
$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
|
16
|
+
|
|
17
|
+
$script:AsTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
|
18
|
+
$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and
|
|
19
|
+
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
|
|
20
|
+
})[0]
|
|
21
|
+
if ($null -eq $script:AsTask) { throw 'AsTask(IAsyncOperation<T>) not found' }
|
|
22
|
+
|
|
23
|
+
$script:OcrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
24
|
+
if ($null -eq $script:OcrEngine) { throw 'No OCR engine for user profile languages' }
|
|
25
|
+
$script:OcrReady = $true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Await-WinRt($op, [Type]$type) {
|
|
29
|
+
$m = $script:AsTask.MakeGenericMethod($type)
|
|
30
|
+
$t = $m.Invoke($null, @($op))
|
|
31
|
+
if (-not $t.Wait(20000)) { throw 'WinRT await timeout' }
|
|
32
|
+
if ($t.IsFaulted) { throw $t.Exception.GetBaseException().Message }
|
|
33
|
+
return $t.Result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Returns an array of @{ text; x; y; w; h } using absolute screen coordinates.
|
|
37
|
+
# $offsetX/$offsetY are the screen position of the captured image's top-left corner.
|
|
38
|
+
function Get-OcrLines([string]$path, [int]$offsetX, [int]$offsetY) {
|
|
39
|
+
Initialize-Ocr
|
|
40
|
+
$file = Await-WinRt ([Windows.Storage.StorageFile]::GetFileFromPathAsync($path)) ([Windows.Storage.StorageFile])
|
|
41
|
+
$stream = Await-WinRt ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
|
|
42
|
+
$decoder = Await-WinRt ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
|
43
|
+
$bmp = Await-WinRt ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
|
44
|
+
$res = Await-WinRt ($script:OcrEngine.RecognizeAsync($bmp)) ([Windows.Media.Ocr.OcrResult])
|
|
45
|
+
|
|
46
|
+
$lines = @()
|
|
47
|
+
foreach ($line in @($res.Lines)) {
|
|
48
|
+
$words = @($line.Words)
|
|
49
|
+
if ($words.Count -eq 0) { continue }
|
|
50
|
+
$minX = [double]::MaxValue; $minY = [double]::MaxValue
|
|
51
|
+
$maxX = 0.0; $maxY = 0.0
|
|
52
|
+
foreach ($wd in $words) {
|
|
53
|
+
$r = $wd.BoundingRect
|
|
54
|
+
if ($r.X -lt $minX) { $minX = $r.X }
|
|
55
|
+
if ($r.Y -lt $minY) { $minY = $r.Y }
|
|
56
|
+
if (($r.X + $r.Width) -gt $maxX) { $maxX = $r.X + $r.Width }
|
|
57
|
+
if (($r.Y + $r.Height) -gt $maxY) { $maxY = $r.Y + $r.Height }
|
|
58
|
+
}
|
|
59
|
+
$lines += @{
|
|
60
|
+
text = $line.Text
|
|
61
|
+
x = [int]($offsetX + $minX)
|
|
62
|
+
y = [int]($offsetY + $minY)
|
|
63
|
+
w = [int]($maxX - $minX)
|
|
64
|
+
h = [int]($maxY - $minY)
|
|
65
|
+
cx = [int]($offsetX + ($minX + $maxX) / 2)
|
|
66
|
+
cy = [int]($offsetY + ($minY + $maxY) / 2)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
$stream.Dispose()
|
|
70
|
+
return $lines
|
|
71
|
+
}
|