@my-life-buddies/cli 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/dist/bin/buddy.js +443 -0
- package/dist/bin/buddy.js.map +7 -0
- package/dist/bin/core.js +5638 -0
- package/dist/bin/core.js.map +7 -0
- package/dist/bin/preview.js +1429 -0
- package/dist/bin/preview.js.map +7 -0
- package/dist/web/app.css +508 -0
- package/dist/web/app.js +724 -0
- package/dist/web/assets/developer-platform-icon.png.base64 +1 -0
- package/dist/web/brand.css +100 -0
- package/dist/web/index.html +236 -0
- package/dist/web/tokens.css +15 -0
- package/package.json +45 -0
- package/resources/README.md +52 -0
- package/resources/agent-template/package-lock.json +1726 -0
- package/resources/agent-template/package.json +16 -0
- package/resources/brand/README.md +11 -0
- package/resources/brand/login-result.css +25 -0
- package/resources/brand/logo.png +0 -0
- package/resources/brand/tokens.css +100 -0
- package/resources/buddy-creator/INSTALL.md +25 -0
- package/resources/buddy-creator/PATCHES.md +33 -0
- package/resources/buddy-creator/SKILL.md +66 -0
- package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +154 -0
- package/resources/buddy-creator/agents/openai.yaml +4 -0
- package/resources/buddy-creator/assets/licenses/bail.txt +26 -0
- package/resources/buddy-creator/assets/licenses/ccount.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +26 -0
- package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +26 -0
- package/resources/buddy-creator/assets/licenses/debug.txt +24 -0
- package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +26 -0
- package/resources/buddy-creator/assets/licenses/dequal.txt +25 -0
- package/resources/buddy-creator/assets/licenses/devlop.txt +26 -0
- package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +13 -0
- package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +26 -0
- package/resources/buddy-creator/assets/licenses/extend.txt +27 -0
- package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +26 -0
- package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +25 -0
- package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +13 -0
- package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-decimal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +13 -0
- package/resources/buddy-creator/assets/licenses/longest-streak.txt +26 -0
- package/resources/buddy-creator/assets/licenses/markdown-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +27 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +26 -0
- package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +26 -0
- package/resources/buddy-creator/assets/licenses/micromark.txt +26 -0
- package/resources/buddy-creator/assets/licenses/ms.txt +25 -0
- package/resources/buddy-creator/assets/licenses/parse-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +25 -0
- package/resources/buddy-creator/assets/licenses/property-information.txt +26 -0
- package/resources/buddy-creator/assets/licenses/react-dom.txt +25 -0
- package/resources/buddy-creator/assets/licenses/react-markdown.txt +25 -0
- package/resources/buddy-creator/assets/licenses/react.txt +25 -0
- package/resources/buddy-creator/assets/licenses/remark-gfm.txt +26 -0
- package/resources/buddy-creator/assets/licenses/remark-parse.txt +25 -0
- package/resources/buddy-creator/assets/licenses/remark-rehype.txt +26 -0
- package/resources/buddy-creator/assets/licenses/remark-stringify.txt +25 -0
- package/resources/buddy-creator/assets/licenses/scheduler.txt +25 -0
- package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +26 -0
- package/resources/buddy-creator/assets/licenses/stringify-entities.txt +26 -0
- package/resources/buddy-creator/assets/licenses/style-to-js.txt +26 -0
- package/resources/buddy-creator/assets/licenses/style-to-object.txt +26 -0
- package/resources/buddy-creator/assets/licenses/trim-lines.txt +26 -0
- package/resources/buddy-creator/assets/licenses/trough.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--debug.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--estree.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--hast.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--mdast.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--ms.txt +25 -0
- package/resources/buddy-creator/assets/licenses/types--unist.txt +25 -0
- package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +19 -0
- package/resources/buddy-creator/assets/licenses/unified.txt +25 -0
- package/resources/buddy-creator/assets/licenses/unist-util-is.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-position.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +26 -0
- package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +26 -0
- package/resources/buddy-creator/assets/licenses/vfile-message.txt +26 -0
- package/resources/buddy-creator/assets/licenses/vfile.txt +25 -0
- package/resources/buddy-creator/assets/licenses/zwitch.txt +26 -0
- package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +39 -0
- package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +4 -0
- package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
- package/resources/buddy-creator/assets/preview/brand.css +100 -0
- package/resources/buddy-creator/assets/preview/index.html +5 -0
- package/resources/buddy-creator/assets/preview/reader.css +165 -0
- package/resources/buddy-creator/assets/preview/reader.js +135 -0
- package/resources/buddy-creator/references/artifact-schema.md +68 -0
- package/resources/buddy-creator/references/catalog.json +401 -0
- package/resources/buddy-creator/references/definition.md +24 -0
- package/resources/buddy-creator/references/dialogue-examples.md +133 -0
- package/resources/buddy-creator/references/host-guide.md +213 -0
- package/resources/buddy-creator/references/interview.md +235 -0
- package/resources/buddy-creator/references/knowledge.md +40 -0
- package/resources/buddy-creator/references/methods.md +77 -0
- package/resources/buddy-creator/references/opening.md +14 -0
- package/resources/buddy-creator/references/preview-panel.md +66 -0
- package/resources/buddy-creator/references/recovery.md +43 -0
- package/resources/buddy-creator/references/service.md +74 -0
- package/resources/buddy-creator/scripts/buddy.py +108 -0
- package/resources/buddy-creator/scripts/buddy_client.py +67 -0
- package/resources/buddy-creator/scripts/buddy_core.py +803 -0
- package/resources/buddy-creator/scripts/completion.py +240 -0
- package/resources/buddy-creator/scripts/interview.py +215 -0
- package/resources/buddy-creator/scripts/preview.py +577 -0
- package/resources/buddy-creator/scripts/preview_panel.py +80 -0
- package/resources/buddy-creator/scripts/sources.py +239 -0
- package/resources/buddy-creator/version.json +8 -0
- package/resources/buddy-creator.manifest.json +696 -0
package/dist/web/app.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const $ = (selector) => document.querySelector(selector);
|
|
5
|
+
const elements = {
|
|
6
|
+
projectName: $("#project-name"),
|
|
7
|
+
connectionState: $("#connection-state"),
|
|
8
|
+
connectionLabel: $("#connection-label"),
|
|
9
|
+
devControl: $("#dev-control"),
|
|
10
|
+
errorBanner: $("#error-banner"),
|
|
11
|
+
errorMessage: $("#error-message"),
|
|
12
|
+
pairDevice: $("#pair-device"),
|
|
13
|
+
pairingPanel: $("#pairing-panel"),
|
|
14
|
+
pairingBody: $("#pairing-body"),
|
|
15
|
+
runPicker: $("#run-picker"),
|
|
16
|
+
previousRun: $("#previous-run"),
|
|
17
|
+
nextRun: $("#next-run"),
|
|
18
|
+
followLatestRun: $("#follow-latest-run"),
|
|
19
|
+
runReference: $("#run-reference"),
|
|
20
|
+
runId: $("#run-id"),
|
|
21
|
+
copyRunId: $("#copy-run-id"),
|
|
22
|
+
runTitle: $("#run-title"),
|
|
23
|
+
runStatus: $("#run-status"),
|
|
24
|
+
metricAccept: $("#metric-accept"),
|
|
25
|
+
metricFirst: $("#metric-first"),
|
|
26
|
+
metricResponse: $("#metric-response"),
|
|
27
|
+
metricMessages: $("#metric-messages"),
|
|
28
|
+
overviewEmpty: $("#overview-empty"),
|
|
29
|
+
overviewContent: $("#overview-content"),
|
|
30
|
+
waterfall: $("#waterfall"),
|
|
31
|
+
runSummaryCopy: $("#run-summary-copy"),
|
|
32
|
+
environmentAgent: $("#environment-agent"),
|
|
33
|
+
environmentRuntime: $("#environment-runtime"),
|
|
34
|
+
environmentModel: $("#environment-model"),
|
|
35
|
+
timelineList: $("#timeline-list"),
|
|
36
|
+
contextList: $("#context-list"),
|
|
37
|
+
contextSource: $("#context-source"),
|
|
38
|
+
protocolLog: $("#protocol-log"),
|
|
39
|
+
buddyName: $("#buddy-name"),
|
|
40
|
+
buddyStatus: $("#buddy-status"),
|
|
41
|
+
chatMessages: $("#chat-messages"),
|
|
42
|
+
chatEmpty: $("#chat-empty"),
|
|
43
|
+
composer: $("#composer"),
|
|
44
|
+
messageInput: $("#message-input"),
|
|
45
|
+
sendMessage: $("#send-message"),
|
|
46
|
+
toast: $("#toast"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const state = {
|
|
50
|
+
project: undefined,
|
|
51
|
+
buddy: undefined,
|
|
52
|
+
dev: { phase: "idle", revision: 0 },
|
|
53
|
+
simulator: { messages: [] },
|
|
54
|
+
debug: {
|
|
55
|
+
source: "observed",
|
|
56
|
+
generatedAt: new Date().toISOString(),
|
|
57
|
+
runs: [],
|
|
58
|
+
events: [],
|
|
59
|
+
messages: [],
|
|
60
|
+
},
|
|
61
|
+
selectedRunId: "",
|
|
62
|
+
followLatestRun: true,
|
|
63
|
+
reportedFailureRevision: -1,
|
|
64
|
+
polling: undefined,
|
|
65
|
+
pollingBusy: false,
|
|
66
|
+
toastTimer: undefined,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function clear(node) {
|
|
70
|
+
while (node.firstChild) node.removeChild(node.firstChild);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function node(tag, attributes = {}, text) {
|
|
74
|
+
const element = document.createElement(tag);
|
|
75
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
76
|
+
if (name === "class") element.className = value;
|
|
77
|
+
else if (name.includes("-")) element.setAttribute(name, value);
|
|
78
|
+
else element[name] = value;
|
|
79
|
+
}
|
|
80
|
+
if (text !== undefined) element.textContent = text;
|
|
81
|
+
return element;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function api(path, options = {}) {
|
|
85
|
+
const response = await fetch(path, {
|
|
86
|
+
...options,
|
|
87
|
+
headers: {
|
|
88
|
+
Accept: "application/json",
|
|
89
|
+
...(options.body ? { "Content-Type": "application/json" } : {}),
|
|
90
|
+
...(options.headers || {}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const value = await response.json().catch(() => ({}));
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const error = new Error(value?.error?.message || `请求失败(HTTP ${response.status})`);
|
|
96
|
+
error.code = value?.error?.code || "PREVIEW_REQUEST_FAILED";
|
|
97
|
+
error.status = response.status;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function showError(cause) {
|
|
104
|
+
elements.errorMessage.textContent = cause instanceof Error ? cause.message : "操作未能完成";
|
|
105
|
+
elements.errorBanner.hidden = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toast(message) {
|
|
109
|
+
window.clearTimeout(state.toastTimer);
|
|
110
|
+
elements.toast.textContent = message;
|
|
111
|
+
elements.toast.hidden = false;
|
|
112
|
+
state.toastTimer = window.setTimeout(() => {
|
|
113
|
+
elements.toast.hidden = true;
|
|
114
|
+
}, 2200);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatDuration(value) {
|
|
118
|
+
if (!Number.isFinite(value)) return "—";
|
|
119
|
+
if (value < 1000) return `${Math.round(value)} ms`;
|
|
120
|
+
return `${(value / 1000).toFixed(value < 10_000 ? 2 : 1)} s`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatTime(value, seconds = true) {
|
|
124
|
+
const date = new Date(value);
|
|
125
|
+
if (!Number.isFinite(date.getTime())) return "—";
|
|
126
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
127
|
+
hour: "2-digit",
|
|
128
|
+
minute: "2-digit",
|
|
129
|
+
...(seconds ? { second: "2-digit" } : {}),
|
|
130
|
+
hour12: false,
|
|
131
|
+
}).format(date);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function dayKey(value) {
|
|
135
|
+
const date = new Date(value);
|
|
136
|
+
if (!Number.isFinite(date.getTime())) return undefined;
|
|
137
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function dayLabel(value) {
|
|
141
|
+
const date = new Date(value);
|
|
142
|
+
if (!Number.isFinite(date.getTime())) return undefined;
|
|
143
|
+
const today = new Date();
|
|
144
|
+
const yesterday = new Date(today);
|
|
145
|
+
yesterday.setDate(today.getDate() - 1);
|
|
146
|
+
if (dayKey(date) === dayKey(today)) return "今天";
|
|
147
|
+
if (dayKey(date) === dayKey(yesterday)) return "昨天";
|
|
148
|
+
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function messageAvatar(role) {
|
|
152
|
+
const avatar = node("span", {
|
|
153
|
+
class: `message-avatar ${role === "user" ? "person-avatar" : "buddy-message-avatar"}`,
|
|
154
|
+
"aria-hidden": "true",
|
|
155
|
+
});
|
|
156
|
+
if (role !== "user") avatar.textContent = "搭";
|
|
157
|
+
return avatar;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function phaseCopy(phase) {
|
|
161
|
+
return {
|
|
162
|
+
idle: "未启动",
|
|
163
|
+
starting: "正在启动",
|
|
164
|
+
ready: "DEV 在线",
|
|
165
|
+
stopping: "正在停止",
|
|
166
|
+
failed: "启动失败",
|
|
167
|
+
}[phase] || phase;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applyDevState(next) {
|
|
171
|
+
if (!next || !Number.isFinite(next.revision)) return false;
|
|
172
|
+
if (Number.isFinite(state.dev?.revision) && next.revision < state.dev.revision) return false;
|
|
173
|
+
state.dev = next;
|
|
174
|
+
if (next.ready?.buddy) state.buddy = next.ready.buddy;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function runStatusCopy(status) {
|
|
179
|
+
return {
|
|
180
|
+
accepted: "已接收",
|
|
181
|
+
pending: "等待 Agent",
|
|
182
|
+
running: "正在回复",
|
|
183
|
+
cancel_requested: "正在停止",
|
|
184
|
+
completed: "已完成",
|
|
185
|
+
failed: "执行失败",
|
|
186
|
+
cancelled: "已取消",
|
|
187
|
+
idle: "等待消息",
|
|
188
|
+
}[status] || status;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function currentRun() {
|
|
192
|
+
const runs = state.debug.runs || [];
|
|
193
|
+
return runs.find((run) => run.runId === state.selectedRunId) || runs[0];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderProject() {
|
|
197
|
+
if (!state.project) return;
|
|
198
|
+
const localName = state.project.buddyId || "未绑定搭子";
|
|
199
|
+
const buddy = state.dev.ready?.buddy || state.buddy;
|
|
200
|
+
const name = buddy?.name || localName;
|
|
201
|
+
elements.projectName.textContent = name;
|
|
202
|
+
elements.buddyName.textContent = name;
|
|
203
|
+
elements.buddyName.title = buddy?.tagline || "";
|
|
204
|
+
const note = document.querySelector(".preview-note");
|
|
205
|
+
if (note) note.textContent = buddy ? (buddy.isDev ? "开发预览版 · 仅你可见" : "搭搭 App") : "搭搭 App · DEV";
|
|
206
|
+
elements.environmentRuntime.textContent = "Node.js · Buddy Runtime";
|
|
207
|
+
elements.environmentModel.textContent = state.project.model || "未指定";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let pairingRevision = -1;
|
|
211
|
+
let pairingRequest = 0;
|
|
212
|
+
|
|
213
|
+
function renderDev() {
|
|
214
|
+
const phase = state.dev.phase;
|
|
215
|
+
if (!elements.pairingPanel.hidden && pairingRevision !== state.dev.revision) void togglePairing(true);
|
|
216
|
+
elements.connectionState.dataset.phase = phase;
|
|
217
|
+
elements.connectionLabel.textContent = phaseCopy(phase);
|
|
218
|
+
elements.devControl.disabled = phase === "starting" || phase === "stopping";
|
|
219
|
+
elements.devControl.dataset.action = phase === "ready" ? "stop" : "start";
|
|
220
|
+
elements.devControl.textContent = phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
|
|
221
|
+
elements.environmentAgent.textContent = phase === "ready"
|
|
222
|
+
? `${state.dev.ready?.agentId || "已就绪"} · CLI 托管`
|
|
223
|
+
: phaseCopy(phase);
|
|
224
|
+
|
|
225
|
+
const active = state.simulator.activeRun;
|
|
226
|
+
elements.buddyStatus.textContent = phase !== "ready"
|
|
227
|
+
? phaseCopy(phase)
|
|
228
|
+
: active
|
|
229
|
+
? state.simulator.streaming?.text
|
|
230
|
+
? "正在输入…"
|
|
231
|
+
: "正在思考…"
|
|
232
|
+
: "DEV 在线";
|
|
233
|
+
elements.messageInput.disabled = phase !== "ready" || Boolean(active);
|
|
234
|
+
elements.sendMessage.hidden = Boolean(active);
|
|
235
|
+
updateSendButton();
|
|
236
|
+
syncPolling();
|
|
237
|
+
if (
|
|
238
|
+
phase === "failed"
|
|
239
|
+
&& state.dev.error?.message
|
|
240
|
+
&& state.reportedFailureRevision !== state.dev.revision
|
|
241
|
+
) {
|
|
242
|
+
state.reportedFailureRevision = state.dev.revision;
|
|
243
|
+
showError(new Error(state.dev.error.message));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function runOrdinal(run, index, runs) {
|
|
248
|
+
// Older running preview servers have no ordinal yet. Never infer a message
|
|
249
|
+
// association from list positions or timestamps; this is a display number only.
|
|
250
|
+
return Number.isInteger(run.ordinal) && run.ordinal > 0 ? run.ordinal : runs.length - index;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function runOptionLabel(run, index, runs) {
|
|
254
|
+
const date = dayKey(run.startedAt) === dayKey(new Date()) ? "" : `${dayLabel(run.startedAt) || ""} `;
|
|
255
|
+
return `第 ${runOrdinal(run, index, runs)} 轮 · ${date}${formatTime(run.startedAt)} · ${runStatusCopy(run.status)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let runPickerSignature = "";
|
|
259
|
+
|
|
260
|
+
function renderRuns() {
|
|
261
|
+
const runs = state.debug.runs || [];
|
|
262
|
+
const choosing = document.activeElement === elements.runPicker;
|
|
263
|
+
if (!state.selectedRunId || !runs.some((run) => run.runId === state.selectedRunId)) {
|
|
264
|
+
state.followLatestRun = true;
|
|
265
|
+
state.selectedRunId = runs[0]?.runId || "";
|
|
266
|
+
} else if (state.followLatestRun && !choosing) {
|
|
267
|
+
state.selectedRunId = runs[0].runId;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const options = runs.map((run, index) => [run.runId, runOptionLabel(run, index, runs)]);
|
|
271
|
+
const signature = JSON.stringify(options);
|
|
272
|
+
// Polling must not rebuild an open native menu or override its keyboard selection.
|
|
273
|
+
// Apply deferred changes on blur; unchanged snapshots keep the same option nodes.
|
|
274
|
+
if (!choosing) {
|
|
275
|
+
if (signature !== runPickerSignature) {
|
|
276
|
+
elements.runPicker.replaceChildren(...(options.length ? options : [["", "尚无运行记录"]])
|
|
277
|
+
.map(([value, label]) => node("option", { value }, label)));
|
|
278
|
+
runPickerSignature = signature;
|
|
279
|
+
}
|
|
280
|
+
elements.runPicker.value = state.selectedRunId;
|
|
281
|
+
}
|
|
282
|
+
elements.runPicker.disabled = !runs.length;
|
|
283
|
+
const index = runs.findIndex((run) => run.runId === state.selectedRunId);
|
|
284
|
+
elements.previousRun.disabled = index < 0 || index >= runs.length - 1;
|
|
285
|
+
elements.nextRun.disabled = index <= 0;
|
|
286
|
+
elements.followLatestRun.checked = state.followLatestRun;
|
|
287
|
+
|
|
288
|
+
const run = currentRun();
|
|
289
|
+
elements.runTitle.textContent = run ? `第 ${runOrdinal(run, index, runs)} 轮` : "尚无本地回合";
|
|
290
|
+
elements.runReference.hidden = !run;
|
|
291
|
+
elements.runId.textContent = run?.runId || "";
|
|
292
|
+
elements.runId.title = run?.runId || "";
|
|
293
|
+
elements.copyRunId.disabled = !run;
|
|
294
|
+
elements.runStatus.dataset.status = run?.status || "idle";
|
|
295
|
+
elements.runStatus.textContent = runStatusCopy(run?.status || "idle");
|
|
296
|
+
elements.metricAccept.textContent = formatDuration(run?.acceptLatencyMs);
|
|
297
|
+
elements.metricFirst.textContent = formatDuration(run?.firstTextLatencyMs);
|
|
298
|
+
elements.metricResponse.textContent = run?.completion === "no_reply" ? "—" : formatDuration(run?.responseTimeMs);
|
|
299
|
+
elements.metricMessages.textContent = String(state.simulator.messages?.length || 0);
|
|
300
|
+
renderOverview(run);
|
|
301
|
+
renderTimeline(run);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function selectRun(runId) {
|
|
305
|
+
if (!(state.debug.runs || []).some((run) => run.runId === runId)) return;
|
|
306
|
+
state.selectedRunId = runId;
|
|
307
|
+
state.followLatestRun = false;
|
|
308
|
+
renderDebug();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function stepRun(offset) {
|
|
312
|
+
const runs = state.debug.runs || [];
|
|
313
|
+
const index = runs.findIndex((run) => run.runId === state.selectedRunId);
|
|
314
|
+
if (index >= 0 && runs[index + offset]) selectRun(runs[index + offset].runId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function copyRunId() {
|
|
318
|
+
const run = currentRun();
|
|
319
|
+
if (!run) return;
|
|
320
|
+
try {
|
|
321
|
+
await navigator.clipboard.writeText(run.runId);
|
|
322
|
+
toast("回合 ID 已复制");
|
|
323
|
+
} catch {
|
|
324
|
+
showError(new Error("浏览器未允许复制,请手动选择回合 ID"));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function renderOverview(run) {
|
|
329
|
+
elements.overviewEmpty.hidden = Boolean(run);
|
|
330
|
+
elements.overviewContent.hidden = !run;
|
|
331
|
+
if (!run) return;
|
|
332
|
+
clear(elements.waterfall);
|
|
333
|
+
const accept = Number.isFinite(run.acceptLatencyMs) ? run.acceptLatencyMs : 0;
|
|
334
|
+
const first = Number.isFinite(run.firstTextLatencyMs) ? run.firstTextLatencyMs : undefined;
|
|
335
|
+
const total = Number.isFinite(run.responseTimeMs)
|
|
336
|
+
? run.responseTimeMs
|
|
337
|
+
: Math.max(accept, Date.now() - new Date(run.startedAt).getTime());
|
|
338
|
+
const waitingForText = Math.max(0, (first ?? total) - accept);
|
|
339
|
+
const streaming = first === undefined ? 0 : Math.max(0, total - first);
|
|
340
|
+
const maximum = Math.max(total, 1);
|
|
341
|
+
const rows = [
|
|
342
|
+
["App Server 接收", accept],
|
|
343
|
+
[first === undefined ? "等待首段文字" : "首段文字前", waitingForText],
|
|
344
|
+
[run.status === "completed" ? "完整回复生成" : "回复生成中", streaming],
|
|
345
|
+
];
|
|
346
|
+
for (const [label, duration] of rows) {
|
|
347
|
+
const row = node("div", { class: "waterfall-row" });
|
|
348
|
+
row.append(node("span", {}, label));
|
|
349
|
+
const track = node("span", { class: "waterfall-track" });
|
|
350
|
+
const fill = node("span", { class: "waterfall-fill" });
|
|
351
|
+
fill.style.transform = `scaleX(${Math.max(0.015, duration / maximum)})`;
|
|
352
|
+
track.append(fill);
|
|
353
|
+
row.append(track, node("strong", {}, formatDuration(duration)));
|
|
354
|
+
elements.waterfall.append(row);
|
|
355
|
+
}
|
|
356
|
+
elements.runSummaryCopy.textContent = run.status === "failed"
|
|
357
|
+
? "这个本地回合在 Agent 侧执行失败,没有形成完整回复;请到执行时间线和协议日志查看失败位置。"
|
|
358
|
+
: run.status === "cancelled"
|
|
359
|
+
? "这个本地回合已被停止,尚未完成的回复已经作废;此前已经生效的平台写操作不会自动回滚。"
|
|
360
|
+
: run.completion === "no_reply"
|
|
361
|
+
? "这个本地回合已经结束,但没有观察到新的完整回复。"
|
|
362
|
+
: run.status === "completed"
|
|
363
|
+
? `本次本地回合在 ${formatDuration(run.firstTextLatencyMs)} 收到首段文字,并在 ${formatDuration(run.responseTimeMs)} 完成正式回复。`
|
|
364
|
+
: run.status === "cancel_requested"
|
|
365
|
+
? "停止请求已经发往 App Server;输入框会在本地回合真正结束后重新开放。"
|
|
366
|
+
: first === undefined
|
|
367
|
+
? "当前本地回合正在等待第一段真实输出。收到后,模拟器会立即开始追加文字。"
|
|
368
|
+
: "当前本地回合正在生成回复,调试器与 App 模拟器会同步展示文字增量。";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function renderTimeline(run) {
|
|
372
|
+
clear(elements.timelineList);
|
|
373
|
+
const events = (state.debug.events || []).filter((event) => !run || event.runId === run.runId);
|
|
374
|
+
if (!events.length) {
|
|
375
|
+
elements.timelineList.append(node("li", { class: "timeline-empty" }, run ? "这个本地回合暂无可见事件。" : "发一条消息后,执行事件会出现在这里。"));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const event of [...events].reverse()) {
|
|
379
|
+
const item = node("li", { class: "timeline-item" });
|
|
380
|
+
item.append(node("time", { class: "timeline-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
|
|
381
|
+
item.append(node("span", { class: "timeline-dot", "aria-hidden": "true" }));
|
|
382
|
+
const copy = node("div", { class: "timeline-copy" });
|
|
383
|
+
copy.append(node("strong", {}, event.summary));
|
|
384
|
+
copy.append(node("p", {}, `${event.type}${Number.isFinite(event.durationMs) ? ` · ${formatDuration(event.durationMs)}` : ""}`));
|
|
385
|
+
item.append(copy);
|
|
386
|
+
elements.timelineList.append(item);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function renderContext() {
|
|
391
|
+
clear(elements.contextList);
|
|
392
|
+
elements.contextSource.textContent = "平台聊天记录";
|
|
393
|
+
const messages = state.simulator.messages || [];
|
|
394
|
+
if (!messages.length) {
|
|
395
|
+
elements.contextList.append(node("p", { class: "timeline-empty" }, "当前会话还没有消息。"));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
for (const message of messages) {
|
|
399
|
+
const row = node("article", { class: "context-row", "data-role": message.role });
|
|
400
|
+
row.append(node("span", { class: "context-role" }, message.role));
|
|
401
|
+
row.append(node("p", { class: "context-text" }, message.text));
|
|
402
|
+
row.append(node("time", { class: "context-time", dateTime: message.createdAt }, formatTime(message.createdAt)));
|
|
403
|
+
elements.contextList.append(row);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function renderProtocol() {
|
|
408
|
+
clear(elements.protocolLog);
|
|
409
|
+
const events = state.debug.events || [];
|
|
410
|
+
if (!events.length) {
|
|
411
|
+
elements.protocolLog.append(node("p", { class: "timeline-empty" }, "本机还没有协议事件。"));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
for (const event of events) {
|
|
415
|
+
const row = node("div", { class: "protocol-row" });
|
|
416
|
+
row.append(node("time", { class: "protocol-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
|
|
417
|
+
row.append(node("span", { class: "protocol-channel" }, event.channel));
|
|
418
|
+
row.append(node("span", { class: "protocol-type", title: event.summary }, event.type));
|
|
419
|
+
row.append(node("span", { class: "protocol-duration" }, formatDuration(event.durationMs)));
|
|
420
|
+
if (event.type === "agent.stdout" || event.type === "agent.stderr") {
|
|
421
|
+
row.append(node("pre", { class: "runtime-log-text" }, event.summary));
|
|
422
|
+
}
|
|
423
|
+
elements.protocolLog.append(row);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderMessages() {
|
|
428
|
+
clear(elements.chatMessages);
|
|
429
|
+
const messages = state.simulator.messages || [];
|
|
430
|
+
if (!messages.length && !state.simulator.activeRun) {
|
|
431
|
+
elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }));
|
|
432
|
+
}
|
|
433
|
+
let renderedDay;
|
|
434
|
+
for (const message of messages) {
|
|
435
|
+
const messageDay = dayKey(message.createdAt);
|
|
436
|
+
if (messageDay && messageDay !== renderedDay && dayLabel(message.createdAt) !== "今天") {
|
|
437
|
+
elements.chatMessages.append(node("div", { class: "chat-date" }, dayLabel(message.createdAt)));
|
|
438
|
+
renderedDay = messageDay;
|
|
439
|
+
}
|
|
440
|
+
const row = node("div", { class: "message-row", "data-role": message.role });
|
|
441
|
+
const wrap = node("div", { class: "message-content" });
|
|
442
|
+
wrap.append(node("div", { class: "message-bubble" }, message.text));
|
|
443
|
+
if (message.role === "user") row.append(wrap);
|
|
444
|
+
else row.append(messageAvatar("assistant"), wrap);
|
|
445
|
+
elements.chatMessages.append(row);
|
|
446
|
+
}
|
|
447
|
+
if (state.simulator.activeRun) {
|
|
448
|
+
const row = node("div", { class: "message-row typing-row", "data-role": "assistant" });
|
|
449
|
+
const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
|
|
450
|
+
indicator.append(node("i"), node("i"), node("i"));
|
|
451
|
+
row.append(messageAvatar("assistant"), indicator);
|
|
452
|
+
elements.chatMessages.append(row);
|
|
453
|
+
}
|
|
454
|
+
window.requestAnimationFrame(() => {
|
|
455
|
+
elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderDebug() {
|
|
460
|
+
renderRuns();
|
|
461
|
+
renderContext();
|
|
462
|
+
renderProtocol();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderAll() {
|
|
466
|
+
renderProject();
|
|
467
|
+
renderDev();
|
|
468
|
+
renderDebug();
|
|
469
|
+
renderMessages();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function refreshDebug() {
|
|
473
|
+
const value = await api("/api/debug");
|
|
474
|
+
state.debug = value.debug;
|
|
475
|
+
renderDebug();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function refreshSimulator() {
|
|
479
|
+
if (state.dev.phase !== "ready" || state.pollingBusy) return;
|
|
480
|
+
state.pollingBusy = true;
|
|
481
|
+
try {
|
|
482
|
+
const value = await api("/api/simulator/state");
|
|
483
|
+
state.simulator = value.state;
|
|
484
|
+
renderDev();
|
|
485
|
+
renderMessages();
|
|
486
|
+
renderContext();
|
|
487
|
+
await refreshDebug();
|
|
488
|
+
} catch (cause) {
|
|
489
|
+
if (cause?.code !== "SIMULATOR_NOT_READY") showError(cause);
|
|
490
|
+
} finally {
|
|
491
|
+
state.pollingBusy = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function syncPolling() {
|
|
496
|
+
if (state.dev.phase === "ready" && state.polling === undefined) {
|
|
497
|
+
void refreshSimulator();
|
|
498
|
+
state.polling = window.setInterval(() => void refreshSimulator(), 1200);
|
|
499
|
+
} else if (state.dev.phase !== "ready" && state.polling !== undefined) {
|
|
500
|
+
window.clearInterval(state.polling);
|
|
501
|
+
state.polling = undefined;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function controlDev() {
|
|
506
|
+
if (state.dev.phase === "starting" || state.dev.phase === "stopping") return;
|
|
507
|
+
elements.devControl.disabled = true;
|
|
508
|
+
try {
|
|
509
|
+
const path = state.dev.phase === "ready" ? "/api/dev/stop" : "/api/dev/start";
|
|
510
|
+
const value = await api(path, { method: "POST" });
|
|
511
|
+
applyDevState(value.state);
|
|
512
|
+
if (state.dev.phase !== "ready" && path.endsWith("stop")) state.simulator = { messages: state.simulator.messages || [] };
|
|
513
|
+
renderAll();
|
|
514
|
+
} catch (cause) {
|
|
515
|
+
showError(cause);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function updateSendButton() {
|
|
520
|
+
const ready = state.dev.phase === "ready" && !state.simulator.activeRun;
|
|
521
|
+
elements.sendMessage.disabled = !ready || !elements.messageInput.value.trim();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let pendingSend;
|
|
525
|
+
|
|
526
|
+
async function sendMessage(event) {
|
|
527
|
+
event.preventDefault();
|
|
528
|
+
const text = elements.messageInput.value;
|
|
529
|
+
if (!text.trim() || state.dev.phase !== "ready" || state.simulator.activeRun) return;
|
|
530
|
+
elements.messageInput.disabled = true;
|
|
531
|
+
elements.sendMessage.disabled = true;
|
|
532
|
+
// Keep the logical message identity after an unknown outcome; retries are user initiated.
|
|
533
|
+
if (!pendingSend || pendingSend.text !== text) {
|
|
534
|
+
pendingSend = { text, idempotencyKey: window.crypto.randomUUID
|
|
535
|
+
? window.crypto.randomUUID()
|
|
536
|
+
: `preview-${Date.now()}-${Math.random().toString(16).slice(2)}` };
|
|
537
|
+
}
|
|
538
|
+
const { idempotencyKey } = pendingSend;
|
|
539
|
+
try {
|
|
540
|
+
const accepted = await api("/api/simulator/messages", {
|
|
541
|
+
method: "POST",
|
|
542
|
+
body: JSON.stringify({ text, idempotencyKey }),
|
|
543
|
+
});
|
|
544
|
+
pendingSend = undefined;
|
|
545
|
+
elements.messageInput.value = "";
|
|
546
|
+
resizeComposer();
|
|
547
|
+
state.simulator.activeRun = { runId: accepted.runId, status: "pending" };
|
|
548
|
+
renderDev();
|
|
549
|
+
renderMessages();
|
|
550
|
+
await refreshSimulator();
|
|
551
|
+
} catch (cause) {
|
|
552
|
+
showError(cause);
|
|
553
|
+
renderDev();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function resizeComposer() {
|
|
558
|
+
elements.messageInput.style.height = "auto";
|
|
559
|
+
elements.messageInput.style.height = `${Math.min(elements.messageInput.scrollHeight, 78)}px`;
|
|
560
|
+
updateSendButton();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function activateTab(tab) {
|
|
564
|
+
const tabs = [...document.querySelectorAll('[role="tab"]')];
|
|
565
|
+
for (const item of tabs) {
|
|
566
|
+
const selected = item === tab;
|
|
567
|
+
item.setAttribute("aria-selected", String(selected));
|
|
568
|
+
item.tabIndex = selected ? 0 : -1;
|
|
569
|
+
const panel = document.getElementById(item.getAttribute("aria-controls"));
|
|
570
|
+
panel.hidden = !selected;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function togglePairing(force) {
|
|
575
|
+
const requestId = ++pairingRequest;
|
|
576
|
+
pairingRevision = state.dev.revision;
|
|
577
|
+
const open = force ?? elements.pairingPanel.hidden;
|
|
578
|
+
elements.pairingPanel.hidden = !open;
|
|
579
|
+
elements.pairDevice.setAttribute("aria-expanded", String(open));
|
|
580
|
+
if (!open) return;
|
|
581
|
+
clear(elements.pairingBody);
|
|
582
|
+
elements.pairingBody.append(node("p", { class: "pairing-status" }, "正在读取真机入口…"));
|
|
583
|
+
try {
|
|
584
|
+
const { pairing } = await api("/api/device/pairing");
|
|
585
|
+
if (requestId !== pairingRequest || elements.pairingPanel.hidden) return;
|
|
586
|
+
const copy = node("div");
|
|
587
|
+
clear(elements.pairingBody);
|
|
588
|
+
if (pairing.status === "ready") {
|
|
589
|
+
if (typeof pairing.qrCodeDataUrl !== "string" || !pairing.qrCodeDataUrl.startsWith("data:image/png;base64,")) {
|
|
590
|
+
throw new Error("二维码生成失败,请重新打开真机预览。");
|
|
591
|
+
}
|
|
592
|
+
elements.pairingBody.append(node("img", {
|
|
593
|
+
class: "pairing-qr", src: pairing.qrCodeDataUrl, width: "256", height: "256", alt: "打开当前开发版搭子的二维码",
|
|
594
|
+
}));
|
|
595
|
+
copy.append(node("p", { class: "pairing-status" }, "真机入口已准备好"));
|
|
596
|
+
copy.append(node("p", { class: "pairing-hint" }, "使用支持 App 链接的扫码工具打开。手机需安装支持预览入口的搭搭 App,并登录同一开发者账号。"));
|
|
597
|
+
if (pairing.code) copy.append(node("p", { class: "pairing-code" }, pairing.code));
|
|
598
|
+
const link = node("a", { class: "pairing-link", href: pairing.url }, pairing.url);
|
|
599
|
+
copy.append(link);
|
|
600
|
+
} else {
|
|
601
|
+
copy.append(node("p", { class: "pairing-status" }, "真机预览暂不可用"));
|
|
602
|
+
copy.append(node("p", { class: "pairing-hint" }, pairing.reason));
|
|
603
|
+
}
|
|
604
|
+
elements.pairingBody.append(copy);
|
|
605
|
+
} catch (cause) {
|
|
606
|
+
if (requestId !== pairingRequest || elements.pairingPanel.hidden) return;
|
|
607
|
+
clear(elements.pairingBody);
|
|
608
|
+
elements.pairingBody.append(node("p", { class: "pairing-status" }, cause instanceof Error ? cause.message : "真机入口读取失败"));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function copyProtocol() {
|
|
613
|
+
const text = (state.debug.events || []).map((event) => [
|
|
614
|
+
event.timestamp,
|
|
615
|
+
event.channel,
|
|
616
|
+
event.type,
|
|
617
|
+
event.runId || "-",
|
|
618
|
+
Number.isFinite(event.durationMs) ? `${event.durationMs}ms` : "-",
|
|
619
|
+
event.summary,
|
|
620
|
+
].join("\t")).join("\n");
|
|
621
|
+
try {
|
|
622
|
+
await navigator.clipboard.writeText(text);
|
|
623
|
+
toast("协议日志已复制");
|
|
624
|
+
} catch {
|
|
625
|
+
showError(new Error("浏览器未允许复制,请在协议日志中手动选择"));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function connectEvents() {
|
|
630
|
+
const events = new EventSource("/api/events");
|
|
631
|
+
events.addEventListener("dev.state", (event) => {
|
|
632
|
+
try {
|
|
633
|
+
if (applyDevState(JSON.parse(event.data))) renderDev();
|
|
634
|
+
} catch {
|
|
635
|
+
// A malformed local event is ignored; the next API refresh repairs state.
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
events.addEventListener("simulator.state", (event) => {
|
|
639
|
+
try {
|
|
640
|
+
const value = JSON.parse(event.data);
|
|
641
|
+
state.simulator = value.state;
|
|
642
|
+
state.debug = value.debug;
|
|
643
|
+
renderDev();
|
|
644
|
+
renderDebug();
|
|
645
|
+
renderMessages();
|
|
646
|
+
} catch {
|
|
647
|
+
// Polling remains the reconciliation path for a malformed or missed local event.
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function boot() {
|
|
653
|
+
try {
|
|
654
|
+
const [project, dev, debug] = await Promise.all([
|
|
655
|
+
api("/api/project"),
|
|
656
|
+
api("/api/dev"),
|
|
657
|
+
api("/api/debug"),
|
|
658
|
+
]);
|
|
659
|
+
state.project = project.project;
|
|
660
|
+
state.dev = dev.state;
|
|
661
|
+
state.debug = debug.debug;
|
|
662
|
+
renderAll();
|
|
663
|
+
connectEvents();
|
|
664
|
+
// Load the cloud profile independently of Agent startup. If it fails,
|
|
665
|
+
// the local project and DEV controls remain usable and show the error.
|
|
666
|
+
const { buddy } = await api("/api/buddy");
|
|
667
|
+
state.buddy = state.dev.ready?.buddy || buddy;
|
|
668
|
+
renderProject();
|
|
669
|
+
} catch (cause) {
|
|
670
|
+
showError(cause);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
elements.devControl.addEventListener("click", () => void controlDev());
|
|
675
|
+
elements.composer.addEventListener("submit", (event) => void sendMessage(event));
|
|
676
|
+
elements.messageInput.addEventListener("input", resizeComposer);
|
|
677
|
+
elements.messageInput.addEventListener("keydown", (event) => {
|
|
678
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
elements.composer.requestSubmit();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
elements.runPicker.addEventListener("change", () => {
|
|
684
|
+
selectRun(elements.runPicker.value);
|
|
685
|
+
});
|
|
686
|
+
elements.runPicker.addEventListener("blur", renderDebug);
|
|
687
|
+
elements.previousRun.addEventListener("click", () => stepRun(1));
|
|
688
|
+
elements.nextRun.addEventListener("click", () => stepRun(-1));
|
|
689
|
+
elements.followLatestRun.addEventListener("change", () => {
|
|
690
|
+
state.followLatestRun = elements.followLatestRun.checked;
|
|
691
|
+
renderDebug();
|
|
692
|
+
});
|
|
693
|
+
elements.copyRunId.addEventListener("click", () => void copyRunId());
|
|
694
|
+
elements.pairDevice.addEventListener("click", () => void togglePairing());
|
|
695
|
+
$("#close-pairing").addEventListener("click", () => void togglePairing(false));
|
|
696
|
+
$("#dismiss-error").addEventListener("click", () => { elements.errorBanner.hidden = true; });
|
|
697
|
+
$("#copy-protocol").addEventListener("click", () => void copyProtocol());
|
|
698
|
+
document.addEventListener("keydown", (event) => {
|
|
699
|
+
if (event.key === "Escape" && !elements.pairingPanel.hidden) void togglePairing(false);
|
|
700
|
+
});
|
|
701
|
+
for (const tab of document.querySelectorAll('[role="tab"]')) {
|
|
702
|
+
tab.addEventListener("click", () => activateTab(tab));
|
|
703
|
+
tab.addEventListener("keydown", (event) => {
|
|
704
|
+
const tabs = [...document.querySelectorAll('[role="tab"]')].filter((item) => !item.hidden);
|
|
705
|
+
const index = tabs.indexOf(tab);
|
|
706
|
+
const target = event.key === "ArrowRight" || event.key === "ArrowDown"
|
|
707
|
+
? tabs[(index + 1) % tabs.length]
|
|
708
|
+
: event.key === "ArrowLeft" || event.key === "ArrowUp"
|
|
709
|
+
? tabs[(index - 1 + tabs.length) % tabs.length]
|
|
710
|
+
: event.key === "Home"
|
|
711
|
+
? tabs[0]
|
|
712
|
+
: event.key === "End"
|
|
713
|
+
? tabs[tabs.length - 1]
|
|
714
|
+
: undefined;
|
|
715
|
+
if (target) {
|
|
716
|
+
event.preventDefault();
|
|
717
|
+
activateTab(target);
|
|
718
|
+
target.focus();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
void boot();
|
|
724
|
+
})();
|