@soimy/dingtalk 3.6.10 → 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.
Files changed (163) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +11655 -10324
  3. package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
  4. package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
  5. package/dist/src/card/ask-user-question-context.d.ts +1 -1
  6. package/dist/src/card/ask-user-question-store.d.ts +1 -1
  7. package/dist/src/card/ask-user-question.d.ts +1 -1
  8. package/dist/src/card/card-action-handler.d.ts +2 -2
  9. package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
  10. package/dist/src/card/card-run-registry.d.ts +2 -2
  11. package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
  12. package/dist/src/card/card-stop-handler.d.ts +1 -1
  13. package/dist/src/card/card-streaming-mode.d.ts +1 -1
  14. package/dist/src/card/card-task-progress.d.ts +60 -0
  15. package/dist/src/channel.d.ts +6 -6
  16. package/dist/src/command/card-stop-command.d.ts +1 -1
  17. package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
  18. package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
  19. package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
  20. package/dist/src/gateway/channel-gateway.d.ts +1 -1
  21. package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
  22. package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +5 -5
  23. package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
  24. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
  25. package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
  26. package/dist/src/messaging/btw-deliver.d.ts +1 -1
  27. package/dist/src/messaging/channel-outbound.d.ts +1 -1
  28. package/dist/src/messaging/inline-directives.d.ts +42 -0
  29. package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
  30. package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
  31. package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
  32. package/dist/src/messaging/quoted-context.d.ts +2 -2
  33. package/dist/src/messaging/quoted-file-service.d.ts +1 -1
  34. package/dist/src/messaging/quoted-ref.d.ts +2 -2
  35. package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
  36. package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
  37. package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
  38. package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
  39. package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
  40. package/dist/src/platform/channel-status.d.ts +1 -1
  41. package/dist/src/platform/config.d.ts +110 -0
  42. package/dist/src/platform/runtime-events.d.ts +67 -0
  43. package/dist/src/{types.d.ts → platform/types.d.ts} +75 -2
  44. package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
  45. package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
  46. package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
  47. package/dist/src/targeting/agent-routing.d.ts +3 -3
  48. package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
  49. package/index.ts +193 -69
  50. package/openclaw.plugin.json +158 -2
  51. package/package.json +16 -16
  52. package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
  53. package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
  54. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
  55. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
  56. package/src/card/ask-user-question-context.ts +1 -1
  57. package/src/card/ask-user-question-store.ts +2 -2
  58. package/src/card/ask-user-question.ts +11 -7
  59. package/src/card/card-action-handler.ts +2 -2
  60. package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
  61. package/src/card/card-draft-controller.ts +770 -0
  62. package/src/card/card-markdown-image-reroute.ts +8 -2
  63. package/src/card/card-run-registry.ts +17 -7
  64. package/src/{card-service.ts → card/card-service.ts} +91 -61
  65. package/src/card/card-stop-handler.ts +12 -8
  66. package/src/card/card-streaming-mode.ts +1 -1
  67. package/src/card/card-task-progress.ts +330 -0
  68. package/src/card/draft-stream-loop.ts +119 -0
  69. package/src/card/reasoning-answer-split.ts +125 -121
  70. package/src/card/reasoning-block-assembler.ts +122 -123
  71. package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
  72. package/src/card/statusline-renderer.ts +14 -5
  73. package/src/card/task-model-metadata.ts +8 -3
  74. package/src/channel.ts +14 -16
  75. package/src/command/card-stop-command.ts +2 -2
  76. package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
  77. package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
  78. package/src/command/inbound-command-dispatch-service.ts +12 -6
  79. package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
  80. package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
  81. package/src/gateway/channel-gateway.ts +106 -28
  82. package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
  83. package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
  84. package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +144 -85
  85. package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
  86. package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
  87. package/src/messaging/attachment-text-extractor.ts +21 -7
  88. package/src/messaging/btw-deliver.ts +2 -2
  89. package/src/messaging/channel-actions.ts +15 -7
  90. package/src/messaging/channel-outbound.ts +16 -11
  91. package/src/messaging/inline-directives.ts +342 -0
  92. package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
  93. package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
  94. package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
  95. package/src/messaging/quoted-context.ts +2 -2
  96. package/src/messaging/quoted-file-service.ts +311 -294
  97. package/src/messaging/quoted-ref.ts +11 -7
  98. package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
  99. package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
  100. package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
  101. package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
  102. package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
  103. package/src/{send-service.ts → messaging/send-service.ts} +100 -65
  104. package/src/{auth.ts → platform/auth.ts} +2 -2
  105. package/src/platform/channel-status.ts +3 -3
  106. package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
  107. package/src/{config.ts → platform/config.ts} +167 -19
  108. package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
  109. package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
  110. package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
  111. package/src/platform/runtime-events.ts +202 -0
  112. package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
  113. package/src/{types.ts → platform/types.ts} +77 -2
  114. package/src/{http-client.ts → shared/http-client.ts} +1 -3
  115. package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
  116. package/src/{utils.ts → shared/utils.ts} +30 -13
  117. package/src/targeting/agent-name-matcher.ts +1 -1
  118. package/src/targeting/agent-routing.ts +7 -7
  119. package/src/targeting/group-members-store.ts +1 -1
  120. package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
  121. package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
  122. package/src/targeting/target-directory-adapter.ts +3 -3
  123. package/src/targeting/target-directory-store.ts +1 -1
  124. package/src/targeting/target-input.ts +1 -1
  125. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
  126. package/dist/src/config.d.ts +0 -59
  127. package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
  128. package/src/card-draft-controller.ts +0 -637
  129. package/src/draft-stream-loop.ts +0 -119
  130. /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
  131. /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
  132. /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
  133. /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
  134. /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
  135. /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
  136. /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
  137. /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
  138. /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
  139. /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
  140. /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
  141. /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
  142. /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
  143. /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
  144. /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
  145. /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
  146. /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
  147. /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
  148. /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
  149. /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
  150. /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
  151. /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
  152. /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
  153. /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
  154. /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
  155. /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
  156. /package/src/{access-control.ts → platform/access-control.ts} +0 -0
  157. /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
  158. /package/src/{runtime.ts → platform/runtime.ts} +0 -0
  159. /package/src/{session-state.ts → platform/session-state.ts} +0 -0
  160. /package/src/{signature.ts → platform/signature.ts} +0 -0
  161. /package/src/{dedup.ts → shared/dedup.ts} +0 -0
  162. /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
  163. /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
