@soimy/dingtalk 3.5.3 → 3.6.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/README.md +4 -1
- package/index.ts +7 -0
- package/openclaw.plugin.json +153 -5
- package/package.json +1 -1
- package/src/auth.ts +5 -2
- 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-template.ts +14 -3
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +245 -52
- package/src/card-service.ts +408 -23
- package/src/channel.ts +24 -1083
- package/src/config-schema.ts +21 -1
- package/src/config.ts +139 -66
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +637 -0
- package/src/inbound-handler.ts +1276 -975
- package/src/media-utils.ts +6 -0
- package/src/message-context-store.ts +183 -85
- package/src/message-utils.ts +124 -16
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +174 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/onboarding.ts +333 -235
- package/src/path-utils.ts +49 -0
- package/src/platform/channel-status.ts +81 -0
- package/src/reply-strategy-card.ts +373 -64
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -72
- package/src/run-usage-store.ts +59 -0
- package/src/secret-input.ts +216 -0
- package/src/send-service.ts +115 -3
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/targeting/agent-routing.ts +30 -5
- package/src/types.ts +48 -157
|
@@ -4,33 +4,51 @@
|
|
|
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, options?: { stream?: boolean }) => 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>;
|
|
31
45
|
appendToolBeforeCurrentAnswer: (text: string) => Promise<void>;
|
|
46
|
+
/** Drop the current answer draft while keeping sealed earlier turns intact. */
|
|
47
|
+
discardCurrentAnswer: () => void;
|
|
32
48
|
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
33
|
-
notifyNewAssistantTurn: (
|
|
49
|
+
notifyNewAssistantTurn: (options?: {
|
|
50
|
+
discardActiveAnswer?: boolean;
|
|
51
|
+
}) => Promise<void>;
|
|
34
52
|
startAssistantTurn: () => Promise<void>;
|
|
35
53
|
/** Seal the active thinking entry (keep it in timeline) without removing it. */
|
|
36
54
|
sealActiveThinking: () => Promise<void>;
|
|
@@ -44,12 +62,24 @@ export interface CardDraftController {
|
|
|
44
62
|
getLastAnswerContent: () => string;
|
|
45
63
|
/** Current answer-only content composed from all completed answer turns. */
|
|
46
64
|
getFinalAnswerContent: () => string;
|
|
47
|
-
/** 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. */
|
|
48
72
|
getRenderedContent: (options?: {
|
|
49
73
|
fallbackAnswer?: string;
|
|
50
74
|
overrideAnswer?: string;
|
|
51
75
|
compactProcessAnswerSpacing?: boolean;
|
|
52
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;
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
function normalizeProcessText(text: string | undefined): string {
|
|
@@ -60,22 +90,22 @@ function normalizeAnswerText(text: string | undefined): string {
|
|
|
60
90
|
return typeof text === "string" ? text.trimStart() : "";
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.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>`)
|
|
67
97
|
.join("\n");
|
|
68
98
|
}
|
|
69
99
|
|
|
70
|
-
function renderProcessBlock(_kind: "thinking" | "tool", text: string): string {
|
|
71
|
-
return quoteMarkdown(text);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
100
|
export function createCardDraftController(params: {
|
|
75
101
|
card: AICardInstance;
|
|
76
102
|
throttleMs?: number;
|
|
77
103
|
/** Legacy compatibility: verbose mode previously lowered the throttle. */
|
|
78
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;
|
|
79
109
|
log?: Logger;
|
|
80
110
|
}): CardDraftController {
|
|
81
111
|
let failed = false;
|
|
@@ -84,6 +114,9 @@ export function createCardDraftController(params: {
|
|
|
84
114
|
let lastQueuedContent = "";
|
|
85
115
|
let inFlightContent = "";
|
|
86
116
|
let lastAnswerContent = "";
|
|
117
|
+
let lastSentStreamingContent = "";
|
|
118
|
+
let lastQueuedStreamingContent = "";
|
|
119
|
+
let inFlightStreamingContent = "";
|
|
87
120
|
|
|
88
121
|
let timelineEntries: TimelineEntry[] = [];
|
|
89
122
|
let activeThinkingIndex: number | null = null;
|
|
@@ -91,6 +124,52 @@ export function createCardDraftController(params: {
|
|
|
91
124
|
let pendingBoundaryPromise: Promise<void> | null = null;
|
|
92
125
|
|
|
93
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
|
+
};
|
|
94
173
|
|
|
95
174
|
const getFinalAnswerContent = (): string => {
|
|
96
175
|
return timelineEntries
|
|
@@ -135,13 +214,21 @@ export function createCardDraftController(params: {
|
|
|
135
214
|
return null;
|
|
136
215
|
};
|
|
137
216
|
|
|
138
|
-
const
|
|
217
|
+
const renderTimelineAsBlocks = (options: {
|
|
139
218
|
fallbackAnswer?: string;
|
|
140
219
|
overrideAnswer?: string;
|
|
141
|
-
|
|
142
|
-
} = {}): string => {
|
|
220
|
+
} = {}): CardBlock[] => {
|
|
143
221
|
const entries = timelineEntries.map((entry) => ({ ...entry }));
|
|
144
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
|
+
|
|
145
232
|
const overrideAnswer = normalizeAnswerText(options.overrideAnswer);
|
|
146
233
|
if (overrideAnswer) {
|
|
147
234
|
const lastAnswerIndex = [...entries]
|
|
@@ -151,38 +238,46 @@ export function createCardDraftController(params: {
|
|
|
151
238
|
if (lastAnswerIndex !== undefined) {
|
|
152
239
|
entries[lastAnswerIndex] = { kind: "answer", text: overrideAnswer };
|
|
153
240
|
} else {
|
|
154
|
-
|
|
241
|
+
insertAnswerEntry(overrideAnswer);
|
|
155
242
|
}
|
|
156
243
|
} else if (!entries.some((entry) => entry.kind === "answer" && entry.text)) {
|
|
157
244
|
const fallbackAnswer = normalizeAnswerText(options.fallbackAnswer);
|
|
158
245
|
if (fallbackAnswer) {
|
|
159
|
-
|
|
246
|
+
insertAnswerEntry(fallbackAnswer);
|
|
160
247
|
}
|
|
161
248
|
}
|
|
162
249
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
|
176
278
|
}
|
|
177
|
-
const previousKind = entries[index - 1]?.kind;
|
|
178
|
-
const separator =
|
|
179
|
-
compactProcessAnswerSpacing && previousKind
|
|
180
|
-
? "\n"
|
|
181
|
-
: "\n\n";
|
|
182
|
-
rendered += `${separator}${part}`;
|
|
183
279
|
}
|
|
184
|
-
|
|
185
|
-
return rendered;
|
|
280
|
+
return blocks;
|
|
186
281
|
};
|
|
187
282
|
|
|
188
283
|
const sealLiveThinking = () => {
|
|
@@ -193,14 +288,25 @@ export function createCardDraftController(params: {
|
|
|
193
288
|
activeAnswerIndex = null;
|
|
194
289
|
};
|
|
195
290
|
|
|
291
|
+
const discardCurrentAnswer = () => {
|
|
292
|
+
if (activeAnswerIndex === null) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
removeTimelineEntry(activeAnswerIndex);
|
|
296
|
+
queueRender();
|
|
297
|
+
};
|
|
298
|
+
|
|
196
299
|
const clearPendingRender = () => {
|
|
197
300
|
loop.resetPending();
|
|
198
301
|
lastQueuedContent = "";
|
|
199
302
|
};
|
|
200
303
|
|
|
201
304
|
const queueRender = () => {
|
|
202
|
-
const
|
|
203
|
-
|
|
305
|
+
const blocks = renderTimelineAsBlocks();
|
|
306
|
+
const rendered = JSON.stringify(blocks);
|
|
307
|
+
|
|
308
|
+
// Always update blockList via instances API (throttled)
|
|
309
|
+
if (blocks.length === 0) {
|
|
204
310
|
clearPendingRender();
|
|
205
311
|
return;
|
|
206
312
|
}
|
|
@@ -222,8 +328,15 @@ export function createCardDraftController(params: {
|
|
|
222
328
|
if (stopped || failed) {
|
|
223
329
|
return;
|
|
224
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
|
+
}
|
|
225
337
|
await loop.flush();
|
|
226
338
|
await loop.waitForInFlight();
|
|
339
|
+
contentLoop.resetThrottleWindow();
|
|
227
340
|
loop.resetThrottleWindow();
|
|
228
341
|
};
|
|
229
342
|
|
|
@@ -252,14 +365,16 @@ export function createCardDraftController(params: {
|
|
|
252
365
|
sendOrEditStreamMessage: async (content: string) => {
|
|
253
366
|
inFlightContent = content;
|
|
254
367
|
try {
|
|
255
|
-
|
|
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);
|
|
256
371
|
lastSentContent = content;
|
|
257
372
|
lastQueuedContent = "";
|
|
258
373
|
lastAnswerContent = getFinalAnswerContent();
|
|
259
374
|
} catch (err: unknown) {
|
|
260
375
|
failed = true;
|
|
261
376
|
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
-
params.log?.warn?.(`[DingTalk][AICard]
|
|
377
|
+
params.log?.warn?.(`[DingTalk][AICard] BlockList update failed: ${message}`);
|
|
263
378
|
} finally {
|
|
264
379
|
if (inFlightContent === content) {
|
|
265
380
|
inFlightContent = "";
|
|
@@ -268,6 +383,28 @@ export function createCardDraftController(params: {
|
|
|
268
383
|
},
|
|
269
384
|
});
|
|
270
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
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
|
|
271
408
|
const updateReasoning = async (text: string) => {
|
|
272
409
|
await waitForPendingBoundary();
|
|
273
410
|
if (stopped || failed || activeAnswerIndex !== null) {
|
|
@@ -291,7 +428,7 @@ export function createCardDraftController(params: {
|
|
|
291
428
|
queueRender();
|
|
292
429
|
};
|
|
293
430
|
|
|
294
|
-
const updateAnswer = async (text: string, options: { stream?: boolean } = {}) => {
|
|
431
|
+
const updateAnswer = async (text: string, options: { stream?: boolean; renderBlocks?: boolean } = {}) => {
|
|
295
432
|
await waitForPendingBoundary();
|
|
296
433
|
if (stopped || failed) {
|
|
297
434
|
return;
|
|
@@ -312,7 +449,13 @@ export function createCardDraftController(params: {
|
|
|
312
449
|
} else {
|
|
313
450
|
activeAnswerIndex = appendTimelineEntry("answer", normalized);
|
|
314
451
|
}
|
|
315
|
-
if (options.stream
|
|
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) {
|
|
316
459
|
clearPendingRender();
|
|
317
460
|
return;
|
|
318
461
|
}
|
|
@@ -392,12 +535,18 @@ export function createCardDraftController(params: {
|
|
|
392
535
|
queueRender();
|
|
393
536
|
};
|
|
394
537
|
|
|
395
|
-
const notifyNewAssistantTurn = async (
|
|
538
|
+
const notifyNewAssistantTurn = async (options: {
|
|
539
|
+
discardActiveAnswer?: boolean;
|
|
540
|
+
} = {}) => {
|
|
396
541
|
if (stopped || failed) {
|
|
397
542
|
return;
|
|
398
543
|
}
|
|
399
544
|
if (activeAnswerIndex !== null) {
|
|
400
|
-
|
|
545
|
+
if (options.discardActiveAnswer) {
|
|
546
|
+
discardCurrentAnswer();
|
|
547
|
+
} else {
|
|
548
|
+
sealCurrentAnswer();
|
|
549
|
+
}
|
|
401
550
|
await beginBoundaryFlush();
|
|
402
551
|
return;
|
|
403
552
|
}
|
|
@@ -407,6 +556,23 @@ export function createCardDraftController(params: {
|
|
|
407
556
|
}
|
|
408
557
|
};
|
|
409
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
|
+
|
|
410
576
|
return {
|
|
411
577
|
updateAnswer,
|
|
412
578
|
updateReasoning,
|
|
@@ -414,7 +580,9 @@ export function createCardDraftController(params: {
|
|
|
414
580
|
appendThinkingBlock,
|
|
415
581
|
updateTool,
|
|
416
582
|
appendTool: updateTool,
|
|
583
|
+
appendImageBlock,
|
|
417
584
|
appendToolBeforeCurrentAnswer,
|
|
585
|
+
discardCurrentAnswer,
|
|
418
586
|
notifyNewAssistantTurn,
|
|
419
587
|
startAssistantTurn: notifyNewAssistantTurn,
|
|
420
588
|
sealActiveThinking: async () => {
|
|
@@ -426,11 +594,18 @@ export function createCardDraftController(params: {
|
|
|
426
594
|
await beginBoundaryFlush();
|
|
427
595
|
}
|
|
428
596
|
},
|
|
429
|
-
flush: () =>
|
|
430
|
-
|
|
597
|
+
flush: async () => {
|
|
598
|
+
await contentLoop.flush();
|
|
599
|
+
await loop.flush();
|
|
600
|
+
},
|
|
601
|
+
waitForInFlight: async () => {
|
|
602
|
+
await contentLoop.waitForInFlight();
|
|
603
|
+
await loop.waitForInFlight();
|
|
604
|
+
},
|
|
431
605
|
|
|
432
606
|
stop: () => {
|
|
433
607
|
stopped = true;
|
|
608
|
+
contentLoop.stop();
|
|
434
609
|
loop.stop();
|
|
435
610
|
},
|
|
436
611
|
|
|
@@ -438,6 +613,24 @@ export function createCardDraftController(params: {
|
|
|
438
613
|
getLastContent: () => lastSentContent,
|
|
439
614
|
getLastAnswerContent: () => lastAnswerContent,
|
|
440
615
|
getFinalAnswerContent,
|
|
441
|
-
|
|
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,
|
|
442
635
|
};
|
|
443
636
|
}
|