@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.
Files changed (158) hide show
  1. package/README.md +314 -0
  2. package/dist/bin/buddy.js +443 -0
  3. package/dist/bin/buddy.js.map +7 -0
  4. package/dist/bin/core.js +5638 -0
  5. package/dist/bin/core.js.map +7 -0
  6. package/dist/bin/preview.js +1429 -0
  7. package/dist/bin/preview.js.map +7 -0
  8. package/dist/web/app.css +508 -0
  9. package/dist/web/app.js +724 -0
  10. package/dist/web/assets/developer-platform-icon.png.base64 +1 -0
  11. package/dist/web/brand.css +100 -0
  12. package/dist/web/index.html +236 -0
  13. package/dist/web/tokens.css +15 -0
  14. package/package.json +45 -0
  15. package/resources/README.md +52 -0
  16. package/resources/agent-template/package-lock.json +1726 -0
  17. package/resources/agent-template/package.json +16 -0
  18. package/resources/brand/README.md +11 -0
  19. package/resources/brand/login-result.css +25 -0
  20. package/resources/brand/logo.png +0 -0
  21. package/resources/brand/tokens.css +100 -0
  22. package/resources/buddy-creator/INSTALL.md +25 -0
  23. package/resources/buddy-creator/PATCHES.md +33 -0
  24. package/resources/buddy-creator/SKILL.md +66 -0
  25. package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +154 -0
  26. package/resources/buddy-creator/agents/openai.yaml +4 -0
  27. package/resources/buddy-creator/assets/licenses/bail.txt +26 -0
  28. package/resources/buddy-creator/assets/licenses/ccount.txt +26 -0
  29. package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +26 -0
  30. package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +26 -0
  31. package/resources/buddy-creator/assets/licenses/character-entities.txt +26 -0
  32. package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +26 -0
  33. package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +26 -0
  34. package/resources/buddy-creator/assets/licenses/debug.txt +24 -0
  35. package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +26 -0
  36. package/resources/buddy-creator/assets/licenses/dequal.txt +25 -0
  37. package/resources/buddy-creator/assets/licenses/devlop.txt +26 -0
  38. package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +13 -0
  39. package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +26 -0
  40. package/resources/buddy-creator/assets/licenses/extend.txt +27 -0
  41. package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +26 -0
  42. package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +26 -0
  43. package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +25 -0
  44. package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +13 -0
  45. package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +26 -0
  46. package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +26 -0
  47. package/resources/buddy-creator/assets/licenses/is-decimal.txt +26 -0
  48. package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +26 -0
  49. package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +13 -0
  50. package/resources/buddy-creator/assets/licenses/longest-streak.txt +26 -0
  51. package/resources/buddy-creator/assets/licenses/markdown-table.txt +26 -0
  52. package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +26 -0
  53. package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +26 -0
  54. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +26 -0
  55. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +26 -0
  56. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +26 -0
  57. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +26 -0
  58. package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +26 -0
  59. package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +26 -0
  60. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +26 -0
  61. package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +26 -0
  62. package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +26 -0
  63. package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +27 -0
  64. package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +26 -0
  65. package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +26 -0
  66. package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +26 -0
  67. package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +26 -0
  68. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +26 -0
  69. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +26 -0
  70. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +26 -0
  71. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +26 -0
  72. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +26 -0
  73. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +26 -0
  74. package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +26 -0
  75. package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +26 -0
  76. package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +26 -0
  77. package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +26 -0
  78. package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +26 -0
  79. package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +26 -0
  80. package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +26 -0
  81. package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +26 -0
  82. package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +26 -0
  83. package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +26 -0
  84. package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +26 -0
  85. package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +26 -0
  86. package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +26 -0
  87. package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +26 -0
  88. package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +26 -0
  89. package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +26 -0
  90. package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +26 -0
  91. package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +26 -0
  92. package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +26 -0
  93. package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +26 -0
  94. package/resources/buddy-creator/assets/licenses/micromark.txt +26 -0
  95. package/resources/buddy-creator/assets/licenses/ms.txt +25 -0
  96. package/resources/buddy-creator/assets/licenses/parse-entities.txt +26 -0
  97. package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +25 -0
  98. package/resources/buddy-creator/assets/licenses/property-information.txt +26 -0
  99. package/resources/buddy-creator/assets/licenses/react-dom.txt +25 -0
  100. package/resources/buddy-creator/assets/licenses/react-markdown.txt +25 -0
  101. package/resources/buddy-creator/assets/licenses/react.txt +25 -0
  102. package/resources/buddy-creator/assets/licenses/remark-gfm.txt +26 -0
  103. package/resources/buddy-creator/assets/licenses/remark-parse.txt +25 -0
  104. package/resources/buddy-creator/assets/licenses/remark-rehype.txt +26 -0
  105. package/resources/buddy-creator/assets/licenses/remark-stringify.txt +25 -0
  106. package/resources/buddy-creator/assets/licenses/scheduler.txt +25 -0
  107. package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +26 -0
  108. package/resources/buddy-creator/assets/licenses/stringify-entities.txt +26 -0
  109. package/resources/buddy-creator/assets/licenses/style-to-js.txt +26 -0
  110. package/resources/buddy-creator/assets/licenses/style-to-object.txt +26 -0
  111. package/resources/buddy-creator/assets/licenses/trim-lines.txt +26 -0
  112. package/resources/buddy-creator/assets/licenses/trough.txt +25 -0
  113. package/resources/buddy-creator/assets/licenses/types--debug.txt +25 -0
  114. package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +25 -0
  115. package/resources/buddy-creator/assets/licenses/types--estree.txt +25 -0
  116. package/resources/buddy-creator/assets/licenses/types--hast.txt +25 -0
  117. package/resources/buddy-creator/assets/licenses/types--mdast.txt +25 -0
  118. package/resources/buddy-creator/assets/licenses/types--ms.txt +25 -0
  119. package/resources/buddy-creator/assets/licenses/types--unist.txt +25 -0
  120. package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +19 -0
  121. package/resources/buddy-creator/assets/licenses/unified.txt +25 -0
  122. package/resources/buddy-creator/assets/licenses/unist-util-is.txt +26 -0
  123. package/resources/buddy-creator/assets/licenses/unist-util-position.txt +26 -0
  124. package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +26 -0
  125. package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +26 -0
  126. package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +26 -0
  127. package/resources/buddy-creator/assets/licenses/vfile-message.txt +26 -0
  128. package/resources/buddy-creator/assets/licenses/vfile.txt +25 -0
  129. package/resources/buddy-creator/assets/licenses/zwitch.txt +26 -0
  130. package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +39 -0
  131. package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +4 -0
  132. package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
  133. package/resources/buddy-creator/assets/preview/brand.css +100 -0
  134. package/resources/buddy-creator/assets/preview/index.html +5 -0
  135. package/resources/buddy-creator/assets/preview/reader.css +165 -0
  136. package/resources/buddy-creator/assets/preview/reader.js +135 -0
  137. package/resources/buddy-creator/references/artifact-schema.md +68 -0
  138. package/resources/buddy-creator/references/catalog.json +401 -0
  139. package/resources/buddy-creator/references/definition.md +24 -0
  140. package/resources/buddy-creator/references/dialogue-examples.md +133 -0
  141. package/resources/buddy-creator/references/host-guide.md +213 -0
  142. package/resources/buddy-creator/references/interview.md +235 -0
  143. package/resources/buddy-creator/references/knowledge.md +40 -0
  144. package/resources/buddy-creator/references/methods.md +77 -0
  145. package/resources/buddy-creator/references/opening.md +14 -0
  146. package/resources/buddy-creator/references/preview-panel.md +66 -0
  147. package/resources/buddy-creator/references/recovery.md +43 -0
  148. package/resources/buddy-creator/references/service.md +74 -0
  149. package/resources/buddy-creator/scripts/buddy.py +108 -0
  150. package/resources/buddy-creator/scripts/buddy_client.py +67 -0
  151. package/resources/buddy-creator/scripts/buddy_core.py +803 -0
  152. package/resources/buddy-creator/scripts/completion.py +240 -0
  153. package/resources/buddy-creator/scripts/interview.py +215 -0
  154. package/resources/buddy-creator/scripts/preview.py +577 -0
  155. package/resources/buddy-creator/scripts/preview_panel.py +80 -0
  156. package/resources/buddy-creator/scripts/sources.py +239 -0
  157. package/resources/buddy-creator/version.json +8 -0
  158. package/resources/buddy-creator.manifest.json +696 -0
@@ -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
+ })();