@my-life-buddies/cli 0.11.0 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/bin/buddy.js +77 -15
- package/dist/bin/buddy.js.map +2 -2
- package/dist/bin/core.js +708 -287
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +152 -1
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/app.css +143 -0
- package/dist/web/app.js +265 -57
- package/dist/web/index.html +14 -16
- package/dist/web/widgets.js +3 -2
- package/package.json +3 -3
package/dist/web/app.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
"use strict";
|
|
3
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 connectionChecked = false, connectionFailure = "", devAction = "";
|
|
9
|
+
function previewApiUrl(path) {
|
|
10
|
+
if (!hosted) return path;
|
|
11
|
+
const url = new URL(connectionUrl.href + path);
|
|
12
|
+
url.searchParams.set("instance", connectionInstance);
|
|
13
|
+
return url.href;
|
|
14
|
+
}
|
|
15
|
+
window.previewApiUrl = previewApiUrl;
|
|
4
16
|
const $ = (selector) => document.querySelector(selector);
|
|
5
17
|
const elements = {
|
|
6
18
|
projectName: $("#project-name"),
|
|
@@ -53,6 +65,7 @@
|
|
|
53
65
|
buddy: undefined,
|
|
54
66
|
dev: { phase: "idle", revision: 0 },
|
|
55
67
|
simulator: { messages: [] },
|
|
68
|
+
simulatorLoaded: false,
|
|
56
69
|
debug: {
|
|
57
70
|
source: "observed",
|
|
58
71
|
generatedAt: new Date().toISOString(),
|
|
@@ -88,7 +101,8 @@
|
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
async function api(path, options = {}) {
|
|
91
|
-
const
|
|
104
|
+
const instance = connectionInstance;
|
|
105
|
+
const response = await fetch(previewApiUrl(path), {
|
|
92
106
|
...options,
|
|
93
107
|
headers: {
|
|
94
108
|
Accept: "application/json",
|
|
@@ -97,8 +111,9 @@
|
|
|
97
111
|
},
|
|
98
112
|
});
|
|
99
113
|
const value = await response.json().catch(() => ({}));
|
|
114
|
+
if (hosted && (!connectionReady || instance !== connectionInstance)) throw Object.assign(new Error("本地连接已改变,请等待重新连接"), {status:409, code:"PREVIEW_CONNECTION_CHANGED"});
|
|
100
115
|
if (!response.ok) {
|
|
101
|
-
const error = new Error(value?.error?.message || `请求失败(HTTP ${response.status})`);
|
|
116
|
+
const error = new Error((typeof value?.error === "string" ? value.error : value?.error?.message) || `请求失败(HTTP ${response.status})`);
|
|
102
117
|
error.code = value?.error?.code || "PREVIEW_REQUEST_FAILED";
|
|
103
118
|
error.status = response.status;
|
|
104
119
|
throw error;
|
|
@@ -121,14 +136,14 @@
|
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
function formatDuration(value) {
|
|
124
|
-
if (!Number.isFinite(value)) return "
|
|
139
|
+
if (!Number.isFinite(value)) return "未记录";
|
|
125
140
|
if (value < 1000) return `${Math.round(value)} ms`;
|
|
126
141
|
return `${(value / 1000).toFixed(value < 10_000 ? 2 : 1)} s`;
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
function formatTime(value, seconds = true) {
|
|
130
145
|
const date = new Date(value);
|
|
131
|
-
if (!Number.isFinite(date.getTime())) return "
|
|
146
|
+
if (!Number.isFinite(date.getTime())) return "未记录";
|
|
132
147
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
133
148
|
hour: "2-digit",
|
|
134
149
|
minute: "2-digit",
|
|
@@ -172,9 +187,10 @@
|
|
|
172
187
|
|
|
173
188
|
function phaseCopy(phase) {
|
|
174
189
|
return {
|
|
175
|
-
idle: "未启动",
|
|
190
|
+
idle: hosted ? "已连接但 DEV 未启动" : "未启动",
|
|
191
|
+
disconnected: "未连接本地工程",
|
|
176
192
|
starting: "正在启动",
|
|
177
|
-
ready: "DEV
|
|
193
|
+
ready: "DEV 运行中",
|
|
178
194
|
stopping: "正在停止",
|
|
179
195
|
failed: "启动失败",
|
|
180
196
|
}[phase] || phase;
|
|
@@ -183,6 +199,7 @@
|
|
|
183
199
|
function applyDevState(next) {
|
|
184
200
|
if (!next || !Number.isFinite(next.revision)) return false;
|
|
185
201
|
if (Number.isFinite(state.dev?.revision) && next.revision < state.dev.revision) return false;
|
|
202
|
+
if (next.phase === "ready" && state.dev.phase !== "ready") state.simulatorLoaded = false;
|
|
186
203
|
state.dev = next;
|
|
187
204
|
if (next.ready?.buddy) state.buddy = next.ready.buddy;
|
|
188
205
|
return true;
|
|
@@ -230,18 +247,20 @@
|
|
|
230
247
|
let pairingRequest = 0;
|
|
231
248
|
|
|
232
249
|
function renderDev() {
|
|
233
|
-
const phase = state.dev.phase;
|
|
250
|
+
const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
|
|
251
|
+
renderWorkbench();
|
|
234
252
|
if (!elements.pairingPanel.hidden && pairingRevision !== state.dev.revision) void togglePairing(true);
|
|
235
253
|
elements.connectionState.dataset.phase = phase;
|
|
236
|
-
elements.connectionLabel.textContent = phaseCopy(phase);
|
|
237
|
-
elements.devControl.disabled = phase === "starting" || phase === "stopping";
|
|
254
|
+
elements.connectionLabel.textContent = phase === "disconnected" ? "未连接本地工程" : phaseCopy(phase);
|
|
255
|
+
elements.devControl.disabled = !connectionReady || Boolean(devAction) || phase === "starting" || phase === "stopping";
|
|
238
256
|
elements.devControl.dataset.action = phase === "ready" ? "stop" : "start";
|
|
239
|
-
elements.devControl.textContent = phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
|
|
257
|
+
elements.devControl.textContent = devAction === "start" ? "正在启动…" : devAction === "stop" ? "正在停止…" : phase === "failed" ? "重试启动" : phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
|
|
240
258
|
elements.environmentAgent.textContent = phase === "ready"
|
|
241
259
|
? `${state.dev.ready?.agentId || "已就绪"} · CLI 托管`
|
|
242
260
|
: phaseCopy(phase);
|
|
243
261
|
|
|
244
262
|
const active = state.simulator.activeRun;
|
|
263
|
+
elements.pairDevice.disabled = !connectionReady || phase !== "ready";
|
|
245
264
|
elements.buddyStatus.textContent = phase !== "ready"
|
|
246
265
|
? phaseCopy(phase)
|
|
247
266
|
: active
|
|
@@ -252,7 +271,7 @@
|
|
|
252
271
|
elements.resetConversation.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending || state.resetUnavailable;
|
|
253
272
|
elements.resetConversation.title = state.resetUnavailable ? "当前平台暂不支持重开会话" : active ? "请等待当前回复完成后再清空" : "结束当前开发版对话,开始一段全新会话";
|
|
254
273
|
elements.devControl.disabled ||= state.resetting;
|
|
255
|
-
elements.messageInput.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending;
|
|
274
|
+
elements.messageInput.disabled = phase !== "ready" || !state.simulatorLoaded || Boolean(active) || state.resetting || state.sending;
|
|
256
275
|
elements.sendMessage.hidden = Boolean(active);
|
|
257
276
|
updateSendButton();
|
|
258
277
|
syncPolling();
|
|
@@ -266,6 +285,45 @@
|
|
|
266
285
|
}
|
|
267
286
|
}
|
|
268
287
|
|
|
288
|
+
function renderWorkbench() {
|
|
289
|
+
if (!hosted) return;
|
|
290
|
+
const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
|
|
291
|
+
const runs = state.debug.runs || [];
|
|
292
|
+
const active = state.simulator.activeRun;
|
|
293
|
+
document.body.dataset.connection = connectionReady ? "connected" : "disconnected";
|
|
294
|
+
document.body.dataset.devPhase = phase;
|
|
295
|
+
document.body.dataset.hasRuns = String(runs.length > 0);
|
|
296
|
+
document.body.dataset.busy = String(phase === "starting" || phase === "stopping" || state.sending || Boolean(active));
|
|
297
|
+
const guide = $("#workbench-state");
|
|
298
|
+
const history = Boolean(state.simulator.messages?.length);
|
|
299
|
+
$("#debug-buddy-name").textContent = state.buddy?.name || new URL(location.href).searchParams.get("buddyName") || "我的搭子";
|
|
300
|
+
const copy = !connectionChecked ? ["正在检查连接", "读取本地工程的连接状态。"]
|
|
301
|
+
: phase === "disconnected" ? [connectionInstance ? "本地连接已断开" : "连接本地工程", connectionFailure || (connectionInstance ? "正在等待重新连接。保留当前记录,连接恢复后继续调试。" : "点击上方「连接工程」,复制指令到终端运行。")]
|
|
302
|
+
: phase === "starting" ? ["正在启动调试", "正在准备运行环境,启动完成后即可发送消息。"]
|
|
303
|
+
: phase === "stopping" ? ["正在停止调试", "等待运行环境退出,当前记录会保留。"]
|
|
304
|
+
: phase === "failed" ? ["调试未能启动", "查看上方错误信息,修复工程后点击「重试启动」。"]
|
|
305
|
+
: phase === "idle" ? ["工程已连接", "点击上方「启动 DEV」,开始体验搭子。"]
|
|
306
|
+
: !state.simulatorLoaded ? ["正在读取对话", "同步当前会话,完成后即可发送消息。"]
|
|
307
|
+
: state.sending ? ["正在发送消息", "提交成功后,这里会显示本次执行过程。"]
|
|
308
|
+
: active ? ["等待执行记录", "消息已提交,正在同步本次执行过程。"]
|
|
309
|
+
: ["开始一轮调试", history ? "右侧保留了历史对话。本地执行记录从本次连接开始,发一条新消息即可查看。" : "在右侧发一条消息,观察搭子的回复与执行过程。"];
|
|
310
|
+
guide.hidden = phase === "ready" && runs.length > 0;
|
|
311
|
+
$("#workbench-state-title").textContent = copy[0];
|
|
312
|
+
$("#workbench-state-copy").textContent = copy[1];
|
|
313
|
+
$("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "仅用于本地调试";
|
|
314
|
+
const connectButton = $("#get-connect-command");
|
|
315
|
+
connectButton.className = `button ${connectionReady ? "button-secondary" : "button-primary"}`;
|
|
316
|
+
connectButton.textContent = connectionReady ? "连接指令" : connectionInstance ? "重新连接" : "连接工程";
|
|
317
|
+
const hint = $("#preview-hint");
|
|
318
|
+
hint.textContent = phase !== "ready" ? (phase === "disconnected" ? "连接工程后开始体验" : phase === "starting" ? "正在启动,稍后即可发送" : "启动 DEV 后开始体验")
|
|
319
|
+
: !state.simulatorLoaded ? "正在读取对话,请稍候" : state.resetting ? "正在清空对话,请稍候" : state.sending ? "正在发送,请稍候"
|
|
320
|
+
: active ? (state.simulator.streaming?.text ? "正在生成回复" : "消息已送达,等待回复") : "Enter 发送,Shift + Enter 换行";
|
|
321
|
+
if (!state.project) elements.buddyName.textContent = new URL(location.href).searchParams.get("buddyName") || "我的搭子";
|
|
322
|
+
const empty = elements.chatMessages.querySelector(".chat-empty");
|
|
323
|
+
if (empty) empty.textContent = phase === "ready" ? "发一条消息,开始体验" : phase === "disconnected" ? "连接后,在这里体验搭子的回复" : "启动 DEV 后,开始对话";
|
|
324
|
+
elements.messageInput.placeholder = active ? "等待当前回复完成" : phase === "ready" ? "给搭子发一条消息" : "等待调试就绪";
|
|
325
|
+
}
|
|
326
|
+
|
|
269
327
|
function runOrdinal(run, index, runs) {
|
|
270
328
|
// Older running preview servers have no ordinal yet. Never infer a message
|
|
271
329
|
// association from list positions or timestamps; this is a display number only.
|
|
@@ -317,8 +375,8 @@
|
|
|
317
375
|
elements.runStatus.textContent = runStatusCopy(run?.status || "idle");
|
|
318
376
|
elements.metricAccept.textContent = formatDuration(run?.acceptLatencyMs);
|
|
319
377
|
elements.metricFirst.textContent = formatDuration(run?.firstTextLatencyMs);
|
|
320
|
-
elements.metricResponse.textContent =
|
|
321
|
-
elements.metricMessages.textContent = String(state.simulator.messages?.length || 0);
|
|
378
|
+
elements.metricResponse.textContent = formatDuration(run?.responseTimeMs);
|
|
379
|
+
elements.metricMessages.textContent = state.simulatorLoaded ? String(state.simulator.messages?.length || 0) : "读取中";
|
|
322
380
|
renderOverview(run);
|
|
323
381
|
renderTimeline(run);
|
|
324
382
|
}
|
|
@@ -351,46 +409,54 @@
|
|
|
351
409
|
elements.overviewEmpty.hidden = Boolean(run);
|
|
352
410
|
elements.overviewContent.hidden = !run;
|
|
353
411
|
if (!run) return;
|
|
412
|
+
const finished = ["completed", "failed", "cancelled"].includes(run.status);
|
|
413
|
+
const measuredFirstText = Number.isFinite(run.firstTextLatencyMs);
|
|
414
|
+
const hasText = measuredFirstText || (state.simulator.streaming?.runId === run.runId && Boolean(state.simulator.streaming.text));
|
|
415
|
+
const noReply = run.completion === "no_reply";
|
|
416
|
+
const interrupted = ["failed", "cancelled", "cancel_requested"].includes(run.status);
|
|
417
|
+
const progress = $("#run-progress");
|
|
418
|
+
if (progress) {
|
|
419
|
+
clear(progress);
|
|
420
|
+
const stages = [
|
|
421
|
+
["消息已提交", "done"],
|
|
422
|
+
["等待回复", hasText || finished ? "done" : "active"],
|
|
423
|
+
[finished && !hasText ? "未记录流式输出" : "生成回复", hasText ? (finished ? "done" : "active") : finished ? "skipped" : "waiting"],
|
|
424
|
+
[run.status === "failed" ? "执行失败" : run.status === "cancelled" ? "已停止" : noReply ? "未产生回复" : "完成", finished ? (interrupted ? "error" : "done") : "waiting"],
|
|
425
|
+
];
|
|
426
|
+
if (interrupted) stages.forEach(stage => { if (stage[1] === "active") stage[1] = "error"; });
|
|
427
|
+
for (const [label, status] of stages) progress.append(node("li", { "data-stage": status }, label));
|
|
428
|
+
}
|
|
354
429
|
clear(elements.waterfall);
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const streaming = first === undefined ? 0 : Math.max(0, total - first);
|
|
362
|
-
const maximum = Math.max(total, 1);
|
|
363
|
-
const rows = [
|
|
364
|
-
["App Server 接收", accept],
|
|
365
|
-
[first === undefined ? "等待首段文字" : "首段文字前", waitingForText],
|
|
366
|
-
[run.status === "completed" ? "完整回复生成" : "回复生成中", streaming],
|
|
367
|
-
];
|
|
368
|
-
for (const [label, duration] of rows) {
|
|
430
|
+
// Only report observed timings. A finished run with missing telemetry never keeps counting.
|
|
431
|
+
const timingRows = [["消息接收", run.acceptLatencyMs], ["首段文字", run.firstTextLatencyMs], ["总耗时", run.responseTimeMs]];
|
|
432
|
+
const maximum = Math.max(1, ...timingRows.map(([,value]) => Number.isFinite(value) ? value : 0));
|
|
433
|
+
elements.waterfall.hidden = !timingRows.some(([,value]) => Number.isFinite(value));
|
|
434
|
+
for (const [label, duration] of timingRows) {
|
|
435
|
+
if (!Number.isFinite(duration)) continue;
|
|
369
436
|
const row = node("div", { class: "waterfall-row" });
|
|
370
437
|
row.append(node("span", {}, label));
|
|
371
438
|
const track = node("span", { class: "waterfall-track" });
|
|
372
439
|
const fill = node("span", { class: "waterfall-fill" });
|
|
373
|
-
fill.style.transform = `scaleX(${
|
|
440
|
+
fill.style.transform = `scaleX(${duration / maximum})`;
|
|
374
441
|
track.append(fill);
|
|
375
442
|
row.append(track, node("strong", {}, formatDuration(duration)));
|
|
376
443
|
elements.waterfall.append(row);
|
|
377
444
|
}
|
|
378
445
|
elements.runSummaryCopy.textContent = run.status === "failed"
|
|
379
|
-
? "
|
|
380
|
-
: run.status === "cancelled"
|
|
381
|
-
|
|
382
|
-
: run.
|
|
383
|
-
|
|
384
|
-
:
|
|
385
|
-
? `本次本地回合在 ${formatDuration(run.firstTextLatencyMs)} 收到首段文字,并在 ${formatDuration(run.responseTimeMs)} 完成正式回复。`
|
|
386
|
-
: run.status === "cancel_requested"
|
|
387
|
-
? "停止请求已经发往 App Server;输入框会在本地回合真正结束后重新开放。"
|
|
388
|
-
: first === undefined
|
|
389
|
-
? "当前本地回合正在等待第一段真实输出。收到后,模拟器会立即开始追加文字。"
|
|
390
|
-
: "当前本地回合正在生成回复,调试器与 App 模拟器会同步展示文字增量。";
|
|
446
|
+
? "本轮执行失败。查看执行时间线定位问题,或展开协议日志中的错误详情。"
|
|
447
|
+
: run.status === "cancelled" ? "本轮已停止。已完成的平台写入不会自动撤销。"
|
|
448
|
+
: noReply ? "本轮已结束,没有产生新的完整回复。"
|
|
449
|
+
: run.status === "completed" ? (measuredFirstText ? `回复已完成,首段文字耗时 ${formatDuration(run.firstTextLatencyMs)}。` : "回复已完成,本轮未记录首段文字耗时。")
|
|
450
|
+
: run.status === "cancel_requested" ? "正在停止,结束后可以继续发送消息。"
|
|
451
|
+
: hasText ? "正在生成回复,右侧同步展示输出。" : "消息已提交,等待搭子开始回复。";
|
|
391
452
|
}
|
|
392
453
|
|
|
454
|
+
let timelineSignature = "", contextSignature = "", protocolSignature = "";
|
|
455
|
+
const expandedLogs = new Map();
|
|
393
456
|
function renderTimeline(run) {
|
|
457
|
+
const signature = JSON.stringify([run?.runId, state.debug.events]);
|
|
458
|
+
if (signature === timelineSignature) return;
|
|
459
|
+
timelineSignature = signature;
|
|
394
460
|
clear(elements.timelineList);
|
|
395
461
|
const events = (state.debug.events || []).filter((event) => !run || event.runId === run.runId);
|
|
396
462
|
if (!events.length) {
|
|
@@ -410,6 +476,9 @@
|
|
|
410
476
|
}
|
|
411
477
|
|
|
412
478
|
function renderContext() {
|
|
479
|
+
const signature = JSON.stringify(state.simulator.messages);
|
|
480
|
+
if (signature === contextSignature) return;
|
|
481
|
+
contextSignature = signature;
|
|
413
482
|
clear(elements.contextList);
|
|
414
483
|
elements.contextSource.textContent = "平台聊天记录";
|
|
415
484
|
const messages = state.simulator.messages || [];
|
|
@@ -419,7 +488,7 @@
|
|
|
419
488
|
}
|
|
420
489
|
for (const message of messages) {
|
|
421
490
|
const row = node("article", { class: "context-row", "data-role": message.role });
|
|
422
|
-
row.append(node("span", { class: "context-role" }, message.role));
|
|
491
|
+
row.append(node("span", { class: "context-role" }, message.role === "user" ? "用户" : message.role === "assistant" ? "搭子" : message.role));
|
|
423
492
|
row.append(node("p", { class: "context-text" }, message.text));
|
|
424
493
|
row.append(node("time", { class: "context-time", dateTime: message.createdAt }, formatTime(message.createdAt)));
|
|
425
494
|
elements.contextList.append(row);
|
|
@@ -427,13 +496,33 @@
|
|
|
427
496
|
}
|
|
428
497
|
|
|
429
498
|
function renderProtocol() {
|
|
499
|
+
const signature = JSON.stringify(state.debug.events);
|
|
500
|
+
if (signature === protocolSignature) return;
|
|
501
|
+
protocolSignature = signature;
|
|
430
502
|
clear(elements.protocolLog);
|
|
431
503
|
const events = state.debug.events || [];
|
|
432
504
|
if (!events.length) {
|
|
433
505
|
elements.protocolLog.append(node("p", { class: "timeline-empty" }, "本机还没有协议事件。"));
|
|
434
506
|
return;
|
|
435
507
|
}
|
|
436
|
-
for (const event of events) {
|
|
508
|
+
for (const [index, event] of events.entries()) {
|
|
509
|
+
if (hosted) {
|
|
510
|
+
const key = event.eventId || `${event.timestamp}:${event.type}:${index}`;
|
|
511
|
+
const row = node("details", { class: "protocol-entry" });
|
|
512
|
+
const error = event.type === "agent.stderr" || /failed|error/.test(event.type);
|
|
513
|
+
row.open = expandedLogs.get(key) ?? error;
|
|
514
|
+
row.dataset.level = error ? "error" : "info";
|
|
515
|
+
row.addEventListener("toggle", () => expandedLogs.set(key, row.open));
|
|
516
|
+
const summary = node("summary");
|
|
517
|
+
summary.append(node("time", { dateTime: event.timestamp }, formatTime(event.timestamp)));
|
|
518
|
+
summary.append(node("span", { class: "log-summary" }, (event.summary || event.type).split("\n")[0]));
|
|
519
|
+
summary.append(node("span", { class: "log-channel" }, event.channel));
|
|
520
|
+
row.append(summary);
|
|
521
|
+
row.append(node("p", { class: "log-meta" }, `${event.type}${event.runId ? ` / ${event.runId}` : ""}${Number.isFinite(event.durationMs) ? ` / ${formatDuration(event.durationMs)}` : ""}`));
|
|
522
|
+
if (event.summary?.includes("\n")) row.append(node("pre", { class: "runtime-log-text" }, event.summary));
|
|
523
|
+
elements.protocolLog.append(row);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
437
526
|
const row = node("div", { class: "protocol-row" });
|
|
438
527
|
row.append(node("time", { class: "protocol-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
|
|
439
528
|
row.append(node("span", { class: "protocol-channel" }, event.channel));
|
|
@@ -451,6 +540,20 @@
|
|
|
451
540
|
|
|
452
541
|
let messageSignature = "";
|
|
453
542
|
let followMessages = true;
|
|
543
|
+
let liveReply, liveReplyText = "";
|
|
544
|
+
function renderLiveReply() {
|
|
545
|
+
if (!liveReply) return;
|
|
546
|
+
const text = state.simulator.streaming?.runId === state.simulator.activeRun?.runId ? state.simulator.streaming?.text || "" : "";
|
|
547
|
+
if (text === liveReplyText) return;
|
|
548
|
+
liveReplyText = text;
|
|
549
|
+
clear(liveReply);
|
|
550
|
+
if (text) { liveReply.className = "message-content"; liveReply.append(node("div", { class: "message-bubble", "aria-label": "搭子正在回复" }, text)); }
|
|
551
|
+
else {
|
|
552
|
+
const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
|
|
553
|
+
indicator.append(node("i"), node("i"), node("i")); liveReply.append(indicator);
|
|
554
|
+
}
|
|
555
|
+
if (followMessages) followLatestMessages();
|
|
556
|
+
}
|
|
454
557
|
|
|
455
558
|
function followLatestMessages() {
|
|
456
559
|
followMessages = true;
|
|
@@ -460,14 +563,15 @@
|
|
|
460
563
|
|
|
461
564
|
function renderMessages() {
|
|
462
565
|
renderWidgets();
|
|
463
|
-
const signature = JSON.stringify([state.simulator.messages,
|
|
464
|
-
if (signature === messageSignature) return;
|
|
566
|
+
const signature = JSON.stringify([state.simulator.messages, state.simulator.activeRun?.runId]);
|
|
567
|
+
if (signature === messageSignature) { renderLiveReply(); return; }
|
|
465
568
|
messageSignature = signature;
|
|
466
569
|
const previousTop = elements.chatMessages.scrollTop;
|
|
467
570
|
clear(elements.chatMessages);
|
|
571
|
+
liveReply = undefined; liveReplyText = "";
|
|
468
572
|
const messages = state.simulator.messages || [];
|
|
469
573
|
if (!messages.length && !state.simulator.activeRun) {
|
|
470
|
-
elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }));
|
|
574
|
+
elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }, hosted ? (state.dev.phase === "ready" ? "发一条消息,开始体验" : "启动 DEV 后,开始对话") : ""));
|
|
471
575
|
}
|
|
472
576
|
let renderedDay;
|
|
473
577
|
for (const message of messages) {
|
|
@@ -485,9 +589,10 @@
|
|
|
485
589
|
}
|
|
486
590
|
if (state.simulator.activeRun) {
|
|
487
591
|
const row = node("div", { class: "message-row typing-row", "data-role": "assistant" });
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
592
|
+
liveReply = node("div", { class: "message-content" });
|
|
593
|
+
liveReplyText = "\0";
|
|
594
|
+
renderLiveReply();
|
|
595
|
+
row.append(messageAvatar("assistant"), liveReply);
|
|
491
596
|
elements.chatMessages.append(row);
|
|
492
597
|
}
|
|
493
598
|
if (followMessages) followLatestMessages();
|
|
@@ -501,6 +606,7 @@
|
|
|
501
606
|
renderRuns();
|
|
502
607
|
renderContext();
|
|
503
608
|
renderProtocol();
|
|
609
|
+
renderWorkbench();
|
|
504
610
|
}
|
|
505
611
|
|
|
506
612
|
function renderAll() {
|
|
@@ -531,38 +637,45 @@
|
|
|
531
637
|
renderContext();
|
|
532
638
|
await refreshDebug();
|
|
533
639
|
} catch (cause) {
|
|
534
|
-
if (cause?.
|
|
640
|
+
if (hosted && (!connectionReady || cause?.status === 409)) void refreshConnection();
|
|
641
|
+
else if (cause?.code !== "SIMULATOR_NOT_READY") showError(cause);
|
|
535
642
|
} finally {
|
|
536
643
|
state.pollingBusy = false;
|
|
537
644
|
}
|
|
538
645
|
}
|
|
539
646
|
|
|
540
647
|
function syncPolling() {
|
|
541
|
-
if (state.dev.phase === "ready" && state.polling === undefined) {
|
|
648
|
+
if (connectionReady && state.dev.phase === "ready" && state.polling === undefined) {
|
|
542
649
|
void refreshSimulator();
|
|
543
650
|
state.polling = window.setInterval(() => void refreshSimulator(), 1200);
|
|
544
|
-
} else if (state.dev.phase !== "ready" && state.polling !== undefined) {
|
|
651
|
+
} else if ((!connectionReady || state.dev.phase !== "ready") && state.polling !== undefined) {
|
|
545
652
|
window.clearInterval(state.polling);
|
|
546
653
|
state.polling = undefined;
|
|
547
654
|
}
|
|
548
655
|
}
|
|
549
656
|
|
|
550
657
|
async function controlDev() {
|
|
551
|
-
if (state.dev.phase === "starting" || state.dev.phase === "stopping") return;
|
|
552
|
-
|
|
658
|
+
if (!connectionReady || state.dev.phase === "starting" || state.dev.phase === "stopping") return;
|
|
659
|
+
if (devAction) return;
|
|
660
|
+
devAction = state.dev.phase === "ready" ? "stop" : "start";
|
|
661
|
+
elements.errorBanner.hidden = true;
|
|
662
|
+
renderDev();
|
|
553
663
|
try {
|
|
554
|
-
const path =
|
|
664
|
+
const path = `/api/dev/${devAction}`;
|
|
555
665
|
const value = await api(path, { method: "POST" });
|
|
556
666
|
applyDevState(value.state);
|
|
557
667
|
if (state.dev.phase !== "ready" && path.endsWith("stop")) state.simulator = { messages: state.simulator.messages || [] };
|
|
558
668
|
renderAll();
|
|
559
669
|
} catch (cause) {
|
|
560
670
|
showError(cause);
|
|
671
|
+
} finally {
|
|
672
|
+
devAction = "";
|
|
673
|
+
renderDev();
|
|
561
674
|
}
|
|
562
675
|
}
|
|
563
676
|
|
|
564
677
|
function updateSendButton() {
|
|
565
|
-
const ready = state.dev.phase === "ready" && !state.simulator.activeRun && !state.resetting && !state.sending;
|
|
678
|
+
const ready = connectionReady && !devAction && state.simulatorLoaded && state.dev.phase === "ready" && !state.simulator.activeRun && !state.resetting && !state.sending;
|
|
566
679
|
elements.sendMessage.disabled = !ready || !elements.messageInput.value.trim();
|
|
567
680
|
}
|
|
568
681
|
|
|
@@ -571,8 +684,9 @@
|
|
|
571
684
|
async function sendMessage(event) {
|
|
572
685
|
event.preventDefault();
|
|
573
686
|
const text = elements.messageInput.value;
|
|
574
|
-
if (!text.trim() || state.dev.phase !== "ready" || state.simulator.activeRun || state.resetting || state.sending) return;
|
|
687
|
+
if (!text.trim() || !state.simulatorLoaded || devAction || state.dev.phase !== "ready" || state.simulator.activeRun || state.resetting || state.sending) return;
|
|
575
688
|
state.sending = true;
|
|
689
|
+
elements.errorBanner.hidden = true;
|
|
576
690
|
followLatestMessages();
|
|
577
691
|
renderDev();
|
|
578
692
|
elements.messageInput.disabled = true;
|
|
@@ -679,6 +793,7 @@
|
|
|
679
793
|
function applySimulatorState(next) {
|
|
680
794
|
if ((next.conversationRevision || 0) < (state.simulator.conversationRevision || 0)) return false;
|
|
681
795
|
state.simulator = next;
|
|
796
|
+
state.simulatorLoaded = true;
|
|
682
797
|
return true;
|
|
683
798
|
}
|
|
684
799
|
|
|
@@ -686,6 +801,7 @@
|
|
|
686
801
|
if (elements.resetConversation.disabled) return;
|
|
687
802
|
if (!window.confirm("清空当前对话并重新开始?\n这会结束当前开发版会话,重置聊天上下文、记忆和该会话的主动服务。旧记录仅在服务端留档;搭子资料和工程文件会保留。")) return;
|
|
688
803
|
state.resetting = true;
|
|
804
|
+
elements.errorBanner.hidden = true;
|
|
689
805
|
widgets?.reset();
|
|
690
806
|
state.simulatorRequest++;
|
|
691
807
|
elements.resetConversation.textContent = "正在清空…";
|
|
@@ -738,6 +854,7 @@
|
|
|
738
854
|
}
|
|
739
855
|
|
|
740
856
|
async function boot() {
|
|
857
|
+
if (hosted && !connectionReady) { void refreshConnection(); return; }
|
|
741
858
|
try {
|
|
742
859
|
const [project, dev, debug] = await Promise.all([
|
|
743
860
|
api("/api/project"),
|
|
@@ -748,7 +865,7 @@
|
|
|
748
865
|
state.dev = dev.state;
|
|
749
866
|
state.debug = debug.debug;
|
|
750
867
|
renderAll();
|
|
751
|
-
connectEvents();
|
|
868
|
+
if (!hosted) connectEvents();
|
|
752
869
|
// Load the cloud profile independently of Agent startup. If it fails,
|
|
753
870
|
// the local project and DEV controls remain usable and show the error.
|
|
754
871
|
const { buddy } = await api("/api/buddy");
|
|
@@ -759,6 +876,97 @@
|
|
|
759
876
|
}
|
|
760
877
|
}
|
|
761
878
|
|
|
879
|
+
function disconnected(message = "未连接本地工程") {
|
|
880
|
+
connectionReady = false;
|
|
881
|
+
state.dev = { phase: "idle", revision: -1 };
|
|
882
|
+
state.simulatorRequest++;
|
|
883
|
+
widgets?.reset();
|
|
884
|
+
void togglePairing(false);
|
|
885
|
+
renderDev(); syncPolling();
|
|
886
|
+
$("#local-status").textContent = "本地工程";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function refreshConnection() {
|
|
890
|
+
if (connectionBusy) return;
|
|
891
|
+
connectionBusy = true;
|
|
892
|
+
try {
|
|
893
|
+
const response = await fetch(connectionUrl, { signal: AbortSignal.timeout(5000) });
|
|
894
|
+
const value = await response.json();
|
|
895
|
+
if (!response.ok) throw new Error(value.error || "连接状态读取失败");
|
|
896
|
+
connectionChecked = true; connectionFailure = "";
|
|
897
|
+
$("#connect-command").textContent = value.command;
|
|
898
|
+
$("#copy-connect-command").disabled = false;
|
|
899
|
+
if (!value.connected) { disconnected(); return; }
|
|
900
|
+
const changed = connectionInstance !== value.instanceId;
|
|
901
|
+
const newlyConnected = !connectionReady || changed;
|
|
902
|
+
connectionInstance = value.instanceId;
|
|
903
|
+
connectionReady = true;
|
|
904
|
+
const projectName = (value.projectRoot || "").split(/[\\/]/).filter(Boolean).at(-1) || "本地工程";
|
|
905
|
+
$("#local-status").textContent = projectName;
|
|
906
|
+
$("#local-status").title = value.projectRoot || "本地工程";
|
|
907
|
+
$("#machine-name").textContent = value.machineName || "未提供";
|
|
908
|
+
$("#project-root").textContent = value.projectRoot || "未提供";
|
|
909
|
+
if (newlyConnected) {
|
|
910
|
+
$("#connect-instructions").hidden = true;
|
|
911
|
+
$("#get-connect-command").setAttribute("aria-expanded", "false");
|
|
912
|
+
state.dev = { phase: "idle", revision: -1 };
|
|
913
|
+
if (changed) {
|
|
914
|
+
state.simulator = { messages: [] }; state.simulatorLoaded = false; state.debug = { runs: [], events: [], messages: [] };
|
|
915
|
+
state.selectedRunId = ""; pendingSend = undefined; state.buddy = undefined;
|
|
916
|
+
}
|
|
917
|
+
elements.errorBanner.hidden = true;
|
|
918
|
+
renderDev();
|
|
919
|
+
await boot();
|
|
920
|
+
}
|
|
921
|
+
} catch (cause) {
|
|
922
|
+
const message = cause.message || "连接已中断,正在重试…";
|
|
923
|
+
connectionChecked = true; connectionFailure = message;
|
|
924
|
+
disconnected(message);
|
|
925
|
+
if (!$("#connect-instructions").hidden) { $("#connect-command").textContent = message; $("#copy-connect-command").disabled = true; }
|
|
926
|
+
}
|
|
927
|
+
finally { connectionBusy = false; }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function pollHostedDev() {
|
|
931
|
+
if (!connectionReady || devPollBusy) return;
|
|
932
|
+
devPollBusy = true;
|
|
933
|
+
try {
|
|
934
|
+
const [dev, debug] = await Promise.all([api("/api/dev"), api("/api/debug")]);
|
|
935
|
+
applyDevState(dev.state); state.debug = debug.debug; renderAll();
|
|
936
|
+
} catch (cause) {
|
|
937
|
+
// Polling conflicts are connection transitions, already explained by the workbench state.
|
|
938
|
+
if (!connectionReady || cause?.status === 409) void refreshConnection();
|
|
939
|
+
else showError(cause);
|
|
940
|
+
}
|
|
941
|
+
finally { devPollBusy = false; }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (hosted) {
|
|
945
|
+
renderDev();
|
|
946
|
+
const engineeringDetails = $(".connection-details");
|
|
947
|
+
document.addEventListener("pointerdown", event => {
|
|
948
|
+
if (engineeringDetails.open && !engineeringDetails.contains(event.target)) engineeringDetails.open = false;
|
|
949
|
+
});
|
|
950
|
+
document.addEventListener("keydown", event => {
|
|
951
|
+
if (event.key === "Escape" && engineeringDetails.open) {
|
|
952
|
+
engineeringDetails.open = false;
|
|
953
|
+
engineeringDetails.querySelector("summary").focus();
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
$("#get-connect-command").addEventListener("click", () => {
|
|
957
|
+
$("#connect-instructions").hidden = !$("#connect-instructions").hidden;
|
|
958
|
+
$("#get-connect-command").setAttribute("aria-expanded", String(!$("#connect-instructions").hidden));
|
|
959
|
+
void refreshConnection();
|
|
960
|
+
});
|
|
961
|
+
$("#copy-connect-command").addEventListener("click", async () => {
|
|
962
|
+
try { await navigator.clipboard.writeText($("#connect-command").textContent); toast("连接指令已复制,请在当前搭子的工程目录执行"); }
|
|
963
|
+
catch { showError(new Error("请手动复制下方的完整连接指令")); }
|
|
964
|
+
});
|
|
965
|
+
const statusTimer = window.setInterval(() => void refreshConnection(), 2000);
|
|
966
|
+
const devTimer = window.setInterval(() => void pollHostedDev(), 1200);
|
|
967
|
+
window.addEventListener("pagehide", () => { window.clearInterval(statusTimer); window.clearInterval(devTimer); window.clearInterval(state.polling); });
|
|
968
|
+
}
|
|
969
|
+
|
|
762
970
|
elements.resetConversation.addEventListener("click", () => void resetConversation());
|
|
763
971
|
elements.latestMessages.addEventListener("click", followLatestMessages);
|
|
764
972
|
elements.chatMessages.addEventListener("scroll", () => {
|
package/dist/web/index.html
CHANGED
|
@@ -93,10 +93,10 @@
|
|
|
93
93
|
<h2 id="run-title">尚无本地回合</h2>
|
|
94
94
|
<span class="status-pill" id="run-status" data-status="idle">等待消息</span>
|
|
95
95
|
</div>
|
|
96
|
-
<
|
|
96
|
+
<details class="run-reference" id="run-reference" hidden><summary>回合标识</summary>
|
|
97
97
|
<span id="run-id"></span>
|
|
98
98
|
<button type="button" class="text-button" id="copy-run-id" disabled>复制 ID</button>
|
|
99
|
-
</
|
|
99
|
+
</details>
|
|
100
100
|
</div>
|
|
101
101
|
<div class="run-controls">
|
|
102
102
|
<label class="run-picker-label" for="run-picker">切换回合</label>
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
114
|
<dl class="run-metrics" aria-label="本地回合指标">
|
|
115
|
-
<div><dt>接收</dt><dd id="metric-accept"
|
|
116
|
-
<div><dt>首段文字</dt><dd id="metric-first"
|
|
117
|
-
<div><dt>完整耗时</dt><dd id="metric-response"
|
|
118
|
-
<div><dt
|
|
115
|
+
<div><dt>接收</dt><dd id="metric-accept">未记录</dd></div>
|
|
116
|
+
<div><dt>首段文字</dt><dd id="metric-first">未记录</dd></div>
|
|
117
|
+
<div><dt>完整耗时</dt><dd id="metric-response">未记录</dd></div>
|
|
118
|
+
<div><dt>会话消息</dt><dd id="metric-messages">0</dd></div>
|
|
119
119
|
</dl>
|
|
120
120
|
|
|
121
121
|
</div>
|
|
@@ -125,15 +125,14 @@
|
|
|
125
125
|
<div class="overview-grid">
|
|
126
126
|
<article class="run-summary">
|
|
127
127
|
<div class="section-heading">
|
|
128
|
-
<div><h3
|
|
129
|
-
<span class="source-label">本机观测</span>
|
|
128
|
+
<div><h3>执行过程</h3></div>
|
|
130
129
|
</div>
|
|
131
130
|
<div class="empty-state" id="overview-empty">
|
|
132
|
-
<span class="empty-glyph" aria-hidden="true">↗</span>
|
|
133
131
|
<h4>发一条消息,开始记录</h4>
|
|
134
|
-
<p
|
|
132
|
+
<p>右侧发送消息后,这里显示本次执行过程。</p>
|
|
135
133
|
</div>
|
|
136
134
|
<div id="overview-content" hidden>
|
|
135
|
+
<ol class="run-progress" id="run-progress" aria-label="回合执行进度"></ol>
|
|
137
136
|
<div class="waterfall" id="waterfall" aria-label="本地回合耗时分布"></div>
|
|
138
137
|
<p class="run-summary-copy" id="run-summary-copy"></p>
|
|
139
138
|
</div>
|
|
@@ -144,8 +143,8 @@
|
|
|
144
143
|
<h3>DEV · 私聊</h3>
|
|
145
144
|
<dl>
|
|
146
145
|
<div><dt>Agent</dt><dd id="environment-agent">等待启动</dd></div>
|
|
147
|
-
<div><dt>Runtime</dt><dd id="environment-runtime"
|
|
148
|
-
<div><dt>模型</dt><dd id="environment-model"
|
|
146
|
+
<div><dt>Runtime</dt><dd id="environment-runtime">未记录</dd></div>
|
|
147
|
+
<div><dt>模型</dt><dd id="environment-model">未记录</dd></div>
|
|
149
148
|
</dl>
|
|
150
149
|
</article>
|
|
151
150
|
</div>
|
|
@@ -153,15 +152,14 @@
|
|
|
153
152
|
|
|
154
153
|
<section id="panel-timeline" class="debug-panel" role="tabpanel" aria-labelledby="tab-timeline" hidden>
|
|
155
154
|
<div class="section-heading">
|
|
156
|
-
<div><h3>执行时间线</h3><p
|
|
157
|
-
<span class="source-label">真实事件</span>
|
|
155
|
+
<div><h3>执行时间线</h3><p>查看当前回合的事件。</p></div>
|
|
158
156
|
</div>
|
|
159
157
|
<ol class="timeline" id="timeline-list"></ol>
|
|
160
158
|
</section>
|
|
161
159
|
|
|
162
160
|
<section id="panel-context" class="debug-panel" role="tabpanel" aria-labelledby="tab-context" hidden>
|
|
163
161
|
<div class="section-heading">
|
|
164
|
-
<div><h3>当前对话记录</h3><p
|
|
162
|
+
<div><h3>当前对话记录</h3><p>包含当前会话的历史消息,不限于所选回合。</p></div>
|
|
165
163
|
<span class="source-label" id="context-source">平台聊天记录</span>
|
|
166
164
|
</div>
|
|
167
165
|
<div class="context-list" id="context-list"></div>
|
|
@@ -169,7 +167,7 @@
|
|
|
169
167
|
|
|
170
168
|
<section id="panel-protocol" class="debug-panel" role="tabpanel" aria-labelledby="tab-protocol" hidden>
|
|
171
169
|
<div class="section-heading">
|
|
172
|
-
<div><h3>协议日志</h3><p
|
|
170
|
+
<div><h3>协议日志</h3><p>查看当前本地连接的完整日志。</p></div>
|
|
173
171
|
<button class="text-button" id="copy-protocol" type="button">复制可见日志</button>
|
|
174
172
|
</div>
|
|
175
173
|
<div class="protocol-log" id="protocol-log" role="log" aria-live="polite"></div>
|