@soimy/dingtalk 3.6.6 → 3.6.8
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.js +1433 -344
- package/dist/index.js.map +4 -4
- package/dist/src/card/ask-user-question-context.d.ts +5 -2
- package/dist/src/card/ask-user-question-context.d.ts.map +1 -1
- package/dist/src/card/ask-user-question-store.d.ts +42 -0
- package/dist/src/card/ask-user-question-store.d.ts.map +1 -0
- package/dist/src/card/ask-user-question.d.ts +20 -0
- package/dist/src/card/ask-user-question.d.ts.map +1 -1
- package/dist/src/card/card-action-handler.d.ts +1 -0
- package/dist/src/card/card-action-handler.d.ts.map +1 -1
- package/dist/src/card-service.d.ts +4 -1
- package/dist/src/card-service.d.ts.map +1 -1
- package/dist/src/gateway/channel-gateway.d.ts.map +1 -1
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +23 -0
- package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts.map +1 -0
- package/dist/src/gateway/inbound-session-queue.d.ts +46 -0
- package/dist/src/gateway/inbound-session-queue.d.ts.map +1 -0
- package/dist/src/gateway/reply-session-conflict.d.ts +22 -0
- package/dist/src/gateway/reply-session-conflict.d.ts.map +1 -0
- package/dist/src/inbound-handler.d.ts.map +1 -1
- package/dist/src/onboarding.d.ts.map +1 -1
- package/dist/src/targeting/agent-routing.d.ts +17 -1
- package/dist/src/targeting/agent-routing.d.ts.map +1 -1
- package/dist/src/types.d.ts +35 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/access-control.ts +1 -1
- package/src/card/ask-user-question-context.ts +9 -1
- package/src/card/ask-user-question-store.ts +294 -0
- package/src/card/ask-user-question.ts +398 -37
- package/src/card/card-action-handler.ts +2 -0
- package/src/card-service.ts +55 -3
- package/src/gateway/channel-gateway.ts +51 -30
- package/src/gateway/inbound-session-queue-dispatcher.ts +304 -0
- package/src/gateway/inbound-session-queue.ts +244 -0
- package/src/gateway/reply-session-conflict.ts +82 -0
- package/src/inbound-handler.ts +254 -57
- package/src/onboarding.ts +6 -2
- package/src/targeting/agent-routing.ts +84 -19
- package/src/types.ts +36 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { readNamespaceJson, writeNamespaceJsonAtomic } from "../persistence-store";
|
|
2
|
+
import type { Logger } from "../types";
|
|
3
|
+
|
|
4
|
+
const ASK_USER_LIFECYCLE_NAMESPACE = "cards.ask-user.lifecycle";
|
|
5
|
+
const ACTIVE_TTL_MS = 5 * 60 * 1_000;
|
|
6
|
+
const TOMBSTONE_TTL_MS = 30 * 60 * 1_000;
|
|
7
|
+
|
|
8
|
+
export type AskUserActiveState = "reserved" | "pending" | "dispatching";
|
|
9
|
+
|
|
10
|
+
export type AskUserTerminalReason =
|
|
11
|
+
| "delivery_failed"
|
|
12
|
+
| "superseded_by_question"
|
|
13
|
+
| "superseded_by_message"
|
|
14
|
+
| "expired"
|
|
15
|
+
| "cancelled"
|
|
16
|
+
| "empty"
|
|
17
|
+
| "submitted"
|
|
18
|
+
| "pause_failed"
|
|
19
|
+
| "restart_invalidated"
|
|
20
|
+
| "restart_during_dispatch"
|
|
21
|
+
| "dispatch_failed";
|
|
22
|
+
|
|
23
|
+
export interface AskUserLifecycleRecord {
|
|
24
|
+
questionId: string;
|
|
25
|
+
accountId: string;
|
|
26
|
+
questionScopeKey: string;
|
|
27
|
+
outTrackId: string;
|
|
28
|
+
title: string;
|
|
29
|
+
state: AskUserActiveState | "terminal";
|
|
30
|
+
terminalReason?: AskUserTerminalReason;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
updatedAt: number;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AskUserLifecycleState {
|
|
37
|
+
version: 1;
|
|
38
|
+
updatedAt: number;
|
|
39
|
+
records: AskUserLifecycleRecord[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AskUserStoreOptions {
|
|
43
|
+
storePath: string;
|
|
44
|
+
accountId: string;
|
|
45
|
+
log?: Logger;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReserveAskUserQuestionInput {
|
|
50
|
+
questionId: string;
|
|
51
|
+
questionScopeKey: string;
|
|
52
|
+
outTrackId: string;
|
|
53
|
+
title: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AskUserQuestionIdentifier {
|
|
57
|
+
questionId?: string;
|
|
58
|
+
outTrackId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function now(options: AskUserStoreOptions): number {
|
|
62
|
+
return options.now?.() ?? Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emptyState(timestamp: number): AskUserLifecycleState {
|
|
66
|
+
return { version: 1, updatedAt: timestamp, records: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isActiveState(state: AskUserLifecycleRecord["state"]): state is AskUserActiveState {
|
|
70
|
+
return state === "reserved" || state === "pending" || state === "dispatching";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isAnswerableState(state: AskUserLifecycleRecord["state"]): boolean {
|
|
74
|
+
return state === "reserved" || state === "pending";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadState(options: AskUserStoreOptions): AskUserLifecycleState {
|
|
78
|
+
const timestamp = now(options);
|
|
79
|
+
const state = readNamespaceJson<AskUserLifecycleState>(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
80
|
+
storePath: options.storePath,
|
|
81
|
+
scope: { accountId: options.accountId },
|
|
82
|
+
fallback: emptyState(timestamp),
|
|
83
|
+
log: options.log,
|
|
84
|
+
});
|
|
85
|
+
if (state.version !== 1 || !Array.isArray(state.records)) {
|
|
86
|
+
return emptyState(timestamp);
|
|
87
|
+
}
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function persistState(options: AskUserStoreOptions, state: AskUserLifecycleState): void {
|
|
92
|
+
state.updatedAt = now(options);
|
|
93
|
+
writeNamespaceJsonAtomic(ASK_USER_LIFECYCLE_NAMESPACE, {
|
|
94
|
+
storePath: options.storePath,
|
|
95
|
+
scope: { accountId: options.accountId },
|
|
96
|
+
data: state,
|
|
97
|
+
log: options.log,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toTerminal(
|
|
102
|
+
record: AskUserLifecycleRecord,
|
|
103
|
+
reason: AskUserTerminalReason,
|
|
104
|
+
timestamp: number,
|
|
105
|
+
): AskUserLifecycleRecord {
|
|
106
|
+
record.state = "terminal";
|
|
107
|
+
record.terminalReason = reason;
|
|
108
|
+
record.updatedAt = timestamp;
|
|
109
|
+
record.expiresAt = timestamp + TOMBSTONE_TTL_MS;
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cleanupState(state: AskUserLifecycleState, timestamp: number): boolean {
|
|
114
|
+
let changed = false;
|
|
115
|
+
for (const record of state.records) {
|
|
116
|
+
if (isAnswerableState(record.state) && record.expiresAt <= timestamp) {
|
|
117
|
+
toTerminal(record, "expired", timestamp);
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const retained = state.records.filter(
|
|
122
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp,
|
|
123
|
+
);
|
|
124
|
+
if (retained.length !== state.records.length) {
|
|
125
|
+
state.records = retained;
|
|
126
|
+
changed = true;
|
|
127
|
+
}
|
|
128
|
+
return changed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readCleanState(options: AskUserStoreOptions): AskUserLifecycleState {
|
|
132
|
+
const state = loadState(options);
|
|
133
|
+
if (cleanupState(state, now(options))) {
|
|
134
|
+
persistState(options, state);
|
|
135
|
+
}
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findRecord(
|
|
140
|
+
state: AskUserLifecycleState,
|
|
141
|
+
identifier: AskUserQuestionIdentifier,
|
|
142
|
+
): AskUserLifecycleRecord | undefined {
|
|
143
|
+
if (identifier.outTrackId) {
|
|
144
|
+
const byTrackId = state.records.find((record) => record.outTrackId === identifier.outTrackId);
|
|
145
|
+
if (byTrackId) {
|
|
146
|
+
return byTrackId;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (identifier.questionId) {
|
|
150
|
+
return state.records.find((record) => record.questionId === identifier.questionId);
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function reserveAskUserQuestion(
|
|
156
|
+
options: AskUserStoreOptions,
|
|
157
|
+
input: ReserveAskUserQuestionInput,
|
|
158
|
+
): AskUserLifecycleRecord {
|
|
159
|
+
const timestamp = now(options);
|
|
160
|
+
const state = readCleanState(options);
|
|
161
|
+
const record: AskUserLifecycleRecord = {
|
|
162
|
+
questionId: input.questionId,
|
|
163
|
+
accountId: options.accountId,
|
|
164
|
+
questionScopeKey: input.questionScopeKey,
|
|
165
|
+
outTrackId: input.outTrackId,
|
|
166
|
+
title: input.title,
|
|
167
|
+
state: "reserved",
|
|
168
|
+
createdAt: timestamp,
|
|
169
|
+
updatedAt: timestamp,
|
|
170
|
+
expiresAt: timestamp + ACTIVE_TTL_MS,
|
|
171
|
+
};
|
|
172
|
+
state.records = state.records.filter(
|
|
173
|
+
(item) => item.questionId !== input.questionId && item.outTrackId !== input.outTrackId,
|
|
174
|
+
);
|
|
175
|
+
state.records.push(record);
|
|
176
|
+
persistState(options, state);
|
|
177
|
+
return { ...record };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function activateAskUserQuestion(
|
|
181
|
+
options: AskUserStoreOptions,
|
|
182
|
+
questionId: string,
|
|
183
|
+
): { record?: AskUserLifecycleRecord; superseded: AskUserLifecycleRecord[] } {
|
|
184
|
+
const timestamp = now(options);
|
|
185
|
+
const state = readCleanState(options);
|
|
186
|
+
const record = findRecord(state, { questionId });
|
|
187
|
+
if (!record || record.state !== "reserved") {
|
|
188
|
+
return { record: record ? { ...record } : undefined, superseded: [] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const superseded: AskUserLifecycleRecord[] = [];
|
|
192
|
+
for (const candidate of state.records) {
|
|
193
|
+
if (
|
|
194
|
+
candidate !== record &&
|
|
195
|
+
isAnswerableState(candidate.state) &&
|
|
196
|
+
candidate.questionScopeKey === record.questionScopeKey
|
|
197
|
+
) {
|
|
198
|
+
toTerminal(candidate, "superseded_by_question", timestamp);
|
|
199
|
+
superseded.push({ ...candidate });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
record.state = "pending";
|
|
203
|
+
record.updatedAt = timestamp;
|
|
204
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
205
|
+
persistState(options, state);
|
|
206
|
+
return { record: { ...record }, superseded };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function claimAskUserQuestion(
|
|
210
|
+
options: AskUserStoreOptions,
|
|
211
|
+
identifier: AskUserQuestionIdentifier,
|
|
212
|
+
): AskUserLifecycleRecord | undefined {
|
|
213
|
+
const timestamp = now(options);
|
|
214
|
+
const state = readCleanState(options);
|
|
215
|
+
const record = findRecord(state, identifier);
|
|
216
|
+
if (!record || record.state !== "pending") {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
record.state = "dispatching";
|
|
220
|
+
record.updatedAt = timestamp;
|
|
221
|
+
record.expiresAt = timestamp + ACTIVE_TTL_MS;
|
|
222
|
+
persistState(options, state);
|
|
223
|
+
return { ...record };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function terminateAskUserQuestion(
|
|
227
|
+
options: AskUserStoreOptions,
|
|
228
|
+
questionId: string,
|
|
229
|
+
reason: AskUserTerminalReason,
|
|
230
|
+
): AskUserLifecycleRecord | undefined {
|
|
231
|
+
const timestamp = now(options);
|
|
232
|
+
const state = readCleanState(options);
|
|
233
|
+
const record = findRecord(state, { questionId });
|
|
234
|
+
if (!record || !isActiveState(record.state)) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
toTerminal(record, reason, timestamp);
|
|
238
|
+
persistState(options, state);
|
|
239
|
+
return { ...record };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function invalidateAskUserQuestionsInScope(
|
|
243
|
+
options: AskUserStoreOptions,
|
|
244
|
+
questionScopeKey: string,
|
|
245
|
+
reason: AskUserTerminalReason,
|
|
246
|
+
): AskUserLifecycleRecord[] {
|
|
247
|
+
const timestamp = now(options);
|
|
248
|
+
const state = readCleanState(options);
|
|
249
|
+
const invalidated: AskUserLifecycleRecord[] = [];
|
|
250
|
+
for (const record of state.records) {
|
|
251
|
+
if (isAnswerableState(record.state) && record.questionScopeKey === questionScopeKey) {
|
|
252
|
+
toTerminal(record, reason, timestamp);
|
|
253
|
+
invalidated.push({ ...record });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (invalidated.length > 0) {
|
|
257
|
+
persistState(options, state);
|
|
258
|
+
}
|
|
259
|
+
return invalidated;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function resolveAskUserQuestion(
|
|
263
|
+
options: AskUserStoreOptions,
|
|
264
|
+
identifier: AskUserQuestionIdentifier,
|
|
265
|
+
): AskUserLifecycleRecord | undefined {
|
|
266
|
+
const record = findRecord(readCleanState(options), identifier);
|
|
267
|
+
return record ? { ...record } : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function recoverAskUserQuestionsAfterRestart(
|
|
271
|
+
options: AskUserStoreOptions,
|
|
272
|
+
): AskUserLifecycleRecord[] {
|
|
273
|
+
const timestamp = now(options);
|
|
274
|
+
const state = loadState(options);
|
|
275
|
+
const retained = state.records.filter(
|
|
276
|
+
(record) => record.state !== "terminal" || record.expiresAt > timestamp,
|
|
277
|
+
);
|
|
278
|
+
const removedExpiredTombstones = retained.length !== state.records.length;
|
|
279
|
+
state.records = retained;
|
|
280
|
+
const recovered: AskUserLifecycleRecord[] = [];
|
|
281
|
+
for (const record of state.records) {
|
|
282
|
+
if (!isActiveState(record.state)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const reason =
|
|
286
|
+
record.state === "dispatching" ? "restart_during_dispatch" : "restart_invalidated";
|
|
287
|
+
toTerminal(record, reason, timestamp);
|
|
288
|
+
recovered.push({ ...record });
|
|
289
|
+
}
|
|
290
|
+
if (recovered.length > 0 || removedExpiredTombstones) {
|
|
291
|
+
persistState(options, state);
|
|
292
|
+
}
|
|
293
|
+
return recovered;
|
|
294
|
+
}
|