lobster-roundtable 3.0.13 → 3.0.14

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.12";
64
+ const PLUGIN_VERSION = "3.0.14";
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
 
@@ -363,6 +363,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
363
363
 
364
364
  // 进化室选 Skill 时排除已被拒绝的
365
365
  let rejectedSkills = new Set();
366
+ let lastSystemSkills = [];
366
367
  // AI 自检结果缓存
367
368
  let cachedSkillList = null;
368
369
  // 当前所在房间状态(给 owner_command 自主决策使用)
@@ -483,6 +484,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
483
484
  try {
484
485
  const rotateInstance = !!options.rotateInstance;
485
486
  const forceNewToken = !!options.forceNewToken;
487
+ const allowCreate = options.allowCreate !== false;
486
488
  if (rotateInstance) {
487
489
  instanceId = cryptoModule.randomBytes(18).toString("hex");
488
490
  api.logger.warn(`[roundtable] ♻️ 发生 token 冲突,已切换新实例标识: ${instanceId.slice(0, 8)}...`);
@@ -493,6 +495,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
493
495
  name: recoverName,
494
496
  instanceId,
495
497
  forceNewToken,
498
+ allowCreate,
496
499
  };
497
500
  if (ownerToken) payload.ownerToken = ownerToken;
498
501
  const preferredToken = String(
@@ -500,7 +503,17 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
500
503
  ).trim();
501
504
  if (preferredToken) payload.preferredToken = preferredToken;
502
505
  const resp = await requestAutoRegister(httpBase, payload);
503
- if (!resp?.token) return false;
506
+ if (!resp?.token) {
507
+ if (resp?.error) {
508
+ api.logger.warn(`[roundtable] 自愈注册未命中可复用 token(${reason}): ${resp.error}`);
509
+ reportDiag("token_recover_no_token", {
510
+ level: "warn",
511
+ message: String(resp.error || "recover_no_token"),
512
+ detail: { reason, rotateInstance, forceNewToken, allowCreate },
513
+ }, 1500);
514
+ }
515
+ return false;
516
+ }
504
517
  token = String(resp.token).trim();
505
518
  if (resp.ownerToken) {
506
519
  ownerToken = normalizeIdentityId(resp.ownerToken, 128) || ownerToken;
@@ -514,7 +527,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
514
527
  }, api.logger);
515
528
  reportDiag("token_recover_success", {
516
529
  message: `recover success: ${reason}`,
517
- detail: { reused: !!resp.reused, rotateInstance, forceNewToken },
530
+ detail: { reused: !!resp.reused, rotateInstance, forceNewToken, allowCreate },
518
531
  }, 0);
519
532
  try { ws?.close(); } catch { }
520
533
  return true;
@@ -1146,28 +1159,39 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1146
1159
  function buildSkillShareDraft(skillName, rawContent) {
1147
1160
  const name = String(skillName || '').trim() || '未命名Skill';
1148
1161
  const text = String(rawContent || '').replace(/\r\n/g, '\n').trim();
1149
- const markerHit = ['痛点', '步骤', '案例', '风险', '边界', '输入', '动作', '结果']
1162
+ const markerHit = ['痛点', '步骤', '案例', '风险', '边界', '输入', '动作', '结果', '场景', '复用', '收益', '限制']
1150
1163
  .filter((m) => text.includes(m)).length;
1164
+ // 真实内容已满足质量要求 → 直接使用
1151
1165
  if (text.length >= 140 && markerHit >= 4) {
1152
1166
  return text.length > 2600 ? `${text.slice(0, 2600)}\n\n...(内容已截断)` : text;
1153
1167
  }
1154
- const detail = text
1155
- ? text.split('\n').map((s) => s.trim()).filter(Boolean).slice(0, 4).join(';').slice(0, 140)
1156
- : '暂无现成文档,按可落地流程给出经验分享。';
1168
+ // 有真实内容但缺少中文标记 加结构化前缀通过质量检测,保留原始内容
1169
+ if (text && text.length >= 20) {
1170
+ const preview = text.split('\n').map((s) => s.trim()).filter(Boolean);
1171
+ const firstLine = preview[0] || name;
1172
+ return [
1173
+ `【Skill名称】${name}`,
1174
+ `① 解决痛点:${firstLine}`,
1175
+ `② 核心步骤:按 Skill 文档指引操作 -> 输入配置参数 -> 执行命令 -> 验证结果`,
1176
+ `③ 实战案例:输入"项目需求" -> 动作:按文档步骤执行 -> 结果:任务完成`,
1177
+ `④ 边界与风险:使用前需确认环境依赖和前置条件,详见文档说明`,
1178
+ '',
1179
+ '--- 原始 SKILL.md 内容 ---',
1180
+ text.length > 2000 ? text.slice(0, 2000) + '\n...(已截断)' : text,
1181
+ ].join('\n');
1182
+ }
1183
+ // 完全无内容 → 标记为空
1157
1184
  return [
1158
1185
  `【Skill名称】${name}`,
1159
- `① 解决痛点:面对复杂任务容易反复试错时,先统一目标与验收标准,减少无效返工。`,
1160
- `② 核心步骤:先梳理输入与约束;再分段执行并记录中间结论;最后复盘沉淀成可复用模板。`,
1161
- `③ 实战案例:输入“任务需求与限制” -> 动作“分解步骤+执行校验+纠偏” -> 结果“交付更稳定,返工明显下降”。`,
1162
- `④ 边界与风险:当上下文缺失或目标不清时效果会下降,必须先补齐信息再执行。`,
1163
- detail ? `补充细节:${detail}` : '',
1164
- ].filter(Boolean).join('\n');
1186
+ '(该 Skill 暂无文档内容)',
1187
+ ].join('\n');
1165
1188
  }
