@my-life-buddies/cli 0.23.0-dev.1 → 0.23.0-rc.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.
@@ -269,7 +269,10 @@ var PreviewController = class {
269
269
  signal: controller.signal,
270
270
  onLifecycle: ({ phase, error }) => {
271
271
  if (controller.signal.aborted || !this.#handle || this.#state.phase === "stopping") return;
272
- if (phase === "reloading") this.#stopOutputWatchers();
272
+ if (phase === "reloading") {
273
+ this.#stopOutputWatchers();
274
+ void this.#simulator?.resetWidgets?.();
275
+ }
273
276
  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" });
274
277
  this.#setState({ phase, ready: this.#state.ready, ...error ? { error } : {} });
275
278
  },
@@ -294,6 +297,7 @@ var PreviewController = class {
294
297
  () => {
295
298
  if (this.#handle === handle) {
296
299
  this.#handle = void 0;
300
+ void this.#simulator?.disposeWidgets?.();
297
301
  this.#simulator = void 0;
298
302
  this.#stopOutputWatchers();
299
303
  this.#record({ channel: "dev", type: "dev.closed", summary: "DEV \u4F1A\u8BDD\u5DF2\u7ED3\u675F" });
@@ -303,6 +307,7 @@ var PreviewController = class {
303
307
  (cause) => {
304
308
  if (this.#handle === handle) {
305
309
  this.#handle = void 0;
310
+ void this.#simulator?.disposeWidgets?.();
306
311
  this.#simulator = void 0;
307
312
  this.#stopOutputWatchers();
308
313
  this.#record({ channel: "dev", type: "dev.failed", summary: "DEV \u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F" });
@@ -340,10 +345,12 @@ var PreviewController = class {
340
345
  this.#startController?.abort(new Error("Preview requested DEV stop"));
341
346
  const handle = this.#handle;
342
347
  this.#handle = void 0;
348
+ const widgetCleanup = this.#simulator?.disposeWidgets?.();
343
349
  this.#simulator = void 0;
344
350
  this.#stopOutputWatchers();
345
351
  try {
346
352
  if (handle) await handle.close();
353
+ await widgetCleanup;
347
354
  if (this.#startPromise) await this.#startPromise.catch(() => void 0);
348
355
  this.#record({ channel: "dev", type: "dev.stopped", summary: "DEV \u4F1A\u8BDD\u5DF2\u505C\u6B62" });
349
356
  return this.#setState({ phase: "idle" });
@@ -374,7 +381,7 @@ var PreviewController = class {
374
381
  const client = this.#requiredSimulator();
375
382
  const revision = this.#conversationRevision;
376
383
  let result;
377
- if (action === "list" && client.widgets) result = { widgets: await client.widgets(), conversationRevision: revision, capabilities: { documentLifecycle: true, deleteWidget: Boolean(client.deleteWidget) } };
384
+ if (action === "list" && client.widgets) result = { widgets: await client.widgets(), conversationRevision: revision, capabilities: { documentLifecycle: true, widgetEditing: Boolean(client.widgetCommand) } };
378
385
  else if (action === "html" && client.widgetHtml) {
379
386
  let target;
380
387
  if (messageId !== void 0) {
@@ -385,9 +392,6 @@ var PreviewController = class {
385
392
  }
386
393
  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 });
387
394
  result = await client.widgetHtml(id, target);
388
- } else if (action === "delete" && client.deleteWidget) {
389
- await client.deleteWidget(id);
390
- result = { ok: true };
391
395
  } else if (action === "image" && client.widgetImage) result = await client.widgetImage(id, mediaId);
392
396
  else if ((action === "pin" || action === "unpin") && client.pinWidget) {
393
397
  await client.pinWidget(id, action === "pin");
@@ -396,6 +400,17 @@ var PreviewController = class {
396
400
  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 });
397
401
  return result;
398
402
  }
403
+ async simulatorWidgetCommand(id, body) {
404
+ if (this.#resetting) throw Object.assign(new Error("\u4F1A\u8BDD\u6B63\u5728\u91CD\u7F6E"), { status: 409 });
405
+ const client = this.#requiredSimulator(), revision = this.#conversationRevision;
406
+ if (!body || typeof body !== "object" || body.conversationRevision !== revision)
407
+ throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
408
+ if (!client.widgetCommand) throw Object.assign(new Error("\u5F53\u524D\u6A21\u62DF\u5668\u4E0D\u652F\u6301 Widget \u7F16\u8F91"), { status: 501 });
409
+ const result = await client.widgetCommand(id, body);
410
+ if (this.#resetting || client !== this.#simulator || revision !== this.#conversationRevision)
411
+ throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
412
+ return result;
413
+ }
399
414
  async resetSimulator() {
400
415
  if (this.#resetting || this.#sendingMessage || this.#simulatorState.activeRun) {
401
416
  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" });
@@ -915,10 +930,10 @@ function safeEqual(left, right) {
915
930
  return a.length === b.length && timingSafeEqual(a, b);
916
931
  }
917
932
  function cookieValue(request, name) {
918
- for (const part of request.headers.cookie?.split(";") ?? []) {
919
- const separator = part.indexOf("=");
933
+ for (const part2 of request.headers.cookie?.split(";") ?? []) {
934
+ const separator = part2.indexOf("=");
920
935
  if (separator < 0) continue;
921
- if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
936
+ if (part2.slice(0, separator).trim() === name) return part2.slice(separator + 1).trim();
922
937
  }
923
938
  return void 0;
924
939
  }
@@ -952,9 +967,9 @@ function widgetDocument(response, html, render, state = "ready", status = 200) {
952
967
  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'");
953
968
  response.setHeader("Content-Type", "text/html; charset=utf-8");
954
969
  if (validWidgetRender(render)) {
955
- const signal = `<script>window.addEventListener('load',()=>parent.postMessage({channel:'buddy-widget-document',render:${JSON.stringify(render)},state:${JSON.stringify(state)}},'*'),{once:true})</script>`;
956
- if (/<head(?:\s[^>]*)?>/iu.test(html)) html = html.replace(/<head(?:\s[^>]*)?>/iu, (match) => match + signal);
957
- else if (/<!doctype[^>]*>/iu.test(html)) html = html.replace(/<!doctype[^>]*>/iu, (match) => match + signal);
970
+ const script = state === "ready" ? readFileSync(new URL("../web/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})`;
971
+ const signal = `<script>${script}</script>`;
972
+ if (/<!doctype[^>]*>/iu.test(html)) html = html.replace(/<!doctype[^>]*>/iu, (match) => match + signal);
958
973
  else html = signal + html;
959
974
  }
960
975
  response.end(html);
@@ -989,9 +1004,9 @@ async function readJsonBody(request, maximumBytes = 64 * 1024) {
989
1004
  }
990
1005
  function exactObject(value, keys) {
991
1006
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
992
- const object2 = value;
1007
+ const object3 = value;
993
1008
  const expected = new Set(keys);
994
- return Object.keys(object2).length === keys.length && Object.keys(object2).every((key) => expected.has(key)) ? object2 : null;
1009
+ return Object.keys(object3).length === keys.length && Object.keys(object3).every((key) => expected.has(key)) ? object3 : null;
995
1010
  }
996
1011
  function safeIdentifier(value, maximumLength = 200) {
997
1012
  return typeof value === "string" && value.length > 0 && value.length <= maximumLength && !/[\u0000-\u001f\u007f]/u.test(value);
@@ -1105,12 +1120,15 @@ data: ${JSON.stringify(value)}
1105
1120
  return apiError(response, failure.status, failure.code, failure.message);
1106
1121
  }
1107
1122
  }
1108
- const widgetRoute = /^\/api\/simulator\/widgets(?:\/([^/]+)(?:\/(image|pin))?)?$/u.exec(url.pathname);
1123
+ const widgetRoute = /^\/api\/simulator\/widgets(?:\/([^/]+)(?:\/(image|pin|bridge))?)?$/u.exec(url.pathname);
1109
1124
  if (widgetRoute) {
1110
1125
  try {
1111
1126
  const id = widgetRoute[1] ? decodeURIComponent(widgetRoute[1]) : "";
1112
1127
  const suffix = widgetRoute[2];
1113
- if (id && !suffix && request.method === "DELETE") return json(response, 200, await options.controller.simulatorWidgets("delete", id));
1128
+ if (id && suffix === "bridge" && request.method === "POST") {
1129
+ const body = await readJsonBody(request, 7 * 1024 * 1024);
1130
+ return json(response, 200, await options.controller.simulatorWidgetCommand(id, body));
1131
+ }
1114
1132
  if (request.method === "GET" && !id) return json(response, 200, await options.controller.simulatorWidgets("list"));
1115
1133
  if (id && suffix === "pin" && (request.method === "PUT" || request.method === "DELETE")) return json(response, 200, await options.controller.simulatorWidgets(request.method === "PUT" ? "pin" : "unpin", id));
1116
1134
  if (id && request.method === "GET" && suffix === "image") {
@@ -1269,17 +1287,204 @@ data: ${JSON.stringify({
1269
1287
  }
1270
1288
 
1271
1289
  // apps/preview/src/mlb-simulator.ts
1272
- import { createHash } from "node:crypto";
1290
+ import { createHash as createHash2 } from "node:crypto";
1291
+
1292
+ // apps/preview/src/widget-bridge.ts
1293
+ import { createHash, randomUUID } from "node:crypto";
1294
+ import { setTimeout as delay } from "node:timers/promises";
1295
+ function widgetBridgeError(status, message2) {
1296
+ return Object.assign(new Error(message2), { status, publicMessage: message2, code: "WIDGET_BRIDGE_FAILED" });
1297
+ }
1298
+ function object(value) {
1299
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw widgetBridgeError(400, "\u9700\u8981\u6709\u6548\u7684\u64CD\u4F5C\u53C2\u6570");
1300
+ return value;
1301
+ }
1302
+ function part(value) {
1303
+ 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");
1304
+ return encodeURIComponent(value);
1305
+ }
1306
+ function retryable(error) {
1307
+ const status = error?.status;
1308
+ return status === void 0 || status >= 500 || status === 429;
1309
+ }
1310
+ async function retry(run) {
1311
+ for (let attempt = 0; ; attempt++) {
1312
+ try {
1313
+ return await run();
1314
+ } catch (error) {
1315
+ if (attempt >= 2 || !retryable(error)) throw error;
1316
+ await delay(100 * (attempt + 1));
1317
+ }
1318
+ }
1319
+ }
1320
+ var WidgetBridge = class {
1321
+ #sessions = /* @__PURE__ */ new Map();
1322
+ #transport;
1323
+ #timer;
1324
+ #disposed = false;
1325
+ constructor(transport) {
1326
+ this.#transport = transport;
1327
+ this.#timer = setInterval(() => {
1328
+ for (const [key, session] of this.#sessions) if (Date.now() - session.touched > 9e4) {
1329
+ this.#sessions.delete(key);
1330
+ void this.#close(session, "page_lost").catch(() => {
1331
+ });
1332
+ }
1333
+ }, 15e3);
1334
+ this.#timer.unref();
1335
+ }
1336
+ async call(widgetId2, conversationId, value) {
1337
+ if (this.#disposed) throw widgetBridgeError(409, "\u8C03\u8BD5\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6");
1338
+ const body = object(value), render = body.render, method = body.method, requestId = body.requestId;
1339
+ 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");
1340
+ part(widgetId2);
1341
+ let session = this.#sessions.get(render);
1342
+ 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");
1343
+ if (method === "open") {
1344
+ if (session?.closed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
1345
+ if (!session) {
1346
+ if (this.#sessions.size >= 32) throw widgetBridgeError(429, "\u6253\u5F00\u7684\u5C0F\u6302\u4EF6\u8FC7\u591A\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u8BD5");
1347
+ session = {
1348
+ widgetId: widgetId2,
1349
+ conversationId,
1350
+ interactionId: `wi_${randomUUID()}`,
1351
+ deviceId: `preview_${randomUUID()}`,
1352
+ sequence: 0,
1353
+ touched: Date.now(),
1354
+ closed: false,
1355
+ pending: 0,
1356
+ tail: Promise.resolve(),
1357
+ calls: /* @__PURE__ */ new Map()
1358
+ };
1359
+ this.#sessions.set(render, session);
1360
+ session.tail = this.#transport.request("GET", `/app/widgets/${encodeURIComponent(widgetId2)}/metadata?conversationId=${encodeURIComponent(conversationId)}`).catch((error) => {
1361
+ session.closed = true;
1362
+ throw error;
1363
+ });
1364
+ void session.tail.catch(() => {
1365
+ });
1366
+ }
1367
+ await session.tail;
1368
+ if (this.#disposed || session.closed) throw widgetBridgeError(409, "\u8C03\u8BD5\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6");
1369
+ return { ready: true };
1370
+ }
1371
+ if (!session) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u8FDE\u63A5\u5DF2\u5931\u6548\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u65B0\u6253\u5F00");
1372
+ session.touched = Date.now();
1373
+ const serialized = JSON.stringify([method, body.params ?? {}]);
1374
+ if (method !== "media.upload" && Buffer.byteLength(serialized) > 300 * 1024) throw widgetBridgeError(413, "\u64CD\u4F5C\u53C2\u6570\u8FC7\u5927");
1375
+ const signature = createHash("sha256").update(serialized).digest("hex");
1376
+ const previous = session.calls.get(requestId);
1377
+ if (previous) {
1378
+ if (previous.signature !== signature) throw widgetBridgeError(409, "\u4E0D\u80FD\u4F7F\u7528\u540C\u4E00\u8BF7\u6C42\u6807\u8BC6\u6267\u884C\u4E0D\u540C\u64CD\u4F5C");
1379
+ return previous.result;
1380
+ }
1381
+ if (method === "close") return this.#close(session, "user_closed");
1382
+ if (session.closed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
1383
+ 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");
1384
+ if (session.pending >= 16) throw widgetBridgeError(429, "\u64CD\u4F5C\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u4FDD\u5B58\u5B8C\u6210");
1385
+ const current = session;
1386
+ current.pending++;
1387
+ const result = current.tail.then(() => {
1388
+ if (current.closed || this.#disposed) throw widgetBridgeError(409, "\u5C0F\u6302\u4EF6\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
1389
+ return this.#perform(current, method, object(body.params ?? {}), requestId);
1390
+ });
1391
+ current.tail = result.then(() => {
1392
+ current.pending--;
1393
+ }, () => {
1394
+ current.pending--;
1395
+ });
1396
+ if (["records.create", "records.update", "records.delete", "media.upload"].includes(method)) current.calls.set(requestId, { signature, result });
1397
+ return result;
1398
+ }
1399
+ async #perform(s, method, p, requestId) {
1400
+ const base = `/app/widgets/${encodeURIComponent(s.widgetId)}`;
1401
+ if (method === "heartbeat" || method === "resume") {
1402
+ if (!s.sequence) return { active: true };
1403
+ return this.#transport.command(`widget.interaction.${method}`, { interactionId: s.interactionId, widgetId: s.widgetId, lastAckedSequence: s.sequence });
1404
+ }
1405
+ if (method === "media.url") {
1406
+ if (typeof p.mediaId !== "string" || !/^media_[A-Za-z0-9_-]+$/u.test(p.mediaId)) throw widgetBridgeError(400, "\u56FE\u7247\u6807\u8BC6\u65E0\u6548");
1407
+ const image = await this.#transport.image(s.widgetId, p.mediaId);
1408
+ if (!/^image\/(png|jpeg|webp|gif)(?:;|$)/u.test(image.contentType)) throw widgetBridgeError(415, "\u56FE\u7247\u683C\u5F0F\u4E0D\u652F\u6301");
1409
+ return { url: `data:${image.contentType.split(";")[0]};base64,${Buffer.from(image.body).toString("base64")}` };
1410
+ }
1411
+ if (method === "media.upload") {
1412
+ 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");
1413
+ const bytes = Buffer.from(p.base64, "base64");
1414
+ if (bytes.length > 5 * 1024 * 1024 || !bytes.length) throw widgetBridgeError(413, "\u56FE\u7247\u4E0D\u80FD\u8D85\u8FC7 5 MiB");
1415
+ const result = object(await this.#transport.upload(s.conversationId, p.mimeType, bytes));
1416
+ return { mediaId: result.mediaId, width: result.width, height: result.height, mimeType: result.mimeType, size: result.size };
1417
+ }
1418
+ if (!/^records\.(list|get|create|update|delete)$/u.test(method)) throw widgetBridgeError(400, "\u9875\u9762\u8BF7\u6C42\u4E86\u672A\u5F00\u653E\u7684\u80FD\u529B");
1419
+ const path = `${base}/modules/${part(p.module)}/records`;
1420
+ if (method === "records.list" || method === "records.get") {
1421
+ return this.#transport.request("GET", `${path}${method === "records.get" ? `/${part(p.recordId)}` : ""}?conversationId=${encodeURIComponent(s.conversationId)}`);
1422
+ }
1423
+ 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");
1424
+ const action = method.slice(8);
1425
+ const mutation = { conversationId: s.conversationId };
1426
+ if (action !== "delete") mutation.data = object(p.data);
1427
+ if (p.expectedRevision !== void 0) {
1428
+ if (!Number.isSafeInteger(p.expectedRevision) || p.expectedRevision < 0) throw widgetBridgeError(400, "\u8BB0\u5F55\u7248\u672C\u53F7\u65E0\u6548");
1429
+ mutation.expectedRevision = p.expectedRevision;
1430
+ }
1431
+ if (Buffer.byteLength(JSON.stringify(mutation)) > 290 * 1024) throw widgetBridgeError(413, "\u8BB0\u5F55\u5185\u5BB9\u8FC7\u5927");
1432
+ const target = action === "create" ? path : `${path}/${part(p.recordId)}`;
1433
+ const sequence = s.sequence + 1;
1434
+ const headers = {
1435
+ "X-Widget-Interaction-Id": s.interactionId,
1436
+ "X-Widget-Operation-Id": `op_${requestId}`,
1437
+ "X-Widget-Sequence": String(sequence),
1438
+ "X-Widget-Entry-Point": "conversation_detail",
1439
+ "X-Device-Id": s.deviceId
1440
+ };
1441
+ try {
1442
+ const result = await retry(() => this.#transport.request(action === "create" ? "POST" : action === "update" ? "PUT" : "DELETE", target, mutation, headers));
1443
+ s.sequence = sequence;
1444
+ return result;
1445
+ } catch (error) {
1446
+ if (retryable(error)) s.uncertain = requestId;
1447
+ throw error;
1448
+ }
1449
+ }
1450
+ async #close(s, reason) {
1451
+ if (s.closed) return s.tail;
1452
+ s.closed = true;
1453
+ const pending = s.tail;
1454
+ const closing = pending.catch(() => {
1455
+ }).then(() => retry(() => this.#transport.request(
1456
+ "POST",
1457
+ `/app/widgets/${encodeURIComponent(s.widgetId)}/interactions/${s.interactionId}/close`,
1458
+ { conversationId: s.conversationId, lastIssuedSequence: s.sequence + (s.uncertain ? 1 : 0), closeReason: reason }
1459
+ )));
1460
+ s.tail = closing;
1461
+ void closing.catch(() => {
1462
+ });
1463
+ return closing;
1464
+ }
1465
+ async closeAll() {
1466
+ const sessions = [...this.#sessions.values()];
1467
+ this.#sessions.clear();
1468
+ await Promise.allSettled(sessions.map((s) => this.#close(s, "preview_stopped")));
1469
+ }
1470
+ async dispose() {
1471
+ this.#disposed = true;
1472
+ clearInterval(this.#timer);
1473
+ await this.closeAll();
1474
+ }
1475
+ };
1476
+
1477
+ // apps/preview/src/mlb-simulator.ts
1273
1478
  import { normalizePlatformUrl } from "./core.js";
1274
1479
 
1275
1480
  // apps/preview/src/mlb-events.ts
1276
- import { randomUUID } from "node:crypto";
1481
+ import { randomUUID as randomUUID2 } from "node:crypto";
1277
1482
  import { WebSocket } from "ws";
1278
1483
  var MlbEventStreamError = class extends Error {
1279
- constructor(message2, status = 502, retryable = true) {
1484
+ constructor(message2, status = 502, retryable2 = true) {
1280
1485
  super(message2);
1281
1486
  this.status = status;
1282
- this.retryable = retryable;
1487
+ this.retryable = retryable2;
1283
1488
  this.publicMessage = message2;
1284
1489
  }
1285
1490
  status;
@@ -1308,7 +1513,7 @@ function appSocket(url, token) {
1308
1513
  }
1309
1514
  async function mlbAppCommand(url, token, method, params, options = {}) {
1310
1515
  if (options.signal?.aborted) throw options.signal.reason;
1311
- const requestId = randomUUID();
1516
+ const requestId = randomUUID2();
1312
1517
  const socket = appSocket(url, token);
1313
1518
  return await new Promise((resolve, reject) => {
1314
1519
  let settled = false;
@@ -1515,11 +1720,11 @@ function safeText(value, name, maximum = 4096) {
1515
1720
  function endpoint(base, path) {
1516
1721
  return new URL(path.replace(/^\//u, ""), base);
1517
1722
  }
1518
- function object(value) {
1723
+ function object2(value) {
1519
1724
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1520
1725
  }
1521
1726
  function httpError(status, value) {
1522
- const serverMessage = object(value)?.error;
1727
+ const serverMessage = object2(value)?.error;
1523
1728
  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";
1524
1729
  return Object.assign(new Error(message2), {
1525
1730
  code: "MLB_REQUEST_FAILED",
@@ -1528,7 +1733,7 @@ function httpError(status, value) {
1528
1733
  });
1529
1734
  }
1530
1735
  function message(value, index) {
1531
- const input = object(value);
1736
+ const input = object2(value);
1532
1737
  if (!input || typeof input.author !== "string") return null;
1533
1738
  const role = input.author === "buddy" ? "assistant" : input.author === "user" || input.author === "owner" ? "user" : void 0;
1534
1739
  if (!role) return null;
@@ -1542,7 +1747,7 @@ function message(value, index) {
1542
1747
  throw new Error(`messages[${index}].timestamp is invalid`);
1543
1748
  }
1544
1749
  if (role === "assistant" && input.type === "question") {
1545
- const options = object(input.question)?.options;
1750
+ const options = object2(input.question)?.options;
1546
1751
  if (!Array.isArray(options) || options.length < 2 || options.length > 4 || options.some((option) => typeof option !== "string" || !option.trim() || option.length > 30)) {
1547
1752
  throw new Error(`messages[${index}].question is invalid`);
1548
1753
  }
@@ -1553,7 +1758,7 @@ ${options.map((option, i) => `${i + 1}. ${option}`).join("\n")}
1553
1758
  \u8BF7\u76F4\u63A5\u56DE\u590D\u4F60\u7684\u9009\u62E9\uFF0C\u4E5F\u53EF\u4EE5\u8865\u5145\u8BF4\u660E\u3002`;
1554
1759
  }
1555
1760
  if (role === "assistant" && input.type === "permissionRequest") {
1556
- const request = object(input.permissionRequest);
1761
+ const request = object2(input.permissionRequest);
1557
1762
  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";
1558
1763
  }
1559
1764
  return Object.freeze({
@@ -1561,7 +1766,7 @@ ${options.map((option, i) => `${i + 1}. ${option}`).join("\n")}
1561
1766
  role,
1562
1767
  text: text2,
1563
1768
  createdAt: new Date(input.timestamp).toISOString(),
1564
- ...input.type === "widget" && object(input.widget) ? { widget: parseWidget(input.widget) } : {}
1769
+ ...input.type === "widget" && object2(input.widget) ? { widget: parseWidget(input.widget) } : {}
1565
1770
  });
1566
1771
  }
1567
1772
  function widgetId(value) {
@@ -1570,7 +1775,7 @@ function widgetId(value) {
1570
1775
  return id;
1571
1776
  }
1572
1777
  function parseWidget(value) {
1573
- const item = object(value);
1778
+ const item = object2(value);
1574
1779
  const openTarget = parseOpenTarget(item?.openTarget);
1575
1780
  return Object.freeze({
1576
1781
  id: widgetId(item?.id),
@@ -1586,20 +1791,20 @@ function parseWidget(value) {
1586
1791
  }
1587
1792
  function parseOpenTarget(value) {
1588
1793
  if (value === void 0) return void 0;
1589
- const target = object(value), resource = object(target?.resource);
1794
+ const target = object2(value), resource = object2(target?.resource);
1590
1795
  const name = (input) => typeof input === "string" && /^[^\s/\u0000-\u001f\u007f]{1,64}$/u.test(input);
1591
- if (!target || target.version !== 1 || !name(target.entry) || target.resource !== void 0 && (!resource || !name(resource.module) || !name(resource.recordId)) || target.state !== void 0 && !object(target.state)) throw new TypeError("Widget \u6253\u5F00\u76EE\u6807\u683C\u5F0F\u65E0\u6548");
1796
+ 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");
1592
1797
  const normalized = {
1593
1798
  version: 1,
1594
1799
  entry: target.entry,
1595
1800
  ...resource ? { resource: { module: resource.module, recordId: resource.recordId } } : {},
1596
- ...target.state !== void 0 ? { state: object(target.state) } : {}
1801
+ ...target.state !== void 0 ? { state: object2(target.state) } : {}
1597
1802
  };
1598
1803
  if (Buffer.byteLength(JSON.stringify(normalized), "utf8") > 8 * 1024) throw new TypeError("Widget \u6253\u5F00\u76EE\u6807\u8D85\u8FC7 8 KiB");
1599
1804
  return Object.freeze(normalized);
1600
1805
  }
1601
1806
  function localReceipt(prefix, value) {
1602
- return `${prefix}-${createHash("sha256").update(value).digest("hex").slice(0, 20)}`;
1807
+ return `${prefix}-${createHash2("sha256").update(value).digest("hex").slice(0, 20)}`;
1603
1808
  }
1604
1809
  function waitForRetry(milliseconds, signal) {
1605
1810
  if (signal?.aborted) return Promise.reject(signal.reason);
@@ -1620,6 +1825,7 @@ var MlbSimulatorClient = class {
1620
1825
  #buddyId;
1621
1826
  #accessToken;
1622
1827
  #fetch;
1828
+ #widgetBridge;
1623
1829
  #sending = false;
1624
1830
  #resetTarget;
1625
1831
  #resetArchived = false;
@@ -1639,6 +1845,28 @@ var MlbSimulatorClient = class {
1639
1845
  this.#accessToken = safeText(options.auth.developerAccessToken, "developerAccessToken", 16384);
1640
1846
  this.#fetch = options.fetch ?? globalThis.fetch;
1641
1847
  if (typeof this.#fetch !== "function") throw new TypeError("A Fetch API implementation is required");
1848
+ this.#widgetBridge = new WidgetBridge({
1849
+ request: (method, path, body, headers) => this.#request(method, path, { body, headers }),
1850
+ command: (method, params) => {
1851
+ const url = endpoint(this.#baseUrl, "/app/ws");
1852
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1853
+ return mlbAppCommand(url, this.#accessToken, method, params);
1854
+ },
1855
+ image: (id, mediaId) => this.widgetImage(id, mediaId),
1856
+ upload: async (conversationId, mime, bytes) => {
1857
+ const response = await this.#fetch(endpoint(this.#baseUrl, `/app/media/uploads?conversationId=${encodeURIComponent(conversationId)}`), {
1858
+ method: "POST",
1859
+ headers: { Authorization: `Bearer ${this.#accessToken}`, "Content-Type": mime },
1860
+ body: new Uint8Array(bytes),
1861
+ redirect: "error",
1862
+ credentials: "omit",
1863
+ signal: AbortSignal.timeout(25e3)
1864
+ });
1865
+ const value = await response.json().catch(() => void 0);
1866
+ if (!response.ok) throw httpError(response.status, value);
1867
+ return value;
1868
+ }
1869
+ });
1642
1870
  }
1643
1871
  /** Reading a profile must not add the Buddy or trigger a welcome message. */
1644
1872
  async details(options = {}) {
@@ -1661,7 +1889,7 @@ var MlbSimulatorClient = class {
1661
1889
  async widgets() {
1662
1890
  this.#assertResetComplete();
1663
1891
  const id = await this.#ensurePrivate();
1664
- const body = object(await this.#request("GET", `/app/privates/${encodeURIComponent(id)}/widgets`, {}));
1892
+ const body = object2(await this.#request("GET", `/app/privates/${encodeURIComponent(id)}/widgets`, {}));
1665
1893
  if (!Array.isArray(body?.widgets)) throw httpError(502, { error: "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u5C0F\u6302\u4EF6" });
1666
1894
  return body.widgets.map(parseWidget).filter((item) => item.type !== void 0);
1667
1895
  }
@@ -1680,15 +1908,20 @@ var MlbSimulatorClient = class {
1680
1908
  const privateId = await this.#ensurePrivate();
1681
1909
  await this.#request(pinned ? "PUT" : "DELETE", `/app/widgets/${encodeURIComponent(widgetId(id))}/pin`, { ...pinned ? { body: { sourceConversationId: privateId } } : {} });
1682
1910
  }
1683
- async deleteWidget(id) {
1911
+ async widgetCommand(id, body) {
1684
1912
  this.#assertResetComplete();
1685
- const privateId = await this.#ensurePrivate();
1686
- await this.#request("DELETE", `/app/widgets/${encodeURIComponent(widgetId(id))}?conversationId=${encodeURIComponent(privateId)}`, {});
1913
+ return this.#widgetBridge.call(widgetId(id), await this.#ensurePrivate(), body);
1914
+ }
1915
+ async disposeWidgets() {
1916
+ await this.#widgetBridge.dispose();
1917
+ }
1918
+ async resetWidgets() {
1919
+ await this.#widgetBridge.closeAll();
1687
1920
  }
1688
1921
  async widgetImage(id, mediaId) {
1689
1922
  this.#assertResetComplete();
1690
1923
  if (!/^media_[a-zA-Z0-9_-]+$/u.test(mediaId)) throw new TypeError("Invalid media ID");
1691
- const access = object(await this.#request("GET", `/app/media/${encodeURIComponent(mediaId)}?widgetId=${encodeURIComponent(widgetId(id))}`, {}));
1924
+ const access = object2(await this.#request("GET", `/app/media/${encodeURIComponent(mediaId)}?widgetId=${encodeURIComponent(widgetId(id))}`, {}));
1692
1925
  const url = new URL(String(access?.url));
1693
1926
  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");
1694
1927
  return this.#widgetResource(url.href, false);
@@ -1705,11 +1938,11 @@ var MlbSimulatorClient = class {
1705
1938
  let size = 0;
1706
1939
  try {
1707
1940
  while (true) {
1708
- const part = await reader.read();
1709
- if (part.done) break;
1710
- size += part.value.byteLength;
1941
+ const part2 = await reader.read();
1942
+ if (part2.done) break;
1943
+ size += part2.value.byteLength;
1711
1944
  if (size > 4 * 1024 * 1024) throw new Error("\u5C0F\u6302\u4EF6\u5185\u5BB9\u8FC7\u5927\uFF0C\u6682\u65F6\u65E0\u6CD5\u6253\u5F00");
1712
- chunks.push(part.value);
1945
+ chunks.push(part2.value);
1713
1946
  }
1714
1947
  } finally {
1715
1948
  await reader.cancel();
@@ -1722,8 +1955,8 @@ var MlbSimulatorClient = class {
1722
1955
  const value = await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, {
1723
1956
  signal: options.signal
1724
1957
  });
1725
- const body = object(value);
1726
- const conversation = object(body?.private);
1958
+ const body = object2(value);
1959
+ const conversation = object2(body?.private);
1727
1960
  if (!body || conversation?.id !== privateId || conversation?.buddyId !== this.#buddyId || !Array.isArray(body.messages) || typeof body.replying !== "boolean") {
1728
1961
  throw httpError(502, { error: "MLB Server \u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u79C1\u804A\u72B6\u6001" });
1729
1962
  }
@@ -1786,9 +2019,9 @@ var MlbSimulatorClient = class {
1786
2019
  messages: [{ type: "text", text: input.text }],
1787
2020
  clientMessageId
1788
2021
  }, { signal: options.signal });
1789
- const receipt2 = object(value);
2022
+ const receipt2 = object2(value);
1790
2023
  const items = Array.isArray(receipt2?.items) ? receipt2.items : [];
1791
- const item2 = object(items[0]);
2024
+ const item2 = object2(items[0]);
1792
2025
  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") {
1793
2026
  throw httpError(502, { error: "MLB \u6D88\u606F\u56DE\u6267\u65E0\u6548" });
1794
2027
  }
@@ -1801,8 +2034,8 @@ var MlbSimulatorClient = class {
1801
2034
  publicMessage: "\u53D1\u9001\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u804A\u5929\u8BB0\u5F55\u540E\u518D\u91CD\u8BD5"
1802
2035
  });
1803
2036
  }
1804
- const receipt = object(value);
1805
- const item = object(receipt.items[0]);
2037
+ const receipt = object2(value);
2038
+ const item = object2(receipt.items[0]);
1806
2039
  const runId = this.#active?.runId ?? localReceipt("turn", idempotencyKey);
1807
2040
  const messageId = safeText(item.messageId, "receipt.items[0].messageId", 256);
1808
2041
  if (!this.#active) {
@@ -1832,17 +2065,18 @@ var MlbSimulatorClient = class {
1832
2065
  if (this.#sending) throw httpError(409, { error: "\u6D88\u606F\u6B63\u5728\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u6E05\u7A7A" });
1833
2066
  if (!this.#resetTarget) {
1834
2067
  const privateId = await this.#ensurePrivate(options.signal);
1835
- const current = object(await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, options));
1836
- const conversation = object(current?.private);
2068
+ const current = object2(await this.#request("GET", `/app/privates/${encodeURIComponent(privateId)}`, options));
2069
+ const conversation = object2(current?.private);
1837
2070
  if (conversation?.id !== privateId || conversation?.buddyId !== this.#buddyId || typeof current?.replying !== "boolean") {
1838
2071
  throw httpError(502, { error: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u79C1\u804A\u72B6\u6001\uFF0C\u672A\u6E05\u7A7A\u5BF9\u8BDD" });
1839
2072
  }
1840
2073
  if (current.replying) throw httpError(409, { error: "\u8BF7\u7B49\u5F85\u5F53\u524D\u56DE\u590D\u5B8C\u6210\u540E\u518D\u6E05\u7A7A" });
1841
2074
  this.#resetTarget = privateId;
2075
+ await this.#widgetBridge.closeAll();
1842
2076
  }
1843
2077
  try {
1844
2078
  if (!this.#resetArchived) {
1845
- const receipt = object(await this.#request("DELETE", `/app/privates/${encodeURIComponent(this.#resetTarget)}`, options));
2079
+ const receipt = object2(await this.#request("DELETE", `/app/privates/${encodeURIComponent(this.#resetTarget)}`, options));
1846
2080
  if (receipt?.ok !== true) throw httpError(502, { error: "\u65E0\u6CD5\u786E\u8BA4\u65E7\u4F1A\u8BDD\u5DF2\u7ED3\u675F" });
1847
2081
  this.#resetArchived = true;
1848
2082
  this.#privateId = void 0;
@@ -1853,7 +2087,7 @@ var MlbSimulatorClient = class {
1853
2087
  throw httpError(502, { error: "MLB \u672A\u8FD4\u56DE\u65B0\u7684\u4F1A\u8BDD\u8EAB\u4EFD" });
1854
2088
  }
1855
2089
  } catch (cause) {
1856
- const detail = object(cause)?.publicMessage;
2090
+ const detail = object2(cause)?.publicMessage;
1857
2091
  throw Object.assign(new Error("\u4F1A\u8BDD\u91CD\u7F6E\u5C1A\u672A\u5B8C\u6210", { cause }), {
1858
2092
  code: "RESET_INCOMPLETE",
1859
2093
  status: 502,
@@ -1927,7 +2161,7 @@ var MlbSimulatorClient = class {
1927
2161
  } catch (cause) {
1928
2162
  if (options.signal?.aborted) return;
1929
2163
  await reset();
1930
- const status = object(cause)?.status;
2164
+ const status = object2(cause)?.status;
1931
2165
  if (cause instanceof MlbEventStreamError && !cause.retryable || typeof status === "number" && status >= 400 && status < 500 && status !== 429 || reconnects >= 4) throw cause;
1932
2166
  }
1933
2167
  await waitForRetry(Math.min(1e3, (reconnects + 1) * 150), options.signal);
@@ -1941,7 +2175,7 @@ var MlbSimulatorClient = class {
1941
2175
  "/app/privates",
1942
2176
  { signal, body: { buddyId: this.#buddyId } }
1943
2177
  ).then((value) => {
1944
- const body = object(value);
2178
+ const body = object2(value);
1945
2179
  if (body?.ok !== true || typeof body.id !== "string" || !/^p_[a-zA-Z0-9_-]{1,253}$/u.test(body.id)) {
1946
2180
  throw httpError(502, { error: "MLB \u521B\u5EFA\u79C1\u804A\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u7684 privateId" });
1947
2181
  }
@@ -1957,7 +2191,8 @@ var MlbSimulatorClient = class {
1957
2191
  async #request(method, path, options) {
1958
2192
  const headers = new Headers({
1959
2193
  Accept: "application/json",
1960
- Authorization: `Bearer ${this.#accessToken}`
2194
+ Authorization: `Bearer ${this.#accessToken}`,
2195
+ ...options.headers
1961
2196
  });
1962
2197
  if (options.body !== void 0) headers.set("Content-Type", "application/json");
1963
2198
  const response = await this.#fetch(endpoint(this.#baseUrl, path), {
@@ -1976,7 +2211,7 @@ var MlbSimulatorClient = class {
1976
2211
  // apps/preview/src/bridge.ts
1977
2212
  import { hostname } from "node:os";
1978
2213
  import { randomBytes as randomBytes2 } from "node:crypto";
1979
- import { setTimeout as delay } from "node:timers/promises";
2214
+ import { setTimeout as delay2 } from "node:timers/promises";
1980
2215
  async function connectPreviewBridge(options) {
1981
2216
  const connectionId = randomBytes2(32).toString("base64url");
1982
2217
  const shutdown = new AbortController();
@@ -2139,7 +2374,7 @@ async function connectPreviewBridge(options) {
2139
2374
  break;
2140
2375
  } else if (status && status >= 400 && status < 500 && status !== 429) throw cause;
2141
2376
  }
2142
- await delay(replies.size ? 20 : 500, void 0, { signal }).catch(() => void 0);
2377
+ await delay2(replies.size ? 20 : 500, void 0, { signal }).catch(() => void 0);
2143
2378
  }
2144
2379
  } finally {
2145
2380
  clearInterval(watchdog);