@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,1564 @@
|
|
|
1
|
+
// src/core/i18n.ts
|
|
2
|
+
var DICT = {
|
|
3
|
+
en: {
|
|
4
|
+
auth: "Authentication error",
|
|
5
|
+
timeout: "Operation timed out",
|
|
6
|
+
offline: "Device offline",
|
|
7
|
+
locked: "Device locked",
|
|
8
|
+
protocol: "Protocol error",
|
|
9
|
+
unknown: "Error"
|
|
10
|
+
},
|
|
11
|
+
zh: {
|
|
12
|
+
auth: "\u8BA4\u8BC1\u9519\u8BEF",
|
|
13
|
+
timeout: "\u64CD\u4F5C\u8D85\u65F6",
|
|
14
|
+
offline: "\u8BBE\u5907\u79BB\u7EBF",
|
|
15
|
+
locked: "\u8BBE\u5907\u5DF2\u9501\u5B9A",
|
|
16
|
+
protocol: "\u534F\u8BAE\u9519\u8BEF",
|
|
17
|
+
unknown: "\u9519\u8BEF"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var current = "en";
|
|
21
|
+
function setLocale(l) {
|
|
22
|
+
if (l === "en" || l === "zh") current = l;
|
|
23
|
+
}
|
|
24
|
+
function getLocale() {
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
function t(code) {
|
|
28
|
+
return DICT[current][code] ?? DICT.en[code];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/core/errors.ts
|
|
32
|
+
var AixError = class extends Error {
|
|
33
|
+
constructor(message, code = "unknown", opts) {
|
|
34
|
+
super(code === "unknown" ? message : `${t(code)}: ${message}`);
|
|
35
|
+
this.name = new.target.name;
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.status = opts?.status;
|
|
38
|
+
this.detail = opts?.detail;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var AixAuthError = class extends AixError {
|
|
42
|
+
constructor(m) {
|
|
43
|
+
super(m, "auth");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var AixTimeout = class extends AixError {
|
|
47
|
+
constructor(m) {
|
|
48
|
+
super(m, "timeout");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var AixDeviceOffline = class extends AixError {
|
|
52
|
+
constructor(m) {
|
|
53
|
+
super(m, "offline");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var AixLocked = class extends AixError {
|
|
57
|
+
constructor(m) {
|
|
58
|
+
super(m, "locked");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var AixProtocolError = class extends AixError {
|
|
62
|
+
constructor(m) {
|
|
63
|
+
super(m, "protocol");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function errorFromRet(error) {
|
|
67
|
+
if (error === "no node" || error.includes("offline") || error.includes("no-node")) return new AixDeviceOffline(error);
|
|
68
|
+
if (error.includes("timed out") || error.includes("timeout")) return new AixTimeout(error);
|
|
69
|
+
return new AixError(error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/core/protocol.ts
|
|
73
|
+
function parseIncoming(text) {
|
|
74
|
+
let v;
|
|
75
|
+
try {
|
|
76
|
+
v = JSON.parse(text);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new AixProtocolError(`bad json frame: ${String(e)}`);
|
|
79
|
+
}
|
|
80
|
+
if (typeof v !== "object" || v === null) throw new AixProtocolError("frame must be an object");
|
|
81
|
+
const o = v;
|
|
82
|
+
const hasT = typeof o.t === "string";
|
|
83
|
+
const hasExec = typeof o.run_id === "string" && o.event !== void 0;
|
|
84
|
+
if (!hasT && !hasExec) throw new AixProtocolError("frame missing string `t`");
|
|
85
|
+
return v;
|
|
86
|
+
}
|
|
87
|
+
function isRetFrame(f) {
|
|
88
|
+
return f.t === "ret";
|
|
89
|
+
}
|
|
90
|
+
function isFridaEventFrame(f) {
|
|
91
|
+
return f.t === "frida_event";
|
|
92
|
+
}
|
|
93
|
+
function isScriptletEventFrame(f) {
|
|
94
|
+
return f.t === "scriptlet_event";
|
|
95
|
+
}
|
|
96
|
+
function isStatusFrame(f) {
|
|
97
|
+
return f.t === "status";
|
|
98
|
+
}
|
|
99
|
+
function isExecutionFrame(f) {
|
|
100
|
+
const e = f.event;
|
|
101
|
+
return typeof f.run_id === "string" && typeof f.device_id === "string" && typeof e === "object" && e !== null && typeof e.kind === "string";
|
|
102
|
+
}
|
|
103
|
+
function isEnvEventFrame(f) {
|
|
104
|
+
if (typeof f !== "object" || f === null) return false;
|
|
105
|
+
const k = f.kind;
|
|
106
|
+
return k === "started" || k === "step" || k === "log" || k === "finished";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/resources/auth-provider.ts
|
|
110
|
+
var PROJECT_KEY_HEADER = "X-Project-Key";
|
|
111
|
+
var PROJECT_SECRET_HEADER = "X-Project-Secret";
|
|
112
|
+
function find(creds, kind) {
|
|
113
|
+
return creds.find((c) => c.kind === kind);
|
|
114
|
+
}
|
|
115
|
+
function buildAuthProvider(input) {
|
|
116
|
+
const creds = Array.isArray(input) ? input : [input];
|
|
117
|
+
return {
|
|
118
|
+
async headers(scope) {
|
|
119
|
+
if (scope === "public") return {};
|
|
120
|
+
if (scope === "caller") {
|
|
121
|
+
const project = find(creds, "project");
|
|
122
|
+
if (project) {
|
|
123
|
+
return { [PROJECT_KEY_HEADER]: project.key, [PROJECT_SECRET_HEADER]: project.secret };
|
|
124
|
+
}
|
|
125
|
+
const user = find(creds, "user");
|
|
126
|
+
if (user) return { Authorization: `Bearer ${await user.getToken()}` };
|
|
127
|
+
throw new AixAuthError("caller scope requires a project or user credential");
|
|
128
|
+
}
|
|
129
|
+
const dev = find(creds, "developer");
|
|
130
|
+
if (dev) return { Authorization: `Bearer ${await dev.getToken()}` };
|
|
131
|
+
throw new AixAuthError(`${scope} scope requires a developer credential`);
|
|
132
|
+
},
|
|
133
|
+
async callerBearer() {
|
|
134
|
+
const user = find(creds, "user");
|
|
135
|
+
if (user) return user.getToken();
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function jwtExpMs(token) {
|
|
141
|
+
const seg = token.split(".")[1];
|
|
142
|
+
if (!seg) return null;
|
|
143
|
+
try {
|
|
144
|
+
const b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
|
|
145
|
+
const json = typeof atob === "function" ? atob(b64) : globalThis.Buffer?.from(b64, "base64")?.toString("binary");
|
|
146
|
+
if (!json) return null;
|
|
147
|
+
const exp = JSON.parse(json).exp;
|
|
148
|
+
return typeof exp === "number" ? exp * 1e3 : null;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function createCachedUserCredential(opts) {
|
|
154
|
+
const skew = opts.refreshSkewMs ?? 6e4;
|
|
155
|
+
const fallbackTtl = opts.fallbackTtlMs ?? 6e5;
|
|
156
|
+
const now = opts.now ?? (() => Date.now());
|
|
157
|
+
let token = null;
|
|
158
|
+
let expiresAt = 0;
|
|
159
|
+
let inflight = null;
|
|
160
|
+
const refresh = async () => {
|
|
161
|
+
const r = await opts.mint();
|
|
162
|
+
const t2 = typeof r === "string" ? r : r.token;
|
|
163
|
+
const expiresIn = typeof r === "string" ? void 0 : r.expiresIn;
|
|
164
|
+
expiresAt = expiresIn != null ? now() + expiresIn * 1e3 : jwtExpMs(t2) ?? now() + fallbackTtl;
|
|
165
|
+
token = t2;
|
|
166
|
+
return t2;
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
kind: "user",
|
|
170
|
+
getToken: () => {
|
|
171
|
+
if (token && now() < expiresAt - skew) return token;
|
|
172
|
+
if (!inflight) inflight = refresh().finally(() => {
|
|
173
|
+
inflight = null;
|
|
174
|
+
});
|
|
175
|
+
return inflight;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/core/config.ts
|
|
181
|
+
function joinUrl(base, path) {
|
|
182
|
+
return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
183
|
+
}
|
|
184
|
+
function executionWsUrl(baseUrl, id) {
|
|
185
|
+
return joinUrl(baseUrl, `/ws/execution/${encodeURIComponent(id)}`).replace(/^http/, "ws");
|
|
186
|
+
}
|
|
187
|
+
async function executionStreamEventsAfter(cfg, id, after, limit = 100) {
|
|
188
|
+
const path = `/api/executions/${encodeURIComponent(id)}/events?after=${encodeURIComponent(String(after))}&limit=${encodeURIComponent(String(limit))}`;
|
|
189
|
+
const headers = await cfg.auth.headers("caller");
|
|
190
|
+
const resp = await cfg.fetchFn(joinUrl(cfg.baseUrl, path), { headers });
|
|
191
|
+
if (!resp.ok) {
|
|
192
|
+
const text = await resp.text().catch(() => "");
|
|
193
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`execution events ${resp.status}: ${text}`);
|
|
194
|
+
throw new AixError(`execution events ${resp.status}: ${text}`);
|
|
195
|
+
}
|
|
196
|
+
const result = await resp.json();
|
|
197
|
+
if (!Array.isArray(result.events)) throw new AixError("execution events response is missing events");
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
async function executionWsTicketUrl(cfg, id, after) {
|
|
201
|
+
const path = `/api/executions/${encodeURIComponent(id)}/ticket`;
|
|
202
|
+
const headers = { ...await cfg.auth.headers("caller"), "content-type": "application/json" };
|
|
203
|
+
let resp;
|
|
204
|
+
try {
|
|
205
|
+
resp = await cfg.fetchFn(joinUrl(cfg.baseUrl, path), { method: "POST", headers, body: "{}" });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof AixError) throw error;
|
|
208
|
+
throw new AixError(`execution ticket request failed: ${String(error)}`, "offline");
|
|
209
|
+
}
|
|
210
|
+
if (!resp.ok) {
|
|
211
|
+
const text = await resp.text().catch(() => "");
|
|
212
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`execution ticket ${resp.status}: ${text}`);
|
|
213
|
+
throw new AixError(`execution ticket ${resp.status}: ${text}`);
|
|
214
|
+
}
|
|
215
|
+
const ticket = await resp.json();
|
|
216
|
+
if (!ticket.ticket || !ticket.expires_at) throw new AixError("execution ticket response is missing ticket or expires_at");
|
|
217
|
+
const query = new URLSearchParams({ ticket: ticket.ticket });
|
|
218
|
+
if (after !== void 0) query.set("after", String(after));
|
|
219
|
+
return `${executionWsUrl(cfg.baseUrl, id)}?${query}`;
|
|
220
|
+
}
|
|
221
|
+
async function authHeader(getAuthToken) {
|
|
222
|
+
const tok = await getAuthToken();
|
|
223
|
+
return { Authorization: `Bearer ${tok}` };
|
|
224
|
+
}
|
|
225
|
+
function resolveConfig(cfg) {
|
|
226
|
+
const fetchFn = cfg.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
227
|
+
const wsFactory = cfg.wsFactory ?? ((url) => new globalThis.WebSocket(url));
|
|
228
|
+
const rtcFactory = cfg.rtcFactory ?? ((config) => {
|
|
229
|
+
const Ctor = globalThis.RTCPeerConnection;
|
|
230
|
+
if (!Ctor) throw new AixError("WebRTC unavailable: no RTCPeerConnection (inject cfg.rtcFactory)");
|
|
231
|
+
return new Ctor(config);
|
|
232
|
+
});
|
|
233
|
+
const creds = [];
|
|
234
|
+
if (cfg.auth) creds.push(...Array.isArray(cfg.auth) ? cfg.auth : [cfg.auth]);
|
|
235
|
+
if (cfg.getAuthToken && !creds.some((c) => c.kind === "user")) {
|
|
236
|
+
creds.push({ kind: "user", getToken: cfg.getAuthToken });
|
|
237
|
+
}
|
|
238
|
+
if (creds.length === 0) throw new AixError("AixConfig requires `auth` or `getAuthToken`");
|
|
239
|
+
const auth = buildAuthProvider(creds);
|
|
240
|
+
const getAuthToken = cfg.getAuthToken ?? (async () => {
|
|
241
|
+
const t2 = await auth.callerBearer();
|
|
242
|
+
if (t2 == null) throw new AixAuthError("no user bearer token configured (set auth.user or getAuthToken)");
|
|
243
|
+
return t2;
|
|
244
|
+
});
|
|
245
|
+
return { baseUrl: cfg.baseUrl, getAuthToken, auth, fetchFn, wsFactory, rtcFactory, localNode: cfg.localNode };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/core/transport.ts
|
|
249
|
+
var ReconnectingSocket = class {
|
|
250
|
+
constructor(o) {
|
|
251
|
+
this.o = o;
|
|
252
|
+
this.ws = null;
|
|
253
|
+
this.closed = false;
|
|
254
|
+
this.attempt = 0;
|
|
255
|
+
this.reconnectTimer = null;
|
|
256
|
+
this.base = o.baseDelayMs ?? 500;
|
|
257
|
+
this.max = o.maxDelayMs ?? 1e4;
|
|
258
|
+
}
|
|
259
|
+
async start() {
|
|
260
|
+
await this.connect();
|
|
261
|
+
}
|
|
262
|
+
async connect() {
|
|
263
|
+
if (this.closed) return;
|
|
264
|
+
const url = await this.o.urlProvider();
|
|
265
|
+
const ws = this.o.wsFactory(url);
|
|
266
|
+
this.ws = ws;
|
|
267
|
+
ws.onopen = () => {
|
|
268
|
+
this.attempt = 0;
|
|
269
|
+
this.o.onOpen?.();
|
|
270
|
+
};
|
|
271
|
+
ws.onmessage = (ev) => {
|
|
272
|
+
if (typeof ev.data !== "string") return;
|
|
273
|
+
this.o.onMessage(ev.data);
|
|
274
|
+
};
|
|
275
|
+
ws.onerror = () => {
|
|
276
|
+
};
|
|
277
|
+
ws.onclose = () => {
|
|
278
|
+
this.ws = null;
|
|
279
|
+
this.o.onClose?.();
|
|
280
|
+
if (!this.closed) this.scheduleReconnect();
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
scheduleReconnect() {
|
|
284
|
+
const exp = Math.min(this.max, this.base * 2 ** this.attempt);
|
|
285
|
+
const jitter = exp * 0.25 * Math.random();
|
|
286
|
+
this.attempt++;
|
|
287
|
+
this.reconnectTimer = setTimeout(() => {
|
|
288
|
+
this.reconnectTimer = null;
|
|
289
|
+
void this.connect().catch(() => {
|
|
290
|
+
if (!this.closed) this.scheduleReconnect();
|
|
291
|
+
});
|
|
292
|
+
}, exp + jitter);
|
|
293
|
+
}
|
|
294
|
+
send(text) {
|
|
295
|
+
if (!this.ws) throw new Error("socket not connected");
|
|
296
|
+
this.ws.send(text);
|
|
297
|
+
}
|
|
298
|
+
isConnected() {
|
|
299
|
+
return this.ws !== null;
|
|
300
|
+
}
|
|
301
|
+
close() {
|
|
302
|
+
if (this.reconnectTimer !== null) {
|
|
303
|
+
clearTimeout(this.reconnectTimer);
|
|
304
|
+
this.reconnectTimer = null;
|
|
305
|
+
}
|
|
306
|
+
this.closed = true;
|
|
307
|
+
this.ws?.close();
|
|
308
|
+
this.ws = null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/core/env-run.ts
|
|
313
|
+
function envBody(action, input) {
|
|
314
|
+
const os = input.os ?? "mac";
|
|
315
|
+
const body = { os };
|
|
316
|
+
if (action === "setup" && input.route) body.route = input.route;
|
|
317
|
+
if (action === "teardown" && input.purge) body.purge = true;
|
|
318
|
+
return body;
|
|
319
|
+
}
|
|
320
|
+
var EnvRun = class _EnvRun {
|
|
321
|
+
constructor(reqId, stream, socket) {
|
|
322
|
+
this.reqId = reqId;
|
|
323
|
+
this.stream = stream;
|
|
324
|
+
this.socket = socket;
|
|
325
|
+
this.eventListeners = /* @__PURE__ */ new Set();
|
|
326
|
+
this.doneListeners = /* @__PURE__ */ new Set();
|
|
327
|
+
this.doneFired = false;
|
|
328
|
+
this.last = null;
|
|
329
|
+
this.cursor = 0;
|
|
330
|
+
this.pollTimer = null;
|
|
331
|
+
this.closed = false;
|
|
332
|
+
// `start()` opens the socket before callers can register listeners. Keep
|
|
333
|
+
// frames received in that small window so the first Started/Step events do
|
|
334
|
+
// not disappear and leave progress UIs stuck at 0%.
|
|
335
|
+
this.pendingEvents = [];
|
|
336
|
+
}
|
|
337
|
+
/** Trigger an env action on an owned node and start streaming its progress. */
|
|
338
|
+
static async start(cfg, nodeId, action, input = {}) {
|
|
339
|
+
const resolved = "fetchFn" in cfg ? cfg : resolveConfig(cfg);
|
|
340
|
+
const url = joinUrl(resolved.baseUrl, `/api/nodes/${encodeURIComponent(nodeId)}/env/${action}`);
|
|
341
|
+
const headers = { ...await resolved.auth.headers("caller"), "content-type": "application/json" };
|
|
342
|
+
const resp = await resolved.fetchFn(url, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers,
|
|
345
|
+
body: JSON.stringify(envBody(action, input))
|
|
346
|
+
});
|
|
347
|
+
if (!resp.ok) {
|
|
348
|
+
const t2 = await resp.text().catch(() => "");
|
|
349
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`env ${action} ${resp.status}: ${t2}`);
|
|
350
|
+
throw new AixError(`env ${action} ${resp.status}: ${t2}`);
|
|
351
|
+
}
|
|
352
|
+
const data = await resp.json();
|
|
353
|
+
let run = null;
|
|
354
|
+
let cursor = 0;
|
|
355
|
+
let processing = Promise.resolve();
|
|
356
|
+
const applyFrame = (frame) => {
|
|
357
|
+
if (frame.run_id !== data.req_id) return;
|
|
358
|
+
if (typeof frame.cursor !== "number") {
|
|
359
|
+
run?.onMessage(JSON.stringify(frame.event));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (frame.cursor <= cursor) return;
|
|
363
|
+
cursor = frame.cursor;
|
|
364
|
+
run?.onMessage(JSON.stringify({ cursor: frame.cursor, ...frame.event }));
|
|
365
|
+
};
|
|
366
|
+
const fetchCatchup = async (after) => {
|
|
367
|
+
let nextAfter = after;
|
|
368
|
+
do {
|
|
369
|
+
const page = await executionStreamEventsAfter(resolved, data.req_id, nextAfter);
|
|
370
|
+
for (const event of page.events) applyFrame(event);
|
|
371
|
+
nextAfter = page.next_cursor ?? cursor;
|
|
372
|
+
if (!page.has_more) break;
|
|
373
|
+
} while (true);
|
|
374
|
+
};
|
|
375
|
+
const processFrame = async (frame) => {
|
|
376
|
+
if (frame.run_id !== data.req_id) return;
|
|
377
|
+
if (typeof frame.cursor !== "number") {
|
|
378
|
+
run?.onMessage(JSON.stringify(frame.event));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (frame.cursor <= cursor) return;
|
|
382
|
+
if (frame.cursor > cursor + 1) {
|
|
383
|
+
await fetchCatchup(cursor);
|
|
384
|
+
}
|
|
385
|
+
applyFrame(frame);
|
|
386
|
+
};
|
|
387
|
+
const socket = new ReconnectingSocket({
|
|
388
|
+
// Fetch a new one-time ticket after every stream reconnect and resume
|
|
389
|
+
// after the latest durable event cursor.
|
|
390
|
+
urlProvider: async () => executionWsTicketUrl(resolved, data.req_id, cursor),
|
|
391
|
+
wsFactory: resolved.wsFactory,
|
|
392
|
+
onMessage: (text) => {
|
|
393
|
+
let frame;
|
|
394
|
+
try {
|
|
395
|
+
frame = JSON.parse(text);
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (!frame.run_id || !frame.event) {
|
|
400
|
+
run?.onMessage(text);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
processing = processing.then(() => processFrame(frame)).catch(() => void 0);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
run = new _EnvRun(data.req_id, data.stream ?? `/ws/execution/${data.req_id}`, socket);
|
|
407
|
+
const currentRun = run;
|
|
408
|
+
let failures = 0;
|
|
409
|
+
const poll = async () => {
|
|
410
|
+
if (currentRun.closed) return;
|
|
411
|
+
try {
|
|
412
|
+
await (processing = processing.catch(() => void 0).then(() => fetchCatchup(cursor)));
|
|
413
|
+
failures = 0;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (++failures >= 3 && !currentRun.closed) {
|
|
416
|
+
currentRun.onMessage(JSON.stringify({
|
|
417
|
+
kind: "finished",
|
|
418
|
+
ok: false,
|
|
419
|
+
error: `Environment progress connection failed: ${String(error)}`,
|
|
420
|
+
hint: "The node may still be installing. Check its status before starting setup again."
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!currentRun.closed) currentRun.pollTimer = setTimeout(() => {
|
|
425
|
+
void poll();
|
|
426
|
+
}, 3e3);
|
|
427
|
+
};
|
|
428
|
+
currentRun.pollTimer = setTimeout(() => {
|
|
429
|
+
void poll();
|
|
430
|
+
}, 3e3);
|
|
431
|
+
try {
|
|
432
|
+
await socket.start();
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
return run;
|
|
436
|
+
}
|
|
437
|
+
on(event, cb) {
|
|
438
|
+
if (event === "event") {
|
|
439
|
+
const fn2 = cb;
|
|
440
|
+
this.eventListeners.add(fn2);
|
|
441
|
+
if (this.pendingEvents.length > 0) {
|
|
442
|
+
const pending = this.pendingEvents;
|
|
443
|
+
this.pendingEvents = [];
|
|
444
|
+
for (const ev of pending) fn2(ev);
|
|
445
|
+
}
|
|
446
|
+
return () => this.eventListeners.delete(fn2);
|
|
447
|
+
}
|
|
448
|
+
const fn = cb;
|
|
449
|
+
this.doneListeners.add(fn);
|
|
450
|
+
if (this.doneFired && this.last) {
|
|
451
|
+
const f = this.last;
|
|
452
|
+
queueMicrotask(() => fn(f));
|
|
453
|
+
}
|
|
454
|
+
return () => this.doneListeners.delete(fn);
|
|
455
|
+
}
|
|
456
|
+
onMessage(text) {
|
|
457
|
+
let frame;
|
|
458
|
+
try {
|
|
459
|
+
frame = JSON.parse(text);
|
|
460
|
+
} catch {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!isEnvEventFrame(frame)) return;
|
|
464
|
+
const cursor = frame.cursor;
|
|
465
|
+
if (typeof cursor === "number") {
|
|
466
|
+
if (cursor <= this.cursor) return;
|
|
467
|
+
this.cursor = cursor;
|
|
468
|
+
}
|
|
469
|
+
const ev = frame;
|
|
470
|
+
if (this.eventListeners.size === 0) this.pendingEvents.push(ev);
|
|
471
|
+
else for (const cb of this.eventListeners) cb(ev);
|
|
472
|
+
if (ev.kind === "finished" && !this.doneFired) {
|
|
473
|
+
this.doneFired = true;
|
|
474
|
+
this.last = ev;
|
|
475
|
+
for (const cb of this.doneListeners) cb(this.last);
|
|
476
|
+
this.close();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
close() {
|
|
480
|
+
this.closed = true;
|
|
481
|
+
if (this.pollTimer !== null) {
|
|
482
|
+
clearTimeout(this.pollTimer);
|
|
483
|
+
this.pollTimer = null;
|
|
484
|
+
}
|
|
485
|
+
this.socket.close();
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var ENV_OK_STATES = /* @__PURE__ */ new Set(["ok", "skipped", "present", "done"]);
|
|
489
|
+
var ENV_BAD_STATES = /* @__PURE__ */ new Set(["failed", "error"]);
|
|
490
|
+
function summarizeEnv(events) {
|
|
491
|
+
const meta = /* @__PURE__ */ new Map();
|
|
492
|
+
const steps = /* @__PURE__ */ new Map();
|
|
493
|
+
const logs = [];
|
|
494
|
+
let total = 0;
|
|
495
|
+
let finished = null;
|
|
496
|
+
for (const e of events) {
|
|
497
|
+
if (e.kind === "started") {
|
|
498
|
+
total = e.total;
|
|
499
|
+
e.steps.forEach((s, i) => {
|
|
500
|
+
meta.set(i, s);
|
|
501
|
+
const existing = steps.get(i);
|
|
502
|
+
steps.set(i, existing ? { ...existing, label: s.label } : {
|
|
503
|
+
index: i,
|
|
504
|
+
id: s.id,
|
|
505
|
+
label: s.label,
|
|
506
|
+
state: "pending",
|
|
507
|
+
pct: 0
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
} else if (e.kind === "step") {
|
|
511
|
+
const m = meta.get(e.index);
|
|
512
|
+
steps.set(e.index, {
|
|
513
|
+
index: e.index,
|
|
514
|
+
id: e.id,
|
|
515
|
+
label: m?.label ?? e.id,
|
|
516
|
+
state: e.state,
|
|
517
|
+
pct: e.pct,
|
|
518
|
+
detail: e.detail
|
|
519
|
+
});
|
|
520
|
+
} else if (e.kind === "log") {
|
|
521
|
+
logs.push(`${e.id}: ${e.line}`);
|
|
522
|
+
} else if (e.kind === "finished") {
|
|
523
|
+
finished = e;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const list = [...steps.values()].sort((a, b) => a.index - b.index);
|
|
527
|
+
const anyBad = list.some((s) => ENV_BAD_STATES.has(s.state));
|
|
528
|
+
const allGood = list.length > 0 && list.every((s) => ENV_OK_STATES.has(s.state));
|
|
529
|
+
const ready = finished ? finished.ok && !anyBad && (allGood || !!finished.result?.docker_socket) : false;
|
|
530
|
+
return { total: total || list.length, steps: list, logs, finished, ready, running: !finished && events.length > 0 };
|
|
531
|
+
}
|
|
532
|
+
function envBlocker(finished) {
|
|
533
|
+
if (finished && !finished.ok && finished.code === "WSL_UNAVAILABLE") return "wsl-missing";
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
function detectEnvOs() {
|
|
537
|
+
const ua = typeof navigator !== "undefined" && (navigator.userAgent || navigator.platform) || "";
|
|
538
|
+
if (/win/i.test(ua)) return "win";
|
|
539
|
+
if (/mac|darwin|iphone|ipad/i.test(ua)) return "mac";
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
async function runEnvToFinish(cfg, nodeId, action, input = {}) {
|
|
543
|
+
const run = await EnvRun.start(cfg, nodeId, action, input);
|
|
544
|
+
const events = [];
|
|
545
|
+
return new Promise((resolve) => {
|
|
546
|
+
run.on("event", (e) => events.push(e));
|
|
547
|
+
run.on("done", () => resolve(summarizeEnv(events)));
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/core/resources/http.ts
|
|
552
|
+
function buildQuery(query) {
|
|
553
|
+
if (!query) return "";
|
|
554
|
+
const parts = [];
|
|
555
|
+
for (const [k, v] of Object.entries(query)) {
|
|
556
|
+
if (v === void 0 || v === null) continue;
|
|
557
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
558
|
+
}
|
|
559
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
560
|
+
}
|
|
561
|
+
var RestClient = class {
|
|
562
|
+
constructor(cfg) {
|
|
563
|
+
this.cfg = cfg;
|
|
564
|
+
}
|
|
565
|
+
async request(method, path, opts = {}) {
|
|
566
|
+
const scope = opts.scope ?? "caller";
|
|
567
|
+
const url = joinUrl(this.cfg.baseUrl, path) + buildQuery(opts.query);
|
|
568
|
+
const headers = {
|
|
569
|
+
Accept: "application/json",
|
|
570
|
+
...await this.cfg.auth.headers(scope),
|
|
571
|
+
...opts.headers ?? {}
|
|
572
|
+
};
|
|
573
|
+
let body;
|
|
574
|
+
if (opts.body !== void 0) {
|
|
575
|
+
body = typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body);
|
|
576
|
+
if (!("Content-Type" in headers) && !("content-type" in headers)) {
|
|
577
|
+
headers["Content-Type"] = "application/json";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
let resp;
|
|
581
|
+
try {
|
|
582
|
+
resp = await this.cfg.fetchFn(url, { method, headers, body });
|
|
583
|
+
} catch (e) {
|
|
584
|
+
if (e instanceof AixError) throw e;
|
|
585
|
+
throw new AixError(`${method} ${path} request failed: ${String(e)}`, "offline");
|
|
586
|
+
}
|
|
587
|
+
if (!resp.ok) {
|
|
588
|
+
const text = await resp.text().catch(() => "");
|
|
589
|
+
let detail;
|
|
590
|
+
try {
|
|
591
|
+
const j = JSON.parse(text);
|
|
592
|
+
const m = typeof j.message === "string" ? j.message : typeof j.error === "string" ? j.error : void 0;
|
|
593
|
+
detail = m || void 0;
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`${method} ${path} ${resp.status}: ${text}`);
|
|
597
|
+
if (resp.status === 423) throw new AixLocked(`${method} ${path} 423: ${text}`);
|
|
598
|
+
throw new AixError(detail ?? `${method} ${path} ${resp.status}: ${text}`, "unknown", { status: resp.status, detail });
|
|
599
|
+
}
|
|
600
|
+
if (resp.status === 204) return void 0;
|
|
601
|
+
const ct = resp.headers.get?.("content-type") ?? "";
|
|
602
|
+
if (!ct.includes("application/json")) return await resp.text();
|
|
603
|
+
return await resp.json();
|
|
604
|
+
}
|
|
605
|
+
get(path, opts) {
|
|
606
|
+
return this.request("GET", path, opts);
|
|
607
|
+
}
|
|
608
|
+
post(path, body, opts) {
|
|
609
|
+
return this.request("POST", path, { ...opts, body });
|
|
610
|
+
}
|
|
611
|
+
put(path, body, opts) {
|
|
612
|
+
return this.request("PUT", path, { ...opts, body });
|
|
613
|
+
}
|
|
614
|
+
patch(path, body, opts) {
|
|
615
|
+
return this.request("PATCH", path, { ...opts, body });
|
|
616
|
+
}
|
|
617
|
+
del(path, opts) {
|
|
618
|
+
return this.request("DELETE", path, opts);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
function restClient(cfg) {
|
|
622
|
+
return new RestClient(cfg);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/core/resources/auth.ts
|
|
626
|
+
var AuthApi = class {
|
|
627
|
+
constructor(c) {
|
|
628
|
+
this.c = c;
|
|
629
|
+
}
|
|
630
|
+
login(email, password) {
|
|
631
|
+
return this.c.post("/api/auth/login", { email, password }, { scope: "public" });
|
|
632
|
+
}
|
|
633
|
+
register(email, password) {
|
|
634
|
+
return this.c.post("/api/auth/register", { email, password }, { scope: "public" });
|
|
635
|
+
}
|
|
636
|
+
sendCode(email) {
|
|
637
|
+
return this.c.post("/api/auth/send-code", { email }, { scope: "public" });
|
|
638
|
+
}
|
|
639
|
+
verifyCode(email, code) {
|
|
640
|
+
return this.c.post("/api/auth/verify-code", { email, code }, { scope: "public" });
|
|
641
|
+
}
|
|
642
|
+
setPassword(password) {
|
|
643
|
+
return this.c.post("/api/auth/set-password", { password }, { scope: "developer" });
|
|
644
|
+
}
|
|
645
|
+
/** Exchange the developer bearer for a project-scoped user token. */
|
|
646
|
+
projectUser(projectId) {
|
|
647
|
+
return this.c.post("/api/auth/project-user", { project_id: projectId }, { scope: "developer" });
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// src/core/resources/projects.ts
|
|
652
|
+
var ProjectsApi = class {
|
|
653
|
+
constructor(c) {
|
|
654
|
+
this.c = c;
|
|
655
|
+
}
|
|
656
|
+
list() {
|
|
657
|
+
return this.c.get("/api/projects", { scope: "developer" });
|
|
658
|
+
}
|
|
659
|
+
create(input) {
|
|
660
|
+
return this.c.post("/api/projects", input, { scope: "developer" });
|
|
661
|
+
}
|
|
662
|
+
remove(id) {
|
|
663
|
+
return this.c.del(`/api/projects/${encodeURIComponent(id)}`, { scope: "developer" });
|
|
664
|
+
}
|
|
665
|
+
rotateSecret(id) {
|
|
666
|
+
return this.c.post(`/api/projects/${encodeURIComponent(id)}/rotate-secret`, void 0, { scope: "developer" });
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/core/resources/users.ts
|
|
671
|
+
var UsersApi = class {
|
|
672
|
+
constructor(c) {
|
|
673
|
+
this.c = c;
|
|
674
|
+
}
|
|
675
|
+
/** POST /api/users — provision (or reuse) a user and mint their token. */
|
|
676
|
+
provision(input) {
|
|
677
|
+
return this.c.post("/api/users", input, { scope: "caller" });
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/core/resources/devices.ts
|
|
682
|
+
var DevicesApi = class {
|
|
683
|
+
constructor(c) {
|
|
684
|
+
this.c = c;
|
|
685
|
+
}
|
|
686
|
+
list() {
|
|
687
|
+
return this.c.get("/api/devices");
|
|
688
|
+
}
|
|
689
|
+
/** Register/upsert a device (`POST /api/devices`). */
|
|
690
|
+
register(body) {
|
|
691
|
+
return this.c.post("/api/devices", body);
|
|
692
|
+
}
|
|
693
|
+
/** Provision a virtual device (`POST /api/devices/provision`). The spec is
|
|
694
|
+
* opaque; `node_id` (optional) routes to a specific usable node. */
|
|
695
|
+
provision(spec) {
|
|
696
|
+
return this.c.post("/api/devices/provision", spec);
|
|
697
|
+
}
|
|
698
|
+
/** Brand→model identity catalog for the create-wizard's 品牌 → 机型 picker
|
|
699
|
+
* (`GET /api/devices/profile-catalog`). Static read-only data; the chosen
|
|
700
|
+
* `model` is passed back via `ProvisionSpec.model` to lock the fingerprint base. */
|
|
701
|
+
profileCatalog() {
|
|
702
|
+
return this.c.get("/api/devices/profile-catalog");
|
|
703
|
+
}
|
|
704
|
+
/** The single generic live-op entry point, proxied to the owning node. Any
|
|
705
|
+
* node-supported op works here without a dedicated method; the typed helpers
|
|
706
|
+
* below are thin wrappers over it. */
|
|
707
|
+
op(deviceId, op) {
|
|
708
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/op`, op);
|
|
709
|
+
}
|
|
710
|
+
screenshot(deviceId) {
|
|
711
|
+
return this.op(deviceId, { op: "screenshot" });
|
|
712
|
+
}
|
|
713
|
+
captureUiTree(deviceId) {
|
|
714
|
+
return this.op(deviceId, { op: "capture_ui_tree" });
|
|
715
|
+
}
|
|
716
|
+
/** Power / lifecycle op (`POST /api/devices/:id/power`). Resolves with the
|
|
717
|
+
* node's power JSON receipt (e.g. `{ rebooted: true }`, `{ stopped: true }`,
|
|
718
|
+
* `{ device_id, reset: true }`) — the backend awaits the node response, so a
|
|
719
|
+
* resolved promise means the op actually ran. Rejects (RestClient throws on
|
|
720
|
+
* non-2xx) carry the node/backend error. */
|
|
721
|
+
power(deviceId, action, extra) {
|
|
722
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/power`, { action, ...extra });
|
|
723
|
+
}
|
|
724
|
+
/** Typed device-control op via the power lane (install / inject / screen-record). */
|
|
725
|
+
control(deviceId, op) {
|
|
726
|
+
return this.power(deviceId, "control", { op });
|
|
727
|
+
}
|
|
728
|
+
installApkUrl(deviceId, url) {
|
|
729
|
+
return this.control(deviceId, { op: "install", payload: { source: { kind: "url", value: url } } });
|
|
730
|
+
}
|
|
731
|
+
/** Install an iOS App Store app by bundle id via the node's ipatool local
|
|
732
|
+
* library (downloads on miss). `control` lane, fire-and-forget. */
|
|
733
|
+
installIosBundle(deviceId, bundleId) {
|
|
734
|
+
return this.control(deviceId, { op: "install", payload: { source: { kind: "ios_bundle", value: bundleId } } });
|
|
735
|
+
}
|
|
736
|
+
/** Search APKPure for installable apps (`GET /api/apps/search?q=`). */
|
|
737
|
+
searchApps(q) {
|
|
738
|
+
return this.c.get(`/api/apps/search?q=${encodeURIComponent(q)}`);
|
|
739
|
+
}
|
|
740
|
+
/** Install by APKPure package name; the backend resolves the download url and
|
|
741
|
+
* the node downloads + installs (apk or xapk). `POST /api/devices/:id/install-app`. */
|
|
742
|
+
installByPackage(deviceId, pkg, version) {
|
|
743
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/install-app`, { package: pkg, version });
|
|
744
|
+
}
|
|
745
|
+
/** Install an apk/xapk already uploaded to R2 (key from `filesUploadUrl`). */
|
|
746
|
+
installByKey(deviceId, key) {
|
|
747
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/install-app`, { r2_key: key });
|
|
748
|
+
}
|
|
749
|
+
lease(deviceId) {
|
|
750
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/lease`);
|
|
751
|
+
}
|
|
752
|
+
release(deviceId) {
|
|
753
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/release`);
|
|
754
|
+
}
|
|
755
|
+
claim(deviceId) {
|
|
756
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/claim`);
|
|
757
|
+
}
|
|
758
|
+
// ── 回收站 / 软删除(058) ──
|
|
759
|
+
/** 软删除(移入回收站)—`DELETE /api/devices/:id`。在线节点即时销毁容器,
|
|
760
|
+
* 离线节点标记 pending_destroy,待节点重连对账。3 天后自动彻底清除。 */
|
|
761
|
+
delete(deviceId) {
|
|
762
|
+
return this.c.del(`/api/devices/${encodeURIComponent(deviceId)}`);
|
|
763
|
+
}
|
|
764
|
+
/** 列回收站内的设备(`GET /api/devices/recycle`)。 */
|
|
765
|
+
recycle() {
|
|
766
|
+
return this.c.get("/api/devices/recycle");
|
|
767
|
+
}
|
|
768
|
+
/** 从回收站恢复(`POST /api/devices/:id/restore`)。容器已销毁的虚拟设备会按存储 spec 重建。 */
|
|
769
|
+
restore(deviceId) {
|
|
770
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/restore`);
|
|
771
|
+
}
|
|
772
|
+
/** 彻底删除(`DELETE /api/devices/:id/purge`)。不可恢复。 */
|
|
773
|
+
purge(deviceId) {
|
|
774
|
+
return this.c.del(`/api/devices/${encodeURIComponent(deviceId)}/purge`);
|
|
775
|
+
}
|
|
776
|
+
/** 清空回收站(`DELETE /api/devices/recycle`)。彻底删除当前用户所有回收站设备。 */
|
|
777
|
+
purgeAll() {
|
|
778
|
+
return this.c.del("/api/devices/recycle");
|
|
779
|
+
}
|
|
780
|
+
/** Update mutable device fields — `PATCH /api/devices/:id`.
|
|
781
|
+
* `ios_transport` switches an iOS device's live-mirror VIDEO source
|
|
782
|
+
* (`replaykit` device-app WebRTC, default | `iosusbadapter` host screenshot
|
|
783
|
+
* over usbmux, driven by go-ios); control is always host-mediated and
|
|
784
|
+
* unaffected. (Legacy value `pymobiledevice3` was renamed to `iosusbadapter`.)
|
|
785
|
+
* `sleep_timeout_min` sets the inactivity sleep timeout (0 = disabled). */
|
|
786
|
+
update(deviceId, patch) {
|
|
787
|
+
return this.c.patch(`/api/devices/${encodeURIComponent(deviceId)}`, patch);
|
|
788
|
+
}
|
|
789
|
+
/** Update only the device proxy and retain a typed patch receipt. */
|
|
790
|
+
updateProxy(deviceId, proxy) {
|
|
791
|
+
return this.c.patch(`/api/devices/${encodeURIComponent(deviceId)}`, { proxy });
|
|
792
|
+
}
|
|
793
|
+
/** Probe the device's configured proxy and report its egress IP/connectivity. */
|
|
794
|
+
probeProxy(deviceId) {
|
|
795
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/proxy/probe`, {});
|
|
796
|
+
}
|
|
797
|
+
/** 休眠设备 (`POST /api/devices/:id/sleep`)。
|
|
798
|
+
* 虚拟机:截图 + 快照 + 停机(关计费)。真机:锁屏(best-effort)。 */
|
|
799
|
+
sleep(deviceId) {
|
|
800
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/sleep`);
|
|
801
|
+
}
|
|
802
|
+
/** 唤醒设备 (`POST /api/devices/:id/wakeup`)。
|
|
803
|
+
* 虚拟机:恢复快照 + 重建计费。真机:发唤醒键(best-effort)。 */
|
|
804
|
+
wakeup(deviceId) {
|
|
805
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/wakeup`);
|
|
806
|
+
}
|
|
807
|
+
// ── 设备指纹 (device fingerprint / device_profile) ──
|
|
808
|
+
/** Current fingerprint + curated editable subset + dropdown choices + apply
|
|
809
|
+
* semantics (`GET /api/devices/:id/profile`). */
|
|
810
|
+
getProfile(deviceId) {
|
|
811
|
+
return this.c.get(`/api/devices/${encodeURIComponent(deviceId)}/profile`);
|
|
812
|
+
}
|
|
813
|
+
/** One-click regenerate the fingerprint (fresh identifiers). Optional
|
|
814
|
+
* `model`/`android_version` constrain the catalog base. */
|
|
815
|
+
regenerateProfile(deviceId, opts = {}) {
|
|
816
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/profile`, { regenerate: true, ...opts });
|
|
817
|
+
}
|
|
818
|
+
/** Apply curated edits onto the current fingerprint, keeping unique
|
|
819
|
+
* identifiers (`POST /api/devices/:id/profile`). */
|
|
820
|
+
setProfileOverrides(deviceId, overrides) {
|
|
821
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/profile`, { overrides });
|
|
822
|
+
}
|
|
823
|
+
/** Real-device only: set which apps the LSPosed spoof module disguises (and
|
|
824
|
+
* force-stops on re-push for immediate effect). Persisted to `meta.spec.scope_pkgs`
|
|
825
|
+
* and pushed live with the current fingerprint (`POST /api/devices/:id/profile`). */
|
|
826
|
+
setProfileScope(deviceId, scopePkgs) {
|
|
827
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/profile`, { scope_pkgs: scopePkgs });
|
|
828
|
+
}
|
|
829
|
+
/** Real-device only: **start** the first-time install of the LSPosed identity
|
|
830
|
+
* layer (+ spoof module) on a rooted device (`POST /api/devices/:id/anti-detect`,
|
|
831
|
+
* `202`). The multi-reboot install (~minutes) runs in the background — poll
|
|
832
|
+
* `getProfile().disguise_install_status` (`installing`→`installed`/`failed`).
|
|
833
|
+
* After install, fingerprint edits push live via `setProfileOverrides`/`regenerateProfile`. */
|
|
834
|
+
installAntiDetect(deviceId) {
|
|
835
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/anti-detect`, {});
|
|
836
|
+
}
|
|
837
|
+
/** Real-device only: toggle the disguise on/off WITHOUT uninstalling LSPosed (停用伪装).
|
|
838
|
+
* `enabled:false` empties the on-device spoof config + relaunches scoped apps so they read
|
|
839
|
+
* the genuine identity; `enabled:true` re-pushes the stored fingerprint. App-layer effect is
|
|
840
|
+
* immediate; `resetprop` props (model/hardware) fully revert only on the next device reboot.
|
|
841
|
+
* Requires the anti-detect layer installed (`POST /api/devices/:id/disguise`). */
|
|
842
|
+
setDisguiseEnabled(deviceId, enabled) {
|
|
843
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/disguise`, { enabled });
|
|
844
|
+
}
|
|
845
|
+
// 文件传输(R2 临时中转):字节直传 R2,控制面只过 presigned URL。
|
|
846
|
+
/** 申请直传 R2 的 presigned PUT;返回 `key` 供后续 `filesPush` 引用。 */
|
|
847
|
+
filesUploadUrl(deviceId, filename) {
|
|
848
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/files/upload-url`, { filename });
|
|
849
|
+
}
|
|
850
|
+
/** 让节点从 R2 拉 `key`、adb push 到设备 `dest`;成功后后端删除 R2 对象。 */
|
|
851
|
+
filesPush(deviceId, key, dest) {
|
|
852
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/files/push`, { key, dest });
|
|
853
|
+
}
|
|
854
|
+
/** 让节点 adb pull 设备 `src`、PUT 到 R2;返回 presigned GET 供客户端下载。 */
|
|
855
|
+
filesPull(deviceId, src, filename) {
|
|
856
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/files/pull`, { src, filename });
|
|
857
|
+
}
|
|
858
|
+
// 设备会话(DataChannel 多路复用):开一条通用点对点会话,字节/命令走 WebRTC
|
|
859
|
+
// 不经服务端。会话本身是通用的(文件管理 + 终端 + 传输都在一条 DC 上多路复用),
|
|
860
|
+
// 故不再带 direction/dest/src —— 具体操作在 DataChannel 上以 `{id,t,...}` 驱动。
|
|
861
|
+
/** 开一条设备会话:让后端编排目标节点 join `/ws/signal`(节点 = offerer,建
|
|
862
|
+
* "ctrl" DataChannel),返回 `session_id`(SDK 据此 mint viewer token、作
|
|
863
|
+
* answerer 建链、经 `ondatachannel` 拿到 "ctrl" 通道)。这是 DataChannel 多路
|
|
864
|
+
* 复用会话(通道 A)的握手入口;push/pull 失败时由上层回退到 R2 中转
|
|
865
|
+
* (`filesUploadUrl` / `filesPush` / `filesPull`)。 */
|
|
866
|
+
dcSession(deviceId) {
|
|
867
|
+
return this.c.post(`/api/devices/${encodeURIComponent(deviceId)}/dc-session`, {});
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// src/core/resources/nodes.ts
|
|
872
|
+
var NodesApi = class {
|
|
873
|
+
constructor(c, cfg) {
|
|
874
|
+
this.c = c;
|
|
875
|
+
this.cfg = cfg;
|
|
876
|
+
}
|
|
877
|
+
list() {
|
|
878
|
+
return this.c.get("/api/nodes");
|
|
879
|
+
}
|
|
880
|
+
/** Register a node. Accepts a name or a full input ({ name, scope?, region? }). */
|
|
881
|
+
register(input) {
|
|
882
|
+
const body = typeof input === "string" ? { name: input } : input;
|
|
883
|
+
return this.c.post("/api/nodes", body);
|
|
884
|
+
}
|
|
885
|
+
/** Update name/scope/region of an owned node (`PATCH /api/nodes/:id`). */
|
|
886
|
+
update(nodeId, patch) {
|
|
887
|
+
return this.c.patch(`/api/nodes/${nodeId}`, patch);
|
|
888
|
+
}
|
|
889
|
+
/** Rotate the node token, returning a fresh one (`POST /api/nodes/:id/token`). */
|
|
890
|
+
rotateToken(nodeId) {
|
|
891
|
+
return this.c.post(`/api/nodes/${nodeId}/token`, {});
|
|
892
|
+
}
|
|
893
|
+
/** Delete (revoke) an owned node (`DELETE /api/nodes/:id`). */
|
|
894
|
+
remove(nodeId) {
|
|
895
|
+
return this.c.del(`/api/nodes/${nodeId}`);
|
|
896
|
+
}
|
|
897
|
+
/** Liveness/latency probe (`POST /api/nodes/:id/probe`). Round-trips a ping to
|
|
898
|
+
* the node; `online:false` when unreachable. */
|
|
899
|
+
probe(nodeId) {
|
|
900
|
+
return this.c.post(`/api/nodes/${nodeId}/probe`, {});
|
|
901
|
+
}
|
|
902
|
+
/** Ask an owned node to restart itself (`POST /api/nodes/:id/restart`). */
|
|
903
|
+
restart(nodeId) {
|
|
904
|
+
return this.c.post(`/api/nodes/${nodeId}/restart`, {});
|
|
905
|
+
}
|
|
906
|
+
/** Persist the only remotely configurable runtime value. The node validates
|
|
907
|
+
* the native absolute path and returns a restart-required receipt. */
|
|
908
|
+
setDataRoot(nodeId, dataRoot) {
|
|
909
|
+
return this.c.post(`/api/nodes/${nodeId}/runtime/data-root`, { data_root: dataRoot });
|
|
910
|
+
}
|
|
911
|
+
/** Submit a durable node-owned agent task (`POST /api/agent-tasks`). */
|
|
912
|
+
submitAgentTask(input) {
|
|
913
|
+
return this.c.post("/api/agent-tasks", input);
|
|
914
|
+
}
|
|
915
|
+
/** Reload a durable AgentTask, including each target's current attempt. */
|
|
916
|
+
getAgentTask(taskId) {
|
|
917
|
+
return this.c.get(`/api/agent-tasks/${taskId}`);
|
|
918
|
+
}
|
|
919
|
+
/** Explicit read alias for callers that use CRUD-style naming. */
|
|
920
|
+
readAgentTask(taskId) {
|
|
921
|
+
return this.getAgentTask(taskId);
|
|
922
|
+
}
|
|
923
|
+
/** Stop one stable target without cancelling the other devices in its task. */
|
|
924
|
+
cancelAgentTaskTarget(taskId, targetId) {
|
|
925
|
+
return this.c.post(`/api/agent-tasks/${taskId}/targets/${targetId}/cancel`, {});
|
|
926
|
+
}
|
|
927
|
+
/** Create a new immutable attempt for a failed/cancelled target. */
|
|
928
|
+
retryAgentTaskTarget(taskId, targetId) {
|
|
929
|
+
return this.c.post(`/api/agent-tasks/${taskId}/targets/${targetId}/retry`, {});
|
|
930
|
+
}
|
|
931
|
+
/** Resolve the target's current approval gate. */
|
|
932
|
+
approveAgentTaskTarget(taskId, targetId) {
|
|
933
|
+
return this.c.post(`/api/agent-tasks/${taskId}/targets/${targetId}/approve`, {});
|
|
934
|
+
}
|
|
935
|
+
/** Fetch cursor-ordered events missed while the task stream was disconnected. */
|
|
936
|
+
agentTaskEvents(taskId, after) {
|
|
937
|
+
return this.c.get(`/api/agent-tasks/${taskId}/events`, { query: { after } });
|
|
938
|
+
}
|
|
939
|
+
/** Devices the node currently reports, with claim status (`POST /:id/devices/discover`). */
|
|
940
|
+
discoverDevices(nodeId) {
|
|
941
|
+
return this.c.post(`/api/nodes/${nodeId}/devices/discover`, {});
|
|
942
|
+
}
|
|
943
|
+
/** Enumerate adb mDNS services on a node (`POST /api/nodes/:id/adb/scan`). */
|
|
944
|
+
adbScan(nodeId) {
|
|
945
|
+
return this.c.post(`/api/nodes/${nodeId}/adb/scan`, {});
|
|
946
|
+
}
|
|
947
|
+
/** Pair a wireless device (`POST /api/nodes/:id/adb/pair`). */
|
|
948
|
+
adbPair(nodeId, addr, code) {
|
|
949
|
+
return this.c.post(`/api/nodes/${nodeId}/adb/pair`, { addr, code });
|
|
950
|
+
}
|
|
951
|
+
/** Connect an already-paired wireless device (`POST /api/nodes/:id/adb/connect`). */
|
|
952
|
+
adbConnect(nodeId, addr) {
|
|
953
|
+
return this.c.post(`/api/nodes/${nodeId}/adb/connect`, { addr });
|
|
954
|
+
}
|
|
955
|
+
// ── iOS wireless (real-device lane) — node-scoped control verbs ──
|
|
956
|
+
// iOS pairing is the on-device "Trust" dialog (no IP+code like Android adb):
|
|
957
|
+
// scan → pair (USB trust) → wifi (enable wireless lane) → reachable wirelessly.
|
|
958
|
+
/** List connected iOS devices + their usbmux lanes (`{op:'scan'}`). */
|
|
959
|
+
iosScan(nodeId) {
|
|
960
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "scan" });
|
|
961
|
+
}
|
|
962
|
+
/** Trigger the on-device Trust dialog for a udid (`{op:'pair'}`). */
|
|
963
|
+
iosPair(nodeId, udid) {
|
|
964
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "pair", udid });
|
|
965
|
+
}
|
|
966
|
+
/** Toggle wifi-connections for a trusted udid (`{op:'wifi'}`). */
|
|
967
|
+
iosWifi(nodeId, udid, enable) {
|
|
968
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "wifi", udid, enable });
|
|
969
|
+
}
|
|
970
|
+
// ── iOS App Store account + library ──
|
|
971
|
+
/** 节点 iOS App Store 账号状态(`{op:'account_info'}`)。 */
|
|
972
|
+
iosAccount(nodeId) {
|
|
973
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "account_info" });
|
|
974
|
+
}
|
|
975
|
+
/** 登录节点 iOS 账号;`needs_2fa` 时带 `auth_code` 重试(`{op:'account_login'}`)。 */
|
|
976
|
+
iosAccountLogin(nodeId, body) {
|
|
977
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "account_login", ...body });
|
|
978
|
+
}
|
|
979
|
+
iosAccountLogout(nodeId) {
|
|
980
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "account_logout" });
|
|
981
|
+
}
|
|
982
|
+
/** App Store 搜索(`{op:'store_search'}`)。 */
|
|
983
|
+
iosStoreSearch(nodeId, term, limit = 20) {
|
|
984
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "store_search", term, limit });
|
|
985
|
+
}
|
|
986
|
+
/** 下载入节点本地库(`{op:'download'}`)。 */
|
|
987
|
+
iosDownload(nodeId, bundleId, name) {
|
|
988
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "download", bundle_id: bundleId, name });
|
|
989
|
+
}
|
|
990
|
+
/** 节点本地 IPA 库(已下载,模版多选来源)(`{op:'apps'}`)。 */
|
|
991
|
+
iosApps(nodeId) {
|
|
992
|
+
return this.c.post(`/api/nodes/${nodeId}/ios/control`, { op: "apps" });
|
|
993
|
+
}
|
|
994
|
+
// ── env base (redroid host) — trigger + stream over the shared /ws/execution lane ──
|
|
995
|
+
/** Configure the redroid host base on an owned node, streaming progress.
|
|
996
|
+
* `POST /api/nodes/:id/env/setup` → subscribe `/ws/execution/:req_id`. */
|
|
997
|
+
envSetup(nodeId, input = {}) {
|
|
998
|
+
return EnvRun.start(this.cfg, nodeId, "setup", input);
|
|
999
|
+
}
|
|
1000
|
+
/** Probe the host base without changing it (`POST /api/nodes/:id/env/status`). */
|
|
1001
|
+
envStatus(nodeId, input = {}) {
|
|
1002
|
+
return EnvRun.start(this.cfg, nodeId, "status", input);
|
|
1003
|
+
}
|
|
1004
|
+
/** Stop (or `purge`) the host base (`POST /api/nodes/:id/env/teardown`). */
|
|
1005
|
+
envTeardown(nodeId, input = {}) {
|
|
1006
|
+
return EnvRun.start(this.cfg, nodeId, "teardown", input);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// src/core/resources/clusters.ts
|
|
1011
|
+
var ClustersApi = class {
|
|
1012
|
+
constructor(c) {
|
|
1013
|
+
this.c = c;
|
|
1014
|
+
}
|
|
1015
|
+
list() {
|
|
1016
|
+
return this.c.get("/api/clusters");
|
|
1017
|
+
}
|
|
1018
|
+
create(input) {
|
|
1019
|
+
return this.c.post("/api/clusters", {
|
|
1020
|
+
name: input.name,
|
|
1021
|
+
description: input.description ?? null,
|
|
1022
|
+
device_ids: input.device_ids,
|
|
1023
|
+
default_mode: input.default_mode ?? { type: "broadcast" }
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
remove(id) {
|
|
1027
|
+
return this.c.del(`/api/clusters/${encodeURIComponent(id)}`);
|
|
1028
|
+
}
|
|
1029
|
+
setDevices(id, deviceIds) {
|
|
1030
|
+
return this.c.patch(`/api/clusters/${encodeURIComponent(id)}/devices`, { device_ids: deviceIds });
|
|
1031
|
+
}
|
|
1032
|
+
run(id, flow) {
|
|
1033
|
+
return this.c.post(`/api/clusters/${encodeURIComponent(id)}/run`, { flow });
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/core/resources/scripts.ts
|
|
1038
|
+
var ScriptsApi = class {
|
|
1039
|
+
constructor(c) {
|
|
1040
|
+
this.c = c;
|
|
1041
|
+
}
|
|
1042
|
+
list() {
|
|
1043
|
+
return this.c.get("/api/scripts");
|
|
1044
|
+
}
|
|
1045
|
+
get(id) {
|
|
1046
|
+
return this.c.get(`/api/scripts/${encodeURIComponent(id)}`);
|
|
1047
|
+
}
|
|
1048
|
+
create(script) {
|
|
1049
|
+
return this.c.post("/api/scripts", script);
|
|
1050
|
+
}
|
|
1051
|
+
update(id, script) {
|
|
1052
|
+
return this.c.put(`/api/scripts/${encodeURIComponent(id)}`, script);
|
|
1053
|
+
}
|
|
1054
|
+
/** Create when `script.id` is unset, else update. Returns the saved entity so
|
|
1055
|
+
* callers can capture the server-assigned `id` on first create — otherwise a
|
|
1056
|
+
* new script's id is lost and every later autosave re-creates it. */
|
|
1057
|
+
save(script) {
|
|
1058
|
+
return script.id ? this.update(script.id, script) : this.create(script);
|
|
1059
|
+
}
|
|
1060
|
+
remove(id) {
|
|
1061
|
+
return this.c.del(`/api/scripts/${encodeURIComponent(id)}`);
|
|
1062
|
+
}
|
|
1063
|
+
/** Change a script's visibility (`user` | `project` | `system`). */
|
|
1064
|
+
setVisibility(id, visibility) {
|
|
1065
|
+
return this.c.patch(`/api/scripts/${encodeURIComponent(id)}/visibility`, { visibility });
|
|
1066
|
+
}
|
|
1067
|
+
/** Manual: ask the text LLM for a title + description, written back to the script. */
|
|
1068
|
+
generateMeta(id) {
|
|
1069
|
+
return this.c.post(`/api/scripts/${encodeURIComponent(id)}/generate-meta`);
|
|
1070
|
+
}
|
|
1071
|
+
/** Manual: generate a cover/icon image, store it, and write back stable URLs. */
|
|
1072
|
+
generateCover(id) {
|
|
1073
|
+
return this.c.post(`/api/scripts/${encodeURIComponent(id)}/generate-cover`);
|
|
1074
|
+
}
|
|
1075
|
+
async duplicate(id) {
|
|
1076
|
+
const src = await this.get(id);
|
|
1077
|
+
return this.create({ ...src, id: void 0, name: `${src.name} copy` });
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/core/resources/shortcuts.ts
|
|
1082
|
+
var ShortcutsApi = class {
|
|
1083
|
+
constructor(c) {
|
|
1084
|
+
this.c = c;
|
|
1085
|
+
}
|
|
1086
|
+
list(platform) {
|
|
1087
|
+
const q = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
|
1088
|
+
return this.c.get(`/api/shortcuts${q}`);
|
|
1089
|
+
}
|
|
1090
|
+
create(body) {
|
|
1091
|
+
return this.c.post("/api/shortcuts", body);
|
|
1092
|
+
}
|
|
1093
|
+
update(id, body) {
|
|
1094
|
+
return this.c.put(`/api/shortcuts/${encodeURIComponent(id)}`, body);
|
|
1095
|
+
}
|
|
1096
|
+
remove(id) {
|
|
1097
|
+
return this.c.del(`/api/shortcuts/${encodeURIComponent(id)}`);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// src/core/resources/locators.ts
|
|
1102
|
+
var LocatorsApi = class {
|
|
1103
|
+
constructor(c) {
|
|
1104
|
+
this.c = c;
|
|
1105
|
+
}
|
|
1106
|
+
list() {
|
|
1107
|
+
return this.c.get("/api/locators");
|
|
1108
|
+
}
|
|
1109
|
+
create(input) {
|
|
1110
|
+
return this.c.post("/api/locators", input);
|
|
1111
|
+
}
|
|
1112
|
+
remove(id) {
|
|
1113
|
+
return this.c.del(`/api/locators/${encodeURIComponent(id)}`);
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// src/core/resources/fleet.ts
|
|
1118
|
+
var TERMINAL_RUN = /* @__PURE__ */ new Set(["done", "failed", "partial", "cancelled", "canceled"]);
|
|
1119
|
+
var TERMINAL_DEVICE = /* @__PURE__ */ new Set(["done", "failed", "cancelled", "canceled"]);
|
|
1120
|
+
function isFleetRunTerminal(detail) {
|
|
1121
|
+
if (detail.status && TERMINAL_RUN.has(detail.status)) return true;
|
|
1122
|
+
const devices = detail.devices ?? [];
|
|
1123
|
+
return devices.length > 0 && devices.every((d) => TERMINAL_DEVICE.has(d.status));
|
|
1124
|
+
}
|
|
1125
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1126
|
+
var FleetApi = class {
|
|
1127
|
+
constructor(c) {
|
|
1128
|
+
this.c = c;
|
|
1129
|
+
}
|
|
1130
|
+
start(body) {
|
|
1131
|
+
return this.c.post("/api/fleet/runs", {
|
|
1132
|
+
script: body.script,
|
|
1133
|
+
mode: body.mode ?? "Broadcast",
|
|
1134
|
+
device_ids: body.device_ids
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
get(runId) {
|
|
1138
|
+
return this.c.get(`/api/fleet/runs/${encodeURIComponent(runId)}`);
|
|
1139
|
+
}
|
|
1140
|
+
/** Poll `get(runId)` until the run is terminal (see {@link isFleetRunTerminal})
|
|
1141
|
+
* or `timeoutMs` elapses. Always resolves with the latest `FleetRunDetail`
|
|
1142
|
+
* (never rejects on timeout); inspect `isFleetRunTerminal(detail)` to tell a
|
|
1143
|
+
* finished run from a polling-cap hit. Used to turn the fire-and-forget
|
|
1144
|
+
* `start()` into a real receipt for shortcut/copy runs. */
|
|
1145
|
+
async waitForRun(runId, opts = {}) {
|
|
1146
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1147
|
+
const intervalMs = opts.intervalMs ?? 800;
|
|
1148
|
+
const deadline = Date.now() + timeoutMs;
|
|
1149
|
+
let detail = await this.get(runId);
|
|
1150
|
+
while (!isFleetRunTerminal(detail) && Date.now() < deadline) {
|
|
1151
|
+
await sleep(intervalMs);
|
|
1152
|
+
detail = await this.get(runId);
|
|
1153
|
+
}
|
|
1154
|
+
return detail;
|
|
1155
|
+
}
|
|
1156
|
+
cancel(runId) {
|
|
1157
|
+
return this.c.post(`/api/fleet/runs/${encodeURIComponent(runId)}/cancel`);
|
|
1158
|
+
}
|
|
1159
|
+
pause(runId) {
|
|
1160
|
+
return this.c.post(`/api/fleet/runs/${encodeURIComponent(runId)}/pause`);
|
|
1161
|
+
}
|
|
1162
|
+
resume(runId) {
|
|
1163
|
+
return this.c.post(`/api/fleet/runs/${encodeURIComponent(runId)}/resume`);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
// src/core/resources/tasks.ts
|
|
1168
|
+
var TasksApi = class {
|
|
1169
|
+
constructor(c) {
|
|
1170
|
+
this.c = c;
|
|
1171
|
+
}
|
|
1172
|
+
/** Current status of one task (`GET /api/webhook/tasks/:id`). */
|
|
1173
|
+
get(id) {
|
|
1174
|
+
return this.c.get(`/api/webhook/tasks/${encodeURIComponent(id)}`);
|
|
1175
|
+
}
|
|
1176
|
+
/** List the caller-project's tasks, newest first (paginated, filterable). */
|
|
1177
|
+
list(filter) {
|
|
1178
|
+
return this.c.get("/api/webhook/tasks", {
|
|
1179
|
+
query: {
|
|
1180
|
+
device_id: filter?.deviceId,
|
|
1181
|
+
status: filter?.status,
|
|
1182
|
+
limit: filter?.limit,
|
|
1183
|
+
offset: filter?.offset
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/** Cancel a task (DB-side: not-yet-terminal targets → cancelled). */
|
|
1188
|
+
cancel(id) {
|
|
1189
|
+
return this.c.post(`/api/webhook/tasks/${encodeURIComponent(id)}/cancel`);
|
|
1190
|
+
}
|
|
1191
|
+
/** Pause a task: still-`pending` targets → `paused` so nodes skip them
|
|
1192
|
+
* (`pending_for_external` only pulls `pending`). In-flight targets keep
|
|
1193
|
+
* running — pause only holds back not-yet-started work. */
|
|
1194
|
+
pause(id) {
|
|
1195
|
+
return this.c.post(`/api/webhook/tasks/${encodeURIComponent(id)}/pause`);
|
|
1196
|
+
}
|
|
1197
|
+
/** Resume a paused task: `paused` targets → `pending` so nodes pick them up again. */
|
|
1198
|
+
resume(id) {
|
|
1199
|
+
return this.c.post(`/api/webhook/tasks/${encodeURIComponent(id)}/resume`);
|
|
1200
|
+
}
|
|
1201
|
+
/** Persisted event history for a task, newest first. */
|
|
1202
|
+
events(id, opts) {
|
|
1203
|
+
return this.c.get(`/api/webhook/tasks/${encodeURIComponent(id)}/events`, {
|
|
1204
|
+
query: { limit: opts?.limit, offset: opts?.offset }
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// src/core/resources/webhooks.ts
|
|
1210
|
+
var WebhooksApi = class {
|
|
1211
|
+
constructor(c) {
|
|
1212
|
+
this.c = c;
|
|
1213
|
+
}
|
|
1214
|
+
list() {
|
|
1215
|
+
return this.c.get("/api/webhooks/configs", { scope: "developer" }).then((r) => r.webhooks ?? []);
|
|
1216
|
+
}
|
|
1217
|
+
create(input) {
|
|
1218
|
+
return this.c.post("/api/webhooks/configs", input, { scope: "developer" });
|
|
1219
|
+
}
|
|
1220
|
+
setEnabled(id, enabled) {
|
|
1221
|
+
return this.c.patch(`/api/webhooks/configs/${encodeURIComponent(id)}`, { enabled }, { scope: "developer" });
|
|
1222
|
+
}
|
|
1223
|
+
remove(id) {
|
|
1224
|
+
return this.c.del(`/api/webhooks/configs/${encodeURIComponent(id)}`, { scope: "developer" });
|
|
1225
|
+
}
|
|
1226
|
+
trigger(eventType, payload) {
|
|
1227
|
+
return this.c.post("/api/webhooks/trigger", { event_type: eventType, payload }, { scope: "developer" });
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
// src/core/resources/llm.ts
|
|
1232
|
+
var LlmApi = class {
|
|
1233
|
+
constructor(c) {
|
|
1234
|
+
this.c = c;
|
|
1235
|
+
}
|
|
1236
|
+
test(opts) {
|
|
1237
|
+
return this.c.post(
|
|
1238
|
+
"/api/llm/test",
|
|
1239
|
+
{ token: opts?.token, model: opts?.model, base_url: opts?.baseUrl },
|
|
1240
|
+
{ scope: "developer" }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// src/core/resources/admin.ts
|
|
1246
|
+
var AdminApi = class {
|
|
1247
|
+
constructor(c) {
|
|
1248
|
+
this.c = c;
|
|
1249
|
+
}
|
|
1250
|
+
// rates
|
|
1251
|
+
listRates() {
|
|
1252
|
+
return this.c.get("/api/admin/rates", { scope: "admin" });
|
|
1253
|
+
}
|
|
1254
|
+
upsertRate(body) {
|
|
1255
|
+
return this.c.put("/api/admin/rates", body, { scope: "admin" });
|
|
1256
|
+
}
|
|
1257
|
+
// project rebate / node platform flag
|
|
1258
|
+
setRebate(projectId, ratio) {
|
|
1259
|
+
return this.c.put(`/api/admin/projects/${encodeURIComponent(projectId)}/rebate`, { ratio }, { scope: "admin" });
|
|
1260
|
+
}
|
|
1261
|
+
setNodePlatform(nodeId, isPlatform) {
|
|
1262
|
+
return this.c.put(`/api/admin/nodes/${encodeURIComponent(nodeId)}/platform`, { is_platform: isPlatform }, { scope: "admin" });
|
|
1263
|
+
}
|
|
1264
|
+
// plans + reports
|
|
1265
|
+
listPlans() {
|
|
1266
|
+
return this.c.get("/api/admin/plans", { scope: "admin" });
|
|
1267
|
+
}
|
|
1268
|
+
createPlan(body) {
|
|
1269
|
+
return this.c.put("/api/admin/plans", body, { scope: "admin" });
|
|
1270
|
+
}
|
|
1271
|
+
usage() {
|
|
1272
|
+
return this.c.get("/api/admin/billing/usage", { scope: "admin" });
|
|
1273
|
+
}
|
|
1274
|
+
ledger() {
|
|
1275
|
+
return this.c.get("/api/admin/billing/ledger", { scope: "admin" });
|
|
1276
|
+
}
|
|
1277
|
+
// platform OEM (white-label defaults every project inherits)
|
|
1278
|
+
getOem() {
|
|
1279
|
+
return this.c.get("/api/admin/oem", { scope: "admin" });
|
|
1280
|
+
}
|
|
1281
|
+
putOem(body) {
|
|
1282
|
+
return this.c.put("/api/admin/oem", body, { scope: "admin" });
|
|
1283
|
+
}
|
|
1284
|
+
// image artifacts
|
|
1285
|
+
listImages() {
|
|
1286
|
+
return this.c.get("/api/images", { scope: "admin" });
|
|
1287
|
+
}
|
|
1288
|
+
upsertImage(body) {
|
|
1289
|
+
return this.c.post("/api/images", body, { scope: "admin" });
|
|
1290
|
+
}
|
|
1291
|
+
setImageActive(id, active) {
|
|
1292
|
+
return this.c.put(`/api/images/${encodeURIComponent(id)}/active`, { active }, { scope: "admin" });
|
|
1293
|
+
}
|
|
1294
|
+
deleteImage(id) {
|
|
1295
|
+
return this.c.del(`/api/images/${encodeURIComponent(id)}`, { scope: "admin" });
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// src/core/resources/images.ts
|
|
1300
|
+
var ImagesApi = class {
|
|
1301
|
+
constructor(c) {
|
|
1302
|
+
this.c = c;
|
|
1303
|
+
}
|
|
1304
|
+
/** Provisionable versions for a platform, drawn from the active catalog
|
|
1305
|
+
* (`GET /api/images/options`). Omit `platform` for all platforms. */
|
|
1306
|
+
options(platform) {
|
|
1307
|
+
const q = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
|
1308
|
+
return this.c.get(`/api/images/options${q}`);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// src/core/resources/billing.ts
|
|
1313
|
+
var BillingApi = class {
|
|
1314
|
+
constructor(c) {
|
|
1315
|
+
this.c = c;
|
|
1316
|
+
}
|
|
1317
|
+
/** 当前钱包余额 + 最近流水(`GET /api/billing/balance`)。 */
|
|
1318
|
+
balance() {
|
|
1319
|
+
return this.c.get("/api/billing/balance");
|
|
1320
|
+
}
|
|
1321
|
+
/** 名下项目订阅列表(`GET /api/billing/subscriptions`)。 */
|
|
1322
|
+
subscriptions() {
|
|
1323
|
+
return this.c.get("/api/billing/subscriptions");
|
|
1324
|
+
}
|
|
1325
|
+
/** 订阅一个套餐(`POST /api/billing/subscriptions`)。`plan_id` 由调用方传入。 */
|
|
1326
|
+
subscribe(input) {
|
|
1327
|
+
return this.c.post("/api/billing/subscriptions", input);
|
|
1328
|
+
}
|
|
1329
|
+
/** 自有节点日费(`GET /api/billing/own-node-charges`)。 */
|
|
1330
|
+
ownNodeCharges() {
|
|
1331
|
+
return this.c.get("/api/billing/own-node-charges");
|
|
1332
|
+
}
|
|
1333
|
+
/** 充值到指定用户钱包(`POST /api/admin/billing/balance/topup`,admin scope)。 */
|
|
1334
|
+
topup(input) {
|
|
1335
|
+
return this.c.post("/api/admin/billing/balance/topup", input, { scope: "admin" });
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// src/core/resources/tokens.ts
|
|
1340
|
+
var ApiTokensApi = class {
|
|
1341
|
+
constructor(c) {
|
|
1342
|
+
this.c = c;
|
|
1343
|
+
}
|
|
1344
|
+
/** 列出名下令牌(无明文/hash;`GET /api/tokens`)。 */
|
|
1345
|
+
list() {
|
|
1346
|
+
return this.c.get("/api/tokens");
|
|
1347
|
+
}
|
|
1348
|
+
/** 创建一个 PAT(`POST /api/tokens`);响应 `token` 明文仅此一次。 */
|
|
1349
|
+
create(input) {
|
|
1350
|
+
return this.c.post("/api/tokens", input);
|
|
1351
|
+
}
|
|
1352
|
+
/** 撤销一个 PAT(`DELETE /api/tokens/:id`,204)。 */
|
|
1353
|
+
revoke(id) {
|
|
1354
|
+
return this.c.del(`/api/tokens/${encodeURIComponent(id)}`);
|
|
1355
|
+
}
|
|
1356
|
+
/** 轮换一个 PAT(重签明文,旧 hash 失效;`POST /api/tokens/:id/rotate`)。 */
|
|
1357
|
+
rotate(id) {
|
|
1358
|
+
return this.c.post(`/api/tokens/${encodeURIComponent(id)}/rotate`);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/core/local-node.ts
|
|
1363
|
+
function toWs(httpBase) {
|
|
1364
|
+
return httpBase.replace(/^http/, "ws");
|
|
1365
|
+
}
|
|
1366
|
+
var LocalNodeController = class {
|
|
1367
|
+
constructor(client, opts) {
|
|
1368
|
+
this.client = client;
|
|
1369
|
+
this.opts = opts;
|
|
1370
|
+
/** The node id this machine is bound to once `ensure()` has run. */
|
|
1371
|
+
this.nodeId = null;
|
|
1372
|
+
/** Last best-effort failure captured by `ensure()`, for host UIs that want to surface it. */
|
|
1373
|
+
this.lastError = null;
|
|
1374
|
+
}
|
|
1375
|
+
async startHost(token, backendWs) {
|
|
1376
|
+
const opts = { token, backendWs };
|
|
1377
|
+
if (this.opts.requireSdkUpdate !== void 0) opts.requireSdkUpdate = this.opts.requireSdkUpdate;
|
|
1378
|
+
await this.opts.host.start(opts);
|
|
1379
|
+
}
|
|
1380
|
+
get scope() {
|
|
1381
|
+
return this.opts.scope ?? "personal";
|
|
1382
|
+
}
|
|
1383
|
+
get backendWs() {
|
|
1384
|
+
return this.opts.backendWs ?? toWs(this.client.cfg.baseUrl);
|
|
1385
|
+
}
|
|
1386
|
+
/** Ensure THIS machine runs as its OWN node owned by the signed-in caller,
|
|
1387
|
+
* starting it via the host. The node is keyed by machine name (`nameHint`), so
|
|
1388
|
+
* each of the user's machines gets a distinct node (no shared-node churn).
|
|
1389
|
+
* Idempotent: keeps a running match, else restarts the persisted/rotated node,
|
|
1390
|
+
* else registers one named after the machine. Returns the bound node id, or
|
|
1391
|
+
* null on failure (best-effort). */
|
|
1392
|
+
async ensure() {
|
|
1393
|
+
const { host } = this.opts;
|
|
1394
|
+
this.lastError = null;
|
|
1395
|
+
try {
|
|
1396
|
+
const status = await host.status();
|
|
1397
|
+
const ws = this.backendWs;
|
|
1398
|
+
const nodes = await this.client.nodes.list();
|
|
1399
|
+
const mine = nodes.filter((n) => n.is_owner && n.scope === this.scope);
|
|
1400
|
+
const machine = this.opts.nameHint ?? "desktop-node";
|
|
1401
|
+
const ours = mine.find((n) => n.name === machine);
|
|
1402
|
+
if (status.running) {
|
|
1403
|
+
if (status.nodeId && ours && status.nodeId === ours.id) {
|
|
1404
|
+
return await this.bind(status.nodeId);
|
|
1405
|
+
}
|
|
1406
|
+
await host.stop();
|
|
1407
|
+
}
|
|
1408
|
+
if (status.hasToken && status.nodeId && ours && status.nodeId === ours.id) {
|
|
1409
|
+
await this.startHost(null, ws);
|
|
1410
|
+
return await this.bind(status.nodeId);
|
|
1411
|
+
}
|
|
1412
|
+
if (ours) {
|
|
1413
|
+
const r2 = await this.client.nodes.rotateToken(ours.id);
|
|
1414
|
+
await this.startHost(r2.token, ws);
|
|
1415
|
+
return await this.bind(ours.id);
|
|
1416
|
+
}
|
|
1417
|
+
const r = await this.client.nodes.register({ name: machine, scope: this.scope });
|
|
1418
|
+
await this.startHost(r.token, ws);
|
|
1419
|
+
return await this.bind(r.id);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
this.lastError = e;
|
|
1422
|
+
console.warn("[localNode] ensure failed:", e);
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/** Record the bound node id and, in headless `'auto'` mode, ensure its redroid
|
|
1427
|
+
* host base is configured before returning. Always returns the id. */
|
|
1428
|
+
async bind(id) {
|
|
1429
|
+
this.nodeId = id;
|
|
1430
|
+
await this.maybeAutoEnv();
|
|
1431
|
+
return id;
|
|
1432
|
+
}
|
|
1433
|
+
/** Resolve the env OS: explicit option wins, else auto-detect; `null` (Linux/
|
|
1434
|
+
* unknown) means "no base needed" and the gate is skipped. */
|
|
1435
|
+
get envOs() {
|
|
1436
|
+
return this.opts.envOs !== void 0 ? this.opts.envOs : detectEnvOs();
|
|
1437
|
+
}
|
|
1438
|
+
/** HEADLESS auto env-init: probe `env/status` on the bound node and, if the
|
|
1439
|
+
* redroid base is missing, run `env/setup` to completion — no confirmation.
|
|
1440
|
+
* No-op unless `envInit: 'auto'` and the OS needs a base. Best-effort. */
|
|
1441
|
+
async maybeAutoEnv() {
|
|
1442
|
+
if (this.opts.envInit !== "auto" || !this.nodeId) return;
|
|
1443
|
+
const os = this.envOs;
|
|
1444
|
+
if (os !== "win" && os !== "mac") return;
|
|
1445
|
+
try {
|
|
1446
|
+
const status = await this.runEnv("status", os);
|
|
1447
|
+
if (status) return;
|
|
1448
|
+
await this.runEnv("setup", os);
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
console.warn("[localNode] env auto-init failed:", e);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/** Run an env action on the bound node to completion. For `'status'` returns
|
|
1454
|
+
* whether the base is already ready; for `'setup'` returns whether it ok'd. */
|
|
1455
|
+
async runEnv(action, os) {
|
|
1456
|
+
const nodeId = this.nodeId;
|
|
1457
|
+
const run = action === "status" ? await this.client.nodes.envStatus(nodeId, { os }) : await this.client.nodes.envSetup(nodeId, { os, route: this.opts.envRoute });
|
|
1458
|
+
const events = [];
|
|
1459
|
+
return new Promise((resolve) => {
|
|
1460
|
+
run.on("event", (e) => events.push(e));
|
|
1461
|
+
run.on("done", () => resolve(summarizeEnv(events).ready));
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
/** Stop the local node (e.g. on logout). Best-effort. */
|
|
1465
|
+
async stop() {
|
|
1466
|
+
try {
|
|
1467
|
+
await this.opts.host.stop();
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
console.warn("[localNode] stop failed:", e);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/** Devices connected to THIS local node — `devices.list()` filtered by node id.
|
|
1473
|
+
* Empty until `ensure()` has bound `nodeId`. */
|
|
1474
|
+
async listDevices() {
|
|
1475
|
+
if (!this.nodeId) return [];
|
|
1476
|
+
const all = await this.client.devices.list();
|
|
1477
|
+
return all.filter((d) => d.node_id === this.nodeId);
|
|
1478
|
+
}
|
|
1479
|
+
/** Subscribe to this node's device list (polled). Fires immediately, then every
|
|
1480
|
+
* `pollMs`. Returns an unsubscribe fn. */
|
|
1481
|
+
onDevices(cb) {
|
|
1482
|
+
let alive = true;
|
|
1483
|
+
const tick = () => {
|
|
1484
|
+
this.listDevices().then((d) => {
|
|
1485
|
+
if (alive) cb(d);
|
|
1486
|
+
}).catch(() => {
|
|
1487
|
+
});
|
|
1488
|
+
};
|
|
1489
|
+
tick();
|
|
1490
|
+
const id = setInterval(tick, this.opts.pollMs ?? 4e3);
|
|
1491
|
+
return () => {
|
|
1492
|
+
alive = false;
|
|
1493
|
+
clearInterval(id);
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
// src/core/resources/app-templates.ts
|
|
1499
|
+
var AppTemplatesApi = class {
|
|
1500
|
+
constructor(c) {
|
|
1501
|
+
this.c = c;
|
|
1502
|
+
}
|
|
1503
|
+
list(platform) {
|
|
1504
|
+
const q = platform ? `?platform=${platform}` : "";
|
|
1505
|
+
return this.c.get(`/api/app-templates${q}`);
|
|
1506
|
+
}
|
|
1507
|
+
create(body) {
|
|
1508
|
+
return this.c.post("/api/app-templates", body);
|
|
1509
|
+
}
|
|
1510
|
+
update(id, body) {
|
|
1511
|
+
return this.c.put(`/api/app-templates/${encodeURIComponent(id)}`, body);
|
|
1512
|
+
}
|
|
1513
|
+
remove(id) {
|
|
1514
|
+
return this.c.del(`/api/app-templates/${encodeURIComponent(id)}`);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
// src/core/resources/client.ts
|
|
1519
|
+
var AixClient = class {
|
|
1520
|
+
constructor(cfg) {
|
|
1521
|
+
this.cfg = "fetchFn" in cfg && "auth" in cfg && typeof cfg.auth?.headers === "function" ? cfg : resolveConfig(cfg);
|
|
1522
|
+
this.http = new RestClient(this.cfg);
|
|
1523
|
+
this.auth = new AuthApi(this.http);
|
|
1524
|
+
this.projects = new ProjectsApi(this.http);
|
|
1525
|
+
this.users = new UsersApi(this.http);
|
|
1526
|
+
this.devices = new DevicesApi(this.http);
|
|
1527
|
+
this.nodes = new NodesApi(this.http, this.cfg);
|
|
1528
|
+
this.clusters = new ClustersApi(this.http);
|
|
1529
|
+
this.scripts = new ScriptsApi(this.http);
|
|
1530
|
+
this.shortcuts = new ShortcutsApi(this.http);
|
|
1531
|
+
this.locators = new LocatorsApi(this.http);
|
|
1532
|
+
this.fleet = new FleetApi(this.http);
|
|
1533
|
+
this.tasks = new TasksApi(this.http);
|
|
1534
|
+
this.webhooks = new WebhooksApi(this.http);
|
|
1535
|
+
this.llm = new LlmApi(this.http);
|
|
1536
|
+
this.admin = new AdminApi(this.http);
|
|
1537
|
+
this.images = new ImagesApi(this.http);
|
|
1538
|
+
this.billing = new BillingApi(this.http);
|
|
1539
|
+
this.tokens = new ApiTokensApi(this.http);
|
|
1540
|
+
this.appTemplates = new AppTemplatesApi(this.http);
|
|
1541
|
+
if (this.cfg.localNode) {
|
|
1542
|
+
this.localNode = new LocalNodeController(this, this.cfg.localNode);
|
|
1543
|
+
if (this.cfg.localNode.auto) void this.localNode.ensure();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* 打开一条设备会话(DataChannel 多路复用):文件管理 + 终端 + 传输 + 通用流/
|
|
1548
|
+
* 端口转发。返回的 `DeviceSession` 用完请 `close()`。
|
|
1549
|
+
*
|
|
1550
|
+
* 解耦说明:`device-session.ts` 静态依赖本文件(`AixClient`),为避免模块求值期
|
|
1551
|
+
* 循环依赖,这里用**动态 import** 拿 `DeviceSession`;并把 `this.cfg`
|
|
1552
|
+
* (ResolvedConfig)而非 `this` 传给 `open`(`open` 内部用 cfg + 自建 DevicesApi
|
|
1553
|
+
* 即可,不必回依赖 client 实例)。
|
|
1554
|
+
*/
|
|
1555
|
+
async deviceSession(deviceId, opts = {}) {
|
|
1556
|
+
const { DeviceSession } = await import('./device-session-KZVGLPOI.js');
|
|
1557
|
+
return DeviceSession.open(this.cfg, deviceId, opts);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
function createClient(cfg) {
|
|
1561
|
+
return new AixClient(cfg);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
export { AdminApi, AixAuthError, AixClient, AixDeviceOffline, AixError, AixLocked, AixProtocolError, AixTimeout, ApiTokensApi, AuthApi, BillingApi, ClustersApi, DevicesApi, EnvRun, FleetApi, ImagesApi, LlmApi, LocalNodeController, LocatorsApi, NodesApi, ProjectsApi, ReconnectingSocket, RestClient, ScriptsApi, ShortcutsApi, TasksApi, UsersApi, WebhooksApi, authHeader, buildAuthProvider, createCachedUserCredential, createClient, detectEnvOs, envBlocker, errorFromRet, executionStreamEventsAfter, executionWsTicketUrl, executionWsUrl, getLocale, isEnvEventFrame, isExecutionFrame, isFleetRunTerminal, isFridaEventFrame, isRetFrame, isScriptletEventFrame, isStatusFrame, joinUrl, parseIncoming, resolveConfig, restClient, runEnvToFinish, setLocale, summarizeEnv, t };
|