made-bridge 0.3.4 → 0.3.6
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/package.json +1 -1
- package/public/app.js +82 -5
- package/public/i18n-messages.js +28 -0
- package/public/styles.css +44 -0
- package/public/tasks.html +10 -1
- package/src/agent-sprites.mjs +74 -9
- package/src/agents/events.mjs +95 -1
- package/src/agents/shared.mjs +7 -2
- package/src/config.mjs +10 -4
- package/src/i18n.mjs +3 -2
- package/src/installed.mjs +18 -11
- package/src/jobs.mjs +69 -1
- package/src/plugins/enhance.mjs +1 -0
- package/src/plugins/hot-reload.mjs +64 -1
- package/src/plugins/registry.mjs +2 -1
- package/src/providers.mjs +4 -1
- package/src/server.mjs +18 -2
- package/src/sessions.mjs +1 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
6
6
|
const $ = (id) => document.getElementById(id);
|
|
7
7
|
const taskPage = document.body.dataset.page === 'tasks';
|
|
8
8
|
const assistantsPage = document.body.dataset.page === 'assistants';
|
|
9
|
-
const state = { jobs: [], sessions: [], providers: [], projects: [], connection: {}, voice: {}, models: {}, pairing: { open: false, pending: [], paired: [] }, tunnel: { status: 'off' }, receiver: { status: 'disabled' }, receiverPasswordVisible: false, token: null, lastSyncedAt: null, loaded: false, error: '', busy: new Set(), pairBusy: false, pairWindowBusy: false, tunnelBusy: false, dismissedPairing: new Set(), selectedSessionId: null, previewProvider: '', previewSessionId: null, sessionFormOpen: false, legacySessionsOpen: false, renamingSession: false, refreshing: false };
|
|
9
|
+
const state = { jobs: [], sessions: [], providers: [], projects: [], connection: {}, voice: {}, models: {}, pairing: { open: false, pending: [], paired: [] }, tunnel: { status: 'off' }, receiver: { status: 'disabled' }, receiverPasswordVisible: false, token: null, lastSyncedAt: null, loaded: false, error: '', busy: new Set(), pairBusy: false, pairWindowBusy: false, tunnelBusy: false, dismissedPairing: new Set(), selectedSessionId: null, previewProvider: '', previewSessionId: null, sessionFormOpen: false, legacySessionsOpen: false, renamingSession: false, refreshing: false, historyOpen: false };
|
|
10
10
|
if (taskPage) {
|
|
11
11
|
try { state.selectedSessionId = sessionStorage.getItem('vibe-selected-session'); } catch { /* Selection works without storage. */ }
|
|
12
12
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -64,6 +64,9 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
64
64
|
const canConfirm = (job) => job.status === 'waiting_confirmation';
|
|
65
65
|
const canCancel = (job) => ['waiting_confirmation', 'queued', 'running'].includes(job.status);
|
|
66
66
|
const timeText = (value) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? '' : new Intl.DateTimeFormat(getLocale(), { hour: '2-digit', minute: '2-digit' }).format(date); };
|
|
67
|
+
const turnAnswer = (job) => userFacingJobError(job) || (job.status === 'running' ? job.progress : '') || job.result ||
|
|
68
|
+
(job.status === 'waiting_confirmation' ? t('app.074') :
|
|
69
|
+
job.status === 'queued' ? t('app.075') : t('app.076'));
|
|
67
70
|
const setFormMessage = (message, success = false) => { setUiMessage($('formMessage'), message); $('formMessage').classList.toggle('success', success); };
|
|
68
71
|
let toastTimer;
|
|
69
72
|
function showToast(message, error = false) { const toast = $('toast'); setUiMessage(toast, message); toast.classList.toggle('error', error); toast.classList.add('visible'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('visible'), 4200); }
|
|
@@ -294,6 +297,28 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
294
297
|
showToast(error.message || t('app.065'), true);
|
|
295
298
|
} finally { state.busy.delete('rename-session'); render(); }
|
|
296
299
|
}
|
|
300
|
+
/** 工具调用轨迹(仅桥接器网页端展示,不下发设备):可折叠列表。 */
|
|
301
|
+
function toolTrace(job) {
|
|
302
|
+
const calls = Array.isArray(job.toolCalls) ? job.toolCalls.filter((call) => call && safeString(call.name)) : [];
|
|
303
|
+
if (!calls.length) return null;
|
|
304
|
+
const details = document.createElement('details');
|
|
305
|
+
details.className = 'tool-trace';
|
|
306
|
+
const summary = document.createElement('summary');
|
|
307
|
+
summary.textContent = t('app.231', { v0: calls.length });
|
|
308
|
+
details.append(summary);
|
|
309
|
+
const list = text('div', 'tool-trace-list');
|
|
310
|
+
for (const call of [...calls].reverse()) {
|
|
311
|
+
const line = text('div', `tool-trace-line ${call.state === 'completed' ? 'done' : 'active'}`);
|
|
312
|
+
line.append(
|
|
313
|
+
text('span', 'tool-trace-mark', call.state === 'completed' ? '✓' : '…'),
|
|
314
|
+
text('span', 'tool-trace-name', safeString(call.name)),
|
|
315
|
+
);
|
|
316
|
+
if (safeString(call.detail)) line.append(text('code', 'tool-trace-detail', safeString(call.detail)));
|
|
317
|
+
list.append(line);
|
|
318
|
+
}
|
|
319
|
+
details.append(list);
|
|
320
|
+
return details;
|
|
321
|
+
}
|
|
297
322
|
function renderJobs() {
|
|
298
323
|
if (!$('jobList')) return;
|
|
299
324
|
const list = $('jobList'); list.replaceChildren();
|
|
@@ -314,10 +339,9 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
314
339
|
const top = text('div', 'job-top');
|
|
315
340
|
top.append(text('div', 'job-meta', t('app.071', { v0: (timeText(job.createdAt) || t('app.047')) })), text('span', `job-status ${info.css}`, info.label));
|
|
316
341
|
card.append(top, text('div', 'job-instruction', t('app.073', { v0: (job.instruction || t('app.072')) })));
|
|
317
|
-
const answer =
|
|
318
|
-
(job.status === 'waiting_confirmation' ? t('app.074') :
|
|
319
|
-
job.status === 'queued' ? t('app.075') : t('app.076'));
|
|
342
|
+
const answer = turnAnswer(job);
|
|
320
343
|
card.append(text('p', `job-summary turn-answer ${job.error && !job.retryPending ? 'error' : ''}`, t('app.077', { v0: (providerName(job.provider)), v1: (safeString(answer)) })));
|
|
344
|
+
const trace = toolTrace(job); if (trace) card.append(trace);
|
|
321
345
|
if (job.provider === 'codex' && job.retryPending) {
|
|
322
346
|
card.append(text('p', 'job-retry-notice', t('app.078')));
|
|
323
347
|
}
|
|
@@ -328,6 +352,40 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
328
352
|
if (actions.children.length) { const bottom = text('div', 'job-bottom'); bottom.append(actions); card.append(bottom); }
|
|
329
353
|
list.append(card);
|
|
330
354
|
}
|
|
355
|
+
/** 历史会话抽屉:当前任务的全部指令与回复,从第一轮按时间往下排。数据来自 /api/state 的 jobs。 */
|
|
356
|
+
function renderHistory() {
|
|
357
|
+
const button = $('historyDrawerButton');
|
|
358
|
+
const overlay = $('historyDrawerOverlay');
|
|
359
|
+
if (!button || !overlay) return;
|
|
360
|
+
const session = selectedSession();
|
|
361
|
+
const jobs = session ? jobsForSession(session.id) : [];
|
|
362
|
+
button.hidden = !session || !!state.error;
|
|
363
|
+
button.setAttribute('aria-expanded', String(state.historyOpen));
|
|
364
|
+
overlay.hidden = !state.historyOpen;
|
|
365
|
+
if (!state.historyOpen) return;
|
|
366
|
+
$('historyDrawerMeta').textContent = session ? `${session.title || t('app.045')} — ${t('app.227')}` : t('app.227');
|
|
367
|
+
const list = $('historyList'); list.replaceChildren();
|
|
368
|
+
if (!jobs.length) {
|
|
369
|
+
list.append(text('p', 'history-empty', t('app.229')));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// /api/state 的 jobs 是新→旧,反转成第一轮在最上面,编号也按时间顺序。
|
|
373
|
+
[...jobs].reverse().forEach((job, index) => {
|
|
374
|
+
const info = jobStatus(job);
|
|
375
|
+
const turn = text('article', 'history-turn');
|
|
376
|
+
const top = text('div', 'history-turn-top');
|
|
377
|
+
top.append(
|
|
378
|
+
text('span', 'turn-number', t('app.228', { v0: index + 1 })),
|
|
379
|
+
text('span', 'history-turn-time', formatDateTime(job.createdAt)),
|
|
380
|
+
text('span', `job-status ${info.css}`, info.label),
|
|
381
|
+
);
|
|
382
|
+
turn.append(top, text('div', 'history-turn-instruction', job.instruction || t('app.072')));
|
|
383
|
+
const answer = turnAnswer(job);
|
|
384
|
+
if (answer) turn.append(text('p', `history-turn-answer ${job.error && !job.retryPending ? 'error' : ''}`, answer));
|
|
385
|
+
const trace = toolTrace(job); if (trace) turn.append(trace);
|
|
386
|
+
list.append(turn);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
331
389
|
const boardEncoder = new TextEncoder();
|
|
332
390
|
function boardCleanText(value) {
|
|
333
391
|
return safeString(value)
|
|
@@ -912,7 +970,7 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
912
970
|
setUiMessage($('firmwareMessage'), error.message || '');
|
|
913
971
|
} finally { firmwareBusy = false; }
|
|
914
972
|
}
|
|
915
|
-
function render() { renderSelects(); renderSessions(); renderJobs(); renderBoard(); renderPairing(); renderTunnel(); renderReceiver(); renderFirmware(); renderConnection(); renderModelSettings(); renderVoiceSettings(); renderProviders(); }
|
|
973
|
+
function render() { renderSelects(); renderSessions(); renderJobs(); renderHistory(); renderBoard(); renderPairing(); renderTunnel(); renderReceiver(); renderFirmware(); renderConnection(); renderModelSettings(); renderVoiceSettings(); renderProviders(); }
|
|
916
974
|
function applyLanguage(locale) {
|
|
917
975
|
const changed = setLocale(locale);
|
|
918
976
|
if (!changed && document.documentElement.lang === locale) {
|
|
@@ -992,6 +1050,25 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
|
|
|
992
1050
|
state.legacySessionsOpen = !state.legacySessionsOpen;
|
|
993
1051
|
renderSessions();
|
|
994
1052
|
});
|
|
1053
|
+
$('historyDrawerButton')?.addEventListener('click', () => {
|
|
1054
|
+
state.historyOpen = !state.historyOpen;
|
|
1055
|
+
renderHistory();
|
|
1056
|
+
});
|
|
1057
|
+
$('historyDrawerClose')?.addEventListener('click', () => {
|
|
1058
|
+
state.historyOpen = false;
|
|
1059
|
+
renderHistory();
|
|
1060
|
+
});
|
|
1061
|
+
$('historyDrawerOverlay')?.addEventListener('click', (event) => {
|
|
1062
|
+
if (event.target !== event.currentTarget) return;
|
|
1063
|
+
state.historyOpen = false;
|
|
1064
|
+
renderHistory();
|
|
1065
|
+
});
|
|
1066
|
+
document.addEventListener('keydown', (event) => {
|
|
1067
|
+
if (event.key === 'Escape' && state.historyOpen) {
|
|
1068
|
+
state.historyOpen = false;
|
|
1069
|
+
renderHistory();
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
995
1072
|
$('sessionForm')?.addEventListener('submit', (event) => { event.preventDefault(); createSession(); });
|
|
996
1073
|
$('renameSessionButton')?.addEventListener('click', () => {
|
|
997
1074
|
const session = selectedSession();
|
package/public/i18n-messages.js
CHANGED
|
@@ -900,6 +900,34 @@ export const messages = {
|
|
|
900
900
|
"zh-CN": "创建失败",
|
|
901
901
|
"en": "Creation failed"
|
|
902
902
|
},
|
|
903
|
+
"app.226": {
|
|
904
|
+
"zh-CN": "历史记录",
|
|
905
|
+
"en": "History"
|
|
906
|
+
},
|
|
907
|
+
"app.227": {
|
|
908
|
+
"zh-CN": "这个任务的全部指令与回复,第一轮在最上面。",
|
|
909
|
+
"en": "Every instruction and reply of this task, oldest first."
|
|
910
|
+
},
|
|
911
|
+
"app.228": {
|
|
912
|
+
"zh-CN": "第 {v0} 轮",
|
|
913
|
+
"en": "Turn {v0}"
|
|
914
|
+
},
|
|
915
|
+
"app.229": {
|
|
916
|
+
"zh-CN": "这个任务还没有对话记录。",
|
|
917
|
+
"en": "No conversation history for this task yet."
|
|
918
|
+
},
|
|
919
|
+
"app.230": {
|
|
920
|
+
"zh-CN": "关闭",
|
|
921
|
+
"en": "Close"
|
|
922
|
+
},
|
|
923
|
+
"app.231": {
|
|
924
|
+
"zh-CN": "工具调用({v0})",
|
|
925
|
+
"en": "Tool calls ({v0})"
|
|
926
|
+
},
|
|
927
|
+
"app.232": {
|
|
928
|
+
"zh-CN": "进行中",
|
|
929
|
+
"en": "running"
|
|
930
|
+
},
|
|
903
931
|
"tunnel.official.title": {
|
|
904
932
|
"zh-CN": "官方入口",
|
|
905
933
|
"en": "Official links"
|
package/public/styles.css
CHANGED
|
@@ -443,3 +443,47 @@
|
|
|
443
443
|
.tunnel-service-guide { padding: 12px; }
|
|
444
444
|
.tunnel-official-link { flex: 1 1 auto; }
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
/* 历史会话抽屉:任务页「最新进展」旁的按钮拉开,展示当前任务的全部指令与回复 */
|
|
448
|
+
.history-overlay { position: fixed; inset: 0; z-index: 15; background: rgba(74, 62, 44, .38); backdrop-filter: blur(4px); }
|
|
449
|
+
.history-overlay[hidden] { display: none; }
|
|
450
|
+
.history-drawer { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; width: min(560px, 100%); height: 100%; background: var(--bg); border-left: 1px solid var(--line); box-shadow: -18px 0 48px rgba(74, 62, 44, .22); animation: history-in .22s ease; }
|
|
451
|
+
@keyframes history-in { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
452
|
+
.history-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 22px 22px 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
453
|
+
.history-head h2 { margin-top: 6px; font-size: var(--font-heading); font-weight: 700; }
|
|
454
|
+
.history-head .panel-intro { margin-top: 6px; }
|
|
455
|
+
.history-head .icon-button { flex: 0 0 auto; }
|
|
456
|
+
.history-list { flex: 1; display: grid; gap: 10px; align-content: start; padding: 18px 22px 30px; overflow-y: auto; }
|
|
457
|
+
.history-empty { color: var(--dim); font-size: var(--font-small); padding: 13px; }
|
|
458
|
+
.history-turn { border: 1px solid var(--line); border-radius: 15px; background: var(--panel); padding: 14px 15px; }
|
|
459
|
+
.history-turn-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; color: var(--dim); font-size: var(--font-small); font-weight: 600; }
|
|
460
|
+
.history-turn-time { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
461
|
+
.history-turn-instruction { margin: 9px 0 0; font-size: var(--font-body); font-weight: 600; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
462
|
+
.history-turn-answer { margin: 7px 0 0; color: var(--muted); font-size: var(--font-body); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
463
|
+
.history-turn-answer.error { color: var(--red); }
|
|
464
|
+
|
|
465
|
+
/* 工具调用轨迹:仅桥接器网页端展示,可折叠;设备端不携带该数据 */
|
|
466
|
+
.tool-trace { margin: 2px 0 12px; border: 1px dashed var(--line); border-radius: 12px; background: var(--panel); }
|
|
467
|
+
.tool-trace summary { padding: 9px 13px; color: var(--muted); font-size: var(--font-small); font-weight: 700; cursor: pointer; user-select: none; list-style: none; display: flex; align-items: center; gap: 7px; }
|
|
468
|
+
.tool-trace summary::-webkit-details-marker { display: none; }
|
|
469
|
+
.tool-trace summary::after { content: '+'; margin-left: auto; color: var(--dim); font-size: 16px; }
|
|
470
|
+
.tool-trace[open] summary::after { content: '–'; }
|
|
471
|
+
.tool-trace summary:hover { color: var(--green-deep); }
|
|
472
|
+
.tool-trace-list { display: grid; gap: 6px; max-height: 320px; overflow-y: auto; padding: 4px 13px 12px; }
|
|
473
|
+
.tool-trace-line { display: flex; align-items: baseline; gap: 8px; min-width: 0; font-size: var(--font-small); line-height: 1.5; }
|
|
474
|
+
.tool-trace-mark { flex: 0 0 auto; color: var(--green); font-weight: 700; }
|
|
475
|
+
.tool-trace-line.active .tool-trace-mark { color: var(--blue); animation: tool-trace-pulse 1.2s ease-in-out infinite; }
|
|
476
|
+
@keyframes tool-trace-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
|
477
|
+
.tool-trace-name { flex: 0 0 auto; color: var(--text); font-weight: 600; }
|
|
478
|
+
.tool-trace-detail { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; color: var(--muted); background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 2px 8px; font-size: 13px; }
|
|
479
|
+
@media (max-width: 650px) {
|
|
480
|
+
.tool-trace-line { flex-wrap: wrap; }
|
|
481
|
+
.tool-trace-detail { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@media (max-width: 650px) {
|
|
485
|
+
.history-head { padding: 18px 16px 13px; }
|
|
486
|
+
.history-list { padding: 14px 16px 24px; }
|
|
487
|
+
.history-turn { padding: 13px 14px; }
|
|
488
|
+
.history-turn-top { align-items: flex-start; }
|
|
489
|
+
}
|
package/public/tasks.html
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
</section>
|
|
54
54
|
|
|
55
55
|
<section class="panel queue" aria-labelledby="queue-title">
|
|
56
|
-
<div class="panel-head"><div><h2 id="queue-title"><span data-i18n="static.132">最新进展</span></h2><p class="panel-intro"><span data-i18n="static.133">显示这个任务最新一次指令、执行状态和助手回复。</span></p></div></div>
|
|
56
|
+
<div class="panel-head"><div><h2 id="queue-title"><span data-i18n="static.132">最新进展</span></h2><p class="panel-intro"><span data-i18n="static.133">显示这个任务最新一次指令、执行状态和助手回复。</span></p></div><button id="historyDrawerButton" class="small-button" type="button" aria-expanded="false" aria-controls="historyDrawerOverlay" hidden data-i18n="app.226">历史记录</button></div>
|
|
57
57
|
<div id="jobList" class="job-list" aria-live="polite"><div class="empty"><div class="empty-icon" aria-hidden="true">⌁</div><strong><span data-i18n="static.122">正在读取任务</span></strong><span><span data-i18n="static.134">正在连接本机桥接服务…</span></span></div></div>
|
|
58
58
|
</section>
|
|
59
59
|
|
|
@@ -70,6 +70,15 @@
|
|
|
70
70
|
</main>
|
|
71
71
|
<footer class="footer"><span><span data-i18n="static.082">码得 · 多助手桥接</span></span><span id="lastUpdated"><span data-i18n="static.083">等待首次同步</span></span></footer>
|
|
72
72
|
</div>
|
|
73
|
+
<div id="historyDrawerOverlay" class="history-overlay" hidden>
|
|
74
|
+
<aside class="history-drawer" role="dialog" aria-modal="true" aria-labelledby="historyDrawerTitle">
|
|
75
|
+
<div class="history-head">
|
|
76
|
+
<div><span class="section-kicker"><span data-i18n="static.125">当前任务</span></span><h2 id="historyDrawerTitle"><span data-i18n="app.226">历史记录</span></h2><p id="historyDrawerMeta" class="panel-intro"></p></div>
|
|
77
|
+
<button id="historyDrawerClose" class="icon-button" type="button" title="关闭" aria-label="关闭" data-i18n-title="app.230" data-i18n-aria-label="app.230">×</button>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="historyList" class="history-list"></div>
|
|
80
|
+
</aside>
|
|
81
|
+
</div>
|
|
73
82
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
74
83
|
<div id="pairingOverlay" class="pairing-overlay" hidden>
|
|
75
84
|
<section class="pairing-dialog" role="dialog" aria-modal="true" aria-labelledby="pairingDialogTitle" aria-describedby="pairingDialogDescription">
|
package/src/agent-sprites.mjs
CHANGED
|
@@ -52,24 +52,81 @@ function readManifestFile(dir) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 整帧状态动画表(可选)。素材明说「每个状态播哪几帧、多快、眨眼怎么插」,
|
|
57
|
+
* 固件照表播放,不推断素材结构。有这张表时 layers 可以省略。
|
|
58
|
+
* 返回归一化的 { [state]: { frames: [...], ms } } 或 null(没声明)。
|
|
59
|
+
*/
|
|
60
|
+
function validateFramesTable(raw) {
|
|
61
|
+
if (raw.frames === undefined) return null;
|
|
62
|
+
const table = raw.frames;
|
|
63
|
+
if (!table || typeof table !== 'object' || Array.isArray(table)) {
|
|
64
|
+
throw new SpriteError('manifest.frames 必须是对象');
|
|
65
|
+
}
|
|
66
|
+
const allowed = new Set([...MOOD_NAMES, 'blink']);
|
|
67
|
+
const out = {};
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const [state, entry] of Object.entries(table)) {
|
|
70
|
+
if (!allowed.has(state)) throw new SpriteError(`frames 含未知状态:${state}`);
|
|
71
|
+
let files = null;
|
|
72
|
+
const extra = {};
|
|
73
|
+
if (Array.isArray(entry)) {
|
|
74
|
+
files = entry;
|
|
75
|
+
} else if (entry && typeof entry === 'object') {
|
|
76
|
+
const keys = Object.keys(entry);
|
|
77
|
+
if (keys.some((key) => !['frames', 'ms', 'period_ms', 'hold_ms'].includes(key))) {
|
|
78
|
+
throw new SpriteError(`frames.${state} 含不支持的字段`);
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(entry.frames)) throw new SpriteError(`frames.${state}.frames 必须是数组`);
|
|
81
|
+
files = entry.frames;
|
|
82
|
+
if (entry.ms !== undefined && !isInt(entry.ms, 30, 2000)) throw new SpriteError(`frames.${state}.ms 范围 30–2000`);
|
|
83
|
+
if (entry.ms !== undefined) extra.ms = entry.ms;
|
|
84
|
+
if (entry.period_ms !== undefined && !isInt(entry.period_ms, 500, 60000)) throw new SpriteError(`frames.${state}.period_ms 范围 500–60000`);
|
|
85
|
+
if (entry.period_ms !== undefined) extra.period_ms = entry.period_ms;
|
|
86
|
+
if (entry.hold_ms !== undefined && !isInt(entry.hold_ms, 30, 2000)) throw new SpriteError(`frames.${state}.hold_ms 范围 30–2000`);
|
|
87
|
+
if (entry.hold_ms !== undefined) extra.hold_ms = entry.hold_ms;
|
|
88
|
+
} else {
|
|
89
|
+
throw new SpriteError(`frames.${state} 必须是数组或对象`);
|
|
90
|
+
}
|
|
91
|
+
if (files.length < 1 || files.length > MAX_FRAMES ||
|
|
92
|
+
files.some((file) => typeof file !== 'string' || !FILE_NAME.test(file))) {
|
|
93
|
+
throw new SpriteError(`frames.${state} 需要 1–${MAX_FRAMES} 个 PNG 文件名`);
|
|
94
|
+
}
|
|
95
|
+
total += files.length;
|
|
96
|
+
if (total > 12) throw new SpriteError('frames 总帧数超过 12(设备内存上限)');
|
|
97
|
+
out[state] = { frames: [...files], ...extra };
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(out).length < 1 || !out.idle) throw new SpriteError('frames 至少要声明 idle 轨道');
|
|
100
|
+
if (raw.frame_ms !== undefined && !isInt(raw.frame_ms, 30, 2000)) {
|
|
101
|
+
throw new SpriteError('manifest.frame_ms 范围 30–2000');
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
55
106
|
function validateManifest(raw) {
|
|
56
107
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new SpriteError('manifest 必须是对象');
|
|
57
|
-
if (Object.keys(raw).some((key) => !['version', 'canvas', 'layers', 'motion', 'emotions', 'actions', 'commands'].includes(key))) {
|
|
108
|
+
if (Object.keys(raw).some((key) => !['version', 'canvas', 'layers', 'frames', 'frame_ms', 'motion', 'emotions', 'actions', 'commands'].includes(key))) {
|
|
58
109
|
throw new SpriteError('manifest 包含不支持的字段');
|
|
59
110
|
}
|
|
60
111
|
if (!isInt(raw.version, 1, 2147483647)) throw new SpriteError('manifest.version 必须是正整数');
|
|
112
|
+
// 整帧状态动画(声明式):有 frames 表就不再需要 layers。固件只照表播放,
|
|
113
|
+
// 不去推断素材结构;老素材(layers 分层叠加)行为不变。
|
|
114
|
+
const frameTracks = validateFramesTable(raw);
|
|
61
115
|
const canvas = raw.canvas;
|
|
62
116
|
if (!canvas || typeof canvas !== 'object' || Array.isArray(canvas) ||
|
|
63
117
|
!isInt(canvas.w, 1, 512) || !isInt(canvas.h, 1, 512) ||
|
|
64
118
|
Object.keys(canvas).some((key) => !['w', 'h'].includes(key))) {
|
|
65
119
|
throw new SpriteError('manifest.canvas 必须是 {w, h}(各 1–512)');
|
|
66
120
|
}
|
|
67
|
-
|
|
68
|
-
|
|
121
|
+
// 整帧模式(frames 表)不需要 layers;分层模式 layers 必填。
|
|
122
|
+
if (!frameTracks) {
|
|
123
|
+
if (!Array.isArray(raw.layers) || raw.layers.length < 1 || raw.layers.length > MAX_LAYERS) {
|
|
124
|
+
throw new SpriteError(`manifest.layers 需要 1–${MAX_LAYERS} 层`);
|
|
125
|
+
}
|
|
69
126
|
}
|
|
70
127
|
const seen = new Set();
|
|
71
128
|
const referenced = new Set();
|
|
72
|
-
for (const layer of raw.layers) {
|
|
129
|
+
if (Array.isArray(raw.layers)) for (const layer of raw.layers) {
|
|
73
130
|
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) throw new SpriteError('layers 条目必须是对象');
|
|
74
131
|
const keys = Object.keys(layer);
|
|
75
132
|
if (keys.some((key) => !['id', 'file', 'frames', 'x', 'y', 'z', 'blink', 'talk', 'alert'].includes(key))) {
|
|
@@ -225,6 +282,7 @@ function validateManifest(raw) {
|
|
|
225
282
|
return {
|
|
226
283
|
version: raw.version, canvas: canvas, layers: raw.layers, motion: raw.motion ?? null,
|
|
227
284
|
emotions: raw.emotions ?? null, actions: raw.actions ?? null, commands: raw.commands ?? null,
|
|
285
|
+
frames: frameTracks ?? null, frame_ms: raw.frame_ms ?? null,
|
|
228
286
|
};
|
|
229
287
|
}
|
|
230
288
|
|
|
@@ -269,11 +327,19 @@ export function readSprite(packageDir) {
|
|
|
269
327
|
if (action && typeof action.gif === 'string') gifReferencedHint.add(action.gif);
|
|
270
328
|
}
|
|
271
329
|
}
|
|
330
|
+
const declaredNames = new Set();
|
|
331
|
+
if (Array.isArray(manifest.layers)) {
|
|
332
|
+
for (const layer of manifest.layers) {
|
|
333
|
+
for (const file of layer.frames !== undefined ? layer.frames : [layer.file]) declaredNames.add(file);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (manifest.frames) {
|
|
337
|
+
for (const track of Object.values(manifest.frames)) for (const file of track.frames) declaredNames.add(file);
|
|
338
|
+
}
|
|
272
339
|
for (const name of fs.readdirSync(spriteDir)) {
|
|
273
340
|
const lower = name.toLowerCase();
|
|
274
341
|
if (lower.endsWith('.png')) {
|
|
275
|
-
const referenced =
|
|
276
|
-
(layer.frames !== undefined ? layer.frames : [layer.file]).includes(name));
|
|
342
|
+
const referenced = declaredNames.has(name);
|
|
277
343
|
if (!referenced) {
|
|
278
344
|
console.error(`[plugins] sprite 提示:${path.basename(spriteDir)}/${name} 未被 manifest 引用,不会被下发(建议删除旧素材省空间)`);
|
|
279
345
|
}
|
|
@@ -283,8 +349,7 @@ export function readSprite(packageDir) {
|
|
|
283
349
|
}
|
|
284
350
|
let total = 0;
|
|
285
351
|
const files = [];
|
|
286
|
-
for (const
|
|
287
|
-
for (const file of layer.frames !== undefined ? layer.frames : [layer.file]) {
|
|
352
|
+
for (const file of declaredNames) {
|
|
288
353
|
const target = path.join(spriteDir, file);
|
|
289
354
|
let bytes;
|
|
290
355
|
try {
|
|
@@ -297,7 +362,6 @@ export function readSprite(packageDir) {
|
|
|
297
362
|
if (total > SPRITE_MAX_TOTAL_BYTES) throw new SpriteError(`sprite/ 整包超过 512 KB`);
|
|
298
363
|
const data = fs.readFileSync(target);
|
|
299
364
|
files.push({ file, bytes, sha256: createHash('sha256').update(data).digest('hex') });
|
|
300
|
-
}
|
|
301
365
|
}
|
|
302
366
|
// GIF 动作引用的动画文件(多个动作共用同一文件只下发一份)
|
|
303
367
|
if (manifest.actions) {
|
|
@@ -321,6 +385,7 @@ export function readSprite(packageDir) {
|
|
|
321
385
|
const value = Object.freeze({
|
|
322
386
|
dir: spriteDir, version: manifest.version, canvas: manifest.canvas,
|
|
323
387
|
layers: manifest.layers, motion: manifest.motion, emotions: manifest.emotions,
|
|
388
|
+
frames: manifest.frames, frame_ms: manifest.frame_ms,
|
|
324
389
|
actions: manifest.actions, commands: manifest.commands,
|
|
325
390
|
files: Object.freeze(files.map((file) => Object.freeze(file))),
|
|
326
391
|
});
|
package/src/agents/events.mjs
CHANGED
|
@@ -36,19 +36,108 @@ export function streamActivity(event) {
|
|
|
36
36
|
return '';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// 工具调用捕获:从 CLI 原始 JSONL 事件里抽 {name, detail, state},只进桥接器
|
|
40
|
+
// 任务记录(网页端折叠展示),绝不混入设备进度文本。codex 的 item.* 事件与
|
|
41
|
+
// Claude 风格的 tool_call/assistant 事件都覆盖;认不出的形状安静返回 null。
|
|
42
|
+
const TOOL_DETAIL_LIMIT = 600;
|
|
43
|
+
|
|
44
|
+
function truncate(value, limit = TOOL_DETAIL_LIMIT) {
|
|
45
|
+
const text = String(value ?? '').trim();
|
|
46
|
+
return text.length <= limit ? text : `${text.slice(0, limit)}…(截断)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toolDetail(parts) {
|
|
50
|
+
return truncate(parts.filter((part) => String(part || '').trim()).join(' '));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function toolCallFromEvent(event) {
|
|
54
|
+
if (event.type === 'tool_call' && event.tool_call && typeof event.tool_call === 'object') {
|
|
55
|
+
const name = Object.keys(event.tool_call)[0] || '';
|
|
56
|
+
const args = event.tool_call[name];
|
|
57
|
+
return {
|
|
58
|
+
name: truncate(name, 120),
|
|
59
|
+
detail: toolDetail([name, typeof args === 'string' ? args : JSON.stringify(args)]),
|
|
60
|
+
state: event.subtype === 'completed' ? 'completed' : 'started',
|
|
61
|
+
key: '',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (event.type === 'stream_event' && event.event?.type === 'content_block_start' &&
|
|
65
|
+
event.event.content_block?.type === 'tool_use') {
|
|
66
|
+
return {
|
|
67
|
+
name: truncate(event.event.content_block.name, 120),
|
|
68
|
+
detail: toolDetail([event.event.content_block.name,
|
|
69
|
+
JSON.stringify(event.event.content_block.input ?? '')]),
|
|
70
|
+
state: 'started', key: String(event.event.index ?? ''),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
|
|
74
|
+
const block = event.message.content.find((part) => part?.type === 'tool_use');
|
|
75
|
+
if (!block) return null;
|
|
76
|
+
return {
|
|
77
|
+
name: truncate(block.name, 120),
|
|
78
|
+
detail: toolDetail([block.name, JSON.stringify(block.input ?? '')]),
|
|
79
|
+
state: 'started', key: String(block.id ?? ''),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (event.type !== 'item.started' && event.type !== 'item.completed') return null;
|
|
83
|
+
const item = event.item;
|
|
84
|
+
if (!item || typeof item !== 'object') return null;
|
|
85
|
+
const completed = event.type === 'item.completed';
|
|
86
|
+
switch (item.type) {
|
|
87
|
+
case 'command_execution':
|
|
88
|
+
return { name: '命令执行', detail: toolDetail([item.command, item.cwd]), state: completed ? 'completed' : 'started', key: String(item.id ?? '') };
|
|
89
|
+
case 'file_change':
|
|
90
|
+
return {
|
|
91
|
+
name: '文件修改',
|
|
92
|
+
detail: toolDetail(Array.isArray(item.changes)
|
|
93
|
+
? item.changes.map((change) => `${change?.path || ''}${change?.kind ? ` (${change.kind})` : ''}`)
|
|
94
|
+
: []),
|
|
95
|
+
state: completed ? 'completed' : 'started', key: String(item.id ?? ''),
|
|
96
|
+
};
|
|
97
|
+
case 'mcp_tool_call': case 'collab_tool_call':
|
|
98
|
+
return { name: truncate(item.tool || '工具调用', 120), detail: toolDetail([item.tool, JSON.stringify(item.arguments ?? '')]), state: completed ? 'completed' : 'started', key: String(item.id ?? '') };
|
|
99
|
+
case 'web_search':
|
|
100
|
+
return { name: '网页搜索', detail: toolDetail([item.query]), state: completed ? 'completed' : 'started', key: String(item.id ?? '') };
|
|
101
|
+
case 'reasoning': case 'agent_message': case 'todo_list': case 'error':
|
|
102
|
+
return null;
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
39
108
|
// Only categorical activity is emitted by builtin adapters; raw tool arguments
|
|
40
|
-
// and reasoning never become the device progress text.
|
|
109
|
+
// and reasoning never become the device progress text. Tool calls are drained
|
|
110
|
+
// separately (takeToolCalls) for the bridge UI only.
|
|
41
111
|
export function jsonLineEvents(interpret, onProgress = () => {}, onSession = () => {}) {
|
|
42
112
|
let pending = '';
|
|
43
113
|
let rawTail = '';
|
|
44
114
|
let discardingOversized = false;
|
|
45
115
|
let sawEvent = false;
|
|
46
116
|
const state = { lastMessage: '', assistantDeltas: '', finalResult: '', terminalError: '' };
|
|
117
|
+
// 半开工具调用:key → 在 calls 里的引用。completed 事件来时把对应条目置为
|
|
118
|
+
// 完成;对不上号(CLI 没给 id 或乱序)就当作独立条目追加,宁可多一行。
|
|
119
|
+
const open = new Map();
|
|
120
|
+
const calls = [];
|
|
121
|
+
const record = (call) => {
|
|
122
|
+
if (!call || !call.name) return;
|
|
123
|
+
if (call.state === 'completed' && call.key && open.has(call.key)) {
|
|
124
|
+
const entry = open.get(call.key);
|
|
125
|
+
entry.state = 'completed';
|
|
126
|
+
if (call.detail && call.detail.length > entry.detail.length) entry.detail = call.detail;
|
|
127
|
+
open.delete(call.key);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const entry = { name: call.name, detail: call.detail || '', state: call.state, at: new Date().toISOString() };
|
|
131
|
+
calls.push(entry);
|
|
132
|
+
if (calls.length > 80) calls.splice(0, calls.length - 80);
|
|
133
|
+
if (entry.state === 'started' && call.key) open.set(call.key, entry);
|
|
134
|
+
};
|
|
47
135
|
const consume = (line) => {
|
|
48
136
|
let event;
|
|
49
137
|
try { event = JSON.parse(line); } catch { return; }
|
|
50
138
|
if (!event || typeof event !== 'object' || typeof event.type !== 'string') return;
|
|
51
139
|
sawEvent = true;
|
|
140
|
+
record(toolCallFromEvent(event));
|
|
52
141
|
const update = interpret(event, state) || {};
|
|
53
142
|
if (typeof update.sessionId === 'string') onSession(update.sessionId);
|
|
54
143
|
if (update.activity) onProgress(update.activity);
|
|
@@ -69,12 +158,17 @@ export function jsonLineEvents(interpret, onProgress = () => {}, onSession = ()
|
|
|
69
158
|
},
|
|
70
159
|
finish() {
|
|
71
160
|
if (!discardingOversized && pending.trim()) consume(pending);
|
|
161
|
+
for (const entry of open.values()) entry.state = 'completed';
|
|
162
|
+
open.clear();
|
|
72
163
|
return {
|
|
73
164
|
result: (state.finalResult || state.lastMessage || (sawEvent ? '任务已执行,未返回文字摘要。' : rawTail.trim())).slice(-8000),
|
|
74
165
|
hasAssistantText: Boolean(state.finalResult || state.lastMessage || !sawEvent),
|
|
75
166
|
error: state.terminalError, rawTail,
|
|
76
167
|
};
|
|
77
168
|
},
|
|
169
|
+
takeToolCalls() {
|
|
170
|
+
return calls.splice(0, calls.length);
|
|
171
|
+
},
|
|
78
172
|
};
|
|
79
173
|
}
|
|
80
174
|
|
package/src/agents/shared.mjs
CHANGED
|
@@ -109,7 +109,7 @@ function changeFooter(state, message) {
|
|
|
109
109
|
return state.changed ? `${message('改动文件:')}\n${state.changed}` : message('没有检测到代码改动。');
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export function runCli({ job, project, signal, onProgress, onSession, config, model }, adapter) {
|
|
112
|
+
export function runCli({ job, project, signal, onProgress, onSession, onTool, config, model }, adapter) {
|
|
113
113
|
const startingGitSnapshot = repositorySnapshot(project.path);
|
|
114
114
|
const message = (value) => translateKnownMessage(value, config.locale);
|
|
115
115
|
// 每轮都拼上权限边界:用户原文一字不改,边界文案按界面语言翻译。
|
|
@@ -139,8 +139,10 @@ export function runCli({ job, project, signal, onProgress, onSession, config, mo
|
|
|
139
139
|
let timer;
|
|
140
140
|
// 卡死看门狗:编程工具进程连续 idleMs 没有任何输出(进度、流式、stderr 都算)
|
|
141
141
|
// 视为卡死,杀掉进程树并把错误标记为可自动重跑,由任务车道决定是否重试。
|
|
142
|
+
// 默认跟 config.mjs 对齐到 15 分钟:CLI 工具的 stdout/stderr 都算活动,但素材
|
|
143
|
+
// 管线里外部生图是异步轮询,单张上限 300s 且期间不吐字,5 分钟会误杀。
|
|
142
144
|
const idleMs = Number(config?.stuckIdleMs)
|
|
143
|
-
|| Math.max(1, Number(config?.stuckIdleMinutes) ||
|
|
145
|
+
|| Math.max(1, Number(config?.stuckIdleMinutes) || 15) * 60 * 1000;
|
|
144
146
|
let idleTimer;
|
|
145
147
|
const bumpIdle = () => {
|
|
146
148
|
clearTimeout(idleTimer);
|
|
@@ -189,6 +191,9 @@ export function runCli({ job, project, signal, onProgress, onSession, config, mo
|
|
|
189
191
|
child.stdout.on('data', (chunk) => {
|
|
190
192
|
bumpIdle();
|
|
191
193
|
events.write(chunk);
|
|
194
|
+
// 工具调用只走 onTool(桥接器任务记录,网页端折叠展示),不进设备进度文本。
|
|
195
|
+
const forwarded = events.takeToolCalls?.() || [];
|
|
196
|
+
for (const call of forwarded) onTool?.(call);
|
|
192
197
|
});
|
|
193
198
|
child.stderr.on('data', (chunk) => {
|
|
194
199
|
bumpIdle();
|
package/src/config.mjs
CHANGED
|
@@ -128,9 +128,11 @@ export function loadConfig(env = process.env) {
|
|
|
128
128
|
};
|
|
129
129
|
});
|
|
130
130
|
const defaultProject = raw.defaultProject || projects[0].id;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// 默认编程助手只是可选偏好(未指定 provider 时兜底),不再是必须存在的"主力":
|
|
132
|
+
// 可以留空——没有助手享有特权,入口处没指定就明确报错让用户选。
|
|
133
|
+
const defaultProvider = typeof raw.defaultProvider === 'string' && PLUGIN_ID.test(raw.defaultProvider) ? raw.defaultProvider : '';
|
|
134
|
+
if (!seen.has(defaultProject)) {
|
|
135
|
+
throw new Error('默认项目不在配置中');
|
|
134
136
|
}
|
|
135
137
|
const port = Number(raw.port || 8787);
|
|
136
138
|
const devicePort = Number(raw.devicePort || 8788);
|
|
@@ -147,7 +149,11 @@ export function loadConfig(env = process.env) {
|
|
|
147
149
|
}
|
|
148
150
|
// 卡死自愈:任务连续 N 分钟没有任何输出(进度/流式/stderr 都算)视为卡死,
|
|
149
151
|
// 桥接器杀掉编程工具进程并自动重跑,最多 maxAutoRetries 次。
|
|
150
|
-
|
|
152
|
+
// 默认 15 分钟而不是 5 分钟:素材管线里一次外部生图是「提交异步任务 + 每 6s
|
|
153
|
+
// 轮询」,单张最长 300s(body 240s + sheet 300s ≈ 9 分钟),且轮询期间工具
|
|
154
|
+
// 不吐字。5 分钟会把一条正在正常跑的长任务当成卡死杀掉(2026-10-24 雪球
|
|
155
|
+
// jojo 任务被自己的看门狗反复打断实锤)。
|
|
156
|
+
const stuckIdleMinutes = Number(env.VIBE_STUCK_IDLE_MINUTES ?? raw.stuckIdleMinutes ?? 15);
|
|
151
157
|
if (!Number.isInteger(stuckIdleMinutes) || stuckIdleMinutes < 1 || stuckIdleMinutes > 60) {
|
|
152
158
|
throw new Error('卡死判定时长必须是 1 到 60 之间的整数(分钟)');
|
|
153
159
|
}
|
package/src/i18n.mjs
CHANGED
|
@@ -59,8 +59,9 @@ const messages = new Map([
|
|
|
59
59
|
['插件状态检查必须同步返回', 'The plugin status check must return synchronously'],
|
|
60
60
|
['任务已取消', 'Task cancelled'],
|
|
61
61
|
['默认编程助手未安装或已禁用', 'The default assistant is not installed or is disabled'],
|
|
62
|
-
['
|
|
63
|
-
['
|
|
62
|
+
['请先选择编程助手', 'Choose an assistant first'],
|
|
63
|
+
['请先在电脑桥接器里选择编程助手', 'Choose an assistant in the desktop bridge first'],
|
|
64
|
+
['请指定编程助手(createTask 需要显式 agentId)', 'Specify an assistant (createTask requires an explicit agentId)'],
|
|
64
65
|
['配对请求格式无效', 'Invalid pairing request'],
|
|
65
66
|
['设备名称无效', 'Invalid device name'],
|
|
66
67
|
['电脑配对窗口未开启', 'Pairing is not open on the computer'],
|
package/src/installed.mjs
CHANGED
|
@@ -189,11 +189,12 @@ export async function setPluginEnabled(config, id, enabled) {
|
|
|
189
189
|
if (!PLUGIN_ID.test(id || '')) throw fail('插件不存在');
|
|
190
190
|
const installed = (await listInstalled(config)).plugins;
|
|
191
191
|
if (!installed.some((plugin) => plugin.id === id)) throw fail('插件不存在');
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!enabled && id ===
|
|
196
|
-
|
|
192
|
+
// 默认助手只是可选偏好,不再是"主力":停用它时直接清掉默认指向,不拦人。
|
|
193
|
+
const previousDefault = config.defaultProvider || '';
|
|
194
|
+
let defaultCleared = false;
|
|
195
|
+
if (!enabled && id === previousDefault) {
|
|
196
|
+
config.defaultProvider = '';
|
|
197
|
+
defaultCleared = true;
|
|
197
198
|
}
|
|
198
199
|
const previous = [...(config.disabledPlugins || [])];
|
|
199
200
|
const next = new Set(previous);
|
|
@@ -204,12 +205,17 @@ export async function setPluginEnabled(config, id, enabled) {
|
|
|
204
205
|
await reload(config);
|
|
205
206
|
} catch (error) {
|
|
206
207
|
config.disabledPlugins = previous;
|
|
208
|
+
if (defaultCleared) config.defaultProvider = previousDefault;
|
|
207
209
|
throw error;
|
|
208
210
|
}
|
|
209
211
|
try {
|
|
210
|
-
writeConfig(config, (raw) => {
|
|
212
|
+
writeConfig(config, (raw) => {
|
|
213
|
+
raw.disabledPlugins = config.disabledPlugins;
|
|
214
|
+
if (defaultCleared) raw.defaultProvider = '';
|
|
215
|
+
});
|
|
211
216
|
} catch (error) {
|
|
212
217
|
config.disabledPlugins = previous;
|
|
218
|
+
if (defaultCleared) config.defaultProvider = previousDefault;
|
|
213
219
|
await reload(config);
|
|
214
220
|
throw error;
|
|
215
221
|
}
|
|
@@ -252,11 +258,10 @@ export async function removePlugin(config, { id = '', file = '' } = {}) {
|
|
|
252
258
|
target = removalTarget(config, target);
|
|
253
259
|
const described = await describeModule(target, config);
|
|
254
260
|
const ids = described.map((plugin) => plugin.id).filter((item) => PLUGIN_ID.test(item));
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
261
|
+
// 默认助手只是可选偏好,不再是"主力":删的就是默认助手时清掉指向,不拦人。
|
|
262
|
+
const previousDefault = config.defaultProvider || '';
|
|
263
|
+
const removesDefault = ids.includes(previousDefault);
|
|
264
|
+
if (removesDefault) config.defaultProvider = '';
|
|
260
265
|
const previousDisabled = [...(config.disabledPlugins || [])];
|
|
261
266
|
const previousPlugins = [...(config.plugins || [])];
|
|
262
267
|
config.disabledPlugins = [...new Set([...previousDisabled, ...ids])];
|
|
@@ -266,10 +271,12 @@ export async function removePlugin(config, { id = '', file = '' } = {}) {
|
|
|
266
271
|
writeConfig(config, (raw) => {
|
|
267
272
|
raw.disabledPlugins = config.disabledPlugins;
|
|
268
273
|
if (Array.isArray(raw.plugins)) raw.plugins = config.plugins;
|
|
274
|
+
if (removesDefault) raw.defaultProvider = '';
|
|
269
275
|
});
|
|
270
276
|
} catch (error) {
|
|
271
277
|
config.disabledPlugins = previousDisabled;
|
|
272
278
|
config.plugins = previousPlugins;
|
|
279
|
+
if (removesDefault) config.defaultProvider = previousDefault;
|
|
273
280
|
await reload(config).catch(() => {});
|
|
274
281
|
throw error;
|
|
275
282
|
}
|
package/src/jobs.mjs
CHANGED
|
@@ -224,7 +224,9 @@ export class JobStore {
|
|
|
224
224
|
projectFor(job) {
|
|
225
225
|
const configured = this.projects.find((item) => item.id === job.projectId);
|
|
226
226
|
const dir = this.workspacePath(job.vibeSessionId);
|
|
227
|
-
|
|
227
|
+
// 每个会话跑在自己的沙箱目录里,但把选定项目的真实路径一并带给插件
|
|
228
|
+
// (originPath):助手类插件可以只读地回看真正的代码,写入仍留在沙箱。
|
|
229
|
+
return dir ? { ...configured, path: dir, originPath: configured?.path || '' } : configured;
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
createSession(payload) {
|
|
@@ -381,6 +383,34 @@ export class JobStore {
|
|
|
381
383
|
job.updatedAt = now();
|
|
382
384
|
this.save();
|
|
383
385
|
this.hooks?.notify('started', job);
|
|
386
|
+
// —— 插件型助手的卡死看门狗 ——
|
|
387
|
+
// CLI 型插件由 agents/shared.mjs 的子进程 idle 看门狗覆盖;插件型(自带
|
|
388
|
+
// run() 与工具环的助手)此前完全没有超时:模型请求/工具调用不返回就永久
|
|
389
|
+
// 挂着,任务永远停在 running(2026-10-24 雪球 jojo 任务挂 15 分钟零输出
|
|
390
|
+
// 零产物实锤)。这里按「多久没有任何进展」判定,中止后交给车道自动重跑。
|
|
391
|
+
// 阈值必须大于素材管线的合法最长静默时长:外部生图是异步任务轮询,单张
|
|
392
|
+
// 上限 300s(body 240s + sheet 300s ≈ 9 分钟),期间完全没有文本进度。
|
|
393
|
+
// 默认 15 分钟(见 config.mjs)。
|
|
394
|
+
let lastActivity = Date.now();
|
|
395
|
+
const touchActivity = () => { lastActivity = Date.now(); };
|
|
396
|
+
const stallMs = Math.max(1, Number(this.stuckIdleMinutes) || 15) * 60_000;
|
|
397
|
+
let stalled = false;
|
|
398
|
+
const stallTimer = setInterval(() => {
|
|
399
|
+
if (job.status !== 'running') return;
|
|
400
|
+
const idle = Date.now() - lastActivity;
|
|
401
|
+
if (idle < stallMs) return;
|
|
402
|
+
stalled = true;
|
|
403
|
+
controller.abort(new Error(`任务 ${Math.round(idle / 60_000)} 分钟没有任何进展,疑似卡死`));
|
|
404
|
+
}, 15_000);
|
|
405
|
+
if (stallTimer.unref) stallTimer.unref();
|
|
406
|
+
// 单轮硬上限 30 分钟:素材管线一次从零生成(body + sheet)最坏约 10 分钟,
|
|
407
|
+
// 加上模型侧重试仍有余量。
|
|
408
|
+
const hardTimer = setTimeout(() => {
|
|
409
|
+
if (job.status !== 'running') return;
|
|
410
|
+
stalled = true;
|
|
411
|
+
controller.abort(new Error('任务单轮运行超过 30 分钟,已停止'));
|
|
412
|
+
}, 30 * 60_000);
|
|
413
|
+
if (hardTimer.unref) hardTimer.unref();
|
|
384
414
|
try {
|
|
385
415
|
this.ensureWorkspace(job.vibeSessionId);
|
|
386
416
|
const project = this.projectFor(job);
|
|
@@ -393,6 +423,7 @@ export class JobStore {
|
|
|
393
423
|
if (!progress || progress === job.progress) return;
|
|
394
424
|
job.progress = progress;
|
|
395
425
|
job.progressAt = now();
|
|
426
|
+
touchActivity();
|
|
396
427
|
job.updatedAt = job.progressAt;
|
|
397
428
|
const current = Date.now();
|
|
398
429
|
if (current - (this.progressSavedAt.get(job.id) || 0) >= 750) {
|
|
@@ -411,6 +442,34 @@ export class JobStore {
|
|
|
411
442
|
job.updatedAt = now();
|
|
412
443
|
this.save();
|
|
413
444
|
}
|
|
445
|
+
}, (tool) => {
|
|
446
|
+
// 工具调用轨迹:仅存任务记录给桥接器网页端折叠展示,设备端
|
|
447
|
+
// /device/tasks 是字段白名单,天然带不出去。
|
|
448
|
+
if (job.status !== 'running') return;
|
|
449
|
+
const name = String(tool?.name || '').trim();
|
|
450
|
+
if (!name) return;
|
|
451
|
+
const entry = {
|
|
452
|
+
name: name.slice(0, 120),
|
|
453
|
+
detail: String(tool?.detail || '').slice(0, 600),
|
|
454
|
+
state: tool?.state === 'completed' ? 'completed' : 'started',
|
|
455
|
+
at: now(),
|
|
456
|
+
};
|
|
457
|
+
const calls = Array.isArray(job.toolCalls) ? job.toolCalls : [];
|
|
458
|
+
// 同名同参数的 completed 覆盖前一条 started,避免每个工具占两行。
|
|
459
|
+
const previous = calls.length - 1 >= 0 ? calls[calls.length - 1] : null;
|
|
460
|
+
if (previous && previous.state === 'started' && previous.name === entry.name &&
|
|
461
|
+
previous.detail === entry.detail) {
|
|
462
|
+
previous.state = 'completed';
|
|
463
|
+
} else {
|
|
464
|
+
calls.push(entry);
|
|
465
|
+
}
|
|
466
|
+
job.toolCalls = calls.slice(-40);
|
|
467
|
+
touchActivity();
|
|
468
|
+
const current = Date.now();
|
|
469
|
+
if (current - (this.progressSavedAt.get(job.id) || 0) >= 750) {
|
|
470
|
+
this.progressSavedAt.set(job.id, current);
|
|
471
|
+
this.save();
|
|
472
|
+
}
|
|
414
473
|
});
|
|
415
474
|
if (job.status === 'running') {
|
|
416
475
|
if (!outcome || !['completed', 'handed_off'].includes(outcome.status || 'completed')) {
|
|
@@ -432,6 +491,13 @@ export class JobStore {
|
|
|
432
491
|
this.hooks?.notify(job.status, job);
|
|
433
492
|
}
|
|
434
493
|
} catch (error) {
|
|
494
|
+
if (stalled && job.status === 'running') {
|
|
495
|
+
// 看门狗中止(插件型助手):按 CLI 型同样的语义交给车道自动重跑。
|
|
496
|
+
error = Object.assign(
|
|
497
|
+
new Error(`任务 ${Math.round(stallMs / 60_000)} 分钟没有任何进展,疑似卡死:已终止`),
|
|
498
|
+
{ retryable: true, autoRetry: true },
|
|
499
|
+
);
|
|
500
|
+
}
|
|
435
501
|
if (job.status === 'running') {
|
|
436
502
|
// 卡死自愈:看门狗杀掉无输出的编程工具进程后,同车道自动重跑,
|
|
437
503
|
// 最多 maxAutoRetries 次;重跑的是同一条指令(native 会话会 resume 同一 CLI 线程)。
|
|
@@ -460,6 +526,8 @@ export class JobStore {
|
|
|
460
526
|
this.hooks?.notify('failed', job);
|
|
461
527
|
}
|
|
462
528
|
} finally {
|
|
529
|
+
clearInterval(stallTimer);
|
|
530
|
+
clearTimeout(hardTimer);
|
|
463
531
|
this.controllers.delete(job.id);
|
|
464
532
|
this.progressSavedAt.delete(job.id);
|
|
465
533
|
}
|
package/src/plugins/enhance.mjs
CHANGED
|
@@ -91,6 +91,7 @@ export function createEnhancement(config) {
|
|
|
91
91
|
if (!wired.jobs) throw new Error('桥接器尚未就绪');
|
|
92
92
|
createTaskGuard();
|
|
93
93
|
const provider = agentId || config.defaultProvider;
|
|
94
|
+
if (!provider) throw new Error('请指定编程助手(createTask 需要显式 agentId)');
|
|
94
95
|
const project = projectId || wired.jobs.defaultProject;
|
|
95
96
|
const session = wired.jobs.createSession({ provider, projectId: project, title });
|
|
96
97
|
const job = wired.jobs.submit({ instruction, sessionId: session.id, provider, projectId: project, source: 'plugin' });
|
|
@@ -92,6 +92,14 @@ export async function reloadConfiguredPlugins(config) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const RELOAD_DELAY_MS = 300;
|
|
95
|
+
// 热加载单次超时:reloadConfiguredPlugins 一旦挂起(插件 import 死锁/网络
|
|
96
|
+
// 等待),reloading 会永远停在 true,之后所有变更只排队、不再加载也
|
|
97
|
+
// 不打日志——表现为「桥接器对素材改动完全无反应」(2026-10-24 实锤)。
|
|
98
|
+
// 超时后强制解锁并记账,下一轮变更还能再试。
|
|
99
|
+
const RELOAD_TIMEOUT_MS = 30000;
|
|
100
|
+
// fs.watch 在 macOS 长跑进程里会静默失效(事件流断掉,无错误、无回调)。
|
|
101
|
+
// 轮询兜底:按 mtime 快照比对,watch 哑了也能热加载。
|
|
102
|
+
const POLL_INTERVAL_MS = 3000;
|
|
95
103
|
|
|
96
104
|
function relevantChange(filename) {
|
|
97
105
|
if (!filename) return true;
|
|
@@ -137,6 +145,12 @@ export function watchPlugins(config, { onReload } = {}) {
|
|
|
137
145
|
if (closed) return;
|
|
138
146
|
if (reloading) { pending = true; return; }
|
|
139
147
|
reloading = true;
|
|
148
|
+
let timedOut = false;
|
|
149
|
+
const guard = setTimeout(() => {
|
|
150
|
+
timedOut = true;
|
|
151
|
+
console.error(`[plugins] 热加载超过 ${RELOAD_TIMEOUT_MS}ms 未返回,强制解锁`
|
|
152
|
+
+ '(保留上一份插件;下一轮变更会再试)');
|
|
153
|
+
}, RELOAD_TIMEOUT_MS);
|
|
140
154
|
try {
|
|
141
155
|
await reloadConfiguredPlugins(config);
|
|
142
156
|
const ids = config.pluginsRuntime?.agents?.().map((plugin) => plugin.id) ?? [];
|
|
@@ -146,7 +160,11 @@ export function watchPlugins(config, { onReload } = {}) {
|
|
|
146
160
|
console.error(`[plugins] 热加载失败,继续使用上一份:${error.message}`);
|
|
147
161
|
onReload?.(error);
|
|
148
162
|
} finally {
|
|
163
|
+
clearTimeout(guard);
|
|
149
164
|
reloading = false;
|
|
165
|
+
// 无论成败都刷新快照:否则轮询兜底会把同一次改动再触发一遍
|
|
166
|
+
// (fs.watch 与轮询双通道各来一次)。
|
|
167
|
+
snapshotMtimes();
|
|
150
168
|
if (pending && !closed) {
|
|
151
169
|
pending = false;
|
|
152
170
|
schedule();
|
|
@@ -173,13 +191,58 @@ export function watchPlugins(config, { onReload } = {}) {
|
|
|
173
191
|
arm(filename, false);
|
|
174
192
|
arm(dir, false);
|
|
175
193
|
}
|
|
176
|
-
|
|
194
|
+
// ---- 轮询兜底(fs.watch 静默失效时仍能热加载)----
|
|
195
|
+
const mtimes = new Map();
|
|
196
|
+
|
|
197
|
+
function collect(dir, depth, out) {
|
|
198
|
+
let entries;
|
|
199
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (entry.isDirectory()) {
|
|
202
|
+
if (depth <= 0 || entry.name === 'node_modules') continue;
|
|
203
|
+
collect(path.join(dir, entry.name), depth - 1, out);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!relevantChange(entry.name)) continue;
|
|
207
|
+
const full = path.join(dir, entry.name);
|
|
208
|
+
try { out.set(full, fs.statSync(full).mtimeMs); } catch { /* 文件刚被删。 */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function scan() {
|
|
213
|
+
const out = new Map();
|
|
214
|
+
for (const root of pluginWatchRoots(config)) collect(root, 4, out);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function snapshotMtimes() {
|
|
219
|
+
mtimes.clear();
|
|
220
|
+
for (const [file, ms] of scan()) mtimes.set(file, ms);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function poll() {
|
|
224
|
+
if (closed || reloading) return;
|
|
225
|
+
const current = scan();
|
|
226
|
+
let changed = false;
|
|
227
|
+
for (const [file, ms] of current) {
|
|
228
|
+
if (mtimes.get(file) !== ms) { changed = true; break; }
|
|
229
|
+
}
|
|
230
|
+
if (!changed && current.size !== mtimes.size) changed = true;
|
|
231
|
+
mtimes.clear();
|
|
232
|
+
for (const [file, ms] of current) mtimes.set(file, ms);
|
|
233
|
+
if (changed && mtimes.size) schedule();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
snapshotMtimes();
|
|
237
|
+
const pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
|
238
|
+
if (watchers.length) console.error('[plugins] 正在监视插件变更(含轮询兜底)');
|
|
177
239
|
|
|
178
240
|
return {
|
|
179
241
|
close() {
|
|
180
242
|
closed = true;
|
|
181
243
|
if (timer) clearTimeout(timer);
|
|
182
244
|
timer = null;
|
|
245
|
+
clearInterval(pollTimer);
|
|
183
246
|
for (const watcher of watchers) {
|
|
184
247
|
try { watcher.close(); } catch { /* 已经关掉。 */ }
|
|
185
248
|
}
|
package/src/plugins/registry.mjs
CHANGED
|
@@ -156,6 +156,7 @@ export async function loadConfiguredPlugins(config) {
|
|
|
156
156
|
await loadFile(filename, label);
|
|
157
157
|
}
|
|
158
158
|
const registry = createPluginRegistry(plugins);
|
|
159
|
-
|
|
159
|
+
// 默认助手是可选偏好:配置了才校验存在性;留空表示没有兜底助手。
|
|
160
|
+
if (config.defaultProvider && !registry.hasAgent(config.defaultProvider)) throw new Error('默认编程助手未安装或已禁用');
|
|
160
161
|
return registry;
|
|
161
162
|
}
|
package/src/providers.mjs
CHANGED
|
@@ -60,7 +60,7 @@ export function providerStates(config, locale = config.locale) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function createRunner(config) {
|
|
63
|
-
const runner = async (job, project, signal, onProgress = () => {}, onSession = () => {}) => {
|
|
63
|
+
const runner = async (job, project, signal, onProgress = () => {}, onSession = () => {}, onTool = () => {}) => {
|
|
64
64
|
const plugin = agentFor(config, job.provider);
|
|
65
65
|
if (!plugin) throw new Error('编程工具插件未加载,请先安装或启用对应插件');
|
|
66
66
|
if (signal.aborted) throw new Error('任务已取消');
|
|
@@ -71,6 +71,9 @@ export function createRunner(config) {
|
|
|
71
71
|
job, project, signal, config, model,
|
|
72
72
|
onProgress: plugin.capabilities.progress ? onProgress : () => {},
|
|
73
73
|
onSession: plugin.capabilities.session === 'native' ? onSession : () => {},
|
|
74
|
+
// 工具调用轨迹(仅桥接器网页端展示):CLI 型插件的 runCli 会自动消费,
|
|
75
|
+
// 不认识该字段的旧插件安全忽略。
|
|
76
|
+
onTool,
|
|
74
77
|
});
|
|
75
78
|
};
|
|
76
79
|
runner.agent = (id) => agentFor(config, id);
|
package/src/server.mjs
CHANGED
|
@@ -251,7 +251,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
251
251
|
const publicTunnel = (value = tunnel.summary()) => localizeStatus(value, config.locale);
|
|
252
252
|
if (!config.pluginsRuntime && (config.plugins?.length || config.disabledPlugins?.length)) throw new Error('请先 await loadConfiguredPlugins(config) 并赋值 config.pluginsRuntime');
|
|
253
253
|
config.pluginsRuntime ||= createPluginRegistry();
|
|
254
|
-
if (!config.pluginsRuntime.hasAgent(config.defaultProvider)) throw new Error('默认编程助手未安装或已禁用');
|
|
254
|
+
if (config.defaultProvider && !config.pluginsRuntime.hasAgent(config.defaultProvider)) throw new Error('默认编程助手未安装或已禁用');
|
|
255
255
|
if (!config.speech) initializeSpeechSettings(config);
|
|
256
256
|
config.models = { ...Object.fromEntries(config.pluginsRuntime.agents().filter(p => p.capabilities.model).map(p => [p.id, p.model?.default || ''])), ...(config.codexModel ? {codex: config.codexModel} : {}), ...config.models };
|
|
257
257
|
validateModelSettings(Object.fromEntries(Object.entries(publicModels(config)).filter(([, value]) => value !== null)), config.pluginsRuntime);
|
|
@@ -909,6 +909,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
909
909
|
}
|
|
910
910
|
if (req.method === 'POST' && url.pathname === '/device/sessions') {
|
|
911
911
|
const provider = url.searchParams.get('provider') || config.defaultProvider;
|
|
912
|
+
if (!provider) throw new Error('请先在电脑桥接器里选择编程助手');
|
|
912
913
|
const projectId = url.searchParams.get('projectId') || config.defaultProject;
|
|
913
914
|
if (!config.pluginsRuntime.hasAgent(provider)) throw new Error('未知编程工具');
|
|
914
915
|
const payload = await body(req);
|
|
@@ -966,6 +967,9 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
966
967
|
deviceSend(res, 200, {
|
|
967
968
|
version: plugin.sprite.version, canvas: plugin.sprite.canvas,
|
|
968
969
|
layers: plugin.sprite.layers, ...(plugin.sprite.motion ? { motion: plugin.sprite.motion } : {}),
|
|
970
|
+
// 整帧状态动画(声明式,固件照表播);老素材没有这两项,照旧走分层叠加
|
|
971
|
+
...(plugin.sprite.frames ? { frames: plugin.sprite.frames } : {}),
|
|
972
|
+
...(plugin.sprite.frame_ms ? { frame_ms: plugin.sprite.frame_ms } : {}),
|
|
969
973
|
...(plugin.sprite.emotions ? { emotions: plugin.sprite.emotions } : {}),
|
|
970
974
|
...(plugin.sprite.actions ? { actions: plugin.sprite.actions } : {}),
|
|
971
975
|
...(plugin.sprite.commands ? { commands: plugin.sprite.commands } : {}),
|
|
@@ -1045,7 +1049,10 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
1045
1049
|
}
|
|
1046
1050
|
const match = url.pathname.match(/^\/device\/tasks\/([a-z0-9-]+)\/(confirm|cancel)$/);
|
|
1047
1051
|
if (req.method === 'POST' && match) {
|
|
1048
|
-
|
|
1052
|
+
const confirmed = publicJob(match[2] === 'confirm' ? store.confirm(match[1]) : store.cancel(match[1]), deviceLocale);
|
|
1053
|
+
// 工具调用轨迹只给桥接器网页端,任何设备回包都不携带。
|
|
1054
|
+
const { toolCalls, ...deviceJob } = confirmed;
|
|
1055
|
+
deviceSend(res, 200, deviceJob);
|
|
1049
1056
|
return;
|
|
1050
1057
|
}
|
|
1051
1058
|
// 板端截屏回传:固件把当前画面以 RGB565 裸帧 POST 上来(tools/shot.mjs 触发)。
|
|
@@ -1147,6 +1154,15 @@ if (isEntry) {
|
|
|
1147
1154
|
process.once('SIGINT', shutdown);
|
|
1148
1155
|
process.once('SIGTERM', shutdown);
|
|
1149
1156
|
process.once('exit', () => { try { app.tunnel.process?.kill('SIGTERM'); } catch { /* Already exited. */ } });
|
|
1157
|
+
// 无声退出兜底(2026-10-20):桥接器在设备锁屏反复断链场景会无栈消失
|
|
1158
|
+
// (一天三次,2min~25min 不等),未捕获异常/拒绝会静默杀进程。桥接器
|
|
1159
|
+
// 是常驻服务——记录后继续跑,状态机脏了也强过整个掉线。
|
|
1160
|
+
process.on('uncaughtException', (error) => {
|
|
1161
|
+
console.error(`[fatal] uncaughtException(继续运行):${error?.stack || error}`);
|
|
1162
|
+
});
|
|
1163
|
+
process.on('unhandledRejection', (reason) => {
|
|
1164
|
+
console.error(`[fatal] unhandledRejection(继续运行):${reason?.stack || reason}`);
|
|
1165
|
+
});
|
|
1150
1166
|
app.listen().catch((error) => {
|
|
1151
1167
|
console.error(error.message);
|
|
1152
1168
|
process.exitCode = 1;
|
package/src/sessions.mjs
CHANGED
|
@@ -65,6 +65,7 @@ export class SessionStore {
|
|
|
65
65
|
|
|
66
66
|
create({ provider, projectId, title, legacy = false, cliSessionId = '' } = {}) {
|
|
67
67
|
const selectedProvider = provider || this.defaultProvider;
|
|
68
|
+
if (!selectedProvider) throw new Error('请先选择编程助手');
|
|
68
69
|
const selectedProject = projectId || this.defaultProject;
|
|
69
70
|
const plugin = this.agent(selectedProvider);
|
|
70
71
|
if (!plugin && !(legacy && PROVIDER_ID.test(selectedProvider))) throw new Error('不支持的编程工具');
|