lobster-roundtable 3.0.8 → 3.0.10

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
@@ -19,6 +19,8 @@ const cryptoModule = require("crypto");
19
19
  const {
20
20
  getOpenClawDir,
21
21
  isOpenClawConfigSyncEnabled,
22
+ getOpenClawApiPort,
23
+ getOpenClawApiToken,
22
24
  } = require("./src/env.js");
23
25
  const { readTextSafe, parseJsonFileFlexible } = require("./src/fileio.js");
24
26
  // Node.js 原生 HTTP 请求工具(避免依赖外部命令)
@@ -59,7 +61,7 @@ try {
59
61
  }
60
62
 
61
63
  const CHANNEL_ID = "lobster-roundtable";
62
- const PLUGIN_VERSION = "3.0.8";
64
+ const PLUGIN_VERSION = "3.0.10";
63
65
  const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
64
66
  const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
65
67
 
@@ -552,17 +554,17 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
552
554
  } catch { }
553
555
  }
554
556
 
555
- function safeDiagText(v, max = 280) {
556
- const s = String(v || "").trim();
557
- return s.length > max ? s.slice(0, max) : s;
558
- }
557
+ function safeDiagText(v, max = 280) {
558
+ const s = String(v || "").trim();
559
+ return s.length > max ? s.slice(0, max) : s;
560
+ }
559
561
 
560
- function maskTokenForDiag(raw) {
561
- const s = String(raw || "").trim();
562
- if (!s) return "";
563
- if (s.length <= 8) return s;
564
- return `${s.slice(0, 6)}...${s.slice(-4)}`;
565
- }
562
+ function maskTokenForDiag(raw) {
563
+ const s = String(raw || "").trim();
564
+ if (!s) return "";
565
+ if (s.length <= 8) return s;
566
+ return `${s.slice(0, 6)}...${s.slice(-4)}`;
567
+ }
566
568
 