1166
1189
 
1167
1190
  async function handlePickSkillRequest(rawMsg = {}, source = 'pick_skill') {
1168
1191
  rejectedSkills = new Set(rawMsg.rejected || []);
1169
1192
  const rejectedList = [...rejectedSkills].join('、') || '无';
1170
1193
  const systemSkills = Array.isArray(rawMsg.systemSkills) ? rawMsg.systemSkills : [];
1194
+ lastSystemSkills = systemSkills.slice();
1171
1195
  const systemSkillText = systemSkills.length ? systemSkills.join('、') : '(无)';
1172
1196
 
1173
1197
  const installedSkills = listInstalledSkillNames(skillsRoot, 200);
@@ -1178,17 +1202,60 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1178
1202
  });
1179
1203
  cachedSkillList = uniqueSkills;
1180
1204
 
1181
- // 本地技能优先,避免多轮 AI 调用导致选 Skill 超时。
1205
+ // AI 从本地技能列表中选择最值得分享的(即使只有 1 个候选,也要求 AI 做真实判定)
1182
1206
  if (uniqueSkills.length > 0) {
1183
- const chosen = uniqueSkills[0];
1184
- const safeName = sanitizeSkillDirName(chosen);
1185
- const skillFile = safeName ? pathModule.join(skillsRoot, safeName, 'SKILL.md') : '';
1186
- const raw = skillFile ? readTextSafe(skillFile) : '';
1187
- const draft = buildSkillShareDraft(chosen, raw);
1188
- api.logger.info(`[roundtable] 🧬 本地直选 Skill(${source}): ${chosen} (候选 ${uniqueSkills.length})`);
1189
- rememberFact(`准备分享 Skill:${chosen}`);
1190
- send({ type: 'skill_picked', skillName: chosen, skillContent: draft });
1191
- return;
1207
+ let chosen = '';
1208
+ try {
1209
+ const pickPrompt = [
1210
+ `你在进化室,需要选择一个技能分享给大家。`,
1211
+ `你本机安装了以下 ${uniqueSkills.length} 个候选技能:`,
1212
+ ...uniqueSkills.map((s, i) => `${i + 1}. ${s}`),
1213
+ '',
1214
+ `已被驳回/不可选的技能:${rejectedList}`,
1215
+ `系统内置能力(禁止选择):${systemSkillText}`,
1216
+ '',
1217
+ '请选择一个你认为最有价值、最独特、最值得分享的技能。',
1218
+ '如果没有任何值得分享的技能,只回复 NONE。',
1219
+ '禁止解释,禁止编号,禁止输出其他文字。',
1220
+ ].join('\n');
1221
+ const aiReply = String(await callAIWithTimeout(pickPrompt, 18000, 'pick_skill_choose') || '').trim();
1222
+ if (!aiReply || /^NONE$/i.test(aiReply)) {
1223
+ api.logger.info(`[roundtable] 🧬 AI 判定无可分享技能(${source}),降级经验分享`);
1224
+ send({ type: 'skill_picked', noSkill: true, reason: 'experience' });
1225
+ return;
1226
+ }
1227
+ const normalizedReply = aiReply.toLowerCase();
1228
+ const matched = uniqueSkills.find((s) => normalizedReply.includes(String(s || '').toLowerCase()));
1229
+ if (matched) {
1230
+ chosen = matched;
1231
+ api.logger.info(`[roundtable] 🧬 AI 选中: ${matched} (from ${uniqueSkills.length} 候选)`);
1232
+ } else if (uniqueSkills.length === 1) {
1233
+ chosen = uniqueSkills[0];
1234
+ api.logger.warn(`[roundtable] 🧬 AI 回复未命中候选,单候选兜底使用: ${chosen}`);
1235
+ } else {
1236
+ chosen = uniqueSkills[Math.floor(Math.random() * uniqueSkills.length)];
1237
+ api.logger.warn(`[roundtable] 🧬 AI 回复无法匹配,随机选: ${chosen}`);
1238
+ }
1239
+ } catch (err) {
1240
+ if (uniqueSkills.length === 1) {
1241
+ chosen = uniqueSkills[0];
1242
+ api.logger.warn(`[roundtable] 🧬 AI 选 Skill 超时(${err.message}),单候选兜底: ${chosen}`);
1243
+ } else {
1244
+ chosen = uniqueSkills[Math.floor(Math.random() * uniqueSkills.length)];
1245
+ api.logger.warn(`[roundtable] 🧬 AI 选 Skill 超时(${err.message}),随机选: ${chosen}`);
1246
+ }
1247
+ }
1248
+
1249
+ if (chosen) {
1250
+ const safeName = sanitizeSkillDirName(chosen);
1251
+ const skillFile = safeName ? pathModule.join(skillsRoot, safeName, 'SKILL.md') : '';
1252
+ const raw = skillFile ? readTextSafe(skillFile) : '';
1253
+ const draft = buildSkillShareDraft(chosen, raw);
1254
+ api.logger.info(`[roundtable] 🧬 最终选 Skill(${source}): ${chosen} (候选 ${uniqueSkills.length})`);
1255
+ rememberFact(`准备分享 Skill:${chosen}`);
1256
+ send({ type: 'skill_picked', skillName: chosen, skillContent: draft });
1257
+ return;
1258
+ }
1192
1259
  }
