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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "made-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "码得(made)独立语音编程设备的电脑侧桥接器:工作台 API、设备接口、任务调度与插件运行时。一条命令启动,配合 ESP32 固件使用,源码在 github.com/jobop/made。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 });
@@ -1,4 +1,7 @@
1
- export const CAPABILITY_NAME = /^[a-z][a-z0-9_]{0,15}(?:\.[a-z][a-z0-9_]{0,15}){0,2}$/;
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: action names derived from the device command catalog (`action.<name>` entries, decided by the current assistant sprite). Advisory only — the board silently ignores unknown action commands. */
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;
@@ -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
- if (!Array.isArray(list) || list.length > 16 || list.length < min || list.some((name) => typeof name !== 'string' || !/^[a-z][a-z0-9_]{0,15}(?:\.[a-z][a-z0-9_]{0,15}){0,2}$/.test(name))) fail(`${key} 无效`);
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
- firmwareUrl: firmwareSummary.otaUrl,
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
  }