567
569
  function reportDiag(event, payload = {}, throttleMs = 8000) {
568
570
  if (!event) return;
@@ -1038,12 +1040,12 @@ function maskTokenForDiag(raw) {
1038
1040
  return msg.includes('timeout');
1039
1041
  }
1040
1042
 
1041
- async function callAIWithTimeout(prompt, timeoutMs = 30000, tag = 'callAI', options = {}) {
1043
+ async function callAIWithTimeout(prompt, timeoutMs = 30000, tag = 'callAI') {
1042
1044
  const ms = Math.max(5000, parseInt(timeoutMs, 10) || 30000);
1043
1045
  let timeoutId = null;
1044
1046
  try {
1045
1047
  return await Promise.race([
1046
- callAI(api, core, myName, prompt, options),
1048
+ callAI(api, core, myName, prompt),
1047
1049
  new Promise((_, reject) => {
1048
1050
  timeoutId = setTimeout(() => reject(new Error(`${tag}_timeout_${ms}`)), ms);
1049
1051
  }),
@@ -1201,12 +1203,7 @@ function maskTokenForDiag(raw) {
1201
1203
  `SKILL_NAME: 你要分享的技能名`,
1202
1204
  `SKILL_DESC: 用中文给出可审核、可复用的分享稿(至少140字,包含痛点/步骤/案例/风险,必须有“输入->动作->结果”链路)`,
1203
1205
  ].join('\n');
1204
- const reply = String(await callAIWithTimeout(oneShotPrompt, 28000, 'pick_skill_one_shot', {
1205
- turnContext: 'pick_skill',
1206
- roomMode: 'evolution',
1207
- roomId: currentRoomId || '',
1208
- senderId: 'evo-room',
1209
- }) || '').trim();
1206
+ const reply = String(await callAIWithTimeout(oneShotPrompt, 28000, 'pick_skill_one_shot') || '').trim();
1210
1207
  if (!reply || (/NONE/i.test(reply) && !/SKILL_NAME/i.test(reply))) {
1211
1208
  api.logger.info(`[roundtable] 🧬 ${source} 兜底:无可分享技能,降级经验分享`);
1212
1209
  send({ type: 'skill_picked', noSkill: true, reason: 'experience' });
@@ -1731,11 +1728,7 @@ function maskTokenForDiag(raw) {
1731
1728
  case "prompt": {
1732
1729
  const turnContext = String(msg.turnContext || '').trim();
1733
1730
  const promptText = String(msg.text || '').trim();
1734
- const routeOptions = {
1735
- turnContext,
1736
- roomMode: currentRoomMode || String(msg.roomMode || ''),
1737
- roomId: currentRoomId || String(msg.roomId || ''),
1738
- };
1731
+
1739
1732
  if (!promptText) {
1740
1733
  const syntheticPrompt = [
1741
1734
  `你在龙虾房间中,收到一个缺失正文的阶段提示(turnContext=${turnContext || 'generic'})。`,
@@ -1746,8 +1739,7 @@ function maskTokenForDiag(raw) {
1746
1739
  const retry = String(await callAIWithTimeout(
1747
1740
  syntheticPrompt,
1748
1741
  Math.max(7000, Math.min(12000, getPromptTimeoutMs(turnContext))),
1749
- `prompt_empty_rebuild_${turnContext || 'generic'}`,
1750
- routeOptions
1742
+ `prompt_empty_rebuild_${turnContext || 'generic'}`
1751
1743
  ) || '').trim();
1752
1744
  if (retry) {
1753
1745
  send({ type: 'bot_reply', text: retry });
@@ -1782,8 +1774,7 @@ function maskTokenForDiag(raw) {
1782
1774
  const reply = await callAIWithTimeout(
1783
1775
  promptText,
1784
1776
  timeoutMs,
1785
- `prompt_${turnContext || 'generic'}`,
1786
- routeOptions
1777
+ `prompt_${turnContext || 'generic'}`
1787
1778
  );
1788
1779
  const normalized = String(reply || '').trim();
1789
1780
  if (normalized) {
@@ -1795,8 +1786,7 @@ function maskTokenForDiag(raw) {
1795
1786
  retried = String(await callAIWithTimeout(
1796
1787
  promptText,
1797
1788
  Math.max(6000, Math.min(12000, Math.floor(timeoutMs * 0.55))),
1798
- `prompt_retry_${turnContext || 'generic'}`,
1799
- routeOptions
1789
+ `prompt_retry_${turnContext || 'generic'}`
1800
1790
  ) || '').trim();
1801
1791
  } catch { }
1802
1792
  if (retried) {
@@ -1826,8 +1816,7 @@ function maskTokenForDiag(raw) {
1826
1816
  recovered = String(await callAIWithTimeout(
1827
1817
  promptText,
1828
1818
  Math.max(6000, Math.min(14000, Math.floor(timeoutMs * 0.6))),
1829
- `prompt_timeout_retry_${turnContext || 'generic'}`,
1830
- routeOptions
1819
+ `prompt_timeout_retry_${turnContext || 'generic'}`
1831
1820
  ) || '').trim();
1832
1821
  } catch { }
1833
1822
  }
@@ -1972,12 +1961,7 @@ function maskTokenForDiag(raw) {
1972
1961
  `任一条件不满足 -> 不通过。`,
1973
1962
  `只需回复 "通过" 或 "不通过",不要回复其他内容。`,
1974
1963
  ].join("\n");
1975
- const voteReply = await callAI(api, core, myName, votePrompt, {
1976
- turnContext: 'evo_vote',
1977
- roomMode: 'evolution',
1978
- roomId: currentRoomId || '',
1979
- senderId: 'evo-room',
1980
- });
1964
+ const voteReply = await callAI(api, core, myName, votePrompt);
1981
1965
  const decision = String(voteReply || '').trim();
1982
1966
  const rejected = /不通过|否决|拒绝|不建议/.test(decision);
1983
1967
  const approve = !rejected && /通过/.test(decision);
@@ -2230,20 +2214,21 @@ function resolveGatewayHttpOptions(api) {
2230
2214
  const cfg = (api && typeof api.config === "object" && api.config) ? api.config : {};
2231
2215
  const apiCfg = (cfg.api && typeof cfg.api === "object") ? cfg.api : {};
2232
2216
  const gatewayCfg = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway : {};
2233
- const listenCfg = (cfg.listen && typeof cfg.listen === "object") ? cfg.listen : {};
2234
- const portCandidates = [apiCfg.port, gatewayCfg.port, listenCfg.port];
2235
- let port = 18789;
2236
- for (const candidate of portCandidates) {
2237
- const parsed = parseInt(candidate, 10);
2238
- if (Number.isFinite(parsed) && parsed > 0) {
2239
- port = parsed;
2240
- break;
2241
- }
2242
- }
2243
- return {
2244
- port,
2245
- token: String(apiCfg.token || "").trim(),
2246
- };
2217
+
2218
+ // 端口:环境变量 > config.api.port > config.gateway.port > config.listen.port > 默认 18789
2219
+ const envPort = getOpenClawApiPort();
2220
+ const cfgPort = parseInt(apiCfg.port || gatewayCfg.port || cfg.listen?.port, 10);
2221
+ const port = (envPort !== 18789) ? envPort
2222
+ : (Number.isFinite(cfgPort) && cfgPort > 0) ? cfgPort
2223
+ : 18789;
2224
+
2225
+ // Token:环境变量 > config.api.token > config.gateway.auth.token
2226
+ const envToken = getOpenClawApiToken();
2227
+ const cfgApiToken = String(apiCfg.token || "").trim();
2228
+ const cfgGatewayToken = String(gatewayCfg.auth?.token || "").trim();
2229
+ const token = envToken || cfgApiToken || cfgGatewayToken;
2230
+
2231
+ return { port, token };
2247
2232
  }
2248
2233
 
2249
2234
  /**
@@ -2309,199 +2294,13 @@ function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000, httpOptions =
2309
2294
  });
2310
2295
  }
2311
2296
 
2312
- function pushRuntimeRouteCandidate(list, candidate) {
2313
- if (!candidate) return;
2314
- const sessionKey = String(candidate.sessionKey || '').trim();
2315
- if (!sessionKey) return;
2316
- const chatType = String(candidate.chatType || 'direct').trim().toLowerCase() === 'group' ? 'group' : 'direct';
2317
- const peerId = String(candidate.peerId || '').trim();
2318
- const dedupeKey = `${sessionKey}|${chatType}|${peerId}`;
2319
- if (list.some((x) => x.__k === dedupeKey)) return;
2320
- list.push({ ...candidate, chatType, peerId, __k: dedupeKey });
2321
- }
2322
-
2323
- function resolveRuntimeRouteCandidates(api, core, senderId, options = {}) {
2324
- const candidates = [];
2325
- const resolver = core?.channel?.routing?.resolveAgentRoute;
2326
- const accountId = String(api?.accountId || "default");
2327
- const sender = String(senderId || "roundtable-host").trim() || "roundtable-host";
2328
- const roomId = String(options.roomId || '').trim();
2329
- const roomMode = String(options.roomMode || '').trim().toLowerCase();
2330
-
2331
- const peerCandidates = [];
2332
- if (roomId) peerCandidates.push({ kind: "group", id: roomId, chatType: "group" });
2333
- peerCandidates.push({ kind: "direct", id: sender, chatType: "direct" });
2334
- peerCandidates.push({ kind: "group", id: sender, chatType: "group" });
2335
- if (roomMode) peerCandidates.push({ kind: "group", id: `${CHANNEL_ID}:${roomMode}`, chatType: "group" });
2336
-
2337
- if (typeof resolver === "function") {
2338
- for (const peer of peerCandidates) {
2339
- try {
2340
- const route = resolver({
2341
- cfg: api?.config,
2342
- channel: CHANNEL_ID,
2343
- accountId,
2344
- peer: { kind: peer.kind, id: peer.id },
2345
- });
2346
- pushRuntimeRouteCandidate(candidates, {
2347
- source: "resolver",
2348
- sessionKey: route?.sessionKey,
2349
- accountId: String(route?.accountId || accountId),
2350
- chatType: peer.chatType,
2351
- peerId: peer.id,
2352
- });
2353
- } catch (err) {
2354
- api?.logger?.warn?.(
2355
- `[roundtable] resolveAgentRoute(${peer.kind}:${peer.id}) 失败: ${err?.message || err}`
2356
- );
2357
- }
2358
- }
2359
- }
2360
-
2361
- if (roomId) {
2362
- pushRuntimeRouteCandidate(candidates, {
2363
- source: "fallback",
2364
- sessionKey: `roundtable:room:${roomId}`,
2365
- accountId,
2366
- chatType: "group",
2367
- peerId: roomId,
2368
- });
2369
- }
2370
- pushRuntimeRouteCandidate(candidates, {
2371
- source: "fallback",
2372
- sessionKey: `roundtable:${sender}`,
2373
- accountId,
2374
- chatType: "direct",
2375
- peerId: sender,
2376
- });
2377
-
2378
- return candidates.map(({ __k, ...x }) => x);
2379
- }
2380
-
2381
- async function dispatchRuntimeOnce(api, core, prompt, sessionLabel, senderName, senderId, routeCandidate, attemptIndex = 0) {
2382
- const sessionKey = String(routeCandidate?.sessionKey || `roundtable:${senderId}`);
2383
- const accountId = String(routeCandidate?.accountId || api?.accountId || "default");
2384
- const chatType = String(routeCandidate?.chatType || 'direct').toLowerCase() === 'group' ? 'group' : 'direct';
2385
- const peerId = String(routeCandidate?.peerId || senderId || 'roundtable-host');
2386
- const timestamp = Date.now();
2387
- const toAddress = chatType === 'group'
2388
- ? `${CHANNEL_ID}:group:${peerId}`
2389
- : `${CHANNEL_ID}:bot:${peerId}`;
2390
-
2391
- const ctxPayload = core.channel.reply.finalizeInboundContext({
2392
- Body: prompt,
2393
- BodyForAgent: prompt,
2394
- RawBody: prompt,
2395
- CommandBody: prompt,
2396
- From: `${CHANNEL_ID}:server`,
2397
- To: toAddress,
2398
- SessionKey: sessionKey,
2399
- AccountId: accountId,
2400
- ChatType: chatType,
2401
- ConversationLabel: sessionLabel,
2402
- SenderName: senderName,
2403
- SenderId: senderId,
2404
- GroupSubject: chatType === 'group' ? peerId : undefined,
2405
- Provider: CHANNEL_ID,
2406
- Surface: CHANNEL_ID,
2407
- MessageSid: `rt-${timestamp}-${attemptIndex}`,
2408
- Timestamp: timestamp,
2409
- OriginatingChannel: CHANNEL_ID,
2410
- OriginatingTo: toAddress,
2411
- CommandAuthorized: true,
2412
- });
2413
-
2414
- const finalParts = [];
2415
- const blockParts = [];
2416
- const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2417
- ctx: ctxPayload,
2418
- cfg: api.config,
2419
- dispatcherOptions: {
2420
- deliver: async (payload, info) => {
2421
- const text = typeof payload?.text === "string" ? payload.text.trim() : "";
2422
- if (!text) return;
2423
- if (info?.kind === "block") blockParts.push(text);
2424
- else finalParts.push(text);
2425
- },
2426
- onError: (err, info) => {
2427
- api.logger.error(`[roundtable] dispatch ${info.kind} 失败: ${String(err)}`);
2428
- },
2429
- },
2430
- });
2431
-
2432
- const finalReply = finalParts.join("\n").trim();
2433
- if (finalReply) return finalReply;
2434
-
2435
- const blockReply = blockParts.join("\n").trim();
2436
- if (blockReply) return blockReply;
2437
-
2438
- if (!dispatchResult?.queuedFinal) {
2439
- const counts = dispatchResult?.counts ? JSON.stringify(dispatchResult.counts) : "{}";
2440
- throw new Error(`runtime_no_reply_queued counts=${counts}`);
2441
- }
2442
- throw new Error("runtime_empty_reply queuedFinal=true");
2443
- }
2444
-
2445
- /**
2446
- * 通过 runtime 内部管线调 AI(优先路径,在支持的 OpenClaw 版本上使用)
2447
- */
2448
- async function callAIViaRuntime(
2449
- api,
2450
- core,
2451
- prompt,
2452
- sessionLabel = '龙虾圆桌',
2453
- senderName = '圆桌主持人',
2454
- senderId = 'roundtable-host',
2455
- options = {}
2456
- ) {
2457
- const routeCandidates = resolveRuntimeRouteCandidates(api, core, senderId, options);
2458
- const failures = [];
2459
-
2460
- for (let i = 0; i < routeCandidates.length; i++) {
2461
- const candidate = routeCandidates[i];
2462
- try {
2463
- const reply = await dispatchRuntimeOnce(
2464
- api,
2465
- core,
2466
- prompt,
2467
- sessionLabel,
2468
- senderName,
2469
- senderId,
2470
- candidate,
2471
- i + 1
2472
- );
2473
- if (i > 0) {
2474
- const diag = (api && typeof api.__rtReportDiag === 'function') ? api.__rtReportDiag : null;
2475
- diag?.("runtime_route_retry_success", {
2476
- level: "warn",
2477
- message: `runtime recovered on candidate #${i + 1}`,
2478
- detail: {
2479
- attempt: i + 1,
2480
- source: candidate.source || 'unknown',
2481
- chatType: candidate.chatType,
2482
- peerId: candidate.peerId,
2483
- },
2484
- }, 1200);
2485
- }
2486
- return reply;
2487
- } catch (err) {
2488
- const reason = String(err?.message || err);
2489
- failures.push(`#${i + 1}(${candidate.chatType}:${candidate.peerId || 'na'}): ${reason}`);
2490
- }
2491
- }
2492
-
2493
- throw new Error(`runtime_all_candidates_failed ${failures.join(' | ')}`);
2494
- }
2495
-
2496
2297
  /**
2497
- * 核心 AI 调用:优先 runtime API 降级 HTTP API
2298
+ * 核心 AI 调用:直接通过 Gateway HTTP API (/v1/chat/completions) 调用 LLM
2498
2299
  * 构造圆桌讨论的完整 prompt,让 Gateway 用用户配置的 AI 模型生成回复
2499
2300
  */
2500
2301
  async function generateReply(api, core, myName, persona, maxTokens, msg, factualContext = '') {
2501
- // 兼容服务器字段:可能是 msg.history 或 msg.context
2502
2302
  const rawHistory = msg.history || parseContext(msg.context);
2503
2303
  let systemPrompt = buildSystemPrompt(myName, persona, msg.topic, msg.roomMode);
2504
- // 融合服务器下发的模式/角色提示(辩论正反方、进化评审等)
2505
2304
  if (msg.modePrompt) {
2506
2305
  systemPrompt += "\n\n" + msg.modePrompt;
2507
2306
  }
@@ -2511,95 +2310,14 @@ async function generateReply(api, core, myName, persona, maxTokens, msg, factual
2511
2310
  : '';
2512
2311
 
2513
2312
  const fullPrompt = `${systemPrompt}${memoryBlock}\n\n---\n\n${userMessage}`;
2514
-
2515
- // 优先 runtime API → 降级 HTTP API
2516
- const hasRuntime = !!(core?.channel?.reply?.finalizeInboundContext &&
2517
- core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher);
2518
-
2519
- if (hasRuntime) {
2520
- try {
2521
- return await callAIViaRuntime(
2522
- api,
2523
- core,
2524
- fullPrompt,
2525
- '龙虾圆桌',
2526
- '圆桌主持人',
2527
- 'roundtable-host',
2528
- {
2529
- roomMode: String(msg?.roomMode || ''),
2530
- roomId: String(msg?.roomId || msg?.room?.id || ''),
2531
- }
2532
- );
2533
- } catch (err) {
2534
- api.logger.warn(`[roundtable] runtime API 调用失败,降级 HTTP: ${err.message}`);
2535
- const diag = (api && typeof api.__rtReportDiag === 'function') ? api.__rtReportDiag : null;
2536
- diag?.("runtime_to_http_fallback", {
2537
- level: "warn",
2538
- message: `generate_reply runtime failed: ${err.message}`,
2539
- reason: String(err.message || err),
2540
- detail: {
2541
- phase: "generate_reply",
2542
- roomMode: String(msg?.roomMode || ''),
2543
- topicLen: String(msg?.topic || '').length,
2544
- },
2545
- }, 1200);
2546
- }
2547
- }
2548
-
2549
2313
  return await callAIViaHTTP(fullPrompt, maxTokens || 500, 65000, resolveGatewayHttpOptions(api));
2550
2314
  }
2551
2315
 
2552
2316
  /**
2553
2317
  * 精简版 AI 调用:给一个 prompt,拿一个回复
2554
- * 用于进化室选 Skill 等不需要完整对话上下文的场景
2555
- * 优先 runtime API → 降级 HTTP API
2318
+ * 用于进化室选 Skill、评审、投票等不需要完整对话上下文的场景
2319
+ * 直接通过 Gateway HTTP API 调用
2556
2320
  */
2557
- async function callAI(api, core, myName, prompt, options = {}) {
2558
- const turnContext = String(options.turnContext || '').trim().toLowerCase();
2559
- const senderId = (() => {
2560
- const explicit = String(options.senderId || '').trim();
2561
- if (explicit) return explicit;
2562
- if (turnContext === 'pick_skill' || turnContext.startsWith('evo_')) return 'evo-room';
2563
- if (turnContext.startsWith('debate_')) return 'debate-room';
2564
- if (turnContext.startsWith('host_')) return 'roundtable-host';
2565
- return 'roundtable-host';
2566
- })();
2567
- const sessionLabel = String(options.sessionLabel || '龙虾圆桌·选Skill');
2568
- const hasRuntime = !!(core?.channel?.reply?.finalizeInboundContext &&
2569
- core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher);
2570
-
2571
- if (hasRuntime) {
2572
- try {
2573
- return await callAIViaRuntime(
2574
- api,
2575
- core,
2576
- prompt,
2577
- sessionLabel,
2578
- '进化室',
2579
- senderId,
2580
- {
2581
- turnContext,
2582
- roomMode: String(options.roomMode || ''),
2583
- roomId: String(options.roomId || ''),
2584
- }
2585
- );
2586
- } catch (err) {
2587
- api.logger.warn(`[roundtable] callAI runtime 失败,降级 HTTP: ${err.message}`);
2588
- const diag = (api && typeof api.__rtReportDiag === 'function') ? api.__rtReportDiag : null;
2589
- diag?.("runtime_to_http_fallback", {
2590
- level: "warn",
2591
- message: `call_ai runtime failed: ${err.message}`,
2592
- reason: String(err.message || err),
2593
- detail: {
2594
- phase: "call_ai",
2595
- promptLen: String(prompt || '').length,
2596
- turnContext,
2597
- roomMode: String(options.roomMode || ''),
2598
- roomId: String(options.roomId || ''),
2599
- },
2600
- }, 1200);
2601
- }
2602
- }
2603
-
2321
+ async function callAI(api, core, myName, prompt) {
2604
2322
  return await callAIViaHTTP(prompt, 500, 30000, resolveGatewayHttpOptions(api));
2605
2323
  }
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.8",
8
+ "version": "3.0.10",
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.8",
3
+ "version": "3.0.10",
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
+ }