@coclaw/openclaw-coclaw 0.15.0 → 0.16.0

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": "@coclaw/openclaw-coclaw",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "OpenClaw CoClaw channel plugin for remote chat",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "node-datachannel": "0.32.2",
63
- "@coclaw/pion-node": "^0.1.2",
63
+ "@coclaw/pion-node": "^0.1.3",
64
64
  "werift": "^0.19.0",
65
65
  "ws": "^8.19.0"
66
66
  },
@@ -2,7 +2,7 @@
2
2
  * updater-check.js — 版本检查
3
3
  *
4
4
  * 通过 `npm view` 查询 registry 最新版本,与本地 package.json 对比。
5
- * 选择 npm view 而非直接 fetch registry API,是因为它自动继承用户完整的
5
+ * 选择 npm view 而非自己打 registry HTTP 接口,是因为它自动继承用户完整的
6
6
  * npm 环境配置(registry 镜像、proxy、scoped registry、auth token 等),
7
7
  * 避免自行解析多层 .npmrc 的复杂性。每小时一次的频率下进程启动开销可忽略。
8
8
  */
@@ -110,6 +110,27 @@ export class WebRtcPeer {
110
110
  }
111
111
  }
112
112
 
113
+ /**
114
+ * 向指定 connId 的 rpc DC 单播一个 JSON 帧(不走 server 中转)。
115
+ * 若 session/DC 未就绪返回 false,由调用方决定是否重试。
116
+ * @param {string} connId
117
+ * @param {object} payload - 完整的 JSON 帧(通常是 { type: 'event', event, payload })
118
+ * @returns {boolean} true=已入队发送,false=未能发送(session 不存在 / DC 未 open)
119
+ */
120
+ sendTo(connId, payload) {
121
+ const session = this.__sessions.get(connId);
122
+ if (!session) return false;
123
+ const q = session.rpcSendQueue;
124
+ if (!q || session.rpcChannel?.readyState !== 'open') return false;
125
+ try {
126
+ q.send(JSON.stringify(payload));
127
+ return true;
128
+ } catch (err) {
129
+ this.__logDebug(`[${connId}] sendTo failed: ${err.message}`);
130
+ return false;
131
+ }
132
+ }
133
+
113
134
  async __handleOffer(msg) {
114
135
  const connId = msg.fromConnId;
115
136
  const isIceRestart = !!msg.payload?.iceRestart;
@@ -307,6 +328,9 @@ export class WebRtcPeer {
307
328
  if (pair) {
308
329
  this.__logNominatedPair(connId, pair);
309
330
  }
331
+ // ICE restart 或初次选中都会触发;让出一次 CPU 后再单播 transport 信息。
332
+ // 签名去重保证 pair 不变时不会重复发送。
333
+ queueMicrotask(() => this.__sendPeerTransport(connId));
310
334
  };
311
335
  }
312
336
 
@@ -424,6 +448,12 @@ export class WebRtcPeer {
424
448
  dc.onopen = () => {
425
449
  this.__remoteLog(`dc.open conn=${connId} label=${dc.label}`);
426
450
  this.logger.info?.(`${this.__rtcTag} [${connId}] DataChannel "${dc.label}" opened`);
451
+ // rpc DC 建立后,把本端 transport 信息单播给 UI。
452
+ // queueMicrotask 让出一次 CPU:确保 pion 侧 selectedCandidatePair setter 已 assign,
453
+ // 同时避免在 onopen 同步栈里触发可能的重入。
454
+ if (dc.label === 'rpc') {
455
+ queueMicrotask(() => this.__sendPeerTransport(connId));
456
+ }
427
457
  };
428
458
  dc.onclose = () => {
429
459
  this.__remoteLog(`dc.closed conn=${connId} label=${dc.label}`);
@@ -481,12 +511,51 @@ export class WebRtcPeer {
481
511
  }
482
512
 
483
513
  __logNominatedPair(connId, pair) {
484
- const localInfo = `${pair.local?.type ?? '?'} ${pair.local?.address ?? pair.local?.host ?? '?'}:${pair.local?.port ?? '?'}`;
485
- const remoteInfo = `${pair.remote?.type ?? '?'} ${pair.remote?.address ?? pair.remote?.host ?? '?'}:${pair.remote?.port ?? '?'}`;
514
+ const l = pair.local, r = pair.remote;
515
+ const lProto = (l?.protocol ?? '?').toLowerCase();
516
+ const rProto = (r?.protocol ?? '?').toLowerCase();
517
+ const lRelay = l?.relayProtocol ? `(${String(l.relayProtocol).toLowerCase()})` : '';
518
+ const localInfo = `${l?.type ?? '?'}/${lProto}${lRelay} ${l?.address ?? l?.host ?? '?'}:${l?.port ?? '?'}`;
519
+ const remoteInfo = `${r?.type ?? '?'}/${rProto} ${r?.address ?? r?.host ?? '?'}:${r?.port ?? '?'}`;
486
520
  this.__remoteLog(`rtc.ice-nominated conn=${connId} local=${localInfo} remote=${remoteInfo}`);
487
521
  this.logger.info?.(`${this.__rtcTag} [${connId}] ICE nominated: local=${localInfo} remote=${remoteInfo}`);
488
522
  }
489
523
 
524
+ /**
525
+ * 把当前 session 本端 candidate 的 transport 信息(type/protocol/relayProtocol)
526
+ * 通过 coclaw.rtc.peerTransport 事件单播给对应 UI。已内置签名去重,
527
+ * 同一签名不会重复发送;发送失败(DC 未 open)时回滚签名允许后续重试。
528
+ *
529
+ * @param {string} connId
530
+ */
531
+ __sendPeerTransport(connId) {
532
+ const session = this.__sessions.get(connId);
533
+ if (!session) return;
534
+ const local = session.pc?.selectedCandidatePair?.local;
535
+ if (!local) return; // nominated pair 尚未产生
536
+ const payload = {
537
+ candidateType: local.type ?? 'unknown',
538
+ protocol: String(local.protocol ?? 'udp').toLowerCase(),
539
+ relayProtocol: local.relayProtocol
540
+ ? String(local.relayProtocol).toLowerCase()
541
+ : null,
542
+ };
543
+ const sig = `${payload.candidateType}|${payload.protocol}|${payload.relayProtocol ?? ''}`;
544
+ if (session.__lastPeerTransportSig === sig) return;
545
+ session.__lastPeerTransportSig = sig;
546
+ const ok = this.sendTo(connId, {
547
+ type: 'event',
548
+ event: 'coclaw.rtc.peerTransport',
549
+ payload,
550
+ });
551
+ if (!ok) {
552
+ // DC 尚未 open,回滚签名以便 dc.onopen 再次触发时重发
553
+ session.__lastPeerTransportSig = null;
554
+ return;
555
+ }
556
+ this.__remoteLog(`rtc.peer-transport conn=${connId} type=${payload.candidateType} proto=${payload.protocol} relay=${payload.relayProtocol ?? '-'}`);
557
+ }
558
+
490
559
  __remoteLog(msg) {
491
560
  remoteLog(this.__impl ? `${msg} rtc=${this.__impl}` : msg);
492
561
  }