@my-life-buddies/cli 0.23.0-dev.1 → 0.23.0-dev.5
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 +14 -11
- package/dist/bin/buddy.js +1 -8
- package/dist/bin/buddy.js.map +2 -2
- package/dist/bin/core.js +1497 -1437
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +290 -55
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/widget-document.js +41 -0
- package/dist/web/widgets.js +88 -54
- package/package.json +8 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 注入隔离的 Widget 文档;只允许向宿主发送固定方法,不提供身份、Token 或网络代理。
|
|
2
|
+
((render) => {
|
|
3
|
+
let counter = 0, closed = false, gesture = 0;
|
|
4
|
+
const pending = new Map();
|
|
5
|
+
document.addEventListener('click', event => { if (event.isTrusted) gesture = Date.now(); }, true);
|
|
6
|
+
function request(method, params = {}) {
|
|
7
|
+
if (closed) return Promise.reject(new Error('小挂件已关闭'));
|
|
8
|
+
const requestId = `${render}-${++counter}`;
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const timer = setTimeout(() => { pending.delete(requestId); reject(new Error('操作等待超时,请关闭后重新打开并核对数据')); }, 90000);
|
|
11
|
+
pending.set(requestId, {resolve, reject, timer});
|
|
12
|
+
parent.postMessage({channel:'buddy-widget-bridge', render, requestId, method, params}, '*');
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
window.addEventListener('message', event => {
|
|
16
|
+
const data = event.data;
|
|
17
|
+
if (event.source !== parent || data?.channel !== 'buddy-widget-reply' || data.render !== render) return;
|
|
18
|
+
const call = pending.get(data.requestId); if (!call) return;
|
|
19
|
+
pending.delete(data.requestId); clearTimeout(call.timer);
|
|
20
|
+
if (data.error) call.reject(Object.assign(new Error(data.error.message), {status:data.error.status, code:data.error.code}));
|
|
21
|
+
else call.resolve(data.value);
|
|
22
|
+
});
|
|
23
|
+
const records = Object.freeze({
|
|
24
|
+
list: module => request('records.list', {module}).then(value => value.records),
|
|
25
|
+
get: (module, recordId) => request('records.get', {module, recordId}).then(value => value.record),
|
|
26
|
+
create: (module, data) => request('records.create', {module, data}),
|
|
27
|
+
update: (module, recordId, data, options = {}) => request('records.update', {module, recordId, data, expectedRevision:options.expectedRevision}),
|
|
28
|
+
delete: (module, recordId, options = {}) => request('records.delete', {module, recordId, expectedRevision:options.expectedRevision}),
|
|
29
|
+
});
|
|
30
|
+
const media = Object.freeze({
|
|
31
|
+
url: mediaId => request('media.url', {mediaId}).then(value => value.url),
|
|
32
|
+
pickAndUpload: () => {
|
|
33
|
+
if (Date.now() - gesture > 1000) return Promise.reject(new Error('请点击按钮后选择图片'));
|
|
34
|
+
gesture = 0; return request('media.pickAndUpload');
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(window, 'dadaWidget', { value: Object.freeze({records, media,
|
|
38
|
+
close: async () => { await request('close'); closed = true; },
|
|
39
|
+
}), configurable:false, writable:false });
|
|
40
|
+
window.addEventListener('load', () => parent.postMessage({channel:'buddy-widget-document', render, state:'ready'}, '*'), {once:true});
|
|
41
|
+
})(__WIDGET_RENDER__);
|
package/dist/web/widgets.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Native point sizes share the phone's scale; Web content remains in an opaque sandbox. */
|
|
1
|
+
/* SIT Widget 编辑宿主。页面保持不透明沙箱,身份和记录操作由 CLI 代理。 */
|
|
3
2
|
(() => {
|
|
4
3
|
const el = (tag, className = '', text) => {
|
|
5
4
|
const node = document.createElement(tag); node.className = className;
|
|
@@ -31,8 +30,8 @@
|
|
|
31
30
|
let failed = false, signature = '', selected, focusBefore, openRevision = 0, frameObserver;
|
|
32
31
|
let collapseTimer, closeTimer, loadTimer, documentFrame, documentToken, documentReady;
|
|
33
32
|
let phase = 'normal', choices, pill, busy = false, refreshPromise;
|
|
34
|
-
let documentLifecycle = false,
|
|
35
|
-
const timers = () => { clearTimeout(loadTimer); frameObserver?.disconnect(); frameObserver = undefined; documentFrame = undefined; documentReady = undefined; };
|
|
33
|
+
let documentLifecycle = false, editing = false, conversationRevision = 0, widgetSession;
|
|
34
|
+
const timers = () => { endSession(); clearTimeout(loadTimer); frameObserver?.disconnect(); frameObserver = undefined; documentFrame = undefined; documentReady = undefined; };
|
|
36
35
|
const scale = () => device.clientWidth / 415;
|
|
37
36
|
const resize = new ResizeObserver(() => { phone.style.setProperty('--widget-unit', `${scale()}px`); });
|
|
38
37
|
resize.observe(device);
|
|
@@ -97,16 +96,11 @@
|
|
|
97
96
|
const previousIDs = items.map(item => item.id).join(',');
|
|
98
97
|
items = value.widgets || []; failed = false;
|
|
99
98
|
documentLifecycle = value.capabilities?.documentLifecycle === true;
|
|
100
|
-
|
|
99
|
+
editing = value.capabilities?.widgetEditing === true;
|
|
100
|
+
conversationRevision = value.conversationRevision ?? 0;
|
|
101
101
|
if (selected && !items.some(item => item.id === selected.id)) close(true);
|
|
102
102
|
if (previousIDs !== items.map(item => item.id).join(',')) { setPhase('normal'); scheduleCollapse(); }
|
|
103
|
-
|
|
104
|
-
if (selected && items.find(item => item.id === selected.id)?.role !== 'owner') {
|
|
105
|
-
overlay.querySelector('.widget-options')?.remove();
|
|
106
|
-
overlay.querySelector('.widget-confirm-backdrop')?.remove();
|
|
107
|
-
overlay.querySelector('.widget-native-share')?.remove();
|
|
108
|
-
const more = overlay.querySelector('.widget-more'); if (more) more.hidden = true;
|
|
109
|
-
}
|
|
103
|
+
|
|
110
104
|
} catch { if (mine === generation) failed = true; }
|
|
111
105
|
finally { if (mine === generation) { loading = false; lastRefresh = Date.now(); render(); } }
|
|
112
106
|
}
|
|
@@ -115,39 +109,11 @@
|
|
|
115
109
|
const note = el('div', `widget-pin-note${error ? ' is-error' : ''}`, text); note.setAttribute('role', 'status'); overlay.append(note);
|
|
116
110
|
setTimeout(() => note.remove(), 4000);
|
|
117
111
|
}
|
|
118
|
-
function showConfirmation(card, item, mine, current) {
|
|
119
|
-
const shade = el('div', 'widget-confirm-backdrop');
|
|
120
|
-
const dialog = el('section', 'widget-confirm'); dialog.setAttribute('role', 'alertdialog'); dialog.setAttribute('aria-label', '删除小挂件?');
|
|
121
|
-
dialog.append(el('h4', '', '删除小挂件?'), el('p', '', '将删除小挂件和其中的数据,所有会话里的引用及首页固定都会失效。此操作不可恢复。'));
|
|
122
|
-
const actions = el('div', 'widget-confirm-actions');
|
|
123
|
-
const cancel = button('取消', () => { if (!busy) { shade.remove(); card.querySelector('.widget-more')?.focus(); } });
|
|
124
|
-
const remove = button('删除', async () => {
|
|
125
|
-
if (busy) return;
|
|
126
|
-
busy = true; cancel.disabled = true; remove.disabled = true; remove.textContent = '正在删除…';
|
|
127
|
-
try {
|
|
128
|
-
await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}`, {method:'DELETE'});
|
|
129
|
-
if (mine !== openRevision || current !== generation) return;
|
|
130
|
-
items = items.filter(value => value.id !== item.id); render(); busy = false; close();
|
|
131
|
-
} catch {
|
|
132
|
-
if (mine !== openRevision || current !== generation) return;
|
|
133
|
-
shade.remove(); notify('操作失败,请稍后重试', true);
|
|
134
|
-
} finally { if (mine === openRevision && current === generation) busy = false; }
|
|
135
|
-
}, 'widget-destructive');
|
|
136
|
-
actions.append(cancel, remove); dialog.append(actions); shade.append(dialog); card.append(shade); cancel.focus();
|
|
137
|
-
}
|
|
138
|
-
function showShare(card) {
|
|
139
|
-
// Sharing chooses real conversations in the App; the simulator never fabricates recipients.
|
|
140
|
-
const notice = el('div', 'widget-native-share');
|
|
141
|
-
notice.append(el('h4', '', '在搭搭 App 中分享'), el('p', '', '使用真机预览打开这个搭子,在小挂件的「分享」中选择聊天。'));
|
|
142
|
-
notice.append(button('打开真机预览', () => { close(true); document.querySelector('#pair-device').click(); }), button('返回小挂件', () => notice.remove()));
|
|
143
|
-
card.append(notice); notice.querySelector('button').focus();
|
|
144
|
-
}
|
|
145
112
|
function ownerMenu(card, item, mine, current) {
|
|
146
113
|
const old = card.querySelector('.widget-options'); if (old) { old.remove(); return; }
|
|
147
114
|
const currentItem = items.find(value => value.id === item.id);
|
|
148
|
-
if (currentItem
|
|
115
|
+
if (!currentItem || busy) return;
|
|
149
116
|
const menu = el('div', 'widget-options'); menu.setAttribute('role', 'menu');
|
|
150
|
-
const share = button('分享', () => { menu.remove(); showShare(card); });
|
|
151
117
|
const pin = button(currentItem.isPinned ? '从首页取消固定' : '固定到首页', async () => {
|
|
152
118
|
if (busy) return; busy = true; menu.querySelectorAll('button').forEach(node => { node.disabled = true; });
|
|
153
119
|
try {
|
|
@@ -158,10 +124,8 @@
|
|
|
158
124
|
if (mine === openRevision && current === generation) { menu.remove(); notify('操作失败,请稍后重试', true); }
|
|
159
125
|
} finally { if (mine === openRevision && current === generation) busy = false; }
|
|
160
126
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for (const row of [share, pin, ...(canDelete ? [remove] : [])]) { row.setAttribute('role', 'menuitem'); menu.append(row); }
|
|
164
|
-
card.append(menu); share.focus();
|
|
127
|
+
pin.prepend(menuIcon(currentItem.isPinned ? 'unpin' : 'pin'));
|
|
128
|
+
pin.setAttribute('role', 'menuitem'); menu.append(pin); card.append(menu); pin.focus();
|
|
165
129
|
}
|
|
166
130
|
async function open(item, messageId) {
|
|
167
131
|
if (!item.type || !ready) return;
|
|
@@ -191,6 +155,22 @@
|
|
|
191
155
|
};
|
|
192
156
|
const frame = el('iframe', 'widget-document'); frame.title = item.title; frame.setAttribute('sandbox', 'allow-scripts'); frame.referrerPolicy = 'no-referrer'; frame.style.visibility = 'hidden';
|
|
193
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
|
+
}
|
|
194
174
|
const url = new URL(window.previewApiUrl?.(`/api/simulator/widgets/${encodeURIComponent(item.id)}`) ?? `/api/simulator/widgets/${encodeURIComponent(item.id)}`, location.href);
|
|
195
175
|
if (messageId !== undefined) url.searchParams.set('messageId', messageId);
|
|
196
176
|
url.searchParams.set('render', documentToken); frame.src = url.href;
|
|
@@ -210,11 +190,65 @@
|
|
|
210
190
|
}); frameObserver.observe(body);
|
|
211
191
|
loadTimer = setTimeout(failedLoad, 15000);
|
|
212
192
|
await refresh(true);
|
|
213
|
-
if (mine === openRevision && current === generation) more.hidden = failed || items.
|
|
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
|
+
}
|
|
214
201
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
252
|
});
|
|
219
253
|
overlay.addEventListener('click', event => { if (event.target === overlay) close(); });
|
|
220
254
|
document.addEventListener('pointerdown', event => {
|
|
@@ -225,11 +259,11 @@
|
|
|
225
259
|
document.addEventListener('keydown', event => {
|
|
226
260
|
if (event.key === 'Escape') {
|
|
227
261
|
if (busy) return;
|
|
228
|
-
const nested = overlay.querySelector('.widget-
|
|
262
|
+
const nested = overlay.querySelector('.widget-options');
|
|
229
263
|
if (nested) { nested.remove(); overlay.querySelector('.widget-more')?.focus(); } else if (!overlay.hidden) close(); else closeChoices();
|
|
230
264
|
}
|
|
231
265
|
if (event.key === 'Tab' && !overlay.hidden) {
|
|
232
|
-
const scope = overlay
|
|
266
|
+
const scope = overlay;
|
|
233
267
|
const controls = [...scope.querySelectorAll('button:not(:disabled), iframe')].filter(node => !node.hidden && node.getClientRects().length);
|
|
234
268
|
const first = controls[0], last = controls.at(-1);
|
|
235
269
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
|
@@ -240,7 +274,7 @@
|
|
|
240
274
|
clearTimeout(collapseTimer); closeChoices(false);
|
|
241
275
|
if (!document.hidden) { setPhase('normal'); scheduleCollapse(); void refresh(true); }
|
|
242
276
|
});
|
|
243
|
-
window.addEventListener('pagehide', () => { resize.disconnect();
|
|
277
|
+
window.addEventListener('pagehide', () => { resize.disconnect(); close(true); clearTimeout(collapseTimer); clearTimeout(closeTimer); });
|
|
244
278
|
return {
|
|
245
279
|
update({active, conversationRevision, devRevision, reloading = false}) {
|
|
246
280
|
const key = `${conversationRevision || 0}:${devRevision || 0}`;
|
|
@@ -248,7 +282,7 @@
|
|
|
248
282
|
else if (selected && (key !== scope || reloading)) reopen = { id: selected.id, messageId: selected.messageId };
|
|
249
283
|
conversationScope = conversationRevision;
|
|
250
284
|
if (key !== scope || !active) {
|
|
251
|
-
scope = key; generation++; ready = false; items = []; failed = false; loading = false; lastRefresh = 0; busy = false; documentLifecycle = false;
|
|
285
|
+
scope = key; generation++; ready = false; items = []; failed = false; loading = false; lastRefresh = 0; busy = false; documentLifecycle = false; editing = false;
|
|
252
286
|
clearTimeout(collapseTimer); close(true);
|
|
253
287
|
}
|
|
254
288
|
ready = active; render();
|
|
@@ -271,7 +305,7 @@
|
|
|
271
305
|
}
|
|
272
306
|
details.append(el('p', item.type && item.summary?.trim() ? '' : 'widget-secondary', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
|
|
273
307
|
if (item.type) card.append(button('去查看', () => open(item, messageId), 'widget-entry-open'));
|
|
274
|
-
const permission =
|
|
308
|
+
const permission = item.role ? '会话成员可编辑、固定' : undefined;
|
|
275
309
|
const metadata = el('div', 'widget-entry-metadata');
|
|
276
310
|
if (item.creatorName?.trim()) metadata.append(el('span', 'widget-creator', `${item.creatorName.trim()}创建`));
|
|
277
311
|
if (permission) metadata.append(el('span', 'widget-permission', (metadata.children.length ? '|' : '') + permission));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@my-life-buddies/cli",
|
|
3
|
-
"version": "0.23.0-dev.
|
|
3
|
+
"version": "0.23.0-dev.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "搭搭 SIT 开发命令行工具",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,7 +41,12 @@
|
|
|
41
41
|
"ws": "8.21.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@buddy/cli-core": "0.23.0-dev.
|
|
45
|
-
"@buddy/preview": "0.23.0-dev.
|
|
44
|
+
"@buddy/cli-core": "0.23.0-dev.5",
|
|
45
|
+
"@buddy/preview": "0.23.0-dev.5"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+ssh://git@code.devops.xiaohongshu.com/dada/cli.git",
|
|
50
|
+
"directory": "apps/cli"
|
|
46
51
|
}
|
|
47
52
|
}
|