lobster-roundtable 3.0.2 → 3.0.4
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/index.js +2 -2
- package/main.js +41 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.js +75 -73
- package/src/env.js +33 -0
package/index.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require("fs");
|
|
11
|
-
const os = require("os");
|
|
12
11
|
const path = require("path");
|
|
12
|
+
const { getOpenClawDir } = require("./src/env.js");
|
|
13
13
|
|
|
14
14
|
const PLUGIN_ID = "lobster-roundtable";
|
|
15
15
|
const PLUGIN_NAME = "龙虾圆桌";
|
|
@@ -19,7 +19,7 @@ const PLUGIN_NAME = "龙虾圆桌";
|
|
|
19
19
|
*/
|
|
20
20
|
function cleanLegacyInstanceIdFromConfig(logger) {
|
|
21
21
|
try {
|
|
22
|
-
const ocDir =
|
|
22
|
+
const ocDir = getOpenClawDir();
|
|
23
23
|
const configPath = path.join(ocDir, "openclaw.json");
|
|
24
24
|
if (!fs.existsSync(configPath)) return false;
|
|
25
25
|
const raw = fs.readFileSync(configPath, "utf-8");
|
package/main.js
CHANGED
|
@@ -16,7 +16,13 @@ const httpModule = require("http");
|
|
|
16
16
|
const httpsModule = require("https");
|
|
17
17
|
const fsModule = require("fs");
|
|
18
18
|
const cryptoModule = require("crypto");
|
|
19
|
-
|
|
19
|
+
const {
|
|
20
|
+
getOpenClawDir,
|
|
21
|
+
isOpenClawConfigSyncEnabled,
|
|
22
|
+
getOpenClawApiPort,
|
|
23
|
+
getOpenClawApiToken,
|
|
24
|
+
} = require("./src/env.js");
|
|
25
|
+
// Node.js 原生 HTTP 请求工具(避免依赖外部命令)
|
|
20
26
|
function httpRequest(urlStr, options = {}) {
|
|
21
27
|
return new Promise((resolve, reject) => {
|
|
22
28
|
const url = new URL(urlStr);
|
|
@@ -46,7 +52,7 @@ try {
|
|
|
46
52
|
WebSocket = require("ws");
|
|
47
53
|
} catch {
|
|
48
54
|
try {
|
|
49
|
-
const ocDir =
|
|
55
|
+
const ocDir = getOpenClawDir();
|
|
50
56
|
WebSocket = require(pathModule.join(ocDir, "node_modules", "ws"));
|
|
51
57
|
} catch {
|
|
52
58
|
// 最后的后备
|
|
@@ -54,8 +60,8 @@ try {
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
const CHANNEL_ID = "lobster-roundtable";
|
|
57
|
-
const PLUGIN_VERSION = "3.0.
|
|
58
|
-
const ENABLE_OPENCLAW_CONFIG_SYNC =
|
|
63
|
+
const PLUGIN_VERSION = "3.0.4";
|
|
64
|
+
const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
|
|
59
65
|
const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
|
|
60
66
|
|
|
61
67
|
function normalizeIdentityId(raw, max = 96) {
|
|
@@ -65,7 +71,7 @@ function normalizeIdentityId(raw, max = 96) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
function buildOpenClawHome() {
|
|
68
|
-
return
|
|
74
|
+
return getOpenClawDir();
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
function sanitizeSkillDirName(name) {
|
|
@@ -316,6 +322,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
|
|
|
316
322
|
const configuredName = String(cfg.name || "").trim();
|
|
317
323
|
let token = String(cfg.token || "").trim();
|
|
318
324
|
let ownerToken = normalizeIdentityId(cfg.ownerToken, 128);
|
|
325
|
+
let tokenSource = token ? 'config' : 'none'; // 追踪 token 来源
|
|
319
326
|
const persona = String(cfg.persona || "").trim();
|
|
320
327
|
const maxTokens = cfg.maxTokens || 150;
|
|
321
328
|
|
|
@@ -364,12 +371,15 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
|
|
|
364
371
|
} else {
|
|
365
372
|
api.logger.warn("[roundtable] ⚠️ 检测到配置 token 与本地缓存不一致,优先使用缓存 token");
|
|
366
373
|
token = cachedToken;
|
|
374
|
+
tokenSource = 'cache';
|
|
367
375
|
}
|
|
368
376
|
} else if (cachedToken && !token) {
|
|
369
377
|
api.logger.info("[roundtable] 📦 从本地缓存加载 token");
|
|
370
378
|
token = cachedToken;
|
|
379
|
+
tokenSource = 'cache';
|
|
371
380
|
} else if (cachedToken && token === cachedToken) {
|
|
372
381
|
api.logger.info("[roundtable] 📦 命中本地 token 缓存");
|
|
382
|
+
tokenSource = 'config+cache';
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
const cachedOwnerToken = normalizeIdentityId(readTextSafe(ownerTokenCacheFile), 128);
|
|
@@ -392,6 +402,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
|
|
|
392
402
|
});
|
|
393
403
|
if (resp.token) {
|
|
394
404
|
token = resp.token;
|
|
405
|
+
tokenSource = 'auto-register';
|
|
395
406
|
if (resp.ownerToken) ownerToken = normalizeIdentityId(resp.ownerToken, 128) || ownerToken;
|
|
396
407
|
api.logger.info(`[roundtable] 🔑 自动注册成功!名称: ${resp.name}${resp.reused ? '(复用)' : '(新建)'}`);
|
|
397
408
|
writeTextSafe(tokenCacheFile, token);
|
|
@@ -418,7 +429,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
|
|
|
418
429
|
if (!ENABLE_OPENCLAW_CONFIG_SYNC) {
|
|
419
430
|
api.logger.info("[roundtable] 配置同步已禁用:仅写本地 token 缓存,避免触发 gateway 重启");
|
|
420
431
|
}
|
|
421
|
-
api.logger.info(`[roundtable] v${PLUGIN_VERSION} 启动(Channel
|
|
432
|
+
api.logger.info(`[roundtable] v${PLUGIN_VERSION} 启动(Channel 模式)token=${token ? token.slice(0, 8) + '...' : '无'} source=${tokenSource}`);
|
|
422
433
|
return startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFile, {
|
|
423
434
|
ocHome,
|
|
424
435
|
instanceId,
|
|
@@ -428,6 +439,7 @@ module.exports = async function initRoundtable(api, core, hasRuntimeAPI) {
|
|
|
428
439
|
wsScope,
|
|
429
440
|
ownerToken,
|
|
430
441
|
ownerTokenCacheFile,
|
|
442
|
+
tokenSource,
|
|
431
443
|
});
|
|
432
444
|
};
|
|
433
445
|
|
|
@@ -473,6 +485,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
|
|
|
473
485
|
normalizeIdentityId(readTextSafe(ownerTokenCacheFile), 128);
|
|
474
486
|
let instanceId = normalizeIdentityId(identityCtx.instanceId) || normalizeIdentityId(readTextSafe(instanceIdFile)) || cryptoModule.randomBytes(18).toString("hex");
|
|
475
487
|
const sessionId = normalizeIdentityId(identityCtx.sessionId, 120) || cryptoModule.randomBytes(12).toString("hex");
|
|
488
|
+
const tokenSource = identityCtx.tokenSource || 'unknown';
|
|
476
489
|
let recoveringToken = false;
|
|
477
490
|
|
|
478
491
|
// Token 冲突熔断器:防止无限重试导致日志爆炸和 CPU 空转
|
|
@@ -765,7 +778,10 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
|
|
|
765
778
|
sessionId,
|
|
766
779
|
skillNames: installedSkills,
|
|
767
780
|
roomId: rejoinRoom || undefined,
|
|
781
|
+
pluginVersion: PLUGIN_VERSION,
|
|
782
|
+
tokenSource: tokenSource || 'unknown',
|
|
768
783
|
});
|
|
784
|
+
api.logger.info(`[roundtable] 🔗 bot_connect 已发送 (token=${token.slice(0, 8)}... source=${tokenSource || 'unknown'})`);
|
|
769
785
|
};
|
|
770
786
|
|
|
771
787
|
ws.onmessage = async (event) => {
|
|
@@ -1605,6 +1621,23 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
|
|
|
1605
1621
|
api.logger.info(
|
|
1606
1622
|
`[roundtable] 🦞 已连接大厅,待机中。可用房间: ${(msg.rooms || []).map(r => r.id).join(', ')}`
|
|
1607
1623
|
);
|
|
1624
|
+
// ═══ 连接健康报告 ═══
|
|
1625
|
+
api.logger.info([
|
|
1626
|
+
`[roundtable] ═══ 连接健康报告 ═══`,
|
|
1627
|
+
` 插件版本: v${PLUGIN_VERSION}`,
|
|
1628
|
+
` Bot 名称: ${myName}`,
|
|
1629
|
+
` Token: ${token ? token.slice(0, 8) + '...' : '无'}`,
|
|
1630
|
+
` Token 来源: ${tokenSource}`,
|
|
1631
|
+
` Instance: ${instanceId ? instanceId.slice(0, 8) + '...' : '无'}`,
|
|
1632
|
+
` Session: ${sessionId ? sessionId.slice(0, 8) + '...' : '无'}`,
|
|
1633
|
+
` 服务端地址: ${wsUrl}`,
|
|
1634
|
+
` 服务端确认名: ${msg.name || '(未返回)'}`,
|
|
1635
|
+
` 服务端时间: ${msg.serverTime || '(未返回)'}`,
|
|
1636
|
+
` 可用房间: ${(msg.rooms || []).length} 个`,
|
|
1637
|
+
` Runtime API: ${core?.channel?.reply ? '✅' : '❌ (HTTP 降级)'}`,
|
|
1638
|
+
` 状态: ✅ 在线`,
|
|
1639
|
+
`[roundtable] ═══════════════════`,
|
|
1640
|
+
].join('\n'));
|
|
1608
1641
|
rememberFact('回到大厅待机');
|
|
1609
1642
|
send({ type: 'bot_status', status: 'lobby', text: '☕ 大厅待机中' });
|
|
1610
1643
|
if (autonomyEnabled) scheduleAutonomyTick(900);
|
|
@@ -2031,8 +2064,8 @@ function parseContext(context) {
|
|
|
2031
2064
|
*/
|
|
2032
2065
|
function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000) {
|
|
2033
2066
|
return new Promise((resolve, reject) => {
|
|
2034
|
-
const OC_PORT =
|
|
2035
|
-
const OC_TOKEN =
|
|
2067
|
+
const OC_PORT = getOpenClawApiPort();
|
|
2068
|
+
const OC_TOKEN = getOpenClawApiToken();
|
|
2036
2069
|
const body = JSON.stringify({
|
|
2037
2070
|
model: 'default',
|
|
2038
2071
|
messages: [{ role: 'user', content: String(prompt || '') }],
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.js
CHANGED
|
@@ -146,79 +146,81 @@ const roundtablePlugin = {
|
|
|
146
146
|
* - 必须保持 Promise 挂起,直�?abortSignal 触发�?resolve
|
|
147
147
|
* - 参照 nextcloud-talk/src/channel.ts L337 的做�?
|
|
148
148
|
*/
|
|
149
|
-
startAccount: (ctx) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
ctx.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (ctx.abortSignal
|
|
215
|
-
onAbort
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// 如果没有 abortSignal
|
|
220
|
-
});
|
|
221
|
-
|
|
149
|
+
startAccount: async (ctx) => {
|
|
150
|
+
let bot = null;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const initRoundtable = require("../main.js");
|
|
154
|
+
const apiAdapter = buildApiAdapter(ctx);
|
|
155
|
+
const core = ctx.runtime;
|
|
156
|
+
|
|
157
|
+
const hasRuntimeAPI = !!(
|
|
158
|
+
core?.channel?.reply?.finalizeInboundContext &&
|
|
159
|
+
core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!hasRuntimeAPI) {
|
|
163
|
+
ctx.log?.info?.(
|
|
164
|
+
"[roundtable] runtime API 不可用,将使�?Gateway HTTP API 调用 AI(兼容模式)"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ctx.log?.info?.("[roundtable] Gateway 正在启动龙虾圆桌...");
|
|
169
|
+
|
|
170
|
+
bot = await initRoundtable(apiAdapter, core, hasRuntimeAPI);
|
|
171
|
+
|
|
172
|
+
if (!bot || typeof bot.stop !== "function") {
|
|
173
|
+
throw new Error("[roundtable] initRoundtable 未返回有效的 { stop } 对象");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
ctx.setStatus?.({
|
|
177
|
+
accountId: ctx.accountId || "default",
|
|
178
|
+
running: true,
|
|
179
|
+
lastStartAt: Date.now(),
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
ctx.log?.error?.(`[roundtable] 启动失败: ${err.message}`);
|
|
183
|
+
ctx.setStatus?.({
|
|
184
|
+
accountId: ctx.accountId || "default",
|
|
185
|
+
running: false,
|
|
186
|
+
lastError: err.message,
|
|
187
|
+
});
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 核心:Promise 保持 pending,直�?abortSignal 触发后 resolve
|
|
192
|
+
// 这样 Gateway 不会认为通道退出并触发重启循环
|
|
193
|
+
await new Promise((resolve) => {
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
ctx.log?.info?.("[roundtable] 收到 Gateway abortSignal,正在停�?..");
|
|
196
|
+
try {
|
|
197
|
+
bot?.stop?.();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
ctx.log?.warn?.(`[roundtable] 停止 bot 时出现错误: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
ctx.setStatus?.({
|
|
202
|
+
accountId: ctx.accountId || "default",
|
|
203
|
+
running: false,
|
|
204
|
+
lastStopAt: Date.now(),
|
|
205
|
+
});
|
|
206
|
+
resolve();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (ctx.abortSignal?.aborted) {
|
|
210
|
+
onAbort();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (ctx.abortSignal) {
|
|
215
|
+
ctx.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 如果没有 abortSignal,保持 pending:通道保持运行态
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return { stop: () => { } };
|
|
223
|
+
},
|
|
222
224
|
},
|
|
223
225
|
};
|
|
224
226
|
|
package/src/env.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function readEnv(name) {
|
|
7
|
+
if (!name) return "";
|
|
8
|
+
return String(process.env[name] || "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getOpenClawDir() {
|
|
12
|
+
return readEnv("OPENCLAW_DIR") || path.join(os.homedir(), ".openclaw");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isOpenClawConfigSyncEnabled() {
|
|
16
|
+
return readEnv("LOBSTER_RT_SYNC_OPENCLAW").trim() === "1";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getOpenClawApiPort() {
|
|
20
|
+
const parsed = parseInt(readEnv("OPENCLAW_PORT") || "18789", 10);
|
|
21
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 18789;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getOpenClawApiToken() {
|
|
25
|
+
return (readEnv("OPENCLAW_API_TOKEN") || readEnv("OPENCLAW_TOKEN")).trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
getOpenClawDir,
|
|
30
|
+
isOpenClawConfigSyncEnabled,
|
|
31
|
+
getOpenClawApiPort,
|
|
32
|
+
getOpenClawApiToken,
|
|
33
|
+
};
|