@my-life-buddies/cli 0.13.3 → 0.14.1
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 +11 -7
- package/dist/bin/buddy.js +1 -1
- package/dist/bin/buddy.js.map +1 -1
- package/dist/bin/core.js +233 -54
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +281 -19
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/app.css +65 -6
- package/dist/web/app.js +92 -33
- package/dist/web/diagnostics.js +184 -0
- package/dist/web/index.html +34 -34
- package/package.json +3 -3
- package/resources/agent-template/package-lock.json +4 -4
- package/resources/agent-template/package.json +2 -2
package/dist/web/app.css
CHANGED
|
@@ -326,7 +326,8 @@
|
|
|
326
326
|
.empty-state p { max-width: 30rem; margin: 0; color: var(--color-ink-soft); font-size: var(--text-sm); line-height: 1.65; }
|
|
327
327
|
|
|
328
328
|
.waterfall { display: grid; gap: var(--space-md); margin-block-start: var(--space-xl); padding-block-end: var(--space-lg); border-block-end: var(--rule-hair) solid var(--color-rule); }
|
|
329
|
-
.
|
|
329
|
+
.timing-heading { display:flex; justify-content:space-between; flex-wrap:wrap; gap:6px 16px; color:var(--color-muted); font-size:11px; line-height:1.7; }
|
|
330
|
+
.waterfall-row { display: grid; grid-template-columns: 6rem minmax(2rem, 1fr) 8.5rem; align-items: center; gap: var(--space-md); font-size: var(--text-xs); }
|
|
330
331
|
.waterfall-track { height: 0.375rem; overflow: hidden; border-radius: var(--radius-pill); background: var(--color-paper-soft); }
|
|
331
332
|
.waterfall-fill { display: block; height: 100%; min-width: var(--space-2xs); transform-origin: left; border-radius: inherit; background: var(--color-ink); }
|
|
332
333
|
.waterfall-row strong { font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 500; text-align: end; }
|
|
@@ -553,20 +554,21 @@ body[data-hosted-preview] .hosted-page-heading p { display:flex; align-items:cen
|
|
|
553
554
|
body[data-hosted-preview] .hosted-page-heading p span:first-child { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; max-width:60%; }
|
|
554
555
|
body[data-hosted-preview] .hosted-page-heading h1 { margin:0; font-size:26px; line-height:1.3; letter-spacing:-.035em; font-weight:650; }
|
|
555
556
|
body[data-hosted-preview] .local-connection { border-top:1px solid var(--color-rule); }
|
|
556
|
-
|
|
557
|
+
/* Hosted tabs are normal flex content, not the standalone sticky sidebar. */
|
|
558
|
+
body[data-hosted-preview] .debugger > .sidebar { flex:none; position:static; z-index:auto; width:auto; padding:0 20px; border:0; border-bottom:1px solid var(--color-rule); }
|
|
557
559
|
body[data-hosted-preview] .sidebar-section { margin:0; }
|
|
558
560
|
body[data-hosted-preview] .debug-tabs { display:flex; flex-wrap:nowrap; gap:20px; margin:0; }
|
|
559
561
|
body[data-hosted-preview] .debug-tabs button { width:auto; min-height:44px; padding:10px 0; font-size:13px; gap:7px; border-radius:0; white-space:nowrap; }
|
|
560
562
|
body[data-hosted-preview] .debug-tabs button svg { width:15px; height:15px; }
|
|
561
563
|
body[data-hosted-preview] .debug-tabs button[aria-selected="true"] { background:transparent; box-shadow:inset 0 -2px var(--color-accent); }
|
|
562
564
|
body[data-hosted-preview] .run-strip { flex:none; padding:18px 20px 14px; border-bottom:1px solid var(--color-rule); }
|
|
563
|
-
body[data-hosted-preview] .run-heading { align-items:center; flex-wrap:
|
|
565
|
+
body[data-hosted-preview] .run-heading { align-items:center; flex-wrap:wrap; gap:16px; }
|
|
564
566
|
body[data-hosted-preview] .run-identity { flex:1; }
|
|
565
567
|
body[data-hosted-preview] .run-identity > .eyebrow { display:none; }
|
|
566
568
|
body[data-hosted-preview] .run-title-line { margin:0; flex-wrap:nowrap; gap:8px; }
|
|
567
569
|
body[data-hosted-preview] .run-title-line h2 { font-size:17px; letter-spacing:0; }
|
|
568
570
|
body[data-hosted-preview] .status-pill { font-size:11px; padding:3px 7px; }
|
|
569
|
-
body[data-hosted-preview] .run-controls { flex:0 1
|
|
571
|
+
body[data-hosted-preview] .run-controls { flex:0 1 340px; min-width:min(100%,340px); max-width:100%; gap:6px; }
|
|
570
572
|
body[data-hosted-preview] .run-picker-label { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
|
|
571
573
|
body[data-hosted-preview] :is(.run-picker,.run-step) { min-height:30px; border-radius:6px; font-size:11px; padding-block:4px; }
|
|
572
574
|
body[data-hosted-preview] .run-step { width:30px; }
|
|
@@ -594,7 +596,7 @@ body[data-hosted-preview] .run-summary-copy { margin:16px 0 0; font-size:13px; l
|
|
|
594
596
|
.run-progress li[data-stage="active"] { border-color:var(--color-accent); color:var(--color-accent); font-weight:600; }
|
|
595
597
|
.run-progress li[data-stage="error"] { border-color:var(--color-error); color:var(--color-error); }
|
|
596
598
|
body[data-hosted-preview] .waterfall { margin:0; padding:0; border:0; gap:12px; }
|
|
597
|
-
body[data-hosted-preview] .waterfall-row { grid-template-columns:
|
|
599
|
+
body[data-hosted-preview] .waterfall-row { grid-template-columns:84px minmax(0,1fr) 132px; gap:12px; font-size:12px; }
|
|
598
600
|
body[data-hosted-preview] .waterfall-track { height:3px; background:transparent; }
|
|
599
601
|
body[data-hosted-preview] .waterfall-fill { min-width:0; background:var(--color-accent); opacity:.45; }
|
|
600
602
|
body[data-hosted-preview] .workbench-state { flex:none; padding:26px 24px; border-bottom:1px solid var(--color-rule); }
|
|
@@ -602,7 +604,7 @@ body[data-hosted-preview] .workbench-state h2 { font-size:18px; font-weight:600;
|
|
|
602
604
|
body[data-hosted-preview] .workbench-state p { max-width:480px; color:var(--color-muted); font-size:13px; line-height:1.8; margin:0; }
|
|
603
605
|
body[data-hosted-preview] .state-mark { display:block; width:32px; height:3px; margin-bottom:20px; background:var(--color-accent); border-radius:2px; }
|
|
604
606
|
body[data-hosted-preview][data-has-runs="false"] .run-strip { display:none; }
|
|
605
|
-
|
|
607
|
+
|
|
606
608
|
body[data-hosted-preview][data-dev-phase="disconnected"][data-has-runs="false"] .workbench-state { padding-top:56px; padding-bottom:56px; }
|
|
607
609
|
body[data-hosted-preview][data-dev-phase="disconnected"] .topbar-actions { display:none; }
|
|
608
610
|
body[data-hosted-preview][data-dev-phase="disconnected"] :is(.run-progress li,.replying-indicator i) { animation-play-state:paused; }
|
|
@@ -660,3 +662,60 @@ body[data-hosted-preview] .pairing-panel { top:80px; }
|
|
|
660
662
|
body[data-hosted-preview] .run-controls { flex-basis:100%; }
|
|
661
663
|
body[data-hosted-preview] .simulator-area { padding-inline:12px; }
|
|
662
664
|
}
|
|
665
|
+
|
|
666
|
+
/* Runtime diagnostics share the existing Preview typography and semantic colors. */
|
|
667
|
+
.diagnostic-source { flex:none; color:var(--color-muted); font-size:11px; }
|
|
668
|
+
.diagnostic-filters { display:flex; flex-wrap:wrap; gap:12px; margin:20px 0 12px; }
|
|
669
|
+
.diagnostic-filters label { display:flex; flex:1 1 130px; min-width:0; flex-direction:column; gap:6px; color:var(--color-ink-soft); font-size:11px; }
|
|
670
|
+
.diagnostic-filters .diagnostic-search { flex:1.5 1 160px; }
|
|
671
|
+
.diagnostic-filters :is(select,input) { width:100%; min-width:0; min-height:36px; padding:7px 10px; border:1px solid var(--color-rule-strong); border-radius:6px; background:var(--color-paper-raised); color:var(--color-ink); font:inherit; font-size:12px; }
|
|
672
|
+
.diagnostic-filters :is(select,input):focus-visible { outline:2px solid var(--color-accent); outline-offset:2px; }
|
|
673
|
+
.diagnostic-note { color:var(--color-muted); font-size:11px; line-height:1.8; margin:12px 0 16px; }
|
|
674
|
+
.diagnostic-entry { border-bottom:1px solid var(--color-rule); min-width:0; }
|
|
675
|
+
.diagnostic-entry > summary { display:flex; align-items:start; gap:10px; padding:16px 0; cursor:pointer; list-style:none; }
|
|
676
|
+
.diagnostic-entry > summary::-webkit-details-marker { display:none; }
|
|
677
|
+
.diagnostic-entry > summary::after { content:'›'; color:var(--color-muted); font-size:18px; line-height:20px; flex:none; }
|
|
678
|
+
.diagnostic-entry[open] > summary::after { transform:rotate(90deg); }
|
|
679
|
+
.diagnostic-entry > summary:hover .diagnostic-heading strong { color:var(--color-accent); }
|
|
680
|
+
.diagnostic-entry > summary:focus-visible { outline:2px solid var(--color-accent); outline-offset:3px; border-radius:3px; }
|
|
681
|
+
.diagnostic-heading { display:flex; flex-direction:column; gap:5px; min-width:0; flex:1; }
|
|
682
|
+
.diagnostic-heading strong { font-size:13px; font-weight:550; line-height:1.5; overflow-wrap:anywhere; }
|
|
683
|
+
.diagnostic-preview { color:var(--color-muted); font-size:12px; line-height:1.6; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
684
|
+
.diagnostic-badge { flex:none; color:var(--color-ink-soft); background:var(--color-paper-soft); border-radius:4px; padding:3px 5px; min-width:36px; font:10px/1.4 var(--font-mono); text-align:center; }
|
|
685
|
+
.diagnostic-badge[data-kind="response"] { color:var(--color-accent); background:var(--color-accent-soft, var(--color-paper-soft)); }
|
|
686
|
+
.diagnostic-entry[data-level="error"] > summary .diagnostic-badge { color:var(--color-error); background:var(--color-error-soft); }
|
|
687
|
+
.diagnostic-entry[data-level="warn"] > summary .diagnostic-badge { color:var(--color-warning); background:var(--color-warning-soft); }
|
|
688
|
+
.diagnostic-entry time { color:var(--color-muted); font:11px/1.8 var(--font-mono); white-space:nowrap; }
|
|
689
|
+
.diagnostic-body { padding:0 0 18px; }
|
|
690
|
+
.diagnostic-meta { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:4px 12px; color:var(--color-muted); font-size:11px; margin:0 0 12px; }
|
|
691
|
+
.diagnostic-meta > span { overflow-wrap:anywhere; min-width:0; }
|
|
692
|
+
.diagnostic-meta button { font-size:11px; }
|
|
693
|
+
.diagnostic-section { border:1px solid var(--color-rule); border-radius:6px; margin:8px 0 0; overflow:hidden; }
|
|
694
|
+
.diagnostic-section > summary { cursor:pointer; padding:10px 12px; font-size:12px; color:var(--color-ink-soft); }
|
|
695
|
+
.diagnostic-section[open] > summary { border-bottom:1px solid var(--color-rule); }
|
|
696
|
+
.diagnostic-section pre { margin:0; padding:14px; max-height:420px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere; font:11px/1.85 var(--font-mono); background:var(--color-paper-soft); tab-size:2; }
|
|
697
|
+
.diagnostic-metrics { display:flex; flex-wrap:wrap; gap:16px 24px; margin:18px 0; }
|
|
698
|
+
.diagnostic-metrics div { min-width:70px; }
|
|
699
|
+
.diagnostic-metrics dt { color:var(--color-muted); font-size:10px; margin-bottom:6px; }
|
|
700
|
+
.diagnostic-metrics dd { margin:0; font:12px var(--font-mono); color:var(--color-ink); }
|
|
701
|
+
.metric-help { max-width:220px; margin:6px 0 0; color:var(--color-muted); font:11px/1.7 var(--font-body); white-space:normal; overflow-wrap:anywhere; }
|
|
702
|
+
.run-metrics dd:has(.metric-help) { overflow:visible; white-space:normal; }
|
|
703
|
+
.diagnostic-warning { color:var(--color-error); background:var(--color-error-soft); padding:10px 12px; font-size:12px; line-height:1.8; border-radius:6px; overflow-wrap:anywhere; }
|
|
704
|
+
.diagnostic-empty { padding:44px 0 52px; }
|
|
705
|
+
.diagnostic-empty strong { font-size:15px; font-weight:550; }
|
|
706
|
+
.diagnostic-empty p { max-width:420px; color:var(--color-muted); font-size:12px; line-height:1.9; }
|
|
707
|
+
.diagnostic-explanation { margin-top:24px; color:var(--color-muted); font-size:11px; line-height:1.9; }
|
|
708
|
+
.diagnostic-explanation summary { cursor:pointer; }
|
|
709
|
+
.diagnostic-explanation p { margin:10px 0; }
|
|
710
|
+
.diagnostic-history { margin-top:24px; padding-top:16px; border-top:1px solid var(--color-rule); }
|
|
711
|
+
.diagnostic-history > summary { cursor:pointer; font-size:13px; color:var(--color-ink-soft); }
|
|
712
|
+
.diagnostic-history #context-source { font-size:10px; margin-left:8px; color:var(--color-muted); }
|
|
713
|
+
#panel-overview .run-strip, body[data-hosted-preview] #panel-overview .run-strip { padding:0 0 16px; }
|
|
714
|
+
#panel-protocol .protocol-log { display:block; border:0; }
|
|
715
|
+
@media (max-width:650px) {
|
|
716
|
+
.diagnostic-filters { gap:10px; }
|
|
717
|
+
.diagnostic-filters .diagnostic-search { flex-basis:100%; }
|
|
718
|
+
.diagnostic-entry > summary { gap:7px; }
|
|
719
|
+
.diagnostic-entry time { font-size:10px; }
|
|
720
|
+
.diagnostic-source { display:none; }
|
|
721
|
+
}
|
package/dist/web/app.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const connectionUrl = hosted ? new URL(`../api/buddies/${encodeURIComponent(hostedBuddyId)}/preview`, location.href) : undefined;
|
|
7
7
|
let connectionInstance = "", connectionReady = !hosted, connectionBusy = false, devPollBusy = false;
|
|
8
8
|
let connectionChecked = false, connectionFailure = "", devAction = "";
|
|
9
|
+
let eventSource, eventSourceInstance = "", liveEventsActive = false, pollingInterval = 0;
|
|
9
10
|
function previewApiUrl(path) {
|
|
10
11
|
if (!hosted) return path;
|
|
11
12
|
const url = new URL(connectionUrl.href + path);
|
|
@@ -271,8 +272,8 @@
|
|
|
271
272
|
elements.resetConversation.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending || state.resetUnavailable;
|
|
272
273
|
elements.resetConversation.title = state.resetUnavailable ? "当前平台暂不支持重开会话" : active ? "请等待当前回复完成后再清空" : "结束当前开发版对话,开始一段全新会话";
|
|
273
274
|
elements.devControl.disabled ||= state.resetting;
|
|
274
|
-
elements.messageInput.disabled = phase !== "ready" || !state.simulatorLoaded ||
|
|
275
|
-
elements.sendMessage.hidden =
|
|
275
|
+
elements.messageInput.disabled = phase !== "ready" || !state.simulatorLoaded || state.resetting || state.sending;
|
|
276
|
+
elements.sendMessage.hidden = false;
|
|
276
277
|
updateSendButton();
|
|
277
278
|
syncPolling();
|
|
278
279
|
if (
|
|
@@ -304,10 +305,10 @@
|
|
|
304
305
|
: phase === "failed" ? ["调试未能启动", "查看上方错误信息,修复工程后点击「重试启动」。"]
|
|
305
306
|
: phase === "idle" ? ["工程已连接", "点击上方「启动 DEV」,开始体验搭子。"]
|
|
306
307
|
: !state.simulatorLoaded ? ["正在读取对话", "同步当前会话,完成后即可发送消息。"]
|
|
307
|
-
: state.sending ? ["正在发送消息", "
|
|
308
|
-
: active ? ["等待执行记录", "
|
|
309
|
-
: ["开始一轮调试", history ? "
|
|
310
|
-
guide.hidden = phase === "ready" && runs.length > 0;
|
|
308
|
+
: state.sending ? ["正在发送消息", "提交成功后,这里会显示消息与模型日志。"]
|
|
309
|
+
: active ? ["等待执行记录", "消息已提交,正在同步消息与模型日志。"]
|
|
310
|
+
: ["开始一轮调试", history ? "右侧保留了历史对话。本地调试记录从本次连接开始,发一条新消息即可查看。" : "在右侧发一条消息,观察搭子的回复与模型日志。"];
|
|
311
|
+
guide.hidden = phase === "ready" && (runs.length > 0 || (state.debug.diagnostics?.records || []).some(record => record.kind !== "log"));
|
|
311
312
|
$("#workbench-state-title").textContent = copy[0];
|
|
312
313
|
$("#workbench-state-copy").textContent = copy[1];
|
|
313
314
|
$("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "仅用于本地调试";
|
|
@@ -321,7 +322,7 @@
|
|
|
321
322
|
if (!state.project) elements.buddyName.textContent = new URL(location.href).searchParams.get("buddyName") || "我的搭子";
|
|
322
323
|
const empty = elements.chatMessages.querySelector(".chat-empty");
|
|
323
324
|
if (empty) empty.textContent = phase === "ready" ? "发一条消息,开始体验" : phase === "disconnected" ? "连接后,在这里体验搭子的回复" : "启动 DEV 后,开始对话";
|
|
324
|
-
elements.messageInput.placeholder = active ? "
|
|
325
|
+
elements.messageInput.placeholder = active ? "可以继续补充消息" : phase === "ready" ? "给搭子发一条消息" : "等待调试就绪";
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
function runOrdinal(run, index, runs) {
|
|
@@ -332,7 +333,7 @@
|
|
|
332
333
|
|
|
333
334
|
function runOptionLabel(run, index, runs) {
|
|
334
335
|
const date = dayKey(run.startedAt) === dayKey(new Date()) ? "" : `${dayLabel(run.startedAt) || ""} `;
|
|
335
|
-
return `第 ${runOrdinal(run, index, runs)} 轮 · ${date}${formatTime(run.startedAt)} · ${runStatusCopy(run.status)}`;
|
|
336
|
+
return `第 ${runOrdinal(run, index, runs)} 轮 · ${date}${formatTime(run.startedAt)} · ${(run.completion === "no_reply" && run.status === "completed" ? "未观察到回复" : runStatusCopy(run.status))}`;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
let runPickerSignature = "";
|
|
@@ -372,10 +373,10 @@
|
|
|
372
373
|
elements.runId.title = run?.runId || "";
|
|
373
374
|
elements.copyRunId.disabled = !run;
|
|
374
375
|
elements.runStatus.dataset.status = run?.status || "idle";
|
|
375
|
-
elements.runStatus.textContent = runStatusCopy(run?.status || "idle");
|
|
376
|
-
elements.metricAccept.textContent =
|
|
377
|
-
elements.metricFirst.textContent =
|
|
378
|
-
elements.metricResponse.textContent =
|
|
376
|
+
elements.runStatus.textContent = run?.completion === "no_reply" && run.status === "completed" ? "未观察到回复" : runStatusCopy(run?.status || "idle");
|
|
377
|
+
elements.metricAccept.textContent = formatElapsedSeconds(run?.acceptLatencyMs);
|
|
378
|
+
elements.metricFirst.textContent = formatElapsedSeconds(run?.firstTextLatencyMs);
|
|
379
|
+
elements.metricResponse.textContent = run?.completion === "no_reply" ? "未观察到回复" : formatElapsedSeconds(replyEndLatency(run));
|
|
379
380
|
elements.metricMessages.textContent = state.simulatorLoaded ? String(state.simulator.messages?.length || 0) : "读取中";
|
|
380
381
|
renderOverview(run);
|
|
381
382
|
renderTimeline(run);
|
|
@@ -405,6 +406,18 @@
|
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
408
|
|
|
409
|
+
function formatElapsedSeconds(value) {
|
|
410
|
+
return Number.isFinite(value) ? `${Number((value / 1000).toFixed(3))} 秒` : "未记录";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function replyEndLatency(run) {
|
|
414
|
+
if (run?.status !== "completed") return undefined;
|
|
415
|
+
// updatedAt records Preview observing the terminal state. responseTimeMs and
|
|
416
|
+
// completedAt use the platform message timestamp and are not reply-end timings.
|
|
417
|
+
const elapsed = Date.parse(run.updatedAt) - Date.parse(run.startedAt);
|
|
418
|
+
return Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
function renderOverview(run) {
|
|
409
422
|
elements.overviewEmpty.hidden = Boolean(run);
|
|
410
423
|
elements.overviewContent.hidden = !run;
|
|
@@ -421,16 +434,19 @@
|
|
|
421
434
|
["消息已提交", "done"],
|
|
422
435
|
["等待回复", hasText || finished ? "done" : "active"],
|
|
423
436
|
[finished && !hasText ? "未记录流式输出" : "生成回复", hasText ? (finished ? "done" : "active") : finished ? "skipped" : "waiting"],
|
|
424
|
-
[run.status === "failed" ? "执行失败" : run.status === "cancelled" ? "已停止" : noReply ? "
|
|
437
|
+
[run.status === "failed" ? "执行失败" : run.status === "cancelled" ? "已停止" : noReply ? "未观察到回复" : "完成", finished ? (interrupted ? "error" : "done") : "waiting"],
|
|
425
438
|
];
|
|
426
439
|
if (interrupted) stages.forEach(stage => { if (stage[1] === "active") stage[1] = "error"; });
|
|
427
440
|
for (const [label, status] of stages) progress.append(node("li", { "data-stage": status }, label));
|
|
428
441
|
}
|
|
429
442
|
clear(elements.waterfall);
|
|
430
443
|
// Only report observed timings. A finished run with missing telemetry never keeps counting.
|
|
431
|
-
const timingRows = [["
|
|
444
|
+
const timingRows = [["消息被接收", run.acceptLatencyMs], ["首字耗时", run.firstTextLatencyMs], ["回复结束", noReply ? undefined : replyEndLatency(run)]];
|
|
432
445
|
const maximum = Math.max(1, ...timingRows.map(([,value]) => Number.isFinite(value) ? value : 0));
|
|
433
446
|
elements.waterfall.hidden = !timingRows.some(([,value]) => Number.isFinite(value));
|
|
447
|
+
const timingHeading = node("div", { class: "timing-heading" });
|
|
448
|
+
timingHeading.append(node("span", {}, "起点:Preview 发起发送(0 秒)"), node("span", {}, "累计时间,不可相加"));
|
|
449
|
+
elements.waterfall.append(timingHeading);
|
|
434
450
|
for (const [label, duration] of timingRows) {
|
|
435
451
|
if (!Number.isFinite(duration)) continue;
|
|
436
452
|
const row = node("div", { class: "waterfall-row" });
|
|
@@ -439,26 +455,29 @@
|
|
|
439
455
|
const fill = node("span", { class: "waterfall-fill" });
|
|
440
456
|
fill.style.transform = `scaleX(${duration / maximum})`;
|
|
441
457
|
track.append(fill);
|
|
442
|
-
row.append(track, node("strong", {},
|
|
458
|
+
row.append(track, node("strong", {}, `发送后 ${formatElapsedSeconds(duration)}`));
|
|
443
459
|
elements.waterfall.append(row);
|
|
444
460
|
}
|
|
445
461
|
elements.runSummaryCopy.textContent = run.status === "failed"
|
|
446
|
-
? "
|
|
462
|
+
? "本轮执行失败。请查看运行日志中的错误详情。"
|
|
447
463
|
: run.status === "cancelled" ? "本轮已停止。已完成的平台写入不会自动撤销。"
|
|
448
|
-
: noReply ? "
|
|
449
|
-
: run.status === "completed" ? (measuredFirstText ?
|
|
464
|
+
: noReply ? "回复状态已结束,未观察到新的可见回复;不据此判断执行是否成功。"
|
|
465
|
+
: run.status === "completed" ? (measuredFirstText ? "" : "本轮未记录首字耗时。")
|
|
450
466
|
: run.status === "cancel_requested" ? "正在停止,结束后可以继续发送消息。"
|
|
451
467
|
: hasText ? "正在生成回复,右侧同步展示输出。" : "消息已提交,等待搭子开始回复。";
|
|
468
|
+
elements.runSummaryCopy.hidden = !elements.runSummaryCopy.textContent;
|
|
452
469
|
}
|
|
453
470
|
|
|
454
471
|
let timelineSignature = "", contextSignature = "", protocolSignature = "";
|
|
455
472
|
const expandedLogs = new Map();
|
|
473
|
+
const diagnostics = window.createPreviewDiagnostics?.({ node, formatTime, formatDuration, toast, showError });
|
|
474
|
+
diagnostics?.update(state.debug);
|
|
456
475
|
function renderTimeline(run) {
|
|
457
476
|
const signature = JSON.stringify([run?.runId, state.debug.events]);
|
|
458
477
|
if (signature === timelineSignature) return;
|
|
459
478
|
timelineSignature = signature;
|
|
460
479
|
clear(elements.timelineList);
|
|
461
|
-
const events = (state.debug.events || []).filter((event) => !run || event.runId === run.runId);
|
|
480
|
+
const events = (state.debug.events || []).filter((event) => event.type !== "run.output.delta" && (!run || event.runId === run.runId));
|
|
462
481
|
if (!events.length) {
|
|
463
482
|
elements.timelineList.append(node("li", { class: "timeline-empty" }, run ? "这个本地回合暂无可见事件。" : "发一条消息后,执行事件会出现在这里。"));
|
|
464
483
|
return;
|
|
@@ -496,6 +515,7 @@
|
|
|
496
515
|
}
|
|
497
516
|
|
|
498
517
|
function renderProtocol() {
|
|
518
|
+
if (diagnostics) { diagnostics.update(state.debug); return; }
|
|
499
519
|
const signature = JSON.stringify(state.debug.events);
|
|
500
520
|
if (signature === protocolSignature) return;
|
|
501
521
|
protocolSignature = signature;
|
|
@@ -540,15 +560,22 @@
|
|
|
540
560
|
|
|
541
561
|
let messageSignature = "";
|
|
542
562
|
let followMessages = true;
|
|
543
|
-
let liveReply, liveReplyText = "";
|
|
563
|
+
let liveReply, liveReplyBubble, liveReplyText = "";
|
|
544
564
|
function renderLiveReply() {
|
|
545
565
|
if (!liveReply) return;
|
|
546
566
|
const text = state.simulator.streaming?.runId === state.simulator.activeRun?.runId ? state.simulator.streaming?.text || "" : "";
|
|
547
567
|
if (text === liveReplyText) return;
|
|
548
568
|
liveReplyText = text;
|
|
549
|
-
|
|
550
|
-
|
|
569
|
+
if (text) {
|
|
570
|
+
if (!liveReplyBubble) {
|
|
571
|
+
clear(liveReply);
|
|
572
|
+
liveReplyBubble = node("div", { class: "message-bubble", "aria-label": "搭子正在回复" });
|
|
573
|
+
liveReplyBubble.append(document.createTextNode(text));
|
|
574
|
+
liveReply.append(liveReplyBubble);
|
|
575
|
+
} else liveReplyBubble.firstChild.data = text;
|
|
576
|
+
}
|
|
551
577
|
else {
|
|
578
|
+
clear(liveReply); liveReplyBubble = undefined;
|
|
552
579
|
const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
|
|
553
580
|
indicator.append(node("i"), node("i"), node("i")); liveReply.append(indicator);
|
|
554
581
|
}
|
|
@@ -568,7 +595,7 @@
|
|
|
568
595
|
messageSignature = signature;
|
|
569
596
|
const previousTop = elements.chatMessages.scrollTop;
|
|
570
597
|
clear(elements.chatMessages);
|
|
571
|
-
liveReply = undefined; liveReplyText = "";
|
|
598
|
+
liveReply = undefined; liveReplyBubble = undefined; liveReplyText = "";
|
|
572
599
|
const messages = state.simulator.messages || [];
|
|
573
600
|
if (!messages.length && !state.simulator.activeRun) {
|
|
574
601
|
elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }, hosted ? (state.dev.phase === "ready" ? "发一条消息,开始体验" : "启动 DEV 后,开始对话") : ""));
|
|
@@ -645,9 +672,14 @@
|
|
|
645
672
|
}
|
|
646
673
|
|
|
647
674
|
function syncPolling() {
|
|
675
|
+
const interval = liveEventsActive ? 30000 : 1200;
|
|
676
|
+
if (state.polling !== undefined && pollingInterval !== interval) {
|
|
677
|
+
window.clearInterval(state.polling); state.polling = undefined;
|
|
678
|
+
}
|
|
648
679
|
if (connectionReady && state.dev.phase === "ready" && state.polling === undefined) {
|
|
649
680
|
void refreshSimulator();
|
|
650
|
-
|
|
681
|
+
pollingInterval = interval;
|
|
682
|
+
state.polling = window.setInterval(() => void refreshSimulator(), interval);
|
|
651
683
|
} else if ((!connectionReady || state.dev.phase !== "ready") && state.polling !== undefined) {
|
|
652
684
|
window.clearInterval(state.polling);
|
|
653
685
|
state.polling = undefined;
|
|
@@ -675,7 +707,7 @@
|
|
|
675
707
|
}
|
|
676
708
|
|
|
677
709
|
function updateSendButton() {
|
|
678
|
-
const ready = connectionReady && !devAction && state.simulatorLoaded && state.dev.phase === "ready" && !state.
|
|
710
|
+
const ready = connectionReady && !devAction && state.simulatorLoaded && state.dev.phase === "ready" && !state.resetting && !state.sending;
|
|
679
711
|
elements.sendMessage.disabled = !ready || !elements.messageInput.value.trim();
|
|
680
712
|
}
|
|
681
713
|
|
|
@@ -684,7 +716,7 @@
|
|
|
684
716
|
async function sendMessage(event) {
|
|
685
717
|
event.preventDefault();
|
|
686
718
|
const text = elements.messageInput.value;
|
|
687
|
-
if (!text.trim() || !state.simulatorLoaded || devAction || state.dev.phase !== "ready" || state.
|
|
719
|
+
if (!text.trim() || !state.simulatorLoaded || devAction || state.dev.phase !== "ready" || state.resetting || state.sending) return;
|
|
688
720
|
state.sending = true;
|
|
689
721
|
elements.errorBanner.hidden = true;
|
|
690
722
|
followLatestMessages();
|
|
@@ -706,10 +738,10 @@
|
|
|
706
738
|
pendingSend = undefined;
|
|
707
739
|
elements.messageInput.value = "";
|
|
708
740
|
resizeComposer();
|
|
709
|
-
state.simulator.activeRun = { runId: accepted.runId, status: "pending" };
|
|
741
|
+
if (state.simulator.latestRun?.runId !== accepted.runId) state.simulator.activeRun = { runId: accepted.runId, status: "pending" };
|
|
710
742
|
renderDev();
|
|
711
743
|
renderMessages();
|
|
712
|
-
|
|
744
|
+
void refreshSimulator();
|
|
713
745
|
} catch (cause) {
|
|
714
746
|
showError(cause);
|
|
715
747
|
} finally {
|
|
@@ -774,6 +806,7 @@
|
|
|
774
806
|
}
|
|
775
807
|
|
|
776
808
|
async function copyProtocol() {
|
|
809
|
+
if (diagnostics) return diagnostics.copyLogs();
|
|
777
810
|
const text = (state.debug.events || []).map((event) => [
|
|
778
811
|
event.timestamp,
|
|
779
812
|
event.channel,
|
|
@@ -792,6 +825,8 @@
|
|
|
792
825
|
|
|
793
826
|
function applySimulatorState(next) {
|
|
794
827
|
if ((next.conversationRevision || 0) < (state.simulator.conversationRevision || 0)) return false;
|
|
828
|
+
if (Number.isFinite(next.stateRevision) && Number.isFinite(state.simulator.stateRevision)
|
|
829
|
+
&& next.stateRevision < state.simulator.stateRevision) return false;
|
|
795
830
|
state.simulator = next;
|
|
796
831
|
state.simulatorLoaded = true;
|
|
797
832
|
return true;
|
|
@@ -831,8 +866,20 @@
|
|
|
831
866
|
}
|
|
832
867
|
|
|
833
868
|
function connectEvents() {
|
|
834
|
-
|
|
869
|
+
if (eventSource && eventSourceInstance === connectionInstance) return;
|
|
870
|
+
closeEvents();
|
|
871
|
+
const events = new EventSource(previewApiUrl("/api/events"));
|
|
872
|
+
eventSource = events; eventSourceInstance = connectionInstance;
|
|
873
|
+
events.addEventListener("open", () => {
|
|
874
|
+
if (eventSource !== events) return;
|
|
875
|
+
liveEventsActive = true; syncPolling();
|
|
876
|
+
});
|
|
877
|
+
events.addEventListener("error", () => {
|
|
878
|
+
if (eventSource !== events) return;
|
|
879
|
+
liveEventsActive = false; syncPolling();
|
|
880
|
+
});
|
|
835
881
|
events.addEventListener("dev.state", (event) => {
|
|
882
|
+
if (eventSource !== events) return;
|
|
836
883
|
try {
|
|
837
884
|
if (applyDevState(JSON.parse(event.data))) { renderDev(); renderWidgets(); }
|
|
838
885
|
} catch {
|
|
@@ -840,12 +887,15 @@
|
|
|
840
887
|
}
|
|
841
888
|
});
|
|
842
889
|
events.addEventListener("simulator.state", (event) => {
|
|
890
|
+
if (eventSource !== events || state.resetting) return;
|
|
843
891
|
try {
|
|
844
892
|
const value = JSON.parse(event.data);
|
|
893
|
+
const before = JSON.stringify([state.simulator.activeRun, Boolean(state.simulator.streaming?.text), state.simulatorLoaded]);
|
|
845
894
|
if (!applySimulatorState(value.state)) return;
|
|
846
|
-
state.debug = value.debug;
|
|
847
|
-
|
|
848
|
-
|
|
895
|
+
if (value.debug) state.debug = value.debug;
|
|
896
|
+
const after = JSON.stringify([state.simulator.activeRun, Boolean(state.simulator.streaming?.text), state.simulatorLoaded]);
|
|
897
|
+
if (before !== after) renderDev();
|
|
898
|
+
if (value.debug) renderDebug();
|
|
849
899
|
renderMessages();
|
|
850
900
|
} catch {
|
|
851
901
|
// Polling remains the reconciliation path for a malformed or missed local event.
|
|
@@ -853,6 +903,11 @@
|
|
|
853
903
|
});
|
|
854
904
|
}
|
|
855
905
|
|
|
906
|
+
function closeEvents() {
|
|
907
|
+
eventSource?.close(); eventSource = undefined; eventSourceInstance = ""; liveEventsActive = false;
|
|
908
|
+
}
|
|
909
|
+
window.addEventListener("pagehide", closeEvents);
|
|
910
|
+
|
|
856
911
|
async function boot() {
|
|
857
912
|
if (hosted && !connectionReady) { void refreshConnection(); return; }
|
|
858
913
|
try {
|
|
@@ -877,6 +932,7 @@
|
|
|
877
932
|
}
|
|
878
933
|
|
|
879
934
|
function disconnected(message = "未连接本地工程") {
|
|
935
|
+
closeEvents();
|
|
880
936
|
connectionReady = false;
|
|
881
937
|
state.dev = { phase: "idle", revision: -1 };
|
|
882
938
|
state.simulatorRequest++;
|
|
@@ -898,6 +954,7 @@
|
|
|
898
954
|
$("#copy-connect-command").disabled = false;
|
|
899
955
|
if (!value.connected) { disconnected(); return; }
|
|
900
956
|
const changed = connectionInstance !== value.instanceId;
|
|
957
|
+
if (changed) closeEvents();
|
|
901
958
|
const newlyConnected = !connectionReady || changed;
|
|
902
959
|
connectionInstance = value.instanceId;
|
|
903
960
|
connectionReady = true;
|
|
@@ -918,6 +975,7 @@
|
|
|
918
975
|
renderDev();
|
|
919
976
|
await boot();
|
|
920
977
|
}
|
|
978
|
+
if (value.liveEvents) connectEvents();
|
|
921
979
|
} catch (cause) {
|
|
922
980
|
const message = cause.message || "连接已中断,正在重试…";
|
|
923
981
|
connectionChecked = true; connectionFailure = message;
|
|
@@ -932,7 +990,8 @@
|
|
|
932
990
|
devPollBusy = true;
|
|
933
991
|
try {
|
|
934
992
|
const [dev, debug] = await Promise.all([api("/api/dev"), api("/api/debug")]);
|
|
935
|
-
applyDevState(dev.state)
|
|
993
|
+
if (applyDevState(dev.state)) renderDev();
|
|
994
|
+
state.debug = debug.debug; renderDebug();
|
|
936
995
|
} catch (cause) {
|
|
937
996
|
// Polling conflicts are connection transitions, already explained by the workbench state.
|
|
938
997
|
if (!connectionReady || cause?.status === 409) void refreshConnection();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
"use strict";
|
|
3
|
+
window.createPreviewDiagnostics = ({ node, formatTime, formatDuration, toast, showError }) => {
|
|
4
|
+
const $ = id => document.getElementById(id);
|
|
5
|
+
const models = $("model-records"), logs = $("protocol-log");
|
|
6
|
+
const modelConversation = $("models-conversation"), logConversation = $("logs-conversation");
|
|
7
|
+
const kind = $("models-kind"), level = $("logs-level");
|
|
8
|
+
const modelSearch = $("models-search"), logSearch = $("logs-search");
|
|
9
|
+
let snapshot = {}, conversation = "", visibleLogs = [];
|
|
10
|
+
const cached = new Map(), expanded = new Map();
|
|
11
|
+
const object = value => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12
|
+
const json = value => typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
13
|
+
const number = value => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
14
|
+
const metricHelp = {
|
|
15
|
+
"模型响应耗时": "本次模型请求发出至响应结束。",
|
|
16
|
+
"模型首字耗时": "本次模型请求发出至首段输出。",
|
|
17
|
+
"结束原因": "模型停止原因;toolUse 表示请求调用工具。",
|
|
18
|
+
"输入 Token": "本次调用的输入 Token 数。",
|
|
19
|
+
"输出 Token": "本次调用的输出 Token 数。",
|
|
20
|
+
"缓存读取": "本次调用读取缓存的 Token 数。",
|
|
21
|
+
"缓存写入": "本次调用写入缓存的 Token 数。",
|
|
22
|
+
"总 Token": "本次调用上报的 Token 总数。",
|
|
23
|
+
};
|
|
24
|
+
function section(label, value, key) {
|
|
25
|
+
const details = node("details", { class: "diagnostic-section", "data-detail": key });
|
|
26
|
+
details.append(node("summary", {}, label));
|
|
27
|
+
details.append(node("pre", {}, json(value)));
|
|
28
|
+
return details;
|
|
29
|
+
}
|
|
30
|
+
function metrics(pairs) {
|
|
31
|
+
const list = node("dl", { class: "diagnostic-metrics" });
|
|
32
|
+
for (const [label, value] of pairs) {
|
|
33
|
+
const item = node("div");
|
|
34
|
+
const detail = node("dd", {}, value);
|
|
35
|
+
if (metricHelp[label]) {
|
|
36
|
+
detail.append(node("p", { class: "metric-help" }, metricHelp[label]));
|
|
37
|
+
}
|
|
38
|
+
item.append(node("dt", {}, label), detail);
|
|
39
|
+
list.append(item);
|
|
40
|
+
}
|
|
41
|
+
return list;
|
|
42
|
+
}
|
|
43
|
+
async function copy(text) {
|
|
44
|
+
try { await navigator.clipboard.writeText(text); toast("日志已复制"); }
|
|
45
|
+
catch { showError(new Error("浏览器未允许复制,请展开原始日志后手动选择")); }
|
|
46
|
+
}
|
|
47
|
+
function describe(record) {
|
|
48
|
+
const data = object(record.data), payload = object(data.payload);
|
|
49
|
+
if (record.kind === "request") {
|
|
50
|
+
const messages = payload.messages || payload.input;
|
|
51
|
+
const count = Array.isArray(messages) ? `${messages.length} 条上下文` : "请求上下文";
|
|
52
|
+
return `${typeof payload.model === "string" ? payload.model : "模型未上报"} · ${count}${Array.isArray(payload.tools) ? ` · ${payload.tools.length} 个工具` : ""}`;
|
|
53
|
+
}
|
|
54
|
+
const content = Array.isArray(data.content) ? data.content : [];
|
|
55
|
+
const texts = content.filter(item => item?.type === "text").map(item => item.text).filter(text => typeof text === "string");
|
|
56
|
+
const tools = content.filter(item => item?.type === "toolCall").map(item => item.name).filter(name => typeof name === "string");
|
|
57
|
+
return typeof data.error === "string" ? data.error : tools.length ? `请求工具:${tools.join("、")}` : texts.join(" ") || record.title;
|
|
58
|
+
}
|
|
59
|
+
function row(record, model) {
|
|
60
|
+
const data = object(record.data);
|
|
61
|
+
const details = node("details", { class: "diagnostic-entry", "data-record": record.id, "data-detail": "entry" });
|
|
62
|
+
details.dataset.level = data.error || data.stopReason === "error" ? "error" : record.level;
|
|
63
|
+
const summary = node("summary");
|
|
64
|
+
summary.append(node("span", { class: "diagnostic-badge", "data-kind": record.kind }, model ? record.kind === "request" ? "请求" : "响应" : record.level.toUpperCase()));
|
|
65
|
+
const heading = node("span", { class: "diagnostic-heading" });
|
|
66
|
+
heading.append(node("strong", {}, model ? record.kind === "request" ? "模型请求" : "模型响应" : record.title));
|
|
67
|
+
if (model) heading.append(node("span", { class: "diagnostic-preview" }, describe(record)));
|
|
68
|
+
summary.append(heading, node("time", { dateTime: record.timestamp, title: record.timestamp }, formatTime(record.timestamp)));
|
|
69
|
+
details.append(summary);
|
|
70
|
+
const body = node("div", { class: "diagnostic-body" });
|
|
71
|
+
const meta = node("div", { class: "diagnostic-meta" });
|
|
72
|
+
meta.append(node("span", {}, `${record.convoKey || "未标注会话"} · ${record.source}`));
|
|
73
|
+
const button = node("button", { type: "button", class: "text-button" }, "复制原始日志");
|
|
74
|
+
button.addEventListener("click", () => void copy(record.raw));
|
|
75
|
+
meta.append(button); body.append(meta);
|
|
76
|
+
if (record.truncated || (model && !record.data)) {
|
|
77
|
+
body.append(node("p", { class: "diagnostic-warning" }, record.truncated
|
|
78
|
+
? "日志已截断,无法还原完整数据。请查看原始日志。"
|
|
79
|
+
: "尚未收到完整数据,或日志格式无法解析。已保留原始日志。"));
|
|
80
|
+
}
|
|
81
|
+
if (model && record.data) {
|
|
82
|
+
if (record.kind === "request") {
|
|
83
|
+
const payload = object(data.payload);
|
|
84
|
+
const messages = payload.messages ?? payload.input;
|
|
85
|
+
const system = payload.system ?? (Array.isArray(messages) ? messages.filter(item => ["system", "developer"].includes(item?.role)) : undefined);
|
|
86
|
+
if (system != null && (!Array.isArray(system) || system.length)) body.append(section("系统提示词", system, "system"));
|
|
87
|
+
if (messages != null) body.append(section(`请求上下文${Array.isArray(messages) ? ` · ${messages.length} 条` : ""}`, messages, "messages"));
|
|
88
|
+
if (payload.tools != null) body.append(section(`可用工具${Array.isArray(payload.tools) ? ` · ${payload.tools.length} 个` : ""}`, payload.tools, "tools"));
|
|
89
|
+
const { messages: _messages, input: _input, system: _system, tools: _tools, ...parameters } = payload;
|
|
90
|
+
body.append(section("模型与请求参数", parameters, "parameters"));
|
|
91
|
+
body.append(section("完整请求体", data.payload ?? data, "payload"));
|
|
92
|
+
} else {
|
|
93
|
+
const usage = object(data.usage);
|
|
94
|
+
body.append(metrics([
|
|
95
|
+
["模型响应耗时", number(data.ms) ? formatDuration(data.ms) : "未上报"],
|
|
96
|
+
["模型首字耗时", number(data.firstTokenMs) ? formatDuration(data.firstTokenMs) : "未上报"],
|
|
97
|
+
["结束原因", typeof data.stopReason === "string" ? data.stopReason : "未上报"],
|
|
98
|
+
]));
|
|
99
|
+
const tokenFields = [["输入 Token", "input"], ["输出 Token", "output"], ["缓存读取", "cacheRead"], ["缓存写入", "cacheWrite"], ["总 Token", "totalTokens"]];
|
|
100
|
+
body.append(metrics(tokenFields.map(([label, key]) => [label, number(usage[key]) ? String(usage[key]) : "未上报"])));
|
|
101
|
+
if (typeof data.error === "string" && data.error) body.append(node("p", { class: "diagnostic-warning" }, data.error));
|
|
102
|
+
const content = Array.isArray(data.content) ? data.content : [];
|
|
103
|
+
content.forEach((block, index) => {
|
|
104
|
+
if (!block || typeof block !== "object") return;
|
|
105
|
+
const label = block.type === "text" ? "响应正文" : block.type === "thinking" ? "思考内容(日志已提供)" : block.type === "toolCall" ? `工具调用请求 · ${block.name || "未命名"}` : `响应片段 · ${block.type || index + 1}`;
|
|
106
|
+
body.append(section(label, block.type === "text" ? block.text : block, `content-${index}`));
|
|
107
|
+
});
|
|
108
|
+
if (data.content != null && !content.length) body.append(section("响应内容", data.content, "content"));
|
|
109
|
+
if (data.usage != null) body.append(section("完整用量", data.usage, "usage"));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
body.append(section("原始日志", record.raw, "raw"));
|
|
113
|
+
details.append(body);
|
|
114
|
+
return details;
|
|
115
|
+
}
|
|
116
|
+
function reconcile(container, records, model, emptyText) {
|
|
117
|
+
const prefix = model ? "model:" : "log:";
|
|
118
|
+
const wanted = new Set();
|
|
119
|
+
records.forEach((record, index) => {
|
|
120
|
+
const key = prefix + record.id;
|
|
121
|
+
wanted.add(key);
|
|
122
|
+
const signature = JSON.stringify(record);
|
|
123
|
+
let entry = cached.get(key);
|
|
124
|
+
if (!entry || entry.signature !== signature) {
|
|
125
|
+
if (entry) expanded.set(key, [...entry.node.querySelectorAll("details"), entry.node].filter(item => item.open).map(item => item.dataset.detail));
|
|
126
|
+
const element = row(record, model);
|
|
127
|
+
for (const item of [...element.querySelectorAll("details"), element]) item.open = (expanded.get(key) || []).includes(item.dataset.detail);
|
|
128
|
+
entry?.node.remove();
|
|
129
|
+
entry = { node: element, signature };
|
|
130
|
+
cached.set(key, entry);
|
|
131
|
+
}
|
|
132
|
+
if (container.children[index] !== entry.node) container.insertBefore(entry.node, container.children[index] || null);
|
|
133
|
+
});
|
|
134
|
+
for (const child of [...container.children]) if (!wanted.has(prefix + child.dataset.record)) child.remove();
|
|
135
|
+
if (!records.length) {
|
|
136
|
+
const empty = node("div", { class: "diagnostic-empty" });
|
|
137
|
+
empty.append(node("strong", {}, emptyText[0]), node("p", {}, emptyText[1]));
|
|
138
|
+
container.append(empty);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function updateConversations(records) {
|
|
142
|
+
const keys = [...new Set(records.map(record => record.convoKey).filter(Boolean))].sort();
|
|
143
|
+
if (conversation && !keys.includes(conversation)) keys.push(conversation);
|
|
144
|
+
for (const select of [modelConversation, logConversation]) {
|
|
145
|
+
if (document.activeElement === select) continue;
|
|
146
|
+
const signature = JSON.stringify(keys);
|
|
147
|
+
if (select.dataset.options !== signature) {
|
|
148
|
+
select.replaceChildren(node("option", { value: "" }, "全部会话"), ...keys.map(key => node("option", { value: key }, key)));
|
|
149
|
+
select.dataset.options = signature;
|
|
150
|
+
}
|
|
151
|
+
select.value = conversation;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function render() {
|
|
155
|
+
const runtime = snapshot.diagnostics?.records || [];
|
|
156
|
+
updateConversations(runtime);
|
|
157
|
+
const matches = (record, search) => (!conversation || record.convoKey === conversation) && (!search || record.raw.toLocaleLowerCase().includes(search.toLocaleLowerCase()));
|
|
158
|
+
const modelRecords = runtime.filter(record => record.kind === "request" || record.kind === "response");
|
|
159
|
+
const filteredModels = modelRecords.filter(record => (!kind.value || record.kind === kind.value) && matches(record, modelSearch.value.trim()));
|
|
160
|
+
reconcile(models, filteredModels, true, modelRecords.length
|
|
161
|
+
? ["没有匹配的模型记录", "试试其他会话、类型或关键词。"]
|
|
162
|
+
: ["等待模型调用", "启动 DEV 并发送消息后,Runtime 提供的模型请求与响应会出现在这里。旧版本或未输出这些日志的 Runtime 可先查看运行日志。"]);
|
|
163
|
+
$("models-count").textContent = `${filteredModels.length} 条记录${snapshot.diagnostics?.dropped ? ` · 已淘汰 ${snapshot.diagnostics.dropped} 条较早日志` : ""}`;
|
|
164
|
+
const events = snapshot.events || [];
|
|
165
|
+
const deltas = events.filter(event => event.type === "run.output.delta").length;
|
|
166
|
+
const other = events.filter(event => event.type !== "run.output.delta" && (!snapshot.diagnostics || !/^agent\.(stdout|stderr)$/.test(event.type))).map(event => ({
|
|
167
|
+
id: event.eventId, timestamp: event.timestamp, source: event.channel, level: /failed|error|stderr/.test(event.type) ? "error" : "info", title: event.summary || event.type,
|
|
168
|
+
kind: "log", raw: `${event.timestamp} ${event.channel} ${event.type}${event.runId ? ` ${event.runId}` : ""}\n${event.summary || ""}`,
|
|
169
|
+
}));
|
|
170
|
+
const allLogs = [...runtime, ...other].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
171
|
+
visibleLogs = allLogs.filter(record => (!level.value || record.level === level.value || level.value === "error" && (record.data?.error || record.data?.stopReason === "error")) && matches(record, logSearch.value.trim()));
|
|
172
|
+
reconcile(logs, visibleLogs, false, allLogs.length ? ["没有匹配的日志", "调整会话、级别或搜索条件后再试。"] : ["等待运行日志", "启动 DEV 后,这里会保留进程输出与连接事件。"]);
|
|
173
|
+
$("logs-count").textContent = `${visibleLogs.length} 条可见日志${deltas ? ` · 已折叠 ${deltas} 条文字分片事件` : ""}${snapshot.diagnostics?.dropped ? ` · 已淘汰 ${snapshot.diagnostics.dropped} 条较早日志` : ""}`;
|
|
174
|
+
const retained = new Set([...runtime.map(record => record.id), ...other.map(record => record.id)]);
|
|
175
|
+
for (const key of cached.keys()) if (!retained.has(key.slice(key.indexOf(":") + 1))) { cached.delete(key); expanded.delete(key); }
|
|
176
|
+
}
|
|
177
|
+
for (const select of [modelConversation, logConversation]) {
|
|
178
|
+
select.addEventListener("change", () => { conversation = select.value; render(); });
|
|
179
|
+
select.addEventListener("blur", render);
|
|
180
|
+
}
|
|
181
|
+
for (const input of [kind, level, modelSearch, logSearch]) input.addEventListener(input.type === "search" ? "input" : "change", render);
|
|
182
|
+
return { update(next) { snapshot = next; render(); }, copyLogs() { return copy(visibleLogs.map(record => record.raw).join("\n\n")); } };
|
|
183
|
+
};
|
|
184
|
+
})();
|