@automagik/omni 2.260904.2 → 2.260906.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/dist/index.js CHANGED
@@ -128298,7 +128298,7 @@ import { fileURLToPath } from "url";
128298
128298
  // package.json
128299
128299
  var package_default = {
128300
128300
  name: "@automagik/omni",
128301
- version: "2.260904.2",
128301
+ version: "2.260906.1",
128302
128302
  repository: {
128303
128303
  type: "git",
128304
128304
  url: "git+https://github.com/automagik-dev/omni.git",
@@ -242910,7 +242910,7 @@ var init_sentry_scrub = __esm(() => {
242910
242910
  var require_package7 = __commonJS((exports, module) => {
242911
242911
  module.exports = {
242912
242912
  name: "@omni/api",
242913
- version: "2.260904.2",
242913
+ version: "2.260906.1",
242914
242914
  type: "module",
242915
242915
  exports: {
242916
242916
  ".": {
@@ -380157,11 +380157,28 @@ function decodeAscEmoji(text) {
380157
380157
  }
380158
380158
  });
380159
380159
  }
380160
+ var TRANSLITERATIONS = [
380161
+ [/[\u2014\u2013\u2212]/g, "-"],
380162
+ [/\u2026/g, "..."],
380163
+ [/[\u201C\u201D\u201E]/g, '"'],
380164
+ [/[\u2018\u2019\u201A]/g, "'"],
380165
+ [/\u2192/g, "->"],
380166
+ [/\u2190/g, "<-"],
380167
+ [/\u2265/g, ">="],
380168
+ [/\u2264/g, "<="],
380169
+ [/\u00A0/g, " "],
380170
+ [/[\u2022\u00B7]/g, "-"]
380171
+ ];
380172
+ function nonLatin1Left(text) {
380173
+ return [...new Set([...text].filter((ch) => (ch.codePointAt(0) ?? 0) > 255))];
380174
+ }
380160
380175
  function encodeAscEmoji(text) {
380161
- return text.replace(EMOJI_RUN, (run) => `##${[...run].map((ch) => (ch.codePointAt(0) ?? 0).toString(16)).join("-")}##`);
380176
+ const withMarkers = text.replace(EMOJI_RUN, (run) => `##${[...run].map((ch) => (ch.codePointAt(0) ?? 0).toString(16)).join("-")}##`);
380177
+ return TRANSLITERATIONS.reduce((acc, [pattern, ascii]) => acc.replace(pattern, ascii), withMarkers);
380162
380178
  }
380163
380179
 
380164
380180
  // ../channel-asc-flow/src/handlers/webhook.ts
380181
+ var holdTimeoutMs = () => Number(process.env.ASC_FLOW_HOLD_MS ?? 40000);
380165
380182
  var MAX_BODY_BYTES = 64 * 1024;
380166
380183
  function json(body, status = 200) {
380167
380184
  return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -380178,13 +380195,16 @@ function firstString(...values) {
380178
380195
  }
380179
380196
  function parseInboundTurn(body) {
380180
380197
  const codAtendimento = firstString(body.codAtendimento, body.cod_atendimento);
380181
- const text = decodeAscEmoji(firstString(body.chatInput, body.message));
380198
+ const typed = decodeAscEmoji(firstString(body.chatInput));
380199
+ const fallback = decodeAscEmoji(firstString(body.message));
380200
+ const text = typed || fallback;
380182
380201
  if (!codAtendimento || !text)
380183
380202
  return null;
380184
380203
  const messageId = firstString(body.messageId, body.idMensagem);
380185
380204
  return {
380186
380205
  codAtendimento,
380187
380206
  text,
380207
+ fromFallback: !typed,
380188
380208
  phone: firstString(body.phone, body.telefone),
380189
380209
  ...messageId ? { messageId } : {}
380190
380210
  };
@@ -380232,12 +380252,26 @@ async function handleAscFlowWebhookRequest(request, plugin2, instanceId, verifyT
380232
380252
  if (ready) {
380233
380253
  return json(ready);
380234
380254
  }
380255
+ if (turn.fromFallback && plugin2.hasSeenCod(instanceId, turn.codAtendimento)) {
380256
+ logger5.debug("[asc-flow] loop-back with no chatInput \u2014 treating as a poll", {
380257
+ instanceId,
380258
+ codAtendimento: turn.codAtendimento
380259
+ });
380260
+ return pending();
380261
+ }
380262
+ if (await plugin2.isStaleFlowReplay(instanceId, turn)) {
380263
+ return pending();
380264
+ }
380235
380265
  const isRedelivery = turn.messageId ? dedupeCache.isDuplicate(instanceId, turn.messageId, "asc-flow", logger5) : plugin2.isRedeliveryOfTurnInFlight(instanceId, turn);
380236
380266
  if (isRedelivery) {
380237
380267
  return pending();
380238
380268
  }
380239
380269
  await plugin2.handleInboundTurn(instanceId, turn);
380240
- return pending();
380270
+ const holdMs = holdTimeoutMs();
380271
+ if (holdMs <= 0)
380272
+ return pending();
380273
+ const held = await plugin2.waitForTurn(instanceId, turn.codAtendimento, turn.text, holdMs);
380274
+ return held ? json(held) : pending();
380241
380275
  }
380242
380276
 
380243
380277
  // ../channel-asc-flow/src/utils/handoff.ts
@@ -380313,6 +380347,16 @@ function buildGenesysFields(read, logger5, mode) {
380313
380347
  init_src2();
380314
380348
  var MAX_OPTIONS = 10;
380315
380349
  var MAX_BODY_TEXT = 1024;
380350
+ var MAX_BUTTONS = 3;
380351
+ var BUTTON_TITLE_MAX = 20;
380352
+ function shortenTitle(title) {
380353
+ const value = title.trim();
380354
+ if (value.length <= BUTTON_TITLE_MAX)
380355
+ return value;
380356
+ const head = value.slice(0, BUTTON_TITLE_MAX);
380357
+ const boundary = head.lastIndexOf(" ");
380358
+ return (boundary >= 8 ? head.slice(0, boundary) : head).trim();
380359
+ }
380316
380360
  function foldTitle(value) {
380317
380361
  return value.normalize("NFD").replace(/\p{Mn}/gu, "").toLocaleLowerCase().split(/\s+/).filter(Boolean).join(" ");
380318
380362
  }
@@ -380338,14 +380382,19 @@ function buildUra(body, buttons, listOptions = {}) {
380338
380382
  const type = plan.interactive.type;
380339
380383
  if (type !== "button" && type !== "list")
380340
380384
  return null;
380341
- const titles = titlesOf(plan.interactive);
380385
+ let titles = titlesOf(plan.interactive);
380386
+ let asButtons = type === "button";
380387
+ if (!asButtons && replyButtons.length <= MAX_BUTTONS) {
380388
+ titles = replyButtons.map((b2) => shortenTitle(b2.text ?? ""));
380389
+ asButtons = true;
380390
+ }
380342
380391
  if (titles.length !== replyButtons.length || titles.some((t) => !t))
380343
380392
  return null;
380344
380393
  if (new Set(titles.map(foldTitle)).size !== titles.length)
380345
380394
  return null;
380346
380395
  return {
380347
380396
  ura_opcoes: Object.fromEntries(titles.map((title, i) => [String(i + 1), title])),
380348
- forcar_botoes: type === "button"
380397
+ forcar_botoes: asButtons
380349
380398
  };
380350
380399
  }
380351
380400
  function splitBubbles(text) {
@@ -380564,6 +380613,47 @@ function buildReplyField(replyTo) {
380564
380613
  return /^\d+$/.test(trimmed) ? { id_mensagem_resposta: Number(trimmed) } : {};
380565
380614
  }
380566
380615
 
380616
+ // ../channel-asc-flow/src/utils/turn-freshness.ts
380617
+ async function latestInboundText(client, codAtendimento) {
380618
+ const { status, body } = await client.get("/atendimento", { codigo_atendimento: codAtendimento });
380619
+ if (status !== 200 || typeof body !== "object" || body === null)
380620
+ return null;
380621
+ const list = body.mensagens;
380622
+ if (!Array.isArray(list))
380623
+ return null;
380624
+ for (const raw of [...list].reverse()) {
380625
+ if (String(raw.boleano_entrante ?? "") !== "1")
380626
+ continue;
380627
+ const text = decodeAscEmoji(String(raw.descricao_msg ?? "").trim());
380628
+ if (text)
380629
+ return text;
380630
+ }
380631
+ return null;
380632
+ }
380633
+ async function isStaleFlowReplay(params) {
380634
+ const { client, instanceId, codAtendimento, text, logger: logger5 } = params;
380635
+ let latest;
380636
+ try {
380637
+ latest = await latestInboundText(client, codAtendimento);
380638
+ } catch (err2) {
380639
+ logger5.warn("[asc-flow] could not read the atendimento to date this turn \u2014 processing it", {
380640
+ instanceId,
380641
+ codAtendimento,
380642
+ err: String(err2)
380643
+ });
380644
+ return false;
380645
+ }
380646
+ if (latest === null)
380647
+ return false;
380648
+ if (latest === text.trim())
380649
+ return false;
380650
+ logger5.info("[asc-flow] flow restarted with a stale input variable \u2014 dropping the replay", {
380651
+ instanceId,
380652
+ codAtendimento
380653
+ });
380654
+ return true;
380655
+ }
380656
+
380567
380657
  // ../channel-asc-flow/src/plugin.ts
380568
380658
  var DEFAULT_ASC_FLOW_BASE_URL = "https://sac-notredame.ascbrazil.com.br";
380569
380659
  var REST_PREFIX = "/rest/v2";
@@ -380573,11 +380663,19 @@ var UNDELIVERABLE_ERROR = "no ASC flow turn is polling this cod_atendimento \u20
380573
380663
  var HANDOFF_REFUSED_ERROR = "handoff refused: the transfer never held, so the turn answers without it";
380574
380664
  var IN_FLIGHT_SWEEP_MS = 60000;
380575
380665
  var IN_FLIGHT_MAX_ENTRIES = 5000;
380576
- function resolveOutboundText(message2) {
380666
+ function resolveOutboundText(message2, logger5) {
380577
380667
  const formatMode = message2.metadata?.messageFormatMode ?? "convert";
380578
380668
  const text = message2.content.text ?? message2.content.caption ?? "";
380579
380669
  const formatted = formatMode === "passthrough" ? text : markdownToWhatsApp(text);
380580
- return encodeAscEmoji(formatted);
380670
+ const encoded = encodeAscEmoji(formatted);
380671
+ const restante = nonLatin1Left(encoded);
380672
+ if (restante.length > 0) {
380673
+ logger5?.warn('[asc-flow] characters the platform cannot carry \u2014 they will arrive as "?"', {
380674
+ chars: restante.join(" "),
380675
+ codepoints: restante.map((c) => (c.codePointAt(0) ?? 0).toString(16)).join(" ")
380676
+ });
380677
+ }
380678
+ return encoded;
380581
380679
  }
380582
380680
  function collectTurnParts(message2, turn) {
380583
380681
  const meta = message2.metadata ?? {};
@@ -380618,6 +380716,8 @@ function buildReadyBody(turn) {
380618
380716
  resposta: turn.delivered ? "" : turn.lastBubble || OUTBOUND_MEDIA_FALLBACK_TEXT,
380619
380717
  hand_off: turn.handoff ? "sim" : "nao",
380620
380718
  bolhas: turn.bubbles,
380719
+ fila_vq: "",
380720
+ motivo_transf_vq: "",
380621
380721
  ...turn.handoff ?? {},
380622
380722
  ...turn.ura ?? {}
380623
380723
  };
@@ -380670,7 +380770,9 @@ class AscFlowPlugin extends BaseChannelPlugin {
380670
380770
  config: ascFlowConfig,
380671
380771
  dedupeCache: createInboundDedupeCache(),
380672
380772
  inFlight: new Map,
380673
- lastSweepAt: Date.now()
380773
+ lastSweepAt: Date.now(),
380774
+ seenCods: new Set,
380775
+ lastAnswered: new Map
380674
380776
  });
380675
380777
  await this.updateInstanceStatus(instanceId, config2, {
380676
380778
  state: "connected",
@@ -380725,12 +380827,10 @@ class AscFlowPlugin extends BaseChannelPlugin {
380725
380827
  if (!turnMessage)
380726
380828
  return { success: true, timestamp: Date.now() };
380727
380829
  const content = turnMessage.content;
380728
- const text = resolveOutboundText(turnMessage);
380830
+ const text = resolveOutboundText(turnMessage, this.logger);
380729
380831
  const { rich, bubbles } = await this.prepareTurn(turnMessage, text);
380730
380832
  const lastBubble = bubbles[bubbles.length - 1] ?? "";
380731
380833
  const ura = rich ? null : buildUra(lastBubble, content.buttons, listOptionsOf(content));
380732
- if (!rich && !ura && !polling)
380733
- return this.refuseUndeliverable(instanceId, to, content.type);
380734
380834
  const { delivered } = await this.deliver(state, cod, {
380735
380835
  text,
380736
380836
  bubbles,
@@ -380739,6 +380839,8 @@ class AscFlowPlugin extends BaseChannelPlugin {
380739
380839
  ura,
380740
380840
  message: turnMessage
380741
380841
  });
380842
+ if (!delivered && !polling)
380843
+ return this.refuseUndeliverable(instanceId, to, content.type);
380742
380844
  const handoff = meta.isHandoff === true ? await this.runHandoff(state, instanceId, cod, meta, delivered ? "" : lastBubble) : null;
380743
380845
  const handoffRefused = meta.isHandoff === true && handoff === null;
380744
380846
  this.resolveTurn(state, to, buildReadyBody({ delivered, lastBubble, bubbles, handoff, ura }), answering, correlationId);
@@ -380893,13 +380995,19 @@ class AscFlowPlugin extends BaseChannelPlugin {
380893
380995
  async deliver(state, cod, turn) {
380894
380996
  const reply = buildReplyField(turn.message.replyTo);
380895
380997
  if (turn.rich) {
380896
- return { delivered: await this.sendMensagem(state, cod, turn.text, { ...turn.rich, ...reply }) };
380998
+ if (await this.sendMensagem(state, cod, turn.text, { ...turn.rich, ...reply }))
380999
+ return { delivered: true };
381000
+ const texto = turn.bubbles.length > 0 ? turn.bubbles : [OUTBOUND_MEDIA_FALLBACK_TEXT];
381001
+ return { delivered: await this.pushBubbles(state, cod, texto) > 0 };
380897
381002
  }
380898
- await this.pushLeadingBubbles(state, cod, turn.bubbles);
380899
381003
  if (turn.ura) {
380900
- return { delivered: await this.sendMensagem(state, cod, turn.lastBubble, { ...turn.ura, ...reply }) };
381004
+ await this.pushBubbles(state, cod, turn.bubbles.slice(0, -1));
381005
+ if (await this.sendMensagem(state, cod, turn.lastBubble, { ...turn.ura, ...reply }))
381006
+ return { delivered: true };
381007
+ return { delivered: await this.pushBubbles(state, cod, [turn.lastBubble]) > 0 };
380901
381008
  }
380902
- return { delivered: false };
381009
+ const pushed = await this.pushBubbles(state, cod, turn.bubbles);
381010
+ return { delivered: pushed > 0 };
380903
381011
  }
380904
381012
  async sendMensagem(state, cod, text, extra) {
380905
381013
  try {
@@ -380920,8 +381028,9 @@ class AscFlowPlugin extends BaseChannelPlugin {
380920
381028
  return false;
380921
381029
  }
380922
381030
  }
380923
- async pushLeadingBubbles(state, cod, bubbles) {
380924
- for (const bubble of bubbles.slice(0, -1)) {
381031
+ async pushBubbles(state, cod, bubbles) {
381032
+ let enviadas = 0;
381033
+ for (const bubble of bubbles) {
380925
381034
  try {
380926
381035
  await state.client.call("/callbackFlowMsg", {
380927
381036
  cod_atendimento: cod,
@@ -380929,15 +381038,21 @@ class AscFlowPlugin extends BaseChannelPlugin {
380929
381038
  msg_usuario: bubble,
380930
381039
  entrante: 0
380931
381040
  });
380932
- await state.client.call("/sendIndicador", { cod, tipo: 1 });
381041
+ enviadas++;
381042
+ if (enviadas < bubbles.length) {
381043
+ await state.client.call("/sendIndicador", { cod, tipo: 1 });
381044
+ }
380933
381045
  } catch (err2) {
380934
- this.logger.warn("[asc-flow] leading bubble push failed \u2014 degrading to resposta", {
381046
+ this.logger.warn("[asc-flow] bubble push failed \u2014 stopping so the rest do not arrive out of order", {
380935
381047
  cod,
381048
+ enviadas,
381049
+ total: bubbles.length,
380936
381050
  err: String(err2)
380937
381051
  });
380938
- return;
381052
+ return enviadas;
380939
381053
  }
380940
381054
  }
381055
+ return enviadas;
380941
381056
  }
380942
381057
  async handleWebhook(request) {
380943
381058
  const url = new URL(request.url);
@@ -380957,6 +381072,12 @@ class AscFlowPlugin extends BaseChannelPlugin {
380957
381072
  if (inboundState) {
380958
381073
  this.sweepInFlight(instanceId, inboundState);
380959
381074
  inboundState.inFlight.set(turn.codAtendimento, { text: turn.text, at: Date.now() });
381075
+ if (inboundState.seenCods.size >= IN_FLIGHT_MAX_ENTRIES) {
381076
+ const oldest = inboundState.seenCods.values().next().value;
381077
+ if (oldest !== undefined)
381078
+ inboundState.seenCods.delete(oldest);
381079
+ }
381080
+ inboundState.seenCods.add(turn.codAtendimento);
380960
381081
  }
380961
381082
  await this.sendTyping(instanceId, turn.codAtendimento);
380962
381083
  const externalId = turn.messageId ?? crypto.randomUUID();
@@ -381004,6 +381125,9 @@ class AscFlowPlugin extends BaseChannelPlugin {
381004
381125
  getLogger() {
381005
381126
  return this.logger;
381006
381127
  }
381128
+ hasSeenCod(instanceId, codAtendimento) {
381129
+ return this.ascFlowInstances.get(instanceId)?.seenCods.has(codAtendimento) ?? false;
381130
+ }
381007
381131
  sweepInFlight(instanceId, state) {
381008
381132
  const now = Date.now();
381009
381133
  if (now - state.lastSweepAt < IN_FLIGHT_SWEEP_MS && state.inFlight.size < IN_FLIGHT_MAX_ENTRIES)
@@ -381061,8 +381185,45 @@ class AscFlowPlugin extends BaseChannelPlugin {
381061
381185
  ageMs
381062
381186
  });
381063
381187
  }
381188
+ if (state.lastAnswered.size >= IN_FLIGHT_MAX_ENTRIES) {
381189
+ const oldest = state.lastAnswered.keys().next().value;
381190
+ if (oldest !== undefined)
381191
+ state.lastAnswered.delete(oldest);
381192
+ }
381193
+ state.lastAnswered.set(codAtendimento, text.trim());
381064
381194
  return entry.ready;
381065
381195
  }
381196
+ async isStaleFlowReplay(instanceId, turn) {
381197
+ const state = this.ascFlowInstances.get(instanceId);
381198
+ if (!state)
381199
+ return false;
381200
+ if (state.lastAnswered.get(turn.codAtendimento) !== turn.text.trim())
381201
+ return false;
381202
+ return isStaleFlowReplay({
381203
+ client: state.client,
381204
+ instanceId,
381205
+ codAtendimento: turn.codAtendimento,
381206
+ text: turn.text,
381207
+ logger: this.logger
381208
+ });
381209
+ }
381210
+ async waitForTurn(instanceId, codAtendimento, text, timeoutMs) {
381211
+ const deadline = Date.now() + timeoutMs;
381212
+ while (Date.now() < deadline) {
381213
+ await new Promise((resolve2) => setTimeout(resolve2, 250));
381214
+ if (!this.ascFlowInstances.has(instanceId))
381215
+ return null;
381216
+ const ready = this.takeReadyTurn(instanceId, codAtendimento, text);
381217
+ if (ready)
381218
+ return ready;
381219
+ }
381220
+ this.logger.warn("[asc-flow] held the request to its deadline with no agent answer", {
381221
+ instanceId,
381222
+ codAtendimento,
381223
+ timeoutMs
381224
+ });
381225
+ return null;
381226
+ }
381066
381227
  isRedeliveryOfTurnInFlight(instanceId, turn) {
381067
381228
  const state = this.ascFlowInstances.get(instanceId);
381068
381229
  const entry = state?.inFlight.get(turn.codAtendimento);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/omni",
3
- "version": "2.260904.2",
3
+ "version": "2.260906.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/automagik-dev/omni.git",