openclaw-lark-multi-agent 1.5.0 → 1.5.1
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/feishu-bot.d.ts +9 -0
- package/dist/feishu-bot.js +160 -9
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -290,6 +290,15 @@ export declare class FeishuBot {
|
|
|
290
290
|
*/
|
|
291
291
|
private sendErrorNotice;
|
|
292
292
|
private sendFinalMessage;
|
|
293
|
+
/** Feishu code 230025: message content over the per-message limit. */
|
|
294
|
+
private isContentTooLongError;
|
|
295
|
+
/** Split text into Feishu-safe chunks (default well under the 4000-char text limit).
|
|
296
|
+
* Prefers paragraph, then line boundaries; hard-cuts only when a single line
|
|
297
|
+
* is longer than the limit. Joining all chunks reproduces the original text. */
|
|
298
|
+
private splitIntoChunks;
|
|
299
|
+
/** Deliver oversized content as multiple plain-text messages (used on 230025).
|
|
300
|
+
* Keeps the model footer on the final chunk. Returns the first message id. */
|
|
301
|
+
private sendChunkedText;
|
|
293
302
|
private sendMessage;
|
|
294
303
|
/**
|
|
295
304
|
* Enqueue a message send to guarantee ordering per chat.
|
package/dist/feishu-bot.js
CHANGED
|
@@ -37,6 +37,40 @@ const AUTO_RETRY_DONE_PHRASE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RET
|
|
|
37
37
|
// it is a NEW user message that arrived while it was still working, and should
|
|
38
38
|
// be handled together with the current task. The live-status card and reactions
|
|
39
39
|
// still key off the ORIGINAL text; only what OpenClaw receives is wrapped.
|
|
40
|
+
/** Damerau-Levenshtein distance (insert/delete/substitute/transpose), bounded. */
|
|
41
|
+
function damerauLevenshtein(a, b) {
|
|
42
|
+
const la = a.length, lb = b.length;
|
|
43
|
+
if (Math.abs(la - lb) > 1)
|
|
44
|
+
return 2; // fast reject for threshold-1 use
|
|
45
|
+
const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0));
|
|
46
|
+
for (let i = 0; i <= la; i++)
|
|
47
|
+
d[i][0] = i;
|
|
48
|
+
for (let j = 0; j <= lb; j++)
|
|
49
|
+
d[0][j] = j;
|
|
50
|
+
for (let i = 1; i <= la; i++) {
|
|
51
|
+
for (let j = 1; j <= lb; j++) {
|
|
52
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
53
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
54
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
55
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return d[la][lb];
|
|
60
|
+
}
|
|
61
|
+
/** Find a bridge command within one edit of `name` (typo hint); undefined if none. */
|
|
62
|
+
function findNearMissCommand(name, commands) {
|
|
63
|
+
if (!name.startsWith("/") || name.length < 3)
|
|
64
|
+
return undefined;
|
|
65
|
+
let best;
|
|
66
|
+
for (const cmd of commands) {
|
|
67
|
+
if (damerauLevenshtein(name, cmd) <= 1) {
|
|
68
|
+
if (best === undefined || cmd.length < best.length)
|
|
69
|
+
best = cmd;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
40
74
|
function steerInjectionPrefix(en) {
|
|
41
75
|
const zh = process.env.OPENCLAW_LARK_MULTI_AGENT_STEER_PREFIX
|
|
42
76
|
|| "【用户中途插入的新消息,请与当前任务一起处理】";
|
|
@@ -570,6 +604,28 @@ export class FeishuBot {
|
|
|
570
604
|
const commandName = preliminaryCommandName;
|
|
571
605
|
const bridgeCommands = preliminaryBridgeCommands;
|
|
572
606
|
const isCommand = isBridgeCommand && bridgeCommands.has(commandName);
|
|
607
|
+
// Typo hint: a near-miss single-slash command (e.g. "/chiarman") must not
|
|
608
|
+
// silently fall through into the chairman/model chat path — that is how
|
|
609
|
+
// "I set the chairman but someone else keeps answering" confusion starts.
|
|
610
|
+
if (!isCommand && isBridgeCommand) {
|
|
611
|
+
const nearMiss = findNearMissCommand(commandName, bridgeCommands);
|
|
612
|
+
if (nearMiss) {
|
|
613
|
+
const groupBotCount = Array.from(FeishuBot.allBots.values()).filter((bot) => bot.store === this.store).length;
|
|
614
|
+
const shouldHint = chatType === "p2p"
|
|
615
|
+
|| (routing.hasTargetedMention ? routing.isCurrentBotMentioned : (groupBotCount === 1 || this.isDiscussionCoordinator()));
|
|
616
|
+
if (shouldHint) {
|
|
617
|
+
const en = this.isEn(chatId);
|
|
618
|
+
await this.replyMessage(messageId, en
|
|
619
|
+
? `❓ Unknown command \`${commandName}\`. Did you mean \`${nearMiss}\`?\nRe-send the correct command to execute it. Commands starting with // are passed to OpenClaw.`
|
|
620
|
+
: `❓ 未知命令 \`${commandName}\`,你是想输入 \`${nearMiss}\` 吗?\n重新发送正确命令即可执行;双斜杠 // 开头的命令会转交给 OpenClaw。`);
|
|
621
|
+
if (insertedId > 0) {
|
|
622
|
+
this.store.markSynced(this.config.name, chatId, insertedId);
|
|
623
|
+
this.store.clearPendingTrigger(this.config.name, chatId, insertedId);
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
573
629
|
if (isCommand) {
|
|
574
630
|
// In group chats, most bridge commands must be explicitly routed to this
|
|
575
631
|
// bot or @all. /discuss is a group-level command, so an unmentioned
|
|
@@ -3149,6 +3205,16 @@ export class FeishuBot {
|
|
|
3149
3205
|
const model = this.replyModelFooters.get(messageId);
|
|
3150
3206
|
const status = this.replyStatusFooters.get(messageId);
|
|
3151
3207
|
const card = this.buildMarkdownCard(text, model, status);
|
|
3208
|
+
// Best-effort chat resolution for chunked/error paths: the reply target is
|
|
3209
|
+
// normally a stored inbound message, so the store knows its chat.
|
|
3210
|
+
const resolveChatId = () => {
|
|
3211
|
+
try {
|
|
3212
|
+
return this.store.getMessageByMessageId(messageId)?.chatId || undefined;
|
|
3213
|
+
}
|
|
3214
|
+
catch {
|
|
3215
|
+
return undefined;
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3152
3218
|
try {
|
|
3153
3219
|
const res = await this.client.im.message.reply({
|
|
3154
3220
|
path: { message_id: messageId },
|
|
@@ -3159,17 +3225,35 @@ export class FeishuBot {
|
|
|
3159
3225
|
});
|
|
3160
3226
|
return res?.data?.message_id || res?.message_id;
|
|
3161
3227
|
}
|
|
3162
|
-
catch {
|
|
3228
|
+
catch (err) {
|
|
3229
|
+
const chatId = resolveChatId();
|
|
3230
|
+
if (this.isOutOfChatError(err) && chatId)
|
|
3231
|
+
this.markCurrentBotUnavailable(chatId, err);
|
|
3232
|
+
// Content over Feishu's per-message limit: chunk and deliver instead of
|
|
3233
|
+
// retrying the same oversized payload (retries would fail forever).
|
|
3234
|
+
if (this.isContentTooLongError(err) && chatId) {
|
|
3235
|
+
return await this.sendChunkedText(chatId, text, model, status, messageId);
|
|
3236
|
+
}
|
|
3163
3237
|
// Fallback to plain text if card fails; retain model attribution.
|
|
3164
3238
|
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : text;
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3239
|
+
try {
|
|
3240
|
+
const res = await this.client.im.message.reply({
|
|
3241
|
+
path: { message_id: messageId },
|
|
3242
|
+
data: {
|
|
3243
|
+
content: JSON.stringify({ text: fallbackText }),
|
|
3244
|
+
msg_type: "text",
|
|
3245
|
+
},
|
|
3246
|
+
});
|
|
3247
|
+
return res?.data?.message_id || res?.message_id;
|
|
3248
|
+
}
|
|
3249
|
+
catch (fallbackErr) {
|
|
3250
|
+
if (this.isOutOfChatError(fallbackErr) && chatId)
|
|
3251
|
+
this.markCurrentBotUnavailable(chatId, fallbackErr);
|
|
3252
|
+
if (this.isContentTooLongError(fallbackErr) && chatId) {
|
|
3253
|
+
return await this.sendChunkedText(chatId, text, model, status, messageId);
|
|
3254
|
+
}
|
|
3255
|
+
throw fallbackErr;
|
|
3256
|
+
}
|
|
3173
3257
|
}
|
|
3174
3258
|
}
|
|
3175
3259
|
extractBridgeAttachments(reply) {
|
|
@@ -3517,6 +3601,59 @@ export class FeishuBot {
|
|
|
3517
3601
|
this.sendStatusFooters.delete(chatId);
|
|
3518
3602
|
}
|
|
3519
3603
|
}
|
|
3604
|
+
/** Feishu code 230025: message content over the per-message limit. */
|
|
3605
|
+
isContentTooLongError(err) {
|
|
3606
|
+
const e = err;
|
|
3607
|
+
const code = e?.response?.data?.code ?? e?.data?.code ?? e?.code;
|
|
3608
|
+
const msg = String(e?.response?.data?.msg ?? e?.data?.msg ?? e?.message ?? e?.msg ?? "");
|
|
3609
|
+
return code === 230025 || msg.includes("230025") || /length of the message content/i.test(msg);
|
|
3610
|
+
}
|
|
3611
|
+
/** Split text into Feishu-safe chunks (default well under the 4000-char text limit).
|
|
3612
|
+
* Prefers paragraph, then line boundaries; hard-cuts only when a single line
|
|
3613
|
+
* is longer than the limit. Joining all chunks reproduces the original text. */
|
|
3614
|
+
splitIntoChunks(text, limit = 3200) {
|
|
3615
|
+
if (text.length <= limit)
|
|
3616
|
+
return [text];
|
|
3617
|
+
const chunks = [];
|
|
3618
|
+
let rest = text;
|
|
3619
|
+
while (rest.length > limit) {
|
|
3620
|
+
let cut = rest.lastIndexOf("\n\n", limit);
|
|
3621
|
+
if (cut < limit * 0.5)
|
|
3622
|
+
cut = rest.lastIndexOf("\n", limit);
|
|
3623
|
+
if (cut < limit * 0.5)
|
|
3624
|
+
cut = rest.lastIndexOf(" ", limit);
|
|
3625
|
+
if (cut < limit * 0.5)
|
|
3626
|
+
cut = limit;
|
|
3627
|
+
chunks.push(rest.slice(0, cut));
|
|
3628
|
+
rest = rest.slice(cut).replace(/^\n+/, "");
|
|
3629
|
+
}
|
|
3630
|
+
if (rest.length > 0)
|
|
3631
|
+
chunks.push(rest);
|
|
3632
|
+
return chunks;
|
|
3633
|
+
}
|
|
3634
|
+
/** Deliver oversized content as multiple plain-text messages (used on 230025).
|
|
3635
|
+
* Keeps the model footer on the final chunk. Returns the first message id. */
|
|
3636
|
+
async sendChunkedText(chatId, text, model, status, replyTo) {
|
|
3637
|
+
const suffix = model?.trim() ? `\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : "";
|
|
3638
|
+
const chunks = this.splitIntoChunks(text);
|
|
3639
|
+
if (suffix && chunks.length > 0)
|
|
3640
|
+
chunks[chunks.length - 1] += suffix;
|
|
3641
|
+
let firstId;
|
|
3642
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
3643
|
+
const payload = {
|
|
3644
|
+
content: JSON.stringify({ text: chunks[i] }),
|
|
3645
|
+
msg_type: "text",
|
|
3646
|
+
};
|
|
3647
|
+
const res = replyTo
|
|
3648
|
+
? await this.client.im.message.reply({ path: { message_id: replyTo }, data: payload })
|
|
3649
|
+
: await this.client.im.message.create({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, ...payload } });
|
|
3650
|
+
const id = res?.data?.message_id || res?.message_id;
|
|
3651
|
+
if (i === 0)
|
|
3652
|
+
firstId = id;
|
|
3653
|
+
}
|
|
3654
|
+
this.store.clearBotUnavailableInChat(this.config.name, chatId);
|
|
3655
|
+
return firstId;
|
|
3656
|
+
}
|
|
3520
3657
|
async sendMessage(chatId, text) {
|
|
3521
3658
|
const model = this.sendModelFooters.get(chatId);
|
|
3522
3659
|
const status = this.sendStatusFooters.get(chatId);
|
|
@@ -3537,6 +3674,17 @@ export class FeishuBot {
|
|
|
3537
3674
|
console.warn(`[${this.config.name}] sendMessage interactive failed:`, JSON.stringify(err?.response?.data || err?.data || { message: err.message }));
|
|
3538
3675
|
if (this.isOutOfChatError(err))
|
|
3539
3676
|
this.markCurrentBotUnavailable(chatId, err);
|
|
3677
|
+
// Content over Feishu's per-message limit: chunk and deliver instead of
|
|
3678
|
+
// retrying the same oversized payload (retries would fail forever).
|
|
3679
|
+
if (this.isContentTooLongError(err)) {
|
|
3680
|
+
try {
|
|
3681
|
+
return await this.sendChunkedText(chatId, text, model, status);
|
|
3682
|
+
}
|
|
3683
|
+
catch (chunkErr) {
|
|
3684
|
+
console.warn(`[${this.config.name}] sendMessage chunked delivery failed:`, chunkErr.message);
|
|
3685
|
+
throw chunkErr;
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3540
3688
|
// Fallback to plain text; retain model attribution.
|
|
3541
3689
|
try {
|
|
3542
3690
|
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}${status ? ` · ${status}` : ""}` : text;
|
|
@@ -3555,6 +3703,9 @@ export class FeishuBot {
|
|
|
3555
3703
|
console.warn(`[${this.config.name}] sendMessage text failed:`, JSON.stringify(fallbackErr?.response?.data || fallbackErr?.data || { message: fallbackErr.message }));
|
|
3556
3704
|
if (this.isOutOfChatError(fallbackErr))
|
|
3557
3705
|
this.markCurrentBotUnavailable(chatId, fallbackErr);
|
|
3706
|
+
if (this.isContentTooLongError(fallbackErr)) {
|
|
3707
|
+
return await this.sendChunkedText(chatId, text, model, status);
|
|
3708
|
+
}
|
|
3558
3709
|
throw fallbackErr;
|
|
3559
3710
|
}
|
|
3560
3711
|
}
|