@my-life-buddies/cli 0.27.1-dev.2 → 0.28.0-dev.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/dist/web/app.js DELETED
@@ -1,1155 +0,0 @@
1
- (() => {
2
- "use strict";
3
-
4
- const hosted = document.body?.hasAttribute?.("data-hosted-preview") === true;
5
- const hostedBuddyId = hosted ? new URL(location.href).searchParams.get("buddyId") : "";
6
- const connectionUrl = hosted ? new URL(`../api/buddies/${encodeURIComponent(hostedBuddyId)}/preview`, location.href) : undefined;
7
- let connectionInstance = "", connectionReady = !hosted, connectionBusy = false, devPollBusy = false;
8
- let disconnectBusy = false, connectionEpoch = 0;
9
- let connectionChecked = false, connectionFailure = "", devAction = "";
10
- let eventSource, eventSourceInstance = "", liveEventsActive = false, pollingInterval = 0;
11
- function previewApiUrl(path) {
12
- if (!hosted) return path;
13
- const url = new URL(connectionUrl.href + path);
14
- url.searchParams.set("instance", connectionInstance);
15
- return url.href;
16
- }
17
- window.previewApiUrl = previewApiUrl;
18
- const $ = (selector) => document.querySelector(selector);
19
- const elements = {
20
- projectName: $("#project-name"),
21
- connectionState: $("#connection-state"),
22
- connectionLabel: $("#connection-label"),
23
- devControl: $("#dev-control"),
24
- errorBanner: $("#error-banner"),
25
- errorMessage: $("#error-message"),
26
- resetConversation: $("#reset-conversation"),
27
- latestMessages: $("#latest-messages"),
28
- pairDevice: $("#pair-device"),
29
- pairingPanel: $("#pairing-panel"),
30
- pairingBody: $("#pairing-body"),
31
- runPicker: $("#run-picker"),
32
- previousRun: $("#previous-run"),
33
- nextRun: $("#next-run"),
34
- followLatestRun: $("#follow-latest-run"),
35
- runReference: $("#run-reference"),
36
- runId: $("#run-id"),
37
- copyRunId: $("#copy-run-id"),
38
- runTitle: $("#run-title"),
39
- runStatus: $("#run-status"),
40
- metricAccept: $("#metric-accept"),
41
- metricFirst: $("#metric-first"),
42
- metricResponse: $("#metric-response"),
43
- metricMessages: $("#metric-messages"),
44
- overviewEmpty: $("#overview-empty"),
45
- overviewContent: $("#overview-content"),
46
- waterfall: $("#waterfall"),
47
- runSummaryCopy: $("#run-summary-copy"),
48
- environmentAgent: $("#environment-agent"),
49
- environmentRuntime: $("#environment-runtime"),
50
- environmentModel: $("#environment-model"),
51
- timelineList: $("#timeline-list"),
52
- contextList: $("#context-list"),
53
- contextSource: $("#context-source"),
54
- protocolLog: $("#protocol-log"),
55
- buddyName: $("#buddy-name"),
56
- buddyStatus: $("#buddy-status"),
57
- chatMessages: $("#chat-messages"),
58
- chatEmpty: $("#chat-empty"),
59
- composer: $("#composer"),
60
- messageInput: $("#message-input"),
61
- sendMessage: $("#send-message"),
62
- toast: $("#toast"),
63
- };
64
-
65
- const state = {
66
- project: undefined,
67
- buddy: undefined,
68
- dev: { phase: "idle", revision: 0 },
69
- simulator: { messages: [] },
70
- simulatorLoaded: false,
71
- debug: {
72
- source: "observed",
73
- generatedAt: new Date().toISOString(),
74
- runs: [],
75
- events: [],
76
- messages: [],
77
- },
78
- selectedRunId: "",
79
- followLatestRun: true,
80
- reportedFailureRevision: -1,
81
- polling: undefined,
82
- pollingBusy: false,
83
- resetting: false,
84
- resetUnavailable: false,
85
- sending: false,
86
- simulatorRequest: 0,
87
- toastTimer: undefined,
88
- };
89
-
90
- function clear(node) {
91
- while (node.firstChild) node.removeChild(node.firstChild);
92
- }
93
-
94
- function node(tag, attributes = {}, text) {
95
- const element = document.createElement(tag);
96
- for (const [name, value] of Object.entries(attributes)) {
97
- if (name === "class") element.className = value;
98
- else if (name.includes("-")) element.setAttribute(name, value);
99
- else element[name] = value;
100
- }
101
- if (text !== undefined) element.textContent = text;
102
- return element;
103
- }
104
-
105
- async function api(path, options = {}) {
106
- const instance = connectionInstance;
107
- const response = await fetch(previewApiUrl(path), {
108
- ...options,
109
- headers: {
110
- Accept: "application/json",
111
- ...(options.body ? { "Content-Type": "application/json" } : {}),
112
- ...(options.headers || {}),
113
- },
114
- });
115
- const value = await response.json().catch(() => ({}));
116
- if (hosted && (disconnectBusy || !connectionReady || instance !== connectionInstance)) throw Object.assign(new Error("本地连接已改变,请等待重新连接"), {status:409, code:"PREVIEW_CONNECTION_CHANGED"});
117
- if (!response.ok) {
118
- const error = new Error((typeof value?.error === "string" ? value.error : value?.error?.message) || `请求失败(HTTP ${response.status})`);
119
- error.code = value?.error?.code || "PREVIEW_REQUEST_FAILED";
120
- error.status = response.status;
121
- throw error;
122
- }
123
- return value;
124
- }
125
-
126
- function showError(cause) {
127
- elements.errorMessage.textContent = cause instanceof Error ? cause.message : "操作未能完成";
128
- elements.errorBanner.hidden = false;
129
- }
130
-
131
- function toast(message) {
132
- window.clearTimeout(state.toastTimer);
133
- elements.toast.textContent = message;
134
- elements.toast.hidden = false;
135
- state.toastTimer = window.setTimeout(() => {
136
- elements.toast.hidden = true;
137
- }, 2200);
138
- }
139
-
140
- function formatDuration(value) {
141
- if (!Number.isFinite(value)) return "未记录";
142
- if (value < 1000) return `${Math.round(value)} ms`;
143
- return `${(value / 1000).toFixed(value < 10_000 ? 2 : 1)} s`;
144
- }
145
-
146
- function formatTime(value, seconds = true) {
147
- const date = new Date(value);
148
- if (!Number.isFinite(date.getTime())) return "未记录";
149
- return new Intl.DateTimeFormat("zh-CN", {
150
- hour: "2-digit",
151
- minute: "2-digit",
152
- ...(seconds ? { second: "2-digit" } : {}),
153
- hour12: false,
154
- }).format(date);
155
- }
156
-
157
- function dayKey(value) {
158
- const date = new Date(value);
159
- if (!Number.isFinite(date.getTime())) return undefined;
160
- return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
161
- }
162
-
163
- function dayLabel(value) {
164
- const date = new Date(value);
165
- if (!Number.isFinite(date.getTime())) return undefined;
166
- const today = new Date();
167
- const yesterday = new Date(today);
168
- yesterday.setDate(today.getDate() - 1);
169
- if (dayKey(date) === dayKey(today)) return "今天";
170
- if (dayKey(date) === dayKey(yesterday)) return "昨天";
171
- return `${date.getMonth() + 1}月${date.getDate()}日`;
172
- }
173
-
174
- function messageAvatar(role) {
175
- const avatar = node("span", {
176
- class: `message-avatar ${role === "user" ? "person-avatar" : "buddy-message-avatar"}`,
177
- "aria-hidden": "true",
178
- });
179
- if (role !== "user") {
180
- const picture = (state.dev.ready?.buddy || state.buddy)?.avatarPreview;
181
- if (picture) {
182
- const image = node("img", {src: picture, alt: ""});
183
- image.addEventListener("error", () => { avatar.textContent = "搭"; }, {once: true});
184
- avatar.append(image);
185
- } else avatar.textContent = "搭";
186
- }
187
- return avatar;
188
- }
189
-
190
- function phaseCopy(phase) {
191
- return {
192
- idle: hosted ? "已连接但 DEV 未启动" : "未启动",
193
- disconnected: "未连接本地工程",
194
- starting: "正在启动",
195
- reloading: "正在重载",
196
- reload_failed: "重载失败 · 等待修正",
197
- ready: "DEV 运行中",
198
- stopping: "正在停止",
199
- failed: "启动失败",
200
- }[phase] || phase;
201
- }
202
-
203
- function applyDevState(next) {
204
- if (!next || !Number.isFinite(next.revision)) return false;
205
- if (Number.isFinite(state.dev?.revision) && next.revision < state.dev.revision) return false;
206
- if (next.phase === "ready" && state.dev.phase !== "ready") state.simulatorLoaded = false;
207
- if (state.dev.phase === "reload_failed" && ["ready", "reloading"].includes(next.phase)) elements.errorBanner.hidden = true;
208
- state.dev = next;
209
- if (next.ready?.buddy) state.buddy = next.ready.buddy;
210
- return true;
211
- }
212
-
213
- function runStatusCopy(status) {
214
- return {
215
- accepted: "已接收",
216
- pending: "等待 Agent",
217
- running: "正在回复",
218
- cancel_requested: "正在停止",
219
- completed: "已完成",
220
- failed: "执行失败",
221
- cancelled: "已取消",
222
- idle: "等待消息",
223
- }[status] || status;
224
- }
225
-
226
- function currentRun() {
227
- const runs = state.debug.runs || [];
228
- return runs.find((run) => run.runId === state.selectedRunId) || runs[0];
229
- }
230
-
231
- function renderProject() {
232
- if (!state.project) return;
233
- const localName = state.project.buddyId || "未绑定搭子";
234
- const buddy = state.dev.ready?.buddy || state.buddy;
235
- const name = buddy?.name || localName;
236
- elements.projectName.textContent = name;
237
- elements.buddyName.textContent = name;
238
- elements.buddyName.title = buddy?.tagline || "";
239
- const picture = document.querySelector("#buddy-profile-avatar");
240
- if (picture) {
241
- picture.hidden = !buddy?.avatarPreview;
242
- if (buddy?.avatarPreview) picture.src = buddy.avatarPreview;
243
- else picture.removeAttribute("src");
244
- }
245
- const note = document.querySelector(".preview-note");
246
- if (note) note.textContent = buddy ? (buddy.isDev ? "开发预览版 · 仅你可见" : "搭搭 App") : "搭搭 App · DEV";
247
- elements.environmentRuntime.textContent = "Node.js · Buddy Runtime";
248
- if (!toolInventory) elements.environmentModel.textContent = state.project.model || "未读取";
249
- renderToolInventory();
250
- }
251
-
252
- let pairingRevision = -1;
253
- let pairingRequest = 0;
254
-
255
- function renderDev() {
256
- const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
257
- renderWorkbench();
258
- renderToolInventory();
259
- if (!elements.pairingPanel.hidden && pairingRevision !== state.dev.revision) void togglePairing(true);
260
- elements.connectionState.dataset.phase = phase;
261
- elements.connectionLabel.textContent = phase === "disconnected" ? "未连接本地工程" : phaseCopy(phase);
262
- elements.devControl.disabled = disconnectBusy || !connectionReady || Boolean(devAction) || phase === "starting" || phase === "stopping";
263
- elements.devControl.dataset.action = ["ready", "reloading", "reload_failed"].includes(phase) ? "stop" : "start";
264
- elements.devControl.textContent = devAction === "start" ? "正在启动…" : devAction === "stop" ? "正在停止…" : phase === "failed" ? "重试启动" : ["ready", "reloading", "reload_failed"].includes(phase) ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
265
- elements.environmentAgent.textContent = phase === "ready"
266
- ? `${state.dev.ready?.agentId || "已就绪"} · CLI 托管`
267
- : phaseCopy(phase);
268
-
269
- const active = state.simulator.activeRun;
270
- elements.pairDevice.disabled = !connectionReady || phase !== "ready";
271
- elements.buddyStatus.textContent = phase !== "ready"
272
- ? phaseCopy(phase)
273
- : active
274
- ? state.simulator.streaming?.text
275
- ? "正在输入…"
276
- : "正在思考…"
277
- : "DEV 在线";
278
- elements.resetConversation.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending || state.resetUnavailable;
279
- elements.resetConversation.title = state.resetUnavailable ? "当前平台暂不支持重开会话" : active ? "请等待当前回复完成后再清空" : "结束当前开发版对话,开始一段全新会话";
280
- elements.devControl.disabled ||= state.resetting;
281
- elements.messageInput.disabled = phase !== "ready" || !state.simulatorLoaded || state.resetting || state.sending;
282
- elements.sendMessage.hidden = false;
283
- updateSendButton();
284
- syncPolling();
285
- if (
286
- (phase === "failed" || phase === "reload_failed")
287
- && state.dev.error?.message
288
- && state.reportedFailureRevision !== state.dev.revision
289
- ) {
290
- state.reportedFailureRevision = state.dev.revision;
291
- showError(new Error(state.dev.error.message));
292
- }
293
- }
294
-
295
- function renderWorkbench() {
296
- if (!hosted) return;
297
- const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
298
- const runs = state.debug.runs || [];
299
- const active = state.simulator.activeRun;
300
- document.body.dataset.connection = connectionReady ? "connected" : "disconnected";
301
- document.body.dataset.devPhase = phase;
302
- document.body.dataset.hasRuns = String(runs.length > 0);
303
- document.body.dataset.busy = String(phase === "starting" || phase === "stopping" || state.sending || Boolean(active));
304
- const guide = $("#workbench-state");
305
- const history = Boolean(state.simulator.messages?.length);
306
- $("#debug-buddy-name").textContent = state.buddy?.name || new URL(location.href).searchParams.get("buddyName") || "我的搭子";
307
- const copy = !connectionChecked ? ["正在检查连接", "读取本地工程的连接状态。"]
308
- : phase === "disconnected" ? [connectionInstance ? "本地连接已断开" : "连接本地工程", connectionFailure || (connectionInstance ? "正在等待重新连接。保留当前记录,连接恢复后继续调试。" : "点击上方「连接工程」,复制指令到终端运行。")]
309
- : phase === "starting" ? ["正在启动调试", "正在准备运行环境,启动完成后即可发送消息。"]
310
- : phase === "reloading" ? ["正在重载", "本地文件已修改,正在重新加载;当前回复可能中断。"]
311
- : phase === "reload_failed" ? ["重载失败", state.dev.error?.message || "修正文件并保存后自动重试,也可停止 DEV。"]
312
- : phase === "stopping" ? ["正在停止调试", "等待运行环境退出,当前记录会保留。"]
313
- : phase === "failed" ? ["调试未能启动", "查看上方错误信息,修复工程后点击「重试启动」。"]
314
- : phase === "idle" ? ["工程已连接", "点击上方「启动 DEV」,开始体验搭子。"]
315
- : !state.simulatorLoaded ? ["正在读取对话", "同步当前会话,完成后即可发送消息。"]
316
- : state.sending ? ["正在发送消息", "提交成功后,这里会显示消息与模型日志。"]
317
- : active ? ["等待执行记录", "消息已提交,正在同步消息与模型日志。"]
318
- : ["开始一轮调试", history ? "右侧保留了历史对话。本地调试记录从本次连接开始,发一条新消息即可查看。" : "在右侧发一条消息,观察搭子的回复与模型日志。"];
319
- guide.hidden = phase === "ready" && (runs.length > 0 || (state.debug.diagnostics?.records || []).some(record => record.kind !== "log"));
320
- $("#workbench-state-title").textContent = copy[0];
321
- $("#workbench-state-copy").textContent = copy[1];
322
- $("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "保存自动重载,可能中断回复;Widget 数据不自动迁移。";
323
- $("#force-disconnect").hidden = !connectionInstance;
324
- $("#force-disconnect").disabled = disconnectBusy;
325
- $("#force-disconnect").textContent = disconnectBusy ? "正在断开…" : "强制断连";
326
- const connectButton = $("#get-connect-command");
327
- connectButton.className = `button ${connectionReady ? "button-secondary" : "button-primary"}`;
328
- connectButton.textContent = connectionReady ? "连接指令" : connectionInstance ? "重新连接" : "连接工程";
329
- const hint = $("#preview-hint");
330
- hint.textContent = phase !== "ready" ? (phase === "disconnected" ? "连接工程后开始体验" : phase === "starting" ? "正在启动,稍后即可发送" : "启动 DEV 后开始体验")
331
- : !state.simulatorLoaded ? "正在读取对话,请稍候" : state.resetting ? "正在清空对话,请稍候" : state.sending ? "正在发送,请稍候"
332
- : active ? (state.simulator.streaming?.text ? "正在生成回复" : "消息已送达,等待回复") : "Enter 发送,Shift + Enter 换行";
333
- if (!state.project) elements.buddyName.textContent = new URL(location.href).searchParams.get("buddyName") || "我的搭子";
334
- const empty = elements.chatMessages.querySelector(".chat-empty");
335
- if (empty) empty.textContent = phase === "ready" ? "发一条消息,开始体验" : phase === "disconnected" ? "连接后,在这里体验搭子的回复" : "启动 DEV 后,开始对话";
336
- elements.messageInput.placeholder = active ? "可以继续补充消息" : phase === "ready" ? "给搭子发一条消息" : "等待调试就绪";
337
- }
338
-
339
- function runOrdinal(run, index, runs) {
340
- // Older running preview servers have no ordinal yet. Never infer a message
341
- // association from list positions or timestamps; this is a display number only.
342
- return Number.isInteger(run.ordinal) && run.ordinal > 0 ? run.ordinal : runs.length - index;
343
- }
344
-
345
- function runOptionLabel(run, index, runs) {
346
- const date = dayKey(run.startedAt) === dayKey(new Date()) ? "" : `${dayLabel(run.startedAt) || ""} `;
347
- return `第 ${runOrdinal(run, index, runs)} 轮 · ${date}${formatTime(run.startedAt)} · ${(run.completion === "no_reply" && run.status === "completed" ? "未观察到回复" : runStatusCopy(run.status))}`;
348
- }
349
-
350
- let runPickerSignature = "";
351
-
352
- function renderRuns() {
353
- const runs = state.debug.runs || [];
354
- const choosing = document.activeElement === elements.runPicker;
355
- if (!state.selectedRunId || !runs.some((run) => run.runId === state.selectedRunId)) {
356
- state.followLatestRun = true;
357
- state.selectedRunId = runs[0]?.runId || "";
358
- } else if (state.followLatestRun && !choosing) {
359
- state.selectedRunId = runs[0].runId;
360
- }
361
-
362
- const options = runs.map((run, index) => [run.runId, runOptionLabel(run, index, runs)]);
363
- const signature = JSON.stringify(options);
364
- // Polling must not rebuild an open native menu or override its keyboard selection.
365
- // Apply deferred changes on blur; unchanged snapshots keep the same option nodes.
366
- if (!choosing) {
367
- if (signature !== runPickerSignature) {
368
- elements.runPicker.replaceChildren(...(options.length ? options : [["", "尚无运行记录"]])
369
- .map(([value, label]) => node("option", { value }, label)));
370
- runPickerSignature = signature;
371
- }
372
- elements.runPicker.value = state.selectedRunId;
373
- }
374
- elements.runPicker.disabled = !runs.length;
375
- const index = runs.findIndex((run) => run.runId === state.selectedRunId);
376
- elements.previousRun.disabled = index < 0 || index >= runs.length - 1;
377
- elements.nextRun.disabled = index <= 0;
378
- elements.followLatestRun.checked = state.followLatestRun;
379
-
380
- const run = currentRun();
381
- elements.runTitle.textContent = run ? `第 ${runOrdinal(run, index, runs)} 轮` : "尚无本地回合";
382
- elements.runReference.hidden = !run;
383
- elements.runId.textContent = run?.runId || "";
384
- elements.runId.title = run?.runId || "";
385
- elements.copyRunId.disabled = !run;
386
- elements.runStatus.dataset.status = run?.status || "idle";
387
- elements.runStatus.textContent = run?.completion === "no_reply" && run.status === "completed" ? "未观察到回复" : runStatusCopy(run?.status || "idle");
388
- elements.metricAccept.textContent = formatElapsedSeconds(run?.acceptLatencyMs);
389
- elements.metricFirst.textContent = formatElapsedSeconds(run?.firstTextLatencyMs);
390
- elements.metricResponse.textContent = run?.completion === "no_reply" ? "未观察到回复" : formatElapsedSeconds(replyEndLatency(run));
391
- elements.metricMessages.textContent = state.simulatorLoaded ? String(state.simulator.messages?.length || 0) : "读取中";
392
- renderOverview(run);
393
- renderTimeline(run);
394
- }
395
-
396
- function selectRun(runId) {
397
- if (!(state.debug.runs || []).some((run) => run.runId === runId)) return;
398
- state.selectedRunId = runId;
399
- state.followLatestRun = false;
400
- renderDebug();
401
- }
402
-
403
- function stepRun(offset) {
404
- const runs = state.debug.runs || [];
405
- const index = runs.findIndex((run) => run.runId === state.selectedRunId);
406
- if (index >= 0 && runs[index + offset]) selectRun(runs[index + offset].runId);
407
- }
408
-
409
- async function copyRunId() {
410
- const run = currentRun();
411
- if (!run) return;
412
- try {
413
- await navigator.clipboard.writeText(run.runId);
414
- toast("回合 ID 已复制");
415
- } catch {
416
- showError(new Error("浏览器未允许复制,请手动选择回合 ID"));
417
- }
418
- }
419
-
420
- function formatElapsedSeconds(value) {
421
- return Number.isFinite(value) ? `${Number((value / 1000).toFixed(3))} 秒` : "未记录";
422
- }
423
-
424
- function replyEndLatency(run) {
425
- if (run?.status !== "completed") return undefined;
426
- // updatedAt records Preview observing the terminal state. responseTimeMs and
427
- // completedAt use the platform message timestamp and are not reply-end timings.
428
- const elapsed = Date.parse(run.updatedAt) - Date.parse(run.startedAt);
429
- return Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : undefined;
430
- }
431
-
432
- function renderOverview(run) {
433
- elements.overviewEmpty.hidden = Boolean(run);
434
- elements.overviewContent.hidden = !run;
435
- if (!run) return;
436
- const finished = ["completed", "failed", "cancelled"].includes(run.status);
437
- const measuredFirstText = Number.isFinite(run.firstTextLatencyMs);
438
- const hasText = measuredFirstText || (state.simulator.streaming?.runId === run.runId && Boolean(state.simulator.streaming.text));
439
- const noReply = run.completion === "no_reply";
440
- const interrupted = ["failed", "cancelled", "cancel_requested"].includes(run.status);
441
- const progress = $("#run-progress");
442
- if (progress) {
443
- clear(progress);
444
- const stages = [
445
- ["消息已提交", "done"],
446
- ["等待回复", hasText || finished ? "done" : "active"],
447
- [finished && !hasText ? "未记录流式输出" : "生成回复", hasText ? (finished ? "done" : "active") : finished ? "skipped" : "waiting"],
448
- [run.status === "failed" ? "执行失败" : run.status === "cancelled" ? "已停止" : noReply ? "未观察到回复" : "完成", finished ? (interrupted ? "error" : "done") : "waiting"],
449
- ];
450
- if (interrupted) stages.forEach(stage => { if (stage[1] === "active") stage[1] = "error"; });
451
- for (const [label, status] of stages) progress.append(node("li", { "data-stage": status }, label));
452
- }
453
- clear(elements.waterfall);
454
- // Only report observed timings. A finished run with missing telemetry never keeps counting.
455
- const timingRows = [["消息被接收", run.acceptLatencyMs], ["首字耗时", run.firstTextLatencyMs], ["回复结束", noReply ? undefined : replyEndLatency(run)]];
456
- const maximum = Math.max(1, ...timingRows.map(([,value]) => Number.isFinite(value) ? value : 0));
457
- elements.waterfall.hidden = !timingRows.some(([,value]) => Number.isFinite(value));
458
- const timingHeading = node("div", { class: "timing-heading" });
459
- timingHeading.append(node("span", {}, "起点:Preview 发起发送(0 秒)"), node("span", {}, "累计时间,不可相加"));
460
- elements.waterfall.append(timingHeading);
461
- for (const [label, duration] of timingRows) {
462
- if (!Number.isFinite(duration)) continue;
463
- const row = node("div", { class: "waterfall-row" });
464
- row.append(node("span", {}, label));
465
- const track = node("span", { class: "waterfall-track" });
466
- const fill = node("span", { class: "waterfall-fill" });
467
- fill.style.transform = `scaleX(${duration / maximum})`;
468
- track.append(fill);
469
- row.append(track, node("strong", {}, `发送后 ${formatElapsedSeconds(duration)}`));
470
- elements.waterfall.append(row);
471
- }
472
- elements.runSummaryCopy.textContent = run.status === "failed"
473
- ? "本轮执行失败。请查看运行日志中的错误详情。"
474
- : run.status === "cancelled" ? "本轮已停止。已完成的平台写入不会自动撤销。"
475
- : noReply ? "回复状态已结束,未观察到新的可见回复;不据此判断执行是否成功。"
476
- : run.status === "completed" ? (measuredFirstText ? "" : "本轮未记录首字耗时。")
477
- : run.status === "cancel_requested" ? "正在停止,结束后可以继续发送消息。"
478
- : hasText ? "正在生成回复,右侧同步展示输出。" : "消息已提交,等待搭子开始回复。";
479
- elements.runSummaryCopy.hidden = !elements.runSummaryCopy.textContent;
480
- }
481
-
482
- let timelineSignature = "", contextSignature = "", protocolSignature = "";
483
- const expandedLogs = new Map();
484
- const diagnostics = window.createPreviewDiagnostics?.({ node, formatTime, formatDuration, toast, showError });
485
- diagnostics?.update(state.debug);
486
- function renderTimeline(run) {
487
- const signature = JSON.stringify([run?.runId, state.debug.events]);
488
- if (signature === timelineSignature) return;
489
- timelineSignature = signature;
490
- clear(elements.timelineList);
491
- const events = (state.debug.events || []).filter((event) => event.type !== "run.output.delta" && (!run || event.runId === run.runId));
492
- if (!events.length) {
493
- elements.timelineList.append(node("li", { class: "timeline-empty" }, run ? "这个本地回合暂无可见事件。" : "发一条消息后,执行事件会出现在这里。"));
494
- return;
495
- }
496
- for (const event of [...events].sort((a, b) => (Date.parse(b.timestamp) || 0) - (Date.parse(a.timestamp) || 0))) {
497
- const item = node("li", { class: "timeline-item" });
498
- item.append(node("time", { class: "timeline-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
499
- item.append(node("span", { class: "timeline-dot", "aria-hidden": "true" }));
500
- const copy = node("div", { class: "timeline-copy" });
501
- copy.append(node("strong", {}, event.summary));
502
- copy.append(node("p", {}, `${event.type}${Number.isFinite(event.durationMs) ? ` · ${formatDuration(event.durationMs)}` : ""}`));
503
- item.append(copy);
504
- elements.timelineList.append(item);
505
- }
506
- }
507
-
508
- function renderContext() {
509
- const signature = JSON.stringify(state.simulator.messages);
510
- if (signature === contextSignature) return;
511
- contextSignature = signature;
512
- clear(elements.contextList);
513
- elements.contextSource.textContent = "平台聊天记录";
514
- const messages = state.simulator.messages || [];
515
- if (!messages.length) {
516
- elements.contextList.append(node("p", { class: "timeline-empty" }, "当前会话还没有消息。"));
517
- return;
518
- }
519
- for (const message of [...messages].reverse().sort((a, b) => (Date.parse(b.createdAt) || 0) - (Date.parse(a.createdAt) || 0))) {
520
- const row = node("article", { class: "context-row", "data-role": message.role });
521
- row.append(node("span", { class: "context-role" }, message.role === "user" ? "用户" : message.role === "assistant" ? "搭子" : message.role));
522
- row.append(node("p", { class: "context-text" }, message.text));
523
- row.append(node("time", { class: "context-time", dateTime: message.createdAt }, formatTime(message.createdAt)));
524
- elements.contextList.append(row);
525
- }
526
- }
527
-
528
- function renderProtocol() {
529
- if (diagnostics) { diagnostics.update(state.debug); return; }
530
- const signature = JSON.stringify(state.debug.events);
531
- if (signature === protocolSignature) return;
532
- protocolSignature = signature;
533
- clear(elements.protocolLog);
534
- const events = state.debug.events || [];
535
- if (!events.length) {
536
- elements.protocolLog.append(node("p", { class: "timeline-empty" }, "本机还没有协议事件。"));
537
- return;
538
- }
539
- for (const [index, event] of [...events].sort((a, b) => (Date.parse(b.timestamp) || 0) - (Date.parse(a.timestamp) || 0)).entries()) {
540
- if (hosted) {
541
- const key = event.eventId || `${event.timestamp}:${event.type}:${index}`;
542
- const row = node("details", { class: "protocol-entry" });
543
- const error = event.type === "agent.stderr" || /failed|error/.test(event.type);
544
- row.open = expandedLogs.get(key) ?? error;
545
- row.dataset.level = error ? "error" : "info";
546
- row.addEventListener("toggle", () => expandedLogs.set(key, row.open));
547
- const summary = node("summary");
548
- summary.append(node("time", { dateTime: event.timestamp }, formatTime(event.timestamp)));
549
- summary.append(node("span", { class: "log-summary" }, (event.summary || event.type).split("\n")[0]));
550
- summary.append(node("span", { class: "log-channel" }, event.channel));
551
- row.append(summary);
552
- row.append(node("p", { class: "log-meta" }, `${event.type}${event.runId ? ` / ${event.runId}` : ""}${Number.isFinite(event.durationMs) ? ` / ${formatDuration(event.durationMs)}` : ""}`));
553
- if (event.summary?.includes("\n")) row.append(node("pre", { class: "runtime-log-text" }, event.summary));
554
- elements.protocolLog.append(row);
555
- continue;
556
- }
557
- const row = node("div", { class: "protocol-row" });
558
- row.append(node("time", { class: "protocol-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
559
- row.append(node("span", { class: "protocol-channel" }, event.channel));
560
- row.append(node("span", { class: "protocol-type", title: event.summary }, event.type));
561
- row.append(node("span", { class: "protocol-duration" }, formatDuration(event.durationMs)));
562
- if (event.type === "agent.stdout" || event.type === "agent.stderr") {
563
- row.append(node("pre", { class: "runtime-log-text" }, event.summary));
564
- }
565
- elements.protocolLog.append(row);
566
- }
567
- }
568
-
569
- const widgets = window.createSimulatorWidgets?.({ api, avatar: () => messageAvatar("assistant") });
570
- const products = window.createSimulatorProducts?.({ api, refresh: () => refreshSimulator() });
571
- function renderWidgets() { widgets?.update({ reloading: ["reloading", "reload_failed"].includes(state.dev.phase), active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision }); }
572
-
573
- let messageSignature = "";
574
- let followMessages = true;
575
- let liveReply, liveReplyBubble, liveReplyText = "";
576
- function renderMessageText(bubble, text) {
577
- window.renderPreviewMarkdown(bubble, text);
578
- }
579
- function renderLiveReply() {
580
- if (!liveReply) return;
581
- const text = state.simulator.streaming?.runId === state.simulator.activeRun?.runId ? state.simulator.streaming?.text || "" : "";
582
- if (text === liveReplyText) return;
583
- liveReplyText = text;
584
- if (text) {
585
- if (!liveReplyBubble) {
586
- clear(liveReply);
587
- liveReplyBubble = node("div", { class: "message-bubble", "aria-label": "搭子正在回复" });
588
- liveReply.append(liveReplyBubble);
589
- }
590
- renderMessageText(liveReplyBubble, text);
591
- }
592
- else {
593
- clear(liveReply); liveReplyBubble = undefined;
594
- const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
595
- indicator.append(node("i"), node("i"), node("i")); liveReply.append(indicator);
596
- }
597
- if (followMessages) followLatestMessages();
598
- }
599
-
600
- function followLatestMessages() {
601
- followMessages = true;
602
- elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
603
- elements.latestMessages.hidden = true;
604
- }
605
-
606
- function renderMessages() {
607
- renderWidgets();
608
- products?.update({ active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision });
609
- const signature = JSON.stringify([state.simulator.messages, state.simulator.activeRun?.runId, state.dev.phase, state.resetting]);
610
- if (signature === messageSignature) { renderLiveReply(); return; }
611
- messageSignature = signature;
612
- const previousTop = elements.chatMessages.scrollTop;
613
- clear(elements.chatMessages);
614
- liveReply = undefined; liveReplyBubble = undefined; liveReplyText = "";
615
- const messages = state.simulator.messages || [];
616
- if (!messages.length && !state.simulator.activeRun) {
617
- elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }, hosted ? (state.dev.phase === "ready" ? "发一条消息,开始体验" : "启动 DEV 后,开始对话") : ""));
618
- }
619
- let renderedDay;
620
- for (const message of messages) {
621
- const messageDay = dayKey(message.createdAt);
622
- if (messageDay && messageDay !== renderedDay && dayLabel(message.createdAt) !== "今天") {
623
- elements.chatMessages.append(node("div", { class: "chat-date" }, dayLabel(message.createdAt)));
624
- renderedDay = messageDay;
625
- }
626
- const row = node("div", { class: "message-row", "data-role": message.role });
627
- const wrap = node("div", { class: "message-content" });
628
- if (message.product && products) wrap.append(products.card(message.product));
629
- else if (message.widget && widgets) wrap.append(widgets.card(message.widget, message.messageId));
630
- else {
631
- const bubble = node("div", { class: "message-bubble" });
632
- renderMessageText(bubble, message.text);
633
- wrap.append(bubble);
634
- }
635
- if (message.role === "user") row.append(wrap);
636
- else row.append(messageAvatar("assistant"), wrap);
637
- elements.chatMessages.append(row);
638
- }
639
- if (state.simulator.activeRun) {
640
- const row = node("div", { class: "message-row typing-row", "data-role": "assistant" });
641
- liveReply = node("div", { class: "message-content" });
642
- liveReplyText = "\0";
643
- renderLiveReply();
644
- row.append(messageAvatar("assistant"), liveReply);
645
- elements.chatMessages.append(row);
646
- }
647
- if (followMessages) followLatestMessages();
648
- else {
649
- elements.chatMessages.scrollTop = previousTop;
650
- elements.latestMessages.hidden = false;
651
- }
652
- }
653
-
654
- function renderDebug() {
655
- renderRuns();
656
- renderContext();
657
- renderProtocol();
658
- renderWorkbench();
659
- }
660
-
661
- function renderAll() {
662
- renderProject();
663
- renderDev();
664
- renderDebug();
665
- renderMessages();
666
- }
667
-
668
- async function refreshDebug() {
669
- const request = state.simulatorRequest;
670
- const value = await api("/api/debug");
671
- if (state.resetting || request !== state.simulatorRequest) return;
672
- state.debug = value.debug;
673
- renderDebug();
674
- }
675
-
676
- async function refreshSimulator() {
677
- if (state.dev.phase !== "ready" || state.pollingBusy) return;
678
- if (state.resetting) return;
679
- const request = state.simulatorRequest;
680
- state.pollingBusy = true;
681
- try {
682
- const value = await api("/api/simulator/state");
683
- if (state.resetting || request !== state.simulatorRequest || !applySimulatorState(value.state)) return;
684
- renderDev();
685
- renderMessages();
686
- renderContext();
687
- await refreshDebug();
688
- } catch (cause) {
689
- if (hosted && (!connectionReady || cause?.status === 409)) void refreshConnection();
690
- else if (cause?.code !== "SIMULATOR_NOT_READY") showError(cause);
691
- } finally {
692
- state.pollingBusy = false;
693
- }
694
- }
695
-
696
- function syncPolling() {
697
- const interval = liveEventsActive ? 30000 : 1200;
698
- if (state.polling !== undefined && pollingInterval !== interval) {
699
- window.clearInterval(state.polling); state.polling = undefined;
700
- }
701
- if (connectionReady && state.dev.phase === "ready" && state.polling === undefined) {
702
- void refreshSimulator();
703
- pollingInterval = interval;
704
- state.polling = window.setInterval(() => void refreshSimulator(), interval);
705
- } else if ((!connectionReady || state.dev.phase !== "ready") && state.polling !== undefined) {
706
- window.clearInterval(state.polling);
707
- state.polling = undefined;
708
- }
709
- }
710
-
711
- async function controlDev() {
712
- if (disconnectBusy || !connectionReady || state.dev.phase === "starting" || state.dev.phase === "stopping") return;
713
- if (devAction) return;
714
- devAction = ["ready", "reloading", "reload_failed"].includes(state.dev.phase) ? "stop" : "start";
715
- elements.errorBanner.hidden = true;
716
- renderDev();
717
- try {
718
- const path = `/api/dev/${devAction}`;
719
- const value = await api(path, { method: "POST" });
720
- applyDevState(value.state);
721
- if (state.dev.phase !== "ready" && path.endsWith("stop")) state.simulator = { messages: state.simulator.messages || [] };
722
- renderAll();
723
- } catch (cause) {
724
- showError(cause);
725
- } finally {
726
- devAction = "";
727
- renderDev();
728
- }
729
- }
730
-
731
- function updateSendButton() {
732
- const ready = connectionReady && !disconnectBusy && !devAction && state.simulatorLoaded && state.dev.phase === "ready" && !state.resetting && !state.sending;
733
- elements.sendMessage.disabled = !ready || !elements.messageInput.value.trim();
734
- }
735
-
736
- let pendingSend;
737
-
738
- async function sendMessage(event) {
739
- event.preventDefault();
740
- const text = elements.messageInput.value;
741
- if (!text.trim() || !state.simulatorLoaded || devAction || state.dev.phase !== "ready" || state.resetting || state.sending) return;
742
- state.sending = true;
743
- elements.errorBanner.hidden = true;
744
- followLatestMessages();
745
- renderDev();
746
- elements.messageInput.disabled = true;
747
- elements.sendMessage.disabled = true;
748
- // Keep the logical message identity after an unknown outcome; retries are user initiated.
749
- if (!pendingSend || pendingSend.text !== text) {
750
- pendingSend = { text, idempotencyKey: window.crypto.randomUUID
751
- ? window.crypto.randomUUID()
752
- : `preview-${Date.now()}-${Math.random().toString(16).slice(2)}` };
753
- }
754
- const { idempotencyKey } = pendingSend;
755
- try {
756
- const accepted = await api("/api/simulator/messages", {
757
- method: "POST",
758
- body: JSON.stringify({ text, idempotencyKey }),
759
- });
760
- pendingSend = undefined;
761
- elements.messageInput.value = "";
762
- resizeComposer();
763
- if (state.simulator.latestRun?.runId !== accepted.runId) state.simulator.activeRun = { runId: accepted.runId, status: "pending" };
764
- renderDev();
765
- renderMessages();
766
- void refreshSimulator();
767
- } catch (cause) {
768
- showError(cause);
769
- } finally {
770
- state.sending = false;
771
- renderDev();
772
- }
773
- }
774
-
775
- function resizeComposer() {
776
- elements.messageInput.style.height = "auto";
777
- elements.messageInput.style.height = `${Math.min(elements.messageInput.scrollHeight, 78)}px`;
778
- updateSendButton();
779
- }
780
-
781
- function activateTab(tab) {
782
- const tabs = [...document.querySelectorAll('[role="tab"]')];
783
- for (const item of tabs) {
784
- const selected = item === tab;
785
- item.setAttribute("aria-selected", String(selected));
786
- item.tabIndex = selected ? 0 : -1;
787
- const panel = document.getElementById(item.getAttribute("aria-controls"));
788
- panel.hidden = !selected;
789
- }
790
- }
791
-
792
- const toolInventory = window.createPreviewToolInventory?.({ node });
793
- function renderToolInventory() {
794
- toolInventory?.update({
795
- inspection: state.dev.inspection ?? state.project?.inspection,
796
- fallbackModel: state.project?.model,
797
- connected: connectionReady,
798
- buddyId: state.project?.buddyId,
799
- });
800
- }
801
-
802
- async function togglePairing(force) {
803
- const requestId = ++pairingRequest;
804
- pairingRevision = state.dev.revision;
805
- const open = force ?? elements.pairingPanel.hidden;
806
- elements.pairingPanel.hidden = !open;
807
- elements.pairDevice.setAttribute("aria-expanded", String(open));
808
- if (!open) return;
809
- clear(elements.pairingBody);
810
- elements.pairingBody.append(node("p", { class: "pairing-status" }, "正在读取真机入口…"));
811
- try {
812
- const { pairing } = await api("/api/device/pairing");
813
- if (requestId !== pairingRequest || elements.pairingPanel.hidden) return;
814
- const copy = node("div");
815
- clear(elements.pairingBody);
816
- if (pairing.status === "ready") {
817
- if (typeof pairing.qrCodeDataUrl !== "string" || !pairing.qrCodeDataUrl.startsWith("data:image/png;base64,")) {
818
- throw new Error("二维码生成失败,请重新打开真机预览。");
819
- }
820
- elements.pairingBody.append(node("img", {
821
- class: "pairing-qr", src: pairing.qrCodeDataUrl, width: "256", height: "256", alt: "打开当前开发版搭子的二维码",
822
- }));
823
- copy.append(node("p", { class: "pairing-status" }, "真机入口已准备好"));
824
- copy.append(node("p", { class: "pairing-hint" }, "使用支持 App 链接的扫码工具打开。手机需安装支持预览入口的搭搭 App,并登录同一创造者账号。"));
825
- if (pairing.code) copy.append(node("p", { class: "pairing-code" }, pairing.code));
826
- const link = node("a", { class: "pairing-link", href: pairing.url }, pairing.url);
827
- copy.append(link);
828
- } else {
829
- copy.append(node("p", { class: "pairing-status" }, "真机预览暂不可用"));
830
- copy.append(node("p", { class: "pairing-hint" }, pairing.reason));
831
- }
832
- elements.pairingBody.append(copy);
833
- } catch (cause) {
834
- if (requestId !== pairingRequest || elements.pairingPanel.hidden) return;
835
- clear(elements.pairingBody);
836
- elements.pairingBody.append(node("p", { class: "pairing-status" }, cause instanceof Error ? cause.message : "真机入口读取失败"));
837
- }
838
- }
839
-
840
- async function copyProtocol() {
841
- if (diagnostics) return diagnostics.copyLogs();
842
- const text = (state.debug.events || []).map((event) => [
843
- event.timestamp,
844
- event.channel,
845
- event.type,
846
- event.runId || "-",
847
- Number.isFinite(event.durationMs) ? `${event.durationMs}ms` : "-",
848
- event.summary,
849
- ].join("\t")).join("\n");
850
- try {
851
- await navigator.clipboard.writeText(text);
852
- toast("协议日志已复制");
853
- } catch {
854
- showError(new Error("浏览器未允许复制,请在协议日志中手动选择"));
855
- }
856
- }
857
-
858
- function applySimulatorState(next) {
859
- if ((next.conversationRevision || 0) < (state.simulator.conversationRevision || 0)) return false;
860
- if (Number.isFinite(next.stateRevision) && Number.isFinite(state.simulator.stateRevision)
861
- && next.stateRevision < state.simulator.stateRevision) return false;
862
- state.simulator = next;
863
- state.simulatorLoaded = true;
864
- return true;
865
- }
866
-
867
- async function resetConversation() {
868
- if (elements.resetConversation.disabled) return;
869
- if (!window.confirm("清空当前对话并重新开始?\n这会结束当前开发版会话,重置聊天上下文、记忆和该会话的主动服务。旧记录仅在服务端留档;搭子资料和工程文件会保留。")) return;
870
- state.resetting = true;
871
- elements.errorBanner.hidden = true;
872
- widgets?.reset(); products?.reset();
873
- state.simulatorRequest++;
874
- elements.resetConversation.textContent = "正在清空…";
875
- renderDev();
876
- try {
877
- const value = await api("/api/simulator/reset", { method: "POST", body: JSON.stringify({ confirm: true }) });
878
- applySimulatorState(value.state);
879
- applyDevState(value.dev);
880
- state.debug = value.debug;
881
- state.selectedRunId = "";
882
- state.followLatestRun = true;
883
- pendingSend = undefined;
884
- elements.messageInput.value = "";
885
- followMessages = true;
886
- renderAll();
887
- toast("对话已清空,可以重新开始");
888
- } catch (cause) {
889
- if (cause?.code === "RESET_UNSUPPORTED") state.resetUnavailable = true;
890
- showError(cause);
891
- } finally {
892
- state.resetting = false;
893
- elements.resetConversation.textContent = "清空对话";
894
- renderDev();
895
- resizeComposer();
896
- await refreshSimulator();
897
- }
898
- }
899
-
900
- function connectEvents() {
901
- if (eventSource && eventSourceInstance === connectionInstance) return;
902
- closeEvents();
903
- const events = new EventSource(previewApiUrl("/api/events"));
904
- eventSource = events; eventSourceInstance = connectionInstance;
905
- events.addEventListener("open", () => {
906
- if (eventSource !== events) return;
907
- liveEventsActive = true; syncPolling();
908
- });
909
- events.addEventListener("error", () => {
910
- if (eventSource !== events) return;
911
- liveEventsActive = false; syncPolling();
912
- });
913
- events.addEventListener("dev.state", (event) => {
914
- if (eventSource !== events) return;
915
- try {
916
- if (applyDevState(JSON.parse(event.data))) { renderDev(); renderWidgets(); }
917
- } catch {
918
- // A malformed local event is ignored; the next API refresh repairs state.
919
- }
920
- });
921
- events.addEventListener("simulator.state", (event) => {
922
- if (eventSource !== events || state.resetting) return;
923
- try {
924
- const value = JSON.parse(event.data);
925
- const before = JSON.stringify([state.simulator.activeRun, Boolean(state.simulator.streaming?.text), state.simulatorLoaded]);
926
- if (!applySimulatorState(value.state)) return;
927
- if (value.debug) state.debug = value.debug;
928
- const after = JSON.stringify([state.simulator.activeRun, Boolean(state.simulator.streaming?.text), state.simulatorLoaded]);
929
- if (before !== after) renderDev();
930
- if (value.debug) renderDebug();
931
- renderMessages();
932
- } catch {
933
- // Polling remains the reconciliation path for a malformed or missed local event.
934
- }
935
- });
936
- }
937
-
938
- function closeEvents() {
939
- eventSource?.close(); eventSource = undefined; eventSourceInstance = ""; liveEventsActive = false;
940
- }
941
- window.addEventListener("pagehide", closeEvents);
942
-
943
- async function boot() {
944
- if (hosted && !connectionReady) { void refreshConnection(); return; }
945
- try {
946
- const [project, dev, debug] = await Promise.all([
947
- api("/api/project"),
948
- api("/api/dev"),
949
- api("/api/debug"),
950
- ]);
951
- state.project = project.project;
952
- state.dev = dev.state;
953
- state.debug = debug.debug;
954
- renderAll();
955
- if (!hosted) connectEvents();
956
- // Load the cloud profile independently of Agent startup. If it fails,
957
- // the local project and DEV controls remain usable and show the error.
958
- const { buddy } = await api("/api/buddy");
959
- state.buddy = state.dev.ready?.buddy || buddy;
960
- renderProject();
961
- } catch (cause) {
962
- showError(cause);
963
- }
964
- }
965
-
966
- function disconnected(message = "未连接本地工程") {
967
- closeEvents();
968
- connectionReady = false;
969
- state.dev = { phase: "idle", revision: -1 };
970
- state.simulatorRequest++;
971
- widgets?.reset(); products?.reset();
972
- void togglePairing(false);
973
- renderDev(); syncPolling();
974
- $("#local-status").textContent = "本地工程";
975
- }
976
-
977
- async function refreshConnection() {
978
- if (connectionBusy || disconnectBusy) return;
979
- connectionBusy = true;
980
- const epoch = connectionEpoch;
981
- try {
982
- const response = await fetch(connectionUrl, { signal: AbortSignal.timeout(5000) });
983
- const value = await response.json();
984
- if (!response.ok) throw new Error(value.error || "连接状态读取失败");
985
- if (epoch !== connectionEpoch) return;
986
- connectionChecked = true; connectionFailure = "";
987
- $("#connect-command").textContent = value.command.replace(/^buddy-cli(?= )/, "buddy-cli-sit");
988
- $("#copy-connect-command").disabled = false;
989
- $("#stop-command").textContent = value.stopCommand?.replace(/^buddy-cli(?= )/, "buddy-cli-sit") || "buddy-cli-sit dev stop";
990
- if (value.projectRoot) $("#project-root").textContent = value.projectRoot;
991
- if (value.machineName) $("#machine-name").textContent = value.machineName;
992
- if (!value.connected) { connectionInstance = value.instanceId || ""; disconnected(); return; }
993
- const changed = connectionInstance !== value.instanceId;
994
- if (changed) { closeEvents(); $("#disconnect-result").hidden = true; }
995
- const newlyConnected = !connectionReady || changed;
996
- connectionInstance = value.instanceId;
997
- connectionReady = true;
998
- const projectName = (value.projectRoot || "").split(/[\\/]/).filter(Boolean).at(-1) || "本地工程";
999
- $("#local-status").textContent = projectName;
1000
- $("#local-status").title = value.projectRoot || "本地工程";
1001
- $("#machine-name").textContent = value.machineName || "未提供";
1002
- $("#project-root").textContent = value.projectRoot || "未提供";
1003
- if (newlyConnected) {
1004
- $("#connect-instructions").hidden = true;
1005
- $("#get-connect-command").setAttribute("aria-expanded", "false");
1006
- state.dev = { phase: "idle", revision: -1 };
1007
- if (changed) {
1008
- state.simulator = { messages: [] }; state.simulatorLoaded = false; state.debug = { runs: [], events: [], messages: [] };
1009
- state.selectedRunId = ""; pendingSend = undefined; state.buddy = undefined;
1010
- }
1011
- elements.errorBanner.hidden = true;
1012
- renderDev();
1013
- await boot();
1014
- }
1015
- if (value.liveEvents) connectEvents();
1016
- } catch (cause) {
1017
- if (epoch !== connectionEpoch) return;
1018
- const message = cause.message || "连接已中断,正在重试…";
1019
- connectionChecked = true; connectionFailure = message;
1020
- disconnected(message);
1021
- if (!$("#connect-instructions").hidden) { $("#connect-command").textContent = message; $("#copy-connect-command").disabled = true; }
1022
- }
1023
- finally { connectionBusy = false; }
1024
- }
1025
-
1026
- async function forceDisconnect() {
1027
- if (disconnectBusy || !connectionInstance) return;
1028
- if (!window.confirm("强制断开此工程并停止 DEV?若本机无法响应,需要在对应电脑的工程目录执行停止指令。")) return;
1029
- disconnectBusy = true; connectionEpoch++;
1030
- const url = previewApiUrl("/api/connection/disconnect");
1031
- closeEvents(); renderDev();
1032
- try {
1033
- const response = await fetch(url, { method: "POST", signal: AbortSignal.timeout(35_000) });
1034
- const value = await response.json();
1035
- if (!response.ok) throw new Error(value.error || "断连请求失败,请刷新状态后重试");
1036
- connectionInstance = "";
1037
- disconnected();
1038
- $("#disconnect-result").hidden = false;
1039
- $("#disconnect-message").textContent = value.localStopped
1040
- ? "已断开后台连接,并确认当前工程的本地 DEV 已停止。"
1041
- : "已断开后台连接,但无法确认本机 Runtime 已停止。请到原电脑的工程目录执行下面的停止指令。";
1042
- } catch (cause) {
1043
- $("#disconnect-result").hidden = false;
1044
- $("#disconnect-message").textContent = "尚未确认断连和停止结果,请检查终端或执行下面的停止指令,再刷新连接状态。";
1045
- showError(cause);
1046
- } finally { disconnectBusy = false; renderDev(); void refreshConnection(); }
1047
- }
1048
-
1049
- async function pollHostedDev() {
1050
- if (!connectionReady || devPollBusy || disconnectBusy) return;
1051
- devPollBusy = true;
1052
- try {
1053
- const [dev, debug] = await Promise.all([api("/api/dev"), api("/api/debug")]);
1054
- if (applyDevState(dev.state)) renderDev();
1055
- state.debug = debug.debug; renderDebug();
1056
- } catch (cause) {
1057
- // Polling conflicts are connection transitions, already explained by the workbench state.
1058
- if (!connectionReady || cause?.status === 409) void refreshConnection();
1059
- else showError(cause);
1060
- }
1061
- finally { devPollBusy = false; }
1062
- }
1063
-
1064
- if (hosted) {
1065
- renderDev();
1066
- const engineeringDetails = $(".connection-details");
1067
- document.addEventListener("pointerdown", event => {
1068
- if (engineeringDetails.open && !engineeringDetails.contains(event.target)) engineeringDetails.open = false;
1069
- });
1070
- document.addEventListener("keydown", event => {
1071
- if (event.key === "Escape" && engineeringDetails.open) {
1072
- engineeringDetails.open = false;
1073
- engineeringDetails.querySelector("summary").focus();
1074
- }
1075
- });
1076
- $("#force-disconnect").addEventListener("click", () => void forceDisconnect());
1077
- $("#copy-stop-command").addEventListener("click", async () => {
1078
- try { await navigator.clipboard.writeText($("#stop-command").textContent); toast("停止指令已复制,请在原电脑的工程目录执行"); }
1079
- catch { showError(new Error("请手动复制停止指令")); }
1080
- });
1081
- $("#get-connect-command").addEventListener("click", () => {
1082
- $("#connect-instructions").hidden = !$("#connect-instructions").hidden;
1083
- $("#get-connect-command").setAttribute("aria-expanded", String(!$("#connect-instructions").hidden));
1084
- void refreshConnection();
1085
- });
1086
- $("#copy-connect-command").addEventListener("click", async () => {
1087
- try { await navigator.clipboard.writeText($("#connect-command").textContent); toast("连接指令已复制,在工程目录执行后返回当前页面即可"); }
1088
- catch { showError(new Error("请手动复制下方的完整连接指令")); }
1089
- });
1090
- const statusTimer = window.setInterval(() => void refreshConnection(), 2000);
1091
- const devTimer = window.setInterval(() => void pollHostedDev(), 1200);
1092
- window.addEventListener("pagehide", () => { window.clearInterval(statusTimer); window.clearInterval(devTimer); window.clearInterval(state.polling); });
1093
- } else {
1094
- const inspectionTimer = window.setInterval(() => void pollHostedDev(), 2000);
1095
- window.addEventListener("pagehide", () => window.clearInterval(inspectionTimer));
1096
- }
1097
-
1098
- elements.resetConversation.addEventListener("click", () => void resetConversation());
1099
- elements.latestMessages.addEventListener("click", followLatestMessages);
1100
- elements.chatMessages.addEventListener("scroll", () => {
1101
- const chat = elements.chatMessages;
1102
- followMessages = chat.scrollHeight - chat.clientHeight - chat.scrollTop <= 32;
1103
- elements.latestMessages.hidden = followMessages;
1104
- }, { passive: true });
1105
- elements.devControl.addEventListener("click", () => void controlDev());
1106
- elements.composer.addEventListener("submit", (event) => void sendMessage(event));
1107
- elements.messageInput.addEventListener("input", resizeComposer);
1108
- elements.messageInput.addEventListener("keydown", (event) => {
1109
- if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
1110
- event.preventDefault();
1111
- elements.composer.requestSubmit();
1112
- }
1113
- });
1114
- elements.runPicker.addEventListener("change", () => {
1115
- selectRun(elements.runPicker.value);
1116
- });
1117
- elements.runPicker.addEventListener("blur", renderDebug);
1118
- elements.previousRun.addEventListener("click", () => stepRun(1));
1119
- elements.nextRun.addEventListener("click", () => stepRun(-1));
1120
- elements.followLatestRun.addEventListener("change", () => {
1121
- state.followLatestRun = elements.followLatestRun.checked;
1122
- renderDebug();
1123
- });
1124
- elements.copyRunId.addEventListener("click", () => void copyRunId());
1125
- elements.pairDevice.addEventListener("click", () => void togglePairing());
1126
- $("#close-pairing").addEventListener("click", () => void togglePairing(false));
1127
- $("#dismiss-error").addEventListener("click", () => { elements.errorBanner.hidden = true; });
1128
- $("#copy-protocol").addEventListener("click", () => void copyProtocol());
1129
- document.addEventListener("keydown", (event) => {
1130
- if (event.key === "Escape" && !elements.pairingPanel.hidden) void togglePairing(false);
1131
- });
1132
- for (const tab of document.querySelectorAll('[role="tab"]')) {
1133
- tab.addEventListener("click", () => activateTab(tab));
1134
- tab.addEventListener("keydown", (event) => {
1135
- const tabs = [...document.querySelectorAll('[role="tab"]')].filter((item) => !item.hidden);
1136
- const index = tabs.indexOf(tab);
1137
- const target = event.key === "ArrowRight" || event.key === "ArrowDown"
1138
- ? tabs[(index + 1) % tabs.length]
1139
- : event.key === "ArrowLeft" || event.key === "ArrowUp"
1140
- ? tabs[(index - 1 + tabs.length) % tabs.length]
1141
- : event.key === "Home"
1142
- ? tabs[0]
1143
- : event.key === "End"
1144
- ? tabs[tabs.length - 1]
1145
- : undefined;
1146
- if (target) {
1147
- event.preventDefault();
1148
- activateTab(target);
1149
- target.focus();
1150
- }
1151
- });
1152
- }
1153
-
1154
- void boot();
1155
- })();