@soimy/dingtalk 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -23
- package/index.ts +7 -0
- package/openclaw.plugin.json +799 -0
- package/package.json +5 -5
- package/src/card/card-markdown-image-reroute.ts +106 -0
- package/src/card/card-run-registry.ts +54 -1
- package/src/card/card-stop-handler.ts +10 -20
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/card-template.ts +14 -3
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +326 -54
- package/src/card-service.ts +479 -8
- package/src/channel.ts +19 -1062
- package/src/config-schema.ts +81 -38
- package/src/config.ts +142 -4
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +636 -0
- package/src/inbound-handler.ts +489 -49
- package/src/media-utils.ts +169 -7
- package/src/message-utils.ts +153 -17
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +173 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/messaging/quoted-file-service.ts +9 -4
- package/src/onboarding.ts +323 -205
- package/src/platform/channel-status.ts +81 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +568 -44
- package/src/reply-strategy-markdown.ts +2 -2
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -56
- package/src/run-usage-store.ts +59 -0
- package/src/send-service.ts +225 -7
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/targeting/agent-routing.ts +44 -28
- package/src/types.ts +49 -117
- package/src/utils.ts +25 -0
|
@@ -4,33 +4,54 @@
|
|
|
4
4
|
* The controller keeps a single rendered card timeline made of:
|
|
5
5
|
* - sealed process blocks (`thinking` / `tool`)
|
|
6
6
|
* - an optional live thinking block
|
|
7
|
-
* - accumulated answer turns rendered as
|
|
7
|
+
* - accumulated answer turns rendered as JSON CardBlock[]
|
|
8
8
|
*
|
|
9
9
|
* It delegates throttling and single-flight transport guarantees to
|
|
10
10
|
* {@link createDraftStreamLoop}.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
clearAICardStreamingContent,
|
|
15
|
+
streamAICardContent,
|
|
16
|
+
updateAICardBlockList,
|
|
17
|
+
} from "./card-service";
|
|
14
18
|
import { createDraftStreamLoop } from "./draft-stream-loop";
|
|
15
|
-
import type { AICardInstance, Logger } from "./types";
|
|
19
|
+
import type { AICardInstance, CardBlock, Logger } from "./types";
|
|
16
20
|
|
|
17
|
-
type TimelineEntryKind = "thinking" | "tool" | "answer";
|
|
21
|
+
type TimelineEntryKind = "thinking" | "tool" | "answer" | "image";
|
|
18
22
|
|
|
19
23
|
type TimelineEntry = {
|
|
20
24
|
kind: TimelineEntryKind;
|
|
21
25
|
text: string;
|
|
26
|
+
mediaId?: string;
|
|
22
27
|
};
|
|
23
28
|
|
|
29
|
+
// DingTalk markdown variable token definitions:
|
|
30
|
+
// https://open.dingtalk.com/document/development/markdown-variable-new
|
|
31
|
+
const PROCESS_BLOCK_FONT_SIZE_TOKEN = "common_footnote_text_style__font_size";
|
|
32
|
+
// DingTalk markdown variable token definitions:
|
|
33
|
+
// https://open.dingtalk.com/document/development/markdown-variable-new
|
|
34
|
+
const PROCESS_BLOCK_FONT_COLOR_TOKEN_V2 = "common_level2_base_color";
|
|
35
|
+
|
|
24
36
|
export interface CardDraftController {
|
|
25
|
-
updateAnswer: (text: string) => Promise<void>;
|
|
37
|
+
updateAnswer: (text: string, options?: { stream?: boolean; renderBlocks?: boolean }) => Promise<void>;
|
|
26
38
|
updateReasoning: (text: string) => Promise<void>;
|
|
27
39
|
updateThinking: (text: string) => Promise<void>;
|
|
28
40
|
appendThinkingBlock: (text: string) => Promise<void>;
|
|
29
41
|
updateTool: (text: string) => Promise<void>;
|
|
30
42
|
appendTool: (text: string) => Promise<void>;
|
|
43
|
+
/** Append an image block (type=3) with an uploaded mediaId. */
|
|
44
|
+
appendImageBlock: (mediaId: string, text?: string) => Promise<void>;
|
|
45
|
+
appendToolBeforeCurrentAnswer: (text: string) => Promise<void>;
|
|
46
|
+
/** Drop the current answer draft while keeping sealed earlier turns intact. */
|
|
47
|
+
discardCurrentAnswer: () => void;
|
|
31
48
|
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
32
|
-
notifyNewAssistantTurn: (
|
|
49
|
+
notifyNewAssistantTurn: (options?: {
|
|
50
|
+
discardActiveAnswer?: boolean;
|
|
51
|
+
}) => Promise<void>;
|
|
33
52
|
startAssistantTurn: () => Promise<void>;
|
|
53
|
+
/** Seal the active thinking entry (keep it in timeline) without removing it. */
|
|
54
|
+
sealActiveThinking: () => Promise<void>;
|
|
34
55
|
flush: () => Promise<void>;
|
|
35
56
|
waitForInFlight: () => Promise<void>;
|
|
36
57
|
stop: () => void;
|
|
@@ -41,12 +62,24 @@ export interface CardDraftController {
|
|
|
41
62
|
getLastAnswerContent: () => string;
|
|
42
63
|
/** Current answer-only content composed from all completed answer turns. */
|
|
43
64
|
getFinalAnswerContent: () => string;
|
|
44
|
-
/** Current rendered timeline
|
|
65
|
+
/** Current rendered timeline as CardBlock[] JSON string for blockList parameter. */
|
|
66
|
+
getRenderedBlocks: (options?: {
|
|
67
|
+
fallbackAnswer?: string;
|
|
68
|
+
overrideAnswer?: string;
|
|
69
|
+
compactProcessAnswerSpacing?: boolean;
|
|
70
|
+
}) => string;
|
|
71
|
+
/** Current rendered timeline as pure markdown text for content parameter and fallback. */
|
|
45
72
|
getRenderedContent: (options?: {
|
|
46
73
|
fallbackAnswer?: string;
|
|
47
74
|
overrideAnswer?: string;
|
|
48
75
|
compactProcessAnswerSpacing?: boolean;
|
|
49
76
|
}) => string;
|
|
77
|
+
/** Stream answer text to content key for real-time display. Only available when realTimeStreamEnabled=true. */
|
|
78
|
+
streamContent?: (text: string) => Promise<void>;
|
|
79
|
+
/** Clear the streaming content key. Only available when realTimeStreamEnabled=true. */
|
|
80
|
+
clearStreamingContent?: () => Promise<void>;
|
|
81
|
+
/** Whether real-time streaming is enabled. */
|
|
82
|
+
isRealTimeStreamEnabled: () => boolean;
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
function normalizeProcessText(text: string | undefined): string {
|
|
@@ -57,28 +90,33 @@ function normalizeAnswerText(text: string | undefined): string {
|
|
|
57
90
|
return typeof text === "string" ? text.trimStart() : "";
|
|
58
91
|
}
|
|
59
92
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.map((line) =>
|
|
93
|
+
function wrapProcessBlockMarkdown(text: string): string {
|
|
94
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
95
|
+
return lines
|
|
96
|
+
.map((line) => `> <font sizeToken=${PROCESS_BLOCK_FONT_SIZE_TOKEN} colorTokenV2=${PROCESS_BLOCK_FONT_COLOR_TOKEN_V2}>${line}</font>`)
|
|
64
97
|
.join("\n");
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
function renderProcessBlock(_kind: "thinking" | "tool", text: string): string {
|
|
68
|
-
return quoteMarkdown(text);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
100
|
export function createCardDraftController(params: {
|
|
72
101
|
card: AICardInstance;
|
|
73
102
|
throttleMs?: number;
|
|
74
103
|
/** Legacy compatibility: verbose mode previously lowered the throttle. */
|
|
75
104
|
verboseMode?: boolean;
|
|
105
|
+
/** Enable real-time streaming to content key for answer display. */
|
|
106
|
+
realTimeStreamEnabled?: boolean;
|
|
107
|
+
/** Optional callback to get the current statusLine for piggy-backing on blockList updates. */
|
|
108
|
+
getStatusLine?: () => string | undefined;
|
|
76
109
|
log?: Logger;
|
|
77
110
|
}): CardDraftController {
|
|
78
111
|
let failed = false;
|
|
79
112
|
let stopped = false;
|
|
80
113
|
let lastSentContent = "";
|
|
114
|
+
let lastQueuedContent = "";
|
|
115
|
+
let inFlightContent = "";
|
|
81
116
|
let lastAnswerContent = "";
|
|
117
|
+
let lastSentStreamingContent = "";
|
|
118
|
+
let lastQueuedStreamingContent = "";
|
|
119
|
+
let inFlightStreamingContent = "";
|
|
82
120
|
|
|
83
121
|
let timelineEntries: TimelineEntry[] = [];
|
|
84
122
|
let activeThinkingIndex: number | null = null;
|
|
@@ -86,6 +124,52 @@ export function createCardDraftController(params: {
|
|
|
86
124
|
let pendingBoundaryPromise: Promise<void> | null = null;
|
|
87
125
|
|
|
88
126
|
const effectiveThrottleMs = params.throttleMs ?? (params.verboseMode ? 50 : 300);
|
|
127
|
+
const realTimeStreamEnabled = params.realTimeStreamEnabled ?? false;
|
|
128
|
+
let hasStreamingContent = false;
|
|
129
|
+
|
|
130
|
+
const clearPendingStreamingContent = () => {
|
|
131
|
+
contentLoop.resetPending();
|
|
132
|
+
lastQueuedStreamingContent = "";
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const streamContentToCard = async (text: string) => {
|
|
136
|
+
if (!realTimeStreamEnabled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const normalized = normalizeAnswerText(text);
|
|
140
|
+
if (!normalized.trim()) {
|
|
141
|
+
clearPendingStreamingContent();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (normalized === lastSentStreamingContent) {
|
|
145
|
+
const hasNewerInFlight = !!inFlightStreamingContent && inFlightStreamingContent !== normalized;
|
|
146
|
+
if (!hasNewerInFlight) {
|
|
147
|
+
clearPendingStreamingContent();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (normalized === lastQueuedStreamingContent) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
lastQueuedStreamingContent = normalized;
|
|
155
|
+
contentLoop.update(normalized);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const clearStreamingContentFromCard = async () => {
|
|
159
|
+
clearPendingStreamingContent();
|
|
160
|
+
await contentLoop.waitForInFlight();
|
|
161
|
+
if (!realTimeStreamEnabled || !hasStreamingContent) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await clearAICardStreamingContent(params.card, params.log);
|
|
166
|
+
hasStreamingContent = false;
|
|
167
|
+
lastSentStreamingContent = "";
|
|
168
|
+
} catch (err: unknown) {
|
|
169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
170
|
+
params.log?.debug?.(`[DingTalk][AICard] Failed to clear streaming content: ${message}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
89
173
|
|
|
90
174
|
const getFinalAnswerContent = (): string => {
|
|
91
175
|
return timelineEntries
|
|
@@ -121,13 +205,30 @@ export function createCardDraftController(params: {
|
|
|
121
205
|
return activeAnswerIndex;
|
|
122
206
|
};
|
|
123
207
|
|
|
124
|
-
const
|
|
208
|
+
const findLastAnswerEntryIndex = (): number | null => {
|
|
209
|
+
for (let index = timelineEntries.length - 1; index >= 0; index -= 1) {
|
|
210
|
+
if (timelineEntries[index]?.kind === "answer") {
|
|
211
|
+
return index;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const renderTimelineAsBlocks = (options: {
|
|
125
218
|
fallbackAnswer?: string;
|
|
126
219
|
overrideAnswer?: string;
|
|
127
|
-
|
|
128
|
-
} = {}): string => {
|
|
220
|
+
} = {}): CardBlock[] => {
|
|
129
221
|
const entries = timelineEntries.map((entry) => ({ ...entry }));
|
|
130
222
|
|
|
223
|
+
const insertAnswerEntry = (text: string) => {
|
|
224
|
+
const firstImageIndex = entries.findIndex((entry) => entry.kind === "image");
|
|
225
|
+
if (firstImageIndex >= 0) {
|
|
226
|
+
entries.splice(firstImageIndex, 0, { kind: "answer", text });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
entries.push({ kind: "answer", text });
|
|
230
|
+
};
|
|
231
|
+
|
|
131
232
|
const overrideAnswer = normalizeAnswerText(options.overrideAnswer);
|
|
132
233
|
if (overrideAnswer) {
|
|
133
234
|
const lastAnswerIndex = [...entries]
|
|
@@ -137,38 +238,46 @@ export function createCardDraftController(params: {
|
|
|
137
238
|
if (lastAnswerIndex !== undefined) {
|
|
138
239
|
entries[lastAnswerIndex] = { kind: "answer", text: overrideAnswer };
|
|
139
240
|
} else {
|
|
140
|
-
|
|
241
|
+
insertAnswerEntry(overrideAnswer);
|
|
141
242
|
}
|
|
142
243
|
} else if (!entries.some((entry) => entry.kind === "answer" && entry.text)) {
|
|
143
244
|
const fallbackAnswer = normalizeAnswerText(options.fallbackAnswer);
|
|
144
245
|
if (fallbackAnswer) {
|
|
145
|
-
|
|
246
|
+
insertAnswerEntry(fallbackAnswer);
|
|
146
247
|
}
|
|
147
248
|
}
|
|
148
249
|
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
250
|
+
const blocks: CardBlock[] = [];
|
|
251
|
+
for (const entry of entries) {
|
|
252
|
+
if (!entry) { continue; }
|
|
253
|
+
switch (entry.kind) {
|
|
254
|
+
case "answer":
|
|
255
|
+
if (entry.text?.trim()) {
|
|
256
|
+
blocks.push({ type: 0, markdown: entry.text });
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "thinking":
|
|
260
|
+
if (entry.text?.trim()) {
|
|
261
|
+
blocks.push({ type: 1, markdown: wrapProcessBlockMarkdown(entry.text) });
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case "tool":
|
|
265
|
+
if (entry.text?.trim()) {
|
|
266
|
+
blocks.push({ type: 2, markdown: wrapProcessBlockMarkdown(entry.text) });
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case "image":
|
|
270
|
+
if (entry.mediaId?.trim()) {
|
|
271
|
+
blocks.push({
|
|
272
|
+
type: 3,
|
|
273
|
+
mediaId: entry.mediaId,
|
|
274
|
+
...(entry.text?.trim() ? { text: entry.text } : {}),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
162
278
|
}
|
|
163
|
-
const previousKind = entries[index - 1]?.kind;
|
|
164
|
-
const separator =
|
|
165
|
-
compactProcessAnswerSpacing && previousKind
|
|
166
|
-
? "\n"
|
|
167
|
-
: "\n\n";
|
|
168
|
-
rendered += `${separator}${part}`;
|
|
169
279
|
}
|
|
170
|
-
|
|
171
|
-
return rendered;
|
|
280
|
+
return blocks;
|
|
172
281
|
};
|
|
173
282
|
|
|
174
283
|
const sealLiveThinking = () => {
|
|
@@ -179,21 +288,55 @@ export function createCardDraftController(params: {
|
|
|
179
288
|
activeAnswerIndex = null;
|
|
180
289
|
};
|
|
181
290
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (rendered) {
|
|
185
|
-
loop.update(rendered);
|
|
291
|
+
const discardCurrentAnswer = () => {
|
|
292
|
+
if (activeAnswerIndex === null) {
|
|
186
293
|
return;
|
|
187
294
|
}
|
|
295
|
+
removeTimelineEntry(activeAnswerIndex);
|
|
296
|
+
queueRender();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const clearPendingRender = () => {
|
|
188
300
|
loop.resetPending();
|
|
301
|
+
lastQueuedContent = "";
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const queueRender = () => {
|
|
305
|
+
const blocks = renderTimelineAsBlocks();
|
|
306
|
+
const rendered = JSON.stringify(blocks);
|
|
307
|
+
|
|
308
|
+
// Always update blockList via instances API (throttled)
|
|
309
|
+
if (blocks.length === 0) {
|
|
310
|
+
clearPendingRender();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (rendered === lastSentContent) {
|
|
314
|
+
const hasNewerInFlight = !!inFlightContent && inFlightContent !== rendered;
|
|
315
|
+
if (!hasNewerInFlight) {
|
|
316
|
+
clearPendingRender();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (rendered === lastQueuedContent) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
lastQueuedContent = rendered;
|
|
324
|
+
loop.update(rendered);
|
|
189
325
|
};
|
|
190
326
|
|
|
191
327
|
const flushBoundaryFrame = async () => {
|
|
192
328
|
if (stopped || failed) {
|
|
193
329
|
return;
|
|
194
330
|
}
|
|
331
|
+
await contentLoop.flush();
|
|
332
|
+
await contentLoop.waitForInFlight();
|
|
333
|
+
// Clear streaming content before committing blockList at boundary
|
|
334
|
+
if (hasStreamingContent) {
|
|
335
|
+
await clearStreamingContentFromCard();
|
|
336
|
+
}
|
|
195
337
|
await loop.flush();
|
|
196
338
|
await loop.waitForInFlight();
|
|
339
|
+
contentLoop.resetThrottleWindow();
|
|
197
340
|
loop.resetThrottleWindow();
|
|
198
341
|
};
|
|
199
342
|
|
|
@@ -220,14 +363,44 @@ export function createCardDraftController(params: {
|
|
|
220
363
|
throttleMs: effectiveThrottleMs,
|
|
221
364
|
isStopped: () => stopped || failed,
|
|
222
365
|
sendOrEditStreamMessage: async (content: string) => {
|
|
366
|
+
inFlightContent = content;
|
|
223
367
|
try {
|
|
224
|
-
|
|
368
|
+
// Use instances API for blockList (not streaming API)
|
|
369
|
+
const statusLine = params.getStatusLine?.();
|
|
370
|
+
await updateAICardBlockList(params.card, content, params.log, statusLine ? { statusLine } : undefined);
|
|
225
371
|
lastSentContent = content;
|
|
372
|
+
lastQueuedContent = "";
|
|
226
373
|
lastAnswerContent = getFinalAnswerContent();
|
|
227
374
|
} catch (err: unknown) {
|
|
228
375
|
failed = true;
|
|
229
376
|
const message = err instanceof Error ? err.message : String(err);
|
|
230
|
-
params.log?.warn?.(`[DingTalk][AICard]
|
|
377
|
+
params.log?.warn?.(`[DingTalk][AICard] BlockList update failed: ${message}`);
|
|
378
|
+
} finally {
|
|
379
|
+
if (inFlightContent === content) {
|
|
380
|
+
inFlightContent = "";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const contentLoop = createDraftStreamLoop({
|
|
387
|
+
throttleMs: effectiveThrottleMs,
|
|
388
|
+
isStopped: () => stopped || failed,
|
|
389
|
+
sendOrEditStreamMessage: async (content: string) => {
|
|
390
|
+
inFlightStreamingContent = content;
|
|
391
|
+
try {
|
|
392
|
+
await streamAICardContent(params.card, content, params.log);
|
|
393
|
+
hasStreamingContent = true;
|
|
394
|
+
lastSentStreamingContent = content;
|
|
395
|
+
lastQueuedStreamingContent = "";
|
|
396
|
+
lastAnswerContent = content;
|
|
397
|
+
} catch (err: unknown) {
|
|
398
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
399
|
+
params.log?.debug?.(`[DingTalk][AICard] Failed to stream content: ${message}`);
|
|
400
|
+
} finally {
|
|
401
|
+
if (inFlightStreamingContent === content) {
|
|
402
|
+
inFlightStreamingContent = "";
|
|
403
|
+
}
|
|
231
404
|
}
|
|
232
405
|
},
|
|
233
406
|
});
|
|
@@ -255,7 +428,7 @@ export function createCardDraftController(params: {
|
|
|
255
428
|
queueRender();
|
|
256
429
|
};
|
|
257
430
|
|
|
258
|
-
const updateAnswer = async (text: string) => {
|
|
431
|
+
const updateAnswer = async (text: string, options: { stream?: boolean; renderBlocks?: boolean } = {}) => {
|
|
259
432
|
await waitForPendingBoundary();
|
|
260
433
|
if (stopped || failed) {
|
|
261
434
|
return;
|
|
@@ -276,6 +449,16 @@ export function createCardDraftController(params: {
|
|
|
276
449
|
} else {
|
|
277
450
|
activeAnswerIndex = appendTimelineEntry("answer", normalized);
|
|
278
451
|
}
|
|
452
|
+
if (options.stream !== false) {
|
|
453
|
+
await streamContentToCard(normalized);
|
|
454
|
+
} else {
|
|
455
|
+
clearPendingStreamingContent();
|
|
456
|
+
}
|
|
457
|
+
const shouldRenderBlocks = options.renderBlocks ?? (options.stream !== false);
|
|
458
|
+
if (!shouldRenderBlocks) {
|
|
459
|
+
clearPendingRender();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
279
462
|
queueRender();
|
|
280
463
|
};
|
|
281
464
|
|
|
@@ -323,21 +506,73 @@ export function createCardDraftController(params: {
|
|
|
323
506
|
queueRender();
|
|
324
507
|
};
|
|
325
508
|
|
|
326
|
-
const
|
|
509
|
+
const appendToolBeforeCurrentAnswer = async (text: string) => {
|
|
510
|
+
await waitForPendingBoundary();
|
|
327
511
|
if (stopped || failed) {
|
|
328
512
|
return;
|
|
329
513
|
}
|
|
330
|
-
|
|
514
|
+
const normalized = normalizeProcessText(text);
|
|
515
|
+
if (!normalized) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (timelineEntries.length > 0) {
|
|
519
|
+
await flushBoundaryFrame();
|
|
520
|
+
}
|
|
521
|
+
sealLiveThinking();
|
|
522
|
+
const insertionIndex = findCurrentSegmentAnswerIndex() ?? findLastAnswerEntryIndex();
|
|
523
|
+
if (insertionIndex !== null) {
|
|
524
|
+
timelineEntries.splice(insertionIndex, 0, { kind: "tool", text: normalized });
|
|
525
|
+
if (activeAnswerIndex !== null && activeAnswerIndex >= insertionIndex) {
|
|
526
|
+
activeAnswerIndex += 1;
|
|
527
|
+
}
|
|
528
|
+
if (activeThinkingIndex !== null && activeThinkingIndex >= insertionIndex) {
|
|
529
|
+
activeThinkingIndex += 1;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
331
532
|
sealCurrentAnswer();
|
|
533
|
+
appendTimelineEntry("tool", normalized);
|
|
534
|
+
}
|
|
535
|
+
queueRender();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const notifyNewAssistantTurn = async (options: {
|
|
539
|
+
discardActiveAnswer?: boolean;
|
|
540
|
+
} = {}) => {
|
|
541
|
+
if (stopped || failed) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (activeAnswerIndex !== null) {
|
|
545
|
+
if (options.discardActiveAnswer) {
|
|
546
|
+
discardCurrentAnswer();
|
|
547
|
+
} else {
|
|
548
|
+
sealCurrentAnswer();
|
|
549
|
+
}
|
|
332
550
|
await beginBoundaryFlush();
|
|
333
551
|
return;
|
|
334
552
|
}
|
|
335
553
|
if (activeThinkingIndex !== null) {
|
|
336
554
|
removeTimelineEntry(activeThinkingIndex);
|
|
337
|
-
|
|
555
|
+
clearPendingRender();
|
|
338
556
|
}
|
|
339
557
|
};
|
|
340
558
|
|
|
559
|
+
const appendImageBlock = async (mediaId: string, text = "") => {
|
|
560
|
+
await waitForPendingBoundary();
|
|
561
|
+
if (stopped || failed) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (!mediaId.trim()) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (timelineEntries.length > 0) {
|
|
568
|
+
await flushBoundaryFrame();
|
|
569
|
+
}
|
|
570
|
+
sealLiveThinking();
|
|
571
|
+
sealCurrentAnswer();
|
|
572
|
+
timelineEntries.push({ kind: "image", text, mediaId });
|
|
573
|
+
queueRender();
|
|
574
|
+
};
|
|
575
|
+
|
|
341
576
|
return {
|
|
342
577
|
updateAnswer,
|
|
343
578
|
updateReasoning,
|
|
@@ -345,13 +580,32 @@ export function createCardDraftController(params: {
|
|
|
345
580
|
appendThinkingBlock,
|
|
346
581
|
updateTool,
|
|
347
582
|
appendTool: updateTool,
|
|
583
|
+
appendImageBlock,
|
|
584
|
+
appendToolBeforeCurrentAnswer,
|
|
585
|
+
discardCurrentAnswer,
|
|
348
586
|
notifyNewAssistantTurn,
|
|
349
587
|
startAssistantTurn: notifyNewAssistantTurn,
|
|
350
|
-
|
|
351
|
-
|
|
588
|
+
sealActiveThinking: async () => {
|
|
589
|
+
if (stopped || failed) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (activeThinkingIndex !== null) {
|
|
593
|
+
sealLiveThinking();
|
|
594
|
+
await beginBoundaryFlush();
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
flush: async () => {
|
|
598
|
+
await contentLoop.flush();
|
|
599
|
+
await loop.flush();
|
|
600
|
+
},
|
|
601
|
+
waitForInFlight: async () => {
|
|
602
|
+
await contentLoop.waitForInFlight();
|
|
603
|
+
await loop.waitForInFlight();
|
|
604
|
+
},
|
|
352
605
|
|
|
353
606
|
stop: () => {
|
|
354
607
|
stopped = true;
|
|
608
|
+
contentLoop.stop();
|
|
355
609
|
loop.stop();
|
|
356
610
|
},
|
|
357
611
|
|
|
@@ -359,6 +613,24 @@ export function createCardDraftController(params: {
|
|
|
359
613
|
getLastContent: () => lastSentContent,
|
|
360
614
|
getLastAnswerContent: () => lastAnswerContent,
|
|
361
615
|
getFinalAnswerContent,
|
|
362
|
-
|
|
616
|
+
getRenderedBlocks: (options?: { fallbackAnswer?: string; overrideAnswer?: string }) => {
|
|
617
|
+
const blocks = renderTimelineAsBlocks(options);
|
|
618
|
+
if (blocks.length === 0) {
|
|
619
|
+
return "";
|
|
620
|
+
}
|
|
621
|
+
return JSON.stringify(blocks);
|
|
622
|
+
},
|
|
623
|
+
getRenderedContent: (options?: { fallbackAnswer?: string; overrideAnswer?: string; compactProcessAnswerSpacing?: boolean }) => {
|
|
624
|
+
const blocks = renderTimelineAsBlocks(options);
|
|
625
|
+
// Extract markdown from answer blocks (type: 0) and join with double newlines
|
|
626
|
+
const answerTexts = blocks
|
|
627
|
+
.filter((block) => block.type === 0 && "markdown" in block && block.markdown)
|
|
628
|
+
.map((block) => ("markdown" in block ? block.markdown : ""));
|
|
629
|
+
return answerTexts.join("\n\n");
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
streamContent: realTimeStreamEnabled ? streamContentToCard : undefined,
|
|
633
|
+
clearStreamingContent: realTimeStreamEnabled ? clearStreamingContentFromCard : undefined,
|
|
634
|
+
isRealTimeStreamEnabled: () => realTimeStreamEnabled,
|
|
363
635
|
};
|
|
364
636
|
}
|