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.
@@ -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
+ }