lobster-roundtable 3.0.8 → 3.0.9

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.9";
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);
@@ -2229,21 +2213,20 @@ function parseContext(context) {
2229
2213
  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
- 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
- };
2216
+
2217
+ // 端口:环境变量 > config.api.port > config.gateway.port > config.listen.port > 默认 18789
2218
+ const envPort = getOpenClawApiPort();
2219
+ const cfgPort = parseInt(apiCfg.port || cfg.gateway?.port || cfg.listen?.port, 10);
2220
+ const port = (envPort !== 18789) ? envPort
2221
+ : (Number.isFinite(cfgPort) && cfgPort > 0) ? cfgPort
2222
+ : 18789;
2223
+
2224
+ // Token:环境变量 > config.api.token
2225
+ const envToken = getOpenClawApiToken();
2226
+ const cfgToken = String(apiCfg.token || "").trim();
2227
+ const token = envToken || cfgToken;
2228
+
2229
+ return { port, token };
2247
2230
  }
2248
2231
 
2249
2232
  /**
@@ -2309,199 +2292,13 @@ function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000, httpOptions =
2309
2292
  });
2310
2293
  }
2311
2294
 
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
2295
  /**
2497
- * 核心 AI 调用:优先 runtime API 降级 HTTP API
2296
+ * 核心 AI 调用:直接通过 Gateway HTTP API (/v1/chat/completions) 调用 LLM
2498
2297
  * 构造圆桌讨论的完整 prompt,让 Gateway 用用户配置的 AI 模型生成回复
2499
2298
  */
2500
2299
  async function generateReply(api, core, myName, persona, maxTokens, msg, factualContext = '') {
2501
- // 兼容服务器字段:可能是 msg.history 或 msg.context
2502
2300
  const rawHistory = msg.history || parseContext(msg.context);
2503
2301
  let systemPrompt = buildSystemPrompt(myName, persona, msg.topic, msg.roomMode);
2504
- // 融合服务器下发的模式/角色提示(辩论正反方、进化评审等)
2505
2302
  if (msg.modePrompt) {
2506
2303
  systemPrompt += "\n\n" + msg.modePrompt;
2507
2304
  }
@@ -2511,95 +2308,14 @@ async function generateReply(api, core, myName, persona, maxTokens, msg, factual
2511
2308
  : '';
2512
2309
 
2513
2310
  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
2311
  return await callAIViaHTTP(fullPrompt, maxTokens || 500, 65000, resolveGatewayHttpOptions(api));
2550
2312
  }
2551
2313
 
2552
2314
  /**
2553
2315
  * 精简版 AI 调用:给一个 prompt,拿一个回复
2554
- * 用于进化室选 Skill 等不需要完整对话上下文的场景
2555
- * 优先 runtime API → 降级 HTTP API
2316
+ * 用于进化室选 Skill、评审、投票等不需要完整对话上下文的场景
2317
+ * 直接通过 Gateway HTTP API 调用
2556
2318
  */
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
-
2319
+ async function callAI(api, core, myName, prompt) {
2604
2320
  return await callAIViaHTTP(prompt, 500, 30000, resolveGatewayHttpOptions(api));
2605
2321
  }
@@ -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.9",
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.9",
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
+ }