openclaw-lark-multi-agent 1.0.12 → 1.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/dist/config.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { type Locale } from "./i18n.js";
1
2
  export interface BotConfig {
2
3
  name: string;
3
4
  appId: string;
4
5
  appSecret: string;
5
6
  model: string;
7
+ locale?: Locale;
6
8
  }
7
9
  export interface OpenClawConfig {
8
10
  baseUrl: string;
@@ -13,5 +15,7 @@ export interface AppConfig {
13
15
  bots: BotConfig[];
14
16
  /** Optional Feishu/Lark open_id for model-drift notifications */
15
17
  adminOpenId?: string;
18
+ /** Default UI/prompt language. Bot-level locale overrides this. */
19
+ locale?: Locale;
16
20
  }
17
21
  export declare function loadConfig(path?: string): AppConfig;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { resolve } from "path";
3
+ import { normalizeLocale } from "./i18n.js";
3
4
  export function loadConfig(path) {
4
5
  const configPath = path || resolve(process.cwd(), "config.json");
5
6
  const raw = readFileSync(configPath, "utf-8");
@@ -15,6 +16,10 @@ export function loadConfig(path) {
15
16
  throw new Error(`Bot "${bot.name}" missing appId, appSecret, or model`);
16
17
  }
17
18
  }
19
+ config.locale = normalizeLocale(config.locale);
20
+ for (const bot of config.bots) {
21
+ bot.locale = normalizeLocale(bot.locale || config.locale);
22
+ }
18
23
  // Validate uniqueness
19
24
  const names = config.bots.map((b) => b.name);
20
25
  const appIds = config.bots.map((b) => b.appId);
@@ -1,15 +1,24 @@
1
+ import { type Locale } from "./i18n.js";
1
2
  export type ReplyResult = {
2
3
  botName: string;
3
4
  text: string;
4
5
  visible: boolean;
5
6
  error?: string;
6
7
  };
8
+ export type DiscussionCompleteReason = "chairman_final" | "all_no_reply" | "max_rounds";
9
+ export type DiscussionCompleteEvent = {
10
+ chatId: string;
11
+ reason: DiscussionCompleteReason;
12
+ chairmanName?: string;
13
+ };
7
14
  type DiscussionSession = {
8
15
  id: string;
9
16
  chatId: string;
10
17
  rootMessageId: string;
11
18
  topic: string;
12
19
  participants: string[];
20
+ chairmanName?: string;
21
+ locale: Locale;
13
22
  currentRound: number;
14
23
  maxRounds: number;
15
24
  completedRounds: Array<{
@@ -26,9 +35,11 @@ export type DiscussionParticipant = {
26
35
  }): Promise<ReplyResult>;
27
36
  };
28
37
  export declare class DiscussionManager {
38
+ private defaultLocale;
29
39
  private sessions;
30
40
  private seenRoots;
31
41
  private readonly seenRootTtlMs;
42
+ constructor(defaultLocale?: Locale);
32
43
  isActive(chatId: string): boolean;
33
44
  stop(chatId: string): boolean;
34
45
  status(chatId: string): DiscussionSession | null;
@@ -38,10 +49,15 @@ export declare class DiscussionManager {
38
49
  topic: string;
39
50
  maxRounds: number;
40
51
  participants: DiscussionParticipant[];
52
+ chairman?: DiscussionParticipant;
41
53
  sendSystemMessage?: (text: string) => Promise<void>;
54
+ onComplete?: (event: DiscussionCompleteEvent) => Promise<void>;
55
+ locale?: Locale;
42
56
  }): boolean;
43
57
  private pruneSeenRoots;
44
58
  private runLoop;
59
+ private hasFinalSummaryMarker;
60
+ private buildChairmanPrompt;
45
61
  private buildPrompt;
46
62
  }
47
63
  export declare const discussionManager: DiscussionManager;
