@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,668 @@
|
|
|
1
|
+
import { joinUrl, AixAuthError, AixError, AixClient, resolveConfig, DevicesApi, RestClient, ReconnectingSocket } from './chunk-PCYERBNZ.js';
|
|
2
|
+
|
|
3
|
+
// src/core/webrtc-rest.ts
|
|
4
|
+
async function getIceServers(cfg) {
|
|
5
|
+
const url = joinUrl(cfg.baseUrl, "/api/ice");
|
|
6
|
+
const resp = await cfg.fetchFn(url, { headers: { ...await cfg.auth.headers("caller") } });
|
|
7
|
+
if (!resp.ok) {
|
|
8
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`ice ${resp.status}`);
|
|
9
|
+
throw new AixError(`ice ${resp.status}`);
|
|
10
|
+
}
|
|
11
|
+
const data = await resp.json();
|
|
12
|
+
return data.iceServers ?? [];
|
|
13
|
+
}
|
|
14
|
+
async function mintViewerToken(cfg, session) {
|
|
15
|
+
const url = joinUrl(cfg.baseUrl, "/api/signal/tokens");
|
|
16
|
+
const headers = { ...await cfg.auth.headers("caller"), "content-type": "application/json" };
|
|
17
|
+
const resp = await cfg.fetchFn(url, { method: "POST", headers, body: JSON.stringify({ session }) });
|
|
18
|
+
if (!resp.ok) {
|
|
19
|
+
if (resp.status === 401 || resp.status === 403) throw new AixAuthError(`signal token ${resp.status}`);
|
|
20
|
+
throw new AixError(`signal token ${resp.status}`);
|
|
21
|
+
}
|
|
22
|
+
return (await resp.json()).token;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/core/device-session.ts
|
|
26
|
+
var CHUNK_SIZE = 64 * 1024;
|
|
27
|
+
var BUFFER_HIGH = 4 * 1024 * 1024;
|
|
28
|
+
var BUFFER_LOW = 1 * 1024 * 1024;
|
|
29
|
+
var DC_CONNECT_TIMEOUT_MS = 15e3;
|
|
30
|
+
function toCtx(source) {
|
|
31
|
+
if (source instanceof AixClient) {
|
|
32
|
+
return { cfg: source.cfg, devices: source.devices };
|
|
33
|
+
}
|
|
34
|
+
const cfg = "fetchFn" in source && typeof source.fetchFn === "function" ? source : resolveConfig(source);
|
|
35
|
+
return { cfg, devices: new DevicesApi(new RestClient(cfg)) };
|
|
36
|
+
}
|
|
37
|
+
function asBytes(data) {
|
|
38
|
+
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
39
|
+
}
|
|
40
|
+
function asArrayBuffer(bytes) {
|
|
41
|
+
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
42
|
+
return bytes.buffer;
|
|
43
|
+
}
|
|
44
|
+
return bytes.slice().buffer;
|
|
45
|
+
}
|
|
46
|
+
async function sha256Hex(bytes) {
|
|
47
|
+
const subtle = globalThis.crypto?.subtle;
|
|
48
|
+
if (!subtle) throw new AixError("crypto.subtle unavailable: cannot hash file (need browser or Node 18+)");
|
|
49
|
+
const digest = await subtle.digest("SHA-256", asArrayBuffer(bytes));
|
|
50
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
51
|
+
}
|
|
52
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
53
|
+
function bytesToBase64(bytes) {
|
|
54
|
+
const B = globalThis.Buffer;
|
|
55
|
+
if (B) return B.from(bytes).toString("base64");
|
|
56
|
+
const c = (n) => B64_CHARS[n & 63];
|
|
57
|
+
let out = "";
|
|
58
|
+
let i = 0;
|
|
59
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
60
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
61
|
+
out += c(n >> 18) + c(n >> 12) + c(n >> 6) + c(n);
|
|
62
|
+
}
|
|
63
|
+
const rem = bytes.length - i;
|
|
64
|
+
if (rem === 1) {
|
|
65
|
+
const n = bytes[i] << 16;
|
|
66
|
+
out += c(n >> 18) + c(n >> 12) + "==";
|
|
67
|
+
} else if (rem === 2) {
|
|
68
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8;
|
|
69
|
+
out += c(n >> 18) + c(n >> 12) + c(n >> 6) + "=";
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
var B64_LOOKUP = (() => {
|
|
74
|
+
const t = new Int16Array(256).fill(-1);
|
|
75
|
+
for (let i = 0; i < B64_CHARS.length; i++) t[B64_CHARS.charCodeAt(i)] = i;
|
|
76
|
+
return t;
|
|
77
|
+
})();
|
|
78
|
+
function base64ToBytes(b64) {
|
|
79
|
+
const B = globalThis.Buffer;
|
|
80
|
+
if (B) return Uint8Array.from(B.from(b64, "base64"));
|
|
81
|
+
let len = b64.length;
|
|
82
|
+
while (len > 0 && b64[len - 1] === "=") len--;
|
|
83
|
+
const outLen = len * 3 >> 2;
|
|
84
|
+
const out = new Uint8Array(outLen);
|
|
85
|
+
let acc = 0;
|
|
86
|
+
let bits = 0;
|
|
87
|
+
let o = 0;
|
|
88
|
+
for (let i = 0; i < len; i++) {
|
|
89
|
+
const v = B64_LOOKUP[b64.charCodeAt(i)] ?? -1;
|
|
90
|
+
if (v < 0) continue;
|
|
91
|
+
acc = acc << 6 | v;
|
|
92
|
+
bits += 6;
|
|
93
|
+
if (bits >= 8) {
|
|
94
|
+
bits -= 8;
|
|
95
|
+
out[o++] = acc >> bits & 255;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function waitForDrain(dc) {
|
|
101
|
+
const cur = dc.bufferedAmount;
|
|
102
|
+
if (cur == null || cur <= BUFFER_LOW) return Promise.resolve();
|
|
103
|
+
if (!("onbufferedamountlow" in dc)) return Promise.resolve();
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
dc.bufferedAmountLowThreshold = BUFFER_LOW;
|
|
106
|
+
dc.onbufferedamountlow = () => {
|
|
107
|
+
dc.onbufferedamountlow = null;
|
|
108
|
+
resolve();
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function openCtrlChannel(cfg, session, timeoutMs) {
|
|
113
|
+
const iceServers = await getIceServers(cfg);
|
|
114
|
+
const token = await mintViewerToken(cfg, session);
|
|
115
|
+
const pc = cfg.rtcFactory({ iceServers });
|
|
116
|
+
let selfPeer = null;
|
|
117
|
+
let remotePeer = null;
|
|
118
|
+
let ctrl = null;
|
|
119
|
+
let remoteDescriptionReady = false;
|
|
120
|
+
const pendingIce = [];
|
|
121
|
+
const signalUrl = joinUrl(cfg.baseUrl, "/ws/signal").replace(/^http/, "ws");
|
|
122
|
+
let socket;
|
|
123
|
+
const send = (msg) => {
|
|
124
|
+
try {
|
|
125
|
+
socket.send(JSON.stringify(msg));
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
pc.onicecandidate = (ev) => {
|
|
130
|
+
if (ev.candidate && remotePeer) send({ type: "ice", session, to: remotePeer, candidate: ev.candidate });
|
|
131
|
+
};
|
|
132
|
+
pc.ondatachannel = (ev) => {
|
|
133
|
+
if (ev.channel.label === "ctrl" || !ctrl) {
|
|
134
|
+
ctrl = ev.channel;
|
|
135
|
+
ctrl.binaryType = "arraybuffer";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const onSignal = async (text) => {
|
|
139
|
+
let m;
|
|
140
|
+
try {
|
|
141
|
+
m = JSON.parse(text);
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
switch (m.type) {
|
|
146
|
+
case "welcome":
|
|
147
|
+
selfPeer = m.peer_id ?? null;
|
|
148
|
+
break;
|
|
149
|
+
case "peers": {
|
|
150
|
+
const node = (m.members ?? []).find((p) => p !== selfPeer);
|
|
151
|
+
if (node && !remotePeer) remotePeer = node;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "offer":
|
|
155
|
+
if (m.sdp && m.from) {
|
|
156
|
+
remotePeer = m.from;
|
|
157
|
+
await pc.setRemoteDescription({ type: "offer", sdp: m.sdp });
|
|
158
|
+
remoteDescriptionReady = true;
|
|
159
|
+
for (const candidate of pendingIce.splice(0)) await pc.addIceCandidate(candidate);
|
|
160
|
+
const answer = await pc.createAnswer();
|
|
161
|
+
await pc.setLocalDescription(answer);
|
|
162
|
+
send({ type: "answer", session, to: m.from, sdp: answer.sdp });
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case "ice":
|
|
166
|
+
if (m.candidate) {
|
|
167
|
+
if (!remoteDescriptionReady) pendingIce.push(m.candidate);
|
|
168
|
+
else await pc.addIceCandidate(m.candidate);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
socket = new ReconnectingSocket({
|
|
174
|
+
urlProvider: async () => signalUrl,
|
|
175
|
+
wsFactory: cfg.wsFactory,
|
|
176
|
+
onOpen: () => send({ type: "join", session, role: "viewer", token }),
|
|
177
|
+
onMessage: (t) => void onSignal(t)
|
|
178
|
+
});
|
|
179
|
+
const dispose = () => {
|
|
180
|
+
try {
|
|
181
|
+
socket.send(JSON.stringify({ type: "leave", session }));
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
ctrl?.close();
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
pc.close();
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
socket.close();
|
|
193
|
+
};
|
|
194
|
+
await socket.start();
|
|
195
|
+
try {
|
|
196
|
+
const opened = await new Promise((resolve, reject) => {
|
|
197
|
+
let settled = false;
|
|
198
|
+
const finish = (dc, err) => {
|
|
199
|
+
if (settled) return;
|
|
200
|
+
settled = true;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
clearInterval(poll);
|
|
203
|
+
if (err) reject(err);
|
|
204
|
+
else resolve(dc);
|
|
205
|
+
};
|
|
206
|
+
const timer = setTimeout(() => finish(void 0, new Error("DataChannel \u5EFA\u94FE\u8D85\u65F6")), timeoutMs);
|
|
207
|
+
const poll = setInterval(() => {
|
|
208
|
+
if (!ctrl) return;
|
|
209
|
+
const dc = ctrl;
|
|
210
|
+
if (dc.readyState === "open") return finish(dc);
|
|
211
|
+
dc.onopen = () => finish(dc);
|
|
212
|
+
}, 30);
|
|
213
|
+
pc.onconnectionstatechange = () => {
|
|
214
|
+
if (pc.connectionState === "failed" || pc.connectionState === "closed") {
|
|
215
|
+
finish(void 0, new Error(`PeerConnection ${pc.connectionState}`));
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
return { dc: opened, dispose };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
dispose();
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
var DeviceSession = class _DeviceSession {
|
|
226
|
+
constructor(session, dc, dispose) {
|
|
227
|
+
this.session = session;
|
|
228
|
+
this.dc = dc;
|
|
229
|
+
this.dispose = dispose;
|
|
230
|
+
this.nextId = 1;
|
|
231
|
+
/** id → 该操作的响应路由回调(多路复用核心)。 */
|
|
232
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
233
|
+
this.closed = false;
|
|
234
|
+
this.dc.onmessage = (ev) => {
|
|
235
|
+
if (typeof ev.data !== "string") return;
|
|
236
|
+
let m;
|
|
237
|
+
try {
|
|
238
|
+
m = JSON.parse(ev.data);
|
|
239
|
+
} catch {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const h = this.pending.get(String(m.id));
|
|
243
|
+
if (h) h(m);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 打开一个设备会话:dcSession 握手 → answerer 建链 → 等 "ctrl" 通道 open。
|
|
248
|
+
* @param source `AixClient` 或 `AixConfig | ResolvedConfig`
|
|
249
|
+
* @param deviceId 设备 id
|
|
250
|
+
* @param opts.dcTimeoutMs 建链超时(ms),默认 15000
|
|
251
|
+
*/
|
|
252
|
+
static async open(source, deviceId, opts = {}) {
|
|
253
|
+
const ctx = toCtx(source);
|
|
254
|
+
const timeoutMs = opts.dcTimeoutMs ?? DC_CONNECT_TIMEOUT_MS;
|
|
255
|
+
const { session_id } = await ctx.devices.dcSession(deviceId);
|
|
256
|
+
const { dc, dispose } = await openCtrlChannel(ctx.cfg, session_id, timeoutMs);
|
|
257
|
+
return new _DeviceSession(session_id, dc, dispose);
|
|
258
|
+
}
|
|
259
|
+
allocId() {
|
|
260
|
+
return String(this.nextId++);
|
|
261
|
+
}
|
|
262
|
+
ensureOpen() {
|
|
263
|
+
if (this.closed) throw new AixError("DeviceSession already closed");
|
|
264
|
+
if (this.dc.readyState !== "open") throw new AixError(`DeviceSession DataChannel not open (${this.dc.readyState})`);
|
|
265
|
+
}
|
|
266
|
+
sendReq(req) {
|
|
267
|
+
this.dc.send(JSON.stringify(req));
|
|
268
|
+
}
|
|
269
|
+
// ---- 文件管理 ----------------------------------------------------------
|
|
270
|
+
/** 列目录:返回 entries。节点 `ls -la` 解析后回 `list_ok`。 */
|
|
271
|
+
listFiles(path) {
|
|
272
|
+
this.ensureOpen();
|
|
273
|
+
const id = this.allocId();
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
this.pending.set(id, (m) => {
|
|
276
|
+
if (m.t === "list_ok") {
|
|
277
|
+
this.pending.delete(id);
|
|
278
|
+
resolve(m.entries);
|
|
279
|
+
} else if (m.t === "err") {
|
|
280
|
+
this.pending.delete(id);
|
|
281
|
+
reject(new AixError(m.message));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
this.sendReq({ id, t: "list", path });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** 删除文件/目录(recursive=true 走 `rm -rf`)。 */
|
|
288
|
+
deleteFile(path, recursive = false) {
|
|
289
|
+
return this.simpleOk((id) => ({ id, t: "delete", path, recursive }));
|
|
290
|
+
}
|
|
291
|
+
/** 新建目录(`mkdir -p`)。 */
|
|
292
|
+
mkdir(path) {
|
|
293
|
+
return this.simpleOk((id) => ({ id, t: "mkdir", path }));
|
|
294
|
+
}
|
|
295
|
+
/** 重命名/移动(`mv from to`)。 */
|
|
296
|
+
rename(from, to) {
|
|
297
|
+
return this.simpleOk((id) => ({ id, t: "rename", from, to }));
|
|
298
|
+
}
|
|
299
|
+
/** 共享:发一条请求(由 id 构造完整帧),等节点回 `ok` / `err`。 */
|
|
300
|
+
simpleOk(makeReq) {
|
|
301
|
+
this.ensureOpen();
|
|
302
|
+
const id = this.allocId();
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
this.pending.set(id, (m) => {
|
|
305
|
+
if (m.t === "ok") {
|
|
306
|
+
this.pending.delete(id);
|
|
307
|
+
resolve();
|
|
308
|
+
} else if (m.t === "err") {
|
|
309
|
+
this.pending.delete(id);
|
|
310
|
+
reject(new AixError(m.message));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
this.sendReq(makeReq(id));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// ---- 终端 --------------------------------------------------------------
|
|
317
|
+
/** 执行命令并收齐输出:返回 `{stdout, stderr, exitCode}`。 */
|
|
318
|
+
async exec(cmd) {
|
|
319
|
+
let stdout = "";
|
|
320
|
+
let stderr = "";
|
|
321
|
+
const code = await this.execStream(cmd, {
|
|
322
|
+
onStdout: (d) => {
|
|
323
|
+
stdout += d;
|
|
324
|
+
},
|
|
325
|
+
onStderr: (d) => {
|
|
326
|
+
stderr += d;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return { stdout, stderr, exitCode: code };
|
|
330
|
+
}
|
|
331
|
+
/** 流式执行命令:stdout/stderr 分块回调,resolve 退出码。 */
|
|
332
|
+
execStream(cmd, hooks = {}) {
|
|
333
|
+
this.ensureOpen();
|
|
334
|
+
const id = this.allocId();
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
this.pending.set(id, (m) => {
|
|
337
|
+
switch (m.t) {
|
|
338
|
+
case "stdout":
|
|
339
|
+
hooks.onStdout?.(m.data);
|
|
340
|
+
break;
|
|
341
|
+
case "stderr":
|
|
342
|
+
hooks.onStderr?.(m.data);
|
|
343
|
+
break;
|
|
344
|
+
case "exit":
|
|
345
|
+
this.pending.delete(id);
|
|
346
|
+
resolve(m.code);
|
|
347
|
+
break;
|
|
348
|
+
case "err":
|
|
349
|
+
this.pending.delete(id);
|
|
350
|
+
reject(new AixError(m.message));
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
this.sendReq({ id, t: "exec", cmd });
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// ---- 文件传输 ----------------------------------------------------------
|
|
358
|
+
/** 推字节到设备 `dest`:put → chunk... → eof,节点逐块回 `ack` 推进进度。 */
|
|
359
|
+
async pushFile(input, onProgress) {
|
|
360
|
+
this.ensureOpen();
|
|
361
|
+
const id = this.allocId();
|
|
362
|
+
const bytes = asBytes(input.data);
|
|
363
|
+
const total = bytes.length;
|
|
364
|
+
const sha256 = await sha256Hex(bytes);
|
|
365
|
+
let ackResolve = null;
|
|
366
|
+
let failed = null;
|
|
367
|
+
this.pending.set(id, (m) => {
|
|
368
|
+
if (m.t === "ack") {
|
|
369
|
+
onProgress?.({ loaded: Math.min(m.offset, total), total, channel: "dc" });
|
|
370
|
+
ackResolve?.();
|
|
371
|
+
} else if (m.t === "done" || m.t === "ok") {
|
|
372
|
+
this.pending.delete(id);
|
|
373
|
+
ackResolve?.();
|
|
374
|
+
} else if (m.t === "err") {
|
|
375
|
+
failed = new AixError(m.message);
|
|
376
|
+
this.pending.delete(id);
|
|
377
|
+
ackResolve?.();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
this.sendReq({ id, t: "put", name: input.filename, size: total, sha256, dest: input.dest });
|
|
382
|
+
for (let offset = 0; offset < total; offset += CHUNK_SIZE) {
|
|
383
|
+
if (failed) throw failed;
|
|
384
|
+
const slice = bytes.subarray(offset, Math.min(offset + CHUNK_SIZE, total));
|
|
385
|
+
this.sendReq({ id, t: "chunk", offset, bytes: bytesToBase64(slice) });
|
|
386
|
+
if ((this.dc.bufferedAmount ?? 0) > BUFFER_HIGH) await waitForDrain(this.dc);
|
|
387
|
+
}
|
|
388
|
+
this.sendReq({ id, t: "eof" });
|
|
389
|
+
await new Promise((resolve) => {
|
|
390
|
+
ackResolve = resolve;
|
|
391
|
+
setTimeout(resolve, 1e4);
|
|
392
|
+
});
|
|
393
|
+
if (failed) throw failed;
|
|
394
|
+
onProgress?.({ loaded: total, total, channel: "dc" });
|
|
395
|
+
} finally {
|
|
396
|
+
this.pending.delete(id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** 从设备 `src` 拉字节,收齐后返回 Uint8Array。节点流式回 chunk + done。 */
|
|
400
|
+
pullFile(src, onProgress) {
|
|
401
|
+
this.ensureOpen();
|
|
402
|
+
const id = this.allocId();
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
const chunks = [];
|
|
405
|
+
let total = 0;
|
|
406
|
+
let loaded = 0;
|
|
407
|
+
this.pending.set(id, (m) => {
|
|
408
|
+
switch (m.t) {
|
|
409
|
+
case "chunk": {
|
|
410
|
+
const b = base64ToBytes(m.bytes);
|
|
411
|
+
chunks.push(b);
|
|
412
|
+
loaded += b.length;
|
|
413
|
+
if (m.size && !total) total = m.size;
|
|
414
|
+
onProgress?.({ loaded, total: total || loaded, channel: "dc" });
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "done": {
|
|
418
|
+
this.pending.delete(id);
|
|
419
|
+
const out = new Uint8Array(loaded);
|
|
420
|
+
let off = 0;
|
|
421
|
+
for (const c of chunks) {
|
|
422
|
+
out.set(c, off);
|
|
423
|
+
off += c.length;
|
|
424
|
+
}
|
|
425
|
+
onProgress?.({ loaded, total: m.size || total || loaded, channel: "dc" });
|
|
426
|
+
resolve(out);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case "err":
|
|
430
|
+
this.pending.delete(id);
|
|
431
|
+
reject(new AixError(m.message));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
this.sendReq({ id, t: "get", src });
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// ---- 通用流:openStream / shell / 端口转发 ------------------------------
|
|
439
|
+
/**
|
|
440
|
+
* 打开一条通用双工字节流(多路复用,沿用同一 DC)。
|
|
441
|
+
* - `{kind:'tcp',host,port}`:节点 `TcpStream::connect(host:port)`,host 是
|
|
442
|
+
* **节点可达**的地址(设备 adb-forward 出的端口、节点本机/局域网服务等)。
|
|
443
|
+
* - `{kind:'exec',cmd}`:节点 spawn `adb shell <cmd>`(交互式:write→stdin,
|
|
444
|
+
* stdout/stderr 合并→onData)。
|
|
445
|
+
* 返回的 `DcStream` 在收到节点 `opened` 后即可用;若节点回 `err` 则 reject。
|
|
446
|
+
*/
|
|
447
|
+
openStream(target) {
|
|
448
|
+
this.ensureOpen();
|
|
449
|
+
const id = this.allocId();
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
let opened = false;
|
|
452
|
+
const dataCbs = [];
|
|
453
|
+
const closeCbs = [];
|
|
454
|
+
let streamClosed = false;
|
|
455
|
+
const finalize = () => {
|
|
456
|
+
if (streamClosed) return;
|
|
457
|
+
streamClosed = true;
|
|
458
|
+
this.pending.delete(id);
|
|
459
|
+
for (const cb of closeCbs) {
|
|
460
|
+
try {
|
|
461
|
+
cb();
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const stream = {
|
|
467
|
+
write: (bytes) => {
|
|
468
|
+
if (streamClosed) return;
|
|
469
|
+
this.sendReq({ id, t: "data", bytes: bytesToBase64(asBytes(bytes)) });
|
|
470
|
+
},
|
|
471
|
+
onData: (cb) => {
|
|
472
|
+
dataCbs.push(cb);
|
|
473
|
+
},
|
|
474
|
+
onClose: (cb) => {
|
|
475
|
+
if (streamClosed) {
|
|
476
|
+
try {
|
|
477
|
+
cb();
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
closeCbs.push(cb);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
close: () => {
|
|
485
|
+
if (streamClosed) return;
|
|
486
|
+
try {
|
|
487
|
+
this.sendReq({ id, t: "close" });
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
finalize();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
this.pending.set(id, (m) => {
|
|
494
|
+
switch (m.t) {
|
|
495
|
+
case "opened":
|
|
496
|
+
if (!opened) {
|
|
497
|
+
opened = true;
|
|
498
|
+
resolve(stream);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
case "data": {
|
|
502
|
+
const b = base64ToBytes(m.bytes);
|
|
503
|
+
for (const cb of dataCbs) {
|
|
504
|
+
try {
|
|
505
|
+
cb(b);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case "close":
|
|
512
|
+
finalize();
|
|
513
|
+
break;
|
|
514
|
+
case "err":
|
|
515
|
+
if (!opened) {
|
|
516
|
+
this.pending.delete(id);
|
|
517
|
+
reject(new AixError(m.message));
|
|
518
|
+
} else {
|
|
519
|
+
finalize();
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
this.sendReq(
|
|
525
|
+
target.kind === "tcp" ? { id, t: "open", kind: "tcp", host: target.host, port: target.port } : { id, t: "open", kind: "exec", cmd: target.cmd }
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/** 交互式终端:`= openStream({kind:'exec',cmd})`,cmd 默认 `sh`(交互式 shell)。 */
|
|
530
|
+
shell(cmd = "sh") {
|
|
531
|
+
return this.openStream({ kind: "exec", cmd });
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* 本地端口转发(**仅 Node 环境**):用 `node:net` 监听 `localPort`,每个进来的
|
|
535
|
+
* 连接都开一条 `openStream({kind:'tcp',host,port})` 并双向 pipe。返回停止函数
|
|
536
|
+
* (关闭监听 + 现有连接)。浏览器环境无 `node:net`,调用会抛"仅 node 可用"。
|
|
537
|
+
*
|
|
538
|
+
* 用动态 import('node:net') + try,避免把 node:net 打进浏览器 bundle
|
|
539
|
+
* (frontend 不会调用本方法)。
|
|
540
|
+
*/
|
|
541
|
+
async forwardLocalPort(localPort, remote) {
|
|
542
|
+
this.ensureOpen();
|
|
543
|
+
let net;
|
|
544
|
+
try {
|
|
545
|
+
const mod = "node:net";
|
|
546
|
+
net = await import(
|
|
547
|
+
/* @vite-ignore */
|
|
548
|
+
/* webpackIgnore: true */
|
|
549
|
+
mod
|
|
550
|
+
);
|
|
551
|
+
} catch {
|
|
552
|
+
throw new AixError("forwardLocalPort \u4EC5 node \u53EF\u7528(\u5F53\u524D\u73AF\u5883\u65E0 node:net)");
|
|
553
|
+
}
|
|
554
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
555
|
+
const server = net.createServer((socket) => {
|
|
556
|
+
sockets.add(socket);
|
|
557
|
+
void (async () => {
|
|
558
|
+
let stream;
|
|
559
|
+
try {
|
|
560
|
+
stream = await this.openStream({ kind: "tcp", host: remote.host, port: remote.port });
|
|
561
|
+
} catch {
|
|
562
|
+
socket.destroy();
|
|
563
|
+
sockets.delete(socket);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
stream.onData((b) => {
|
|
567
|
+
socket.write(b.slice());
|
|
568
|
+
});
|
|
569
|
+
stream.onClose(() => {
|
|
570
|
+
socket.end();
|
|
571
|
+
});
|
|
572
|
+
socket.on("data", (chunk) => {
|
|
573
|
+
stream.write(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
|
|
574
|
+
});
|
|
575
|
+
socket.on("close", () => {
|
|
576
|
+
stream.close();
|
|
577
|
+
sockets.delete(socket);
|
|
578
|
+
});
|
|
579
|
+
socket.on("error", () => {
|
|
580
|
+
stream.close();
|
|
581
|
+
sockets.delete(socket);
|
|
582
|
+
});
|
|
583
|
+
})();
|
|
584
|
+
});
|
|
585
|
+
await new Promise((resolve, reject) => {
|
|
586
|
+
server.once("error", reject);
|
|
587
|
+
server.listen(localPort, () => {
|
|
588
|
+
server.off?.("error", reject);
|
|
589
|
+
resolve();
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
return () => {
|
|
593
|
+
try {
|
|
594
|
+
server.close();
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
for (const s of sockets) {
|
|
598
|
+
try {
|
|
599
|
+
s.destroy();
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
sockets.clear();
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** 关闭会话:断开 DC + pc + 信令 socket,清空 pending。 */
|
|
607
|
+
close() {
|
|
608
|
+
if (this.closed) return;
|
|
609
|
+
this.closed = true;
|
|
610
|
+
this.pending.clear();
|
|
611
|
+
this.dispose();
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
async function pushViaR2(ctx, deviceId, input, onProgress) {
|
|
615
|
+
const bytes = asBytes(input.data);
|
|
616
|
+
const total = bytes.length;
|
|
617
|
+
onProgress?.({ loaded: 0, total, channel: "r2" });
|
|
618
|
+
const { key, upload_url } = await ctx.devices.filesUploadUrl(deviceId, input.filename);
|
|
619
|
+
const put = await ctx.cfg.fetchFn(upload_url, { method: "PUT", body: asArrayBuffer(bytes) });
|
|
620
|
+
if (!put.ok) throw new AixError(`R2 \u4E0A\u4F20\u5931\u8D25 ${put.status}`);
|
|
621
|
+
onProgress?.({ loaded: total, total, channel: "r2" });
|
|
622
|
+
await ctx.devices.filesPush(deviceId, key, input.dest);
|
|
623
|
+
}
|
|
624
|
+
async function pullViaR2(ctx, deviceId, src, onProgress) {
|
|
625
|
+
const filename = src.split("/").pop() || "download.bin";
|
|
626
|
+
onProgress?.({ loaded: 0, total: 0, channel: "r2" });
|
|
627
|
+
const { download_url } = await ctx.devices.filesPull(deviceId, src, filename);
|
|
628
|
+
const resp = await ctx.cfg.fetchFn(download_url);
|
|
629
|
+
if (!resp.ok) throw new AixError(`R2 \u4E0B\u8F7D\u5931\u8D25 ${resp.status}`);
|
|
630
|
+
const data = new Uint8Array(await resp.arrayBuffer());
|
|
631
|
+
onProgress?.({ loaded: data.length, total: data.length, channel: "r2" });
|
|
632
|
+
return { data, filename };
|
|
633
|
+
}
|
|
634
|
+
async function pushFile(source, deviceId, input, opts = {}) {
|
|
635
|
+
const ctx = toCtx(source);
|
|
636
|
+
try {
|
|
637
|
+
const s = await DeviceSession.open(ctx.cfg, deviceId, { dcTimeoutMs: opts.dcTimeoutMs });
|
|
638
|
+
try {
|
|
639
|
+
await s.pushFile(input, opts.onProgress);
|
|
640
|
+
} finally {
|
|
641
|
+
s.close();
|
|
642
|
+
}
|
|
643
|
+
return { channel: "dc", fellBack: false };
|
|
644
|
+
} catch (err) {
|
|
645
|
+
if (typeof console !== "undefined") console.warn("[device-session] push \u901A\u9053 A \u5931\u8D25,\u56DE\u9000 R2:", err);
|
|
646
|
+
await pushViaR2(ctx, deviceId, input, opts.onProgress);
|
|
647
|
+
return { channel: "r2", fellBack: true };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function pullFile(source, deviceId, src, opts = {}) {
|
|
651
|
+
const ctx = toCtx(source);
|
|
652
|
+
const filename = src.split("/").pop() || "download.bin";
|
|
653
|
+
try {
|
|
654
|
+
const s = await DeviceSession.open(ctx.cfg, deviceId, { dcTimeoutMs: opts.dcTimeoutMs });
|
|
655
|
+
try {
|
|
656
|
+
const data = await s.pullFile(src, opts.onProgress);
|
|
657
|
+
return { channel: "dc", fellBack: false, data, filename };
|
|
658
|
+
} finally {
|
|
659
|
+
s.close();
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
if (typeof console !== "undefined") console.warn("[device-session] pull \u901A\u9053 A \u5931\u8D25,\u56DE\u9000 R2:", err);
|
|
663
|
+
const r = await pullViaR2(ctx, deviceId, src, opts.onProgress);
|
|
664
|
+
return { channel: "r2", fellBack: true, data: r.data, filename: r.filename };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export { DeviceSession, getIceServers, mintViewerToken, pullFile, pushFile };
|