@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
|
@@ -1,5 +1,141 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
|
6
|
+
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
FeishuStreamingSession,
|
|
11
|
+
mergeStreamingText,
|
|
12
|
+
resolveStreamingCardSendMode,
|
|
13
|
+
} from "./streaming-card.js";
|
|
14
|
+
|
|
15
|
+
type StreamingSessionState = {
|
|
16
|
+
cardId: string;
|
|
17
|
+
messageId: string;
|
|
18
|
+
sequence: number;
|
|
19
|
+
currentText: string;
|
|
20
|
+
hasNote: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function setStreamingSessionInternals(
|
|
24
|
+
session: FeishuStreamingSession,
|
|
25
|
+
values: {
|
|
26
|
+
state: StreamingSessionState;
|
|
27
|
+
lastUpdateTime?: number;
|
|
28
|
+
},
|
|
29
|
+
) {
|
|
30
|
+
const internals = session as unknown as {
|
|
31
|
+
state: StreamingSessionState;
|
|
32
|
+
lastUpdateTime: number;
|
|
33
|
+
};
|
|
34
|
+
internals.state = values.state;
|
|
35
|
+
if (values.lastUpdateTime !== undefined) {
|
|
36
|
+
internals.lastUpdateTime = values.lastUpdateTime;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("FeishuStreamingSession", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.useRealTimers();
|
|
43
|
+
fetchWithSsrFGuardMock.mockReset();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function mockFetches(updateBodies: string[]) {
|
|
47
|
+
fetchWithSsrFGuardMock.mockImplementation(
|
|
48
|
+
async ({ url, init }: { url: string; init?: { body?: string } }) => {
|
|
49
|
+
const release = vi.fn(async () => {});
|
|
50
|
+
if (url.includes("/auth/")) {
|
|
51
|
+
return {
|
|
52
|
+
response: {
|
|
53
|
+
ok: true,
|
|
54
|
+
json: async () => ({
|
|
55
|
+
code: 0,
|
|
56
|
+
msg: "ok",
|
|
57
|
+
tenant_access_token: "token",
|
|
58
|
+
expire: 7200,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
release,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (url.includes("/elements/content/content")) {
|
|
65
|
+
updateBodies.push(init?.body ?? "");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
response: {
|
|
69
|
+
ok: true,
|
|
70
|
+
json: async () => ({ code: 0, msg: "ok" }),
|
|
71
|
+
},
|
|
72
|
+
release,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it("flushes throttled pending text after the throttle window", async () => {
|
|
79
|
+
vi.useFakeTimers();
|
|
80
|
+
vi.setSystemTime(1_000);
|
|
81
|
+
const updateBodies: string[] = [];
|
|
82
|
+
mockFetches(updateBodies);
|
|
83
|
+
|
|
84
|
+
const session = new FeishuStreamingSession({} as never, {
|
|
85
|
+
appId: "app_pending_flush",
|
|
86
|
+
appSecret: "secret",
|
|
87
|
+
});
|
|
88
|
+
setStreamingSessionInternals(session, {
|
|
89
|
+
state: {
|
|
90
|
+
cardId: "card_1",
|
|
91
|
+
messageId: "om_1",
|
|
92
|
+
sequence: 1,
|
|
93
|
+
currentText: "hello",
|
|
94
|
+
hasNote: false,
|
|
95
|
+
},
|
|
96
|
+
lastUpdateTime: 1_000,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await session.update("hello small");
|
|
100
|
+
expect(updateBodies).toHaveLength(0);
|
|
101
|
+
|
|
102
|
+
await vi.advanceTimersByTimeAsync(160);
|
|
103
|
+
|
|
104
|
+
expect(updateBodies).toHaveLength(1);
|
|
105
|
+
expect(JSON.parse(updateBodies[0] ?? "{}")).toMatchObject({
|
|
106
|
+
content: "hello small",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("pushes natural-boundary updates immediately inside the throttle window", async () => {
|
|
111
|
+
vi.useFakeTimers();
|
|
112
|
+
vi.setSystemTime(2_000);
|
|
113
|
+
const updateBodies: string[] = [];
|
|
114
|
+
mockFetches(updateBodies);
|
|
115
|
+
|
|
116
|
+
const session = new FeishuStreamingSession({} as never, {
|
|
117
|
+
appId: "app_boundary_flush",
|
|
118
|
+
appSecret: "secret",
|
|
119
|
+
});
|
|
120
|
+
setStreamingSessionInternals(session, {
|
|
121
|
+
state: {
|
|
122
|
+
cardId: "card_2",
|
|
123
|
+
messageId: "om_2",
|
|
124
|
+
sequence: 1,
|
|
125
|
+
currentText: "hello",
|
|
126
|
+
hasNote: false,
|
|
127
|
+
},
|
|
128
|
+
lastUpdateTime: 2_000,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await session.update("hello!");
|
|
132
|
+
|
|
133
|
+
expect(updateBodies).toHaveLength(1);
|
|
134
|
+
expect(JSON.parse(updateBodies[0] ?? "{}")).toMatchObject({
|
|
135
|
+
content: "hello!",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
3
139
|
|
|
4
140
|
describe("mergeStreamingText", () => {
|
|
5
141
|
it("prefers the latest full text when it already includes prior text", () => {
|
package/src/streaming-card.ts
CHANGED
|
@@ -3,14 +3,30 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Client } from "@larksuiteoapi/node-sdk";
|
|
6
|
-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/
|
|
6
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
7
|
+
import { getFeishuUserAgent } from "./client.js";
|
|
8
|
+
import { resolveFeishuCardTemplate, type CardHeaderConfig } from "./send.js";
|
|
7
9
|
import type { FeishuDomain } from "./types.js";
|
|
8
10
|
|
|
9
11
|
type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain };
|
|
10
|
-
type CardState = {
|
|
12
|
+
type CardState = {
|
|
13
|
+
cardId: string;
|
|
14
|
+
messageId: string;
|
|
15
|
+
sequence: number;
|
|
16
|
+
currentText: string;
|
|
17
|
+
hasNote: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Options for customising the initial streaming card appearance. */
|
|
21
|
+
type StreamingCardOptions = {
|
|
22
|
+
/** Optional header with title and color template. */
|
|
23
|
+
header?: CardHeaderConfig;
|
|
24
|
+
/** Optional grey note footer text. */
|
|
25
|
+
note?: string;
|
|
26
|
+
};
|
|
11
27
|
|
|
12
28
|
/** Optional header for streaming cards (title bar with color template) */
|
|
13
|
-
|
|
29
|
+
type StreamingCardHeader = {
|
|
14
30
|
title: string;
|
|
15
31
|
/** Color template: blue, green, red, orange, purple, indigo, wathet, turquoise, yellow, grey, carmine, violet, lime */
|
|
16
32
|
template?: string;
|
|
@@ -23,6 +39,9 @@ type StreamingStartOptions = {
|
|
|
23
39
|
header?: StreamingCardHeader;
|
|
24
40
|
};
|
|
25
41
|
|
|
42
|
+
const STREAMING_UPDATE_THROTTLE_MS = 160;
|
|
43
|
+
const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
|
|
44
|
+
|
|
26
45
|
// Token cache (keyed by domain + appId)
|
|
27
46
|
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
|
28
47
|
|
|
@@ -61,7 +80,7 @@ async function getToken(creds: Credentials): Promise<string> {
|
|
|
61
80
|
url: `${resolveApiBase(creds.domain)}/auth/v3/tenant_access_token/internal`,
|
|
62
81
|
init: {
|
|
63
82
|
method: "POST",
|
|
64
|
-
headers: { "Content-Type": "application/json" },
|
|
83
|
+
headers: { "Content-Type": "application/json", "User-Agent": getFeishuUserAgent() },
|
|
65
84
|
body: JSON.stringify({ app_id: creds.appId, app_secret: creds.appSecret }),
|
|
66
85
|
},
|
|
67
86
|
policy: { allowedHostnames: resolveAllowedHostnames(creds.domain) },
|
|
@@ -96,6 +115,20 @@ function truncateSummary(text: string, max = 50): string {
|
|
|
96
115
|
return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
|
|
97
116
|
}
|
|
98
117
|
|
|
118
|
+
function hasNaturalStreamingBoundary(text: string): boolean {
|
|
119
|
+
return /[\n。!?!?;;::]$/.test(text);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shouldPushStreamingUpdate(previousText: string, nextText: string): boolean {
|
|
123
|
+
if (!previousText) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (hasNaturalStreamingBoundary(nextText)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
export function mergeStreamingText(
|
|
100
133
|
previousText: string | undefined,
|
|
101
134
|
nextText: string | undefined,
|
|
@@ -152,7 +185,8 @@ export class FeishuStreamingSession {
|
|
|
152
185
|
private log?: (msg: string) => void;
|
|
153
186
|
private lastUpdateTime = 0;
|
|
154
187
|
private pendingText: string | null = null;
|
|
155
|
-
private
|
|
188
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
189
|
+
private updateThrottleMs = STREAMING_UPDATE_THROTTLE_MS;
|
|
156
190
|
|
|
157
191
|
constructor(client: Client, creds: Credentials, log?: (msg: string) => void) {
|
|
158
192
|
this.client = client;
|
|
@@ -163,13 +197,24 @@ export class FeishuStreamingSession {
|
|
|
163
197
|
async start(
|
|
164
198
|
receiveId: string,
|
|
165
199
|
receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
|
|
166
|
-
options?: StreamingStartOptions,
|
|
200
|
+
options?: StreamingCardOptions & StreamingStartOptions,
|
|
167
201
|
): Promise<void> {
|
|
168
202
|
if (this.state) {
|
|
169
203
|
return;
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
const apiBase = resolveApiBase(this.creds.domain);
|
|
207
|
+
const elements: Record<string, unknown>[] = [
|
|
208
|
+
{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" },
|
|
209
|
+
];
|
|
210
|
+
if (options?.note) {
|
|
211
|
+
elements.push({ tag: "hr" });
|
|
212
|
+
elements.push({
|
|
213
|
+
tag: "markdown",
|
|
214
|
+
content: `<font color='grey'>${options.note}</font>`,
|
|
215
|
+
element_id: "note",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
173
218
|
const cardJson: Record<string, unknown> = {
|
|
174
219
|
schema: "2.0",
|
|
175
220
|
config: {
|
|
@@ -177,14 +222,12 @@ export class FeishuStreamingSession {
|
|
|
177
222
|
summary: { content: "[Generating...]" },
|
|
178
223
|
streaming_config: { print_frequency_ms: { default: 50 }, print_step: { default: 1 } },
|
|
179
224
|
},
|
|
180
|
-
body: {
|
|
181
|
-
elements: [{ tag: "markdown", content: "⏳ Thinking...", element_id: "content" }],
|
|
182
|
-
},
|
|
225
|
+
body: { elements },
|
|
183
226
|
};
|
|
184
227
|
if (options?.header) {
|
|
185
228
|
cardJson.header = {
|
|
186
229
|
title: { tag: "plain_text", content: options.header.title },
|
|
187
|
-
template: options.header.template ?? "blue",
|
|
230
|
+
template: resolveFeishuCardTemplate(options.header.template) ?? "blue",
|
|
188
231
|
};
|
|
189
232
|
}
|
|
190
233
|
|
|
@@ -196,6 +239,7 @@ export class FeishuStreamingSession {
|
|
|
196
239
|
headers: {
|
|
197
240
|
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
198
241
|
"Content-Type": "application/json",
|
|
242
|
+
"User-Agent": getFeishuUserAgent(),
|
|
199
243
|
},
|
|
200
244
|
body: JSON.stringify({ type: "card_json", data: JSON.stringify(cardJson) }),
|
|
201
245
|
},
|
|
@@ -257,7 +301,13 @@ export class FeishuStreamingSession {
|
|
|
257
301
|
throw new Error(`Send card failed: ${sendRes.msg}`);
|
|
258
302
|
}
|
|
259
303
|
|
|
260
|
-
this.state = {
|
|
304
|
+
this.state = {
|
|
305
|
+
cardId,
|
|
306
|
+
messageId: sendRes.data.message_id,
|
|
307
|
+
sequence: 1,
|
|
308
|
+
currentText: "",
|
|
309
|
+
hasNote: !!options?.note,
|
|
310
|
+
};
|
|
261
311
|
this.log?.(`Started streaming: cardId=${cardId}, messageId=${sendRes.data.message_id}`);
|
|
262
312
|
}
|
|
263
313
|
|
|
@@ -274,6 +324,7 @@ export class FeishuStreamingSession {
|
|
|
274
324
|
headers: {
|
|
275
325
|
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
276
326
|
"Content-Type": "application/json",
|
|
327
|
+
"User-Agent": getFeishuUserAgent(),
|
|
277
328
|
},
|
|
278
329
|
body: JSON.stringify({
|
|
279
330
|
content: text,
|
|
@@ -290,6 +341,28 @@ export class FeishuStreamingSession {
|
|
|
290
341
|
.catch((error) => onError?.(error));
|
|
291
342
|
}
|
|
292
343
|
|
|
344
|
+
private clearFlushTimer(): void {
|
|
345
|
+
if (this.flushTimer) {
|
|
346
|
+
clearTimeout(this.flushTimer);
|
|
347
|
+
this.flushTimer = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private schedulePendingFlush(): void {
|
|
352
|
+
if (this.flushTimer || !this.pendingText || this.closed) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const delayMs = Math.max(0, this.updateThrottleMs - (Date.now() - this.lastUpdateTime));
|
|
356
|
+
this.flushTimer = setTimeout(() => {
|
|
357
|
+
this.flushTimer = null;
|
|
358
|
+
const pending = this.pendingText;
|
|
359
|
+
if (!pending || this.closed) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
void this.update(pending);
|
|
363
|
+
}, delayMs);
|
|
364
|
+
}
|
|
365
|
+
|
|
293
366
|
async update(text: string): Promise<void> {
|
|
294
367
|
if (!this.state || this.closed) {
|
|
295
368
|
return;
|
|
@@ -298,35 +371,69 @@ export class FeishuStreamingSession {
|
|
|
298
371
|
if (!mergedInput || mergedInput === this.state.currentText) {
|
|
299
372
|
return;
|
|
300
373
|
}
|
|
374
|
+
this.pendingText = mergedInput;
|
|
375
|
+
this.clearFlushTimer();
|
|
301
376
|
|
|
302
|
-
|
|
377
|
+
const shouldForceUpdate = shouldPushStreamingUpdate(this.state.currentText, mergedInput);
|
|
303
378
|
const now = Date.now();
|
|
304
|
-
if (now - this.lastUpdateTime < this.updateThrottleMs) {
|
|
305
|
-
this.
|
|
379
|
+
if (!shouldForceUpdate && now - this.lastUpdateTime < this.updateThrottleMs) {
|
|
380
|
+
this.schedulePendingFlush();
|
|
306
381
|
return;
|
|
307
382
|
}
|
|
308
|
-
this.pendingText = null;
|
|
309
383
|
this.lastUpdateTime = now;
|
|
310
384
|
|
|
311
385
|
this.queue = this.queue.then(async () => {
|
|
312
386
|
if (!this.state || this.closed) {
|
|
313
387
|
return;
|
|
314
388
|
}
|
|
315
|
-
const
|
|
389
|
+
const nextText = this.pendingText ?? mergedInput;
|
|
390
|
+
const mergedText = mergeStreamingText(this.state.currentText, nextText);
|
|
316
391
|
if (!mergedText || mergedText === this.state.currentText) {
|
|
317
392
|
return;
|
|
318
393
|
}
|
|
394
|
+
this.pendingText = null;
|
|
319
395
|
this.state.currentText = mergedText;
|
|
320
396
|
await this.updateCardContent(mergedText, (e) => this.log?.(`Update failed: ${String(e)}`));
|
|
321
397
|
});
|
|
322
398
|
await this.queue;
|
|
323
399
|
}
|
|
324
400
|
|
|
325
|
-
async
|
|
401
|
+
private async updateNoteContent(note: string): Promise<void> {
|
|
402
|
+
if (!this.state || !this.state.hasNote) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const apiBase = resolveApiBase(this.creds.domain);
|
|
406
|
+
this.state.sequence += 1;
|
|
407
|
+
await fetchWithSsrFGuard({
|
|
408
|
+
url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/elements/note/content`,
|
|
409
|
+
init: {
|
|
410
|
+
method: "PUT",
|
|
411
|
+
headers: {
|
|
412
|
+
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
413
|
+
"Content-Type": "application/json",
|
|
414
|
+
"User-Agent": getFeishuUserAgent(),
|
|
415
|
+
},
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
content: `<font color='grey'>${note}</font>`,
|
|
418
|
+
sequence: this.state.sequence,
|
|
419
|
+
uuid: `n_${this.state.cardId}_${this.state.sequence}`,
|
|
420
|
+
}),
|
|
421
|
+
},
|
|
422
|
+
policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
|
|
423
|
+
auditContext: "feishu.streaming-card.note-update",
|
|
424
|
+
})
|
|
425
|
+
.then(async ({ release }) => {
|
|
426
|
+
await release();
|
|
427
|
+
})
|
|
428
|
+
.catch((e) => this.log?.(`Note update failed: ${String(e)}`));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async close(finalText?: string, options?: { note?: string }): Promise<void> {
|
|
326
432
|
if (!this.state || this.closed) {
|
|
327
433
|
return;
|
|
328
434
|
}
|
|
329
435
|
this.closed = true;
|
|
436
|
+
this.clearFlushTimer();
|
|
330
437
|
await this.queue;
|
|
331
438
|
|
|
332
439
|
const pendingMerged = mergeStreamingText(this.state.currentText, this.pendingText ?? undefined);
|
|
@@ -339,6 +446,11 @@ export class FeishuStreamingSession {
|
|
|
339
446
|
this.state.currentText = text;
|
|
340
447
|
}
|
|
341
448
|
|
|
449
|
+
// Update note with final model/provider info
|
|
450
|
+
if (options?.note) {
|
|
451
|
+
await this.updateNoteContent(options.note);
|
|
452
|
+
}
|
|
453
|
+
|
|
342
454
|
// Close streaming mode
|
|
343
455
|
this.state.sequence += 1;
|
|
344
456
|
await fetchWithSsrFGuard({
|
|
@@ -348,6 +460,7 @@ export class FeishuStreamingSession {
|
|
|
348
460
|
headers: {
|
|
349
461
|
Authorization: `Bearer ${await getToken(this.creds)}`,
|
|
350
462
|
"Content-Type": "application/json; charset=utf-8",
|
|
463
|
+
"User-Agent": getFeishuUserAgent(),
|
|
351
464
|
},
|
|
352
465
|
body: JSON.stringify({
|
|
353
466
|
settings: JSON.stringify({
|
|
@@ -364,8 +477,11 @@ export class FeishuStreamingSession {
|
|
|
364
477
|
await release();
|
|
365
478
|
})
|
|
366
479
|
.catch((e) => this.log?.(`Close failed: ${String(e)}`));
|
|
480
|
+
const finalState = this.state;
|
|
481
|
+
this.state = null;
|
|
482
|
+
this.pendingText = null;
|
|
367
483
|
|
|
368
|
-
this.log?.(`Closed streaming: cardId=${
|
|
484
|
+
this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
|
|
369
485
|
}
|
|
370
486
|
|
|
371
487
|
isActive(): boolean {
|