@openclaw/feishu 2026.3.13 → 2026.5.2-beta.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/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1827 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +406 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +403 -26
- package/src/media.ts +509 -132
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +453 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/reply-dispatcher.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
|
2
|
+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
3
|
+
import {
|
|
4
|
+
resolveSendableOutboundReplyParts,
|
|
5
|
+
resolveTextChunksWithFallback,
|
|
6
|
+
sendMediaWithLeadingCaption,
|
|
7
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
8
|
+
import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-runtime";
|
|
9
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
10
|
+
import { createFeishuClient } from "./client.js";
|
|
11
|
+
import { sendMediaFeishu } from "./media.js";
|
|
12
|
+
import type { MentionTarget } from "./mention-target.types.js";
|
|
13
|
+
import { buildMentionedCardContent } from "./mention.js";
|
|
1
14
|
import {
|
|
2
15
|
createReplyPrefixContext,
|
|
3
|
-
createTypingCallbacks,
|
|
4
|
-
logTypingFailure,
|
|
5
16
|
type ClawdbotConfig,
|
|
17
|
+
type OutboundIdentity,
|
|
6
18
|
type ReplyPayload,
|
|
7
19
|
type RuntimeEnv,
|
|
8
|
-
} from "
|
|
9
|
-
import { resolveFeishuAccount } from "./accounts.js";
|
|
10
|
-
import { createFeishuClient } from "./client.js";
|
|
11
|
-
import { sendMediaFeishu } from "./media.js";
|
|
12
|
-
import type { MentionTarget } from "./mention.js";
|
|
13
|
-
import { buildMentionedCardContent } from "./mention.js";
|
|
20
|
+
} from "./reply-dispatcher-runtime-api.js";
|
|
14
21
|
import { getFeishuRuntime } from "./runtime.js";
|
|
15
|
-
import {
|
|
22
|
+
import { sendMessageFeishu, sendStructuredCardFeishu, type CardHeaderConfig } from "./send.js";
|
|
16
23
|
import { FeishuStreamingSession, mergeStreamingText } from "./streaming-card.js";
|
|
17
24
|
import { resolveReceiveIdType } from "./targets.js";
|
|
18
25
|
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
|
|
@@ -26,6 +33,30 @@ function shouldUseCard(text: string): boolean {
|
|
|
26
33
|
* Messages older than this are likely replays after context compaction (#30418). */
|
|
27
34
|
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000;
|
|
28
35
|
const MS_EPOCH_MIN = 1_000_000_000_000;
|
|
36
|
+
const STREAMING_START_FAILURE_BACKOFF_MS = 60_000;
|
|
37
|
+
const streamingStartBackoffUntilByAccount = new Map<string, number>();
|
|
38
|
+
|
|
39
|
+
function isStreamingStartBackedOff(accountId: string, now = Date.now()): boolean {
|
|
40
|
+
const backoffUntil = streamingStartBackoffUntilByAccount.get(accountId);
|
|
41
|
+
if (backoffUntil === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (backoffUntil <= now) {
|
|
45
|
+
streamingStartBackoffUntilByAccount.delete(accountId);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rememberStreamingStartFailure(accountId: string, now = Date.now()): number {
|
|
52
|
+
const backoffUntil = now + STREAMING_START_FAILURE_BACKOFF_MS;
|
|
53
|
+
streamingStartBackoffUntilByAccount.set(accountId, backoffUntil);
|
|
54
|
+
return backoffUntil;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clearFeishuStreamingStartBackoffForTests() {
|
|
58
|
+
streamingStartBackoffUntilByAccount.clear();
|
|
59
|
+
}
|
|
29
60
|
|
|
30
61
|
function normalizeEpochMs(timestamp: number | undefined): number | undefined {
|
|
31
62
|
if (!Number.isFinite(timestamp) || timestamp === undefined || timestamp <= 0) {
|
|
@@ -36,11 +67,46 @@ function normalizeEpochMs(timestamp: number | undefined): number | undefined {
|
|
|
36
67
|
return timestamp < MS_EPOCH_MIN ? timestamp * 1000 : timestamp;
|
|
37
68
|
}
|
|
38
69
|
|
|
39
|
-
|
|
70
|
+
/** Build a card header from agent identity config. */
|
|
71
|
+
function resolveCardHeader(
|
|
72
|
+
agentId: string,
|
|
73
|
+
identity: OutboundIdentity | undefined,
|
|
74
|
+
): CardHeaderConfig | undefined {
|
|
75
|
+
const name = identity?.name?.trim() || (agentId === "main" ? "" : agentId);
|
|
76
|
+
const emoji = identity?.emoji?.trim();
|
|
77
|
+
const title = (emoji ? `${emoji} ${name}` : name).trim();
|
|
78
|
+
if (!title) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
title,
|
|
83
|
+
template: identity?.theme ?? "blue",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Build a card note footer from agent identity and model context. */
|
|
88
|
+
function resolveCardNote(
|
|
89
|
+
agentId: string,
|
|
90
|
+
identity: OutboundIdentity | undefined,
|
|
91
|
+
prefixCtx: { model?: string; provider?: string },
|
|
92
|
+
): string {
|
|
93
|
+
const name = identity?.name?.trim() || agentId;
|
|
94
|
+
const parts: string[] = [`Agent: ${name}`];
|
|
95
|
+
if (prefixCtx.model) {
|
|
96
|
+
parts.push(`Model: ${prefixCtx.model}`);
|
|
97
|
+
}
|
|
98
|
+
if (prefixCtx.provider) {
|
|
99
|
+
parts.push(`Provider: ${prefixCtx.provider}`);
|
|
100
|
+
}
|
|
101
|
+
return parts.join(" | ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type CreateFeishuReplyDispatcherParams = {
|
|
40
105
|
cfg: ClawdbotConfig;
|
|
41
106
|
agentId: string;
|
|
42
107
|
runtime: RuntimeEnv;
|
|
43
108
|
chatId: string;
|
|
109
|
+
allowReasoningPreview?: boolean;
|
|
44
110
|
replyToMessageId?: string;
|
|
45
111
|
/** When true, preserve typing indicator on reply target but send messages without reply metadata */
|
|
46
112
|
skipReplyToInMessages?: boolean;
|
|
@@ -50,6 +116,7 @@ export type CreateFeishuReplyDispatcherParams = {
|
|
|
50
116
|
rootId?: string;
|
|
51
117
|
mentionTargets?: MentionTarget[];
|
|
52
118
|
accountId?: string;
|
|
119
|
+
identity?: OutboundIdentity;
|
|
53
120
|
/** Epoch ms when the inbound message was created. Used to suppress typing
|
|
54
121
|
* indicators on old/replayed messages after context compaction (#30418). */
|
|
55
122
|
messageCreateTimeMs?: number;
|
|
@@ -68,66 +135,78 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
68
135
|
rootId,
|
|
69
136
|
mentionTargets,
|
|
70
137
|
accountId,
|
|
138
|
+
identity,
|
|
71
139
|
} = params;
|
|
72
140
|
const sendReplyToMessageId = skipReplyToInMessages ? undefined : replyToMessageId;
|
|
73
141
|
const threadReplyMode = threadReply === true;
|
|
74
142
|
const effectiveReplyInThread = threadReplyMode ? true : replyInThread;
|
|
75
|
-
const account =
|
|
143
|
+
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
76
144
|
const prefixContext = createReplyPrefixContext({ cfg, agentId });
|
|
77
145
|
|
|
78
146
|
let typingState: TypingIndicatorState | null = null;
|
|
79
|
-
const typingCallbacks =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
const { typingCallbacks } = createChannelReplyPipeline({
|
|
148
|
+
cfg,
|
|
149
|
+
agentId,
|
|
150
|
+
channel: "feishu",
|
|
151
|
+
accountId,
|
|
152
|
+
typing: {
|
|
153
|
+
start: async () => {
|
|
154
|
+
// Check if typing indicator is enabled (default: true)
|
|
155
|
+
if (!(account.config.typingIndicator ?? true)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!replyToMessageId) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Skip typing indicator for old messages — likely replays after context
|
|
162
|
+
// compaction that would flood users with stale notifications (#30418).
|
|
163
|
+
const messageCreateTimeMs = normalizeEpochMs(params.messageCreateTimeMs);
|
|
164
|
+
if (
|
|
165
|
+
messageCreateTimeMs !== undefined &&
|
|
166
|
+
Date.now() - messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
|
|
167
|
+
) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Feishu reactions persist until explicitly removed, so skip keepalive
|
|
171
|
+
// re-adds when a reaction already exists. Re-adding the same emoji
|
|
172
|
+
// triggers a new push notification for every call (#28660).
|
|
173
|
+
if (typingState?.reactionId) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
typingState = await addTypingIndicator({
|
|
177
|
+
cfg,
|
|
178
|
+
messageId: replyToMessageId,
|
|
179
|
+
accountId,
|
|
180
|
+
runtime: params.runtime,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
stop: async () => {
|
|
184
|
+
if (!typingState) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await removeTypingIndicator({
|
|
188
|
+
cfg,
|
|
189
|
+
state: typingState,
|
|
190
|
+
accountId,
|
|
191
|
+
runtime: params.runtime,
|
|
192
|
+
});
|
|
193
|
+
typingState = null;
|
|
194
|
+
},
|
|
195
|
+
onStartError: (err) =>
|
|
196
|
+
logTypingFailure({
|
|
197
|
+
log: (message) => params.runtime.log?.(message),
|
|
198
|
+
channel: "feishu",
|
|
199
|
+
action: "start",
|
|
200
|
+
error: err,
|
|
201
|
+
}),
|
|
202
|
+
onStopError: (err) =>
|
|
203
|
+
logTypingFailure({
|
|
204
|
+
log: (message) => params.runtime.log?.(message),
|
|
205
|
+
channel: "feishu",
|
|
206
|
+
action: "stop",
|
|
207
|
+
error: err,
|
|
208
|
+
}),
|
|
116
209
|
},
|
|
117
|
-
onStartError: (err) =>
|
|
118
|
-
logTypingFailure({
|
|
119
|
-
log: (message) => params.runtime.log?.(message),
|
|
120
|
-
channel: "feishu",
|
|
121
|
-
action: "start",
|
|
122
|
-
error: err,
|
|
123
|
-
}),
|
|
124
|
-
onStopError: (err) =>
|
|
125
|
-
logTypingFailure({
|
|
126
|
-
log: (message) => params.runtime.log?.(message),
|
|
127
|
-
channel: "feishu",
|
|
128
|
-
action: "stop",
|
|
129
|
-
error: err,
|
|
130
|
-
}),
|
|
131
210
|
});
|
|
132
211
|
|
|
133
212
|
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "feishu", accountId, {
|
|
@@ -136,18 +215,61 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
136
215
|
const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu");
|
|
137
216
|
const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg, channel: "feishu" });
|
|
138
217
|
const renderMode = account.config?.renderMode ?? "auto";
|
|
139
|
-
|
|
140
|
-
const streamingEnabled
|
|
141
|
-
!threadReplyMode && account.config?.streaming !== false && renderMode !== "raw";
|
|
218
|
+
const streamingEnabled = account.config?.streaming !== false && renderMode !== "raw";
|
|
219
|
+
const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
|
|
142
220
|
|
|
143
221
|
let streaming: FeishuStreamingSession | null = null;
|
|
144
222
|
let streamText = "";
|
|
145
223
|
let lastPartial = "";
|
|
224
|
+
let reasoningText = "";
|
|
225
|
+
let statusLine = "";
|
|
226
|
+
let snapshotBaseText = "";
|
|
227
|
+
let lastSnapshotTextLength = 0;
|
|
146
228
|
const deliveredFinalTexts = new Set<string>();
|
|
147
229
|
let partialUpdateQueue: Promise<void> = Promise.resolve();
|
|
148
230
|
let streamingStartPromise: Promise<void> | null = null;
|
|
231
|
+
let streamingClosedForReply = false;
|
|
232
|
+
let streamingCloseErroredForReply = false;
|
|
149
233
|
type StreamTextUpdateMode = "snapshot" | "delta";
|
|
150
234
|
|
|
235
|
+
const formatReasoningPrefix = (thinking: string): string => {
|
|
236
|
+
if (!thinking) {
|
|
237
|
+
return "";
|
|
238
|
+
}
|
|
239
|
+
const withoutLabel = thinking.replace(/^Reasoning:\n/, "");
|
|
240
|
+
const plain = withoutLabel.replace(/^_(.*)_$/gm, "$1");
|
|
241
|
+
const lines = plain.split("\n").map((line) => `> ${line}`);
|
|
242
|
+
return `> 💭 **Thinking**\n${lines.join("\n")}`;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const buildCombinedStreamText = (thinking: string, answer: string): string => {
|
|
246
|
+
const parts: string[] = [];
|
|
247
|
+
if (thinking) {
|
|
248
|
+
parts.push(formatReasoningPrefix(thinking));
|
|
249
|
+
}
|
|
250
|
+
if (thinking && answer) {
|
|
251
|
+
parts.push("\n\n---\n\n");
|
|
252
|
+
}
|
|
253
|
+
if (answer) {
|
|
254
|
+
parts.push(answer);
|
|
255
|
+
}
|
|
256
|
+
if (statusLine) {
|
|
257
|
+
parts.push(parts.length > 0 ? `\n\n${statusLine}` : statusLine);
|
|
258
|
+
}
|
|
259
|
+
return parts.join("");
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const flushStreamingCardUpdate = (combined: string) => {
|
|
263
|
+
partialUpdateQueue = partialUpdateQueue.then(async () => {
|
|
264
|
+
if (streamingStartPromise) {
|
|
265
|
+
await streamingStartPromise;
|
|
266
|
+
}
|
|
267
|
+
if (streaming?.isActive()) {
|
|
268
|
+
await streaming.update(combined);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
151
273
|
const queueStreamingUpdate = (
|
|
152
274
|
nextText: string,
|
|
153
275
|
options?: {
|
|
@@ -165,20 +287,42 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
165
287
|
lastPartial = nextText;
|
|
166
288
|
}
|
|
167
289
|
const mode = options?.mode ?? "snapshot";
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
290
|
+
if (mode === "delta") {
|
|
291
|
+
streamText = `${streamText}${nextText}`;
|
|
292
|
+
} else {
|
|
293
|
+
const currentSnapshotText = snapshotBaseText
|
|
294
|
+
? streamText.slice(snapshotBaseText.length)
|
|
295
|
+
: streamText;
|
|
296
|
+
const startsNewSnapshotBlock =
|
|
297
|
+
lastSnapshotTextLength >= 20 &&
|
|
298
|
+
nextText.length < lastSnapshotTextLength * 0.5 &&
|
|
299
|
+
!currentSnapshotText.includes(nextText);
|
|
300
|
+
if (startsNewSnapshotBlock) {
|
|
301
|
+
snapshotBaseText = streamText;
|
|
302
|
+
streamText = `${snapshotBaseText}${nextText}`;
|
|
303
|
+
} else {
|
|
304
|
+
streamText = `${snapshotBaseText}${mergeStreamingText(currentSnapshotText, nextText)}`;
|
|
176
305
|
}
|
|
177
|
-
|
|
306
|
+
lastSnapshotTextLength = nextText.length;
|
|
307
|
+
}
|
|
308
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const queueReasoningUpdate = (nextThinking: string) => {
|
|
312
|
+
if (!nextThinking) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
reasoningText = nextThinking;
|
|
316
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
178
317
|
};
|
|
179
318
|
|
|
180
319
|
const startStreaming = () => {
|
|
181
|
-
if (
|
|
320
|
+
if (
|
|
321
|
+
!streamingEnabled ||
|
|
322
|
+
streamingStartPromise ||
|
|
323
|
+
streaming ||
|
|
324
|
+
isStreamingStartBackedOff(account.accountId)
|
|
325
|
+
) {
|
|
182
326
|
return;
|
|
183
327
|
}
|
|
184
328
|
streamingStartPromise = (async () => {
|
|
@@ -194,104 +338,155 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
194
338
|
params.runtime.log?.(`feishu[${account.accountId}] ${message}`),
|
|
195
339
|
);
|
|
196
340
|
try {
|
|
341
|
+
const cardHeader = resolveCardHeader(agentId, identity);
|
|
342
|
+
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
197
343
|
await streaming.start(chatId, resolveReceiveIdType(chatId), {
|
|
198
344
|
replyToMessageId,
|
|
199
345
|
replyInThread: effectiveReplyInThread,
|
|
200
346
|
rootId,
|
|
347
|
+
header: cardHeader,
|
|
348
|
+
note: cardNote,
|
|
201
349
|
});
|
|
350
|
+
streamingStartBackoffUntilByAccount.delete(account.accountId);
|
|
202
351
|
} catch (error) {
|
|
203
|
-
|
|
352
|
+
rememberStreamingStartFailure(account.accountId);
|
|
353
|
+
params.runtime.error?.(
|
|
354
|
+
`feishu[${account.accountId}]: streaming start failed; using non-streaming card fallback for ${
|
|
355
|
+
STREAMING_START_FAILURE_BACKOFF_MS / 1000
|
|
356
|
+
}s: ${String(error)}`,
|
|
357
|
+
);
|
|
204
358
|
streaming = null;
|
|
359
|
+
streamingStartPromise = null;
|
|
205
360
|
}
|
|
206
361
|
})();
|
|
207
362
|
};
|
|
208
363
|
|
|
209
|
-
const closeStreaming = async () => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await partialUpdateQueue;
|
|
214
|
-
if (streaming?.isActive()) {
|
|
215
|
-
let text = streamText;
|
|
216
|
-
if (mentionTargets?.length) {
|
|
217
|
-
text = buildMentionedCardContent(mentionTargets, text);
|
|
364
|
+
const closeStreaming = async (options?: { markClosedForReply?: boolean }) => {
|
|
365
|
+
try {
|
|
366
|
+
if (streamingStartPromise) {
|
|
367
|
+
await streamingStartPromise;
|
|
218
368
|
}
|
|
219
|
-
await
|
|
369
|
+
await partialUpdateQueue;
|
|
370
|
+
if (streaming?.isActive()) {
|
|
371
|
+
statusLine = "";
|
|
372
|
+
let text = buildCombinedStreamText(reasoningText, streamText);
|
|
373
|
+
if (mentionTargets?.length) {
|
|
374
|
+
text = buildMentionedCardContent(mentionTargets, text);
|
|
375
|
+
}
|
|
376
|
+
const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
377
|
+
await streaming.close(text, { note: finalNote });
|
|
378
|
+
// Track the raw streamed text so the duplicate-final check in deliver()
|
|
379
|
+
// can skip the redundant text delivery that arrives after onIdle closes
|
|
380
|
+
// the streaming card.
|
|
381
|
+
if (streamText) {
|
|
382
|
+
deliveredFinalTexts.add(streamText);
|
|
383
|
+
if (options?.markClosedForReply !== false && !streamingCloseErroredForReply) {
|
|
384
|
+
streamingClosedForReply = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
streaming = null;
|
|
390
|
+
streamingStartPromise = null;
|
|
391
|
+
partialUpdateQueue = Promise.resolve();
|
|
392
|
+
streamText = "";
|
|
393
|
+
lastPartial = "";
|
|
394
|
+
reasoningText = "";
|
|
395
|
+
statusLine = "";
|
|
396
|
+
snapshotBaseText = "";
|
|
397
|
+
lastSnapshotTextLength = 0;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const updateStreamingStatusLine = (nextStatusLine: string) => {
|
|
402
|
+
statusLine = nextStatusLine;
|
|
403
|
+
if (!streaming?.isActive() && !streamingStartPromise && renderMode !== "card") {
|
|
404
|
+
return;
|
|
220
405
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
streamText = "";
|
|
224
|
-
lastPartial = "";
|
|
406
|
+
startStreaming();
|
|
407
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
225
408
|
};
|
|
226
409
|
|
|
227
410
|
const sendChunkedTextReply = async (params: {
|
|
228
411
|
text: string;
|
|
229
412
|
useCard: boolean;
|
|
230
413
|
infoKind?: string;
|
|
414
|
+
sendChunk: (params: { chunk: string; isFirst: boolean }) => Promise<void>;
|
|
231
415
|
}) => {
|
|
232
|
-
let first = true;
|
|
233
416
|
const chunkSource = params.useCard
|
|
234
417
|
? params.text
|
|
235
418
|
: core.channel.text.convertMarkdownTables(params.text, tableMode);
|
|
236
|
-
|
|
419
|
+
const chunks = resolveTextChunksWithFallback(
|
|
237
420
|
chunkSource,
|
|
238
|
-
textChunkLimit,
|
|
239
|
-
|
|
240
|
-
)) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
replyToMessageId: sendReplyToMessageId,
|
|
246
|
-
replyInThread: effectiveReplyInThread,
|
|
247
|
-
mentions: first ? mentionTargets : undefined,
|
|
248
|
-
accountId,
|
|
249
|
-
};
|
|
250
|
-
if (params.useCard) {
|
|
251
|
-
await sendMarkdownCardFeishu(message);
|
|
252
|
-
} else {
|
|
253
|
-
await sendMessageFeishu(message);
|
|
254
|
-
}
|
|
255
|
-
first = false;
|
|
421
|
+
core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode),
|
|
422
|
+
);
|
|
423
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
424
|
+
await params.sendChunk({
|
|
425
|
+
chunk,
|
|
426
|
+
isFirst: index === 0,
|
|
427
|
+
});
|
|
256
428
|
}
|
|
257
429
|
if (params.infoKind === "final") {
|
|
258
430
|
deliveredFinalTexts.add(params.text);
|
|
259
431
|
}
|
|
260
432
|
};
|
|
261
433
|
|
|
434
|
+
const sendMediaReplies = async (payload: ReplyPayload) => {
|
|
435
|
+
await sendMediaWithLeadingCaption({
|
|
436
|
+
mediaUrls: resolveSendableOutboundReplyParts(payload).mediaUrls,
|
|
437
|
+
caption: "",
|
|
438
|
+
send: async ({ mediaUrl }) => {
|
|
439
|
+
await sendMediaFeishu({
|
|
440
|
+
cfg,
|
|
441
|
+
to: chatId,
|
|
442
|
+
mediaUrl,
|
|
443
|
+
replyToMessageId: sendReplyToMessageId,
|
|
444
|
+
replyInThread: effectiveReplyInThread,
|
|
445
|
+
accountId,
|
|
446
|
+
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
|
|
262
452
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
263
453
|
core.channel.reply.createReplyDispatcherWithTyping({
|
|
264
454
|
responsePrefix: prefixContext.responsePrefix,
|
|
265
455
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
266
456
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
267
|
-
onReplyStart: () => {
|
|
457
|
+
onReplyStart: async () => {
|
|
268
458
|
deliveredFinalTexts.clear();
|
|
459
|
+
streamingClosedForReply = false;
|
|
460
|
+
streamingCloseErroredForReply = false;
|
|
269
461
|
if (streamingEnabled && renderMode === "card") {
|
|
270
462
|
startStreaming();
|
|
271
463
|
}
|
|
272
|
-
|
|
464
|
+
await typingCallbacks?.onReplyStart?.();
|
|
273
465
|
},
|
|
274
466
|
deliver: async (payload: ReplyPayload, info) => {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
: [];
|
|
282
|
-
const hasText = Boolean(text.trim());
|
|
283
|
-
const hasMedia = mediaList.length > 0;
|
|
467
|
+
const reply = resolveSendableOutboundReplyParts(payload);
|
|
468
|
+
const text = reply.text;
|
|
469
|
+
const hasText = reply.hasText;
|
|
470
|
+
const hasMedia = reply.hasMedia;
|
|
471
|
+
const useCard =
|
|
472
|
+
hasText && (renderMode === "card" || (renderMode === "auto" && shouldUseCard(text)));
|
|
284
473
|
const skipTextForDuplicateFinal =
|
|
285
474
|
info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
|
286
|
-
const
|
|
475
|
+
const skipTextForClosedStreamingFinal =
|
|
476
|
+
info?.kind === "final" &&
|
|
477
|
+
hasText &&
|
|
478
|
+
streamingClosedForReply &&
|
|
479
|
+
!streamingCloseErroredForReply &&
|
|
480
|
+
streamingEnabled &&
|
|
481
|
+
useCard;
|
|
482
|
+
const shouldDeliverText =
|
|
483
|
+
hasText && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
|
|
287
484
|
|
|
288
485
|
if (!shouldDeliverText && !hasMedia) {
|
|
289
486
|
return;
|
|
290
487
|
}
|
|
291
488
|
|
|
292
489
|
if (shouldDeliverText) {
|
|
293
|
-
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
294
|
-
|
|
295
490
|
if (info?.kind === "block") {
|
|
296
491
|
// Drop internal block chunks unless we can safely consume them as
|
|
297
492
|
// streaming-card fallback content.
|
|
@@ -315,62 +510,81 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
315
510
|
if (info?.kind === "block") {
|
|
316
511
|
// Some runtimes emit block payloads without onPartial/final callbacks.
|
|
317
512
|
// Mirror block text into streamText so onIdle close still sends content.
|
|
318
|
-
queueStreamingUpdate(text, { mode: "delta" });
|
|
513
|
+
queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true });
|
|
319
514
|
}
|
|
320
515
|
if (info?.kind === "final") {
|
|
321
|
-
streamText =
|
|
322
|
-
|
|
323
|
-
|
|
516
|
+
streamText = text;
|
|
517
|
+
snapshotBaseText = "";
|
|
518
|
+
lastSnapshotTextLength = text.length;
|
|
519
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
324
520
|
}
|
|
325
521
|
// Send media even when streaming handled the text
|
|
326
522
|
if (hasMedia) {
|
|
327
|
-
|
|
328
|
-
|
|
523
|
+
await sendMediaReplies(payload);
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (useCard) {
|
|
529
|
+
const cardHeader = resolveCardHeader(agentId, identity);
|
|
530
|
+
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
531
|
+
await sendChunkedTextReply({
|
|
532
|
+
text,
|
|
533
|
+
useCard: true,
|
|
534
|
+
infoKind: info?.kind,
|
|
535
|
+
sendChunk: async ({ chunk, isFirst }) => {
|
|
536
|
+
await sendStructuredCardFeishu({
|
|
329
537
|
cfg,
|
|
330
538
|
to: chatId,
|
|
331
|
-
|
|
539
|
+
text: chunk,
|
|
332
540
|
replyToMessageId: sendReplyToMessageId,
|
|
333
541
|
replyInThread: effectiveReplyInThread,
|
|
542
|
+
mentions: isFirst ? mentionTargets : undefined,
|
|
334
543
|
accountId,
|
|
544
|
+
header: cardHeader,
|
|
545
|
+
note: cardNote,
|
|
335
546
|
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (useCard) {
|
|
342
|
-
await sendChunkedTextReply({ text, useCard: true, infoKind: info?.kind });
|
|
547
|
+
},
|
|
548
|
+
});
|
|
343
549
|
} else {
|
|
344
|
-
await sendChunkedTextReply({
|
|
550
|
+
await sendChunkedTextReply({
|
|
551
|
+
text,
|
|
552
|
+
useCard: false,
|
|
553
|
+
infoKind: info?.kind,
|
|
554
|
+
sendChunk: async ({ chunk, isFirst }) => {
|
|
555
|
+
await sendMessageFeishu({
|
|
556
|
+
cfg,
|
|
557
|
+
to: chatId,
|
|
558
|
+
text: chunk,
|
|
559
|
+
replyToMessageId: sendReplyToMessageId,
|
|
560
|
+
replyInThread: effectiveReplyInThread,
|
|
561
|
+
mentions: isFirst ? mentionTargets : undefined,
|
|
562
|
+
accountId,
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
});
|
|
345
566
|
}
|
|
346
567
|
}
|
|
347
568
|
|
|
348
569
|
if (hasMedia) {
|
|
349
|
-
|
|
350
|
-
await sendMediaFeishu({
|
|
351
|
-
cfg,
|
|
352
|
-
to: chatId,
|
|
353
|
-
mediaUrl,
|
|
354
|
-
replyToMessageId: sendReplyToMessageId,
|
|
355
|
-
replyInThread: effectiveReplyInThread,
|
|
356
|
-
accountId,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
570
|
+
await sendMediaReplies(payload);
|
|
359
571
|
}
|
|
360
572
|
},
|
|
361
573
|
onError: async (error, info) => {
|
|
574
|
+
streamingCloseErroredForReply = true;
|
|
575
|
+
streamingClosedForReply = false;
|
|
362
576
|
params.runtime.error?.(
|
|
363
577
|
`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`,
|
|
364
578
|
);
|
|
365
|
-
await closeStreaming();
|
|
366
|
-
typingCallbacks
|
|
579
|
+
await closeStreaming({ markClosedForReply: false });
|
|
580
|
+
typingCallbacks?.onIdle?.();
|
|
367
581
|
},
|
|
368
582
|
onIdle: async () => {
|
|
369
583
|
await closeStreaming();
|
|
370
|
-
typingCallbacks
|
|
584
|
+
typingCallbacks?.onIdle?.();
|
|
371
585
|
},
|
|
372
586
|
onCleanup: () => {
|
|
373
|
-
typingCallbacks
|
|
587
|
+
typingCallbacks?.onCleanup?.();
|
|
374
588
|
},
|
|
375
589
|
});
|
|
376
590
|
|
|
@@ -385,12 +599,51 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
385
599
|
if (!payload.text) {
|
|
386
600
|
return;
|
|
387
601
|
}
|
|
388
|
-
|
|
602
|
+
const cleaned = stripReasoningTagsFromText(payload.text, {
|
|
603
|
+
mode: "strict",
|
|
604
|
+
trim: "both",
|
|
605
|
+
});
|
|
606
|
+
if (!cleaned) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
queueStreamingUpdate(cleaned, {
|
|
389
610
|
dedupeWithLastPartial: true,
|
|
390
611
|
mode: "snapshot",
|
|
391
612
|
});
|
|
392
613
|
}
|
|
393
614
|
: undefined,
|
|
615
|
+
onReasoningStream: reasoningPreviewEnabled
|
|
616
|
+
? (payload: ReplyPayload) => {
|
|
617
|
+
if (!payload.text) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
startStreaming();
|
|
621
|
+
queueReasoningUpdate(payload.text);
|
|
622
|
+
}
|
|
623
|
+
: undefined,
|
|
624
|
+
onReasoningEnd: reasoningPreviewEnabled ? () => {} : undefined,
|
|
625
|
+
onToolStart: streamingEnabled
|
|
626
|
+
? (payload: { name?: string; phase?: string }) => {
|
|
627
|
+
updateStreamingStatusLine(
|
|
628
|
+
`🔧 **Using: ${payload.name ?? payload.phase ?? "tool"}...**`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
: undefined,
|
|
632
|
+
onAssistantMessageStart: streamingEnabled
|
|
633
|
+
? () => {
|
|
634
|
+
updateStreamingStatusLine("");
|
|
635
|
+
}
|
|
636
|
+
: undefined,
|
|
637
|
+
onCompactionStart: streamingEnabled
|
|
638
|
+
? () => {
|
|
639
|
+
updateStreamingStatusLine("📦 **Compacting context...**");
|
|
640
|
+
}
|
|
641
|
+
: undefined,
|
|
642
|
+
onCompactionEnd: streamingEnabled
|
|
643
|
+
? () => {
|
|
644
|
+
updateStreamingStatusLine("");
|
|
645
|
+
}
|
|
646
|
+
: undefined,
|
|
394
647
|
},
|
|
395
648
|
markDispatchIdle,
|
|
396
649
|
};
|