@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1
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/GLOSSARY.md +38 -0
- package/LICENSE +21 -0
- package/README.md +165 -2
- package/assets/accessibility.cs +208 -0
- package/assets/browser-indicators.cs +37 -0
- package/assets/capture.cs +215 -0
- package/assets/clipboard.cs +197 -0
- package/assets/controls.cs +249 -0
- package/assets/indicator-preview.png +0 -0
- package/assets/indicators.cs +252 -0
- package/assets/input-timing.cs +42 -0
- package/assets/node-LICENSE.txt +2732 -0
- package/assets/ocr.cs +86 -0
- package/assets/ownership.cs +59 -0
- package/assets/pointer.cs +241 -0
- package/assets/puppeteer-LICENSE.txt +202 -0
- package/assets/readme-banner.svg +33 -0
- package/assets/stop.cs +197 -0
- package/assets/text.cs +279 -0
- package/assets/wgc.cs +167 -0
- package/assets/window-management.cs +217 -0
- package/assets/window-marker.cs +23 -0
- package/assets/window-references.cs +80 -0
- package/assets/windows.cs +127 -0
- package/dist/accessibility.d.ts +29 -0
- package/dist/accessibility.js +258 -0
- package/dist/browser-actions.d.ts +22 -0
- package/dist/browser-actions.js +246 -0
- package/dist/browser-indicators.d.ts +23 -0
- package/dist/browser-indicators.js +69 -0
- package/dist/browser-profiles.d.ts +10 -0
- package/dist/browser-profiles.js +174 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +2071 -0
- package/dist/capture.d.ts +74 -0
- package/dist/capture.js +199 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +384 -0
- package/dist/desktop.d.ts +20 -0
- package/dist/desktop.js +42 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +409 -0
- package/dist/input-timing.d.ts +24 -0
- package/dist/input-timing.js +70 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +85 -0
- package/dist/native.d.ts +11 -0
- package/dist/native.js +146 -0
- package/dist/ocr.d.ts +52 -0
- package/dist/ocr.js +246 -0
- package/dist/ownership.d.ts +4 -0
- package/dist/ownership.js +24 -0
- package/dist/pointer.d.ts +128 -0
- package/dist/pointer.js +198 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +72 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +449 -0
- package/dist/stop.d.ts +24 -0
- package/dist/stop.js +156 -0
- package/dist/text.d.ts +120 -0
- package/dist/text.js +212 -0
- package/dist/waits.d.ts +19 -0
- package/dist/waits.js +262 -0
- package/dist/window-management.d.ts +25 -0
- package/dist/window-management.js +167 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +553 -0
- package/docs/README.md +55 -0
- package/docs/adr/0001-preserve-reference-behavior.md +9 -0
- package/docs/adr/0002-window-lifetime-references.md +11 -0
- package/docs/adr/0003-exclusive-desktop-control.md +11 -0
- package/docs/adr/0004-pi-first-acceptance.md +9 -0
- package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
- package/docs/agents/domain.md +9 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +9 -0
- package/docs/browser.md +113 -0
- package/docs/implementation-plan.md +153 -0
- package/docs/indicators.md +53 -0
- package/docs/native-interop-research.md +17 -0
- package/docs/ownership.md +25 -0
- package/docs/pi-extension-research.md +57 -0
- package/docs/pointer.md +77 -0
- package/docs/runtime.md +42 -0
- package/docs/settings.md +33 -0
- package/docs/spec.md +230 -0
- package/docs/stopping.md +83 -0
- package/docs/text.md +99 -0
- package/docs/tickets.md +85 -0
- package/docs/validation.md +1420 -0
- package/docs/windows.md +171 -0
- package/package.json +134 -4
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { release } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { provisionRuntime } from "./runtime.js";
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
let cachedWindowsEnvironment;
|
|
10
|
+
export async function windowsHostEnvironment(signal) {
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
if (!process.env.LOCALAPPDATA)
|
|
13
|
+
throw new Error("WINDOWS_DATA_DIRECTORY_MISSING: LOCALAPPDATA is required");
|
|
14
|
+
return { localData: process.env.LOCALAPPDATA, architecture: process.arch };
|
|
15
|
+
}
|
|
16
|
+
if (cachedWindowsEnvironment)
|
|
17
|
+
return cachedWindowsEnvironment;
|
|
18
|
+
let environment;
|
|
19
|
+
try {
|
|
20
|
+
// Fixed variable names, not expanded values: paths containing shell characters remain data.
|
|
21
|
+
const result = await run("cmd.exe", ["/u", "/d", "/c", "set LOCALAPPDATA & set PROCESSOR_ARCHITECTURE"], { signal, timeout: 5000, maxBuffer: 65536, encoding: "buffer" });
|
|
22
|
+
const values = new Map(result.stdout
|
|
23
|
+
.toString("utf16le")
|
|
24
|
+
.split(/\r?\n/u)
|
|
25
|
+
.map((line) => {
|
|
26
|
+
const equals = line.indexOf("=");
|
|
27
|
+
return [line.slice(0, equals), line.slice(equals + 1)];
|
|
28
|
+
}));
|
|
29
|
+
environment = {
|
|
30
|
+
localData: values.get("LOCALAPPDATA") ?? "",
|
|
31
|
+
architecture: values.get("PROCESSOR_ARCHITECTURE") ?? "",
|
|
32
|
+
};
|
|
33
|
+
if (!environment.localData || !environment.architecture)
|
|
34
|
+
throw new Error("Windows environment is incomplete");
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
throw new Error("WSL_BOOTSTRAP_FAILED: enable Windows executable interop and ensure cmd.exe and wslpath are available", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
const translate = async (mode, path) => (await run("wslpath", [mode, path], { signal, timeout: 5000 })).stdout.trim();
|
|
41
|
+
const localData = await translate("-u", environment.localData);
|
|
42
|
+
const architecture = environment.architecture === "AMD64"
|
|
43
|
+
? "x64"
|
|
44
|
+
: environment.architecture === "ARM64"
|
|
45
|
+
? "arm64"
|
|
46
|
+
: environment.architecture;
|
|
47
|
+
cachedWindowsEnvironment = { localData, architecture };
|
|
48
|
+
return cachedWindowsEnvironment;
|
|
49
|
+
}
|
|
50
|
+
async function wslWorkerLaunch(worker, signal) {
|
|
51
|
+
const { localData, architecture } = await windowsHostEnvironment(signal);
|
|
52
|
+
const translate = async (mode, path) => (await run("wslpath", [mode, path], { signal, timeout: 5000 })).stdout.trim();
|
|
53
|
+
const executable = await provisionRuntime({
|
|
54
|
+
directory: join(localData, "pi-computer-use", "runtime"),
|
|
55
|
+
architecture,
|
|
56
|
+
signal,
|
|
57
|
+
});
|
|
58
|
+
const windowsWorker = await translate("-w", worker);
|
|
59
|
+
return { executable, args: [windowsWorker], cwd: localData };
|
|
60
|
+
}
|
|
61
|
+
export async function nativeWorkerLaunch(signal) {
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
const wsl = process.platform === "linux" && /microsoft.*wsl2/i.test(release());
|
|
64
|
+
if (process.platform !== "win32" && !wsl) {
|
|
65
|
+
throw new Error("UNSUPPORTED_HOST: computer use requires native Windows or WSL2 with Windows interop");
|
|
66
|
+
}
|
|
67
|
+
const worker = fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
68
|
+
try {
|
|
69
|
+
await access(worker);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error("WORKER_ASSET_MISSING: install the built package, or run npm run build in a checkout");
|
|
73
|
+
}
|
|
74
|
+
if (wsl)
|
|
75
|
+
return wslWorkerLaunch(worker, signal);
|
|
76
|
+
const localData = process.env.LOCALAPPDATA;
|
|
77
|
+
if (!localData)
|
|
78
|
+
throw new Error("WINDOWS_DATA_DIRECTORY_MISSING: LOCALAPPDATA is required for the private runtime cache");
|
|
79
|
+
const executable = await provisionRuntime({
|
|
80
|
+
directory: join(localData, "pi-computer-use", "runtime"),
|
|
81
|
+
architecture: process.arch,
|
|
82
|
+
signal,
|
|
83
|
+
});
|
|
84
|
+
return { executable, args: [worker], cwd: localData };
|
|
85
|
+
}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { WorkerClient, type WorkerLaunch } from "./client.js";
|
|
2
|
+
import type { WorkerResult } from "./protocol.js";
|
|
3
|
+
export declare function compileNative(sourceNames: string[], references?: string[], signal?: AbortSignal): Promise<WorkerLaunch>;
|
|
4
|
+
export declare function retainInput(): void;
|
|
5
|
+
export declare function confirmInputCleanup(): void;
|
|
6
|
+
export declare function runNativeOperation<T>(operationId: string, action: () => T): T;
|
|
7
|
+
declare class NativeDesktopClient extends WorkerClient {
|
|
8
|
+
execute(action: string, args: Record<string, unknown>, signal?: AbortSignal, operationId?: string | undefined): Promise<WorkerResult>;
|
|
9
|
+
}
|
|
10
|
+
export declare const nativeDesktop: NativeDesktopClient;
|
|
11
|
+
export {};
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { access, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { WorkerClient } from "./client.js";
|
|
8
|
+
export async function compileNative(sourceNames, references = [], signal) {
|
|
9
|
+
if (process.platform !== "win32")
|
|
10
|
+
throw new Error("UNSUPPORTED_HOST: native desktop access requires Windows");
|
|
11
|
+
const sources = sourceNames.map((name) => fileURLToPath(new URL(`../assets/${name}`, import.meta.url)));
|
|
12
|
+
const digest = createHash("sha256");
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
digest.update(source);
|
|
15
|
+
try {
|
|
16
|
+
digest.update(await readFile(source));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`NATIVE_RESOURCE_MISSING: required resource ${source}`, {
|
|
20
|
+
cause: error,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
digest.update(references.join("\n"));
|
|
25
|
+
const hash = digest.digest("hex").slice(0, 20);
|
|
26
|
+
const data = process.env.LOCALAPPDATA;
|
|
27
|
+
const windows = process.env.WINDIR;
|
|
28
|
+
if (!data || !windows)
|
|
29
|
+
throw new Error("NATIVE_SETUP_FAILED: LOCALAPPDATA and WINDIR are required");
|
|
30
|
+
const directory = join(data, "pi-computer-use", "native", hash);
|
|
31
|
+
const executable = join(directory, "windows.exe");
|
|
32
|
+
try {
|
|
33
|
+
await access(executable);
|
|
34
|
+
return { executable, args: [], cwd: directory };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* Compile this package's native source once per content version. */
|
|
38
|
+
}
|
|
39
|
+
const compiler = join(windows, "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe");
|
|
40
|
+
try {
|
|
41
|
+
await access(compiler);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error("NATIVE_COMPILER_MISSING: Windows .NET Framework64 csc.exe is unavailable");
|
|
45
|
+
}
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
await mkdir(directory, { recursive: true });
|
|
48
|
+
const temporary = join(directory, `windows-${randomUUID()}.exe`);
|
|
49
|
+
try {
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(compiler, [
|
|
52
|
+
"/nologo",
|
|
53
|
+
"/target:exe",
|
|
54
|
+
`/out:${temporary}`,
|
|
55
|
+
"/r:System.Web.Extensions.dll",
|
|
56
|
+
...references.map((reference) => `/r:${reference}`),
|
|
57
|
+
...sources,
|
|
58
|
+
], {
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
windowsHide: true,
|
|
61
|
+
cwd: dirname(compiler),
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
let diagnostic = "";
|
|
65
|
+
for (const stream of [child.stdout, child.stderr])
|
|
66
|
+
stream.on("data", (chunk) => {
|
|
67
|
+
diagnostic = (diagnostic + chunk).slice(-4096);
|
|
68
|
+
});
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
child.kill();
|
|
71
|
+
reject(new Error("NATIVE_COMPILE_TIMEOUT"));
|
|
72
|
+
}, 30_000);
|
|
73
|
+
child.once("error", (error) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
reject(error);
|
|
76
|
+
});
|
|
77
|
+
child.once("close", (code) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
code === 0
|
|
80
|
+
? resolve()
|
|
81
|
+
: reject(new Error(`NATIVE_COMPILE_FAILED: ${diagnostic.trim()}`));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
try {
|
|
86
|
+
await rename(temporary, executable);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!(error instanceof Error &&
|
|
90
|
+
"code" in error &&
|
|
91
|
+
(error.code === "EEXIST" || error.code === "EPERM")))
|
|
92
|
+
throw error;
|
|
93
|
+
await access(executable);
|
|
94
|
+
}
|
|
95
|
+
return { executable, args: [], cwd: directory };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await rm(temporary, { force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let inputPending = false;
|
|
102
|
+
export function retainInput() {
|
|
103
|
+
inputPending = true;
|
|
104
|
+
}
|
|
105
|
+
export function confirmInputCleanup() {
|
|
106
|
+
inputPending = false;
|
|
107
|
+
}
|
|
108
|
+
const operationContext = new AsyncLocalStorage();
|
|
109
|
+
export function runNativeOperation(operationId, action) {
|
|
110
|
+
return operationContext.run(operationId, action);
|
|
111
|
+
}
|
|
112
|
+
class NativeDesktopClient extends WorkerClient {
|
|
113
|
+
execute(action, args, signal, operationId = operationContext.getStore()) {
|
|
114
|
+
return super.execute(action, args, signal, operationId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const nativeDesktop = new NativeDesktopClient({
|
|
118
|
+
launch: async (signal) => {
|
|
119
|
+
const names = [
|
|
120
|
+
"windows.cs",
|
|
121
|
+
"window-marker.cs",
|
|
122
|
+
"window-references.cs",
|
|
123
|
+
"accessibility.cs",
|
|
124
|
+
"capture.cs",
|
|
125
|
+
"controls.cs",
|
|
126
|
+
"window-management.cs",
|
|
127
|
+
"pointer.cs",
|
|
128
|
+
"browser-indicators.cs",
|
|
129
|
+
"text.cs",
|
|
130
|
+
"input-timing.cs",
|
|
131
|
+
];
|
|
132
|
+
const framework = join(process.env.WINDIR ?? "", "Microsoft.NET", "Framework64", "v4.0.30319");
|
|
133
|
+
return compileNative(names, [
|
|
134
|
+
"System.Drawing.dll",
|
|
135
|
+
"Microsoft.CSharp.dll",
|
|
136
|
+
"System.Windows.Forms.dll",
|
|
137
|
+
...[
|
|
138
|
+
"WindowsBase.dll",
|
|
139
|
+
"UIAutomationClient.dll",
|
|
140
|
+
"UIAutomationTypes.dll",
|
|
141
|
+
].map((name) => join(framework, "WPF", name)),
|
|
142
|
+
], signal);
|
|
143
|
+
},
|
|
144
|
+
timeoutMs: 40_000,
|
|
145
|
+
canTerminate: () => !inputPending,
|
|
146
|
+
});
|
package/dist/ocr.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
export declare const ocrParameters: {
|
|
3
|
+
type: string;
|
|
4
|
+
additionalProperties: boolean;
|
|
5
|
+
properties: {
|
|
6
|
+
windowRef: {
|
|
7
|
+
type: string;
|
|
8
|
+
minLength: number;
|
|
9
|
+
};
|
|
10
|
+
windowTitle: {
|
|
11
|
+
type: string;
|
|
12
|
+
minLength: number;
|
|
13
|
+
};
|
|
14
|
+
processId: {
|
|
15
|
+
type: string;
|
|
16
|
+
minimum: number;
|
|
17
|
+
};
|
|
18
|
+
nativeWindowHandle: {
|
|
19
|
+
type: string;
|
|
20
|
+
minimum: number;
|
|
21
|
+
};
|
|
22
|
+
activate: {
|
|
23
|
+
type: string;
|
|
24
|
+
default: boolean;
|
|
25
|
+
};
|
|
26
|
+
scope: {
|
|
27
|
+
type: string;
|
|
28
|
+
enum: string[];
|
|
29
|
+
default: string;
|
|
30
|
+
};
|
|
31
|
+
maxWidth: {
|
|
32
|
+
type: string;
|
|
33
|
+
minimum: number;
|
|
34
|
+
maximum: number;
|
|
35
|
+
default: number;
|
|
36
|
+
};
|
|
37
|
+
query: {
|
|
38
|
+
type: string;
|
|
39
|
+
minLength: number;
|
|
40
|
+
};
|
|
41
|
+
resolveControl: {
|
|
42
|
+
type: string;
|
|
43
|
+
default: boolean;
|
|
44
|
+
};
|
|
45
|
+
includeScreenshot: {
|
|
46
|
+
type: string;
|
|
47
|
+
default: boolean;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function ocrDesktop(args: Record<string, unknown>, signal?: AbortSignal): Promise<WorkerResult>;
|
|
52
|
+
export declare function disposeOcr(): Promise<void>;
|
package/dist/ocr.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { screenshot } from "./capture.js";
|
|
3
|
+
import { WorkerClient } from "./client.js";
|
|
4
|
+
import { compileNative, nativeDesktop } from "./native.js";
|
|
5
|
+
export const ocrParameters = {
|
|
6
|
+
type: "object",
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
properties: {
|
|
9
|
+
windowRef: { type: "string", minLength: 1 },
|
|
10
|
+
windowTitle: { type: "string", minLength: 1 },
|
|
11
|
+
processId: { type: "integer", minimum: 1 },
|
|
12
|
+
nativeWindowHandle: { type: "integer", minimum: 1 },
|
|
13
|
+
activate: { type: "boolean", default: false },
|
|
14
|
+
scope: {
|
|
15
|
+
type: "string",
|
|
16
|
+
enum: ["active_window", "desktop"],
|
|
17
|
+
default: "active_window",
|
|
18
|
+
},
|
|
19
|
+
maxWidth: { type: "integer", minimum: 0, maximum: 7680, default: 1920 },
|
|
20
|
+
query: { type: "string", minLength: 1 },
|
|
21
|
+
resolveControl: { type: "boolean", default: false },
|
|
22
|
+
includeScreenshot: { type: "boolean", default: false },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const createEngine = () => new WorkerClient({
|
|
26
|
+
launch: async (signal) => {
|
|
27
|
+
const windows = process.env.WINDIR;
|
|
28
|
+
if (!windows)
|
|
29
|
+
throw new Error("OCR_UNAVAILABLE: Windows metadata is unavailable");
|
|
30
|
+
const framework = join(windows, "Microsoft.NET", "Framework64", "v4.0.30319");
|
|
31
|
+
const metadata = join(windows, "System32", "WinMetadata");
|
|
32
|
+
return compileNative(["ocr.cs"], [
|
|
33
|
+
"System.Drawing.dll",
|
|
34
|
+
"System.Core.dll",
|
|
35
|
+
...["System.Runtime.dll", "System.Runtime.WindowsRuntime.dll"].map((name) => join(framework, name)),
|
|
36
|
+
...[
|
|
37
|
+
"Windows.Foundation.winmd",
|
|
38
|
+
"Windows.Media.winmd",
|
|
39
|
+
"Windows.Graphics.winmd",
|
|
40
|
+
"Windows.Globalization.winmd",
|
|
41
|
+
].map((name) => join(metadata, name)),
|
|
42
|
+
], signal);
|
|
43
|
+
},
|
|
44
|
+
timeoutMs: 20_000,
|
|
45
|
+
});
|
|
46
|
+
let engine = createEngine();
|
|
47
|
+
function union(words) {
|
|
48
|
+
if (words.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const x = Math.min(...words.map((word) => word.x)), y = Math.min(...words.map((word) => word.y));
|
|
51
|
+
return {
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
width: Math.max(...words.map((word) => word.x + word.width)) - x,
|
|
55
|
+
height: Math.max(...words.map((word) => word.y + word.height)) - y,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const normalize = (text) => text.replace(/\s/gu, "").toLowerCase();
|
|
59
|
+
export async function ocrDesktop(args, signal) {
|
|
60
|
+
const fail = (error) => ({
|
|
61
|
+
ok: false,
|
|
62
|
+
action: "ocr",
|
|
63
|
+
error,
|
|
64
|
+
conclusive: false,
|
|
65
|
+
});
|
|
66
|
+
for (const [key, value] of Object.entries(args)) {
|
|
67
|
+
const rule = ocrParameters.properties[key];
|
|
68
|
+
if (!rule)
|
|
69
|
+
return fail(`INVALID_ARGUMENTS: unknown option ${key}`);
|
|
70
|
+
if (rule.type === "integer"
|
|
71
|
+
? !Number.isSafeInteger(value)
|
|
72
|
+
: typeof value !== rule.type)
|
|
73
|
+
return fail(`INVALID_ARGUMENTS: invalid ${key}`);
|
|
74
|
+
}
|
|
75
|
+
if ((args.windowRef !== undefined && !String(args.windowRef).trim()) ||
|
|
76
|
+
(args.query !== undefined && !normalize(String(args.query))) ||
|
|
77
|
+
(args.windowTitle !== undefined && !String(args.windowTitle).trim()) ||
|
|
78
|
+
[args.processId, args.nativeWindowHandle].some((value) => value !== undefined && Number(value) < 1) ||
|
|
79
|
+
(args.scope !== undefined &&
|
|
80
|
+
args.scope !== "active_window" &&
|
|
81
|
+
args.scope !== "desktop") ||
|
|
82
|
+
(args.maxWidth !== undefined &&
|
|
83
|
+
(Number(args.maxWidth) < 0 || Number(args.maxWidth) > 7680)))
|
|
84
|
+
return fail("INVALID_ARGUMENTS: invalid OCR target, query, scope or maxWidth");
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
const captureArgs = {
|
|
87
|
+
maxWidth: args.maxWidth ?? 1920,
|
|
88
|
+
};
|
|
89
|
+
for (const key of [
|
|
90
|
+
"scope",
|
|
91
|
+
"windowRef",
|
|
92
|
+
"windowTitle",
|
|
93
|
+
"processId",
|
|
94
|
+
"nativeWindowHandle",
|
|
95
|
+
"activate",
|
|
96
|
+
])
|
|
97
|
+
if (args[key] !== undefined)
|
|
98
|
+
captureArgs[key] = args[key];
|
|
99
|
+
const captured = await screenshot(captureArgs, signal);
|
|
100
|
+
signal?.throwIfAborted();
|
|
101
|
+
if (!captured.ok)
|
|
102
|
+
return fail(`OCR_CAPTURE_FAILED: ${captured.error}`);
|
|
103
|
+
const image = captured.screenshot;
|
|
104
|
+
if (!image || image.windowCaptureFailed)
|
|
105
|
+
return fail("OCR_CAPTURE_FAILED: target has no capturable surface; desktop fallback is not target evidence");
|
|
106
|
+
try {
|
|
107
|
+
const recognized = await engine.execute("recognize", { png: image.base64 }, signal);
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
if (!recognized.ok)
|
|
110
|
+
return { ...recognized, action: "ocr", conclusive: false };
|
|
111
|
+
const origin = image.origin;
|
|
112
|
+
const scale = Number(image.imageScale), scaleY = Number(image.imageScaleY ?? scale);
|
|
113
|
+
if (!(scale > 0 && scaleY > 0))
|
|
114
|
+
return fail("OCR_CAPTURE_FAILED: invalid image mapping");
|
|
115
|
+
const lines = recognized.lines.map((line) => {
|
|
116
|
+
const words = line.words.map((word) => ({
|
|
117
|
+
text: word.text,
|
|
118
|
+
x: origin.x + word.x / scale,
|
|
119
|
+
y: origin.y + word.y / scaleY,
|
|
120
|
+
width: word.width / scale,
|
|
121
|
+
height: word.height / scaleY,
|
|
122
|
+
}));
|
|
123
|
+
return { text: line.text, words, boundingBox: union(words) };
|
|
124
|
+
});
|
|
125
|
+
const result = {
|
|
126
|
+
ok: true,
|
|
127
|
+
action: "ocr",
|
|
128
|
+
text: recognized.text,
|
|
129
|
+
lines,
|
|
130
|
+
language: recognized.language,
|
|
131
|
+
availableLanguages: recognized.availableLanguages,
|
|
132
|
+
source: args.scope === "desktop" ? "desktop" : "window",
|
|
133
|
+
...(captured.windowRef === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: { windowRef: captured.windowRef }),
|
|
136
|
+
...(captured.windowRefUnavailable === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { windowRefUnavailable: captured.windowRefUnavailable }),
|
|
139
|
+
coordinateSpace: "screen",
|
|
140
|
+
imageBounds: image.bounds,
|
|
141
|
+
imageScale: scale,
|
|
142
|
+
imageScaleY: scaleY,
|
|
143
|
+
captureMethod: image.method,
|
|
144
|
+
occludedPossible: image.occludedPossible,
|
|
145
|
+
captureWarnings: image.captureWarnings,
|
|
146
|
+
conclusive: image.occludedPossible !== true,
|
|
147
|
+
captureConclusive: image.occludedPossible !== true,
|
|
148
|
+
};
|
|
149
|
+
if (args.includeScreenshot)
|
|
150
|
+
result.screenshot = image;
|
|
151
|
+
if (args.query !== undefined) {
|
|
152
|
+
const matched = [];
|
|
153
|
+
const needle = normalize(String(args.query));
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const normalizedWords = line.words.map((word) => normalize(word.text));
|
|
156
|
+
const offset = normalizedWords.join("").indexOf(needle);
|
|
157
|
+
if (offset < 0)
|
|
158
|
+
continue;
|
|
159
|
+
let position = 0;
|
|
160
|
+
const words = line.words.filter((_, index) => {
|
|
161
|
+
const start = position;
|
|
162
|
+
position += normalizedWords[index]?.length ?? 0;
|
|
163
|
+
return position > offset && start < offset + needle.length;
|
|
164
|
+
});
|
|
165
|
+
const bounds = union(words);
|
|
166
|
+
if (!bounds)
|
|
167
|
+
continue;
|
|
168
|
+
const x = bounds.x + bounds.width / 2, y = bounds.y + bounds.height / 2;
|
|
169
|
+
let control = null;
|
|
170
|
+
let controlError;
|
|
171
|
+
if (args.resolveControl && !image.occludedPossible) {
|
|
172
|
+
const target = {
|
|
173
|
+
x: Math.round(x),
|
|
174
|
+
y: Math.round(y),
|
|
175
|
+
detailLevel: "full",
|
|
176
|
+
};
|
|
177
|
+
for (const key of [
|
|
178
|
+
"windowRef",
|
|
179
|
+
"windowTitle",
|
|
180
|
+
"processId",
|
|
181
|
+
"nativeWindowHandle",
|
|
182
|
+
])
|
|
183
|
+
if ((captured[key] ?? args[key]) !== undefined)
|
|
184
|
+
target[key] = captured[key] ?? args[key];
|
|
185
|
+
try {
|
|
186
|
+
const observed = await nativeDesktop.execute("element_info", target, signal);
|
|
187
|
+
if (observed.ok &&
|
|
188
|
+
observed.element &&
|
|
189
|
+
typeof observed.element === "object") {
|
|
190
|
+
const element = observed.element;
|
|
191
|
+
control = Object.fromEntries([
|
|
192
|
+
"controlType",
|
|
193
|
+
"name",
|
|
194
|
+
"automationId",
|
|
195
|
+
"className",
|
|
196
|
+
"boundingBox",
|
|
197
|
+
]
|
|
198
|
+
.filter((key) => element[key] !== undefined)
|
|
199
|
+
.map((key) => [key, element[key]]));
|
|
200
|
+
}
|
|
201
|
+
else
|
|
202
|
+
controlError = String(observed.error);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (signal?.aborted)
|
|
206
|
+
throw error;
|
|
207
|
+
controlError = String(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
matched.push({
|
|
211
|
+
source: "ocr",
|
|
212
|
+
line: line.text,
|
|
213
|
+
word: { text: words.map((word) => word.text).join(" "), x, y },
|
|
214
|
+
boundingBox: bounds,
|
|
215
|
+
control,
|
|
216
|
+
...(controlError ? { controlError } : {}),
|
|
217
|
+
});
|
|
218
|
+
if (matched.length === 3)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
result.query = args.query;
|
|
222
|
+
result.matched = matched;
|
|
223
|
+
if (!matched.length)
|
|
224
|
+
result.conclusive = false;
|
|
225
|
+
if (!matched.length)
|
|
226
|
+
result.note =
|
|
227
|
+
"No literal OCR word sequence matched (case-insensitive, ignoring whitespace); recognition is not proof that the text is absent.";
|
|
228
|
+
}
|
|
229
|
+
if (image.occludedPossible)
|
|
230
|
+
result.note =
|
|
231
|
+
"Capture may contain occluding content; OCR cannot establish target text absence or control identity.";
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (signal?.aborted) {
|
|
236
|
+
const retired = engine;
|
|
237
|
+
engine = createEngine();
|
|
238
|
+
await retired.dispose();
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
return fail(`OCR_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export async function disposeOcr() {
|
|
245
|
+
await engine.dispose();
|
|
246
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
export declare function acquireDesktop(operationId: string, signal?: AbortSignal): Promise<WorkerResult>;
|
|
3
|
+
export declare function releaseDesktop(operationId: string): Promise<WorkerResult>;
|
|
4
|
+
export declare function disposeOwnership(): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WorkerClient } from "./client.js";
|
|
2
|
+
import { compileNative } from "./native.js";
|
|
3
|
+
const control = new WorkerClient({
|
|
4
|
+
launch: (signal) => compileNative(["ownership.cs"], [], signal),
|
|
5
|
+
});
|
|
6
|
+
let held;
|
|
7
|
+
export async function acquireDesktop(operationId, signal) {
|
|
8
|
+
const result = await control.execute("acquire", { operationId }, signal);
|
|
9
|
+
if (result.ok)
|
|
10
|
+
held = operationId;
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export async function releaseDesktop(operationId) {
|
|
14
|
+
if (held !== operationId)
|
|
15
|
+
return { ok: true, action: "operation_end", released: false };
|
|
16
|
+
const result = await control.execute("release", { operationId });
|
|
17
|
+
if (result.ok)
|
|
18
|
+
held = undefined;
|
|
19
|
+
return { ...result, action: "operation_end" };
|
|
20
|
+
}
|
|
21
|
+
export async function disposeOwnership() {
|
|
22
|
+
await control.dispose();
|
|
23
|
+
held = undefined;
|
|
24
|
+
}
|