lobster-roundtable 3.0.2 → 3.0.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/index.js CHANGED
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  const fs = require("fs");
11
- const os = require("os");
12
11
  const path = require("path");
12
+ const { getOpenClawDir } = require("./src/env.js");
13
13
 
14
14
  const PLUGIN_ID = "lobster-roundtable";
15
15
  const PLUGIN_NAME = "龙虾圆桌";
@@ -19,7 +19,7 @@ const PLUGIN_NAME = "龙虾圆桌";
19
19
  */
20
20
  function cleanLegacyInstanceIdFromConfig(logger) {
21
21
  try {
22
- const ocDir = process.env.OPENCLAW_DIR || path.join(os.homedir(), ".openclaw");
22
+ const ocDir = getOpenClawDir();
23
23
  const configPath = path.join(ocDir, "openclaw.json");
24
24
  if (!fs.existsSync(configPath)) return false;
25
25
  const raw = fs.readFileSync(configPath, "utf-8");
package/main.js CHANGED
@@ -16,7 +16,13 @@ const httpModule = require("http");
16
16
  const httpsModule = require("https");
17
17
  const fsModule = require("fs");
18
18
  const cryptoModule = require("crypto");
19
- // Node.js 原生 HTTP 请求工具(替代 curl,避免 child_process 安全警告)
19
+ const {
20
+ getOpenClawDir,
21
+ isOpenClawConfigSyncEnabled,
22
+ getOpenClawApiPort,
23
+ getOpenClawApiToken,
24
+ } = require("./src/env.js");
25
+ // Node.js 原生 HTTP 请求工具(避免依赖外部命令)
20
26
  function httpRequest(urlStr, options = {}) {
21
27
  return new Promise((resolve, reject) => {
22
28
  const url = new URL(urlStr);
@@ -46,7 +52,7 @@ try {
46
52
  WebSocket = require("ws");
47
53
  } catch {
48
54
  try {
49
- const ocDir = process.env.OPENCLAW_DIR || os.homedir() + "/.openclaw";
55
+ const ocDir = getOpenClawDir();
50
56
  WebSocket = require(pathModule.join(ocDir, "node_modules", "ws"));
51
57
  } catch {
52
58
  // 最后的后备
@@ -54,8 +60,8 @@ try {
54
60
  }
55
61
 
56
62
  const CHANNEL_ID = "lobster-roundtable";
57
- const PLUGIN_VERSION = "3.0.2";
58
- const ENABLE_OPENCLAW_CONFIG_SYNC = String(process.env.LOBSTER_RT_SYNC_OPENCLAW || "").trim() === "1";
63
+ const PLUGIN_VERSION = "3.0.4";
64
+ const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
59
65
  const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
60
66
 
61
67
  function normalizeIdentityId(raw, max = 96) {
@@ -65,7 +71,7 @@ function normalizeIdentityId(raw, max = 96) {
65
71
  }
66
72
 
67
73
  function buildOpenClawHome() {
68
- return process.env.OPENCLAW_DIR || pathModule.join(os.homedir(), ".openclaw");
74
+ return getOpenClawDir();
69
75
  }
70
76
 
71
77
  function sanitizeSkillDirName(name) {
@@ -316,6 +322,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
316
322
  const configuredName = String(cfg.name || "").trim();
317
323
  let token = String(cfg.token || "").trim();
318
324
  let ownerToken = normalizeIdentityId(cfg.ownerToken, 128);
325
+ let tokenSource = token ? 'config' : 'none'; // 追踪 token 来源
319
326
  const persona = String(cfg.persona || "").trim();
320
327
  const maxTokens = cfg.maxTokens || 150;
321
328
 
@@ -364,12 +371,15 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
364
371
  } else {
365
372
  api.logger.warn("[roundtable] ⚠️ 检测到配置 token 与本地缓存不一致,优先使用缓存 token");
366
373
  token = cachedToken;
374
+ tokenSource = 'cache';
367
375
  }
368
376
  } else if (cachedToken && !token) {
369
377
  api.logger.info("[roundtable] 📦 从本地缓存加载 token");
370
378
  token = cachedToken;
379
+ tokenSource = 'cache';
371
380
  } else if (cachedToken && token === cachedToken) {
372
381
  api.logger.info("[roundtable] 📦 命中本地 token 缓存");
382
+ tokenSource = 'config+cache';
373
383
  }
374
384
 
375
385
  const cachedOwnerToken = normalizeIdentityId(readTextSafe(ownerTokenCacheFile), 128);
@@ -392,6 +402,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
392
402
  });
393
403
  if (resp.token) {
394
404
  token = resp.token;
405
+ tokenSource = 'auto-register';
395
406
  if (resp.ownerToken) ownerToken = normalizeIdentityId(resp.ownerToken, 128) || ownerToken;
396
407
  api.logger.info(`[roundtable] 🔑 自动注册成功!名称: ${resp.name}${resp.reused ? '(复用)' : '(新建)'}`);
397
408
  writeTextSafe(tokenCacheFile, token);
@@ -418,7 +429,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
418
429
  if (!ENABLE_OPENCLAW_CONFIG_SYNC) {
419
430
  api.logger.info("[roundtable] 配置同步已禁用:仅写本地 token 缓存,避免触发 gateway 重启");
420
431
  }
421
- api.logger.info(`[roundtable] v${PLUGIN_VERSION} 启动(Channel 模式)`);
432
+ api.logger.info(`[roundtable] v${PLUGIN_VERSION} 启动(Channel 模式)token=${token ? token.slice(0, 8) + '...' : '无'} source=${tokenSource}`);
422
433
  return startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFile, {
423
434
  ocHome,
424
435
  instanceId,
@@ -428,6 +439,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
428
439
  wsScope,
429
440
  ownerToken,
430
441
  ownerTokenCacheFile,
442
+ tokenSource,
431
443
  });
432
444
  };
433
445
 
@@ -473,6 +485,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
473
485
  normalizeIdentityId(readTextSafe(ownerTokenCacheFile), 128);
474
486
  let instanceId = normalizeIdentityId(identityCtx.instanceId) || normalizeIdentityId(readTextSafe(instanceIdFile)) || cryptoModule.randomBytes(18).toString("hex");
475
487
  const sessionId = normalizeIdentityId(identityCtx.sessionId, 120) || cryptoModule.randomBytes(12).toString("hex");
488
+ const tokenSource = identityCtx.tokenSource || 'unknown';
476
489
  let recoveringToken = false;
477
490
 
478
491
  // Token 冲突熔断器:防止无限重试导致日志爆炸和 CPU 空转
@@ -765,7 +778,10 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
765
778
  sessionId,
766
779
  skillNames: installedSkills,
767
780
  roomId: rejoinRoom || undefined,
781
+ pluginVersion: PLUGIN_VERSION,
782
+ tokenSource: tokenSource || 'unknown',
768
783
  });
784
+ api.logger.info(`[roundtable] 🔗 bot_connect 已发送 (token=${token.slice(0, 8)}... source=${tokenSource || 'unknown'})`);
769
785
  };
770
786
 
771
787
  ws.onmessage = async (event) => {
@@ -1605,6 +1621,23 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1605
1621
  api.logger.info(
1606
1622
  `[roundtable] 🦞 已连接大厅,待机中。可用房间: ${(msg.rooms || []).map(r => r.id).join(', ')}`
1607
1623
  );
1624
+ // ═══ 连接健康报告 ═══
1625
+ api.logger.info([
1626
+ `[roundtable] ═══ 连接健康报告 ═══`,
1627
+ ` 插件版本: v${PLUGIN_VERSION}`,
1628
+ ` Bot 名称: ${myName}`,
1629
+ ` Token: ${token ? token.slice(0, 8) + '...' : '无'}`,
1630
+ ` Token 来源: ${tokenSource}`,
1631
+ ` Instance: ${instanceId ? instanceId.slice(0, 8) + '...' : '无'}`,
1632
+ ` Session: ${sessionId ? sessionId.slice(0, 8) + '...' : '无'}`,
1633
+ ` 服务端地址: ${wsUrl}`,
1634
+ ` 服务端确认名: ${msg.name || '(未返回)'}`,
1635
+ ` 服务端时间: ${msg.serverTime || '(未返回)'}`,
1636
+ ` 可用房间: ${(msg.rooms || []).length} 个`,
1637
+ ` Runtime API: ${core?.channel?.reply ? '✅' : '❌ (HTTP 降级)'}`,
1638
+ ` 状态: ✅ 在线`,
1639
+ `[roundtable] ═══════════════════`,
1640
+ ].join('\n'));
1608
1641
  rememberFact('回到大厅待机');
1609
1642
  send({ type: 'bot_status', status: 'lobby', text: '☕ 大厅待机中' });
1610
1643
  if (autonomyEnabled) scheduleAutonomyTick(900);
@@ -2031,8 +2064,8 @@ function parseContext(context) {
2031
2064
  */
2032
2065
  function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000) {
2033
2066
  return new Promise((resolve, reject) => {
2034
- const OC_PORT = parseInt(process.env.OPENCLAW_PORT || '18789', 10);
2035
- const OC_TOKEN = String(process.env.OPENCLAW_API_TOKEN || process.env.OPENCLAW_TOKEN || '').trim();
2067
+ const OC_PORT = getOpenClawApiPort();
2068
+ const OC_TOKEN = getOpenClawApiToken();
2036
2069
  const body = JSON.stringify({
2037
2070
  model: 'default',
2038
2071
  messages: [{ role: 'user', content: String(prompt || '') }],
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.2",
8
+ "version": "3.0.4",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobster-roundtable",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
5
5
  "license": "MIT",
6
6
  "private": false,
package/src/channel.js CHANGED
@@ -146,79 +146,81 @@ const roundtablePlugin = {
146
146
  * - 必须保持 Promise 挂起,直�?abortSignal 触发�?resolve
147
147
  * - 参照 nextcloud-talk/src/channel.ts L337 的做�?
148
148
  */
149
- startAccount: (ctx) => {
150
- return new Promise((resolve, reject) => {
151
- let bot = null;
152
-
153
- try {
154
- const initRoundtable = require("../main.js");
155
- const apiAdapter = buildApiAdapter(ctx);
156
- const core = ctx.runtime;
157
-
158
- const hasRuntimeAPI = !!(
159
- core?.channel?.reply?.finalizeInboundContext &&
160
- core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher
161
- );
162
-
163
- if (!hasRuntimeAPI) {
164
- ctx.log?.info?.(
165
- "[roundtable] runtime API 不可用,将使�?Gateway HTTP API 调用 AI(兼容模式)"
166
- );
167
- }
168
-
169
- ctx.log?.info?.("[roundtable] Gateway 正在启动龙虾圆桌...");
170
-
171
- bot = await initRoundtable(apiAdapter, core, hasRuntimeAPI);
172
-
173
- if (!bot || typeof bot.stop !== "function") {
174
- const err = new Error("[roundtable] initRoundtable 未返回有效的 { stop } 对象");
175
- ctx.setStatus?.({
176
- accountId: ctx.accountId || "default",
177
- running: false,
178
- lastError: err.message,
179
- });
180
- reject(err);
181
- return;
182
- }
183
-
184
- ctx.setStatus?.({
185
- accountId: ctx.accountId || "default",
186
- running: true,
187
- lastStartAt: Date.now(),
188
- });
189
-
190
- } catch (err) {
191
- ctx.log?.error?.(`[roundtable] 启动失败: ${err.message}`);
192
- ctx.setStatus?.({
193
- accountId: ctx.accountId || "default",
194
- running: false,
195
- lastError: err.message,
196
- });
197
- reject(err);
198
- return;
199
- }
200
-
201
- // 核心:Promise 保持 pending,直�?abortSignal 触发�?resolve
202
- // 这样 Gateway 不会认为通道退出并触发重启循环
203
- const onAbort = () => {
204
- ctx.log?.info?.("[roundtable] 收到 Gateway abortSignal,正在停�?..");
205
- if (bot) bot.stop();
206
- ctx.setStatus?.({
207
- accountId: ctx.accountId || "default",
208
- running: false,
209
- lastStopAt: Date.now(),
210
- });
211
- resolve({ stop: () => { } }); // resolve �?通道正常退�?
212
- };
213
-
214
- if (ctx.abortSignal?.aborted) {
215
- onAbort();
216
- } else if (ctx.abortSignal) {
217
- ctx.abortSignal.addEventListener("abort", onAbort, { once: true });
218
- }
219
- // 如果没有 abortSignal,Promise 永远 pending �?通道永远运行(符合预期)
220
- });
221
- },
149
+ startAccount: async (ctx) => {
150
+ let bot = null;
151
+
152
+ try {
153
+ const initRoundtable = require("../main.js");
154
+ const apiAdapter = buildApiAdapter(ctx);
155
+ const core = ctx.runtime;
156
+
157
+ const hasRuntimeAPI = !!(
158
+ core?.channel?.reply?.finalizeInboundContext &&
159
+ core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher
160
+ );
161
+
162
+ if (!hasRuntimeAPI) {
163
+ ctx.log?.info?.(
164
+ "[roundtable] runtime API 不可用,将使�?Gateway HTTP API 调用 AI(兼容模式)"
165
+ );
166
+ }
167
+
168
+ ctx.log?.info?.("[roundtable] Gateway 正在启动龙虾圆桌...");
169
+
170
+ bot = await initRoundtable(apiAdapter, core, hasRuntimeAPI);
171
+
172
+ if (!bot || typeof bot.stop !== "function") {
173
+ throw new Error("[roundtable] initRoundtable 未返回有效的 { stop } 对象");
174
+ }
175
+
176
+ ctx.setStatus?.({
177
+ accountId: ctx.accountId || "default",
178
+ running: true,
179
+ lastStartAt: Date.now(),
180
+ });
181
+ } catch (err) {
182
+ ctx.log?.error?.(`[roundtable] 启动失败: ${err.message}`);
183
+ ctx.setStatus?.({
184
+ accountId: ctx.accountId || "default",
185
+ running: false,
186
+ lastError: err.message,
187
+ });
188
+ throw err;
189
+ }
190
+
191
+ // 核心:Promise 保持 pending,直�?abortSignal 触发后 resolve
192
+ // 这样 Gateway 不会认为通道退出并触发重启循环
193
+ await new Promise((resolve) => {
194
+ const onAbort = () => {
195
+ ctx.log?.info?.("[roundtable] 收到 Gateway abortSignal,正在停�?..");
196
+ try {
197
+ bot?.stop?.();
198
+ } catch (err) {
199
+ ctx.log?.warn?.(`[roundtable] 停止 bot 时出现错误: ${err.message}`);
200
+ }
201
+ ctx.setStatus?.({
202
+ accountId: ctx.accountId || "default",
203
+ running: false,
204
+ lastStopAt: Date.now(),
205
+ });
206
+ resolve();
207
+ };
208
+
209
+ if (ctx.abortSignal?.aborted) {
210
+ onAbort();
211
+ return;
212
+ }
213
+
214
+ if (ctx.abortSignal) {
215
+ ctx.abortSignal.addEventListener("abort", onAbort, { once: true });
216
+ return;
217
+ }
218
+
219
+ // 如果没有 abortSignal,保持 pending:通道保持运行态
220
+ });
221
+
222
+ return { stop: () => { } };
223
+ },
222
224
  },
223
225
  };
224
226
 
package/src/env.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ function readEnv(name) {
7
+ if (!name) return "";
8
+ return String(process.env[name] || "");
9
+ }
10
+
11
+ function getOpenClawDir() {
12
+ return readEnv("OPENCLAW_DIR") || path.join(os.homedir(), ".openclaw");
13
+ }
14
+
15
+ function isOpenClawConfigSyncEnabled() {
16
+ return readEnv("LOBSTER_RT_SYNC_OPENCLAW").trim() === "1";
17
+ }
18
+
19
+ function getOpenClawApiPort() {
20
+ const parsed = parseInt(readEnv("OPENCLAW_PORT") || "18789", 10);
21
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 18789;
22
+ }
23
+
24
+ function getOpenClawApiToken() {
25
+ return (readEnv("OPENCLAW_API_TOKEN") || readEnv("OPENCLAW_TOKEN")).trim();
26
+ }
27
+
28
+ module.exports = {
29
+ getOpenClawDir,
30
+ isOpenClawConfigSyncEnabled,
31
+ getOpenClawApiPort,
32
+ getOpenClawApiToken,
33
+ };