@teamscala/service-boot 0.23.411 → 0.23.412
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/package.json
CHANGED
|
@@ -148,21 +148,16 @@ export function formatSpanMinutes(minutes: number): string {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* The WhatsApp message id from a
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* delivered.
|
|
151
|
+
* The WhatsApp message id from a messaging_whatsapp_send result. The
|
|
152
|
+
* intrinsic (scala-mcp / messaging-mcp-boot) returns
|
|
153
|
+
* `{ ok, message_id, error }` — the Rust scala-messaging-service owns the
|
|
154
|
+
* channel, picks the device, and records the send in messaging_logs itself.
|
|
155
|
+
* Delivery is only VERIFIED on a real id — a `"true"` ack, an error body, or
|
|
156
|
+
* an empty body is not a message id and must not be recorded as delivered.
|
|
158
157
|
*/
|
|
159
|
-
export function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return /^[A-Z0-9]{8,}$/i.test(results) ? results : null;
|
|
163
|
-
}
|
|
164
|
-
if (results && typeof results === "object") {
|
|
165
|
-
const id = (results as { message_id?: unknown }).message_id;
|
|
158
|
+
export function extractWhatsAppMessageId(sendResult: unknown): string | null {
|
|
159
|
+
if (sendResult && typeof sendResult === "object") {
|
|
160
|
+
const id = (sendResult as { message_id?: unknown }).message_id;
|
|
166
161
|
if (typeof id === "string" && id.length > 0) return id;
|
|
167
162
|
}
|
|
168
163
|
return null;
|
|
@@ -12,7 +12,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
12
12
|
import {
|
|
13
13
|
composeEscalationMessage,
|
|
14
14
|
decideSweep,
|
|
15
|
-
|
|
15
|
+
extractWhatsAppMessageId,
|
|
16
16
|
formatEscalationSample,
|
|
17
17
|
type EscalationEpisode,
|
|
18
18
|
type FaultAggregate,
|
|
@@ -186,23 +186,20 @@ describe("decideSweep — a future-dated lastSeen is not recurrence evidence", (
|
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
describe("
|
|
190
|
-
test("the
|
|
191
|
-
expect(
|
|
189
|
+
describe("extractWhatsAppMessageId — delivery verification", () => {
|
|
190
|
+
test("the messaging_whatsapp_send result shape yields message_id", () => {
|
|
191
|
+
expect(extractWhatsAppMessageId({ ok: true, message_id: "3EB089B9D6ADD58153C561" })).toBe(
|
|
192
192
|
"3EB089B9D6ADD58153C561",
|
|
193
193
|
);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
test("
|
|
197
|
-
expect(extractGowaMessageId({ results: "3EB089B9D6ADD58153C561" })).toBe("3EB089B9D6ADD58153C561");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("a 'true' ack is NOT a message id — delivery stays unverified", () => {
|
|
196
|
+
test("a 'true' ack or an error body is NOT a message id — delivery stays unverified", () => {
|
|
201
197
|
// A boolean-ish ack string must never be recorded as a delivered id:
|
|
202
198
|
// that would mark the episode delivered with an unfollowable handle.
|
|
203
|
-
expect(
|
|
204
|
-
expect(
|
|
205
|
-
expect(
|
|
199
|
+
expect(extractWhatsAppMessageId({ ok: true })).toBeNull();
|
|
200
|
+
expect(extractWhatsAppMessageId({ ok: false, error: "no device" })).toBeNull();
|
|
201
|
+
expect(extractWhatsAppMessageId("true")).toBeNull();
|
|
202
|
+
expect(extractWhatsAppMessageId(null)).toBeNull();
|
|
206
203
|
});
|
|
207
204
|
});
|
|
208
205
|
|
|
@@ -19,7 +19,7 @@ import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
|
19
19
|
import {
|
|
20
20
|
composeEscalationMessage,
|
|
21
21
|
decideSweep,
|
|
22
|
-
|
|
22
|
+
extractWhatsAppMessageId,
|
|
23
23
|
formatEscalationSample,
|
|
24
24
|
type EscalationEpisode,
|
|
25
25
|
type FaultAggregate,
|
|
@@ -276,20 +276,6 @@ async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
|
|
|
276
276
|
return bot as OperatorRelayBot;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
/** GOWA device whose JID matches the PA number, so the escalation is sent BY the PA. */
|
|
280
|
-
async function resolvePaDeviceId(paPhoneDigits: string): Promise<string | undefined> {
|
|
281
|
-
const { userId, organisationId } = await getSystemIdentity();
|
|
282
|
-
const systemCtx: ExecutionContext = { userId, organisationId } as ExecutionContext;
|
|
283
|
-
const { forwardToolToCentralMcp } = await import("@teamscala/chat-completion/central-tool-forwarder");
|
|
284
|
-
const res = (await forwardToolToCentralMcp(
|
|
285
|
-
"api_gowa_devices",
|
|
286
|
-
{ action: "listDevices" },
|
|
287
|
-
systemCtx,
|
|
288
|
-
)) as { results?: Array<{ id: string; jid?: string }> } | undefined;
|
|
289
|
-
const match = (res?.results ?? []).find((device) => (device.jid ?? "").includes(paPhoneDigits));
|
|
290
|
-
return match?.id;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
279
|
async function deliverEscalation(
|
|
294
280
|
kind: string,
|
|
295
281
|
aggregate: FaultAggregate,
|
|
@@ -316,7 +302,12 @@ async function deliverEscalation(
|
|
|
316
302
|
},
|
|
317
303
|
});
|
|
318
304
|
|
|
319
|
-
// 2. Resolve the
|
|
305
|
+
// 2. Resolve the operator relay and send. The send surface is the
|
|
306
|
+
// `messaging_whatsapp_send` intrinsic (scala-mcp / messaging-mcp-boot),
|
|
307
|
+
// which delegates to the Rust scala-messaging-service — the channel owner
|
|
308
|
+
// that picks the device and records the send in messaging_logs. The retired
|
|
309
|
+
// api_gowa_message tool (deactivated 2026-08-26) is no longer callable;
|
|
310
|
+
// naming it here left every escalation send failing with "Unknown tool".
|
|
320
311
|
const bot = await resolveOperatorRelay();
|
|
321
312
|
const operatorPhone = (bot.flow_config as { operator_phone?: string }).operator_phone!;
|
|
322
313
|
const copy = config.copy[kind] ?? config.default_copy;
|
|
@@ -333,23 +324,15 @@ async function deliverEscalation(
|
|
|
333
324
|
const { userId, organisationId } = await getSystemIdentity();
|
|
334
325
|
const systemCtx: ExecutionContext = { userId, organisationId } as ExecutionContext;
|
|
335
326
|
const { forwardToolToCentralMcp } = await import("@teamscala/chat-completion/central-tool-forwarder");
|
|
336
|
-
const paDigits = (bot.external_id ?? "").replace(/\D/g, "");
|
|
337
|
-
const deviceId = paDigits ? await resolvePaDeviceId(paDigits) : undefined;
|
|
338
|
-
if (paDigits && !deviceId) {
|
|
339
|
-
log.warn("[platform-fault-escalation] no GOWA device matched the PA number — sending via default device");
|
|
340
|
-
}
|
|
341
327
|
const sendResult = await forwardToolToCentralMcp(
|
|
342
|
-
"
|
|
343
|
-
{
|
|
344
|
-
action: "sendMessage",
|
|
345
|
-
params: { phone: operatorPhone, message, ...(deviceId ? { device_id: deviceId } : {}) },
|
|
346
|
-
},
|
|
328
|
+
"messaging_whatsapp_send",
|
|
329
|
+
{ phone: operatorPhone, message },
|
|
347
330
|
systemCtx,
|
|
348
331
|
);
|
|
349
332
|
|
|
350
333
|
// 3. VERIFY delivery before recording it: the send must return a WhatsApp
|
|
351
334
|
// message id. No id => throw, the claim stays undelivered, next sweep retries.
|
|
352
|
-
const messageId =
|
|
335
|
+
const messageId = extractWhatsAppMessageId(sendResult);
|
|
353
336
|
if (!messageId) {
|
|
354
337
|
throw new Error(
|
|
355
338
|
`platform-fault-escalation: WhatsApp send for '${kind}' returned no message id — not recording delivery. ` +
|