@aryacode/dsh-engineering 0.4.4 → 0.4.6
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 +45 -0
- package/README.md +11 -8
- package/cordis.patch.yml +3 -2
- package/package.json +3 -2
- package/vendor/l5x/cip-worker.mjs +181 -0
- package/vendor/l5x/lib/core/params.d.ts +3 -1
- package/vendor/l5x/lib/core/params.js +4 -0
- package/vendor/l5x/lib/engine/cip-request.d.ts +85 -0
- package/vendor/l5x/lib/engine/cip-request.js +134 -0
- package/vendor/l5x/lib/engine/cip-transaction.d.ts +115 -0
- package/vendor/l5x/lib/engine/cip-transaction.js +185 -0
- package/vendor/l5x/lib/live.d.ts +5 -0
- package/vendor/l5x/lib/live.js +13 -2
- package/vendor/l5x/lib/tools/live.d.ts +17 -6
- package/vendor/l5x/lib/tools/live.js +177 -15
- package/vendor/optix/bridge/LICENSE +35 -0
- package/vendor/optix/bridge/StudioMCPBridge.cs +7022 -0
- package/vendor/optix/bridge/source.json +10 -0
- package/vendor/optix/icon.svg +13 -0
- package/vendor/optix/lib/index.js +1531 -0
- package/vendor/optix/lib/types/bridge.d.ts +77 -0
- package/vendor/optix/lib/types/index.d.ts +52 -0
- package/vendor/optix/lib/types/ocr.d.ts +46 -0
- package/vendor/optix/lib/types/operations.d.ts +42 -0
- package/vendor/optix/lib/types/preview.d.ts +33 -0
- package/vendor/optix/lib/types/templates.d.ts +18 -0
- package/vendor/optix/locale/en.json +6 -0
- package/vendor/optix/locale/zh.json +6 -0
- package/vendor/optix/templates/arya-optix-ipc/manifest.json +260 -0
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -0
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -0
- package/vendor/optix/templates/arya-optix-ipc/project/IDEVersion.txt +1 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +28 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -0
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +306 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +19 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +10 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4555 -0
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -0
- package/vendor/optix/templates/arya-optix-panel/manifest.json +411 -0
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -0
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -0
- package/vendor/optix/templates/arya-optix-panel/project/IDEVersion.txt +1 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +28 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -0
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +308 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +19 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +10 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4555 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -0
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -0
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
import { assertNever, isJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
//#region lib/types/bridge.js
|
|
11
|
+
/** Bounded direct HTTP transport to the in-Studio Optix bridge. @module dsh-tool-ft-optix/bridge */
|
|
12
|
+
/** A failure after dispatch may leave a mutation's outcome uncertain. */
|
|
13
|
+
var BridgeError = class extends Error {
|
|
14
|
+
/** Whether an authoring request may have reached Studio. */
|
|
15
|
+
mayHaveApplied;
|
|
16
|
+
/**
|
|
17
|
+
* Describe a transport or bridge refusal.
|
|
18
|
+
* @param message - Credential-free diagnostic.
|
|
19
|
+
* @param mayHaveApplied - Whether inspecting the live model is required before retrying.
|
|
20
|
+
*/
|
|
21
|
+
constructor(message, mayHaveApplied) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "BridgeError";
|
|
24
|
+
this.mayHaveApplied = mayHaveApplied;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validate a local endpoint and normalize its trailing slash.
|
|
29
|
+
* @param input - Configured URL.
|
|
30
|
+
* @returns The normalized loopback origin.
|
|
31
|
+
*/
|
|
32
|
+
function bridgeOrigin(input) {
|
|
33
|
+
const url = new URL(input);
|
|
34
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error("bridgeUrls must contain plain HTTP loopback origins without credentials, paths, queries, or fragments");
|
|
35
|
+
return url.origin;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check for a JSON object in an external response.
|
|
39
|
+
* @param value - Parsed bridge JSON.
|
|
40
|
+
* @returns Whether the value is a non-null object rather than an array.
|
|
41
|
+
*/
|
|
42
|
+
function jsonObject(value) {
|
|
43
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
/** Direct bridge transport; writes are never automatically retried or redirected. */
|
|
46
|
+
var OptixBridgeGateway = class {
|
|
47
|
+
config;
|
|
48
|
+
disposed;
|
|
49
|
+
pending = /* @__PURE__ */ new Set();
|
|
50
|
+
/**
|
|
51
|
+
* Bind validated limits and the plugin-disposal signal.
|
|
52
|
+
* @param config - Resolved transport configuration.
|
|
53
|
+
* @param disposed - Aborts all transport work when the plugin unloads.
|
|
54
|
+
*/
|
|
55
|
+
constructor(config, disposed) {
|
|
56
|
+
this.config = config;
|
|
57
|
+
this.disposed = disposed;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Acquire one bounded JSON response without exposing query values in errors.
|
|
61
|
+
* @param origin - Validated configured origin.
|
|
62
|
+
* @param route - Plugin-owned bridge route.
|
|
63
|
+
* @param signal - Caller cancellation.
|
|
64
|
+
* @param query - Operation parameters.
|
|
65
|
+
* @param method - HTTP verb.
|
|
66
|
+
* @param body - Validation batch, when present.
|
|
67
|
+
* @returns Validated lossless JSON; HTTP and bridge errors throw.
|
|
68
|
+
*/
|
|
69
|
+
request(origin, route, signal, query = {}, method = "GET", body) {
|
|
70
|
+
const request = this.acquire(origin, route, signal, query, method, body);
|
|
71
|
+
this.pending.add(request);
|
|
72
|
+
request.then(() => this.pending.delete(request), () => this.pending.delete(request));
|
|
73
|
+
return request;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wait for aborted requests to release response readers.
|
|
77
|
+
* @returns Completion after every current transport request settles.
|
|
78
|
+
*/
|
|
79
|
+
async drain() {
|
|
80
|
+
await Promise.allSettled(this.pending);
|
|
81
|
+
}
|
|
82
|
+
/** Acquire and release one response reader within the configured byte and time limits. */
|
|
83
|
+
async acquire(origin, route, signal, query, method, body) {
|
|
84
|
+
const deadline = AbortSignal.timeout(this.config.requestTimeoutMs);
|
|
85
|
+
const combined = AbortSignal.any([
|
|
86
|
+
signal,
|
|
87
|
+
this.disposed,
|
|
88
|
+
deadline
|
|
89
|
+
]);
|
|
90
|
+
combined.throwIfAborted();
|
|
91
|
+
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
92
|
+
if (payload && Buffer.byteLength(payload) > this.config.maxResponseBytes) throw new BridgeError("Bridge request exceeds maxResponseBytes", false);
|
|
93
|
+
const url = new URL(`/bridge/${route}`, origin);
|
|
94
|
+
url.search = new URLSearchParams(query).toString();
|
|
95
|
+
if (Buffer.byteLength(url.href) > this.config.maxResponseBytes) throw new BridgeError("Bridge query exceeds maxResponseBytes", false);
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
method,
|
|
99
|
+
redirect: "error",
|
|
100
|
+
signal: combined,
|
|
101
|
+
headers: {
|
|
102
|
+
...this.config.bridgeToken ? { Authorization: `Bearer ${this.config.bridgeToken}` } : {},
|
|
103
|
+
...payload ? { "Content-Type": "application/json" } : {}
|
|
104
|
+
},
|
|
105
|
+
...method === "POST" ? { body: payload ?? "" } : {}
|
|
106
|
+
});
|
|
107
|
+
const reader = response.body?.getReader();
|
|
108
|
+
if (!reader) throw new Error("Empty bridge response");
|
|
109
|
+
const chunks = [];
|
|
110
|
+
let size = 0;
|
|
111
|
+
try {
|
|
112
|
+
while (true) {
|
|
113
|
+
const chunk = await reader.read();
|
|
114
|
+
if (chunk.done) break;
|
|
115
|
+
size += chunk.value.byteLength;
|
|
116
|
+
if (size > this.config.maxResponseBytes) throw new Error("Bridge response exceeds maxResponseBytes");
|
|
117
|
+
chunks.push(chunk.value);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
await reader.cancel();
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
124
|
+
if (!isJsonValue(value)) throw new Error("Bridge returned non-lossless JSON");
|
|
125
|
+
const json = value;
|
|
126
|
+
if (!jsonObject(json)) throw new Error("Bridge returned a non-object response");
|
|
127
|
+
if (!response.ok || json.error !== void 0 && route !== "expr/validate") throw new BridgeError(`Bridge ${route} refused the request (HTTP ${response.status}): ${JSON.stringify(json.error ?? json)}`, method === "POST" && route !== "validate_ops" && route !== "expr/validate");
|
|
128
|
+
return json;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof BridgeError) throw error;
|
|
131
|
+
throw new BridgeError(`Bridge ${route} ${combined.aborted ? "cancelled or timed out" : "unavailable or returned invalid JSON"}; inspect Studio before retrying authoring`, method === "POST" && route !== "validate_ops" && route !== "expr/validate");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Select exactly one loaded Studio project by its bridge-reported BrowseName.
|
|
136
|
+
* @param project - Exact name returned by health discovery.
|
|
137
|
+
* @param signal - Caller cancellation.
|
|
138
|
+
* @returns The serving origin and its health facts.
|
|
139
|
+
*/
|
|
140
|
+
async select(project, signal) {
|
|
141
|
+
if (!project.trim()) throw new Error("project must be non-empty");
|
|
142
|
+
const matches = [];
|
|
143
|
+
for (const origin of this.config.bridgeUrls) {
|
|
144
|
+
signal.throwIfAborted();
|
|
145
|
+
let health;
|
|
146
|
+
try {
|
|
147
|
+
health = await this.request(origin, "health", signal);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (signal.aborted || this.disposed.aborted) throw error;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (health.project === project && health.model_loaded === true) matches.push({
|
|
153
|
+
origin,
|
|
154
|
+
health
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (matches.length > 1) throw new Error("Ambiguous Studio project: configure a single serving bridge URL");
|
|
158
|
+
const selected = matches[0];
|
|
159
|
+
if (!selected) throw new Error(`No loaded bridge for project ${project}; open it in Studio and run StartBridge`);
|
|
160
|
+
return selected;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region lib/types/operations.js
|
|
165
|
+
/** Closed authoring requests and their Optix bridge routes. @module dsh-tool-ft-optix/operations */
|
|
166
|
+
const text$1 = {
|
|
167
|
+
type: "string",
|
|
168
|
+
required: true
|
|
169
|
+
};
|
|
170
|
+
const optionalText = { type: "string" };
|
|
171
|
+
const mode = {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: [
|
|
174
|
+
"Read",
|
|
175
|
+
"Write",
|
|
176
|
+
"ReadWrite"
|
|
177
|
+
]
|
|
178
|
+
};
|
|
179
|
+
/** Bridge-supported batch operations; unknown fields are rejected before validation or writes. */
|
|
180
|
+
const operations = {
|
|
181
|
+
set_property: {
|
|
182
|
+
route: "node/property",
|
|
183
|
+
fields: {
|
|
184
|
+
path: text$1,
|
|
185
|
+
name: text$1,
|
|
186
|
+
value: text$1,
|
|
187
|
+
locale: optionalText
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
bind: {
|
|
191
|
+
route: "node/bind",
|
|
192
|
+
fields: {
|
|
193
|
+
path: text$1,
|
|
194
|
+
name: text$1,
|
|
195
|
+
source_path: optionalText,
|
|
196
|
+
raw_path: optionalText,
|
|
197
|
+
mode
|
|
198
|
+
},
|
|
199
|
+
aliases: {
|
|
200
|
+
source_path: "source",
|
|
201
|
+
raw_path: "raw"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
create_widget: {
|
|
205
|
+
route: "ui/widget",
|
|
206
|
+
fields: {
|
|
207
|
+
screen: text$1,
|
|
208
|
+
name: text$1,
|
|
209
|
+
widget_type: text$1
|
|
210
|
+
},
|
|
211
|
+
aliases: { widget_type: "type" }
|
|
212
|
+
},
|
|
213
|
+
create_variable: {
|
|
214
|
+
route: "model/variable",
|
|
215
|
+
fields: {
|
|
216
|
+
parent: text$1,
|
|
217
|
+
name: text$1,
|
|
218
|
+
datatype: text$1
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
create_folder: {
|
|
222
|
+
route: "model/folder",
|
|
223
|
+
fields: {
|
|
224
|
+
parent: text$1,
|
|
225
|
+
name: text$1
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
create_object: {
|
|
229
|
+
route: "model/object",
|
|
230
|
+
fields: {
|
|
231
|
+
parent: text$1,
|
|
232
|
+
name: text$1,
|
|
233
|
+
object_type: optionalText
|
|
234
|
+
},
|
|
235
|
+
aliases: { object_type: "type" }
|
|
236
|
+
},
|
|
237
|
+
create_type: {
|
|
238
|
+
route: "model/type",
|
|
239
|
+
fields: {
|
|
240
|
+
parent: text$1,
|
|
241
|
+
name: text$1,
|
|
242
|
+
base_type: optionalText
|
|
243
|
+
},
|
|
244
|
+
aliases: { base_type: "base" }
|
|
245
|
+
},
|
|
246
|
+
create_alias: {
|
|
247
|
+
route: "node/alias",
|
|
248
|
+
fields: {
|
|
249
|
+
parent_path: text$1,
|
|
250
|
+
name: text$1,
|
|
251
|
+
target_path: optionalText,
|
|
252
|
+
kind: optionalText
|
|
253
|
+
},
|
|
254
|
+
aliases: {
|
|
255
|
+
parent_path: "parent",
|
|
256
|
+
target_path: "target"
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
delete: {
|
|
260
|
+
route: "node/delete",
|
|
261
|
+
fields: { path: text$1 },
|
|
262
|
+
destructive: true
|
|
263
|
+
},
|
|
264
|
+
move: {
|
|
265
|
+
route: "node/move",
|
|
266
|
+
fields: {
|
|
267
|
+
path: text$1,
|
|
268
|
+
new_parent: text$1,
|
|
269
|
+
new_name: optionalText
|
|
270
|
+
},
|
|
271
|
+
destructive: true
|
|
272
|
+
},
|
|
273
|
+
reorder: {
|
|
274
|
+
route: "node/reorder",
|
|
275
|
+
fields: {
|
|
276
|
+
path: text$1,
|
|
277
|
+
position: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: ["front", "back"]
|
|
280
|
+
},
|
|
281
|
+
index: { type: "integer" }
|
|
282
|
+
},
|
|
283
|
+
destructive: true
|
|
284
|
+
},
|
|
285
|
+
wire_event: {
|
|
286
|
+
route: "node/event",
|
|
287
|
+
fields: {
|
|
288
|
+
path: text$1,
|
|
289
|
+
event_type: text$1,
|
|
290
|
+
method_path: optionalText,
|
|
291
|
+
command: optionalText,
|
|
292
|
+
variable: optionalText,
|
|
293
|
+
value: optionalText,
|
|
294
|
+
args: optionalText,
|
|
295
|
+
object_raw: optionalText,
|
|
296
|
+
replace: { type: "boolean" }
|
|
297
|
+
},
|
|
298
|
+
aliases: {
|
|
299
|
+
event_type: "event",
|
|
300
|
+
method_path: "method"
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
attach_expression: {
|
|
304
|
+
route: "node/attach-expression",
|
|
305
|
+
fields: {
|
|
306
|
+
path: text$1,
|
|
307
|
+
prop_name: text$1,
|
|
308
|
+
expression: text$1,
|
|
309
|
+
sources: optionalText
|
|
310
|
+
},
|
|
311
|
+
aliases: { prop_name: "name" }
|
|
312
|
+
},
|
|
313
|
+
attach_formatter: {
|
|
314
|
+
route: "node/attach-formatter",
|
|
315
|
+
fields: {
|
|
316
|
+
path: text$1,
|
|
317
|
+
prop_name: text$1,
|
|
318
|
+
format: text$1,
|
|
319
|
+
sources: optionalText,
|
|
320
|
+
mode
|
|
321
|
+
},
|
|
322
|
+
aliases: { prop_name: "name" }
|
|
323
|
+
},
|
|
324
|
+
attach_string_formatter: {
|
|
325
|
+
route: "node/attach-string-formatter",
|
|
326
|
+
fields: {
|
|
327
|
+
path: text$1,
|
|
328
|
+
prop_name: text$1,
|
|
329
|
+
format: text$1,
|
|
330
|
+
sources: optionalText,
|
|
331
|
+
raw: { type: "boolean" },
|
|
332
|
+
mode
|
|
333
|
+
},
|
|
334
|
+
aliases: { prop_name: "name" }
|
|
335
|
+
},
|
|
336
|
+
retype: {
|
|
337
|
+
route: "node/retype",
|
|
338
|
+
fields: {
|
|
339
|
+
path: text$1,
|
|
340
|
+
datatype: text$1,
|
|
341
|
+
dims: optionalText
|
|
342
|
+
},
|
|
343
|
+
destructive: true
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
/** Exact-one operation schema for the native tool registry. */
|
|
347
|
+
const operationSchema = { oneOf: Object.entries(operations).map(([op, spec]) => ({
|
|
348
|
+
type: "object",
|
|
349
|
+
additionalProperties: false,
|
|
350
|
+
properties: {
|
|
351
|
+
op: {
|
|
352
|
+
type: "string",
|
|
353
|
+
const: op,
|
|
354
|
+
required: true
|
|
355
|
+
},
|
|
356
|
+
...spec.fields
|
|
357
|
+
}
|
|
358
|
+
})) };
|
|
359
|
+
/** Resolve an operation name admitted by the public request type. */
|
|
360
|
+
function specification(op) {
|
|
361
|
+
const spec = operations[op];
|
|
362
|
+
if (!spec) throw new Error(`Unsupported Optix operation: ${op}`);
|
|
363
|
+
return spec;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Check cross-field rules not expressible by the schema DSL.
|
|
367
|
+
* @param ops - Schema-validated batch operations.
|
|
368
|
+
*/
|
|
369
|
+
function checkOperations(ops) {
|
|
370
|
+
for (const op of ops) {
|
|
371
|
+
for (const [key, value] of Object.entries(op)) if (typeof value === "string" && value.length === 0 && key !== "value") throw new Error(`${op.op}: ${key} must be non-empty`);
|
|
372
|
+
if (op.op === "bind" && Boolean(op.source_path) === Boolean(op.raw_path)) throw new Error("bind requires exactly one of source_path or raw_path");
|
|
373
|
+
if (op.op === "reorder" && (op.position !== void 0 === (op.index !== void 0) || typeof op.index === "number" && op.index < 0)) throw new Error("reorder requires position or a non-negative index");
|
|
374
|
+
if (op.op === "wire_event") {
|
|
375
|
+
if (Boolean(op.command) === Boolean(op.method_path)) throw new Error("wire_event requires exactly one of command or method_path");
|
|
376
|
+
if (op.object_raw && !op.method_path) throw new Error("object_raw requires method_path");
|
|
377
|
+
if (op.args && op.command && op.command !== "OpenDialog" && op.command !== "OpenKeyboard") throw new Error("command arguments require OpenDialog or OpenKeyboard");
|
|
378
|
+
}
|
|
379
|
+
if ((op.prop_name ?? op.name) === "DisplayName" && [
|
|
380
|
+
"bind",
|
|
381
|
+
"attach_expression",
|
|
382
|
+
"attach_formatter",
|
|
383
|
+
"attach_string_formatter"
|
|
384
|
+
].includes(op.op)) throw new Error("DisplayName is an attribute and cannot carry a dynamic link or converter");
|
|
385
|
+
if (op.op === "attach_string_formatter") {
|
|
386
|
+
const count = typeof op.sources === "string" ? op.sources.split(",").filter((source) => source.trim()).length : 0;
|
|
387
|
+
if ([...String(op.format).replaceAll("{{", "").replaceAll("}}", "").matchAll(/\{(\d+)\s*(?:,[^{}:]*)?(?::[^{}]*)?\}/g)].some((match) => Number(match[1]) >= count)) throw new Error("unbound_placeholder: every formatter placeholder needs a source");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Resolve a batch operation into a bridge route and query parameters.
|
|
393
|
+
* @param op - Validated operation.
|
|
394
|
+
* @returns The closed route and encoded field values.
|
|
395
|
+
*/
|
|
396
|
+
function operationRequest(op) {
|
|
397
|
+
const spec = specification(op.op);
|
|
398
|
+
const query = {};
|
|
399
|
+
for (const [key, value] of Object.entries(op)) if (key !== "op") query[spec.aliases?.[key] ?? key] = key === "raw" ? value ? "1" : "0" : String(value);
|
|
400
|
+
return {
|
|
401
|
+
route: op.op === "set_property" && op.name === "DisplayName" ? "node/displayname" : spec.route,
|
|
402
|
+
query
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Determine whether a batch needs destructive-operation enablement.
|
|
407
|
+
* @param ops - Validated batch.
|
|
408
|
+
* @returns Whether any operation deletes, replaces, moves, reorders, or retypes existing model state.
|
|
409
|
+
*/
|
|
410
|
+
function isDestructive(ops) {
|
|
411
|
+
return ops.some((op) => specification(op.op).destructive || op.op === "wire_event" && op.replace === true);
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region lib/types/ocr.js
|
|
415
|
+
/** Local Tesseract canvas recognition without an MCP server. @module dsh-tool-ft-optix/ocr */
|
|
416
|
+
/**
|
|
417
|
+
* Parse Tesseract TSV output, rejecting malformed rows rather than trusting their coordinates.
|
|
418
|
+
* @param tsv - Bounded external OCR output.
|
|
419
|
+
* @returns Recognized words with normalized confidence and screenshot coordinates.
|
|
420
|
+
*/
|
|
421
|
+
function parseOcr(tsv) {
|
|
422
|
+
const rows = tsv.split(/\r?\n/).filter((row) => row.length > 0);
|
|
423
|
+
if (rows.shift() !== "level page_num block_num par_num line_num word_num left top width height conf text") throw new Error("Tesseract returned an invalid TSV header");
|
|
424
|
+
const words = [];
|
|
425
|
+
for (const row of rows) {
|
|
426
|
+
const fields = row.split(" ");
|
|
427
|
+
if (fields.length < 12) throw new Error("Tesseract returned an incomplete TSV row");
|
|
428
|
+
if (fields[0] !== "5") continue;
|
|
429
|
+
const numbers = fields.slice(1, 11).map(Number);
|
|
430
|
+
if (numbers.some((value) => !Number.isFinite(value)) || numbers.slice(0, 9).some((value) => !Number.isSafeInteger(value) || value < 0)) throw new Error("Tesseract returned invalid word coordinates");
|
|
431
|
+
const confidence = Number(fields[10]);
|
|
432
|
+
if (confidence < 0 || confidence > 100) throw new Error("Tesseract returned invalid word confidence");
|
|
433
|
+
const text = fields.slice(11).join(" ").trim();
|
|
434
|
+
if (!text) continue;
|
|
435
|
+
words.push({
|
|
436
|
+
text,
|
|
437
|
+
confidence: confidence / 100,
|
|
438
|
+
x: Number(fields[6]),
|
|
439
|
+
y: Number(fields[7]),
|
|
440
|
+
width: Number(fields[8]),
|
|
441
|
+
height: Number(fields[9]),
|
|
442
|
+
line: fields.slice(1, 5).join(":")
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return words;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Recognize screenshot bytes through a configured host Tesseract installation.
|
|
449
|
+
* Cancellation and deadlines kill the owned process; completion awaits process close before removing its private image.
|
|
450
|
+
* @param config - Explicit OCR installation.
|
|
451
|
+
* @param image - Bounded viewport PNG bytes.
|
|
452
|
+
* @param psm - Tesseract page segmentation mode.
|
|
453
|
+
* @param timeoutMs - Process deadline.
|
|
454
|
+
* @param maxBytes - Maximum collected stdout and stderr bytes.
|
|
455
|
+
* @param signal - Caller and plugin cancellation.
|
|
456
|
+
* @returns Text, words, and mean word confidence; an empty frame has null confidence.
|
|
457
|
+
*/
|
|
458
|
+
async function recognize(config, image, psm, timeoutMs, maxBytes, signal) {
|
|
459
|
+
signal.throwIfAborted();
|
|
460
|
+
const directory = await mkdtemp(join(tmpdir(), "dsh-optix-ocr-"));
|
|
461
|
+
try {
|
|
462
|
+
const path = join(directory, "viewport.png");
|
|
463
|
+
await writeFile(path, image, { signal });
|
|
464
|
+
signal.throwIfAborted();
|
|
465
|
+
const args = [
|
|
466
|
+
path,
|
|
467
|
+
"stdout",
|
|
468
|
+
"-l",
|
|
469
|
+
config.language,
|
|
470
|
+
"--psm",
|
|
471
|
+
String(psm),
|
|
472
|
+
...config.tessdataDirectory ? ["--tessdata-dir", config.tessdataDirectory] : [],
|
|
473
|
+
"-c",
|
|
474
|
+
"tessedit_create_tsv=1"
|
|
475
|
+
];
|
|
476
|
+
const completed = Promise.withResolvers();
|
|
477
|
+
const child = execFile(config.executable, args, {
|
|
478
|
+
cwd: directory,
|
|
479
|
+
env: scrubbedParentEnv(),
|
|
480
|
+
windowsHide: true,
|
|
481
|
+
timeout: timeoutMs,
|
|
482
|
+
maxBuffer: maxBytes,
|
|
483
|
+
encoding: "utf8",
|
|
484
|
+
signal
|
|
485
|
+
}, (error, stdout) => {
|
|
486
|
+
if (error) completed.reject(new Error("Optix OCR failed; check the configured Tesseract executable, language data, deadline, and output limit", { cause: error }));
|
|
487
|
+
else completed.resolve(stdout);
|
|
488
|
+
});
|
|
489
|
+
const closed = new Promise((resolve) => {
|
|
490
|
+
child.once("close", () => {
|
|
491
|
+
resolve();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
let tsv;
|
|
495
|
+
try {
|
|
496
|
+
tsv = await completed.promise;
|
|
497
|
+
} finally {
|
|
498
|
+
await closed;
|
|
499
|
+
}
|
|
500
|
+
signal.throwIfAborted();
|
|
501
|
+
const words = parseOcr(tsv);
|
|
502
|
+
const lines = /* @__PURE__ */ new Map();
|
|
503
|
+
for (const word of words) {
|
|
504
|
+
const line = lines.get(word.line) ?? [];
|
|
505
|
+
line.push(word.text);
|
|
506
|
+
lines.set(word.line, line);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
text: [...lines.values()].map((line) => line.join(" ")).join("\n"),
|
|
510
|
+
words,
|
|
511
|
+
confidence: words.length ? words.reduce((total, word) => total + word.confidence, 0) / words.length : null
|
|
512
|
+
};
|
|
513
|
+
} finally {
|
|
514
|
+
await rm(directory, {
|
|
515
|
+
recursive: true,
|
|
516
|
+
force: true
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region lib/types/preview.js
|
|
522
|
+
/** Direct Chromium CDP preview without an MCP transport. @module dsh-tool-ft-optix/preview */
|
|
523
|
+
/**
|
|
524
|
+
* Resolve a preview URL without permitting remote or credential-bearing targets.
|
|
525
|
+
* @param input - Configured preview URL.
|
|
526
|
+
* @returns The normalized loopback URL.
|
|
527
|
+
*/
|
|
528
|
+
function previewTarget(input) {
|
|
529
|
+
const url = new URL(input);
|
|
530
|
+
bridgeOrigin(url.origin);
|
|
531
|
+
if (url.username || url.password || url.hash) throw new Error("previewUrl cannot contain credentials or a fragment");
|
|
532
|
+
return url.href;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Register optional direct-CDP preview tools; disposal disconnects owned clients and awaits their completion.
|
|
536
|
+
* @param ctx - Tool context with durable attachments for screenshots.
|
|
537
|
+
* @param config - Explicit browser target and limits.
|
|
538
|
+
*/
|
|
539
|
+
function applyPreview(ctx, config) {
|
|
540
|
+
const attachments = ctx.get("attachments");
|
|
541
|
+
if (!attachments) throw new Error("Optix preview requires a durable attachment provider");
|
|
542
|
+
const disposed = new AbortController();
|
|
543
|
+
const clients = /* @__PURE__ */ new Set();
|
|
544
|
+
const pending = /* @__PURE__ */ new Set();
|
|
545
|
+
const target = new URL(config.previewUrl);
|
|
546
|
+
let busy = false;
|
|
547
|
+
ctx.effect(() => async () => {
|
|
548
|
+
disposed.abort();
|
|
549
|
+
await Promise.allSettled([...clients].map((browser) => browser.close()));
|
|
550
|
+
await Promise.allSettled(pending);
|
|
551
|
+
});
|
|
552
|
+
/** Acquire one unambiguous preview page; external Chromium stays running on disconnect. */
|
|
553
|
+
function withPage(signal, run) {
|
|
554
|
+
if (busy) return Promise.reject(/* @__PURE__ */ new Error("An Optix preview call is already running"));
|
|
555
|
+
busy = true;
|
|
556
|
+
const work = (async () => {
|
|
557
|
+
const combined = AbortSignal.any([signal, disposed.signal]);
|
|
558
|
+
combined.throwIfAborted();
|
|
559
|
+
const { chromium } = await import("playwright-core");
|
|
560
|
+
const browser = await chromium.connectOverCDP(config.cdpUrl, {
|
|
561
|
+
timeout: config.requestTimeoutMs,
|
|
562
|
+
noDefaults: true
|
|
563
|
+
});
|
|
564
|
+
clients.add(browser);
|
|
565
|
+
let disconnected;
|
|
566
|
+
const disconnect = () => {
|
|
567
|
+
disconnected = Promise.allSettled([browser.close()]);
|
|
568
|
+
};
|
|
569
|
+
combined.addEventListener("abort", disconnect, { once: true });
|
|
570
|
+
try {
|
|
571
|
+
combined.throwIfAborted();
|
|
572
|
+
const pages = browser.contexts().flatMap((context) => context.pages()).filter((page) => {
|
|
573
|
+
const url = new URL(page.url());
|
|
574
|
+
return url.origin === target.origin && url.pathname === target.pathname && url.search === target.search;
|
|
575
|
+
});
|
|
576
|
+
const page = pages[0];
|
|
577
|
+
if (pages.length !== 1 || !page) throw new Error("Open exactly one tab at the configured Optix previewUrl");
|
|
578
|
+
page.setDefaultTimeout(config.requestTimeoutMs);
|
|
579
|
+
return await run(page);
|
|
580
|
+
} finally {
|
|
581
|
+
combined.removeEventListener("abort", disconnect);
|
|
582
|
+
if (disconnected) await disconnected;
|
|
583
|
+
else await browser.close();
|
|
584
|
+
clients.delete(browser);
|
|
585
|
+
}
|
|
586
|
+
})();
|
|
587
|
+
pending.add(work);
|
|
588
|
+
work.then(() => {
|
|
589
|
+
pending.delete(work);
|
|
590
|
+
busy = false;
|
|
591
|
+
}, () => {
|
|
592
|
+
pending.delete(work);
|
|
593
|
+
busy = false;
|
|
594
|
+
});
|
|
595
|
+
return work;
|
|
596
|
+
}
|
|
597
|
+
/** Capture within the configured viewport and encoded image limits. */
|
|
598
|
+
async function capture(page) {
|
|
599
|
+
const viewport = await page.evaluate(
|
|
600
|
+
/* v8 ignore next */
|
|
601
|
+
() => ({
|
|
602
|
+
width: innerWidth,
|
|
603
|
+
height: innerHeight
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
if (viewport.width * viewport.height > config.maxPreviewPixels) throw new Error("Optix preview exceeds maxPreviewPixels");
|
|
607
|
+
const bytes = await page.screenshot({
|
|
608
|
+
type: "png",
|
|
609
|
+
scale: "css",
|
|
610
|
+
timeout: config.requestTimeoutMs
|
|
611
|
+
});
|
|
612
|
+
if (bytes.byteLength > config.maxResponseBytes) throw new Error("Optix screenshot exceeds maxResponseBytes");
|
|
613
|
+
return {
|
|
614
|
+
bytes,
|
|
615
|
+
width: viewport.width,
|
|
616
|
+
height: viewport.height
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const ocr = config.ocr;
|
|
620
|
+
if (ocr) ctx.effect(() => ctx.tools.register(defineTool({
|
|
621
|
+
name: "optix_ocr",
|
|
622
|
+
description: "Read rendered Optix canvas text by capturing the configured Chromium preview and invoking the configured host Tesseract executable directly, without MCP or Python. Returns word boxes in screenshot pixels, confidence fractions, and the durable source image reference. OCR can misread values; inspect optix_screenshot and verify against the live model before acting.",
|
|
623
|
+
parameters: { psm: {
|
|
624
|
+
type: "integer",
|
|
625
|
+
required: true,
|
|
626
|
+
description: "Tesseract page segmentation mode from 0 through 13; 6 for a text block, 11 for scattered HMI labels."
|
|
627
|
+
} },
|
|
628
|
+
output: {
|
|
629
|
+
schema: { type: "json" },
|
|
630
|
+
render: (_args, value) => [{
|
|
631
|
+
type: "text",
|
|
632
|
+
text: JSON.stringify(value, null, 2)
|
|
633
|
+
}]
|
|
634
|
+
},
|
|
635
|
+
presentCall: () => ({
|
|
636
|
+
card: "generic",
|
|
637
|
+
title: "Read Optix canvas text",
|
|
638
|
+
kind: "read"
|
|
639
|
+
}),
|
|
640
|
+
async execute(args, exec) {
|
|
641
|
+
if (args.psm < 0 || args.psm > 13) throw new Error("OCR psm must be between 0 and 13");
|
|
642
|
+
return withPage(exec.signal, async (page) => {
|
|
643
|
+
const { bytes, width, height } = await capture(page);
|
|
644
|
+
const sourceImage = await attachments.saveImage({
|
|
645
|
+
data: bytes,
|
|
646
|
+
mediaType: "image/png",
|
|
647
|
+
name: "optix-ocr-source.png"
|
|
648
|
+
});
|
|
649
|
+
const result = await recognize(ocr, bytes, args.psm, config.requestTimeoutMs, config.maxResponseBytes, AbortSignal.any([exec.signal, disposed.signal]));
|
|
650
|
+
return {
|
|
651
|
+
url: page.url(),
|
|
652
|
+
sourceImage: {
|
|
653
|
+
attachmentId: sourceImage.attachmentId,
|
|
654
|
+
mediaType: sourceImage.mediaType,
|
|
655
|
+
width: sourceImage.width,
|
|
656
|
+
height: sourceImage.height,
|
|
657
|
+
bytes: sourceImage.bytes
|
|
658
|
+
},
|
|
659
|
+
text: result.text,
|
|
660
|
+
words: result.words.map((word) => ({ ...word })),
|
|
661
|
+
confidence: result.confidence,
|
|
662
|
+
coordinateSpace: {
|
|
663
|
+
width,
|
|
664
|
+
height,
|
|
665
|
+
units: "viewport CSS pixels"
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
})));
|
|
671
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
672
|
+
name: "optix_screenshot",
|
|
673
|
+
description: "Capture the configured Optix emulator browser viewport through direct CDP and return a durable image. Use after live authoring and emulator rebuild to verify rendering. Requires preview enabled, an attachment provider, and exactly one already-open preview tab.",
|
|
674
|
+
parameters: {},
|
|
675
|
+
output: {
|
|
676
|
+
schema: {
|
|
677
|
+
type: "object",
|
|
678
|
+
additionalProperties: false,
|
|
679
|
+
properties: {
|
|
680
|
+
url: {
|
|
681
|
+
type: "string",
|
|
682
|
+
required: true
|
|
683
|
+
},
|
|
684
|
+
attachment: {
|
|
685
|
+
type: "object",
|
|
686
|
+
additionalProperties: false,
|
|
687
|
+
required: true,
|
|
688
|
+
properties: {
|
|
689
|
+
attachmentId: {
|
|
690
|
+
type: "string",
|
|
691
|
+
required: true
|
|
692
|
+
},
|
|
693
|
+
mediaType: {
|
|
694
|
+
type: "string",
|
|
695
|
+
enum: [
|
|
696
|
+
"image/png",
|
|
697
|
+
"image/jpeg",
|
|
698
|
+
"image/webp",
|
|
699
|
+
"image/gif"
|
|
700
|
+
],
|
|
701
|
+
required: true
|
|
702
|
+
},
|
|
703
|
+
bytes: {
|
|
704
|
+
type: "integer",
|
|
705
|
+
required: true
|
|
706
|
+
},
|
|
707
|
+
width: {
|
|
708
|
+
type: "integer",
|
|
709
|
+
required: true
|
|
710
|
+
},
|
|
711
|
+
height: {
|
|
712
|
+
type: "integer",
|
|
713
|
+
required: true
|
|
714
|
+
},
|
|
715
|
+
name: { type: "string" },
|
|
716
|
+
originalDimensions: {
|
|
717
|
+
type: "object",
|
|
718
|
+
additionalProperties: false,
|
|
719
|
+
properties: {
|
|
720
|
+
width: {
|
|
721
|
+
type: "integer",
|
|
722
|
+
required: true
|
|
723
|
+
},
|
|
724
|
+
height: {
|
|
725
|
+
type: "integer",
|
|
726
|
+
required: true
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
render: (_args, value) => [{
|
|
735
|
+
type: "text",
|
|
736
|
+
text: `Optix preview at ${value.url}: ${value.attachment.width}×${value.attachment.height}.`
|
|
737
|
+
}, {
|
|
738
|
+
type: "image",
|
|
739
|
+
attachment: {
|
|
740
|
+
...value.attachment,
|
|
741
|
+
attachmentId: value.attachment.attachmentId
|
|
742
|
+
}
|
|
743
|
+
}]
|
|
744
|
+
},
|
|
745
|
+
presentCall: () => ({
|
|
746
|
+
card: "generic",
|
|
747
|
+
title: "Capture Optix preview",
|
|
748
|
+
kind: "read"
|
|
749
|
+
}),
|
|
750
|
+
async execute(_args, exec) {
|
|
751
|
+
return withPage(exec.signal, async (page) => {
|
|
752
|
+
const { bytes } = await capture(page);
|
|
753
|
+
const attachment = await attachments.saveImage({
|
|
754
|
+
data: bytes,
|
|
755
|
+
mediaType: "image/png",
|
|
756
|
+
name: "optix-preview.png"
|
|
757
|
+
});
|
|
758
|
+
return {
|
|
759
|
+
url: page.url(),
|
|
760
|
+
attachment
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
})));
|
|
765
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
766
|
+
name: "optix_preview",
|
|
767
|
+
description: "Inspect browser text or interact with the explicitly configured Optix emulator preview through direct CDP. Canvas controls may have no DOM text; inspect optix_screenshot and use viewport CSS-pixel coordinates. Input requires allowPreviewInput and may change runtime values.",
|
|
768
|
+
parameters: { request: {
|
|
769
|
+
required: true,
|
|
770
|
+
oneOf: [
|
|
771
|
+
{
|
|
772
|
+
type: "object",
|
|
773
|
+
additionalProperties: false,
|
|
774
|
+
properties: { action: {
|
|
775
|
+
type: "string",
|
|
776
|
+
const: "text",
|
|
777
|
+
required: true
|
|
778
|
+
} }
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
type: "object",
|
|
782
|
+
additionalProperties: false,
|
|
783
|
+
properties: {
|
|
784
|
+
action: {
|
|
785
|
+
type: "string",
|
|
786
|
+
const: "click",
|
|
787
|
+
required: true
|
|
788
|
+
},
|
|
789
|
+
x: {
|
|
790
|
+
type: "number",
|
|
791
|
+
required: true
|
|
792
|
+
},
|
|
793
|
+
y: {
|
|
794
|
+
type: "number",
|
|
795
|
+
required: true
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
type: "object",
|
|
801
|
+
additionalProperties: false,
|
|
802
|
+
properties: {
|
|
803
|
+
action: {
|
|
804
|
+
type: "string",
|
|
805
|
+
const: "type",
|
|
806
|
+
required: true
|
|
807
|
+
},
|
|
808
|
+
text: {
|
|
809
|
+
type: "string",
|
|
810
|
+
required: true
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
type: "object",
|
|
816
|
+
additionalProperties: false,
|
|
817
|
+
properties: {
|
|
818
|
+
action: {
|
|
819
|
+
type: "string",
|
|
820
|
+
const: "key",
|
|
821
|
+
required: true
|
|
822
|
+
},
|
|
823
|
+
key: {
|
|
824
|
+
type: "string",
|
|
825
|
+
required: true
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
]
|
|
830
|
+
} },
|
|
831
|
+
output: {
|
|
832
|
+
schema: {
|
|
833
|
+
type: "object",
|
|
834
|
+
additionalProperties: false,
|
|
835
|
+
properties: {
|
|
836
|
+
action: {
|
|
837
|
+
type: "string",
|
|
838
|
+
required: true
|
|
839
|
+
},
|
|
840
|
+
url: {
|
|
841
|
+
type: "string",
|
|
842
|
+
required: true
|
|
843
|
+
},
|
|
844
|
+
text: { type: "string" }
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
render: (_args, value) => [{
|
|
848
|
+
type: "text",
|
|
849
|
+
text: JSON.stringify(value, null, 2)
|
|
850
|
+
}]
|
|
851
|
+
},
|
|
852
|
+
presentCall: (args) => ({
|
|
853
|
+
card: "generic",
|
|
854
|
+
title: `Optix preview ${args.request.action}`,
|
|
855
|
+
kind: args.request.action === "text" ? "read" : "edit"
|
|
856
|
+
}),
|
|
857
|
+
async execute(args, exec) {
|
|
858
|
+
const request = args.request;
|
|
859
|
+
if (request.action !== "text" && !config.allowPreviewInput) throw new Error("Preview input requires allowPreviewInput in plugin configuration");
|
|
860
|
+
return withPage(exec.signal, async (page) => {
|
|
861
|
+
switch (request.action) {
|
|
862
|
+
case "text": {
|
|
863
|
+
const value = await page.locator("body").innerText();
|
|
864
|
+
if (Buffer.byteLength(value) > config.maxResponseBytes) throw new Error("Preview text exceeds maxResponseBytes");
|
|
865
|
+
return {
|
|
866
|
+
action: request.action,
|
|
867
|
+
url: page.url(),
|
|
868
|
+
text: value
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
case "click": {
|
|
872
|
+
const viewport = await page.evaluate(
|
|
873
|
+
/* v8 ignore next */
|
|
874
|
+
() => ({
|
|
875
|
+
width: innerWidth,
|
|
876
|
+
height: innerHeight
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
if (request.x < 0 || request.y < 0 || request.x >= viewport.width || request.y >= viewport.height) throw new Error("Click coordinates must be inside the preview viewport");
|
|
880
|
+
await page.mouse.click(request.x, request.y);
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
case "type":
|
|
884
|
+
await page.keyboard.insertText(request.text);
|
|
885
|
+
break;
|
|
886
|
+
case "key":
|
|
887
|
+
await page.keyboard.press(request.key);
|
|
888
|
+
break;
|
|
889
|
+
/* v8 ignore next -- closed schema union; unknown actions fail registry validation. */
|
|
890
|
+
default: assertNever(request);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
action: request.action,
|
|
894
|
+
url: page.url()
|
|
895
|
+
};
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
})));
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region lib/types/templates.js
|
|
902
|
+
/** Immutable user-supplied Optix starters and NetLogic acquisition. @module dsh-tool-ft-optix/templates */
|
|
903
|
+
const ids = ["arya-optix-ipc", "arya-optix-panel"];
|
|
904
|
+
/**
|
|
905
|
+
* Parse the shipped manifest before exposing project file access.
|
|
906
|
+
* @param text - Manifest JSON acquired from the package assets.
|
|
907
|
+
* @returns JSON object containing the file list; rejects invalid manifests.
|
|
908
|
+
*/
|
|
909
|
+
function parseTemplateManifest(text) {
|
|
910
|
+
const parsed = JSON.parse(text);
|
|
911
|
+
if (!isJsonValue(parsed)) throw new Error("Optix template manifest is invalid");
|
|
912
|
+
const manifest = parsed;
|
|
913
|
+
if (!jsonObject(manifest) || !Array.isArray(manifest.files)) throw new Error("Optix template manifest is invalid");
|
|
914
|
+
return {
|
|
915
|
+
...manifest,
|
|
916
|
+
files: manifest.files
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Register access to packaged starters without modifying an existing project.
|
|
921
|
+
* @param ctx - Harness tool runtime context.
|
|
922
|
+
* @param maxBytes - Maximum bytes returned by a file acquisition.
|
|
923
|
+
*/
|
|
924
|
+
function applyTemplates(ctx, maxBytes) {
|
|
925
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
926
|
+
name: "optix_template",
|
|
927
|
+
description: "Discover packaged Arya Optix IPC and OptixPanel starter projects, or read their NetLogic and project files. Copy the selected project directory into a new workspace directory with filesystem tools; preserve the original project name. Never overwrite an existing project. Templates include bridge 1.0.7; exported method invocation requires upgrading to the separately supplied session-safe bridge. No Studio connection is required.",
|
|
928
|
+
parameters: {
|
|
929
|
+
template: {
|
|
930
|
+
type: "string",
|
|
931
|
+
enum: ids,
|
|
932
|
+
required: true
|
|
933
|
+
},
|
|
934
|
+
path: {
|
|
935
|
+
type: "string",
|
|
936
|
+
description: "Exact manifest-listed project-relative file to read; omit to return the manifest and source directory. NetLogic: ProjectFiles/NetSolution/StudioMCPBridge.cs."
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
output: {
|
|
940
|
+
schema: { type: "json" },
|
|
941
|
+
render: (_args, value) => [{
|
|
942
|
+
type: "text",
|
|
943
|
+
text: JSON.stringify(value, null, 2)
|
|
944
|
+
}]
|
|
945
|
+
},
|
|
946
|
+
presentCall: () => ({
|
|
947
|
+
card: "generic",
|
|
948
|
+
title: "Read Optix starter project",
|
|
949
|
+
kind: "read"
|
|
950
|
+
}),
|
|
951
|
+
async execute(args, exec) {
|
|
952
|
+
exec.signal.throwIfAborted();
|
|
953
|
+
const root = new URL(`../templates/${args.template}/`, import.meta.url);
|
|
954
|
+
const manifest = parseTemplateManifest(await readFile(new URL("manifest.json", root), "utf8"));
|
|
955
|
+
if (args.path === void 0) return {
|
|
956
|
+
manifest,
|
|
957
|
+
sourceDirectory: fileURLToPath(new URL("project/", root)),
|
|
958
|
+
bridgeVersion: "1.0.7",
|
|
959
|
+
instructions: "Copy to a new directory, open the .optix file in Studio 1.7.4.32, compile NetSolution, and execute StartBridge. The class name StudioMCPBridge is retained; no MCP service is needed."
|
|
960
|
+
};
|
|
961
|
+
const path = args.path;
|
|
962
|
+
if (!path || path.includes("\\") || path.split("/").some((part) => !part || part === "." || part === "..") || !manifest.files.some((file) => file !== null && typeof file === "object" && !Array.isArray(file) && file.path === path && file.text === true)) throw new Error("Choose an exact text file path listed in the template manifest");
|
|
963
|
+
const bytes = await readFile(new URL(`project/${path.split("/").map(encodeURIComponent).join("/")}`, root));
|
|
964
|
+
exec.signal.throwIfAborted();
|
|
965
|
+
if (bytes.byteLength > maxBytes) throw new Error("Template file exceeds maxResponseBytes");
|
|
966
|
+
return {
|
|
967
|
+
template: args.template,
|
|
968
|
+
path,
|
|
969
|
+
content: new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
})));
|
|
973
|
+
}
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region lib/types/index.js
|
|
976
|
+
/** Native FactoryTalk Optix discovery and live authoring tools. @module dsh-tool-ft-optix */
|
|
977
|
+
/** Cordis plugin name. */
|
|
978
|
+
const name = "tool-ft-optix";
|
|
979
|
+
/** Tools are dispatched through the harness policy and session pipeline. */
|
|
980
|
+
const inject = ["tools"];
|
|
981
|
+
/** Runtime configuration; no connection is opened during plugin registration. */
|
|
982
|
+
const Config = z.object({
|
|
983
|
+
bridgeUrls: z.array(z.string()).default(["http://127.0.0.1:8768"]),
|
|
984
|
+
bridgeToken: z.string().default(""),
|
|
985
|
+
allowWrites: z.boolean().default(false),
|
|
986
|
+
allowDestructive: z.boolean().default(false),
|
|
987
|
+
allowInvoke: z.boolean().default(false),
|
|
988
|
+
requestTimeoutMs: z.natural().min(1).max(2147483647).default(2e4),
|
|
989
|
+
maxResponseBytes: z.natural().min(1).default(2e6),
|
|
990
|
+
maxBatchOps: z.natural().min(1).default(40),
|
|
991
|
+
maxMapNodes: z.natural().min(1).default(800),
|
|
992
|
+
maxMapDepth: z.natural().min(1).default(8),
|
|
993
|
+
preview: z.boolean().default(false),
|
|
994
|
+
cdpUrl: z.string().default("http://127.0.0.1:9223"),
|
|
995
|
+
previewUrl: z.string().default("http://127.0.0.1:8081/"),
|
|
996
|
+
allowPreviewInput: z.boolean().default(false),
|
|
997
|
+
maxPreviewPixels: z.natural().min(1).default(8e6),
|
|
998
|
+
ocr: z.union([z.const(null), z.object({
|
|
999
|
+
executable: z.string(),
|
|
1000
|
+
language: z.string(),
|
|
1001
|
+
tessdataDirectory: z.string()
|
|
1002
|
+
})]).default(null)
|
|
1003
|
+
});
|
|
1004
|
+
const projectParameter = {
|
|
1005
|
+
type: "string",
|
|
1006
|
+
required: true,
|
|
1007
|
+
description: "Exact loaded project BrowseName from optix_status."
|
|
1008
|
+
};
|
|
1009
|
+
const text = {
|
|
1010
|
+
type: "string",
|
|
1011
|
+
required: true
|
|
1012
|
+
};
|
|
1013
|
+
const jsonOutput = {
|
|
1014
|
+
schema: { type: "json" },
|
|
1015
|
+
render: (_args, value) => [{
|
|
1016
|
+
type: "text",
|
|
1017
|
+
text: JSON.stringify(value, null, 2)
|
|
1018
|
+
}]
|
|
1019
|
+
};
|
|
1020
|
+
/**
|
|
1021
|
+
* Resolve defaults before constructing transport or registering tools.
|
|
1022
|
+
* @param config - Plugin configuration.
|
|
1023
|
+
* @returns Validated complete deployment settings.
|
|
1024
|
+
*/
|
|
1025
|
+
function resolve(config) {
|
|
1026
|
+
const resolved = {
|
|
1027
|
+
bridgeUrls: config.bridgeUrls ?? ["http://127.0.0.1:8768"],
|
|
1028
|
+
bridgeToken: config.bridgeToken ?? "",
|
|
1029
|
+
allowWrites: config.allowWrites ?? false,
|
|
1030
|
+
allowDestructive: config.allowDestructive ?? false,
|
|
1031
|
+
allowInvoke: config.allowInvoke ?? false,
|
|
1032
|
+
requestTimeoutMs: config.requestTimeoutMs ?? 2e4,
|
|
1033
|
+
maxResponseBytes: config.maxResponseBytes ?? 2e6,
|
|
1034
|
+
maxBatchOps: config.maxBatchOps ?? 40,
|
|
1035
|
+
maxMapNodes: config.maxMapNodes ?? 800,
|
|
1036
|
+
maxMapDepth: config.maxMapDepth ?? 8,
|
|
1037
|
+
preview: config.preview ?? false,
|
|
1038
|
+
cdpUrl: config.cdpUrl ?? "http://127.0.0.1:9223",
|
|
1039
|
+
previewUrl: config.previewUrl ?? "http://127.0.0.1:8081/",
|
|
1040
|
+
allowPreviewInput: config.allowPreviewInput ?? false,
|
|
1041
|
+
maxPreviewPixels: config.maxPreviewPixels ?? 8e6,
|
|
1042
|
+
ocr: config.ocr ?? null
|
|
1043
|
+
};
|
|
1044
|
+
for (const key of [
|
|
1045
|
+
"requestTimeoutMs",
|
|
1046
|
+
"maxResponseBytes",
|
|
1047
|
+
"maxBatchOps",
|
|
1048
|
+
"maxMapNodes",
|
|
1049
|
+
"maxMapDepth",
|
|
1050
|
+
"maxPreviewPixels"
|
|
1051
|
+
]) if (!Number.isSafeInteger(resolved[key]) || resolved[key] < 1) throw new Error(`${key} must be a positive safe integer`);
|
|
1052
|
+
if (resolved.requestTimeoutMs > 2147483647) throw new Error("requestTimeoutMs exceeds the Node timer range");
|
|
1053
|
+
resolved.bridgeUrls = resolved.bridgeUrls.map(bridgeOrigin);
|
|
1054
|
+
if (!resolved.bridgeUrls.length || new Set(resolved.bridgeUrls).size !== resolved.bridgeUrls.length) throw new Error("bridgeUrls must be non-empty and unique");
|
|
1055
|
+
if (/[\r\n]/.test(resolved.bridgeToken)) throw new Error("bridgeToken cannot contain header delimiters");
|
|
1056
|
+
resolved.cdpUrl = bridgeOrigin(resolved.cdpUrl);
|
|
1057
|
+
resolved.previewUrl = previewTarget(resolved.previewUrl);
|
|
1058
|
+
if (resolved.ocr) {
|
|
1059
|
+
if (!resolved.preview) throw new Error("Optix OCR requires preview enabled");
|
|
1060
|
+
if (!resolved.ocr.executable.trim() || resolved.ocr.executable.includes("\0")) throw new Error("OCR executable must be non-empty");
|
|
1061
|
+
if (!/^[a-zA-Z0-9_]+(?:\+[a-zA-Z0-9_]+)*$/.test(resolved.ocr.language)) throw new Error("OCR language must contain installed language names joined with +");
|
|
1062
|
+
}
|
|
1063
|
+
return resolved;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Require authoritative validation fields rather than treating a partial report as approval.
|
|
1067
|
+
* @param value - External validation result.
|
|
1068
|
+
* @param count - Number of submitted operations.
|
|
1069
|
+
* @returns Whether strict preflight accepted every operation.
|
|
1070
|
+
*/
|
|
1071
|
+
function acceptedValidation(value, count) {
|
|
1072
|
+
if (typeof value.ok !== "boolean" || value.op_count !== count || !Array.isArray(value.errors) || !Array.isArray(value.warnings)) throw new Error("Bridge returned an incomplete batch validation report");
|
|
1073
|
+
return value.ok && value.errors.length === 0 && value.warnings.length === 0;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Register native tools and abort transport work on plugin disposal.
|
|
1077
|
+
* @param ctx - Cordis context with the harness tool runtime.
|
|
1078
|
+
* @param config - Connection and authoring settings.
|
|
1079
|
+
*/
|
|
1080
|
+
function apply(ctx, config = {}) {
|
|
1081
|
+
const settings = resolve(config);
|
|
1082
|
+
applyTemplates(ctx, settings.maxResponseBytes);
|
|
1083
|
+
if (settings.preview) ctx.inject(["attachments"], (scope) => {
|
|
1084
|
+
applyPreview(scope, settings);
|
|
1085
|
+
});
|
|
1086
|
+
const disposed = new AbortController();
|
|
1087
|
+
const bridge = new OptixBridgeGateway(settings, disposed.signal);
|
|
1088
|
+
ctx.effect(() => async () => {
|
|
1089
|
+
disposed.abort();
|
|
1090
|
+
await bridge.drain();
|
|
1091
|
+
});
|
|
1092
|
+
let editing = false;
|
|
1093
|
+
/** One process-local authoring owner prevents interleaved batches. */
|
|
1094
|
+
async function exclusive(run) {
|
|
1095
|
+
if (editing) throw new Error("An Optix authoring call is already running; wait for its result before submitting another");
|
|
1096
|
+
editing = true;
|
|
1097
|
+
try {
|
|
1098
|
+
return await run();
|
|
1099
|
+
} finally {
|
|
1100
|
+
editing = false;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/** Config enablement is checked before any live operation is dispatched. */
|
|
1104
|
+
function allowWrite(destructive = false) {
|
|
1105
|
+
if (!settings.allowWrites) throw new Error("Optix live authoring is disabled; set allowWrites in the plugin configuration");
|
|
1106
|
+
if (destructive && !settings.allowDestructive) throw new Error("This operation requires allowDestructive in the plugin configuration");
|
|
1107
|
+
}
|
|
1108
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
1109
|
+
name: "optix_status",
|
|
1110
|
+
description: "Discover loaded FactoryTalk Optix Studio projects directly through configured local bridges. Use before selecting a project. No MCP or Python service is required.",
|
|
1111
|
+
parameters: {},
|
|
1112
|
+
output: jsonOutput,
|
|
1113
|
+
presentCall: () => ({
|
|
1114
|
+
card: "generic",
|
|
1115
|
+
title: "Discover Optix projects",
|
|
1116
|
+
kind: "read"
|
|
1117
|
+
}),
|
|
1118
|
+
async execute(_args, exec) {
|
|
1119
|
+
const endpoints = [];
|
|
1120
|
+
for (const origin of settings.bridgeUrls) {
|
|
1121
|
+
exec.signal.throwIfAborted();
|
|
1122
|
+
try {
|
|
1123
|
+
endpoints.push({
|
|
1124
|
+
origin,
|
|
1125
|
+
available: true,
|
|
1126
|
+
health: await bridge.request(origin, "health", exec.signal)
|
|
1127
|
+
});
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (exec.signal.aborted || disposed.signal.aborted) throw error;
|
|
1130
|
+
endpoints.push({
|
|
1131
|
+
origin,
|
|
1132
|
+
available: false,
|
|
1133
|
+
error: "Bridge unavailable; open the project in Studio and run StartBridge"
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
endpoints,
|
|
1139
|
+
authoring: settings.allowWrites,
|
|
1140
|
+
destructive: settings.allowDestructive,
|
|
1141
|
+
invoke: settings.allowInvoke
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
})));
|
|
1145
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
1146
|
+
name: "optix_read",
|
|
1147
|
+
description: "Read the live Optix model, screens, widget types, schemas, variable members, or CLR diagnostics. Start with map, inspect declared properties with type_schema, then author. Disk files may be stale while Studio is open.",
|
|
1148
|
+
parameters: {
|
|
1149
|
+
project: projectParameter,
|
|
1150
|
+
request: {
|
|
1151
|
+
required: true,
|
|
1152
|
+
oneOf: [
|
|
1153
|
+
{
|
|
1154
|
+
type: "object",
|
|
1155
|
+
additionalProperties: false,
|
|
1156
|
+
properties: {
|
|
1157
|
+
action: {
|
|
1158
|
+
type: "string",
|
|
1159
|
+
const: "map",
|
|
1160
|
+
required: true
|
|
1161
|
+
},
|
|
1162
|
+
path: { type: "string" },
|
|
1163
|
+
depth: { type: "integer" },
|
|
1164
|
+
max: { type: "integer" },
|
|
1165
|
+
mode: {
|
|
1166
|
+
type: "string",
|
|
1167
|
+
enum: ["overview", "detail"]
|
|
1168
|
+
},
|
|
1169
|
+
match: { type: "string" },
|
|
1170
|
+
ids: { type: "boolean" }
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
type: "object",
|
|
1175
|
+
additionalProperties: false,
|
|
1176
|
+
properties: {
|
|
1177
|
+
action: {
|
|
1178
|
+
type: "string",
|
|
1179
|
+
const: "node",
|
|
1180
|
+
required: true
|
|
1181
|
+
},
|
|
1182
|
+
path: text
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
type: "object",
|
|
1187
|
+
additionalProperties: false,
|
|
1188
|
+
properties: { action: {
|
|
1189
|
+
type: "string",
|
|
1190
|
+
const: "screens",
|
|
1191
|
+
required: true
|
|
1192
|
+
} }
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
type: "object",
|
|
1196
|
+
additionalProperties: false,
|
|
1197
|
+
properties: { action: {
|
|
1198
|
+
type: "string",
|
|
1199
|
+
const: "ui_types",
|
|
1200
|
+
required: true
|
|
1201
|
+
} }
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
type: "object",
|
|
1205
|
+
additionalProperties: false,
|
|
1206
|
+
properties: { action: {
|
|
1207
|
+
type: "string",
|
|
1208
|
+
const: "schema",
|
|
1209
|
+
required: true
|
|
1210
|
+
} }
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
type: "object",
|
|
1214
|
+
additionalProperties: false,
|
|
1215
|
+
properties: {
|
|
1216
|
+
action: {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
const: "type_schema",
|
|
1219
|
+
required: true
|
|
1220
|
+
},
|
|
1221
|
+
type: text
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
type: "object",
|
|
1226
|
+
additionalProperties: false,
|
|
1227
|
+
properties: {
|
|
1228
|
+
action: {
|
|
1229
|
+
type: "string",
|
|
1230
|
+
const: "type_info",
|
|
1231
|
+
required: true
|
|
1232
|
+
},
|
|
1233
|
+
path: text
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
type: "object",
|
|
1238
|
+
additionalProperties: false,
|
|
1239
|
+
properties: {
|
|
1240
|
+
action: {
|
|
1241
|
+
type: "string",
|
|
1242
|
+
const: "variable_members",
|
|
1243
|
+
required: true
|
|
1244
|
+
},
|
|
1245
|
+
path: text,
|
|
1246
|
+
name: text
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
type: "object",
|
|
1251
|
+
additionalProperties: false,
|
|
1252
|
+
properties: {
|
|
1253
|
+
action: {
|
|
1254
|
+
type: "string",
|
|
1255
|
+
const: "clr_type",
|
|
1256
|
+
required: true
|
|
1257
|
+
},
|
|
1258
|
+
name: text
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
]
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
output: jsonOutput,
|
|
1265
|
+
presentCall: (args) => ({
|
|
1266
|
+
card: "generic",
|
|
1267
|
+
title: `Read Optix ${args.request.action}`,
|
|
1268
|
+
kind: "read"
|
|
1269
|
+
}),
|
|
1270
|
+
async execute(args, exec) {
|
|
1271
|
+
const routes = {
|
|
1272
|
+
map: "map",
|
|
1273
|
+
node: "nodes",
|
|
1274
|
+
screens: "screens",
|
|
1275
|
+
ui_types: "types/ui",
|
|
1276
|
+
schema: "schema/dump",
|
|
1277
|
+
type_schema: "types/schema",
|
|
1278
|
+
type_info: "node/typeinfo",
|
|
1279
|
+
variable_members: "node/varmembers",
|
|
1280
|
+
clr_type: "diag/clrtype"
|
|
1281
|
+
};
|
|
1282
|
+
const request = args.request;
|
|
1283
|
+
const query = {};
|
|
1284
|
+
for (const [key, value] of Object.entries(request)) if (key !== "action") query[key] = typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
1285
|
+
if (request.action === "map") {
|
|
1286
|
+
const max = request.max ?? settings.maxMapNodes;
|
|
1287
|
+
const depth = request.depth ?? Math.min(3, settings.maxMapDepth);
|
|
1288
|
+
if (max < 1 || max > settings.maxMapNodes || depth < 1 || depth > settings.maxMapDepth) throw new Error("Requested map exceeds configured node or depth limits");
|
|
1289
|
+
query.max = String(max);
|
|
1290
|
+
query.depth = String(depth);
|
|
1291
|
+
if (!Object.hasOwn(query, "mode")) query.mode = "overview";
|
|
1292
|
+
}
|
|
1293
|
+
const { origin } = await bridge.select(args.project, exec.signal);
|
|
1294
|
+
return {
|
|
1295
|
+
project: args.project,
|
|
1296
|
+
origin,
|
|
1297
|
+
response: await bridge.request(origin, routes[request.action], exec.signal, query)
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
})));
|
|
1301
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
1302
|
+
name: "optix_edit",
|
|
1303
|
+
description: "Strictly validate then sequentially apply a typed Optix authoring batch. dry_run defaults to true. Live writes require allowWrites; destructive operations also require allowDestructive. Stops on first failure; reports partial or indeterminate outcomes. No automatic retry or batch rollback. Re-read the model before retrying an indeterminate operation.",
|
|
1304
|
+
parameters: {
|
|
1305
|
+
project: projectParameter,
|
|
1306
|
+
ops: {
|
|
1307
|
+
type: "array",
|
|
1308
|
+
items: operationSchema,
|
|
1309
|
+
required: true
|
|
1310
|
+
},
|
|
1311
|
+
dry_run: { type: "boolean" }
|
|
1312
|
+
},
|
|
1313
|
+
output: jsonOutput,
|
|
1314
|
+
presentCall: (args) => ({
|
|
1315
|
+
card: "generic",
|
|
1316
|
+
title: args.dry_run === false ? "Author Optix model" : "Validate Optix edits",
|
|
1317
|
+
kind: args.dry_run === false ? "edit" : "read"
|
|
1318
|
+
}),
|
|
1319
|
+
async execute(args, exec) {
|
|
1320
|
+
const ops = args.ops;
|
|
1321
|
+
if (!ops.length || ops.length > settings.maxBatchOps) throw new Error("Batch must contain 1..maxBatchOps operations");
|
|
1322
|
+
checkOperations(ops);
|
|
1323
|
+
const dryRun = args.dry_run !== false;
|
|
1324
|
+
if (!dryRun) allowWrite(isDestructive(ops));
|
|
1325
|
+
return exclusive(async () => {
|
|
1326
|
+
const { origin, health: selectedHealth } = await bridge.select(args.project, exec.signal);
|
|
1327
|
+
const normalized = ops.map((op) => op.prop_name === void 0 ? op : {
|
|
1328
|
+
...op,
|
|
1329
|
+
name: op.prop_name
|
|
1330
|
+
});
|
|
1331
|
+
const validation = await bridge.request(origin, "validate_ops", exec.signal, {}, "POST", {
|
|
1332
|
+
ops: normalized,
|
|
1333
|
+
strict: true
|
|
1334
|
+
});
|
|
1335
|
+
if (!acceptedValidation(validation, ops.length)) return {
|
|
1336
|
+
status: "refused",
|
|
1337
|
+
applied: 0,
|
|
1338
|
+
validation
|
|
1339
|
+
};
|
|
1340
|
+
if (dryRun) return {
|
|
1341
|
+
status: "validated",
|
|
1342
|
+
applied: 0,
|
|
1343
|
+
validation
|
|
1344
|
+
};
|
|
1345
|
+
const results = [];
|
|
1346
|
+
for (const [index, op] of ops.entries()) {
|
|
1347
|
+
let dispatched = false;
|
|
1348
|
+
try {
|
|
1349
|
+
const health = await bridge.request(origin, "health", exec.signal);
|
|
1350
|
+
if (health.project !== args.project || health.model_loaded !== true || health.pid !== selectedHealth.pid || health.project_path !== selectedHealth.project_path) throw new Error("Serving Studio project changed during the batch");
|
|
1351
|
+
const request = operationRequest(op);
|
|
1352
|
+
dispatched = true;
|
|
1353
|
+
const result = await bridge.request(origin, request.route, exec.signal, request.query, "POST");
|
|
1354
|
+
if (result.ok !== true) throw new BridgeError("Bridge did not acknowledge the authoring operation", true);
|
|
1355
|
+
results.push(result);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
return {
|
|
1358
|
+
status: !dispatched || error instanceof BridgeError && !error.mayHaveApplied ? "partial" : "indeterminate",
|
|
1359
|
+
applied: results.length,
|
|
1360
|
+
failed_index: index,
|
|
1361
|
+
results,
|
|
1362
|
+
validation,
|
|
1363
|
+
error: String(error),
|
|
1364
|
+
retry: "Inspect the live model; do not replay the entire batch automatically"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
status: "completed",
|
|
1370
|
+
applied: results.length,
|
|
1371
|
+
results,
|
|
1372
|
+
validation
|
|
1373
|
+
};
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
})));
|
|
1377
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
1378
|
+
name: "optix_validate_expression",
|
|
1379
|
+
description: "Validate an Optix ExpressionEvaluator formula without attaching it or changing the model. Sources are comma-separated node paths in placeholder order.",
|
|
1380
|
+
parameters: {
|
|
1381
|
+
project: projectParameter,
|
|
1382
|
+
expression: text,
|
|
1383
|
+
sources: { type: "string" }
|
|
1384
|
+
},
|
|
1385
|
+
output: jsonOutput,
|
|
1386
|
+
presentCall: () => ({
|
|
1387
|
+
card: "generic",
|
|
1388
|
+
title: "Validate Optix expression",
|
|
1389
|
+
kind: "read"
|
|
1390
|
+
}),
|
|
1391
|
+
async execute(args, exec) {
|
|
1392
|
+
const { origin } = await bridge.select(args.project, exec.signal);
|
|
1393
|
+
return bridge.request(origin, "expr/validate", exec.signal, {
|
|
1394
|
+
expression: args.expression,
|
|
1395
|
+
...args.sources === void 0 ? {} : { sources: args.sources }
|
|
1396
|
+
}, "POST");
|
|
1397
|
+
}
|
|
1398
|
+
})));
|
|
1399
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
1400
|
+
name: "optix_action",
|
|
1401
|
+
description: "Apply one bridge-validated Optix action outside the batch validator: create NetLogic, ensure a web engine, add a translation, convert an instance to a type, or invoke an exported method. Invocation requires allowInvoke and a session-safe bridge; conversion requires allowDestructive. No retries. A successful model change does not save Studio or prove runtime rendering.",
|
|
1402
|
+
parameters: {
|
|
1403
|
+
project: projectParameter,
|
|
1404
|
+
request: {
|
|
1405
|
+
required: true,
|
|
1406
|
+
oneOf: [
|
|
1407
|
+
{
|
|
1408
|
+
type: "object",
|
|
1409
|
+
additionalProperties: false,
|
|
1410
|
+
properties: {
|
|
1411
|
+
action: {
|
|
1412
|
+
type: "string",
|
|
1413
|
+
const: "create_netlogic",
|
|
1414
|
+
required: true
|
|
1415
|
+
},
|
|
1416
|
+
parent: text,
|
|
1417
|
+
name: text
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
type: "object",
|
|
1422
|
+
additionalProperties: false,
|
|
1423
|
+
properties: {
|
|
1424
|
+
action: {
|
|
1425
|
+
type: "string",
|
|
1426
|
+
const: "ensure_web_engine",
|
|
1427
|
+
required: true
|
|
1428
|
+
},
|
|
1429
|
+
port: {
|
|
1430
|
+
type: "integer",
|
|
1431
|
+
required: true
|
|
1432
|
+
},
|
|
1433
|
+
ip: {
|
|
1434
|
+
type: "string",
|
|
1435
|
+
const: "127.0.0.1",
|
|
1436
|
+
required: true
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
type: "object",
|
|
1442
|
+
additionalProperties: false,
|
|
1443
|
+
properties: {
|
|
1444
|
+
action: {
|
|
1445
|
+
type: "string",
|
|
1446
|
+
const: "add_translation",
|
|
1447
|
+
required: true
|
|
1448
|
+
},
|
|
1449
|
+
key: text,
|
|
1450
|
+
value: text,
|
|
1451
|
+
locale: text
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
type: "object",
|
|
1456
|
+
additionalProperties: false,
|
|
1457
|
+
properties: {
|
|
1458
|
+
action: {
|
|
1459
|
+
type: "string",
|
|
1460
|
+
const: "convert_to_type",
|
|
1461
|
+
required: true
|
|
1462
|
+
},
|
|
1463
|
+
path: text,
|
|
1464
|
+
type_name: text,
|
|
1465
|
+
types_folder: text,
|
|
1466
|
+
replace: {
|
|
1467
|
+
type: "boolean",
|
|
1468
|
+
required: true
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
type: "object",
|
|
1474
|
+
additionalProperties: false,
|
|
1475
|
+
properties: {
|
|
1476
|
+
action: {
|
|
1477
|
+
type: "string",
|
|
1478
|
+
const: "invoke",
|
|
1479
|
+
required: true
|
|
1480
|
+
},
|
|
1481
|
+
path: text,
|
|
1482
|
+
method: text,
|
|
1483
|
+
args: { type: "string" }
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
]
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
output: jsonOutput,
|
|
1490
|
+
presentCall: (args) => ({
|
|
1491
|
+
card: "generic",
|
|
1492
|
+
title: `Optix ${args.request.action.replaceAll("_", " ")}`,
|
|
1493
|
+
kind: "edit"
|
|
1494
|
+
}),
|
|
1495
|
+
async execute(args, exec) {
|
|
1496
|
+
const request = args.request;
|
|
1497
|
+
allowWrite(request.action === "convert_to_type");
|
|
1498
|
+
if (request.action === "invoke" && !settings.allowInvoke) throw new Error("Method invocation requires allowInvoke in plugin configuration");
|
|
1499
|
+
if (request.action === "ensure_web_engine" && (request.port < 1 || request.port > 65535)) throw new Error("Web engine port must be 1..65535");
|
|
1500
|
+
return exclusive(async () => {
|
|
1501
|
+
const { origin, health } = await bridge.select(args.project, exec.signal);
|
|
1502
|
+
if (request.action === "invoke" && health.invoke_session !== "root") throw new Error("invoke_unsupported_bridge: install the session-safe bridge before invoking methods");
|
|
1503
|
+
const routes = {
|
|
1504
|
+
create_netlogic: "model/netlogic",
|
|
1505
|
+
ensure_web_engine: "setup/web-engine",
|
|
1506
|
+
add_translation: "i18n/translation",
|
|
1507
|
+
convert_to_type: "node/convert-to-type",
|
|
1508
|
+
invoke: "node/invoke"
|
|
1509
|
+
};
|
|
1510
|
+
const query = {};
|
|
1511
|
+
for (const [key, value] of Object.entries(request)) if (key !== "action") query[key] = String(value);
|
|
1512
|
+
try {
|
|
1513
|
+
const result = await bridge.request(origin, routes[request.action], exec.signal, query, "POST");
|
|
1514
|
+
if (result.ok !== true) throw new BridgeError("Bridge did not acknowledge the authoring action", true);
|
|
1515
|
+
return {
|
|
1516
|
+
status: "completed",
|
|
1517
|
+
response: result
|
|
1518
|
+
};
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
return {
|
|
1521
|
+
status: error instanceof BridgeError && error.mayHaveApplied ? "indeterminate" : "failed",
|
|
1522
|
+
error: String(error),
|
|
1523
|
+
retry: "Inspect the live model before retrying"
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
})));
|
|
1529
|
+
}
|
|
1530
|
+
//#endregion
|
|
1531
|
+
export { Config, apply, inject, name };
|