@adminforth/agent 1.45.0 → 1.46.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/agentTurnService.ts +526 -0
- package/build.log +1 -1
- package/chatSurfaceService.ts +370 -0
- package/dist/agentTurnService.d.ts +70 -0
- package/dist/agentTurnService.js +453 -0
- package/dist/chatSurfaceService.d.ts +32 -0
- package/dist/chatSurfaceService.js +265 -0
- package/dist/endpoints/chatSurfaces.d.ts +3 -0
- package/dist/endpoints/chatSurfaces.js +91 -0
- package/dist/endpoints/context.d.ts +30 -0
- package/dist/endpoints/context.js +1 -0
- package/dist/endpoints/core.d.ts +3 -0
- package/dist/endpoints/core.js +106 -0
- package/dist/endpoints/sessions.d.ts +3 -0
- package/dist/endpoints/sessions.js +177 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +4 -47
- package/dist/index.js +37 -917
- package/dist/sessionStore.d.ts +19 -0
- package/dist/sessionStore.js +83 -0
- package/endpoints/chatSurfaces.ts +93 -0
- package/endpoints/context.ts +66 -0
- package/endpoints/core.ts +113 -0
- package/endpoints/sessions.ts +183 -0
- package/errors.ts +10 -0
- package/index.ts +48 -1053
- package/package.json +1 -1
- package/sessionStore.ts +94 -0
- package/agentResponseEvents.ts +0 -1
- package/dist/agentResponseEvents.d.ts +0 -1
- package/dist/agentResponseEvents.js +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdminUser,
|
|
3
|
+
ChatSurfaceAdapter,
|
|
4
|
+
ChatSurfaceEventSink,
|
|
5
|
+
ChatSurfaceIncomingMessage,
|
|
6
|
+
IAdminForth,
|
|
7
|
+
} from "adminforth";
|
|
8
|
+
import { Filters, logger } from "adminforth";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
import type { AgentEventEmitter } from "./agentEvents.js";
|
|
11
|
+
import type {
|
|
12
|
+
HandleTurnInput,
|
|
13
|
+
RunAndPersistAgentResponseInput,
|
|
14
|
+
RunAndPersistAgentResponseResult,
|
|
15
|
+
} from "./agentTurnService.js";
|
|
16
|
+
import type { PluginOptions } from "./types.js";
|
|
17
|
+
import type { AgentSessionStore } from "./sessionStore.js";
|
|
18
|
+
import { getErrorMessage, isAbortError } from "./errors.js";
|
|
19
|
+
import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
|
|
20
|
+
|
|
21
|
+
type ChatSurfaceConnectAction = {
|
|
22
|
+
type: "url";
|
|
23
|
+
label: string;
|
|
24
|
+
url: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ChatSurfaceIncomingMessageWithAudio = ChatSurfaceIncomingMessage & {
|
|
28
|
+
audio?: {
|
|
29
|
+
buffer: Buffer;
|
|
30
|
+
filename: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ChatSurfaceEventSinkWithAudio = ChatSurfaceEventSink & {
|
|
36
|
+
emit(event: Parameters<ChatSurfaceEventSink["emit"]>[0] | {
|
|
37
|
+
type: "audio";
|
|
38
|
+
audio: Buffer;
|
|
39
|
+
filename: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
}): void | Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
|
|
45
|
+
createConnectAction?(input: {
|
|
46
|
+
token: string;
|
|
47
|
+
}): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ChatSurfaceLinkTokenPayload = {
|
|
51
|
+
surface: string;
|
|
52
|
+
adminUserId: AdminUser["pk"];
|
|
53
|
+
expiresAt: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
|
|
57
|
+
const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
|
|
58
|
+
|
|
59
|
+
export class ChatSurfaceService {
|
|
60
|
+
private linkTokens = new Map<string, ChatSurfaceLinkTokenPayload>();
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private getAdminforth: () => IAdminForth,
|
|
64
|
+
private options: PluginOptions,
|
|
65
|
+
private sessionStore: AgentSessionStore,
|
|
66
|
+
private handleTurn: (input: HandleTurnInput) => Promise<unknown>,
|
|
67
|
+
private runAndPersistAgentResponse: (
|
|
68
|
+
input: RunAndPersistAgentResponseInput,
|
|
69
|
+
) => Promise<RunAndPersistAgentResponseResult>,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
getConnectActionAdapters() {
|
|
73
|
+
return (this.options.chatSurfaceAdapters ?? [])
|
|
74
|
+
.map((adapter) => adapter as ChatSurfaceAdapterWithConnectAction)
|
|
75
|
+
.filter((adapter) => adapter.createConnectAction);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
createLinkToken(surface: string, adminUser: AdminUser) {
|
|
79
|
+
for (const [token, payload] of this.linkTokens) {
|
|
80
|
+
if (payload.expiresAt <= Date.now()) {
|
|
81
|
+
this.linkTokens.delete(token);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const token = randomUUID();
|
|
86
|
+
this.linkTokens.set(token, {
|
|
87
|
+
surface,
|
|
88
|
+
adminUserId: adminUser.pk,
|
|
89
|
+
expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return token;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private consumeLinkToken(surface: string, token: string) {
|
|
96
|
+
const payload = this.linkTokens.get(token);
|
|
97
|
+
this.linkTokens.delete(token);
|
|
98
|
+
|
|
99
|
+
if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private createEventEmitter(sink: ChatSurfaceEventSink): AgentEventEmitter {
|
|
107
|
+
return async (event) => {
|
|
108
|
+
if (event.type === "text-delta") {
|
|
109
|
+
await sink.emit({
|
|
110
|
+
type: "text_delta",
|
|
111
|
+
delta: event.delta,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (event.type === "response") {
|
|
117
|
+
await sink.emit({
|
|
118
|
+
type: "done",
|
|
119
|
+
text: event.text,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.type === "error") {
|
|
125
|
+
await sink.emit({
|
|
126
|
+
type: "error",
|
|
127
|
+
message: event.error,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async handleLink(
|
|
134
|
+
incoming: ChatSurfaceIncomingMessage,
|
|
135
|
+
sink: ChatSurfaceEventSink,
|
|
136
|
+
) {
|
|
137
|
+
if (incoming.metadata?.isStartCommand !== true) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
142
|
+
const adminforth = this.getAdminforth();
|
|
143
|
+
const authResourceId = adminforth.config.auth!.usersResourceId!;
|
|
144
|
+
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
145
|
+
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
146
|
+
const linkedAdminUserRecord = (
|
|
147
|
+
await adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
|
|
148
|
+
).find((user) => user[externalUserIdField]?.[incoming.surface] === incoming.externalUserId);
|
|
149
|
+
|
|
150
|
+
if (linkedAdminUserRecord) {
|
|
151
|
+
await sink.emit({
|
|
152
|
+
type: "done",
|
|
153
|
+
text: `${incoming.surface} account is already connected to AdminForth.`,
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (typeof incoming.metadata?.startPayload !== "string") {
|
|
159
|
+
await sink.emit({
|
|
160
|
+
type: "done",
|
|
161
|
+
text: `Open AdminForth and connect your ${incoming.surface} account from Chat Surfaces settings.`,
|
|
162
|
+
});
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const payload = this.consumeLinkToken(incoming.surface, incoming.metadata.startPayload);
|
|
167
|
+
if (!payload) {
|
|
168
|
+
await sink.emit({
|
|
169
|
+
type: "error",
|
|
170
|
+
message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
|
|
171
|
+
});
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const adminUserRecord = await adminforth.resource(authResourceId).get([
|
|
176
|
+
Filters.EQ(primaryKeyField, payload.adminUserId),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
await adminforth.resource(authResourceId).update(payload.adminUserId, {
|
|
180
|
+
[externalUserIdField]: {
|
|
181
|
+
...(adminUserRecord[externalUserIdField] ?? {}),
|
|
182
|
+
[incoming.surface]: incoming.externalUserId,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
await sink.emit({
|
|
186
|
+
type: "done",
|
|
187
|
+
text: `${incoming.surface} account connected to AdminForth.`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async handleAudioMessage(
|
|
194
|
+
incoming: ChatSurfaceIncomingMessageWithAudio,
|
|
195
|
+
sink: ChatSurfaceEventSinkWithAudio,
|
|
196
|
+
adminUser: AdminUser,
|
|
197
|
+
) {
|
|
198
|
+
const audioAdapter = this.options.audioAdapter;
|
|
199
|
+
if (!audioAdapter) {
|
|
200
|
+
await sink.emit({
|
|
201
|
+
type: "error",
|
|
202
|
+
message: "Audio adapter is not configured for AdminForth Agent.",
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let transcription;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
transcription = await audioAdapter.transcribe({
|
|
211
|
+
buffer: incoming.audio!.buffer,
|
|
212
|
+
filename: incoming.audio!.filename,
|
|
213
|
+
mimeType: incoming.audio!.mimeType,
|
|
214
|
+
language: "auto",
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (isAbortError(error)) {
|
|
218
|
+
logger.info(`Agent ${incoming.surface} surface speech transcription aborted`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
logger.error(`Agent ${incoming.surface} surface speech transcription failed:\n${getErrorMessage(error)}`);
|
|
223
|
+
await sink.emit({
|
|
224
|
+
type: "error",
|
|
225
|
+
message: "Speech transcription failed. Check server logs for details.",
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!transcription.text) {
|
|
231
|
+
await sink.emit({
|
|
232
|
+
type: "error",
|
|
233
|
+
message: "Speech transcription is empty",
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const agentResponse = await this.handleAgentSurfaceResponse(
|
|
239
|
+
incoming,
|
|
240
|
+
sink,
|
|
241
|
+
adminUser,
|
|
242
|
+
transcription.text,
|
|
243
|
+
{ emitDone: false },
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (!agentResponse || agentResponse.aborted || agentResponse.failed) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await sink.emit({
|
|
251
|
+
type: "done",
|
|
252
|
+
text: agentResponse.text,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const speech = await audioAdapter.synthesize({
|
|
257
|
+
text: sanitizeSpeechText(agentResponse.text),
|
|
258
|
+
stream: false,
|
|
259
|
+
format: "opus",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await sink.emit({
|
|
263
|
+
type: "audio",
|
|
264
|
+
audio: speech.audio,
|
|
265
|
+
filename: "agent-response.ogg",
|
|
266
|
+
mimeType: speech.mimeType,
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (isAbortError(error)) {
|
|
270
|
+
logger.info(`Agent ${incoming.surface} surface speech synthesis aborted`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
logger.error(`Agent ${incoming.surface} surface speech synthesis failed:\n${getErrorMessage(error)}`);
|
|
275
|
+
await sink.emit({
|
|
276
|
+
type: "error",
|
|
277
|
+
message: getErrorMessage(error),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async handleAgentSurfaceResponse(
|
|
283
|
+
incoming: ChatSurfaceIncomingMessage,
|
|
284
|
+
sink: ChatSurfaceEventSink,
|
|
285
|
+
adminUser: AdminUser,
|
|
286
|
+
prompt: string,
|
|
287
|
+
options?: { emitDone?: boolean },
|
|
288
|
+
) {
|
|
289
|
+
const emitDone = options?.emitDone ?? true;
|
|
290
|
+
const sessionId = await this.sessionStore.getOrCreateChatSurfaceSession(
|
|
291
|
+
{ ...incoming, prompt },
|
|
292
|
+
adminUser,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (emitDone) {
|
|
296
|
+
await this.handleTurn({
|
|
297
|
+
prompt,
|
|
298
|
+
sessionId,
|
|
299
|
+
modeName: incoming.modeName,
|
|
300
|
+
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
301
|
+
adminUser,
|
|
302
|
+
emit: this.createEventEmitter(sink),
|
|
303
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
304
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
305
|
+
});
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const agentResponse = await this.runAndPersistAgentResponse({
|
|
310
|
+
prompt,
|
|
311
|
+
sessionId,
|
|
312
|
+
modeName: incoming.modeName,
|
|
313
|
+
userTimeZone: incoming.userTimeZone ?? "UTC",
|
|
314
|
+
adminUser,
|
|
315
|
+
emit: this.createEventEmitter(sink),
|
|
316
|
+
failureLogMessage: `Agent ${incoming.surface} surface response failed`,
|
|
317
|
+
abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (agentResponse.failed) {
|
|
321
|
+
await sink.emit({
|
|
322
|
+
type: "error",
|
|
323
|
+
message: agentResponse.text,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return agentResponse;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async handleMessage(
|
|
331
|
+
adapter: ChatSurfaceAdapter,
|
|
332
|
+
incoming: ChatSurfaceIncomingMessage,
|
|
333
|
+
sink: ChatSurfaceEventSink,
|
|
334
|
+
) {
|
|
335
|
+
if (await this.handleLink(incoming, sink)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const adminforth = this.getAdminforth();
|
|
340
|
+
const authResourceId = adminforth.config.auth!.usersResourceId!;
|
|
341
|
+
const authResource = adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
|
|
342
|
+
const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
|
|
343
|
+
const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
|
|
344
|
+
const adminUserRecord = (
|
|
345
|
+
await adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
|
|
346
|
+
).find((user) => user[externalUserIdField]?.[adapter.name] === incoming.externalUserId);
|
|
347
|
+
|
|
348
|
+
if (!adminUserRecord) {
|
|
349
|
+
await sink.emit({
|
|
350
|
+
type: "error",
|
|
351
|
+
message: "This chat account is not authorized to use AdminForth Agent.",
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const adminUser = {
|
|
357
|
+
pk: adminUserRecord[primaryKeyField],
|
|
358
|
+
username: adminUserRecord[adminforth.config.auth!.usernameField],
|
|
359
|
+
dbUser: adminUserRecord,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const incomingWithAudio = incoming as ChatSurfaceIncomingMessageWithAudio;
|
|
363
|
+
if (incomingWithAudio.audio) {
|
|
364
|
+
await this.handleAudioMessage(incomingWithAudio, sink as ChatSurfaceEventSinkWithAudio, adminUser);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await this.handleAgentSurfaceResponse(incoming, sink, adminUser, incoming.prompt);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AdminUser, AudioAdapter, IAdminForth } from "adminforth";
|
|
2
|
+
import type { BaseCheckpointSaver } from "@langchain/langgraph";
|
|
3
|
+
import type { AgentEventEmitter } from "./agentEvents.js";
|
|
4
|
+
import type { CurrentPageContext } from "./agent/tools/getUserLocation.js";
|
|
5
|
+
import type { AgentSessionStore } from "./sessionStore.js";
|
|
6
|
+
import type { PluginOptions } from "./types.js";
|
|
7
|
+
export type RunAndPersistAgentResponseInput = {
|
|
8
|
+
prompt: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
modeName?: string | null;
|
|
11
|
+
userTimeZone: string;
|
|
12
|
+
currentPage?: CurrentPageContext;
|
|
13
|
+
abortSignal?: AbortSignal;
|
|
14
|
+
adminUser: AdminUser;
|
|
15
|
+
emit?: AgentEventEmitter;
|
|
16
|
+
failureLogMessage: string;
|
|
17
|
+
abortLogMessage: string;
|
|
18
|
+
};
|
|
19
|
+
export type RunAndPersistAgentResponseResult = {
|
|
20
|
+
text: string;
|
|
21
|
+
turnId: string;
|
|
22
|
+
aborted: boolean;
|
|
23
|
+
failed: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type HandleTurnInput = Omit<RunAndPersistAgentResponseInput, "failureLogMessage" | "abortLogMessage"> & {
|
|
26
|
+
emit: AgentEventEmitter;
|
|
27
|
+
failureLogMessage?: string;
|
|
28
|
+
abortLogMessage?: string;
|
|
29
|
+
};
|
|
30
|
+
export type HandleSpeechTurnInput = Omit<HandleTurnInput, "prompt"> & {
|
|
31
|
+
audioAdapter: AudioAdapter;
|
|
32
|
+
audio: {
|
|
33
|
+
buffer: Buffer;
|
|
34
|
+
filename: string;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type AgentTurnServiceOptions = {
|
|
39
|
+
getAdminforth: () => IAdminForth;
|
|
40
|
+
getPluginInstanceId: () => string;
|
|
41
|
+
options: PluginOptions;
|
|
42
|
+
sessionStore: AgentSessionStore;
|
|
43
|
+
getCheckpointer: () => BaseCheckpointSaver;
|
|
44
|
+
getInternalAgentResourceIds: () => string[];
|
|
45
|
+
getAgentSystemPrompt: () => Promise<string>;
|
|
46
|
+
};
|
|
47
|
+
export declare class AgentTurnService {
|
|
48
|
+
private serviceOptions;
|
|
49
|
+
constructor(serviceOptions: AgentTurnServiceOptions);
|
|
50
|
+
private runAgentTurn;
|
|
51
|
+
runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<{
|
|
52
|
+
text: string;
|
|
53
|
+
turnId: any;
|
|
54
|
+
aborted: boolean;
|
|
55
|
+
failed: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
handleTurn(input: HandleTurnInput): Promise<{
|
|
58
|
+
text: string;
|
|
59
|
+
turnId: any;
|
|
60
|
+
aborted: boolean;
|
|
61
|
+
failed: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
handleSpeechTurn(input: HandleSpeechTurnInput): Promise<{
|
|
64
|
+
text: string;
|
|
65
|
+
turnId: any;
|
|
66
|
+
aborted: boolean;
|
|
67
|
+
failed: boolean;
|
|
68
|
+
} | null>;
|
|
69
|
+
}
|
|
70
|
+
export {};
|