openclaw-lark-multi-agent 1.0.16 → 1.0.18

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/README.md CHANGED
@@ -39,6 +39,10 @@ All of them connect to the same OpenClaw Gateway while keeping sessions, queues,
39
39
  - Feishu CardKit v2 Markdown rendering, including native table elements for pipe tables
40
40
  - Bridge-level slash commands and escaped OpenClaw slash commands
41
41
  - `/discuss` mode for barrier-style multi-bot group discussion, including per-round markers and no-reply status notices
42
+ - `/chairman` role: a single per-group chairman that answers plain messages when no bot is in Free mode, and acts as host, challenger, and summarizer inside `/discuss`
43
+ - `/locale zh|en` per-group language, with bot-level and global locale fallbacks for discussion prompts and system notices
44
+ - Shared group history catch-up: when a bot is newly mentioned after missing messages, it receives the unseen group messages it has not synced yet (large history is offloaded to a local file)
45
+ - Concurrency guards: a global `chat.send` limiter and a serialized maintenance limiter (e.g. `sessions.compact`) prevent multi-bot fan-out from saturating the gateway
42
46
  - Linux systemd installer with separate runtime and state directories
43
47
 
44
48
  ## Architecture
@@ -228,10 +232,12 @@ Bridge-level commands use a single slash and are handled by this project:
228
232
  - `/compact` — compact the OpenClaw session
229
233
  - `/reset` — reset the OpenClaw session
230
234
  - `/verbose` — toggle tool-call messages for this bot in this chat
231
- - `/free` — toggle this bot's Free mode in the current group chat
235
+ - `/free [on|off]` — toggle, or explicitly enable/disable, this bot's Free mode in the current group chat
232
236
  - `/mute` — toggle this bot's mute mode in the current group chat
233
237
  - `/mode` — show this bot's current mode in the current chat
234
- - `/discuss on|off|status|stop|rounds N` — control group-level multi-bot discussion mode
238
+ - `/discuss on|off|status|stop|rounds N` — control group-level multi-bot discussion mode (requires a chairman to enable; default 10 rounds)
239
+ - `/chairman [@Bot|off]` — set, view, or clear the single chairman for this group
240
+ - `/locale [zh|en]` — set or view this group's language
235
241
 
236
242
  OpenClaw-level slash commands can be sent by escaping with a double slash:
237
243
 
@@ -298,17 +304,33 @@ the mention-only message is treated as a trigger and is combined with the previo
298
304
 
299
305
  Bot messages do not trigger other bots unless they mention them. The anti-loop guard is counted per bot per chat: other bots' replies do not consume the current bot's streak budget, and a human message resets the streak.
300
306
 
