@my-life-buddies/cli-windowscompat 0.0.0-stage → 0.29.0-dev.0
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 +19 -2
- package/dist/bin/buddy.js +542 -0
- package/dist/bin/buddy.js.map +7 -0
- package/dist/bin/command-runner.js +529 -0
- package/dist/bin/command-runner.js.map +7 -0
- package/dist/bin/core.js +9939 -0
- package/dist/bin/core.js.map +7 -0
- package/dist/bin/platform.js +543 -0
- package/dist/bin/platform.js.map +7 -0
- package/dist/bin/preview.js +2924 -0
- package/dist/bin/preview.js.map +7 -0
- package/dist/bin/resources/widget-document.js +41 -0
- package/package.json +54 -4
- package/resources/README.md +10 -0
- package/resources/agent-template/buddy-dev-control.mjs +49 -0
- package/resources/agent-template/package-lock.json +23 -0
- package/resources/agent-template/package.json +14 -0
- package/resources/brand/README.md +11 -0
- package/resources/brand/login-result.css +25 -0
- package/resources/brand/logo.png +0 -0
- package/resources/brand/tokens.css +100 -0
|
@@ -0,0 +1,2924 @@
|
|
|
1
|
+
// apps/preview/src/controller.ts
|
|
2
|
+
import { readBuddyConfig } from "./core.js";
|
|
3
|
+
|
|
4
|
+
// apps/preview/src/device-preview.ts
|
|
5
|
+
function createBuddyPreviewUrl(buddyId) {
|
|
6
|
+
if (!/^[a-zA-Z0-9._-]{1,124}-dev$/u.test(buddyId)) {
|
|
7
|
+
throw new TypeError("\u771F\u673A\u9884\u89C8\u9700\u8981\u4E0D\u8D85\u8FC7 128 \u5B57\u7B26\u7684\u5F00\u53D1\u7248 Buddy ID");
|
|
8
|
+
}
|
|
9
|
+
return `mlb://preview?${new URLSearchParams({ buddyId })}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// apps/preview/src/runtime-diagnostics.ts
|
|
13
|
+
function parseInspectedData(text2) {
|
|
14
|
+
let at = 0;
|
|
15
|
+
const space = () => {
|
|
16
|
+
while (/\s/u.test(text2[at] ?? "") && at < text2.length) at++;
|
|
17
|
+
};
|
|
18
|
+
const fail = () => {
|
|
19
|
+
throw new Error("Unsupported log data");
|
|
20
|
+
};
|
|
21
|
+
function string() {
|
|
22
|
+
const quote = text2[at++];
|
|
23
|
+
let result = "";
|
|
24
|
+
while (at < text2.length) {
|
|
25
|
+
const char = text2[at++];
|
|
26
|
+
if (char === quote) return result;
|
|
27
|
+
if (char !== "\\") {
|
|
28
|
+
result += char;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const escaped = text2[at++];
|
|
32
|
+
const escapes = { n: "\n", r: "\r", t: " ", b: "\b", f: "\f", v: "\v", "0": "\0", "\\": "\\", "'": "'", '"': '"', "`": "`", "$": "$" };
|
|
33
|
+
if (escaped === "x" || escaped === "u") {
|
|
34
|
+
const length = escaped === "x" ? 2 : 4;
|
|
35
|
+
const hex = text2.slice(at, at + length);
|
|
36
|
+
if (!new RegExp(`^[0-9a-fA-F]{${length}}$`, "u").test(hex)) fail();
|
|
37
|
+
result += String.fromCharCode(parseInt(hex, 16));
|
|
38
|
+
at += length;
|
|
39
|
+
} else if (escaped !== void 0 && Object.hasOwn(escapes, escaped)) result += escapes[escaped];
|
|
40
|
+
else fail();
|
|
41
|
+
}
|
|
42
|
+
return fail();
|
|
43
|
+
}
|
|
44
|
+
function value(depth = 0) {
|
|
45
|
+
if (depth > 40) fail();
|
|
46
|
+
space();
|
|
47
|
+
const char = text2[at];
|
|
48
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
49
|
+
let result = string();
|
|
50
|
+
space();
|
|
51
|
+
while (text2[at] === "+") {
|
|
52
|
+
at++;
|
|
53
|
+
space();
|
|
54
|
+
if (!["'", '"', "`"].includes(text2[at] ?? "")) fail();
|
|
55
|
+
result += string();
|
|
56
|
+
space();
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
if (char === "{" || char === "[") {
|
|
61
|
+
const array = char === "[";
|
|
62
|
+
const end = array ? "]" : "}";
|
|
63
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
64
|
+
const items = [];
|
|
65
|
+
at++;
|
|
66
|
+
space();
|
|
67
|
+
while (text2[at] !== end) {
|
|
68
|
+
if (at >= text2.length) fail();
|
|
69
|
+
if (array) items.push(value(depth + 1));
|
|
70
|
+
else {
|
|
71
|
+
const quoted = ["'", '"', "`"].includes(text2[at] ?? "");
|
|
72
|
+
const key = quoted ? string() : /^[\w$]+/u.exec(text2.slice(at))?.[0];
|
|
73
|
+
if (key === void 0) return fail();
|
|
74
|
+
if (!quoted) at += key.length;
|
|
75
|
+
space();
|
|
76
|
+
if (text2[at++] !== ":" || Object.hasOwn(result, key)) fail();
|
|
77
|
+
result[key] = value(depth + 1);
|
|
78
|
+
}
|
|
79
|
+
space();
|
|
80
|
+
if (text2[at] === end) break;
|
|
81
|
+
if (text2[at++] !== ",") fail();
|
|
82
|
+
space();
|
|
83
|
+
}
|
|
84
|
+
at++;
|
|
85
|
+
return array ? items : result;
|
|
86
|
+
}
|
|
87
|
+
const literal = /^(true|false|null|undefined|[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(?=[\s,\]}]|$)/iu.exec(text2.slice(at))?.[0];
|
|
88
|
+
if (!literal) return fail();
|
|
89
|
+
at += literal.length;
|
|
90
|
+
if (literal === "true") return true;
|
|
91
|
+
if (literal === "false") return false;
|
|
92
|
+
if (literal === "null" || literal === "undefined") return null;
|
|
93
|
+
const number = Number(literal);
|
|
94
|
+
return Number.isFinite(number) ? number : fail();
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (text2.length > 98304) return void 0;
|
|
98
|
+
const result = value();
|
|
99
|
+
space();
|
|
100
|
+
return at === text2.length && result !== null && typeof result === "object" && !Array.isArray(result) ? result : void 0;
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var HEADER = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[[^\]\r\n]+\] (INFO|WARN|ERROR)\s+(.+)$/u;
|
|
106
|
+
var MAX_RECORD = 98304;
|
|
107
|
+
var MAX_BYTES = 15e5;
|
|
108
|
+
var RuntimeDiagnostics = class {
|
|
109
|
+
#sequence = 0;
|
|
110
|
+
#records = [];
|
|
111
|
+
#active = /* @__PURE__ */ new Map();
|
|
112
|
+
#sizes = /* @__PURE__ */ new Map();
|
|
113
|
+
#bytes = 0;
|
|
114
|
+
#dropped = 0;
|
|
115
|
+
write(source, text2, observedAt) {
|
|
116
|
+
for (const line of text2.split(/\r?\n/u)) {
|
|
117
|
+
if (!line) continue;
|
|
118
|
+
const header = HEADER.exec(line);
|
|
119
|
+
const previousIndex = this.#records.findIndex((record3) => record3.id === this.#active.get(source));
|
|
120
|
+
const previous = this.#records[previousIndex];
|
|
121
|
+
const continuation = !header && previous && (/^\s/u.test(line) || line === "}");
|
|
122
|
+
let record2;
|
|
123
|
+
if (continuation) {
|
|
124
|
+
const raw = `${previous.raw}
|
|
125
|
+
${line}`;
|
|
126
|
+
record2 = { ...previous, raw: raw.slice(0, MAX_RECORD), truncated: previous.truncated || raw.length > MAX_RECORD || line.endsWith(" \u2026[truncated]") };
|
|
127
|
+
} else {
|
|
128
|
+
const title = header ? header[3].replace(/ \{.*$/u, "") : line.slice(0, 180);
|
|
129
|
+
const time = header ? Date.parse(`${header[1].replace(" ", "T")}+08:00`) : NaN;
|
|
130
|
+
record2 = {
|
|
131
|
+
id: `runtime-${++this.#sequence}`,
|
|
132
|
+
timestamp: Number.isFinite(time) ? new Date(time).toISOString() : observedAt,
|
|
133
|
+
source,
|
|
134
|
+
level: header ? header[2].toLowerCase() : source === "agent.stderr" ? "error" : "info",
|
|
135
|
+
title,
|
|
136
|
+
kind: header && title === "\u6A21\u578B\u8BF7\u6C42\u4F53" ? "request" : header && title === "\u6A21\u578B\u54CD\u5E94" ? "response" : "log",
|
|
137
|
+
raw: line.slice(0, MAX_RECORD),
|
|
138
|
+
truncated: line.length > MAX_RECORD || line.endsWith(" \u2026[truncated]")
|
|
139
|
+
};
|
|
140
|
+
this.#active.set(source, record2.id);
|
|
141
|
+
}
|
|
142
|
+
if (!record2.truncated && line.trimEnd().endsWith("}")) {
|
|
143
|
+
const head = HEADER.exec(record2.raw.split("\n")[0]);
|
|
144
|
+
const offset = head ? record2.raw.indexOf(" {", record2.raw.indexOf(head[3])) : -1;
|
|
145
|
+
const data = offset < 0 ? void 0 : parseInspectedData(record2.raw.slice(offset + 1));
|
|
146
|
+
if (data) record2 = { ...record2, data, ...typeof data.convoKey === "string" ? { convoKey: data.convoKey } : {} };
|
|
147
|
+
}
|
|
148
|
+
if (continuation && previous.data) {
|
|
149
|
+
const { data: _data, convoKey: _convoKey, ...rawRecord } = record2;
|
|
150
|
+
record2 = rawRecord;
|
|
151
|
+
}
|
|
152
|
+
this.#bytes -= this.#sizes.get(record2.id) ?? 0;
|
|
153
|
+
const size = Buffer.byteLength(JSON.stringify(record2));
|
|
154
|
+
this.#sizes.set(record2.id, size);
|
|
155
|
+
this.#bytes += size;
|
|
156
|
+
if (continuation) this.#records[previousIndex] = record2;
|
|
157
|
+
else this.#records.push(record2);
|
|
158
|
+
while (this.#records.length > 200 || this.#bytes > MAX_BYTES) {
|
|
159
|
+
const removed = this.#records.shift();
|
|
160
|
+
this.#bytes -= this.#sizes.get(removed.id) ?? 0;
|
|
161
|
+
this.#sizes.delete(removed.id);
|
|
162
|
+
this.#dropped++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
boundary() {
|
|
167
|
+
this.#active.clear();
|
|
168
|
+
}
|
|
169
|
+
clear() {
|
|
170
|
+
this.#records = [];
|
|
171
|
+
this.#active.clear();
|
|
172
|
+
this.#sizes.clear();
|
|
173
|
+
this.#bytes = 0;
|
|
174
|
+
this.#dropped = 0;
|
|
175
|
+
}
|
|
176
|
+
snapshot() {
|
|
177
|
+
return { records: [...this.#records].reverse(), dropped: this.#dropped };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// apps/preview/src/project-inspection.ts
|
|
182
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
183
|
+
import { createRequire } from "node:module";
|
|
184
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
185
|
+
import { parse } from "acorn";
|
|
186
|
+
import { RUNTIME_PACKAGE_NAME } from "./platform.js";
|
|
187
|
+
var Unknown = class {
|
|
188
|
+
constructor(reason = "\u914D\u7F6E\u4F9D\u8D56\u8FD0\u884C\u65F6\u4FE1\u606F\u6216\u6682\u4E0D\u652F\u6301\u7684\u8868\u8FBE\u5F0F\u3002", textPrefix) {
|
|
189
|
+
this.reason = reason;
|
|
190
|
+
this.textPrefix = textPrefix;
|
|
191
|
+
}
|
|
192
|
+
reason;
|
|
193
|
+
textPrefix;
|
|
194
|
+
};
|
|
195
|
+
var Api = class {
|
|
196
|
+
constructor(name) {
|
|
197
|
+
this.name = name;
|
|
198
|
+
}
|
|
199
|
+
name;
|
|
200
|
+
};
|
|
201
|
+
var PlatformTool = class {
|
|
202
|
+
constructor(name) {
|
|
203
|
+
this.name = name;
|
|
204
|
+
}
|
|
205
|
+
name;
|
|
206
|
+
};
|
|
207
|
+
var Fn = class {
|
|
208
|
+
constructor(node, scope) {
|
|
209
|
+
this.node = node;
|
|
210
|
+
this.scope = scope;
|
|
211
|
+
}
|
|
212
|
+
node;
|
|
213
|
+
scope;
|
|
214
|
+
};
|
|
215
|
+
var Fields = class _Fields {
|
|
216
|
+
constructor(entries, optional = false) {
|
|
217
|
+
this.entries = entries;
|
|
218
|
+
this.optional = optional;
|
|
219
|
+
}
|
|
220
|
+
entries;
|
|
221
|
+
optional;
|
|
222
|
+
get(key) {
|
|
223
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
224
|
+
const entry = this.entries[i];
|
|
225
|
+
if (entry.key !== void 0) {
|
|
226
|
+
if (entry.key === key) return entry.value();
|
|
227
|
+
} else {
|
|
228
|
+
const spread = entry.value();
|
|
229
|
+
if (!(spread instanceof _Fields)) return new Unknown("\u5BF9\u8C61\u5305\u542B\u52A8\u6001\u5C55\u5F00\uFF0C\u65E0\u6CD5\u786E\u5B9A\u6700\u7EC8\u914D\u7F6E\u3002");
|
|
230
|
+
const keys = spread.keys();
|
|
231
|
+
if (!keys || keys.includes(key)) return spread.get(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
keys() {
|
|
237
|
+
const keys = /* @__PURE__ */ new Set();
|
|
238
|
+
for (const entry of this.entries) {
|
|
239
|
+
if (entry.key !== void 0) keys.add(entry.key);
|
|
240
|
+
else {
|
|
241
|
+
const spread = entry.value(), nested = spread instanceof _Fields ? spread.keys() : void 0;
|
|
242
|
+
if (!nested) return void 0;
|
|
243
|
+
nested.forEach((key) => keys.add(key));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return [...keys];
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var fields = (data) => new Fields(Object.entries(data).map(([key, value]) => ({ key, value: () => value })));
|
|
250
|
+
var unknown = () => new Unknown();
|
|
251
|
+
var isPrimitive = (value) => value === null || value === void 0 || ["string", "number", "boolean"].includes(typeof value);
|
|
252
|
+
var Scope = class {
|
|
253
|
+
constructor(file, parent) {
|
|
254
|
+
this.file = file;
|
|
255
|
+
this.parent = parent;
|
|
256
|
+
}
|
|
257
|
+
file;
|
|
258
|
+
parent;
|
|
259
|
+
bindings = /* @__PURE__ */ new Map();
|
|
260
|
+
mutable = /* @__PURE__ */ new Set();
|
|
261
|
+
get(name) {
|
|
262
|
+
if (this.mutable.has(name)) return new Unknown("\u914D\u7F6E\u5305\u542B\u53EF\u53D8\u503C\u6216\u540E\u7EED\u4FEE\u6539\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
|
|
263
|
+
const binding = this.bindings.get(name);
|
|
264
|
+
return binding ? binding() : this.parent?.get(name) ?? new Unknown();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
var within = (root, file) => {
|
|
268
|
+
const path = relative(root, file);
|
|
269
|
+
return !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
270
|
+
};
|
|
271
|
+
var keyOf = (node, computed = false) => !computed && node.type === "Identifier" ? node.name : node.type === "Literal" && ["string", "number"].includes(typeof node.value) ? String(node.value) : void 0;
|
|
272
|
+
var unwrap = (node) => node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" ? node.declaration ?? node : node;
|
|
273
|
+
function walk(node, visit, functions = false) {
|
|
274
|
+
visit(node);
|
|
275
|
+
if (!functions && /^(?:Function|ArrowFunction)/u.test(node.type)) return;
|
|
276
|
+
for (const value of Object.values(node)) {
|
|
277
|
+
if (Array.isArray(value)) {
|
|
278
|
+
for (const child of value) if (child?.type) walk(child, visit, functions);
|
|
279
|
+
} else if (value && typeof value === "object" && value.type) walk(value, visit, functions);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
var Reader = class {
|
|
283
|
+
sources = /* @__PURE__ */ new Map();
|
|
284
|
+
dependencies = /* @__PURE__ */ new Map();
|
|
285
|
+
root;
|
|
286
|
+
runtimeRoot;
|
|
287
|
+
bytes = 0;
|
|
288
|
+
steps = 0;
|
|
289
|
+
depth = 0;
|
|
290
|
+
constructor(root) {
|
|
291
|
+
this.root = realpathSync(root);
|
|
292
|
+
}
|
|
293
|
+
tick() {
|
|
294
|
+
if (++this.steps > 5e4) throw new Error("inspection budget");
|
|
295
|
+
}
|
|
296
|
+
lazy(read) {
|
|
297
|
+
let state = 0, result;
|
|
298
|
+
return () => {
|
|
299
|
+
this.tick();
|
|
300
|
+
if (state === 1) return new Unknown("\u914D\u7F6E\u5B58\u5728\u5FAA\u73AF\u5F15\u7528\u3002");
|
|
301
|
+
if (state === 2) return result;
|
|
302
|
+
state = 1;
|
|
303
|
+
try {
|
|
304
|
+
result = read();
|
|
305
|
+
} catch {
|
|
306
|
+
result = new Unknown("\u914D\u7F6E\u65E0\u6CD5\u9759\u6001\u8BFB\u53D6\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
|
|
307
|
+
}
|
|
308
|
+
state = 2;
|
|
309
|
+
return result;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
source(path) {
|
|
313
|
+
if (!this.dependencies.has(path)) this.dependencies.set(path, fingerprint(path));
|
|
314
|
+
const file = realpathSync(path);
|
|
315
|
+
const inRuntime = this.runtimeRoot && within(this.runtimeRoot, file);
|
|
316
|
+
if ((!within(this.root, file) || relative(this.root, file).split(sep).includes("node_modules")) && !inRuntime) throw new Error("outside project");
|
|
317
|
+
if (![".js", ".mjs", ".cjs"].includes(extname(file))) throw new Error("unsupported source");
|
|
318
|
+
const existing = this.sources.get(file);
|
|
319
|
+
if (existing) return existing;
|
|
320
|
+
const size = statSync(file).size;
|
|
321
|
+
if (this.sources.size >= 80 || size > 512e3 || (this.bytes += size) > 3e6) throw new Error("inspection size");
|
|
322
|
+
const tree = parse(readFileSync(file, "utf8"), { ecmaVersion: "latest", sourceType: "module" });
|
|
323
|
+
const scope = new Scope(file), exports = /* @__PURE__ */ new Map();
|
|
324
|
+
const result = { tree, scope, exports };
|
|
325
|
+
this.sources.set(file, result);
|
|
326
|
+
this.bind(tree.body, scope);
|
|
327
|
+
for (const statement of tree.body) {
|
|
328
|
+
if (statement.type === "ImportDeclaration") {
|
|
329
|
+
for (const specifier of statement.specifiers) {
|
|
330
|
+
const name = specifier.type === "ImportDefaultSpecifier" ? "default" : specifier.type === "ImportNamespaceSpecifier" ? "*" : keyOf(specifier.imported);
|
|
331
|
+
scope.bindings.set(specifier.local.name, this.lazy(() => this.imported(scope.file, statement.source.value, name ?? "")));
|
|
332
|
+
}
|
|
333
|
+
} else if (statement.type === "ExportDefaultDeclaration") {
|
|
334
|
+
exports.set("default", this.lazy(() => this.expression(statement.declaration, scope)));
|
|
335
|
+
} else if (statement.type === "ExportNamedDeclaration") {
|
|
336
|
+
const declaration = statement.declaration;
|
|
337
|
+
if (declaration?.type === "FunctionDeclaration") exports.set(declaration.id.name, () => scope.get(declaration.id.name));
|
|
338
|
+
if (declaration?.type === "VariableDeclaration") {
|
|
339
|
+
for (const item of declaration.declarations) if (item.id.type === "Identifier") exports.set(item.id.name, () => scope.get(item.id.name));
|
|
340
|
+
}
|
|
341
|
+
for (const specifier of statement.specifiers) {
|
|
342
|
+
const name = keyOf(specifier.exported), local = keyOf(specifier.local);
|
|
343
|
+
exports.set(name, this.lazy(() => statement.source ? this.imported(file, statement.source.value, local) : scope.get(local)));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
imported(file, specifier, name) {
|
|
350
|
+
if (["typebox", "@sinclair/typebox"].includes(specifier) && name === "Type") return new Api("Type");
|
|
351
|
+
if (specifier === "@my-life-buddies/buddy-runtime" && name === "BuddyServer") return new Api("BuddyServer");
|
|
352
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) return new Unknown("\u914D\u7F6E\u6765\u81EA\u5916\u90E8\u4F9D\u8D56\uFF0C\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\u3002");
|
|
353
|
+
const target = resolve(dirname(file), specifier);
|
|
354
|
+
for (const candidate of [target, `${target}.mjs`, `${target}.js`, join(target, "index.js")]) {
|
|
355
|
+
try {
|
|
356
|
+
const source = this.source(candidate);
|
|
357
|
+
return name === "*" ? new Fields([...source.exports].map(([key, value]) => ({ key, value }))) : source.exports.get(name)?.() ?? unknown();
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return new Unknown("\u5F15\u7528\u7684\u672C\u5730\u6E90\u6587\u4EF6\u65E0\u6CD5\u8BFB\u53D6\u6216\u4E0D\u652F\u6301\u9759\u6001\u5206\u6790\u3002");
|
|
362
|
+
}
|
|
363
|
+
bind(statements, scope) {
|
|
364
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
365
|
+
const localValues = /* @__PURE__ */ new Set();
|
|
366
|
+
for (const item of statements) {
|
|
367
|
+
const statement = unwrap(item);
|
|
368
|
+
if (statement.type === "FunctionDeclaration" && statement.id) scope.bindings.set(statement.id.name, () => new Fn(statement, scope));
|
|
369
|
+
if (statement.type === "VariableDeclaration") for (const declaration of statement.declarations) {
|
|
370
|
+
this.pattern(declaration.id, this.lazy(() => this.expression(declaration.init, scope)), scope);
|
|
371
|
+
if (declaration.id.type === "Identifier") localValues.add(declaration.id.name);
|
|
372
|
+
let source = declaration.init;
|
|
373
|
+
while (source?.type === "MemberExpression") source = source.object;
|
|
374
|
+
if (declaration.id.type === "Identifier" && source?.type === "Identifier") aliases.set(declaration.id.name, source.name);
|
|
375
|
+
if (statement.kind !== "const") walk(declaration.id, (node) => {
|
|
376
|
+
if (node.type === "Identifier") scope.mutable.add(node.name);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const item of statements) walk(item, (node) => {
|
|
381
|
+
if (node.type === "ExpressionStatement" && node.expression.type === "CallExpression") {
|
|
382
|
+
for (const argument of node.expression.arguments) {
|
|
383
|
+
let root = argument;
|
|
384
|
+
while (root.type === "MemberExpression") root = root.object;
|
|
385
|
+
if (root.type === "Identifier" && localValues.has(root.name)) scope.mutable.add(root.name);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let target;
|
|
389
|
+
if (node.type === "AssignmentExpression") target = node.left;
|
|
390
|
+
if (node.type === "UpdateExpression") target = node.argument;
|
|
391
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && ["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin", "assign", "defineProperty"].includes(keyOf(node.callee.property, node.callee.computed) ?? "")) target = node.callee.object.name === "Object" ? node.arguments[0] : node.callee.object;
|
|
392
|
+
while (target?.type === "MemberExpression") target = target.object;
|
|
393
|
+
if (target?.type === "Identifier") scope.mutable.add(target.name);
|
|
394
|
+
});
|
|
395
|
+
for (let changed = true; changed; ) {
|
|
396
|
+
changed = false;
|
|
397
|
+
for (const [alias, original] of aliases) if (scope.mutable.has(alias) && !scope.mutable.has(original)) {
|
|
398
|
+
scope.mutable.add(original);
|
|
399
|
+
changed = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
pattern(pattern, value, scope) {
|
|
404
|
+
if (pattern.type === "Identifier") scope.bindings.set(pattern.name, value);
|
|
405
|
+
else if (pattern.type === "ObjectPattern") for (const property of pattern.properties) {
|
|
406
|
+
if (property.type !== "Property") continue;
|
|
407
|
+
const key = keyOf(property.key, property.computed);
|
|
408
|
+
this.pattern(property.value, this.lazy(() => key === void 0 ? unknown() : this.member(value(), key)), scope);
|
|
409
|
+
}
|
|
410
|
+
else if (pattern.type === "ArrayPattern") pattern.elements.forEach((item, index) => {
|
|
411
|
+
if (item) this.pattern(item, this.lazy(() => this.member(value(), String(index))), scope);
|
|
412
|
+
});
|
|
413
|
+
else if (pattern.type === "AssignmentPattern") this.pattern(pattern.left, this.lazy(() => {
|
|
414
|
+
const item = value();
|
|
415
|
+
return item === void 0 ? this.expression(pattern.right, scope) : item;
|
|
416
|
+
}), scope);
|
|
417
|
+
}
|
|
418
|
+
functionScope(fn, args) {
|
|
419
|
+
const scope = new Scope(fn.scope.file, fn.scope);
|
|
420
|
+
fn.node.params.forEach((param, index) => this.pattern(param, () => args[index], scope));
|
|
421
|
+
if (fn.node.body.type === "BlockStatement") this.bind(fn.node.body.body, scope);
|
|
422
|
+
return scope;
|
|
423
|
+
}
|
|
424
|
+
call(fn, args) {
|
|
425
|
+
if (fn instanceof Api && fn.name.startsWith("Type.")) return this.schema(fn.name.slice(5), args);
|
|
426
|
+
if (!(fn instanceof Fn)) return new Unknown("\u51FD\u6570\u8C03\u7528\u7684\u7ED3\u679C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
|
|
427
|
+
if (++this.depth > 32) {
|
|
428
|
+
this.depth--;
|
|
429
|
+
return unknown();
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const scope = this.functionScope(fn, args), body = fn.node.body;
|
|
433
|
+
if (body.type !== "BlockStatement") return this.expression(body, scope);
|
|
434
|
+
for (const statement of body.body) {
|
|
435
|
+
if (statement.type === "ReturnStatement") return this.expression(statement.argument, scope);
|
|
436
|
+
let conditionalReturn = false;
|
|
437
|
+
walk(statement, (node) => {
|
|
438
|
+
if (node.type === "ReturnStatement") conditionalReturn = true;
|
|
439
|
+
});
|
|
440
|
+
if (conditionalReturn) return new Unknown("\u914D\u7F6E\u5DE5\u5382\u5305\u542B\u6761\u4EF6\u8FD4\u56DE\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
|
|
441
|
+
}
|
|
442
|
+
return unknown();
|
|
443
|
+
} finally {
|
|
444
|
+
this.depth--;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
member(object3, key) {
|
|
448
|
+
if (object3 instanceof Fields) return object3.get(key);
|
|
449
|
+
if (object3 instanceof Api) return object3.name === "conversation.tools" ? new PlatformTool(key) : new Api(`${object3.name}.${key}`);
|
|
450
|
+
if (Array.isArray(object3)) return key === "length" ? object3.length : /^\d+$/u.test(key) ? object3[Number(key)] : unknown();
|
|
451
|
+
return object3 instanceof Unknown ? object3 : unknown();
|
|
452
|
+
}
|
|
453
|
+
expression(node, scope) {
|
|
454
|
+
this.tick();
|
|
455
|
+
if (!node) return void 0;
|
|
456
|
+
switch (node.type) {
|
|
457
|
+
case "Literal":
|
|
458
|
+
return node.regex || node.bigint ? unknown() : node.value;
|
|
459
|
+
case "Identifier":
|
|
460
|
+
return node.name === "undefined" ? void 0 : scope.get(node.name);
|
|
461
|
+
case "FunctionDeclaration":
|
|
462
|
+
case "FunctionExpression":
|
|
463
|
+
case "ArrowFunctionExpression":
|
|
464
|
+
return new Fn(node, scope);
|
|
465
|
+
case "AwaitExpression":
|
|
466
|
+
case "ChainExpression":
|
|
467
|
+
return this.expression(node.argument ?? node.expression, scope);
|
|
468
|
+
case "ObjectExpression":
|
|
469
|
+
return new Fields(node.properties.map((property) => ({ key: property.type === "SpreadElement" ? void 0 : keyOf(property.key, property.computed), value: this.lazy(() => property.type === "SpreadElement" ? this.expression(property.argument, scope) : property.kind === "get" || property.kind === "set" ? unknown() : property.computed && keyOf(property.key, true) === void 0 ? unknown() : this.expression(property.value, scope)) })));
|
|
470
|
+
case "ArrayExpression": {
|
|
471
|
+
const values = [];
|
|
472
|
+
for (const element of node.elements) {
|
|
473
|
+
if (element?.type === "SpreadElement") {
|
|
474
|
+
const value = this.expression(element.argument, scope);
|
|
475
|
+
values.push(...Array.isArray(value) ? value : [value instanceof Unknown ? value : unknown()]);
|
|
476
|
+
} else values.push(this.expression(element, scope));
|
|
477
|
+
}
|
|
478
|
+
return values;
|
|
479
|
+
}
|
|
480
|
+
case "MemberExpression": {
|
|
481
|
+
const key = node.computed ? this.expression(node.property, scope) : node.property.name;
|
|
482
|
+
return typeof key === "string" || typeof key === "number" ? this.member(this.expression(node.object, scope), String(key)) : unknown();
|
|
483
|
+
}
|
|
484
|
+
case "CallExpression": {
|
|
485
|
+
if (node.callee.type === "MemberExpression" && keyOf(node.callee.property, node.callee.computed) === "map") {
|
|
486
|
+
const values = this.expression(node.callee.object, scope), fn = this.expression(node.arguments[0], scope);
|
|
487
|
+
return Array.isArray(values) ? values.map((value, index) => this.call(fn, [value, index])) : unknown();
|
|
488
|
+
}
|
|
489
|
+
return this.call(this.expression(node.callee, scope), node.arguments.map((arg) => this.expression(arg, scope)));
|
|
490
|
+
}
|
|
491
|
+
case "ConditionalExpression": {
|
|
492
|
+
const test = this.expression(node.test, scope);
|
|
493
|
+
return isPrimitive(test) ? this.expression(test ? node.consequent : node.alternate, scope) : new Unknown("\u914D\u7F6E\u6839\u636E\u73AF\u5883\u6216\u4F1A\u8BDD\u52A8\u6001\u9009\u62E9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
|
|
494
|
+
}
|
|
495
|
+
case "LogicalExpression": {
|
|
496
|
+
const left = this.expression(node.left, scope);
|
|
497
|
+
if (left instanceof Unknown) return left;
|
|
498
|
+
if (node.operator === "??") return left == null ? this.expression(node.right, scope) : left;
|
|
499
|
+
return (node.operator === "&&" ? Boolean(left) : !left) ? this.expression(node.right, scope) : left;
|
|
500
|
+
}
|
|
501
|
+
case "BinaryExpression": {
|
|
502
|
+
const left = this.expression(node.left, scope), right = this.expression(node.right, scope);
|
|
503
|
+
if (!isPrimitive(left) || !isPrimitive(right)) return node.operator === "+" && typeof left === "string" ? new Unknown("\u63CF\u8FF0\u5305\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002", left) : unknown();
|
|
504
|
+
if (node.operator === "+" && (typeof left === "string" || typeof right === "string")) return `${left}${right}`;
|
|
505
|
+
if (node.operator === "===") return left === right;
|
|
506
|
+
if (node.operator === "!==") return left !== right;
|
|
507
|
+
return unknown();
|
|
508
|
+
}
|
|
509
|
+
case "UnaryExpression": {
|
|
510
|
+
const value = this.expression(node.argument, scope);
|
|
511
|
+
if (!isPrimitive(value)) return unknown();
|
|
512
|
+
return node.operator === "!" ? !value : node.operator === "-" && typeof value === "number" ? -value : unknown();
|
|
513
|
+
}
|
|
514
|
+
case "TemplateLiteral": {
|
|
515
|
+
let result = "";
|
|
516
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
517
|
+
result += node.quasis[i].value.cooked;
|
|
518
|
+
if (i < node.expressions.length) {
|
|
519
|
+
const value = this.expression(node.expressions[i], scope);
|
|
520
|
+
if (!isPrimitive(value)) return new Unknown("\u63CF\u8FF0\u5305\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002", result);
|
|
521
|
+
result += String(value);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
default:
|
|
527
|
+
return unknown();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
schema(name, args) {
|
|
531
|
+
const first = args[0];
|
|
532
|
+
if (name === "Optional") return first instanceof Fields ? new Fields(first.entries, true) : unknown();
|
|
533
|
+
const scalar = { String: "string", Number: "number", Integer: "integer", Boolean: "boolean", Null: "null" };
|
|
534
|
+
let schema, options;
|
|
535
|
+
if (scalar[name]) {
|
|
536
|
+
schema = fields({ type: scalar[name] });
|
|
537
|
+
options = first;
|
|
538
|
+
} else if (name === "Object" && first instanceof Fields) {
|
|
539
|
+
const keys = first.keys();
|
|
540
|
+
if (!keys) return unknown();
|
|
541
|
+
const required = [];
|
|
542
|
+
for (const key of keys) {
|
|
543
|
+
const value = first.get(key);
|
|
544
|
+
if (!(value instanceof Fields)) return unknown();
|
|
545
|
+
if (!value.optional) required.push(key);
|
|
546
|
+
}
|
|
547
|
+
schema = fields({ type: "object", properties: first, ...required.length ? { required } : {} });
|
|
548
|
+
options = args[1];
|
|
549
|
+
} else if (name === "Array") {
|
|
550
|
+
schema = fields({ type: "array", items: first });
|
|
551
|
+
options = args[1];
|
|
552
|
+
} else if (name === "Literal" && isPrimitive(first) && first !== void 0) {
|
|
553
|
+
schema = fields({ const: first, type: first === null ? "null" : typeof first });
|
|
554
|
+
options = args[1];
|
|
555
|
+
} else if (name === "Union" && Array.isArray(first)) {
|
|
556
|
+
schema = first.length === 1 && first[0] instanceof Fields ? first[0] : fields({ anyOf: first });
|
|
557
|
+
options = args[1];
|
|
558
|
+
} else if (name === "Any" || name === "Unknown") {
|
|
559
|
+
schema = fields({});
|
|
560
|
+
options = first;
|
|
561
|
+
} else return new Unknown("\u53C2\u6570\u4F7F\u7528\u6682\u4E0D\u652F\u6301\u7684 Schema \u6784\u9020\u65B9\u6CD5\u3002");
|
|
562
|
+
return options === void 0 ? schema : options instanceof Fields ? new Fields([...schema.entries, { value: () => options }]) : unknown();
|
|
563
|
+
}
|
|
564
|
+
json(value, depth = 0) {
|
|
565
|
+
this.tick();
|
|
566
|
+
if (depth > 30) throw new Error("schema depth");
|
|
567
|
+
if (isPrimitive(value)) return value;
|
|
568
|
+
if (Array.isArray(value)) return value.map((item) => this.json(item, depth + 1));
|
|
569
|
+
if (value instanceof Fields) {
|
|
570
|
+
const keys = value.keys();
|
|
571
|
+
if (!keys) throw new Error("dynamic schema");
|
|
572
|
+
return Object.fromEntries(keys.map((key) => [key, this.json(value.get(key), depth + 1)]));
|
|
573
|
+
}
|
|
574
|
+
throw new Error("dynamic schema");
|
|
575
|
+
}
|
|
576
|
+
runtime() {
|
|
577
|
+
try {
|
|
578
|
+
const path = createRequire(join(this.root, "package.json")).resolve("@my-life-buddies/buddy-runtime/package.json");
|
|
579
|
+
this.dependencies.set(path, fingerprint(path));
|
|
580
|
+
const manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
581
|
+
if (manifest.name !== RUNTIME_PACKAGE_NAME) throw new Error("runtime package");
|
|
582
|
+
this.runtimeRoot = realpathSync(dirname(path));
|
|
583
|
+
const source = this.source(join(this.runtimeRoot, "dist/conversation/agent.js"));
|
|
584
|
+
const factory = source.exports.get("createAgent")?.();
|
|
585
|
+
const scope = factory instanceof Fn ? this.functionScope(factory, [unknown()]) : void 0;
|
|
586
|
+
return { tools: scope?.get("platformTools") ?? unknown(), builtin: source.exports.has("builtinTools") };
|
|
587
|
+
} catch {
|
|
588
|
+
return { tools: unknown(), builtin: true };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
inspect() {
|
|
592
|
+
const source = this.source(join(this.root, "start.mjs"));
|
|
593
|
+
const starts = [];
|
|
594
|
+
walk(source.tree, (node) => {
|
|
595
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
596
|
+
if (keyOf(node.callee.property, node.callee.computed) !== "start") return;
|
|
597
|
+
const owner = this.expression(node.callee.object, source.scope);
|
|
598
|
+
if (owner instanceof Api && owner.name === "BuddyServer") starts.push(node);
|
|
599
|
+
});
|
|
600
|
+
const conversation = new Fields([{ value: () => new Unknown("\u914D\u7F6E\u4F9D\u8D56\u5F53\u524D\u4F1A\u8BDD\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002") }, { key: "tools", value: () => new Api("conversation.tools") }]);
|
|
601
|
+
const options = starts.length === 1 ? this.call(this.expression(starts[0].arguments[0], source.scope), [conversation]) : new Unknown("\u672A\u8BC6\u522B\u5230\u552F\u4E00\u7684 BuddyServer.start \u5DE5\u5382\u5165\u53E3\u3002");
|
|
602
|
+
const initial = this.member(options, "initialState"), model = this.member(initial, "model");
|
|
603
|
+
const toolValues = this.member(initial, "tools"), runtime = this.runtime();
|
|
604
|
+
const tools = [], unresolved = [];
|
|
605
|
+
const list = toolValues === void 0 && initial instanceof Fields ? [] : Array.isArray(toolValues) ? toolValues : [toolValues];
|
|
606
|
+
let payloadSize = 0;
|
|
607
|
+
for (const item of list) {
|
|
608
|
+
if (tools.length + unresolved.length >= 128 || payloadSize > 25e4) {
|
|
609
|
+
unresolved.push({ reason: "\u914D\u7F6E\u6E05\u5355\u8D85\u51FA\u9759\u6001\u5C55\u793A\u4E0A\u9650\uFF0C\u5176\u4F59\u5DE5\u5177\u8BF7\u5728\u6A21\u578B\u8BF7\u6C42\u4E2D\u67E5\u770B\u3002" });
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
const origin = item instanceof PlatformTool ? "platform" : "custom";
|
|
613
|
+
const value = item instanceof PlatformTool ? this.member(runtime.tools, item.name) : item;
|
|
614
|
+
if (item instanceof PlatformTool && value === void 0) {
|
|
615
|
+
unresolved.push({ name: item.name, reason: "\u5F53\u524D\u5B89\u88C5\u7684 Runtime \u672A\u63D0\u4F9B\u6B64\u5E73\u53F0\u5DE5\u5177\uFF0C\u8BF7\u68C0\u67E5\u6CE8\u518C\u914D\u7F6E\u3002" });
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const name = item instanceof PlatformTool ? item.name : this.member(value, "name");
|
|
619
|
+
if (typeof name !== "string" || !/^[\w.-]{1,128}$/u.test(name)) {
|
|
620
|
+
unresolved.push({ reason: value instanceof Unknown ? value.reason : "\u5DE5\u5177\u540D\u79F0\u6216\u6CE8\u518C\u5173\u7CFB\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\u3002" });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const description = this.member(value, "description"), parameters = this.member(value, "parameters");
|
|
624
|
+
let schema;
|
|
625
|
+
try {
|
|
626
|
+
const json2 = this.json(parameters);
|
|
627
|
+
if (json2 && typeof json2 === "object" && !Array.isArray(json2) && JSON.stringify(json2).length <= 32768) schema = json2;
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
const reason = [typeof description !== "string" ? description instanceof Unknown && description.textPrefix ? "\u63CF\u8FF0\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u8BFB\u53D6\u90E8\u5206\u3002" : "\u5DE5\u5177\u63CF\u8FF0\u5C1A\u672A\u9759\u6001\u8BFB\u53D6\u3002" : "", !schema ? "\u53C2\u6570\u5B9A\u4E49\u5C1A\u672A\u9759\u6001\u8BFB\u53D6\u3002" : ""].join("");
|
|
631
|
+
const descriptionText = typeof description === "string" ? description : description instanceof Unknown ? description.textPrefix : void 0;
|
|
632
|
+
const tool = { name, origin, ...descriptionText ? { description: descriptionText.slice(0, 4096) } : {}, ...schema ? { parameters: schema } : {}, ...reason ? { reason } : {} };
|
|
633
|
+
payloadSize += Buffer.byteLength(JSON.stringify(tool));
|
|
634
|
+
tools.push(tool);
|
|
635
|
+
}
|
|
636
|
+
if (runtime.builtin) unresolved.push({ name: "Runtime \u5185\u7F6E\u5DE5\u5177", reason: "Runtime \u53EF\u80FD\u6309\u6A21\u578B\u80FD\u529B\u8FFD\u52A0\u5DE5\u5177\uFF1B\u662F\u5426\u542F\u7528\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\uFF0C\u53EF\u5728\u6A21\u578B\u8BF7\u6C42\u4E2D\u67E5\u770B\u3002" });
|
|
637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
638
|
+
for (const tool of tools) {
|
|
639
|
+
if (seen.has(tool.name)) unresolved.push({ name: tool.name, reason: "\u6CE8\u518C\u5217\u8868\u4E2D\u5B58\u5728\u540C\u540D\u5DE5\u5177\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u7A0B\u914D\u7F6E\u3002" });
|
|
640
|
+
seen.add(tool.name);
|
|
641
|
+
}
|
|
642
|
+
return { status: unresolved.length || tools.some((tool) => tool.reason) ? "partial" : "complete", model: typeof model === "string" && model.length > 0 && model.length <= 128 ? { status: "known", value: model } : { status: "dynamic", reason: model instanceof Unknown ? model.reason : "\u6A21\u578B\u914D\u7F6E\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002" }, tools, unresolved };
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
var fingerprint = (path) => {
|
|
646
|
+
try {
|
|
647
|
+
const stat = statSync(path);
|
|
648
|
+
return `${realpathSync(path)}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
649
|
+
} catch {
|
|
650
|
+
return "missing";
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
function createProjectInspector(projectRoot) {
|
|
654
|
+
let cached;
|
|
655
|
+
let files = /* @__PURE__ */ new Map();
|
|
656
|
+
return async () => {
|
|
657
|
+
if (cached && [...files].every(([path, stamp]) => fingerprint(path) === stamp)) return cached;
|
|
658
|
+
let reader;
|
|
659
|
+
try {
|
|
660
|
+
reader = new Reader(projectRoot);
|
|
661
|
+
cached = reader.inspect();
|
|
662
|
+
} catch {
|
|
663
|
+
cached = { status: "partial", model: { status: "dynamic", reason: "\u5DE5\u7A0B\u5165\u53E3\u65E0\u6CD5\u9759\u6001\u8BFB\u53D6\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002" }, tools: [], unresolved: [{ reason: "\u5DE5\u5177\u6CE8\u518C\u5173\u7CFB\u5C1A\u672A\u8BFB\u53D6\uFF1B\u8BF7\u786E\u8BA4\u5165\u53E3\u53CA\u5F15\u7528\u6587\u4EF6\u5B58\u5728\u4E14\u8BED\u6CD5\u6709\u6548\u3002" }] };
|
|
664
|
+
}
|
|
665
|
+
files = new Map([join(projectRoot, "start.mjs"), join(projectRoot, "node_modules/@my-life-buddies/buddy-runtime/package.json")].map((path) => [path, fingerprint(path)]));
|
|
666
|
+
for (const [path, stamp] of reader?.dependencies ?? []) files.set(path, stamp);
|
|
667
|
+
return cached;
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// apps/preview/src/controller.ts
|
|
672
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
673
|
+
function publicFailure(cause) {
|
|
674
|
+
const publicMessage = typeof cause === "object" && cause !== null && "publicMessage" in cause && typeof cause.publicMessage === "string" && cause.publicMessage.trim() && cause.publicMessage.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(cause.publicMessage) ? cause.publicMessage : void 0;
|
|
675
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" && cause.code.trim()) {
|
|
676
|
+
return { code: cause.code, message: publicMessage ?? "DEV \u4F1A\u8BDD\u672A\u80FD\u5B8C\u6210" };
|
|
677
|
+
}
|
|
678
|
+
return { code: "DEV_START_FAILED", message: "DEV \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25" };
|
|
679
|
+
}
|
|
680
|
+
var PreviewController = class {
|
|
681
|
+
#projectRoot;
|
|
682
|
+
#devStarter;
|
|
683
|
+
#readConfig;
|
|
684
|
+
#readBuddyDetails;
|
|
685
|
+
#now;
|
|
686
|
+
#readInspection;
|
|
687
|
+
#inspectionPromise;
|
|
688
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
689
|
+
#simulatorListeners = /* @__PURE__ */ new Set();
|
|
690
|
+
#runs = /* @__PURE__ */ new Map();
|
|
691
|
+
#outputWatchers = /* @__PURE__ */ new Map();
|
|
692
|
+
#failedOutputRuns = /* @__PURE__ */ new Set();
|
|
693
|
+
#observedActiveRuns = /* @__PURE__ */ new Set();
|
|
694
|
+
#events = [];
|
|
695
|
+
#diagnostics = new RuntimeDiagnostics();
|
|
696
|
+
#runOrdinal = 0;
|
|
697
|
+
#conversationRevision = 0;
|
|
698
|
+
#simulatorRevision = 0;
|
|
699
|
+
#resetting = false;
|
|
700
|
+
#sendingMessage = false;
|
|
701
|
+
get resetting() {
|
|
702
|
+
return this.#resetting;
|
|
703
|
+
}
|
|
704
|
+
#state = Object.freeze({ phase: "idle", revision: 0 });
|
|
705
|
+
#handle;
|
|
706
|
+
#simulator;
|
|
707
|
+
#startController;
|
|
708
|
+
#startPromise;
|
|
709
|
+
#stopPromise;
|
|
710
|
+
#messages = [];
|
|
711
|
+
#simulatorState = Object.freeze({ messages: Object.freeze([]) });
|
|
712
|
+
#lastActiveRunId;
|
|
713
|
+
#eventSequence = 0;
|
|
714
|
+
constructor(options) {
|
|
715
|
+
this.#projectRoot = options.projectRoot;
|
|
716
|
+
this.#devStarter = options.devStarter;
|
|
717
|
+
this.#readConfig = options.readConfig ?? ((root) => readBuddyConfig(root));
|
|
718
|
+
this.#readBuddyDetails = options.readBuddyDetails;
|
|
719
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
720
|
+
this.#readInspection = options.readInspection ?? createProjectInspector(options.projectRoot);
|
|
721
|
+
}
|
|
722
|
+
get projectRoot() {
|
|
723
|
+
return this.#projectRoot;
|
|
724
|
+
}
|
|
725
|
+
state() {
|
|
726
|
+
return this.#state;
|
|
727
|
+
}
|
|
728
|
+
async refreshInspection() {
|
|
729
|
+
if (this.#closed) return;
|
|
730
|
+
if (this.#inspectionPromise) return this.#inspectionPromise;
|
|
731
|
+
this.#inspectionPromise = this.#readInspection().then((inspection) => {
|
|
732
|
+
if (!this.#closed && JSON.stringify(inspection) !== JSON.stringify(this.#state.inspection)) {
|
|
733
|
+
this.#setState({ ...this.#state, inspection });
|
|
734
|
+
}
|
|
735
|
+
}).finally(() => {
|
|
736
|
+
this.#inspectionPromise = void 0;
|
|
737
|
+
});
|
|
738
|
+
return this.#inspectionPromise;
|
|
739
|
+
}
|
|
740
|
+
async project() {
|
|
741
|
+
return this.#readConfig(this.#projectRoot);
|
|
742
|
+
}
|
|
743
|
+
async buddyDetails() {
|
|
744
|
+
if (!this.#readBuddyDetails) return this.#state.ready?.buddy ?? null;
|
|
745
|
+
const config = await this.project();
|
|
746
|
+
return this.#readBuddyDetails(config.buddyId);
|
|
747
|
+
}
|
|
748
|
+
subscribe(listener) {
|
|
749
|
+
this.#listeners.add(listener);
|
|
750
|
+
return () => this.#listeners.delete(listener);
|
|
751
|
+
}
|
|
752
|
+
subscribeSimulator(listener) {
|
|
753
|
+
this.#simulatorListeners.add(listener);
|
|
754
|
+
return () => this.#simulatorListeners.delete(listener);
|
|
755
|
+
}
|
|
756
|
+
simulatorSnapshot() {
|
|
757
|
+
return Object.freeze({ ...this.#simulatorState, conversationRevision: this.#conversationRevision, stateRevision: this.#simulatorRevision });
|
|
758
|
+
}
|
|
759
|
+
#closed = false;
|
|
760
|
+
start() {
|
|
761
|
+
if (this.#closed) return Promise.reject(Object.assign(new Error("\u672C\u5730\u8FDE\u63A5\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u8FDE\u63A5\u6307\u4EE4"), { code: "PREVIEW_CLOSED", status: 409 }));
|
|
762
|
+
if (this.#startPromise) return this.#startPromise;
|
|
763
|
+
if (this.#handle) return Promise.resolve(this.#state);
|
|
764
|
+
if (this.#state.phase === "stopping") {
|
|
765
|
+
return Promise.reject(Object.assign(new Error("DEV \u4F1A\u8BDD\u6B63\u5728\u505C\u6B62"), { code: "DEV_STOPPING" }));
|
|
766
|
+
}
|
|
767
|
+
const controller = new AbortController();
|
|
768
|
+
this.#diagnostics.boundary();
|
|
769
|
+
this.#startController = controller;
|
|
770
|
+
this.#record({ channel: "dev", type: "dev.start_requested", summary: "\u8BF7\u6C42\u542F\u52A8 DEV \u4F1A\u8BDD" });
|
|
771
|
+
this.#setState({ phase: "starting" });
|
|
772
|
+
const startPromise = this.#devStarter.start({
|
|
773
|
+
projectRoot: this.#projectRoot,
|
|
774
|
+
signal: controller.signal,
|
|
775
|
+
onLifecycle: ({ phase, error }) => {
|
|
776
|
+
if (controller.signal.aborted || !this.#handle || this.#state.phase === "stopping") return;
|
|
777
|
+
if (phase === "reloading") {
|
|
778
|
+
this.#stopOutputWatchers();
|
|
779
|
+
void this.#simulator?.resetWidgets?.();
|
|
780
|
+
}
|
|
781
|
+
this.#record({ channel: "dev", type: `dev.${phase}`, summary: phase === "reloading" ? "\u672C\u5730\u6587\u4EF6\u5DF2\u4FEE\u6539\uFF0C\u6B63\u5728\u91CD\u8F7D\uFF1B\u5F53\u524D\u56DE\u590D\u53EF\u80FD\u4E2D\u65AD" : phase === "reload_failed" ? error?.message ?? "\u91CD\u8F7D\u5931\u8D25\uFF0C\u4FEE\u6B63\u6587\u4EF6\u540E\u81EA\u52A8\u91CD\u8BD5" : "\u91CD\u8F7D\u5B8C\u6210\uFF0CDEV \u5DF2\u5C31\u7EEA" });
|
|
782
|
+
this.#setState({ phase, ready: this.#state.ready, ...error ? { error } : {} });
|
|
783
|
+
void this.refreshInspection();
|
|
784
|
+
},
|
|
785
|
+
onLog: ({ source, text: text2 }) => {
|
|
786
|
+
if (controller.signal.aborted) return;
|
|
787
|
+
this.#diagnostics.write(source, text2, this.#now().toISOString());
|
|
788
|
+
this.#record({ channel: "dev", type: source, summary: text2.slice(0, 512) });
|
|
789
|
+
this.#notifySimulator();
|
|
790
|
+
}
|
|
791
|
+
}).then(({ handle, ready, simulator }) => {
|
|
792
|
+
if (controller.signal.aborted) {
|
|
793
|
+
return handle.close().then(() => this.#setState({ phase: "idle" }));
|
|
794
|
+
}
|
|
795
|
+
this.#handle = handle;
|
|
796
|
+
this.#simulator = simulator;
|
|
797
|
+
this.#record({
|
|
798
|
+
channel: "dev",
|
|
799
|
+
type: "dev.ready",
|
|
800
|
+
summary: ready.agentId ? `Agent ${ready.agentId} \u5DF2\u5C31\u7EEA` : "Agent \u5DF2\u5C31\u7EEA"
|
|
801
|
+
});
|
|
802
|
+
void handle.closed.then(
|
|
803
|
+
() => {
|
|
804
|
+
if (this.#handle === handle) {
|
|
805
|
+
this.#handle = void 0;
|
|
806
|
+
void this.#simulator?.disposeWidgets?.();
|
|
807
|
+
this.#simulator = void 0;
|
|
808
|
+
this.#stopOutputWatchers();
|
|
809
|
+
this.#record({ channel: "dev", type: "dev.closed", summary: "DEV \u4F1A\u8BDD\u5DF2\u7ED3\u675F" });
|
|
810
|
+
this.#setState({ phase: "idle" });
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
(cause) => {
|
|
814
|
+
if (this.#handle === handle) {
|
|
815
|
+
this.#handle = void 0;
|
|
816
|
+
void this.#simulator?.disposeWidgets?.();
|
|
817
|
+
this.#simulator = void 0;
|
|
818
|
+
this.#stopOutputWatchers();
|
|
819
|
+
this.#record({ channel: "dev", type: "dev.failed", summary: "DEV \u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F" });
|
|
820
|
+
this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
return this.#setState({ phase: "ready", ready });
|
|
825
|
+
}).catch((cause) => {
|
|
826
|
+
if (controller.signal.aborted) return this.#setState({ phase: "idle" });
|
|
827
|
+
this.#record({ channel: "dev", type: "dev.start_failed", summary: "DEV \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25" });
|
|
828
|
+
return this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
829
|
+
}).finally(() => {
|
|
830
|
+
if (this.#startController === controller) this.#startController = void 0;
|
|
831
|
+
if (this.#startPromise === startPromise) this.#startPromise = void 0;
|
|
832
|
+
});
|
|
833
|
+
this.#startPromise = startPromise;
|
|
834
|
+
return startPromise;
|
|
835
|
+
}
|
|
836
|
+
stop() {
|
|
837
|
+
if (this.#stopPromise) return this.#stopPromise;
|
|
838
|
+
const stopping = this.#stop();
|
|
839
|
+
this.#stopPromise = stopping;
|
|
840
|
+
const clear = () => {
|
|
841
|
+
if (this.#stopPromise === stopping) this.#stopPromise = void 0;
|
|
842
|
+
};
|
|
843
|
+
void stopping.then(clear, clear);
|
|
844
|
+
return stopping;
|
|
845
|
+
}
|
|
846
|
+
async #stop() {
|
|
847
|
+
if (this.#state.phase === "idle") return this.#state;
|
|
848
|
+
if (this.#state.phase === "stopping") return this.#state;
|
|
849
|
+
this.#setState({ phase: "stopping" });
|
|
850
|
+
this.#record({ channel: "dev", type: "dev.stop_requested", summary: "\u8BF7\u6C42\u505C\u6B62 DEV \u4F1A\u8BDD" });
|
|
851
|
+
this.#startController?.abort(new Error("Preview requested DEV stop"));
|
|
852
|
+
const handle = this.#handle;
|
|
853
|
+
this.#handle = void 0;
|
|
854
|
+
const widgetCleanup = this.#simulator?.disposeWidgets?.();
|
|
855
|
+
this.#simulator = void 0;
|
|
856
|
+
this.#stopOutputWatchers();
|
|
857
|
+
try {
|
|
858
|
+
if (handle) await handle.close();
|
|
859
|
+
await widgetCleanup;
|
|
860
|
+
if (this.#startPromise) await this.#startPromise.catch(() => void 0);
|
|
861
|
+
this.#record({ channel: "dev", type: "dev.stopped", summary: "DEV \u4F1A\u8BDD\u5DF2\u505C\u6B62" });
|
|
862
|
+
return this.#setState({ phase: "idle" });
|
|
863
|
+
} catch (cause) {
|
|
864
|
+
this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
865
|
+
throw cause;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async close() {
|
|
869
|
+
this.#closed = true;
|
|
870
|
+
await this.stop();
|
|
871
|
+
this.#listeners.clear();
|
|
872
|
+
this.#simulatorListeners.clear();
|
|
873
|
+
}
|
|
874
|
+
async simulatorState(signal) {
|
|
875
|
+
if (this.#resetting) return this.simulatorSnapshot();
|
|
876
|
+
const simulator = this.#requiredSimulator();
|
|
877
|
+
const revision = this.#conversationRevision;
|
|
878
|
+
const state = await simulator.state({ signal });
|
|
879
|
+
if (!this.#resetting && revision === this.#conversationRevision && simulator === this.#simulator) {
|
|
880
|
+
this.#observeSimulatorState(state);
|
|
881
|
+
}
|
|
882
|
+
return this.simulatorSnapshot();
|
|
883
|
+
}
|
|
884
|
+
/** Never allow an old conversation's delayed widget response to enter the new one. */
|
|
885
|
+
async simulatorWidgets(action, id = "", mediaId = "", messageId) {
|
|
886
|
+
if (this.#resetting) throw Object.assign(new Error("\u4F1A\u8BDD\u6B63\u5728\u91CD\u7F6E"), { status: 409 });
|
|
887
|
+
const client = this.#requiredSimulator();
|
|
888
|
+
const revision = this.#conversationRevision;
|
|
889
|
+
let result;
|
|
890
|
+
if (action === "list" && client.widgets) result = { widgets: await client.widgets(), conversationRevision: revision, capabilities: { documentLifecycle: true, widgetEditing: Boolean(client.widgetCommand) } };
|
|
891
|
+
else if (action === "html" && client.widgetHtml) {
|
|
892
|
+
let target;
|
|
893
|
+
if (messageId !== void 0) {
|
|
894
|
+
const state = await client.state();
|
|
895
|
+
const message2 = state.messages.find((item) => item.messageId === messageId && item.widget?.id === id);
|
|
896
|
+
if (!message2?.widget) throw Object.assign(new Error("\u8FD9\u6761\u5C0F\u6302\u4EF6\u6D88\u606F\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u5237\u65B0\u5BF9\u8BDD\u540E\u91CD\u8BD5"), { status: 404 });
|
|
897
|
+
target = message2.widget.openTarget;
|
|
898
|
+
}
|
|
899
|
+
if (this.#resetting || revision !== this.#conversationRevision || client !== this.#simulator) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
|
|
900
|
+
result = await client.widgetHtml(id, target);
|
|
901
|
+
} else if (action === "image" && client.widgetImage) result = await client.widgetImage(id, mediaId);
|
|
902
|
+
else if ((action === "pin" || action === "unpin") && client.pinWidget) {
|
|
903
|
+
await client.pinWidget(id, action === "pin");
|
|
904
|
+
result = { ok: true };
|
|
905
|
+
} else throw Object.assign(new Error("\u5F53\u524D\u6A21\u62DF\u5668\u4E0D\u652F\u6301\u5C0F\u6302\u4EF6"), { status: 501 });
|
|
906
|
+
if (this.#resetting || revision !== this.#conversationRevision || client !== this.#simulator) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
async simulatorWidgetCommand(id, body) {
|
|
910
|
+
if (this.#resetting) throw Object.assign(new Error("\u4F1A\u8BDD\u6B63\u5728\u91CD\u7F6E"), { status: 409 });
|
|
911
|
+
const client = this.#requiredSimulator(), revision = this.#conversationRevision;
|
|
912
|
+
if (!body || typeof body !== "object" || body.conversationRevision !== revision)
|
|
913
|
+
throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
|
|
914
|
+
if (!client.widgetCommand) throw Object.assign(new Error("\u5F53\u524D\u6A21\u62DF\u5668\u4E0D\u652F\u6301 Widget \u7F16\u8F91"), { status: 501 });
|
|
915
|
+
const result = await client.widgetCommand(id, body);
|
|
916
|
+
if (this.#resetting || client !== this.#simulator || revision !== this.#conversationRevision)
|
|
917
|
+
throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
#productOperationPending = false;
|
|
921
|
+
async simulatorProduct(offerId, input) {
|
|
922
|
+
if (this.#resetting || this.#productOperationPending) throw Object.assign(new Error("\u4F1A\u8BDD\u6B63\u5728\u5904\u7406\u5176\u4ED6\u64CD\u4F5C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"), { status: 409 });
|
|
923
|
+
const client = this.#requiredSimulator(), revision = this.#conversationRevision;
|
|
924
|
+
if (input && input.conversationRevision !== revision) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5546\u54C1\u5361"), { status: 409 });
|
|
925
|
+
if (!client.productOffer || input && !client.purchaseProduct) throw Object.assign(new Error("\u8BF7\u66F4\u65B0 CLI \u540E\u91CD\u8FDE\uFF0C\u4EE5\u9884\u89C8\u5546\u54C1\u5361"), { status: 501 });
|
|
926
|
+
const state = await client.state();
|
|
927
|
+
if (!state.messages.some((message2) => message2.product?.offerId === offerId)) throw Object.assign(new Error("\u5546\u54C1\u5361\u5DF2\u4E0D\u5728\u5F53\u524D\u5BF9\u8BDD\u4E2D\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00"), { status: 404 });
|
|
928
|
+
if (this.#resetting || this.#productOperationPending || client !== this.#simulator || revision !== this.#conversationRevision) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\u6216\u6B63\u5728\u8D2D\u4E70\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"), { status: 409 });
|
|
929
|
+
this.#productOperationPending = true;
|
|
930
|
+
try {
|
|
931
|
+
const result = input ? await client.purchaseProduct(offerId, input) : await client.productOffer(offerId);
|
|
932
|
+
if (this.#resetting || client !== this.#simulator || revision !== this.#conversationRevision) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u5237\u65B0\u72B6\u6001"), { status: 409 });
|
|
933
|
+
return result;
|
|
934
|
+
} finally {
|
|
935
|
+
this.#productOperationPending = false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async resetSimulator() {
|
|
939
|
+
if (this.#resetting || this.#sendingMessage || this.#productOperationPending || this.#simulatorState.activeRun) {
|
|
940
|
+
throw Object.assign(new Error("\u8BF7\u7B49\u5F85\u5F53\u524D\u56DE\u590D\u6216\u64CD\u4F5C\u5B8C\u6210\u540E\u518D\u6E05\u7A7A"), { status: 409, code: "SIMULATOR_BUSY" });
|
|
941
|
+
}
|
|
942
|
+
const simulator = this.#requiredSimulator();
|
|
943
|
+
if (!simulator.reset) {
|
|
944
|
+
throw Object.assign(new Error("\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u6E05\u7A7A\u5BF9\u8BDD"), { status: 501, code: "RESET_UNSUPPORTED", publicMessage: "\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u6E05\u7A7A\u5BF9\u8BDD" });
|
|
945
|
+
}
|
|
946
|
+
this.#resetting = true;
|
|
947
|
+
try {
|
|
948
|
+
await simulator.reset();
|
|
949
|
+
this.#conversationRevision++;
|
|
950
|
+
this.#stopOutputWatchers();
|
|
951
|
+
this.#runs.clear();
|
|
952
|
+
this.#observedActiveRuns.clear();
|
|
953
|
+
this.#events.length = 0;
|
|
954
|
+
this.#diagnostics.clear();
|
|
955
|
+
this.#runOrdinal = 0;
|
|
956
|
+
this.#lastActiveRunId = void 0;
|
|
957
|
+
this.#messages = Object.freeze([]);
|
|
958
|
+
this.#simulatorState = Object.freeze({ messages: this.#messages });
|
|
959
|
+
this.#notifySimulator();
|
|
960
|
+
return this.simulatorSnapshot();
|
|
961
|
+
} finally {
|
|
962
|
+
this.#resetting = false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async sendSimulatorMessage(input, signal) {
|
|
966
|
+
if (this.#resetting || this.#sendingMessage) throw Object.assign(new Error("\u6A21\u62DF\u5668\u6B63\u5728\u5904\u7406\u5176\u4ED6\u64CD\u4F5C"), { status: 409, code: "SIMULATOR_BUSY" });
|
|
967
|
+
this.#sendingMessage = true;
|
|
968
|
+
try {
|
|
969
|
+
const started = this.#now();
|
|
970
|
+
this.#record({ channel: "client", type: "message.send", summary: "\u53D1\u9001\u4E00\u6761\u79C1\u804A\u6D88\u606F" });
|
|
971
|
+
const accepted = await this.#requiredSimulator().send(input, { signal });
|
|
972
|
+
if (this.#runs.has(accepted.runId)) {
|
|
973
|
+
if (this.#simulatorState.activeRun?.runId === accepted.runId && !this.#simulatorState.messages.some((message2) => message2.messageId === accepted.messageId)) {
|
|
974
|
+
this.#record({
|
|
975
|
+
channel: "app_server",
|
|
976
|
+
type: "message.accepted",
|
|
977
|
+
summary: "\u8FFD\u52A0\u6D88\u606F\u5DF2\u63A5\u6536",
|
|
978
|
+
runId: accepted.runId,
|
|
979
|
+
durationMs: Math.max(0, this.#now().getTime() - started.getTime())
|
|
980
|
+
});
|
|
981
|
+
this.#simulatorState = Object.freeze({
|
|
982
|
+
...this.#simulatorState,
|
|
983
|
+
messages: Object.freeze([...this.#simulatorState.messages, Object.freeze({
|
|
984
|
+
messageId: accepted.messageId,
|
|
985
|
+
role: "user",
|
|
986
|
+
text: input.text,
|
|
987
|
+
createdAt: this.#now().toISOString()
|
|
988
|
+
})])
|
|
989
|
+
});
|
|
990
|
+
this.#messages = this.#simulatorState.messages;
|
|
991
|
+
this.#notifySimulator();
|
|
992
|
+
}
|
|
993
|
+
return accepted;
|
|
994
|
+
}
|
|
995
|
+
const acceptedAt = this.#now();
|
|
996
|
+
const latency = Math.max(0, acceptedAt.getTime() - started.getTime());
|
|
997
|
+
this.#upsertRun({
|
|
998
|
+
runId: accepted.runId,
|
|
999
|
+
messageId: accepted.messageId,
|
|
1000
|
+
status: "accepted",
|
|
1001
|
+
startedAt: started.toISOString(),
|
|
1002
|
+
updatedAt: acceptedAt.toISOString(),
|
|
1003
|
+
acceptLatencyMs: latency
|
|
1004
|
+
});
|
|
1005
|
+
this.#lastActiveRunId = accepted.runId;
|
|
1006
|
+
this.#record({
|
|
1007
|
+
channel: "app_server",
|
|
1008
|
+
type: "run.accepted",
|
|
1009
|
+
summary: "App Server \u5DF2\u63A5\u53D7\u6D88\u606F\uFF0C\u5DF2\u8BB0\u5F55\u53D1\u9001\u56DE\u6267",
|
|
1010
|
+
runId: accepted.runId,
|
|
1011
|
+
durationMs: latency
|
|
1012
|
+
});
|
|
1013
|
+
this.#simulatorState = Object.freeze({
|
|
1014
|
+
messages: Object.freeze([
|
|
1015
|
+
...this.#simulatorState.messages.filter((message2) => message2.messageId !== accepted.messageId),
|
|
1016
|
+
Object.freeze({
|
|
1017
|
+
messageId: accepted.messageId,
|
|
1018
|
+
role: "user",
|
|
1019
|
+
text: input.text,
|
|
1020
|
+
createdAt: acceptedAt.toISOString()
|
|
1021
|
+
})
|
|
1022
|
+
]),
|
|
1023
|
+
latestRun: Object.freeze({ runId: accepted.runId, status: "pending" }),
|
|
1024
|
+
activeRun: Object.freeze({ runId: accepted.runId, status: "pending" })
|
|
1025
|
+
});
|
|
1026
|
+
this.#messages = this.#simulatorState.messages;
|
|
1027
|
+
this.#notifySimulator();
|
|
1028
|
+
this.#startOutputWatcher(this.#requiredSimulator(), accepted.runId);
|
|
1029
|
+
return accepted;
|
|
1030
|
+
} finally {
|
|
1031
|
+
this.#sendingMessage = false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async cancelSimulatorRun(runId, signal) {
|
|
1035
|
+
this.#record({
|
|
1036
|
+
channel: "client",
|
|
1037
|
+
type: "run.cancel_requested",
|
|
1038
|
+
// Local debug event, not an MLB SSE event.
|
|
1039
|
+
summary: "\u5F00\u53D1\u8005\u8BF7\u6C42\u505C\u6B62\u5F53\u524D\u56DE\u590D",
|
|
1040
|
+
runId
|
|
1041
|
+
});
|
|
1042
|
+
const result = await this.#requiredSimulator().cancel(runId, { signal });
|
|
1043
|
+
if (result.status === "cancel_requested") {
|
|
1044
|
+
this.#updateRunStatus(runId, "cancel_requested");
|
|
1045
|
+
if (this.#simulatorState.activeRun?.runId === runId) {
|
|
1046
|
+
this.#simulatorState = Object.freeze({
|
|
1047
|
+
messages: this.#simulatorState.messages,
|
|
1048
|
+
latestRun: Object.freeze({ runId, status: "cancel_requested" }),
|
|
1049
|
+
activeRun: Object.freeze({ runId, status: "cancel_requested" }),
|
|
1050
|
+
...this.#simulatorState.streaming?.runId === runId ? { streaming: this.#simulatorState.streaming } : {}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
this.#record({
|
|
1055
|
+
channel: "app_server",
|
|
1056
|
+
type: `run.${result.status}`,
|
|
1057
|
+
summary: result.status === "cancel_requested" ? "App Server \u5DF2\u63A5\u53D7\u505C\u6B62\u8BF7\u6C42" : "Run \u5DF2\u7ECF\u7ED3\u675F",
|
|
1058
|
+
runId
|
|
1059
|
+
});
|
|
1060
|
+
if (result.status === "already_terminal") {
|
|
1061
|
+
await this.simulatorState(signal).catch(() => this.#notifySimulator());
|
|
1062
|
+
} else {
|
|
1063
|
+
this.#notifySimulator();
|
|
1064
|
+
}
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
1067
|
+
debugSnapshot() {
|
|
1068
|
+
return Object.freeze({
|
|
1069
|
+
source: "observed",
|
|
1070
|
+
generatedAt: this.#now().toISOString(),
|
|
1071
|
+
runs: Object.freeze([...this.#runs.values()].reverse()),
|
|
1072
|
+
events: Object.freeze([...this.#events].reverse()),
|
|
1073
|
+
messages: Object.freeze([...this.#messages]),
|
|
1074
|
+
diagnostics: this.#diagnostics.snapshot()
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
devicePairing() {
|
|
1078
|
+
const pairing = this.#state.phase === "ready" ? this.#state.ready?.devicePairing : void 0;
|
|
1079
|
+
if (pairing) {
|
|
1080
|
+
try {
|
|
1081
|
+
if (pairing.url === createBuddyPreviewUrl(this.#state.ready?.agentId ?? "") && pairing.url.length <= 2048 && (pairing.code === void 0 || /^[A-Z0-9-]{4,32}$/iu.test(pairing.code)) && (pairing.expiresAt === void 0 || Number.isFinite(Date.parse(pairing.expiresAt)))) {
|
|
1082
|
+
return Object.freeze({
|
|
1083
|
+
status: "ready",
|
|
1084
|
+
url: pairing.url,
|
|
1085
|
+
...pairing.code === void 0 ? {} : { code: pairing.code },
|
|
1086
|
+
...pairing.expiresAt === void 0 ? {} : { expiresAt: pairing.expiresAt }
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
} catch {
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return Object.freeze({
|
|
1093
|
+
status: "unavailable",
|
|
1094
|
+
reason: this.#state.phase !== "ready" ? "\u8BF7\u5148\u542F\u52A8 DEV \u4F1A\u8BDD" : pairing ? "\u5F53\u524D DEV \u73AF\u5883\u8FD4\u56DE\u4E86\u65E0\u6548\u7684\u771F\u673A\u5165\u53E3" : "\u5F53\u524D DEV \u4F1A\u8BDD\u5C1A\u672A\u63D0\u4F9B\u642D\u642D App \u9884\u89C8\u94FE\u63A5"
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
#requiredSimulator() {
|
|
1098
|
+
if (this.#state.phase !== "ready" || !this.#simulator) {
|
|
1099
|
+
throw Object.assign(new Error("\u6A21\u62DF\u5668\u9700\u8981\u5148\u542F\u52A8 DEV \u4F1A\u8BDD"), {
|
|
1100
|
+
code: "SIMULATOR_NOT_READY",
|
|
1101
|
+
status: 409,
|
|
1102
|
+
publicMessage: "\u8BF7\u5148\u542F\u52A8 DEV \u4F1A\u8BDD"
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return this.#simulator;
|
|
1106
|
+
}
|
|
1107
|
+
#observeSimulatorState(state) {
|
|
1108
|
+
const previousStreaming = this.#simulatorState.streaming;
|
|
1109
|
+
const incomingActiveRun = state.activeRun;
|
|
1110
|
+
const existingActiveRun = incomingActiveRun ? this.#runs.get(incomingActiveRun.runId) : void 0;
|
|
1111
|
+
if (existingActiveRun && TERMINAL_RUN_STATUSES.has(existingActiveRun.status)) {
|
|
1112
|
+
this.#notifySimulator();
|
|
1113
|
+
return this.#simulatorState;
|
|
1114
|
+
}
|
|
1115
|
+
let streaming = state.streaming ?? (incomingActiveRun?.runId === previousStreaming?.runId ? previousStreaming : void 0);
|
|
1116
|
+
if (streaming && previousStreaming?.runId === streaming.runId) {
|
|
1117
|
+
if (streaming.nextSequence < previousStreaming.nextSequence) {
|
|
1118
|
+
streaming = previousStreaming;
|
|
1119
|
+
} else if (streaming.nextSequence === previousStreaming.nextSequence && streaming.text !== previousStreaming.text) {
|
|
1120
|
+
throw Object.assign(new Error("App Server \u6539\u5199\u4E86\u5DF2\u89C2\u5BDF\u5230\u7684\u6D41\u5F0F\u6587\u5B57"), {
|
|
1121
|
+
code: "OUTPUT_STREAM_PROTOCOL_ERROR",
|
|
1122
|
+
publicMessage: "App Server \u8FD4\u56DE\u4E86\u4E0D\u4E00\u81F4\u7684\u6D41\u5F0F\u72B6\u6001"
|
|
1123
|
+
});
|
|
1124
|
+
} else if (streaming.nextSequence > previousStreaming.nextSequence && !streaming.text.startsWith(previousStreaming.text)) {
|
|
1125
|
+
throw Object.assign(new Error("App Server \u6539\u5199\u4E86\u5DF2\u89C2\u5BDF\u5230\u7684\u6D41\u5F0F\u6587\u5B57"), {
|
|
1126
|
+
code: "OUTPUT_STREAM_PROTOCOL_ERROR",
|
|
1127
|
+
publicMessage: "App Server \u8FD4\u56DE\u4E86\u4E0D\u4E00\u81F4\u7684\u6D41\u5F0F\u72B6\u6001"
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
this.#simulatorState = Object.freeze({
|
|
1132
|
+
messages: Object.freeze([...state.messages]),
|
|
1133
|
+
...state.latestRun === void 0 ? {} : { latestRun: Object.freeze({ ...state.latestRun }) },
|
|
1134
|
+
...state.activeRun === void 0 ? {} : { activeRun: Object.freeze({ ...state.activeRun }) },
|
|
1135
|
+
...streaming === void 0 ? {} : { streaming: Object.freeze({ ...streaming }) }
|
|
1136
|
+
});
|
|
1137
|
+
this.#messages = this.#simulatorState.messages;
|
|
1138
|
+
const now = this.#now();
|
|
1139
|
+
const active = this.#simulatorState.activeRun;
|
|
1140
|
+
if (active) {
|
|
1141
|
+
this.#observedActiveRuns.add(active.runId);
|
|
1142
|
+
const existing2 = this.#runs.get(active.runId);
|
|
1143
|
+
const restoredFirstTextLatency = existing2?.firstTextLatencyMs ?? (this.#simulatorState.streaming?.runId === active.runId && this.#simulatorState.streaming.nextSequence > 0 && this.#simulatorState.streaming.text.length > 0 ? Math.max(0, now.getTime() - Date.parse(existing2?.startedAt ?? now.toISOString())) : void 0);
|
|
1144
|
+
this.#upsertRun({
|
|
1145
|
+
runId: active.runId,
|
|
1146
|
+
...existing2?.messageId ? { messageId: existing2.messageId } : {},
|
|
1147
|
+
status: active.status,
|
|
1148
|
+
startedAt: existing2?.startedAt ?? now.toISOString(),
|
|
1149
|
+
updatedAt: now.toISOString(),
|
|
1150
|
+
...existing2?.acceptLatencyMs === void 0 ? {} : { acceptLatencyMs: existing2.acceptLatencyMs },
|
|
1151
|
+
...restoredFirstTextLatency === void 0 ? {} : { firstTextLatencyMs: restoredFirstTextLatency }
|
|
1152
|
+
});
|
|
1153
|
+
if (existing2?.firstTextLatencyMs === void 0 && restoredFirstTextLatency !== void 0) {
|
|
1154
|
+
this.#record({
|
|
1155
|
+
channel: "app_server",
|
|
1156
|
+
type: "run.output.restored",
|
|
1157
|
+
summary: "\u4ECE App Server \u72B6\u6001\u6062\u590D\u5F53\u524D\u6D41\u5F0F\u6587\u5B57",
|
|
1158
|
+
runId: active.runId,
|
|
1159
|
+
durationMs: restoredFirstTextLatency
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
if (this.#lastActiveRunId !== active.runId || existing2?.status !== active.status) {
|
|
1163
|
+
this.#record({
|
|
1164
|
+
channel: "app_server",
|
|
1165
|
+
type: `run.${active.status}`,
|
|
1166
|
+
summary: active.status === "pending" ? "Run \u6B63\u5728\u7B49\u5F85 Agent" : "Agent \u6B63\u5728\u5904\u7406 Run",
|
|
1167
|
+
runId: active.runId
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
this.#lastActiveRunId = active.runId;
|
|
1171
|
+
if (this.#simulator) this.#startOutputWatcher(this.#simulator, active.runId);
|
|
1172
|
+
this.#notifySimulator();
|
|
1173
|
+
return this.#simulatorState;
|
|
1174
|
+
}
|
|
1175
|
+
if (!this.#lastActiveRunId) {
|
|
1176
|
+
this.#notifySimulator();
|
|
1177
|
+
return this.#simulatorState;
|
|
1178
|
+
}
|
|
1179
|
+
const runId = this.#lastActiveRunId;
|
|
1180
|
+
const existing = this.#runs.get(runId);
|
|
1181
|
+
if (existing && !["completed", "failed", "cancelled"].includes(existing.status)) {
|
|
1182
|
+
const assistant = [...this.#simulatorState.messages].reverse().find((message2) => message2.role === "assistant" && Date.parse(message2.createdAt) >= Date.parse(existing.startedAt));
|
|
1183
|
+
const elapsed = Math.max(0, now.getTime() - Date.parse(existing.startedAt));
|
|
1184
|
+
const terminalStatus = this.#simulatorState.latestRun?.runId === runId && (this.#simulatorState.latestRun.status === "completed" || this.#simulatorState.latestRun.status === "failed" || this.#simulatorState.latestRun.status === "cancelled") ? this.#simulatorState.latestRun.status : void 0;
|
|
1185
|
+
if (!assistant && !terminalStatus && !this.#observedActiveRuns.has(runId) && elapsed < 5e3) {
|
|
1186
|
+
this.#notifySimulator();
|
|
1187
|
+
return this.#simulatorState;
|
|
1188
|
+
}
|
|
1189
|
+
const completedAt = assistant && Number.isFinite(Date.parse(assistant.createdAt)) ? new Date(assistant.createdAt) : now;
|
|
1190
|
+
const status = terminalStatus ?? "completed";
|
|
1191
|
+
this.#upsertRun({
|
|
1192
|
+
...existing,
|
|
1193
|
+
status,
|
|
1194
|
+
updatedAt: now.toISOString(),
|
|
1195
|
+
completedAt: completedAt.toISOString(),
|
|
1196
|
+
responseTimeMs: Math.max(0, completedAt.getTime() - Date.parse(existing.startedAt)),
|
|
1197
|
+
completion: assistant ? "reply" : "no_reply"
|
|
1198
|
+
});
|
|
1199
|
+
this.#record({
|
|
1200
|
+
channel: "app_server",
|
|
1201
|
+
type: `run.${status}`,
|
|
1202
|
+
summary: assistant ? "\u5B8C\u6574\u56DE\u590D\u5DF2\u8FD4\u56DE\uFF0C\u672C\u5730\u56DE\u5408\u7ED3\u675F" : status === "failed" ? "Agent \u672C\u5730\u56DE\u5408\u6267\u884C\u5931\u8D25\uFF0C\u672A\u4EA7\u751F\u7528\u6237\u56DE\u590D" : status === "cancelled" ? "Agent \u672C\u5730\u56DE\u5408\u5DF2\u53D6\u6D88\uFF0C\u672A\u5B8C\u6210\u56DE\u590D\u5DF2\u4F5C\u5E9F" : "\u672C\u5730\u56DE\u5408\u5DF2\u7ED3\u675F\uFF0C\u672A\u89C2\u5BDF\u5230\u65B0\u7684\u5B8C\u6574\u56DE\u590D",
|
|
1203
|
+
runId,
|
|
1204
|
+
durationMs: Math.max(0, completedAt.getTime() - Date.parse(existing.startedAt))
|
|
1205
|
+
});
|
|
1206
|
+
this.#stopOutputWatcher(runId);
|
|
1207
|
+
}
|
|
1208
|
+
this.#observedActiveRuns.delete(runId);
|
|
1209
|
+
this.#lastActiveRunId = void 0;
|
|
1210
|
+
this.#notifySimulator();
|
|
1211
|
+
return this.#simulatorState;
|
|
1212
|
+
}
|
|
1213
|
+
#startOutputWatcher(simulator, runId) {
|
|
1214
|
+
if (!simulator.watchOutput || this.#outputWatchers.has(runId) || this.#failedOutputRuns.has(runId)) return;
|
|
1215
|
+
const controller = new AbortController();
|
|
1216
|
+
this.#outputWatchers.set(runId, controller);
|
|
1217
|
+
void simulator.watchOutput(runId, {
|
|
1218
|
+
signal: controller.signal,
|
|
1219
|
+
onEvent: (event) => {
|
|
1220
|
+
if (controller.signal.aborted || this.#resetting || simulator !== this.#simulator) return;
|
|
1221
|
+
return this.#observeOutputEvent(event);
|
|
1222
|
+
}
|
|
1223
|
+
}).catch((cause) => {
|
|
1224
|
+
if (controller.signal.aborted) return;
|
|
1225
|
+
this.#failedOutputRuns.add(runId);
|
|
1226
|
+
this.#record({
|
|
1227
|
+
channel: "app_server",
|
|
1228
|
+
type: "run.output.disconnected",
|
|
1229
|
+
summary: cause && typeof cause === "object" && "status" in cause && cause.status === 401 ? "MLB \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u542F DEV" : "\u6D41\u5F0F\u8FDE\u63A5\u4E2D\u65AD\uFF0C\u6A21\u62DF\u5668\u5C06\u4F7F\u7528 App Server \u72B6\u6001\u6062\u590D",
|
|
1230
|
+
runId
|
|
1231
|
+
});
|
|
1232
|
+
this.#notifySimulator();
|
|
1233
|
+
void this.simulatorState().catch(() => void 0);
|
|
1234
|
+
}).finally(() => {
|
|
1235
|
+
if (this.#outputWatchers.get(runId) === controller) this.#outputWatchers.delete(runId);
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
async #observeOutputEvent(event) {
|
|
1239
|
+
if (event.type === "state") {
|
|
1240
|
+
if (event.resetStreaming) {
|
|
1241
|
+
const { streaming: _streaming, ...state } = this.#simulatorState;
|
|
1242
|
+
this.#simulatorState = Object.freeze(state);
|
|
1243
|
+
}
|
|
1244
|
+
this.#observeSimulatorState(event.state);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (event.type === "reset") {
|
|
1248
|
+
if (this.#simulatorState.streaming?.runId === event.runId) {
|
|
1249
|
+
const { streaming: _streaming, ...state } = this.#simulatorState;
|
|
1250
|
+
this.#simulatorState = Object.freeze(state);
|
|
1251
|
+
this.#notifySimulator();
|
|
1252
|
+
}
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const now = this.#now();
|
|
1256
|
+
const existing = this.#runs.get(event.runId);
|
|
1257
|
+
if (event.type === "delta") {
|
|
1258
|
+
if (existing && TERMINAL_RUN_STATUSES.has(existing.status) || this.#simulatorState.latestRun?.runId === event.runId && TERMINAL_RUN_STATUSES.has(this.#simulatorState.latestRun.status)) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const previous = this.#simulatorState.streaming;
|
|
1262
|
+
const expected = previous?.runId === event.runId ? previous.nextSequence : 0;
|
|
1263
|
+
if (event.sequence < expected) return;
|
|
1264
|
+
if (event.sequence > expected) {
|
|
1265
|
+
throw Object.assign(new Error("App Server \u8FD4\u56DE\u7684\u6D41\u5F0F\u5E8F\u53F7\u4E0D\u8FDE\u7EED"), { code: "OUTPUT_SEQUENCE_MISMATCH" });
|
|
1266
|
+
}
|
|
1267
|
+
const text2 = `${previous?.runId === event.runId ? previous.text : ""}${event.text}`;
|
|
1268
|
+
const firstTextLatencyMs = existing?.firstTextLatencyMs ?? (existing ? Math.max(0, now.getTime() - Date.parse(existing.startedAt)) : void 0);
|
|
1269
|
+
if (existing) {
|
|
1270
|
+
this.#upsertRun({
|
|
1271
|
+
...existing,
|
|
1272
|
+
status: "running",
|
|
1273
|
+
updatedAt: now.toISOString(),
|
|
1274
|
+
...firstTextLatencyMs === void 0 ? {} : { firstTextLatencyMs }
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
this.#simulatorState = Object.freeze({
|
|
1278
|
+
messages: this.#simulatorState.messages,
|
|
1279
|
+
latestRun: Object.freeze({ runId: event.runId, status: "running" }),
|
|
1280
|
+
activeRun: Object.freeze({ runId: event.runId, status: "running" }),
|
|
1281
|
+
streaming: Object.freeze({
|
|
1282
|
+
runId: event.runId,
|
|
1283
|
+
text: text2,
|
|
1284
|
+
nextSequence: event.sequence + 1,
|
|
1285
|
+
updatedAt: now.toISOString()
|
|
1286
|
+
})
|
|
1287
|
+
});
|
|
1288
|
+
this.#record({
|
|
1289
|
+
channel: "app_server",
|
|
1290
|
+
type: event.sequence === 0 ? "run.output.started" : "run.output.delta",
|
|
1291
|
+
summary: event.sequence === 0 ? "\u9996\u6BB5\u6D41\u5F0F\u6587\u5B57\u5DF2\u5230\u8FBE" : `\u6536\u5230\u7B2C ${event.sequence + 1} \u6BB5\u6D41\u5F0F\u6587\u5B57`,
|
|
1292
|
+
runId: event.runId,
|
|
1293
|
+
...firstTextLatencyMs === void 0 || event.sequence !== 0 ? {} : { durationMs: firstTextLatencyMs }
|
|
1294
|
+
});
|
|
1295
|
+
this.#notifySimulator();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.#record({
|
|
1299
|
+
channel: "app_server",
|
|
1300
|
+
type: `run.output.${event.type}`,
|
|
1301
|
+
summary: event.type === "completed" ? "\u6D41\u5F0F\u8F93\u51FA\u5B8C\u6210\uFF0C\u6B63\u5728\u786E\u8BA4\u6B63\u5F0F\u56DE\u590D" : event.type === "failed" ? "\u672C\u5730\u56DE\u5408\u5931\u8D25\uFF0C\u4E34\u65F6\u6D41\u5F0F\u6587\u5B57\u5DF2\u4F5C\u5E9F" : "\u672C\u5730\u56DE\u5408\u5DF2\u53D6\u6D88\uFF0C\u4E34\u65F6\u6D41\u5F0F\u6587\u5B57\u5DF2\u4F5C\u5E9F",
|
|
1302
|
+
runId: event.runId
|
|
1303
|
+
});
|
|
1304
|
+
if (event.type !== "completed") {
|
|
1305
|
+
this.#simulatorState = Object.freeze({
|
|
1306
|
+
messages: this.#simulatorState.messages,
|
|
1307
|
+
latestRun: Object.freeze({ runId: event.runId, status: event.status })
|
|
1308
|
+
});
|
|
1309
|
+
this.#notifySimulator();
|
|
1310
|
+
}
|
|
1311
|
+
await this.simulatorState().catch(() => void 0);
|
|
1312
|
+
}
|
|
1313
|
+
#stopOutputWatchers() {
|
|
1314
|
+
for (const controller of this.#outputWatchers.values()) {
|
|
1315
|
+
controller.abort(new Error("Preview DEV session closed"));
|
|
1316
|
+
}
|
|
1317
|
+
this.#outputWatchers.clear();
|
|
1318
|
+
this.#failedOutputRuns.clear();
|
|
1319
|
+
}
|
|
1320
|
+
#stopOutputWatcher(runId) {
|
|
1321
|
+
this.#failedOutputRuns.delete(runId);
|
|
1322
|
+
const controller = this.#outputWatchers.get(runId);
|
|
1323
|
+
if (!controller) return;
|
|
1324
|
+
this.#outputWatchers.delete(runId);
|
|
1325
|
+
controller.abort(new Error("Agent local turn reached a terminal state"));
|
|
1326
|
+
}
|
|
1327
|
+
#notifySimulator() {
|
|
1328
|
+
this.#simulatorRevision++;
|
|
1329
|
+
if (this.#simulatorListeners.size === 0) return;
|
|
1330
|
+
const value = Object.freeze({ state: this.simulatorSnapshot(), debug: this.debugSnapshot() });
|
|
1331
|
+
for (const listener of this.#simulatorListeners) {
|
|
1332
|
+
try {
|
|
1333
|
+
listener(value);
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
#upsertRun(run) {
|
|
1339
|
+
const ordinal = this.#runs.get(run.runId)?.ordinal ?? ++this.#runOrdinal;
|
|
1340
|
+
this.#runs.set(run.runId, Object.freeze({ ...run, ordinal }));
|
|
1341
|
+
while (this.#runs.size > 100) {
|
|
1342
|
+
const oldest = this.#runs.keys().next().value;
|
|
1343
|
+
if (!oldest) break;
|
|
1344
|
+
this.#runs.delete(oldest);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
#updateRunStatus(runId, status) {
|
|
1348
|
+
const existing = this.#runs.get(runId);
|
|
1349
|
+
if (!existing) return;
|
|
1350
|
+
this.#upsertRun({ ...existing, status, updatedAt: this.#now().toISOString() });
|
|
1351
|
+
}
|
|
1352
|
+
#record(input) {
|
|
1353
|
+
this.#eventSequence += 1;
|
|
1354
|
+
this.#events.push(Object.freeze({
|
|
1355
|
+
...input,
|
|
1356
|
+
eventId: `event-${this.#eventSequence}`,
|
|
1357
|
+
timestamp: this.#now().toISOString()
|
|
1358
|
+
}));
|
|
1359
|
+
if (this.#events.length > 300) this.#events.splice(0, this.#events.length - 300);
|
|
1360
|
+
}
|
|
1361
|
+
#setState(next) {
|
|
1362
|
+
this.#state = Object.freeze({ ...this.#state.inspection ? { inspection: this.#state.inspection } : {}, ...next, revision: this.#state.revision + 1 });
|
|
1363
|
+
for (const listener of this.#listeners) {
|
|
1364
|
+
try {
|
|
1365
|
+
listener(this.#state);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return this.#state;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
// apps/preview/src/server.ts
|
|
1374
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
1375
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1376
|
+
import { createServer } from "node:http";
|
|
1377
|
+
import QRCode from "qrcode";
|
|
1378
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
1379
|
+
var SESSION_COOKIE_PREFIX = "buddy_preview_session";
|
|
1380
|
+
function closeServer(server) {
|
|
1381
|
+
if (!server.listening) return Promise.resolve();
|
|
1382
|
+
return new Promise((resolve2, reject) => {
|
|
1383
|
+
server.close((error) => error ? reject(error) : resolve2());
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function writeHeaders(response) {
|
|
1387
|
+
response.setHeader("Cache-Control", "no-store");
|
|
1388
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1389
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
1390
|
+
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
1391
|
+
response.setHeader(
|
|
1392
|
+
"Content-Security-Policy",
|
|
1393
|
+
"default-src 'self'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
function json(response, status, body) {
|
|
1397
|
+
const encoded = JSON.stringify(body);
|
|
1398
|
+
response.statusCode = status;
|
|
1399
|
+
writeHeaders(response);
|
|
1400
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1401
|
+
response.setHeader("Content-Length", Buffer.byteLength(encoded));
|
|
1402
|
+
response.end(encoded);
|
|
1403
|
+
}
|
|
1404
|
+
function safeEqual(left, right) {
|
|
1405
|
+
const a = Buffer.from(left);
|
|
1406
|
+
const b = Buffer.from(right);
|
|
1407
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1408
|
+
}
|
|
1409
|
+
function cookieValue(request, name) {
|
|
1410
|
+
for (const part2 of request.headers.cookie?.split(";") ?? []) {
|
|
1411
|
+
const separator = part2.indexOf("=");
|
|
1412
|
+
if (separator < 0) continue;
|
|
1413
|
+
if (part2.slice(0, separator).trim() === name) return part2.slice(separator + 1).trim();
|
|
1414
|
+
}
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
function requestUrl(request, origin) {
|
|
1418
|
+
try {
|
|
1419
|
+
return new URL(request.url ?? "/", origin);
|
|
1420
|
+
} catch {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function allowedHost(request, port) {
|
|
1425
|
+
const value = request.headers.host;
|
|
1426
|
+
if (!value) return false;
|
|
1427
|
+
try {
|
|
1428
|
+
const parsed = new URL(`http://${value}`);
|
|
1429
|
+
return LOOPBACK_HOSTS.has(parsed.hostname.replace(/^\[|\]$/gu, "")) && Number(parsed.port || 80) === port;
|
|
1430
|
+
} catch {
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function allowedOrigin(request, origin) {
|
|
1435
|
+
const value = request.headers.origin;
|
|
1436
|
+
return value === void 0 || value === origin.origin;
|
|
1437
|
+
}
|
|
1438
|
+
function validWidgetRender(value) {
|
|
1439
|
+
return value !== null && /^[a-zA-Z0-9-]{16,64}$/u.test(value);
|
|
1440
|
+
}
|
|
1441
|
+
function widgetDocument(response, html, render, state = "ready", status = 200) {
|
|
1442
|
+
writeHeaders(response);
|
|
1443
|
+
response.statusCode = status;
|
|
1444
|
+
response.setHeader("Content-Security-Policy", "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
|
|
1445
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1446
|
+
if (validWidgetRender(render)) {
|
|
1447
|
+
const script = state === "ready" ? readFileSync2(new URL("./resources/widget-document.js", import.meta.url), "utf8").replace("__WIDGET_RENDER__", JSON.stringify(render)) : `window.addEventListener('load',()=>parent.postMessage({channel:'buddy-widget-document',render:${JSON.stringify(render)},state:'error'},'*'),{once:true})`;
|
|
1448
|
+
const signal = `<script>${script}</script>`;
|
|
1449
|
+
if (/<!doctype[^>]*>/iu.test(html)) html = html.replace(/<!doctype[^>]*>/iu, (match) => match + signal);
|
|
1450
|
+
else html = signal + html;
|
|
1451
|
+
}
|
|
1452
|
+
response.end(html);
|
|
1453
|
+
}
|
|
1454
|
+
function apiError(response, status, code, message2) {
|
|
1455
|
+
json(response, status, { error: { code, message: message2 } });
|
|
1456
|
+
}
|
|
1457
|
+
async function readJsonBody(request, maximumBytes = 64 * 1024) {
|
|
1458
|
+
const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1459
|
+
if (contentType !== "application/json") {
|
|
1460
|
+
throw Object.assign(new Error("\u9700\u8981 application/json"), { code: "JSON_REQUIRED", status: 415 });
|
|
1461
|
+
}
|
|
1462
|
+
const declared = Number(request.headers["content-length"] ?? 0);
|
|
1463
|
+
if (!Number.isFinite(declared) || declared < 0 || declared > maximumBytes) {
|
|
1464
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u8FC7\u5927"), { code: "BODY_TOO_LARGE", status: 413 });
|
|
1465
|
+
}
|
|
1466
|
+
const chunks = [];
|
|
1467
|
+
let size = 0;
|
|
1468
|
+
for await (const chunk of request) {
|
|
1469
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1470
|
+
size += buffer.length;
|
|
1471
|
+
if (size > maximumBytes) {
|
|
1472
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u8FC7\u5927"), { code: "BODY_TOO_LARGE", status: 413 });
|
|
1473
|
+
}
|
|
1474
|
+
chunks.push(buffer);
|
|
1475
|
+
}
|
|
1476
|
+
try {
|
|
1477
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1478
|
+
} catch {
|
|
1479
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u4E0D\u662F\u6709\u6548 JSON"), { code: "INVALID_JSON", status: 400 });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function exactObject(value, keys) {
|
|
1483
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1484
|
+
const object3 = value;
|
|
1485
|
+
const expected = new Set(keys);
|
|
1486
|
+
return Object.keys(object3).length === keys.length && Object.keys(object3).every((key) => expected.has(key)) ? object3 : null;
|
|
1487
|
+
}
|
|
1488
|
+
function safeIdentifier(value, maximumLength = 200) {
|
|
1489
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximumLength && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
1490
|
+
}
|
|
1491
|
+
function requestFailure(cause) {
|
|
1492
|
+
if (typeof cause === "object" && cause !== null) {
|
|
1493
|
+
const status = "status" in cause && typeof cause.status === "number" && Number.isInteger(cause.status) && cause.status >= 400 && cause.status <= 599 ? cause.status : 502;
|
|
1494
|
+
const code = "code" in cause && safeIdentifier(cause.code, 80) ? cause.code : "SIMULATOR_REQUEST_FAILED";
|
|
1495
|
+
const message2 = "publicMessage" in cause && typeof cause.publicMessage === "string" && cause.publicMessage.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(cause.publicMessage) ? cause.publicMessage : status < 500 && cause instanceof Error ? cause.message : "\u6A21\u62DF\u5668\u8BF7\u6C42\u672A\u80FD\u5B8C\u6210";
|
|
1496
|
+
return { status, code, message: message2 };
|
|
1497
|
+
}
|
|
1498
|
+
return { status: 502, code: "SIMULATOR_REQUEST_FAILED", message: "\u6A21\u62DF\u5668\u8BF7\u6C42\u672A\u80FD\u5B8C\u6210" };
|
|
1499
|
+
}
|
|
1500
|
+
function publicProject(config) {
|
|
1501
|
+
return {
|
|
1502
|
+
buddyId: config.buddyId
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
async function startPreviewControlServer(options) {
|
|
1506
|
+
const host = options.host ?? "127.0.0.1";
|
|
1507
|
+
const port = options.port ?? 0;
|
|
1508
|
+
if (!LOOPBACK_HOSTS.has(host)) throw new TypeError("Preview control server must bind to loopback");
|
|
1509
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535) throw new TypeError("Preview port must be 0\u201365535");
|
|
1510
|
+
const sessionToken = randomBytes(32).toString("base64url");
|
|
1511
|
+
let bootstrapAvailable = true;
|
|
1512
|
+
let origin;
|
|
1513
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1514
|
+
let eventRevision = 0;
|
|
1515
|
+
const publish = (event, value) => {
|
|
1516
|
+
eventRevision += 1;
|
|
1517
|
+
const payload = `id: ${eventRevision}
|
|
1518
|
+
event: ${event}
|
|
1519
|
+
data: ${JSON.stringify(value)}
|
|
1520
|
+
|
|
1521
|
+
`;
|
|
1522
|
+
for (const response of listeners) {
|
|
1523
|
+
try {
|
|
1524
|
+
response.write(payload);
|
|
1525
|
+
} catch {
|
|
1526
|
+
listeners.delete(response);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
const unsubscribeDev = options.controller.subscribe((state) => publish("dev.state", state));
|
|
1531
|
+
const unsubscribeSimulator = options.controller.subscribeSimulator((value) => publish("simulator.state", value));
|
|
1532
|
+
const server = createServer(async (request, response) => {
|
|
1533
|
+
if (!origin) return apiError(response, 503, "PREVIEW_STARTING", "Preview \u6B63\u5728\u542F\u52A8");
|
|
1534
|
+
if (!allowedHost(request, Number(origin.port)) || !allowedOrigin(request, origin)) {
|
|
1535
|
+
return apiError(response, 403, "PREVIEW_ORIGIN_FORBIDDEN", "\u8BF7\u6C42\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
1536
|
+
}
|
|
1537
|
+
const sessionCookie = `${SESSION_COOKIE_PREFIX}_${origin.port}`;
|
|
1538
|
+
const url = requestUrl(request, origin);
|
|
1539
|
+
if (!url) return apiError(response, 400, "INVALID_URL", "\u8BF7\u6C42\u5730\u5740\u65E0\u6548");
|
|
1540
|
+
if (request.method === "GET" && bootstrapAvailable && url.pathname === `/bootstrap/${sessionToken}`) {
|
|
1541
|
+
bootstrapAvailable = false;
|
|
1542
|
+
response.statusCode = 303;
|
|
1543
|
+
writeHeaders(response);
|
|
1544
|
+
response.setHeader(
|
|
1545
|
+
"Set-Cookie",
|
|
1546
|
+
`${sessionCookie}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`
|
|
1547
|
+
);
|
|
1548
|
+
response.setHeader("Location", "/");
|
|
1549
|
+
response.end();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const suppliedToken = cookieValue(request, sessionCookie);
|
|
1553
|
+
if (!suppliedToken || !safeEqual(suppliedToken, sessionToken)) {
|
|
1554
|
+
return apiError(response, 401, "PREVIEW_UNAUTHORIZED", "\u672C\u5730\u8C03\u8BD5\u63A5\u53E3\u9700\u8981 CLI \u5185\u90E8\u4F1A\u8BDD");
|
|
1555
|
+
}
|
|
1556
|
+
if (request.method === "GET" && url.pathname === "/api/project") {
|
|
1557
|
+
try {
|
|
1558
|
+
await options.controller.refreshInspection();
|
|
1559
|
+
return json(response, 200, { project: { ...publicProject(await options.controller.project()), inspection: options.controller.state().inspection } });
|
|
1560
|
+
} catch {
|
|
1561
|
+
return apiError(response, 422, "PROJECT_CONFIG_INVALID", "\u9879\u76EE\u914D\u7F6E\u65E0\u6CD5\u8BFB\u53D6\u6216\u6821\u9A8C\u5931\u8D25");
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (request.method === "GET" && url.pathname === "/api/dev") {
|
|
1565
|
+
await options.controller.refreshInspection();
|
|
1566
|
+
return json(response, 200, { state: options.controller.state() });
|
|
1567
|
+
}
|
|
1568
|
+
if (request.method === "GET" && url.pathname === "/api/buddy") {
|
|
1569
|
+
try {
|
|
1570
|
+
return json(response, 200, { buddy: await options.controller.buddyDetails() });
|
|
1571
|
+
} catch (cause) {
|
|
1572
|
+
const failure = requestFailure(cause);
|
|
1573
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (request.method === "GET" && url.pathname === "/api/debug") {
|
|
1577
|
+
return json(response, 200, { debug: options.controller.debugSnapshot() });
|
|
1578
|
+
}
|
|
1579
|
+
if (request.method === "GET" && url.pathname === "/api/device/pairing") {
|
|
1580
|
+
const pairing = options.controller.devicePairing();
|
|
1581
|
+
if (pairing.status !== "ready") return json(response, 200, { pairing });
|
|
1582
|
+
const qrCodeDataUrl = await QRCode.toDataURL(pairing.url, {
|
|
1583
|
+
errorCorrectionLevel: "M",
|
|
1584
|
+
margin: 4,
|
|
1585
|
+
width: 256
|
|
1586
|
+
});
|
|
1587
|
+
const current = options.controller.devicePairing();
|
|
1588
|
+
return json(response, 200, { pairing: current.status === "ready" && current.url === pairing.url ? { ...current, qrCodeDataUrl } : current });
|
|
1589
|
+
}
|
|
1590
|
+
if (request.method === "GET" && url.pathname === "/api/simulator/state") {
|
|
1591
|
+
try {
|
|
1592
|
+
return json(response, 200, { state: await options.controller.simulatorState() });
|
|
1593
|
+
} catch (cause) {
|
|
1594
|
+
const failure = requestFailure(cause);
|
|
1595
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const productRoute = /^\/api\/simulator\/products\/([A-Za-z0-9_-]{1,120})(\/mock-purchase)?$/u.exec(url.pathname);
|
|
1599
|
+
if (productRoute) {
|
|
1600
|
+
try {
|
|
1601
|
+
if (request.method === "GET" && !productRoute[2]) return json(response, 200, await options.controller.simulatorProduct(productRoute[1]));
|
|
1602
|
+
if (request.method === "POST" && productRoute[2]) {
|
|
1603
|
+
const body = exactObject(await readJsonBody(request), ["expectedVersion", "idempotencyKey", "conversationRevision"]);
|
|
1604
|
+
if (!body || !Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1 || !Number.isSafeInteger(body.conversationRevision) || !safeIdentifier(body.idempotencyKey, 128))
|
|
1605
|
+
return apiError(response, 400, "INVALID_PURCHASE", "\u8BF7\u91CD\u65B0\u6253\u5F00\u5546\u54C1\u5361\u5E76\u786E\u8BA4\u7248\u672C");
|
|
1606
|
+
return json(response, 200, await options.controller.simulatorProduct(productRoute[1], body));
|
|
1607
|
+
}
|
|
1608
|
+
return apiError(response, 405, "METHOD_NOT_ALLOWED", "\u4E0D\u652F\u6301\u8FD9\u4E2A\u5546\u54C1\u64CD\u4F5C");
|
|
1609
|
+
} catch (cause) {
|
|
1610
|
+
const failure = requestFailure(cause);
|
|
1611
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const widgetRoute = /^\/api\/simulator\/widgets(?:\/([^/]+)(?:\/(image|pin|bridge))?)?$/u.exec(url.pathname);
|
|
1615
|
+
if (widgetRoute) {
|
|
1616
|
+
try {
|
|
1617
|
+
const id = widgetRoute[1] ? decodeURIComponent(widgetRoute[1]) : "";
|
|
1618
|
+
const suffix = widgetRoute[2];
|
|
1619
|
+
if (id && suffix === "bridge" && request.method === "POST") {
|
|
1620
|
+
const body = await readJsonBody(request, 7 * 1024 * 1024);
|
|
1621
|
+
return json(response, 200, await options.controller.simulatorWidgetCommand(id, body));
|
|
1622
|
+
}
|
|
1623
|
+
if (request.method === "GET" && !id) return json(response, 200, await options.controller.simulatorWidgets("list"));
|
|
1624
|
+
if (id && suffix === "pin" && (request.method === "PUT" || request.method === "DELETE")) return json(response, 200, await options.controller.simulatorWidgets(request.method === "PUT" ? "pin" : "unpin", id));
|
|
1625
|
+
if (id && request.method === "GET" && suffix === "image") {
|
|
1626
|
+
const image = await options.controller.simulatorWidgets("image", id, url.searchParams.get("mediaId") ?? "");
|
|
1627
|
+
if (!/^image\/(png|jpeg|webp|gif)(;|$)/u.test(image.contentType)) throw new Error("\u56FE\u7247\u683C\u5F0F\u4E0D\u652F\u6301");
|
|
1628
|
+
writeHeaders(response);
|
|
1629
|
+
response.setHeader("Content-Type", image.contentType);
|
|
1630
|
+
response.end(image.body);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (id && request.method === "GET" && !suffix) {
|
|
1634
|
+
const html = await options.controller.simulatorWidgets("html", id, "", url.searchParams.get("messageId") ?? void 0);
|
|
1635
|
+
widgetDocument(response, html, url.searchParams.get("render"));
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
return apiError(response, 405, "METHOD_NOT_ALLOWED", "\u4E0D\u652F\u6301\u8FD9\u4E2A\u5C0F\u6302\u4EF6\u64CD\u4F5C");
|
|
1639
|
+
} catch (cause) {
|
|
1640
|
+
const failure = requestFailure(cause);
|
|
1641
|
+
const render = url.searchParams.get("render");
|
|
1642
|
+
if (request.method === "GET" && widgetRoute[1] && !widgetRoute[2] && validWidgetRender(render)) {
|
|
1643
|
+
widgetDocument(response, '<!doctype html><html><head><meta charset="utf-8"></head><body>\u5C0F\u6302\u4EF6\u6682\u65F6\u65E0\u6CD5\u6253\u5F00</body></html>', render, "error", failure.status);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (request.method === "GET" && url.pathname === "/api/events") {
|
|
1650
|
+
response.statusCode = 200;
|
|
1651
|
+
writeHeaders(response);
|
|
1652
|
+
response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
1653
|
+
response.setHeader("Connection", "keep-alive");
|
|
1654
|
+
eventRevision += 1;
|
|
1655
|
+
response.write(`id: ${eventRevision}
|
|
1656
|
+
event: dev.state
|
|
1657
|
+
data: ${JSON.stringify(options.controller.state())}
|
|
1658
|
+
|
|
1659
|
+
`);
|
|
1660
|
+
eventRevision += 1;
|
|
1661
|
+
response.write(`id: ${eventRevision}
|
|
1662
|
+
event: simulator.state
|
|
1663
|
+
data: ${JSON.stringify({
|
|
1664
|
+
state: options.controller.simulatorSnapshot(),
|
|
1665
|
+
debug: options.controller.debugSnapshot()
|
|
1666
|
+
})}
|
|
1667
|
+
|
|
1668
|
+
`);
|
|
1669
|
+
listeners.add(response);
|
|
1670
|
+
request.once("close", () => listeners.delete(response));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (request.method === "POST" && options.controller.resetting) {
|
|
1674
|
+
return apiError(response, 409, "SIMULATOR_BUSY", "\u6B63\u5728\u6E05\u7A7A\u5BF9\u8BDD\u5E76\u91CD\u542F DEV\uFF0C\u8BF7\u7A0D\u5019");
|
|
1675
|
+
}
|
|
1676
|
+
if (request.method === "POST" && url.pathname === "/api/simulator/reset") {
|
|
1677
|
+
try {
|
|
1678
|
+
const body = exactObject(await readJsonBody(request), ["confirm"]);
|
|
1679
|
+
if (body?.confirm !== true) return apiError(response, 400, "RESET_CONFIRMATION_REQUIRED", "\u8BF7\u786E\u8BA4\u6E05\u7A7A\u5F53\u524D\u5F00\u53D1\u7248\u5BF9\u8BDD");
|
|
1680
|
+
const state = await options.controller.resetSimulator();
|
|
1681
|
+
return json(response, 200, { state, dev: options.controller.state(), debug: options.controller.debugSnapshot() });
|
|
1682
|
+
} catch (cause) {
|
|
1683
|
+
const failure = requestFailure(cause);
|
|
1684
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (request.method === "POST" && url.pathname === "/api/dev/start") {
|
|
1688
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
1689
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
1690
|
+
}
|
|
1691
|
+
void options.controller.start();
|
|
1692
|
+
return json(response, 202, { state: options.controller.state() });
|
|
1693
|
+
}
|
|
1694
|
+
if (request.method === "POST" && url.pathname === "/api/dev/stop") {
|
|
1695
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
1696
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
1697
|
+
}
|
|
1698
|
+
try {
|
|
1699
|
+
return json(response, 200, { state: await options.controller.stop() });
|
|
1700
|
+
} catch {
|
|
1701
|
+
return apiError(response, 500, "DEV_STOP_FAILED", "DEV \u4F1A\u8BDD\u672A\u80FD\u5B89\u5168\u505C\u6B62\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u8FDB\u7A0B");
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (request.method === "POST" && url.pathname === "/api/simulator/messages") {
|
|
1705
|
+
try {
|
|
1706
|
+
const body = exactObject(await readJsonBody(request), ["text", "idempotencyKey"]);
|
|
1707
|
+
if (!body || typeof body.text !== "string" || !body.text.trim() || Buffer.byteLength(body.text, "utf8") > 32 * 1024 || !safeIdentifier(body.idempotencyKey, 128)) {
|
|
1708
|
+
return apiError(response, 400, "INVALID_MESSAGE", "\u6D88\u606F\u6216\u5E42\u7B49\u6807\u8BC6\u4E0D\u7B26\u5408\u8981\u6C42");
|
|
1709
|
+
}
|
|
1710
|
+
const accepted = await options.controller.sendSimulatorMessage({
|
|
1711
|
+
text: body.text,
|
|
1712
|
+
idempotencyKey: body.idempotencyKey
|
|
1713
|
+
});
|
|
1714
|
+
return json(response, 202, accepted);
|
|
1715
|
+
} catch (cause) {
|
|
1716
|
+
const failure = requestFailure(cause);
|
|
1717
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const cancelMatch = /^\/api\/simulator\/runs\/([^/]+)\/cancel$/u.exec(url.pathname);
|
|
1721
|
+
if (request.method === "POST" && cancelMatch) {
|
|
1722
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
1723
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
1724
|
+
}
|
|
1725
|
+
let runId;
|
|
1726
|
+
try {
|
|
1727
|
+
runId = decodeURIComponent(cancelMatch[1]);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return apiError(response, 400, "INVALID_RUN_ID", "Run ID \u4E0D\u7B26\u5408\u8981\u6C42");
|
|
1730
|
+
}
|
|
1731
|
+
if (!safeIdentifier(runId)) return apiError(response, 400, "INVALID_RUN_ID", "Run ID \u4E0D\u7B26\u5408\u8981\u6C42");
|
|
1732
|
+
try {
|
|
1733
|
+
return json(response, 200, await options.controller.cancelSimulatorRun(runId));
|
|
1734
|
+
} catch (cause) {
|
|
1735
|
+
const failure = requestFailure(cause);
|
|
1736
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return apiError(response, 404, "NOT_FOUND", "\u63A5\u53E3\u4E0D\u5B58\u5728");
|
|
1740
|
+
});
|
|
1741
|
+
try {
|
|
1742
|
+
await new Promise((resolve2, reject) => {
|
|
1743
|
+
server.once("error", reject);
|
|
1744
|
+
server.listen(port, host, () => {
|
|
1745
|
+
server.off("error", reject);
|
|
1746
|
+
resolve2();
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
const address = server.address();
|
|
1750
|
+
if (!address) throw new Error("Preview server has no address");
|
|
1751
|
+
const urlHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
1752
|
+
origin = new URL(`http://${urlHost}:${address.port}`);
|
|
1753
|
+
} catch (cause) {
|
|
1754
|
+
unsubscribeDev();
|
|
1755
|
+
unsubscribeSimulator();
|
|
1756
|
+
await closeServer(server).catch(() => void 0);
|
|
1757
|
+
throw cause;
|
|
1758
|
+
}
|
|
1759
|
+
const bootstrapUrl = new URL(`/bootstrap/${sessionToken}`, origin);
|
|
1760
|
+
return Object.freeze({
|
|
1761
|
+
origin,
|
|
1762
|
+
bootstrapUrl,
|
|
1763
|
+
async close() {
|
|
1764
|
+
unsubscribeDev();
|
|
1765
|
+
unsubscribeSimulator();
|
|
1766
|
+
for (const response of listeners) response.end();
|
|
1767
|
+
listeners.clear();
|
|
1768
|
+
let controllerFailure;
|
|
1769
|
+
try {
|
|
1770
|
+
await options.controller.close();
|
|
1771
|
+
} catch (cause) {
|
|
1772
|
+
controllerFailure = cause;
|
|
1773
|
+
}
|
|
1774
|
+
await closeServer(server);
|
|
1775
|
+
if (controllerFailure !== void 0) throw controllerFailure;
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// apps/preview/src/mlb-simulator.ts
|
|
1781
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1782
|
+
|
|
1783
|
+
// apps/preview/src/widget-bridge.ts
|
|
1784
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1785
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1786
|
+
function widgetBridgeError(status, message2) {
|
|
1787
|
+
return Object.assign(new Error(message2), { status, publicMessage: message2, code: "WIDGET_BRIDGE_FAILED" });
|
|
1788
|
+
}
|
|
1789
|
+
function object(value) {
|
|
1790
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw widgetBridgeError(400, "\u9700\u8981\u6709\u6548\u7684\u64CD\u4F5C\u53C2\u6570");
|
|
1791
|
+
return value;
|
|
1792
|
+
}
|
|
1793
|
+
function part(value) {
|
|
1794
|
+
if (typeof value !== "string" || !value.trim() || value.length > 64 || /[\s/\\\u0000-\u001f\u007f]/u.test(value) || value === "." || value === "..") throw widgetBridgeError(400, "\u6A21\u5757\u6216\u8BB0\u5F55\u6807\u8BC6\u65E0\u6548");
|
|
1795
|
+
return encodeURIComponent(value);
|
|
1796
|
+
}
|
|
1797
|
+
function retryable(error) {
|
|
1798
|
+
const status = error?.status;
|
|
1799
|
+
return status === void 0 || status >= 500 || status === 429;
|
|
1800
|
+
}
|
|
1801
|
+
async function retry(run) {
|
|
1802
|
+
for (let attempt = 0; ; attempt++) {
|
|
1803
|
+
try {
|
|
1804
|
+
return await run();
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
if (attempt >= 2 || !retryable(error)) throw error;
|
|
1807
|
+
await delay(100 * (attempt + 1));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
var WidgetBridge = class {
|
|
1812
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
1813
|
+
#transport;
|
|
1814
|
+
#timer;
|
|
1815
|
+
#disposed = false;
|
|
1816
|
+
constructor(transport) {
|
|
1817
|
+
this.#transport = transport;
|
|
1818
|
+
this.#timer = setInterval(() => {
|
|
1819
|
+
for (const [key, session] of this.#sessions) if (Date.now() - session.touched > 9e4) {
|
|
1820
|
+
this.#sessions.delete(key);
|
|
1821
|
+
void this.#close(session, "page_lost").catch(() => {
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}, 15e3);
|
|
1825
|
+
this.#timer.unref();
|
|
1826
|
+
}
|
|
1827
|
+
async call(widgetId2, conversationId, value) {
|
|
1828
|
+
if (this.#disposed) throw widgetBridgeError(409, "\u8C03\u8BD5\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6");
|
|
1829
|
+
const body = object(value), render = body.render, method = body.method, requestId = body.requestId;
|
|
1830
|
+
if (typeof render !== "string" || !/^[a-zA-Z0-9-]{16,64}$/u.test(render) || typeof requestId !== "string" || !/^[a-zA-Z0-9-]{16,64}$/u.test(requestId) || typeof method !== "string") throw widgetBridgeError(400, "\u5C0F\u6302\u4EF6\u4EA4\u4E92\u6807\u8BC6\u65E0\u6548");
|
|
1831
|
+
part(widgetId2);
|
|
1832
|
+
let session = this.#sessions.get(render);
|
|
1833
|
+
if (session && (session.widgetId !== widgetId2 || session.conversationId !== conversationId)) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
|
|
1834
|
+
if (method === "open") {
|
|
1835
|
+
if (session?.closed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
|
|
1836
|
+
if (!session) {
|
|
1837
|
+
if (this.#sessions.size >= 32) throw widgetBridgeError(429, "\u6253\u5F00\u7684\u5C0F\u6302\u4EF6\u8FC7\u591A\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u8BD5");
|
|
1838
|
+
session = {
|
|
1839
|
+
widgetId: widgetId2,
|
|
1840
|
+
conversationId,
|
|
1841
|
+
interactionId: `wi_${randomUUID()}`,
|
|
1842
|
+
deviceId: `preview_${randomUUID()}`,
|
|
1843
|
+
sequence: 0,
|
|
1844
|
+
touched: Date.now(),
|
|
1845
|
+
closed: false,
|
|
1846
|
+
pending: 0,
|
|
1847
|
+
tail: Promise.resolve(),
|
|
1848
|
+
calls: /* @__PURE__ */ new Map()
|
|
1849
|
+
};
|
|
1850
|
+
this.#sessions.set(render, session);
|
|
1851
|
+
session.tail = this.#transport.request("GET", `/app/widgets/${encodeURIComponent(widgetId2)}/metadata?conversationId=${encodeURIComponent(conversationId)}`).catch((error) => {
|
|
1852
|
+
session.closed = true;
|
|
1853
|
+
throw error;
|
|
1854
|
+
});
|
|
1855
|
+
void session.tail.catch(() => {
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
await session.tail;
|
|
1859
|
+
if (this.#disposed || session.closed) throw widgetBridgeError(409, "\u8C03\u8BD5\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6");
|
|
1860
|
+
return { ready: true };
|
|
1861
|
+
}
|
|
1862
|
+
if (!session) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u8FDE\u63A5\u5DF2\u5931\u6548\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u65B0\u6253\u5F00");
|
|
1863
|
+
session.touched = Date.now();
|
|
1864
|
+
const serialized = JSON.stringify([method, body.params ?? {}]);
|
|
1865
|
+
if (method !== "media.upload" && Buffer.byteLength(serialized) > 300 * 1024) throw widgetBridgeError(413, "\u64CD\u4F5C\u53C2\u6570\u8FC7\u5927");
|
|
1866
|
+
const signature = createHash("sha256").update(serialized).digest("hex");
|
|
1867
|
+
const previous = session.calls.get(requestId);
|
|
1868
|
+
if (previous) {
|
|
1869
|
+
if (previous.signature !== signature) throw widgetBridgeError(409, "\u4E0D\u80FD\u4F7F\u7528\u540C\u4E00\u8BF7\u6C42\u6807\u8BC6\u6267\u884C\u4E0D\u540C\u64CD\u4F5C");
|
|
1870
|
+
return previous.result;
|
|
1871
|
+
}
|
|
1872
|
+
if (method === "close") return this.#close(session, "user_closed");
|
|
1873
|
+
if (session.closed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
|
|
1874
|
+
if (session.calls.size >= 256) throw widgetBridgeError(429, "\u672C\u6B21\u7F16\u8F91\u64CD\u4F5C\u8F83\u591A\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u65B0\u6253\u5F00");
|
|
1875
|
+
if (session.pending >= 16) throw widgetBridgeError(429, "\u64CD\u4F5C\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u4FDD\u5B58\u5B8C\u6210");
|
|
1876
|
+
const current = session;
|
|
1877
|
+
current.pending++;
|
|
1878
|
+
const result = current.tail.then(() => {
|
|
1879
|
+
if (current.closed || this.#disposed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
|
|
1880
|
+
return this.#perform(current, method, object(body.params ?? {}), requestId);
|
|
1881
|
+
});
|
|
1882
|
+
current.tail = result.then(() => {
|
|
1883
|
+
current.pending--;
|
|
1884
|
+
}, () => {
|
|
1885
|
+
current.pending--;
|
|
1886
|
+
});
|
|
1887
|
+
if (["records.create", "records.update", "records.delete", "media.upload"].includes(method)) current.calls.set(requestId, { signature, result });
|
|
1888
|
+
return result;
|
|
1889
|
+
}
|
|
1890
|
+
async #perform(s, method, p, requestId) {
|
|
1891
|
+
const base = `/app/widgets/${encodeURIComponent(s.widgetId)}`;
|
|
1892
|
+
if (method === "heartbeat" || method === "resume") {
|
|
1893
|
+
if (!s.sequence) return { active: true };
|
|
1894
|
+
return this.#transport.command(`widget.interaction.${method}`, { interactionId: s.interactionId, widgetId: s.widgetId, lastAckedSequence: s.sequence });
|
|
1895
|
+
}
|
|
1896
|
+
if (method === "media.url") {
|
|
1897
|
+
if (typeof p.mediaId !== "string" || !/^media_[A-Za-z0-9_-]+$/u.test(p.mediaId)) throw widgetBridgeError(400, "\u56FE\u7247\u6807\u8BC6\u65E0\u6548");
|
|
1898
|
+
const image = await this.#transport.image(s.widgetId, p.mediaId);
|
|
1899
|
+
if (!/^image\/(png|jpeg|webp|gif)(?:;|$)/u.test(image.contentType)) throw widgetBridgeError(415, "\u56FE\u7247\u683C\u5F0F\u4E0D\u652F\u6301");
|
|
1900
|
+
return { url: `data:${image.contentType.split(";")[0]};base64,${Buffer.from(image.body).toString("base64")}` };
|
|
1901
|
+
}
|
|
1902
|
+
if (method === "media.upload") {
|
|
1903
|
+
if (typeof p.mimeType !== "string" || !["image/png", "image/jpeg", "image/webp"].includes(p.mimeType) || typeof p.base64 !== "string" || p.base64.length > 7 * 1024 * 1024 || !/^[A-Za-z0-9+/]+={0,2}$/u.test(p.base64)) throw widgetBridgeError(400, "\u8BF7\u9009\u62E9 JPEG\u3001PNG \u6216 WebP \u56FE\u7247");
|
|
1904
|
+
const bytes = Buffer.from(p.base64, "base64");
|
|
1905
|
+
if (bytes.length > 5 * 1024 * 1024 || !bytes.length) throw widgetBridgeError(413, "\u56FE\u7247\u4E0D\u80FD\u8D85\u8FC7 5 MiB");
|
|
1906
|
+
const result = object(await this.#transport.upload(s.conversationId, p.mimeType, bytes));
|
|
1907
|
+
return { mediaId: result.mediaId, width: result.width, height: result.height, mimeType: result.mimeType, size: result.size };
|
|
1908
|
+
}
|
|
1909
|
+
if (!/^records\.(list|get|create|update|delete)$/u.test(method)) throw widgetBridgeError(400, "\u9875\u9762\u8BF7\u6C42\u4E86\u672A\u5F00\u653E\u7684\u80FD\u529B");
|
|
1910
|
+
const path = `${base}/modules/${part(p.module)}/records`;
|
|
1911
|
+
if (method === "records.list" || method === "records.get") {
|
|
1912
|
+
return this.#transport.request("GET", `${path}${method === "records.get" ? `/${part(p.recordId)}` : ""}?conversationId=${encodeURIComponent(s.conversationId)}`);
|
|
1913
|
+
}
|
|
1914
|
+
if (s.uncertain) throw widgetBridgeError(409, "\u4E0A\u6B21\u4FDD\u5B58\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u65B0\u6253\u5F00\u5E76\u6838\u5BF9\u6570\u636E");
|
|
1915
|
+
const action = method.slice(8);
|
|
1916
|
+
const mutation = { conversationId: s.conversationId };
|
|
1917
|
+
if (action !== "delete") mutation.data = object(p.data);
|
|
1918
|
+
if (p.expectedRevision !== void 0) {
|
|
1919
|
+
if (!Number.isSafeInteger(p.expectedRevision) || p.expectedRevision < 0) throw widgetBridgeError(400, "\u8BB0\u5F55\u7248\u672C\u53F7\u65E0\u6548");
|
|
1920
|
+
mutation.expectedRevision = p.expectedRevision;
|
|
1921
|
+
}
|
|
1922
|
+
if (Buffer.byteLength(JSON.stringify(mutation)) > 290 * 1024) throw widgetBridgeError(413, "\u8BB0\u5F55\u5185\u5BB9\u8FC7\u5927");
|
|
1923
|
+
const target = action === "create" ? path : `${path}/${part(p.recordId)}`;
|
|
1924
|
+
const sequence = s.sequence + 1;
|
|
1925
|
+
const headers = {
|
|
1926
|
+
"X-Widget-Interaction-Id": s.interactionId,
|
|
1927
|
+
"X-Widget-Operation-Id": `op_${requestId}`,
|
|
1928
|
+
"X-Widget-Sequence": String(sequence),
|
|
1929
|
+
"X-Widget-Entry-Point": "conversation_detail",
|
|
1930
|
+
"X-Device-Id": s.deviceId
|
|
1931
|
+
};
|
|
1932
|
+
try {
|
|
1933
|
+
const result = await retry(() => this.#transport.request(action === "create" ? "POST" : action === "update" ? "PUT" : "DELETE", target, mutation, headers));
|
|
1934
|
+
s.sequence = sequence;
|
|
1935
|
+
return result;
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
if (retryable(error)) s.uncertain = requestId;
|
|
1938
|
+
throw error;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async #close(s, reason) {
|
|
1942
|
+
if (s.closed) return s.tail;
|
|
1943
|
+
s.closed = true;
|
|
1944
|
+
const pending = s.tail;
|
|
1945
|
+
const closing = pending.catch(() => {
|
|
1946
|
+
}).then(() => retry(() => this.#transport.request(
|
|
1947
|
+
"POST",
|
|
1948
|
+
`/app/widgets/${encodeURIComponent(s.widgetId)}/interactions/${s.interactionId}/close`,
|
|
1949
|
+
{ conversationId: s.conversationId, lastIssuedSequence: s.sequence + (s.uncertain ? 1 : 0), closeReason: reason }
|
|
1950
|
+
)));
|
|
1951
|
+
s.tail = closing;
|
|
1952
|
+
void closing.catch(() => {
|
|
1953
|
+
});
|
|
1954
|
+
return closing;
|
|
1955
|
+
}
|
|
1956
|
+
async closeAll() {
|
|
1957
|
+
const sessions = [...this.#sessions.values()];
|
|
1958
|
+
this.#sessions.clear();
|
|
1959
|
+
await Promise.allSettled(sessions.map((s) => this.#close(s, "preview_stopped")));
|
|
1960
|
+
}
|
|
1961
|
+
async dispose() {
|
|
1962
|
+
this.#disposed = true;
|
|
1963
|
+
clearInterval(this.#timer);
|
|
1964
|
+
await this.closeAll();
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
// apps/preview/src/mlb-simulator.ts
|
|
1969
|
+
import { normalizePlatformUrl } from "./core.js";
|
|
1970
|
+
|
|
1971
|
+
// apps/preview/src/mlb-events.ts
|
|
1972
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1973
|
+
import { WebSocket } from "ws";
|
|
1974
|
+
var MlbEventStreamError = class extends Error {
|
|
1975
|
+
constructor(message2, status = 502, retryable2 = true) {
|
|
1976
|
+
super(message2);
|
|
1977
|
+
this.status = status;
|
|
1978
|
+
this.retryable = retryable2;
|
|
1979
|
+
this.publicMessage = message2;
|
|
1980
|
+
}
|
|
1981
|
+
status;
|
|
1982
|
+
retryable;
|
|
1983
|
+
code = "MLB_EVENT_STREAM_FAILED";
|
|
1984
|
+
publicMessage;
|
|
1985
|
+
};
|
|
1986
|
+
var MlbAppCommandError = class extends Error {
|
|
1987
|
+
constructor(message2, status = 502) {
|
|
1988
|
+
super(message2);
|
|
1989
|
+
this.status = status;
|
|
1990
|
+
this.publicMessage = message2;
|
|
1991
|
+
}
|
|
1992
|
+
status;
|
|
1993
|
+
code = "MLB_REQUEST_FAILED";
|
|
1994
|
+
publicMessage;
|
|
1995
|
+
};
|
|
1996
|
+
function appSocket(url, token) {
|
|
1997
|
+
return new WebSocket(url, {
|
|
1998
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1999
|
+
followRedirects: false,
|
|
2000
|
+
handshakeTimeout: 1e4,
|
|
2001
|
+
maxPayload: 64 * 1024,
|
|
2002
|
+
perMessageDeflate: false
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
async function mlbAppCommand(url, token, method, params, options = {}) {
|
|
2006
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
2007
|
+
const requestId = randomUUID2();
|
|
2008
|
+
const socket = appSocket(url, token);
|
|
2009
|
+
return await new Promise((resolve2, reject) => {
|
|
2010
|
+
let settled = false;
|
|
2011
|
+
let ready = false;
|
|
2012
|
+
let sent = false;
|
|
2013
|
+
const cleanup = () => {
|
|
2014
|
+
clearTimeout(timer);
|
|
2015
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2016
|
+
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
|
|
2017
|
+
};
|
|
2018
|
+
const finish = (value) => {
|
|
2019
|
+
if (settled) return;
|
|
2020
|
+
settled = true;
|
|
2021
|
+
cleanup();
|
|
2022
|
+
resolve2(value);
|
|
2023
|
+
};
|
|
2024
|
+
const fail = (cause) => {
|
|
2025
|
+
if (settled) return;
|
|
2026
|
+
settled = true;
|
|
2027
|
+
cleanup();
|
|
2028
|
+
reject(cause);
|
|
2029
|
+
};
|
|
2030
|
+
const abort = () => fail(options.signal?.reason);
|
|
2031
|
+
const timer = setTimeout(() => fail(new MlbAppCommandError("MLB WebSocket \u547D\u4EE4\u7B49\u5F85\u8D85\u65F6")), 3e4);
|
|
2032
|
+
timer.unref();
|
|
2033
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2034
|
+
socket.on("error", () => fail(new MlbAppCommandError("\u65E0\u6CD5\u8FDE\u63A5 MLB WebSocket")));
|
|
2035
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
2036
|
+
const status = response.statusCode ?? 502;
|
|
2037
|
+
fail(new MlbAppCommandError(
|
|
2038
|
+
status === 401 ? "MLB \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u542F DEV" : `MLB WebSocket \u63E1\u624B\u5931\u8D25\uFF08HTTP ${status}\uFF09`,
|
|
2039
|
+
status
|
|
2040
|
+
));
|
|
2041
|
+
response.destroy();
|
|
2042
|
+
});
|
|
2043
|
+
socket.on("close", (code) => fail(code === 4401 ? new MlbAppCommandError("MLB \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u542F DEV", 401) : new MlbAppCommandError("MLB WebSocket \u8FDE\u63A5\u4E2D\u65AD")));
|
|
2044
|
+
socket.on("message", (raw, binary) => {
|
|
2045
|
+
if (settled) return;
|
|
2046
|
+
try {
|
|
2047
|
+
if (binary) throw new Error("binary");
|
|
2048
|
+
const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
2049
|
+
const frame = JSON.parse(bytes.toString("utf8"));
|
|
2050
|
+
const data = frame.data;
|
|
2051
|
+
if (!frame || frame.v !== 1 || !data || typeof data !== "object" || Array.isArray(data)) {
|
|
2052
|
+
throw new Error("envelope");
|
|
2053
|
+
}
|
|
2054
|
+
if (frame.type === "event") {
|
|
2055
|
+
if (!ready) {
|
|
2056
|
+
if (frame.event !== "ready" || data.resync !== true || typeof data.connectionId !== "string" || !data.connectionId) throw new Error("ready required");
|
|
2057
|
+
ready = true;
|
|
2058
|
+
if (!sent) {
|
|
2059
|
+
sent = true;
|
|
2060
|
+
socket.send(JSON.stringify({ v: 1, type: "request", requestId, method, params }));
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (frame.type !== "response" || !ready || frame.requestId !== requestId || !Number.isSafeInteger(frame.status) || frame.status < 100 || frame.status > 599) {
|
|
2066
|
+
throw new Error("response");
|
|
2067
|
+
}
|
|
2068
|
+
const status = frame.status;
|
|
2069
|
+
if (status >= 200 && status < 300) {
|
|
2070
|
+
finish(data);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const serverMessage = data.error;
|
|
2074
|
+
fail(new MlbAppCommandError(
|
|
2075
|
+
typeof serverMessage === "string" && serverMessage.trim() ? serverMessage.slice(0, 512) : status >= 500 ? "MLB Server \u6682\u65F6\u4E0D\u53EF\u7528" : "\u6A21\u62DF\u5668\u8BF7\u6C42\u672A\u80FD\u5B8C\u6210",
|
|
2076
|
+
status
|
|
2077
|
+
));
|
|
2078
|
+
} catch {
|
|
2079
|
+
fail(new MlbAppCommandError("MLB WebSocket \u8FD4\u56DE\u4E86\u65E0\u6548\u547D\u4EE4\u56DE\u6267"));
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
if (options.signal?.aborted) abort();
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
async function* mlbAppEvents(url, token, options = {}) {
|
|
2086
|
+
if (options.signal?.aborted) return;
|
|
2087
|
+
const socket = appSocket(url, token);
|
|
2088
|
+
const queue = [];
|
|
2089
|
+
let queuedBytes = 0;
|
|
2090
|
+
let ready = false;
|
|
2091
|
+
let ended = false;
|
|
2092
|
+
let failure;
|
|
2093
|
+
let wake;
|
|
2094
|
+
const finish = (cause) => {
|
|
2095
|
+
if (ended) return;
|
|
2096
|
+
ended = true;
|
|
2097
|
+
failure = cause;
|
|
2098
|
+
wake?.();
|
|
2099
|
+
};
|
|
2100
|
+
let timer = setTimeout(() => finish(new MlbEventStreamError("MLB WebSocket \u672A\u5B8C\u6210\u521D\u59CB\u5316")), 1e4);
|
|
2101
|
+
timer.unref();
|
|
2102
|
+
const heartbeat = () => {
|
|
2103
|
+
if (!ready || ended) return;
|
|
2104
|
+
clearTimeout(timer);
|
|
2105
|
+
timer = setTimeout(() => finish(new MlbEventStreamError("MLB WebSocket \u5FC3\u8DF3\u4E2D\u65AD")), 6e4);
|
|
2106
|
+
timer.unref();
|
|
2107
|
+
};
|
|
2108
|
+
const abort = () => finish();
|
|
2109
|
+
socket.on("ping", heartbeat);
|
|
2110
|
+
socket.on("error", () => finish(new MlbEventStreamError("\u65E0\u6CD5\u8FDE\u63A5 MLB WebSocket")));
|
|
2111
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
2112
|
+
const status = response.statusCode ?? 502;
|
|
2113
|
+
finish(new MlbEventStreamError(
|
|
2114
|
+
status === 401 ? "MLB \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u542F DEV" : `MLB WebSocket \u63E1\u624B\u5931\u8D25\uFF08HTTP ${status}\uFF09`,
|
|
2115
|
+
status,
|
|
2116
|
+
status >= 500 || status === 429
|
|
2117
|
+
));
|
|
2118
|
+
response.destroy();
|
|
2119
|
+
});
|
|
2120
|
+
socket.on("close", (code) => finish(code === 4401 ? new MlbEventStreamError("MLB \u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u542F DEV", 401, false) : new MlbEventStreamError("MLB WebSocket \u8FDE\u63A5\u4E2D\u65AD", 502, ![1002, 1003, 1007, 1008, 1009].includes(code))));
|
|
2121
|
+
socket.on("message", (raw, binary) => {
|
|
2122
|
+
if (ended) return;
|
|
2123
|
+
try {
|
|
2124
|
+
if (binary) throw new Error("binary");
|
|
2125
|
+
const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
2126
|
+
const frame = JSON.parse(bytes.toString("utf8"));
|
|
2127
|
+
if (!frame || frame.v !== 1 || frame.type !== "event" || typeof frame.event !== "string" || !frame.data || typeof frame.data !== "object" || Array.isArray(frame.data)) throw new Error("envelope");
|
|
2128
|
+
if (!ready && frame.event !== "ready") throw new Error("ready required");
|
|
2129
|
+
if (frame.event === "ready" && (frame.data.resync !== true || typeof frame.data.connectionId !== "string" || !frame.data.connectionId)) throw new Error("invalid ready");
|
|
2130
|
+
ready = true;
|
|
2131
|
+
heartbeat();
|
|
2132
|
+
queuedBytes += bytes.byteLength;
|
|
2133
|
+
if (queuedBytes > 1024 * 1024 || queue.length >= 1024) {
|
|
2134
|
+
finish(new MlbEventStreamError("MLB \u4E8B\u4EF6\u63A5\u6536\u79EF\u538B\uFF0C\u6B63\u5728\u91CD\u65B0\u540C\u6B65"));
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
queue.push({ frame: { event: frame.event, data: frame.data }, bytes: bytes.byteLength });
|
|
2138
|
+
wake?.();
|
|
2139
|
+
} catch {
|
|
2140
|
+
finish(new MlbEventStreamError("MLB WebSocket \u8FD4\u56DE\u4E86\u65E0\u6548\u4E8B\u4EF6", 502, false));
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2144
|
+
if (options.signal?.aborted) abort();
|
|
2145
|
+
try {
|
|
2146
|
+
while (!ended) {
|
|
2147
|
+
const item = queue.shift();
|
|
2148
|
+
if (item) {
|
|
2149
|
+
queuedBytes -= item.bytes;
|
|
2150
|
+
yield item.frame;
|
|
2151
|
+
} else {
|
|
2152
|
+
await new Promise((resolve2) => {
|
|
2153
|
+
wake = resolve2;
|
|
2154
|
+
});
|
|
2155
|
+
wake = void 0;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (failure && !options.signal?.aborted) throw failure;
|
|
2159
|
+
} finally {
|
|
2160
|
+
ended = true;
|
|
2161
|
+
clearTimeout(timer);
|
|
2162
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2163
|
+
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// apps/preview/src/buddy-details.ts
|
|
2168
|
+
function invalid() {
|
|
2169
|
+
throw Object.assign(new Error("MLB \u8FD4\u56DE\u7684\u5F00\u53D1\u7248\u642D\u5B50\u8BE6\u60C5\u4E0D\u7B26\u5408\u63A5\u53E3\u7EA6\u5B9A"), {
|
|
2170
|
+
code: "BUDDY_DETAILS_INVALID",
|
|
2171
|
+
status: 502,
|
|
2172
|
+
publicMessage: "MLB \u8FD4\u56DE\u7684\u5F00\u53D1\u7248\u642D\u5B50\u8BE6\u60C5\u4E0D\u7B26\u5408\u63A5\u53E3\u7EA6\u5B9A"
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
function record(value) {
|
|
2176
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
|
|
2177
|
+
return value;
|
|
2178
|
+
}
|
|
2179
|
+
function text(value, multiline = false) {
|
|
2180
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > 32 * 1024 || (multiline ? /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u : /[\u0000-\u001f\u007f]/u).test(value)) return invalid();
|
|
2181
|
+
return value;
|
|
2182
|
+
}
|
|
2183
|
+
function nullableText(value, multiline = false) {
|
|
2184
|
+
return value === null ? null : text(value, multiline);
|
|
2185
|
+
}
|
|
2186
|
+
function parsePreviewBuddyDetails(value, expectedId) {
|
|
2187
|
+
const source = record(value);
|
|
2188
|
+
if (source.id !== expectedId || source.isDev !== true || typeof source.name !== "string" || !source.name.trim() || source.privateId !== null && (typeof source.privateId !== "string" || !/^p_[a-zA-Z0-9_-]{1,253}$/u.test(source.privateId)) || !Number.isSafeInteger(source.addedCount) || source.addedCount < 0) return invalid();
|
|
2189
|
+
return Object.freeze({
|
|
2190
|
+
id: expectedId,
|
|
2191
|
+
isDev: true,
|
|
2192
|
+
name: text(source.name),
|
|
2193
|
+
avatar: nullableText(source.avatar),
|
|
2194
|
+
tagline: nullableText(source.tagline),
|
|
2195
|
+
intro: nullableText(source.intro, true),
|
|
2196
|
+
creatorUserId: nullableText(source.creatorUserId),
|
|
2197
|
+
creatorUserName: nullableText(source.creatorUserName),
|
|
2198
|
+
creatorUserAvatar: nullableText(source.creatorUserAvatar),
|
|
2199
|
+
privateId: source.privateId,
|
|
2200
|
+
addedCount: source.addedCount
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// apps/preview/src/mlb-simulator.ts
|
|
2205
|
+
function safeText(value, name, maximum = 4096) {
|
|
2206
|
+
if (typeof value !== "string" || !value.trim() || value.length > maximum || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
2207
|
+
throw new TypeError(`${name} is invalid`);
|
|
2208
|
+
}
|
|
2209
|
+
return value;
|
|
2210
|
+
}
|
|
2211
|
+
function endpoint(base, path) {
|
|
2212
|
+
return new URL(path.replace(/^\//u, ""), base);
|
|
2213
|
+
}
|
|
2214
|
+
function object2(value) {
|
|
2215
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2216
|
+
}
|
|
2217
|
+
function httpError(status, value) {
|
|
2218
|
+
const serverMessage = object2(object2(value)?.error)?.message ?? object2(value)?.error;
|
|
2219
|
+
const message2 = typeof serverMessage === "string" && serverMessage.trim() ? serverMessage.slice(0, 512) : status >= 500 ? "MLB Server \u6682\u65F6\u4E0D\u53EF\u7528" : "\u6A21\u62DF\u5668\u8BF7\u6C42\u672A\u80FD\u5B8C\u6210";
|
|
2220
|
+
return Object.assign(new Error(message2), {
|
|
2221
|
+
code: typeof object2(object2(value)?.error)?.code === "string" ? object2(object2(value)?.error).code : "MLB_REQUEST_FAILED",
|
|
2222
|
+
status,
|
|
2223
|
+
publicMessage: message2
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
function message(value, index) {
|
|
2227
|
+
const input = object2(value);
|
|
2228
|
+
if (!input || typeof input.author !== "string") return null;
|
|
2229
|
+
const role = input.author === "buddy" ? "assistant" : input.author === "user" || input.author === "owner" ? "user" : void 0;
|
|
2230
|
+
if (!role) return null;
|
|
2231
|
+
const messageId = safeText(input.id, `messages[${index}].id`, 256);
|
|
2232
|
+
let text2 = ["widget", "product"].includes(String(input.type)) && input.text === void 0 ? "" : input.text;
|
|
2233
|
+
if (typeof text2 !== "string" || Buffer.byteLength(text2, "utf8") > 48 * 1024) {
|
|
2234
|
+
throw new Error(`messages[${index}].text is invalid`);
|
|
2235
|
+
}
|
|
2236
|
+
if (input.type === "tool" && !text2.trim()) return null;
|
|
2237
|
+
if (typeof input.timestamp !== "number" || !Number.isFinite(input.timestamp)) {
|
|
2238
|
+
throw new Error(`messages[${index}].timestamp is invalid`);
|
|
2239
|
+
}
|
|
2240
|
+
if (role === "assistant" && input.type === "question") {
|
|
2241
|
+
const options = object2(input.question)?.options;
|
|
2242
|
+
if (!Array.isArray(options) || options.length < 2 || options.length > 4 || options.some((option) => typeof option !== "string" || !option.trim() || option.length > 30)) {
|
|
2243
|
+
throw new Error(`messages[${index}].question is invalid`);
|
|
2244
|
+
}
|
|
2245
|
+
text2 += `
|
|
2246
|
+
|
|
2247
|
+
${options.map((option, i) => `${i + 1}. ${option}`).join("\n")}
|
|
2248
|
+
|
|
2249
|
+
\u8BF7\u76F4\u63A5\u56DE\u590D\u4F60\u7684\u9009\u62E9\uFF0C\u4E5F\u53EF\u4EE5\u8865\u5145\u8BF4\u660E\u3002`;
|
|
2250
|
+
}
|
|
2251
|
+
if (role === "assistant" && input.type === "permissionRequest") {
|
|
2252
|
+
const request = object2(input.permissionRequest);
|
|
2253
|
+
text2 += request?.status === "pending" ? "\n\n\u8BF7\u5728\u642D\u642D App \u4E2D\u5904\u7406\u8FD9\u5F20\u6388\u6743\u5361\uFF1B\u6D4F\u89C8\u5668\u9884\u89C8\u4E0D\u4EE3\u66FF\u7CFB\u7EDF\u6388\u6743\u3002" : "\n\n\u6388\u6743\u5361\u7684\u5904\u7406\u7ED3\u679C\u8BF7\u4EE5\u642D\u642D App \u4E2D\u7684\u72B6\u6001\u4E3A\u51C6\u3002";
|
|
2254
|
+
}
|
|
2255
|
+
return Object.freeze({
|
|
2256
|
+
messageId,
|
|
2257
|
+
role,
|
|
2258
|
+
text: text2,
|
|
2259
|
+
createdAt: new Date(input.timestamp).toISOString(),
|
|
2260
|
+
...input.type === "widget" && object2(input.widget) ? { widget: parseWidget(input.widget) } : {},
|
|
2261
|
+
...input.type === "product" && role === "assistant" ? { product: parseProduct(input.product) } : {}
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
function parseProduct(value) {
|
|
2265
|
+
const product = object2(value);
|
|
2266
|
+
if (!product || !Number.isSafeInteger(product.version) || Number(product.version) < 1) throw new TypeError("\u5546\u54C1\u5361\u7248\u672C\u65E0\u6548");
|
|
2267
|
+
return Object.freeze({
|
|
2268
|
+
offerId: safeText(product.offerId, "offerId", 120),
|
|
2269
|
+
productId: safeText(product.productId, "productId", 120),
|
|
2270
|
+
version: Number(product.version),
|
|
2271
|
+
title: safeText(product.title, "\u5546\u54C1\u540D\u79F0", 512),
|
|
2272
|
+
summary: typeof product.summary === "string" ? product.summary.slice(0, 4096) : "",
|
|
2273
|
+
buttonText: typeof product.buttonText === "string" && product.buttonText.trim() ? product.buttonText.slice(0, 100) : "\u67E5\u770B\u65B9\u6848"
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
function widgetId(value) {
|
|
2277
|
+
const id = safeText(value, "widgetId", 64);
|
|
2278
|
+
if (/[\s/\\]/u.test(id) || id === "." || id === "..") throw new TypeError("widgetId is invalid");
|
|
2279
|
+
return id;
|
|
2280
|
+
}
|
|
2281
|
+
function parseWidget(value) {
|
|
2282
|
+
const item = object2(value);
|
|
2283
|
+
const openTarget = parseOpenTarget(item?.openTarget);
|
|
2284
|
+
return Object.freeze({
|
|
2285
|
+
id: widgetId(item?.id),
|
|
2286
|
+
title: typeof item?.title === "string" && item.title.trim() ? item.title : "\u5C0F\u6302\u4EF6",
|
|
2287
|
+
...typeof item?.type === "string" ? { type: item.type } : {},
|
|
2288
|
+
...typeof item?.summary === "string" ? { summary: item.summary } : {},
|
|
2289
|
+
...typeof item?.imageMediaId === "string" && /^media_[a-zA-Z0-9_-]+$/u.test(item.imageMediaId) ? { imageMediaId: item.imageMediaId } : {},
|
|
2290
|
+
...typeof item?.isPinned === "boolean" ? { isPinned: item.isPinned } : {},
|
|
2291
|
+
...typeof item?.creatorName === "string" ? { creatorName: item.creatorName } : {},
|
|
2292
|
+
...item?.role === "owner" || item?.role === "editor" || item?.role === "viewer" ? { role: item.role } : {},
|
|
2293
|
+
...openTarget ? { openTarget } : {}
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
function parseOpenTarget(value) {
|
|
2297
|
+
if (value === void 0) return void 0;
|
|
2298
|
+
const target = object2(value), resource = object2(target?.resource);
|
|
2299
|
+
const name = (input) => typeof input === "string" && /^[^\s/\u0000-\u001f\u007f]{1,64}$/u.test(input);
|
|
2300
|
+
if (!target || target.version !== 1 || !name(target.entry) || target.resource !== void 0 && (!resource || !name(resource.module) || !name(resource.recordId)) || target.state !== void 0 && !object2(target.state)) throw new TypeError("Widget \u6253\u5F00\u76EE\u6807\u683C\u5F0F\u65E0\u6548");
|
|
2301
|
+
const normalized = {
|
|
2302
|
+
version: 1,
|
|
2303
|
+
entry: target.entry,
|
|
2304
|
+
...resource ? { resource: { module: resource.module, recordId: resource.recordId } } : {},
|
|
2305
|
+
...target.state !== void 0 ? { state: object2(target.state) } : {}
|
|
2306
|
+
};
|
|
2307
|
+
if (Buffer.byteLength(JSON.stringify(normalized), "utf8") > 8 * 1024) throw new TypeError("Widget \u6253\u5F00\u76EE\u6807\u8D85\u8FC7 8 KiB");
|
|
2308
|
+
return Object.freeze(normalized);
|
|
2309
|
+
}
|
|
2310
|
+
function localReceipt(prefix, value) {
|
|
2311
|
+
return `${prefix}-${createHash2("sha256").update(value).digest("hex").slice(0, 20)}`;
|
|
2312
|
+
}
|
|
2313
|
+
function waitForRetry(milliseconds, signal) {
|
|
2314
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
2315
|
+
return new Promise((resolve2, reject) => {
|
|
2316
|
+
const abort = () => {
|
|
2317
|
+
clearTimeout(timer);
|
|
2318
|
+
reject(signal?.reason);
|
|
2319
|
+
};
|
|
2320
|
+
const timer = setTimeout(() => {
|
|
2321
|
+
signal?.removeEventListener("abort", abort);
|
|
2322
|
+
resolve2();
|
|
2323
|
+
}, milliseconds);
|
|
2324
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
var MlbSimulatorClient = class {
|
|
2328
|
+
#baseUrl;
|
|
2329
|
+
#buddyId;
|
|
2330
|
+
#accessToken;
|
|
2331
|
+
#fetch;
|
|
2332
|
+
#widgetBridge;
|
|
2333
|
+
#sending = false;
|
|
2334
|
+
#resetTarget;
|
|
2335
|
+
#resetArchived = false;
|
|
2336
|
+
#lastSend;
|
|
2337
|
+
#privateId;
|
|
2338
|
+
#privatePromise;
|
|
2339
|
+
#active;
|
|
2340
|
+
#latest;
|
|
2341
|
+
#streaming;
|
|
2342
|
+
#messages = Object.freeze([]);
|
|
2343
|
+
constructor(options) {
|
|
2344
|
+
this.#baseUrl = normalizePlatformUrl(options.baseUrl);
|
|
2345
|
+
this.#buddyId = safeText(options.buddyId, "buddyId", 256);
|
|
2346
|
+
if (!this.#buddyId.endsWith("-dev")) {
|
|
2347
|
+
throw new TypeError("MLB simulator buddyId must use the -dev identity");
|
|
2348
|
+
}
|
|
2349
|
+
this.#accessToken = safeText(options.auth.developerAccessToken, "developerAccessToken", 16384);
|
|
2350
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
2351
|
+
if (typeof this.#fetch !== "function") throw new TypeError("A Fetch API implementation is required");
|
|
2352
|
+
this.#widgetBridge = new WidgetBridge({
|
|
2353
|
+
request: (method, path, body, headers) => this.#request(method, path, { body, headers }),
|
|
2354
|
+
command: (method, params) => {
|
|
2355
|
+
const url = endpoint(this.#baseUrl, "/app/ws");
|
|
2356
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2357
|
+
return mlbAppCommand(url, this.#accessToken, method, params);
|
|
2358
|
+
},
|
|
2359
|
+
image: (id, mediaId) => this.widgetImage(id, mediaId),
|
|
2360
|
+
upload: async (conversationId, mime, bytes) => {
|
|
2361
|
+
const response = await this.#fetch(endpoint(this.#baseUrl, `/app/media/uploads?conversationId=${encodeURIComponent(conversationId)}`), {
|
|
2362
|
+
method: "POST",
|
|
2363
|
+
headers: { Authorization: `Bearer ${this.#accessToken}`, "Content-Type": mime },
|
|
2364
|
+
body: new Uint8Array(bytes),
|
|
2365
|
+
redirect: "error",
|
|
2366
|
+
credentials: "omit",
|
|
2367
|
+
signal: AbortSignal.timeout(25e3)
|
|
2368
|
+
});
|
|
2369
|
+
const value = await response.json().catch(() => void 0);
|
|
2370
|
+
if (!response.ok) throw httpError(response.status, value);
|
|
2371
|
+
return value;
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
/** Reading a profile must not add the Buddy or trigger a welcome message. */
|
|
2376
|
+
async details(options = {}) {
|
|
2377
|
+
const signal = AbortSignal.any([AbortSignal.timeout(3e4), ...options.signal ? [options.signal] : []]);
|
|
2378
|
+
const details = parsePreviewBuddyDetails(await this.#request("GET", `/app/buddies/${encodeURIComponent(this.#buddyId)}`, { signal }), this.#buddyId);
|
|
2379
|
+
this.#privateId = details.privateId ?? void 0;
|
|
2380
|
+
if (!details.avatar) return details;
|
|
2381
|
+
let avatarPreview = null;
|
|
2382
|
+
try {
|
|
2383
|
+
const url = new URL(details.avatar, this.#baseUrl);
|
|
2384
|
+
if (url.origin !== this.#baseUrl.origin || url.username || url.password || !url.pathname.startsWith("/media/")) throw new Error("Unsupported avatar URL");
|
|
2385
|
+
const image = await this.#widgetResource(url.href, false);
|
|
2386
|
+
const contentType = image.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
2387
|
+
if (!["image/png", "image/jpeg", "image/webp"].includes(contentType)) throw new Error("Unsupported avatar type");
|
|
2388
|
+
avatarPreview = `data:${contentType};base64,${Buffer.from(image.body).toString("base64")}`;
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
return Object.freeze({ ...details, avatarPreview });
|
|
2392
|
+
}
|
|
2393
|
+
async productOffer(offerId) {
|
|
2394
|
+
this.#assertResetComplete();
|
|
2395
|
+
const detail = object2(await this.#request("GET", `/app/product-offers/${encodeURIComponent(safeText(offerId, "offerId", 120))}`, {}));
|
|
2396
|
+
if (detail?.buddyId !== this.#buddyId || detail?.offerId !== offerId || detail.paymentChannel !== "mock" || detail.chargedAmountMinor !== 0)
|
|
2397
|
+
throw httpError(409, { error: "\u4EC5\u652F\u6301\u5F53\u524D DEV \u642D\u5B50\u7684\u96F6\u6263\u8D39\u6A21\u62DF\u8D2D\u4E70" });
|
|
2398
|
+
return detail;
|
|
2399
|
+
}
|
|
2400
|
+
async purchaseProduct(offerId, input) {
|
|
2401
|
+
await this.productOffer(offerId);
|
|
2402
|
+
return this.#request("POST", `/app/product-offers/${encodeURIComponent(offerId)}/mock-purchase`, { body: { expectedVersion: input.expectedVersion, idempotencyKey: safeText(input.idempotencyKey, "\u5E42\u7B49\u952E", 128) } });
|
|
2403
|
+
}
|
|
2404
|
+
async widgets() {
|
|
2405
|
+
this.#assertResetComplete();
|
|
2406
|
+
const id = await this.#ensurePrivate();
|
|
2407
|
+
const body = object2(await this.#request("GET", `/app/privates/${encodeURIComponent(id)}/widgets`, {}));
|
|
2408
|
+
if (!Array.isArray(body?.widgets)) throw httpError(502, { error: "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u5C0F\u6302\u4EF6" });
|
|
2409
|
+
return body.widgets.map(parseWidget).filter((item) => item.type !== void 0);
|
|
2410
|
+
}
|
|
2411
|
+
async widgetHtml(id, openTarget) {
|
|
2412
|
+
this.#assertResetComplete();
|
|
2413
|
+
const target = parseOpenTarget(openTarget);
|
|
2414
|
+
const privateId = await this.#ensurePrivate();
|
|
2415
|
+
const result = await this.#widgetResource(`/app/widgets/${encodeURIComponent(widgetId(id))}/presentation`, true, {
|
|
2416
|
+
method: "POST",
|
|
2417
|
+
body: JSON.stringify({ conversationId: privateId, ...target ? { openTarget: target } : {} })
|
|
2418
|
+
});
|
|
2419
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(result.body);
|
|
2420
|
+
}
|
|
2421
|
+
async pinWidget(id, pinned) {
|
|
2422
|
+
this.#assertResetComplete();
|
|
2423
|
+
const privateId = await this.#ensurePrivate();
|
|
2424
|
+
await this.#request(pinned ? "PUT" : "DELETE", `/app/widgets/${encodeURIComponent(widgetId(id))}/pin`, { ...pinned ? { body: { sourceConversationId: privateId } } : {} });
|
|
2425
|
+
}
|
|
2426
|
+
async widgetCommand(id, body) {
|
|
2427
|
+
this.#assertResetComplete();
|
|
2428
|
+
return this.#widgetBridge.call(widgetId(id), await this.#ensurePrivate(), body);
|
|
2429
|
+
}
|
|
2430
|
+
async disposeWidgets() {
|
|
2431
|
+
await this.#widgetBridge.dispose();
|
|
2432
|
+
}
|
|
2433
|
+
async resetWidgets() {
|
|
2434
|
+
await this.#widgetBridge.closeAll();
|
|
2435
|
+
}
|
|
2436
|
+
async widgetImage(id, mediaId) {
|
|
2437
|
+
this.#assertResetComplete();
|
|
2438
|
+
if (!/^media_[a-zA-Z0-9_-]+$/u.test(mediaId)) throw new TypeError("Invalid media ID");
|
|
2439
|
+
const access = object2(await this.#request("GET", `/app/media/${encodeURIComponent(mediaId)}?widgetId=${encodeURIComponent(widgetId(id))}`, {}));
|
|
2440
|
+
const url = new URL(String(access?.url));
|
|
2441
|
+
if (url.protocol !== "https:" || url.username || url.password || access?.mediaId !== mediaId || typeof access.expiresAt !== "number" || access.expiresAt <= Date.now()) throw new Error("\u56FE\u7247\u94FE\u63A5\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5");
|
|
2442
|
+
return this.#widgetResource(url.href, false);
|
|
2443
|
+
}
|
|
2444
|
+
async #widgetResource(path, authenticated = true, init = {}) {
|
|
2445
|
+
const response = await this.#fetch(authenticated ? endpoint(this.#baseUrl, path) : new URL(path), { ...init, headers: {
|
|
2446
|
+
...authenticated ? { Authorization: `Bearer ${this.#accessToken}` } : {},
|
|
2447
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
2448
|
+
}, credentials: "omit", redirect: "error", signal: AbortSignal.timeout(15e3) });
|
|
2449
|
+
if (!response.ok) throw httpError(response.status, { error: response.status === 403 || response.status === 404 ? "\u5C0F\u6302\u4EF6\u5DF2\u5220\u9664\u3001\u9875\u9762\u672A\u4E0A\u4F20\uFF0C\u6216\u4F60\u5DF2\u4E0D\u5728\u8FD9\u4E2A\u4F1A\u8BDD\u4E2D\u3002" : "\u5C0F\u6302\u4EF6\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u3002" });
|
|
2450
|
+
const reader = response.body?.getReader();
|
|
2451
|
+
if (!reader) throw new Error("\u5C0F\u6302\u4EF6\u5185\u5BB9\u4E3A\u7A7A");
|
|
2452
|
+
const chunks = [];
|
|
2453
|
+
let size = 0;
|
|
2454
|
+
try {
|
|
2455
|
+
while (true) {
|
|
2456
|
+
const part2 = await reader.read();
|
|
2457
|
+
if (part2.done) break;
|
|
2458
|
+
size += part2.value.byteLength;
|
|
2459
|
+
if (size > 4 * 1024 * 1024) throw new Error("\u5C0F\u6302\u4EF6\u5185\u5BB9\u8FC7\u5927\uFF0C\u6682\u65F6\u65E0\u6CD5\u6253\u5F00");
|
|
2460
|
+
chunks.push(part2.value);
|
|
2461
|
+
}
|
|
2462
|
+
} finally {
|
|
2463
|
+
await reader.cancel();
|
|
2464
|
+
}
|
|
2465
|
+
return { body: Buffer.concat(chunks), contentType: response.headers.get("content-type") ?? "text/html" };
|
|
2466
|
+
}
|
|
2467
|
+
async state(options = {}) {
|
|
2468
|
+
this.#assertResetComplete();
|
|
2469
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
2470
|
+
const value = await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, {
|
|
2471
|
+
signal: options.signal
|
|
2472
|
+
});
|
|
2473
|
+
const body = object2(value);
|
|
2474
|
+
const conversation = object2(body?.private);
|
|
2475
|
+
if (!body || conversation?.id !== privateId || conversation?.buddyId !== this.#buddyId || !Array.isArray(body.messages) || typeof body.replying !== "boolean") {
|
|
2476
|
+
throw httpError(502, { error: "MLB Server \u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u79C1\u804A\u72B6\u6001" });
|
|
2477
|
+
}
|
|
2478
|
+
this.#messages = Object.freeze(body.messages.map((item, index) => message(item, index)).filter((item) => item !== null));
|
|
2479
|
+
if (this.#active) {
|
|
2480
|
+
const replied = this.#messages.some((item) => item.role === "assistant" && Date.parse(item.createdAt) > this.#active.messageTimestamp);
|
|
2481
|
+
if (body.replying) {
|
|
2482
|
+
this.#active = Object.freeze({ ...this.#active, observedReplying: true });
|
|
2483
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "running" });
|
|
2484
|
+
} else if (replied) {
|
|
2485
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "completed" });
|
|
2486
|
+
this.#active = void 0;
|
|
2487
|
+
this.#streaming = void 0;
|
|
2488
|
+
} else if (this.#active.observedReplying) {
|
|
2489
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "completed" });
|
|
2490
|
+
this.#active = void 0;
|
|
2491
|
+
this.#streaming = void 0;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return Object.freeze({
|
|
2495
|
+
messages: this.#messages,
|
|
2496
|
+
...this.#latest === void 0 ? {} : { latestRun: this.#latest },
|
|
2497
|
+
...this.#active === void 0 ? {} : {
|
|
2498
|
+
activeRun: Object.freeze({
|
|
2499
|
+
runId: this.#active.runId,
|
|
2500
|
+
status: body.replying ? "running" : "pending"
|
|
2501
|
+
})
|
|
2502
|
+
},
|
|
2503
|
+
...this.#streaming === void 0 ? {} : { streaming: this.#streaming }
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
async send(input, options = {}) {
|
|
2507
|
+
if (typeof input.text !== "string" || !input.text.trim() || Buffer.byteLength(input.text, "utf8") > 32 * 1024) {
|
|
2508
|
+
throw new TypeError("message text is invalid");
|
|
2509
|
+
}
|
|
2510
|
+
this.#assertResetComplete();
|
|
2511
|
+
const idempotencyKey = safeText(input.idempotencyKey, "idempotencyKey", 128);
|
|
2512
|
+
if (this.#lastSend?.key === idempotencyKey) {
|
|
2513
|
+
if (this.#lastSend.text !== input.text.trim()) throw httpError(409, { error: "\u540C\u4E00\u6761\u6D88\u606F\u4E0D\u80FD\u6539\u5199\u5185\u5BB9" });
|
|
2514
|
+
return this.#lastSend.receipt;
|
|
2515
|
+
}
|
|
2516
|
+
if (this.#sending) {
|
|
2517
|
+
throw Object.assign(new Error("\u6D88\u606F\u6B63\u5728\u53D1\u9001"), {
|
|
2518
|
+
code: "SEND_IN_PROGRESS",
|
|
2519
|
+
status: 409,
|
|
2520
|
+
publicMessage: "\u6D88\u606F\u6B63\u5728\u53D1\u9001\uFF0C\u8BF7\u7A0D\u5019"
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
this.#sending = true;
|
|
2524
|
+
try {
|
|
2525
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
2526
|
+
const clientMessageId = localReceipt("client", idempotencyKey);
|
|
2527
|
+
let value;
|
|
2528
|
+
try {
|
|
2529
|
+
const url = endpoint(this.#baseUrl, "/app/ws");
|
|
2530
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2531
|
+
value = await mlbAppCommand(url, this.#accessToken, "message.send", {
|
|
2532
|
+
kind: "private",
|
|
2533
|
+
id: privateId,
|
|
2534
|
+
messages: [{ type: "text", text: input.text }],
|
|
2535
|
+
clientMessageId
|
|
2536
|
+
}, { signal: options.signal });
|
|
2537
|
+
const receipt2 = object2(value);
|
|
2538
|
+
const items = Array.isArray(receipt2?.items) ? receipt2.items : [];
|
|
2539
|
+
const item2 = object2(items[0]);
|
|
2540
|
+
if (receipt2?.ok !== true || receipt2.clientMessageId !== clientMessageId || items.length !== 1 || item2?.index !== 0 || typeof item2.messageId !== "string" || !item2.messageId.trim() || !Number.isSafeInteger(item2.timestamp) || item2.timestamp < 0 || typeof receipt2.shouldReply !== "boolean") {
|
|
2541
|
+
throw httpError(502, { error: "MLB \u6D88\u606F\u56DE\u6267\u65E0\u6548" });
|
|
2542
|
+
}
|
|
2543
|
+
safeText(item2.messageId, "receipt.items[0].messageId", 256);
|
|
2544
|
+
} catch (cause) {
|
|
2545
|
+
if (cause && typeof cause === "object" && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500) throw cause;
|
|
2546
|
+
throw Object.assign(new Error("\u53D1\u9001\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u804A\u5929\u8BB0\u5F55\u540E\u518D\u91CD\u8BD5"), {
|
|
2547
|
+
code: "MLB_SEND_OUTCOME_UNKNOWN",
|
|
2548
|
+
status: 502,
|
|
2549
|
+
publicMessage: "\u53D1\u9001\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u804A\u5929\u8BB0\u5F55\u540E\u518D\u91CD\u8BD5"
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
const receipt = object2(value);
|
|
2553
|
+
const item = object2(receipt.items[0]);
|
|
2554
|
+
const runId = this.#active?.runId ?? localReceipt("turn", idempotencyKey);
|
|
2555
|
+
const messageId = safeText(item.messageId, "receipt.items[0].messageId", 256);
|
|
2556
|
+
if (!this.#active) {
|
|
2557
|
+
this.#active = receipt.shouldReply ? Object.freeze({
|
|
2558
|
+
runId,
|
|
2559
|
+
messageTimestamp: item.timestamp,
|
|
2560
|
+
observedReplying: false
|
|
2561
|
+
}) : void 0;
|
|
2562
|
+
this.#latest = Object.freeze({ runId, status: receipt.shouldReply ? "pending" : "completed" });
|
|
2563
|
+
this.#streaming = void 0;
|
|
2564
|
+
}
|
|
2565
|
+
const accepted = Object.freeze({ messageId, runId });
|
|
2566
|
+
this.#lastSend = { key: idempotencyKey, text: input.text.trim(), receipt: accepted };
|
|
2567
|
+
return accepted;
|
|
2568
|
+
} finally {
|
|
2569
|
+
this.#sending = false;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
#assertResetComplete() {
|
|
2573
|
+
if (this.#resetTarget) throw Object.assign(new Error("\u8BF7\u518D\u6B21\u70B9\u51FB\u6E05\u7A7A\u5BF9\u8BDD\uFF0C\u5B8C\u6210\u65B0\u4F1A\u8BDD\u521B\u5EFA"), {
|
|
2574
|
+
code: "RESET_INCOMPLETE",
|
|
2575
|
+
status: 409,
|
|
2576
|
+
publicMessage: "\u4F1A\u8BDD\u91CD\u7F6E\u5C1A\u672A\u5B8C\u6210\uFF0C\u8BF7\u518D\u6B21\u70B9\u51FB\u300C\u6E05\u7A7A\u5BF9\u8BDD\u300D\u91CD\u8BD5"
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
async reset(options = {}) {
|
|
2580
|
+
if (this.#sending) throw httpError(409, { error: "\u6D88\u606F\u6B63\u5728\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u6E05\u7A7A" });
|
|
2581
|
+
if (!this.#resetTarget) {
|
|
2582
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
2583
|
+
const current = object2(await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, options));
|
|
2584
|
+
const conversation = object2(current?.private);
|
|
2585
|
+
if (conversation?.id !== privateId || conversation?.buddyId !== this.#buddyId || typeof current?.replying !== "boolean") {
|
|
2586
|
+
throw httpError(502, { error: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u79C1\u804A\u72B6\u6001\uFF0C\u672A\u6E05\u7A7A\u5BF9\u8BDD" });
|
|
2587
|
+
}
|
|
2588
|
+
if (current.replying) throw httpError(409, { error: "\u8BF7\u7B49\u5F85\u5F53\u524D\u56DE\u590D\u5B8C\u6210\u540E\u518D\u6E05\u7A7A" });
|
|
2589
|
+
this.#resetTarget = privateId;
|
|
2590
|
+
await this.#widgetBridge.closeAll();
|
|
2591
|
+
}
|
|
2592
|
+
try {
|
|
2593
|
+
if (!this.#resetArchived) {
|
|
2594
|
+
const receipt = object2(await this.#request("DELETE", `/app/privates/${encodeURIComponent(this.#resetTarget)}`, options));
|
|
2595
|
+
if (receipt?.ok !== true) throw httpError(502, { error: "\u65E0\u6CD5\u786E\u8BA4\u65E7\u4F1A\u8BDD\u5DF2\u7ED3\u675F" });
|
|
2596
|
+
this.#resetArchived = true;
|
|
2597
|
+
this.#privateId = void 0;
|
|
2598
|
+
}
|
|
2599
|
+
const nextId = await this.#ensurePrivate(options.signal);
|
|
2600
|
+
if (nextId === this.#resetTarget) {
|
|
2601
|
+
this.#privateId = void 0;
|
|
2602
|
+
throw httpError(502, { error: "MLB \u672A\u8FD4\u56DE\u65B0\u7684\u4F1A\u8BDD\u8EAB\u4EFD" });
|
|
2603
|
+
}
|
|
2604
|
+
} catch (cause) {
|
|
2605
|
+
const detail = object2(cause)?.publicMessage;
|
|
2606
|
+
throw Object.assign(new Error("\u4F1A\u8BDD\u91CD\u7F6E\u5C1A\u672A\u5B8C\u6210", { cause }), {
|
|
2607
|
+
code: "RESET_INCOMPLETE",
|
|
2608
|
+
status: 502,
|
|
2609
|
+
publicMessage: `${this.#resetArchived ? "\u65E7\u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF0C\u65B0\u4F1A\u8BDD\u521B\u5EFA\u672A\u5B8C\u6210" : "\u5C1A\u672A\u786E\u8BA4\u65E7\u4F1A\u8BDD\u5DF2\u7ED3\u675F"}\uFF0C\u8BF7\u518D\u6B21\u70B9\u51FB\u300C\u6E05\u7A7A\u5BF9\u8BDD\u300D\u91CD\u8BD5\u3002${typeof detail === "string" ? `\uFF08${detail}\uFF09` : ""}`
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
this.#active = void 0;
|
|
2613
|
+
this.#latest = void 0;
|
|
2614
|
+
this.#streaming = void 0;
|
|
2615
|
+
this.#lastSend = void 0;
|
|
2616
|
+
this.#messages = Object.freeze([]);
|
|
2617
|
+
this.#resetTarget = void 0;
|
|
2618
|
+
this.#resetArchived = false;
|
|
2619
|
+
}
|
|
2620
|
+
async cancel(_runId, _options = {}) {
|
|
2621
|
+
throw Object.assign(new Error("\u5F53\u524D MLB \u534F\u8BAE\u5C1A\u672A\u63D0\u4F9B\u7EC8\u6B62\u56DE\u590D\u63A5\u53E3"), {
|
|
2622
|
+
code: "TURN_CANCEL_UNSUPPORTED",
|
|
2623
|
+
status: 501,
|
|
2624
|
+
publicMessage: "\u5F53\u524D\u7248\u672C\u6682\u4E0D\u652F\u6301\u7EC8\u6B62\u56DE\u590D"
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
async watchOutput(runId, options) {
|
|
2628
|
+
const safeRunId = safeText(runId, "runId", 256);
|
|
2629
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
2630
|
+
const url = endpoint(this.#baseUrl, "/app/ws");
|
|
2631
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2632
|
+
let sequence = 0;
|
|
2633
|
+
let assistantIds = new Set(this.#messages.filter((message2) => message2.role === "assistant").map((message2) => message2.messageId));
|
|
2634
|
+
const reset = async () => {
|
|
2635
|
+
sequence = 0;
|
|
2636
|
+
this.#streaming = void 0;
|
|
2637
|
+
await options.onEvent({ type: "reset", runId: safeRunId });
|
|
2638
|
+
};
|
|
2639
|
+
for (let reconnects = 0; !options.signal?.aborted; reconnects++) {
|
|
2640
|
+
try {
|
|
2641
|
+
for await (const frame of mlbAppEvents(url, this.#accessToken, {
|
|
2642
|
+
signal: options.signal
|
|
2643
|
+
})) {
|
|
2644
|
+
const { data } = frame;
|
|
2645
|
+
if (frame.event === "ready") {
|
|
2646
|
+
await reset();
|
|
2647
|
+
} else if (data.kind !== "private" || data.id !== privateId || data.buddyId !== this.#buddyId) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
if (frame.event === "delta" && typeof data.text === "string" && data.text.length > 0) {
|
|
2651
|
+
const text2 = `${this.#streaming?.runId === safeRunId ? this.#streaming.text : ""}${data.text}`;
|
|
2652
|
+
if (Buffer.byteLength(text2, "utf8") > 48 * 1024) throw new MlbEventStreamError("MLB \u6D41\u5F0F\u56DE\u590D\u8D85\u8FC7\u5927\u5C0F\u9650\u5236", 502, false);
|
|
2653
|
+
this.#streaming = Object.freeze({ runId: safeRunId, text: text2, nextSequence: sequence + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2654
|
+
await options.onEvent({ type: "delta", runId: safeRunId, sequence, text: data.text });
|
|
2655
|
+
sequence++;
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
if (frame.event !== "chat" && frame.event !== "ready") continue;
|
|
2659
|
+
let state = await this.state({ signal: options.signal });
|
|
2660
|
+
const nextAssistantIds = new Set(state.messages.filter((message2) => message2.role === "assistant").map((message2) => message2.messageId));
|
|
2661
|
+
const committedReply = [...nextAssistantIds].some((id) => !assistantIds.has(id));
|
|
2662
|
+
assistantIds = nextAssistantIds;
|
|
2663
|
+
if (committedReply && state.activeRun) {
|
|
2664
|
+
sequence = 0;
|
|
2665
|
+
this.#streaming = void 0;
|
|
2666
|
+
const { streaming: _streaming, ...snapshot } = state;
|
|
2667
|
+
state = snapshot;
|
|
2668
|
+
}
|
|
2669
|
+
if (state.latestRun?.runId === safeRunId && (state.latestRun.status === "completed" || state.latestRun.status === "failed")) {
|
|
2670
|
+
await options.onEvent({ type: state.latestRun.status, runId: safeRunId, status: state.latestRun.status });
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
await options.onEvent({ type: "state", runId: safeRunId, state, resetStreaming: committedReply });
|
|
2674
|
+
}
|
|
2675
|
+
if (options.signal?.aborted) return;
|
|
2676
|
+
} catch (cause) {
|
|
2677
|
+
if (options.signal?.aborted) return;
|
|
2678
|
+
await reset();
|
|
2679
|
+
const status = object2(cause)?.status;
|
|
2680
|
+
if (cause instanceof MlbEventStreamError && !cause.retryable || typeof status === "number" && status >= 400 && status < 500 && status !== 429 || reconnects >= 4) throw cause;
|
|
2681
|
+
}
|
|
2682
|
+
await waitForRetry(Math.min(1e3, (reconnects + 1) * 150), options.signal);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
async #ensurePrivate(signal) {
|
|
2686
|
+
if (this.#privateId) return this.#privateId;
|
|
2687
|
+
if (!this.#privatePromise) {
|
|
2688
|
+
const pending = this.#request(
|
|
2689
|
+
"POST",
|
|
2690
|
+
"/app/privates",
|
|
2691
|
+
{ signal, body: { buddyId: this.#buddyId } }
|
|
2692
|
+
).then((value) => {
|
|
2693
|
+
const body = object2(value);
|
|
2694
|
+
if (body?.ok !== true || typeof body.id !== "string" || !/^p_[a-zA-Z0-9_-]{1,253}$/u.test(body.id)) {
|
|
2695
|
+
throw httpError(502, { error: "MLB \u521B\u5EFA\u79C1\u804A\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u7684 privateId" });
|
|
2696
|
+
}
|
|
2697
|
+
this.#privateId = body.id;
|
|
2698
|
+
return body.id;
|
|
2699
|
+
}).finally(() => {
|
|
2700
|
+
if (this.#privatePromise === pending) this.#privatePromise = void 0;
|
|
2701
|
+
});
|
|
2702
|
+
this.#privatePromise = pending;
|
|
2703
|
+
}
|
|
2704
|
+
return this.#privatePromise;
|
|
2705
|
+
}
|
|
2706
|
+
async #request(method, path, options) {
|
|
2707
|
+
const headers = new Headers({
|
|
2708
|
+
Accept: "application/json",
|
|
2709
|
+
Authorization: `Bearer ${this.#accessToken}`,
|
|
2710
|
+
...options.headers
|
|
2711
|
+
});
|
|
2712
|
+
if (options.body !== void 0) headers.set("Content-Type", "application/json");
|
|
2713
|
+
const response = await this.#fetch(endpoint(this.#baseUrl, path), {
|
|
2714
|
+
method,
|
|
2715
|
+
headers,
|
|
2716
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
2717
|
+
signal: AbortSignal.any([AbortSignal.timeout(3e4), ...options.signal ? [options.signal] : []]),
|
|
2718
|
+
redirect: "error"
|
|
2719
|
+
});
|
|
2720
|
+
const value = await response.json().catch(() => void 0);
|
|
2721
|
+
if (!response.ok) throw httpError(response.status, value);
|
|
2722
|
+
return value;
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// apps/preview/src/bridge.ts
|
|
2727
|
+
import { hostname } from "node:os";
|
|
2728
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2729
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
2730
|
+
async function connectPreviewBridge(options) {
|
|
2731
|
+
const connectionId = randomBytes2(32).toString("base64url");
|
|
2732
|
+
const shutdown = new AbortController();
|
|
2733
|
+
const signal = AbortSignal.any([options.signal, shutdown.signal]);
|
|
2734
|
+
const running = /* @__PURE__ */ new Map();
|
|
2735
|
+
const replies = /* @__PURE__ */ new Map();
|
|
2736
|
+
const completed = /* @__PURE__ */ new Set();
|
|
2737
|
+
let lastContact = Date.now();
|
|
2738
|
+
let stopping;
|
|
2739
|
+
let interrupted = false;
|
|
2740
|
+
let disconnecting = false;
|
|
2741
|
+
async function call(path, token, payload, requestSignal = signal) {
|
|
2742
|
+
const response = await fetch(new URL(path, options.platformOrigin), {
|
|
2743
|
+
method: "POST",
|
|
2744
|
+
redirect: "error",
|
|
2745
|
+
signal: AbortSignal.any([requestSignal, AbortSignal.timeout(4e3)]),
|
|
2746
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
2747
|
+
body: JSON.stringify(payload ?? {})
|
|
2748
|
+
});
|
|
2749
|
+
const value = await response.json();
|
|
2750
|
+
if (!response.ok) throw Object.assign(new Error(value.error || "\u540E\u53F0\u8FDE\u63A5\u5931\u8D25"), { status: response.status });
|
|
2751
|
+
return value;
|
|
2752
|
+
}
|
|
2753
|
+
const attach = async () => {
|
|
2754
|
+
const result = await call("/cli/preview/connect", await options.token(), {
|
|
2755
|
+
connectionId,
|
|
2756
|
+
forceDisconnect: true,
|
|
2757
|
+
buddyId: options.buddyId,
|
|
2758
|
+
projectRoot: options.controller.projectRoot,
|
|
2759
|
+
machineName: hostname(),
|
|
2760
|
+
platformOrigin: options.platformOrigin
|
|
2761
|
+
});
|
|
2762
|
+
lastContact = Date.now();
|
|
2763
|
+
return result;
|
|
2764
|
+
};
|
|
2765
|
+
const bootstrap = await fetch(options.localServer.bootstrapUrl, { redirect: "manual", signal });
|
|
2766
|
+
const cookie = bootstrap.headers.get("set-cookie")?.split(";", 1)[0];
|
|
2767
|
+
if (bootstrap.status !== 303 || !cookie) throw new Error("\u672C\u5730\u8C03\u8BD5\u670D\u52A1\u521D\u59CB\u5316\u5931\u8D25");
|
|
2768
|
+
const initial = await attach();
|
|
2769
|
+
if (initial.reused) return { reused: true, completion: Promise.resolve(), async close() {
|
|
2770
|
+
} };
|
|
2771
|
+
const pendingEvents = /* @__PURE__ */ new Map();
|
|
2772
|
+
let eventSequence = 0;
|
|
2773
|
+
let publishing;
|
|
2774
|
+
let retryEvents;
|
|
2775
|
+
const flushEvents = () => {
|
|
2776
|
+
if (!initial.liveEvents || signal.aborted || publishing || retryEvents || !pendingEvents.size) return;
|
|
2777
|
+
publishing = (async () => {
|
|
2778
|
+
while (!signal.aborted && pendingEvents.size) {
|
|
2779
|
+
const events = [...pendingEvents.values()].sort((a, b) => a.sequence - b.sequence);
|
|
2780
|
+
try {
|
|
2781
|
+
await call("/cli/preview/events", connectionId, { events });
|
|
2782
|
+
for (const event of events) if (pendingEvents.get(event.type) === event) pendingEvents.delete(event.type);
|
|
2783
|
+
} catch {
|
|
2784
|
+
if (!signal.aborted) retryEvents = setTimeout(() => {
|
|
2785
|
+
retryEvents = void 0;
|
|
2786
|
+
flushEvents();
|
|
2787
|
+
}, 500);
|
|
2788
|
+
break;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
})().finally(() => {
|
|
2792
|
+
publishing = void 0;
|
|
2793
|
+
});
|
|
2794
|
+
};
|
|
2795
|
+
const queueEvent = (type, value) => {
|
|
2796
|
+
if (!initial.liveEvents || signal.aborted) return;
|
|
2797
|
+
pendingEvents.set(type, { sequence: eventSequence++, type, value });
|
|
2798
|
+
flushEvents();
|
|
2799
|
+
};
|
|
2800
|
+
const snapshotEvents = () => {
|
|
2801
|
+
queueEvent("dev.state", options.controller.state());
|
|
2802
|
+
queueEvent("simulator.state", { state: options.controller.simulatorSnapshot() });
|
|
2803
|
+
};
|
|
2804
|
+
const unsubscribeDev = options.controller.subscribe((state) => queueEvent("dev.state", state));
|
|
2805
|
+
const unsubscribeSimulator = options.controller.subscribeSimulator(({ state }) => queueEvent("simulator.state", { state }));
|
|
2806
|
+
snapshotEvents();
|
|
2807
|
+
const watchdog = setInterval(() => {
|
|
2808
|
+
if (Date.now() - lastContact < 1e4 || stopping || interrupted) return;
|
|
2809
|
+
interrupted = true;
|
|
2810
|
+
stopping = options.controller.stop();
|
|
2811
|
+
void stopping.catch(() => shutdown.abort());
|
|
2812
|
+
options.onStatus?.("\u540E\u53F0\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u505C\u6B62 DEV\uFF1B\u8FDE\u63A5\u6062\u590D\u540E\u53EF\u5728\u540E\u53F0\u91CD\u65B0\u542F\u52A8\u3002");
|
|
2813
|
+
}, 500);
|
|
2814
|
+
watchdog.unref();
|
|
2815
|
+
async function execute(command) {
|
|
2816
|
+
try {
|
|
2817
|
+
if (command.path === "/api/connection/disconnect" && command.method === "POST") {
|
|
2818
|
+
disconnecting = true;
|
|
2819
|
+
await options.controller.close();
|
|
2820
|
+
await options.stopLocalDev?.();
|
|
2821
|
+
replies.set(command.id, { id: command.id, status: 200, contentType: "application/json", body: Buffer.from(JSON.stringify({ localStopped: Boolean(options.stopLocalDev) })).toString("base64") });
|
|
2822
|
+
options.onStatus?.("\u540E\u53F0\u5DF2\u8BF7\u6C42\u5F3A\u5236\u65AD\u8FDE\uFF0C\u5F53\u524D\u5DE5\u7A0B DEV \u5DF2\u505C\u6B62\u3002\u9700\u8981\u8C03\u8BD5\u65F6\u91CD\u65B0\u6267\u884C\u8FDE\u63A5\u6307\u4EE4\u3002");
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
if (disconnecting) throw new Error("\u6B63\u5728\u65AD\u5F00\u8FDE\u63A5");
|
|
2826
|
+
if (!command.path.startsWith("/api/") || command.path === "/api/events" || command.path.includes("..")) throw new Error("\u8C03\u8BD5\u8DEF\u5F84\u65E0\u6548");
|
|
2827
|
+
const target = new URL(command.path, options.localServer.origin);
|
|
2828
|
+
if (target.origin !== options.localServer.origin.origin) throw new Error("\u8C03\u8BD5\u8DEF\u5F84\u65E0\u6548");
|
|
2829
|
+
if ((await options.controller.project()).buddyId !== options.buddyId) throw new Error("\u5DE5\u7A0B buddyId \u5DF2\u6539\u53D8\uFF0C\u8BF7\u9000\u51FA CLI \u540E\u91CD\u65B0\u8FDE\u63A5\u5BF9\u5E94\u642D\u5B50");
|
|
2830
|
+
signal.throwIfAborted();
|
|
2831
|
+
if (interrupted || disconnecting) throw new Error("\u672C\u5730\u8FDE\u63A5\u5DF2\u4E2D\u65AD");
|
|
2832
|
+
const response = await fetch(target, {
|
|
2833
|
+
method: command.method,
|
|
2834
|
+
redirect: "error",
|
|
2835
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(28e3)]),
|
|
2836
|
+
headers: { Cookie: cookie, ...command.body ? { "Content-Type": "application/json" } : {} },
|
|
2837
|
+
...command.body ? { body: command.body } : {}
|
|
2838
|
+
});
|
|
2839
|
+
const chunks = [];
|
|
2840
|
+
let size = 0;
|
|
2841
|
+
for await (const chunk of response.body ?? []) {
|
|
2842
|
+
size += chunk.length;
|
|
2843
|
+
if (size > 8 * 1024 * 1024) throw new Error("\u8C03\u8BD5\u54CD\u5E94\u8FC7\u5927");
|
|
2844
|
+
chunks.push(Buffer.from(chunk));
|
|
2845
|
+
}
|
|
2846
|
+
replies.set(command.id, { id: command.id, status: response.status, contentType: response.headers.get("content-type") ?? "application/json", body: Buffer.concat(chunks).toString("base64") });
|
|
2847
|
+
} catch {
|
|
2848
|
+
replies.set(command.id, { id: command.id, status: 502, contentType: "application/json", body: Buffer.from(JSON.stringify({ error: { code: "PREVIEW_LOCAL_FAILED", message: "\u672C\u5730\u8C03\u8BD5\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7EC8\u7AEF\u53CA\u5DE5\u7A0B buddyId" } })).toString("base64") });
|
|
2849
|
+
} finally {
|
|
2850
|
+
running.delete(command.id);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
const completion = (async () => {
|
|
2854
|
+
try {
|
|
2855
|
+
while (!signal.aborted) {
|
|
2856
|
+
try {
|
|
2857
|
+
if (stopping) {
|
|
2858
|
+
await stopping;
|
|
2859
|
+
stopping = void 0;
|
|
2860
|
+
}
|
|
2861
|
+
const result = await call("/cli/preview/poll", connectionId, { replies: [...replies.values()].slice(0, 1) });
|
|
2862
|
+
lastContact = Date.now();
|
|
2863
|
+
const recovered = interrupted;
|
|
2864
|
+
if (stopping) {
|
|
2865
|
+
await stopping;
|
|
2866
|
+
stopping = void 0;
|
|
2867
|
+
}
|
|
2868
|
+
if (recovered) {
|
|
2869
|
+
interrupted = false;
|
|
2870
|
+
options.onStatus?.("\u5DF2\u91CD\u65B0\u8FDE\u63A5\u540E\u53F0\uFF0C\u8BF7\u5728\u5F00\u53D1\u8C03\u8BD5\u9875\u542F\u52A8 DEV\u3002");
|
|
2871
|
+
}
|
|
2872
|
+
for (const id of result.acknowledged ?? []) {
|
|
2873
|
+
replies.delete(id);
|
|
2874
|
+
completed.add(id);
|
|
2875
|
+
}
|
|
2876
|
+
while (completed.size > 256) completed.delete(completed.values().next().value);
|
|
2877
|
+
for (const command of result.commands ?? []) {
|
|
2878
|
+
if (recovered && !running.has(command.id) && !replies.has(command.id) && !completed.has(command.id)) {
|
|
2879
|
+
replies.set(command.id, { id: command.id, status: 409, contentType: "application/json", body: Buffer.from(JSON.stringify({ error: { code: "PREVIEW_RECONNECTED", message: "\u672C\u5730\u8FDE\u63A5\u5DF2\u6062\u590D\uFF0C\u8BF7\u91CD\u65B0\u64CD\u4F5C" } })).toString("base64") });
|
|
2880
|
+
}
|
|
2881
|
+
if (!running.has(command.id) && !replies.has(command.id) && !completed.has(command.id) && running.size < 32) running.set(command.id, execute(command));
|
|
2882
|
+
}
|
|
2883
|
+
} catch (cause) {
|
|
2884
|
+
if (signal.aborted) break;
|
|
2885
|
+
const status = cause.status;
|
|
2886
|
+
if (status === 410) {
|
|
2887
|
+
await options.controller.stop();
|
|
2888
|
+
options.onStatus?.("\u540E\u53F0\u8FDE\u63A5\u5DF2\u7ED3\u675F\uFF1B\u8BF7\u91CD\u65B0\u6267\u884C\u8FDE\u63A5\u6307\u4EE4\u3002\u672A\u81EA\u52A8\u91CD\u65B0\u8FDE\u63A5\u6216\u542F\u52A8 DEV\u3002");
|
|
2889
|
+
break;
|
|
2890
|
+
} else if (status && status >= 400 && status < 500 && status !== 429) throw cause;
|
|
2891
|
+
}
|
|
2892
|
+
await delay2(replies.size ? 20 : 500, void 0, { signal }).catch(() => void 0);
|
|
2893
|
+
}
|
|
2894
|
+
} finally {
|
|
2895
|
+
clearInterval(watchdog);
|
|
2896
|
+
unsubscribeDev();
|
|
2897
|
+
unsubscribeSimulator();
|
|
2898
|
+
if (retryEvents) clearTimeout(retryEvents);
|
|
2899
|
+
shutdown.abort();
|
|
2900
|
+
await publishing;
|
|
2901
|
+
}
|
|
2902
|
+
})();
|
|
2903
|
+
void completion.catch(() => void 0);
|
|
2904
|
+
return {
|
|
2905
|
+
reused: false,
|
|
2906
|
+
completion,
|
|
2907
|
+
async close() {
|
|
2908
|
+
shutdown.abort();
|
|
2909
|
+
await completion.catch(() => void 0);
|
|
2910
|
+
await Promise.allSettled(running.values());
|
|
2911
|
+
await stopping;
|
|
2912
|
+
await options.controller.stop();
|
|
2913
|
+
await call("/cli/preview/disconnect", connectionId, {}, AbortSignal.timeout(2e3)).catch(() => void 0);
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
export {
|
|
2918
|
+
MlbSimulatorClient,
|
|
2919
|
+
PreviewController,
|
|
2920
|
+
connectPreviewBridge,
|
|
2921
|
+
createBuddyPreviewUrl,
|
|
2922
|
+
startPreviewControlServer
|
|
2923
|
+
};
|
|
2924
|
+
//# sourceMappingURL=preview.js.map
|