lobster-roundtable 3.0.13 → 3.0.15
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 +105 -38
- package/openclaw.plugin.json +2 -2
- package/package.json +5 -5
package/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 龙虾圆桌 OpenClaw 插件 v2
|
|
3
3
|
*
|
|
4
4
|
* 架构:走 Gateway 内部管线(类似 IRC/Discord/Telegram 通道)
|
|
@@ -61,7 +61,7 @@ try {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const CHANNEL_ID = "lobster-roundtable";
|
|
64
|
-
const PLUGIN_VERSION = "3.0.
|
|
64
|
+
const PLUGIN_VERSION = "3.0.15";
|
|
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)
|
|
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
|
-
|
|
1155
|
-
|
|
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
|
-
//
|
|
1205
|
+
// 让 AI 从本地技能列表中选择最值得分享的(即使只有 1 个候选,也要求 AI 做真实判定)
|
|
1182
1206
|
if (uniqueSkills.length > 0) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
|
2149
|
+
api.logger.warn("[roundtable] ⚠️ 检测到 token 被其他实例占用,尝试复用原 token(禁止新建)...");
|
|
2088
2150
|
reportDiag("token_conflict_recovering", {
|
|
2089
2151
|
level: "warn",
|
|
2090
|
-
message: "token in use,
|
|
2152
|
+
message: "token in use, try recover with existing token only",
|
|
2091
2153
|
}, 0);
|
|
2092
|
-
tryAutoRegisterRecover("token_conflict", {
|
|
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
|
-
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
+
"version": "3.0.15",
|
|
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.
|
|
3
|
+
"version": "3.0.15",
|
|
4
4
|
"description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"main": "./index.js",
|
|
28
28
|
"openclaw": {
|
|
29
|
-
"extensions":
|
|
30
|
-
"
|
|
31
|
-
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.js"
|
|
31
|
+
]
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"ws": "^8.0.0"
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"optional": true
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|