@soimy/dingtalk 3.6.11 → 3.7.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/dist/index.d.ts +1 -1
- package/dist/index.js +11684 -10583
- package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
- package/dist/src/card/ask-user-question-context.d.ts +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +1 -1
- package/dist/src/card/ask-user-question.d.ts +1 -1
- package/dist/src/card/card-action-handler.d.ts +2 -2
- package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
- package/dist/src/card/card-run-registry.d.ts +2 -2
- package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
- package/dist/src/card/card-stop-handler.d.ts +1 -1
- package/dist/src/card/card-streaming-mode.d.ts +1 -1
- package/dist/src/card/card-task-progress.d.ts +60 -0
- package/dist/src/channel.d.ts +6 -6
- package/dist/src/command/card-stop-command.d.ts +1 -1
- package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
- package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
- package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
- package/dist/src/gateway/channel-gateway.d.ts +1 -1
- package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
- package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +1 -1
- package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
- package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
- package/dist/src/messaging/btw-deliver.d.ts +1 -1
- package/dist/src/messaging/channel-outbound.d.ts +1 -1
- package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
- package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
- package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
- package/dist/src/messaging/quoted-context.d.ts +2 -2
- package/dist/src/messaging/quoted-file-service.d.ts +1 -1
- package/dist/src/messaging/quoted-ref.d.ts +2 -2
- package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
- package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
- package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
- package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
- package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
- package/dist/src/platform/channel-status.d.ts +1 -1
- package/dist/src/platform/config.d.ts +110 -0
- package/dist/src/platform/runtime-events.d.ts +67 -0
- package/dist/src/{types.d.ts → platform/types.d.ts} +74 -1
- package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
- package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
- package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
- package/dist/src/targeting/agent-routing.d.ts +3 -3
- package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
- package/index.ts +193 -69
- package/openclaw.plugin.json +158 -2
- package/package.json +16 -16
- package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
- package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
- package/src/card/ask-user-question-context.ts +1 -1
- package/src/card/ask-user-question-store.ts +2 -2
- package/src/card/ask-user-question.ts +11 -7
- package/src/card/card-action-handler.ts +2 -2
- package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
- package/src/card/card-draft-controller.ts +770 -0
- package/src/card/card-markdown-image-reroute.ts +8 -2
- package/src/card/card-run-registry.ts +17 -7
- package/src/{card-service.ts → card/card-service.ts} +91 -61
- package/src/card/card-stop-handler.ts +12 -8
- package/src/card/card-streaming-mode.ts +1 -1
- package/src/card/card-task-progress.ts +330 -0
- package/src/card/draft-stream-loop.ts +119 -0
- package/src/card/reasoning-answer-split.ts +125 -121
- package/src/card/reasoning-block-assembler.ts +122 -123
- package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
- package/src/card/statusline-renderer.ts +14 -5
- package/src/card/task-model-metadata.ts +8 -3
- package/src/channel.ts +14 -16
- package/src/command/card-stop-command.ts +2 -2
- package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
- package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
- package/src/command/inbound-command-dispatch-service.ts +12 -6
- package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
- package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
- package/src/gateway/channel-gateway.ts +106 -28
- package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
- package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
- package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +139 -84
- package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
- package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
- package/src/messaging/attachment-text-extractor.ts +21 -7
- package/src/messaging/btw-deliver.ts +2 -2
- package/src/messaging/channel-actions.ts +15 -7
- package/src/messaging/channel-outbound.ts +16 -11
- package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
- package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
- package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
- package/src/messaging/quoted-context.ts +2 -2
- package/src/messaging/quoted-file-service.ts +311 -294
- package/src/messaging/quoted-ref.ts +11 -7
- package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
- package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
- package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
- package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
- package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
- package/src/{send-service.ts → messaging/send-service.ts} +100 -65
- package/src/{auth.ts → platform/auth.ts} +2 -2
- package/src/platform/channel-status.ts +3 -3
- package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
- package/src/{config.ts → platform/config.ts} +167 -19
- package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
- package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
- package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
- package/src/platform/runtime-events.ts +202 -0
- package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
- package/src/{types.ts → platform/types.ts} +76 -1
- package/src/{http-client.ts → shared/http-client.ts} +1 -3
- package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
- package/src/{utils.ts → shared/utils.ts} +30 -13
- package/src/targeting/agent-name-matcher.ts +1 -1
- package/src/targeting/agent-routing.ts +7 -7
- package/src/targeting/group-members-store.ts +1 -1
- package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
- package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
- package/src/targeting/target-directory-adapter.ts +3 -3
- package/src/targeting/target-directory-store.ts +1 -1
- package/src/targeting/target-input.ts +1 -1
- package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
- package/dist/src/config.d.ts +0 -59
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
- package/src/card-draft-controller.ts +0 -637
- package/src/draft-stream-loop.ts +0 -119
- /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
- /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
- /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
- /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
- /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
- /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
- /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
- /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
- /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
- /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
- /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
- /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
- /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
- /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
- /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
- /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
- /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
- /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
- /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
- /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
- /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
- /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
- /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
- /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
- /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
- /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
- /package/src/{access-control.ts → platform/access-control.ts} +0 -0
- /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
- /package/src/{runtime.ts → platform/runtime.ts} +0 -0
- /package/src/{session-state.ts → platform/session-state.ts} +0 -0
- /package/src/{signature.ts → platform/signature.ts} +0 -0
- /package/src/{dedup.ts → shared/dedup.ts} +0 -0
- /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
- /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card draft controller for throttled AI Card streaming updates.
|
|
3
|
+
*
|
|
4
|
+
* The controller keeps a single rendered card timeline made of:
|
|
5
|
+
* - sealed process blocks (`thinking` / `tool`)
|
|
6
|
+
* - an optional live thinking block
|
|
7
|
+
* - accumulated answer turns rendered as JSON CardBlock[]
|
|
8
|
+
*
|
|
9
|
+
* It delegates throttling and single-flight transport guarantees to
|
|
10
|
+
* {@link createDraftStreamLoop}.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AICardInstance, CardBlock, Logger } from "../platform/types";
|
|
14
|
+
import {
|
|
15
|
+
clearAICardStreamingContent,
|
|
16
|
+
streamAICardContent,
|
|
17
|
+
updateAICardBlockList,
|
|
18
|
+
} from "./card-service";
|
|
19
|
+
import { createDraftStreamLoop } from "./draft-stream-loop";
|
|
20
|
+
|
|
21
|
+
type TimelineEntryKind = "progress" | "thinking" | "tool" | "answer" | "image";
|
|
22
|
+
|
|
23
|
+
type TimelineEntry = {
|
|
24
|
+
kind: TimelineEntryKind;
|
|
25
|
+
text: string;
|
|
26
|
+
mediaId?: string;
|
|
27
|
+
};
|
|
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
|
+
|
|
36
|
+
export interface CardDraftController {
|
|
37
|
+
updateProgress: (text: string) => Promise<void>;
|
|
38
|
+
clearProgress: (options?: { clearRemoteWhenEmpty?: boolean }) => Promise<void>;
|
|
39
|
+
updateAnswer: (
|
|
40
|
+
text: string,
|
|
41
|
+
options?: { stream?: boolean; renderBlocks?: boolean },
|
|
42
|
+
) => Promise<void>;
|
|
43
|
+
updateReasoning: (text: string) => Promise<void>;
|
|
44
|
+
updateThinking: (text: string) => Promise<void>;
|
|
45
|
+
appendThinkingBlock: (text: string) => Promise<void>;
|
|
46
|
+
updateTool: (text: string) => Promise<void>;
|
|
47
|
+
appendTool: (text: string) => Promise<void>;
|
|
48
|
+
/** Append an image block (type=3) with an uploaded mediaId. */
|
|
49
|
+
appendImageBlock: (mediaId: string, text?: string) => Promise<void>;
|
|
50
|
+
appendToolBeforeCurrentAnswer: (text: string) => Promise<void>;
|
|
51
|
+
/** Drop the current answer draft while keeping sealed earlier turns intact. */
|
|
52
|
+
discardCurrentAnswer: () => void;
|
|
53
|
+
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
54
|
+
notifyNewAssistantTurn: (options?: { discardActiveAnswer?: boolean }) => Promise<void>;
|
|
55
|
+
startAssistantTurn: () => Promise<void>;
|
|
56
|
+
/** Seal the active thinking entry (keep it in timeline) without removing it. */
|
|
57
|
+
sealActiveThinking: () => Promise<void>;
|
|
58
|
+
flush: () => Promise<void>;
|
|
59
|
+
waitForInFlight: () => Promise<void>;
|
|
60
|
+
stop: () => void;
|
|
61
|
+
isFailed: () => boolean;
|
|
62
|
+
/** Last content successfully sent to card. */
|
|
63
|
+
getLastContent: () => string;
|
|
64
|
+
/** Last answer-only content successfully sent to card. */
|
|
65
|
+
getLastAnswerContent: () => string;
|
|
66
|
+
/** Current answer-only content composed from all completed answer turns. */
|
|
67
|
+
getFinalAnswerContent: () => string;
|
|
68
|
+
/** Current rendered timeline as CardBlock[] JSON string for blockList parameter. */
|
|
69
|
+
getRenderedBlocks: (options?: {
|
|
70
|
+
fallbackAnswer?: string;
|
|
71
|
+
overrideAnswer?: string;
|
|
72
|
+
compactProcessAnswerSpacing?: boolean;
|
|
73
|
+
}) => string;
|
|
74
|
+
/** Current rendered timeline as pure markdown text for content parameter and fallback. */
|
|
75
|
+
getRenderedContent: (options?: {
|
|
76
|
+
fallbackAnswer?: string;
|
|
77
|
+
overrideAnswer?: string;
|
|
78
|
+
compactProcessAnswerSpacing?: boolean;
|
|
79
|
+
}) => string;
|
|
80
|
+
/** Stream answer text to content key for real-time display. Only available when realTimeStreamEnabled=true. */
|
|
81
|
+
streamContent?: (text: string) => Promise<void>;
|
|
82
|
+
/** Clear the streaming content key. Only available when realTimeStreamEnabled=true. */
|
|
83
|
+
clearStreamingContent?: () => Promise<void>;
|
|
84
|
+
/** Whether real-time streaming is enabled. */
|
|
85
|
+
isRealTimeStreamEnabled: () => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeProcessText(text: string | undefined): string {
|
|
89
|
+
return typeof text === "string" ? text.trim() : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeAnswerText(text: string | undefined): string {
|
|
93
|
+
return typeof text === "string" ? text.trimStart() : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function wrapProcessBlockMarkdown(text: string): string {
|
|
97
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
98
|
+
return lines
|
|
99
|
+
.map(
|
|
100
|
+
(line) =>
|
|
101
|
+
`> <font sizeToken=${PROCESS_BLOCK_FONT_SIZE_TOKEN} colorTokenV2=${PROCESS_BLOCK_FONT_COLOR_TOKEN_V2}>${line}</font>`,
|
|
102
|
+
)
|
|
103
|
+
.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render the progress summary as one card block per field.
|
|
108
|
+
*
|
|
109
|
+
* Real-device finding: with all four fields inside a single multi-line markdown
|
|
110
|
+
* block, the DingTalk client only re-rendered the trailing line — the elapsed
|
|
111
|
+
* time ticked while "已完成:N 步" stayed pinned at its first value, even though
|
|
112
|
+
* the frame we sent contained the new count. Splitting the fields into separate
|
|
113
|
+
* blocks makes each field update independently, like the trailing line did.
|
|
114
|
+
*
|
|
115
|
+
* The fields deliberately avoid the quoted process-block style: a blockquote
|
|
116
|
+
* indents the text behind a quote bar, which reads poorly for a status summary.
|
|
117
|
+
* The footnote size/color tokens still mark these blocks as process metadata.
|
|
118
|
+
*/
|
|
119
|
+
function renderProgressBlocks(text: string): CardBlock[] {
|
|
120
|
+
return text
|
|
121
|
+
.split("\n")
|
|
122
|
+
.filter((line) => line.trim())
|
|
123
|
+
.map((line) => ({
|
|
124
|
+
type: 2 as const,
|
|
125
|
+
markdown: `<font sizeToken=${PROCESS_BLOCK_FONT_SIZE_TOKEN} colorTokenV2=${PROCESS_BLOCK_FONT_COLOR_TOKEN_V2}>${line}</font>`,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createCardDraftController(params: {
|
|
130
|
+
card: AICardInstance;
|
|
131
|
+
throttleMs?: number;
|
|
132
|
+
/** Legacy compatibility: verbose mode previously lowered the throttle. */
|
|
133
|
+
verboseMode?: boolean;
|
|
134
|
+
/** Enable real-time streaming to content key for answer display. */
|
|
135
|
+
realTimeStreamEnabled?: boolean;
|
|
136
|
+
/** Optional callback to get the current statusLine for piggy-backing on blockList updates. */
|
|
137
|
+
getStatusLine?: () => string | undefined;
|
|
138
|
+
log?: Logger;
|
|
139
|
+
}): CardDraftController {
|
|
140
|
+
let failed = false;
|
|
141
|
+
let stopped = false;
|
|
142
|
+
let lastSentContent = "";
|
|
143
|
+
let lastQueuedContent = "";
|
|
144
|
+
let inFlightContent = "";
|
|
145
|
+
let lastAnswerContent = "";
|
|
146
|
+
let lastSentStreamingContent = "";
|
|
147
|
+
let lastQueuedStreamingContent = "";
|
|
148
|
+
let inFlightStreamingContent = "";
|
|
149
|
+
|
|
150
|
+
let timelineEntries: TimelineEntry[] = [];
|
|
151
|
+
let activeThinkingIndex: number | null = null;
|
|
152
|
+
let activeAnswerIndex: number | null = null;
|
|
153
|
+
let pendingBoundaryPromise: Promise<void> | null = null;
|
|
154
|
+
|
|
155
|
+
const effectiveThrottleMs = params.throttleMs ?? (params.verboseMode ? 50 : 300);
|
|
156
|
+
const realTimeStreamEnabled = params.realTimeStreamEnabled ?? false;
|
|
157
|
+
let hasStreamingContent = false;
|
|
158
|
+
|
|
159
|
+
const clearPendingStreamingContent = () => {
|
|
160
|
+
contentLoop.resetPending();
|
|
161
|
+
lastQueuedStreamingContent = "";
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const streamContentToCard = async (text: string) => {
|
|
165
|
+
if (!realTimeStreamEnabled) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const normalized = normalizeAnswerText(text);
|
|
169
|
+
if (!normalized.trim()) {
|
|
170
|
+
clearPendingStreamingContent();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (normalized === lastSentStreamingContent) {
|
|
174
|
+
const hasNewerInFlight =
|
|
175
|
+
!!inFlightStreamingContent && inFlightStreamingContent !== normalized;
|
|
176
|
+
if (!hasNewerInFlight) {
|
|
177
|
+
clearPendingStreamingContent();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (normalized === lastQueuedStreamingContent) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
lastQueuedStreamingContent = normalized;
|
|
185
|
+
contentLoop.update(normalized);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const clearStreamingContentFromCard = async () => {
|
|
189
|
+
clearPendingStreamingContent();
|
|
190
|
+
await contentLoop.waitForInFlight();
|
|
191
|
+
if (!realTimeStreamEnabled || !hasStreamingContent) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
await clearAICardStreamingContent(params.card, params.log);
|
|
196
|
+
hasStreamingContent = false;
|
|
197
|
+
lastSentStreamingContent = "";
|
|
198
|
+
} catch (err: unknown) {
|
|
199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
+
params.log?.debug?.(`[DingTalk][AICard] Failed to clear streaming content: ${message}`);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const getFinalAnswerContent = (): string => {
|
|
205
|
+
return timelineEntries
|
|
206
|
+
.filter((entry) => entry.kind === "answer" && entry.text)
|
|
207
|
+
.map((entry) => entry.text)
|
|
208
|
+
.join("\n\n");
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const removeTimelineEntry = (index: number) => {
|
|
212
|
+
timelineEntries.splice(index, 1);
|
|
213
|
+
if (activeThinkingIndex !== null) {
|
|
214
|
+
if (activeThinkingIndex === index) {
|
|
215
|
+
activeThinkingIndex = null;
|
|
216
|
+
} else if (activeThinkingIndex > index) {
|
|
217
|
+
activeThinkingIndex -= 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (activeAnswerIndex !== null) {
|
|
221
|
+
if (activeAnswerIndex === index) {
|
|
222
|
+
activeAnswerIndex = null;
|
|
223
|
+
} else if (activeAnswerIndex > index) {
|
|
224
|
+
activeAnswerIndex -= 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const appendTimelineEntry = (kind: TimelineEntryKind, text: string): number => {
|
|
230
|
+
timelineEntries.push({ kind, text });
|
|
231
|
+
return timelineEntries.length - 1;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const updateProgress = async (text: string) => {
|
|
235
|
+
await waitForPendingBoundary();
|
|
236
|
+
if (stopped || failed) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const normalized = normalizeProcessText(text);
|
|
240
|
+
if (!normalized) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const progressIndex = timelineEntries.findIndex((entry) => entry.kind === "progress");
|
|
244
|
+
if (progressIndex >= 0) {
|
|
245
|
+
timelineEntries[progressIndex] = { kind: "progress", text: normalized };
|
|
246
|
+
} else {
|
|
247
|
+
timelineEntries.unshift({ kind: "progress", text: normalized });
|
|
248
|
+
if (activeThinkingIndex !== null) {
|
|
249
|
+
activeThinkingIndex += 1;
|
|
250
|
+
}
|
|
251
|
+
if (activeAnswerIndex !== null) {
|
|
252
|
+
activeAnswerIndex += 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
queueRender();
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const clearRemoteBlocks = async () => {
|
|
259
|
+
// The normal render path intentionally skips empty block lists, so an
|
|
260
|
+
// explicit teardown has to push the empty state itself: otherwise the
|
|
261
|
+
// reader keeps seeing the last "⏳ 任务处理中" frame on a card that was
|
|
262
|
+
// never recalled (ask-user takeover with a failed recall).
|
|
263
|
+
clearPendingRender();
|
|
264
|
+
if (stopped || failed) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const statusLine = params.getStatusLine?.();
|
|
269
|
+
await updateAICardBlockList(
|
|
270
|
+
params.card,
|
|
271
|
+
"[]",
|
|
272
|
+
params.log,
|
|
273
|
+
statusLine ? { statusLine } : undefined,
|
|
274
|
+
);
|
|
275
|
+
// Force the next render to re-send instead of trusting a stale cache.
|
|
276
|
+
lastSentContent = "";
|
|
277
|
+
lastAnswerContent = "";
|
|
278
|
+
} catch (err: unknown) {
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
params.log?.warn?.(`[DingTalk][AICard] Failed to clear stale card blocks: ${message}`);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const clearProgress = async (options: { clearRemoteWhenEmpty?: boolean } = {}) => {
|
|
285
|
+
await waitForPendingBoundary();
|
|
286
|
+
// A failed card never re-renders (the draft stream loop is stopped), but
|
|
287
|
+
// the timeline entry must still go away so a stale "任务处理中" block can
|
|
288
|
+
// never be picked up by a later render/fallback read.
|
|
289
|
+
if (stopped) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const progressIndex = timelineEntries.findIndex((entry) => entry.kind === "progress");
|
|
293
|
+
if (progressIndex < 0) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
removeTimelineEntry(progressIndex);
|
|
297
|
+
if (timelineEntries.length === 0 && options.clearRemoteWhenEmpty) {
|
|
298
|
+
await clearRemoteBlocks();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
queueRender();
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const findCurrentSegmentAnswerIndex = (): number | null => {
|
|
305
|
+
return activeAnswerIndex;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const findLastAnswerEntryIndex = (): number | null => {
|
|
309
|
+
for (let index = timelineEntries.length - 1; index >= 0; index -= 1) {
|
|
310
|
+
if (timelineEntries[index]?.kind === "answer") {
|
|
311
|
+
return index;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const renderTimelineAsBlocks = (
|
|
318
|
+
options: {
|
|
319
|
+
fallbackAnswer?: string;
|
|
320
|
+
overrideAnswer?: string;
|
|
321
|
+
} = {},
|
|
322
|
+
): CardBlock[] => {
|
|
323
|
+
const entries = timelineEntries.map((entry) => ({ ...entry }));
|
|
324
|
+
|
|
325
|
+
const insertAnswerEntry = (text: string) => {
|
|
326
|
+
const firstImageIndex = entries.findIndex((entry) => entry.kind === "image");
|
|
327
|
+
if (firstImageIndex >= 0) {
|
|
328
|
+
entries.splice(firstImageIndex, 0, { kind: "answer", text });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
entries.push({ kind: "answer", text });
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const overrideAnswer = normalizeAnswerText(options.overrideAnswer);
|
|
335
|
+
if (overrideAnswer) {
|
|
336
|
+
const lastAnswerIndex = [...entries]
|
|
337
|
+
.map((entry, index) => ({ entry, index }))
|
|
338
|
+
.toReversed()
|
|
339
|
+
.find(({ entry }) => entry.kind === "answer")?.index;
|
|
340
|
+
if (lastAnswerIndex !== undefined) {
|
|
341
|
+
entries[lastAnswerIndex] = { kind: "answer", text: overrideAnswer };
|
|
342
|
+
} else {
|
|
343
|
+
insertAnswerEntry(overrideAnswer);
|
|
344
|
+
}
|
|
345
|
+
} else if (!entries.some((entry) => entry.kind === "answer" && entry.text)) {
|
|
346
|
+
const fallbackAnswer = normalizeAnswerText(options.fallbackAnswer);
|
|
347
|
+
if (fallbackAnswer) {
|
|
348
|
+
insertAnswerEntry(fallbackAnswer);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const blocks: CardBlock[] = [];
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (!entry) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
switch (entry.kind) {
|
|
358
|
+
case "progress":
|
|
359
|
+
if (entry.text?.trim()) {
|
|
360
|
+
blocks.push(...renderProgressBlocks(entry.text));
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
case "answer":
|
|
364
|
+
if (entry.text?.trim()) {
|
|
365
|
+
blocks.push({ type: 0, markdown: entry.text });
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "thinking":
|
|
369
|
+
if (entry.text?.trim()) {
|
|
370
|
+
blocks.push({ type: 1, markdown: wrapProcessBlockMarkdown(entry.text) });
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case "tool":
|
|
374
|
+
if (entry.text?.trim()) {
|
|
375
|
+
blocks.push({ type: 2, markdown: wrapProcessBlockMarkdown(entry.text) });
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case "image":
|
|
379
|
+
if (entry.mediaId?.trim()) {
|
|
380
|
+
blocks.push({
|
|
381
|
+
type: 3,
|
|
382
|
+
mediaId: entry.mediaId,
|
|
383
|
+
...(entry.text?.trim() ? { text: entry.text } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return blocks;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const sealLiveThinking = () => {
|
|
393
|
+
activeThinkingIndex = null;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const sealCurrentAnswer = () => {
|
|
397
|
+
activeAnswerIndex = null;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const discardCurrentAnswer = () => {
|
|
401
|
+
if (activeAnswerIndex === null) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
removeTimelineEntry(activeAnswerIndex);
|
|
405
|
+
queueRender();
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const clearPendingRender = () => {
|
|
409
|
+
loop.resetPending();
|
|
410
|
+
lastQueuedContent = "";
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const queueRender = () => {
|
|
414
|
+
const blocks = renderTimelineAsBlocks();
|
|
415
|
+
const rendered = JSON.stringify(blocks);
|
|
416
|
+
|
|
417
|
+
// Always update blockList via instances API (throttled)
|
|
418
|
+
if (blocks.length === 0) {
|
|
419
|
+
clearPendingRender();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (rendered === lastSentContent) {
|
|
423
|
+
const hasNewerInFlight = !!inFlightContent && inFlightContent !== rendered;
|
|
424
|
+
if (!hasNewerInFlight) {
|
|
425
|
+
clearPendingRender();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (rendered === lastQueuedContent) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
lastQueuedContent = rendered;
|
|
433
|
+
loop.update(rendered);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const flushBoundaryFrame = async () => {
|
|
437
|
+
if (stopped || failed) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Clear the live streaming content before committing blockList at the boundary.
|
|
441
|
+
// This drops any queued fake-streaming tail so the empty stream update can stop
|
|
442
|
+
// the animation before the blockList frame lands.
|
|
443
|
+
if (hasStreamingContent) {
|
|
444
|
+
await clearStreamingContentFromCard();
|
|
445
|
+
}
|
|
446
|
+
await loop.flush();
|
|
447
|
+
await loop.waitForInFlight();
|
|
448
|
+
contentLoop.resetThrottleWindow();
|
|
449
|
+
loop.resetThrottleWindow();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const beginBoundaryFlush = () => {
|
|
453
|
+
if (pendingBoundaryPromise) {
|
|
454
|
+
return pendingBoundaryPromise;
|
|
455
|
+
}
|
|
456
|
+
const current = flushBoundaryFrame().finally(() => {
|
|
457
|
+
if (pendingBoundaryPromise === current) {
|
|
458
|
+
pendingBoundaryPromise = null;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
pendingBoundaryPromise = current;
|
|
462
|
+
return current;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const waitForPendingBoundary = async () => {
|
|
466
|
+
if (pendingBoundaryPromise) {
|
|
467
|
+
await pendingBoundaryPromise;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const loop = createDraftStreamLoop({
|
|
472
|
+
throttleMs: effectiveThrottleMs,
|
|
473
|
+
isStopped: () => stopped || failed,
|
|
474
|
+
sendOrEditStreamMessage: async (content: string) => {
|
|
475
|
+
inFlightContent = content;
|
|
476
|
+
try {
|
|
477
|
+
// Use instances API for blockList (not streaming API)
|
|
478
|
+
const statusLine = params.getStatusLine?.();
|
|
479
|
+
// Wire-level trace for real-device card debugging: how often a
|
|
480
|
+
// card is updated and how large each frame is. Deliberately
|
|
481
|
+
// metadata-only — the rendered body carries user content (card
|
|
482
|
+
// answers), so it must never be copied into application logs.
|
|
483
|
+
params.log?.debug?.(
|
|
484
|
+
`[DingTalk][AICard] BlockList frame card=${params.card.outTrackId || params.card.cardInstanceId} ` +
|
|
485
|
+
`len=${content.length}`,
|
|
486
|
+
);
|
|
487
|
+
await updateAICardBlockList(
|
|
488
|
+
params.card,
|
|
489
|
+
content,
|
|
490
|
+
params.log,
|
|
491
|
+
statusLine ? { statusLine } : undefined,
|
|
492
|
+
);
|
|
493
|
+
lastSentContent = content;
|
|
494
|
+
lastQueuedContent = "";
|
|
495
|
+
lastAnswerContent = getFinalAnswerContent();
|
|
496
|
+
} catch (err: unknown) {
|
|
497
|
+
failed = true;
|
|
498
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
499
|
+
params.log?.warn?.(`[DingTalk][AICard] BlockList update failed: ${message}`);
|
|
500
|
+
} finally {
|
|
501
|
+
if (inFlightContent === content) {
|
|
502
|
+
inFlightContent = "";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const contentLoop = createDraftStreamLoop({
|
|
509
|
+
throttleMs: effectiveThrottleMs,
|
|
510
|
+
isStopped: () => stopped || failed,
|
|
511
|
+
sendOrEditStreamMessage: async (content: string) => {
|
|
512
|
+
inFlightStreamingContent = content;
|
|
513
|
+
try {
|
|
514
|
+
await streamAICardContent(params.card, content, params.log);
|
|
515
|
+
hasStreamingContent = true;
|
|
516
|
+
lastSentStreamingContent = content;
|
|
517
|
+
lastQueuedStreamingContent = "";
|
|
518
|
+
lastAnswerContent = content;
|
|
519
|
+
} catch (err: unknown) {
|
|
520
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
521
|
+
params.log?.debug?.(`[DingTalk][AICard] Failed to stream content: ${message}`);
|
|
522
|
+
} finally {
|
|
523
|
+
if (inFlightStreamingContent === content) {
|
|
524
|
+
inFlightStreamingContent = "";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const updateReasoning = async (text: string) => {
|
|
531
|
+
await waitForPendingBoundary();
|
|
532
|
+
if (stopped || failed || activeAnswerIndex !== null) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const normalized = normalizeProcessText(text);
|
|
536
|
+
if (!normalized) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (activeThinkingIndex === null && timelineEntries.length > 0) {
|
|
540
|
+
const lastKind = timelineEntries.at(-1)?.kind;
|
|
541
|
+
if (lastKind && lastKind !== "thinking") {
|
|
542
|
+
await flushBoundaryFrame();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (activeThinkingIndex !== null) {
|
|
546
|
+
timelineEntries[activeThinkingIndex] = { kind: "thinking", text: normalized };
|
|
547
|
+
} else {
|
|
548
|
+
activeThinkingIndex = appendTimelineEntry("thinking", normalized);
|
|
549
|
+
}
|
|
550
|
+
queueRender();
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const updateAnswer = async (
|
|
554
|
+
text: string,
|
|
555
|
+
options: { stream?: boolean; renderBlocks?: boolean } = {},
|
|
556
|
+
) => {
|
|
557
|
+
await waitForPendingBoundary();
|
|
558
|
+
if (stopped || failed) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const normalized = normalizeAnswerText(text);
|
|
562
|
+
if (!normalized.trim()) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (activeAnswerIndex === null && timelineEntries.length > 0) {
|
|
566
|
+
const lastKind = timelineEntries.at(-1)?.kind;
|
|
567
|
+
if (lastKind && lastKind !== "answer") {
|
|
568
|
+
await flushBoundaryFrame();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
sealLiveThinking();
|
|
572
|
+
if (activeAnswerIndex !== null) {
|
|
573
|
+
timelineEntries[activeAnswerIndex] = { kind: "answer", text: normalized };
|
|
574
|
+
} else {
|
|
575
|
+
activeAnswerIndex = appendTimelineEntry("answer", normalized);
|
|
576
|
+
}
|
|
577
|
+
if (options.stream !== false) {
|
|
578
|
+
await streamContentToCard(normalized);
|
|
579
|
+
} else {
|
|
580
|
+
clearPendingStreamingContent();
|
|
581
|
+
}
|
|
582
|
+
const shouldRenderBlocks = options.renderBlocks ?? options.stream !== false;
|
|
583
|
+
if (!shouldRenderBlocks) {
|
|
584
|
+
clearPendingRender();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
queueRender();
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const updateTool = async (text: string) => {
|
|
591
|
+
await waitForPendingBoundary();
|
|
592
|
+
if (stopped || failed) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const normalized = normalizeProcessText(text);
|
|
596
|
+
if (!normalized) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (timelineEntries.length > 0) {
|
|
600
|
+
await flushBoundaryFrame();
|
|
601
|
+
}
|
|
602
|
+
sealLiveThinking();
|
|
603
|
+
sealCurrentAnswer();
|
|
604
|
+
appendTimelineEntry("tool", normalized);
|
|
605
|
+
queueRender();
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const appendThinkingBlock = async (text: string) => {
|
|
609
|
+
await waitForPendingBoundary();
|
|
610
|
+
if (stopped || failed) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const normalized = normalizeProcessText(text);
|
|
614
|
+
if (!normalized) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (timelineEntries.length > 0) {
|
|
618
|
+
await flushBoundaryFrame();
|
|
619
|
+
}
|
|
620
|
+
sealLiveThinking();
|
|
621
|
+
const currentSegmentAnswerIndex = findCurrentSegmentAnswerIndex();
|
|
622
|
+
if (currentSegmentAnswerIndex !== null) {
|
|
623
|
+
timelineEntries.splice(currentSegmentAnswerIndex, 0, { kind: "thinking", text: normalized });
|
|
624
|
+
if (activeAnswerIndex !== null && activeAnswerIndex >= currentSegmentAnswerIndex) {
|
|
625
|
+
activeAnswerIndex += 1;
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
sealCurrentAnswer();
|
|
629
|
+
appendTimelineEntry("thinking", normalized);
|
|
630
|
+
}
|
|
631
|
+
queueRender();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const appendToolBeforeCurrentAnswer = async (text: string) => {
|
|
635
|
+
await waitForPendingBoundary();
|
|
636
|
+
if (stopped || failed) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const normalized = normalizeProcessText(text);
|
|
640
|
+
if (!normalized) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (timelineEntries.length > 0) {
|
|
644
|
+
await flushBoundaryFrame();
|
|
645
|
+
}
|
|
646
|
+
sealLiveThinking();
|
|
647
|
+
const insertionIndex = findCurrentSegmentAnswerIndex() ?? findLastAnswerEntryIndex();
|
|
648
|
+
if (insertionIndex !== null) {
|
|
649
|
+
timelineEntries.splice(insertionIndex, 0, { kind: "tool", text: normalized });
|
|
650
|
+
if (activeAnswerIndex !== null && activeAnswerIndex >= insertionIndex) {
|
|
651
|
+
activeAnswerIndex += 1;
|
|
652
|
+
}
|
|
653
|
+
if (activeThinkingIndex !== null && activeThinkingIndex >= insertionIndex) {
|
|
654
|
+
activeThinkingIndex += 1;
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
sealCurrentAnswer();
|
|
658
|
+
appendTimelineEntry("tool", normalized);
|
|
659
|
+
}
|
|
660
|
+
queueRender();
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const notifyNewAssistantTurn = async (
|
|
664
|
+
options: {
|
|
665
|
+
discardActiveAnswer?: boolean;
|
|
666
|
+
} = {},
|
|
667
|
+
) => {
|
|
668
|
+
if (stopped || failed) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (activeAnswerIndex !== null) {
|
|
672
|
+
if (options.discardActiveAnswer) {
|
|
673
|
+
discardCurrentAnswer();
|
|
674
|
+
} else {
|
|
675
|
+
sealCurrentAnswer();
|
|
676
|
+
queueRender();
|
|
677
|
+
}
|
|
678
|
+
await beginBoundaryFlush();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (activeThinkingIndex !== null) {
|
|
682
|
+
removeTimelineEntry(activeThinkingIndex);
|
|
683
|
+
clearPendingRender();
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const appendImageBlock = async (mediaId: string, text = "") => {
|
|
688
|
+
await waitForPendingBoundary();
|
|
689
|
+
if (stopped || failed) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!mediaId.trim()) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (timelineEntries.length > 0) {
|
|
696
|
+
await flushBoundaryFrame();
|
|
697
|
+
}
|
|
698
|
+
sealLiveThinking();
|
|
699
|
+
sealCurrentAnswer();
|
|
700
|
+
timelineEntries.push({ kind: "image", text, mediaId });
|
|
701
|
+
queueRender();
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
updateProgress,
|
|
706
|
+
clearProgress,
|
|
707
|
+
updateAnswer,
|
|
708
|
+
updateReasoning,
|
|
709
|
+
updateThinking: updateReasoning,
|
|
710
|
+
appendThinkingBlock,
|
|
711
|
+
updateTool,
|
|
712
|
+
appendTool: updateTool,
|
|
713
|
+
appendImageBlock,
|
|
714
|
+
appendToolBeforeCurrentAnswer,
|
|
715
|
+
discardCurrentAnswer,
|
|
716
|
+
notifyNewAssistantTurn,
|
|
717
|
+
startAssistantTurn: notifyNewAssistantTurn,
|
|
718
|
+
sealActiveThinking: async () => {
|
|
719
|
+
if (stopped || failed) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (activeThinkingIndex !== null) {
|
|
723
|
+
sealLiveThinking();
|
|
724
|
+
await beginBoundaryFlush();
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
flush: async () => {
|
|
728
|
+
await contentLoop.flush();
|
|
729
|
+
await loop.flush();
|
|
730
|
+
},
|
|
731
|
+
waitForInFlight: async () => {
|
|
732
|
+
await contentLoop.waitForInFlight();
|
|
733
|
+
await loop.waitForInFlight();
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
stop: () => {
|
|
737
|
+
stopped = true;
|
|
738
|
+
contentLoop.stop();
|
|
739
|
+
loop.stop();
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
isFailed: () => failed,
|
|
743
|
+
getLastContent: () => lastSentContent,
|
|
744
|
+
getLastAnswerContent: () => lastAnswerContent,
|
|
745
|
+
getFinalAnswerContent,
|
|
746
|
+
getRenderedBlocks: (options?: { fallbackAnswer?: string; overrideAnswer?: string }) => {
|
|
747
|
+
const blocks = renderTimelineAsBlocks(options);
|
|
748
|
+
if (blocks.length === 0) {
|
|
749
|
+
return "";
|
|
750
|
+
}
|
|
751
|
+
return JSON.stringify(blocks);
|
|
752
|
+
},
|
|
753
|
+
getRenderedContent: (options?: {
|
|
754
|
+
fallbackAnswer?: string;
|
|
755
|
+
overrideAnswer?: string;
|
|
756
|
+
compactProcessAnswerSpacing?: boolean;
|
|
757
|
+
}) => {
|
|
758
|
+
const blocks = renderTimelineAsBlocks(options);
|
|
759
|
+
// Extract markdown from answer blocks (type: 0) and join with double newlines
|
|
760
|
+
const answerTexts = blocks
|
|
761
|
+
.filter((block) => block.type === 0 && "markdown" in block && block.markdown)
|
|
762
|
+
.map((block) => ("markdown" in block ? block.markdown : ""));
|
|
763
|
+
return answerTexts.join("\n\n");
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
streamContent: realTimeStreamEnabled ? streamContentToCard : undefined,
|
|
767
|
+
clearStreamingContent: realTimeStreamEnabled ? clearStreamingContentFromCard : undefined,
|
|
768
|
+
isRealTimeStreamEnabled: () => realTimeStreamEnabled,
|
|
769
|
+
};
|
|
770
|
+
}
|