@openclaw/feishu 2026.3.12 → 2026.5.1-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 +1653 -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 +115 -22
- 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 +798 -786
- 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 +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -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 +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +77 -25
- 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 +76 -35
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- 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 +413 -87
- package/src/media.ts +488 -154
- 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 +220 -313
- 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 +194 -92
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +24 -36
- 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 +297 -39
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +272 -0
- package/src/monitor.webhook-security.test.ts +125 -91
- package/src/monitor.webhook.test-helpers.ts +116 -0
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +627 -53
- 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 +122 -118
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +23 -60
- 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 +721 -168
- package/src/reply-dispatcher.ts +422 -172
- 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 +127 -42
- package/src/send.test.ts +386 -4
- package/src/send.ts +486 -164
- 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 +479 -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
|
-
|
|
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)}`;
|
|
173
305
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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,34 +338,115 @@ 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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
text =
|
|
364
|
+
const closeStreaming = async (options?: { markClosedForReply?: boolean }) => {
|
|
365
|
+
try {
|
|
366
|
+
if (streamingStartPromise) {
|
|
367
|
+
await streamingStartPromise;
|
|
368
|
+
}
|
|
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
|
+
}
|
|
218
387
|
}
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
406
|
+
startStreaming();
|
|
407
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const sendChunkedTextReply = async (params: {
|
|
411
|
+
text: string;
|
|
412
|
+
useCard: boolean;
|
|
413
|
+
infoKind?: string;
|
|
414
|
+
sendChunk: (params: { chunk: string; isFirst: boolean }) => Promise<void>;
|
|
415
|
+
}) => {
|
|
416
|
+
const chunkSource = params.useCard
|
|
417
|
+
? params.text
|
|
418
|
+
: core.channel.text.convertMarkdownTables(params.text, tableMode);
|
|
419
|
+
const chunks = resolveTextChunksWithFallback(
|
|
420
|
+
chunkSource,
|
|
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
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (params.infoKind === "final") {
|
|
430
|
+
deliveredFinalTexts.add(params.text);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
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
|
+
});
|
|
225
450
|
};
|
|
226
451
|
|
|
227
452
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
@@ -229,34 +454,39 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
229
454
|
responsePrefix: prefixContext.responsePrefix,
|
|
230
455
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
231
456
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
232
|
-
onReplyStart: () => {
|
|
457
|
+
onReplyStart: async () => {
|
|
233
458
|
deliveredFinalTexts.clear();
|
|
459
|
+
streamingClosedForReply = false;
|
|
460
|
+
streamingCloseErroredForReply = false;
|
|
234
461
|
if (streamingEnabled && renderMode === "card") {
|
|
235
462
|
startStreaming();
|
|
236
463
|
}
|
|
237
|
-
|
|
464
|
+
await typingCallbacks?.onReplyStart?.();
|
|
238
465
|
},
|
|
239
466
|
deliver: async (payload: ReplyPayload, info) => {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
: [];
|
|
247
|
-
const hasText = Boolean(text.trim());
|
|
248
|
-
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)));
|
|
249
473
|
const skipTextForDuplicateFinal =
|
|
250
474
|
info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
|
251
|
-
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;
|
|
252
484
|
|
|
253
485
|
if (!shouldDeliverText && !hasMedia) {
|
|
254
486
|
return;
|
|
255
487
|
}
|
|
256
488
|
|
|
257
489
|
if (shouldDeliverText) {
|
|
258
|
-
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
259
|
-
|
|
260
490
|
if (info?.kind === "block") {
|
|
261
491
|
// Drop internal block chunks unless we can safely consume them as
|
|
262
492
|
// streaming-card fallback content.
|
|
@@ -280,100 +510,81 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
280
510
|
if (info?.kind === "block") {
|
|
281
511
|
// Some runtimes emit block payloads without onPartial/final callbacks.
|
|
282
512
|
// Mirror block text into streamText so onIdle close still sends content.
|
|
283
|
-
queueStreamingUpdate(text, { mode: "delta" });
|
|
513
|
+
queueStreamingUpdate(text, { mode: "delta", dedupeWithLastPartial: true });
|
|
284
514
|
}
|
|
285
515
|
if (info?.kind === "final") {
|
|
286
|
-
streamText =
|
|
287
|
-
|
|
288
|
-
|
|
516
|
+
streamText = text;
|
|
517
|
+
snapshotBaseText = "";
|
|
518
|
+
lastSnapshotTextLength = text.length;
|
|
519
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
289
520
|
}
|
|
290
521
|
// Send media even when streaming handled the text
|
|
291
522
|
if (hasMedia) {
|
|
292
|
-
|
|
293
|
-
await sendMediaFeishu({
|
|
294
|
-
cfg,
|
|
295
|
-
to: chatId,
|
|
296
|
-
mediaUrl,
|
|
297
|
-
replyToMessageId: sendReplyToMessageId,
|
|
298
|
-
replyInThread: effectiveReplyInThread,
|
|
299
|
-
accountId,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
523
|
+
await sendMediaReplies(payload);
|
|
302
524
|
}
|
|
303
525
|
return;
|
|
304
526
|
}
|
|
305
527
|
|
|
306
|
-
let first = true;
|
|
307
528
|
if (useCard) {
|
|
308
|
-
|
|
529
|
+
const cardHeader = resolveCardHeader(agentId, identity);
|
|
530
|
+
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
531
|
+
await sendChunkedTextReply({
|
|
309
532
|
text,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
533
|
+
useCard: true,
|
|
534
|
+
infoKind: info?.kind,
|
|
535
|
+
sendChunk: async ({ chunk, isFirst }) => {
|
|
536
|
+
await sendStructuredCardFeishu({
|
|
537
|
+
cfg,
|
|
538
|
+
to: chatId,
|
|
539
|
+
text: chunk,
|
|
540
|
+
replyToMessageId: sendReplyToMessageId,
|
|
541
|
+
replyInThread: effectiveReplyInThread,
|
|
542
|
+
mentions: isFirst ? mentionTargets : undefined,
|
|
543
|
+
accountId,
|
|
544
|
+
header: cardHeader,
|
|
545
|
+
note: cardNote,
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
});
|
|
327
549
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
if (info?.kind === "final") {
|
|
346
|
-
deliveredFinalTexts.add(text);
|
|
347
|
-
}
|
|
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
|
+
});
|
|
348
566
|
}
|
|
349
567
|
}
|
|
350
568
|
|
|
351
569
|
if (hasMedia) {
|
|
352
|
-
|
|
353
|
-
await sendMediaFeishu({
|
|
354
|
-
cfg,
|
|
355
|
-
to: chatId,
|
|
356
|
-
mediaUrl,
|
|
357
|
-
replyToMessageId: sendReplyToMessageId,
|
|
358
|
-
replyInThread: effectiveReplyInThread,
|
|
359
|
-
accountId,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
570
|
+
await sendMediaReplies(payload);
|
|
362
571
|
}
|
|
363
572
|
},
|
|
364
573
|
onError: async (error, info) => {
|
|
574
|
+
streamingCloseErroredForReply = true;
|
|
575
|
+
streamingClosedForReply = false;
|
|
365
576
|
params.runtime.error?.(
|
|
366
577
|
`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`,
|
|
367
578
|
);
|
|
368
|
-
await closeStreaming();
|
|
369
|
-
typingCallbacks
|
|
579
|
+
await closeStreaming({ markClosedForReply: false });
|
|
580
|
+
typingCallbacks?.onIdle?.();
|
|
370
581
|
},
|
|
371
582
|
onIdle: async () => {
|
|
372
583
|
await closeStreaming();
|
|
373
|
-
typingCallbacks
|
|
584
|
+
typingCallbacks?.onIdle?.();
|
|
374
585
|
},
|
|
375
586
|
onCleanup: () => {
|
|
376
|
-
typingCallbacks
|
|
587
|
+
typingCallbacks?.onCleanup?.();
|
|
377
588
|
},
|
|
378
589
|
});
|
|
379
590
|
|
|
@@ -388,12 +599,51 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
388
599
|
if (!payload.text) {
|
|
389
600
|
return;
|
|
390
601
|
}
|
|
391
|
-
|
|
602
|
+
const cleaned = stripReasoningTagsFromText(payload.text, {
|
|
603
|
+
mode: "strict",
|
|
604
|
+
trim: "both",
|
|
605
|
+
});
|
|
606
|
+
if (!cleaned) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
queueStreamingUpdate(cleaned, {
|
|
392
610
|
dedupeWithLastPartial: true,
|
|
393
611
|
mode: "snapshot",
|
|
394
612
|
});
|
|
395
613
|
}
|
|
396
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,
|
|
397
647
|
},
|
|
398
648
|
markDispatchIdle,
|
|
399
649
|
};
|