@my-life-buddies/cli 0.27.1-dev.2 → 0.27.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.
@@ -1,193 +0,0 @@
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 responseKind = $("models-response-kind");
9
- const modelSearch = $("models-search"), logSearch = $("logs-search");
10
- let snapshot = {}, conversation = "", visibleLogs = [];
11
- const cached = new Map(), expanded = new Map();
12
- const object = value => value && typeof value === "object" && !Array.isArray(value) ? value : {};
13
- const json = value => typeof value === "string" ? value : JSON.stringify(value, null, 2);
14
- const number = value => typeof value === "number" && Number.isFinite(value) && value >= 0;
15
- const newestFirst = (a, b) => (Date.parse(b.timestamp) || 0) - (Date.parse(a.timestamp) || 0);
16
- const metricHelp = {
17
- "模型响应耗时": "本次模型请求发出至响应结束。",
18
- "模型首字耗时": "本次模型请求发出至首段输出。",
19
- "结束原因": "模型停止原因;toolUse 表示请求调用工具。",
20
- "输入 Token": "本次调用的输入 Token 数。",
21
- "输出 Token": "本次调用的输出 Token 数。",
22
- "缓存读取": "本次调用读取缓存的 Token 数。",
23
- "缓存写入": "本次调用写入缓存的 Token 数。",
24
- "总 Token": "本次调用上报的 Token 总数。",
25
- };
26
- function section(label, value, key) {
27
- const details = node("details", { class: "diagnostic-section", "data-detail": key });
28
- details.append(node("summary", {}, label));
29
- details.append(node("pre", {}, json(value)));
30
- return details;
31
- }
32
- function metrics(pairs) {
33
- const list = node("dl", { class: "diagnostic-metrics" });
34
- for (const [label, value] of pairs) {
35
- const item = node("div");
36
- const detail = node("dd", {}, value);
37
- if (metricHelp[label]) {
38
- detail.append(node("p", { class: "metric-help" }, metricHelp[label]));
39
- }
40
- item.append(node("dt", {}, label), detail);
41
- list.append(item);
42
- }
43
- return list;
44
- }
45
- async function copy(text) {
46
- try { await navigator.clipboard.writeText(text); toast("日志已复制"); }
47
- catch { showError(new Error("浏览器未允许复制,请展开原始日志后手动选择")); }
48
- }
49
- function describe(record) {
50
- const data = object(record.data), payload = object(data.payload);
51
- if (record.kind === "request") {
52
- const messages = payload.messages || payload.input;
53
- const count = Array.isArray(messages) ? `${messages.length} 条上下文` : "请求上下文";
54
- return `${typeof payload.model === "string" ? payload.model : "模型未上报"} · ${count}${Array.isArray(payload.tools) ? ` · ${payload.tools.length} 个工具` : ""}`;
55
- }
56
- const content = Array.isArray(data.content) ? data.content : [];
57
- const texts = content.filter(item => item?.type === "text").map(item => item.text).filter(text => typeof text === "string");
58
- const tools = content.filter(item => item?.type === "toolCall").map(item => item.name).filter(name => typeof name === "string");
59
- return typeof data.error === "string" ? data.error : tools.length ? `工具调用:${tools.join("、")}` : texts.join(" ") || record.title;
60
- }
61
- function row(record, model) {
62
- const data = object(record.data);
63
- const details = node("details", { class: "diagnostic-entry", "data-record": record.id, "data-detail": "entry" });
64
- details.dataset.level = data.error || data.stopReason === "error" ? "error" : record.level;
65
- const summary = node("summary");
66
- summary.append(node("span", { class: "diagnostic-badge", "data-kind": record.kind }, model ? record.kind === "request" ? "请求" : "响应" : record.level.toUpperCase()));
67
- const heading = node("span", { class: "diagnostic-heading" });
68
- heading.append(node("strong", {}, model ? record.kind === "request" ? "模型请求" : "模型响应" : record.title));
69
- if (model) heading.append(node("span", { class: "diagnostic-preview" }, describe(record)));
70
- summary.append(heading, node("time", { dateTime: record.timestamp, title: record.timestamp }, formatTime(record.timestamp)));
71
- details.append(summary);
72
- const body = node("div", { class: "diagnostic-body" });
73
- const meta = node("div", { class: "diagnostic-meta" });
74
- meta.append(node("span", {}, `${record.convoKey || "未标注会话"} · ${record.source}`));
75
- const button = node("button", { type: "button", class: "text-button" }, "复制原始日志");
76
- button.addEventListener("click", () => void copy(record.raw));
77
- meta.append(button); body.append(meta);
78
- if (record.truncated || (model && !record.data)) {
79
- body.append(node("p", { class: "diagnostic-warning" }, record.truncated
80
- ? "日志已截断,无法还原完整数据。请查看原始日志。"
81
- : "尚未收到完整数据,或日志格式无法解析。已保留原始日志。"));
82
- }
83
- if (model && record.data) {
84
- if (record.kind === "request") {
85
- const payload = object(data.payload);
86
- const messages = payload.messages ?? payload.input;
87
- const system = payload.system ?? (Array.isArray(messages) ? messages.filter(item => ["system", "developer"].includes(item?.role)) : undefined);
88
- if (system != null && (!Array.isArray(system) || system.length)) body.append(section("系统提示词", system, "system"));
89
- if (messages != null) body.append(section(`请求上下文${Array.isArray(messages) ? ` · ${messages.length} 条` : ""}`, messages, "messages"));
90
- if (payload.tools != null) body.append(section(`可用工具${Array.isArray(payload.tools) ? ` · ${payload.tools.length} 个` : ""}`, payload.tools, "tools"));
91
- const { messages: _messages, input: _input, system: _system, tools: _tools, ...parameters } = payload;
92
- body.append(section("模型与请求参数", parameters, "parameters"));
93
- body.append(section("完整请求体", data.payload ?? data, "payload"));
94
- } else {
95
- const usage = object(data.usage);
96
- body.append(metrics([
97
- ["模型响应耗时", number(data.ms) ? formatDuration(data.ms) : "未上报"],
98
- ["模型首字耗时", number(data.firstTokenMs) ? formatDuration(data.firstTokenMs) : "未上报"],
99
- ["结束原因", typeof data.stopReason === "string" ? data.stopReason : "未上报"],
100
- ]));
101
- const tokenFields = [["输入 Token", "input"], ["输出 Token", "output"], ["缓存读取", "cacheRead"], ["缓存写入", "cacheWrite"], ["总 Token", "totalTokens"]];
102
- body.append(metrics(tokenFields.map(([label, key]) => [label, number(usage[key]) ? String(usage[key]) : "未上报"])));
103
- if (typeof data.error === "string" && data.error) body.append(node("p", { class: "diagnostic-warning" }, data.error));
104
- const content = Array.isArray(data.content) ? data.content : [];
105
- content.forEach((block, index) => {
106
- if (!block || typeof block !== "object") return;
107
- const label = block.type === "text" ? "响应正文" : block.type === "thinking" ? "思考内容(日志已提供)" : block.type === "toolCall" ? `工具调用请求 · ${block.name || "未命名"}` : `响应片段 · ${block.type || index + 1}`;
108
- body.append(section(label, block.type === "text" ? block.text : block, `content-${index}`));
109
- });
110
- if (data.content != null && !content.length) body.append(section("响应内容", data.content, "content"));
111
- if (data.usage != null) body.append(section("完整用量", data.usage, "usage"));
112
- }
113
- }
114
- body.append(section("原始日志", record.raw, "raw"));
115
- details.append(body);
116
- return details;
117
- }
118
- function reconcile(container, records, model, emptyText) {
119
- const prefix = model ? "model:" : "log:";
120
- const wanted = new Set();
121
- records.forEach((record, index) => {
122
- const key = prefix + record.id;
123
- wanted.add(key);
124
- const signature = JSON.stringify(record);
125
- let entry = cached.get(key);
126
- if (!entry || entry.signature !== signature) {
127
- if (entry) expanded.set(key, [...entry.node.querySelectorAll("details"), entry.node].filter(item => item.open).map(item => item.dataset.detail));
128
- const element = row(record, model);
129
- for (const item of [...element.querySelectorAll("details"), element]) item.open = (expanded.get(key) || []).includes(item.dataset.detail);
130
- entry?.node.remove();
131
- entry = { node: element, signature };
132
- cached.set(key, entry);
133
- }
134
- if (container.children[index] !== entry.node) container.insertBefore(entry.node, container.children[index] || null);
135
- });
136
- for (const child of [...container.children]) if (!wanted.has(prefix + child.dataset.record)) child.remove();
137
- if (!records.length) {
138
- const empty = node("div", { class: "diagnostic-empty" });
139
- empty.append(node("strong", {}, emptyText[0]), node("p", {}, emptyText[1]));
140
- container.append(empty);
141
- }
142
- }
143
- function updateConversations(records) {
144
- const keys = [...new Set(records.map(record => record.convoKey).filter(Boolean))].sort();
145
- if (conversation && !keys.includes(conversation)) keys.push(conversation);
146
- for (const select of [modelConversation, logConversation]) {
147
- if (document.activeElement === select) continue;
148
- const signature = JSON.stringify(keys);
149
- if (select.dataset.options !== signature) {
150
- select.replaceChildren(node("option", { value: "" }, "全部会话"), ...keys.map(key => node("option", { value: key }, key)));
151
- select.dataset.options = signature;
152
- }
153
- select.value = conversation;
154
- }
155
- }
156
- function render() {
157
- const runtime = snapshot.diagnostics?.records || [];
158
- updateConversations(runtime);
159
- const matches = (record, search) => (!conversation || record.convoKey === conversation) && (!search || record.raw.toLocaleLowerCase().includes(search.toLocaleLowerCase()));
160
- const modelRecords = runtime.filter(record => record.kind === "request" || record.kind === "response").sort(newestFirst);
161
- if ($("models-response-filter")) $("models-response-filter").hidden = kind.value === "request";
162
- const filteredModels = modelRecords.filter(record => {
163
- if ((kind.value && record.kind !== kind.value) || !matches(record, modelSearch.value.trim())) return false;
164
- if (!responseKind?.value || kind.value === "request") return true;
165
- if (record.kind !== "response" || record.truncated || !Array.isArray(record.data?.content)) return false;
166
- const hasTool = record.data.content.some(block => block?.type === "toolCall");
167
- return responseKind.value === "tool" ? hasTool : !hasTool && record.data.content.some(block => block?.type === "text");
168
- });
169
- reconcile(models, filteredModels, true, modelRecords.length
170
- ? ["没有匹配的模型记录", "试试其他会话、类型或关键词。"]
171
- : ["等待模型调用", "启动 DEV 并发送消息后,Runtime 提供的模型请求与响应会出现在这里。旧版本或未输出这些日志的 Runtime 可先查看运行日志。"]);
172
- $("models-count").textContent = `${filteredModels.length} 条记录${snapshot.diagnostics?.dropped ? ` · 已淘汰 ${snapshot.diagnostics.dropped} 条较早日志` : ""}`;
173
- const events = snapshot.events || [];
174
- const deltas = events.filter(event => event.type === "run.output.delta").length;
175
- const other = events.filter(event => event.type !== "run.output.delta" && (!snapshot.diagnostics || !/^agent\.(stdout|stderr)$/.test(event.type))).map(event => ({
176
- id: event.eventId, timestamp: event.timestamp, source: event.channel, level: /failed|error|stderr/.test(event.type) ? "error" : "info", title: event.summary || event.type,
177
- kind: "log", raw: `${event.timestamp} ${event.channel} ${event.type}${event.runId ? ` ${event.runId}` : ""}\n${event.summary || ""}`,
178
- }));
179
- const allLogs = [...runtime, ...other].sort(newestFirst);
180
- 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()));
181
- reconcile(logs, visibleLogs, false, allLogs.length ? ["没有匹配的日志", "调整会话、级别或搜索条件后再试。"] : ["等待运行日志", "启动 DEV 后,这里会保留进程输出与连接事件。"]);
182
- $("logs-count").textContent = `${visibleLogs.length} 条可见日志${deltas ? ` · 已折叠 ${deltas} 条文字分片事件` : ""}${snapshot.diagnostics?.dropped ? ` · 已淘汰 ${snapshot.diagnostics.dropped} 条较早日志` : ""}`;
183
- const retained = new Set([...runtime.map(record => record.id), ...other.map(record => record.id)]);
184
- for (const key of cached.keys()) if (!retained.has(key.slice(key.indexOf(":") + 1))) { cached.delete(key); expanded.delete(key); }
185
- }
186
- for (const select of [modelConversation, logConversation]) {
187
- select.addEventListener("change", () => { conversation = select.value; render(); });
188
- select.addEventListener("blur", render);
189
- }
190
- for (const input of [kind, level, modelSearch, logSearch, responseKind].filter(Boolean)) input.addEventListener(input.type === "search" ? "input" : "change", render);
191
- return { update(next) { snapshot = next; render(); }, copyLogs() { return copy(visibleLogs.map(record => record.raw).join("\n\n")); } };
192
- };
193
- })();
@@ -1,268 +0,0 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="color-scheme" content="light dark">
7
- <meta name="theme-color" content="#f4f8fc" media="(prefers-color-scheme: light)">
8
- <meta name="theme-color" content="#101922" media="(prefers-color-scheme: dark)">
9
- <title>搭搭本地调试</title>
10
- <link rel="icon" type="image/png" href="/assets/developer-platform-icon.png">
11
- <link rel="stylesheet" href="/assets/brand.css">
12
- <link rel="stylesheet" href="/assets/tokens.css">
13
- <link rel="stylesheet" href="/assets/app.css">
14
- <script src="/assets/iphone-16-max.js" defer></script>
15
- <script src="/assets/widgets.js" defer></script>
16
- <script src="/assets/products.js" defer></script>
17
- <script src="/assets/diagnostics.js" defer></script>
18
- <script src="/assets/tool-inventory.js" defer></script>
19
- <script src="/assets/chat-markdown.js" defer></script>
20
- <script src="/assets/app.js" defer></script>
21
- <link rel="stylesheet" href="/assets/widgets.css">
22
- <link rel="stylesheet" href="/assets/products.css">
23
- </head>
24
- <body>
25
- <a class="skip-link" href="#debug-content">跳到调试内容</a>
26
-
27
- <div class="app-shell">
28
- <aside class="sidebar" aria-label="预览调试导航">
29
- <div class="brand" aria-label="搭搭本地调试">
30
- <img class="brand-mark" src="/assets/developer-platform-icon.png" alt="">
31
- <span class="brand-name">搭搭本地调试</span>
32
- </div>
33
-
34
- <div class="sidebar-section">
35
- <p class="sidebar-label">开发调试</p>
36
- <nav class="debug-tabs" role="tablist" aria-label="本地回合调试视图">
37
- <button id="tab-overview" role="tab" aria-selected="true" aria-controls="panel-overview" tabindex="0">
38
- <svg aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg><span>运行概况</span>
39
- </button>
40
- <button id="tab-models" role="tab" aria-selected="false" aria-controls="panel-models" tabindex="-1">
41
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 5h16v14H4ZM8 9h8M8 13h5"/></svg><span>模型调用</span>
42
- </button>
43
- <button id="tab-protocol" role="tab" aria-selected="false" aria-controls="panel-protocol" tabindex="-1">
44
- <svg aria-hidden="true" viewBox="0 0 24 24"><rect x="3.5" y="5" width="17" height="14" rx="2"/><path d="m7.5 9 3 3-3 3M13 15h3.5"/></svg><span>运行日志</span>
45
- </button>
46
- <button id="tab-tools" role="tab" aria-selected="false" aria-controls="panel-tools" tabindex="-1">
47
- <!-- Lucide wrench — ISC License, Copyright (c) 2026 Lucide Icons and Contributors.
48
- Permission to use, copy, modify, and/or distribute this software for any
49
- purpose with or without fee is hereby granted, provided that the above
50
- copyright notice and this permission notice appear in all copies.
51
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
52
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
53
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
54
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
55
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
56
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
57
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -->
58
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/></svg><span>工具列表</span>
59
- </button>
60
- </nav>
61
- </div>
62
-
63
- </aside>
64
-
65
- <div class="application">
66
- <header class="topbar">
67
- <div class="project-heading">
68
- <h1 id="project-name">正在读取项目…</h1>
69
- </div>
70
-
71
- <div class="topbar-actions">
72
- <div class="connection-state" id="connection-state" data-phase="idle" aria-live="polite">
73
- <span class="connection-dot" aria-hidden="true"></span>
74
- <span id="connection-label">未启动</span>
75
- </div>
76
- <button class="button button-primary" id="dev-control" type="button">启动 DEV</button>
77
- </div>
78
- </header>
79
-
80
- <aside class="pairing-panel" id="pairing-panel" aria-labelledby="pairing-title" hidden>
81
- <div>
82
- <p class="eyebrow">Device preview</p>
83
- <h2 id="pairing-title">在搭搭 App 中继续</h2>
84
- </div>
85
- <button class="icon-button" id="close-pairing" type="button" aria-label="关闭真机预览面板">
86
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="m6 6 12 12M18 6 6 18"/></svg>
87
- </button>
88
- <div class="pairing-body" id="pairing-body">
89
- <div class="pairing-placeholder" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
90
- <div>
91
- <p class="pairing-status">正在读取真机入口…</p>
92
- <p class="pairing-hint">真机将打开当前搭子的 DEV 详情页。</p>
93
- </div>
94
- </div>
95
- </aside>
96
-
97
- <div class="error-banner" id="error-banner" role="alert" hidden>
98
- <span id="error-message"></span>
99
- <button type="button" id="dismiss-error">知道了</button>
100
- </div>
101
-
102
- <main class="workspace" id="debug-content">
103
- <section class="debugger" aria-label="本地回合调试器">
104
- <div class="debug-panels">
105
- <section id="panel-models" class="debug-panel" role="tabpanel" aria-labelledby="tab-models" hidden>
106
- <div class="section-heading"><div><h3>模型调用</h3></div></div>
107
- <div class="diagnostic-filters">
108
- <label>会话<select id="models-conversation" aria-label="模型会话"><option value="">全部会话</option></select></label>
109
- <label>类型<select id="models-kind"><option value="">请求与响应</option><option value="request">模型请求</option><option value="response">模型响应</option></select></label>
110
- <label id="models-response-filter">响应类型<select id="models-response-kind"><option value="">全部响应</option><option value="tool">工具调用</option><option value="text">文本响应</option></select></label>
111
- <label class="diagnostic-search">搜索<input id="models-search" type="search" placeholder="搜索模型、工具或内容" autocomplete="off"></label>
112
- </div>
113
- <p class="diagnostic-note" id="models-count"></p>
114
- <div id="model-records" class="diagnostic-records"></div>
115
- </section>
116
- <section id="panel-tools" class="debug-panel" role="tabpanel" aria-labelledby="tab-tools" hidden>
117
- <div class="section-heading"><div><h3>工具列表 <span id="tools-total"></span></h3><p id="tools-caption">连接本地工程后,查看已识别的工具及用途。</p></div><span id="tools-read-state" class="inventory-read-state"></span></div>
118
- <p id="tools-notice" class="inventory-notice" role="status" hidden></p>
119
- <div id="tools-filters" class="diagnostic-filters" hidden>
120
- <label class="diagnostic-search">搜索<input id="tools-search" type="search" placeholder="搜索工具名称或用途" autocomplete="off"></label>
121
- <label>来源<select id="tools-origin"><option value="">全部来源</option><option value="custom">自定义工具</option><option value="platform">平台工具</option></select></label>
122
- </div>
123
- <div id="tool-inventory"></div>
124
- <p class="diagnostic-note" id="tools-footer" hidden>来自本地工程分析。每次请求实际提供的工具可在「模型调用」中查看。</p>
125
- </section>
126
- <section id="panel-overview" class="debug-panel" role="tabpanel" aria-labelledby="tab-overview">
127
- <div class="run-strip">
128
- <div class="run-heading">
129
- <div class="run-identity">
130
- <p class="eyebrow">当前回合</p>
131
- <div class="run-title-line">
132
- <h2 id="run-title">尚无本地回合</h2>
133
- <span class="status-pill" id="run-status" data-status="idle">等待消息</span>
134
- </div>
135
- <details class="run-reference" id="run-reference" hidden><summary>本地发送回执 ID</summary>
136
- <span id="run-id"></span>
137
- <button type="button" class="text-button" id="copy-run-id" disabled>复制 ID</button>
138
- </details>
139
- </div>
140
- <div class="run-controls">
141
- <label class="run-picker-label" for="run-picker">切换回合</label>
142
- <div class="run-navigation">
143
- <button type="button" class="run-step" id="previous-run" aria-label="上一轮" title="上一轮" disabled>←</button>
144
- <select id="run-picker" class="run-picker" disabled>
145
- <option value="">尚无运行记录</option>
146
- </select>
147
- <button type="button" class="run-step" id="next-run" aria-label="下一轮" title="下一轮" disabled>→</button>
148
- </div>
149
- <label class="run-follow"><input type="checkbox" id="follow-latest-run" checked>跟随最新回合</label>
150
- </div>
151
- </div>
152
-
153
- <dl class="run-metrics" aria-label="本地回合指标">
154
- <div><dt>消息被接收</dt><dd><span id="metric-accept">未记录</span><p class="metric-help">Preview 发起发送至平台确认接收。</p></dd></div>
155
- <div><dt>首字耗时</dt><dd><span id="metric-first">未记录</span><p class="metric-help">Preview 发起发送至收到首段文字。</p></dd></div>
156
- <div><dt>回复结束</dt><dd><span id="metric-response">未记录</span><p class="metric-help">Preview 发起发送至观察到回复结束。</p></dd></div>
157
- <div><dt>会话消息</dt><dd><span id="metric-messages">0</span><p class="metric-help">当前会话的消息总数,包含历史消息。</p></dd></div>
158
- </dl>
159
-
160
- </div>
161
- <div class="overview-grid">
162
- <article class="run-summary">
163
- <div class="section-heading">
164
- <div><h3>回复观察</h3></div>
165
- </div>
166
- <div class="empty-state" id="overview-empty">
167
- <h4>发一条消息,开始记录</h4>
168
- <p>右侧发送消息后,这里显示消息发送与回复的观察结果。</p>
169
- </div>
170
- <div id="overview-content" hidden>
171
- <ol class="run-progress" id="run-progress" aria-label="回合执行进度"></ol>
172
- <div class="waterfall" id="waterfall" aria-label="消息时间节点,均从发起发送计时"></div>
173
- <p class="run-summary-copy" id="run-summary-copy"></p>
174
- </div>
175
- </article>
176
-
177
- <article class="environment-note">
178
- <p class="eyebrow">运行环境</p>
179
- <h3>DEV · 私聊</h3>
180
- <dl>
181
- <div><dt>Agent</dt><dd id="environment-agent">等待启动</dd></div>
182
- <div><dt>Runtime</dt><dd id="environment-runtime">未记录</dd></div>
183
- <div><dt>模型</dt><dd id="environment-model">未记录</dd></div>
184
- </dl>
185
- </article>
186
- </div>
187
- <details class="diagnostic-history"><summary>消息与连接事件</summary><ol class="timeline" id="timeline-list"></ol></details>
188
- <details class="diagnostic-history"><summary>平台对话记录 <span id="context-source">平台聊天记录</span></summary><div class="context-list" id="context-list"></div></details>
189
- </section>
190
-
191
- <section id="panel-protocol" class="debug-panel" role="tabpanel" aria-labelledby="tab-protocol" hidden>
192
- <div class="section-heading">
193
- <div><h3>运行日志</h3></div>
194
- <button class="text-button" id="copy-protocol" type="button">复制可见日志</button>
195
- </div>
196
- <div class="diagnostic-filters">
197
- <label>会话<select id="logs-conversation" aria-label="日志会话"><option value="">全部会话</option></select></label>
198
- <label>级别<select id="logs-level"><option value="">全部级别</option><option value="error">ERROR</option><option value="warn">WARN</option><option value="info">INFO</option></select></label>
199
- <label class="diagnostic-search">搜索<input id="logs-search" type="search" placeholder="搜索日志内容" autocomplete="off"></label>
200
- </div>
201
- <p class="diagnostic-note" id="logs-count"></p>
202
- <div class="protocol-log" id="protocol-log" role="log" aria-live="polite"></div>
203
- </section>
204
- </div>
205
- </section>
206
-
207
- <section class="simulator-area" aria-label="搭搭 App 模拟器">
208
- <div class="simulator-caption">
209
- <h2>模拟私聊场景</h2>
210
- <div class="simulator-actions">
211
- <button class="simulator-device" id="reset-conversation" type="button" disabled>清空对话</button>
212
- <button class="simulator-device" id="pair-device" type="button" aria-expanded="false" aria-controls="pairing-panel">
213
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M8 2.75h8A2.25 2.25 0 0 1 18.25 5v14A2.25 2.25 0 0 1 16 21.25H8A2.25 2.25 0 0 1 5.75 19V5A2.25 2.25 0 0 1 8 2.75Z"/><path d="M10 18.25h4"/></svg>
214
- <span>真机预览</span>
215
- </button>
216
- </div>
217
- </div>
218
-
219
- <div class="device-stage">
220
- <div class="phone" aria-label="iPhone 模拟器">
221
- <iphone-16-max mode="light">
222
- <div class="phone-screen">
223
- <header class="chat-navigation">
224
- <button class="back-chevron" type="button" aria-label="返回(预览中不可用)" disabled>
225
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></svg>
226
- </button>
227
- <img id="buddy-profile-avatar" alt="搭子头像" hidden><div class="buddy-identity"><h3 id="buddy-name">我的搭子</h3><p id="buddy-status" class="sr-only">DEV 未连接</p></div>
228
- <div class="chat-navigation-actions" aria-hidden="true">
229
- <button class="chat-more" type="button" aria-label="更多(预览中不可用)" disabled tabindex="-1">
230
- <svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="19" cy="12" r="1.3"/></svg>
231
- </button>
232
- </div>
233
- </header>
234
-
235
- <div id="widget-shortcut" class="widget-shortcut" hidden></div>
236
- <div id="widget-overlay" class="widget-overlay" hidden></div>
237
- <div class="chat-messages" id="chat-messages" aria-live="polite" aria-label="模拟器聊天记录">
238
- <div class="chat-empty" id="chat-empty" aria-label="暂无消息"></div>
239
- </div>
240
-
241
- <button id="latest-messages" class="latest-messages" type="button" hidden>回到最新消息 ↓</button>
242
- <form class="composer" id="composer">
243
- <button class="composer-tool composer-add" type="button" aria-label="添加内容(预览中不可用)" disabled tabindex="-1">
244
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
245
- </button>
246
- <label class="sr-only" for="message-input">发送消息</label>
247
- <textarea id="message-input" rows="1" maxlength="32768" placeholder="发消息或长按发送语音" disabled></textarea>
248
- <button class="composer-tool composer-voice" type="button" aria-label="发送语音(预览中不可用)" disabled tabindex="-1">
249
- <svg aria-hidden="true" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M6 11a6 6 0 0 0 12 0M12 17v4M9 21h6"/></svg>
250
- </button>
251
- <button class="send-button" id="send-message" type="submit" aria-label="发送消息" disabled>
252
- <svg aria-hidden="true" viewBox="0 0 24 24"><path d="m6 12 6-6 6 6M12 6v12"/></svg>
253
- </button>
254
- </form>
255
- <div class="home-indicator" aria-hidden="true"></div>
256
- </div>
257
- </iphone-16-max>
258
- </div>
259
- </div>
260
- <p class="keyboard-hint"><kbd>Enter</kbd> 发送 · 回复期间请等待</p>
261
- </section>
262
- </main>
263
- </div>
264
- </div>
265
-
266
- <div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
267
- </body>
268
- </html>
@@ -1,19 +0,0 @@
1
- /* 随手机画布缩放;详情在手机内独立滚动,购买按钮保持可见。 */
2
- .product-card { display:flex; flex-direction:column; gap:calc(var(--widget-unit, 1px)*10); width:100%; min-width:0; padding:calc(var(--widget-unit, 1px)*18); border:1px solid #ddc9af; border-radius:calc(var(--widget-unit, 1px)*20); background:linear-gradient(145deg,#fffaf4,#eed8bf); color:#513516; text-align:left; white-space:normal; overflow-wrap:anywhere; font:inherit; }
3
- .product-card strong { font-size:1.1em; }
4
- .product-card-label { font-size:.75em; opacity:.75; }
5
- .product-card-action { border-top:1px solid #b9976d55; padding-top:.6em; font-weight:600; }
6
- .product-overlay[hidden] { display:none; }
7
- .product-overlay { position:absolute; inset:0; z-index:30; display:flex; align-items:flex-end; background:#0005; font-size:calc(var(--widget-unit, 1px)*16); color:#513516; }
8
- .product-sheet { width:100%; min-width:0; max-height:87%; display:flex; flex-direction:column; overflow:hidden; border-radius:calc(var(--widget-unit, 1px)*28) calc(var(--widget-unit, 1px)*28) 0 0; background:#fff8ef; box-sizing:border-box; }
9
- .product-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:.6em; padding:1em; border-bottom:1px solid #e7d9c7; }
10
- .product-sheet-header h3 { font-size:1.1em; margin:0; min-width:0; overflow-wrap:anywhere; }
11
- .product-sheet button { font:inherit; cursor:pointer; border:1px solid #d9c4a9; background:#fff; color:#513516; border-radius:.8em; padding:.7em; }
12
- .product-sheet button:disabled { opacity:.55; cursor:wait; }
13
- .product-sheet-content { overflow-y:auto; overscroll-behavior:contain; padding:0 1em 1em; min-height:0; overflow-wrap:anywhere; }
14
- .product-sheet h4 { font-size:1em; margin:1em 0 .4em; }
15
- .product-sheet p { white-space:pre-wrap; line-height:1.55; margin:.5em 0; }
16
- .product-sheet .product-notice { font-size:.85em; padding:.75em; background:#f1e4d2; border-radius:.8em; }
17
- .product-sheet-footer { flex:none; padding:.8em 1em 1.8em; border-top:1px solid #e7d9c7; background:#fff8ef; }
18
- .product-sheet .product-primary { width:100%; background:#63451f; color:white; font-weight:600; }
19
- .product-success { color:#28603c; }
@@ -1,104 +0,0 @@
1
- /* 商品卡预览:只使用当前 DEV 会话的真实卡片和零扣费模拟购买接口。 */
2
- (() => {
3
- const el = (tag, className, text) => { const item = document.createElement(tag); item.className = className; if (text !== undefined) item.textContent = text; return item; };
4
- const button = (text, action, className = '') => { const item = el('button', className, text); item.type = 'button'; item.addEventListener('click', action); return item; };
5
- window.createSimulatorProducts = ({ api, refresh }) => {
6
- const phone = document.querySelector('.phone-screen');
7
- const overlay = el('section', 'product-overlay'); overlay.hidden = true; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', '商品方案与模拟购买'); phone.append(overlay);
8
- let active = false, scope, revision, generation = 0, selected, busy = false, previousFocus;
9
- const purchaseKeys = new Map();
10
- function close(force = false) {
11
- if (busy && !force) return;
12
- generation++; selected = undefined; busy = false; overlay.hidden = true; overlay.replaceChildren();
13
- for (const child of phone.children) if (child !== overlay) child.inert = false;
14
- previousFocus?.focus(); previousFocus = undefined;
15
- }
16
- function shell(title) {
17
- overlay.replaceChildren();
18
- const sheet = el('div', 'product-sheet'), header = el('header', 'product-sheet-header');
19
- const dismiss = button('关闭', () => close()); dismiss.disabled = busy;
20
- header.append(el('h3', '', title), dismiss); sheet.append(header); overlay.append(sheet); return sheet;
21
- }
22
- function section(container, title, text) { if (!text) return; container.append(el('h4', '', title), el('p', '', text)); }
23
- function credentialCopy(credential) { return ({ pending: '购买已确认,等待搭子开通', active: '服务使用中', ended: '服务已结束', revoked: '模拟服务已解除' })[credential?.state] || '购买已确认'; }
24
- async function open(card) {
25
- if (!active) return;
26
- selected = card; previousFocus ||= document.activeElement; overlay.hidden = false;
27
- for (const child of phone.children) if (child !== overlay) child.inert = true;
28
- await load();
29
- }
30
- async function load(note = '') {
31
- const card = selected, mine = ++generation;
32
- if (!card) return;
33
- busy = false; shell('读取商品方案…').append(el('p', 'product-notice', '正在读取平台商品和购买状态'));
34
- try {
35
- const detail = await api(`/api/simulator/products/${encodeURIComponent(card.offerId)}`);
36
- if (mine !== generation || !active) return;
37
- const product = detail.product;
38
- if (!product || !Number.isSafeInteger(detail.version) || detail.paymentChannel !== 'mock' || detail.chargedAmountMinor !== 0) throw new Error('当前方案不支持零扣费预览');
39
- const sheet = shell(product.name), content = el('div', 'product-sheet-content');
40
- content.append(el('p', 'product-notice', '开发预览 · 零扣费模拟购买 · 不影响正式搭子的购买权益'));
41
- if (note) content.append(el('p', 'product-notice', note));
42
- section(content, '服务介绍', product.description);
43
- section(content, '本次关注', detail.personalization?.needSummary || detail.personalization?.cardSummary);
44
- const benefits = el('div', 'product-benefits');
45
- for (const benefit of product.benefits || []) section(benefits, benefit.title, benefit.description);
46
- content.append(benefits);
47
- const price = product.requestedPrice;
48
- section(content, '方案价格', `${price.currency} ${(price.amountMinor / 100).toFixed(2)} / ${product.pricingUnitLabel}(本次实际扣费 0 元)`);
49
- section(content, '开始条件', product.productTerms?.startCondition);
50
- section(content, '结束条件', product.productTerms?.endCondition);
51
- section(content, '服务边界', (product.limitations || []).join('\n'));
52
- const footer = el('footer', 'product-sheet-footer');
53
- if (detail.existingPurchase) {
54
- footer.append(el('p', 'product-success', credentialCopy(detail.existingPurchase.credential)), button('返回对话', () => close(), 'product-primary'));
55
- } else if (!detail.canPurchase) {
56
- footer.append(el('p', 'product-notice', detail.unavailableReason || '暂不可购买'), button('重新加载', () => load()));
57
- } else {
58
- const confirm = button('确认模拟购买 · 0 元', async () => {
59
- if (busy || mine !== generation || !active) return;
60
- busy = true; confirm.disabled = true; confirm.textContent = '正在确认…';
61
- const key = `${card.offerId}:${detail.version}`;
62
- if (!purchaseKeys.has(key)) purchaseKeys.set(key, `preview-${crypto.randomUUID()}`);
63
- try {
64
- const result = await api(`/api/simulator/products/${encodeURIComponent(card.offerId)}/mock-purchase`, { method: 'POST', body: JSON.stringify({ expectedVersion: detail.version, idempotencyKey: purchaseKeys.get(key), conversationRevision: revision }) });
65
- if (mine !== generation) return;
66
- if (result.status !== 'simulated_success' || result.chargedAmountMinor !== 0) throw new Error('尚未确认购买结果,请重新读取状态');
67
- busy = false; await load('模拟购买成功,实际扣费 0 元。回到对话后继续体验服务。'); await refresh();
68
- } catch (error) {
69
- if (mine !== generation) return;
70
- busy = false; await load(error.code === 'PRODUCT_VERSION_CHANGED' ? '商品已更新,请重新查看方案并确认。' : `上次结果尚未确认:${error.message}。已重新读取状态,重试会复用同一购买请求。`);
71
- }
72
- }, 'product-primary');
73
- footer.append(confirm);
74
- }
75
- sheet.append(content, footer); sheet.querySelector('button')?.focus();
76
- } catch (error) {
77
- if (mine !== generation) return;
78
- const sheet = shell('商品暂时无法读取'); sheet.append(el('p', 'product-notice', error.message), button('重试', () => load()));
79
- }
80
- }
81
- overlay.addEventListener('keydown', event => {
82
- if (event.key === 'Escape') { event.preventDefault(); close(); }
83
- if (event.key === 'Tab') {
84
- const buttons = [...overlay.querySelectorAll('button:not(:disabled)')];
85
- const first = buttons[0], last = buttons.at(-1);
86
- if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
87
- else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
88
- }
89
- });
90
- return {
91
- card(product) {
92
- const card = button('', () => open(product), 'product-card'); card.disabled = !active;
93
- card.setAttribute('aria-label', `${product.title},${product.buttonText}`);
94
- card.append(el('span', 'product-card-label', '服务方案'), el('strong', '', product.title), el('span', 'product-card-summary', product.summary), el('span', 'product-card-action', `${product.buttonText} ›`)); return card;
95
- },
96
- update(next) {
97
- const nextScope = `${next.conversationRevision}:${next.devRevision}`;
98
- if (scope !== undefined && scope !== nextScope || !next.active) close(true);
99
- active = next.active; scope = nextScope; revision = next.conversationRevision;
100
- },
101
- reset() { active = false; scope = undefined; close(true); purchaseKeys.clear(); },
102
- };
103
- };
104
- })();
@@ -1,15 +0,0 @@
1
- /* Preview-only tokens. Shared brand tokens are loaded first from brand.css. */
2
- :root {
3
- --color-error: var(--color-danger);
4
- --color-error-soft: var(--color-danger-soft);
5
- /* The simulated phone preserves the App appearance in either desktop theme. */
6
- --color-phone-paper: #ffffff;
7
- --color-phone-surface: #f3f4f6;
8
- --color-phone-ink: #20242a;
9
- --color-phone-muted: #66727d;
10
- --color-phone-accent: #24c9f1;
11
- --rule-hair: 1px;
12
- --page-gutter: clamp(1.25rem, 3vw, 3rem);
13
- --ease-in-out: cubic-bezier(.65, 0, .35, 1);
14
- --shadow-phone: 0 1.5rem 3rem var(--color-shadow);
15
- }