@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,94 +0,0 @@
1
- (() => {
2
- "use strict";
3
- // UI contract only. The CLI supplies inspection; this view never executes project code.
4
- window.createPreviewToolInventory = ({ node }) => {
5
- const $ = id => document.getElementById(id);
6
- const list = $("tool-inventory"), search = $("tools-search"), origin = $("tools-origin");
7
- const expanded = new Set();
8
- let snapshot = {}, signature = "", known = [];
9
- const record = value => value && typeof value === "object" && !Array.isArray(value) ? value : {};
10
- const text = value => typeof value === "string" ? value : "";
11
- const sourceLabel = value => ({ custom: "自定义工具", platform: "平台工具" })[value] || "来源未确定";
12
- function renderList() {
13
- const query = search.value.trim().toLocaleLowerCase();
14
- const filtered = known.filter(tool => (!origin.value || tool.origin === origin.value)
15
- && (!query || `${tool.name} ${tool.description || ""}`.toLocaleLowerCase().includes(query)));
16
- const fragment = document.createDocumentFragment();
17
- for (const tool of filtered) {
18
- const row = node("details", { class: "inventory-tool", open: expanded.has(tool.name) });
19
- row.addEventListener("toggle", () => row.open ? expanded.add(tool.name) : expanded.delete(tool.name));
20
- const heading = node("summary");
21
- const label = node("div", { class: "inventory-tool-title" });
22
- label.append(node("strong", {}, tool.name), node("span", { class: "inventory-origin" }, sourceLabel(tool.origin)));
23
- heading.append(label, node("p", {}, text(tool.description) || "工具描述尚未读取。"));
24
- const body = node("div", { class: "inventory-tool-body" });
25
- if (text(tool.reason)) body.append(node("p", { class: "diagnostic-note" }, tool.reason));
26
- const schema = record(tool.parameters), properties = record(schema.properties);
27
- const names = Object.keys(properties), required = Array.isArray(schema.required) ? schema.required : [];
28
- body.append(node("h4", {}, "参数"));
29
- if (names.length) {
30
- const table = node("table", { class: "inventory-parameters" });
31
- const head = node("thead"), header = node("tr");
32
- for (const title of ["名称", "类型", "说明"]) header.append(node("th", { scope: "col" }, title));
33
- head.append(header); table.append(head);
34
- const rows = node("tbody");
35
- for (const name of names) {
36
- const param = record(properties[name]), tr = node("tr");
37
- tr.append(node("td", {}, `${name}${required.includes(name) ? " · 必填" : ""}`),
38
- node("td", {}, Array.isArray(param.type) ? param.type.join(" / ") : text(param.type) || "见 Schema"),
39
- node("td", {}, text(param.description) || "未提供说明"));
40
- rows.append(tr);
41
- }
42
- table.append(rows); body.append(table);
43
- } else body.append(node("p", { class: "diagnostic-note" }, tool.parameters ? "无固定参数字段,详见 Schema。" : "参数定义尚未读取。"));
44
- if (tool.parameters) {
45
- const schemaView = node("details", { class: "diagnostic-section" });
46
- schemaView.append(node("summary", {}, "参数 Schema"), node("pre", {}, JSON.stringify(tool.parameters, null, 2)));
47
- body.append(schemaView);
48
- }
49
- row.append(heading, body); fragment.append(row);
50
- }
51
- if (!filtered.length) {
52
- const empty = node("div", { class: "diagnostic-empty" });
53
- const status = snapshot.inspection?.status;
54
- empty.append(node("strong", {}, known.length ? "没有匹配的工具" : status === "analyzing" ? "正在分析工具注册关系" : status === "complete" ? "未配置工具" : "工具列表尚未读取"));
55
- empty.append(node("p", {}, known.length ? "请调整工具名称、用途关键词或来源。" : status === "complete" ? "当前工程中未识别到已注册的工具。" : status === "analyzing" ? "读取完成后自动展示,无需发送消息。" : snapshot.connected ? "当前连接尚未提供工程分析结果。未读取不表示没有工具。" : "连接本地工程后,工具及用途会显示在这里。"));
56
- fragment.append(empty);
57
- }
58
- const unresolved = snapshot.inspection?.unresolved;
59
- if (Array.isArray(unresolved) && !query && !origin.value) {
60
- for (const item of unresolved) {
61
- const pending = node("div", { class: "inventory-unresolved" });
62
- pending.append(node("strong", {}, text(item.name) || "动态工具 · 待确认"), node("p", {}, text(item.reason) || "依赖运行时信息,无法静态确定。"));
63
- fragment.append(pending);
64
- }
65
- }
66
- list.replaceChildren(fragment);
67
- }
68
- function update(next) {
69
- const nextSignature = JSON.stringify(next);
70
- if (nextSignature === signature) return;
71
- if (snapshot.buddyId !== next.buddyId) { expanded.clear(); search.value = ""; origin.value = ""; }
72
- signature = nextSignature; snapshot = next;
73
- const inspection = record(next.inspection), model = record(inspection.model);
74
- known = (Array.isArray(inspection.tools) ? inspection.tools : []).filter(tool => tool && typeof tool.name === "string");
75
- const loading = inspection.status === "analyzing", partial = inspection.status === "partial";
76
- const incomplete = partial && (!Array.isArray(inspection.unresolved) || inspection.unresolved.length > 0);
77
- const modelLabel = model.status === "dynamic" ? "动态配置,待运行确认" : text(model.value) || text(next.fallbackModel) || (loading ? "分析中" : "未读取");
78
- $("environment-model").replaceChildren(node("span", {}, modelLabel));
79
- if (text(model.reason)) $("environment-model").append(node("p", { class: "inventory-model-note" }, model.reason));
80
- $("tools-total").textContent = known.length ? `${known.length}${incomplete ? " 已识别" : ""}` : "";
81
- $("tools-caption").textContent = known.length ? `${known.filter(tool => tool.origin === "custom").length} 个自定义工具 · ${known.filter(tool => tool.origin === "platform").length} 个平台工具` : "本地工程中已注册的工具及用途。";
82
- $("tools-read-state").textContent = loading ? "分析中" : !next.connected && next.inspection ? "上次读取" : incomplete ? "部分已识别" : partial ? "部分详情待确认" : inspection.status === "complete" ? "已读取" : "未读取";
83
- const notes = [];
84
- if (!next.connected && next.inspection) notes.push("连接已断开,以下为上次读取的结果。");
85
- if (incomplete) notes.push("部分工具依赖运行时信息,当前列表可能不完整。每次请求实际提供的工具可在「模型调用」中查看。");
86
- else if (partial) notes.push("部分工具的描述或参数尚未完整读取,展开对应工具可查看说明。");
87
- $("tools-notice").hidden = !notes.length; $("tools-notice").textContent = notes.join(" ");
88
- $("tools-filters").hidden = !known.length; $("tools-footer").hidden = !next.inspection;
89
- renderList();
90
- }
91
- search.addEventListener("input", renderList); origin.addEventListener("change", renderList);
92
- return { update };
93
- };
94
- })();
@@ -1,62 +0,0 @@
1
- /* iOS ff1845b: dimensions in native points, scaled once with the device canvas. */
2
- .widget-shortcut { position:absolute; z-index:3; top:calc(var(--widget-unit, 1px) * 100); left:calc(var(--widget-unit, 1px) * 12); max-width:calc(100% - calc(var(--widget-unit, 1px) * 24)); }
3
- .widget-shortcut[hidden], .widget-overlay[hidden], .widget-more[hidden] { display:none; }
4
- .widget-pill { display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 8); height:calc(var(--widget-unit, 1px) * 52); max-width:calc(var(--widget-unit, 1px) * 260); border-radius:99px; padding:0 calc(var(--widget-unit, 1px) * 20) 0 calc(var(--widget-unit, 1px) * 16); color:#000; background:linear-gradient(110deg,#fff,#bbf1a7); border:calc(var(--widget-unit, 1px) * .5) solid #0000001f; font-size:calc(var(--widget-unit, 1px) * 16); font-weight:400; box-shadow:none; transition:padding .22s ease-in-out; }
5
- .widget-pill .widget-pill-avatar { width:calc(var(--widget-unit, 1px) * 28); height:calc(var(--widget-unit, 1px) * 28); flex:none; border-radius:50%; }
6
- .widget-pill-title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
7
- .widget-chevron { margin-left:calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 18); }
8
- .widget-shortcut[data-phase="collapsed"] .widget-pill { width:calc(var(--widget-unit, 1px) * 52); padding:0 calc(var(--widget-unit, 1px) * 12); }
9
- .widget-shortcut[data-phase="collapsed"] :is(.widget-pill-title,.widget-chevron) { display:none; }
10
- .widget-choices,.widget-options { position:absolute; z-index:5; background:#fff; border:calc(var(--widget-unit, 1px) * .3) solid #0000001f; border-radius:calc(var(--widget-unit, 1px) * 16); box-shadow:0 calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 16) #0000000d; overflow:auto; }
11
- .widget-choices { top:calc(100% + calc(var(--widget-unit, 1px) * 8)); left:0; width:calc(var(--widget-unit, 1px) * 240); max-height:calc(var(--widget-unit, 1px) * 320); padding:calc(var(--widget-unit, 1px) * 4) 0; }
12
- .widget-choices button { display:flex; align-items:center; justify-content:space-between; gap:calc(var(--widget-unit, 1px) * 8); width:100%; min-height:calc(var(--widget-unit, 1px) * 48); padding:calc(var(--widget-unit, 1px) * 12) calc(var(--widget-unit, 1px) * 16); text-align:left; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 16); font-weight:400; }
13
- .widget-choices button>span:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
14
- .widget-arrow { color:#0009; }
15
- .widget-choices button:hover,.widget-options button:hover { background:#f1f3f4; }
16
- .widget-overlay { position:absolute; z-index:10; inset:0; display:flex; align-items:center; justify-content:center; background:#0003; opacity:1; animation:widget-fade .22s ease-in-out; transition:opacity .22s ease-in-out; }
17
- .widget-overlay[data-state="closing"] { opacity:0; pointer-events:none; }
18
- .widget-modal { position:relative; width:calc(100% - calc(var(--widget-unit, 1px) * 32)); height:65%; border-radius:calc(var(--widget-unit, 1px) * 24); background:#fff; color:#000; overflow:hidden; display:flex; flex-direction:column; padding-bottom:calc(var(--widget-unit, 1px) * 12); }
19
- .widget-modal-header { display:flex; gap:0; align-items:center; min-height:calc(var(--widget-unit, 1px) * 52); padding:calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 4) 0; flex:none; }
20
- .widget-modal-header button,.widget-more-slot { width:calc(var(--widget-unit, 1px) * 44); height:calc(var(--widget-unit, 1px) * 44); flex:none; padding:0; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 22); font-weight:400; }
21
- .widget-modal-header h3 { flex:1; min-width:0; margin:0; text-align:center; font-size:calc(var(--widget-unit, 1px) * 14); font-weight:500; line-height:1.4; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
22
- .widget-modal-body { flex:1; position:relative; min-height:0; overflow:hidden; font-size:calc(var(--widget-unit, 1px) * 14); }
23
- .widget-document { position:absolute; top:0; left:0; border:0; display:block; background:#fff; }
24
- .widget-loading,.widget-failure { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:calc(var(--widget-unit, 1px) * 12); padding:calc(var(--widget-unit, 1px) * 24); text-align:center; color:#0009; }
25
- .widget-loading p,.widget-failure p { margin:0; font-size:calc(var(--widget-unit, 1px) * 14); line-height:1.5; }
26
- .widget-failure h4 { margin:0; font-size:calc(var(--widget-unit, 1px) * 17); font-weight:600; color:#000; }
27
- .widget-spinner { width:calc(var(--widget-unit, 1px) * 20); height:calc(var(--widget-unit, 1px) * 20); border:calc(var(--widget-unit, 1px) * 2) solid #0002; border-top-color:#0009; border-radius:50%; animation:widget-spin .85s linear infinite; }
28
- .widget-failure-mark { display:grid; place-items:center; width:calc(var(--widget-unit, 1px) * 32); height:calc(var(--widget-unit, 1px) * 32); border:calc(var(--widget-unit, 1px) * 1.5) solid #0006; border-radius:50%; font-size:calc(var(--widget-unit, 1px) * 22); }
29
- .widget-failure button { margin-top:calc(var(--widget-unit, 1px) * 4); padding:calc(var(--widget-unit, 1px) * 10) calc(var(--widget-unit, 1px) * 16); border-radius:calc(var(--widget-unit, 1px) * 8); background:#f1f3f4; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); }
30
- .widget-options { top:calc(var(--widget-unit, 1px) * 52); right:calc(var(--widget-unit, 1px) * 8); width:calc(var(--widget-unit, 1px) * 232); padding:calc(var(--widget-unit, 1px) * 8) 0; }
31
- /* Menu rhythm and monochrome icons match the native action menu. */
32
- .widget-options button { position:relative; display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 6); width:100%; min-height:calc(var(--widget-unit, 1px) * 36); padding:calc(var(--widget-unit, 1px) * 6) calc(var(--widget-unit, 1px) * 16); text-align:left; background:transparent; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); font-weight:400; }
33
- .widget-options button+button { margin-top:calc(var(--widget-unit, 1px) * 8); }
34
- .widget-options button+button::before { content:""; position:absolute; top:calc(var(--widget-unit, 1px) * -4); left:calc(var(--widget-unit, 1px) * 16); right:calc(var(--widget-unit, 1px) * 16); height:calc(var(--widget-unit, 1px) * .3); background:#00000026; }
35
- .widget-options svg { width:calc(var(--widget-unit, 1px) * 20); height:calc(var(--widget-unit, 1px) * 20); flex:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
36
- .widget-options .widget-destructive,.widget-confirm .widget-destructive { color:#d73340; }
37
- .widget-options button:disabled,.widget-confirm button:disabled { opacity:.45; cursor:wait; }
38
- .widget-confirm-backdrop { position:absolute; inset:0; z-index:7; display:grid; place-items:center; background:#0003; padding:calc(var(--widget-unit, 1px) * 20); }
39
- .widget-confirm { width:calc(var(--widget-unit, 1px) * 270); max-width:100%; padding:calc(var(--widget-unit, 1px) * 20) calc(var(--widget-unit, 1px) * 16) calc(var(--widget-unit, 1px) * 10); border-radius:calc(var(--widget-unit, 1px) * 16); background:#fff; text-align:center; }
40
- .widget-confirm h4 { margin:0 0 calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 17); font-weight:600; }
41
- .widget-confirm p { margin:0; font-size:calc(var(--widget-unit, 1px) * 13); line-height:1.5; }
42
- .widget-confirm-actions { display:flex; margin-top:calc(var(--widget-unit, 1px) * 16); border-top:calc(var(--widget-unit, 1px) * .5) solid #0002; }
43
- .widget-confirm-actions button { flex:1; padding:calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 16); background:transparent; color:#000; }
44
- .widget-native-share { position:absolute; z-index:6; inset:calc(var(--widget-unit, 1px) * 52) 0 0; padding:calc(var(--widget-unit, 1px) * 28) calc(var(--widget-unit, 1px) * 24); background:#fff; font-size:calc(var(--widget-unit, 1px) * 14); }
45
- .widget-native-share h4 { margin:0 0 calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 17); }
46
- .widget-native-share p { line-height:1.6; color:#0009; margin:0 0 calc(var(--widget-unit, 1px) * 24); }
47
- .widget-native-share button { display:block; width:100%; margin-top:calc(var(--widget-unit, 1px) * 12); padding:calc(var(--widget-unit, 1px) * 12); border-radius:calc(var(--widget-unit, 1px) * 8); color:#000; background:#f1f3f4; font-size:calc(var(--widget-unit, 1px) * 14); }
48
- .widget-pin-note { position:absolute; z-index:8; top:calc(17.5% + calc(var(--widget-unit, 1px) * 58)); left:50%; transform:translateX(-50%); max-width:90%; border-radius:calc(var(--widget-unit, 1px) * 16); background:#f1f3f4f5; box-shadow:0 calc(var(--widget-unit, 1px) * 4) calc(var(--widget-unit, 1px) * 16) #0001; padding:calc(var(--widget-unit, 1px) * 12) calc(var(--widget-unit, 1px) * 16); font-size:calc(var(--widget-unit, 1px) * 12); text-align:center; }
49
- .widget-entry { width:calc(var(--widget-unit, 1px) * 297); max-width:100%; padding:calc(var(--widget-unit, 1px) * 12); border:calc(var(--widget-unit, 1px) * .3) solid #00000026; border-radius:calc(var(--widget-unit, 1px) * 16); background:#fff; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); }
50
- .widget-entry h4 { margin:0 0 calc(var(--widget-unit, 1px) * 12); font:500 calc(var(--widget-unit, 1px) * 14)/1.4 -apple-system,BlinkMacSystemFont,sans-serif; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
51
- .widget-entry p { margin:0; color:#000; font-size:calc(var(--widget-unit, 1px) * 14); line-height:1.4; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden; }
52
- .widget-entry p.widget-secondary { color:#0009; }
53
- .widget-entry-details { display:flex; align-items:center; gap:calc(var(--widget-unit, 1px) * 12); }
54
- .widget-entry-photo { width:calc(var(--widget-unit, 1px) * 64); height:calc(var(--widget-unit, 1px) * 64); border-radius:50%; object-fit:cover; flex:none; }
55
- .widget-entry-open { width:100%; height:calc(var(--widget-unit, 1px) * 40); min-height:0; padding:0; margin-top:calc(var(--widget-unit, 1px) * 12); background:#f1f3f4; color:#000; border-radius:calc(var(--widget-unit, 1px) * 8); font-size:calc(var(--widget-unit, 1px) * 14); font-weight:500; }
56
- .widget-entry-metadata { display:flex; min-width:0; margin-top:calc(var(--widget-unit, 1px) * 12); font-size:calc(var(--widget-unit, 1px) * 11); line-height:1.4; color:#0009; }
57
- .widget-creator { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
58
- .widget-permission { flex:none; }
59
- .widget-entry button:focus-visible,.widget-shortcut button:focus-visible,.widget-overlay button:focus-visible { outline:2px solid #007a9b; outline-offset:2px; }
60
- @keyframes widget-fade { from { opacity:0; } to { opacity:1; } }
61
- @keyframes widget-spin { to { transform:rotate(360deg); } }
62
- @media(prefers-reduced-motion:reduce) { .widget-overlay { animation-duration:.1s; transition-duration:.1s; } .widget-pill { transition:none; } }
@@ -1,317 +0,0 @@
1
- /* SIT Widget 编辑宿主。页面保持不透明沙箱,身份和记录操作由 CLI 代理。 */
2
- (() => {
3
- const el = (tag, className = '', text) => {
4
- const node = document.createElement(tag); node.className = className;
5
- if (text !== undefined) node.textContent = text;
6
- return node;
7
- };
8
- const button = (label, action, className = '') => {
9
- const node = el('button', className, label); node.type = 'button'; node.addEventListener('click', action); return node;
10
- };
11
- const menuIcon = name => {
12
- const paths = {
13
- share: 'M12 15V3m-4 4 4-4 4 4M7 11H5v10h14V11h-2',
14
- pin: 'm9 3 6 0-1 6 4 4v2H6v-2l4-4ZM12 15v6',
15
- unpin: 'm3 3 18 18M9 3h6l-1 6 4 4v2M6 15h6m0 0v6',
16
- trash: 'M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14M10 11v6m4-6v6',
17
- };
18
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
19
- svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
20
- const path = document.createElementNS(svg.namespaceURI, 'path'); path.setAttribute('d', paths[name]);
21
- svg.append(path); return svg;
22
- };
23
- window.createSimulatorWidgets = ({ api, avatar }) => {
24
- const shortcut = document.querySelector('#widget-shortcut');
25
- const overlay = document.querySelector('#widget-overlay');
26
- const phone = document.querySelector('.phone-screen');
27
- const device = document.querySelector('.phone');
28
- let reopen, conversationScope;
29
- let items = [], scope = '', generation = 0, ready = false, loading = false, lastRefresh = 0;
30
- let failed = false, signature = '', selected, focusBefore, openRevision = 0, frameObserver;
31
- let collapseTimer, closeTimer, loadTimer, documentFrame, documentToken, documentReady;
32
- let phase = 'normal', choices, pill, busy = false, refreshPromise;
33
- let documentLifecycle = false, editing = false, conversationRevision = 0, widgetSession;
34
- const timers = () => { endSession(); clearTimeout(loadTimer); frameObserver?.disconnect(); frameObserver = undefined; documentFrame = undefined; documentReady = undefined; };
35
- const scale = () => device.clientWidth / 415;
36
- const resize = new ResizeObserver(() => { phone.style.setProperty('--widget-unit', `${scale()}px`); });
37
- resize.observe(device);
38
- function setPhase(value) { phase = value; shortcut.dataset.phase = value; pill?.setAttribute('aria-expanded', String(value === 'choosing')); }
39
- function scheduleCollapse() {
40
- clearTimeout(collapseTimer);
41
- if (!ready || !items.length || selected || document.hidden || phase !== 'normal') return;
42
- collapseTimer = setTimeout(() => setPhase('collapsed'), 5000);
43
- }
44
- function closeChoices(reset = true) {
45
- choices?.remove(); choices = undefined;
46
- if (reset) { setPhase('normal'); scheduleCollapse(); }
47
- }
48
- function finishClose() {
49
- overlay.hidden = true; overlay.replaceChildren(); delete overlay.dataset.state;
50
- for (const child of phone.children) if (child !== overlay) child.inert = false;
51
- if (focusBefore?.isConnected) focusBefore.focus(); focusBefore = undefined; setPhase('normal'); scheduleCollapse();
52
- }
53
- function close(immediate = false) {
54
- if (busy && !immediate) return;
55
- clearTimeout(closeTimer); timers(); openRevision++; selected = undefined;
56
- if (immediate || overlay.hidden) { finishClose(); return; }
57
- overlay.dataset.state = 'closing';
58
- closeTimer = setTimeout(finishClose, window.matchMedia('(prefers-reduced-motion:reduce)').matches ? 100 : 220);
59
- }
60
- function render() {
61
- const next = JSON.stringify([ready, items, failed]);
62
- if (next === signature) return;
63
- signature = next;
64
- const choosing = phase === 'choosing';
65
- closeChoices(false); shortcut.replaceChildren(); shortcut.hidden = !ready || (!items.length && !failed);
66
- if (shortcut.hidden) return;
67
- if (!items.length) { shortcut.append(button('重新加载小挂件', () => refresh(true), 'widget-pill')); return; }
68
- const title = items.length === 1 ? items[0].title : '全部小挂件';
69
- pill = button('', () => {
70
- clearTimeout(collapseTimer);
71
- if (items.length === 1) { open(items[0]); return; }
72
- if (choices) { closeChoices(); return; }
73
- setPhase('choosing');
74
- choices = el('div', 'widget-choices'); choices.setAttribute('role', 'menu'); choices.setAttribute('aria-label', '会话小挂件列表');
75
- for (const item of items) {
76
- const row = button('', () => { closeChoices(false); open(item); }); row.setAttribute('role', 'menuitem');
77
- row.append(el('span', '', item.title), el('span', 'widget-arrow', '›')); choices.append(row);
78
- }
79
- shortcut.append(choices); choices.firstChild?.focus();
80
- }, 'widget-pill');
81
- pill.setAttribute('aria-label', title);
82
- const image = avatar(); image.classList.add('widget-pill-avatar');
83
- pill.append(image, el('span', 'widget-pill-title', title));
84
- if (items.length > 1) { pill.append(el('span', 'widget-chevron', '›')); pill.setAttribute('aria-haspopup', 'menu'); }
85
- shortcut.append(pill); setPhase(choosing ? 'normal' : phase);
86
- }
87
- async function refresh(force = false) {
88
- if (!ready) return;
89
- if (loading) return refreshPromise;
90
- if (!force && Date.now() - lastRefresh < 1500) return;
91
- loading = true; const mine = generation;
92
- try {
93
- refreshPromise = api('/api/simulator/widgets');
94
- const value = await refreshPromise;
95
- if (mine !== generation || !ready) return;
96
- const previousIDs = items.map(item => item.id).join(',');
97
- items = value.widgets || []; failed = false;
98
- documentLifecycle = value.capabilities?.documentLifecycle === true;
99
- editing = value.capabilities?.widgetEditing === true;
100
- conversationRevision = value.conversationRevision ?? 0;
101
- if (selected && !items.some(item => item.id === selected.id)) close(true);
102
- if (previousIDs !== items.map(item => item.id).join(',')) { setPhase('normal'); scheduleCollapse(); }
103
-
104
- } catch { if (mine === generation) failed = true; }
105
- finally { if (mine === generation) { loading = false; lastRefresh = Date.now(); render(); } }
106
- }
107
- function notify(text, error = false) {
108
- overlay.querySelector('.widget-pin-note')?.remove();
109
- const note = el('div', `widget-pin-note${error ? ' is-error' : ''}`, text); note.setAttribute('role', 'status'); overlay.append(note);
110
- setTimeout(() => note.remove(), 4000);
111
- }
112
- function ownerMenu(card, item, mine, current) {
113
- const old = card.querySelector('.widget-options'); if (old) { old.remove(); return; }
114
- const currentItem = items.find(value => value.id === item.id);
115
- if (!currentItem || busy) return;
116
- const menu = el('div', 'widget-options'); menu.setAttribute('role', 'menu');
117
- const pin = button(currentItem.isPinned ? '从首页取消固定' : '固定到首页', async () => {
118
- if (busy) return; busy = true; menu.querySelectorAll('button').forEach(node => { node.disabled = true; });
119
- try {
120
- await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}/pin`, {method:currentItem.isPinned ? 'DELETE' : 'PUT'});
121
- if (mine !== openRevision || current !== generation) return;
122
- currentItem.isPinned = !currentItem.isPinned; menu.remove(); notify(currentItem.isPinned ? '已固定到首页' : '已从首页取消固定');
123
- } catch {
124
- if (mine === openRevision && current === generation) { menu.remove(); notify('操作失败,请稍后重试', true); }
125
- } finally { if (mine === openRevision && current === generation) busy = false; }
126
- });
127
- pin.prepend(menuIcon(currentItem.isPinned ? 'unpin' : 'pin'));
128
- pin.setAttribute('role', 'menuitem'); menu.append(pin); card.append(menu); pin.focus();
129
- }
130
- async function open(item, messageId) {
131
- if (!item.type || !ready) return;
132
- const mine = ++openRevision, current = generation;
133
- if (overlay.hidden) focusBefore = document.activeElement;
134
- clearTimeout(collapseTimer); clearTimeout(closeTimer); closeChoices(false); timers(); busy = false;
135
- selected = { ...item, messageId }; document.querySelector('#message-input').blur();
136
- overlay.replaceChildren(); overlay.hidden = false; overlay.dataset.state = 'opening';
137
- const card = el('section', 'widget-modal'); card.setAttribute('role', 'dialog'); card.setAttribute('aria-modal', 'true'); card.setAttribute('aria-label', item.title);
138
- const header = el('header', 'widget-modal-header');
139
- const dismiss = button('×', () => close()); dismiss.setAttribute('aria-label', '关闭小挂件');
140
- const more = button('···', () => ownerMenu(card, item, mine, current), 'widget-more'); more.setAttribute('aria-label', '小挂件更多'); more.hidden = true;
141
- const slot = el('div', 'widget-more-slot'); slot.append(more);
142
- header.append(dismiss, el('h3', '', item.title), slot);
143
- const body = el('div', 'widget-modal-body'); body.setAttribute('aria-busy', 'true');
144
- const loadingView = el('div', 'widget-loading'); loadingView.setAttribute('role', 'status');
145
- loadingView.append(el('span', 'widget-spinner'), el('p', '', '正在打开小挂件…'));
146
- body.append(loadingView); card.append(header, body); overlay.append(card);
147
- for (const child of phone.children) if (child !== overlay) child.inert = true;
148
- dismiss.focus();
149
- const failedLoad = () => {
150
- if (mine !== openRevision || current !== generation) return;
151
- timers(); body.setAttribute('aria-busy', 'false');
152
- const failure = el('div', 'widget-failure'); failure.setAttribute('role', 'status');
153
- failure.append(el('span', 'widget-failure-mark', '!'), el('h4', '', '小挂件暂时无法打开'), el('p', '', '加载失败,请稍后重试'), button('重新加载', () => open(item, messageId)));
154
- body.replaceChildren(failure);
155
- };
156
- const frame = el('iframe', 'widget-document'); frame.title = item.title; frame.setAttribute('sandbox', 'allow-scripts'); frame.referrerPolicy = 'no-referrer'; frame.style.visibility = 'hidden';
157
- documentFrame = frame; documentToken = crypto.randomUUID();
158
- if (editing) {
159
- const session = {id:item.id, render:documentToken, revision:conversationRevision, closed:false};
160
- widgetSession = session;
161
- session.ready = bridgeCall(session, 'open');
162
- try { await session.ready; }
163
- catch (error) { if (mine === openRevision && current === generation) { failedLoad(); notify(error.message, true); } return; }
164
- if (mine !== openRevision || current !== generation || session.closed) return;
165
- session.heartbeat = setInterval(() => {
166
- if (session.closed) return;
167
- bridgeCall(session, 'heartbeat').catch(error => {
168
- if (session.closed) return;
169
- if (error.status === 404 || error.status === 409) { close(true); notify('小挂件连接已失效,请重新打开', true); }
170
- else session.resume = true;
171
- });
172
- }, 30000);
173
- }
174
- const url = new URL(window.previewApiUrl?.(`/api/simulator/widgets/${encodeURIComponent(item.id)}`) ?? `/api/simulator/widgets/${encodeURIComponent(item.id)}`, location.href);
175
- if (messageId !== undefined) url.searchParams.set('messageId', messageId);
176
- url.searchParams.set('render', documentToken); frame.src = url.href;
177
- documentReady = state => {
178
- if (mine !== openRevision || current !== generation) return;
179
- if (state !== 'ready') { failedLoad(); return; }
180
- clearTimeout(loadTimer); loadingView.remove(); frame.style.visibility = 'visible'; body.setAttribute('aria-busy', 'false');
181
- };
182
- frame.addEventListener('error', failedLoad);
183
- // Older CLI bridges cannot send lifecycle signals. Keep their document load usable.
184
- frame.addEventListener('load', () => { if (!documentLifecycle) documentReady?.('ready'); });
185
- body.append(frame);
186
- frameObserver = new ResizeObserver(() => {
187
- const factor = scale(); if (!factor) return;
188
- frame.style.width = `${body.clientWidth / factor}px`; frame.style.height = `${body.clientHeight / factor}px`;
189
- frame.style.transform = `scale(${factor})`; frame.style.transformOrigin = 'top left';
190
- }); frameObserver.observe(body);
191
- loadTimer = setTimeout(failedLoad, 15000);
192
- await refresh(true);
193
- if (mine === openRevision && current === generation) more.hidden = failed || !items.some(value => value.id === item.id);
194
- }
195
- async function bridgeCall(session, method, params = {}, requestId = crypto.randomUUID()) {
196
- const body = JSON.stringify({render:session.render, conversationRevision:session.revision, method, params, requestId});
197
- for (let attempt = 0; ; attempt++) {
198
- try { return await api(`/api/simulator/widgets/${encodeURIComponent(session.id)}/bridge`, {method:'POST', body, ...(method === 'close' ? {keepalive:true} : {})}); }
199
- catch (error) { if (attempt >= 1 || (error.status && error.status < 500)) throw error; }
200
- }
201
- }
202
- function endSession() {
203
- const session = widgetSession; widgetSession = undefined;
204
- if (!session || session.closed) return;
205
- session.closed = true; clearInterval(session.heartbeat); session.cancelPicker?.();
206
- // 等正在建立的交互完成;CLI 会等待已发出的保存,再幂等关闭。
207
- void session.ready.then(() => bridgeCall(session, 'close')).catch(() => {});
208
- }
209
- function pickImage(session) {
210
- if (session.cancelPicker) return Promise.reject(new Error('请先完成当前图片选择'));
211
- return new Promise((resolve, reject) => {
212
- const input = el('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp'; input.hidden = true;
213
- const finish = (error, value) => { session.cancelPicker = undefined; input.remove(); error ? reject(error) : resolve(value); };
214
- session.cancelPicker = () => finish(new Error('图片选择已取消'));
215
- input.addEventListener('cancel', () => finish(new Error('图片选择已取消')));
216
- input.addEventListener('change', async () => {
217
- try {
218
- const file = input.files?.[0];
219
- if (!file) throw new Error('图片选择已取消');
220
- if (!['image/jpeg','image/png','image/webp'].includes(file.type) || file.size > 5 * 1024 * 1024) throw new Error('请选择 5 MiB 以内的 JPEG、PNG 或 WebP 图片');
221
- const base64 = await new Promise((done, fail) => {
222
- const reader = new FileReader(); reader.onload = () => done(String(reader.result).split(',')[1]); reader.onerror = () => fail(new Error('无法读取图片')); reader.readAsDataURL(file);
223
- });
224
- if (session.closed) throw new Error('小挂件已关闭');
225
- finish(null, await bridgeCall(session, 'media.upload', {mimeType:file.type, base64}));
226
- } catch (error) { finish(error); }
227
- });
228
- overlay.append(input); input.click();
229
- });
230
- }
231
- window.addEventListener('message', async event => {
232
- if (!documentFrame || event.source !== documentFrame.contentWindow || event.data?.render !== documentToken) return;
233
- if (event.data.channel === 'buddy-widget-document') { documentReady?.(event.data.state); return; }
234
- if (event.data.channel !== 'buddy-widget-bridge') return;
235
- const {method, params, requestId} = event.data, session = widgetSession;
236
- const target = event.source, render = documentToken;
237
- const reply = value => target.postMessage({channel:'buddy-widget-reply', render, requestId, ...value}, '*');
238
- try {
239
- if (!session || session.closed) throw new Error('当前页面不支持编辑,请重新打开小挂件');
240
- if (typeof requestId !== 'string' || !/^[a-zA-Z0-9-]{16,64}$/.test(requestId)
241
- || !['records.list','records.get','records.create','records.update','records.delete','media.url','media.pickAndUpload','close'].includes(method)) throw new Error('页面请求了未开放的能力');
242
- await session.ready;
243
- if (session.closed) throw new Error('小挂件已关闭');
244
- if (session.resume) { await bridgeCall(session, 'resume'); session.resume = false; }
245
- const value = method === 'media.pickAndUpload' ? await pickImage(session) : await bridgeCall(session, method, params, requestId);
246
- if (session !== widgetSession || session.closed) return;
247
- reply({value});
248
- if (method === 'close') close();
249
- } catch (error) {
250
- reply({error:{message:error.message || '操作未完成,请重试', status:error.status, code:error.code}});
251
- }
252
- });
253
- overlay.addEventListener('click', event => { if (event.target === overlay) close(); });
254
- document.addEventListener('pointerdown', event => {
255
- if (choices && !shortcut.contains(event.target)) closeChoices();
256
- const menu = overlay.querySelector('.widget-options');
257
- if (menu && !menu.contains(event.target) && !event.target.closest('.widget-more')) menu.remove();
258
- });
259
- document.addEventListener('keydown', event => {
260
- if (event.key === 'Escape') {
261
- if (busy) return;
262
- const nested = overlay.querySelector('.widget-options');
263
- if (nested) { nested.remove(); overlay.querySelector('.widget-more')?.focus(); } else if (!overlay.hidden) close(); else closeChoices();
264
- }
265
- if (event.key === 'Tab' && !overlay.hidden) {
266
- const scope = overlay;
267
- const controls = [...scope.querySelectorAll('button:not(:disabled), iframe')].filter(node => !node.hidden && node.getClientRects().length);
268
- const first = controls[0], last = controls.at(-1);
269
- if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
270
- else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
271
- }
272
- });
273
- document.addEventListener('visibilitychange', () => {
274
- clearTimeout(collapseTimer); closeChoices(false);
275
- if (!document.hidden) { setPhase('normal'); scheduleCollapse(); void refresh(true); }
276
- });
277
- window.addEventListener('pagehide', () => { resize.disconnect(); close(true); clearTimeout(collapseTimer); clearTimeout(closeTimer); });
278
- return {
279
- update({active, conversationRevision, devRevision, reloading = false}) {
280
- const key = `${conversationRevision || 0}:${devRevision || 0}`;
281
- if (conversationScope !== conversationRevision || (!active && !reloading)) reopen = undefined;
282
- else if (selected && (key !== scope || reloading)) reopen = { id: selected.id, messageId: selected.messageId };
283
- conversationScope = conversationRevision;
284
- if (key !== scope || !active) {
285
- scope = key; generation++; ready = false; items = []; failed = false; loading = false; lastRefresh = 0; busy = false; documentLifecycle = false; editing = false;
286
- clearTimeout(collapseTimer); close(true);
287
- }
288
- ready = active; render();
289
- const mine = generation;
290
- void refresh().then(() => {
291
- if (!ready || mine !== generation || !reopen) return;
292
- const item = items.find(value => value.id === reopen.id), messageId = reopen.messageId;
293
- reopen = undefined;
294
- if (item) void open(item, messageId);
295
- });
296
- },
297
- reset() { reopen = undefined; generation++; ready = false; items = []; loading = false; failed = false; busy = false; clearTimeout(collapseTimer); close(true); render(); },
298
- card(item, messageId) {
299
- const card = el('div', 'widget-entry'); card.append(el('h4', '', item.title));
300
- const details = el('div', 'widget-entry-details');
301
- if (item.type && item.imageMediaId) {
302
- const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`;
303
- image.src = window.previewApiUrl?.(`/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`) ?? `/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`;
304
- image.addEventListener('error', () => { image.hidden = true; }); details.append(image);
305
- }
306
- details.append(el('p', item.type && item.summary?.trim() ? '' : 'widget-secondary', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
307
- if (item.type) card.append(button('去查看', () => open(item, messageId), 'widget-entry-open'));
308
- const permission = item.role ? '会话成员可编辑、固定' : undefined;
309
- const metadata = el('div', 'widget-entry-metadata');
310
- if (item.creatorName?.trim()) metadata.append(el('span', 'widget-creator', `${item.creatorName.trim()}创建`));
311
- if (permission) metadata.append(el('span', 'widget-permission', (metadata.children.length ? '|' : '') + permission));
312
- if (metadata.children.length) card.append(metadata);
313
- return card;
314
- },
315
- };
316
- };
317
- })();
File without changes