@gakr-gakr/whatsapp 0.1.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/action-runtime-api.ts +1 -0
- package/action-runtime.runtime.ts +1 -0
- package/api.ts +67 -0
- package/auth-presence.ts +80 -0
- package/autobot.plugin.json +23 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/constants.ts +1 -0
- package/contract-api.ts +29 -0
- package/directory-contract-api.ts +4 -0
- package/doctor-contract-api.ts +8 -0
- package/index.ts +16 -0
- package/legacy-session-surface-api.ts +6 -0
- package/legacy-state-migrations-api.ts +1 -0
- package/light-runtime-api.ts +12 -0
- package/login-qr-api.ts +1 -0
- package/login-qr-runtime.ts +23 -0
- package/outbound-payload-test-api.ts +1 -0
- package/package.json +76 -0
- package/runtime-api.ts +84 -0
- package/secret-contract-api.ts +4 -0
- package/security-contract-api.ts +4 -0
- package/setup-entry.ts +21 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-config.ts +77 -0
- package/src/account-ids.ts +17 -0
- package/src/account-types.ts +5 -0
- package/src/accounts.ts +176 -0
- package/src/action-runtime-target-auth.ts +27 -0
- package/src/action-runtime.ts +76 -0
- package/src/active-listener.ts +17 -0
- package/src/agent-tools-login.ts +113 -0
- package/src/approval-auth.ts +27 -0
- package/src/auth-store.runtime.ts +1 -0
- package/src/auth-store.ts +494 -0
- package/src/auto-reply/config.runtime.ts +16 -0
- package/src/auto-reply/constants.ts +1 -0
- package/src/auto-reply/deliver-reply.ts +332 -0
- package/src/auto-reply/loggers.ts +6 -0
- package/src/auto-reply/mentions.ts +131 -0
- package/src/auto-reply/monitor/ack-reaction.ts +99 -0
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
- package/src/auto-reply/monitor/broadcast.ts +153 -0
- package/src/auto-reply/monitor/commands.ts +19 -0
- package/src/auto-reply/monitor/echo.ts +64 -0
- package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
- package/src/auto-reply/monitor/group-activation.ts +73 -0
- package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
- package/src/auto-reply/monitor/group-gating.ts +218 -0
- package/src/auto-reply/monitor/group-members.ts +65 -0
- package/src/auto-reply/monitor/inbound-context.ts +92 -0
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
- package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
- package/src/auto-reply/monitor/last-route.ts +61 -0
- package/src/auto-reply/monitor/listener-log.ts +28 -0
- package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
- package/src/auto-reply/monitor/message-line.ts +54 -0
- package/src/auto-reply/monitor/on-message.ts +333 -0
- package/src/auto-reply/monitor/peer.ts +17 -0
- package/src/auto-reply/monitor/process-message.ts +584 -0
- package/src/auto-reply/monitor/runtime-api.ts +36 -0
- package/src/auto-reply/monitor/status-reaction.ts +108 -0
- package/src/auto-reply/monitor-state.ts +114 -0
- package/src/auto-reply/monitor.ts +720 -0
- package/src/auto-reply/reply-resolver.runtime.ts +1 -0
- package/src/auto-reply/types.ts +48 -0
- package/src/auto-reply/util.ts +62 -0
- package/src/auto-reply.impl.ts +6 -0
- package/src/auto-reply.ts +1 -0
- package/src/channel-actions.runtime.ts +7 -0
- package/src/channel-actions.ts +85 -0
- package/src/channel-outbound.ts +87 -0
- package/src/channel-react-action.runtime.ts +10 -0
- package/src/channel-react-action.ts +247 -0
- package/src/channel.runtime.ts +117 -0
- package/src/channel.setup.ts +32 -0
- package/src/channel.ts +356 -0
- package/src/command-policy.ts +7 -0
- package/src/config-accessors.ts +22 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +24 -0
- package/src/connection-controller-registry.ts +49 -0
- package/src/connection-controller.ts +680 -0
- package/src/creds-files.ts +19 -0
- package/src/creds-persistence.ts +71 -0
- package/src/directory-config.ts +40 -0
- package/src/doctor-contract.ts +11 -0
- package/src/doctor.ts +56 -0
- package/src/document-filename.ts +17 -0
- package/src/group-intro.ts +15 -0
- package/src/group-policy.ts +40 -0
- package/src/group-session-contract.ts +20 -0
- package/src/group-session-key.ts +42 -0
- package/src/heartbeat.ts +34 -0
- package/src/identity.ts +164 -0
- package/src/inbound/access-control.ts +187 -0
- package/src/inbound/dedupe.ts +132 -0
- package/src/inbound/extract.ts +484 -0
- package/src/inbound/lifecycle.ts +39 -0
- package/src/inbound/media.ts +128 -0
- package/src/inbound/monitor.ts +1042 -0
- package/src/inbound/outbound-mentions.ts +260 -0
- package/src/inbound/runtime-api.ts +7 -0
- package/src/inbound/save-media.runtime.ts +1 -0
- package/src/inbound/send-api.ts +203 -0
- package/src/inbound/send-result.ts +109 -0
- package/src/inbound/types.ts +107 -0
- package/src/inbound-policy.ts +215 -0
- package/src/inbound.ts +9 -0
- package/src/login-qr.ts +542 -0
- package/src/login.ts +83 -0
- package/src/media.ts +10 -0
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
- package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
- package/src/normalize-target.ts +148 -0
- package/src/normalize.ts +8 -0
- package/src/outbound-adapter.ts +36 -0
- package/src/outbound-base.ts +256 -0
- package/src/outbound-media-contract.ts +307 -0
- package/src/outbound-media.runtime.ts +41 -0
- package/src/outbound-send-deps.ts +1 -0
- package/src/outbound-test-support.ts +16 -0
- package/src/qa-driver.runtime.ts +189 -0
- package/src/qr-image.ts +1 -0
- package/src/qr-terminal.ts +1 -0
- package/src/quoted-message.ts +184 -0
- package/src/reaction-level.ts +24 -0
- package/src/reconnect.ts +55 -0
- package/src/resolve-outbound-target.ts +58 -0
- package/src/runtime-api.ts +59 -0
- package/src/runtime-group-policy.ts +16 -0
- package/src/runtime.ts +9 -0
- package/src/security-contract.ts +47 -0
- package/src/security-fix.ts +71 -0
- package/src/send.ts +342 -0
- package/src/session-contract.ts +43 -0
- package/src/session-errors.ts +125 -0
- package/src/session-route.ts +32 -0
- package/src/session.runtime.ts +8 -0
- package/src/session.ts +327 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-finalize.ts +450 -0
- package/src/setup-surface.ts +71 -0
- package/src/setup-test-helpers.ts +217 -0
- package/src/shared.ts +291 -0
- package/src/socket-timing.ts +38 -0
- package/src/state-migrations.ts +55 -0
- package/src/status-issues.ts +185 -0
- package/src/system-prompt.ts +31 -0
- package/src/targets-runtime.ts +221 -0
- package/src/text-runtime.ts +18 -0
- package/src/vcard.ts +84 -0
- package/targets.ts +5 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TIMING,
|
|
3
|
+
type StatusReactionController,
|
|
4
|
+
} from "autobot/plugin-sdk/channel-feedback";
|
|
5
|
+
import type { CommandTurnContext } from "autobot/plugin-sdk/channel-inbound";
|
|
6
|
+
import { deliverInboundReplyWithMessageSendContext } from "autobot/plugin-sdk/channel-message";
|
|
7
|
+
import { hasVisibleInboundReplyDispatch } from "autobot/plugin-sdk/inbound-reply-dispatch";
|
|
8
|
+
import { buildInboundHistoryFromEntries } from "autobot/plugin-sdk/reply-history";
|
|
9
|
+
import type { FinalizedMsgContext } from "autobot/plugin-sdk/reply-runtime";
|
|
10
|
+
import {
|
|
11
|
+
type DeliverableWhatsAppOutboundPayload,
|
|
12
|
+
normalizeWhatsAppOutboundPayload,
|
|
13
|
+
normalizeWhatsAppPayloadTextPreservingIndentation,
|
|
14
|
+
} from "../../outbound-media-contract.js";
|
|
15
|
+
import type { WhatsAppReplyDeliveryResult } from "../deliver-reply.js";
|
|
16
|
+
import type { WebInboundMsg } from "../types.js";
|
|
17
|
+
import { formatGroupMembers } from "./group-members.js";
|
|
18
|
+
import type { GroupHistoryEntry } from "./inbound-context.js";
|
|
19
|
+
import {
|
|
20
|
+
createChannelMessageReplyPipeline,
|
|
21
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
22
|
+
finalizeInboundContext,
|
|
23
|
+
getAgentScopedMediaLocalRoots,
|
|
24
|
+
jidToE164,
|
|
25
|
+
logVerbose,
|
|
26
|
+
resolveChannelMessageSourceReplyDeliveryMode,
|
|
27
|
+
resolveChunkMode,
|
|
28
|
+
resolveIdentityNamePrefix,
|
|
29
|
+
resolveInboundLastRouteSessionKey,
|
|
30
|
+
resolveMarkdownTableMode,
|
|
31
|
+
resolveSendableOutboundReplyParts,
|
|
32
|
+
resolveTextChunkLimit,
|
|
33
|
+
shouldLogVerbose,
|
|
34
|
+
toLocationContext,
|
|
35
|
+
type getChildLogger,
|
|
36
|
+
type getReplyFromConfig,
|
|
37
|
+
type LoadConfigFn,
|
|
38
|
+
type ReplyPayload,
|
|
39
|
+
type resolveAgentRoute,
|
|
40
|
+
} from "./inbound-dispatch.runtime.js";
|
|
41
|
+
|
|
42
|
+
type ReplyLifecycleKind = "tool" | "block" | "final";
|
|
43
|
+
type ChannelReplyOnModelSelected = NonNullable<
|
|
44
|
+
ReturnType<typeof createChannelMessageReplyPipeline>["onModelSelected"]
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
type WhatsAppDispatchPipeline = {
|
|
48
|
+
responsePrefix?: string;
|
|
49
|
+
} & Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
type VisibleReplyTarget = {
|
|
52
|
+
id?: string;
|
|
53
|
+
body?: string;
|
|
54
|
+
sender?: {
|
|
55
|
+
label?: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ReplyThreadingContext = {
|
|
60
|
+
implicitCurrentMessage?: "default" | "allow" | "deny";
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type SenderContext = {
|
|
64
|
+
id?: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
e164?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ReplyDeliveryInfo = { kind: ReplyLifecycleKind };
|
|
70
|
+
|
|
71
|
+
type PendingWhatsAppMediaOnlyPayload = {
|
|
72
|
+
info: ReplyDeliveryInfo;
|
|
73
|
+
mediaUrls: Set<string>;
|
|
74
|
+
payload: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type WhatsAppMediaOnlyFlushResult = {
|
|
78
|
+
delivered: number;
|
|
79
|
+
droppedDuplicateMedia: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function logWhatsAppReplyDeliveryError(params: {
|
|
83
|
+
err: unknown;
|
|
84
|
+
info: ReplyDeliveryInfo;
|
|
85
|
+
connectionId: string;
|
|
86
|
+
conversationId: string;
|
|
87
|
+
msg: WebInboundMsg;
|
|
88
|
+
replyLogger: ReturnType<typeof getChildLogger>;
|
|
89
|
+
}) {
|
|
90
|
+
params.replyLogger.error(
|
|
91
|
+
{
|
|
92
|
+
err: params.err,
|
|
93
|
+
replyKind: params.info.kind,
|
|
94
|
+
correlationId: params.msg.id ?? null,
|
|
95
|
+
connectionId: params.connectionId,
|
|
96
|
+
conversationId: params.conversationId,
|
|
97
|
+
chatId: params.msg.chatId ?? null,
|
|
98
|
+
to: params.msg.from ?? null,
|
|
99
|
+
from: params.msg.to ?? null,
|
|
100
|
+
},
|
|
101
|
+
"auto-reply delivery failed",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveWhatsAppDisableBlockStreaming(cfg: ReturnType<LoadConfigFn>): boolean | undefined {
|
|
106
|
+
if (typeof cfg.channels?.whatsapp?.blockStreaming !== "boolean") {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return !cfg.channels.whatsapp.blockStreaming;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveWhatsAppDeliverablePayload(
|
|
113
|
+
payload: ReplyPayload,
|
|
114
|
+
info: { kind: ReplyLifecycleKind },
|
|
115
|
+
): ReplyPayload | null {
|
|
116
|
+
if (payload.isReasoning === true || payload.isCompactionNotice === true) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (payload.isError === true) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (info.kind === "tool") {
|
|
123
|
+
if (!resolveSendableOutboundReplyParts(payload).hasMedia) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return { ...payload, text: undefined };
|
|
127
|
+
}
|
|
128
|
+
return payload;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getWhatsAppPayloadMediaUrls(payload: ReplyPayload): Set<string> {
|
|
132
|
+
return new Set(
|
|
133
|
+
[
|
|
134
|
+
...(Array.isArray(payload.mediaUrls) ? payload.mediaUrls : []),
|
|
135
|
+
...(typeof payload.mediaUrl === "string" ? [payload.mediaUrl] : []),
|
|
136
|
+
]
|
|
137
|
+
.map((url) => url.trim())
|
|
138
|
+
.filter(Boolean),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasWhatsAppMediaUrlOverlap(left: Set<string>, right: Set<string>): boolean {
|
|
143
|
+
for (const url of left) {
|
|
144
|
+
if (right.has(url)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function shouldDeferWhatsAppMediaOnlyPayload(params: {
|
|
152
|
+
info: ReplyDeliveryInfo;
|
|
153
|
+
mediaUrls: Set<string>;
|
|
154
|
+
reply: ReturnType<typeof resolveSendableOutboundReplyParts>;
|
|
155
|
+
}): boolean {
|
|
156
|
+
return (
|
|
157
|
+
params.info.kind !== "final" &&
|
|
158
|
+
params.reply.hasMedia &&
|
|
159
|
+
!params.reply.text.trim() &&
|
|
160
|
+
params.mediaUrls.size > 0
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createWhatsAppMediaOnlyReplyCoalescer(params: {
|
|
165
|
+
deliver: (pending: PendingWhatsAppMediaOnlyPayload) => Promise<void>;
|
|
166
|
+
}) {
|
|
167
|
+
const pendingMediaOnlyPayloads: PendingWhatsAppMediaOnlyPayload[] = [];
|
|
168
|
+
const flushExceptDuplicateMedia = async (
|
|
169
|
+
mediaUrls?: Set<string>,
|
|
170
|
+
): Promise<WhatsAppMediaOnlyFlushResult> => {
|
|
171
|
+
const flushResult: WhatsAppMediaOnlyFlushResult = {
|
|
172
|
+
delivered: 0,
|
|
173
|
+
droppedDuplicateMedia: 0,
|
|
174
|
+
};
|
|
175
|
+
const pending = pendingMediaOnlyPayloads.splice(0);
|
|
176
|
+
for (const candidate of pending) {
|
|
177
|
+
if (mediaUrls && hasWhatsAppMediaUrlOverlap(candidate.mediaUrls, mediaUrls)) {
|
|
178
|
+
flushResult.droppedDuplicateMedia += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
await params.deliver(candidate);
|
|
182
|
+
flushResult.delivered += 1;
|
|
183
|
+
}
|
|
184
|
+
return flushResult;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
defer(pending: PendingWhatsAppMediaOnlyPayload) {
|
|
189
|
+
pendingMediaOnlyPayloads.push(pending);
|
|
190
|
+
},
|
|
191
|
+
flushExceptDuplicateMedia,
|
|
192
|
+
flushAll: () => flushExceptDuplicateMedia(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function logWhatsAppMediaOnlyFlushResult(result: WhatsAppMediaOnlyFlushResult) {
|
|
197
|
+
if (!shouldLogVerbose()) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (result.droppedDuplicateMedia > 0) {
|
|
201
|
+
logVerbose(
|
|
202
|
+
`Dropped ${result.droppedDuplicateMedia} deferred media-only WhatsApp reply payload(s) superseded by captioned media`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (result.delivered > 0) {
|
|
206
|
+
logVerbose(`Flushed ${result.delivered} deferred media-only WhatsApp reply payload(s)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveWhatsAppResponsePrefix(params: {
|
|
211
|
+
cfg: ReturnType<LoadConfigFn>;
|
|
212
|
+
agentId: string;
|
|
213
|
+
isSelfChat: boolean;
|
|
214
|
+
pipelineResponsePrefix?: string;
|
|
215
|
+
}): string | undefined {
|
|
216
|
+
const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
|
|
217
|
+
return (
|
|
218
|
+
params.pipelineResponsePrefix ??
|
|
219
|
+
(configuredResponsePrefix === undefined && params.isSelfChat
|
|
220
|
+
? resolveIdentityNamePrefix(params.cfg, params.agentId)
|
|
221
|
+
: undefined)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildWhatsAppInboundContext(params: {
|
|
226
|
+
bodyForAgent?: string;
|
|
227
|
+
combinedBody: string;
|
|
228
|
+
commandBody?: string;
|
|
229
|
+
commandAuthorized?: boolean;
|
|
230
|
+
commandTurn?: CommandTurnContext;
|
|
231
|
+
commandSource?: "text";
|
|
232
|
+
conversationId: string;
|
|
233
|
+
groupHistory?: GroupHistoryEntry[];
|
|
234
|
+
groupMemberRoster?: Map<string, string>;
|
|
235
|
+
groupSystemPrompt?: string;
|
|
236
|
+
msg: WebInboundMsg;
|
|
237
|
+
rawBody?: string;
|
|
238
|
+
route: ReturnType<typeof resolveAgentRoute>;
|
|
239
|
+
sender: SenderContext;
|
|
240
|
+
transcript?: string;
|
|
241
|
+
mediaTranscribedIndexes?: number[];
|
|
242
|
+
replyThreading?: ReplyThreadingContext;
|
|
243
|
+
visibleReplyTo?: VisibleReplyTarget;
|
|
244
|
+
}) {
|
|
245
|
+
const inboundHistory =
|
|
246
|
+
params.msg.chatType === "group"
|
|
247
|
+
? buildInboundHistoryFromEntries({
|
|
248
|
+
entries: (params.groupHistory ?? []).map((entry) => ({
|
|
249
|
+
sender: entry.sender,
|
|
250
|
+
body: entry.body,
|
|
251
|
+
timestamp: entry.timestamp,
|
|
252
|
+
messageId: entry.id,
|
|
253
|
+
})),
|
|
254
|
+
limit: params.groupHistory?.length ?? 1,
|
|
255
|
+
})
|
|
256
|
+
: undefined;
|
|
257
|
+
|
|
258
|
+
const result = finalizeInboundContext({
|
|
259
|
+
Body: params.combinedBody,
|
|
260
|
+
BodyForAgent: params.bodyForAgent ?? params.msg.body,
|
|
261
|
+
InboundHistory: inboundHistory,
|
|
262
|
+
RawBody: params.rawBody ?? params.msg.body,
|
|
263
|
+
CommandBody: params.commandBody ?? params.msg.body,
|
|
264
|
+
Transcript: params.transcript,
|
|
265
|
+
From: params.msg.from,
|
|
266
|
+
To: params.msg.to,
|
|
267
|
+
SessionKey: params.route.sessionKey,
|
|
268
|
+
AccountId: params.route.accountId,
|
|
269
|
+
MessageSid: params.msg.id,
|
|
270
|
+
ReplyToId: params.visibleReplyTo?.id,
|
|
271
|
+
ReplyToBody: params.visibleReplyTo?.body,
|
|
272
|
+
ReplyToSender: params.visibleReplyTo?.sender?.label,
|
|
273
|
+
MediaPath: params.msg.mediaPath,
|
|
274
|
+
MediaUrl: params.msg.mediaUrl,
|
|
275
|
+
MediaType: params.msg.mediaType,
|
|
276
|
+
MediaTranscribedIndexes: params.mediaTranscribedIndexes,
|
|
277
|
+
ChatType: params.msg.chatType,
|
|
278
|
+
Timestamp: params.msg.timestamp,
|
|
279
|
+
ConversationLabel: params.msg.chatType === "group" ? params.conversationId : params.msg.from,
|
|
280
|
+
GroupSubject: params.msg.groupSubject,
|
|
281
|
+
GroupMembers: formatGroupMembers({
|
|
282
|
+
participants: params.msg.groupParticipants,
|
|
283
|
+
roster: params.groupMemberRoster,
|
|
284
|
+
fallbackE164: params.sender.e164,
|
|
285
|
+
}),
|
|
286
|
+
SenderName: params.sender.name,
|
|
287
|
+
SenderId: params.sender.id ?? params.sender.e164,
|
|
288
|
+
SenderE164: params.sender.e164,
|
|
289
|
+
CommandAuthorized: params.commandAuthorized,
|
|
290
|
+
CommandTurn: params.commandTurn,
|
|
291
|
+
CommandSource:
|
|
292
|
+
params.commandSource ??
|
|
293
|
+
(params.commandTurn?.source === "native" || params.commandTurn?.source === "text"
|
|
294
|
+
? params.commandTurn.source
|
|
295
|
+
: undefined),
|
|
296
|
+
ReplyThreading: params.replyThreading,
|
|
297
|
+
WasMentioned: params.msg.wasMentioned,
|
|
298
|
+
GroupSystemPrompt: params.groupSystemPrompt,
|
|
299
|
+
UntrustedStructuredContext: params.msg.untrustedStructuredContext,
|
|
300
|
+
...(params.msg.location ? toLocationContext(params.msg.location) : {}),
|
|
301
|
+
Provider: "whatsapp",
|
|
302
|
+
Surface: "whatsapp",
|
|
303
|
+
OriginatingChannel: "whatsapp",
|
|
304
|
+
OriginatingTo: params.msg.from,
|
|
305
|
+
});
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function normalizeCommandTurnFromContext(value: unknown): CommandTurnContext | undefined {
|
|
310
|
+
if (!value || typeof value !== "object") {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const record = value as Partial<CommandTurnContext>;
|
|
314
|
+
const kind = record.kind;
|
|
315
|
+
const source = record.source;
|
|
316
|
+
if (kind === "native" && source === "native" && typeof record.authorized === "boolean") {
|
|
317
|
+
return {
|
|
318
|
+
kind: "native",
|
|
319
|
+
source: "native",
|
|
320
|
+
authorized: record.authorized,
|
|
321
|
+
commandName: typeof record.commandName === "string" ? record.commandName : undefined,
|
|
322
|
+
body: typeof record.body === "string" ? record.body : undefined,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (kind === "text-slash" && source === "text" && typeof record.authorized === "boolean") {
|
|
326
|
+
return {
|
|
327
|
+
kind: "text-slash",
|
|
328
|
+
source: "text",
|
|
329
|
+
authorized: record.authorized,
|
|
330
|
+
commandName: typeof record.commandName === "string" ? record.commandName : undefined,
|
|
331
|
+
body: typeof record.body === "string" ? record.body : undefined,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (kind === "normal" && source === "message") {
|
|
335
|
+
return {
|
|
336
|
+
kind: "normal",
|
|
337
|
+
source: "message",
|
|
338
|
+
authorized: false,
|
|
339
|
+
commandName: typeof record.commandName === "string" ? record.commandName : undefined,
|
|
340
|
+
body: typeof record.body === "string" ? record.body : undefined,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resolveWhatsAppDmRouteTarget(params: {
|
|
347
|
+
msg: WebInboundMsg;
|
|
348
|
+
senderE164?: string;
|
|
349
|
+
normalizeE164: (value: string) => string | null;
|
|
350
|
+
}): string | undefined {
|
|
351
|
+
if (params.msg.chatType === "group") {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
if (params.senderE164) {
|
|
355
|
+
return params.normalizeE164(params.senderE164) ?? undefined;
|
|
356
|
+
}
|
|
357
|
+
if (params.msg.from.includes("@")) {
|
|
358
|
+
return jidToE164(params.msg.from) ?? undefined;
|
|
359
|
+
}
|
|
360
|
+
return params.normalizeE164(params.msg.from) ?? undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function updateWhatsAppMainLastRoute(params: {
|
|
364
|
+
backgroundTasks: Set<Promise<unknown>>;
|
|
365
|
+
cfg: ReturnType<LoadConfigFn>;
|
|
366
|
+
ctx: Record<string, unknown>;
|
|
367
|
+
dmRouteTarget?: string;
|
|
368
|
+
pinnedMainDmRecipient: string | null;
|
|
369
|
+
route: ReturnType<typeof resolveAgentRoute>;
|
|
370
|
+
updateLastRoute: (params: {
|
|
371
|
+
cfg: ReturnType<LoadConfigFn>;
|
|
372
|
+
backgroundTasks: Set<Promise<unknown>>;
|
|
373
|
+
storeAgentId: string;
|
|
374
|
+
sessionKey: string;
|
|
375
|
+
channel: "whatsapp";
|
|
376
|
+
to: string;
|
|
377
|
+
accountId?: string;
|
|
378
|
+
ctx: Record<string, unknown>;
|
|
379
|
+
warn: ReturnType<typeof getChildLogger>["warn"];
|
|
380
|
+
}) => void;
|
|
381
|
+
warn: ReturnType<typeof getChildLogger>["warn"];
|
|
382
|
+
}) {
|
|
383
|
+
const shouldUpdateMainLastRoute =
|
|
384
|
+
!params.pinnedMainDmRecipient || params.pinnedMainDmRecipient === params.dmRouteTarget;
|
|
385
|
+
const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
|
|
386
|
+
route: params.route,
|
|
387
|
+
sessionKey: params.route.sessionKey,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
params.dmRouteTarget &&
|
|
392
|
+
inboundLastRouteSessionKey === params.route.mainSessionKey &&
|
|
393
|
+
shouldUpdateMainLastRoute
|
|
394
|
+
) {
|
|
395
|
+
params.updateLastRoute({
|
|
396
|
+
cfg: params.cfg,
|
|
397
|
+
backgroundTasks: params.backgroundTasks,
|
|
398
|
+
storeAgentId: params.route.agentId,
|
|
399
|
+
sessionKey: params.route.mainSessionKey,
|
|
400
|
+
channel: "whatsapp",
|
|
401
|
+
to: params.dmRouteTarget,
|
|
402
|
+
accountId: params.route.accountId,
|
|
403
|
+
ctx: params.ctx,
|
|
404
|
+
warn: params.warn,
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (
|
|
410
|
+
params.dmRouteTarget &&
|
|
411
|
+
inboundLastRouteSessionKey === params.route.mainSessionKey &&
|
|
412
|
+
params.pinnedMainDmRecipient
|
|
413
|
+
) {
|
|
414
|
+
logVerbose(
|
|
415
|
+
`Skipping main-session last route update for ${params.dmRouteTarget} (pinned owner ${params.pinnedMainDmRecipient})`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function dispatchWhatsAppBufferedReply(params: {
|
|
421
|
+
cfg: ReturnType<LoadConfigFn>;
|
|
422
|
+
connectionId: string;
|
|
423
|
+
context: Record<string, unknown>;
|
|
424
|
+
conversationId: string;
|
|
425
|
+
deliverReply: (params: {
|
|
426
|
+
replyResult: ReplyPayload;
|
|
427
|
+
normalizedReplyResult?: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
|
|
428
|
+
msg: WebInboundMsg;
|
|
429
|
+
mediaLocalRoots: readonly string[];
|
|
430
|
+
maxMediaBytes: number;
|
|
431
|
+
textLimit: number;
|
|
432
|
+
chunkMode?: ReturnType<typeof resolveChunkMode>;
|
|
433
|
+
replyLogger: ReturnType<typeof getChildLogger>;
|
|
434
|
+
connectionId?: string;
|
|
435
|
+
skipLog?: boolean;
|
|
436
|
+
tableMode?: ReturnType<typeof resolveMarkdownTableMode>;
|
|
437
|
+
}) => Promise<WhatsAppReplyDeliveryResult>;
|
|
438
|
+
groupHistories: Map<string, GroupHistoryEntry[]>;
|
|
439
|
+
groupHistoryKey: string;
|
|
440
|
+
maxMediaBytes: number;
|
|
441
|
+
maxMediaTextChunkLimit?: number;
|
|
442
|
+
msg: WebInboundMsg;
|
|
443
|
+
onModelSelected?: ChannelReplyOnModelSelected;
|
|
444
|
+
rememberSentText: (
|
|
445
|
+
text: string | undefined,
|
|
446
|
+
opts: {
|
|
447
|
+
combinedBody?: string;
|
|
448
|
+
combinedBodySessionKey?: string;
|
|
449
|
+
logVerboseMessage?: boolean;
|
|
450
|
+
},
|
|
451
|
+
) => void;
|
|
452
|
+
replyLogger: ReturnType<typeof getChildLogger>;
|
|
453
|
+
replyPipeline: WhatsAppDispatchPipeline;
|
|
454
|
+
replyResolver: typeof getReplyFromConfig;
|
|
455
|
+
route: ReturnType<typeof resolveAgentRoute>;
|
|
456
|
+
shouldClearGroupHistory: boolean;
|
|
457
|
+
statusReactionController?: StatusReactionController | null;
|
|
458
|
+
}) {
|
|
459
|
+
const statusReactionController = params.statusReactionController ?? null;
|
|
460
|
+
const statusReactionTiming = {
|
|
461
|
+
...DEFAULT_TIMING,
|
|
462
|
+
...params.cfg.messages?.statusReactions?.timing,
|
|
463
|
+
};
|
|
464
|
+
const removeAckAfterReply = params.cfg.messages?.removeAckAfterReply ?? false;
|
|
465
|
+
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
|
|
466
|
+
const chunkMode = resolveChunkMode(params.cfg, "whatsapp", params.route.accountId);
|
|
467
|
+
const tableMode = resolveMarkdownTableMode({
|
|
468
|
+
cfg: params.cfg,
|
|
469
|
+
channel: "whatsapp",
|
|
470
|
+
accountId: params.route.accountId,
|
|
471
|
+
});
|
|
472
|
+
const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId);
|
|
473
|
+
const sourceReplyChatType =
|
|
474
|
+
typeof params.context.ChatType === "string" ? params.context.ChatType : params.msg.chatType;
|
|
475
|
+
const sourceReplyCommandSource =
|
|
476
|
+
params.context.CommandSource === "native" || params.context.CommandSource === "text"
|
|
477
|
+
? params.context.CommandSource
|
|
478
|
+
: undefined;
|
|
479
|
+
const sourceReplyCommandTurn = normalizeCommandTurnFromContext(params.context.CommandTurn);
|
|
480
|
+
const sourceReplyCommandAuthorized =
|
|
481
|
+
typeof params.context.CommandAuthorized === "boolean"
|
|
482
|
+
? params.context.CommandAuthorized
|
|
483
|
+
: undefined;
|
|
484
|
+
const sourceReplyDeliveryMode =
|
|
485
|
+
sourceReplyChatType === "group" || sourceReplyChatType === "channel"
|
|
486
|
+
? resolveChannelMessageSourceReplyDeliveryMode({
|
|
487
|
+
cfg: params.cfg,
|
|
488
|
+
ctx: {
|
|
489
|
+
ChatType: sourceReplyChatType,
|
|
490
|
+
CommandTurn: sourceReplyCommandTurn,
|
|
491
|
+
CommandSource: sourceReplyCommandSource,
|
|
492
|
+
CommandAuthorized: sourceReplyCommandAuthorized,
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
: undefined;
|
|
496
|
+
const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
|
|
497
|
+
const disableBlockStreaming = sourceRepliesAreToolOnly
|
|
498
|
+
? true
|
|
499
|
+
: resolveWhatsAppDisableBlockStreaming(params.cfg);
|
|
500
|
+
let didSendReply = false;
|
|
501
|
+
let didLogHeartbeatStrip = false;
|
|
502
|
+
|
|
503
|
+
const deliverNormalizedPayload = async (
|
|
504
|
+
normalizedDeliveryPayload: DeliverableWhatsAppOutboundPayload<ReplyPayload>,
|
|
505
|
+
info: ReplyDeliveryInfo,
|
|
506
|
+
) => {
|
|
507
|
+
const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
|
|
508
|
+
if (!reply.hasMedia && !reply.text.trim()) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const delivery = await params.deliverReply({
|
|
512
|
+
replyResult: normalizedDeliveryPayload,
|
|
513
|
+
normalizedReplyResult: normalizedDeliveryPayload,
|
|
514
|
+
msg: params.msg,
|
|
515
|
+
mediaLocalRoots,
|
|
516
|
+
maxMediaBytes: params.maxMediaBytes,
|
|
517
|
+
textLimit,
|
|
518
|
+
chunkMode,
|
|
519
|
+
replyLogger: params.replyLogger,
|
|
520
|
+
connectionId: params.connectionId,
|
|
521
|
+
skipLog: false,
|
|
522
|
+
tableMode,
|
|
523
|
+
});
|
|
524
|
+
if (!delivery.providerAccepted) {
|
|
525
|
+
params.replyLogger.warn(
|
|
526
|
+
{
|
|
527
|
+
correlationId: params.msg.id ?? null,
|
|
528
|
+
connectionId: params.connectionId,
|
|
529
|
+
conversationId: params.conversationId,
|
|
530
|
+
chatId: params.msg.chatId,
|
|
531
|
+
to: params.msg.from,
|
|
532
|
+
from: params.msg.to,
|
|
533
|
+
replyKind: info.kind,
|
|
534
|
+
},
|
|
535
|
+
"auto-reply was not accepted by WhatsApp provider",
|
|
536
|
+
);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
didSendReply = true;
|
|
540
|
+
const shouldLog = normalizedDeliveryPayload.text ? true : undefined;
|
|
541
|
+
params.rememberSentText(normalizedDeliveryPayload.text, {
|
|
542
|
+
combinedBody: params.context.Body as string | undefined,
|
|
543
|
+
combinedBodySessionKey: params.route.sessionKey,
|
|
544
|
+
logVerboseMessage: shouldLog,
|
|
545
|
+
});
|
|
546
|
+
const fromDisplay =
|
|
547
|
+
params.msg.chatType === "group" ? params.conversationId : (params.msg.from ?? "unknown");
|
|
548
|
+
if (shouldLogVerbose()) {
|
|
549
|
+
const preview = normalizedDeliveryPayload.text != null ? reply.text : "<media>";
|
|
550
|
+
logVerbose(`Reply body: ${preview}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({
|
|
555
|
+
deliver: async (pending) => {
|
|
556
|
+
await deliverNormalizedPayload(pending.payload, pending.info);
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (statusReactionController) {
|
|
561
|
+
void statusReactionController.setThinking();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({
|
|
565
|
+
ctx: params.context,
|
|
566
|
+
cfg: params.cfg,
|
|
567
|
+
replyResolver: params.replyResolver,
|
|
568
|
+
dispatcherOptions: {
|
|
569
|
+
...params.replyPipeline,
|
|
570
|
+
onHeartbeatStrip: () => {
|
|
571
|
+
if (!didLogHeartbeatStrip) {
|
|
572
|
+
didLogHeartbeatStrip = true;
|
|
573
|
+
logVerbose("Stripped stray HEARTBEAT_OK token from web reply");
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
deliver: async (payload: ReplyPayload, info: { kind: ReplyLifecycleKind }) => {
|
|
577
|
+
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
|
|
578
|
+
if (!deliveryPayload) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const normalizedOutboundPayload = normalizeWhatsAppOutboundPayload(deliveryPayload, {
|
|
582
|
+
normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation,
|
|
583
|
+
});
|
|
584
|
+
const normalizedDeliveryPayload =
|
|
585
|
+
deliveryPayload.text === undefined
|
|
586
|
+
? { ...normalizedOutboundPayload, text: undefined }
|
|
587
|
+
: normalizedOutboundPayload;
|
|
588
|
+
const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
|
|
589
|
+
if (!reply.hasMedia && !reply.text.trim()) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (!reply.hasMedia) {
|
|
593
|
+
logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll());
|
|
594
|
+
const durable = await deliverInboundReplyWithMessageSendContext({
|
|
595
|
+
cfg: params.cfg,
|
|
596
|
+
channel: "whatsapp",
|
|
597
|
+
accountId: params.route.accountId,
|
|
598
|
+
agentId: params.route.agentId,
|
|
599
|
+
ctxPayload: params.context as FinalizedMsgContext,
|
|
600
|
+
payload: normalizedDeliveryPayload,
|
|
601
|
+
info,
|
|
602
|
+
to: params.msg.from,
|
|
603
|
+
formatting: {
|
|
604
|
+
textLimit,
|
|
605
|
+
tableMode,
|
|
606
|
+
chunkMode,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
if (durable.status === "failed") {
|
|
610
|
+
throw durable.error;
|
|
611
|
+
}
|
|
612
|
+
if (durable.status === "handled_visible") {
|
|
613
|
+
didSendReply = true;
|
|
614
|
+
const shouldLog = normalizedDeliveryPayload.text ? true : undefined;
|
|
615
|
+
params.rememberSentText(normalizedDeliveryPayload.text, {
|
|
616
|
+
combinedBody: params.context.Body as string | undefined,
|
|
617
|
+
combinedBodySessionKey: params.route.sessionKey,
|
|
618
|
+
logVerboseMessage: shouldLog,
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (durable.status === "handled_no_send") {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
await deliverNormalizedPayload(normalizedDeliveryPayload, info);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const mediaUrls = getWhatsAppPayloadMediaUrls(normalizedDeliveryPayload);
|
|
629
|
+
if (shouldDeferWhatsAppMediaOnlyPayload({ info, mediaUrls, reply })) {
|
|
630
|
+
mediaOnlyCoalescer.defer({
|
|
631
|
+
info,
|
|
632
|
+
mediaUrls,
|
|
633
|
+
payload: normalizedDeliveryPayload,
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
logWhatsAppMediaOnlyFlushResult(
|
|
638
|
+
await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls),
|
|
639
|
+
);
|
|
640
|
+
await deliverNormalizedPayload(normalizedDeliveryPayload, info);
|
|
641
|
+
},
|
|
642
|
+
onReplyStart: params.msg.sendComposing,
|
|
643
|
+
...(statusReactionController
|
|
644
|
+
? {
|
|
645
|
+
onCompactionStart: async () => {
|
|
646
|
+
await statusReactionController.setCompacting();
|
|
647
|
+
},
|
|
648
|
+
onCompactionEnd: async () => {
|
|
649
|
+
statusReactionController.cancelPending();
|
|
650
|
+
await statusReactionController.setThinking();
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
: {}),
|
|
654
|
+
onError: (err, info) => {
|
|
655
|
+
logWhatsAppReplyDeliveryError({
|
|
656
|
+
err,
|
|
657
|
+
info,
|
|
658
|
+
connectionId: params.connectionId,
|
|
659
|
+
conversationId: params.conversationId,
|
|
660
|
+
msg: params.msg,
|
|
661
|
+
replyLogger: params.replyLogger,
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
replyOptions: {
|
|
666
|
+
disableBlockStreaming,
|
|
667
|
+
...(sourceReplyDeliveryMode ? { sourceReplyDeliveryMode } : {}),
|
|
668
|
+
onModelSelected: params.onModelSelected,
|
|
669
|
+
...(statusReactionController
|
|
670
|
+
? {
|
|
671
|
+
onToolStart: async (payload: { name?: string }) => {
|
|
672
|
+
const toolName = payload.name?.trim();
|
|
673
|
+
if (toolName) {
|
|
674
|
+
await statusReactionController.setTool(toolName);
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
}
|
|
678
|
+
: {}),
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll());
|
|
682
|
+
|
|
683
|
+
const didQueueVisibleReply = hasVisibleInboundReplyDispatch({ queuedFinal, counts });
|
|
684
|
+
if (!didQueueVisibleReply) {
|
|
685
|
+
if (statusReactionController) {
|
|
686
|
+
void finalizeWhatsAppStatusReaction({
|
|
687
|
+
controller: statusReactionController,
|
|
688
|
+
outcome: "error",
|
|
689
|
+
hasFinalResponse: false,
|
|
690
|
+
removeAckAfterReply,
|
|
691
|
+
timing: statusReactionTiming,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (params.shouldClearGroupHistory) {
|
|
695
|
+
params.groupHistories.set(params.groupHistoryKey, []);
|
|
696
|
+
}
|
|
697
|
+
logVerbose("Skipping auto-reply: silent token or no text/media returned from resolver");
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (statusReactionController) {
|
|
702
|
+
void finalizeWhatsAppStatusReaction({
|
|
703
|
+
controller: statusReactionController,
|
|
704
|
+
outcome: didSendReply ? "done" : "error",
|
|
705
|
+
hasFinalResponse: didSendReply,
|
|
706
|
+
removeAckAfterReply,
|
|
707
|
+
timing: statusReactionTiming,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (params.shouldClearGroupHistory) {
|
|
712
|
+
params.groupHistories.set(params.groupHistoryKey, []);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return didSendReply;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function finalizeWhatsAppStatusReaction(params: {
|
|
719
|
+
controller: StatusReactionController;
|
|
720
|
+
outcome: "done" | "error";
|
|
721
|
+
hasFinalResponse: boolean;
|
|
722
|
+
removeAckAfterReply: boolean;
|
|
723
|
+
timing: typeof DEFAULT_TIMING;
|
|
724
|
+
}): Promise<void> {
|
|
725
|
+
if (params.outcome === "done") {
|
|
726
|
+
await params.controller.setDone();
|
|
727
|
+
if (params.removeAckAfterReply) {
|
|
728
|
+
await new Promise<void>((resolve) => setTimeout(resolve, params.timing.doneHoldMs));
|
|
729
|
+
await params.controller.clear();
|
|
730
|
+
} else {
|
|
731
|
+
await params.controller.restoreInitial();
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
await params.controller.setError();
|
|
736
|
+
if (params.hasFinalResponse) {
|
|
737
|
+
if (params.removeAckAfterReply) {
|
|
738
|
+
await new Promise<void>((resolve) => setTimeout(resolve, params.timing.errorHoldMs));
|
|
739
|
+
await params.controller.clear();
|
|
740
|
+
} else {
|
|
741
|
+
await params.controller.restoreInitial();
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (params.removeAckAfterReply) {
|
|
746
|
+
await new Promise<void>((resolve) => setTimeout(resolve, params.timing.errorHoldMs));
|
|
747
|
+
}
|
|
748
|
+
await params.controller.restoreInitial();
|
|
749
|
+
}
|