made-bridge 0.3.0 → 0.3.2
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/src/agent-sprites.mjs +21 -3
- package/src/board-link.mjs +4 -1
- package/src/i18n.mjs +2 -0
- package/src/installed.mjs +11 -0
- package/src/plugins/contracts.d.ts +1 -1
- package/src/plugins/registry.mjs +3 -1
- package/src/server.mjs +10 -3
package/package.json
CHANGED
package/src/agent-sprites.mjs
CHANGED
|
@@ -54,7 +54,7 @@ function readManifestFile(dir) {
|
|
|
54
54
|
|
|
55
55
|
function validateManifest(raw) {
|
|
56
56
|
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'].includes(key))) {
|
|
57
|
+
if (Object.keys(raw).some((key) => !['version', 'canvas', 'layers', 'motion', 'emotions', 'actions', 'commands'].includes(key))) {
|
|
58
58
|
throw new SpriteError('manifest 包含不支持的字段');
|
|
59
59
|
}
|
|
60
60
|
if (!isInt(raw.version, 1, 2147483647)) throw new SpriteError('manifest.version 必须是正整数');
|
|
@@ -204,9 +204,27 @@ function validateManifest(raw) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
// 两级绑定模型(docs/SPRITES.md):commands 表(可选)定义「命令绑素材」
|
|
208
|
+
// ——抽象命令号 A1–A9 → actions 表动作名,纯编号不定语义。事件绑定(增强
|
|
209
|
+
// 插件)只发命令号;声明了本表的素材只认命令号,语义名直发废弃。
|
|
210
|
+
if (raw.commands !== undefined) {
|
|
211
|
+
const commands = raw.commands;
|
|
212
|
+
if (!commands || typeof commands !== 'object' || Array.isArray(commands)) {
|
|
213
|
+
throw new SpriteError('commands 必须是对象(A1–A9 → 动作名)');
|
|
214
|
+
}
|
|
215
|
+
const keys = Object.keys(commands);
|
|
216
|
+
if (keys.length > 9) throw new SpriteError('commands 最多 9 个命令号(A1–A9)');
|
|
217
|
+
const actionNames = new Set(Object.keys(raw.actions ?? {}));
|
|
218
|
+
for (const [slot, target] of Object.entries(commands)) {
|
|
219
|
+
if (!/^A[1-9]$/.test(slot)) throw new SpriteError(`commands.${slot} 命令号必须是 A1–A9`);
|
|
220
|
+
if (typeof target !== 'string' || !actionNames.has(target)) {
|
|
221
|
+
throw new SpriteError(`commands.${slot} 映射目标必须是 actions 表已声明的动作名`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
207
225
|
return {
|
|
208
226
|
version: raw.version, canvas: canvas, layers: raw.layers, motion: raw.motion ?? null,
|
|
209
|
-
emotions: raw.emotions ?? null, actions: raw.actions ?? null,
|
|
227
|
+
emotions: raw.emotions ?? null, actions: raw.actions ?? null, commands: raw.commands ?? null,
|
|
210
228
|
};
|
|
211
229
|
}
|
|
212
230
|
|
|
@@ -299,7 +317,7 @@ export function readSprite(packageDir) {
|
|
|
299
317
|
const value = Object.freeze({
|
|
300
318
|
dir: spriteDir, version: manifest.version, canvas: manifest.canvas,
|
|
301
319
|
layers: manifest.layers, motion: manifest.motion, emotions: manifest.emotions,
|
|
302
|
-
actions: manifest.actions,
|
|
320
|
+
actions: manifest.actions, commands: manifest.commands,
|
|
303
321
|
files: Object.freeze(files.map((file) => Object.freeze(file))),
|
|
304
322
|
});
|
|
305
323
|
cache.set(spriteDir, { stamp, value });
|
package/src/board-link.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// 能力名:段 = 字母开头的 [A-Za-z0-9_]{0,15},1–3 段点分。大写段用于行动
|
|
2
|
+
// 系统的抽象命令号(action.A1–A9,两级绑定模型 docs/SPRITES.md);老语义
|
|
3
|
+
// 动作名与事件名全小写不受影响。
|
|
4
|
+
export const CAPABILITY_NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,15}(?:\.[a-zA-Z][a-zA-Z0-9_]{0,15}){0,2}$/;
|
|
2
5
|
const FIELD_KEY = /^[a-z][a-z0-9_]{0,15}$/;
|
|
3
6
|
const COMMAND_ID = /^[1-9][0-9]{0,15}$/;
|
|
4
7
|
const MAX_ITEMS = 32;
|
package/src/i18n.mjs
CHANGED
|
@@ -59,6 +59,8 @@ 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
|
+
['这是默认编程助手,不能停用;请先在设置里把默认编程助手切换为其他助手', 'This is the default assistant and cannot be disabled; switch the default assistant to another one in settings first'],
|
|
63
|
+
['这是默认编程助手,不能删除;请先在设置里把默认编程助手切换为其他助手', 'This is the default assistant and cannot be removed; switch the default assistant to another one in settings first'],
|
|
62
64
|
['配对请求格式无效', 'Invalid pairing request'],
|
|
63
65
|
['设备名称无效', 'Invalid device name'],
|
|
64
66
|
['电脑配对窗口未开启', 'Pairing is not open on the computer'],
|
package/src/installed.mjs
CHANGED
|
@@ -189,6 +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
|
+
// 默认编程助手不能停用:loadConfiguredPlugins 会守卫 defaultProvider,
|
|
193
|
+
// 但那里的报错不含操作指引,用户只会看到一句没头没尾的「未安装或已禁用」。
|
|
194
|
+
// 在动作入口给出可操作的前置报错。
|
|
195
|
+
if (!enabled && id === config.defaultProvider) {
|
|
196
|
+
throw fail('这是默认编程助手,不能停用;请先在设置里把默认编程助手切换为其他助手');
|
|
197
|
+
}
|
|
192
198
|
const previous = [...(config.disabledPlugins || [])];
|
|
193
199
|
const next = new Set(previous);
|
|
194
200
|
if (enabled) next.delete(id);
|
|
@@ -246,6 +252,11 @@ export async function removePlugin(config, { id = '', file = '' } = {}) {
|
|
|
246
252
|
target = removalTarget(config, target);
|
|
247
253
|
const described = await describeModule(target, config);
|
|
248
254
|
const ids = described.map((plugin) => plugin.id).filter((item) => PLUGIN_ID.test(item));
|
|
255
|
+
// 同 setPluginEnabled:删默认助手会被 defaultProvider 守卫拦下但报错没有指引,
|
|
256
|
+
// 在动作入口就说清楚(2026-10-18 实锤:删除 codex 后桥接器重启都起不来)。
|
|
257
|
+
if (ids.includes(config.defaultProvider)) {
|
|
258
|
+
throw fail('这是默认编程助手,不能删除;请先在设置里把默认编程助手切换为其他助手');
|
|
259
|
+
}
|
|
249
260
|
const previousDisabled = [...(config.disabledPlugins || [])];
|
|
250
261
|
const previousPlugins = [...(config.plugins || [])];
|
|
251
262
|
config.disabledPlugins = [...new Set([...previousDisabled, ...ids])];
|
|
@@ -159,7 +159,7 @@ export interface BridgeFacade {
|
|
|
159
159
|
cancelJob(id: string): Record<string, unknown>;
|
|
160
160
|
themes(): { name: string; title: string; sound: boolean; icon: boolean; background: boolean }[];
|
|
161
161
|
devices(): string[];
|
|
162
|
-
/** Action system:
|
|
162
|
+
/** Action system: command names derived from the device command catalog (`action.<name>` entries). Two-level binding model (docs/SPRITES.md): sprites declaring a `commands` map expose abstract slots `action.A1`–`action.A9` ("command binds artwork", decided by the sprite author); legacy sprites expose semantic action names (deprecated). Advisory only — the board silently ignores unknown action commands. */
|
|
163
163
|
deviceActions(deviceId: string): string[];
|
|
164
164
|
/** Enqueue a board command directly; only the board-side catalog constrains the name. */
|
|
165
165
|
send(deviceId: string, command: BoardCommand, audio?: Uint8Array): QueuedBoardCommand;
|
package/src/plugins/registry.mjs
CHANGED
|
@@ -33,7 +33,9 @@ export function validatePlugin(plugin) {
|
|
|
33
33
|
} else if (plugin.kind === 'board-plugin') {
|
|
34
34
|
const names = (key, min) => {
|
|
35
35
|
const list = plugin[key];
|
|
36
|
-
|
|
36
|
+
// 能力名与桥接器 CAPABILITY_NAME 同形(字母开头的段,1–3 段点分);
|
|
37
|
+
// 大写段用于行动系统的抽象命令号(action.A1–A9,docs/SPRITES.md)。
|
|
38
|
+
if (!Array.isArray(list) || list.length > 16 || list.length < min || list.some((name) => typeof name !== 'string' || !/^[a-zA-Z][a-zA-Z0-9_]{0,15}(?:\.[a-zA-Z][a-zA-Z0-9_]{0,15}){0,2}$/.test(name))) fail(`${key} 无效`);
|
|
37
39
|
if (new Set(list).size !== list.length) fail(`${key} 有重复项`);
|
|
38
40
|
};
|
|
39
41
|
names('events', 1);
|
package/src/server.mjs
CHANGED
|
@@ -807,7 +807,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
807
807
|
return;
|
|
808
808
|
}
|
|
809
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') ?? '-'} 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()}`);
|
|
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()}`);
|
|
811
811
|
if (process.env.VIBE_UI_DIAGNOSTICS === '1') {
|
|
812
812
|
const fields = Object.fromEntries(['uiStage', 'uiAge', 'bootAge', 'exit', 'provider', 'displayStage', 'displayCount']
|
|
813
813
|
.map((key) => [key, url.searchParams.get(key)])
|
|
@@ -930,7 +930,11 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
930
930
|
projects: config.projects.filter(({ id }) => id === config.defaultProject).map(({ id, label }) => ({ id, label: devicePreview(label, 64) })),
|
|
931
931
|
defaultProject: config.defaultProject,
|
|
932
932
|
voice: { mode: voice.mode, available: voice.available },
|
|
933
|
-
|
|
933
|
+
// 只有固件仓库里存在已导入的当前包(有 sha/版本)才 advertise OTA:
|
|
934
|
+
// 只配了 otaUrl 却没传固件时,设备会拿到无 sha 的 url → 永远 nomark
|
|
935
|
+
// → 弹更新确认 → waiting_confirmation 任务常驻 → 素材 preload 被门禁
|
|
936
|
+
// 拦死(GIF 动作等新文件永远下载不到)。
|
|
937
|
+
firmwareUrl: firmwareSummary.current ? firmwareSummary.otaUrl : '',
|
|
934
938
|
firmwareVersion: firmwareSummary.current?.version || '',
|
|
935
939
|
firmwareSha256: firmwareSummary.current?.sha256 || '',
|
|
936
940
|
firmwareSize: firmwareSummary.current?.size || 0,
|
|
@@ -946,8 +950,10 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
946
950
|
return;
|
|
947
951
|
}
|
|
948
952
|
// 形象素材分片拉取:单次 ≤192KB,兼容 USB 帧转发 256KB 上限(docs/SPRITES.md)。
|
|
953
|
+
// 扩展名含 .gif:GIF 动作素材(actions[].gif)也在 files 清单里,路由
|
|
954
|
+
// 只放行 .png 的话设备永远拉不到,playAction 报 gif_unavailable。
|
|
949
955
|
const spriteMatch = req.method === 'GET' &&
|
|
950
|
-
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)$/);
|
|
951
957
|
if (spriteMatch) {
|
|
952
958
|
const plugin = config.pluginsRuntime.agent(spriteMatch[1]);
|
|
953
959
|
if (spriteMatch[2] === 'manifest.json') {
|
|
@@ -960,6 +966,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
|
|
|
960
966
|
layers: plugin.sprite.layers, ...(plugin.sprite.motion ? { motion: plugin.sprite.motion } : {}),
|
|
961
967
|
...(plugin.sprite.emotions ? { emotions: plugin.sprite.emotions } : {}),
|
|
962
968
|
...(plugin.sprite.actions ? { actions: plugin.sprite.actions } : {}),
|
|
969
|
+
...(plugin.sprite.commands ? { commands: plugin.sprite.commands } : {}),
|
|
963
970
|
});
|
|
964
971
|
return;
|
|
965
972
|
}
|