@my-life-buddies/cli 0.26.0-dev.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -119
- package/dist/bin/buddy.js +38 -47
- package/dist/bin/buddy.js.map +2 -2
- package/dist/bin/core.js +415 -374
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +65 -5
- package/dist/bin/preview.js.map +2 -2
- package/dist/web/app.js +9 -6
- package/dist/web/index.html +2 -0
- package/dist/web/products.css +19 -0
- package/dist/web/products.js +104 -0
- package/package.json +6 -6
package/dist/web/app.js
CHANGED
|
@@ -567,6 +567,7 @@
|
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
const widgets = window.createSimulatorWidgets?.({ api, avatar: () => messageAvatar("assistant") });
|
|
570
|
+
const products = window.createSimulatorProducts?.({ api, refresh: () => refreshSimulator() });
|
|
570
571
|
function renderWidgets() { widgets?.update({ reloading: ["reloading", "reload_failed"].includes(state.dev.phase), active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision }); }
|
|
571
572
|
|
|
572
573
|
let messageSignature = "";
|
|
@@ -604,7 +605,8 @@
|
|
|
604
605
|
|
|
605
606
|
function renderMessages() {
|
|
606
607
|
renderWidgets();
|
|
607
|
-
|
|
608
|
+
products?.update({ active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision });
|
|
609
|
+
const signature = JSON.stringify([state.simulator.messages, state.simulator.activeRun?.runId, state.dev.phase, state.resetting]);
|
|
608
610
|
if (signature === messageSignature) { renderLiveReply(); return; }
|
|
609
611
|
messageSignature = signature;
|
|
610
612
|
const previousTop = elements.chatMessages.scrollTop;
|
|
@@ -623,7 +625,8 @@
|
|
|
623
625
|
}
|
|
624
626
|
const row = node("div", { class: "message-row", "data-role": message.role });
|
|
625
627
|
const wrap = node("div", { class: "message-content" });
|
|
626
|
-
if (message.
|
|
628
|
+
if (message.product && products) wrap.append(products.card(message.product));
|
|
629
|
+
else if (message.widget && widgets) wrap.append(widgets.card(message.widget, message.messageId));
|
|
627
630
|
else {
|
|
628
631
|
const bubble = node("div", { class: "message-bubble" });
|
|
629
632
|
renderMessageText(bubble, message.text);
|
|
@@ -866,7 +869,7 @@
|
|
|
866
869
|
if (!window.confirm("清空当前对话并重新开始?\n这会结束当前开发版会话,重置聊天上下文、记忆和该会话的主动服务。旧记录仅在服务端留档;搭子资料和工程文件会保留。")) return;
|
|
867
870
|
state.resetting = true;
|
|
868
871
|
elements.errorBanner.hidden = true;
|
|
869
|
-
widgets?.reset();
|
|
872
|
+
widgets?.reset(); products?.reset();
|
|
870
873
|
state.simulatorRequest++;
|
|
871
874
|
elements.resetConversation.textContent = "正在清空…";
|
|
872
875
|
renderDev();
|
|
@@ -965,7 +968,7 @@
|
|
|
965
968
|
connectionReady = false;
|
|
966
969
|
state.dev = { phase: "idle", revision: -1 };
|
|
967
970
|
state.simulatorRequest++;
|
|
968
|
-
widgets?.reset();
|
|
971
|
+
widgets?.reset(); products?.reset();
|
|
969
972
|
void togglePairing(false);
|
|
970
973
|
renderDev(); syncPolling();
|
|
971
974
|
$("#local-status").textContent = "本地工程";
|
|
@@ -981,9 +984,9 @@
|
|
|
981
984
|
if (!response.ok) throw new Error(value.error || "连接状态读取失败");
|
|
982
985
|
if (epoch !== connectionEpoch) return;
|
|
983
986
|
connectionChecked = true; connectionFailure = "";
|
|
984
|
-
$("#connect-command").textContent = value.command.replace(/^buddy-cli(?= )/, "buddy-cli
|
|
987
|
+
$("#connect-command").textContent = value.command.replace(/^buddy-cli(?= )/, "buddy-cli");
|
|
985
988
|
$("#copy-connect-command").disabled = false;
|
|
986
|
-
$("#stop-command").textContent = value.stopCommand?.replace(/^buddy-cli(?= )/, "buddy-cli
|
|
989
|
+
$("#stop-command").textContent = value.stopCommand?.replace(/^buddy-cli(?= )/, "buddy-cli") || "buddy-cli dev stop";
|
|
987
990
|
if (value.projectRoot) $("#project-root").textContent = value.projectRoot;
|
|
988
991
|
if (value.machineName) $("#machine-name").textContent = value.machineName;
|
|
989
992
|
if (!value.connected) { connectionInstance = value.instanceId || ""; disconnected(); return; }
|
package/dist/web/index.html
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
<link rel="stylesheet" href="/assets/app.css">
|
|
14
14
|
<script src="/assets/iphone-16-max.js" defer></script>
|
|
15
15
|
<script src="/assets/widgets.js" defer></script>
|
|
16
|
+
<script src="/assets/products.js" defer></script>
|
|
16
17
|
<script src="/assets/diagnostics.js" defer></script>
|
|
17
18
|
<script src="/assets/tool-inventory.js" defer></script>
|
|
18
19
|
<script src="/assets/chat-markdown.js" defer></script>
|
|
19
20
|
<script src="/assets/app.js" defer></script>
|
|
20
21
|
<link rel="stylesheet" href="/assets/widgets.css">
|
|
22
|
+
<link rel="stylesheet" href="/assets/products.css">
|
|
21
23
|
</head>
|
|
22
24
|
<body>
|
|
23
25
|
<a class="skip-link" href="#debug-content">跳到调试内容</a>
|
|
@@ -0,0 +1,19 @@
|
|
|
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; }
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@my-life-buddies/cli",
|
|
3
|
-
"version": "0.26.0
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "搭搭
|
|
5
|
+
"description": "搭搭 Developer Platform 命令行工具",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
8
8
|
"registry": "https://registry.npmjs.org/",
|
|
9
|
-
"tag": "
|
|
9
|
+
"tag": "latest"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=24"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"darwin"
|
|
16
16
|
],
|
|
17
17
|
"bin": {
|
|
18
|
-
"buddy-cli
|
|
18
|
+
"buddy-cli": "dist/bin/buddy.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist/bin",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"acorn": "8.15.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@buddy/cli-core": "0.26.0
|
|
46
|
-
"@buddy/preview": "0.26.0
|
|
45
|
+
"@buddy/cli-core": "0.26.0",
|
|
46
|
+
"@buddy/preview": "0.26.0"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|