lobster-roundtable 3.0.10 → 3.0.12

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.10";
64
+ const PLUGIN_VERSION = "3.0.12";
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
 
@@ -2295,8 +2295,74 @@ function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000, httpOptions =
2295
2295
  }
2296
2296
 
2297
2297
  /**
2298
- * 核心 AI 调用:直接通过 Gateway HTTP API (/v1/chat/completions) 调用 LLM
2299
- * 构造圆桌讨论的完整 prompt,让 Gateway 用用户配置的 AI 模型生成回复
2298
+ * 通过 Gateway 内部 runtime API 调用 AI(标准 channel 插件路径)
2299
+ * 不依赖 HTTP 端点,直接走 agent 管线,适配所有 AI 后端
2300
+ */
2301
+ async function callAIViaRuntime(api, core, prompt, timeoutMs = 45000) {
2302
+ const accountId = String(api?.accountId || "default");
2303
+ const sessionKey = `${CHANNEL_ID}:${accountId}:group:roundtable`;
2304
+ const timestamp = Date.now();
2305
+
2306
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
2307
+ Body: prompt,
2308
+ BodyForAgent: prompt,
2309
+ RawBody: prompt,
2310
+ CommandBody: prompt,
2311
+ From: `${CHANNEL_ID}:server`,
2312
+ To: `${CHANNEL_ID}:group:roundtable`,
2313
+ SessionKey: sessionKey,
2314
+ AccountId: accountId,
2315
+ ChatType: "group",
2316
+ ConversationLabel: "龙虾圆桌",
2317
+ SenderName: "圆桌服务器",
2318
+ SenderId: "roundtable-server",
2319
+ GroupSubject: "roundtable",
2320
+ Provider: CHANNEL_ID,
2321
+ Surface: CHANNEL_ID,
2322
+ MessageSid: `rt-${timestamp}-${Math.random().toString(36).slice(2, 8)}`,
2323
+ Timestamp: timestamp,
2324
+ OriginatingChannel: CHANNEL_ID,
2325
+ OriginatingTo: `${CHANNEL_ID}:group:roundtable`,
2326
+ CommandAuthorized: true,
2327
+ });
2328
+
2329
+ const parts = [];
2330
+ let dispatchError = null;
2331
+
2332
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2333
+ ctx: ctxPayload,
2334
+ cfg: api.config,
2335
+ dispatcherOptions: {
2336
+ deliver: async (payload, info) => {
2337
+ const text = typeof payload?.text === "string" ? payload.text.trim() : "";
2338
+ if (text) parts.push(text);
2339
+ },
2340
+ onError: (err, info) => {
2341
+ dispatchError = err;
2342
+ api.logger.error(`[roundtable] dispatch deliver error (${info?.kind || 'unknown'}): ${String(err)}`);
2343
+ },
2344
+ },
2345
+ });
2346
+
2347
+ const reply = parts.join("\n").trim();
2348
+ if (reply) return reply;
2349
+
2350
+ // 详细诊断信息
2351
+ const diagInfo = {
2352
+ queuedFinal: dispatchResult?.queuedFinal,
2353
+ counts: dispatchResult?.counts,
2354
+ partsLen: parts.length,
2355
+ dispatchError: dispatchError ? String(dispatchError) : null,
2356
+ sessionKey,
2357
+ accountId,
2358
+ };
2359
+ throw new Error(`runtime_no_reply: ${JSON.stringify(diagInfo)}`);
2360
+ }
2361
+
2362
+ /**
2363
+ * 核心 AI 调用:runtime dispatch → HTTP 降级
2364
+ * runtime 是 OpenClaw 标准 channel 插件 API,适配所有 AI 后端(kimi/gemini/qwen/...)
2365
+ * HTTP 仅在 runtime 不可用时降级使用
2300
2366
  */