@@ -1,637 +0,0 @@
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 {
14
- clearAICardStreamingContent,
15
- streamAICardContent,
16
- updateAICardBlockList,
17
- } from "./card-service";
18
- import { createDraftStreamLoop } from "./draft-stream-loop";
19
- import type { AICardInstance, CardBlock, Logger } from "./types";
20
-
21
- type TimelineEntryKind = "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
- updateAnswer: (text: string, options?: { stream?: boolean; renderBlocks?: boolean }) => Promise<void>;
38
- updateReasoning: (text: string) => Promise<void>;
39
- updateThinking: (text: string) => Promise<void>;
40
- appendThinkingBlock: (text: string) => Promise<void>;
41
- updateTool: (text: string) => Promise<void>;
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;
48
- /** Signal that a new assistant turn has started (e.g. after a tool call). */
49
- notifyNewAssistantTurn: (options?: {
50
- discardActiveAnswer?: boolean;
51
- }) => Promise<void>;
52
- startAssistantTurn: () => Promise<void>;
53
- /** Seal the active thinking entry (keep it in timeline) without removing it. */
54
- sealActiveThinking: () => Promise<void>;
55
- flush: () => Promise<void>;
56
- waitForInFlight: () => Promise<void>;
57
- stop: () => void;
58
- isFailed: () => boolean;
59
- /** Last content successfully sent to card. */
60
- getLastContent: () => string;
61
- /** Last answer-only content successfully sent to card. */
62
- getLastAnswerContent: () => string;
63
- /** Current answer-only content composed from all completed answer turns. */
64
- getFinalAnswerContent: () => string;
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. */
72
- getRenderedContent: (options?: {
73
- fallbackAnswer?: string;
74
- overrideAnswer?: string;
75
- compactProcessAnswerSpacing?: boolean;
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;
83
- }
84
-
85
- function normalizeProcessText(text: string | undefined): string {
86
- return typeof text === "string" ? text.trim() : "";
87
- }
88
-
89
- function normalizeAnswerText(text: string | undefined): string {
90
- return typeof text === "string" ? text.trimStart() : "";
91
- }
92
-
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>`)
97
- .join("\n");
98
- }
99
-
100
- export function createCardDraftController(params: {
101
- card: AICardInstance;
102
- throttleMs?: number;
103
- /** Legacy compatibility: verbose mode previously lowered the throttle. */
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;
109
- log?: Logger;
110
- }): CardDraftController {
111
- let failed = false;
112
- let stopped = false;
113
- let lastSentContent = "";
114
- let lastQueuedContent = "";
115
- let inFlightContent = "";
116
- let lastAnswerContent = "";
117
- let lastSentStreamingContent = "";
118
- let lastQueuedStreamingContent = "";
119
- let inFlightStreamingContent = "";
120
-
121
- let timelineEntries: TimelineEntry[] = [];
122
- let activeThinkingIndex: number | null = null;
123
- let activeAnswerIndex: number | null = null;
124
- let pendingBoundaryPromise: Promise<void> | null = null;
125
-
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
- };
173
-
174
- const getFinalAnswerContent = (): string => {
175
- return timelineEntries
176
- .filter((entry) => entry.kind === "answer" && entry.text)
177
- .map((entry) => entry.text)
178
- .join("\n\n");
179
- };
180
-
181
- const removeTimelineEntry = (index: number) => {
182
- timelineEntries.splice(index, 1);
183
- if (activeThinkingIndex !== null) {
184
- if (activeThinkingIndex === index) {
185
- activeThinkingIndex = null;
186
- } else if (activeThinkingIndex > index) {
187
- activeThinkingIndex -= 1;
188
- }
189
- }
190
- if (activeAnswerIndex !== null) {
191
- if (activeAnswerIndex === index) {
192
- activeAnswerIndex = null;
193
- } else if (activeAnswerIndex > index) {
194
- activeAnswerIndex -= 1;
195
- }
196
- }
197
- };
198
-
199
- const appendTimelineEntry = (kind: TimelineEntryKind, text: string): number => {
200
- timelineEntries.push({ kind, text });
201
- return timelineEntries.length - 1;
202
- };
203
-
204
- const findCurrentSegmentAnswerIndex = (): number | null => {
205
- return activeAnswerIndex;
206
- };
207
-
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: {
218
- fallbackAnswer?: string;
219
- overrideAnswer?: string;
220
- } = {}): CardBlock[] => {
221
- const entries = timelineEntries.map((entry) => ({ ...entry }));
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
-
232
- const overrideAnswer = normalizeAnswerText(options.overrideAnswer);
233
- if (overrideAnswer) {
234
- const lastAnswerIndex = [...entries]
235
- .map((entry, index) => ({ entry, index }))
236
- .toReversed()
237
- .find(({ entry }) => entry.kind === "answer")?.index;
238
- if (lastAnswerIndex !== undefined) {
239
- entries[lastAnswerIndex] = { kind: "answer", text: overrideAnswer };
240
- } else {
241
- insertAnswerEntry(overrideAnswer);
242
- }
243
- } else if (!entries.some((entry) => entry.kind === "answer" && entry.text)) {
244
- const fallbackAnswer = normalizeAnswerText(options.fallbackAnswer);
245
- if (fallbackAnswer) {
246
- insertAnswerEntry(fallbackAnswer);
247
- }
248
- }
249
-
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;
278
- }
279
- }
280
- return blocks;
281
- };
282
-
283
- const sealLiveThinking = () => {
284
- activeThinkingIndex = null;
285
- };
286
-
287
- const sealCurrentAnswer = () => {
288
- activeAnswerIndex = null;
289
- };
290
-
291
- const discardCurrentAnswer = () => {
292
- if (activeAnswerIndex === null) {
293
- return;
294
- }
295
- removeTimelineEntry(activeAnswerIndex);
296
- queueRender();
297
- };
298
-
299
- const clearPendingRender = () => {
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);
325
- };
326
-
327
- const flushBoundaryFrame = async () => {
328
- if (stopped || failed) {
329
- return;
330
- }
331
- // Clear the live streaming content before committing blockList at the boundary.
332
- // This drops any queued fake-streaming tail so the empty stream update can stop
333
- // the animation before the blockList frame lands.
334
- if (hasStreamingContent) {
335
- await clearStreamingContentFromCard();
336
- }
337
- await loop.flush();
338
- await loop.waitForInFlight();
339
- contentLoop.resetThrottleWindow();
340
- loop.resetThrottleWindow();
341
- };
342
-
343
- const beginBoundaryFlush = () => {
344
- if (pendingBoundaryPromise) {
345
- return pendingBoundaryPromise;
346
- }
347
- const current = flushBoundaryFrame().finally(() => {
348
- if (pendingBoundaryPromise === current) {
349
- pendingBoundaryPromise = null;
350
- }
351
- });
352
- pendingBoundaryPromise = current;
353
- return current;
354
- };
355
-
356
- const waitForPendingBoundary = async () => {
357
- if (pendingBoundaryPromise) {
358
- await pendingBoundaryPromise;
359
- }
360
- };
361
-
362
- const loop = createDraftStreamLoop({
363
- throttleMs: effectiveThrottleMs,
364
- isStopped: () => stopped || failed,
365
- sendOrEditStreamMessage: async (content: string) => {
366
- inFlightContent = content;
367
- try {
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);
371
- lastSentContent = content;
372
- lastQueuedContent = "";
373
- lastAnswerContent = getFinalAnswerContent();
374
- } catch (err: unknown) {
375
- failed = true;
376
- const message = err instanceof Error ? err.message : String(err);
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
- }
404
- }
405
- },
406
- });
407
-
408
- const updateReasoning = async (text: string) => {
409
- await waitForPendingBoundary();
410
- if (stopped || failed || activeAnswerIndex !== null) {
411
- return;
412
- }
413
- const normalized = normalizeProcessText(text);
414
- if (!normalized) {
415
- return;
416
- }
417
- if (activeThinkingIndex === null && timelineEntries.length > 0) {
418
- const lastKind = timelineEntries.at(-1)?.kind;
419
- if (lastKind && lastKind !== "thinking") {
420
- await flushBoundaryFrame();
421
- }
422
- }
423
- if (activeThinkingIndex !== null) {
424
- timelineEntries[activeThinkingIndex] = { kind: "thinking", text: normalized };
425
- } else {
426
- activeThinkingIndex = appendTimelineEntry("thinking", normalized);
427
- }
428
- queueRender();
429
- };
430
-
431
- const updateAnswer = async (text: string, options: { stream?: boolean; renderBlocks?: boolean } = {}) => {
432
- await waitForPendingBoundary();
433
- if (stopped || failed) {
434
- return;
435
- }
436
- const normalized = normalizeAnswerText(text);
437
- if (!normalized.trim()) {
438
- return;
439
- }
440
- if (activeAnswerIndex === null && timelineEntries.length > 0) {
441
- const lastKind = timelineEntries.at(-1)?.kind;
442
- if (lastKind && lastKind !== "answer") {
443
- await flushBoundaryFrame();
444
- }
445
- }
446
- sealLiveThinking();
447
- if (activeAnswerIndex !== null) {
448
- timelineEntries[activeAnswerIndex] = { kind: "answer", text: normalized };
449
- } else {
450
- activeAnswerIndex = appendTimelineEntry("answer", normalized);
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
- }
462
- queueRender();
463
- };
464
-
465
- const updateTool = async (text: string) => {
466
- await waitForPendingBoundary();
467
- if (stopped || failed) {
468
- return;
469
- }
470
- const normalized = normalizeProcessText(text);
471
- if (!normalized) {
472
- return;
473
- }
474
- if (timelineEntries.length > 0) {
475
- await flushBoundaryFrame();
476
- }
477
- sealLiveThinking();
478
- sealCurrentAnswer();
479
- appendTimelineEntry("tool", normalized);
480
- queueRender();
481
- };
482
-
483
- const appendThinkingBlock = async (text: string) => {
484
- await waitForPendingBoundary();
485
- if (stopped || failed) {
486
- return;
487
- }
488
- const normalized = normalizeProcessText(text);
489
- if (!normalized) {
490
- return;
491
- }
492
- if (timelineEntries.length > 0) {
493
- await flushBoundaryFrame();
494
- }
495
- sealLiveThinking();
496
- const currentSegmentAnswerIndex = findCurrentSegmentAnswerIndex();
497
- if (currentSegmentAnswerIndex !== null) {
498
- timelineEntries.splice(currentSegmentAnswerIndex, 0, { kind: "thinking", text: normalized });
499
- if (activeAnswerIndex !== null && activeAnswerIndex >= currentSegmentAnswerIndex) {
500
- activeAnswerIndex += 1;
501
- }
502
- } else {
503
- sealCurrentAnswer();
504
- appendTimelineEntry("thinking", normalized);
505
- }
506
- queueRender();
507
- };
508
-
509
- const appendToolBeforeCurrentAnswer = async (text: string) => {
510
- await waitForPendingBoundary();
511
- if (stopped || failed) {
512
- return;
513
- }
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 {
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
- queueRender();
550
- }
551
- await beginBoundaryFlush();
552
- return;
553
- }
554
- if (activeThinkingIndex !== null) {
555
- removeTimelineEntry(activeThinkingIndex);
556
- clearPendingRender();
557
- }
558
- };
559
-
560
- const appendImageBlock = async (mediaId: string, text = "") => {
561
- await waitForPendingBoundary();
562
- if (stopped || failed) {
563
- return;
564
- }
565
- if (!mediaId.trim()) {
566
- return;
567
- }
568
- if (timelineEntries.length > 0) {
569
- await flushBoundaryFrame();
570
- }
571
- sealLiveThinking();
572
- sealCurrentAnswer();
573
- timelineEntries.push({ kind: "image", text, mediaId });
574
- queueRender();
575
- };
576
-
577
- return {
578
- updateAnswer,
579
- updateReasoning,
580
- updateThinking: updateReasoning,
581
- appendThinkingBlock,
582
- updateTool,
583
- appendTool: updateTool,
584
- appendImageBlock,
585
- appendToolBeforeCurrentAnswer,
586
- discardCurrentAnswer,
587
- notifyNewAssistantTurn,
588
- startAssistantTurn: notifyNewAssistantTurn,
589
- sealActiveThinking: async () => {
590
- if (stopped || failed) {
591
- return;
592
- }
593
- if (activeThinkingIndex !== null) {
594
- sealLiveThinking();
595
- await beginBoundaryFlush();
596
- }
597
- },
598
- flush: async () => {
599
- await contentLoop.flush();
600
- await loop.flush();
601
- },
602
- waitForInFlight: async () => {
603
- await contentLoop.waitForInFlight();
604
- await loop.waitForInFlight();
605
- },
606
-
607
- stop: () => {
608
- stopped = true;
609
- contentLoop.stop();
610
- loop.stop();
611
- },
612
-
613
- isFailed: () => failed,
614
- getLastContent: () => lastSentContent,
615
- getLastAnswerContent: () => lastAnswerContent,
616
- getFinalAnswerContent,
617
- getRenderedBlocks: (options?: { fallbackAnswer?: string; overrideAnswer?: string }) => {
618
- const blocks = renderTimelineAsBlocks(options);
619
- if (blocks.length === 0) {
620
- return "";
621
- }
622
- return JSON.stringify(blocks);
623
- },
624
- getRenderedContent: (options?: { fallbackAnswer?: string; overrideAnswer?: string; compactProcessAnswerSpacing?: boolean }) => {
625
- const blocks = renderTimelineAsBlocks(options);
626
- // Extract markdown from answer blocks (type: 0) and join with double newlines
627
- const answerTexts = blocks
628
- .filter((block) => block.type === 0 && "markdown" in block && block.markdown)
629
- .map((block) => ("markdown" in block ? block.markdown : ""));
630
- return answerTexts.join("\n\n");
631
- },
632
-
633
- streamContent: realTimeStreamEnabled ? streamContentToCard : undefined,
634
- clearStreamingContent: realTimeStreamEnabled ? clearStreamingContentFromCard : undefined,
635
- isRealTimeStreamEnabled: () => realTimeStreamEnabled,
636
- };
637
- }