made-bridge 0.3.2 → 0.3.4

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.2",
3
+ "version": "0.3.4",
4
4
  "description": "码得(made)独立语音编程设备的电脑侧桥接器:工作台 API、设备接口、任务调度与插件运行时。一条命令启动,配合 ESP32 固件使用,源码在 github.com/jobop/made。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -259,12 +259,14 @@ export function readSprite(packageDir) {
259
259
  const raw = readManifestFile(spriteDir);
260
260
  if (!raw) return null;
261
261
  const manifest = validateManifest(raw);
262
- // 引用完整性:目录里的 PNG 必须全部被 manifest 层引用、GIF 必须被某个
263
- // GIF 动作引用,帮开发者抓住文件名拼写错误。
264
- const gifReferenced = new Set();
262
+ // 引用完整性只做提示不做拒绝(2026-10-20 用户拍板):目录里未被 manifest
263
+ // 引用的 PNG/GIF 属于换绑/改形象后忘记删的旧素材,拒整包会让助手形象
264
+ // 整体加载不到(雪球实锤),代价远大于死重。清理责任放在 made-kit skill
265
+ // 层软性强调。manifest 引用不存在的文件(拼写错)仍是硬错误(下方 stat)。
266
+ const gifReferencedHint = new Set();
265
267
  if (manifest.actions) {
266
268
  for (const action of Object.values(manifest.actions)) {
267
- if (action && typeof action.gif === 'string') gifReferenced.add(action.gif);
269
+ if (action && typeof action.gif === 'string') gifReferencedHint.add(action.gif);
268
270
  }
269
271
  }
270
272
  for (const name of fs.readdirSync(spriteDir)) {
@@ -272,9 +274,11 @@ export function readSprite(packageDir) {
272
274
  if (lower.endsWith('.png')) {
273
275
  const referenced = manifest.layers.some((layer) =>
274
276
  (layer.frames !== undefined ? layer.frames : [layer.file]).includes(name));
275
- if (!referenced) throw new SpriteError(`sprite/${name} 未被 manifest 引用`);
276
- } else if (lower.endsWith('.gif')) {
277
- if (!gifReferenced.has(name)) throw new SpriteError(`sprite/${name} 未被任何 GIF 动作引用`);
277
+ if (!referenced) {
278
+ console.error(`[plugins] sprite 提示:${path.basename(spriteDir)}/${name} 未被 manifest 引用,不会被下发(建议删除旧素材省空间)`);
279
+ }
280
+ } else if (lower.endsWith('.gif') && !gifReferencedHint.has(name)) {
281
+ console.error(`[plugins] sprite 提示:${path.basename(spriteDir)}/${name} 未被任何 GIF 动作引用,不会被下发(建议删除旧素材省空间)`);
278
282
  }
279
283
  }
280
284
  let total = 0;
@@ -248,6 +248,9 @@ export function createBoardLink() {
248
248
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw fail('事件无效');
249
249
  if (typeof payload.name !== 'string' || !CAPABILITY_NAME.test(payload.name)) throw fail('事件无效');
250
250
  const fields = parseFields(payload.fields);
251
+ // 设备事件一律落日志(含未订阅被拒的):调试/烧机观察通道依赖它,
252
+ // 例如 burn-in 压测的水位事件 burnin.mem 就没有任何插件订阅。
253
+ console.log('[board-event]', deviceId.slice(-6), payload.name, JSON.stringify(fields));
251
254
  const catalog = catalogs.get(deviceId);
252
255
  if (!catalog || !catalog.events.some((item) => item.name === payload.name)) throw fail('事件不在板侧清单');
253
256
  const subscribed = this.subscription(deviceId, runtime, config);
package/src/server.mjs CHANGED
@@ -582,7 +582,9 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
582
582
  }
583
583
  }
584
584
  if (!queued) throw new Error(lastError || '命令未能下发');