2301
2367
  async function generateReply(api, core, myName, persona, maxTokens, msg, factualContext = '') {
2302
2368
  const rawHistory = msg.history || parseContext(msg.context);
@@ -2310,14 +2376,45 @@ async function generateReply(api, core, myName, persona, maxTokens, msg, factual
2310
2376
  : '';
2311
2377
 
2312
2378
  const fullPrompt = `${systemPrompt}${memoryBlock}\n\n---\n\n${userMessage}`;
2313
- return await callAIViaHTTP(fullPrompt, maxTokens || 500, 65000, resolveGatewayHttpOptions(api));
2379
+
2380
+ // 1. 优先 runtime dispatch(标准路径,所有 OpenClaw 都支持)
2381
+ const hasRuntime = !!(core?.channel?.reply?.finalizeInboundContext &&
2382
+ core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher);
2383
+
2384
+ if (hasRuntime) {
2385
+ try {
2386
+ return await callAIViaRuntime(api, core, fullPrompt, 65000);
2387
+ } catch (err) {
2388
+ api.logger.warn(`[roundtable] generateReply runtime failed: ${err.message}`);
2389
+ }
2390
+ }
2391
+
2392
+ // 2. 降级 HTTP
2393
+ try {
2394
+ return await callAIViaHTTP(fullPrompt, maxTokens || 500, 65000, resolveGatewayHttpOptions(api));
2395
+ } catch (httpErr) {
2396
+ api.logger.warn(`[roundtable] generateReply HTTP also failed: ${httpErr.message}`);
2397
+ throw httpErr;
2398
+ }
2314
2399
  }
2315
2400
 
2316
2401
  /**
2317
2402
  * 精简版 AI 调用:给一个 prompt,拿一个回复
2318
- * 用于进化室选 Skill、评审、投票等不需要完整对话上下文的场景
2319
- * 直接通过 Gateway HTTP API 调用
2403
+ * 用于进化室选 Skill、评审、投票等场景
2404
+ * runtime dispatch HTTP 降级
2320
2405
  */
2321
2406
  async function callAI(api, core, myName, prompt) {
2407
+ const hasRuntime = !!(core?.channel?.reply?.finalizeInboundContext &&
2408
+ core?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher);
2409
+
2410
+ if (hasRuntime) {
2411
+ try {
2412
+ return await callAIViaRuntime(api, core, prompt, 30000);
2413
+ } catch (err) {
2414
+ api.logger.warn(`[roundtable] callAI runtime failed: ${err.message}`);
2415
+ }
2416
+ }
2417
+
2322
2418
  return await callAIViaHTTP(prompt, 500, 30000, resolveGatewayHttpOptions(api));
2323
2419
  }
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.10",
8
+ "version": "3.0.12",
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.10",
3
+ "version": "3.0.12",
4
4
  "description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
5
5
  "license": "MIT",
6
6
  "private": false,
package/src/channel.js CHANGED
@@ -1,17 +1,17 @@
1
- "use strict";
2
-
3
- /**
4
- * 龙虾圆桌 ChannelPlugin 定义
5
- * 参照 OpenClaw 官方 IRC Channel 插件 (extensions/irc/src/channel.ts)
6
- *
1
+ "use strict";
2
+
3
+ /**
4
+ * 龙虾圆桌 ChannelPlugin 定义
5
+ * 参照 OpenClaw 官方 IRC Channel 插件 (extensions/irc/src/channel.ts)
6
+ *
7
7
  * ChannelGatewayContext 官方字段�?
8
- * cfg, accountId, account, runtime, abortSignal, log, getStatus, setStatus
9
- *
10
- * 关键生命周期规则(参�?server-channels.ts L203/L220):
8
+ * cfg, accountId, account, runtime, abortSignal, log, getStatus, setStatus
9
+ *
10
+ * 关键生命周期规则(参�?server-channels.ts L203/L220):
11
11
  * startAccount 返回�?Promise 结束 = 通道退�?�?Gateway 会自动重�?
12
- * 因此 startAccount 必须保持挂起直到 abortSignal 触发
13
- */
14
-
12
+ * 因此 startAccount 必须保持挂起直到 abortSignal 触发
13
+ */
14
+
15
15
  const CHANNEL_ID = "lobster-roundtable";
16
16
 
17
17
  function createOutboundMessageId() {
@@ -32,31 +32,31 @@ function mergeConfigPreferExplicitChannel(pluginEntryConfig, channelConfig) {
32
32
  }
33
33
  return merged;
34
34
  }
35
-
36
- /**
37
- * �?ChannelGatewayContext 适配�?main.js 所需�?api 兼容对象
38
- *
35
+
36
+ /**
37
+ * �?ChannelGatewayContext 适配�?main.js 所需�?api 兼容对象
38
+ *
39
39
  * main.js 依赖�?api 字段�?
40
- * - api.logger �?ctx.log
41
- * - api.pluginConfig �?ctx.cfg.plugins.entries[id].config
42
- * - api.runtime �?ctx.runtime
40
+ * - api.logger �?ctx.log
41
+ * - api.pluginConfig �?ctx.cfg.plugins.entries[id].config
42
+ * - api.runtime �?ctx.runtime
43
43
  * - api.config �?ctx.cfg (完�?OpenClawConfig,用�?dispatchReply �?cfg 参数�?
44
- */
44
+ */
45
45
  function buildApiAdapter(ctx) {
46
46
  // 双源读取:channels.<id>(官方标准路径)优先,plugins.entries(兼容现有安装)降级。
47
47
  // 关键:channels 中空字符串不覆盖 plugins.entries 中已有 token/url,避免“loaded 但离线”。
48
48
  const channelConfig = ctx.cfg?.channels?.[CHANNEL_ID] || {};
49
49
  const pluginEntryConfig = ctx.cfg?.plugins?.entries?.[CHANNEL_ID]?.config || {};
50
50
  const pluginConfig = mergeConfigPreferExplicitChannel(pluginEntryConfig, channelConfig);
51
-
52
- return {
53
- // main.js �?api.logger.info/warn/error
54
- logger: {
55
- info: (...args) => ctx.log?.info?.(...args),
56
- warn: (...args) => ctx.log?.warn?.(...args),
57
- error: (...args) => ctx.log?.error?.(...args),
58
- debug: (...args) => ctx.log?.debug?.(...args),
59
- },
51
+
52
+ return {
53
+ // main.js �?api.logger.info/warn/error
54
+ logger: {
55
+ info: (...args) => ctx.log?.info?.(...args),
56
+ warn: (...args) => ctx.log?.warn?.(...args),
57
+ error: (...args) => ctx.log?.error?.(...args),
58
+ debug: (...args) => ctx.log?.debug?.(...args),
59
+ },
60
60
  // main.js �?api.pluginConfig
61
61
  pluginConfig,
62
62
  // main.js �?api.runtime
@@ -67,32 +67,32 @@ function buildApiAdapter(ctx) {
67
67
  config: ctx.cfg,
68
68
  };
69
69
  }
70
-
71
- /**
72
- * @type {import("openclaw/plugin-sdk").ChannelPlugin}
73
- */
74
- const roundtablePlugin = {
75
- id: CHANNEL_ID,
76
-
77
- meta: {
78
- id: CHANNEL_ID,
79
- label: "龙虾圆桌",
80
- selectionLabel: "Lobster Roundtable",
81
- icon: "🦞",
82
- description: "�?AI 圆桌讨论插件",
83
- blurb: "让你�?AI 自动参与多智能体圆桌讨论",
84
- docsPath: "/channels/lobster-roundtable",
85
- },
86
-
87
- capabilities: {
88
- chatTypes: ["group"],
89
- media: false,
90
- blockStreaming: false,
91
- },
92
-
93
- reload: { configPrefixes: [`channels.${CHANNEL_ID}`, `plugins.entries.${CHANNEL_ID}`] },
94
-
95
- config: {
70
+
71
+ /**
72
+ * @type {import("openclaw/plugin-sdk").ChannelPlugin}
73
+ */
74
+ const roundtablePlugin = {
75
+ id: CHANNEL_ID,
76
+
77
+ meta: {
78
+ id: CHANNEL_ID,
79
+ label: "龙虾圆桌",
80
+ selectionLabel: "Lobster Roundtable",
81
+ icon: "🦞",
82
+ description: "�?AI 圆桌讨论插件",
83
+ blurb: "让你�?AI 自动参与多智能体圆桌讨论",
84
+ docsPath: "/channels/lobster-roundtable",
85
+ },
86
+
87
+ capabilities: {
88
+ chatTypes: ["group"],
89
+ media: false,
90
+ blockStreaming: false,
91
+ },
92
+
93
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`, `plugins.entries.${CHANNEL_ID}`] },
94
+
95
+ config: {
96
96
  listAccountIds: () => ["default"],
97
97
  resolveAccount: (cfg, accountId) => {
98
98
  const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
@@ -101,70 +101,70 @@ const roundtablePlugin = {
101
101
  return {
102
102
  accountId: accountId || "default",
103
103
  enabled: true,
104
- // main.js 支持默认 URL + 自动注册,因此始终视�?configured
105
- // 不拦截零配置启动,让 main.js 自己处理
106
- configured: true,
107
- config: pluginCfg,
108
- };
109
- },
110
- defaultAccountId: () => "default",
111
- isConfigured: () => true,
112
- describeAccount: (account) => ({
113
- accountId: account.accountId,
114
- enabled: account.enabled,
115
- configured: account.configured,
116
- }),
117
- },
118
-
119
- outbound: {
120
- deliveryMode: "direct",
121
- sendText: async (ctx) => {
122
- // 龙虾圆桌的出站消息通过内部 WS 发送(bot.send()),
104
+ // main.js 支持默认 URL + 自动注册,因此始终视�?configured
105
+ // 不拦截零配置启动,让 main.js 自己处理
106
+ configured: true,
107
+ config: pluginCfg,
108
+ };
109
+ },
110
+ defaultAccountId: () => "default",
111
+ isConfigured: () => true,
112
+ describeAccount: (account) => ({
113
+ accountId: account.accountId,
114
+ enabled: account.enabled,
115
+ configured: account.configured,
116
+ }),
117
+ },
118
+
119
+ outbound: {
120
+ deliveryMode: "direct",
121
+ sendText: async (ctx) => {
122
+ // 龙虾圆桌的出站消息通过内部 WS 发送(bot.send()),
123
123
  // 不走 OpenClaw 的标�?outbound 管线�?
124
- return { channel: CHANNEL_ID, messageId: createOutboundMessageId() };
125
- },
126
- sendMedia: async (ctx) => {
127
- // 圆桌不支持媒体消息,但必须提供此方法
124
+ return { channel: CHANNEL_ID, messageId: createOutboundMessageId() };
125
+ },
126
+ sendMedia: async (ctx) => {
127
+ // 圆桌不支持媒体消息,但必须提供此方法
128
128
  // 否则 deliver.ts 会判�?outbound 未配�?
129
- return { channel: CHANNEL_ID, messageId: createOutboundMessageId() };
130
- },
131
- },
132
-
133
- status: {
134
- defaultRuntime: {
135
- accountId: "default",
136
- running: false,
137
- lastStartAt: null,
138
- lastStopAt: null,
139
- lastError: null,
140
- },
141
- buildChannelSummary: ({ snapshot }) => ({
142
- configured: snapshot.configured ?? false,
143
- running: snapshot.running ?? false,
144
- lastStartAt: snapshot.lastStartAt ?? null,
145
- lastStopAt: snapshot.lastStopAt ?? null,
146
- lastError: snapshot.lastError ?? null,
147
- }),
148
- buildAccountSnapshot: ({ account, runtime }) => ({
149
- accountId: account.accountId,
150
- enabled: account.enabled,
151
- configured: account.configured,
152
- running: runtime?.running ?? false,
153
- lastStartAt: runtime?.lastStartAt ?? null,
154
- lastStopAt: runtime?.lastStopAt ?? null,
155
- lastError: runtime?.lastError ?? null,
156
- }),
157
- },
158
-
159
- gateway: {
160
- /**
129
+ return { channel: CHANNEL_ID, messageId: createOutboundMessageId() };
130
+ },
131
+ },
132
+
133
+ status: {
134
+ defaultRuntime: {
135
+ accountId: "default",
136
+ running: false,
137
+ lastStartAt: null,
138
+ lastStopAt: null,
139
+ lastError: null,
140
+ },
141
+ buildChannelSummary: ({ snapshot }) => ({
142
+ configured: snapshot.configured ?? false,
143
+ running: snapshot.running ?? false,
144
+ lastStartAt: snapshot.lastStartAt ?? null,
145
+ lastStopAt: snapshot.lastStopAt ?? null,
146
+ lastError: snapshot.lastError ?? null,
147
+ }),
148
+ buildAccountSnapshot: ({ account, runtime }) => ({
149
+ accountId: account.accountId,
150
+ enabled: account.enabled,
151
+ configured: account.configured,
152
+ running: runtime?.running ?? false,
153
+ lastStartAt: runtime?.lastStartAt ?? null,
154
+ lastStopAt: runtime?.lastStopAt ?? null,
155
+ lastError: runtime?.lastError ?? null,
156
+ }),
157
+ },
158
+
159
+ gateway: {
160
+ /**
161
161
  * Gateway 调用此函数启�?Channel�?
162
- *
163
- * 关键生命周期约束(参�?server-channels.ts):
164
- * - startAccount 返回�?Promise resolve = 通道退�?�?Gateway 自动重启
165
- * - 必须保持 Promise 挂起,直�?abortSignal 触发�?resolve
162
+ *
163
+ * 关键生命周期约束(参�?server-channels.ts):
164
+ * - startAccount 返回�?Promise resolve = 通道退�?�?Gateway 自动重启
165
+ * - 必须保持 Promise 挂起,直�?abortSignal 触发�?resolve
166
166
  * - 参照 nextcloud-talk/src/channel.ts L337 的做�?
167
- */
167
+ */
168
168
  startAccount: async (ctx) => {
169
169
  let bot = null;
170
170
 
@@ -180,8 +180,48 @@ const roundtablePlugin = {
180
180
 
181
181
  if (!hasRuntimeAPI) {
182
182
  ctx.log?.info?.(
183
- "[roundtable] runtime API 不可用,将使�?Gateway HTTP API 调用 AI(兼容模式)"
183
+ "[roundtable] runtime API 不可用,将使?Gateway HTTP API 调用 AI(兼容模式)"
184
184
  );
185
+
186
+ // ═══ 自动启用 chatCompletions HTTP 端点 ═══
187
+ // OpenClaw 默认禁用 /v1/chat/completions(405),需要在配置中启用
188
+ try {
189
+ const os = require("os");
190
+ const path = require("path");
191
+ const fs = require("fs");
192
+ const openclawDir = process.env.OPENCLAW_DIR || path.join(os.homedir(), ".openclaw");
193
+ const configPath = path.join(openclawDir, "openclaw.json");
194
+
195
+ if (fs.existsSync(configPath)) {
196
+ const raw = fs.readFileSync(configPath, "utf8");
197
+ const config = JSON.parse(raw);
198
+ const enabled = config?.gateway?.http?.endpoints?.chatCompletions?.enabled;
199
+
200
+ if (!enabled) {
201
+ // 安全地合并配置,不覆盖现有字段
202
+ config.gateway = config.gateway || {};
203
+ config.gateway.http = config.gateway.http || {};
204
+ config.gateway.http.endpoints = config.gateway.http.endpoints || {};
205
+ config.gateway.http.endpoints.chatCompletions = { enabled: true };
206
+
207
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
208
+ ctx.log?.info?.(
209
+ "[roundtable] ✅ 已自动启用 gateway.http.endpoints.chatCompletions"
210
+ );
211
+ ctx.log?.info?.(
212
+ "[roundtable] ⚠️ 需要重启 Gateway 使配置生效: openclaw gateway restart"
213
+ );
214
+ } else {
215
+ ctx.log?.info?.(
216
+ "[roundtable] chatCompletions 端点已启用,HTTP AI 调用可用"
217
+ );
218
+ }
219
+ }
220
+ } catch (autoEnableErr) {
221
+ ctx.log?.warn?.(
222
+ `[roundtable] 自动启用 chatCompletions 失败: ${autoEnableErr.message}`
223
+ );
224
+ }
185
225
  }
186
226
 
187
227
  ctx.log?.info?.("[roundtable] Gateway 正在启动龙虾圆桌...");
@@ -240,8 +280,8 @@ const roundtablePlugin = {
240
280
 
241
281
  return { stop: () => { } };
242
282
  },
243
- },
244
- };
245
-
246
- module.exports = { roundtablePlugin, CHANNEL_ID };
247
-
283
+ },
284
+ };
285
+
286
+ module.exports = { roundtablePlugin, CHANNEL_ID };
287
+