@@ -1,8 +1,13 @@
1
1
  import { randomUUID } from "crypto";
2
+ import { getI18n } from "./i18n.js";
2
3
  export class DiscussionManager {
4
+ defaultLocale;
3
5
  sessions = new Map();
4
6
  seenRoots = new Map();
5
7
  seenRootTtlMs = 6 * 60 * 60 * 1000;
8
+ constructor(defaultLocale = "zh") {
9
+ this.defaultLocale = defaultLocale;
10
+ }
6
11
  isActive(chatId) {
7
12
  return this.sessions.get(chatId)?.status === "running";
8
13
  }
@@ -26,8 +31,11 @@ export class DiscussionManager {
26
31
  this.seenRoots.set(key, Date.now());
27
32
  if (this.isActive(params.chatId))
28
33
  this.stop(params.chatId);
29
- const participants = params.participants.filter((p, index, arr) => arr.findIndex((x) => x.name === p.name) === index);
30
- if (participants.length === 0) {
34
+ const chairman = params.chairman;
35
+ const participants = params.participants
36
+ .filter((p) => p.name !== chairman?.name)
37
+ .filter((p, index, arr) => arr.findIndex((x) => x.name === p.name) === index);
38
+ if (participants.length === 0 && !chairman) {
31
39
  this.seenRoots.delete(key);
32
40
  return false;
33
41
  }
@@ -36,14 +44,16 @@ export class DiscussionManager {
36
44
  chatId: params.chatId,
37
45
  rootMessageId: params.rootMessageId,
38
46
  topic: params.topic,
39
- participants: participants.map((p) => p.name),
47
+ participants: [...participants.map((p) => p.name), ...(chairman ? [chairman.name] : [])],
48
+ chairmanName: chairman?.name,
49
+ locale: params.locale || this.defaultLocale,
40
50
  currentRound: 1,
41
51
  maxRounds: params.maxRounds,
42
52
  completedRounds: [],
43
53
  status: "running",
44
54
  };
45
55
  this.sessions.set(params.chatId, session);
46
- void this.runLoop(session.id, participants, params.sendSystemMessage).catch((err) => {
56
+ void this.runLoop(session.id, participants, chairman, params.sendSystemMessage, params.onComplete).catch((err) => {
47
57
  console.warn(`[Discussion] loop failed for ${params.chatId}:`, err instanceof Error ? err.message : String(err));
48
58
  const current = this.sessions.get(params.chatId);
49
59
  if (current?.id === session.id)
@@ -57,7 +67,7 @@ export class DiscussionManager {
57
67
  this.seenRoots.delete(key);
58
68
  }
59
69
  }
60
- async runLoop(sessionId, participants, sendSystemMessage) {
70
+ async runLoop(sessionId, participants, chairman, sendSystemMessage, onComplete) {
61
71
  while (true) {
62
72
  const session = Array.from(this.sessions.values()).find((s) => s.id === sessionId);
63
73
  if (!session || session.status !== "running")
@@ -101,62 +111,88 @@ export class DiscussionManager {
101
111
  const errorNames = participants
102
112
  .map((participant) => participant.name)
103
113
  .filter((name) => (replies[name] || "").trim().startsWith("[ERROR]"));
104
- const allNoReply = participants.every((participant) => {
114
+ const allNoReply = participants.length > 0 && participants.every((participant) => {
105
115
  const text = (replies[participant.name] || "").trim();
106
116
  return !text || text.toUpperCase() === "NO_REPLY" || text.startsWith("[ERROR]");
107
117
  });
108
118
  if (sendSystemMessage && !allNoReply && (noReplyNames.length > 0 || errorNames.length > 0)) {
109
119
  const parts = [];
120
+ const t = getI18n(current.locale).labels;
110
121
  if (noReplyNames.length > 0)
111
- parts.push(`${noReplyNames.join("、")} 无新增回复`);
122
+ parts.push(`${noReplyNames.join("、")} ${t.noNewReply}`);
112
123
  if (errorNames.length > 0)
113
- parts.push(`${errorNames.join("、")} 出错`);
114
- await sendSystemMessage(`💬 第 ${current.currentRound}/${current.maxRounds} 轮:${parts.join(";")}`).catch(() => { });
124
+ parts.push(`${errorNames.join("、")} ${t.error}`);
125
+ await sendSystemMessage(t.discussionRoundNotice(current.currentRound, current.maxRounds, parts.join(current.locale === "zh" ? ";" : "; "))).catch(() => { });
126
+ }
127
+ const mustFinish = allNoReply || current.currentRound >= current.maxRounds;
128
+ if (chairman) {
129
+ const chairmanPrompt = this.buildChairmanPrompt(current, replies, mustFinish);
130
+ const chairResult = await chairman.runDiscussionTurn(current.chatId, chairmanPrompt, { round: current.currentRound, maxRounds: current.maxRounds });
131
+ const chairText = (chairResult.text || "").trim();
132
+ replies[chairman.name] = chairText;
133
+ const latest = current.completedRounds[current.completedRounds.length - 1];
134
+ if (latest)
135
+ latest.replies[chairman.name] = chairText;
136
+ const wantsFinal = this.hasFinalSummaryMarker(chairText);
137
+ if (mustFinish || wantsFinal) {
138
+ current.status = "completed";
139
+ this.sessions.delete(current.chatId);
140
+ if (onComplete)
141
+ await onComplete({ chatId: current.chatId, reason: "chairman_final", chairmanName: chairman.name }).catch(() => { });
142
+ else if (sendSystemMessage)
143
+ await sendSystemMessage(getI18n(current.locale).labels.discussEndedChairman(chairman.name)).catch(() => { });
144
+ return;
145
+ }
115
146
  }
116
- if (allNoReply) {
147
+ else if (allNoReply) {
117
148
  current.status = "completed";
118
149
  this.sessions.delete(current.chatId);
150
+ if (onComplete)
151
+ await onComplete({ chatId: current.chatId, reason: "all_no_reply" }).catch(() => { });
119
152
  if (sendSystemMessage)
120
- await sendSystemMessage(`💬 Discuss 已结束:第 ${current.currentRound} 轮没有新的有效补充。`).catch(() => { });
153
+ await sendSystemMessage(getI18n(current.locale).labels.discussEndedNoNew(current.currentRound)).catch(() => { });
121
154
  return;
122
155
  }
123
- if (current.currentRound >= current.maxRounds) {
156
+ else if (current.currentRound >= current.maxRounds) {
124
157
  current.status = "completed";
125
158
  this.sessions.delete(current.chatId);
159
+ if (onComplete)
160
+ await onComplete({ chatId: current.chatId, reason: "max_rounds" }).catch(() => { });
126
161
  if (sendSystemMessage)
127
- await sendSystemMessage(`💬 Discuss 已完成:已达到 ${current.maxRounds} 轮。`).catch(() => { });
162
+ await sendSystemMessage(getI18n(current.locale).labels.discussMaxRounds(current.maxRounds)).catch(() => { });
128
163
  return;
129
164
  }
130
165
  current.currentRound += 1;
131
166
  }
132
167
  }
168
+ hasFinalSummaryMarker(text) {
169
+ return /(^|\n)\s*FINAL_SUMMARY\s*[::]/i.test(text) || /(^|\n)\s*最终总结\s*[::]/.test(text);
170
+ }
171
+ buildChairmanPrompt(session, replies, mustFinish) {
172
+ const t = getI18n(session.locale);
173
+ const lines = Object.entries(replies).map(([bot, text]) => `- ${bot}: ${text || "NO_REPLY"}`);
174
+ return t.chairmanPrompt({
175
+ topic: session.topic,
176
+ round: session.currentRound,
177
+ maxRounds: session.maxRounds,
178
+ replies: lines.length ? lines.join("\n") : t.labels.noRegularReplies,
179
+ mustFinish,
180
+ });
181
+ }
133
182
  buildPrompt(session) {
183
+ const t = getI18n(session.locale);
134
184
  const previous = session.completedRounds.length === 0
135
- ? "(暂无,当前是第一轮)"
185
+ ? t.labels.noPreviousRounds
136
186
  : (() => {
137
187
  const round = session.completedRounds[session.completedRounds.length - 1];
138
188
  const lines = Object.entries(round.replies).map(([bot, text]) => `- ${bot}: ${text || "NO_REPLY"}`);
139
189
  return `Round ${round.round}:\n${lines.join("\n")}`;
140
190
  })();
141
- return [
142
- "这是一个多智能体结构化讨论。",
143
- "",
144
- "话题:",
145
- session.topic,
146
- "",
147
- `当前轮次:${session.currentRound}`,
148
- "",
149
- "已完成的轮次:",
191
+ return t.discussParticipantPrompt({
192
+ topic: session.topic,
193
+ round: session.currentRound,
150
194
  previous,
151
- "",
152
- "本轮其他 bot 的回复你暂时看不到,请基于同一份上下文独立给出观点。",
153
- "",
154
- "要求:",
155
- "1. 不要重复前几轮已经说过的观点。",
156
- "2. 只补充新的、有价值的信息。",
157
- "3. 如果没有新东西,回复 NO_REPLY。",
158
- "4. 简洁作答。",
159
- ].join("\n");
195
+ });
160
196
  }
161
197
  }
162
198
  export const discussionManager = new DiscussionManager();
@@ -36,6 +36,7 @@ export declare class FeishuBot {
36
36
  /** Active chatSend trigger target so final replies and proactive session.message share one delivery key. */
37
37
  private activeDeliveryTargets;
38
38
  private adminOpenId;
39
+ private locale;
39
40
  private static allBots;
40
41
  constructor(config: BotConfig, openclawClient: OpenClawClient, store: MessageStore, adminOpenId?: string);
41
42
  private handleMessageRecalled;
@@ -45,6 +46,10 @@ export declare class FeishuBot {
45
46
  * Format: lma-<botname>-<chatId>
46
47
  */
47
48
  getSessionKey(chatId: string): string;
49
+ private chatLocale;
50
+ private isEn;
51
+ private lmaBridgePolicy;
52
+ private injectBridgePolicy;
48
53
  /**
49
54
  * Ensure the session for a given chatId exists with the correct model.
50
55
  * Lazy: only creates on first message in that chat.
@@ -74,6 +79,7 @@ export declare class FeishuBot {
74
79
  private processQueueInner;
75
80
  private shouldHandleBridgeCommand;
76
81
  private shouldRespond;
82
+ private getRoutingIntent;
77
83
  private isMentioned;
78
84
  private isAllMention;
79
85
  private isAllMentionItem;
@@ -93,8 +99,12 @@ export declare class FeishuBot {
93
99
  private deliverySourceId;
94
100
  private enqueueAndDispatchDelivery;
95
101
  private dispatchPendingDeliveries;
102
+ private hasFreeModeBot;
103
+ private mentionedBotNames;
96
104
  private isDiscussionCoordinator;
97
105
  private getDiscussionParticipants;
106
+ private getChairmanParticipant;
107
+ private asDiscussionParticipant;
98
108
  private runDiscussionTurn;
99
109
  private replyMessage;
100
110
  private extractBridgeAttachments;
@@ -125,7 +135,9 @@ export declare class FeishuBot {
125
135
  * Finds the bot's own reaction of that type and deletes it.
126
136
  */
127
137
  private removeReaction;
138
+ private handleLocaleCommand;
128
139
  private handleDiscussCommand;
140
+ private handleChairmanCommand;
129
141
  /**
130
142
  * Handle /status command: show current session info.
131
143
  */