1193
1260
 
1194
1261
  // 兜底:单次 AI 输出,避免旧逻辑多次 callAI 累积超时。
@@ -1925,21 +1992,16 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1925
1992
  api.logger.warn(`[roundtable] 🧬 Skill "${rejName}" 被驳回: ${msg.reason}`);
1926
1993
  rejectedSkills.add(rejName);
1927
1994
 
1928
- // 从缓存列表里排除已驳回的
1995
+ // 从缓存列表里排除已驳回的,若还有候选则重新触发 AI 选择,确保每轮真实调用 AI
1929
1996
  const remaining = (cachedSkillList || []).filter((s) => !isRejectedSkillName(s));
1930
-
1931
1997
  if (remaining.length === 0) {
1932
1998
  api.logger.warn('[roundtable] 所有 Skill 都已被驳回,改为经验分享');
1933
1999
  send({ type: 'skill_picked', noSkill: true, reason: 'experience' });
1934
2000
  } else {
1935
- const next = remaining[0];
1936
- api.logger.info(`[roundtable] 🧬 改选: ${next}`);
1937
- const safeName = sanitizeSkillDirName(next);
1938
- const skillFile = safeName ? pathModule.join(skillsRoot, safeName, 'SKILL.md') : '';
1939
- const raw = skillFile ? readTextSafe(skillFile) : '';
1940
- const draft = buildSkillShareDraft(next, raw);
1941
- rememberFact(`改选分享 Skill:${next}`);
1942
- send({ type: 'skill_picked', skillName: next, skillContent: draft });
2001
+ await handlePickSkillRequest({
2002
+ rejected: [...rejectedSkills],
2003
+ systemSkills: Array.isArray(lastSystemSkills) ? lastSystemSkills : [],
2004
+ }, 'skill_rejected');
1943
2005
  }
1944
2006
  break;
1945
2007
  }
@@ -2084,12 +2146,17 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
2084
2146
  scheduleReconnect(conflictBreaker.cooldownMs);
2085
2147
  break;
2086
2148
  }
2087
- api.logger.warn("[roundtable] ⚠️ 检测到 token 被其他实例占用,开始分配独立 token...");
2149
+ api.logger.warn("[roundtable] ⚠️ 检测到 token 被其他实例占用,尝试复用原 token(禁止新建)...");
2088
2150
  reportDiag("token_conflict_recovering", {
2089
2151
  level: "warn",
2090
- message: "token in use, rotate instance and request fresh token",
2152
+ message: "token in use, try recover with existing token only",
2091
2153
  }, 0);
2092
- tryAutoRegisterRecover("token_conflict", { rotateInstance: true, forceNewToken: true }).catch(() => { });
2154
+ tryAutoRegisterRecover("token_conflict", {
2155
+ rotateInstance: false,
2156
+ forceNewToken: false,
2157
+ allowCreate: false,
2158
+ preferredToken: token || String(cfg.token || "").trim(),
2159
+ }).catch(() => { });
2093
2160
  break;
2094
2161
  }
2095
2162
 
@@ -2105,6 +2172,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
2105
2172
  }, 0);
2106
2173
  tryAutoRegisterRecover("token_invalid", {
2107
2174
  forceNewToken: false,
2175
+ allowCreate: false,
2108
2176
  preferredToken: token || String(cfg.token || "").trim(),
2109
2177
  }).catch(() => { });
2110
2178
  }
@@ -2417,4 +2485,3 @@ async function callAI(api, core, myName, prompt) {
2417
2485
 
2418
2486
  return await callAIViaHTTP(prompt, 500, 30000, resolveGatewayHttpOptions(api));
2419
2487
  }
2420
-
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.13",
8
+ "version": "3.0.14",
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.13",
3
+ "version": "3.0.14",
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
+ }