openclaw-lark-multi-agent 1.4.3 → 1.4.4
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 +5 -4
- package/README.zh-CN.md +6 -0
- package/dist/feishu-bot.d.ts +9 -5
- package/dist/feishu-bot.js +108 -188
- package/dist/message-store.d.ts +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ All of them connect to the same OpenClaw Gateway while keeping sessions, queues,
|
|
|
37
37
|
- Feishu image download and OpenClaw multimodal attachment forwarding
|
|
38
38
|
- Bridge attachment marker protocol for generated files/images/documents
|
|
39
39
|
- Feishu CardKit v2 Markdown rendering, including native table elements for pipe tables
|
|
40
|
-
- Live status card: in non-verbose mode each run shows a
|
|
40
|
+
- Live status card: in non-verbose mode each run shows a self-updating Feishu interactive card (title + recent activity window with per-line timestamps + elapsed footer, refreshed once per second), which collapses to tool-call count + elapsed on a clean finish; the final answer is sent separately at the bottom with a model footer
|
|
41
41
|
- Bridge-level slash commands and escaped OpenClaw slash commands
|
|
42
42
|
- `/discuss` mode for barrier-style multi-bot group discussion, including per-round markers and no-reply status notices
|
|
43
43
|
- `/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`
|
|
@@ -463,7 +463,7 @@ While running, the card shows:
|
|
|
463
463
|
- a rolling window of the most recent activity lines (tool start `▸`, tool end
|
|
464
464
|
`✓`, and intermediate assistant text `•`), each prefixed with the relative
|
|
465
465
|
time `mm:ss` since the run started;
|
|
466
|
-
- a footer with elapsed time
|
|
466
|
+
- a footer with elapsed time.
|
|
467
467
|
|
|
468
468
|
The card is created lazily (a fast reply that finishes within the create delay
|
|
469
469
|
never spawns a card, to avoid flicker) and is updated with `im.message.patch`,
|
|
@@ -479,8 +479,9 @@ When the run ends:
|
|
|
479
479
|
header, so the steps leading up to the failure stay visible for debugging.
|
|
480
480
|
|
|
481
481
|
The final answer is always delivered separately through the normal interactive-card
|
|
482
|
-
delivery path, so
|
|
483
|
-
|
|
482
|
+
delivery path, so it is created at completion time and remains below any user messages
|
|
483
|
+
inserted during the run. Its footer shows only the bound model name. The live-status
|
|
484
|
+
card remains a distinct earlier message and owns tool-call count + elapsed time.
|
|
484
485
|
|
|
485
486
|
Live status is on by default and can be toggled per bot/chat with
|
|
486
487
|
`/livestatus [on|off]`. It can be disabled globally by setting
|
package/README.zh-CN.md
CHANGED
|
@@ -408,6 +408,12 @@ v1 行为边界:如果消息已经进入 OpenClaw 正在处理,暂不 abort
|
|
|
408
408
|
|
|
409
409
|
如果 Agent 需要发送生成的文件、图片或文档,应使用 bridge attachment marker 协议,而不是直接调用飞书消息工具。桥接层会从可见回复中剥离 marker,校验文件路径位于配置的附件目录下,然后上传/发送附件,并写入本地上下文。Markdown 文档也可以通过这个路径转换成飞书云文档。
|
|
410
410
|
|
|
411
|
+
## Live Status 与最终回复
|
|
412
|
+
|
|
413
|
+
非 verbose 模式下,执行过程显示一张可更新的 live status 卡片。运行中显示最近活动和耗时;完成后收缩为“累计工具调用次数 + 总耗时”。
|
|
414
|
+
|
|
415
|
+
最终回复不会覆盖旧的 live status,而是在任务完成时新发一张卡片,因此用户执行途中插入的新消息不会排在最终答案之后。最终回复卡片底部只显示本次使用的模型名;工具调用次数和耗时只保留在 live status 中。
|
|
416
|
+
|
|
411
417
|
## 数据模型
|
|
412
418
|
|
|
413
419
|
SQLite 状态位于配置的数据目录。主要表:
|
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export declare class FeishuBot {
|
|
|
35
35
|
private sendQueue;
|
|
36
36
|
/** Per-chat delayed runtime failure notifications, canceled if a real reply arrives. */
|
|
37
37
|
private delayedFailureTimers;
|
|
38
|
+
/** Ephemeral model footer overrides used by final-message wrappers. Keeping
|
|
39
|
+
* replyMessage/sendMessage two-argument APIs avoids contaminating ordinary
|
|
40
|
+
* command/error/card sends with model attribution. */
|
|
41
|
+
private replyModelFooters;
|
|
42
|
+
private sendModelFooters;
|
|
38
43
|
/** Per-chat durable outbox retry timer. */
|
|
39
44
|
private deliveryRetryTimers;
|
|
40
45
|
/** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
|
|
@@ -122,11 +127,8 @@ export declare class FeishuBot {
|
|
|
122
127
|
private buildMarkdownCard;
|
|
123
128
|
private deliveryOwnsVisiblePayload;
|
|
124
129
|
private completionSummary;
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
private patchFinalAnswerCard;
|
|
128
|
-
/** Best-effort cleanup when a final-card patch was too large/failed and the
|
|
129
|
-
* answer had to be delivered as a separate message. */
|
|
130
|
+
/** Best-effort terminal cleanup for status-only rows and legacy v1.4.3
|
|
131
|
+
* outbox entries that once targeted the live-status card. */
|
|
130
132
|
private patchLiveStatusDoneSummary;
|
|
131
133
|
private formatUserVisibleError;
|
|
132
134
|
private isBridgeControlReply;
|
|
@@ -227,6 +229,7 @@ export declare class FeishuBot {
|
|
|
227
229
|
private replyTextMessage;
|
|
228
230
|
private editTextMessage;
|
|
229
231
|
private deleteMessageById;
|
|
232
|
+
private replyFinalMessage;
|
|
230
233
|
private replyMessage;
|
|
231
234
|
private extractBridgeAttachments;
|
|
232
235
|
/**
|
|
@@ -271,6 +274,7 @@ export declare class FeishuBot {
|
|
|
271
274
|
/**
|
|
272
275
|
* Send a proactive message to a chat (not a reply).
|
|
273
276
|
*/
|
|
277
|
+
private sendFinalMessage;
|
|
274
278
|
private sendMessage;
|
|
275
279
|
/**
|
|
276
280
|
* Enqueue a message send to guarantee ordering per chat.
|
package/dist/feishu-bot.js
CHANGED
|
@@ -90,6 +90,11 @@ export class FeishuBot {
|
|
|
90
90
|
sendQueue = new Map();
|
|
91
91
|
/** Per-chat delayed runtime failure notifications, canceled if a real reply arrives. */
|
|
92
92
|
delayedFailureTimers = new Map();
|
|
93
|
+
/** Ephemeral model footer overrides used by final-message wrappers. Keeping
|
|
94
|
+
* replyMessage/sendMessage two-argument APIs avoids contaminating ordinary
|
|
95
|
+
* command/error/card sends with model attribution. */
|
|
96
|
+
replyModelFooters = new Map();
|
|
97
|
+
sendModelFooters = new Map();
|
|
93
98
|
/** Per-chat durable outbox retry timer. */
|
|
94
99
|
deliveryRetryTimers = new Map();
|
|
95
100
|
/** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
|
|
@@ -233,18 +238,14 @@ export class FeishuBot {
|
|
|
233
238
|
if (sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0))
|
|
234
239
|
this.cancelDelayedFailure(chatId);
|
|
235
240
|
const activeTarget = this.activeDeliveryTargets.get(chatId);
|
|
236
|
-
let finalStatusMeta;
|
|
237
241
|
try {
|
|
242
|
+
await this.enqueueAndDispatchDelivery(chatId, sourceType, this.deliverySourceId(sourceType === "verbose_transcript" ? "verbose" : "proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments, activeTarget?.messageId, activeTarget ? `trigger:${activeTarget.triggerId}` : undefined, sourceType === "verbose_transcript" ? undefined : this.config.model);
|
|
238
243
|
if (sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0)) {
|
|
239
|
-
finalStatusMeta = await activeTarget?.liveStatus?.prepareTerminalDelivery(false);
|
|
240
|
-
}
|
|
241
|
-
await this.enqueueAndDispatchDelivery(chatId, sourceType, this.deliverySourceId(sourceType === "verbose_transcript" ? "verbose" : "proactive", `${Date.now()}:${Math.random()}:${parsed.text.trim()}|${JSON.stringify(parsed.attachments)}`), parsed.text.trim(), parsed.attachments, activeTarget?.messageId, activeTarget ? `trigger:${activeTarget.triggerId}` : undefined, finalStatusMeta);
|
|
242
|
-
if (!finalStatusMeta && sourceType !== "verbose_transcript" && (parsed.text.trim() || parsed.attachments.length > 0)) {
|
|
243
244
|
await activeTarget?.liveStatus?.complete().catch(() => { });
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
catch (err) {
|
|
247
|
-
if (
|
|
248
|
+
if (sourceType !== "verbose_transcript")
|
|
248
249
|
await activeTarget?.liveStatus?.fail().catch(() => { });
|
|
249
250
|
throw err;
|
|
250
251
|
}
|
|
@@ -1435,21 +1436,12 @@ export class FeishuBot {
|
|
|
1435
1436
|
await liveStatus?.complete().catch(() => { });
|
|
1436
1437
|
}
|
|
1437
1438
|
else {
|
|
1438
|
-
let finalStatusMeta;
|
|
1439
1439
|
try {
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
// freeze the card so the outbox can durably collapse it to a done
|
|
1446
|
-
// summary (including disabled-but-existing cards).
|
|
1447
|
-
if ((shouldReply && visibleReply.trim()) || hasAttachments) {
|
|
1448
|
-
finalStatusMeta = await liveStatus?.prepareTerminalDelivery(false);
|
|
1449
|
-
}
|
|
1450
|
-
await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${triggerId}|${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`, finalStatusMeta);
|
|
1451
|
-
if (!finalStatusMeta)
|
|
1452
|
-
await liveStatus?.complete();
|
|
1440
|
+
// Keep live status and final answer as separate messages. The final
|
|
1441
|
+
// answer is created at completion time so it remains at the bottom
|
|
1442
|
+
// even when the user inserted more messages during the run.
|
|
1443
|
+
await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${triggerId}|${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`, this.config.model);
|
|
1444
|
+
await liveStatus?.complete();
|
|
1453
1445
|
// Mark every merged trigger as delivered so retries/restarts do
|
|
1454
1446
|
// not re-process any of them.
|
|
1455
1447
|
for (const id of mergedTriggerIds)
|
|
@@ -1461,11 +1453,7 @@ export class FeishuBot {
|
|
|
1461
1453
|
// enqueueAndDispatchDelivery already sent a user-visible delivery
|
|
1462
1454
|
// failure. Do not fall through to the generic provider-error path;
|
|
1463
1455
|
// that creates a second misleading "bot did not complete" message.
|
|
1464
|
-
|
|
1465
|
-
// normal interrupted status. A prepared card has already been
|
|
1466
|
-
// patched/fallen back by the durable dispatcher.
|
|
1467
|
-
if (!finalStatusMeta)
|
|
1468
|
-
await liveStatus?.fail().catch(() => { });
|
|
1456
|
+
await liveStatus?.fail().catch(() => { });
|
|
1469
1457
|
console.warn(`[${this.config.name}] assistant delivery failed after notification:`, this.errorSummary(err));
|
|
1470
1458
|
}
|
|
1471
1459
|
}
|
|
@@ -1714,13 +1702,19 @@ export class FeishuBot {
|
|
|
1714
1702
|
}
|
|
1715
1703
|
return s;
|
|
1716
1704
|
}
|
|
1717
|
-
buildMarkdownCard(text) {
|
|
1705
|
+
buildMarkdownCard(text, model) {
|
|
1706
|
+
const elements = buildFeishuCardElements(text);
|
|
1707
|
+
if (model?.trim()) {
|
|
1708
|
+
elements.push({ tag: "hr" });
|
|
1709
|
+
elements.push({
|
|
1710
|
+
tag: "markdown",
|
|
1711
|
+
content: `<font color='grey'>🧠 ${this.escapeCardText(model.trim())}</font>`,
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1718
1714
|
return {
|
|
1719
1715
|
schema: "2.0",
|
|
1720
1716
|
config: { wide_screen_mode: true },
|
|
1721
|
-
body: {
|
|
1722
|
-
elements: buildFeishuCardElements(text),
|
|
1723
|
-
},
|
|
1717
|
+
body: { elements },
|
|
1724
1718
|
};
|
|
1725
1719
|
}
|
|
1726
1720
|
deliveryOwnsVisiblePayload(item) {
|
|
@@ -1738,33 +1732,12 @@ export class FeishuBot {
|
|
|
1738
1732
|
}
|
|
1739
1733
|
completionSummary(meta, emoji = "✅") {
|
|
1740
1734
|
const en = meta.locale === "en";
|
|
1741
|
-
|
|
1735
|
+
return en
|
|
1742
1736
|
? `${emoji} ${meta.toolCalls} tool call${meta.toolCalls === 1 ? "" : "s"} · ⏱ ${meta.elapsed}`
|
|
1743
1737
|
: `${emoji} 累计${meta.toolCalls} 次工具调用 · ⏱ 耗时${meta.elapsed}`;
|
|
1744
|
-
return meta.model ? `${core} · 🧠 ${this.escapeCardText(meta.model)}` : core;
|
|
1745
|
-
}
|
|
1746
|
-
/** Final answer card used to replace the running live-status card in place. */
|
|
1747
|
-
buildFinalAnswerCard(text, meta) {
|
|
1748
|
-
const elements = buildFeishuCardElements(text);
|
|
1749
|
-
elements.push({ tag: "hr" });
|
|
1750
|
-
elements.push({
|
|
1751
|
-
tag: "markdown",
|
|
1752
|
-
content: `<font color='grey'>${this.completionSummary(meta)}</font>`,
|
|
1753
|
-
});
|
|
1754
|
-
return {
|
|
1755
|
-
schema: "2.0",
|
|
1756
|
-
config: { update_multi: true, width_mode: "fill" },
|
|
1757
|
-
body: { elements },
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
async patchFinalAnswerCard(messageId, text, meta, _chatId) {
|
|
1761
|
-
await this.client.im.message.patch({
|
|
1762
|
-
path: { message_id: messageId },
|
|
1763
|
-
data: { content: JSON.stringify(this.buildFinalAnswerCard(text, meta)) },
|
|
1764
|
-
});
|
|
1765
1738
|
}
|
|
1766
|
-
/** Best-effort cleanup
|
|
1767
|
-
*
|
|
1739
|
+
/** Best-effort terminal cleanup for status-only rows and legacy v1.4.3
|
|
1740
|
+
* outbox entries that once targeted the live-status card. */
|
|
1768
1741
|
async patchLiveStatusDoneSummary(messageId, meta, chatId) {
|
|
1769
1742
|
const view = {
|
|
1770
1743
|
title: meta.locale === "en" ? `✅ ${this.config.name} done` : `✅ ${this.config.name} 已完成`,
|
|
@@ -2094,58 +2067,27 @@ export class FeishuBot {
|
|
|
2094
2067
|
const sourceId = `discussion-system:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
2095
2068
|
await this.enqueueAndDispatchDelivery(chatId, "discussion_system", sourceId, text);
|
|
2096
2069
|
}
|
|
2097
|
-
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey,
|
|
2070
|
+
async enqueueAndDispatchDelivery(chatId, sourceType, sourceId, text, attachments = [], replyToMessageId, deliveryKey, finalReplyModel) {
|
|
2098
2071
|
if (!text.trim() && attachments.length === 0)
|
|
2099
2072
|
return;
|
|
2100
2073
|
const attachmentsJson = JSON.stringify(attachments);
|
|
2101
2074
|
const normalizedPayload = `${text.trim()}|${attachmentsJson}`;
|
|
2102
2075
|
const contentHash = this.stableHash(normalizedPayload);
|
|
2103
2076
|
const finalDeliveryKey = deliveryKey || sourceId;
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
chatId,
|
|
2108
|
-
botName: this.config.name,
|
|
2109
|
-
sourceType: `${sourceType}_status_cleanup`,
|
|
2110
|
-
sourceId: `${sourceId}:status-cleanup:${suffix}:${meta.messageId}`,
|
|
2111
|
-
deliveryKey: `${finalDeliveryKey}:status-cleanup:${suffix}:${meta.messageId}`,
|
|
2112
|
-
contentHash: "",
|
|
2113
|
-
content: "",
|
|
2114
|
-
attachmentsJson: "[]",
|
|
2115
|
-
replyToMessageId: replyToMessageId || "",
|
|
2116
|
-
deliveryMode: "patch_live_status",
|
|
2117
|
-
targetMessageId: meta.messageId,
|
|
2118
|
-
deliveryMetaJson: JSON.stringify(meta),
|
|
2119
|
-
textDelivered: true,
|
|
2120
|
-
cleanupPending: true,
|
|
2121
|
-
});
|
|
2122
|
-
if (cleanupId)
|
|
2123
|
-
await this.dispatchPendingDeliveries(chatId, replyToMessageId);
|
|
2124
|
-
};
|
|
2125
|
-
const finishDedupedStatus = async () => {
|
|
2126
|
-
if (!liveStatusFinal)
|
|
2127
|
-
return;
|
|
2128
|
-
await enqueueDurableStatusCleanup(liveStatusFinal, "content-dedupe");
|
|
2129
|
-
};
|
|
2077
|
+
const deliveryMetaJson = finalReplyModel?.trim()
|
|
2078
|
+
? JSON.stringify({ model: finalReplyModel.trim() })
|
|
2079
|
+
: "{}";
|
|
2130
2080
|
if (sourceType === "verbose_transcript") {
|
|
2131
|
-
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, ["verbose_transcript"]))
|
|
2132
|
-
await finishDedupedStatus();
|
|
2081
|
+
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, ["verbose_transcript"]))
|
|
2133
2082
|
return;
|
|
2134
|
-
|
|
2135
|
-
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, ["verbose_transcript"])) {
|
|
2136
|
-
await finishDedupedStatus();
|
|
2083
|
+
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, ["verbose_transcript"]))
|
|
2137
2084
|
return;
|
|
2138
|
-
}
|
|
2139
2085
|
}
|
|
2140
2086
|
else if (!deliveryKey) {
|
|
2141
|
-
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, [sourceType]))
|
|
2142
|
-
await finishDedupedStatus();
|
|
2087
|
+
if (this.store.hasRecentSimilarDelivery(this.config.name, chatId, contentHash, 60_000, [sourceType]))
|
|
2143
2088
|
return;
|
|
2144
|
-
|
|
2145
|
-
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, [sourceType])) {
|
|
2146
|
-
await finishDedupedStatus();
|
|
2089
|
+
if (this.store.hasRecentOverlappingDelivery(this.config.name, chatId, text, attachmentsJson, 60_000, 8, [sourceType]))
|
|
2147
2090
|
return;
|
|
2148
|
-
}
|
|
2149
2091
|
}
|
|
2150
2092
|
const deliveryId = this.store.enqueueDelivery({
|
|
2151
2093
|
sessionKey: this.deliverySessionKey(chatId),
|
|
@@ -2158,9 +2100,9 @@ export class FeishuBot {
|
|
|
2158
2100
|
content: text,
|
|
2159
2101
|
attachmentsJson,
|
|
2160
2102
|
replyToMessageId: replyToMessageId || "",
|
|
2161
|
-
deliveryMode:
|
|
2162
|
-
targetMessageId:
|
|
2163
|
-
deliveryMetaJson
|
|
2103
|
+
deliveryMode: "send",
|
|
2104
|
+
targetMessageId: "",
|
|
2105
|
+
deliveryMetaJson,
|
|
2164
2106
|
});
|
|
2165
2107
|
if (deliveryId === null) {
|
|
2166
2108
|
// Another path already owns this stable logical delivery (commonly the
|
|
@@ -2182,9 +2124,9 @@ export class FeishuBot {
|
|
|
2182
2124
|
sourceType: `${sourceType}_recovery`, sourceId: `${sourceId}:recovery`,
|
|
2183
2125
|
deliveryKey: recoveryKey, contentHash, content: text, attachmentsJson: "[]",
|
|
2184
2126
|
replyToMessageId: replyToMessageId || "",
|
|
2185
|
-
deliveryMode:
|
|
2186
|
-
targetMessageId:
|
|
2187
|
-
deliveryMetaJson
|
|
2127
|
+
deliveryMode: "send",
|
|
2128
|
+
targetMessageId: "",
|
|
2129
|
+
deliveryMetaJson,
|
|
2188
2130
|
});
|
|
2189
2131
|
}
|
|
2190
2132
|
}
|
|
@@ -2199,11 +2141,9 @@ export class FeishuBot {
|
|
|
2199
2141
|
sourceType: "assistant_visible_authoritative", sourceId: `${sourceId}:authoritative`,
|
|
2200
2142
|
deliveryKey: correctionKey, contentHash, content: text, attachmentsJson: "[]",
|
|
2201
2143
|
replyToMessageId: replyToMessageId || "",
|
|
2202
|
-
deliveryMode:
|
|
2203
|
-
targetMessageId:
|
|
2204
|
-
deliveryMetaJson
|
|
2205
|
-
? JSON.stringify(liveStatusFinal)
|
|
2206
|
-
: existing.deliveryMetaJson,
|
|
2144
|
+
deliveryMode: "send",
|
|
2145
|
+
targetMessageId: "",
|
|
2146
|
+
deliveryMetaJson,
|
|
2207
2147
|
});
|
|
2208
2148
|
}
|
|
2209
2149
|
// A competing final may add attachments that the first owner did not see.
|
|
@@ -2265,34 +2205,6 @@ export class FeishuBot {
|
|
|
2265
2205
|
}
|
|
2266
2206
|
// Dispatch recovery/correction/supplement rows together in creation order.
|
|
2267
2207
|
await this.dispatchPendingDeliveries(chatId, replyToMessageId);
|
|
2268
|
-
// A correction/recovery row can intentionally patch the newly-frozen card.
|
|
2269
|
-
// Never enqueue a compact cleanup for that same card afterwards: it would
|
|
2270
|
-
// overwrite the authoritative answer that was just patched into place.
|
|
2271
|
-
const competingRows = this.store.listDeliveriesByKeyPrefix(this.config.name, chatId, `${finalDeliveryKey}:`);
|
|
2272
|
-
const finalCardOwnsVisiblePayload = Boolean(liveStatusFinal) && [existing, ...competingRows]
|
|
2273
|
-
.some((row) => row?.targetMessageId === liveStatusFinal.messageId && this.deliveryOwnsVisiblePayload(row));
|
|
2274
|
-
if (liveStatusFinal && existing?.targetMessageId !== liveStatusFinal.messageId && !finalCardOwnsVisiblePayload) {
|
|
2275
|
-
const cleanupJson = JSON.stringify(liveStatusFinal);
|
|
2276
|
-
const cleanupId = this.store.enqueueDelivery({
|
|
2277
|
-
sessionKey: this.deliverySessionKey(chatId),
|
|
2278
|
-
chatId,
|
|
2279
|
-
botName: this.config.name,
|
|
2280
|
-
sourceType: `${sourceType}_status_cleanup`,
|
|
2281
|
-
sourceId: `${sourceId}:status-cleanup:${liveStatusFinal.messageId}`,
|
|
2282
|
-
deliveryKey: `${finalDeliveryKey}:status-cleanup:${liveStatusFinal.messageId}`,
|
|
2283
|
-
contentHash: "",
|
|
2284
|
-
content: "",
|
|
2285
|
-
attachmentsJson: "[]",
|
|
2286
|
-
replyToMessageId: replyToMessageId || "",
|
|
2287
|
-
deliveryMode: "patch_live_status",
|
|
2288
|
-
targetMessageId: liveStatusFinal.messageId,
|
|
2289
|
-
deliveryMetaJson: cleanupJson,
|
|
2290
|
-
textDelivered: true,
|
|
2291
|
-
cleanupPending: true,
|
|
2292
|
-
});
|
|
2293
|
-
if (cleanupId)
|
|
2294
|
-
await this.dispatchPendingDeliveries(chatId, replyToMessageId);
|
|
2295
|
-
}
|
|
2296
2208
|
return;
|
|
2297
2209
|
}
|
|
2298
2210
|
await this.dispatchPendingDeliveries(chatId, replyToMessageId);
|
|
@@ -2326,49 +2238,39 @@ export class FeishuBot {
|
|
|
2326
2238
|
let failureStage = "text";
|
|
2327
2239
|
try {
|
|
2328
2240
|
const attachments = JSON.parse(item.attachmentsJson || "[]");
|
|
2329
|
-
//
|
|
2330
|
-
//
|
|
2331
|
-
//
|
|
2332
|
-
// the retry budget is exhausted do we fall back to a new message.
|
|
2241
|
+
// Final text is always sent as a NEW message. Never patch it into the
|
|
2242
|
+
// older live-status card: user messages inserted during the run must
|
|
2243
|
+
// stay above the eventual final answer, not below it.
|
|
2333
2244
|
if (item.content.trim() && !item.textDelivered) {
|
|
2334
2245
|
const replyTarget = item.replyToMessageId || replyToMessageId;
|
|
2335
|
-
const shouldReplyToSource = replyTarget && (item.sourceType
|
|
2336
|
-
|
|
2246
|
+
const shouldReplyToSource = Boolean(replyTarget) && (item.sourceType.startsWith("assistant_visible")
|
|
2247
|
+
|| item.sourceType === "verbose_transcript"
|
|
2248
|
+
|| item.sourceType === "provider_error"
|
|
2249
|
+
|| item.sourceType === "delayed_error");
|
|
2250
|
+
let finalModel;
|
|
2251
|
+
try {
|
|
2252
|
+
const meta = JSON.parse(item.deliveryMetaJson || "{}");
|
|
2253
|
+
finalModel = typeof meta.model === "string" && meta.model.trim() ? meta.model.trim() : undefined;
|
|
2254
|
+
}
|
|
2255
|
+
catch { /* legacy/malformed metadata: send without model footer */ }
|
|
2256
|
+
if (shouldReplyToSource) {
|
|
2337
2257
|
try {
|
|
2338
|
-
|
|
2339
|
-
await this.patchFinalAnswerCard(item.targetMessageId, item.content, meta, chatId);
|
|
2340
|
-
this.store.markDeliveryTextDelivered(item.id);
|
|
2258
|
+
await this.replyFinalMessage(replyTarget, item.content, finalModel);
|
|
2341
2259
|
}
|
|
2342
|
-
catch
|
|
2343
|
-
|
|
2344
|
-
throw patchErr;
|
|
2345
|
-
console.warn(`[${this.config.name}] final-card patch exhausted; falling back to new message:`, this.errorSummary(patchErr));
|
|
2346
|
-
if (shouldReplyToSource) {
|
|
2347
|
-
try {
|
|
2348
|
-
await this.replyMessage(replyTarget, item.content);
|
|
2349
|
-
}
|
|
2350
|
-
catch {
|
|
2351
|
-
await this.sendMessage(chatId, item.content);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
else {
|
|
2355
|
-
await this.sendMessage(chatId, item.content);
|
|
2356
|
-
}
|
|
2357
|
-
this.store.markDeliveryTextDeliveredWithCleanup(item.id);
|
|
2260
|
+
catch {
|
|
2261
|
+
await this.sendFinalMessage(chatId, item.content, finalModel);
|
|
2358
2262
|
}
|
|
2359
2263
|
}
|
|
2360
2264
|
else {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
await this.sendMessage(chatId, item.content);
|
|
2371
|
-
}
|
|
2265
|
+
await this.sendFinalMessage(chatId, item.content, finalModel);
|
|
2266
|
+
}
|
|
2267
|
+
// Rows created by v1.4.3 may still target a live-status card. Preserve
|
|
2268
|
+
// restart compatibility: send their text as a new message, then
|
|
2269
|
+
// collapse the old status card in the cleanup stage below.
|
|
2270
|
+
if (item.deliveryMode === "patch_live_status" && item.targetMessageId) {
|
|
2271
|
+
this.store.markDeliveryTextDeliveredWithCleanup(item.id);
|
|
2272
|
+
}
|
|
2273
|
+
else {
|
|
2372
2274
|
this.store.markDeliveryTextDelivered(item.id);
|
|
2373
2275
|
}
|
|
2374
2276
|
}
|
|
@@ -2589,7 +2491,6 @@ export class FeishuBot {
|
|
|
2589
2491
|
const roundMarker = `—— 第 ${meta.round}/${meta.maxRounds} 轮 · ${this.config.name}`;
|
|
2590
2492
|
displayReply = `${displayReply}\n\n${roundMarker}`;
|
|
2591
2493
|
}
|
|
2592
|
-
let finalStatusMeta;
|
|
2593
2494
|
if (isVisible || parsedReply.attachments.length > 0) {
|
|
2594
2495
|
const storedContent = [displayReply, ...parsedReply.attachments.map((a) => `[Attachment: ${a.type || "file"} ${a.path}]`)]
|
|
2595
2496
|
.filter(Boolean)
|
|
@@ -2603,14 +2504,10 @@ export class FeishuBot {
|
|
|
2603
2504
|
timestamp: Date.now(),
|
|
2604
2505
|
});
|
|
2605
2506
|
try {
|
|
2606
|
-
|
|
2607
|
-
finalStatusMeta = await liveStatus?.prepareTerminalDelivery(false);
|
|
2608
|
-
}
|
|
2609
|
-
await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ? displayReply : "", parsedReply.attachments, undefined, undefined, finalStatusMeta);
|
|
2507
|
+
await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ? displayReply : "", parsedReply.attachments, undefined, undefined, this.config.model);
|
|
2610
2508
|
}
|
|
2611
2509
|
catch (err) {
|
|
2612
|
-
|
|
2613
|
-
await liveStatus?.fail().catch(() => { });
|
|
2510
|
+
await liveStatus?.fail().catch(() => { });
|
|
2614
2511
|
throw err;
|
|
2615
2512
|
}
|
|
2616
2513
|
}
|
|
@@ -2619,7 +2516,7 @@ export class FeishuBot {
|
|
|
2619
2516
|
if (!isVisible && parsedReply.attachments.length === 0) {
|
|
2620
2517
|
await liveStatus?.noReply().catch(() => { });
|
|
2621
2518
|
}
|
|
2622
|
-
else
|
|
2519
|
+
else {
|
|
2623
2520
|
await liveStatus?.complete().catch(() => { });
|
|
2624
2521
|
}
|
|
2625
2522
|
return { botName: this.config.name, text: cleanVisibleReply, visible: isVisible };
|
|
@@ -2634,7 +2531,6 @@ export class FeishuBot {
|
|
|
2634
2531
|
const summary = this.completionSummary({
|
|
2635
2532
|
toolCalls: view.toolCalls,
|
|
2636
2533
|
elapsed: view.elapsed,
|
|
2637
|
-
model: view.model,
|
|
2638
2534
|
locale: en ? "en" : "zh",
|
|
2639
2535
|
}, statusEmoji);
|
|
2640
2536
|
return {
|
|
@@ -2667,12 +2563,10 @@ export class FeishuBot {
|
|
|
2667
2563
|
elements.push({ tag: "markdown", content: en ? "_working…_" : "_正在启动…_" });
|
|
2668
2564
|
}
|
|
2669
2565
|
elements.push({ tag: "hr" });
|
|
2670
|
-
//
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
footerBits.push(`🧠 ${this.escapeCardText(view.model)}`);
|
|
2675
|
-
elements.push({ tag: "markdown", content: `<font color='grey'>${footerBits.join(" · ")}</font>` });
|
|
2566
|
+
// Running/failed status owns timing; model attribution belongs to the final
|
|
2567
|
+
// reply card so the two messages have clear, non-duplicated responsibilities.
|
|
2568
|
+
const footer = en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`;
|
|
2569
|
+
elements.push({ tag: "markdown", content: `<font color='grey'>${footer}</font>` });
|
|
2676
2570
|
const template = view.state === "failed" ? "orange" : "blue";
|
|
2677
2571
|
return {
|
|
2678
2572
|
schema: "2.0",
|
|
@@ -2833,9 +2727,21 @@ export class FeishuBot {
|
|
|
2833
2727
|
async deleteMessageById(messageId) {
|
|
2834
2728
|
await this.client.im.v1.message.delete({ path: { message_id: messageId } });
|
|
2835
2729
|
}
|
|
2730
|
+
async replyFinalMessage(messageId, text, model) {
|
|
2731
|
+
if (!model?.trim())
|
|
2732
|
+
return this.replyMessage(messageId, text);
|
|
2733
|
+
this.replyModelFooters.set(messageId, model.trim());
|
|
2734
|
+
try {
|
|
2735
|
+
return await this.replyMessage(messageId, text);
|
|
2736
|
+
}
|
|
2737
|
+
finally {
|
|
2738
|
+
this.replyModelFooters.delete(messageId);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2836
2741
|
async replyMessage(messageId, text) {
|
|
2837
2742
|
// Use Feishu CardKit v2 markdown component for full Markdown rendering.
|
|
2838
|
-
const
|
|
2743
|
+
const model = this.replyModelFooters.get(messageId);
|
|
2744
|
+
const card = this.buildMarkdownCard(text, model);
|
|
2839
2745
|
try {
|
|
2840
2746
|
const res = await this.client.im.message.reply({
|
|
2841
2747
|
path: { message_id: messageId },
|
|
@@ -2847,11 +2753,12 @@ export class FeishuBot {
|
|
|
2847
2753
|
return res?.data?.message_id || res?.message_id;
|
|
2848
2754
|
}
|
|
2849
2755
|
catch {
|
|
2850
|
-
// Fallback to plain text if card fails
|
|
2756
|
+
// Fallback to plain text if card fails; retain model attribution.
|
|
2757
|
+
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}` : text;
|
|
2851
2758
|
const res = await this.client.im.message.reply({
|
|
2852
2759
|
path: { message_id: messageId },
|
|
2853
2760
|
data: {
|
|
2854
|
-
content: JSON.stringify({ text }),
|
|
2761
|
+
content: JSON.stringify({ text: fallbackText }),
|
|
2855
2762
|
msg_type: "text",
|
|
2856
2763
|
},
|
|
2857
2764
|
});
|
|
@@ -3179,8 +3086,20 @@ export class FeishuBot {
|
|
|
3179
3086
|
/**
|
|
3180
3087
|
* Send a proactive message to a chat (not a reply).
|
|
3181
3088
|
*/
|
|
3089
|
+
async sendFinalMessage(chatId, text, model) {
|
|
3090
|
+
if (!model?.trim())
|
|
3091
|
+
return this.sendMessage(chatId, text);
|
|
3092
|
+
this.sendModelFooters.set(chatId, model.trim());
|
|
3093
|
+
try {
|
|
3094
|
+
return await this.sendMessage(chatId, text);
|
|
3095
|
+
}
|
|
3096
|
+
finally {
|
|
3097
|
+
this.sendModelFooters.delete(chatId);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3182
3100
|
async sendMessage(chatId, text) {
|
|
3183
|
-
const
|
|
3101
|
+
const model = this.sendModelFooters.get(chatId);
|
|
3102
|
+
const card = this.buildMarkdownCard(text, model);
|
|
3184
3103
|
try {
|
|
3185
3104
|
const res = await this.client.im.message.create({
|
|
3186
3105
|
params: { receive_id_type: "chat_id" },
|
|
@@ -3197,13 +3116,14 @@ export class FeishuBot {
|
|
|
3197
3116
|
console.warn(`[${this.config.name}] sendMessage interactive failed:`, JSON.stringify(err?.response?.data || err?.data || { message: err.message }));
|
|
3198
3117
|
if (this.isOutOfChatError(err))
|
|
3199
3118
|
this.markCurrentBotUnavailable(chatId, err);
|
|
3200
|
-
// Fallback to plain text
|
|
3119
|
+
// Fallback to plain text; retain model attribution.
|
|
3201
3120
|
try {
|
|
3121
|
+
const fallbackText = model?.trim() ? `${text}\n\n🧠 ${model.trim()}` : text;
|
|
3202
3122
|
const res = await this.client.im.message.create({
|
|
3203
3123
|
params: { receive_id_type: "chat_id" },
|
|
3204
3124
|
data: {
|
|
3205
3125
|
receive_id: chatId,
|
|
3206
|
-
content: JSON.stringify({ text }),
|
|
3126
|
+
content: JSON.stringify({ text: fallbackText }),
|
|
3207
3127
|
msg_type: "text",
|
|
3208
3128
|
},
|
|
3209
3129
|
});
|
package/dist/message-store.d.ts
CHANGED
|
@@ -41,12 +41,13 @@ export interface DeliveryOutboxItem {
|
|
|
41
41
|
content: string;
|
|
42
42
|
attachmentsJson: string;
|
|
43
43
|
replyToMessageId: string;
|
|
44
|
-
/** Delivery mode. `patch_live_status`
|
|
45
|
-
*
|
|
44
|
+
/** Delivery mode. New final replies use `send`; `patch_live_status` is kept
|
|
45
|
+
* for status-only cleanup and restart compatibility with legacy outbox rows. */
|
|
46
46
|
deliveryMode?: "send" | "patch_live_status";
|
|
47
47
|
/** Existing interactive-card message id to patch for patch_live_status. */
|
|
48
48
|
targetMessageId?: string;
|
|
49
|
-
/** Persisted JSON
|
|
49
|
+
/** Persisted JSON delivery metadata (for example final-reply model footer,
|
|
50
|
+
* or legacy live-status cleanup metadata). */
|
|
50
51
|
deliveryMetaJson?: string;
|
|
51
52
|
/** Durable stage checkpoint: final text/card is already visible. */
|
|
52
53
|
textDelivered?: boolean;
|