585
- const deadline = Date.now() + 15000;
585
+ // USB 分片上传 240×320 帧实测 >15s(base64 走请求通道),15s 会
586
+ // 误报失败——其实 PNG 随后正常落盘。放宽到 45s。
587
+ const deadline = Date.now() + 45000;
586
588
  while (Date.now() < deadline) {
587
589
  const file = screenshots.latest(before);
588
590
  if (file) {
@@ -599,7 +601,7 @@ export function createApp(config, { run, transcribe, tunnel: providedTunnel, ena
599
601
  }
600
602
  await new Promise((resolve) => setTimeout(resolve, 250));
601
603
  }
602
- throw new Error('码得未在 15 秒内回传截图(固件需要更新到支持 debug.screenshot 的版本)');
604
+ throw new Error('码得未在 45 秒内回传截图(固件需要更新到支持 debug.screenshot 的版本)');
603
605
  }
604
606
  if (url.pathname === '/api/firmware') {
605
607
  const type = String(req.headers['content-type'] || '');
@@ -220,6 +220,9 @@ export class UsbReceiverProtocol {
220
220
 
221
221
  feed(chunk) {
222
222
  if (this.closed) return;
223
+ if (chunk.includes(0x22) && chunk.includes(0x3a) && chunk.length > 60 && !chunk.slice(0, 30).includes('hello')) {
224
+ console.log('[usb-raw]', chunk.length, JSON.stringify(chunk.subarray(0, 120).toString('utf8')));
225
+ }
223
226
  let start = 0;
224
227
  for (let index = 0; index < chunk.length; index += 1) {
225
228
  if (chunk[index] !== 10) continue;
@@ -264,7 +267,7 @@ export class UsbReceiverProtocol {
264
267
  console.error(`[usb-link] log: ${line.slice(0, 240)}`);
265
268
  return;
266
269
  }
267
- console.error(`[usb-link] unparseable: ${line.slice(0, 240)}`);
270
+ console.error(`[usb-link] unparseable len=${line.length} tail=${JSON.stringify(line.slice(-40))}`);
268
271
  this.rejectCurrent(400, 'USB 帧格式无效');
269
272
  return;
270
273
  }
@@ -272,6 +275,9 @@ export class UsbReceiverProtocol {
272
275
  this.rejectCurrent(400, 'USB 帧格式无效');
273
276
  return;
274
277
  }
278
+ if (frame && typeof frame.type === 'string' && frame.type !== 'hello') {
279
+ console.log('[usb-frame]', frame.type, frame.id ?? '', String(frame.path || '').slice(0, 60));
280
+ }
275
281
  if (frame.type === 'hello') {
276
282
  if (frame.version !== 1 || ![undefined, 'receiver', 'direct'].includes(frame.mode)) return;
277
283
  const direct = frame.mode === 'direct';
@@ -285,13 +291,25 @@ export class UsbReceiverProtocol {
285
291
  ssid: direct ? '' : frame.ssid, password: direct ? '' : frame.password };
286
292
  if (this.peer && (this.peer.mode !== peer.mode || this.peer.deviceId !== peer.deviceId)) return;
287
293
  if (this.onHello(peer) === false) return;
288
- console.error(`[usb-link] hello: mode=${peer.mode} ${new Date().toISOString()}`);
294
+ console.error(`[usb-link] hello: mode=${peer.mode} diag=${frame.diag || '-'} ${new Date().toISOString()}`);
289
295
  this.peer = peer;
290
296
  this.ackWindow = peer.mode === 'direct' ? ACK_WINDOW_DIRECT : ACK_WINDOW;
291
297
  this.helloReceived = true;
292
298
  this.onActivity();
293
299
  // The receiver waits for this acknowledgement before forwarding HTTP.
294
300
  // A USB cable being present does not mean the desktop process is ready.
301
+ // ready 直发不进 pendingWork:pendingWork 同时承载 writeResponse 的
302
+ // ack 流控等待——设备偶发丢 ack 时该链最长堵 15s,期间所有重握手
303
+ // 都等不到 ready,设备侧表现为连环 E263 + UI 闪「连接电脑」(实锤
304
+ // 于 2026-10-20 冒烟:ack TIMEOUT id=52 前后 15s 全是 1.5s 间隔握手
305
+ // 超时)。sendLine 走 link.writeQueue,帧原子性与 pendingWork 相同;
306
+ // direct 固件 read_request_frame 本就支持在响应流中处理 ready。
307
+ // receiver 模式保持原排队路径(ready 语义 = 桥接进程就绪)。
308
+ if (peer.mode === 'direct' && !this.closed) {
309
+ this.sendLine(JSON.stringify({ type: 'ready', version: 1, connectionId: this.connectionId }))
310
+ .catch(() => this.dispose());
311
+ return;
312
+ }
295
313
  const generation = this.generation;
296
314
  this.pendingWork = this.pendingWork.then(async () => {
297
315
  if (!this.closed && this.helloReceived && this.generation === generation) {
@@ -318,6 +336,7 @@ export class UsbReceiverProtocol {
318
336
  return;
319
337
  }
320
338
  if (frame.type === 'request') {
339
+ console.log('[usb-req]', frame.method, frame.path, 'id=' + frame.id);
321
340
  if (this.current || this.responding || !validId(frame.id)) {
322
341
  if (this.current) this.rejectCurrent(409, 'USB 请求重叠');
323
342
  return;
@@ -382,6 +401,7 @@ export class UsbReceiverProtocol {
382
401
  return;
383
402
  }
384
403
  if (frame.type === 'end') {
404
+ console.log('[usb-end] id=' + current.id + ' received=' + current.received + ' path=' + current.path.slice(0, 40));
385
405
  if (current.received !== current.length) {
386
406
  this.rejectCurrent(400, 'USB 请求长度不符');
387
407
  return;
@@ -392,6 +412,8 @@ export class UsbReceiverProtocol {
392
412
  try {
393
413
  if (this.closed || this.current !== current || current.controller.signal.aborted) return;
394
414
  const response = await this.forward(request, current.controller.signal);
415
+ console.log('[usb-resp]', response.status, request.method, request.path.slice(0, 40),
416
+ 'body=' + (response.body ? response.body.length : -1));
395
417
  if (this.closed || this.current !== current) return;
396
418
  clearTimeout(current.timer);
397
419
  await this.writeResponse(current.id, response.status, response.contentType, response.body);
@@ -485,7 +507,7 @@ export class UsbReceiverProtocol {
485
507
  console.error(`[usb-link] ack TIMEOUT id=${id} ${new Date().toISOString()}`);
486
508
  this.ackWaiters.delete(id);
487
509
  reject(new Error('USB ACK 超时'));
488
- }, 15_000);
510
+ }, 3_000);
489
511
  timer.unref?.();
490
512
  const check = () => {
491
513
  if ((this.ackCredits.get(id) || 0) < target) return;