@soimy/dingtalk 3.4.2 → 3.5.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 +86 -1421
- package/package.json +17 -4
- package/src/ack-reaction-service.ts +45 -27
- package/src/card/card-action-handler.ts +62 -0
- package/src/card/card-run-registry.ts +118 -0
- package/src/card/card-stop-handler.ts +94 -0
- package/src/card/card-template.ts +20 -0
- package/src/card-callback-service.ts +90 -8
- package/src/card-draft-controller.ts +270 -52
- package/src/card-service.ts +198 -138
- package/src/channel.ts +21 -7
- package/src/command/card-stop-command.ts +96 -0
- package/src/config-schema.ts +0 -18
- package/src/config.ts +28 -11
- package/src/feedback-learning-service.ts +4 -6
- package/src/inbound-handler.ts +193 -29
- package/src/message-context-store.ts +74 -0
- package/src/message-utils.ts +22 -0
- package/src/onboarding.ts +4 -77
- package/src/reply-strategy-card.ts +46 -35
- package/src/reply-strategy.ts +4 -3
- package/src/send-service.ts +42 -64
- package/src/targeting/agent-routing.ts +12 -6
- package/src/types.ts +10 -28
|
@@ -1,61 +1,224 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Card draft controller for throttled AI Card streaming updates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 plain markdown
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - DOES enforce single-flight, latest-wins, phase-gated semantics
|
|
11
|
-
* - Does NOT handle tool append, finalize, or markdown fallback —
|
|
12
|
-
* those stay in inbound-handler's deliver callback.
|
|
9
|
+
* It delegates throttling and single-flight transport guarantees to
|
|
10
|
+
* {@link createDraftStreamLoop}.
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
|
-
import {
|
|
13
|
+
import { streamAICard } from "./card-service";
|
|
16
14
|
import { createDraftStreamLoop } from "./draft-stream-loop";
|
|
17
15
|
import type { AICardInstance, Logger } from "./types";
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
type TimelineEntryKind = "thinking" | "tool" | "answer";
|
|
18
|
+
|
|
19
|
+
type TimelineEntry = {
|
|
20
|
+
kind: TimelineEntryKind;
|
|
21
|
+
text: string;
|
|
22
|
+
};
|
|
20
23
|
|
|
21
24
|
export interface CardDraftController {
|
|
22
|
-
updateAnswer: (text: string) => void
|
|
23
|
-
updateReasoning: (text: string) => void
|
|
25
|
+
updateAnswer: (text: string) => Promise<void>;
|
|
26
|
+
updateReasoning: (text: string) => Promise<void>;
|
|
27
|
+
updateThinking: (text: string) => Promise<void>;
|
|
28
|
+
updateTool: (text: string) => Promise<void>;
|
|
29
|
+
appendTool: (text: string) => Promise<void>;
|
|
24
30
|
/** Signal that a new assistant turn has started (e.g. after a tool call). */
|
|
25
|
-
notifyNewAssistantTurn: () => void
|
|
31
|
+
notifyNewAssistantTurn: () => Promise<void>;
|
|
32
|
+
startAssistantTurn: () => Promise<void>;
|
|
26
33
|
flush: () => Promise<void>;
|
|
27
34
|
waitForInFlight: () => Promise<void>;
|
|
28
35
|
stop: () => void;
|
|
29
36
|
isFailed: () => boolean;
|
|
30
|
-
/** Last content sent to card
|
|
37
|
+
/** Last content successfully sent to card. */
|
|
31
38
|
getLastContent: () => string;
|
|
32
|
-
/** Last content sent to card
|
|
39
|
+
/** Last answer-only content successfully sent to card. */
|
|
33
40
|
getLastAnswerContent: () => string;
|
|
41
|
+
/** Current answer-only content composed from all completed answer turns. */
|
|
42
|
+
getFinalAnswerContent: () => string;
|
|
43
|
+
/** Current rendered timeline, including process blocks and answer text. */
|
|
44
|
+
getRenderedContent: (options?: {
|
|
45
|
+
fallbackAnswer?: string;
|
|
46
|
+
overrideAnswer?: string;
|
|
47
|
+
compactProcessAnswerSpacing?: boolean;
|
|
48
|
+
}) => string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeProcessText(text: string | undefined): string {
|
|
52
|
+
return typeof text === "string" ? text.trim() : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeAnswerText(text: string | undefined): string {
|
|
56
|
+
return typeof text === "string" ? text.trimStart() : "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function quoteMarkdown(text: string): string {
|
|
60
|
+
return text
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((line) => line.trim() ? `> ${line.trim()}` : ">")
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderProcessBlock(_kind: "thinking" | "tool", text: string): string {
|
|
67
|
+
return quoteMarkdown(text);
|
|
34
68
|
}
|
|
35
69
|
|
|
36
70
|
export function createCardDraftController(params: {
|
|
37
71
|
card: AICardInstance;
|
|
38
72
|
throttleMs?: number;
|
|
73
|
+
/** Legacy compatibility: verbose mode previously lowered the throttle. */
|
|
74
|
+
verboseMode?: boolean;
|
|
39
75
|
log?: Logger;
|
|
40
76
|
}): CardDraftController {
|
|
41
|
-
let phase: CardDraftPhase = "idle";
|
|
42
77
|
let failed = false;
|
|
43
78
|
let stopped = false;
|
|
44
79
|
let lastSentContent = "";
|
|
45
80
|
let lastAnswerContent = "";
|
|
46
|
-
|
|
47
|
-
let
|
|
81
|
+
|
|
82
|
+
let timelineEntries: TimelineEntry[] = [];
|
|
83
|
+
let activeThinkingIndex: number | null = null;
|
|
84
|
+
let activeAnswerIndex: number | null = null;
|
|
85
|
+
let pendingBoundaryPromise: Promise<void> | null = null;
|
|
86
|
+
|
|
87
|
+
const effectiveThrottleMs = params.throttleMs ?? (params.verboseMode ? 50 : 300);
|
|
88
|
+
|
|
89
|
+
const getFinalAnswerContent = (): string => {
|
|
90
|
+
return timelineEntries
|
|
91
|
+
.filter((entry) => entry.kind === "answer" && entry.text)
|
|
92
|
+
.map((entry) => entry.text)
|
|
93
|
+
.join("\n\n");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const removeTimelineEntry = (index: number) => {
|
|
97
|
+
timelineEntries.splice(index, 1);
|
|
98
|
+
if (activeThinkingIndex !== null) {
|
|
99
|
+
if (activeThinkingIndex === index) {
|
|
100
|
+
activeThinkingIndex = null;
|
|
101
|
+
} else if (activeThinkingIndex > index) {
|
|
102
|
+
activeThinkingIndex -= 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (activeAnswerIndex !== null) {
|
|
106
|
+
if (activeAnswerIndex === index) {
|
|
107
|
+
activeAnswerIndex = null;
|
|
108
|
+
} else if (activeAnswerIndex > index) {
|
|
109
|
+
activeAnswerIndex -= 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const appendTimelineEntry = (kind: TimelineEntryKind, text: string): number => {
|
|
115
|
+
timelineEntries.push({ kind, text });
|
|
116
|
+
return timelineEntries.length - 1;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const renderTimeline = (options: {
|
|
120
|
+
fallbackAnswer?: string;
|
|
121
|
+
overrideAnswer?: string;
|
|
122
|
+
compactProcessAnswerSpacing?: boolean;
|
|
123
|
+
} = {}): string => {
|
|
124
|
+
const entries = timelineEntries.map((entry) => ({ ...entry }));
|
|
125
|
+
|
|
126
|
+
const overrideAnswer = normalizeAnswerText(options.overrideAnswer);
|
|
127
|
+
if (overrideAnswer) {
|
|
128
|
+
const lastAnswerIndex = [...entries]
|
|
129
|
+
.map((entry, index) => ({ entry, index }))
|
|
130
|
+
.toReversed()
|
|
131
|
+
.find(({ entry }) => entry.kind === "answer")?.index;
|
|
132
|
+
if (lastAnswerIndex !== undefined) {
|
|
133
|
+
entries[lastAnswerIndex] = { kind: "answer", text: overrideAnswer };
|
|
134
|
+
} else {
|
|
135
|
+
entries.push({ kind: "answer", text: overrideAnswer });
|
|
136
|
+
}
|
|
137
|
+
} else if (!entries.some((entry) => entry.kind === "answer" && entry.text)) {
|
|
138
|
+
const fallbackAnswer = normalizeAnswerText(options.fallbackAnswer);
|
|
139
|
+
if (fallbackAnswer) {
|
|
140
|
+
entries.push({ kind: "answer", text: fallbackAnswer });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let rendered = "";
|
|
145
|
+
const compactProcessAnswerSpacing = options.compactProcessAnswerSpacing === true;
|
|
146
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
147
|
+
const entry = entries[index];
|
|
148
|
+
if (!entry?.text) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const part = entry.kind === "answer"
|
|
152
|
+
? entry.text
|
|
153
|
+
: renderProcessBlock(entry.kind, entry.text);
|
|
154
|
+
if (!rendered) {
|
|
155
|
+
rendered = part;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const previousKind = entries[index - 1]?.kind;
|
|
159
|
+
const separator =
|
|
160
|
+
compactProcessAnswerSpacing && previousKind
|
|
161
|
+
? "\n"
|
|
162
|
+
: "\n\n";
|
|
163
|
+
rendered += `${separator}${part}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return rendered;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const sealLiveThinking = () => {
|
|
170
|
+
activeThinkingIndex = null;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const sealCurrentAnswer = () => {
|
|
174
|
+
activeAnswerIndex = null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const queueRender = () => {
|
|
178
|
+
const rendered = renderTimeline({ compactProcessAnswerSpacing: true });
|
|
179
|
+
if (rendered) {
|
|
180
|
+
loop.update(rendered);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
loop.resetPending();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const flushBoundaryFrame = async () => {
|
|
187
|
+
if (stopped || failed) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await loop.flush();
|
|
191
|
+
await loop.waitForInFlight();
|
|
192
|
+
loop.resetThrottleWindow();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const beginBoundaryFlush = () => {
|
|
196
|
+
if (pendingBoundaryPromise) {
|
|
197
|
+
return pendingBoundaryPromise;
|
|
198
|
+
}
|
|
199
|
+
const current = flushBoundaryFrame().finally(() => {
|
|
200
|
+
if (pendingBoundaryPromise === current) {
|
|
201
|
+
pendingBoundaryPromise = null;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
pendingBoundaryPromise = current;
|
|
205
|
+
return current;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const waitForPendingBoundary = async () => {
|
|
209
|
+
if (pendingBoundaryPromise) {
|
|
210
|
+
await pendingBoundaryPromise;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
48
213
|
|
|
49
214
|
const loop = createDraftStreamLoop({
|
|
50
|
-
throttleMs:
|
|
215
|
+
throttleMs: effectiveThrottleMs,
|
|
51
216
|
isStopped: () => stopped || failed,
|
|
52
217
|
sendOrEditStreamMessage: async (content: string) => {
|
|
53
218
|
try {
|
|
54
219
|
await streamAICard(params.card, content, false, params.log);
|
|
55
220
|
lastSentContent = content;
|
|
56
|
-
|
|
57
|
-
lastAnswerContent = content;
|
|
58
|
-
}
|
|
221
|
+
lastAnswerContent = getFinalAnswerContent();
|
|
59
222
|
} catch (err: unknown) {
|
|
60
223
|
failed = true;
|
|
61
224
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -64,41 +227,94 @@ export function createCardDraftController(params: {
|
|
|
64
227
|
},
|
|
65
228
|
});
|
|
66
229
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
230
|
+
const updateReasoning = async (text: string) => {
|
|
231
|
+
await waitForPendingBoundary();
|
|
232
|
+
if (stopped || failed || activeAnswerIndex !== null) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const normalized = normalizeProcessText(text);
|
|
236
|
+
if (!normalized) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (activeThinkingIndex === null && timelineEntries.length > 0) {
|
|
240
|
+
const lastKind = timelineEntries.at(-1)?.kind;
|
|
241
|
+
if (lastKind && lastKind !== "thinking") {
|
|
242
|
+
await flushBoundaryFrame();
|
|
74
243
|
}
|
|
75
|
-
}
|
|
244
|
+
}
|
|
245
|
+
if (activeThinkingIndex !== null) {
|
|
246
|
+
timelineEntries[activeThinkingIndex] = { kind: "thinking", text: normalized };
|
|
247
|
+
} else {
|
|
248
|
+
activeThinkingIndex = appendTimelineEntry("thinking", normalized);
|
|
249
|
+
}
|
|
250
|
+
queueRender();
|
|
251
|
+
};
|
|
76
252
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
253
|
+
const updateAnswer = async (text: string) => {
|
|
254
|
+
await waitForPendingBoundary();
|
|
255
|
+
if (stopped || failed) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const normalized = normalizeAnswerText(text);
|
|
259
|
+
if (!normalized.trim()) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (activeAnswerIndex === null && timelineEntries.length > 0) {
|
|
263
|
+
const lastKind = timelineEntries.at(-1)?.kind;
|
|
264
|
+
if (lastKind && lastKind !== "answer") {
|
|
265
|
+
await flushBoundaryFrame();
|
|
86
266
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
}
|
|
267
|
+
}
|
|
268
|
+
sealLiveThinking();
|
|
269
|
+
if (activeAnswerIndex !== null) {
|
|
270
|
+
timelineEntries[activeAnswerIndex] = { kind: "answer", text: normalized };
|
|
271
|
+
} else {
|
|
272
|
+
activeAnswerIndex = appendTimelineEntry("answer", normalized);
|
|
273
|
+
}
|
|
274
|
+
queueRender();
|
|
275
|
+
};
|
|
92
276
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
277
|
+
const updateTool = async (text: string) => {
|
|
278
|
+
await waitForPendingBoundary();
|
|
279
|
+
if (stopped || failed) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const normalized = normalizeProcessText(text);
|
|
283
|
+
if (!normalized) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (timelineEntries.length > 0) {
|
|
287
|
+
await flushBoundaryFrame();
|
|
288
|
+
}
|
|
289
|
+
sealLiveThinking();
|
|
290
|
+
sealCurrentAnswer();
|
|
291
|
+
appendTimelineEntry("tool", normalized);
|
|
292
|
+
queueRender();
|
|
293
|
+
};
|
|
101
294
|
|
|
295
|
+
const notifyNewAssistantTurn = async () => {
|
|
296
|
+
if (stopped || failed) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (activeAnswerIndex !== null) {
|
|
300
|
+
sealCurrentAnswer();
|
|
301
|
+
await beginBoundaryFlush();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (activeThinkingIndex !== null) {
|
|
305
|
+
removeTimelineEntry(activeThinkingIndex);
|
|
306
|
+
loop.resetPending();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
updateAnswer,
|
|
312
|
+
updateReasoning,
|
|
313
|
+
updateThinking: updateReasoning,
|
|
314
|
+
updateTool,
|
|
315
|
+
appendTool: updateTool,
|
|
316
|
+
notifyNewAssistantTurn,
|
|
317
|
+
startAssistantTurn: notifyNewAssistantTurn,
|
|
102
318
|
flush: () => loop.flush(),
|
|
103
319
|
waitForInFlight: () => loop.waitForInFlight(),
|
|
104
320
|
|
|
@@ -110,5 +326,7 @@ export function createCardDraftController(params: {
|
|
|
110
326
|
isFailed: () => failed,
|
|
111
327
|
getLastContent: () => lastSentContent,
|
|
112
328
|
getLastAnswerContent: () => lastAnswerContent,
|
|
329
|
+
getFinalAnswerContent,
|
|
330
|
+
getRenderedContent: renderTimeline,
|
|
113
331
|
};
|
|
114
332
|
}
|