openclaw-lark-multi-agent 1.0.13 → 1.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/dist/config.d.ts +4 -0
- package/dist/config.js +5 -0
- package/dist/discussion-manager.d.ts +16 -0
- package/dist/discussion-manager.js +68 -32
- package/dist/feishu-bot.d.ts +10 -0
- package/dist/feishu-bot.js +256 -60
- package/dist/i18n.d.ts +60 -0
- package/dist/i18n.js +140 -0
- package/dist/message-store.d.ts +8 -0
- package/dist/message-store.js +59 -9
- package/package.json +1 -1
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
|
|
30
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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();
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -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,8 @@ export declare class FeishuBot {
|
|
|
45
46
|
* Format: lma-<botname>-<chatId>
|
|
46
47
|
*/
|
|
47
48
|
getSessionKey(chatId: string): string;
|
|
49
|
+
private chatLocale;
|
|
50
|
+
private isEn;
|
|
48
51
|
private lmaBridgePolicy;
|
|
49
52
|
private injectBridgePolicy;
|
|
50
53
|
/**
|
|
@@ -76,6 +79,7 @@ export declare class FeishuBot {
|
|
|
76
79
|
private processQueueInner;
|
|
77
80
|
private shouldHandleBridgeCommand;
|
|
78
81
|
private shouldRespond;
|
|
82
|
+
private getRoutingIntent;
|
|
79
83
|
private isMentioned;
|
|
80
84
|
private isAllMention;
|
|
81
85
|
private isAllMentionItem;
|
|
@@ -95,8 +99,12 @@ export declare class FeishuBot {
|
|
|
95
99
|
private deliverySourceId;
|
|
96
100
|
private enqueueAndDispatchDelivery;
|
|
97
101
|
private dispatchPendingDeliveries;
|
|
102
|
+
private hasFreeModeBot;
|
|
103
|
+
private mentionedBotNames;
|
|
98
104
|
private isDiscussionCoordinator;
|
|
99
105
|
private getDiscussionParticipants;
|
|
106
|
+
private getChairmanParticipant;
|
|
107
|
+
private asDiscussionParticipant;
|
|
100
108
|
private runDiscussionTurn;
|
|
101
109
|
private replyMessage;
|
|
102
110
|
private extractBridgeAttachments;
|
|
@@ -127,7 +135,9 @@ export declare class FeishuBot {
|
|
|
127
135
|
* Finds the bot's own reaction of that type and deletes it.
|
|
128
136
|
*/
|
|
129
137
|
private removeReaction;
|
|
138
|
+
private handleLocaleCommand;
|
|
130
139
|
private handleDiscussCommand;
|
|
140
|
+
private handleChairmanCommand;
|
|
131
141
|
/**
|
|
132
142
|
* Handle /status command: show current session info.
|
|
133
143
|
*/
|
package/dist/feishu-bot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as lark from "@larksuiteoapi/node-sdk";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
+
import { getI18n, normalizeLocale } from "./i18n.js";
|
|
3
4
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
4
5
|
import { basename, extname, resolve } from "path";
|
|
5
6
|
import { getBridgeAttachmentsDir } from "./paths.js";
|
|
@@ -44,12 +45,14 @@ export class FeishuBot {
|
|
|
44
45
|
/** Active chatSend trigger target so final replies and proactive session.message share one delivery key. */
|
|
45
46
|
activeDeliveryTargets = new Map();
|
|
46
47
|
adminOpenId;
|
|
48
|
+
locale;
|
|
47
49
|
static allBots = new Map();
|
|
48
50
|
constructor(config, openclawClient, store, adminOpenId) {
|
|
49
51
|
this.config = config;
|
|
50
52
|
this.openclawClient = openclawClient;
|
|
51
53
|
this.store = store;
|
|
52
54
|
this.adminOpenId = adminOpenId || null;
|
|
55
|
+
this.locale = normalizeLocale(config.locale);
|
|
53
56
|
// Session keys are now per-chat: lma-<botname>-<chatId>
|
|
54
57
|
this.client = new lark.Client({
|
|
55
58
|
appId: config.appId,
|
|
@@ -100,13 +103,14 @@ export class FeishuBot {
|
|
|
100
103
|
getSessionKey(chatId) {
|
|
101
104
|
return `lma-${this.config.name.toLowerCase()}-${chatId}`;
|
|
102
105
|
}
|
|
106
|
+
chatLocale(chatId) {
|
|
107
|
+
return normalizeLocale(chatId ? this.store.getChatLocale(chatId) || this.locale : this.locale);
|
|
108
|
+
}
|
|
109
|
+
isEn(chatId) {
|
|
110
|
+
return this.chatLocale(chatId) === "en";
|
|
111
|
+
}
|
|
103
112
|
lmaBridgePolicy() {
|
|
104
|
-
return
|
|
105
|
-
"[LMA bridge policy]",
|
|
106
|
-
"你正在 OpenClaw Lark Multi-Agent bridge 会话中。",
|
|
107
|
-
"不要调用 message、sessions_send、feishu_im_user_message 或任何主动向飞书/外部聊天发送消息的工具。",
|
|
108
|
-
"直接在当前回复中作答;LMA bridge 会负责把最终回复投递回原始飞书群。",
|
|
109
|
-
].join("\n");
|
|
113
|
+
return getI18n(this.locale).bridgePolicy;
|
|
110
114
|
}
|
|
111
115
|
async injectBridgePolicy(sessionKey) {
|
|
112
116
|
await this.openclawClient.injectAssistantMessage({
|
|
@@ -384,6 +388,7 @@ export class FeishuBot {
|
|
|
384
388
|
}
|
|
385
389
|
if (!cleanText.trim())
|
|
386
390
|
return;
|
|
391
|
+
const routing = this.getRoutingIntent(chatType, message, messageType === "text" ? (content.text || "") : "");
|
|
387
392
|
// Commands may be prefixed by @all / @bot in group chats. Strip those
|
|
388
393
|
// leading routing mentions before deciding whether this is a bridge command
|
|
389
394
|
// or an escaped OpenClaw command.
|
|
@@ -415,15 +420,34 @@ export class FeishuBot {
|
|
|
415
420
|
// Single slash commands are handled by the bridge. Double slash commands were
|
|
416
421
|
// already unescaped above and should pass through to OpenClaw instead.
|
|
417
422
|
const isBridgeCommand = !commandText.startsWith("//");
|
|
418
|
-
const isCommand = isBridgeCommand && /^\/(help|status|compact|reset|verbose|free|mute|mode|discuss)/.test(cleanText.trim());
|
|
423
|
+
const isCommand = isBridgeCommand && /^\/(help|status|compact|reset|verbose|free|mute|mode|discuss|chairman|locale)/.test(cleanText.trim());
|
|
419
424
|
if (isCommand) {
|
|
420
425
|
// In group chats, most bridge commands must be explicitly routed to this
|
|
421
426
|
// bot or @all. /discuss is a group-level command, so an unmentioned
|
|
422
427
|
// /discuss command is handled by one coordinator bot to avoid N replies.
|
|
423
428
|
const isDiscussCommand = cleanText.trim().startsWith("/discuss");
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
const isChairmanCommand = cleanText.trim().startsWith("/chairman");
|
|
430
|
+
const isLocaleCommand = cleanText.trim().startsWith("/locale");
|
|
431
|
+
if (chatType !== "p2p") {
|
|
432
|
+
if (isChairmanCommand || isLocaleCommand) {
|
|
433
|
+
// Group-level settings; one coordinator handles them.
|
|
434
|
+
if (!this.isDiscussionCoordinator())
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
else if (isDiscussCommand && !routing.hasTargetedMention) {
|
|
438
|
+
// Untargeted or @all /discuss is group-level; one coordinator handles it.
|
|
439
|
+
if (!this.isDiscussionCoordinator())
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
else if (routing.hasTargetedMention) {
|
|
443
|
+
// Explicitly targeted commands belong only to the mentioned bot.
|
|
444
|
+
if (!routing.isCurrentBotMentioned)
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
else if (!routing.isAllMention) {
|
|
448
|
+
// Other bridge commands in a group need an explicit target or @all.
|
|
426
449
|
return;
|
|
450
|
+
}
|
|
427
451
|
}
|
|
428
452
|
const markCommandSynced = () => {
|
|
429
453
|
if (insertedId > 0) {
|
|
@@ -443,10 +467,12 @@ export class FeishuBot {
|
|
|
443
467
|
`🧹 /compact — 压缩当前 bot 的 OpenClaw session`,
|
|
444
468
|
`🔄 /reset — 重置当前 bot 的 OpenClaw session`,
|
|
445
469
|
`🔊 /verbose — 开关当前聊天里的 Tool Call 显示`,
|
|
446
|
-
`🔓 /free
|
|
470
|
+
`🔓 /free [on|off] — 开关当前 bot 的 free 模式(不 @ 也可回复)`,
|
|
447
471
|
`🤐 /mute — 切换当前 bot 的 mute 模式(禁言,不转发 OpenClaw)`,
|
|
448
472
|
`🎛️ /mode — 查看当前 bot 在当前群聊的模式`,
|
|
449
473
|
`💬 /discuss on|off|status|stop|rounds N — 群级多 bot 连续讨论`,
|
|
474
|
+
`👑 /chairman @Bot|off — 设置/查看/清除本群唯一 Chairman`,
|
|
475
|
+
`🌐 /locale zh|en — 设置/查看当前群语言`,
|
|
450
476
|
`❓ /help — 显示此帮助信息`,
|
|
451
477
|
``,
|
|
452
478
|
`OpenClaw 原生命令(双斜杠,会转成单斜杠发给 OpenClaw)`,
|
|
@@ -509,14 +535,24 @@ export class FeishuBot {
|
|
|
509
535
|
markCommandSynced();
|
|
510
536
|
return;
|
|
511
537
|
}
|
|
538
|
+
const parts = cleanText.trim().split(/\s+/).filter(Boolean);
|
|
539
|
+
const arg = (parts[1] || "toggle").toLowerCase();
|
|
540
|
+
if (!["toggle", "on", "off"].includes(arg)) {
|
|
541
|
+
await this.replyMessage(messageId, "❌ 用法:/free [on|off]");
|
|
542
|
+
markCommandSynced();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
512
545
|
const current = this.store.getBotMode(this.config.name, chatId);
|
|
513
|
-
const next = current === "free" ? "normal" : "free";
|
|
546
|
+
const next = arg === "on" ? "free" : arg === "off" ? "normal" : current === "free" ? "normal" : "free";
|
|
514
547
|
this.store.setBotMode(this.config.name, chatId, next);
|
|
515
548
|
if (next === "free") {
|
|
516
|
-
await this.replyMessage(messageId, `🔓 ${this.config.name} 已切换到 free
|
|
549
|
+
await this.replyMessage(messageId, `🔓 ${this.config.name} 已切换到 free 模式
|
|
550
|
+
不需要 @ 也可以回复普通人类消息;如果消息明确 @ 了其他 bot 或普通人,我不会抢答。
|
|
551
|
+
如需多轮自动讨论,请使用群级命令 /discuss on。`);
|
|
517
552
|
}
|
|
518
553
|
else {
|
|
519
|
-
await this.replyMessage(messageId, `🔒 ${this.config.name} 已切换到 normal
|
|
554
|
+
await this.replyMessage(messageId, `🔒 ${this.config.name} 已切换到 normal 模式
|
|
555
|
+
只有明确 @ 我才会回复`);
|
|
520
556
|
}
|
|
521
557
|
markCommandSynced();
|
|
522
558
|
return;
|
|
@@ -556,26 +592,53 @@ export class FeishuBot {
|
|
|
556
592
|
markCommandSynced();
|
|
557
593
|
return;
|
|
558
594
|
}
|
|
595
|
+
if (cleanText.trim().startsWith("/chairman")) {
|
|
596
|
+
await this.handleChairmanCommand(chatId, chatType, messageId, message.mentions || [], cleanText.trim());
|
|
597
|
+
markCommandSynced();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (cleanText.trim().startsWith("/locale")) {
|
|
601
|
+
await this.handleLocaleCommand(chatId, chatType, messageId, cleanText.trim());
|
|
602
|
+
markCommandSynced();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
559
605
|
}
|
|
560
606
|
// --- Discuss mode: group-level multi-bot round scheduler. It takes over
|
|
561
607
|
// plain human messages so normal Free mode does not duplicate Round 1.
|
|
562
608
|
// Targeted mentions must fall through to normal routing so @GPT still
|
|
563
609
|
// works while discuss mode is enabled.
|
|
564
610
|
if (chatType !== "p2p" && !isBot && this.store.getChatInfo(chatId)?.discuss) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!hasTargetedMention) {
|
|
611
|
+
// Discuss mode owns ordinary and @all human messages. Explicit @bot/@human
|
|
612
|
+
// falls through to normal targeted routing.
|
|
613
|
+
if (!routing.hasTargetedMention) {
|
|
614
|
+
const discussionLocale = this.chatLocale(chatId);
|
|
615
|
+
const chairman = this.getChairmanParticipant(chatId);
|
|
568
616
|
const participants = this.getDiscussionParticipants(chatId);
|
|
569
|
-
if (participants.length > 0) {
|
|
617
|
+
if (participants.length > 0 || chairman) {
|
|
570
618
|
discussionManager.startIfAbsent({
|
|
571
619
|
chatId,
|
|
572
620
|
rootMessageId: messageId,
|
|
573
621
|
topic: cleanText,
|
|
574
|
-
maxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds ||
|
|
622
|
+
maxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds || 10,
|
|
575
623
|
participants,
|
|
624
|
+
chairman,
|
|
576
625
|
sendSystemMessage: async (text) => { await this.sendMessage(chatId, text); },
|
|
626
|
+
locale: discussionLocale,
|
|
627
|
+
onComplete: async (event) => {
|
|
628
|
+
if (event.reason === "chairman_final") {
|
|
629
|
+
this.store.setDiscussMode(event.chatId, false);
|
|
630
|
+
await this.sendMessage(event.chatId, this.isEn(event.chatId)
|
|
631
|
+
? `💬 Discuss ended: Chairman ${event.chairmanName || ""} completed the final summary. Discuss mode has been turned off automatically.`
|
|
632
|
+
: `💬 Discuss 已结束:Chairman ${event.chairmanName || ""} 已完成总结,已自动关闭 Discuss 模式。`);
|
|
633
|
+
}
|
|
634
|
+
},
|
|
577
635
|
});
|
|
578
636
|
}
|
|
637
|
+
else if (this.isDiscussionCoordinator()) {
|
|
638
|
+
await this.sendMessage(chatId, this.isEn(chatId)
|
|
639
|
+
? "💬 Discuss is on, but there are no free bots or Chairman available. Use /free on or /chairman @Bot first."
|
|
640
|
+
: "💬 Discuss 已开启,但当前没有 free bot 或 Chairman 可参与。请先使用 /free on 或 /chairman @Bot。");
|
|
641
|
+
}
|
|
579
642
|
if (insertedId > 0)
|
|
580
643
|
this.store.markSynced(this.config.name, chatId, insertedId);
|
|
581
644
|
return;
|
|
@@ -583,7 +646,7 @@ export class FeishuBot {
|
|
|
583
646
|
}
|
|
584
647
|
// --- Mute mode: do not forward anything to OpenClaw. Only direct mentions get a local notice. ---
|
|
585
648
|
if (chatType !== "p2p" && !isBot && this.store.getBotMode(this.config.name, chatId) === "mute") {
|
|
586
|
-
if (
|
|
649
|
+
if (routing.isCurrentBotMentioned) {
|
|
587
650
|
await this.replyMessage(messageId, `🤐 ${this.config.name} 当前处于 mute 模式,发送 /mute 可解除`);
|
|
588
651
|
if (insertedId > 0) {
|
|
589
652
|
this.store.markSynced(this.config.name, chatId, insertedId);
|
|
@@ -593,7 +656,7 @@ export class FeishuBot {
|
|
|
593
656
|
return;
|
|
594
657
|
}
|
|
595
658
|
// --- Should this bot respond? ---
|
|
596
|
-
if (!this.shouldRespond(chatType, message, isBot, chatId,
|
|
659
|
+
if (!this.shouldRespond(chatType, message, isBot, chatId, routing))
|
|
597
660
|
return;
|
|
598
661
|
if (!isBot && insertedId > 0) {
|
|
599
662
|
this.store.markPendingTrigger(this.config.name, chatId, insertedId);
|
|
@@ -861,34 +924,47 @@ export class FeishuBot {
|
|
|
861
924
|
return true;
|
|
862
925
|
return this.isMentioned(mentions);
|
|
863
926
|
}
|
|
864
|
-
shouldRespond(chatType,
|
|
927
|
+
shouldRespond(chatType, _message, isBot, chatId, routing) {
|
|
865
928
|
if (chatType === "p2p")
|
|
866
929
|
return !isBot;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
// @all in text: all bots respond
|
|
873
|
-
if (this.isAllMention(rawText, mentions))
|
|
874
|
-
return true;
|
|
875
|
-
// Check if this bot is explicitly mentioned
|
|
876
|
-
if (this.isMentioned(mentions))
|
|
930
|
+
// Bot messages: only respond if this bot is explicitly mentioned.
|
|
931
|
+
if (isBot)
|
|
932
|
+
return routing.isCurrentBotMentioned;
|
|
933
|
+
// @all is an explicit broadcast to all non-muted bots.
|
|
934
|
+
if (routing.isAllMention)
|
|
877
935
|
return true;
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
936
|
+
// Explicit targeted mentions are exclusive. If this bot is not one of the
|
|
937
|
+
// mentioned bots, free/chairman must not steal the turn.
|
|
938
|
+
if (routing.hasTargetedMention)
|
|
939
|
+
return routing.isCurrentBotMentioned;
|
|
940
|
+
// Human-only mentions are also exclusive; free/chairman should not jump in.
|
|
941
|
+
if (routing.hasHumanMention)
|
|
883
942
|
return false;
|
|
884
|
-
// No bot mentioned: check current per-bot mode
|
|
885
943
|
if (chatId) {
|
|
886
944
|
if (this.store.getBotMode(this.config.name, chatId) === "free")
|
|
887
945
|
return true;
|
|
946
|
+
// Chairman fallback: if nobody is in free mode, the unique chairman
|
|
947
|
+
// answers ordinary unmentioned messages for the group.
|
|
948
|
+
const chairman = this.store.getChairmanBot(chatId);
|
|
949
|
+
if (chairman === this.config.name && !this.hasFreeModeBot(chatId))
|
|
950
|
+
return true;
|
|
888
951
|
}
|
|
889
|
-
// Default: don't respond without @
|
|
890
952
|
return false;
|
|
891
953
|
}
|
|
954
|
+
getRoutingIntent(chatType, message, rawText) {
|
|
955
|
+
const mentions = message.mentions || [];
|
|
956
|
+
const isAllMention = chatType !== "p2p" && this.isAllMention(rawText, mentions);
|
|
957
|
+
const targetedBotNames = this.mentionedBotNames(mentions);
|
|
958
|
+
const hasHumanMention = mentions.some((m) => !this.isAllMentionItem(m) && !this.mentionedBotName(m));
|
|
959
|
+
const hasTargetedMention = targetedBotNames.length > 0 || hasHumanMention;
|
|
960
|
+
return {
|
|
961
|
+
isAllMention,
|
|
962
|
+
targetedBotNames,
|
|
963
|
+
hasTargetedMention,
|
|
964
|
+
hasHumanMention,
|
|
965
|
+
isCurrentBotMentioned: targetedBotNames.includes(this.config.name),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
892
968
|
isMentioned(mentions) {
|
|
893
969
|
return mentions.some((m) => this.mentionedBotName(m) === this.config.name);
|
|
894
970
|
}
|
|
@@ -905,14 +981,24 @@ export class FeishuBot {
|
|
|
905
981
|
return null;
|
|
906
982
|
const candidates = [this, ...Array.from(FeishuBot.allBots.values()).filter((bot) => bot !== this)];
|
|
907
983
|
for (const bot of candidates) {
|
|
908
|
-
if (mention.id?.app_id === bot.config.appId)
|
|
984
|
+
if (mention.id?.app_id && mention.id.app_id === bot.config.appId)
|
|
909
985
|
return bot.config.name;
|
|
910
|
-
if (bot.botOpenId && mention.id?.open_id === bot.botOpenId)
|
|
986
|
+
if (bot.botOpenId && mention.id?.open_id && mention.id.open_id === bot.botOpenId)
|
|
911
987
|
return bot.config.name;
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
988
|
+
}
|
|
989
|
+
// Name is only a fallback because Feishu should normally provide app_id/open_id.
|
|
990
|
+
// Keep it exact to avoid shared-prefix bots like 万万(GPT) / 万万(Claude)
|
|
991
|
+
// stealing each other's mentions.
|
|
992
|
+
if (typeof mention.name === "string") {
|
|
993
|
+
const raw = mention.name.trim().replace(/^@+/, "").replace(/\s+/g, "").toLowerCase();
|
|
994
|
+
for (const bot of candidates) {
|
|
995
|
+
const botName = bot.config.name.trim().replace(/\s+/g, "").toLowerCase();
|
|
996
|
+
const exactNames = [
|
|
997
|
+
botName,
|
|
998
|
+
`万万(${botName})`,
|
|
999
|
+
`万万(${botName})`,
|
|
1000
|
+
];
|
|
1001
|
+
if (exactNames.includes(raw))
|
|
916
1002
|
return bot.config.name;
|
|
917
1003
|
}
|
|
918
1004
|
}
|
|
@@ -1112,6 +1198,16 @@ export class FeishuBot {
|
|
|
1112
1198
|
});
|
|
1113
1199
|
}
|
|
1114
1200
|
}
|
|
1201
|
+
hasFreeModeBot(chatId) {
|
|
1202
|
+
return Array.from(FeishuBot.allBots.values())
|
|
1203
|
+
.some((bot) => bot.store === this.store && bot.store.getBotMode(bot.config.name, chatId) === "free");
|
|
1204
|
+
}
|
|
1205
|
+
mentionedBotNames(mentions) {
|
|
1206
|
+
const names = mentions
|
|
1207
|
+
.map((mention) => this.mentionedBotName(mention))
|
|
1208
|
+
.filter((name) => Boolean(name));
|
|
1209
|
+
return Array.from(new Set(names));
|
|
1210
|
+
}
|
|
1115
1211
|
isDiscussionCoordinator() {
|
|
1116
1212
|
const bots = Array.from(FeishuBot.allBots.values()).filter((bot) => bot.store === this.store);
|
|
1117
1213
|
if (bots.length === 0)
|
|
@@ -1119,12 +1215,23 @@ export class FeishuBot {
|
|
|
1119
1215
|
return bots[0] === this;
|
|
1120
1216
|
}
|
|
1121
1217
|
getDiscussionParticipants(chatId) {
|
|
1218
|
+
const chairman = this.store.getChairmanBot(chatId);
|
|
1122
1219
|
return Array.from(FeishuBot.allBots.values())
|
|
1123
|
-
.filter((bot) => bot.store === this.store && bot.store.getBotMode(bot.config.name, chatId) === "free")
|
|
1124
|
-
.map((bot) => (
|
|
1220
|
+
.filter((bot) => bot.store === this.store && bot.config.name !== chairman && bot.store.getBotMode(bot.config.name, chatId) === "free")
|
|
1221
|
+
.map((bot) => this.asDiscussionParticipant(bot, chatId));
|
|
1222
|
+
}
|
|
1223
|
+
getChairmanParticipant(chatId) {
|
|
1224
|
+
const chairman = this.store.getChairmanBot(chatId);
|
|
1225
|
+
if (!chairman)
|
|
1226
|
+
return undefined;
|
|
1227
|
+
const bot = Array.from(FeishuBot.allBots.values()).find((candidate) => candidate.store === this.store && candidate.config.name === chairman);
|
|
1228
|
+
return bot ? this.asDiscussionParticipant(bot, chatId) : undefined;
|
|
1229
|
+
}
|
|
1230
|
+
asDiscussionParticipant(bot, chatId) {
|
|
1231
|
+
return {
|
|
1125
1232
|
name: bot.config.name,
|
|
1126
1233
|
runDiscussionTurn: async (_chatId, prompt, meta) => bot.runDiscussionTurn(chatId, prompt, meta),
|
|
1127
|
-
}
|
|
1234
|
+
};
|
|
1128
1235
|
}
|
|
1129
1236
|
async runDiscussionTurn(chatId, prompt, meta) {
|
|
1130
1237
|
const sessionKey = await this.ensureSession(chatId);
|
|
@@ -1151,8 +1258,12 @@ export class FeishuBot {
|
|
|
1151
1258
|
const rawVisibleReply = parsedReply.text.trim();
|
|
1152
1259
|
const discussionMarkerPattern = /\n*—— 第 \d+\/\d+ 轮 · .+$/;
|
|
1153
1260
|
const cleanVisibleReply = rawVisibleReply.replace(discussionMarkerPattern, "").trim();
|
|
1154
|
-
|
|
1155
|
-
|
|
1261
|
+
const userVisibleReply = cleanVisibleReply
|
|
1262
|
+
.replace(/(^|\n)\s*(FINAL_SUMMARY|CHAIRMAN_NOTE)\s*[::]\s*/gi, "$1")
|
|
1263
|
+
.replace(/(^|\n)\s*最终总结\s*[::]\s*/g, "$1")
|
|
1264
|
+
.trim();
|
|
1265
|
+
let displayReply = userVisibleReply;
|
|
1266
|
+
const isVisible = userVisibleReply.length > 0 && userVisibleReply.toUpperCase() !== "NO_REPLY";
|
|
1156
1267
|
if (isVisible && meta) {
|
|
1157
1268
|
const roundMarker = `—— 第 ${meta.round}/${meta.maxRounds} 轮 · ${this.config.name}`;
|
|
1158
1269
|
displayReply = `${displayReply}\n\n${roundMarker}`;
|
|
@@ -1448,49 +1559,125 @@ export class FeishuBot {
|
|
|
1448
1559
|
// ignore
|
|
1449
1560
|
}
|
|
1450
1561
|
}
|
|
1562
|
+
async handleLocaleCommand(chatId, chatType, messageId, text) {
|
|
1563
|
+
if (chatType === "p2p") {
|
|
1564
|
+
await this.replyMessage(messageId, "Locale is only configurable in group chats.");
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const parts = text.split(/\s+/).filter(Boolean);
|
|
1568
|
+
const value = (parts[1] || "").toLowerCase();
|
|
1569
|
+
if (!value) {
|
|
1570
|
+
const locale = this.chatLocale(chatId);
|
|
1571
|
+
await this.replyMessage(messageId, locale === "en" ? "🌐 Current locale: en" : "🌐 当前语言:zh");
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (value !== "zh" && value !== "en") {
|
|
1575
|
+
await this.replyMessage(messageId, "Usage: /locale zh|en");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
this.store.setChatLocale(chatId, value);
|
|
1579
|
+
await this.replyMessage(messageId, value === "en" ? "🌐 Locale set to en" : "🌐 语言已设置为 zh");
|
|
1580
|
+
}
|
|
1451
1581
|
async handleDiscussCommand(chatId, chatType, messageId, text) {
|
|
1452
1582
|
if (chatType === "p2p") {
|
|
1453
|
-
await this.replyMessage(messageId, "
|
|
1583
|
+
await this.replyMessage(messageId, "Discuss mode is only available in group chats.");
|
|
1454
1584
|
return;
|
|
1455
1585
|
}
|
|
1456
1586
|
const parts = text.split(/\s+/).filter(Boolean);
|
|
1457
1587
|
const action = parts[1] || "status";
|
|
1458
1588
|
if (action === "on") {
|
|
1589
|
+
const chairman = this.store.getChairmanBot(chatId);
|
|
1590
|
+
if (!chairman) {
|
|
1591
|
+
await this.replyMessage(messageId, this.isEn(chatId)
|
|
1592
|
+
? "❌ You must set a Chairman before enabling Discuss.\nUsage: /chairman @Bot"
|
|
1593
|
+
: "❌ 开启 Discuss 前必须先设置 Chairman。\n用法:/chairman @某个Bot");
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1459
1596
|
this.store.setDiscussMode(chatId, true);
|
|
1460
|
-
await this.replyMessage(messageId,
|
|
1597
|
+
await this.replyMessage(messageId, this.isEn(chatId)
|
|
1598
|
+
? `💬 Discuss enabled\nChairman: ${chairman}\nParticipants: all free-mode bots in this group + Chairman\nRounds: ${this.store.getChatInfo(chatId)?.discussMaxRounds || 10}`
|
|
1599
|
+
: `💬 Discuss 已开启\nChairman:${chairman}\n参与者:当前群所有 free 模式 bot + Chairman\n轮数:${this.store.getChatInfo(chatId)?.discussMaxRounds || 10}`);
|
|
1461
1600
|
return;
|
|
1462
1601
|
}
|
|
1463
1602
|
if (action === "off") {
|
|
1464
1603
|
this.store.setDiscussMode(chatId, false);
|
|
1465
1604
|
discussionManager.stop(chatId);
|
|
1466
|
-
await this.replyMessage(messageId, "💬 Discuss 已关闭");
|
|
1605
|
+
await this.replyMessage(messageId, this.isEn(chatId) ? "💬 Discuss disabled" : "💬 Discuss 已关闭");
|
|
1467
1606
|
return;
|
|
1468
1607
|
}
|
|
1469
1608
|
if (action === "stop") {
|
|
1470
1609
|
const stopped = discussionManager.stop(chatId);
|
|
1471
|
-
await this.replyMessage(messageId,
|
|
1610
|
+
await this.replyMessage(messageId, this.isEn(chatId)
|
|
1611
|
+
? (stopped ? "💬 Current discuss stopped" : "💬 No active discuss")
|
|
1612
|
+
: (stopped ? "💬 当前 discuss 已停止" : "💬 当前没有运行中的 discuss"));
|
|
1472
1613
|
return;
|
|
1473
1614
|
}
|
|
1474
1615
|
if (action === "rounds") {
|
|
1475
1616
|
const n = Number.parseInt(parts[2] || "", 10);
|
|
1476
1617
|
if (!Number.isFinite(n)) {
|
|
1477
|
-
await this.replyMessage(messageId, "❌
|
|
1618
|
+
await this.replyMessage(messageId, "❌ Usage: /discuss rounds <1-10>");
|
|
1478
1619
|
return;
|
|
1479
1620
|
}
|
|
1480
1621
|
this.store.setDiscussMaxRounds(chatId, n);
|
|
1481
|
-
await this.replyMessage(messageId,
|
|
1622
|
+
await this.replyMessage(messageId, this.isEn(chatId)
|
|
1623
|
+
? `💬 Discuss rounds set to ${this.store.getChatInfo(chatId)?.discussMaxRounds || n}`
|
|
1624
|
+
: `💬 Discuss 轮数已设置为 ${this.store.getChatInfo(chatId)?.discussMaxRounds || n}`);
|
|
1482
1625
|
return;
|
|
1483
1626
|
}
|
|
1484
1627
|
const info = this.store.getChatInfo(chatId);
|
|
1485
1628
|
const active = discussionManager.status(chatId);
|
|
1486
1629
|
const participants = this.getDiscussionParticipants(chatId).map((p) => p.name);
|
|
1487
|
-
await this.replyMessage(messageId, [
|
|
1630
|
+
await this.replyMessage(messageId, this.isEn(chatId) ? [
|
|
1488
1631
|
`💬 Discuss: ${info?.discuss ? "on" : "off"}`,
|
|
1489
|
-
|
|
1490
|
-
|
|
1632
|
+
`Rounds: ${info?.discussMaxRounds || 10}`,
|
|
1633
|
+
`Chairman: ${info?.chairmanBot || "not set"}`,
|
|
1634
|
+
`Participants: ${participants.length ? participants.join(", ") : "(no free bot / chairman)"}`,
|
|
1635
|
+
active ? `Active: round ${active.currentRound}/${active.maxRounds}, topic=${active.topic.slice(0, 80)}` : "Active: none",
|
|
1636
|
+
].join("\n") : [
|
|
1637
|
+
`💬 Discuss: ${info?.discuss ? "on" : "off"}`,
|
|
1638
|
+
`轮数:${info?.discussMaxRounds || 10}`,
|
|
1639
|
+
`Chairman:${info?.chairmanBot || "未设置"}`,
|
|
1640
|
+
`参与者:${participants.length ? participants.join(", ") : "(无 free bot / chairman)"}`,
|
|
1491
1641
|
active ? `运行中:第 ${active.currentRound}/${active.maxRounds} 轮,topic=${active.topic.slice(0, 80)}` : "运行中:无",
|
|
1492
1642
|
].join("\n"));
|
|
1493
1643
|
}
|
|
1644
|
+
async handleChairmanCommand(chatId, chatType, messageId, mentions, text) {
|
|
1645
|
+
if (chatType === "p2p") {
|
|
1646
|
+
await this.replyMessage(messageId, "❌ Chairman 只在群聊中可用");
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const parts = text.split(/\s+/).filter(Boolean);
|
|
1650
|
+
const action = (parts[1] || "status").toLowerCase();
|
|
1651
|
+
if (["off", "clear", "none"].includes(action)) {
|
|
1652
|
+
const previous = this.store.getChairmanBot(chatId);
|
|
1653
|
+
this.store.clearChairmanBot(chatId);
|
|
1654
|
+
await this.replyMessage(messageId, previous ? `✅ 已清除当前群 Chairman(原 ${previous})` : "✅ 当前群没有 Chairman");
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const botNames = this.mentionedBotNames(mentions);
|
|
1658
|
+
if (botNames.length === 0) {
|
|
1659
|
+
const current = this.store.getChairmanBot(chatId);
|
|
1660
|
+
await this.replyMessage(messageId, current
|
|
1661
|
+
? `👑 当前 Chairman:${current}\n作用:普通消息无人 free 时由 TA 回答;Discuss 模式下负责主持、调停和最终总结。`
|
|
1662
|
+
: "👑 当前没有 Chairman\n用法:/chairman @某个Bot");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (botNames.length > 1) {
|
|
1666
|
+
await this.replyMessage(messageId, "❌ 一个群只能设置一个 Chairman。请只 @ 一个 bot。");
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const next = botNames[0];
|
|
1670
|
+
const previous = this.store.getChairmanBot(chatId);
|
|
1671
|
+
this.store.setChairmanBot(chatId, next);
|
|
1672
|
+
const mode = this.store.getBotMode(next, chatId);
|
|
1673
|
+
const lines = [previous && previous !== next ? `✅ Chairman 已从 ${previous} 切换为 ${next}` : `✅ Chairman 已设置为 ${next}`];
|
|
1674
|
+
lines.push("作用:");
|
|
1675
|
+
lines.push("- 当没有 free bot 且普通消息无人被 @ 时,Chairman 会负责回答");
|
|
1676
|
+
lines.push("- Discuss 模式下,Chairman 会参与主持、调停并做最终总结");
|
|
1677
|
+
if (mode === "mute")
|
|
1678
|
+
lines.push(`⚠️ ${next} 当前是 mute,但 Chairman 场景下仍会发言`);
|
|
1679
|
+
await this.replyMessage(messageId, lines.join("\n"));
|
|
1680
|
+
}
|
|
1494
1681
|
/**
|
|
1495
1682
|
* Handle /status command: show current session info.
|
|
1496
1683
|
*/
|
|
@@ -1520,6 +1707,13 @@ export class FeishuBot {
|
|
|
1520
1707
|
const status = session?.status || "unknown";
|
|
1521
1708
|
const verboseStatus = this.store.getBotVerbose(this.config.name, chatId) ? "🔊 开启" : "🔇 关闭";
|
|
1522
1709
|
const mode = chatType === "p2p" ? "normal" : this.store.getBotMode(this.config.name, chatId);
|
|
1710
|
+
const chairman = chatType === "p2p" ? "" : this.store.getChairmanBot(chatId);
|
|
1711
|
+
const chairmanStatus = chatType === "p2p"
|
|
1712
|
+
? "不适用"
|
|
1713
|
+
: chairman
|
|
1714
|
+
? chairman === this.config.name ? `👑 是(${chairman})` : `否(当前:${chairman})`
|
|
1715
|
+
: "未设置";
|
|
1716
|
+
const localeStatus = chatType === "p2p" ? this.locale : this.chatLocale(chatId);
|
|
1523
1717
|
const statusText = [
|
|
1524
1718
|
`📊 ${this.config.name} Bot Status`,
|
|
1525
1719
|
`━━━━━━━━━━━━━━━━━━`,
|
|
@@ -1534,6 +1728,8 @@ export class FeishuBot {
|
|
|
1534
1728
|
`📥 输入: ${fmtK(inputTokens)} | 📤 输出: ${fmtK(outputTokens)}`,
|
|
1535
1729
|
`🔧 Verbose: ${verboseStatus}`,
|
|
1536
1730
|
`🎛️ Mode: ${mode}`,
|
|
1731
|
+
`👑 Chairman: ${chairmanStatus}`,
|
|
1732
|
+
`🌐 Locale: ${localeStatus}`,
|
|
1537
1733
|
].join("\n");
|
|
1538
1734
|
await this.replyMessage(messageId, statusText);
|
|
1539
1735
|
}
|
|
@@ -1593,7 +1789,7 @@ export class FeishuBot {
|
|
|
1593
1789
|
freeDiscussion: this.store.getChatInfo(chatId)?.freeDiscussion || false,
|
|
1594
1790
|
verbose: this.store.getChatInfo(chatId)?.verbose || false,
|
|
1595
1791
|
discuss: this.store.getChatInfo(chatId)?.discuss || false,
|
|
1596
|
-
discussMaxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds ||
|
|
1792
|
+
discussMaxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds || 10,
|
|
1597
1793
|
updatedAt: Date.now(),
|
|
1598
1794
|
});
|
|
1599
1795
|
return;
|
|
@@ -1632,7 +1828,7 @@ export class FeishuBot {
|
|
|
1632
1828
|
freeDiscussion: this.store.getChatInfo(chatId)?.freeDiscussion || false,
|
|
1633
1829
|
verbose: this.store.getChatInfo(chatId)?.verbose || false,
|
|
1634
1830
|
discuss: this.store.getChatInfo(chatId)?.discuss || false,
|
|
1635
|
-
discussMaxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds ||
|
|
1831
|
+
discussMaxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds || 10,
|
|
1636
1832
|
updatedAt: Date.now(),
|
|
1637
1833
|
});
|
|
1638
1834
|
console.log(`[${this.config.name}] Cached chat info: ${chatName} (${chatId.slice(-8)})`);
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Locale = "zh" | "en";
|
|
2
|
+
export declare function normalizeLocale(value?: string | null): Locale;
|
|
3
|
+
export declare function isLocale(value: string): value is Locale;
|
|
4
|
+
declare const dict: {
|
|
5
|
+
readonly zh: {
|
|
6
|
+
readonly bridgePolicy: string;
|
|
7
|
+
readonly discussParticipantPrompt: (p: {
|
|
8
|
+
topic: string;
|
|
9
|
+
round: number;
|
|
10
|
+
previous: string;
|
|
11
|
+
}) => string;
|
|
12
|
+
readonly chairmanPrompt: (p: {
|
|
13
|
+
topic: string;
|
|
14
|
+
round: number;
|
|
15
|
+
maxRounds: number;
|
|
16
|
+
replies: string;
|
|
17
|
+
mustFinish: boolean;
|
|
18
|
+
}) => string;
|
|
19
|
+
readonly labels: {
|
|
20
|
+
readonly noPreviousRounds: "(暂无,当前是第一轮)";
|
|
21
|
+
readonly noRegularReplies: "(本轮没有普通参与者发言)";
|
|
22
|
+
readonly noNewReply: "无新增回复";
|
|
23
|
+
readonly error: "出错";
|
|
24
|
+
readonly discussionRoundNotice: (round: number, max: number, parts: string) => string;
|
|
25
|
+
readonly round: "轮";
|
|
26
|
+
readonly discussEndedChairman: (name: string) => string;
|
|
27
|
+
readonly discussEndedNoNew: (round: number) => string;
|
|
28
|
+
readonly discussMaxRounds: (max: number) => string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
readonly en: {
|
|
32
|
+
readonly bridgePolicy: string;
|
|
33
|
+
readonly discussParticipantPrompt: (p: {
|
|
34
|
+
topic: string;
|
|
35
|
+
round: number;
|
|
36
|
+
previous: string;
|
|
37
|
+
}) => string;
|
|
38
|
+
readonly chairmanPrompt: (p: {
|
|
39
|
+
topic: string;
|
|
40
|
+
round: number;
|
|
41
|
+
maxRounds: number;
|
|
42
|
+
replies: string;
|
|
43
|
+
mustFinish: boolean;
|
|
44
|
+
}) => string;
|
|
45
|
+
readonly labels: {
|
|
46
|
+
readonly noPreviousRounds: "(None yet; this is the first round)";
|
|
47
|
+
readonly noRegularReplies: "(No regular participant replies in this round)";
|
|
48
|
+
readonly noNewReply: "no new reply";
|
|
49
|
+
readonly error: "error";
|
|
50
|
+
readonly discussionRoundNotice: (round: number, max: number, parts: string) => string;
|
|
51
|
+
readonly round: "round";
|
|
52
|
+
readonly discussEndedChairman: (name: string) => string;
|
|
53
|
+
readonly discussEndedNoNew: (round: number) => string;
|
|
54
|
+
readonly discussMaxRounds: (max: number) => string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export type I18n = typeof dict[Locale];
|
|
59
|
+
export declare function getI18n(locale?: string | null): I18n;
|
|
60
|
+
export {};
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export function normalizeLocale(value) {
|
|
2
|
+
const normalized = (value || "").trim().toLowerCase();
|
|
3
|
+
if (normalized.startsWith("en"))
|
|
4
|
+
return "en";
|
|
5
|
+
return "zh";
|
|
6
|
+
}
|
|
7
|
+
export function isLocale(value) {
|
|
8
|
+
return value === "zh" || value === "en";
|
|
9
|
+
}
|
|
10
|
+
const dict = {
|
|
11
|
+
zh: {
|
|
12
|
+
bridgePolicy: [
|
|
13
|
+
"[LMA bridge policy]",
|
|
14
|
+
"你正在 OpenClaw Lark Multi-Agent bridge 会话中。",
|
|
15
|
+
"不要调用 message、sessions_send、feishu_im_user_message 或任何主动向飞书/外部聊天发送消息的工具。",
|
|
16
|
+
"直接在当前回复中作答;LMA bridge 会负责把最终回复投递回原始飞书群。",
|
|
17
|
+
].join("\n"),
|
|
18
|
+
discussParticipantPrompt: (p) => [
|
|
19
|
+
"这是一个多智能体结构化讨论。",
|
|
20
|
+
"",
|
|
21
|
+
"话题:",
|
|
22
|
+
p.topic,
|
|
23
|
+
"",
|
|
24
|
+
`当前轮次:${p.round}`,
|
|
25
|
+
"",
|
|
26
|
+
"已完成的轮次:",
|
|
27
|
+
p.previous,
|
|
28
|
+
"",
|
|
29
|
+
"本轮其他 bot 的回复你暂时看不到,请基于同一份上下文独立给出观点。",
|
|
30
|
+
"",
|
|
31
|
+
"要求:",
|
|
32
|
+
"1. 不要重复前几轮已经说过的观点。",
|
|
33
|
+
"2. 只补充新的、有价值的信息。",
|
|
34
|
+
"3. 如果没有新东西,回复 NO_REPLY。",
|
|
35
|
+
"4. 简洁作答。",
|
|
36
|
+
].join("\n"),
|
|
37
|
+
chairmanPrompt: (p) => [
|
|
38
|
+
"这是一个多智能体结构化讨论。你是本群的 Chairman / 主席。",
|
|
39
|
+
"",
|
|
40
|
+
"话题:",
|
|
41
|
+
p.topic,
|
|
42
|
+
"",
|
|
43
|
+
`当前轮次:${p.round}/${p.maxRounds}`,
|
|
44
|
+
"",
|
|
45
|
+
"本轮发言:",
|
|
46
|
+
p.replies || "(本轮没有普通参与者发言)",
|
|
47
|
+
"",
|
|
48
|
+
"你的职责:",
|
|
49
|
+
"1. 先发表你自己的实质观点和判断,不要只做中立转述。",
|
|
50
|
+
"2. 识别大家已经达成的共识。",
|
|
51
|
+
"3. 扮演质疑者:主动检查薄弱证据、跳跃结论、未验证假设、遗漏风险和可能错误。",
|
|
52
|
+
"4. 识别还没解决的关键分歧。",
|
|
53
|
+
"5. 如果观点冲突,要调停并指出下一轮应聚焦的问题。",
|
|
54
|
+
"6. 如果已经足够清楚,或者本轮必须结束,请做最终总结。",
|
|
55
|
+
"",
|
|
56
|
+
p.mustFinish
|
|
57
|
+
? "本轮必须结束。请以 `FINAL_SUMMARY:` 开头,先给出你的个人判断,再指出你认为仍需警惕的问题/薄弱点,最后给出最终总结、共识、分歧和下一步建议。"
|
|
58
|
+
: "如果应继续讨论,请以 `CHAIRMAN_NOTE:` 开头,先给出你的个人判断,再提出必要质疑(薄弱证据、跳跃结论、未验证假设、遗漏风险),最后简要调停并提出下一轮聚焦问题;如果已经可以结束,请以 `FINAL_SUMMARY:` 开头,先给出你的个人判断和必要质疑,再做最终总结。",
|
|
59
|
+
"",
|
|
60
|
+
"请简洁、有主持感,但必须包含你自己的观点;在需要时要敢于质疑,不要只做中立转述,也不要只重复普通参与者的长篇内容。",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
labels: {
|
|
63
|
+
noPreviousRounds: "(暂无,当前是第一轮)",
|
|
64
|
+
noRegularReplies: "(本轮没有普通参与者发言)",
|
|
65
|
+
noNewReply: "无新增回复",
|
|
66
|
+
error: "出错",
|
|
67
|
+
discussionRoundNotice: (round, max, parts) => `💬 第 ${round}/${max} 轮:${parts}`,
|
|
68
|
+
round: "轮",
|
|
69
|
+
discussEndedChairman: (name) => `💬 Discuss 已结束:Chairman ${name} 已完成总结。`,
|
|
70
|
+
discussEndedNoNew: (round) => `💬 Discuss 已结束:第 ${round} 轮没有新的有效补充。`,
|
|
71
|
+
discussMaxRounds: (max) => `💬 Discuss 已完成:已达到 ${max} 轮。`,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
en: {
|
|
75
|
+
bridgePolicy: [
|
|
76
|
+
"[LMA bridge policy]",
|
|
77
|
+
"You are in an OpenClaw Lark Multi-Agent bridge session.",
|
|
78
|
+
"Do not call message, sessions_send, feishu_im_user_message, or any proactive external-chat sending tool.",
|
|
79
|
+
"Reply directly in the current assistant response; the LMA bridge will deliver the final reply back to the original Feishu chat.",
|
|
80
|
+
].join("\n"),
|
|
81
|
+
discussParticipantPrompt: (p) => [
|
|
82
|
+
"This is a structured multi-agent discussion.",
|
|
83
|
+
"",
|
|
84
|
+
"Topic:",
|
|
85
|
+
p.topic,
|
|
86
|
+
"",
|
|
87
|
+
`Current round: ${p.round}`,
|
|
88
|
+
"",
|
|
89
|
+
"Completed rounds:",
|
|
90
|
+
p.previous,
|
|
91
|
+
"",
|
|
92
|
+
"You cannot see other bots' replies in this round yet. Give an independent view based on the same context.",
|
|
93
|
+
"",
|
|
94
|
+
"Rules:",
|
|
95
|
+
"1. Do not repeat points already made in previous rounds.",
|
|
96
|
+
"2. Only add new, useful information.",
|
|
97
|
+
"3. If you have nothing new, reply exactly NO_REPLY.",
|
|
98
|
+
"4. Be concise.",
|
|
99
|
+
].join("\n"),
|
|
100
|
+
chairmanPrompt: (p) => [
|
|
101
|
+
"This is a structured multi-agent discussion. You are the Chairman of this group.",
|
|
102
|
+
"",
|
|
103
|
+
"Topic:",
|
|
104
|
+
p.topic,
|
|
105
|
+
"",
|
|
106
|
+
`Current round: ${p.round}/${p.maxRounds}`,
|
|
107
|
+
"",
|
|
108
|
+
"This round's replies:",
|
|
109
|
+
p.replies || "(No regular participant replies in this round)",
|
|
110
|
+
"",
|
|
111
|
+
"Your responsibilities:",
|
|
112
|
+
"1. First state your own substantive view and judgment; do not merely summarize neutrally.",
|
|
113
|
+
"2. Identify the consensus already reached.",
|
|
114
|
+
"3. Act as a challenger: inspect weak evidence, logical jumps, unverified assumptions, missed risks, and possible mistakes.",
|
|
115
|
+
"4. Identify unresolved key disagreements.",
|
|
116
|
+
"5. If views conflict, mediate and specify what the next round should focus on.",
|
|
117
|
+
"6. If the discussion is sufficiently clear, or this round must finish, provide a final summary.",
|
|
118
|
+
"",
|
|
119
|
+
p.mustFinish
|
|
120
|
+
? "This round must finish. Start with `FINAL_SUMMARY:`. First give your own judgment, then point out remaining caveats/weak spots, then provide the final summary, consensus, disagreements, and next steps."
|
|
121
|
+
: "If the discussion should continue, start with `CHAIRMAN_NOTE:`. First give your own judgment, then raise necessary challenges (weak evidence, logical jumps, unverified assumptions, missed risks), then mediate briefly and propose the next focus. If it can end now, start with `FINAL_SUMMARY:`, give your own judgment and necessary challenges first, then provide the final summary.",
|
|
122
|
+
"",
|
|
123
|
+
"Be concise and chair-like, but include your own view. Challenge when needed; do not merely restate participants' long answers.",
|
|
124
|
+
].join("\n"),
|
|
125
|
+
labels: {
|
|
126
|
+
noPreviousRounds: "(None yet; this is the first round)",
|
|
127
|
+
noRegularReplies: "(No regular participant replies in this round)",
|
|
128
|
+
noNewReply: "no new reply",
|
|
129
|
+
error: "error",
|
|
130
|
+
discussionRoundNotice: (round, max, parts) => `💬 Round ${round}/${max}: ${parts}`,
|
|
131
|
+
round: "round",
|
|
132
|
+
discussEndedChairman: (name) => `💬 Discuss ended: Chairman ${name} completed the final summary.`,
|
|
133
|
+
discussEndedNoNew: (round) => `💬 Discuss ended: round ${round} had no new useful additions.`,
|
|
134
|
+
discussMaxRounds: (max) => `💬 Discuss completed: reached ${max} rounds.`,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
export function getI18n(locale) {
|
|
139
|
+
return dict[normalizeLocale(locale)];
|
|
140
|
+
}
|
package/dist/message-store.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface ChatInfo {
|
|
|
14
14
|
verbose: boolean;
|
|
15
15
|
discuss: boolean;
|
|
16
16
|
discussMaxRounds: number;
|
|
17
|
+
/** Group-level chairman bot name. Empty means no chairman. */
|
|
18
|
+
chairmanBot?: string;
|
|
19
|
+
locale?: string;
|
|
17
20
|
updatedAt: number;
|
|
18
21
|
}
|
|
19
22
|
export interface ChatMessage {
|
|
@@ -101,6 +104,11 @@ export declare class MessageStore {
|
|
|
101
104
|
setVerbose(chatId: string, verbose: boolean): void;
|
|
102
105
|
setDiscussMode(chatId: string, on: boolean): void;
|
|
103
106
|
setDiscussMaxRounds(chatId: string, rounds: number): void;
|
|
107
|
+
setChatLocale(chatId: string, locale: string): void;
|
|
108
|
+
getChatLocale(chatId: string): string;
|
|
109
|
+
setChairmanBot(chatId: string, botName: string): void;
|
|
110
|
+
clearChairmanBot(chatId: string): void;
|
|
111
|
+
getChairmanBot(chatId: string): string;
|
|
104
112
|
setBotVerbose(botName: string, chatId: string, verbose: boolean): void;
|
|
105
113
|
getBotVerbose(botName: string, chatId: string): boolean;
|
|
106
114
|
setBotMode(botName: string, chatId: string, mode: BotChatMode): void;
|
package/dist/message-store.js
CHANGED
|
@@ -33,7 +33,9 @@ export class MessageStore {
|
|
|
33
33
|
member_names TEXT NOT NULL DEFAULT '',
|
|
34
34
|
verbose INTEGER NOT NULL DEFAULT 0,
|
|
35
35
|
discuss INTEGER NOT NULL DEFAULT 0,
|
|
36
|
-
discuss_max_rounds INTEGER NOT NULL DEFAULT
|
|
36
|
+
discuss_max_rounds INTEGER NOT NULL DEFAULT 10,
|
|
37
|
+
chairman_bot TEXT NOT NULL DEFAULT '',
|
|
38
|
+
locale TEXT NOT NULL DEFAULT '',
|
|
37
39
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
38
40
|
);
|
|
39
41
|
|
|
@@ -181,6 +183,20 @@ export class MessageStore {
|
|
|
181
183
|
catch {
|
|
182
184
|
// Column already exists
|
|
183
185
|
}
|
|
186
|
+
// Migration: add locale column if missing
|
|
187
|
+
try {
|
|
188
|
+
this.db.exec(`ALTER TABLE chat_info ADD COLUMN locale TEXT NOT NULL DEFAULT ''`);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Column already exists
|
|
192
|
+
}
|
|
193
|
+
// Migration: add chairman column if missing
|
|
194
|
+
try {
|
|
195
|
+
this.db.exec(`ALTER TABLE chat_info ADD COLUMN chairman_bot TEXT NOT NULL DEFAULT ''`);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Column already exists
|
|
199
|
+
}
|
|
184
200
|
// Migration: add discuss columns if missing
|
|
185
201
|
try {
|
|
186
202
|
this.db.exec(`ALTER TABLE chat_info ADD COLUMN discuss INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -189,11 +205,14 @@ export class MessageStore {
|
|
|
189
205
|
// Column already exists
|
|
190
206
|
}
|
|
191
207
|
try {
|
|
192
|
-
this.db.exec(`ALTER TABLE chat_info ADD COLUMN discuss_max_rounds INTEGER NOT NULL DEFAULT
|
|
208
|
+
this.db.exec(`ALTER TABLE chat_info ADD COLUMN discuss_max_rounds INTEGER NOT NULL DEFAULT 10`);
|
|
193
209
|
}
|
|
194
210
|
catch {
|
|
195
211
|
// Column already exists
|
|
196
212
|
}
|
|
213
|
+
// Migration: promote the old discuss default (3 rounds) to the new default
|
|
214
|
+
// (10 rounds). Explicit non-default values such as 1/2/5/8 are preserved.
|
|
215
|
+
this.db.prepare(`UPDATE chat_info SET discuss_max_rounds = 10 WHERE discuss_max_rounds = 3`).run();
|
|
197
216
|
// Migration: add owner_bot column if missing
|
|
198
217
|
try {
|
|
199
218
|
this.db.exec(`ALTER TABLE chat_info ADD COLUMN owner_bot TEXT NOT NULL DEFAULT ''`);
|
|
@@ -540,8 +559,8 @@ export class MessageStore {
|
|
|
540
559
|
// --- Chat info ---
|
|
541
560
|
upsertChatInfo(info) {
|
|
542
561
|
this.db.prepare(`
|
|
543
|
-
INSERT INTO chat_info (chat_id, chat_type, chat_name, members, member_names, verbose, free_discussion, owner_bot, discuss, discuss_max_rounds, updated_at)
|
|
544
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
562
|
+
INSERT INTO chat_info (chat_id, chat_type, chat_name, members, member_names, verbose, free_discussion, owner_bot, discuss, discuss_max_rounds, chairman_bot, locale, updated_at)
|
|
563
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
545
564
|
ON CONFLICT (chat_id) DO UPDATE SET
|
|
546
565
|
chat_type = excluded.chat_type,
|
|
547
566
|
chat_name = excluded.chat_name,
|
|
@@ -551,9 +570,11 @@ export class MessageStore {
|
|
|
551
570
|
free_discussion = excluded.free_discussion,
|
|
552
571
|
owner_bot = CASE WHEN excluded.owner_bot != '' THEN excluded.owner_bot ELSE chat_info.owner_bot END,
|
|
553
572
|
discuss = CASE WHEN excluded.discuss != 0 THEN excluded.discuss ELSE chat_info.discuss END,
|
|
554
|
-
discuss_max_rounds = CASE WHEN excluded.discuss_max_rounds !=
|
|
573
|
+
discuss_max_rounds = CASE WHEN excluded.discuss_max_rounds != 10 THEN excluded.discuss_max_rounds ELSE chat_info.discuss_max_rounds END,
|
|
574
|
+
chairman_bot = CASE WHEN excluded.chairman_bot != '' THEN excluded.chairman_bot ELSE chat_info.chairman_bot END,
|
|
575
|
+
locale = CASE WHEN excluded.locale != '' THEN excluded.locale ELSE chat_info.locale END,
|
|
555
576
|
updated_at = excluded.updated_at
|
|
556
|
-
`).run(info.chatId, info.chatType, info.chatName, info.members, info.memberNames, info.verbose ? 1 : 0, info.freeDiscussion ? 1 : 0, info.ownerBot || '', info.discuss ? 1 : 0, info.discussMaxRounds ||
|
|
577
|
+
`).run(info.chatId, info.chatType, info.chatName, info.members, info.memberNames, info.verbose ? 1 : 0, info.freeDiscussion ? 1 : 0, info.ownerBot || '', info.discuss ? 1 : 0, info.discussMaxRounds || 10, info.chairmanBot || '', info.locale || '', info.updatedAt);
|
|
557
578
|
}
|
|
558
579
|
setFreeDiscussion(chatId, on) {
|
|
559
580
|
this.db.prepare(`UPDATE chat_info SET free_discussion = ? WHERE chat_id = ?`).run(on ? 1 : 0, chatId);
|
|
@@ -566,7 +587,7 @@ export class MessageStore {
|
|
|
566
587
|
setDiscussMode(chatId, on) {
|
|
567
588
|
this.db.prepare(`
|
|
568
589
|
INSERT INTO chat_info (chat_id, chat_type, chat_name, discuss, discuss_max_rounds, updated_at)
|
|
569
|
-
VALUES (?, 'group', '', ?,
|
|
590
|
+
VALUES (?, 'group', '', ?, 10, ?)
|
|
570
591
|
ON CONFLICT (chat_id) DO UPDATE SET discuss = excluded.discuss, updated_at = excluded.updated_at
|
|
571
592
|
`).run(chatId, on ? 1 : 0, Date.now());
|
|
572
593
|
}
|
|
@@ -578,6 +599,31 @@ export class MessageStore {
|
|
|
578
599
|
ON CONFLICT (chat_id) DO UPDATE SET discuss_max_rounds = excluded.discuss_max_rounds, updated_at = excluded.updated_at
|
|
579
600
|
`).run(chatId, normalized, Date.now());
|
|
580
601
|
}
|
|
602
|
+
setChatLocale(chatId, locale) {
|
|
603
|
+
this.db.prepare(`
|
|
604
|
+
INSERT INTO chat_info (chat_id, chat_type, chat_name, locale, updated_at)
|
|
605
|
+
VALUES (?, 'group', '', ?, ?)
|
|
606
|
+
ON CONFLICT (chat_id) DO UPDATE SET locale = excluded.locale, updated_at = excluded.updated_at
|
|
607
|
+
`).run(chatId, locale, Date.now());
|
|
608
|
+
}
|
|
609
|
+
getChatLocale(chatId) {
|
|
610
|
+
const row = this.db.prepare(`SELECT locale FROM chat_info WHERE chat_id = ?`).get(chatId);
|
|
611
|
+
return row?.locale || '';
|
|
612
|
+
}
|
|
613
|
+
setChairmanBot(chatId, botName) {
|
|
614
|
+
this.db.prepare(`
|
|
615
|
+
INSERT INTO chat_info (chat_id, chat_type, chat_name, chairman_bot, updated_at)
|
|
616
|
+
VALUES (?, 'group', '', ?, ?)
|
|
617
|
+
ON CONFLICT (chat_id) DO UPDATE SET chairman_bot = excluded.chairman_bot, updated_at = excluded.updated_at
|
|
618
|
+
`).run(chatId, botName, Date.now());
|
|
619
|
+
}
|
|
620
|
+
clearChairmanBot(chatId) {
|
|
621
|
+
this.setChairmanBot(chatId, '');
|
|
622
|
+
}
|
|
623
|
+
getChairmanBot(chatId) {
|
|
624
|
+
const row = this.db.prepare(`SELECT chairman_bot FROM chat_info WHERE chat_id = ?`).get(chatId);
|
|
625
|
+
return row?.chairman_bot || '';
|
|
626
|
+
}
|
|
581
627
|
setBotVerbose(botName, chatId, verbose) {
|
|
582
628
|
this.db.prepare(`
|
|
583
629
|
INSERT INTO bot_chat_settings (bot_name, chat_id, verbose, updated_at)
|
|
@@ -634,7 +680,9 @@ export class MessageStore {
|
|
|
634
680
|
freeDiscussion: !!row.free_discussion,
|
|
635
681
|
verbose: !!row.verbose,
|
|
636
682
|
discuss: !!row.discuss,
|
|
637
|
-
discussMaxRounds: row.discuss_max_rounds ||
|
|
683
|
+
discussMaxRounds: row.discuss_max_rounds || 10,
|
|
684
|
+
chairmanBot: row.chairman_bot || '',
|
|
685
|
+
locale: row.locale || '',
|
|
638
686
|
updatedAt: row.updated_at,
|
|
639
687
|
};
|
|
640
688
|
}
|
|
@@ -650,7 +698,9 @@ export class MessageStore {
|
|
|
650
698
|
freeDiscussion: !!r.free_discussion,
|
|
651
699
|
verbose: !!r.verbose,
|
|
652
700
|
discuss: !!r.discuss,
|
|
653
|
-
discussMaxRounds: r.discuss_max_rounds ||
|
|
701
|
+
discussMaxRounds: r.discuss_max_rounds || 10,
|
|
702
|
+
chairmanBot: r.chairman_bot || '',
|
|
703
|
+
locale: r.locale || '',
|
|
654
704
|
updatedAt: r.updated_at,
|
|
655
705
|
}));
|
|
656
706
|
}
|