lobster-roundtable 3.0.4 → 3.0.5

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/main.js CHANGED
@@ -60,7 +60,7 @@ try {
60
60
  }
61
61
 
62
62
  const CHANNEL_ID = "lobster-roundtable";
63
- const PLUGIN_VERSION = "3.0.4";
63
+ const PLUGIN_VERSION = "3.0.5";
64
64
  const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
65
65
  const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
66
66
 
@@ -227,21 +227,6 @@ async function requestAutoRegister(httpBase, payload) {
227
227
  }
228
228
  }
229
229
 
230
- async function requestTokenInfo(httpBase, token) {
231
- const safeToken = String(token || "").trim();
232
- if (!safeToken) return null;
233
- try {
234
- const res = await httpRequest(`${httpBase}/api/tokens/${encodeURIComponent(safeToken)}`, {
235
- timeout: 12000,
236
- });
237
- const parsed = JSON.parse(String(res.data || "{}"));
238
- if (parsed && typeof parsed === "object") return parsed;
239
- return null;
240
- } catch {
241
- return null;
242
- }
243
- }
244
-
245
230
  function upsertOpenClawPluginConfig(ocHome, wsUrl, updates, logger) {
246
231
  const ocConfigPath = pathModule.join(ocHome, "openclaw.json");
247
232
  if (!fsModule.existsSync(ocConfigPath)) return;
@@ -347,43 +332,25 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
347
332
  writeTextSafe(instanceIdFile, instanceId);
348
333
 
349
334
  const cachedToken = String(readTextSafe(tokenCacheFile) || "").trim();
350
- if (cachedToken && token && token !== cachedToken) {
351
- let useConfiguredToken = false;
352
- let decisionReason = "";
353
- if (ownerToken) {
354
- useConfiguredToken = true;
355
- decisionReason = "配置携带 ownerToken(视为新的安装绑定)";
356
- } else {
357
- try {
358
- const info = await requestTokenInfo(httpBase, token);
359
- if (info?.token && String(info.token).trim() === token) {
360
- useConfiguredToken = true;
361
- decisionReason = "配置 token 在服务端存在(视为新安装)";
362
- }
363
- } catch {
364
- // 服务端不可达时保持缓存优先,避免旧配置回滚
365
- }
366
- }
367
-
368
- if (useConfiguredToken) {
369
- api.logger.warn(`[roundtable] 🔁 检测到新 token,采用配置 token(${decisionReason})`);
370
- writeTextSafe(tokenCacheFile, token);
371
- } else {
372
- api.logger.warn("[roundtable] ⚠️ 检测到配置 token 与本地缓存不一致,优先使用缓存 token");
373
- token = cachedToken;
374
- tokenSource = 'cache';
335
+ if (token) {
336
+ if (cachedToken && token !== cachedToken) {
337
+ api.logger.warn("[roundtable] 🔁 检测到配置 token 与本地缓存不一致,按配置 token 启动并覆盖缓存");
338
+ } else if (cachedToken && token === cachedToken) {
339
+ tokenSource = 'config+cache';
340
+ api.logger.info("[roundtable] 📦 命中本地 token 缓存");
375
341
  }
376
- } else if (cachedToken && !token) {
342
+ writeTextSafe(tokenCacheFile, token);
343
+ } else if (cachedToken) {
377
344
  api.logger.info("[roundtable] 📦 从本地缓存加载 token");
378
345
  token = cachedToken;
379
346
  tokenSource = 'cache';
380
- } else if (cachedToken && token === cachedToken) {
381
- api.logger.info("[roundtable] 📦 命中本地 token 缓存");
382
- tokenSource = 'config+cache';
383
347
  }
384
348
 
385
349
  const cachedOwnerToken = normalizeIdentityId(readTextSafe(ownerTokenCacheFile), 128);
386
- if (!ownerToken && cachedOwnerToken) {
350
+ if (ownerToken && cachedOwnerToken && ownerToken !== cachedOwnerToken) {
351
+ api.logger.warn("[roundtable] 🔁 检测到配置 ownerToken 与缓存不一致,按配置 ownerToken 覆盖缓存");
352
+ writeTextSafe(ownerTokenCacheFile, ownerToken);
353
+ } else if (!ownerToken && cachedOwnerToken) {
387
354
  ownerToken = cachedOwnerToken;
388
355
  api.logger.info("[roundtable] 🔗 从本地缓存恢复 ownerToken");
389
356
  }
@@ -487,6 +454,8 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
487
454
  const sessionId = normalizeIdentityId(identityCtx.sessionId, 120) || cryptoModule.randomBytes(12).toString("hex");
488
455
  const tokenSource = identityCtx.tokenSource || 'unknown';
489
456
  let recoveringToken = false;
457
+ let ackReceivedForSocket = false;
458
+ let awaitingAckTimer = null;
490
459
 
491
460
  // Token 冲突熔断器:防止无限重试导致日志爆炸和 CPU 空转
492
461
  const conflictBreaker = {
@@ -527,6 +496,29 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
527
496
  if (ownerToken) writeTextSafe(ownerTokenCacheFile, ownerToken);
528
497
  }
529
498
 
499
+ function clearAckTimer() {
500
+ if (awaitingAckTimer) {
501
+ clearTimeout(awaitingAckTimer);
502
+ awaitingAckTimer = null;
503
+ }
504
+ }
505
+
506
+ function armAckTimer() {
507
+ clearAckTimer();
508
+ if (!token || !ws || ws.readyState !== WebSocket.OPEN) return;
509
+ awaitingAckTimer = setTimeout(() => {
510
+ if (stopping || !ws || ws.readyState !== WebSocket.OPEN || ackReceivedForSocket) return;
511
+ api.logger.warn("[roundtable] ⚠️ 已连接 WS 但未收到 bot_ack,主动重连");
512
+ reportDiag("bot_ack_timeout", {
513
+ level: "warn",
514
+ message: "websocket open but bot_ack not received in time",
515
+ detail: { timeoutMs: 12000, tokenSource },
516
+ }, 0);
517
+ try { ws.close(4000, "bot-ack-timeout"); } catch { }
518
+ }, 12000);
519
+ if (awaitingAckTimer.unref) awaitingAckTimer.unref();
520
+ }
521
+
530
522
  function nextReconnectDelayMs() {
531
523
  const base = Math.min(30000, Math.round(2500 * Math.pow(1.6, reconnectAttempts)));
532
524
  reconnectAttempts += 1;
@@ -753,6 +745,8 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
753
745
 
754
746
  ws.onopen = () => {
755
747
  reconnectAttempts = 0;
748
+ ackReceivedForSocket = false;
749
+ clearAckTimer();
756
750
  reportDiag("ws_open", {
757
751
  message: token ? "connected with token" : "connected in observe mode",
758
752
  detail: { instanceId, sessionId },
@@ -781,6 +775,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
781
775
  pluginVersion: PLUGIN_VERSION,
782
776
  tokenSource: tokenSource || 'unknown',
783
777
  });
778
+ armAckTimer();
784
779
  api.logger.info(`[roundtable] 🔗 bot_connect 已发送 (token=${token.slice(0, 8)}... source=${tokenSource || 'unknown'})`);
785
780
  };
786
781
 
@@ -805,6 +800,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
805
800
  };
806
801
 
807
802
  ws.onclose = (evt) => {
803
+ clearAckTimer();
808
804
  // 断连后立即清理本地 room 状态,避免重连后状态错乱
809
805
  currentRoomId = null;
810
806
  currentRoomMode = null;
@@ -1600,6 +1596,8 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1600
1596
 
1601
1597
  switch (msg.type) {
1602
1598
  case "bot_ack":
1599
+ ackReceivedForSocket = true;
1600
+ clearAckTimer();
1603
1601
  // 在大厅待机中
1604
1602
  if (msg.status === 'lobby') {
1605
1603
  myName = msg.name || myName;
@@ -1982,6 +1980,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1982
1980
  if (heartbeatTimer) { clearInterval(heartbeatTimer); }
1983
1981
  // 停止重连
1984
1982
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
1983
+ clearAckTimer();
1985
1984
  // 关闭 WS
1986
1985
  if (ws) {
1987
1986
  try { ws.close(1000, "shutdown"); } catch { }
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.4",
8
+ "version": "3.0.5",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
@@ -67,4 +67,4 @@
67
67
  "placeholder": "龙虾"
68
68
  }
69
69
  }
70
- }
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobster-roundtable",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -41,4 +41,4 @@
41
41
  "optional": true
42
42
  }
43
43
  }
44
- }
44
+ }