307
+ ### Context injection
308
+
309
+ Catch-up context is the unseen group history a bot receives alongside the current message. It follows strict rules:
310
+
311
+ - Catch-up is injected only in group chats; private chats never get it.
312
+ - It contains messages this bot has not synced yet (both human and other-bot messages), so a mention-only reply can see the human message it refers to.
313
+ - It excludes the current trigger(s), other pending triggers, this bot's own messages, and escaped native commands.
314
+ - When there is nothing unseen, no context header is added; the current message is sent as-is.
315
+ - Consecutive plain human triggers are merged into a single run instead of being processed one by one. Native commands (`//x`) are always processed on their own and never merged.
316
+ - Escaped native commands (`//status`) are sent verbatim with no catch-up context and no attachment hint.
317
+ - The bridge attachment hint is only injected when the message combines an action word with an artifact word (for example "generate an image and send it"), so ordinary talk that merely mentions "file" or "document" does not trigger it.
318
+
319
+ Persistent constraints (such as "do not call Feishu send tools directly" and the chairman's non-discuss guidance) are injected once when a session is created or reset, never prepended to every message.
320
+
301
321
  ### `/discuss` mode
302
322
 
303
323
  `/discuss` is an explicit group-level multi-agent discussion scheduler. It is separate from Free mode:
304
324
 
305
325
  - `/free` controls whether a single bot may answer plain human messages.
306
- - `/discuss on` lets one coordinator take over plain human messages and run all Free-mode bots in barrier-style rounds.
326
+ - `/discuss on` requires a chairman to be set first (`/chairman @Bot`). It lets one coordinator take over plain human messages and run all Free-mode bots plus the chairman in barrier-style rounds.
307
327
  - Targeted mentions still fall through to normal routing, so `@GPT hello` works even while discuss mode is enabled.
308
328
  - Each participant receives the same round prompt and does not see other participants' replies from the current round until the next round.
329
+ - The chairman speaks last each round: it gives its own view, challenges weak points, mediates disagreements, and decides whether to continue or conclude.
309
330
  - Each visible discussion reply is annotated with a round marker such as `—— 第 2/3 轮 · Claude`.
310
331
  - If some participants return `NO_REPLY` or an empty reply, the coordinator sends a lightweight status notice such as `💬 第 3/3 轮:Qwen、Gemini 无新增回复`.
311
- - When the configured maximum round count is reached, the coordinator sends a completion notice.
332
+ - When the chairman emits a `FINAL_SUMMARY:` line, the discussion ends and discuss mode is automatically turned off; control markers (`FINAL_SUMMARY:` / `CHAIRMAN_NOTE:`) are stripped from what users see.
333
+ - The default round count is 10; reaching it forces the chairman to produce a final summary.
312
334
 
313
335
  Commands:
314
336
 
@@ -317,9 +339,36 @@ Commands:
317
339
  /discuss off
318
340
  /discuss status
319
341
  /discuss stop
320
- /discuss rounds 3
342
+ /discuss rounds 10
321
343
  ```
322
344
 
345
+ ### `/chairman`
346
+
347
+ Each group can have exactly one chairman, set with `/chairman @Bot`. Setting a new chairman replaces the previous one; `@`-ing more than one bot is rejected. The chairman has two roles:
348
+
349
+ - Normal mode: it is only a fallback responder. When no bot is in Free mode and nobody is explicitly addressed, the chairman answers plain messages. It does not summarize, moderate, or challenge other bots outside `/discuss`.
350
+ - Discuss mode: it participates, speaks last each round, challenges, mediates, and produces the final summary.
351
+
352
+ ```text
353
+ /chairman @Bot set the chairman
354
+ /chairman show the current chairman
355
+ /chairman off clear the chairman
356
+ ```
357
+
358
+ `/chairman` is a group-level command handled by one coordinator bot, so it produces a single reply.
359
+
360
+ ### `/locale`
361
+
362
+ Discussion prompts, chairman prompts, and system notices are localized. Language resolves as: group `/locale` setting > bot-level `locale` config > global `locale` config > `zh` (default).
363
+
364
+ ```text
365
+ /locale show the current group language
366
+ /locale zh set this group to Chinese
367
+ /locale en set this group to English
368
+ ```
369
+
370
+ `/locale` is also a group-level command handled by one coordinator bot. The current language is shown in `/status`.
371
+
323
372
  ## Delivery outbox and duplicate prevention
324
373
 
325
374
  All user-visible assistant outputs go through the local `delivery_outbox` before being sent to Feishu. This includes normal chat final replies, proactive `session.message` replies, delayed runtime-error notices, provider-error notices, discussion replies, and attachment marker deliveries.
@@ -363,8 +412,10 @@ For generated files/images/documents, agents should use the bridge attachment ma
363
412
 
364
413
  SQLite state lives in the configured data directory. Important tables:
365
414
 
366
- - `messages` — local conversation log and context
367
- - `sync_state` — per-bot/per-chat sync cursor
415
+ - `messages` — local conversation log and context (includes a `trigger_kind` column to mark escaped native commands)
416
+ - `sync_state` — per-bot/per-chat sync cursor (coarse high-water mark)
417
+ - `message_sync` — per-bot/per-chat/per-message sync ledger for shared group-history catch-up
418
+ - `chat_info` — per-chat settings such as `discuss`, `discuss_max_rounds`, `chairman_bot`, and `locale`
368
419
  - `pending_triggers` — messages that should actively trigger a bot run
369
420
  - `delivered_replies` — delivered response markers for idempotency
370
421
  - `delivery_outbox` — durable user-visible delivery ledger with claim/dedupe state
package/README.zh-CN.md CHANGED
@@ -39,6 +39,10 @@ Lark/飞书里每个机器人都有自己的 App 身份,但 OpenClaw 通常在
39
39
  - Feishu CardKit v2 Markdown 渲染,并把 pipe table 转成原生 table 组件
40
40
  - 桥接层 slash command + 转义后的 OpenClaw slash command
41
41
  - `/discuss` 多 bot 结构化讨论模式,支持轮次标注和无新增回复提示
42
+ - `/chairman` 主席角色:每个群唯一一个主席,在没有 Free 模式 bot 时兑底回答普通消息;在 `/discuss` 里担任主持、质疑者和总结者
43
+ - `/locale zh|en` 群级语言设置,讨论/主席 prompt 和系统消息随语言切换,支持 bot 级和全局 locale 回退
44
+ - 共享群历史 catch-up:某个 bot 错过消息后被重新 @ 时,会拿到它还没同步过的群里发言(超大历史会卸到本地文件)
45
+ - 并发保护:全局 `chat.send` 限流 + 维护类 RPC(如 `sessions.compact`)串行限流,避免多 bot fan-out 打爆 gateway
42
46
  - Linux systemd 安装脚本,运行产物和状态目录分离
43
47
 
44
48
  ## 架构
@@ -228,10 +232,12 @@ openclaw-lark-multi-agent install-windows-service
228
232
  - `/compact` — 压缩 OpenClaw session
229
233
  - `/reset` — 重置 OpenClaw session
230
234
  - `/verbose` — 开关当前 bot 在当前聊天里的 tool-call 展示
231
- - `/free` — 开关当前 bot 在当前群聊里的 Free 模式
235
+ - `/free [on|off]` — 切换,或显式开启/关闭当前 bot 在当前群聊里的 Free 模式
232
236
  - `/mute` — 开关当前 bot 在当前群聊里的 mute 模式
233
237
  - `/mode` — 查看当前 bot 在当前聊天里的模式
234
- - `/discuss on|off|status|stop|rounds N` — 控制群级多 bot 讨论模式
238
+ - `/discuss on|off|status|stop|rounds N` — 控制群级多 bot 讨论模式(需先设置 Chairman;默认 10 轮)
239
+ - `/chairman [@Bot|off]` — 设置/查看/清除本群唯一 Chairman
240
+ - `/locale [zh|en]` — 设置/查看当前群语言
235
241
 
236
242
  如果你想把 slash command 直接发给 OpenClaw,可以用双斜杠转义:
237
243
 
@@ -298,18 +304,34 @@ Free 模式是 per-bot 且保守的:
298
304
 
299
305
  bot 发出的消息默认不会触发其他 bot,除非明确 @。anti-loop 防护按 bot + chat 单独计算:其他 bot 的发言不会消耗当前 bot 的额度,人类发言会重置计数。
300
306
 
307
+ ### 上下文注入
308
+
309
+ catch-up 上下文是 bot 随当前消息一起拿到的群里未看到的发言,遵循严格规则:
310
+
311
+ - catch-up 只在群聊注入;私聊永远不注入。
312
+ - 它包含当前 bot 还没同步过的消息(人类的和其他 bot 的都算),这样纯 @ 触发的回复能看到它要回的那条人类消息。
313
+ - 它排除当前 trigger、其他 pending trigger、当前 bot 自己的消息、以及转义的原生命令。
314
+ - 没有未看到的消息时,不加任何 context header,当前消息原样发送。
315
+ - 连续的普通人类消息会合并成一次 run,不逐条处理。原生命令(`//x`)始终单独处理,不与普通消息合并。
316
+ - 转义的原生命令(`//status`)原样发送,不带 catch-up 上下文、不带 attachment hint。
317
+ - attachment hint 只在消息同时出现「动作词 + 产物词」时注入(如“生成一张图发给我”),普通聊天提到“文件”“文档”不会误触发。
318
+
319
+ 持久约束(如“不要直接调用飞书发送工具”、主席的非 discuss 约束)只在 session 创建/reset 时一次性注入,绝不每条消息 prepend。
320
+
301
321
 
302
322
  ## `/discuss` 讨论模式
303
323
 
304
324
  `/discuss` 是显式的群级多智能体讨论调度器,和 Free 模式分工不同:
305
325
 
306
326
  - `/free` 控制单个 bot 是否可以响应普通人类消息。
307
- - `/discuss on` 让一个 coordinator 接管普通人类消息,并按 barrier-style round 调度所有 Free 模式 bot。
327
+ - `/discuss on` 需先设置 Chairman(`/chairman @Bot`)。它让一个 coordinator 接管普通人类消息,并按 barrier-style round 调度所有 Free 模式 bot 加上 Chairman
308
328
  - 定向 @ 仍然走普通路由,所以 discuss 开启时 `@GPT hello` 仍会只触发 GPT。
309
329
  - 每个参与 bot 在同一轮拿到相同 prompt,本轮内看不到其他 bot 的回复,下一轮才会看到上一轮结果。
330
+ - Chairman 每轮最后发言:先给出自己的观点,质疑薄弱点,调停分歧,并决定继续还是总结。
310
331
  - 每条可见讨论回复会自动追加轮次标注,例如 `—— 第 2/3 轮 · Claude`。
311
332
  - 如果某些 bot 返回 `NO_REPLY` 或空回复,coordinator 会发送轻量提示,例如 `💬 第 3/3 轮:Qwen、Gemini 无新增回复`。
312
- - 达到配置轮数后,coordinator 会发送讨论完成提示。
333
+ - Chairman 输出 `FINAL_SUMMARY:` 行时,讨论结束并自动关闭 discuss 模式;控制标记(`FINAL_SUMMARY:` / `CHAIRMAN_NOTE:`)会从用户可见内容里剔除。
334
+ - 默认轮数是 10;达到上限会强制 Chairman 做最终总结。
313
335
 
314
336
  命令:
315
337
 
@@ -318,9 +340,36 @@ bot 发出的消息默认不会触发其他 bot,除非明确 @。anti-loop 防
318
340
  /discuss off
319
341
  /discuss status
320
342
  /discuss stop
321
- /discuss rounds 3
343
+ /discuss rounds 10
322
344
  ```
323
345
 
346
+ ### `/chairman` 主席
347
+
348
+ 每个群只能有一个 Chairman,用 `/chairman @Bot` 设置。设置新 Chairman 会替换旧的;同时 @ 多个 bot 会报错。Chairman 有两个角色:
349
+
350
+ - 普通模式:只是兑底回答者。没有 bot 处于 Free 模式、也没有明确 @ 任何人时,Chairman 回答普通消息。它在 `/discuss` 之外不总结、不主持、不质疑其他 bot。
351
+ - Discuss 模式:参与讨论,每轮最后发言,质疑、调停、并做最终总结。
352
+
353
+ ```text
354
+ /chairman @Bot 设置 Chairman
355
+ /chairman 查看当前 Chairman
356
+ /chairman off 清除 Chairman
357
+ ```
358
+
359
+ `/chairman` 是群级命令,由一个 coordinator bot 统一处理,只产生一条回复。
360
+
361
+ ### `/locale` 语言
362
+
363
+ 讨论 prompt、主席 prompt 和系统消息都支持中英文。语言优先级:群 `/locale` 设置 > bot 级 `locale` 配置 > 全局 `locale` 配置 > `zh`(默认)。
364
+
365
+ ```text
366
+ /locale 查看当前群语言
367
+ /locale zh 设置为中文
368
+ /locale en 设置为英文
369
+ ```
370
+
371
+ `/locale` 也是群级命令,由一个 coordinator bot 统一处理。当前语言会在 `/status` 里显示。
372
+
324
373
  ## Delivery outbox 与重复投递防护
325
374
 
326
375
  所有用户可见 assistant 输出都会先进入本地 `delivery_outbox`,再统一投递到飞书。覆盖普通 chat final、proactive `session.message`、延迟 runtime error、provider error、discussion 回复和附件 marker。
@@ -363,10 +412,14 @@ v1 行为边界:如果消息已经进入 OpenClaw 正在处理,暂不 abort
363
412
 
364
413
  SQLite 状态位于配置的数据目录。主要表:
365
414
 
366
- - `messages` — 本地对话日志和上下文
367
- - `sync_state` — 每个 bot / chat 的同步游标
415
+ - `messages` — 本地对话日志和上下文(含 `trigger_kind` 列,标记转义的原生命令)
416
+ - `sync_state` — 每个 bot / chat 的同步游标(粗粒度水位线)
417
+ - `message_sync` — 每个 bot / chat / 消息的同步账本,用于共享群历史 catch-up
418
+ - `chat_info` — 每个 chat 的设置,例如 `discuss`、`discuss_max_rounds`、`chairman_bot`、`locale`
368
419
  - `pending_triggers` — 应主动触发 bot run 的消息
369
420
  - `delivered_replies` — 已投递回复标记,用于幂等防重复
421
+ - `delivery_outbox` — 持久化的用户可见投递账本,带 claim/去重状态
422
+ - `recalled_messages` — 已撤回的用户消息,从 pending 和后续上下文中排除
370
423
  - `processed_events` — 飞书事件去重
371
424
  - `bot_chat_settings` — 每个 bot / chat 的设置,例如 verbose mode
372
425
 
package/dist/config.d.ts CHANGED
@@ -19,3 +19,9 @@ export interface AppConfig {
19
19
  locale?: Locale;
20
20
  }
21
21
  export declare function loadConfig(path?: string): AppConfig;
22
+ /**
23
+ * Persist a single bot's model into config.json without touching any other
24
+ * field. Reads the file, mutates only the target bot's `model`, and writes it
25
+ * back. Never replaces the whole config blindly.
26
+ */
27
+ export declare function persistBotModel(configPath: string, botName: string, model: string): void;
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "fs";
1
+ import { readFileSync, writeFileSync } from "fs";
2
2
  import { resolve } from "path";
3
3
  import { normalizeLocale } from "./i18n.js";
4
4
  export function loadConfig(path) {
@@ -31,3 +31,20 @@ export function loadConfig(path) {
31
31
  }
32
32
  return config;
33
33
  }
34
+ /**
35
+ * Persist a single bot's model into config.json without touching any other
36
+ * field. Reads the file, mutates only the target bot's `model`, and writes it
37
+ * back. Never replaces the whole config blindly.
38
+ */
39
+ export function persistBotModel(configPath, botName, model) {
40
+ const resolved = resolve(configPath);
41
+ const raw = readFileSync(resolved, "utf-8");
42
+ const json = JSON.parse(raw);
43
+ if (!Array.isArray(json.bots))
44
+ throw new Error("config.json has no bots array");
45
+ const bot = json.bots.find((b) => b && b.name === botName);
46
+ if (!bot)
47
+ throw new Error(`Bot "${botName}" not found in config.json`);
48
+ bot.model = model;
49
+ writeFileSync(resolved, JSON.stringify(json, null, 2) + "\n", "utf-8");
50
+ }
@@ -25,6 +25,7 @@ export declare class FeishuBot {
25
25
  private pendingAckMessages;
26
26
  /** Per-chat pending tool message sends (to await before final reply) */
27
27
  private pendingToolSends;
28
+ private recentVerboseToolMessages;
28
29
  /** Per-chat processQueue lock to avoid duplicate concurrent chat.send runs */
29
30
  private queueRuns;
30
31
  /** Per-chat serial send queue to guarantee message order */
@@ -37,8 +38,9 @@ export declare class FeishuBot {
37
38
  private activeDeliveryTargets;
38
39
  private adminOpenId;
39
40
  private locale;
41
+ private configPath?;
40
42
  private static allBots;
41
- constructor(config: BotConfig, openclawClient: OpenClawClient, store: MessageStore, adminOpenId?: string);
43
+ constructor(config: BotConfig, openclawClient: OpenClawClient, store: MessageStore, adminOpenId?: string, configPath?: string);
42
44
  private handleMessageRecalled;
43
45
  register(): void;
44
46
  /**
@@ -91,6 +93,8 @@ export declare class FeishuBot {
91
93
  private stripLeadingCommandMentions;
92
94
  private buildMarkdownCard;
93
95
  private formatUserVisibleError;
96
+ private isBridgeControlReply;
97
+ private isLegacyBridgeControlMessage;
94
98
  private isRuntimeFailureText;
95
99
  private cancelDelayedFailure;
96
100
  private setActiveDeliveryTarget;
@@ -139,6 +143,10 @@ export declare class FeishuBot {
139
143
  private handleLocaleCommand;
140
144
  private handleDiscussCommand;
141
145
  private handleChairmanCommand;
146
+ /**
147
+ * Handle /model command: show or switch this bot's bound model.
148
+ */
149
+ private handleModelCommand;
142
150
  /**
143
151
  * Handle /status command: show current session info.
144
152
  */
@@ -153,6 +161,12 @@ export declare class FeishuBot {
153
161
  * then verify via describe.
154
162
  */
155
163
  private handleResetCommand;
164
+ /**
165
+ * Force-stop a stuck run for this bot in this chat. Aborts all active OpenClaw
166
+ * runs for the session, clears the busy lock and every pending trigger, and
167
+ * resets stuck reactions so new messages are processed normally again.
168
+ */
169
+ private handleStopCommand;
156
170
  /**
157
171
  * Fetch chat info (name, type, members) via Feishu API and cache in SQLite.
158
172
  * Called once per chat on first message.