lobster-roundtable 3.0.6 → 3.0.7

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
@@ -61,7 +61,7 @@ try {
61
61
  }
62
62
 
63
63
  const CHANNEL_ID = "lobster-roundtable";
64
- const PLUGIN_VERSION = "3.0.6";
64
+ const PLUGIN_VERSION = "3.0.7";
65
65
  const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
66
66
  const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
67
67
 
@@ -1048,6 +1048,67 @@ function maskTokenForDiag(raw) {
1048
1048
  }
1049
1049
  }
1050
1050
 
1051
+ function getPromptTimeoutMs(turnContext) {
1052
+ const ctx = String(turnContext || '').trim().toLowerCase();
1053
+ switch (ctx) {
1054
+ case 'evo_review':
1055
+ return 22000; // server discuss timeout: 25s
1056
+ case 'evo_share':
1057
+ case 'evo_experience':
1058
+ return 38000; // server share timeout: 45s
1059
+ case 'evo_vote':
1060
+ return 18000; // server vote timeout: 20~30s
1061
+ case 'debate_speak':
1062
+ return 24000; // server debate speak timeout: 30s
1063
+ case 'debate_judge':
1064
+ return 42000; // server judge timeout: 45s
1065
+ case 'host_comment':
1066
+ return 12000; // server host comment timeout: 15s
1067
+ case 'host_summary':
1068
+ return 42000; // server host summary timeout: 45s
1069
+ case 'speaking':
1070
+ return 38000; // server generic speaking timeout: 45s
1071
+ default:
1072
+ return 28000; // conservative default to avoid late replies
1073
+ }
1074
+ }
1075
+
1076
+ function buildPromptFallbackReply(turnContext, promptText = '') {
1077
+ const ctx = String(turnContext || '').trim().toLowerCase();
1078
+ if (ctx === 'evo_vote') return '不通过';
1079
+ if (ctx === 'host_comment') return '这个点挺有意思,我先记下,继续下一位。';
1080
+ if (ctx === 'host_summary') return '本轮先总结到这里:观点已收集,下一轮继续聚焦核心分歧。';
1081
+ if (ctx === 'evo_review') return '我暂时倾向不通过,建议补充可复用步骤与真实案例。';
1082
+ if (ctx === 'evo_share' || ctx === 'evo_experience') {
1083
+ return '我补充一点可落地经验:先明确目标,再小步验证,再按反馈迭代。';
1084
+ }
1085
+ if (ctx === 'debate_judge') return '本轮先给出保守判定:双方都有可取点,建议继续下一轮交锋。';
1086
+ if (ctx === 'debate_speak') return '我方观点是先给可验证证据,再谈结论,避免空泛表态。';
1087
+ if (ctx === 'speaking') return '我补充一个观点:先对齐目标和约束,再讨论方案优先级。';
1088
+ if (String(promptText || '').includes('通过') && String(promptText || '').includes('不通过')) return '不通过';
1089
+ return '我先给一个简短结论:建议先明确目标,再执行并复盘。';
1090
+ }
1091
+
1092
+ function inferLegacyTurnContext(msg = {}) {
1093
+ const roomMode = String(msg.roomMode || '').trim().toLowerCase();
1094
+ const modePrompt = String(msg.modePrompt || '');
1095
+ if (msg.isHostComment) return 'host_comment';
1096
+ if (msg.isHostSummary) return 'host_summary';
1097
+ if (roomMode === 'debate') {
1098
+ if (/评委|打分|评分|本轮结果/.test(modePrompt)) return 'debate_judge';
1099
+ return 'debate_speak';
1100
+ }
1101
+ if (roomMode === 'evolution') {
1102
+ if (msg.parallelReview || /评审|可复用|宁可错杀/.test(modePrompt)) return 'evo_review';
1103
+ if (/只需回复\s*[“"]?通过[”"]?\s*或\s*[“"]?不通过/.test(modePrompt) || /通过.*不通过|不通过.*通过/.test(modePrompt)) {
1104
+ return 'evo_vote';
1105
+ }
1106
+ if (/没有.*Skill|改为分享|使用经验|真实经验/.test(modePrompt)) return 'evo_experience';
1107
+ return 'evo_share';
1108
+ }
1109
+ return 'speaking';
1110
+ }
1111
+
1051
1112
  function buildSkillShareDraft(skillName, rawContent) {
1052
1113
  const name = String(skillName || '').trim() || '未命名Skill';
1053
1114
  const text = String(rawContent || '').replace(/\r\n/g, '\n').trim();
@@ -1123,7 +1184,8 @@ function maskTokenForDiag(raw) {
1123
1184
  send({ type: 'skill_picked', skillName, skillContent });
1124
1185
  } catch (err) {
1125
1186
  api.logger.error(`[roundtable] pick_skill 失败(${source}): ${err.message}`);
1126
- send({ type: 'skill_picked', noSkill: true, reason: 'error' });
1187
+ // 失败时必须降级到经验分享,避免服务端将本轮直接判定为“跳过”导致无发言
1188
+ send({ type: 'skill_picked', noSkill: true, reason: 'experience' });
1127
1189
  }
1128
1190
  }
1129
1191
 
@@ -1633,7 +1695,7 @@ function maskTokenForDiag(raw) {
1633
1695
  const turnContext = String(msg.turnContext || '').trim();
1634
1696
  const promptText = String(msg.text || '').trim();
1635
1697
  if (!promptText) {
1636
- send({ type: 'bot_reply', text: '' });
1698
+ send({ type: 'bot_reply', text: buildPromptFallbackReply(turnContext, promptText) });
1637
1699
  break;
1638
1700
  }
1639
1701
  if (turnContext === 'pick_skill') {
@@ -1641,21 +1703,22 @@ function maskTokenForDiag(raw) {
1641
1703
  break;
1642
1704
  }
1643
1705
  try {
1644
- const timeoutMs = turnContext === 'evo_review'
1645
- ? 22000
1646
- : (turnContext === 'evo_share' || turnContext === 'evo_experience')
1647
- ? 42000
1648
- : (turnContext === 'host_comment' ? 15000 : (turnContext === 'host_summary' ? 45000 : 65000));
1706
+ const timeoutMs = getPromptTimeoutMs(turnContext);
1649
1707
  const reply = await callAIWithTimeout(promptText, timeoutMs, `prompt_${turnContext || 'generic'}`);
1650
- send({ type: 'bot_reply', text: String(reply || '') });
1651
- if (reply) rememberFact(`频道提示(${turnContext || 'generic'})已响应`);
1652
- } catch (err) {
1653
- api.logger.error(`[roundtable] prompt(${turnContext || 'generic'}) 失败: ${err.message}`);
1654
- if (turnContext === 'evo_vote') {
1655
- send({ type: 'bot_reply', text: '不通过' });
1708
+ const normalized = String(reply || '').trim();
1709
+ if (normalized) {
1710
+ send({ type: 'bot_reply', text: normalized });
1711
+ rememberFact(`频道提示(${turnContext || 'generic'})已响应`);
1656
1712
  } else {
1657
- send({ type: 'bot_reply', text: '' });
1713
+ const fallback = buildPromptFallbackReply(turnContext, promptText);
1714
+ api.logger.warn(`[roundtable] prompt(${turnContext || 'generic'}) 返回空文本,使用兜底回复`);
1715
+ send({ type: 'bot_reply', text: fallback });
1716
+ rememberFact(`频道提示(${turnContext || 'generic'})空回复,已兜底`);
1658
1717
  }
1718
+ } catch (err) {
1719
+ api.logger.error(`[roundtable] prompt(${turnContext || 'generic'}) 失败: ${err.message}`);
1720
+ const fallback = buildPromptFallbackReply(turnContext, promptText);
1721
+ send({ type: 'bot_reply', text: fallback });
1659
1722
  }
1660
1723
  break;
1661
1724
  }
@@ -1666,6 +1729,7 @@ function maskTokenForDiag(raw) {
1666
1729
  );
1667
1730
  send({ type: 'bot_status', status: 'speaking', text: `🧠 正在思考:${msg.topic || ''}` });
1668
1731
  try {
1732
+ const turnContext = inferLegacyTurnContext(msg);
1669
1733
  const reply = await generateReply(
1670
1734
  api,
1671
1735
  core,
@@ -1675,22 +1739,27 @@ function maskTokenForDiag(raw) {
1675
1739
  msg,
1676
1740
  getFactualContext(10)
1677
1741
  );
1678
- if (reply) {
1679
- send({ type: "bot_reply", text: reply });
1742
+ const normalized = String(reply || '').trim();
1743
+ if (normalized) {
1744
+ send({ type: "bot_reply", text: normalized });
1680
1745
  api.logger.info(
1681
- `[roundtable] ✅ 已发言: ${reply.slice(0, 60)}...`
1746
+ `[roundtable] ✅ 已发言: ${normalized.slice(0, 60)}...`
1682
1747
  );
1683
- rememberFact(`房间发言:${reply.slice(0, 80)}`);
1748
+ rememberFact(`房间发言:${normalized.slice(0, 80)}`);
1684
1749
  } else {
1685
- send({ type: "bot_reply", text: "" });
1686
- api.logger.warn("[roundtable] AI 返回空内容,跳过本轮");
1687
- send({ type: 'bot_status', status: 'idle', text: '⚪ 本轮空回复' });
1750
+ const fallback = buildPromptFallbackReply(turnContext, msg.modePrompt || msg.topic || '');
1751
+ send({ type: "bot_reply", text: fallback });
1752
+ api.logger.warn(`[roundtable] your_turn(${turnContext}) AI 返回空内容,已使用兜底回复`);
1753
+ rememberFact(`房间发言兜底(${turnContext})`);
1754
+ send({ type: 'bot_status', status: 'idle', text: '⚪ 本轮使用兜底回复' });
1688
1755
  }
1689
1756
  } catch (err) {
1757
+ const turnContext = inferLegacyTurnContext(msg);
1758
+ const fallback = buildPromptFallbackReply(turnContext, msg.modePrompt || msg.topic || '');
1690
1759
  api.logger.error(
1691
1760
  `[roundtable] AI 调用失败: ${err.message}`
1692
1761
  );
1693
- send({ type: "bot_reply", text: "" });
1762
+ send({ type: "bot_reply", text: fallback });
1694
1763
  send({ type: 'bot_status', status: 'error', text: `❌ AI 调用失败: ${err.message}` });
1695
1764
  }
1696
1765
  break;
@@ -1717,26 +1786,12 @@ function maskTokenForDiag(raw) {
1717
1786
  } else {
1718
1787
  const next = remaining[0];
1719
1788
  api.logger.info(`[roundtable] 🧬 改选: ${next}`);
1720
- try {
1721
- const chineseName = await translateSkillNameToChinese(next);
1722
- const finalSkillName = chineseName
1723
- ? (chineseName === next ? chineseName : `${chineseName}(${next})`)
1724
- : next;
1725
- const descPrompt = [
1726
- `请输出可审核、可复用的 Skill 分享稿。`,
1727
- `Skill:${next}(中文名建议:${chineseName || next})`,
1728
- `只讲真实实践,不要编造,不要空话。`,
1729
- `格式:`,
1730
- `【Skill名称】中文名(英文名)`,
1731
- `① 解决痛点 ② 核心步骤 ③ 实战案例 ④ 边界与风险`,
1732
- `全中文,180字以内。`,
1733
- ].join('\n');
1734
- const descReply = await callAI(api, core, myName, descPrompt);
1735
- rememberFact(`改选分享 Skill:${finalSkillName}`);
1736
- send({ type: 'skill_picked', skillName: finalSkillName, skillContent: descReply || `Skill: ${finalSkillName}` });
1737
- } catch {
1738
- send({ type: 'skill_picked', skillName: next, skillContent: `Skill: ${next}` });
1739
- }
1789
+ const safeName = sanitizeSkillDirName(next);
1790
+ const skillFile = safeName ? pathModule.join(skillsRoot, safeName, 'SKILL.md') : '';
1791
+ const raw = skillFile ? readTextSafe(skillFile) : '';
1792
+ const draft = buildSkillShareDraft(next, raw);
1793
+ rememberFact(`改选分享 Skill:${next}`);
1794
+ send({ type: 'skill_picked', skillName: next, skillContent: draft });
1740
1795
  }
1741
1796
  break;
1742
1797
  }
@@ -2036,12 +2091,24 @@ function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000) {
2036
2091
  let data = '';
2037
2092
  res.on('data', (c) => (data += c));
2038
2093
  res.on('end', () => {
2094
+ const status = Number(res.statusCode || 0);
2095
+ if (status < 200 || status >= 300) {
2096
+ const bodyPreview = String(data || '').replace(/\s+/g, ' ').trim().slice(0, 240);
2097
+ reject(new Error(`HTTP AI status ${status}${bodyPreview ? `: ${bodyPreview}` : ''}`));
2098
+ return;
2099
+ }
2039
2100
  try {
2040
2101
  const json = JSON.parse(data);
2041
2102
  const text = json.choices?.[0]?.message?.content || '';
2042
- resolve(String(text).trim());
2043
- } catch {
2044
- resolve('');
2103
+ const normalized = String(text).trim();
2104
+ if (!normalized) {
2105
+ const errMsg = String(json?.error?.message || json?.error || '').trim();
2106
+ reject(new Error(errMsg ? `HTTP AI empty content: ${errMsg}` : 'HTTP AI empty content'));
2107
+ return;
2108
+ }
2109
+ resolve(normalized);
2110
+ } catch (err) {
2111
+ reject(new Error(`HTTP AI invalid JSON: ${err.message}`));
2045
2112
  }
2046
2113
  });
2047
2114
  });
@@ -2083,17 +2150,21 @@ async function callAIViaRuntime(api, core, prompt, sessionLabel = '龙虾圆桌'
2083
2150
  CommandAuthorized: true,
2084
2151
  });
2085
2152
 
2086
- let fullReply = "";
2087
-
2088
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2153
+ const finalParts = [];
2154
+ const blockParts = [];
2155
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2089
2156
  ctx: ctxPayload,
2090
2157
  cfg: api.config,
2091
2158
  dispatcherOptions: {
2092
- deliver: async (payload) => {
2159
+ deliver: async (payload, info) => {
2093
2160
  const text =
2094
2161
  typeof payload?.text === "string" ? payload.text.trim() : "";
2095
2162
  if (text) {
2096
- fullReply += (fullReply ? "\n" : "") + text;
2163
+ if (info?.kind === "block") {
2164
+ blockParts.push(text);
2165
+ } else {
2166
+ finalParts.push(text);
2167
+ }
2097
2168
  }
2098
2169
  },
2099
2170
  onError: (err, info) => {
@@ -2104,7 +2175,16 @@ async function callAIViaRuntime(api, core, prompt, sessionLabel = '龙虾圆桌'
2104
2175
  },
2105
2176
  });
2106
2177
 
2107
- return fullReply.trim();
2178
+ const finalReply = finalParts.join("\n").trim();
2179
+ if (finalReply) return finalReply;
2180
+
2181
+ const blockReply = blockParts.join("\n").trim();
2182
+ if (blockReply) return blockReply;
2183
+
2184
+ if (!dispatchResult?.queuedFinal) {
2185
+ throw new Error("runtime_no_reply_queued");
2186
+ }
2187
+ throw new Error("runtime_empty_reply");
2108
2188
  }
2109
2189
 
2110
2190
  /**
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.6",
8
+ "version": "3.0.7",
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.6",
3
+ "version": "3.0.7",
4
4
  "description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
5
5
  "license": "MIT",
6
6
  "private": false,