made-bridge 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/public/app.js +85 -2
- package/public/i18n-static.js +23 -0
- package/public/index.html +16 -0
- package/src/agent-sprites.mjs +124 -9
- package/src/agents/events.mjs +4 -0
- package/src/agents/shared.mjs +8 -5
- package/src/agents/toolbox.mjs +15 -0
- package/src/board-link.mjs +13 -0
- package/src/config.mjs +8 -5
- package/src/firmware-store.mjs +166 -0
- package/src/i18n.mjs +2 -0
- package/src/installed.mjs +14 -2
- package/src/jobs.mjs +93 -7
- package/src/plugin-package.mjs +13 -3
- package/src/plugins/contracts.d.ts +44 -0
- package/src/plugins/enhance.mjs +5 -1
- package/src/plugins/hot-reload.mjs +189 -0
- package/src/plugins/registry.mjs +8 -4
- package/src/providers.mjs +27 -22
- package/src/server.mjs +113 -6
- package/src/sessions.mjs +1 -2
- package/src/usb-receiver.mjs +74 -26
- package/src/agents/codex.mjs +0 -94
- package/src/agents/cursor.mjs +0 -31
- package/src/agents/index.mjs +0 -7
- package/src/agents/qoder.mjs +0 -34
- package/src/agents/workbuddy.mjs +0 -121
package/src/server.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { loadConfig, packageRoots, ROOT, ensureUserConfig } from './config.mjs';
|
|
11
11
|
import { JobStore } from './jobs.mjs';
|
|
12
12
|
import { VIBE_SESSION_ID } from './sessions.mjs';
|
|
13
|
-
import { createRunner, providerStates
|
|
13
|
+
import { createRunner, providerStates } from './providers.mjs';
|
|
14
14
|
import { loadLanguageSettings, saveLanguageSettings, validateLocale } from './language-settings.mjs';
|
|
15
15
|
import { translateKnownMessage, localizeStatus } from './i18n.mjs';
|
|
16
16
|
import { transcribe as transcribeVoice, transcriptionState, validateVoiceWav } from './transcribe.mjs';
|
|
@@ -22,11 +22,13 @@ import { TunnelManager } from './tunnel.mjs';
|
|
|
22
22
|
import { UsbReceiver, USB_DIRECT_AUTHORITY, USB_RECEIVER_AUTHORITY } from './usb-receiver.mjs';
|
|
23
23
|
|
|
24
24
|
import { createPluginRegistry, loadConfiguredPlugins } from './plugins/registry.mjs';
|
|
25
|
+
import { watchPlugins } from './plugins/hot-reload.mjs';
|
|
25
26
|
import { createEnhancement } from './plugins/enhance.mjs';
|
|
26
27
|
import { listInstalled, listThemes, removePlugin, removeTheme, savePluginSettings, setPluginEnabled, themeExists } from './installed.mjs';
|
|
27
28
|
import { createBoardLink } from './board-link.mjs';
|
|
28
29
|
import { spriteChunk } from './agent-sprites.mjs';
|
|
29
30
|
import { createScreenshotStore, rgb565ToPng, validateScreenshotParams, MAX_UPLOAD } from './board-screenshot.mjs';
|
|
31
|
+
import { createFirmwareStore } from './firmware-store.mjs';
|
|
30
32
|
import { initializeSpeechSettings, saveSpeechSettings } from './speech-settings.mjs';
|
|
31
33
|
import { builtinSpeechRecognizers } from './speech/index.mjs';
|
|
32
34
|
const PAIRED_DISCOVERY_REQUEST = /^VIBE_DISCOVER_PAIRED_V2 ([0-9a-f]{12}) ([0-9a-f]{32})$/;
|
|
@@ -228,8 +230,10 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
228
230
|
const publicSession = (session, locale = config.locale) => ({ ...session,
|
|
229
231
|
...(session.autoTitlePending ? { title: message(session.title, locale) } : {}) });
|
|
230
232
|
const publicJob = (job, locale = config.locale) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
// 桥接器规范文案(bridgeStrings 插件)在输出边界翻译;第三方插件自有的
|
|
234
|
+
// 进度与错误原样返回。
|
|
235
|
+
const canonical = config.pluginsRuntime.agent(job.provider)?.bridgeStrings === true;
|
|
236
|
+
return { ...job, ...(canonical ? { progress: message(job.progress, locale), error: message(job.error, locale),
|
|
233
237
|
...(job.handoffReadError ? { handoffReadError: message(job.handoffReadError, locale) } : {}) } : {}) };
|
|
234
238
|
};
|
|
235
239
|
const publicVoice = () => {
|
|
@@ -256,6 +260,13 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
256
260
|
const pairing = new PairingStore(config.stateDir);
|
|
257
261
|
const boardLink = createBoardLink();
|
|
258
262
|
const screenshots = createScreenshotStore(path.join(config.stateDir, 'screenshots'));
|
|
263
|
+
const firmware = createFirmwareStore(path.join(config.stateDir, 'firmware'));
|
|
264
|
+
let firmwareSync = null;
|
|
265
|
+
const ensureLocalFirmware = async () => {
|
|
266
|
+
if (firmware.deviceOffer().path || !firmware.summary().otaUrl) return;
|
|
267
|
+
firmwareSync ||= firmware.syncFromUrl().finally(() => { firmwareSync = null; });
|
|
268
|
+
await firmwareSync;
|
|
269
|
+
};
|
|
259
270
|
// 增强运行时:回合管道与生命周期通知给 JobStore 用,bridge 句柄给插件上下文用。
|
|
260
271
|
const enhance = createEnhancement(config);
|
|
261
272
|
const tunnel = providedTunnel || new TunnelManager({
|
|
@@ -310,12 +321,12 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
310
321
|
defaultProvider: config.defaultProvider,
|
|
311
322
|
maxParallel: config.maxParallelJobs,
|
|
312
323
|
maxAutoRetries: config.maxAutoRetries,
|
|
313
|
-
pluginsRuntime: config.pluginsRuntime,
|
|
324
|
+
pluginsRuntime: () => config.pluginsRuntime,
|
|
314
325
|
run: run || createRunner(config),
|
|
315
326
|
hooks: enhance,
|
|
316
327
|
// Keep stored built-in failures canonical; each client localizes at its output boundary.
|
|
317
328
|
providerAvailable: (id) => ({ ...providerStates(config, 'zh-CN').find((item) => item.id === id),
|
|
318
|
-
preserveMessage:
|
|
329
|
+
preserveMessage: config.pluginsRuntime.agent(id)?.bridgeStrings !== true }),
|
|
319
330
|
});
|
|
320
331
|
// 任务存储与板侧链路就绪后,把句柄露给管控插件。
|
|
321
332
|
enhance.attach({ jobs: store, board: boardLink, themes: () => listThemes(config) });
|
|
@@ -358,6 +369,10 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
358
369
|
send(res, 200, await listInstalled(config), false);
|
|
359
370
|
return;
|
|
360
371
|
}
|
|
372
|
+
if (req.method === 'GET' && url.pathname === '/api/firmware') {
|
|
373
|
+
send(res, 200, firmware.summary(), false);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
361
376
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
362
377
|
send(res, 200, {
|
|
363
378
|
locale: config.locale,
|
|
@@ -371,6 +386,8 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
371
386
|
tunnel: publicTunnel(),
|
|
372
387
|
voice: publicVoice(),
|
|
373
388
|
models: publicModels(config),
|
|
389
|
+
// 行动系统:deviceId → 可用动作名列表(板侧上报的最新一份)。
|
|
390
|
+
deviceActions: boardLink.allActions(),
|
|
374
391
|
});
|
|
375
392
|
return;
|
|
376
393
|
}
|
|
@@ -584,6 +601,42 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
584
601
|
}
|
|
585
602
|
throw new Error('码得未在 15 秒内回传截图(固件需要更新到支持 debug.screenshot 的版本)');
|
|
586
603
|
}
|
|
604
|
+
if (url.pathname === '/api/firmware') {
|
|
605
|
+
const type = String(req.headers['content-type'] || '');
|
|
606
|
+
if (type.startsWith('application/octet-stream')) {
|
|
607
|
+
const version = String(req.headers['x-firmware-version'] || '');
|
|
608
|
+
let bytes;
|
|
609
|
+
try {
|
|
610
|
+
bytes = await binaryBody(req, 8 * 1024 * 1024);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (error.message === '录音超过 30 秒上限') throw new Error('固件文件超过 8MB');
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
send(res, 200, firmware.importBuffer({
|
|
616
|
+
version, bytes, notes: String(req.headers['x-firmware-notes'] || ''),
|
|
617
|
+
}), false);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const payload = await body(req);
|
|
621
|
+
const action = payload && typeof payload.action === 'string' ? payload.action : '';
|
|
622
|
+
if (action === 'url') {
|
|
623
|
+
send(res, 200, firmware.setOtaUrl(payload.otaUrl), false);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (action === 'sync') {
|
|
627
|
+
send(res, 200, await firmware.syncFromUrl(), false);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (action === 'current') {
|
|
631
|
+
send(res, 200, firmware.setCurrent(payload.id), false);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (action === 'delete') {
|
|
635
|
+
send(res, 200, firmware.remove(payload.id), false);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
throw new Error('固件操作无效');
|
|
639
|
+
}
|
|
587
640
|
const pairAction = url.pathname.match(/^\/api\/pair\/([0-9a-f]{12})\/(confirm|reject)$/);
|
|
588
641
|
if (pairAction) {
|
|
589
642
|
const { nonce } = await body(req);
|
|
@@ -724,7 +777,37 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
724
777
|
deviceSend(res, 401, { error: '设备未授权' });
|
|
725
778
|
return;
|
|
726
779
|
}
|
|
780
|
+
if (req.method === 'GET' && url.pathname === '/device/firmware/current') {
|
|
781
|
+
try { await ensureLocalFirmware(); } catch (error) {
|
|
782
|
+
console.error(`[trace] fw-sync ${error?.message || error} ${new Date().toISOString()}`);
|
|
783
|
+
}
|
|
784
|
+
const offset = Number(url.searchParams.get('offset') || '0');
|
|
785
|
+
const length = Number(url.searchParams.get('length') || '131072');
|
|
786
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > 8 * 1024 * 1024) {
|
|
787
|
+
console.error(`[trace] fw-get off=${url.searchParams.get('offset') ?? '-'} len=${url.searchParams.get('length') ?? '-'} -> 400 ${new Date().toISOString()}`);
|
|
788
|
+
deviceSend(res, 400, { error: '固件偏移无效' });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const slice = firmware.readCurrent(offset, Number.isInteger(length) ? length : 128 * 1024);
|
|
792
|
+
console.error(`[trace] fw-get off=${offset} len=${Number.isInteger(length) ? length : 131072} -> ${slice ? slice.bytes.length : -1}B total=${slice ? slice.item.size : '-'} ${new Date().toISOString()}`);
|
|
793
|
+
if (!slice) {
|
|
794
|
+
deviceSend(res, 404, { error: '没有可更新的固件' });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
res.writeHead(200, {
|
|
798
|
+
'Content-Type': 'application/octet-stream',
|
|
799
|
+
'Content-Length': slice.bytes.length,
|
|
800
|
+
'Cache-Control': 'no-store',
|
|
801
|
+
'X-Content-Type-Options': 'nosniff',
|
|
802
|
+
'X-Firmware-Offset': String(slice.offset),
|
|
803
|
+
'X-Firmware-Size': String(slice.item.size),
|
|
804
|
+
'X-Firmware-Sha256': slice.item.sha256,
|
|
805
|
+
});
|
|
806
|
+
res.end(slice.bytes);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
727
809
|
if (req.method === 'GET' && url.pathname === '/device/heartbeat') {
|
|
810
|
+
console.error(`[trace] hb upMs=${url.searchParams.get('upMs') ?? '-'} wifiReason=${url.searchParams.get('wifiReason') ?? '-'} wifiDrops=${url.searchParams.get('wifiDrops') ?? '-'} wifiSelf=${url.searchParams.get('wifiSelf') ?? '-'} wifiHist=${url.searchParams.get('wifiHist') ?? '-'} heap=${url.searchParams.get('heap') ?? '-'} minHeap=${url.searchParams.get('minHeap') ?? '-'} psram=${url.searchParams.get('psram') ?? '-'} psramMin=${url.searchParams.get('psramMin') ?? '-'} fwStage=${url.searchParams.get('fwStage') ?? '-'} fwWhy=${url.searchParams.get('fwWhy') ?? '-'} fwResult=${url.searchParams.get('fwResult') ?? '-'} fwWritten=${url.searchParams.get('fwWritten') ?? '-'} fwSize=${url.searchParams.get('fwSize') ?? '-'} fwHttp=${url.searchParams.get('fwHttp') ?? '-'} fwTasks=${url.searchParams.get('fwTasks') ?? '-'} fwTask=${url.searchParams.get('fwTask') ?? '-'} ${new Date().toISOString()}`);
|
|
728
811
|
if (process.env.VIBE_UI_DIAGNOSTICS === '1') {
|
|
729
812
|
const fields = Object.fromEntries(['uiStage', 'uiAge', 'bootAge', 'exit', 'provider', 'displayStage', 'displayCount']
|
|
730
813
|
.map((key) => [key, url.searchParams.get(key)])
|
|
@@ -742,6 +825,8 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
742
825
|
deviceSend(res, 200, { events: boardLink.subscription(deviceId, config.pluginsRuntime, config) });
|
|
743
826
|
return;
|
|
744
827
|
}
|
|
828
|
+
// 行动系统:可用动作由设备并入命令清单(action.<名> 条目)上报,
|
|
829
|
+
// 不单开通道;名单从清单派生(boardLink.actionsOf)。
|
|
745
830
|
if (req.method === 'POST' && url.pathname === '/device/events') {
|
|
746
831
|
await boardLink.handleEvent(deviceId, await body(req), config.pluginsRuntime, config, enhance.bridge());
|
|
747
832
|
deviceSend(res, 200, { accepted: true });
|
|
@@ -797,6 +882,11 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
797
882
|
result: result.text,
|
|
798
883
|
resultTruncated: result.truncated,
|
|
799
884
|
error: devicePreview(job.error.slice(-200), 96),
|
|
885
|
+
attachments: (job.attachments || []).map((a) => ({
|
|
886
|
+
type: a.type === 'image' ? 'image' : 'link',
|
|
887
|
+
url: String(a.url || '').slice(0, 2048),
|
|
888
|
+
text: String(a.text || '').slice(0, 120),
|
|
889
|
+
})).filter((a) => a.url),
|
|
800
890
|
};
|
|
801
891
|
}),
|
|
802
892
|
});
|
|
@@ -831,6 +921,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
831
921
|
}
|
|
832
922
|
if (req.method === 'GET' && url.pathname === '/device/config') {
|
|
833
923
|
const voice = transcriptionState(config);
|
|
924
|
+
const firmwareSummary = firmware.summary();
|
|
834
925
|
// Firmware creates sessions in the default project; the full project
|
|
835
926
|
// picker lives on the desktop. Keep room for twelve bounded bitmaps.
|
|
836
927
|
const payload = {
|
|
@@ -839,19 +930,30 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
839
930
|
projects: config.projects.filter(({ id }) => id === config.defaultProject).map(({ id, label }) => ({ id, label: devicePreview(label, 64) })),
|
|
840
931
|
defaultProject: config.defaultProject,
|
|
841
932
|
voice: { mode: voice.mode, available: voice.available },
|
|
933
|
+
// 只有固件仓库里存在已导入的当前包(有 sha/版本)才 advertise OTA:
|
|
934
|
+
// 只配了 otaUrl 却没传固件时,设备会拿到无 sha 的 url → 永远 nomark
|
|
935
|
+
// → 弹更新确认 → waiting_confirmation 任务常驻 → 素材 preload 被门禁
|
|
936
|
+
// 拦死(GIF 动作等新文件永远下载不到)。
|
|
937
|
+
firmwareUrl: firmwareSummary.current ? firmwareSummary.otaUrl : '',
|
|
938
|
+
firmwareVersion: firmwareSummary.current?.version || '',
|
|
939
|
+
firmwareSha256: firmwareSummary.current?.sha256 || '',
|
|
940
|
+
firmwareSize: firmwareSummary.current?.size || 0,
|
|
842
941
|
board: { events: boardLink.subscription(pairing.deviceIdForToken(token), config.pluginsRuntime, config) },
|
|
843
942
|
// 桥接器重启会丢掉板侧命令清单:让板子看到 announce 后重新上报。
|
|
844
943
|
announce: !boardLink.hasCatalog(pairing.deviceIdForToken(token)),
|
|
845
944
|
};
|
|
846
945
|
if (Buffer.byteLength(JSON.stringify(payload)) > 32 * 1024) throw new Error('码得配置超过 32 KB,请检查助手元数据');
|
|
946
|
+
console.error(`[trace] fw-config url=${payload.firmwareUrl || '-'} ver=${payload.firmwareVersion || '-'} sha=${(payload.firmwareSha256 || '-').slice(0, 12)} size=${payload.firmwareSize || 0} ${new Date().toISOString()}`);
|
|
847
947
|
if (process.env.VIBE_UI_DIAGNOSTICS === '1') console.info('[device-config]', deviceId, 'announce=', payload.announce,
|
|
848
948
|
'providers=', payload.providers.map((p, i) => `${i}:${p.id}${p.sprite ? '@' + p.sprite.version : ''}`).join(' ') || 'none');
|
|
849
949
|
deviceSend(res, 200, payload);
|
|
850
950
|
return;
|
|
851
951
|
}
|
|
852
952
|
// 形象素材分片拉取:单次 ≤192KB,兼容 USB 帧转发 256KB 上限(docs/SPRITES.md)。
|
|
953
|
+
// 扩展名含 .gif:GIF 动作素材(actions[].gif)也在 files 清单里,路由
|
|
954
|
+
// 只放行 .png 的话设备永远拉不到,playAction 报 gif_unavailable。
|
|
853
955
|
const spriteMatch = req.method === 'GET' &&
|
|
854
|
-
url.pathname.match(/^\/device\/sprite\/([a-z0-9][a-z0-9_-]{0,39})\/([A-Za-z0-9_-]+\.png|manifest\.json)$/);
|
|
956
|
+
url.pathname.match(/^\/device\/sprite\/([a-z0-9][a-z0-9_-]{0,39})\/([A-Za-z0-9_-]+\.(?:png|gif)|manifest\.json)$/);
|
|
855
957
|
if (spriteMatch) {
|
|
856
958
|
const plugin = config.pluginsRuntime.agent(spriteMatch[1]);
|
|
857
959
|
if (spriteMatch[2] === 'manifest.json') {
|
|
@@ -862,6 +964,8 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
862
964
|
deviceSend(res, 200, {
|
|
863
965
|
version: plugin.sprite.version, canvas: plugin.sprite.canvas,
|
|
864
966
|
layers: plugin.sprite.layers, ...(plugin.sprite.motion ? { motion: plugin.sprite.motion } : {}),
|
|
967
|
+
...(plugin.sprite.emotions ? { emotions: plugin.sprite.emotions } : {}),
|
|
968
|
+
...(plugin.sprite.actions ? { actions: plugin.sprite.actions } : {}),
|
|
865
969
|
});
|
|
866
970
|
return;
|
|
867
971
|
}
|
|
@@ -874,6 +978,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
874
978
|
const chunk = plugin?.sprite ? spriteChunk(plugin.sprite, spriteMatch[2],
|
|
875
979
|
Number(url.searchParams.get('offset')) || 0,
|
|
876
980
|
requested === undefined ? SPRITE_DEVICE_CHUNK : Math.min(requested, SPRITE_DEVICE_CHUNK)) : null;
|
|
981
|
+
console.error(`[trace] sprite ${spriteMatch[1]}/${spriteMatch[2]} off=${url.searchParams.get('offset') ?? 0} -> ${chunk ? chunk.length : -1}B ${new Date().toISOString()}`);
|
|
877
982
|
if (process.env.VIBE_UI_DIAGNOSTICS) {
|
|
878
983
|
console.log(`[device-sprite] ${spriteMatch[2]} offset=${url.searchParams.get('offset') ?? 0} len=${url.searchParams.get('len') ?? '-'} -> ${chunk ? chunk.length : -1}B`);
|
|
879
984
|
}
|
|
@@ -1024,11 +1129,13 @@ if (isEntry) {
|
|
|
1024
1129
|
}
|
|
1025
1130
|
const config = loadConfig();
|
|
1026
1131
|
config.pluginsRuntime = await loadConfiguredPlugins(config);
|
|
1132
|
+
const pluginsWatch = watchPlugins(config);
|
|
1027
1133
|
const app = createApp(config, { enableUsb: true });
|
|
1028
1134
|
let shuttingDown = false;
|
|
1029
1135
|
const shutdown = () => {
|
|
1030
1136
|
if (shuttingDown) return;
|
|
1031
1137
|
shuttingDown = true;
|
|
1138
|
+
pluginsWatch.close();
|
|
1032
1139
|
app.close().then(() => { process.exitCode = 0; }).catch((error) => {
|
|
1033
1140
|
console.error(error.message);
|
|
1034
1141
|
process.exitCode = 1;
|
package/src/sessions.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { builtinAgent } from './agents/index.mjs';
|
|
5
4
|
|
|
6
5
|
export const VIBE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
6
|
export const CLI_SESSION_ID = /^[A-Za-z0-9_-]{6,128}$/;
|
|
@@ -33,7 +32,7 @@ function firstTurnTitle(instruction) {
|
|
|
33
32
|
|
|
34
33
|
export class SessionStore {
|
|
35
34
|
constructor({ file, projects, defaultProject, defaultProvider, pluginsRuntime, agent }) {
|
|
36
|
-
this.agent = agent || (pluginsRuntime ? (id) => pluginsRuntime.agent(id) :
|
|
35
|
+
this.agent = agent || (pluginsRuntime ? (id) => pluginsRuntime.agent(id) : () => undefined);
|
|
37
36
|
this.file = file;
|
|
38
37
|
this.projects = projects;
|
|
39
38
|
this.defaultProject = defaultProject;
|
package/src/usb-receiver.mjs
CHANGED
|
@@ -25,6 +25,8 @@ const VOICE_TIMEOUT_MS = 150_000;
|
|
|
25
25
|
const HELLO_TIMEOUT_MS = 7_000;
|
|
26
26
|
const DISCOVERY_INTERVAL_MS = 2_000;
|
|
27
27
|
const REJECTED_PORT_COOLDOWN_MS = 60_000;
|
|
28
|
+
// 没握手的端口(码得平时只把 USB 当日志口)不能一直占着,否则烧录和串口监视都打不开。
|
|
29
|
+
const IDLE_PORT_COOLDOWN_MS = 3_000;
|
|
28
30
|
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
29
31
|
// Probe only the native ESP USB device and the WCH bridge verified on the
|
|
30
32
|
// supported C3 receiver. A matching USB ID is a candidate, not a ready peer:
|
|
@@ -52,25 +54,45 @@ function limitedText(value, limit = 128) {
|
|
|
52
54
|
return typeof value === 'string' && value.length <= limit && !/[\r\n\x00-\x1f\x7f]/.test(value);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
function usbId(vendor, product) {
|
|
58
|
+
return `${vendor.toString(16)}:${product.toString(16)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 码得的原生 USB Serial/JTAG 不看波特率。对它跑 stty 会改 DTR/RTS,
|
|
62
|
+
// ESP32-S3 因此复位,正在用的 Wi-Fi 连接被拔掉。WCH 才是真串口,要设波特率。
|
|
63
|
+
export function usbSerialNeedsLineCoding(id) {
|
|
64
|
+
return id !== '303a:1001';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseMacUsbSerialDevices(output) {
|
|
68
|
+
const devices = [];
|
|
69
|
+
const seen = new Set();
|
|
57
70
|
const blocks = String(output).split(/(?=^[\s|]*\+-o [^\n]*<class IOUSBHostDevice,)/m);
|
|
58
71
|
for (const block of blocks) {
|
|
59
72
|
if (!block.includes('<class IOUSBHostDevice,')) continue;
|
|
60
73
|
const vendor = block.match(/"idVendor"\s*=\s*(\d+)/);
|
|
61
74
|
const product = block.match(/"idProduct"\s*=\s*(\d+)/);
|
|
62
|
-
|
|
75
|
+
const vendorId = Number(vendor?.[1]);
|
|
76
|
+
const productId = Number(product?.[1]);
|
|
77
|
+
if (!supportedUsbDevice(vendorId, productId)) continue;
|
|
78
|
+
const id = usbId(vendorId, productId);
|
|
63
79
|
for (const match of block.matchAll(/"IOCalloutDevice"\s*=\s*"(\/dev\/cu\.[A-Za-z0-9._-]+)"/g)) {
|
|
64
|
-
|
|
80
|
+
if (seen.has(match[1])) continue;
|
|
81
|
+
seen.add(match[1]);
|
|
82
|
+
devices.push({ path: match[1], id });
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
|
-
return
|
|
85
|
+
return devices;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseMacUsbSerialPorts(output) {
|
|
89
|
+
return parseMacUsbSerialDevices(output).map((device) => device.path);
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
export function
|
|
71
|
-
const
|
|
92
|
+
export function linuxUsbSerialDevices({ sysTtyRoot = '/sys/class/tty', devRoot = '/dev' } = {}) {
|
|
93
|
+
const devices = [];
|
|
72
94
|
let names;
|
|
73
|
-
try { names = fs.readdirSync(sysTtyRoot); } catch { return
|
|
95
|
+
try { names = fs.readdirSync(sysTtyRoot); } catch { return devices; }
|
|
74
96
|
for (const name of names) {
|
|
75
97
|
if (!/^tty(?:ACM|USB)\d+$/.test(name)) continue;
|
|
76
98
|
let directory;
|
|
@@ -82,29 +104,37 @@ export function linuxUsbSerialPorts({ sysTtyRoot = '/sys/class/tty', devRoot = '
|
|
|
82
104
|
const product = Number.parseInt(fs.readFileSync(path.join(directory, 'idProduct'), 'utf8'), 16);
|
|
83
105
|
if (supportedUsbDevice(vendor, product)) {
|
|
84
106
|
const device = path.join(devRoot, name);
|
|
85
|
-
if (fs.existsSync(device))
|
|
107
|
+
if (fs.existsSync(device)) devices.push({ path: device, id: usbId(vendor, product) });
|
|
86
108
|
break;
|
|
87
109
|
}
|
|
88
110
|
} catch { /* Walk to the USB device parent. */ }
|
|
89
111
|
directory = path.dirname(directory);
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
|
-
return
|
|
114
|
+
return devices;
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
export
|
|
117
|
+
export function linuxUsbSerialPorts(options) {
|
|
118
|
+
return linuxUsbSerialDevices(options).map((device) => device.path);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function discoverUsbSerialDevices(platform = process.platform) {
|
|
96
122
|
if (platform === 'darwin') {
|
|
97
123
|
try {
|
|
98
124
|
const result = await execFileAsync('ioreg', ['-p', 'IOService', '-r', '-c', 'IOUSBHostDevice', '-l', '-t', '-w0'], {
|
|
99
125
|
encoding: 'utf8', timeout: 3000, maxBuffer: 8 * 1024 * 1024,
|
|
100
126
|
});
|
|
101
|
-
return
|
|
127
|
+
return parseMacUsbSerialDevices(result.stdout).filter((device) => fs.existsSync(device.path));
|
|
102
128
|
} catch { return []; }
|
|
103
129
|
}
|
|
104
|
-
if (platform === 'linux') return
|
|
130
|
+
if (platform === 'linux') return linuxUsbSerialDevices();
|
|
105
131
|
return [];
|
|
106
132
|
}
|
|
107
133
|
|
|
134
|
+
export async function discoverUsbSerialPorts(platform = process.platform) {
|
|
135
|
+
return (await discoverUsbSerialDevices(platform)).map((device) => device.path);
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
async function configureSerial(port, platform = process.platform) {
|
|
109
139
|
if (!['darwin', 'linux'].includes(platform)) return;
|
|
110
140
|
const settings = ['921600', 'raw', '-echo', 'cs8', '-parenb', '-cstopb',
|
|
@@ -228,6 +258,9 @@ export class UsbReceiverProtocol {
|
|
|
228
258
|
// 会插进来。只有「看着像帧」的坏行才算协议错误;日志必须原样放行,
|
|
229
259
|
// 否则板子一打日志就会掐掉正在传输的响应(素材分片下载失败即此因)。
|
|
230
260
|
if (!line.startsWith('{')) {
|
|
261
|
+
if (/ESP-ROM|rst:0x|boot:0x/.test(line)) {
|
|
262
|
+
console.error(`[trace] usb-boot ${new Date().toISOString()} ${line.slice(0, 180)}`);
|
|
263
|
+
}
|
|
231
264
|
console.error(`[usb-link] log: ${line.slice(0, 240)}`);
|
|
232
265
|
return;
|
|
233
266
|
}
|
|
@@ -541,16 +574,17 @@ export class UsbReceiver {
|
|
|
541
574
|
if (this.polling || this.stopped || this.state.status === 'disabled') return;
|
|
542
575
|
this.polling = true;
|
|
543
576
|
try {
|
|
544
|
-
const
|
|
577
|
+
const devices = this.portPath ? [{ path: this.portPath, id: '' }] : await discoverUsbSerialDevices();
|
|
545
578
|
if (this.stopped) return;
|
|
546
|
-
if (!
|
|
579
|
+
if (!devices.length) {
|
|
547
580
|
if (!this.links.size) {
|
|
548
581
|
this.state.port = this.portPath;
|
|
549
582
|
this.state.error = this.portPath ? 'USB 设备未连接' : '未发现受支持的 USB 设备;可设置 VIBE_USB_PORT';
|
|
550
583
|
}
|
|
551
584
|
return;
|
|
552
585
|
}
|
|
553
|
-
for (const
|
|
586
|
+
for (const device of devices) {
|
|
587
|
+
const port = device.path;
|
|
554
588
|
if (this.stopped) break;
|
|
555
589
|
if (this.links.has(port)) continue;
|
|
556
590
|
if (Date.now() < (this.cooldown.get(port) || 0)) continue;
|
|
@@ -562,9 +596,12 @@ export class UsbReceiver {
|
|
|
562
596
|
const fd = fs.openSync(port, fs.constants.O_RDWR | fs.constants.O_NONBLOCK |
|
|
563
597
|
(fs.constants.O_NOCTTY || 0));
|
|
564
598
|
unregisteredFd = fd;
|
|
599
|
+
const lineCoding = usbSerialNeedsLineCoding(device.id);
|
|
600
|
+
console.error(`[trace] usb-open port=${port} id=${device.id || '-'} stty=${lineCoding} ${new Date().toISOString()}`);
|
|
565
601
|
// WCH resets the baud rate when its final handle closes. Keep our
|
|
566
602
|
// handle open while stty opens, configures and closes its own handle.
|
|
567
|
-
|
|
603
|
+
// 码得自己的口跳过这一步,避免插入 USB 时把 Wi-Fi 连接复位掉。
|
|
604
|
+
if (lineCoding) await configureSerial(port);
|
|
568
605
|
if (this.stopped) break;
|
|
569
606
|
const link = { fd, port, helloTimer: null, inputTimer: null,
|
|
570
607
|
protocol: null, writeQueue: Promise.resolve() };
|
|
@@ -624,6 +661,10 @@ export class UsbReceiver {
|
|
|
624
661
|
refreshAuthenticatedVoice: this.refreshAuthenticatedVoice,
|
|
625
662
|
});
|
|
626
663
|
const inputBuffer = Buffer.allocUnsafe(4096);
|
|
664
|
+
// macOS 的 CDC 驱动偶发返回 0 字节读(并非拔线)。连续空读超过
|
|
665
|
+
// ~1s 才判死,否则一次抖动就把链路拆掉重连(实机表现为「时不时断开」)。
|
|
666
|
+
const ZERO_READ_LIMIT = 50;
|
|
667
|
+
link.zeroReads = 0;
|
|
627
668
|
const readAvailable = () => {
|
|
628
669
|
if (this.links.get(port) !== link) return;
|
|
629
670
|
// TTY drivers may return only 1 KiB even when more data is ready.
|
|
@@ -631,11 +672,16 @@ export class UsbReceiver {
|
|
|
631
672
|
for (let reads = 0; reads < 64; reads += 1) {
|
|
632
673
|
try {
|
|
633
674
|
const length = fs.readSync(fd, inputBuffer, 0, inputBuffer.length, null);
|
|
634
|
-
if (length > 0)
|
|
635
|
-
|
|
675
|
+
if (length > 0) {
|
|
676
|
+
link.zeroReads = 0;
|
|
677
|
+
link.protocol.feed(inputBuffer.subarray(0, length));
|
|
678
|
+
} else if (this.links.get(port) === link && ++link.zeroReads >= ZERO_READ_LIMIT) {
|
|
679
|
+
this.disconnect(link, 'USB 设备已断开');
|
|
680
|
+
return;
|
|
681
|
+
} else break;
|
|
636
682
|
} catch (error) {
|
|
637
683
|
if (error.code === 'EAGAIN' || error.code === 'EWOULDBLOCK') break;
|
|
638
|
-
this.disconnect(link,
|
|
684
|
+
this.disconnect(link, `USB 读取错误 ${error.code || error.message}`);
|
|
639
685
|
return;
|
|
640
686
|
}
|
|
641
687
|
}
|
|
@@ -647,12 +693,11 @@ export class UsbReceiver {
|
|
|
647
693
|
link.inputTimer = setTimeout(readAvailable, 20);
|
|
648
694
|
link.inputTimer.unref?.();
|
|
649
695
|
link.helloTimer = setTimeout(() => {
|
|
650
|
-
if (this.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
696
|
+
if (this.links.get(port) !== link || this.state.status === 'connected') return;
|
|
697
|
+
// 没握手就先放开串口,烧录可以插空占用。冷却只有几秒,板子每 3 秒
|
|
698
|
+
// 发一次 hello,下一轮打开就能接上,不用对准打开的时刻。
|
|
699
|
+
this.cooldown.set(port, Date.now() + IDLE_PORT_COOLDOWN_MS);
|
|
700
|
+
this.disconnect(link, '未握手,已暂时释放串口,马上会再试');
|
|
656
701
|
}, HELLO_TIMEOUT_MS);
|
|
657
702
|
link.helloTimer.unref?.();
|
|
658
703
|
} catch {
|
|
@@ -672,6 +717,9 @@ export class UsbReceiver {
|
|
|
672
717
|
|
|
673
718
|
disconnect(link, error = '') {
|
|
674
719
|
if (this.links.get(link.port) !== link) return;
|
|
720
|
+
// 断开原因必须留痕:连接状态只存最后一条 error,没有这行日志就查不到
|
|
721
|
+
// 「时不时断开」到底是空读判死、写超时还是心跳超时。
|
|
722
|
+
console.error(`[usb-link] disconnect port=${link.port} reason=${error || '未指定'} lastSeen=${this.state.lastSeen || 'never'}`);
|
|
675
723
|
this.links.delete(link.port);
|
|
676
724
|
clearTimeout(link.helloTimer);
|
|
677
725
|
link.protocol?.dispose();
|
package/src/agents/codex.mjs
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
5
|
-
import { probeCli, runCli, gitState } from './shared.mjs';
|
|
6
|
-
import { jsonLineEvents, toolActivity } from './events.mjs';
|
|
7
|
-
|
|
8
|
-
// `codex login status` 只认 ChatGPT/OAuth 登录。当用户在 ~/.codex/config.toml 里配置了
|
|
9
|
-
// 自定义 model_provider(experimental_bearer_token 或 env_key 指向的环境变量)时,
|
|
10
|
-
// CLI 实际可用但登录状态仍显示未登录。这里补充识别这类鉴权方式,避免误报“请先登录”。
|
|
11
|
-
function hasCustomProviderAuth() {
|
|
12
|
-
try {
|
|
13
|
-
const config = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
|
|
14
|
-
const bearer = config.match(/experimental_bearer_token\s*=\s*"([^"]+)"/);
|
|
15
|
-
if (bearer?.[1]) return true;
|
|
16
|
-
const envKey = config.match(/env_key\s*=\s*"([^"]+)"/);
|
|
17
|
-
return Boolean(envKey?.[1] && process.env[envKey[1]]);
|
|
18
|
-
} catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
-
|
|
25
|
-
export function createEvents(onProgress, onSession) {
|
|
26
|
-
return jsonLineEvents((event, state) => {
|
|
27
|
-
const sessionId = event.type === 'thread.started' ? event.thread_id : undefined;
|
|
28
|
-
if (event.item?.type === 'agent_message' && event.type === 'item.completed' && typeof event.item.text === 'string') state.lastMessage = event.item.text;
|
|
29
|
-
if (event.type === 'turn.failed' || event.type === 'error') state.terminalError = '编程工具报告任务失败';
|
|
30
|
-
let activity = '';
|
|
31
|
-
if (event.type === 'turn.started') activity = '正在分析任务';
|
|
32
|
-
if (event.type === 'turn.completed') activity = '正在整理结果';
|
|
33
|
-
if (event.type.startsWith('item.')) {
|
|
34
|
-
const completed = event.type === 'item.completed';
|
|
35
|
-
switch (event.item?.type) {
|
|
36
|
-
case 'command_execution': activity = toolActivity('command', completed); break;
|
|
37
|
-
case 'file_change': activity = toolActivity('edit', completed); break;
|
|
38
|
-
case 'web_search': activity = completed ? '资料检索完成' : '正在查找资料'; break;
|
|
39
|
-
case 'mcp_tool_call': case 'collab_tool_call': activity = toolActivity('', completed); break;
|
|
40
|
-
case 'todo_list': if (completed) activity = '已更新任务步骤'; break;
|
|
41
|
-
case 'agent_message': if (completed) activity = '正在整理答复'; break;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return { sessionId, activity };
|
|
45
|
-
}, onProgress, onSession);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const adapter = {
|
|
49
|
-
label: 'Codex', createEvents,
|
|
50
|
-
command({ cwd, instruction, config, model, sessionId }) {
|
|
51
|
-
const args = ['exec', '--json', '--sandbox', 'workspace-write', '-C', cwd];
|
|
52
|
-
if (sessionId) args.push('resume');
|
|
53
|
-
const selected = model ?? config.codexModel;
|
|
54
|
-
if (selected) args.push('-m', selected);
|
|
55
|
-
if (sessionId) args.push(sessionId);
|
|
56
|
-
args.push('-');
|
|
57
|
-
return { command: 'codex', args, stdin: instruction };
|
|
58
|
-
},
|
|
59
|
-
classifyExit({ job, project, stderr, output, startingGitSnapshot }) {
|
|
60
|
-
const activeWriter = /Failed to create session:\s*thread-store conflict:\s*thread [A-Za-z0-9_-]{6,128} already has an active writer/;
|
|
61
|
-
if (job.sessionId && activeWriter.test(`${stderr}\n${output.rawTail}`)) {
|
|
62
|
-
let unchanged = false;
|
|
63
|
-
try { unchanged = gitState(project.path).snapshot === startingGitSnapshot; }
|
|
64
|
-
catch { /* An unreadable or changed repository cannot be retried safely. */ }
|
|
65
|
-
if (!unchanged) return new Error('Codex 对话正被使用,项目文件也发生变化。请先检查改动。');
|
|
66
|
-
const error = new Error('此 Codex 对话正在另一窗口或进程使用;结束后再次确认即可继续');
|
|
67
|
-
error.code = 'CODEX_SESSION_ACTIVE_WRITER';
|
|
68
|
-
error.retryable = true;
|
|
69
|
-
return error;
|
|
70
|
-
}
|
|
71
|
-
if (/model is not supported when using Codex|Model metadata for/.test(output.rawTail)) {
|
|
72
|
-
return new Error('Codex 当前模型不能用于这个登录账号。请在电脑桥接页面的“编程助手模型”中选择该账号可用的模型。');
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export default {
|
|
78
|
-
apiVersion: 1, kind: 'coding-agent', id: 'codex', label: 'Codex', icon: builtinAgentIcons.fox,
|
|
79
|
-
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
80
|
-
model: { default: 'gpt-6-astra', required: true },
|
|
81
|
-
sessionIdPattern: UUID,
|
|
82
|
-
probe: () => {
|
|
83
|
-
const state = probeCli('codex', ['login', 'status']);
|
|
84
|
-
if (state.available || !hasCustomProviderAuth()) return state;
|
|
85
|
-
return { available: true, mode: 'local_cli', reason: '' };
|
|
86
|
-
},
|
|
87
|
-
run: (context) => runCli(context, adapter),
|
|
88
|
-
createEvents,
|
|
89
|
-
syncJobState(job) {
|
|
90
|
-
if (job.error === 'Codex 当前模型不能用于这个登录账号。请设置 VIBE_CODEX_MODEL 为该账号在 CLI 中可用的模型。') {
|
|
91
|
-
job.error = 'Codex 当前模型不能用于这个登录账号。请在电脑桥接页面的“编程助手模型”中选择该账号可用的模型。';
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
};
|
package/src/agents/cursor.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
2
|
-
import { probeCli, runCli } from './shared.mjs';
|
|
3
|
-
import { jsonLineEvents, assistantEvent, streamActivity } from './events.mjs';
|
|
4
|
-
|
|
5
|
-
export function createEvents(onProgress, onSession) {
|
|
6
|
-
return jsonLineEvents((event, state) => {
|
|
7
|
-
assistantEvent(event, state, { append: true });
|
|
8
|
-
return { sessionId: event.session_id, activity: streamActivity(event) };
|
|
9
|
-
}, onProgress, onSession);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const adapter = {
|
|
13
|
-
label: 'Cursor', createEvents,
|
|
14
|
-
command({ cwd, instruction, model, sessionId }) {
|
|
15
|
-
const args = ['--print', '--trust', '--output-format', 'stream-json', '--stream-partial-output', '--sandbox', 'enabled', '--auto-review', '--workspace', cwd];
|
|
16
|
-
if (sessionId) args.push('--resume', sessionId);
|
|
17
|
-
if (model) args.push('--model', model);
|
|
18
|
-
args.push(instruction);
|
|
19
|
-
return { command: 'cursor-agent', args };
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export default {
|
|
24
|
-
apiVersion: 1, kind: 'coding-agent', id: 'cursor', label: 'Cursor', icon: builtinAgentIcons.rabbit,
|
|
25
|
-
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
26
|
-
model: { default: '', required: false },
|
|
27
|
-
sessionIdPattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
28
|
-
probe: () => probeCli('cursor-agent', ['status']),
|
|
29
|
-
run: (context) => runCli(context, adapter),
|
|
30
|
-
createEvents,
|
|
31
|
-
};
|
package/src/agents/index.mjs
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import codex from './codex.mjs';
|
|
2
|
-
import cursor from './cursor.mjs';
|
|
3
|
-
import qoder from './qoder.mjs';
|
|
4
|
-
import workbuddy from './workbuddy.mjs';
|
|
5
|
-
|
|
6
|
-
export const builtinAgents = Object.freeze([codex, cursor, qoder, workbuddy]);
|
|
7
|
-
export function builtinAgent(id) { return builtinAgents.find((plugin) => plugin.id === id); }
|
package/src/agents/qoder.mjs
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
2
|
-
import { probeCli, runCli } from './shared.mjs';
|
|
3
|
-
import { jsonLineEvents, assistantEvent, streamActivity } from './events.mjs';
|
|
4
|
-
|
|
5
|
-
export function createEvents(onProgress, onSession) {
|
|
6
|
-
return jsonLineEvents((event, state) => {
|
|
7
|
-
assistantEvent(event, state);
|
|
8
|
-
return {
|
|
9
|
-
sessionId: event.type === 'system' && event.subtype === 'init' ? event.session_id || event.data?.session_id : undefined,
|
|
10
|
-
activity: streamActivity(event),
|
|
11
|
-
};
|
|
12
|
-
}, onProgress, onSession);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const adapter = {
|
|
16
|
-
label: 'Qoder', createEvents,
|
|
17
|
-
command({ cwd, instruction, model, sessionId }) {
|
|
18
|
-
const args = ['--print', '--output-format', 'stream-json', '--permission-mode', 'accept_edits', '--cwd', cwd];
|
|
19
|
-
if (sessionId) args.push('--session-id', sessionId);
|
|
20
|
-
if (model) args.push('--model', model);
|
|
21
|
-
args.push(instruction);
|
|
22
|
-
return { command: 'qoder', args };
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default {
|
|
27
|
-
apiVersion: 1, kind: 'coding-agent', id: 'qoder', label: 'Qoder', icon: builtinAgentIcons.owl,
|
|
28
|
-
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
29
|
-
model: { default: '', required: false },
|
|
30
|
-
sessionIdPattern: /^[A-Za-z0-9_-]{6,128}$/,
|
|
31
|
-
probe: () => probeCli('qoder'),
|
|
32
|
-
run: (context) => runCli(context, adapter),
|
|
33
|
-
createEvents,
|
|
34
|
-
};
|