@aixnn/aixautots 0.0.0-stage → 0.5.2
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/README.md +239 -2
- package/dist/ScriptStudio-DaCqOF5H.d.ts +43 -0
- package/dist/agent-runner-V60B0is1.d.ts +57 -0
- package/dist/automation-queue-8HqRJV_K.d.ts +72 -0
- package/dist/chunk-536WVCTB.js +5744 -0
- package/dist/chunk-5BMFMDQR.js +18 -0
- package/dist/chunk-64Q7BBIQ.js +1347 -0
- package/dist/chunk-6YUAACW6.js +866 -0
- package/dist/chunk-AIUOLJZA.js +412 -0
- package/dist/chunk-CVV7FOMQ.js +175 -0
- package/dist/chunk-EHF2WQUR.js +2042 -0
- package/dist/chunk-EVHTAXQG.js +273 -0
- package/dist/chunk-FW23FLAF.js +56 -0
- package/dist/chunk-GKEPMDXO.js +410 -0
- package/dist/chunk-H7MHGYF3.js +341 -0
- package/dist/chunk-MRQ7VJFJ.js +1074 -0
- package/dist/chunk-NWFYTFMI.js +65 -0
- package/dist/chunk-OEZFYKL6.js +1 -0
- package/dist/chunk-PCYERBNZ.js +1564 -0
- package/dist/chunk-PUPVZRKZ.js +668 -0
- package/dist/chunk-SUKFJ73W.js +217 -0
- package/dist/config-Bm1hmXhD.d.ts +1380 -0
- package/dist/coords-LPR_MPyp.d.ts +22 -0
- package/dist/core/index.d.ts +135 -0
- package/dist/core/index.js +5 -0
- package/dist/device-normalize-BaThgnH7.d.ts +43 -0
- package/dist/device-session-KZVGLPOI.js +2 -0
- package/dist/i18n/index.d.ts +30 -0
- package/dist/i18n/index.js +1 -0
- package/dist/mirror-session-C_1vFkRV.d.ts +108 -0
- package/dist/nav/index.d.ts +19 -0
- package/dist/nav/index.js +1 -0
- package/dist/react/index.d.ts +487 -0
- package/dist/react/index.js +7 -0
- package/dist/script-debugger-Cw5jVRoz.d.ts +114 -0
- package/dist/task-agent-runner-eD9-SCJz.d.ts +160 -0
- package/dist/types-C2WXVcam.d.ts +1005 -0
- package/dist/ui/billing.d.ts +48 -0
- package/dist/ui/billing.js +425 -0
- package/dist/ui/device-create.d.ts +55 -0
- package/dist/ui/device-create.js +9 -0
- package/dist/ui/device-mirror.d.ts +42 -0
- package/dist/ui/device-mirror.js +10 -0
- package/dist/ui/devices.d.ts +223 -0
- package/dist/ui/devices.js +15 -0
- package/dist/ui/editor.d.ts +64 -0
- package/dist/ui/editor.js +344 -0
- package/dist/ui/index.d.ts +104 -0
- package/dist/ui/index.js +676 -0
- package/dist/ui/inspector.d.ts +5 -0
- package/dist/ui/inspector.js +1250 -0
- package/dist/ui/market.d.ts +10 -0
- package/dist/ui/market.js +60 -0
- package/dist/ui/mirror.d.ts +52 -0
- package/dist/ui/mirror.js +3 -0
- package/dist/ui/primitives/index.d.ts +101 -0
- package/dist/ui/primitives/index.js +1 -0
- package/dist/ui/script-studio.d.ts +85 -0
- package/dist/ui/script-studio.js +10 -0
- package/dist/ui/scripts.d.ts +18 -0
- package/dist/ui/scripts.js +210 -0
- package/dist/ui/tasks.d.ts +10 -0
- package/dist/ui/tasks.js +1039 -0
- package/dist/use-script-run-Diay7ibY.d.ts +50 -0
- package/package.json +76 -4
- package/styles.css +97 -0
- package/tailwind-preset.cjs +84 -0
|
@@ -0,0 +1,2042 @@
|
|
|
1
|
+
import { MirrorSession, normalizeDevices, submitBatch } from './chunk-H7MHGYF3.js';
|
|
2
|
+
import { useDevice, useAixConfig, useClient, useQuery } from './chunk-SUKFJ73W.js';
|
|
3
|
+
import { FlowRun } from './chunk-GKEPMDXO.js';
|
|
4
|
+
import { isFleetRunTerminal, resolveConfig, joinUrl, AixAuthError, AixError, detectEnvOs, EnvRun, summarizeEnv, ReconnectingSocket, executionWsTicketUrl } from './chunk-PCYERBNZ.js';
|
|
5
|
+
import { createContext, useState, useRef, useEffect, useCallback, useMemo, createElement, useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
function useCommands(deviceId) {
|
|
8
|
+
const { channel, error: openError } = useDevice(deviceId);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [lastResult, setLastResult] = useState(null);
|
|
12
|
+
const mountedRef = useRef(true);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
mountedRef.current = true;
|
|
15
|
+
return () => {
|
|
16
|
+
mountedRef.current = false;
|
|
17
|
+
};
|
|
18
|
+
}, []);
|
|
19
|
+
const adb = useCallback(async (argv, opts) => {
|
|
20
|
+
if (!channel) throw new Error("device channel not ready");
|
|
21
|
+
if (mountedRef.current) {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const r = await channel.adbExec(argv, opts);
|
|
27
|
+
if (mountedRef.current) {
|
|
28
|
+
setLastResult(r);
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
return r;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
34
|
+
if (mountedRef.current) {
|
|
35
|
+
setError(err);
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}, [channel]);
|
|
41
|
+
const wda = useCallback(async (method, path, body) => {
|
|
42
|
+
if (!channel) throw new Error("device channel not ready");
|
|
43
|
+
if (mountedRef.current) {
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const r = await channel.wdaReq(method, path, body);
|
|
49
|
+
if (mountedRef.current) {
|
|
50
|
+
setLastResult(r);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
return r;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
56
|
+
if (mountedRef.current) {
|
|
57
|
+
setError(err);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}, [channel]);
|
|
63
|
+
return { ready: channel !== null, loading, error: error ?? openError, lastResult, adb, wda };
|
|
64
|
+
}
|
|
65
|
+
function useFlowRun() {
|
|
66
|
+
const cfg = useAixConfig();
|
|
67
|
+
const [taskId, setTaskId] = useState(null);
|
|
68
|
+
const [events, setEvents] = useState([]);
|
|
69
|
+
const [done, setDone] = useState(false);
|
|
70
|
+
const [error, setError] = useState(null);
|
|
71
|
+
const runRef = useRef(null);
|
|
72
|
+
const mountedRef = useRef(true);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
mountedRef.current = true;
|
|
75
|
+
return () => {
|
|
76
|
+
mountedRef.current = false;
|
|
77
|
+
runRef.current?.close();
|
|
78
|
+
runRef.current = null;
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
const submit = useCallback(async (body) => {
|
|
82
|
+
runRef.current?.close();
|
|
83
|
+
if (mountedRef.current) {
|
|
84
|
+
setError(null);
|
|
85
|
+
setEvents([]);
|
|
86
|
+
setDone(false);
|
|
87
|
+
setTaskId(null);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const fr = await FlowRun.submit(cfg, body);
|
|
91
|
+
runRef.current = fr;
|
|
92
|
+
fr.on("event", (e) => {
|
|
93
|
+
if (mountedRef.current) setEvents((prev) => [...prev, e]);
|
|
94
|
+
});
|
|
95
|
+
fr.on("done", () => {
|
|
96
|
+
if (mountedRef.current) setDone(true);
|
|
97
|
+
});
|
|
98
|
+
if (mountedRef.current) setTaskId(fr.taskId);
|
|
99
|
+
return { taskId: fr.taskId };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
102
|
+
if (mountedRef.current) setError(err);
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}, [cfg]);
|
|
106
|
+
const close = useCallback(() => {
|
|
107
|
+
runRef.current?.close();
|
|
108
|
+
runRef.current = null;
|
|
109
|
+
}, []);
|
|
110
|
+
return { taskId, events, done, error, submit, close };
|
|
111
|
+
}
|
|
112
|
+
function useMirror(sessionId) {
|
|
113
|
+
const cfg = useAixConfig();
|
|
114
|
+
const [session, setSession] = useState(null);
|
|
115
|
+
const [stream, setStream] = useState(null);
|
|
116
|
+
const [error, setError] = useState(null);
|
|
117
|
+
const ref = useRef(null);
|
|
118
|
+
const mountedRef = useRef(true);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
mountedRef.current = true;
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
setSession(null);
|
|
123
|
+
setStream(null);
|
|
124
|
+
setError(null);
|
|
125
|
+
if (!sessionId) return () => {
|
|
126
|
+
mountedRef.current = false;
|
|
127
|
+
};
|
|
128
|
+
MirrorSession.join(cfg, sessionId).then((ms) => {
|
|
129
|
+
if (cancelled) {
|
|
130
|
+
ms.leave();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
ref.current = ms;
|
|
134
|
+
setSession(ms);
|
|
135
|
+
ms.onStream((s) => {
|
|
136
|
+
if (mountedRef.current) setStream(s);
|
|
137
|
+
});
|
|
138
|
+
ms.onConnectionStateChange(() => {
|
|
139
|
+
if (mountedRef.current) setStream(ms.stream);
|
|
140
|
+
});
|
|
141
|
+
}).catch((e) => {
|
|
142
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
143
|
+
});
|
|
144
|
+
return () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
mountedRef.current = false;
|
|
147
|
+
ref.current?.leave();
|
|
148
|
+
ref.current = null;
|
|
149
|
+
};
|
|
150
|
+
}, [cfg, sessionId]);
|
|
151
|
+
const sendInput = useCallback((msg) => {
|
|
152
|
+
ref.current?.sendInput(msg);
|
|
153
|
+
}, []);
|
|
154
|
+
const setQuality = useCallback((profile) => {
|
|
155
|
+
ref.current?.setQuality(profile);
|
|
156
|
+
}, []);
|
|
157
|
+
return { session, stream, error, sendInput, setQuality };
|
|
158
|
+
}
|
|
159
|
+
var GROUP_MIRROR_MAX = 9;
|
|
160
|
+
function useGroupMirror(ids) {
|
|
161
|
+
const cfg = useAixConfig();
|
|
162
|
+
const [streams, setStreams] = useState(/* @__PURE__ */ new Map());
|
|
163
|
+
const [errors, setErrors] = useState(/* @__PURE__ */ new Map());
|
|
164
|
+
const [connected, setConnected] = useState(0);
|
|
165
|
+
const sessionsRef = useRef(/* @__PURE__ */ new Map());
|
|
166
|
+
const idsKey = ids.slice(0, GROUP_MIRROR_MAX).join("\n");
|
|
167
|
+
const truncated = ids.length > GROUP_MIRROR_MAX;
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
let cancelled = false;
|
|
170
|
+
setStreams(/* @__PURE__ */ new Map());
|
|
171
|
+
setErrors(/* @__PURE__ */ new Map());
|
|
172
|
+
setConnected(0);
|
|
173
|
+
if (truncated) {
|
|
174
|
+
console.warn(`[aixautots] useGroupMirror: ${GROUP_MIRROR_MAX} \u53F0\u8F6F\u4E0A\u9650,\u8D85\u51FA\u90E8\u5206\u5DF2\u622A\u65AD`);
|
|
175
|
+
}
|
|
176
|
+
const take = idsKey ? idsKey.split("\n") : [];
|
|
177
|
+
for (const id of take) {
|
|
178
|
+
MirrorSession.join(cfg, id).then((ms) => {
|
|
179
|
+
if (cancelled) {
|
|
180
|
+
ms.leave();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
sessionsRef.current.set(id, ms);
|
|
184
|
+
setConnected(sessionsRef.current.size);
|
|
185
|
+
ms.onStream((s) => {
|
|
186
|
+
if (!cancelled) setStreams((prev) => new Map(prev).set(id, s));
|
|
187
|
+
});
|
|
188
|
+
}).catch((e) => {
|
|
189
|
+
if (!cancelled) setErrors((prev) => new Map(prev).set(id, e instanceof Error ? e : new Error(String(e))));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return () => {
|
|
193
|
+
cancelled = true;
|
|
194
|
+
for (const ms of sessionsRef.current.values()) ms.leave();
|
|
195
|
+
sessionsRef.current = /* @__PURE__ */ new Map();
|
|
196
|
+
};
|
|
197
|
+
}, [cfg, idsKey, truncated]);
|
|
198
|
+
const broadcastInput = useCallback((msg) => {
|
|
199
|
+
for (const ms of sessionsRef.current.values()) ms.sendInput(msg);
|
|
200
|
+
}, []);
|
|
201
|
+
const setQualityAll = useCallback((profile) => {
|
|
202
|
+
for (const ms of sessionsRef.current.values()) ms.setQuality(profile);
|
|
203
|
+
}, []);
|
|
204
|
+
return { streams, errors, connected, broadcastInput, setQualityAll };
|
|
205
|
+
}
|
|
206
|
+
function useAuth() {
|
|
207
|
+
const c = useClient();
|
|
208
|
+
return {
|
|
209
|
+
login: useCallback((e, p) => c.auth.login(e, p), [c]),
|
|
210
|
+
register: useCallback((e, p) => c.auth.register(e, p), [c]),
|
|
211
|
+
sendCode: useCallback((e) => c.auth.sendCode(e), [c]),
|
|
212
|
+
verifyCode: useCallback((e, code) => c.auth.verifyCode(e, code), [c]),
|
|
213
|
+
setPassword: useCallback((p) => c.auth.setPassword(p), [c]),
|
|
214
|
+
projectUser: useCallback((id) => c.auth.projectUser(id), [c])
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function useProjects() {
|
|
218
|
+
const c = useClient();
|
|
219
|
+
const q = useQuery(() => c.projects.list(), [c]);
|
|
220
|
+
const { setData } = q;
|
|
221
|
+
const create = useCallback(async (input) => {
|
|
222
|
+
const r = await c.projects.create(input);
|
|
223
|
+
setData((prev) => [...prev ?? [], r.project]);
|
|
224
|
+
return r;
|
|
225
|
+
}, [c, setData]);
|
|
226
|
+
const remove = useCallback(async (id) => {
|
|
227
|
+
await c.projects.remove(id);
|
|
228
|
+
setData((prev) => (prev ?? []).filter((p) => p.id !== id));
|
|
229
|
+
}, [c, setData]);
|
|
230
|
+
const rotateSecret = useCallback(async (id) => (await c.projects.rotateSecret(id)).secret, [c]);
|
|
231
|
+
return { ...q, create, remove, rotateSecret };
|
|
232
|
+
}
|
|
233
|
+
function useDevices() {
|
|
234
|
+
const c = useClient();
|
|
235
|
+
return useQuery(() => c.devices.list(), [c]);
|
|
236
|
+
}
|
|
237
|
+
function useDeviceActions(deviceId) {
|
|
238
|
+
const c = useClient();
|
|
239
|
+
return {
|
|
240
|
+
power: useCallback((action, extra) => c.devices.power(deviceId, action, extra), [c, deviceId]),
|
|
241
|
+
control: useCallback((op) => c.devices.control(deviceId, op), [c, deviceId]),
|
|
242
|
+
installApkUrl: useCallback((url) => c.devices.installApkUrl(deviceId, url), [c, deviceId]),
|
|
243
|
+
lease: useCallback(() => c.devices.lease(deviceId), [c, deviceId]),
|
|
244
|
+
release: useCallback(() => c.devices.release(deviceId), [c, deviceId]),
|
|
245
|
+
claim: useCallback(() => c.devices.claim(deviceId), [c, deviceId]),
|
|
246
|
+
screenshot: useCallback(() => c.devices.screenshot(deviceId), [c, deviceId]),
|
|
247
|
+
captureUiTree: useCallback(() => c.devices.captureUiTree(deviceId), [c, deviceId])
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function mapDevice(r) {
|
|
251
|
+
const meta = r.meta ?? {};
|
|
252
|
+
let platform = r.platform ?? meta.platform ?? r.kind ?? "android";
|
|
253
|
+
if (platform === "desktop") {
|
|
254
|
+
const os = String(meta.os ?? "");
|
|
255
|
+
if (os === "windows" || os === "macos") platform = os;
|
|
256
|
+
}
|
|
257
|
+
const online = r.online === true;
|
|
258
|
+
const provisioned = meta.provisioned === true;
|
|
259
|
+
const createdMs = r.created_at ? Date.parse(r.created_at) : 0;
|
|
260
|
+
const booting = !online && provisioned && createdMs > 0 && Date.now() - createdMs < 12e4;
|
|
261
|
+
return {
|
|
262
|
+
id: r.device_id,
|
|
263
|
+
name: r.name || meta.name || r.external_id,
|
|
264
|
+
platform,
|
|
265
|
+
status: online ? "connected" : booting ? "connecting" : "disconnected",
|
|
266
|
+
udid: r.external_id,
|
|
267
|
+
online,
|
|
268
|
+
meta,
|
|
269
|
+
tags: r.tags ?? [],
|
|
270
|
+
note: r.note ?? "",
|
|
271
|
+
nodeId: r.node_id ?? null,
|
|
272
|
+
nodeName: r.node_name ?? null,
|
|
273
|
+
transports: r.transports,
|
|
274
|
+
createdAt: createdMs
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
var DevicesContext = createContext(null);
|
|
278
|
+
function DevicesProvider({ children, autoFetch = true }) {
|
|
279
|
+
const client = useClient();
|
|
280
|
+
const [devices, setDevices] = useState([]);
|
|
281
|
+
const [selectedDeviceId, setSelectedDeviceId] = useState(null);
|
|
282
|
+
const [loading, setLoading] = useState(false);
|
|
283
|
+
const [error, setError] = useState(null);
|
|
284
|
+
const lastSoonRef = useRef(0);
|
|
285
|
+
const fetchDevices = useCallback(
|
|
286
|
+
async (opts) => {
|
|
287
|
+
const silent = opts?.silent === true;
|
|
288
|
+
if (!silent) {
|
|
289
|
+
setLoading(true);
|
|
290
|
+
setError(null);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const rows = await client.devices.list();
|
|
294
|
+
setDevices(normalizeDevices(rows).map(mapDevice));
|
|
295
|
+
setLoading(false);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (silent) setLoading(false);
|
|
298
|
+
else {
|
|
299
|
+
setError(String(err));
|
|
300
|
+
setLoading(false);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
[client]
|
|
305
|
+
);
|
|
306
|
+
const refreshDevicesSoon = useCallback(() => {
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
if (now - lastSoonRef.current < 5e3) return;
|
|
309
|
+
lastSoonRef.current = now;
|
|
310
|
+
void fetchDevices({ silent: true });
|
|
311
|
+
}, [fetchDevices]);
|
|
312
|
+
const selectDevice = useCallback((id) => setSelectedDeviceId(id), []);
|
|
313
|
+
const connectDevice = useCallback(
|
|
314
|
+
async (_platform) => {
|
|
315
|
+
await fetchDevices();
|
|
316
|
+
},
|
|
317
|
+
[fetchDevices]
|
|
318
|
+
);
|
|
319
|
+
const disconnectDevice = useCallback(async (id) => {
|
|
320
|
+
setDevices((prev) => prev.filter((d) => d.id !== id));
|
|
321
|
+
setSelectedDeviceId((sel) => sel === id ? null : sel);
|
|
322
|
+
}, []);
|
|
323
|
+
const provisionDevice = useCallback(
|
|
324
|
+
async (spec) => {
|
|
325
|
+
const res = await client.devices.provision(spec);
|
|
326
|
+
await fetchDevices();
|
|
327
|
+
return res;
|
|
328
|
+
},
|
|
329
|
+
[client, fetchDevices]
|
|
330
|
+
);
|
|
331
|
+
const discoverDevices = useCallback(
|
|
332
|
+
async (nodeId) => {
|
|
333
|
+
const { devices: ds } = await client.nodes.discoverDevices(nodeId);
|
|
334
|
+
return ds;
|
|
335
|
+
},
|
|
336
|
+
[client]
|
|
337
|
+
);
|
|
338
|
+
const enrollDiscovered = useCallback(
|
|
339
|
+
async (d) => {
|
|
340
|
+
if (d.status === "unregistered") {
|
|
341
|
+
await client.devices.register({
|
|
342
|
+
external_id: d.external_id,
|
|
343
|
+
kind: d.platform ?? void 0,
|
|
344
|
+
name: d.model ?? d.external_id
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
await client.devices.claim(d.external_id);
|
|
348
|
+
}
|
|
349
|
+
await fetchDevices();
|
|
350
|
+
},
|
|
351
|
+
[client, fetchDevices]
|
|
352
|
+
);
|
|
353
|
+
const updateTags = useCallback(
|
|
354
|
+
async (deviceId, tags) => {
|
|
355
|
+
await client.devices.update(deviceId, { tags });
|
|
356
|
+
await fetchDevices();
|
|
357
|
+
},
|
|
358
|
+
[client, fetchDevices]
|
|
359
|
+
);
|
|
360
|
+
const updateNote = useCallback(
|
|
361
|
+
async (deviceId, note) => {
|
|
362
|
+
await client.devices.update(deviceId, { note });
|
|
363
|
+
await fetchDevices();
|
|
364
|
+
},
|
|
365
|
+
[client, fetchDevices]
|
|
366
|
+
);
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (autoFetch) void fetchDevices();
|
|
369
|
+
}, [autoFetch]);
|
|
370
|
+
const value = useMemo(
|
|
371
|
+
() => ({
|
|
372
|
+
devices,
|
|
373
|
+
selectedDeviceId,
|
|
374
|
+
loading,
|
|
375
|
+
error,
|
|
376
|
+
fetchDevices,
|
|
377
|
+
refreshDevicesSoon,
|
|
378
|
+
selectDevice,
|
|
379
|
+
connectDevice,
|
|
380
|
+
disconnectDevice,
|
|
381
|
+
provisionDevice,
|
|
382
|
+
discoverDevices,
|
|
383
|
+
enrollDiscovered,
|
|
384
|
+
updateTags,
|
|
385
|
+
updateNote
|
|
386
|
+
}),
|
|
387
|
+
[
|
|
388
|
+
devices,
|
|
389
|
+
selectedDeviceId,
|
|
390
|
+
loading,
|
|
391
|
+
error,
|
|
392
|
+
fetchDevices,
|
|
393
|
+
refreshDevicesSoon,
|
|
394
|
+
selectDevice,
|
|
395
|
+
connectDevice,
|
|
396
|
+
disconnectDevice,
|
|
397
|
+
provisionDevice,
|
|
398
|
+
discoverDevices,
|
|
399
|
+
enrollDiscovered,
|
|
400
|
+
updateTags,
|
|
401
|
+
updateNote
|
|
402
|
+
]
|
|
403
|
+
);
|
|
404
|
+
return createElement(DevicesContext.Provider, { value }, children);
|
|
405
|
+
}
|
|
406
|
+
function useDevicesView() {
|
|
407
|
+
const ctx = useContext(DevicesContext);
|
|
408
|
+
if (!ctx) throw new Error("useDevicesView() must be used within <DevicesProvider>");
|
|
409
|
+
return ctx;
|
|
410
|
+
}
|
|
411
|
+
var NodesContext = createContext(null);
|
|
412
|
+
function NodesProvider({ children, autoFetch = true }) {
|
|
413
|
+
const client = useClient();
|
|
414
|
+
const [nodes, setNodes] = useState([]);
|
|
415
|
+
const [loading, setLoading] = useState(false);
|
|
416
|
+
const [error, setError] = useState(null);
|
|
417
|
+
const fetchNodes = useCallback(async () => {
|
|
418
|
+
setLoading(true);
|
|
419
|
+
setError(null);
|
|
420
|
+
try {
|
|
421
|
+
setNodes(await client.nodes.list());
|
|
422
|
+
setLoading(false);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
setError(String(err));
|
|
425
|
+
setLoading(false);
|
|
426
|
+
}
|
|
427
|
+
}, [client]);
|
|
428
|
+
const refreshNodes = useCallback(async () => {
|
|
429
|
+
try {
|
|
430
|
+
setNodes(await client.nodes.list());
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}, [client]);
|
|
434
|
+
const createNode = useCallback(
|
|
435
|
+
async (name, scope, region) => {
|
|
436
|
+
const res = await client.nodes.register({ name, scope, region: region || null });
|
|
437
|
+
await fetchNodes();
|
|
438
|
+
return res;
|
|
439
|
+
},
|
|
440
|
+
[client, fetchNodes]
|
|
441
|
+
);
|
|
442
|
+
const updateNode = useCallback(
|
|
443
|
+
async (id, patch) => {
|
|
444
|
+
await client.nodes.update(id, patch);
|
|
445
|
+
await fetchNodes();
|
|
446
|
+
},
|
|
447
|
+
[client, fetchNodes]
|
|
448
|
+
);
|
|
449
|
+
const rotateToken = useCallback(
|
|
450
|
+
(id) => client.nodes.rotateToken(id),
|
|
451
|
+
[client]
|
|
452
|
+
);
|
|
453
|
+
const removeNode = useCallback(
|
|
454
|
+
async (id) => {
|
|
455
|
+
await client.nodes.remove(id);
|
|
456
|
+
await fetchNodes();
|
|
457
|
+
},
|
|
458
|
+
[client, fetchNodes]
|
|
459
|
+
);
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (autoFetch) void fetchNodes();
|
|
462
|
+
}, [autoFetch]);
|
|
463
|
+
const value = useMemo(
|
|
464
|
+
() => ({
|
|
465
|
+
nodes,
|
|
466
|
+
loading,
|
|
467
|
+
error,
|
|
468
|
+
fetchNodes,
|
|
469
|
+
refreshNodes,
|
|
470
|
+
createNode,
|
|
471
|
+
updateNode,
|
|
472
|
+
rotateToken,
|
|
473
|
+
removeNode
|
|
474
|
+
}),
|
|
475
|
+
[nodes, loading, error, fetchNodes, refreshNodes, createNode, updateNode, rotateToken, removeNode]
|
|
476
|
+
);
|
|
477
|
+
return createElement(NodesContext.Provider, { value }, children);
|
|
478
|
+
}
|
|
479
|
+
function useNodesView() {
|
|
480
|
+
const ctx = useContext(NodesContext);
|
|
481
|
+
if (!ctx) throw new Error("useNodesView() must be used within <NodesProvider>");
|
|
482
|
+
return ctx;
|
|
483
|
+
}
|
|
484
|
+
function errMsg(reason) {
|
|
485
|
+
if (reason instanceof Error) return reason.message;
|
|
486
|
+
return String(reason);
|
|
487
|
+
}
|
|
488
|
+
var ShortcutsContext = createContext(null);
|
|
489
|
+
function ShortcutsProvider({ children }) {
|
|
490
|
+
const client = useClient();
|
|
491
|
+
const [shortcuts, setShortcuts] = useState([]);
|
|
492
|
+
const [loading, setLoading] = useState(false);
|
|
493
|
+
const [error, setError] = useState(null);
|
|
494
|
+
const fetch = useCallback(
|
|
495
|
+
async (platform) => {
|
|
496
|
+
setLoading(true);
|
|
497
|
+
setError(null);
|
|
498
|
+
try {
|
|
499
|
+
const rows = await client.shortcuts.list(platform);
|
|
500
|
+
setShortcuts(rows);
|
|
501
|
+
setLoading(false);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
setError(String(e));
|
|
504
|
+
setLoading(false);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
[client]
|
|
508
|
+
);
|
|
509
|
+
const create = useCallback(
|
|
510
|
+
async (body) => {
|
|
511
|
+
try {
|
|
512
|
+
const row = await client.shortcuts.create(body);
|
|
513
|
+
setShortcuts((prev) => [...prev, row]);
|
|
514
|
+
return row;
|
|
515
|
+
} catch (e) {
|
|
516
|
+
setError(String(e));
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
[client]
|
|
521
|
+
);
|
|
522
|
+
const update = useCallback(
|
|
523
|
+
async (id, body) => {
|
|
524
|
+
try {
|
|
525
|
+
const row = await client.shortcuts.update(id, body);
|
|
526
|
+
setShortcuts((prev) => prev.map((x) => x.id === id ? row : x));
|
|
527
|
+
return row;
|
|
528
|
+
} catch (e) {
|
|
529
|
+
setError(String(e));
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
[client]
|
|
534
|
+
);
|
|
535
|
+
const remove = useCallback(
|
|
536
|
+
async (id) => {
|
|
537
|
+
try {
|
|
538
|
+
await client.shortcuts.remove(id);
|
|
539
|
+
setShortcuts((prev) => prev.filter((x) => x.id !== id));
|
|
540
|
+
} catch (e) {
|
|
541
|
+
setError(String(e));
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
[client]
|
|
545
|
+
);
|
|
546
|
+
const runShortcut = useCallback(
|
|
547
|
+
async (shortcut, values, deviceIds) => {
|
|
548
|
+
if (deviceIds.length === 0) return null;
|
|
549
|
+
const { run_id } = await client.fleet.start({
|
|
550
|
+
script: {
|
|
551
|
+
name: shortcut.name,
|
|
552
|
+
platform: shortcut.platform === "ios" ? "iOS" : "Android",
|
|
553
|
+
language: shortcut.language,
|
|
554
|
+
code: shortcut.body,
|
|
555
|
+
variables: values
|
|
556
|
+
},
|
|
557
|
+
mode: "Broadcast",
|
|
558
|
+
device_ids: deviceIds
|
|
559
|
+
});
|
|
560
|
+
const detail = await client.fleet.waitForRun(run_id);
|
|
561
|
+
return { run_id, detail, timedOut: !isFleetRunTerminal(detail) };
|
|
562
|
+
},
|
|
563
|
+
[client]
|
|
564
|
+
);
|
|
565
|
+
const runPower = useCallback(
|
|
566
|
+
async (shortcut, deviceIds) => {
|
|
567
|
+
const settled = await Promise.allSettled(
|
|
568
|
+
deviceIds.map((id) => client.devices.power(id, shortcut.body))
|
|
569
|
+
);
|
|
570
|
+
return settled.map((s, i) => {
|
|
571
|
+
const device_id = deviceIds[i] ?? "";
|
|
572
|
+
return s.status === "fulfilled" ? { device_id, ok: true, result: s.value } : { device_id, ok: false, error: errMsg(s.reason) };
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
[client]
|
|
576
|
+
);
|
|
577
|
+
const value = useMemo(
|
|
578
|
+
() => ({ shortcuts, loading, error, fetch, create, update, remove, runShortcut, runPower }),
|
|
579
|
+
[shortcuts, loading, error, fetch, create, update, remove, runShortcut, runPower]
|
|
580
|
+
);
|
|
581
|
+
return createElement(ShortcutsContext.Provider, { value }, children);
|
|
582
|
+
}
|
|
583
|
+
function useShortcutsView() {
|
|
584
|
+
const ctx = useContext(ShortcutsContext);
|
|
585
|
+
if (!ctx) throw new Error("useShortcutsView() must be used within <ShortcutsProvider>");
|
|
586
|
+
return ctx;
|
|
587
|
+
}
|
|
588
|
+
function useScripts() {
|
|
589
|
+
const c = useClient();
|
|
590
|
+
const q = useQuery(() => c.scripts.list(), [c]);
|
|
591
|
+
const { setData, refetch } = q;
|
|
592
|
+
const get = useCallback((id) => c.scripts.get(id), [c]);
|
|
593
|
+
const save = useCallback(async (script) => {
|
|
594
|
+
const r = await c.scripts.save(script);
|
|
595
|
+
await refetch();
|
|
596
|
+
return r;
|
|
597
|
+
}, [c, refetch]);
|
|
598
|
+
const remove = useCallback(async (id) => {
|
|
599
|
+
await c.scripts.remove(id);
|
|
600
|
+
setData((prev) => (prev ?? []).filter((s) => s.id !== id));
|
|
601
|
+
}, [c, setData]);
|
|
602
|
+
const duplicate = useCallback(async (id) => {
|
|
603
|
+
const r = await c.scripts.duplicate(id);
|
|
604
|
+
setData((prev) => [...prev ?? [], r]);
|
|
605
|
+
return r;
|
|
606
|
+
}, [c, setData]);
|
|
607
|
+
const setVisibility = useCallback(async (id, visibility) => {
|
|
608
|
+
const r = await c.scripts.setVisibility(id, visibility);
|
|
609
|
+
setData((prev) => (prev ?? []).map((s) => s.id === id ? { ...s, ...r } : s));
|
|
610
|
+
return r;
|
|
611
|
+
}, [c, setData]);
|
|
612
|
+
return { ...q, get, save, remove, duplicate, setVisibility };
|
|
613
|
+
}
|
|
614
|
+
function useClusters() {
|
|
615
|
+
const c = useClient();
|
|
616
|
+
const q = useQuery(() => c.clusters.list(), [c]);
|
|
617
|
+
const { setData } = q;
|
|
618
|
+
const create = useCallback(async (input) => {
|
|
619
|
+
const cl = await c.clusters.create(input);
|
|
620
|
+
setData((prev) => [...prev ?? [], cl]);
|
|
621
|
+
return cl;
|
|
622
|
+
}, [c, setData]);
|
|
623
|
+
const remove = useCallback(async (id) => {
|
|
624
|
+
await c.clusters.remove(id);
|
|
625
|
+
setData((prev) => (prev ?? []).filter((x) => x.id !== id));
|
|
626
|
+
}, [c, setData]);
|
|
627
|
+
const setDevices = useCallback(async (id, deviceIds) => {
|
|
628
|
+
const cl = await c.clusters.setDevices(id, deviceIds);
|
|
629
|
+
setData((prev) => (prev ?? []).map((x) => x.id === id ? cl : x));
|
|
630
|
+
return cl;
|
|
631
|
+
}, [c, setData]);
|
|
632
|
+
const run = useCallback((id, flow) => c.clusters.run(id, flow), [c]);
|
|
633
|
+
return { ...q, create, remove, setDevices, run };
|
|
634
|
+
}
|
|
635
|
+
var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
|
|
636
|
+
function useFleetRun(pollMs = 1e3) {
|
|
637
|
+
const c = useClient();
|
|
638
|
+
const [runId, setRunId] = useState(null);
|
|
639
|
+
const [status, setStatus] = useState("idle");
|
|
640
|
+
const [progress, setProgress] = useState(0);
|
|
641
|
+
const [detail, setDetail] = useState(null);
|
|
642
|
+
const [error, setError] = useState(null);
|
|
643
|
+
const timer = useRef(null);
|
|
644
|
+
const mounted = useRef(true);
|
|
645
|
+
const stopTimer = useCallback(() => {
|
|
646
|
+
if (timer.current) {
|
|
647
|
+
clearInterval(timer.current);
|
|
648
|
+
timer.current = null;
|
|
649
|
+
}
|
|
650
|
+
}, []);
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
mounted.current = true;
|
|
653
|
+
return () => {
|
|
654
|
+
mounted.current = false;
|
|
655
|
+
stopTimer();
|
|
656
|
+
};
|
|
657
|
+
}, [stopTimer]);
|
|
658
|
+
const start = useCallback(async (script, deviceIds, mode = "Broadcast") => {
|
|
659
|
+
stopTimer();
|
|
660
|
+
setStatus("running");
|
|
661
|
+
setProgress(0);
|
|
662
|
+
setDetail(null);
|
|
663
|
+
setError(null);
|
|
664
|
+
setRunId(null);
|
|
665
|
+
try {
|
|
666
|
+
const { run_id } = await c.fleet.start({ script, mode, device_ids: deviceIds });
|
|
667
|
+
setRunId(run_id);
|
|
668
|
+
timer.current = setInterval(async () => {
|
|
669
|
+
try {
|
|
670
|
+
const d = await c.fleet.get(run_id);
|
|
671
|
+
if (!mounted.current) return;
|
|
672
|
+
setDetail(d);
|
|
673
|
+
const rows = d.devices ?? [];
|
|
674
|
+
const total = rows.length;
|
|
675
|
+
const done = rows.filter((r) => TERMINAL.has(r.status)).length;
|
|
676
|
+
if (total > 0 && done === total) {
|
|
677
|
+
stopTimer();
|
|
678
|
+
const anyFailed = rows.some((r) => r.status === "failed" || r.status === "error");
|
|
679
|
+
setStatus(anyFailed ? "failed" : "completed");
|
|
680
|
+
setProgress(100);
|
|
681
|
+
} else {
|
|
682
|
+
setProgress(total > 0 ? Math.round(done / total * 100) : 0);
|
|
683
|
+
}
|
|
684
|
+
} catch (e) {
|
|
685
|
+
stopTimer();
|
|
686
|
+
if (mounted.current) {
|
|
687
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
688
|
+
setStatus("failed");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}, pollMs);
|
|
692
|
+
return run_id;
|
|
693
|
+
} catch (e) {
|
|
694
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
695
|
+
if (mounted.current) {
|
|
696
|
+
setError(err);
|
|
697
|
+
setStatus("failed");
|
|
698
|
+
}
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
}, [c, pollMs, stopTimer]);
|
|
702
|
+
const cancel = useCallback(async () => {
|
|
703
|
+
stopTimer();
|
|
704
|
+
if (runId) await c.fleet.cancel(runId);
|
|
705
|
+
if (mounted.current) setStatus("completed");
|
|
706
|
+
}, [c, runId, stopTimer]);
|
|
707
|
+
const pause = useCallback(async () => {
|
|
708
|
+
if (!runId) return;
|
|
709
|
+
await c.fleet.pause(runId);
|
|
710
|
+
if (mounted.current) setStatus("paused");
|
|
711
|
+
}, [c, runId]);
|
|
712
|
+
const resume = useCallback(async () => {
|
|
713
|
+
if (!runId) return;
|
|
714
|
+
await c.fleet.resume(runId);
|
|
715
|
+
if (mounted.current) setStatus("running");
|
|
716
|
+
}, [c, runId]);
|
|
717
|
+
const reset = useCallback(() => {
|
|
718
|
+
stopTimer();
|
|
719
|
+
setRunId(null);
|
|
720
|
+
setStatus("idle");
|
|
721
|
+
setProgress(0);
|
|
722
|
+
setDetail(null);
|
|
723
|
+
setError(null);
|
|
724
|
+
}, [stopTimer]);
|
|
725
|
+
return { runId, status, progress, detail, error, start, cancel, pause, resume, reset };
|
|
726
|
+
}
|
|
727
|
+
var TERMINAL_DEVICE_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
|
|
728
|
+
function levelForStatus(status) {
|
|
729
|
+
if (status === "failed" || status === "error") return "error";
|
|
730
|
+
if (status === "cancelled" || status === "paused") return "warn";
|
|
731
|
+
if (status === "done" || status === "completed") return "info";
|
|
732
|
+
return "debug";
|
|
733
|
+
}
|
|
734
|
+
function entry(level, msg, device, ts) {
|
|
735
|
+
return { ts: ts ?? (/* @__PURE__ */ new Date()).toISOString(), level, msg, device };
|
|
736
|
+
}
|
|
737
|
+
function useScriptRun(pollMs = 1e3) {
|
|
738
|
+
const c = useClient();
|
|
739
|
+
const [runId, setRunId] = useState(null);
|
|
740
|
+
const [status, setStatus] = useState("idle");
|
|
741
|
+
const [progress, setProgress] = useState(0);
|
|
742
|
+
const [steps] = useState([]);
|
|
743
|
+
const [logs, setLogs] = useState([]);
|
|
744
|
+
const [error, setError] = useState(null);
|
|
745
|
+
const timer = useRef(null);
|
|
746
|
+
const runIdRef = useRef(null);
|
|
747
|
+
const deviceStatus = useRef({});
|
|
748
|
+
const mounted = useRef(true);
|
|
749
|
+
const stopTimer = useCallback(() => {
|
|
750
|
+
if (timer.current) {
|
|
751
|
+
clearInterval(timer.current);
|
|
752
|
+
timer.current = null;
|
|
753
|
+
}
|
|
754
|
+
}, []);
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
mounted.current = true;
|
|
757
|
+
return () => {
|
|
758
|
+
mounted.current = false;
|
|
759
|
+
stopTimer();
|
|
760
|
+
};
|
|
761
|
+
}, [stopTimer]);
|
|
762
|
+
const appendLogs = useCallback((lines) => {
|
|
763
|
+
if (lines.length && mounted.current) setLogs((prev) => [...prev, ...lines]);
|
|
764
|
+
}, []);
|
|
765
|
+
const startWithBody = useCallback(
|
|
766
|
+
async (scriptBody, deviceIds) => {
|
|
767
|
+
stopTimer();
|
|
768
|
+
deviceStatus.current = {};
|
|
769
|
+
runIdRef.current = null;
|
|
770
|
+
if (!mounted.current) return;
|
|
771
|
+
setRunId(null);
|
|
772
|
+
setStatus("running");
|
|
773
|
+
setProgress(0);
|
|
774
|
+
setError(null);
|
|
775
|
+
setLogs([
|
|
776
|
+
entry(
|
|
777
|
+
"system",
|
|
778
|
+
`Starting run on ${deviceIds.length} device${deviceIds.length === 1 ? "" : "s"}\u2026`
|
|
779
|
+
)
|
|
780
|
+
]);
|
|
781
|
+
try {
|
|
782
|
+
const { run_id } = await c.fleet.start({
|
|
783
|
+
script: scriptBody,
|
|
784
|
+
mode: "Broadcast",
|
|
785
|
+
device_ids: deviceIds
|
|
786
|
+
});
|
|
787
|
+
if (!mounted.current) return;
|
|
788
|
+
runIdRef.current = run_id;
|
|
789
|
+
setRunId(run_id);
|
|
790
|
+
appendLogs([entry("system", `Run submitted \xB7 ${run_id}`)]);
|
|
791
|
+
timer.current = setInterval(async () => {
|
|
792
|
+
try {
|
|
793
|
+
const detail = await c.fleet.get(run_id);
|
|
794
|
+
if (!mounted.current) return;
|
|
795
|
+
const rows = detail.devices ?? [];
|
|
796
|
+
const total = rows.length;
|
|
797
|
+
const done = rows.filter((r) => TERMINAL_DEVICE_STATUSES.has(r.status)).length;
|
|
798
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
799
|
+
const prev = deviceStatus.current;
|
|
800
|
+
const nextStatus = { ...prev };
|
|
801
|
+
const newLines = [];
|
|
802
|
+
for (const r of rows) {
|
|
803
|
+
if (prev[r.device_id] !== r.status) {
|
|
804
|
+
nextStatus[r.device_id] = r.status;
|
|
805
|
+
const suffix = r.last_error ? ` \u2014 ${r.last_error}` : "";
|
|
806
|
+
newLines.push(
|
|
807
|
+
entry(levelForStatus(r.status), `${r.status}${suffix}`, r.device_id)
|
|
808
|
+
);
|
|
809
|
+
if (TERMINAL_DEVICE_STATUSES.has(r.status) && r.output) {
|
|
810
|
+
for (const line of r.output.split("\n")) {
|
|
811
|
+
if (line.trim()) newLines.push(entry("info", line, r.device_id));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
deviceStatus.current = nextStatus;
|
|
817
|
+
if (total > 0 && done === total) {
|
|
818
|
+
const anyFailed = rows.some(
|
|
819
|
+
(r) => r.status === "failed" || r.status === "error"
|
|
820
|
+
);
|
|
821
|
+
stopTimer();
|
|
822
|
+
newLines.push(
|
|
823
|
+
entry(
|
|
824
|
+
anyFailed ? "error" : "system",
|
|
825
|
+
anyFailed ? "Run failed." : "Run completed."
|
|
826
|
+
)
|
|
827
|
+
);
|
|
828
|
+
setStatus(anyFailed ? "failed" : "completed");
|
|
829
|
+
setProgress(100);
|
|
830
|
+
} else {
|
|
831
|
+
setProgress(pct);
|
|
832
|
+
}
|
|
833
|
+
appendLogs(newLines);
|
|
834
|
+
} catch (err) {
|
|
835
|
+
stopTimer();
|
|
836
|
+
if (!mounted.current) return;
|
|
837
|
+
setError(String(err));
|
|
838
|
+
setStatus("failed");
|
|
839
|
+
appendLogs([entry("error", `Poll failed: ${String(err)}`)]);
|
|
840
|
+
}
|
|
841
|
+
}, pollMs);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
if (!mounted.current) return;
|
|
844
|
+
setError(String(err));
|
|
845
|
+
setStatus("failed");
|
|
846
|
+
appendLogs([entry("error", `Failed to start run: ${String(err)}`)]);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
[c, pollMs, stopTimer, appendLogs]
|
|
850
|
+
);
|
|
851
|
+
const startRun = useCallback(
|
|
852
|
+
async (scriptId, deviceIds) => {
|
|
853
|
+
if (deviceIds.length === 0) {
|
|
854
|
+
setError("No device selected");
|
|
855
|
+
setStatus("failed");
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const flow = await c.scripts.get(scriptId);
|
|
859
|
+
await startWithBody(flow, deviceIds);
|
|
860
|
+
},
|
|
861
|
+
[c, startWithBody]
|
|
862
|
+
);
|
|
863
|
+
const runCode = useCallback(
|
|
864
|
+
async (code, language, variables, deviceIds, platform = "Android") => {
|
|
865
|
+
if (deviceIds.length === 0) {
|
|
866
|
+
setError("No device selected");
|
|
867
|
+
setStatus("failed");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
await startWithBody(
|
|
871
|
+
{ name: "(selection)", platform, language, code, variables },
|
|
872
|
+
deviceIds
|
|
873
|
+
);
|
|
874
|
+
},
|
|
875
|
+
[startWithBody]
|
|
876
|
+
);
|
|
877
|
+
const stopRun = useCallback(async () => {
|
|
878
|
+
stopTimer();
|
|
879
|
+
const id = runIdRef.current;
|
|
880
|
+
try {
|
|
881
|
+
if (id) await c.fleet.cancel(id);
|
|
882
|
+
if (mounted.current) {
|
|
883
|
+
setStatus("completed");
|
|
884
|
+
appendLogs([entry("warn", "Run stopped by user.")]);
|
|
885
|
+
}
|
|
886
|
+
} catch (err) {
|
|
887
|
+
if (mounted.current) setError(String(err));
|
|
888
|
+
}
|
|
889
|
+
}, [c, stopTimer, appendLogs]);
|
|
890
|
+
const pauseRun = useCallback(async () => {
|
|
891
|
+
const id = runIdRef.current;
|
|
892
|
+
if (!id) return;
|
|
893
|
+
try {
|
|
894
|
+
await c.fleet.pause(id);
|
|
895
|
+
if (mounted.current) {
|
|
896
|
+
setStatus("paused");
|
|
897
|
+
appendLogs([entry("warn", "Run paused.")]);
|
|
898
|
+
}
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (mounted.current) setError(String(err));
|
|
901
|
+
}
|
|
902
|
+
}, [c, appendLogs]);
|
|
903
|
+
const resumeRun = useCallback(async () => {
|
|
904
|
+
const id = runIdRef.current;
|
|
905
|
+
if (!id) return;
|
|
906
|
+
try {
|
|
907
|
+
await c.fleet.resume(id);
|
|
908
|
+
if (mounted.current) {
|
|
909
|
+
setStatus("running");
|
|
910
|
+
appendLogs([entry("system", "Run resumed.")]);
|
|
911
|
+
}
|
|
912
|
+
} catch (err) {
|
|
913
|
+
if (mounted.current) setError(String(err));
|
|
914
|
+
}
|
|
915
|
+
}, [c, appendLogs]);
|
|
916
|
+
const clearLogs = useCallback(() => {
|
|
917
|
+
if (mounted.current) setLogs([]);
|
|
918
|
+
}, []);
|
|
919
|
+
const reset = useCallback(() => {
|
|
920
|
+
stopTimer();
|
|
921
|
+
runIdRef.current = null;
|
|
922
|
+
deviceStatus.current = {};
|
|
923
|
+
if (!mounted.current) return;
|
|
924
|
+
setRunId(null);
|
|
925
|
+
setStatus("idle");
|
|
926
|
+
setProgress(0);
|
|
927
|
+
setLogs([]);
|
|
928
|
+
setError(null);
|
|
929
|
+
}, [stopTimer]);
|
|
930
|
+
return {
|
|
931
|
+
runId,
|
|
932
|
+
status,
|
|
933
|
+
progress,
|
|
934
|
+
steps,
|
|
935
|
+
logs,
|
|
936
|
+
error,
|
|
937
|
+
startRun,
|
|
938
|
+
runCode,
|
|
939
|
+
stopRun,
|
|
940
|
+
pauseRun,
|
|
941
|
+
resumeRun,
|
|
942
|
+
clearLogs,
|
|
943
|
+
reset
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function log(level, msg, device) {
|
|
947
|
+
return { ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, device };
|
|
948
|
+
}
|
|
949
|
+
function terminal(status) {
|
|
950
|
+
return status === "succeeded" || status === "failed" || status === "cancelled" || status === "expired";
|
|
951
|
+
}
|
|
952
|
+
function eventKey(event) {
|
|
953
|
+
if (event.cursor === void 0) return void 0;
|
|
954
|
+
return [
|
|
955
|
+
event.runId,
|
|
956
|
+
event.cursor,
|
|
957
|
+
event.targetId ?? event.deviceId ?? "",
|
|
958
|
+
event.attemptId ?? "",
|
|
959
|
+
event.stream
|
|
960
|
+
].join("\\u001f");
|
|
961
|
+
}
|
|
962
|
+
function mapTarget(device, target) {
|
|
963
|
+
return {
|
|
964
|
+
...device,
|
|
965
|
+
targetId: target?.target_id,
|
|
966
|
+
attemptId: target?.attempt_id ?? void 0,
|
|
967
|
+
status: target?.status ?? "queued",
|
|
968
|
+
error: target?.error ?? void 0,
|
|
969
|
+
blocker: target?.blocker ?? null,
|
|
970
|
+
evidence: target?.evidence ?? []
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function useTaskAgentRun(runner) {
|
|
974
|
+
const [runId, setRunId] = useState(null);
|
|
975
|
+
const [agent, setAgent] = useState("claude");
|
|
976
|
+
const [concurrency, setConcurrency] = useState(1);
|
|
977
|
+
const [devices, setDevices] = useState([]);
|
|
978
|
+
const [logs, setLogs] = useState([]);
|
|
979
|
+
const [error, setError] = useState(null);
|
|
980
|
+
const runIdRef = useRef(null);
|
|
981
|
+
const eventKeysRef = useRef(/* @__PURE__ */ new Set());
|
|
982
|
+
const pendingEventsRef = useRef(/* @__PURE__ */ new Map());
|
|
983
|
+
const mountedRef = useRef(true);
|
|
984
|
+
const applyEvent = useCallback((event) => {
|
|
985
|
+
if (event.stream === "stdout" || event.stream === "stderr") {
|
|
986
|
+
if (event.chunk) {
|
|
987
|
+
setLogs((current) => [...current, log(event.stream === "stderr" ? "error" : "info", event.chunk, event.deviceId)]);
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (event.stream === "evidence" && event.evidence?.length) {
|
|
992
|
+
setDevices((current) => current.map(
|
|
993
|
+
(device) => device.targetId === event.targetId || device.id === event.deviceId ? { ...device, evidence: [...device.evidence, ...event.evidence] } : device
|
|
994
|
+
));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (!event.status) return;
|
|
998
|
+
const level = event.status === "failed" || event.status === "expired" ? "error" : event.status === "cancelled" || event.status === "cancel_requested" || event.status === "blocked" ? "warn" : "debug";
|
|
999
|
+
const message = event.error ?? event.blocker?.message ?? event.status;
|
|
1000
|
+
setDevices((current) => current.map((device) => {
|
|
1001
|
+
const matches = event.targetId && device.targetId === event.targetId || !event.targetId && device.id === event.deviceId;
|
|
1002
|
+
if (!matches) return device;
|
|
1003
|
+
return {
|
|
1004
|
+
...device,
|
|
1005
|
+
attemptId: event.attemptId ?? device.attemptId,
|
|
1006
|
+
status: event.status,
|
|
1007
|
+
error: event.error,
|
|
1008
|
+
blocker: event.blocker ?? (event.blockedReason ? { code: "blocked", message: event.blockedReason } : null),
|
|
1009
|
+
evidence: event.evidence ? [...device.evidence, ...event.evidence] : device.evidence
|
|
1010
|
+
};
|
|
1011
|
+
}));
|
|
1012
|
+
setLogs((current) => [...current, log(level, message, event.deviceId)]);
|
|
1013
|
+
if (event.stream === "done" && event.error) {
|
|
1014
|
+
setError(event.error);
|
|
1015
|
+
setLogs((current) => [...current, log("error", event.error)]);
|
|
1016
|
+
}
|
|
1017
|
+
}, []);
|
|
1018
|
+
const handleEvent = useCallback((event) => {
|
|
1019
|
+
if (!mountedRef.current) return;
|
|
1020
|
+
if (event.runId === runIdRef.current) {
|
|
1021
|
+
const key = eventKey(event);
|
|
1022
|
+
if (key !== void 0) {
|
|
1023
|
+
if (eventKeysRef.current.has(key)) return;
|
|
1024
|
+
eventKeysRef.current.add(key);
|
|
1025
|
+
}
|
|
1026
|
+
applyEvent(event);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (runIdRef.current === null) {
|
|
1030
|
+
const pending = pendingEventsRef.current.get(event.runId) ?? [];
|
|
1031
|
+
pending.push(event);
|
|
1032
|
+
pendingEventsRef.current.set(event.runId, pending);
|
|
1033
|
+
}
|
|
1034
|
+
}, [applyEvent]);
|
|
1035
|
+
useEffect(() => {
|
|
1036
|
+
mountedRef.current = true;
|
|
1037
|
+
if (!runner) return () => {
|
|
1038
|
+
mountedRef.current = false;
|
|
1039
|
+
};
|
|
1040
|
+
const unsubscribe = runner.subscribe(handleEvent);
|
|
1041
|
+
return () => {
|
|
1042
|
+
mountedRef.current = false;
|
|
1043
|
+
unsubscribe();
|
|
1044
|
+
};
|
|
1045
|
+
}, [runner, handleEvent]);
|
|
1046
|
+
useEffect(() => {
|
|
1047
|
+
if (!runner?.defaults) return;
|
|
1048
|
+
void runner.defaults().then((defaults) => {
|
|
1049
|
+
if (!mountedRef.current) return;
|
|
1050
|
+
if (defaults.agent) setAgent(defaults.agent);
|
|
1051
|
+
if (defaults.concurrency && defaults.concurrency > 0) setConcurrency(defaults.concurrency);
|
|
1052
|
+
});
|
|
1053
|
+
}, [runner]);
|
|
1054
|
+
const start = useCallback(
|
|
1055
|
+
async (input) => {
|
|
1056
|
+
if (!runner?.available || input.devices.length === 0) return;
|
|
1057
|
+
const selected = input.devices.map((device) => mapTarget(device));
|
|
1058
|
+
setAgent(input.agent);
|
|
1059
|
+
setConcurrency(input.concurrency);
|
|
1060
|
+
setDevices(selected);
|
|
1061
|
+
setLogs([log("system", `Starting ${input.agent} task on ${selected.length} device${selected.length === 1 ? "" : "s"}\u2026`)]);
|
|
1062
|
+
setError(null);
|
|
1063
|
+
runIdRef.current = null;
|
|
1064
|
+
eventKeysRef.current.clear();
|
|
1065
|
+
pendingEventsRef.current.clear();
|
|
1066
|
+
setRunId(null);
|
|
1067
|
+
try {
|
|
1068
|
+
const started = await runner.start({
|
|
1069
|
+
agent: input.agent,
|
|
1070
|
+
deviceIds: selected.map((device) => device.id),
|
|
1071
|
+
prompt: input.prompt,
|
|
1072
|
+
concurrency: input.concurrency
|
|
1073
|
+
});
|
|
1074
|
+
if (!mountedRef.current) return;
|
|
1075
|
+
const targetsByDevice = new Map(started.targets.map((target) => [target.device_id, target]));
|
|
1076
|
+
setDevices(input.devices.map((device) => mapTarget(device, targetsByDevice.get(device.id))));
|
|
1077
|
+
runIdRef.current = started.runId;
|
|
1078
|
+
setRunId(started.runId);
|
|
1079
|
+
const pending = pendingEventsRef.current.get(started.runId) ?? [];
|
|
1080
|
+
pendingEventsRef.current.delete(started.runId);
|
|
1081
|
+
for (const event of pending) handleEvent(event);
|
|
1082
|
+
setLogs((current) => [...current, log("system", `Task started \xB7 ${started.runId}`)]);
|
|
1083
|
+
} catch (reason) {
|
|
1084
|
+
if (!mountedRef.current) return;
|
|
1085
|
+
const message = String(reason);
|
|
1086
|
+
setError(message);
|
|
1087
|
+
setDevices((current) => current.map((device) => ({ ...device, status: "failed", error: message })));
|
|
1088
|
+
setLogs((current) => [...current, log("error", `Failed to start task: ${message}`)]);
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
[runner, handleEvent]
|
|
1092
|
+
);
|
|
1093
|
+
const resume = useCallback(
|
|
1094
|
+
async (id, inputDevices) => {
|
|
1095
|
+
if (!runner?.available || !runner.resume || inputDevices.length === 0) return;
|
|
1096
|
+
const selected = inputDevices.map((device) => mapTarget(device));
|
|
1097
|
+
setDevices(selected);
|
|
1098
|
+
setLogs([log("system", `Resuming agent task \xB7 ${id}\u2026`)]);
|
|
1099
|
+
setError(null);
|
|
1100
|
+
runIdRef.current = null;
|
|
1101
|
+
eventKeysRef.current.clear();
|
|
1102
|
+
pendingEventsRef.current.clear();
|
|
1103
|
+
setRunId(null);
|
|
1104
|
+
try {
|
|
1105
|
+
const resumed = await runner.resume(id, inputDevices);
|
|
1106
|
+
if (!mountedRef.current) return;
|
|
1107
|
+
const targetsByDevice = new Map(resumed.targets.map((target) => [target.device_id, target]));
|
|
1108
|
+
setDevices(inputDevices.map((device) => mapTarget(device, targetsByDevice.get(device.id))));
|
|
1109
|
+
if (resumed.agent) setAgent(resumed.agent);
|
|
1110
|
+
if (resumed.concurrency && resumed.concurrency > 0) setConcurrency(resumed.concurrency);
|
|
1111
|
+
runIdRef.current = resumed.runId;
|
|
1112
|
+
setRunId(resumed.runId);
|
|
1113
|
+
const pending = pendingEventsRef.current.get(resumed.runId) ?? [];
|
|
1114
|
+
pendingEventsRef.current.delete(resumed.runId);
|
|
1115
|
+
for (const event of pending) handleEvent(event);
|
|
1116
|
+
setLogs((current) => [...current, log("system", `Task resumed \xB7 ${resumed.runId}`)]);
|
|
1117
|
+
} catch (reason) {
|
|
1118
|
+
if (!mountedRef.current) return;
|
|
1119
|
+
const message = String(reason);
|
|
1120
|
+
setError(message);
|
|
1121
|
+
setLogs((current) => [...current, log("error", `Failed to resume task: ${message}`)]);
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
[runner, handleEvent]
|
|
1125
|
+
);
|
|
1126
|
+
const cancelAll = useCallback(async () => {
|
|
1127
|
+
const id = runIdRef.current;
|
|
1128
|
+
if (!runner || !id) return;
|
|
1129
|
+
try {
|
|
1130
|
+
await runner.stop(id);
|
|
1131
|
+
} catch (reason) {
|
|
1132
|
+
if (mountedRef.current) setError(String(reason));
|
|
1133
|
+
}
|
|
1134
|
+
}, [runner]);
|
|
1135
|
+
const targetFor = useCallback((deviceId) => devices.find((device) => device.id === deviceId)?.targetId, [devices]);
|
|
1136
|
+
const withTarget = useCallback(async (deviceId, action) => {
|
|
1137
|
+
const id = runIdRef.current;
|
|
1138
|
+
const targetId = targetFor(deviceId);
|
|
1139
|
+
if (!id || !targetId) return;
|
|
1140
|
+
try {
|
|
1141
|
+
await action(id, targetId);
|
|
1142
|
+
} catch (reason) {
|
|
1143
|
+
if (mountedRef.current) setError(String(reason));
|
|
1144
|
+
}
|
|
1145
|
+
}, [targetFor]);
|
|
1146
|
+
const cancelDevice = useCallback(
|
|
1147
|
+
(deviceId) => withTarget(deviceId, (taskId, targetId) => runner.stopDevice(taskId, targetId)),
|
|
1148
|
+
[runner, withTarget]
|
|
1149
|
+
);
|
|
1150
|
+
const retryDevice = useCallback(
|
|
1151
|
+
(deviceId) => withTarget(deviceId, (taskId, targetId) => runner.retryDevice(taskId, targetId)),
|
|
1152
|
+
[runner, withTarget]
|
|
1153
|
+
);
|
|
1154
|
+
const approveDevice = useCallback(
|
|
1155
|
+
(deviceId) => withTarget(deviceId, (taskId, targetId) => runner.approveDevice(taskId, targetId)),
|
|
1156
|
+
[runner, withTarget]
|
|
1157
|
+
);
|
|
1158
|
+
const clearLogs = useCallback(() => setLogs([]), []);
|
|
1159
|
+
const reset = useCallback(() => {
|
|
1160
|
+
runIdRef.current = null;
|
|
1161
|
+
eventKeysRef.current.clear();
|
|
1162
|
+
pendingEventsRef.current.clear();
|
|
1163
|
+
setRunId(null);
|
|
1164
|
+
setDevices([]);
|
|
1165
|
+
setLogs([]);
|
|
1166
|
+
setError(null);
|
|
1167
|
+
}, []);
|
|
1168
|
+
const running = devices.some((device) => !terminal(device.status));
|
|
1169
|
+
return {
|
|
1170
|
+
runId,
|
|
1171
|
+
agent,
|
|
1172
|
+
concurrency,
|
|
1173
|
+
devices,
|
|
1174
|
+
logs,
|
|
1175
|
+
error,
|
|
1176
|
+
running,
|
|
1177
|
+
start,
|
|
1178
|
+
resume,
|
|
1179
|
+
cancelAll,
|
|
1180
|
+
cancelDevice,
|
|
1181
|
+
retryDevice,
|
|
1182
|
+
approveDevice,
|
|
1183
|
+
clearLogs,
|
|
1184
|
+
reset
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/ui/devices/task-agent-runner.ts
|
|
1189
|
+
var MAX_TASK_AGENT_CAPACITY = 2147483647;
|
|
1190
|
+
function validTaskAgentCapacity(value) {
|
|
1191
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= MAX_TASK_AGENT_CAPACITY;
|
|
1192
|
+
}
|
|
1193
|
+
function preflightTaskAgentDevice(device, agent, policyId = "device_standard_v1") {
|
|
1194
|
+
if (device.node === void 0) return { ok: true };
|
|
1195
|
+
if (device.node === null || device.node.status === "unknown") {
|
|
1196
|
+
return { ok: false, reason: "Node is unknown" };
|
|
1197
|
+
}
|
|
1198
|
+
if (device.node.status !== "online") return { ok: false, reason: "Node is offline" };
|
|
1199
|
+
const caps = device.node.reported_caps;
|
|
1200
|
+
if (!caps || typeof caps !== "object" || Array.isArray(caps)) {
|
|
1201
|
+
return { ok: false, reason: "Node capabilities are unavailable" };
|
|
1202
|
+
}
|
|
1203
|
+
const taskAgent = caps.task_agent;
|
|
1204
|
+
if (!taskAgent || typeof taskAgent !== "object" || Array.isArray(taskAgent)) {
|
|
1205
|
+
return { ok: false, reason: "Task-agent v2 is unavailable" };
|
|
1206
|
+
}
|
|
1207
|
+
const taskAgentCaps = taskAgent;
|
|
1208
|
+
if (typeof taskAgentCaps.version !== "number" || !Number.isSafeInteger(taskAgentCaps.version) || taskAgentCaps.version < 2) {
|
|
1209
|
+
return { ok: false, reason: "Task-agent v2 is unavailable" };
|
|
1210
|
+
}
|
|
1211
|
+
const agents = taskAgentCaps.agents;
|
|
1212
|
+
const agentCaps = agents && typeof agents === "object" && !Array.isArray(agents) ? agents[agent] : void 0;
|
|
1213
|
+
if (!agentCaps || typeof agentCaps !== "object" || Array.isArray(agentCaps) || agentCaps.available !== true) {
|
|
1214
|
+
return { ok: false, reason: `${agent === "claude" ? "Claude" : "Codex"} is unavailable on this node` };
|
|
1215
|
+
}
|
|
1216
|
+
const policyIds = agentCaps.policy_ids;
|
|
1217
|
+
if (!Array.isArray(policyIds) || !policyIds.includes(policyId)) {
|
|
1218
|
+
return { ok: false, reason: `${policyId} policy is unavailable` };
|
|
1219
|
+
}
|
|
1220
|
+
const maxWorkers = taskAgentCaps.max_workers;
|
|
1221
|
+
if (!validTaskAgentCapacity(maxWorkers)) {
|
|
1222
|
+
return { ok: false, reason: "Node reports no task-agent workers" };
|
|
1223
|
+
}
|
|
1224
|
+
const perDeviceSlots = taskAgentCaps.per_device_slots;
|
|
1225
|
+
if (perDeviceSlots !== void 0 && perDeviceSlots !== null && !validTaskAgentCapacity(perDeviceSlots)) {
|
|
1226
|
+
return { ok: false, reason: "Node reports no per-device task-agent slots" };
|
|
1227
|
+
}
|
|
1228
|
+
return { ok: true };
|
|
1229
|
+
}
|
|
1230
|
+
function normalizeBlocker(blocker) {
|
|
1231
|
+
if (!blocker) return null;
|
|
1232
|
+
if (typeof blocker === "string") return { code: "blocked", message: blocker };
|
|
1233
|
+
if (typeof blocker !== "object") return null;
|
|
1234
|
+
const value = blocker;
|
|
1235
|
+
if (typeof value.code !== "string" || typeof value.message !== "string") return null;
|
|
1236
|
+
return {
|
|
1237
|
+
code: value.code,
|
|
1238
|
+
message: value.message,
|
|
1239
|
+
approval_id: value.approval_id ?? null,
|
|
1240
|
+
data: value.data
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function normalizeEvidence(evidence) {
|
|
1244
|
+
if (!evidence) return [];
|
|
1245
|
+
const items = Array.isArray(evidence) ? evidence : [evidence];
|
|
1246
|
+
return items.flatMap((item) => {
|
|
1247
|
+
if (typeof item !== "object" || item === null) return [];
|
|
1248
|
+
if (typeof item.kind === "string") return [item];
|
|
1249
|
+
return [{ kind: "task", data: item }];
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired"]);
|
|
1253
|
+
function statusOf(status) {
|
|
1254
|
+
switch (status?.toLowerCase()) {
|
|
1255
|
+
case "queued":
|
|
1256
|
+
case "pending":
|
|
1257
|
+
return "queued";
|
|
1258
|
+
case "accepted":
|
|
1259
|
+
case "authorizing":
|
|
1260
|
+
case "preparing":
|
|
1261
|
+
case "dispatching":
|
|
1262
|
+
return "dispatching";
|
|
1263
|
+
case "running":
|
|
1264
|
+
case "started":
|
|
1265
|
+
return "running";
|
|
1266
|
+
case "verifying":
|
|
1267
|
+
return "verifying";
|
|
1268
|
+
case "blocked":
|
|
1269
|
+
return "blocked";
|
|
1270
|
+
case "cancel_requested":
|
|
1271
|
+
return "cancel_requested";
|
|
1272
|
+
case "cancelled":
|
|
1273
|
+
case "canceled":
|
|
1274
|
+
return "cancelled";
|
|
1275
|
+
case "expired":
|
|
1276
|
+
return "expired";
|
|
1277
|
+
// `passed` is what the node SDK emits; `succeeded` is the durable v2 state.
|
|
1278
|
+
case "passed":
|
|
1279
|
+
case "completed":
|
|
1280
|
+
case "complete":
|
|
1281
|
+
case "success":
|
|
1282
|
+
case "succeeded":
|
|
1283
|
+
case "done":
|
|
1284
|
+
return "succeeded";
|
|
1285
|
+
case "failed":
|
|
1286
|
+
case "error":
|
|
1287
|
+
return "failed";
|
|
1288
|
+
default:
|
|
1289
|
+
return "running";
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function logStream(event) {
|
|
1293
|
+
return event.stream === "stderr" || event.level === "error" ? "stderr" : "stdout";
|
|
1294
|
+
}
|
|
1295
|
+
function logChunk(event) {
|
|
1296
|
+
return event.chunk ?? event.line ?? event.message ?? event.output;
|
|
1297
|
+
}
|
|
1298
|
+
function eventTaskId(frame) {
|
|
1299
|
+
return frame.task_id ?? frame.run_id;
|
|
1300
|
+
}
|
|
1301
|
+
function createAgentTaskRunner(cfg, opts = {}) {
|
|
1302
|
+
const resolved = "auth" in cfg && typeof cfg.auth?.headers === "function" ? cfg : resolveConfig(cfg);
|
|
1303
|
+
const basePath = (opts.basePath ?? "/api/agent-tasks").replace(/\/$/, "");
|
|
1304
|
+
const policyId = opts.policyId ?? "device_standard_v1";
|
|
1305
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1306
|
+
const targetsByRun = /* @__PURE__ */ new Map();
|
|
1307
|
+
const emit = (event) => {
|
|
1308
|
+
for (const subscriber of subscribers) subscriber(event);
|
|
1309
|
+
};
|
|
1310
|
+
const post = async (path) => {
|
|
1311
|
+
const url = joinUrl(resolved.baseUrl, path);
|
|
1312
|
+
const headers = { ...await resolved.auth.headers("caller"), "content-type": "application/json" };
|
|
1313
|
+
const resp = await resolved.fetchFn(url, { method: "POST", headers, body: "{}" });
|
|
1314
|
+
if (resp.ok) return;
|
|
1315
|
+
const text = await resp.text().catch(() => "");
|
|
1316
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`agent tasks ${resp.status}: ${text}`);
|
|
1317
|
+
throw new AixError(`agent tasks ${resp.status}: ${text}`);
|
|
1318
|
+
};
|
|
1319
|
+
const targetPath = (runId, targetId, action) => `${basePath}/${encodeURIComponent(runId)}/targets/${encodeURIComponent(targetId)}/${action}`;
|
|
1320
|
+
const attach = async (runId, initialTargets, initialCursor = 0) => {
|
|
1321
|
+
const targets = new Map(initialTargets.map((target) => [target.target_id, target]));
|
|
1322
|
+
targetsByRun.set(runId, targets);
|
|
1323
|
+
let cursor = initialCursor;
|
|
1324
|
+
let processing = Promise.resolve();
|
|
1325
|
+
const appliedCursors = /* @__PURE__ */ new Set();
|
|
1326
|
+
const deviceByTarget = new Map(initialTargets.map((target) => [target.target_id, target.device_id]));
|
|
1327
|
+
const applyFrame = (frame) => {
|
|
1328
|
+
const taskId = eventTaskId(frame);
|
|
1329
|
+
const event = frame.event ?? (frame.kind ? { kind: frame.kind, ...frame.payload ?? {} } : void 0);
|
|
1330
|
+
if (taskId !== runId || !event || !frame.target_id) return;
|
|
1331
|
+
const targetId = frame.target_id;
|
|
1332
|
+
const deviceId = frame.device_id ?? deviceByTarget.get(targetId);
|
|
1333
|
+
const common = {
|
|
1334
|
+
runId,
|
|
1335
|
+
targetId,
|
|
1336
|
+
attemptId: frame.attempt_id,
|
|
1337
|
+
deviceId,
|
|
1338
|
+
cursor: frame.cursor
|
|
1339
|
+
};
|
|
1340
|
+
const kind = event.kind?.toLowerCase();
|
|
1341
|
+
const status = event.status === void 0 ? kind === "started" || kind === "running" ? "running" : kind === "accepted" ? "dispatching" : kind === "blocked" ? "blocked" : void 0 : statusOf(event.status);
|
|
1342
|
+
if (kind === "log") {
|
|
1343
|
+
const chunk = logChunk(event);
|
|
1344
|
+
if (chunk) emit({ ...common, stream: logStream(event), chunk });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (kind === "evidence" || event.evidence?.length) {
|
|
1348
|
+
if (event.evidence?.length) emit({ ...common, stream: "evidence", evidence: event.evidence });
|
|
1349
|
+
if (kind === "evidence" && !status) return;
|
|
1350
|
+
}
|
|
1351
|
+
if (status) {
|
|
1352
|
+
const stream = TERMINAL_STATUSES.has(status) || kind === "finished" ? "done" : "status";
|
|
1353
|
+
if (event.output) emit({ ...common, stream: "stdout", chunk: event.output });
|
|
1354
|
+
emit({
|
|
1355
|
+
...common,
|
|
1356
|
+
stream,
|
|
1357
|
+
status,
|
|
1358
|
+
error: event.error,
|
|
1359
|
+
blocker: event.blocker,
|
|
1360
|
+
...event.blocker?.message ? { blockedReason: event.blocker.message } : {},
|
|
1361
|
+
evidence: event.evidence
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
let processFrame;
|
|
1366
|
+
const fetchCatchup = async (after) => {
|
|
1367
|
+
const headers = await resolved.auth.headers("caller");
|
|
1368
|
+
const url = joinUrl(resolved.baseUrl, `${basePath}/${encodeURIComponent(runId)}/events?after=${encodeURIComponent(String(after))}`);
|
|
1369
|
+
const resp = await resolved.fetchFn(url, { headers });
|
|
1370
|
+
if (!resp.ok) {
|
|
1371
|
+
const text = await resp.text().catch(() => "");
|
|
1372
|
+
throw new AixError(`agent task events ${resp.status}: ${text}`);
|
|
1373
|
+
}
|
|
1374
|
+
const payload = await resp.json();
|
|
1375
|
+
const events = Array.isArray(payload) ? payload : payload.events;
|
|
1376
|
+
for (const frame of events) await processFrame(frame);
|
|
1377
|
+
const nextCursor = Array.isArray(payload) ? void 0 : payload.next_cursor;
|
|
1378
|
+
if (nextCursor !== void 0) cursor = Math.max(cursor, nextCursor);
|
|
1379
|
+
};
|
|
1380
|
+
processFrame = async (frame) => {
|
|
1381
|
+
const frameCursor = frame.cursor;
|
|
1382
|
+
if (frameCursor !== void 0) {
|
|
1383
|
+
if (appliedCursors.has(frameCursor) || frameCursor <= cursor) return;
|
|
1384
|
+
if (frameCursor > cursor + 1) {
|
|
1385
|
+
await fetchCatchup(cursor);
|
|
1386
|
+
return processFrame(frame);
|
|
1387
|
+
}
|
|
1388
|
+
appliedCursors.add(frameCursor);
|
|
1389
|
+
cursor = frameCursor;
|
|
1390
|
+
}
|
|
1391
|
+
applyFrame(frame);
|
|
1392
|
+
};
|
|
1393
|
+
const socket = new ReconnectingSocket({
|
|
1394
|
+
// Every reconnect mints a fresh one-time ticket; cursor remains in the URL
|
|
1395
|
+
// but the caller's bearer token never does.
|
|
1396
|
+
urlProvider: async () => executionWsTicketUrl(resolved, runId, cursor),
|
|
1397
|
+
wsFactory: resolved.wsFactory,
|
|
1398
|
+
onMessage: (text) => {
|
|
1399
|
+
let frame;
|
|
1400
|
+
try {
|
|
1401
|
+
frame = JSON.parse(text);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
processing = processing.then(() => processFrame(frame)).catch((error) => emit({
|
|
1406
|
+
runId,
|
|
1407
|
+
stream: "status",
|
|
1408
|
+
status: "failed",
|
|
1409
|
+
error: `Task event catch-up failed: ${String(error)}`
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
await socket.start();
|
|
1414
|
+
};
|
|
1415
|
+
return {
|
|
1416
|
+
available: true,
|
|
1417
|
+
async start(request) {
|
|
1418
|
+
const url = joinUrl(resolved.baseUrl, basePath);
|
|
1419
|
+
const payload = {
|
|
1420
|
+
agent: request.agent,
|
|
1421
|
+
prompt: request.prompt,
|
|
1422
|
+
device_ids: request.deviceIds,
|
|
1423
|
+
max_concurrency: request.concurrency,
|
|
1424
|
+
policy_id: policyId
|
|
1425
|
+
};
|
|
1426
|
+
const headers = { ...await resolved.auth.headers("caller"), "content-type": "application/json" };
|
|
1427
|
+
const resp = await resolved.fetchFn(url, { method: "POST", headers, body: JSON.stringify(payload) });
|
|
1428
|
+
if (!resp.ok) {
|
|
1429
|
+
const text = await resp.text().catch(() => "");
|
|
1430
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`agent tasks ${resp.status}: ${text}`);
|
|
1431
|
+
throw new AixError(`agent tasks ${resp.status}: ${text}`);
|
|
1432
|
+
}
|
|
1433
|
+
const started = await resp.json();
|
|
1434
|
+
if (!started.task_id || !Array.isArray(started.targets) || started.targets.some((target) => !target.target_id)) {
|
|
1435
|
+
throw new AixError("agent task response is missing stable target ids");
|
|
1436
|
+
}
|
|
1437
|
+
await attach(started.task_id, started.targets);
|
|
1438
|
+
return { runId: started.task_id, targets: started.targets };
|
|
1439
|
+
},
|
|
1440
|
+
async resume(runId, devices) {
|
|
1441
|
+
const url = joinUrl(resolved.baseUrl, `${basePath}/${encodeURIComponent(runId)}`);
|
|
1442
|
+
const headers = await resolved.auth.headers("caller");
|
|
1443
|
+
const resp = await resolved.fetchFn(url, { headers });
|
|
1444
|
+
if (!resp.ok) {
|
|
1445
|
+
const text = await resp.text().catch(() => "");
|
|
1446
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`agent tasks ${resp.status}: ${text}`);
|
|
1447
|
+
throw new AixError(`agent tasks ${resp.status}: ${text}`);
|
|
1448
|
+
}
|
|
1449
|
+
const snapshot = await resp.json();
|
|
1450
|
+
if (snapshot.task_id !== runId || !Array.isArray(snapshot.targets)) {
|
|
1451
|
+
throw new AixError("agent task snapshot is missing stable target ids");
|
|
1452
|
+
}
|
|
1453
|
+
const deviceById = new Map(devices.map((device) => [device.id, device]));
|
|
1454
|
+
const targets = snapshot.targets.map((target) => {
|
|
1455
|
+
const attempt = target.current_attempt;
|
|
1456
|
+
const evidence = normalizeEvidence(target.evidence ?? attempt?.result?.evidence);
|
|
1457
|
+
return {
|
|
1458
|
+
target_id: target.target_id,
|
|
1459
|
+
device_id: target.device_id,
|
|
1460
|
+
status: statusOf(target.status),
|
|
1461
|
+
attempt_id: attempt?.attempt_id ?? null,
|
|
1462
|
+
error: target.error ?? attempt?.error ?? null,
|
|
1463
|
+
blocker: normalizeBlocker(target.blocker),
|
|
1464
|
+
evidence
|
|
1465
|
+
};
|
|
1466
|
+
});
|
|
1467
|
+
await attach(runId, targets);
|
|
1468
|
+
return {
|
|
1469
|
+
runId,
|
|
1470
|
+
targets: targets.filter((target) => deviceById.has(target.device_id)),
|
|
1471
|
+
agent: snapshot.agent === "claude" || snapshot.agent === "codex" ? snapshot.agent : void 0,
|
|
1472
|
+
concurrency: snapshot.max_concurrency
|
|
1473
|
+
};
|
|
1474
|
+
},
|
|
1475
|
+
async stop(runId) {
|
|
1476
|
+
await post(`${basePath}/${encodeURIComponent(runId)}/cancel`);
|
|
1477
|
+
},
|
|
1478
|
+
async stopDevice(runId, targetId) {
|
|
1479
|
+
await post(targetPath(runId, targetId, "cancel"));
|
|
1480
|
+
},
|
|
1481
|
+
async retryDevice(runId, targetId) {
|
|
1482
|
+
await post(targetPath(runId, targetId, "retry"));
|
|
1483
|
+
},
|
|
1484
|
+
async approveDevice(runId, targetId) {
|
|
1485
|
+
await post(targetPath(runId, targetId, "approve"));
|
|
1486
|
+
},
|
|
1487
|
+
subscribe(on) {
|
|
1488
|
+
subscribers.add(on);
|
|
1489
|
+
return () => subscribers.delete(on);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
var TERMINAL2 = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired"]);
|
|
1494
|
+
function useTaskStatus(taskId, pollMs = 0) {
|
|
1495
|
+
const c = useClient();
|
|
1496
|
+
const [data, setData] = useState(null);
|
|
1497
|
+
const [loading, setLoading] = useState(false);
|
|
1498
|
+
const [error, setError] = useState(null);
|
|
1499
|
+
const mounted = useRef(true);
|
|
1500
|
+
const timer = useRef(null);
|
|
1501
|
+
const stop = useCallback(() => {
|
|
1502
|
+
if (timer.current) {
|
|
1503
|
+
clearInterval(timer.current);
|
|
1504
|
+
timer.current = null;
|
|
1505
|
+
}
|
|
1506
|
+
}, []);
|
|
1507
|
+
const fetchOnce = useCallback(async () => {
|
|
1508
|
+
if (!taskId) return;
|
|
1509
|
+
if (mounted.current) {
|
|
1510
|
+
setLoading(true);
|
|
1511
|
+
setError(null);
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
const r = await c.tasks.get(taskId);
|
|
1515
|
+
if (mounted.current) {
|
|
1516
|
+
setData(r);
|
|
1517
|
+
setLoading(false);
|
|
1518
|
+
}
|
|
1519
|
+
if (TERMINAL2.has(r.status)) stop();
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
if (mounted.current) {
|
|
1522
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
1523
|
+
setLoading(false);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}, [c, taskId, stop]);
|
|
1527
|
+
const done = data ? TERMINAL2.has(data.status) : false;
|
|
1528
|
+
useEffect(() => {
|
|
1529
|
+
mounted.current = true;
|
|
1530
|
+
setData(null);
|
|
1531
|
+
if (!taskId) return () => {
|
|
1532
|
+
mounted.current = false;
|
|
1533
|
+
};
|
|
1534
|
+
void fetchOnce();
|
|
1535
|
+
if (pollMs > 0) timer.current = setInterval(() => {
|
|
1536
|
+
void fetchOnce();
|
|
1537
|
+
}, pollMs);
|
|
1538
|
+
return () => {
|
|
1539
|
+
mounted.current = false;
|
|
1540
|
+
stop();
|
|
1541
|
+
};
|
|
1542
|
+
}, [taskId, pollMs, fetchOnce]);
|
|
1543
|
+
return { data, loading, error, done, refetch: fetchOnce };
|
|
1544
|
+
}
|
|
1545
|
+
function useTasks(filter) {
|
|
1546
|
+
const c = useClient();
|
|
1547
|
+
const key = JSON.stringify(filter ?? {});
|
|
1548
|
+
const q = useQuery(() => c.tasks.list(filter).then((r) => r.tasks), [c, key]);
|
|
1549
|
+
const { refetch } = q;
|
|
1550
|
+
const cancel = useCallback(async (id) => {
|
|
1551
|
+
await c.tasks.cancel(id);
|
|
1552
|
+
await refetch();
|
|
1553
|
+
}, [c, refetch]);
|
|
1554
|
+
return { ...q, cancel };
|
|
1555
|
+
}
|
|
1556
|
+
function useAutomationQueue(filter) {
|
|
1557
|
+
const c = useClient();
|
|
1558
|
+
const tasks = useTasks(filter);
|
|
1559
|
+
const { refetch } = tasks;
|
|
1560
|
+
const submit = useCallback(
|
|
1561
|
+
async (config) => {
|
|
1562
|
+
const ids = await submitBatch(c.cfg, config);
|
|
1563
|
+
await refetch();
|
|
1564
|
+
return ids;
|
|
1565
|
+
},
|
|
1566
|
+
[c, refetch]
|
|
1567
|
+
);
|
|
1568
|
+
const pause = useCallback(
|
|
1569
|
+
async (id) => {
|
|
1570
|
+
await c.tasks.pause(id);
|
|
1571
|
+
await refetch();
|
|
1572
|
+
},
|
|
1573
|
+
[c, refetch]
|
|
1574
|
+
);
|
|
1575
|
+
const resume = useCallback(
|
|
1576
|
+
async (id) => {
|
|
1577
|
+
await c.tasks.resume(id);
|
|
1578
|
+
await refetch();
|
|
1579
|
+
},
|
|
1580
|
+
[c, refetch]
|
|
1581
|
+
);
|
|
1582
|
+
return { ...tasks, submitBatch: submit, pause, resume };
|
|
1583
|
+
}
|
|
1584
|
+
function useInspector(deviceId) {
|
|
1585
|
+
const c = useClient();
|
|
1586
|
+
const [screenshot, setScreenshot] = useState(null);
|
|
1587
|
+
const [tree, setTree] = useState(null);
|
|
1588
|
+
const [selected, setSelected] = useState(null);
|
|
1589
|
+
const [loading, setLoading] = useState(false);
|
|
1590
|
+
const [error, setError] = useState(null);
|
|
1591
|
+
const mounted = useRef(true);
|
|
1592
|
+
useEffect(() => {
|
|
1593
|
+
mounted.current = true;
|
|
1594
|
+
return () => {
|
|
1595
|
+
mounted.current = false;
|
|
1596
|
+
};
|
|
1597
|
+
}, []);
|
|
1598
|
+
const captureScreenshot = useCallback(async () => {
|
|
1599
|
+
if (!deviceId) {
|
|
1600
|
+
setError(new Error("no device selected"));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
try {
|
|
1604
|
+
const { png_base64 } = await c.devices.screenshot(deviceId);
|
|
1605
|
+
if (mounted.current) setScreenshot(`data:image/png;base64,${png_base64}`);
|
|
1606
|
+
} catch (e) {
|
|
1607
|
+
if (mounted.current) setError(e instanceof Error ? e : new Error(String(e)));
|
|
1608
|
+
}
|
|
1609
|
+
}, [c, deviceId]);
|
|
1610
|
+
const captureTree = useCallback(async () => {
|
|
1611
|
+
if (!deviceId) {
|
|
1612
|
+
setError(new Error("no device selected"));
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const t = await c.devices.captureUiTree(deviceId);
|
|
1617
|
+
if (mounted.current) setTree(t);
|
|
1618
|
+
} catch (e) {
|
|
1619
|
+
if (mounted.current) setError(e instanceof Error ? e : new Error(String(e)));
|
|
1620
|
+
}
|
|
1621
|
+
}, [c, deviceId]);
|
|
1622
|
+
const refresh = useCallback(async () => {
|
|
1623
|
+
setLoading(true);
|
|
1624
|
+
setError(null);
|
|
1625
|
+
await Promise.all([captureScreenshot(), captureTree()]);
|
|
1626
|
+
if (mounted.current) setLoading(false);
|
|
1627
|
+
}, [captureScreenshot, captureTree]);
|
|
1628
|
+
const select = useCallback((el) => setSelected(el), []);
|
|
1629
|
+
return { screenshot, tree, selected, loading, error, captureScreenshot, captureTree, refresh, select };
|
|
1630
|
+
}
|
|
1631
|
+
var InspectorContext = createContext(null);
|
|
1632
|
+
function InspectorProvider({ children }) {
|
|
1633
|
+
const client = useClient();
|
|
1634
|
+
const { selectedDeviceId } = useDevicesView();
|
|
1635
|
+
const [screenshot, setScreenshot] = useState(null);
|
|
1636
|
+
const [tree, setTree] = useState(null);
|
|
1637
|
+
const [selectedElement, setSelectedElement] = useState(null);
|
|
1638
|
+
const [loading, setLoading] = useState(false);
|
|
1639
|
+
const [error, setError] = useState(null);
|
|
1640
|
+
const [verifyLocator, setVerifyLocator] = useState(null);
|
|
1641
|
+
const [matches, setMatches] = useState([]);
|
|
1642
|
+
const [verifying, setVerifying] = useState(false);
|
|
1643
|
+
const [recording, setRecording] = useState(false);
|
|
1644
|
+
const [recordedLines, setRecordedLines] = useState([]);
|
|
1645
|
+
const deviceIdRef = useRef(selectedDeviceId);
|
|
1646
|
+
deviceIdRef.current = selectedDeviceId;
|
|
1647
|
+
const deviceOp = useCallback(
|
|
1648
|
+
async (op) => {
|
|
1649
|
+
const id = deviceIdRef.current;
|
|
1650
|
+
if (!id) throw new Error("No device selected");
|
|
1651
|
+
return client.devices.op(id, op);
|
|
1652
|
+
},
|
|
1653
|
+
[client]
|
|
1654
|
+
);
|
|
1655
|
+
const captureScreenshot = useCallback(async () => {
|
|
1656
|
+
const id = deviceIdRef.current;
|
|
1657
|
+
if (!id) {
|
|
1658
|
+
setError("No device selected");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const { png_base64 } = await client.devices.screenshot(id);
|
|
1663
|
+
setScreenshot(`data:image/png;base64,${png_base64}`);
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
setError(String(err));
|
|
1666
|
+
}
|
|
1667
|
+
}, [client]);
|
|
1668
|
+
const captureTree = useCallback(async () => {
|
|
1669
|
+
const id = deviceIdRef.current;
|
|
1670
|
+
if (!id) {
|
|
1671
|
+
setError("No device selected");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const t = await client.devices.captureUiTree(id);
|
|
1676
|
+
setTree(t);
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
setError(String(err));
|
|
1679
|
+
}
|
|
1680
|
+
}, [client]);
|
|
1681
|
+
const verifyRef = useRef(async () => {
|
|
1682
|
+
});
|
|
1683
|
+
const refresh = useCallback(async () => {
|
|
1684
|
+
setLoading(true);
|
|
1685
|
+
setError(null);
|
|
1686
|
+
await Promise.all([captureScreenshot(), captureTree()]);
|
|
1687
|
+
setLoading(false);
|
|
1688
|
+
setMatches([]);
|
|
1689
|
+
setVerifyLocator((v) => {
|
|
1690
|
+
if (v) void verifyRef.current(v.strategy, v.value);
|
|
1691
|
+
return v;
|
|
1692
|
+
});
|
|
1693
|
+
}, [captureScreenshot, captureTree]);
|
|
1694
|
+
const selectElement = useCallback((el) => setSelectedElement(el), []);
|
|
1695
|
+
const reset = useCallback(() => {
|
|
1696
|
+
setScreenshot(null);
|
|
1697
|
+
setTree(null);
|
|
1698
|
+
setSelectedElement(null);
|
|
1699
|
+
setError(null);
|
|
1700
|
+
setMatches([]);
|
|
1701
|
+
setVerifyLocator(null);
|
|
1702
|
+
}, []);
|
|
1703
|
+
const tap = useCallback(
|
|
1704
|
+
async (x, y) => {
|
|
1705
|
+
try {
|
|
1706
|
+
await deviceOp({ op: "tap", x, y });
|
|
1707
|
+
await refresh();
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
setError(String(err));
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
[deviceOp, refresh]
|
|
1713
|
+
);
|
|
1714
|
+
const inputText = useCallback(
|
|
1715
|
+
async (text) => {
|
|
1716
|
+
try {
|
|
1717
|
+
await deviceOp({ op: "input_text", text });
|
|
1718
|
+
await refresh();
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
setError(String(err));
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
[deviceOp, refresh]
|
|
1724
|
+
);
|
|
1725
|
+
const pressKey = useCallback(
|
|
1726
|
+
async (key) => {
|
|
1727
|
+
try {
|
|
1728
|
+
await deviceOp({ op: "press_key", key });
|
|
1729
|
+
await refresh();
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
setError(String(err));
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
[deviceOp, refresh]
|
|
1735
|
+
);
|
|
1736
|
+
const swipe = useCallback(
|
|
1737
|
+
async (x1, y1, x2, y2, duration = 0.3) => {
|
|
1738
|
+
try {
|
|
1739
|
+
await deviceOp({ op: "swipe", x1, y1, x2, y2, duration });
|
|
1740
|
+
await refresh();
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
setError(String(err));
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
[deviceOp, refresh]
|
|
1746
|
+
);
|
|
1747
|
+
const longPress = useCallback(
|
|
1748
|
+
async (x, y, durationSecs = 1) => {
|
|
1749
|
+
try {
|
|
1750
|
+
await deviceOp({ op: "swipe", x1: x, y1: y, x2: x, y2: y, duration: durationSecs });
|
|
1751
|
+
await refresh();
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
setError(String(err));
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
[deviceOp, refresh]
|
|
1757
|
+
);
|
|
1758
|
+
const verify = useCallback(
|
|
1759
|
+
async (strategy, value2) => {
|
|
1760
|
+
if (!value2.trim()) {
|
|
1761
|
+
setVerifyLocator(null);
|
|
1762
|
+
setMatches([]);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
setVerifying(true);
|
|
1766
|
+
setError(null);
|
|
1767
|
+
setVerifyLocator({ strategy, value: value2 });
|
|
1768
|
+
try {
|
|
1769
|
+
const found = await deviceOp({
|
|
1770
|
+
op: "find_elements",
|
|
1771
|
+
locator: { strategy, value: value2 }
|
|
1772
|
+
});
|
|
1773
|
+
setMatches(found ?? []);
|
|
1774
|
+
setVerifying(false);
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
setError(String(err));
|
|
1777
|
+
setMatches([]);
|
|
1778
|
+
setVerifying(false);
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
[deviceOp]
|
|
1782
|
+
);
|
|
1783
|
+
verifyRef.current = verify;
|
|
1784
|
+
const clearMatches = useCallback(() => {
|
|
1785
|
+
setMatches([]);
|
|
1786
|
+
setVerifyLocator(null);
|
|
1787
|
+
}, []);
|
|
1788
|
+
const recordLine = useCallback(
|
|
1789
|
+
(line) => setRecordedLines((prev) => recording ? [...prev, line] : prev),
|
|
1790
|
+
[recording]
|
|
1791
|
+
);
|
|
1792
|
+
const removeLine = useCallback(
|
|
1793
|
+
(index) => setRecordedLines((prev) => prev.filter((_, i) => i !== index)),
|
|
1794
|
+
[]
|
|
1795
|
+
);
|
|
1796
|
+
const clearRecording = useCallback(() => setRecordedLines([]), []);
|
|
1797
|
+
const value = useMemo(
|
|
1798
|
+
() => ({
|
|
1799
|
+
screenshot,
|
|
1800
|
+
tree,
|
|
1801
|
+
selectedElement,
|
|
1802
|
+
loading,
|
|
1803
|
+
error,
|
|
1804
|
+
verifyLocator,
|
|
1805
|
+
matches,
|
|
1806
|
+
verifying,
|
|
1807
|
+
recording,
|
|
1808
|
+
recordedLines,
|
|
1809
|
+
captureScreenshot,
|
|
1810
|
+
captureTree,
|
|
1811
|
+
selectElement,
|
|
1812
|
+
refresh,
|
|
1813
|
+
reset,
|
|
1814
|
+
tap,
|
|
1815
|
+
inputText,
|
|
1816
|
+
pressKey,
|
|
1817
|
+
swipe,
|
|
1818
|
+
longPress,
|
|
1819
|
+
verify,
|
|
1820
|
+
clearMatches,
|
|
1821
|
+
setRecording,
|
|
1822
|
+
recordLine,
|
|
1823
|
+
removeLine,
|
|
1824
|
+
clearRecording
|
|
1825
|
+
}),
|
|
1826
|
+
[
|
|
1827
|
+
screenshot,
|
|
1828
|
+
tree,
|
|
1829
|
+
selectedElement,
|
|
1830
|
+
loading,
|
|
1831
|
+
error,
|
|
1832
|
+
verifyLocator,
|
|
1833
|
+
matches,
|
|
1834
|
+
verifying,
|
|
1835
|
+
recording,
|
|
1836
|
+
recordedLines,
|
|
1837
|
+
captureScreenshot,
|
|
1838
|
+
captureTree,
|
|
1839
|
+
selectElement,
|
|
1840
|
+
refresh,
|
|
1841
|
+
reset,
|
|
1842
|
+
tap,
|
|
1843
|
+
inputText,
|
|
1844
|
+
pressKey,
|
|
1845
|
+
swipe,
|
|
1846
|
+
longPress,
|
|
1847
|
+
verify,
|
|
1848
|
+
clearMatches,
|
|
1849
|
+
recordLine,
|
|
1850
|
+
removeLine,
|
|
1851
|
+
clearRecording
|
|
1852
|
+
]
|
|
1853
|
+
);
|
|
1854
|
+
return createElement(InspectorContext.Provider, { value }, children);
|
|
1855
|
+
}
|
|
1856
|
+
function useInspectorView() {
|
|
1857
|
+
const ctx = useContext(InspectorContext);
|
|
1858
|
+
if (!ctx) throw new Error("useInspectorView() must be used within <InspectorProvider>");
|
|
1859
|
+
return ctx;
|
|
1860
|
+
}
|
|
1861
|
+
function useWebhooks() {
|
|
1862
|
+
const c = useClient();
|
|
1863
|
+
const q = useQuery(() => c.webhooks.list(), [c]);
|
|
1864
|
+
const { setData } = q;
|
|
1865
|
+
const create = useCallback(async (input) => {
|
|
1866
|
+
await c.webhooks.create(input);
|
|
1867
|
+
setData((prev) => [...prev ?? [], { id: input.id, url: input.url, events: input.events, enabled: input.enabled }]);
|
|
1868
|
+
}, [c, setData]);
|
|
1869
|
+
const setEnabled = useCallback(async (id, enabled) => {
|
|
1870
|
+
const u = await c.webhooks.setEnabled(id, enabled);
|
|
1871
|
+
setData((prev) => (prev ?? []).map((w) => w.id === id ? { ...w, ...u } : w));
|
|
1872
|
+
}, [c, setData]);
|
|
1873
|
+
const remove = useCallback(async (id) => {
|
|
1874
|
+
await c.webhooks.remove(id);
|
|
1875
|
+
setData((prev) => (prev ?? []).filter((w) => w.id !== id));
|
|
1876
|
+
}, [c, setData]);
|
|
1877
|
+
const trigger = useCallback(async (eventType, payload = { ping: true }) => {
|
|
1878
|
+
await c.webhooks.trigger(eventType, payload);
|
|
1879
|
+
}, [c]);
|
|
1880
|
+
return { ...q, create, setEnabled, remove, trigger };
|
|
1881
|
+
}
|
|
1882
|
+
function useNodes() {
|
|
1883
|
+
const c = useClient();
|
|
1884
|
+
const q = useQuery(() => c.nodes.list(), [c]);
|
|
1885
|
+
const { refetch } = q;
|
|
1886
|
+
const register = useCallback(async (input) => {
|
|
1887
|
+
const r = await c.nodes.register(input);
|
|
1888
|
+
await refetch();
|
|
1889
|
+
return r;
|
|
1890
|
+
}, [c, refetch]);
|
|
1891
|
+
return { ...q, register };
|
|
1892
|
+
}
|
|
1893
|
+
function useEnvGate(nodeId, opts) {
|
|
1894
|
+
const cfg = useAixConfig();
|
|
1895
|
+
const os = opts?.os !== void 0 ? opts.os : detectEnvOs();
|
|
1896
|
+
const autoOpen = opts?.autoOpen ?? true;
|
|
1897
|
+
const [needsSetup, setNeedsSetup] = useState(null);
|
|
1898
|
+
const [ready, setReady] = useState(false);
|
|
1899
|
+
const [probing, setProbing] = useState(false);
|
|
1900
|
+
const [error, setError] = useState(null);
|
|
1901
|
+
const [open, setOpen] = useState(false);
|
|
1902
|
+
const [statusEvents, setStatusEvents] = useState([]);
|
|
1903
|
+
const probedFor = useRef(null);
|
|
1904
|
+
const mounted = useRef(true);
|
|
1905
|
+
useEffect(() => {
|
|
1906
|
+
mounted.current = true;
|
|
1907
|
+
return () => {
|
|
1908
|
+
mounted.current = false;
|
|
1909
|
+
};
|
|
1910
|
+
}, []);
|
|
1911
|
+
const probe = useCallback(async (attempt = 0) => {
|
|
1912
|
+
if (!nodeId || os !== "win" && os !== "mac") {
|
|
1913
|
+
if (mounted.current) {
|
|
1914
|
+
setNeedsSetup(false);
|
|
1915
|
+
setReady(true);
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
setProbing(true);
|
|
1920
|
+
setError(null);
|
|
1921
|
+
try {
|
|
1922
|
+
const run = await EnvRun.start(cfg, nodeId, "status", { os });
|
|
1923
|
+
const events = [];
|
|
1924
|
+
run.on("event", (e) => events.push(e));
|
|
1925
|
+
run.on("done", () => {
|
|
1926
|
+
if (!mounted.current) return;
|
|
1927
|
+
const sum = summarizeEnv(events);
|
|
1928
|
+
setStatusEvents(events);
|
|
1929
|
+
setReady(sum.ready);
|
|
1930
|
+
setNeedsSetup(!sum.ready);
|
|
1931
|
+
setProbing(false);
|
|
1932
|
+
if (!sum.ready && autoOpen) setOpen(true);
|
|
1933
|
+
});
|
|
1934
|
+
} catch (e) {
|
|
1935
|
+
if (!mounted.current) return;
|
|
1936
|
+
if (attempt < 5) {
|
|
1937
|
+
setTimeout(() => {
|
|
1938
|
+
if (mounted.current) void probe(attempt + 1);
|
|
1939
|
+
}, 3e3);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
1943
|
+
setProbing(false);
|
|
1944
|
+
}
|
|
1945
|
+
}, [cfg, nodeId, os, autoOpen]);
|
|
1946
|
+
useEffect(() => {
|
|
1947
|
+
if (!nodeId || probedFor.current === nodeId) return;
|
|
1948
|
+
probedFor.current = nodeId;
|
|
1949
|
+
void probe();
|
|
1950
|
+
}, [nodeId, probe]);
|
|
1951
|
+
const refresh = useCallback(() => {
|
|
1952
|
+
probedFor.current = null;
|
|
1953
|
+
void probe();
|
|
1954
|
+
}, [probe]);
|
|
1955
|
+
return { os, needsSetup, ready, probing, error, open, setOpen, statusEvents, refresh };
|
|
1956
|
+
}
|
|
1957
|
+
function useLocators() {
|
|
1958
|
+
const c = useClient();
|
|
1959
|
+
const q = useQuery(() => c.locators.list(), [c]);
|
|
1960
|
+
const { setData } = q;
|
|
1961
|
+
const create = useCallback(async (input) => {
|
|
1962
|
+
const l = await c.locators.create(input);
|
|
1963
|
+
setData((prev) => [...prev ?? [], l]);
|
|
1964
|
+
return l;
|
|
1965
|
+
}, [c, setData]);
|
|
1966
|
+
const remove = useCallback(async (id) => {
|
|
1967
|
+
await c.locators.remove(id);
|
|
1968
|
+
setData((prev) => (prev ?? []).filter((l) => l.id !== id));
|
|
1969
|
+
}, [c, setData]);
|
|
1970
|
+
return { ...q, create, remove };
|
|
1971
|
+
}
|
|
1972
|
+
function useImages() {
|
|
1973
|
+
const c = useClient();
|
|
1974
|
+
const q = useQuery(() => c.admin.listImages(), [c]);
|
|
1975
|
+
const { setData, refetch } = q;
|
|
1976
|
+
const upsert = useCallback(async (input) => {
|
|
1977
|
+
const img = await c.admin.upsertImage(input);
|
|
1978
|
+
await refetch();
|
|
1979
|
+
return img;
|
|
1980
|
+
}, [c, refetch]);
|
|
1981
|
+
const setActive = useCallback(async (id, active) => {
|
|
1982
|
+
const img = await c.admin.setImageActive(id, active);
|
|
1983
|
+
setData((prev) => (prev ?? []).map((i) => i.id === id ? img : i));
|
|
1984
|
+
}, [c, setData]);
|
|
1985
|
+
const remove = useCallback(async (id) => {
|
|
1986
|
+
await c.admin.deleteImage(id);
|
|
1987
|
+
setData((prev) => (prev ?? []).filter((i) => i.id !== id));
|
|
1988
|
+
}, [c, setData]);
|
|
1989
|
+
return { ...q, upsert, setActive, remove };
|
|
1990
|
+
}
|
|
1991
|
+
function useLlm() {
|
|
1992
|
+
const c = useClient();
|
|
1993
|
+
return {
|
|
1994
|
+
test: useCallback((opts) => c.llm.test(opts), [c])
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
function useAdmin() {
|
|
1998
|
+
const c = useClient();
|
|
1999
|
+
return useMemo(() => ({
|
|
2000
|
+
listRates: () => c.admin.listRates(),
|
|
2001
|
+
upsertRate: (b) => c.admin.upsertRate(b),
|
|
2002
|
+
listPlans: () => c.admin.listPlans(),
|
|
2003
|
+
createPlan: (b) => c.admin.createPlan(b),
|
|
2004
|
+
usage: () => c.admin.usage(),
|
|
2005
|
+
ledger: () => c.admin.ledger(),
|
|
2006
|
+
setRebate: (id, ratio) => c.admin.setRebate(id, ratio),
|
|
2007
|
+
setNodePlatform: (id, p) => c.admin.setNodePlatform(id, p)
|
|
2008
|
+
}), [c]);
|
|
2009
|
+
}
|
|
2010
|
+
function useBilling() {
|
|
2011
|
+
const c = useClient();
|
|
2012
|
+
const q = useQuery(() => c.billing.balance(), [c]);
|
|
2013
|
+
const { refetch } = q;
|
|
2014
|
+
const topup = useCallback(async (input) => {
|
|
2015
|
+
const r = await c.billing.topup(input);
|
|
2016
|
+
await refetch();
|
|
2017
|
+
return r;
|
|
2018
|
+
}, [c, refetch]);
|
|
2019
|
+
return { ...q, topup };
|
|
2020
|
+
}
|
|
2021
|
+
function useApiTokens() {
|
|
2022
|
+
const c = useClient();
|
|
2023
|
+
const q = useQuery(() => c.tokens.list(), [c]);
|
|
2024
|
+
const { refetch } = q;
|
|
2025
|
+
const create = useCallback(async (input) => {
|
|
2026
|
+
const r = await c.tokens.create(input);
|
|
2027
|
+
await refetch();
|
|
2028
|
+
return r;
|
|
2029
|
+
}, [c, refetch]);
|
|
2030
|
+
const revoke = useCallback(async (id) => {
|
|
2031
|
+
await c.tokens.revoke(id);
|
|
2032
|
+
await refetch();
|
|
2033
|
+
}, [c, refetch]);
|
|
2034
|
+
const rotate = useCallback(async (id) => {
|
|
2035
|
+
const r = await c.tokens.rotate(id);
|
|
2036
|
+
await refetch();
|
|
2037
|
+
return r;
|
|
2038
|
+
}, [c, refetch]);
|
|
2039
|
+
return { ...q, create, revoke, rotate };
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
export { DevicesProvider, GROUP_MIRROR_MAX, InspectorProvider, NodesProvider, ShortcutsProvider, createAgentTaskRunner, mapDevice, preflightTaskAgentDevice, useAdmin, useApiTokens, useAuth, useAutomationQueue, useBilling, useClusters, useCommands, useDeviceActions, useDevices, useDevicesView, useEnvGate, useFleetRun, useFlowRun, useGroupMirror, useImages, useInspector, useInspectorView, useLlm, useLocators, useMirror, useNodes, useNodesView, useProjects, useScriptRun, useScripts, useShortcutsView, useTaskAgentRun, useTaskStatus, useTasks, useWebhooks };
|