@my-life-buddies/cli 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/dist/bin/buddy.js +443 -0
- package/dist/bin/buddy.js.map +7 -0
- package/dist/bin/core.js +5638 -0
- package/dist/bin/core.js.map +7 -0
- package/dist/bin/preview.js +1429 -0
- package/dist/bin/preview.js.map +7 -0
- package/dist/web/app.css +508 -0
- package/dist/web/app.js +724 -0
- package/dist/web/assets/developer-platform-icon.png.base64 +1 -0
- package/dist/web/brand.css +100 -0
- package/dist/web/index.html +236 -0
- package/dist/web/tokens.css +15 -0
- package/package.json +45 -0
- package/resources/README.md +52 -0
- package/resources/agent-template/package-lock.json +1726 -0
- package/resources/agent-template/package.json +16 -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
- package/resources/buddy-creator/INSTALL.md +25 -0
- package/resources/buddy-creator/PATCHES.md +33 -0
- package/resources/buddy-creator/SKILL.md +66 -0
- package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +154 -0
- package/resources/buddy-creator/agents/openai.yaml +4 -0
- package/resources/buddy-creator/assets/licenses/bail.txt +26 -0
- package/resources/buddy-creator/assets/licenses/ccount.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +26 -0
- package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +26 -0
- package/resources/buddy-creator/assets/licenses/debug.txt +24 -0
- package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +26 -0
- package/resources/buddy-creator/assets/licenses/dequal.txt +25 -0
- package/resources/buddy-creator/assets/licenses/devlop.txt +26 -0
- package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +13 -0
- package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +26 -0
- package/resources/buddy-creator/assets/licenses/extend.txt +27 -0
- package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +26 -0
- package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +25 -0
- package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +13 -0
- package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-decimal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +13 -0
- package/resources/buddy-creator/assets/licenses/longest-streak.txt +26 -0
- package/resources/buddy-creator/assets/licenses/markdown-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +27 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark.txt +26 -0
- package/resources/buddy-creator/assets/licenses/ms.txt +25 -0
- package/resources/buddy-creator/assets/licenses/parse-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +25 -0
- package/resources/buddy-creator/assets/licenses/property-information.txt +26 -0
- package/resources/buddy-creator/assets/licenses/react-dom.txt +25 -0
- package/resources/buddy-creator/assets/licenses/react-markdown.txt +25 -0
- package/resources/buddy-creator/assets/licenses/react.txt +25 -0
- package/resources/buddy-creator/assets/licenses/remark-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/remark-parse.txt +25 -0
- package/resources/buddy-creator/assets/licenses/remark-rehype.txt +26 -0
- package/resources/buddy-creator/assets/licenses/remark-stringify.txt +25 -0
- package/resources/buddy-creator/assets/licenses/scheduler.txt +25 -0
- package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +26 -0
- package/resources/buddy-creator/assets/licenses/stringify-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/style-to-js.txt +26 -0
- package/resources/buddy-creator/assets/licenses/style-to-object.txt +26 -0
- package/resources/buddy-creator/assets/licenses/trim-lines.txt +26 -0
- package/resources/buddy-creator/assets/licenses/trough.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--debug.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--estree.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--hast.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--mdast.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--ms.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--unist.txt +25 -0
- package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +19 -0
- package/resources/buddy-creator/assets/licenses/unified.txt +25 -0
- package/resources/buddy-creator/assets/licenses/unist-util-is.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-position.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +26 -0
- package/resources/buddy-creator/assets/licenses/vfile-message.txt +26 -0
- package/resources/buddy-creator/assets/licenses/vfile.txt +25 -0
- package/resources/buddy-creator/assets/licenses/zwitch.txt +26 -0
- package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +39 -0
- package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +4 -0
- package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
- package/resources/buddy-creator/assets/preview/brand.css +100 -0
- package/resources/buddy-creator/assets/preview/index.html +5 -0
- package/resources/buddy-creator/assets/preview/reader.css +165 -0
- package/resources/buddy-creator/assets/preview/reader.js +135 -0
- package/resources/buddy-creator/references/artifact-schema.md +68 -0
- package/resources/buddy-creator/references/catalog.json +401 -0
- package/resources/buddy-creator/references/definition.md +24 -0
- package/resources/buddy-creator/references/dialogue-examples.md +133 -0
- package/resources/buddy-creator/references/host-guide.md +213 -0
- package/resources/buddy-creator/references/interview.md +235 -0
- package/resources/buddy-creator/references/knowledge.md +40 -0
- package/resources/buddy-creator/references/methods.md +77 -0
- package/resources/buddy-creator/references/opening.md +14 -0
- package/resources/buddy-creator/references/preview-panel.md +66 -0
- package/resources/buddy-creator/references/recovery.md +43 -0
- package/resources/buddy-creator/references/service.md +74 -0
- package/resources/buddy-creator/scripts/buddy.py +108 -0
- package/resources/buddy-creator/scripts/buddy_client.py +67 -0
- package/resources/buddy-creator/scripts/buddy_core.py +803 -0
- package/resources/buddy-creator/scripts/completion.py +240 -0
- package/resources/buddy-creator/scripts/interview.py +215 -0
- package/resources/buddy-creator/scripts/preview.py +577 -0
- package/resources/buddy-creator/scripts/preview_panel.py +80 -0
- package/resources/buddy-creator/scripts/sources.py +239 -0
- package/resources/buddy-creator/version.json +8 -0
- package/resources/buddy-creator.manifest.json +696 -0
|
@@ -0,0 +1,1429 @@
|
|
|
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/controller.ts
|
|
13
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
14
|
+
function publicFailure(cause) {
|
|
15
|
+
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;
|
|
16
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" && cause.code.trim()) {
|
|
17
|
+
return { code: cause.code, message: publicMessage ?? "DEV \u4F1A\u8BDD\u672A\u80FD\u5B8C\u6210" };
|
|
18
|
+
}
|
|
19
|
+
return { code: "DEV_START_FAILED", message: "DEV \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25" };
|
|
20
|
+
}
|
|
21
|
+
var PreviewController = class {
|
|
22
|
+
#projectRoot;
|
|
23
|
+
#devStarter;
|
|
24
|
+
#readConfig;
|
|
25
|
+
#readBuddyDetails;
|
|
26
|
+
#now;
|
|
27
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
28
|
+
#simulatorListeners = /* @__PURE__ */ new Set();
|
|
29
|
+
#runs = /* @__PURE__ */ new Map();
|
|
30
|
+
#outputWatchers = /* @__PURE__ */ new Map();
|
|
31
|
+
#failedOutputRuns = /* @__PURE__ */ new Set();
|
|
32
|
+
#observedActiveRuns = /* @__PURE__ */ new Set();
|
|
33
|
+
#events = [];
|
|
34
|
+
#runOrdinal = 0;
|
|
35
|
+
#state = Object.freeze({ phase: "idle", revision: 0 });
|
|
36
|
+
#handle;
|
|
37
|
+
#simulator;
|
|
38
|
+
#startController;
|
|
39
|
+
#startPromise;
|
|
40
|
+
#messages = [];
|
|
41
|
+
#simulatorState = Object.freeze({ messages: Object.freeze([]) });
|
|
42
|
+
#lastActiveRunId;
|
|
43
|
+
#eventSequence = 0;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.#projectRoot = options.projectRoot;
|
|
46
|
+
this.#devStarter = options.devStarter;
|
|
47
|
+
this.#readConfig = options.readConfig ?? ((root) => readBuddyConfig(root));
|
|
48
|
+
this.#readBuddyDetails = options.readBuddyDetails;
|
|
49
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
50
|
+
}
|
|
51
|
+
get projectRoot() {
|
|
52
|
+
return this.#projectRoot;
|
|
53
|
+
}
|
|
54
|
+
state() {
|
|
55
|
+
return this.#state;
|
|
56
|
+
}
|
|
57
|
+
async project() {
|
|
58
|
+
return this.#readConfig(this.#projectRoot);
|
|
59
|
+
}
|
|
60
|
+
async buddyDetails() {
|
|
61
|
+
if (!this.#readBuddyDetails) return this.#state.ready?.buddy ?? null;
|
|
62
|
+
const config = await this.project();
|
|
63
|
+
return this.#readBuddyDetails(config.buddyId);
|
|
64
|
+
}
|
|
65
|
+
subscribe(listener) {
|
|
66
|
+
this.#listeners.add(listener);
|
|
67
|
+
return () => this.#listeners.delete(listener);
|
|
68
|
+
}
|
|
69
|
+
subscribeSimulator(listener) {
|
|
70
|
+
this.#simulatorListeners.add(listener);
|
|
71
|
+
return () => this.#simulatorListeners.delete(listener);
|
|
72
|
+
}
|
|
73
|
+
simulatorSnapshot() {
|
|
74
|
+
return this.#simulatorState;
|
|
75
|
+
}
|
|
76
|
+
start() {
|
|
77
|
+
if (this.#startPromise) return this.#startPromise;
|
|
78
|
+
if (this.#state.phase === "ready") return Promise.resolve(this.#state);
|
|
79
|
+
if (this.#state.phase === "stopping") {
|
|
80
|
+
return Promise.reject(Object.assign(new Error("DEV \u4F1A\u8BDD\u6B63\u5728\u505C\u6B62"), { code: "DEV_STOPPING" }));
|
|
81
|
+
}
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
this.#startController = controller;
|
|
84
|
+
this.#record({ channel: "dev", type: "dev.start_requested", summary: "\u8BF7\u6C42\u542F\u52A8 DEV \u4F1A\u8BDD" });
|
|
85
|
+
this.#setState({ phase: "starting" });
|
|
86
|
+
const startPromise = this.#devStarter.start({
|
|
87
|
+
projectRoot: this.#projectRoot,
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
onLog: ({ source, text: text2 }) => {
|
|
90
|
+
if (controller.signal.aborted) return;
|
|
91
|
+
this.#record({ channel: "dev", type: source, summary: text2.slice(0, 8224) });
|
|
92
|
+
this.#notifySimulator();
|
|
93
|
+
}
|
|
94
|
+
}).then(({ handle, ready, simulator }) => {
|
|
95
|
+
if (controller.signal.aborted) {
|
|
96
|
+
return handle.close().then(() => this.#setState({ phase: "idle" }));
|
|
97
|
+
}
|
|
98
|
+
this.#handle = handle;
|
|
99
|
+
this.#simulator = simulator;
|
|
100
|
+
this.#record({
|
|
101
|
+
channel: "dev",
|
|
102
|
+
type: "dev.ready",
|
|
103
|
+
summary: ready.agentId ? `Agent ${ready.agentId} \u5DF2\u5C31\u7EEA` : "Agent \u5DF2\u5C31\u7EEA"
|
|
104
|
+
});
|
|
105
|
+
void handle.closed.then(
|
|
106
|
+
() => {
|
|
107
|
+
if (this.#handle === handle) {
|
|
108
|
+
this.#handle = void 0;
|
|
109
|
+
this.#simulator = void 0;
|
|
110
|
+
this.#stopOutputWatchers();
|
|
111
|
+
this.#record({ channel: "dev", type: "dev.closed", summary: "DEV \u4F1A\u8BDD\u5DF2\u7ED3\u675F" });
|
|
112
|
+
this.#setState({ phase: "idle" });
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
(cause) => {
|
|
116
|
+
if (this.#handle === handle) {
|
|
117
|
+
this.#handle = void 0;
|
|
118
|
+
this.#simulator = void 0;
|
|
119
|
+
this.#stopOutputWatchers();
|
|
120
|
+
this.#record({ channel: "dev", type: "dev.failed", summary: "DEV \u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F" });
|
|
121
|
+
this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
return this.#setState({ phase: "ready", ready });
|
|
126
|
+
}).catch((cause) => {
|
|
127
|
+
if (controller.signal.aborted) return this.#setState({ phase: "idle" });
|
|
128
|
+
this.#record({ channel: "dev", type: "dev.start_failed", summary: "DEV \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25" });
|
|
129
|
+
return this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
130
|
+
}).finally(() => {
|
|
131
|
+
if (this.#startController === controller) this.#startController = void 0;
|
|
132
|
+
if (this.#startPromise === startPromise) this.#startPromise = void 0;
|
|
133
|
+
});
|
|
134
|
+
this.#startPromise = startPromise;
|
|
135
|
+
return startPromise;
|
|
136
|
+
}
|
|
137
|
+
async stop() {
|
|
138
|
+
if (this.#state.phase === "idle") return this.#state;
|
|
139
|
+
if (this.#state.phase === "stopping") return this.#state;
|
|
140
|
+
this.#setState({ phase: "stopping" });
|
|
141
|
+
this.#record({ channel: "dev", type: "dev.stop_requested", summary: "\u8BF7\u6C42\u505C\u6B62 DEV \u4F1A\u8BDD" });
|
|
142
|
+
this.#startController?.abort(new Error("Preview requested DEV stop"));
|
|
143
|
+
const handle = this.#handle;
|
|
144
|
+
this.#handle = void 0;
|
|
145
|
+
this.#simulator = void 0;
|
|
146
|
+
this.#stopOutputWatchers();
|
|
147
|
+
try {
|
|
148
|
+
if (handle) await handle.close();
|
|
149
|
+
if (this.#startPromise) await this.#startPromise.catch(() => void 0);
|
|
150
|
+
this.#record({ channel: "dev", type: "dev.stopped", summary: "DEV \u4F1A\u8BDD\u5DF2\u505C\u6B62" });
|
|
151
|
+
return this.#setState({ phase: "idle" });
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
this.#setState({ phase: "failed", error: publicFailure(cause) });
|
|
154
|
+
throw cause;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async close() {
|
|
158
|
+
await this.stop();
|
|
159
|
+
this.#listeners.clear();
|
|
160
|
+
this.#simulatorListeners.clear();
|
|
161
|
+
}
|
|
162
|
+
async simulatorState(signal) {
|
|
163
|
+
const state = await this.#requiredSimulator().state({ signal });
|
|
164
|
+
return this.#observeSimulatorState(state);
|
|
165
|
+
}
|
|
166
|
+
async sendSimulatorMessage(input, signal) {
|
|
167
|
+
const started = this.#now();
|
|
168
|
+
this.#record({ channel: "client", type: "message.send", summary: "\u53D1\u9001\u4E00\u6761\u79C1\u804A\u6D88\u606F" });
|
|
169
|
+
const accepted = await this.#requiredSimulator().send(input, { signal });
|
|
170
|
+
if (this.#runs.has(accepted.runId)) return accepted;
|
|
171
|
+
const acceptedAt = this.#now();
|
|
172
|
+
const latency = Math.max(0, acceptedAt.getTime() - started.getTime());
|
|
173
|
+
this.#upsertRun({
|
|
174
|
+
runId: accepted.runId,
|
|
175
|
+
messageId: accepted.messageId,
|
|
176
|
+
status: "accepted",
|
|
177
|
+
startedAt: started.toISOString(),
|
|
178
|
+
updatedAt: acceptedAt.toISOString(),
|
|
179
|
+
acceptLatencyMs: latency
|
|
180
|
+
});
|
|
181
|
+
this.#lastActiveRunId = accepted.runId;
|
|
182
|
+
this.#record({
|
|
183
|
+
channel: "app_server",
|
|
184
|
+
type: "run.accepted",
|
|
185
|
+
summary: "App Server \u5DF2\u63A5\u53D7\u6D88\u606F\uFF0C\u5DF2\u8BB0\u5F55\u53D1\u9001\u56DE\u6267",
|
|
186
|
+
runId: accepted.runId,
|
|
187
|
+
durationMs: latency
|
|
188
|
+
});
|
|
189
|
+
this.#simulatorState = Object.freeze({
|
|
190
|
+
messages: Object.freeze([
|
|
191
|
+
...this.#simulatorState.messages.filter((message2) => message2.messageId !== accepted.messageId),
|
|
192
|
+
Object.freeze({
|
|
193
|
+
messageId: accepted.messageId,
|
|
194
|
+
role: "user",
|
|
195
|
+
text: input.text,
|
|
196
|
+
createdAt: acceptedAt.toISOString()
|
|
197
|
+
})
|
|
198
|
+
]),
|
|
199
|
+
latestRun: Object.freeze({ runId: accepted.runId, status: "pending" }),
|
|
200
|
+
activeRun: Object.freeze({ runId: accepted.runId, status: "pending" })
|
|
201
|
+
});
|
|
202
|
+
this.#messages = this.#simulatorState.messages;
|
|
203
|
+
this.#notifySimulator();
|
|
204
|
+
this.#startOutputWatcher(this.#requiredSimulator(), accepted.runId);
|
|
205
|
+
return accepted;
|
|
206
|
+
}
|
|
207
|
+
async cancelSimulatorRun(runId, signal) {
|
|
208
|
+
this.#record({
|
|
209
|
+
channel: "client",
|
|
210
|
+
type: "run.cancel_requested",
|
|
211
|
+
// Local debug event, not an MLB SSE event.
|
|
212
|
+
summary: "\u5F00\u53D1\u8005\u8BF7\u6C42\u505C\u6B62\u5F53\u524D\u56DE\u590D",
|
|
213
|
+
runId
|
|
214
|
+
});
|
|
215
|
+
const result = await this.#requiredSimulator().cancel(runId, { signal });
|
|
216
|
+
if (result.status === "cancel_requested") {
|
|
217
|
+
this.#updateRunStatus(runId, "cancel_requested");
|
|
218
|
+
if (this.#simulatorState.activeRun?.runId === runId) {
|
|
219
|
+
this.#simulatorState = Object.freeze({
|
|
220
|
+
messages: this.#simulatorState.messages,
|
|
221
|
+
latestRun: Object.freeze({ runId, status: "cancel_requested" }),
|
|
222
|
+
activeRun: Object.freeze({ runId, status: "cancel_requested" }),
|
|
223
|
+
...this.#simulatorState.streaming?.runId === runId ? { streaming: this.#simulatorState.streaming } : {}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.#record({
|
|
228
|
+
channel: "app_server",
|
|
229
|
+
type: `run.${result.status}`,
|
|
230
|
+
summary: result.status === "cancel_requested" ? "App Server \u5DF2\u63A5\u53D7\u505C\u6B62\u8BF7\u6C42" : "Run \u5DF2\u7ECF\u7ED3\u675F",
|
|
231
|
+
runId
|
|
232
|
+
});
|
|
233
|
+
if (result.status === "already_terminal") {
|
|
234
|
+
await this.simulatorState(signal).catch(() => this.#notifySimulator());
|
|
235
|
+
} else {
|
|
236
|
+
this.#notifySimulator();
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
debugSnapshot() {
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
source: "observed",
|
|
243
|
+
generatedAt: this.#now().toISOString(),
|
|
244
|
+
runs: Object.freeze([...this.#runs.values()].reverse()),
|
|
245
|
+
events: Object.freeze([...this.#events].reverse()),
|
|
246
|
+
messages: Object.freeze([...this.#messages])
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
devicePairing() {
|
|
250
|
+
const pairing = this.#state.phase === "ready" ? this.#state.ready?.devicePairing : void 0;
|
|
251
|
+
if (pairing) {
|
|
252
|
+
try {
|
|
253
|
+
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)))) {
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
status: "ready",
|
|
256
|
+
url: pairing.url,
|
|
257
|
+
...pairing.code === void 0 ? {} : { code: pairing.code },
|
|
258
|
+
...pairing.expiresAt === void 0 ? {} : { expiresAt: pairing.expiresAt }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
status: "unavailable",
|
|
266
|
+
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"
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
#requiredSimulator() {
|
|
270
|
+
if (this.#state.phase !== "ready" || !this.#simulator) {
|
|
271
|
+
throw Object.assign(new Error("\u6A21\u62DF\u5668\u9700\u8981\u5148\u542F\u52A8 DEV \u4F1A\u8BDD"), {
|
|
272
|
+
code: "SIMULATOR_NOT_READY",
|
|
273
|
+
status: 409,
|
|
274
|
+
publicMessage: "\u8BF7\u5148\u542F\u52A8 DEV \u4F1A\u8BDD"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return this.#simulator;
|
|
278
|
+
}
|
|
279
|
+
#observeSimulatorState(state) {
|
|
280
|
+
const previousStreaming = this.#simulatorState.streaming;
|
|
281
|
+
const incomingActiveRun = state.activeRun;
|
|
282
|
+
const existingActiveRun = incomingActiveRun ? this.#runs.get(incomingActiveRun.runId) : void 0;
|
|
283
|
+
if (existingActiveRun && TERMINAL_RUN_STATUSES.has(existingActiveRun.status)) {
|
|
284
|
+
this.#notifySimulator();
|
|
285
|
+
return this.#simulatorState;
|
|
286
|
+
}
|
|
287
|
+
let streaming = state.streaming ?? (incomingActiveRun?.runId === previousStreaming?.runId ? previousStreaming : void 0);
|
|
288
|
+
if (streaming && previousStreaming?.runId === streaming.runId) {
|
|
289
|
+
if (streaming.nextSequence < previousStreaming.nextSequence) {
|
|
290
|
+
streaming = previousStreaming;
|
|
291
|
+
} else if (streaming.nextSequence === previousStreaming.nextSequence && streaming.text !== previousStreaming.text) {
|
|
292
|
+
throw Object.assign(new Error("App Server \u6539\u5199\u4E86\u5DF2\u89C2\u5BDF\u5230\u7684\u6D41\u5F0F\u6587\u5B57"), {
|
|
293
|
+
code: "OUTPUT_STREAM_PROTOCOL_ERROR",
|
|
294
|
+
publicMessage: "App Server \u8FD4\u56DE\u4E86\u4E0D\u4E00\u81F4\u7684\u6D41\u5F0F\u72B6\u6001"
|
|
295
|
+
});
|
|
296
|
+
} else if (streaming.nextSequence > previousStreaming.nextSequence && !streaming.text.startsWith(previousStreaming.text)) {
|
|
297
|
+
throw Object.assign(new Error("App Server \u6539\u5199\u4E86\u5DF2\u89C2\u5BDF\u5230\u7684\u6D41\u5F0F\u6587\u5B57"), {
|
|
298
|
+
code: "OUTPUT_STREAM_PROTOCOL_ERROR",
|
|
299
|
+
publicMessage: "App Server \u8FD4\u56DE\u4E86\u4E0D\u4E00\u81F4\u7684\u6D41\u5F0F\u72B6\u6001"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.#simulatorState = Object.freeze({
|
|
304
|
+
messages: Object.freeze([...state.messages]),
|
|
305
|
+
...state.latestRun === void 0 ? {} : { latestRun: Object.freeze({ ...state.latestRun }) },
|
|
306
|
+
...state.activeRun === void 0 ? {} : { activeRun: Object.freeze({ ...state.activeRun }) },
|
|
307
|
+
...streaming === void 0 ? {} : { streaming: Object.freeze({ ...streaming }) }
|
|
308
|
+
});
|
|
309
|
+
this.#messages = this.#simulatorState.messages;
|
|
310
|
+
const now = this.#now();
|
|
311
|
+
const active = this.#simulatorState.activeRun;
|
|
312
|
+
if (active) {
|
|
313
|
+
this.#observedActiveRuns.add(active.runId);
|
|
314
|
+
const existing2 = this.#runs.get(active.runId);
|
|
315
|
+
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);
|
|
316
|
+
this.#upsertRun({
|
|
317
|
+
runId: active.runId,
|
|
318
|
+
...existing2?.messageId ? { messageId: existing2.messageId } : {},
|
|
319
|
+
status: active.status,
|
|
320
|
+
startedAt: existing2?.startedAt ?? now.toISOString(),
|
|
321
|
+
updatedAt: now.toISOString(),
|
|
322
|
+
...existing2?.acceptLatencyMs === void 0 ? {} : { acceptLatencyMs: existing2.acceptLatencyMs },
|
|
323
|
+
...restoredFirstTextLatency === void 0 ? {} : { firstTextLatencyMs: restoredFirstTextLatency }
|
|
324
|
+
});
|
|
325
|
+
if (existing2?.firstTextLatencyMs === void 0 && restoredFirstTextLatency !== void 0) {
|
|
326
|
+
this.#record({
|
|
327
|
+
channel: "app_server",
|
|
328
|
+
type: "run.output.restored",
|
|
329
|
+
summary: "\u4ECE App Server \u72B6\u6001\u6062\u590D\u5F53\u524D\u6D41\u5F0F\u6587\u5B57",
|
|
330
|
+
runId: active.runId,
|
|
331
|
+
durationMs: restoredFirstTextLatency
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (this.#lastActiveRunId !== active.runId || existing2?.status !== active.status) {
|
|
335
|
+
this.#record({
|
|
336
|
+
channel: "app_server",
|
|
337
|
+
type: `run.${active.status}`,
|
|
338
|
+
summary: active.status === "pending" ? "Run \u6B63\u5728\u7B49\u5F85 Agent" : "Agent \u6B63\u5728\u5904\u7406 Run",
|
|
339
|
+
runId: active.runId
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
this.#lastActiveRunId = active.runId;
|
|
343
|
+
if (this.#simulator) this.#startOutputWatcher(this.#simulator, active.runId);
|
|
344
|
+
this.#notifySimulator();
|
|
345
|
+
return this.#simulatorState;
|
|
346
|
+
}
|
|
347
|
+
if (!this.#lastActiveRunId) {
|
|
348
|
+
this.#notifySimulator();
|
|
349
|
+
return this.#simulatorState;
|
|
350
|
+
}
|
|
351
|
+
const runId = this.#lastActiveRunId;
|
|
352
|
+
const existing = this.#runs.get(runId);
|
|
353
|
+
if (existing && !["completed", "failed", "cancelled"].includes(existing.status)) {
|
|
354
|
+
const assistant = [...this.#simulatorState.messages].reverse().find((message2) => message2.role === "assistant" && Date.parse(message2.createdAt) >= Date.parse(existing.startedAt));
|
|
355
|
+
const elapsed = Math.max(0, now.getTime() - Date.parse(existing.startedAt));
|
|
356
|
+
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;
|
|
357
|
+
if (!assistant && !terminalStatus && !this.#observedActiveRuns.has(runId) && elapsed < 5e3) {
|
|
358
|
+
this.#notifySimulator();
|
|
359
|
+
return this.#simulatorState;
|
|
360
|
+
}
|
|
361
|
+
const completedAt = assistant && Number.isFinite(Date.parse(assistant.createdAt)) ? new Date(assistant.createdAt) : now;
|
|
362
|
+
const status = terminalStatus ?? "completed";
|
|
363
|
+
this.#upsertRun({
|
|
364
|
+
...existing,
|
|
365
|
+
status,
|
|
366
|
+
updatedAt: now.toISOString(),
|
|
367
|
+
completedAt: completedAt.toISOString(),
|
|
368
|
+
responseTimeMs: Math.max(0, completedAt.getTime() - Date.parse(existing.startedAt)),
|
|
369
|
+
completion: assistant ? "reply" : "no_reply"
|
|
370
|
+
});
|
|
371
|
+
this.#record({
|
|
372
|
+
channel: "app_server",
|
|
373
|
+
type: `run.${status}`,
|
|
374
|
+
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",
|
|
375
|
+
runId,
|
|
376
|
+
durationMs: Math.max(0, completedAt.getTime() - Date.parse(existing.startedAt))
|
|
377
|
+
});
|
|
378
|
+
this.#stopOutputWatcher(runId);
|
|
379
|
+
}
|
|
380
|
+
this.#observedActiveRuns.delete(runId);
|
|
381
|
+
this.#lastActiveRunId = void 0;
|
|
382
|
+
this.#notifySimulator();
|
|
383
|
+
return this.#simulatorState;
|
|
384
|
+
}
|
|
385
|
+
#startOutputWatcher(simulator, runId) {
|
|
386
|
+
if (!simulator.watchOutput || this.#outputWatchers.has(runId) || this.#failedOutputRuns.has(runId)) return;
|
|
387
|
+
const controller = new AbortController();
|
|
388
|
+
this.#outputWatchers.set(runId, controller);
|
|
389
|
+
void simulator.watchOutput(runId, {
|
|
390
|
+
signal: controller.signal,
|
|
391
|
+
onEvent: (event) => this.#observeOutputEvent(event)
|
|
392
|
+
}).catch((cause) => {
|
|
393
|
+
if (controller.signal.aborted) return;
|
|
394
|
+
this.#failedOutputRuns.add(runId);
|
|
395
|
+
this.#record({
|
|
396
|
+
channel: "app_server",
|
|
397
|
+
type: "run.output.disconnected",
|
|
398
|
+
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",
|
|
399
|
+
runId
|
|
400
|
+
});
|
|
401
|
+
this.#notifySimulator();
|
|
402
|
+
void this.simulatorState().catch(() => void 0);
|
|
403
|
+
}).finally(() => {
|
|
404
|
+
if (this.#outputWatchers.get(runId) === controller) this.#outputWatchers.delete(runId);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async #observeOutputEvent(event) {
|
|
408
|
+
if (event.type === "reset") {
|
|
409
|
+
if (this.#simulatorState.streaming?.runId === event.runId) {
|
|
410
|
+
const { streaming: _streaming, ...state } = this.#simulatorState;
|
|
411
|
+
this.#simulatorState = Object.freeze(state);
|
|
412
|
+
this.#notifySimulator();
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const now = this.#now();
|
|
417
|
+
const existing = this.#runs.get(event.runId);
|
|
418
|
+
if (event.type === "delta") {
|
|
419
|
+
if (existing && TERMINAL_RUN_STATUSES.has(existing.status) || this.#simulatorState.latestRun?.runId === event.runId && TERMINAL_RUN_STATUSES.has(this.#simulatorState.latestRun.status)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const previous = this.#simulatorState.streaming;
|
|
423
|
+
const expected = previous?.runId === event.runId ? previous.nextSequence : 0;
|
|
424
|
+
if (event.sequence < expected) return;
|
|
425
|
+
if (event.sequence > expected) {
|
|
426
|
+
throw Object.assign(new Error("App Server \u8FD4\u56DE\u7684\u6D41\u5F0F\u5E8F\u53F7\u4E0D\u8FDE\u7EED"), { code: "OUTPUT_SEQUENCE_MISMATCH" });
|
|
427
|
+
}
|
|
428
|
+
const text2 = `${previous?.runId === event.runId ? previous.text : ""}${event.text}`;
|
|
429
|
+
const firstTextLatencyMs = existing?.firstTextLatencyMs ?? (existing ? Math.max(0, now.getTime() - Date.parse(existing.startedAt)) : void 0);
|
|
430
|
+
if (existing) {
|
|
431
|
+
this.#upsertRun({
|
|
432
|
+
...existing,
|
|
433
|
+
status: "running",
|
|
434
|
+
updatedAt: now.toISOString(),
|
|
435
|
+
...firstTextLatencyMs === void 0 ? {} : { firstTextLatencyMs }
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
this.#simulatorState = Object.freeze({
|
|
439
|
+
messages: this.#simulatorState.messages,
|
|
440
|
+
latestRun: Object.freeze({ runId: event.runId, status: "running" }),
|
|
441
|
+
activeRun: Object.freeze({ runId: event.runId, status: "running" }),
|
|
442
|
+
streaming: Object.freeze({
|
|
443
|
+
runId: event.runId,
|
|
444
|
+
text: text2,
|
|
445
|
+
nextSequence: event.sequence + 1,
|
|
446
|
+
updatedAt: now.toISOString()
|
|
447
|
+
})
|
|
448
|
+
});
|
|
449
|
+
this.#record({
|
|
450
|
+
channel: "app_server",
|
|
451
|
+
type: event.sequence === 0 ? "run.output.started" : "run.output.delta",
|
|
452
|
+
summary: event.sequence === 0 ? "\u9996\u6BB5\u6D41\u5F0F\u6587\u5B57\u5DF2\u5230\u8FBE" : `\u6536\u5230\u7B2C ${event.sequence + 1} \u6BB5\u6D41\u5F0F\u6587\u5B57`,
|
|
453
|
+
runId: event.runId,
|
|
454
|
+
...firstTextLatencyMs === void 0 || event.sequence !== 0 ? {} : { durationMs: firstTextLatencyMs }
|
|
455
|
+
});
|
|
456
|
+
this.#notifySimulator();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.#record({
|
|
460
|
+
channel: "app_server",
|
|
461
|
+
type: `run.output.${event.type}`,
|
|
462
|
+
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",
|
|
463
|
+
runId: event.runId
|
|
464
|
+
});
|
|
465
|
+
if (event.type !== "completed") {
|
|
466
|
+
this.#simulatorState = Object.freeze({
|
|
467
|
+
messages: this.#simulatorState.messages,
|
|
468
|
+
latestRun: Object.freeze({ runId: event.runId, status: event.status })
|
|
469
|
+
});
|
|
470
|
+
this.#notifySimulator();
|
|
471
|
+
}
|
|
472
|
+
await this.simulatorState().catch(() => void 0);
|
|
473
|
+
}
|
|
474
|
+
#stopOutputWatchers() {
|
|
475
|
+
for (const controller of this.#outputWatchers.values()) {
|
|
476
|
+
controller.abort(new Error("Preview DEV session closed"));
|
|
477
|
+
}
|
|
478
|
+
this.#outputWatchers.clear();
|
|
479
|
+
this.#failedOutputRuns.clear();
|
|
480
|
+
}
|
|
481
|
+
#stopOutputWatcher(runId) {
|
|
482
|
+
this.#failedOutputRuns.delete(runId);
|
|
483
|
+
const controller = this.#outputWatchers.get(runId);
|
|
484
|
+
if (!controller) return;
|
|
485
|
+
this.#outputWatchers.delete(runId);
|
|
486
|
+
controller.abort(new Error("Agent local turn reached a terminal state"));
|
|
487
|
+
}
|
|
488
|
+
#notifySimulator() {
|
|
489
|
+
if (this.#simulatorListeners.size === 0) return;
|
|
490
|
+
const value = Object.freeze({ state: this.#simulatorState, debug: this.debugSnapshot() });
|
|
491
|
+
for (const listener of this.#simulatorListeners) {
|
|
492
|
+
try {
|
|
493
|
+
listener(value);
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
#upsertRun(run) {
|
|
499
|
+
const ordinal = this.#runs.get(run.runId)?.ordinal ?? ++this.#runOrdinal;
|
|
500
|
+
this.#runs.set(run.runId, Object.freeze({ ...run, ordinal }));
|
|
501
|
+
while (this.#runs.size > 100) {
|
|
502
|
+
const oldest = this.#runs.keys().next().value;
|
|
503
|
+
if (!oldest) break;
|
|
504
|
+
this.#runs.delete(oldest);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
#updateRunStatus(runId, status) {
|
|
508
|
+
const existing = this.#runs.get(runId);
|
|
509
|
+
if (!existing) return;
|
|
510
|
+
this.#upsertRun({ ...existing, status, updatedAt: this.#now().toISOString() });
|
|
511
|
+
}
|
|
512
|
+
#record(input) {
|
|
513
|
+
this.#eventSequence += 1;
|
|
514
|
+
this.#events.push(Object.freeze({
|
|
515
|
+
...input,
|
|
516
|
+
eventId: `event-${this.#eventSequence}`,
|
|
517
|
+
timestamp: this.#now().toISOString()
|
|
518
|
+
}));
|
|
519
|
+
if (this.#events.length > 300) this.#events.splice(0, this.#events.length - 300);
|
|
520
|
+
}
|
|
521
|
+
#setState(next) {
|
|
522
|
+
this.#state = Object.freeze({ ...next, revision: this.#state.revision + 1 });
|
|
523
|
+
for (const listener of this.#listeners) {
|
|
524
|
+
try {
|
|
525
|
+
listener(this.#state);
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return this.#state;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// apps/preview/src/server.ts
|
|
534
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
535
|
+
import { readFileSync } from "node:fs";
|
|
536
|
+
import { createServer } from "node:http";
|
|
537
|
+
import { createRequire } from "node:module";
|
|
538
|
+
import QRCode from "qrcode";
|
|
539
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
540
|
+
var SESSION_COOKIE = "buddy_preview_session";
|
|
541
|
+
var require2 = createRequire(import.meta.url);
|
|
542
|
+
var BRAND_ICON = Buffer.from(
|
|
543
|
+
readFileSync(new URL("../web/assets/developer-platform-icon.png.base64", import.meta.url), "utf8").trim(),
|
|
544
|
+
"base64"
|
|
545
|
+
);
|
|
546
|
+
var WEB_ASSETS = Object.freeze(/* @__PURE__ */ new Map([
|
|
547
|
+
["/", {
|
|
548
|
+
contentType: "text/html; charset=utf-8",
|
|
549
|
+
body: readFileSync(new URL("../web/index.html", import.meta.url))
|
|
550
|
+
}],
|
|
551
|
+
["/assets/app.css", {
|
|
552
|
+
contentType: "text/css; charset=utf-8",
|
|
553
|
+
body: readFileSync(new URL("../web/app.css", import.meta.url))
|
|
554
|
+
}],
|
|
555
|
+
["/assets/tokens.css", {
|
|
556
|
+
contentType: "text/css; charset=utf-8",
|
|
557
|
+
body: readFileSync(new URL("../web/tokens.css", import.meta.url))
|
|
558
|
+
}],
|
|
559
|
+
["/assets/brand.css", {
|
|
560
|
+
contentType: "text/css; charset=utf-8",
|
|
561
|
+
body: readFileSync(new URL("../web/brand.css", import.meta.url))
|
|
562
|
+
}],
|
|
563
|
+
["/assets/app.js", {
|
|
564
|
+
contentType: "text/javascript; charset=utf-8",
|
|
565
|
+
body: readFileSync(new URL("../web/app.js", import.meta.url))
|
|
566
|
+
}],
|
|
567
|
+
["/assets/developer-platform-icon.png", {
|
|
568
|
+
contentType: "image/png",
|
|
569
|
+
body: BRAND_ICON
|
|
570
|
+
}],
|
|
571
|
+
["/assets/iphone-16-max.js", {
|
|
572
|
+
contentType: "text/javascript; charset=utf-8",
|
|
573
|
+
body: readFileSync(require2.resolve("@sneas/telephone/iphone-16-max.js"))
|
|
574
|
+
}]
|
|
575
|
+
]));
|
|
576
|
+
function closeServer(server) {
|
|
577
|
+
if (!server.listening) return Promise.resolve();
|
|
578
|
+
return new Promise((resolve, reject) => {
|
|
579
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function writeHeaders(response) {
|
|
583
|
+
response.setHeader("Cache-Control", "no-store");
|
|
584
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
585
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
586
|
+
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
587
|
+
response.setHeader(
|
|
588
|
+
"Content-Security-Policy",
|
|
589
|
+
// Telephone injects its frame CSS into a shadow root and updates one inline sizing variable.
|
|
590
|
+
"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'"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
function staticAsset(response, asset) {
|
|
594
|
+
response.statusCode = 200;
|
|
595
|
+
writeHeaders(response);
|
|
596
|
+
response.setHeader("Content-Type", asset.contentType);
|
|
597
|
+
response.setHeader("Content-Length", asset.body.length);
|
|
598
|
+
response.end(asset.body);
|
|
599
|
+
}
|
|
600
|
+
function json(response, status, body) {
|
|
601
|
+
const encoded = JSON.stringify(body);
|
|
602
|
+
response.statusCode = status;
|
|
603
|
+
writeHeaders(response);
|
|
604
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
605
|
+
response.setHeader("Content-Length", Buffer.byteLength(encoded));
|
|
606
|
+
response.end(encoded);
|
|
607
|
+
}
|
|
608
|
+
function safeEqual(left, right) {
|
|
609
|
+
const a = Buffer.from(left);
|
|
610
|
+
const b = Buffer.from(right);
|
|
611
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
612
|
+
}
|
|
613
|
+
function cookieValue(request, name) {
|
|
614
|
+
for (const part of request.headers.cookie?.split(";") ?? []) {
|
|
615
|
+
const separator = part.indexOf("=");
|
|
616
|
+
if (separator < 0) continue;
|
|
617
|
+
if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
|
|
618
|
+
}
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
function requestUrl(request, origin) {
|
|
622
|
+
try {
|
|
623
|
+
return new URL(request.url ?? "/", origin);
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function allowedHost(request, port) {
|
|
629
|
+
const value = request.headers.host;
|
|
630
|
+
if (!value) return false;
|
|
631
|
+
try {
|
|
632
|
+
const parsed = new URL(`http://${value}`);
|
|
633
|
+
return LOOPBACK_HOSTS.has(parsed.hostname.replace(/^\[|\]$/gu, "")) && Number(parsed.port || 80) === port;
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function allowedOrigin(request, origin) {
|
|
639
|
+
const value = request.headers.origin;
|
|
640
|
+
return value === void 0 || value === origin.origin;
|
|
641
|
+
}
|
|
642
|
+
function apiError(response, status, code, message2) {
|
|
643
|
+
json(response, status, { error: { code, message: message2 } });
|
|
644
|
+
}
|
|
645
|
+
async function readJsonBody(request, maximumBytes = 64 * 1024) {
|
|
646
|
+
const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
647
|
+
if (contentType !== "application/json") {
|
|
648
|
+
throw Object.assign(new Error("\u9700\u8981 application/json"), { code: "JSON_REQUIRED", status: 415 });
|
|
649
|
+
}
|
|
650
|
+
const declared = Number(request.headers["content-length"] ?? 0);
|
|
651
|
+
if (!Number.isFinite(declared) || declared < 0 || declared > maximumBytes) {
|
|
652
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u8FC7\u5927"), { code: "BODY_TOO_LARGE", status: 413 });
|
|
653
|
+
}
|
|
654
|
+
const chunks = [];
|
|
655
|
+
let size = 0;
|
|
656
|
+
for await (const chunk of request) {
|
|
657
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
658
|
+
size += buffer.length;
|
|
659
|
+
if (size > maximumBytes) {
|
|
660
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u8FC7\u5927"), { code: "BODY_TOO_LARGE", status: 413 });
|
|
661
|
+
}
|
|
662
|
+
chunks.push(buffer);
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
666
|
+
} catch {
|
|
667
|
+
throw Object.assign(new Error("\u8BF7\u6C42\u6B63\u6587\u4E0D\u662F\u6709\u6548 JSON"), { code: "INVALID_JSON", status: 400 });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function exactObject(value, keys) {
|
|
671
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
672
|
+
const object2 = value;
|
|
673
|
+
const expected = new Set(keys);
|
|
674
|
+
return Object.keys(object2).length === keys.length && Object.keys(object2).every((key) => expected.has(key)) ? object2 : null;
|
|
675
|
+
}
|
|
676
|
+
function safeIdentifier(value, maximumLength = 200) {
|
|
677
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximumLength && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
678
|
+
}
|
|
679
|
+
function requestFailure(cause) {
|
|
680
|
+
if (typeof cause === "object" && cause !== null) {
|
|
681
|
+
const status = "status" in cause && typeof cause.status === "number" && Number.isInteger(cause.status) && cause.status >= 400 && cause.status <= 599 ? cause.status : 502;
|
|
682
|
+
const code = "code" in cause && safeIdentifier(cause.code, 80) ? cause.code : "SIMULATOR_REQUEST_FAILED";
|
|
683
|
+
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";
|
|
684
|
+
return { status, code, message: message2 };
|
|
685
|
+
}
|
|
686
|
+
return { status: 502, code: "SIMULATOR_REQUEST_FAILED", message: "\u6A21\u62DF\u5668\u8BF7\u6C42\u672A\u80FD\u5B8C\u6210" };
|
|
687
|
+
}
|
|
688
|
+
function publicProject(config) {
|
|
689
|
+
return {
|
|
690
|
+
buddyId: config.buddyId,
|
|
691
|
+
runtime: { language: config.runtime.language },
|
|
692
|
+
...config.model === void 0 ? {} : { model: config.model }
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async function startPreviewControlServer(options) {
|
|
696
|
+
const host = options.host ?? "127.0.0.1";
|
|
697
|
+
const port = options.port ?? 0;
|
|
698
|
+
if (!LOOPBACK_HOSTS.has(host)) throw new TypeError("Preview control server must bind to loopback");
|
|
699
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535) throw new TypeError("Preview port must be 0\u201365535");
|
|
700
|
+
const sessionToken = randomBytes(32).toString("base64url");
|
|
701
|
+
let bootstrapAvailable = true;
|
|
702
|
+
let origin;
|
|
703
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
704
|
+
let eventRevision = 0;
|
|
705
|
+
const publish = (event, value) => {
|
|
706
|
+
eventRevision += 1;
|
|
707
|
+
const payload = `id: ${eventRevision}
|
|
708
|
+
event: ${event}
|
|
709
|
+
data: ${JSON.stringify(value)}
|
|
710
|
+
|
|
711
|
+
`;
|
|
712
|
+
for (const response of listeners) {
|
|
713
|
+
try {
|
|
714
|
+
response.write(payload);
|
|
715
|
+
} catch {
|
|
716
|
+
listeners.delete(response);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
const unsubscribeDev = options.controller.subscribe((state) => publish("dev.state", state));
|
|
721
|
+
const unsubscribeSimulator = options.controller.subscribeSimulator((value) => publish("simulator.state", value));
|
|
722
|
+
const server = createServer(async (request, response) => {
|
|
723
|
+
if (!origin) return apiError(response, 503, "PREVIEW_STARTING", "Preview \u6B63\u5728\u542F\u52A8");
|
|
724
|
+
if (!allowedHost(request, Number(origin.port)) || !allowedOrigin(request, origin)) {
|
|
725
|
+
return apiError(response, 403, "PREVIEW_ORIGIN_FORBIDDEN", "\u8BF7\u6C42\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
726
|
+
}
|
|
727
|
+
const url = requestUrl(request, origin);
|
|
728
|
+
if (!url) return apiError(response, 400, "INVALID_URL", "\u8BF7\u6C42\u5730\u5740\u65E0\u6548");
|
|
729
|
+
if (request.method === "GET" && bootstrapAvailable && url.pathname === `/bootstrap/${sessionToken}`) {
|
|
730
|
+
bootstrapAvailable = false;
|
|
731
|
+
response.statusCode = 303;
|
|
732
|
+
writeHeaders(response);
|
|
733
|
+
response.setHeader(
|
|
734
|
+
"Set-Cookie",
|
|
735
|
+
`${SESSION_COOKIE}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`
|
|
736
|
+
);
|
|
737
|
+
response.setHeader("Location", "/");
|
|
738
|
+
response.end();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const suppliedToken = cookieValue(request, SESSION_COOKIE);
|
|
742
|
+
if (!suppliedToken || !safeEqual(suppliedToken, sessionToken)) {
|
|
743
|
+
return apiError(response, 401, "PREVIEW_UNAUTHORIZED", "\u9700\u8981\u4ECE CLI \u63D0\u4F9B\u7684\u4E00\u6B21\u6027\u5165\u53E3\u6253\u5F00 Preview");
|
|
744
|
+
}
|
|
745
|
+
if (request.method === "GET") {
|
|
746
|
+
const asset = WEB_ASSETS.get(url.pathname);
|
|
747
|
+
if (asset) return staticAsset(response, asset);
|
|
748
|
+
}
|
|
749
|
+
if (request.method === "GET" && url.pathname === "/api/project") {
|
|
750
|
+
try {
|
|
751
|
+
return json(response, 200, { project: publicProject(await options.controller.project()) });
|
|
752
|
+
} catch {
|
|
753
|
+
return apiError(response, 422, "PROJECT_CONFIG_INVALID", "\u9879\u76EE\u914D\u7F6E\u65E0\u6CD5\u8BFB\u53D6\u6216\u6821\u9A8C\u5931\u8D25");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (request.method === "GET" && url.pathname === "/api/dev") {
|
|
757
|
+
return json(response, 200, { state: options.controller.state() });
|
|
758
|
+
}
|
|
759
|
+
if (request.method === "GET" && url.pathname === "/api/buddy") {
|
|
760
|
+
try {
|
|
761
|
+
return json(response, 200, { buddy: await options.controller.buddyDetails() });
|
|
762
|
+
} catch (cause) {
|
|
763
|
+
const failure = requestFailure(cause);
|
|
764
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (request.method === "GET" && url.pathname === "/api/debug") {
|
|
768
|
+
return json(response, 200, { debug: options.controller.debugSnapshot() });
|
|
769
|
+
}
|
|
770
|
+
if (request.method === "GET" && url.pathname === "/api/device/pairing") {
|
|
771
|
+
const pairing = options.controller.devicePairing();
|
|
772
|
+
if (pairing.status !== "ready") return json(response, 200, { pairing });
|
|
773
|
+
const qrCodeDataUrl = await QRCode.toDataURL(pairing.url, {
|
|
774
|
+
errorCorrectionLevel: "M",
|
|
775
|
+
margin: 4,
|
|
776
|
+
width: 256
|
|
777
|
+
});
|
|
778
|
+
const current = options.controller.devicePairing();
|
|
779
|
+
return json(response, 200, { pairing: current.status === "ready" && current.url === pairing.url ? { ...current, qrCodeDataUrl } : current });
|
|
780
|
+
}
|
|
781
|
+
if (request.method === "GET" && url.pathname === "/api/simulator/state") {
|
|
782
|
+
try {
|
|
783
|
+
return json(response, 200, { state: await options.controller.simulatorState() });
|
|
784
|
+
} catch (cause) {
|
|
785
|
+
const failure = requestFailure(cause);
|
|
786
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (request.method === "GET" && url.pathname === "/api/events") {
|
|
790
|
+
response.statusCode = 200;
|
|
791
|
+
writeHeaders(response);
|
|
792
|
+
response.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
793
|
+
response.setHeader("Connection", "keep-alive");
|
|
794
|
+
eventRevision += 1;
|
|
795
|
+
response.write(`id: ${eventRevision}
|
|
796
|
+
event: dev.state
|
|
797
|
+
data: ${JSON.stringify(options.controller.state())}
|
|
798
|
+
|
|
799
|
+
`);
|
|
800
|
+
eventRevision += 1;
|
|
801
|
+
response.write(`id: ${eventRevision}
|
|
802
|
+
event: simulator.state
|
|
803
|
+
data: ${JSON.stringify({
|
|
804
|
+
state: options.controller.simulatorSnapshot(),
|
|
805
|
+
debug: options.controller.debugSnapshot()
|
|
806
|
+
})}
|
|
807
|
+
|
|
808
|
+
`);
|
|
809
|
+
listeners.add(response);
|
|
810
|
+
request.once("close", () => listeners.delete(response));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (request.method === "POST" && url.pathname === "/api/dev/start") {
|
|
814
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
815
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
816
|
+
}
|
|
817
|
+
void options.controller.start();
|
|
818
|
+
return json(response, 202, { state: options.controller.state() });
|
|
819
|
+
}
|
|
820
|
+
if (request.method === "POST" && url.pathname === "/api/dev/stop") {
|
|
821
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
822
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
return json(response, 200, { state: await options.controller.stop() });
|
|
826
|
+
} catch {
|
|
827
|
+
return apiError(response, 500, "DEV_STOP_FAILED", "DEV \u4F1A\u8BDD\u672A\u80FD\u5B89\u5168\u505C\u6B62\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u8FDB\u7A0B");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (request.method === "POST" && url.pathname === "/api/simulator/messages") {
|
|
831
|
+
try {
|
|
832
|
+
const body = exactObject(await readJsonBody(request), ["text", "idempotencyKey"]);
|
|
833
|
+
if (!body || typeof body.text !== "string" || !body.text.trim() || Buffer.byteLength(body.text, "utf8") > 32 * 1024 || !safeIdentifier(body.idempotencyKey, 128)) {
|
|
834
|
+
return apiError(response, 400, "INVALID_MESSAGE", "\u6D88\u606F\u6216\u5E42\u7B49\u6807\u8BC6\u4E0D\u7B26\u5408\u8981\u6C42");
|
|
835
|
+
}
|
|
836
|
+
const accepted = await options.controller.sendSimulatorMessage({
|
|
837
|
+
text: body.text,
|
|
838
|
+
idempotencyKey: body.idempotencyKey
|
|
839
|
+
});
|
|
840
|
+
return json(response, 202, accepted);
|
|
841
|
+
} catch (cause) {
|
|
842
|
+
const failure = requestFailure(cause);
|
|
843
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const cancelMatch = /^\/api\/simulator\/runs\/([^/]+)\/cancel$/u.exec(url.pathname);
|
|
847
|
+
if (request.method === "POST" && cancelMatch) {
|
|
848
|
+
if (Number(request.headers["content-length"] ?? 0) > 0) {
|
|
849
|
+
return apiError(response, 400, "BODY_NOT_ALLOWED", "\u8BE5\u64CD\u4F5C\u4E0D\u63A5\u53D7\u8BF7\u6C42\u6B63\u6587");
|
|
850
|
+
}
|
|
851
|
+
let runId;
|
|
852
|
+
try {
|
|
853
|
+
runId = decodeURIComponent(cancelMatch[1]);
|
|
854
|
+
} catch {
|
|
855
|
+
return apiError(response, 400, "INVALID_RUN_ID", "Run ID \u4E0D\u7B26\u5408\u8981\u6C42");
|
|
856
|
+
}
|
|
857
|
+
if (!safeIdentifier(runId)) return apiError(response, 400, "INVALID_RUN_ID", "Run ID \u4E0D\u7B26\u5408\u8981\u6C42");
|
|
858
|
+
try {
|
|
859
|
+
return json(response, 200, await options.controller.cancelSimulatorRun(runId));
|
|
860
|
+
} catch (cause) {
|
|
861
|
+
const failure = requestFailure(cause);
|
|
862
|
+
return apiError(response, failure.status, failure.code, failure.message);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return apiError(response, 404, "NOT_FOUND", "\u63A5\u53E3\u4E0D\u5B58\u5728");
|
|
866
|
+
});
|
|
867
|
+
try {
|
|
868
|
+
await new Promise((resolve, reject) => {
|
|
869
|
+
server.once("error", reject);
|
|
870
|
+
server.listen(port, host, () => {
|
|
871
|
+
server.off("error", reject);
|
|
872
|
+
resolve();
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
const address = server.address();
|
|
876
|
+
if (!address) throw new Error("Preview server has no address");
|
|
877
|
+
const urlHost = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
878
|
+
origin = new URL(`http://${urlHost}:${address.port}`);
|
|
879
|
+
} catch (cause) {
|
|
880
|
+
unsubscribeDev();
|
|
881
|
+
unsubscribeSimulator();
|
|
882
|
+
await closeServer(server).catch(() => void 0);
|
|
883
|
+
throw cause;
|
|
884
|
+
}
|
|
885
|
+
const bootstrapUrl = new URL(`/bootstrap/${sessionToken}`, origin);
|
|
886
|
+
return Object.freeze({
|
|
887
|
+
origin,
|
|
888
|
+
bootstrapUrl,
|
|
889
|
+
async close() {
|
|
890
|
+
unsubscribeDev();
|
|
891
|
+
unsubscribeSimulator();
|
|
892
|
+
for (const response of listeners) response.end();
|
|
893
|
+
listeners.clear();
|
|
894
|
+
let controllerFailure;
|
|
895
|
+
try {
|
|
896
|
+
await options.controller.close();
|
|
897
|
+
} catch (cause) {
|
|
898
|
+
controllerFailure = cause;
|
|
899
|
+
}
|
|
900
|
+
await closeServer(server);
|
|
901
|
+
if (controllerFailure !== void 0) throw controllerFailure;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// apps/preview/src/mlb-simulator.ts
|
|
907
|
+
import { createHash } from "node:crypto";
|
|
908
|
+
import { normalizePlatformUrl } from "./core.js";
|
|
909
|
+
|
|
910
|
+
// apps/preview/src/mlb-events.ts
|
|
911
|
+
import { randomUUID } from "node:crypto";
|
|
912
|
+
import { WebSocket } from "ws";
|
|
913
|
+
var MlbEventStreamError = class extends Error {
|
|
914
|
+
constructor(message2, status = 502, retryable = true) {
|
|
915
|
+
super(message2);
|
|
916
|
+
this.status = status;
|
|
917
|
+
this.retryable = retryable;
|
|
918
|
+
this.publicMessage = message2;
|
|
919
|
+
}
|
|
920
|
+
status;
|
|
921
|
+
retryable;
|
|
922
|
+
code = "MLB_EVENT_STREAM_FAILED";
|
|
923
|
+
publicMessage;
|
|
924
|
+
};
|
|
925
|
+
var MlbAppCommandError = class extends Error {
|
|
926
|
+
constructor(message2, status = 502) {
|
|
927
|
+
super(message2);
|
|
928
|
+
this.status = status;
|
|
929
|
+
this.publicMessage = message2;
|
|
930
|
+
}
|
|
931
|
+
status;
|
|
932
|
+
code = "MLB_REQUEST_FAILED";
|
|
933
|
+
publicMessage;
|
|
934
|
+
};
|
|
935
|
+
function appSocket(url, token) {
|
|
936
|
+
return new WebSocket(url, {
|
|
937
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
938
|
+
followRedirects: false,
|
|
939
|
+
handshakeTimeout: 1e4,
|
|
940
|
+
maxPayload: 64 * 1024,
|
|
941
|
+
perMessageDeflate: false
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
async function mlbAppCommand(url, token, method, params, options = {}) {
|
|
945
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
946
|
+
const requestId = randomUUID();
|
|
947
|
+
const socket = appSocket(url, token);
|
|
948
|
+
return await new Promise((resolve, reject) => {
|
|
949
|
+
let settled = false;
|
|
950
|
+
let ready = false;
|
|
951
|
+
let sent = false;
|
|
952
|
+
const cleanup = () => {
|
|
953
|
+
clearTimeout(timer);
|
|
954
|
+
options.signal?.removeEventListener("abort", abort);
|
|
955
|
+
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
|
|
956
|
+
};
|
|
957
|
+
const finish = (value) => {
|
|
958
|
+
if (settled) return;
|
|
959
|
+
settled = true;
|
|
960
|
+
cleanup();
|
|
961
|
+
resolve(value);
|
|
962
|
+
};
|
|
963
|
+
const fail = (cause) => {
|
|
964
|
+
if (settled) return;
|
|
965
|
+
settled = true;
|
|
966
|
+
cleanup();
|
|
967
|
+
reject(cause);
|
|
968
|
+
};
|
|
969
|
+
const abort = () => fail(options.signal?.reason);
|
|
970
|
+
const timer = setTimeout(() => fail(new MlbAppCommandError("MLB WebSocket \u547D\u4EE4\u7B49\u5F85\u8D85\u65F6")), 3e4);
|
|
971
|
+
timer.unref();
|
|
972
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
973
|
+
socket.on("error", () => fail(new MlbAppCommandError("\u65E0\u6CD5\u8FDE\u63A5 MLB WebSocket")));
|
|
974
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
975
|
+
const status = response.statusCode ?? 502;
|
|
976
|
+
fail(new MlbAppCommandError(
|
|
977
|
+
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`,
|
|
978
|
+
status
|
|
979
|
+
));
|
|
980
|
+
response.destroy();
|
|
981
|
+
});
|
|
982
|
+
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")));
|
|
983
|
+
socket.on("message", (raw, binary) => {
|
|
984
|
+
if (settled) return;
|
|
985
|
+
try {
|
|
986
|
+
if (binary) throw new Error("binary");
|
|
987
|
+
const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
988
|
+
const frame = JSON.parse(bytes.toString("utf8"));
|
|
989
|
+
const data = frame.data;
|
|
990
|
+
if (!frame || frame.v !== 1 || !data || typeof data !== "object" || Array.isArray(data)) {
|
|
991
|
+
throw new Error("envelope");
|
|
992
|
+
}
|
|
993
|
+
if (frame.type === "event") {
|
|
994
|
+
if (!ready) {
|
|
995
|
+
if (frame.event !== "ready" || data.resync !== true || typeof data.connectionId !== "string" || !data.connectionId) throw new Error("ready required");
|
|
996
|
+
ready = true;
|
|
997
|
+
if (!sent) {
|
|
998
|
+
sent = true;
|
|
999
|
+
socket.send(JSON.stringify({ v: 1, type: "request", requestId, method, params }));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (frame.type !== "response" || !ready || frame.requestId !== requestId || !Number.isSafeInteger(frame.status) || frame.status < 100 || frame.status > 599) {
|
|
1005
|
+
throw new Error("response");
|
|
1006
|
+
}
|
|
1007
|
+
const status = frame.status;
|
|
1008
|
+
if (status >= 200 && status < 300) {
|
|
1009
|
+
finish(data);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const serverMessage = data.error;
|
|
1013
|
+
fail(new MlbAppCommandError(
|
|
1014
|
+
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",
|
|
1015
|
+
status
|
|
1016
|
+
));
|
|
1017
|
+
} catch {
|
|
1018
|
+
fail(new MlbAppCommandError("MLB WebSocket \u8FD4\u56DE\u4E86\u65E0\u6548\u547D\u4EE4\u56DE\u6267"));
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
if (options.signal?.aborted) abort();
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
async function* mlbAppEvents(url, token, options = {}) {
|
|
1025
|
+
if (options.signal?.aborted) return;
|
|
1026
|
+
const socket = appSocket(url, token);
|
|
1027
|
+
const queue = [];
|
|
1028
|
+
let queuedBytes = 0;
|
|
1029
|
+
let ready = false;
|
|
1030
|
+
let ended = false;
|
|
1031
|
+
let failure;
|
|
1032
|
+
let wake;
|
|
1033
|
+
const finish = (cause) => {
|
|
1034
|
+
if (ended) return;
|
|
1035
|
+
ended = true;
|
|
1036
|
+
failure = cause;
|
|
1037
|
+
wake?.();
|
|
1038
|
+
};
|
|
1039
|
+
let timer = setTimeout(() => finish(new MlbEventStreamError("MLB WebSocket \u672A\u5B8C\u6210\u521D\u59CB\u5316")), 1e4);
|
|
1040
|
+
timer.unref();
|
|
1041
|
+
const heartbeat = () => {
|
|
1042
|
+
if (!ready || ended) return;
|
|
1043
|
+
clearTimeout(timer);
|
|
1044
|
+
timer = setTimeout(() => finish(new MlbEventStreamError("MLB WebSocket \u5FC3\u8DF3\u4E2D\u65AD")), 6e4);
|
|
1045
|
+
timer.unref();
|
|
1046
|
+
};
|
|
1047
|
+
const abort = () => finish();
|
|
1048
|
+
socket.on("ping", heartbeat);
|
|
1049
|
+
socket.on("error", () => finish(new MlbEventStreamError("\u65E0\u6CD5\u8FDE\u63A5 MLB WebSocket")));
|
|
1050
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
1051
|
+
const status = response.statusCode ?? 502;
|
|
1052
|
+
finish(new MlbEventStreamError(
|
|
1053
|
+
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`,
|
|
1054
|
+
status,
|
|
1055
|
+
status >= 500 || status === 429
|
|
1056
|
+
));
|
|
1057
|
+
response.destroy();
|
|
1058
|
+
});
|
|
1059
|
+
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))));
|
|
1060
|
+
socket.on("message", (raw, binary) => {
|
|
1061
|
+
if (ended) return;
|
|
1062
|
+
try {
|
|
1063
|
+
if (binary) throw new Error("binary");
|
|
1064
|
+
const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
1065
|
+
const frame = JSON.parse(bytes.toString("utf8"));
|
|
1066
|
+
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");
|
|
1067
|
+
if (!ready && frame.event !== "ready") throw new Error("ready required");
|
|
1068
|
+
if (frame.event === "ready" && (frame.data.resync !== true || typeof frame.data.connectionId !== "string" || !frame.data.connectionId)) throw new Error("invalid ready");
|
|
1069
|
+
ready = true;
|
|
1070
|
+
heartbeat();
|
|
1071
|
+
queuedBytes += bytes.byteLength;
|
|
1072
|
+
if (queuedBytes > 1024 * 1024 || queue.length >= 1024) {
|
|
1073
|
+
finish(new MlbEventStreamError("MLB \u4E8B\u4EF6\u63A5\u6536\u79EF\u538B\uFF0C\u6B63\u5728\u91CD\u65B0\u540C\u6B65"));
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
queue.push({ frame: { event: frame.event, data: frame.data }, bytes: bytes.byteLength });
|
|
1077
|
+
wake?.();
|
|
1078
|
+
} catch {
|
|
1079
|
+
finish(new MlbEventStreamError("MLB WebSocket \u8FD4\u56DE\u4E86\u65E0\u6548\u4E8B\u4EF6", 502, false));
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
1083
|
+
if (options.signal?.aborted) abort();
|
|
1084
|
+
try {
|
|
1085
|
+
while (!ended) {
|
|
1086
|
+
const item = queue.shift();
|
|
1087
|
+
if (item) {
|
|
1088
|
+
queuedBytes -= item.bytes;
|
|
1089
|
+
yield item.frame;
|
|
1090
|
+
} else {
|
|
1091
|
+
await new Promise((resolve) => {
|
|
1092
|
+
wake = resolve;
|
|
1093
|
+
});
|
|
1094
|
+
wake = void 0;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (failure && !options.signal?.aborted) throw failure;
|
|
1098
|
+
} finally {
|
|
1099
|
+
ended = true;
|
|
1100
|
+
clearTimeout(timer);
|
|
1101
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1102
|
+
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// apps/preview/src/buddy-details.ts
|
|
1107
|
+
function invalid() {
|
|
1108
|
+
throw Object.assign(new Error("MLB \u8FD4\u56DE\u7684\u5F00\u53D1\u7248\u642D\u5B50\u8BE6\u60C5\u4E0D\u7B26\u5408\u63A5\u53E3\u7EA6\u5B9A"), {
|
|
1109
|
+
code: "BUDDY_DETAILS_INVALID",
|
|
1110
|
+
status: 502,
|
|
1111
|
+
publicMessage: "MLB \u8FD4\u56DE\u7684\u5F00\u53D1\u7248\u642D\u5B50\u8BE6\u60C5\u4E0D\u7B26\u5408\u63A5\u53E3\u7EA6\u5B9A"
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
function record(value) {
|
|
1115
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
function text(value, multiline = false) {
|
|
1119
|
+
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();
|
|
1120
|
+
return value;
|
|
1121
|
+
}
|
|
1122
|
+
function nullableText(value, multiline = false) {
|
|
1123
|
+
return value === null ? null : text(value, multiline);
|
|
1124
|
+
}
|
|
1125
|
+
function parsePreviewBuddyDetails(value, expectedId) {
|
|
1126
|
+
const source = record(value);
|
|
1127
|
+
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();
|
|
1128
|
+
return Object.freeze({
|
|
1129
|
+
id: expectedId,
|
|
1130
|
+
isDev: true,
|
|
1131
|
+
name: text(source.name),
|
|
1132
|
+
avatar: nullableText(source.avatar),
|
|
1133
|
+
tagline: nullableText(source.tagline),
|
|
1134
|
+
intro: nullableText(source.intro, true),
|
|
1135
|
+
creatorUserId: nullableText(source.creatorUserId),
|
|
1136
|
+
creatorUserName: nullableText(source.creatorUserName),
|
|
1137
|
+
creatorUserAvatar: nullableText(source.creatorUserAvatar),
|
|
1138
|
+
privateId: source.privateId,
|
|
1139
|
+
addedCount: source.addedCount
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// apps/preview/src/mlb-simulator.ts
|
|
1144
|
+
function safeText(value, name, maximum = 4096) {
|
|
1145
|
+
if (typeof value !== "string" || !value.trim() || value.length > maximum || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
1146
|
+
throw new TypeError(`${name} is invalid`);
|
|
1147
|
+
}
|
|
1148
|
+
return value;
|
|
1149
|
+
}
|
|
1150
|
+
function endpoint(base, path) {
|
|
1151
|
+
return new URL(path.replace(/^\//u, ""), base);
|
|
1152
|
+
}
|
|
1153
|
+
function object(value) {
|
|
1154
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1155
|
+
}
|
|
1156
|
+
function httpError(status, value) {
|
|
1157
|
+
const serverMessage = object(value)?.error;
|
|
1158
|
+
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";
|
|
1159
|
+
return Object.assign(new Error(message2), {
|
|
1160
|
+
code: "MLB_REQUEST_FAILED",
|
|
1161
|
+
status,
|
|
1162
|
+
publicMessage: message2
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
function message(value, index) {
|
|
1166
|
+
const input = object(value);
|
|
1167
|
+
if (!input || typeof input.author !== "string") return null;
|
|
1168
|
+
const role = input.author === "buddy" ? "assistant" : input.author === "user" || input.author === "owner" ? "user" : void 0;
|
|
1169
|
+
if (!role) return null;
|
|
1170
|
+
const messageId = safeText(input.id, `messages[${index}].id`, 256);
|
|
1171
|
+
if (typeof input.text !== "string" || Buffer.byteLength(input.text, "utf8") > 48 * 1024) {
|
|
1172
|
+
throw new Error(`messages[${index}].text is invalid`);
|
|
1173
|
+
}
|
|
1174
|
+
if (input.type === "tool" && !input.text.trim()) return null;
|
|
1175
|
+
if (typeof input.timestamp !== "number" || !Number.isFinite(input.timestamp)) {
|
|
1176
|
+
throw new Error(`messages[${index}].timestamp is invalid`);
|
|
1177
|
+
}
|
|
1178
|
+
return Object.freeze({
|
|
1179
|
+
messageId,
|
|
1180
|
+
role,
|
|
1181
|
+
text: input.text,
|
|
1182
|
+
createdAt: new Date(input.timestamp).toISOString()
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
function localReceipt(prefix, value) {
|
|
1186
|
+
return `${prefix}-${createHash("sha256").update(value).digest("hex").slice(0, 20)}`;
|
|
1187
|
+
}
|
|
1188
|
+
function waitForRetry(milliseconds, signal) {
|
|
1189
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
1190
|
+
return new Promise((resolve, reject) => {
|
|
1191
|
+
const abort = () => {
|
|
1192
|
+
clearTimeout(timer);
|
|
1193
|
+
reject(signal?.reason);
|
|
1194
|
+
};
|
|
1195
|
+
const timer = setTimeout(() => {
|
|
1196
|
+
signal?.removeEventListener("abort", abort);
|
|
1197
|
+
resolve();
|
|
1198
|
+
}, milliseconds);
|
|
1199
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
var MlbSimulatorClient = class {
|
|
1203
|
+
#baseUrl;
|
|
1204
|
+
#buddyId;
|
|
1205
|
+
#accessToken;
|
|
1206
|
+
#fetch;
|
|
1207
|
+
#sending = false;
|
|
1208
|
+
#lastSend;
|
|
1209
|
+
#privateId;
|
|
1210
|
+
#privatePromise;
|
|
1211
|
+
#active;
|
|
1212
|
+
#latest;
|
|
1213
|
+
#streaming;
|
|
1214
|
+
#messages = Object.freeze([]);
|
|
1215
|
+
constructor(options) {
|
|
1216
|
+
this.#baseUrl = normalizePlatformUrl(options.baseUrl);
|
|
1217
|
+
this.#buddyId = safeText(options.buddyId, "buddyId", 256);
|
|
1218
|
+
if (!this.#buddyId.endsWith("-dev")) {
|
|
1219
|
+
throw new TypeError("MLB simulator buddyId must use the -dev identity");
|
|
1220
|
+
}
|
|
1221
|
+
this.#accessToken = safeText(options.auth.developerAccessToken, "developerAccessToken", 16384);
|
|
1222
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
1223
|
+
if (typeof this.#fetch !== "function") throw new TypeError("A Fetch API implementation is required");
|
|
1224
|
+
}
|
|
1225
|
+
/** Reading a profile must not add the Buddy or trigger a welcome message. */
|
|
1226
|
+
async details(options = {}) {
|
|
1227
|
+
const signal = AbortSignal.any([AbortSignal.timeout(3e4), ...options.signal ? [options.signal] : []]);
|
|
1228
|
+
const details = parsePreviewBuddyDetails(await this.#request("GET", `/app/buddies/${encodeURIComponent(this.#buddyId)}`, { signal }), this.#buddyId);
|
|
1229
|
+
this.#privateId = details.privateId ?? void 0;
|
|
1230
|
+
return details;
|
|
1231
|
+
}
|
|
1232
|
+
async state(options = {}) {
|
|
1233
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
1234
|
+
const value = await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, {
|
|
1235
|
+
signal: options.signal
|
|
1236
|
+
});
|
|
1237
|
+
const body = object(value);
|
|
1238
|
+
const conversation = object(body?.private);
|
|
1239
|
+
if (!body || conversation?.id !== privateId || conversation?.buddyId !== this.#buddyId || !Array.isArray(body.messages) || typeof body.replying !== "boolean") {
|
|
1240
|
+
throw httpError(502, { error: "MLB Server \u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u79C1\u804A\u72B6\u6001" });
|
|
1241
|
+
}
|
|
1242
|
+
this.#messages = Object.freeze(body.messages.map((item, index) => message(item, index)).filter((item) => item !== null));
|
|
1243
|
+
if (this.#active) {
|
|
1244
|
+
const replied = this.#messages.some((item) => item.role === "assistant" && Date.parse(item.createdAt) > this.#active.messageTimestamp);
|
|
1245
|
+
if (body.replying) {
|
|
1246
|
+
this.#active = Object.freeze({ ...this.#active, observedReplying: true });
|
|
1247
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "running" });
|
|
1248
|
+
} else if (replied) {
|
|
1249
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "completed" });
|
|
1250
|
+
this.#active = void 0;
|
|
1251
|
+
this.#streaming = void 0;
|
|
1252
|
+
} else if (this.#active.observedReplying) {
|
|
1253
|
+
this.#latest = Object.freeze({ runId: this.#active.runId, status: "failed" });
|
|
1254
|
+
this.#active = void 0;
|
|
1255
|
+
this.#streaming = void 0;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return Object.freeze({
|
|
1259
|
+
messages: this.#messages,
|
|
1260
|
+
...this.#latest === void 0 ? {} : { latestRun: this.#latest },
|
|
1261
|
+
...this.#active === void 0 ? {} : {
|
|
1262
|
+
activeRun: Object.freeze({
|
|
1263
|
+
runId: this.#active.runId,
|
|
1264
|
+
status: body.replying ? "running" : "pending"
|
|
1265
|
+
})
|
|
1266
|
+
},
|
|
1267
|
+
...this.#streaming === void 0 ? {} : { streaming: this.#streaming }
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async send(input, options = {}) {
|
|
1271
|
+
if (typeof input.text !== "string" || !input.text.trim() || Buffer.byteLength(input.text, "utf8") > 32 * 1024) {
|
|
1272
|
+
throw new TypeError("message text is invalid");
|
|
1273
|
+
}
|
|
1274
|
+
const idempotencyKey = safeText(input.idempotencyKey, "idempotencyKey", 128);
|
|
1275
|
+
if (this.#lastSend?.key === idempotencyKey) {
|
|
1276
|
+
if (this.#lastSend.text !== input.text.trim()) throw httpError(409, { error: "\u540C\u4E00\u6761\u6D88\u606F\u4E0D\u80FD\u6539\u5199\u5185\u5BB9" });
|
|
1277
|
+
return this.#lastSend.receipt;
|
|
1278
|
+
}
|
|
1279
|
+
if (this.#active || this.#sending) {
|
|
1280
|
+
throw Object.assign(new Error("\u4E0A\u4E00\u8F6E\u56DE\u590D\u5C1A\u672A\u7ED3\u675F"), {
|
|
1281
|
+
code: "TURN_IN_PROGRESS",
|
|
1282
|
+
status: 409,
|
|
1283
|
+
publicMessage: "\u8BF7\u7B49\u5F85\u5F53\u524D\u56DE\u590D\u5B8C\u6210\u540E\u518D\u53D1\u9001"
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
this.#sending = true;
|
|
1287
|
+
try {
|
|
1288
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
1289
|
+
const clientMessageId = localReceipt("client", idempotencyKey);
|
|
1290
|
+
let value;
|
|
1291
|
+
try {
|
|
1292
|
+
const url = endpoint(this.#baseUrl, "/app/ws");
|
|
1293
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1294
|
+
value = await mlbAppCommand(url, this.#accessToken, "message.send", {
|
|
1295
|
+
kind: "private",
|
|
1296
|
+
id: privateId,
|
|
1297
|
+
message: input.text,
|
|
1298
|
+
clientMessageId
|
|
1299
|
+
}, { signal: options.signal });
|
|
1300
|
+
const receipt2 = object(value);
|
|
1301
|
+
if (receipt2?.ok !== true || receipt2.clientMessageId !== clientMessageId || typeof receipt2.messageId !== "string" || !receipt2.messageId.trim() || !Number.isSafeInteger(receipt2.timestamp) || receipt2.timestamp < 0 || typeof receipt2.shouldReply !== "boolean") {
|
|
1302
|
+
throw httpError(502, { error: "MLB \u6D88\u606F\u56DE\u6267\u65E0\u6548" });
|
|
1303
|
+
}
|
|
1304
|
+
safeText(receipt2.messageId, "receipt.messageId", 256);
|
|
1305
|
+
} catch (cause) {
|
|
1306
|
+
if (cause && typeof cause === "object" && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500) throw cause;
|
|
1307
|
+
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"), {
|
|
1308
|
+
code: "MLB_SEND_OUTCOME_UNKNOWN",
|
|
1309
|
+
status: 502,
|
|
1310
|
+
publicMessage: "\u53D1\u9001\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u804A\u5929\u8BB0\u5F55\u540E\u518D\u91CD\u8BD5"
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
const receipt = object(value);
|
|
1314
|
+
const runId = localReceipt("turn", idempotencyKey);
|
|
1315
|
+
const messageId = safeText(receipt.messageId, "receipt.messageId", 256);
|
|
1316
|
+
this.#active = receipt.shouldReply ? Object.freeze({
|
|
1317
|
+
runId,
|
|
1318
|
+
messageTimestamp: receipt.timestamp,
|
|
1319
|
+
observedReplying: false
|
|
1320
|
+
}) : void 0;
|
|
1321
|
+
this.#latest = Object.freeze({ runId, status: receipt.shouldReply ? "pending" : "completed" });
|
|
1322
|
+
this.#streaming = void 0;
|
|
1323
|
+
const accepted = Object.freeze({ messageId, runId });
|
|
1324
|
+
this.#lastSend = { key: idempotencyKey, text: input.text.trim(), receipt: accepted };
|
|
1325
|
+
return accepted;
|
|
1326
|
+
} finally {
|
|
1327
|
+
this.#sending = false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async cancel(_runId, _options = {}) {
|
|
1331
|
+
throw Object.assign(new Error("\u5F53\u524D MLB \u534F\u8BAE\u5C1A\u672A\u63D0\u4F9B\u7EC8\u6B62\u56DE\u590D\u63A5\u53E3"), {
|
|
1332
|
+
code: "TURN_CANCEL_UNSUPPORTED",
|
|
1333
|
+
status: 501,
|
|
1334
|
+
publicMessage: "\u5F53\u524D\u7248\u672C\u6682\u4E0D\u652F\u6301\u7EC8\u6B62\u56DE\u590D"
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
async watchOutput(runId, options) {
|
|
1338
|
+
const safeRunId = safeText(runId, "runId", 256);
|
|
1339
|
+
const privateId = await this.#ensurePrivate(options.signal);
|
|
1340
|
+
const url = endpoint(this.#baseUrl, "/app/ws");
|
|
1341
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1342
|
+
let sequence = 0;
|
|
1343
|
+
const reset = async () => {
|
|
1344
|
+
sequence = 0;
|
|
1345
|
+
this.#streaming = void 0;
|
|
1346
|
+
await options.onEvent({ type: "reset", runId: safeRunId });
|
|
1347
|
+
};
|
|
1348
|
+
for (let reconnects = 0; !options.signal?.aborted; reconnects++) {
|
|
1349
|
+
try {
|
|
1350
|
+
for await (const frame of mlbAppEvents(url, this.#accessToken, {
|
|
1351
|
+
signal: options.signal
|
|
1352
|
+
})) {
|
|
1353
|
+
const { data } = frame;
|
|
1354
|
+
if (frame.event === "ready") {
|
|
1355
|
+
await reset();
|
|
1356
|
+
} else if (data.kind !== "private" || data.id !== privateId || data.buddyId !== this.#buddyId) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (frame.event === "delta" && typeof data.text === "string" && data.text.length > 0) {
|
|
1360
|
+
const text2 = `${this.#streaming?.runId === safeRunId ? this.#streaming.text : ""}${data.text}`;
|
|
1361
|
+
if (Buffer.byteLength(text2, "utf8") > 48 * 1024) throw new MlbEventStreamError("MLB \u6D41\u5F0F\u56DE\u590D\u8D85\u8FC7\u5927\u5C0F\u9650\u5236", 502, false);
|
|
1362
|
+
this.#streaming = Object.freeze({ runId: safeRunId, text: text2, nextSequence: sequence + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1363
|
+
await options.onEvent({ type: "delta", runId: safeRunId, sequence, text: data.text });
|
|
1364
|
+
sequence++;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (frame.event !== "chat" && frame.event !== "ready") continue;
|
|
1368
|
+
const state = await this.state({ signal: options.signal });
|
|
1369
|
+
if (state.latestRun?.runId === safeRunId && (state.latestRun.status === "completed" || state.latestRun.status === "failed")) {
|
|
1370
|
+
await options.onEvent({ type: state.latestRun.status, runId: safeRunId, status: state.latestRun.status });
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (options.signal?.aborted) return;
|
|
1375
|
+
} catch (cause) {
|
|
1376
|
+
if (options.signal?.aborted) return;
|
|
1377
|
+
await reset();
|
|
1378
|
+
const status = object(cause)?.status;
|
|
1379
|
+
if (cause instanceof MlbEventStreamError && !cause.retryable || typeof status === "number" && status >= 400 && status < 500 && status !== 429 || reconnects >= 4) throw cause;
|
|
1380
|
+
}
|
|
1381
|
+
await waitForRetry(Math.min(1e3, (reconnects + 1) * 150), options.signal);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
async #ensurePrivate(signal) {
|
|
1385
|
+
if (this.#privateId) return this.#privateId;
|
|
1386
|
+
if (!this.#privatePromise) {
|
|
1387
|
+
const pending = this.#request(
|
|
1388
|
+
"POST",
|
|
1389
|
+
"/app/privates",
|
|
1390
|
+
{ signal, body: { buddyId: this.#buddyId } }
|
|
1391
|
+
).then((value) => {
|
|
1392
|
+
const body = object(value);
|
|
1393
|
+
if (body?.ok !== true || typeof body.id !== "string" || !/^p_[a-zA-Z0-9_-]{1,253}$/u.test(body.id)) {
|
|
1394
|
+
throw httpError(502, { error: "MLB \u521B\u5EFA\u79C1\u804A\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u7684 privateId" });
|
|
1395
|
+
}
|
|
1396
|
+
this.#privateId = body.id;
|
|
1397
|
+
return body.id;
|
|
1398
|
+
}).finally(() => {
|
|
1399
|
+
if (this.#privatePromise === pending) this.#privatePromise = void 0;
|
|
1400
|
+
});
|
|
1401
|
+
this.#privatePromise = pending;
|
|
1402
|
+
}
|
|
1403
|
+
return this.#privatePromise;
|
|
1404
|
+
}
|
|
1405
|
+
async #request(method, path, options) {
|
|
1406
|
+
const headers = new Headers({
|
|
1407
|
+
Accept: "application/json",
|
|
1408
|
+
Authorization: `Bearer ${this.#accessToken}`
|
|
1409
|
+
});
|
|
1410
|
+
if (options.body !== void 0) headers.set("Content-Type", "application/json");
|
|
1411
|
+
const response = await this.#fetch(endpoint(this.#baseUrl, path), {
|
|
1412
|
+
method,
|
|
1413
|
+
headers,
|
|
1414
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
1415
|
+
signal: AbortSignal.any([AbortSignal.timeout(3e4), ...options.signal ? [options.signal] : []]),
|
|
1416
|
+
redirect: "error"
|
|
1417
|
+
});
|
|
1418
|
+
const value = await response.json().catch(() => void 0);
|
|
1419
|
+
if (!response.ok) throw httpError(response.status, value);
|
|
1420
|
+
return value;
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
export {
|
|
1424
|
+
MlbSimulatorClient,
|
|
1425
|
+
PreviewController,
|
|
1426
|
+
createBuddyPreviewUrl,
|
|
1427
|
+
startPreviewControlServer
|
|
1428
|
+
};
|
|
1429
|
+
//# sourceMappingURL=preview.js.map
|