@automagik/omni 2.260429.5 → 2.260430.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/db/drizzle/0033_close_contact_logs.sql +47 -0
- package/db/drizzle/meta/_journal.json +7 -0
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/index.js +100 -4
- package/dist/server/index.js +319 -8
- package/package.json +10 -10
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Close-contact endpoint audit table.
|
|
2
|
+
--
|
|
3
|
+
-- Records every agent→close-contact event with full payload. Written
|
|
4
|
+
-- synchronously in the /messages/send/close-contact route so no data is
|
|
5
|
+
-- lost. The route also reads this table at close-time to count recent
|
|
6
|
+
-- rows for the same (chat_uuid, outcome) within the configured escalation
|
|
7
|
+
-- window — that count drives the auto-promotion of soft outcomes to hard
|
|
8
|
+
-- terminal (recorded back as `escalated = TRUE` on the new row). See the
|
|
9
|
+
-- design doc at .genie/wishes/559-close-contact/design.md §6 + §8.1 for
|
|
10
|
+
-- the loop-bound proof.
|
|
11
|
+
--
|
|
12
|
+
-- Hot path is the recent-count query, indexed on
|
|
13
|
+
-- (chat_uuid, outcome, sent_at). Per-instance BI feed is indexed on
|
|
14
|
+
-- (instance_id, sent_at). chat_id and agent_id get their own indexes for
|
|
15
|
+
-- support drilldowns.
|
|
16
|
+
--
|
|
17
|
+
-- The `outcome` column is a varchar(32) free string at the DB level; the
|
|
18
|
+
-- TypeScript layer constrains it to the `closeContactOutcomes` const array
|
|
19
|
+
-- in `packages/db/src/schema.ts` (kept in sync with `CloseContactOutcome`
|
|
20
|
+
-- in `@omni/core/events`). No CHECK constraint here so the BI/CRM tooling
|
|
21
|
+
-- can extend the taxonomy without a migration.
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS "close_contact_logs" (
|
|
24
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
25
|
+
"instance_id" uuid REFERENCES "instances"("id") ON DELETE SET NULL,
|
|
26
|
+
"chat_uuid" uuid REFERENCES "chats"("id") ON DELETE SET NULL,
|
|
27
|
+
"chat_id" varchar(255) NOT NULL,
|
|
28
|
+
"to_phone" varchar(100) NOT NULL,
|
|
29
|
+
"text" text NOT NULL,
|
|
30
|
+
"outcome" varchar(32) NOT NULL,
|
|
31
|
+
"reason" text,
|
|
32
|
+
"close_fields" jsonb,
|
|
33
|
+
"agent_id" uuid REFERENCES "agents"("id") ON DELETE SET NULL,
|
|
34
|
+
"external_message_id" varchar(255),
|
|
35
|
+
"escalated" boolean NOT NULL DEFAULT false,
|
|
36
|
+
"sent_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
37
|
+
"metadata" jsonb
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_outcome_sent_at_idx"
|
|
41
|
+
ON "close_contact_logs" ("chat_uuid", "outcome", "sent_at");
|
|
42
|
+
CREATE INDEX IF NOT EXISTS "close_contact_logs_instance_sent_at_idx"
|
|
43
|
+
ON "close_contact_logs" ("instance_id", "sent_at");
|
|
44
|
+
CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_id_idx"
|
|
45
|
+
ON "close_contact_logs" ("chat_id");
|
|
46
|
+
CREATE INDEX IF NOT EXISTS "close_contact_logs_agent_idx"
|
|
47
|
+
ON "close_contact_logs" ("agent_id");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,CA4T/C"}
|
package/dist/index.js
CHANGED
|
@@ -5755,6 +5755,7 @@ var init_types = __esm(() => {
|
|
|
5755
5755
|
"chat.idle_timeout",
|
|
5756
5756
|
"chat.handoff_activated",
|
|
5757
5757
|
"chat.archived",
|
|
5758
|
+
"chat.closed",
|
|
5758
5759
|
"follow_up.armed",
|
|
5759
5760
|
"follow_up.fired",
|
|
5760
5761
|
"follow_up.skipped",
|
|
@@ -27730,7 +27731,8 @@ var init_follow_up = __esm(() => {
|
|
|
27730
27731
|
"sequence_complete",
|
|
27731
27732
|
"agent_error",
|
|
27732
27733
|
"send_failed",
|
|
27733
|
-
"session_cleared"
|
|
27734
|
+
"session_cleared",
|
|
27735
|
+
"contact_closed"
|
|
27734
27736
|
]);
|
|
27735
27737
|
FixedScheduleSchema = exports_external.object({
|
|
27736
27738
|
kind: exports_external.literal("fixed"),
|
|
@@ -58589,6 +58591,8 @@ __export(exports_schema, {
|
|
|
58589
58591
|
contentTypes: () => contentTypes,
|
|
58590
58592
|
consumerOffsets: () => consumerOffsets,
|
|
58591
58593
|
conditionOperators: () => conditionOperators,
|
|
58594
|
+
closeContactOutcomes: () => closeContactOutcomes,
|
|
58595
|
+
closeContactLogs: () => closeContactLogs,
|
|
58592
58596
|
chatsRelations: () => chatsRelations,
|
|
58593
58597
|
chats: () => chats,
|
|
58594
58598
|
chatTypes: () => chatTypes,
|
|
@@ -58632,7 +58636,7 @@ __export(exports_schema, {
|
|
|
58632
58636
|
accessRules: () => accessRules,
|
|
58633
58637
|
accessModes: () => accessModes
|
|
58634
58638
|
});
|
|
58635
|
-
var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
|
|
58639
|
+
var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, closeContactOutcomes, closeContactLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
|
|
58636
58640
|
var init_schema2 = __esm(() => {
|
|
58637
58641
|
init_events();
|
|
58638
58642
|
init_drizzle_orm();
|
|
@@ -59258,6 +59262,28 @@ var init_schema2 = __esm(() => {
|
|
|
59258
59262
|
sentAtIdx: index("handoff_logs_sent_at_idx").on(table3.sentAt),
|
|
59259
59263
|
agentIdx: index("handoff_logs_agent_idx").on(table3.agentId)
|
|
59260
59264
|
}));
|
|
59265
|
+
closeContactOutcomes = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
|
|
59266
|
+
closeContactLogs = pgTable("close_contact_logs", {
|
|
59267
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
59268
|
+
instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "set null" }),
|
|
59269
|
+
chatUuid: uuid("chat_uuid").references(() => chats.id, { onDelete: "set null" }),
|
|
59270
|
+
chatId: varchar("chat_id", { length: 255 }).notNull(),
|
|
59271
|
+
toPhone: varchar("to_phone", { length: 100 }).notNull(),
|
|
59272
|
+
text: text("text").notNull(),
|
|
59273
|
+
outcome: varchar("outcome", { length: 32 }).notNull().$type(),
|
|
59274
|
+
reason: text("reason"),
|
|
59275
|
+
closeFields: jsonb("close_fields").$type(),
|
|
59276
|
+
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
59277
|
+
externalMessageId: varchar("external_message_id", { length: 255 }),
|
|
59278
|
+
escalated: boolean("escalated").notNull().default(false),
|
|
59279
|
+
sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(),
|
|
59280
|
+
metadata: jsonb("metadata").$type()
|
|
59281
|
+
}, (table3) => ({
|
|
59282
|
+
chatOutcomeSentAtIdx: index("close_contact_logs_chat_outcome_sent_at_idx").on(table3.chatUuid, table3.outcome, table3.sentAt),
|
|
59283
|
+
instanceSentAtIdx: index("close_contact_logs_instance_sent_at_idx").on(table3.instanceId, table3.sentAt),
|
|
59284
|
+
chatIdIdx: index("close_contact_logs_chat_id_idx").on(table3.chatId),
|
|
59285
|
+
agentIdx: index("close_contact_logs_agent_idx").on(table3.agentId)
|
|
59286
|
+
}));
|
|
59261
59287
|
accessRules = pgTable("access_rules", {
|
|
59262
59288
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
59263
59289
|
instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "cascade" }),
|
|
@@ -59892,7 +59918,8 @@ var init_schema2 = __esm(() => {
|
|
|
59892
59918
|
"sequence_complete",
|
|
59893
59919
|
"agent_error",
|
|
59894
59920
|
"send_failed",
|
|
59895
|
-
"session_cleared"
|
|
59921
|
+
"session_cleared",
|
|
59922
|
+
"contact_closed"
|
|
59896
59923
|
];
|
|
59897
59924
|
chatFollowUpState = pgTable("chat_follow_up_state", {
|
|
59898
59925
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -113953,7 +113980,7 @@ import { fileURLToPath } from "url";
|
|
|
113953
113980
|
// package.json
|
|
113954
113981
|
var package_default = {
|
|
113955
113982
|
name: "@automagik/omni",
|
|
113956
|
-
version: "2.
|
|
113983
|
+
version: "2.260430.1",
|
|
113957
113984
|
description: "LLM-optimized CLI for Omni",
|
|
113958
113985
|
type: "module",
|
|
113959
113986
|
bin: {
|
|
@@ -122464,8 +122491,77 @@ function createMessagesCommand() {
|
|
|
122464
122491
|
error(`Failed to edit message: ${message2}`);
|
|
122465
122492
|
}
|
|
122466
122493
|
});
|
|
122494
|
+
messages3.command("close-contact").description("Close a chat terminally (won/lost) or with a soft cooldown (redirected_sac/unqualified/no_response/other)").requiredOption("--instance <id>", "Instance ID").requiredOption("--chat <chatId>", "Chat DB UUID to close").requiredOption("--to <recipient>", "Recipient phone or platform ID").requiredOption("--text <text>", "Farewell message shown to the lead").requiredOption("--outcome <outcome>", "Outcome: won | lost | redirected_sac | unqualified | no_response | other").option("--reason <reason>", "Free-text rationale persisted in close_contact_logs").option("--close-fields <jsonOrPath>", "Structured BI/CRM payload \u2014 inline JSON or path to a JSON file").action(handleCloseContact);
|
|
122467
122495
|
return messages3;
|
|
122468
122496
|
}
|
|
122497
|
+
var VALID_CLOSE_OUTCOMES = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
|
|
122498
|
+
async function parseCloseFields(raw2) {
|
|
122499
|
+
const trimmed = raw2.trim();
|
|
122500
|
+
if (trimmed.startsWith("{")) {
|
|
122501
|
+
return JSON.parse(trimmed);
|
|
122502
|
+
}
|
|
122503
|
+
const fs2 = await import("fs/promises");
|
|
122504
|
+
const text3 = await fs2.readFile(trimmed, "utf-8");
|
|
122505
|
+
return JSON.parse(text3);
|
|
122506
|
+
}
|
|
122507
|
+
async function postCloseContact(body) {
|
|
122508
|
+
const _cfg = (await Promise.resolve().then(() => (init_config(), exports_config))).loadConfig();
|
|
122509
|
+
const baseUrl = _cfg.apiUrl ?? "http://localhost:8882";
|
|
122510
|
+
const apiKey = _cfg.apiKey ?? "";
|
|
122511
|
+
const resp = await fetch(`${baseUrl}/api/v2/messages/send/close-contact`, {
|
|
122512
|
+
method: "POST",
|
|
122513
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
122514
|
+
body: JSON.stringify(body)
|
|
122515
|
+
});
|
|
122516
|
+
if (!resp.ok) {
|
|
122517
|
+
const err2 = await resp.json().catch(() => ({}));
|
|
122518
|
+
const errMsg = typeof err2.error === "string" ? err2.error : err2.error?.message ?? `API error: ${resp.status}`;
|
|
122519
|
+
throw new Error(errMsg);
|
|
122520
|
+
}
|
|
122521
|
+
const data2 = await resp.json();
|
|
122522
|
+
return data2.data ?? {};
|
|
122523
|
+
}
|
|
122524
|
+
async function buildCloseContactBody(options) {
|
|
122525
|
+
let closeFields;
|
|
122526
|
+
if (options.closeFields) {
|
|
122527
|
+
try {
|
|
122528
|
+
closeFields = await parseCloseFields(options.closeFields);
|
|
122529
|
+
} catch (err2) {
|
|
122530
|
+
error(`Failed to parse --close-fields: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
122531
|
+
return null;
|
|
122532
|
+
}
|
|
122533
|
+
}
|
|
122534
|
+
const instanceId = await resolveInstanceId(options.instance);
|
|
122535
|
+
const resolvedChatId = await resolveChatId(options.chat);
|
|
122536
|
+
const body = {
|
|
122537
|
+
instanceId,
|
|
122538
|
+
chatId: resolvedChatId,
|
|
122539
|
+
to: options.to,
|
|
122540
|
+
text: options.text,
|
|
122541
|
+
outcome: options.outcome
|
|
122542
|
+
};
|
|
122543
|
+
if (options.reason)
|
|
122544
|
+
body.reason = options.reason;
|
|
122545
|
+
if (closeFields)
|
|
122546
|
+
body.closeFields = closeFields;
|
|
122547
|
+
return body;
|
|
122548
|
+
}
|
|
122549
|
+
async function handleCloseContact(options) {
|
|
122550
|
+
if (!VALID_CLOSE_OUTCOMES.includes(options.outcome)) {
|
|
122551
|
+
error(`Invalid --outcome '${options.outcome}'. Must be one of: ${VALID_CLOSE_OUTCOMES.join(", ")}`);
|
|
122552
|
+
return;
|
|
122553
|
+
}
|
|
122554
|
+
try {
|
|
122555
|
+
const body = await buildCloseContactBody(options);
|
|
122556
|
+
if (!body)
|
|
122557
|
+
return;
|
|
122558
|
+
const result = await postCloseContact(body);
|
|
122559
|
+
const cooldownPart = result.closeUntil ? `, closeUntil=${result.closeUntil}` : "";
|
|
122560
|
+
success(`Chat closed (${options.outcome}): terminal=${result.terminal ?? false}, escalated=${result.escalated ?? false}${cooldownPart}`);
|
|
122561
|
+
} catch (err2) {
|
|
122562
|
+
error(`Failed to close contact: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
122563
|
+
}
|
|
122564
|
+
}
|
|
122469
122565
|
|
|
122470
122566
|
// src/commands/open.ts
|
|
122471
122567
|
init_output();
|
package/dist/server/index.js
CHANGED
|
@@ -181,6 +181,7 @@ var init_types2 = __esm(() => {
|
|
|
181
181
|
"chat.idle_timeout",
|
|
182
182
|
"chat.handoff_activated",
|
|
183
183
|
"chat.archived",
|
|
184
|
+
"chat.closed",
|
|
184
185
|
"follow_up.armed",
|
|
185
186
|
"follow_up.fired",
|
|
186
187
|
"follow_up.skipped",
|
|
@@ -22074,7 +22075,8 @@ var init_follow_up = __esm(() => {
|
|
|
22074
22075
|
"sequence_complete",
|
|
22075
22076
|
"agent_error",
|
|
22076
22077
|
"send_failed",
|
|
22077
|
-
"session_cleared"
|
|
22078
|
+
"session_cleared",
|
|
22079
|
+
"contact_closed"
|
|
22078
22080
|
]);
|
|
22079
22081
|
FixedScheduleSchema = exports_external.object({
|
|
22080
22082
|
kind: exports_external.literal("fixed"),
|
|
@@ -224554,7 +224556,7 @@ var init_sentry_scrub = __esm(() => {
|
|
|
224554
224556
|
var require_package8 = __commonJS((exports, module) => {
|
|
224555
224557
|
module.exports = {
|
|
224556
224558
|
name: "@omni/api",
|
|
224557
|
-
version: "2.
|
|
224559
|
+
version: "2.260430.1",
|
|
224558
224560
|
type: "module",
|
|
224559
224561
|
exports: {
|
|
224560
224562
|
".": {
|
|
@@ -229981,6 +229983,8 @@ __export(exports_schema, {
|
|
|
229981
229983
|
contentTypes: () => contentTypes,
|
|
229982
229984
|
consumerOffsets: () => consumerOffsets,
|
|
229983
229985
|
conditionOperators: () => conditionOperators,
|
|
229986
|
+
closeContactOutcomes: () => closeContactOutcomes,
|
|
229987
|
+
closeContactLogs: () => closeContactLogs,
|
|
229984
229988
|
chatsRelations: () => chatsRelations,
|
|
229985
229989
|
chats: () => chats,
|
|
229986
229990
|
chatTypes: () => chatTypes,
|
|
@@ -230024,7 +230028,7 @@ __export(exports_schema, {
|
|
|
230024
230028
|
accessRules: () => accessRules,
|
|
230025
230029
|
accessModes: () => accessModes
|
|
230026
230030
|
});
|
|
230027
|
-
var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages2, omniEvents, handoffLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
|
|
230031
|
+
var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages2, omniEvents, handoffLogs, closeContactOutcomes, closeContactLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
|
|
230028
230032
|
var init_schema2 = __esm(() => {
|
|
230029
230033
|
init_events();
|
|
230030
230034
|
init_drizzle_orm();
|
|
@@ -230650,6 +230654,28 @@ var init_schema2 = __esm(() => {
|
|
|
230650
230654
|
sentAtIdx: index("handoff_logs_sent_at_idx").on(table3.sentAt),
|
|
230651
230655
|
agentIdx: index("handoff_logs_agent_idx").on(table3.agentId)
|
|
230652
230656
|
}));
|
|
230657
|
+
closeContactOutcomes = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
|
|
230658
|
+
closeContactLogs = pgTable("close_contact_logs", {
|
|
230659
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
230660
|
+
instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "set null" }),
|
|
230661
|
+
chatUuid: uuid("chat_uuid").references(() => chats.id, { onDelete: "set null" }),
|
|
230662
|
+
chatId: varchar("chat_id", { length: 255 }).notNull(),
|
|
230663
|
+
toPhone: varchar("to_phone", { length: 100 }).notNull(),
|
|
230664
|
+
text: text("text").notNull(),
|
|
230665
|
+
outcome: varchar("outcome", { length: 32 }).notNull().$type(),
|
|
230666
|
+
reason: text("reason"),
|
|
230667
|
+
closeFields: jsonb("close_fields").$type(),
|
|
230668
|
+
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
230669
|
+
externalMessageId: varchar("external_message_id", { length: 255 }),
|
|
230670
|
+
escalated: boolean("escalated").notNull().default(false),
|
|
230671
|
+
sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(),
|
|
230672
|
+
metadata: jsonb("metadata").$type()
|
|
230673
|
+
}, (table3) => ({
|
|
230674
|
+
chatOutcomeSentAtIdx: index("close_contact_logs_chat_outcome_sent_at_idx").on(table3.chatUuid, table3.outcome, table3.sentAt),
|
|
230675
|
+
instanceSentAtIdx: index("close_contact_logs_instance_sent_at_idx").on(table3.instanceId, table3.sentAt),
|
|
230676
|
+
chatIdIdx: index("close_contact_logs_chat_id_idx").on(table3.chatId),
|
|
230677
|
+
agentIdx: index("close_contact_logs_agent_idx").on(table3.agentId)
|
|
230678
|
+
}));
|
|
230653
230679
|
accessRules = pgTable("access_rules", {
|
|
230654
230680
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
230655
230681
|
instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "cascade" }),
|
|
@@ -231284,7 +231310,8 @@ var init_schema2 = __esm(() => {
|
|
|
231284
231310
|
"sequence_complete",
|
|
231285
231311
|
"agent_error",
|
|
231286
231312
|
"send_failed",
|
|
231287
|
-
"session_cleared"
|
|
231313
|
+
"session_cleared",
|
|
231314
|
+
"contact_closed"
|
|
231288
231315
|
];
|
|
231289
231316
|
chatFollowUpState = pgTable("chat_follow_up_state", {
|
|
231290
231317
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -284644,6 +284671,55 @@ async function dispatchToAgent(services, instance4, msgs, triggerType, channel4,
|
|
|
284644
284671
|
});
|
|
284645
284672
|
await dispatchViaLegacy(services, instance4, msgs, triggerType, channel4, chatId, senderId, personId, senderName, traceId, perThreadExtraContext, senderAgentId);
|
|
284646
284673
|
}
|
|
284674
|
+
async function applyCloseContactGate(services, chatRecordId, instanceId, chatId, chatSettings) {
|
|
284675
|
+
if (chatSettings?.closed === true) {
|
|
284676
|
+
log94.debug("Chat closed (terminal), skipping dispatch", {
|
|
284677
|
+
instanceId,
|
|
284678
|
+
chatId,
|
|
284679
|
+
outcome: chatSettings.closeOutcome ?? null
|
|
284680
|
+
});
|
|
284681
|
+
return "skip";
|
|
284682
|
+
}
|
|
284683
|
+
if (!chatSettings?.closeUntil)
|
|
284684
|
+
return "pass";
|
|
284685
|
+
const closeUntilMs = new Date(chatSettings.closeUntil).getTime();
|
|
284686
|
+
if (!Number.isFinite(closeUntilMs))
|
|
284687
|
+
return "pass";
|
|
284688
|
+
if (Date.now() < closeUntilMs) {
|
|
284689
|
+
log94.debug("Chat in close cooldown, skipping dispatch", {
|
|
284690
|
+
instanceId,
|
|
284691
|
+
chatId,
|
|
284692
|
+
closeUntil: chatSettings.closeUntil,
|
|
284693
|
+
outcome: chatSettings.closeOutcome ?? null
|
|
284694
|
+
});
|
|
284695
|
+
return "skip";
|
|
284696
|
+
}
|
|
284697
|
+
if (!chatRecordId)
|
|
284698
|
+
return "pass";
|
|
284699
|
+
try {
|
|
284700
|
+
await services.chats.update(chatRecordId, {
|
|
284701
|
+
settings: {
|
|
284702
|
+
...chatSettings,
|
|
284703
|
+
agentPaused: false,
|
|
284704
|
+
closeUntil: null,
|
|
284705
|
+
agentResumedAt: new Date().toISOString()
|
|
284706
|
+
}
|
|
284707
|
+
});
|
|
284708
|
+
log94.info("Close cooldown expired, agent reopened", {
|
|
284709
|
+
instanceId,
|
|
284710
|
+
chatId,
|
|
284711
|
+
closeOutcome: chatSettings.closeOutcome ?? null
|
|
284712
|
+
});
|
|
284713
|
+
return "reopened";
|
|
284714
|
+
} catch (err) {
|
|
284715
|
+
log94.warn("Failed to flip close cooldown state, deferring dispatch", {
|
|
284716
|
+
instanceId,
|
|
284717
|
+
chatId,
|
|
284718
|
+
error: String(err)
|
|
284719
|
+
});
|
|
284720
|
+
return "skip";
|
|
284721
|
+
}
|
|
284722
|
+
}
|
|
284647
284723
|
async function processAgentResponse(services, instance4, messages4, triggerType, db2, eventBus) {
|
|
284648
284724
|
const firstMessage = messages4[0];
|
|
284649
284725
|
if (!firstMessage)
|
|
@@ -284664,7 +284740,12 @@ async function processAgentResponse(services, instance4, messages4, triggerType,
|
|
|
284664
284740
|
}
|
|
284665
284741
|
const chatRecord = await services.chats.findByExternalIdSmart(instance4.id, chatId);
|
|
284666
284742
|
const chatSettings = chatRecord?.settings;
|
|
284667
|
-
const
|
|
284743
|
+
const gateResult = await applyCloseContactGate(services, chatRecord?.id ?? null, instance4.id, chatId, chatSettings);
|
|
284744
|
+
if (gateResult === "skip") {
|
|
284745
|
+
ackHandle.remove();
|
|
284746
|
+
return;
|
|
284747
|
+
}
|
|
284748
|
+
const isAgentPaused = chatSettings?.agentPaused === true && gateResult !== "reopened";
|
|
284668
284749
|
if (isAgentPaused) {
|
|
284669
284750
|
log94.debug("Agent paused (handoff active), skipping dispatch", { instanceId: instance4.id, chatId });
|
|
284670
284751
|
ackHandle.remove();
|
|
@@ -287129,7 +287210,8 @@ function createServices(db2, eventBus) {
|
|
|
287129
287210
|
consumerOffsets: new ConsumerOffsetService(db2),
|
|
287130
287211
|
followUpLifecycle: new FollowUpLifecycleService(db2, eventBus),
|
|
287131
287212
|
followUpSweeper: new FollowUpSweeperService(db2, eventBus),
|
|
287132
|
-
genieHosts: new GenieHostsService(db2)
|
|
287213
|
+
genieHosts: new GenieHostsService(db2),
|
|
287214
|
+
eventBus
|
|
287133
287215
|
};
|
|
287134
287216
|
}
|
|
287135
287217
|
var init_services = __esm(() => {
|
|
@@ -295152,6 +295234,34 @@ var init_chats2 = __esm(() => {
|
|
|
295152
295234
|
} catch {}
|
|
295153
295235
|
return c.json({ success: true, sessionId, sessionStrategy });
|
|
295154
295236
|
});
|
|
295237
|
+
chatsRoutes.post("/:id/reopen-contact", async (c) => {
|
|
295238
|
+
const id = c.req.param("id");
|
|
295239
|
+
const services = c.get("services");
|
|
295240
|
+
const chat2 = await services.chats.getById(id);
|
|
295241
|
+
if (!chat2)
|
|
295242
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
295243
|
+
if (chat2.instanceId)
|
|
295244
|
+
checkInstanceAccess(c.get("apiKey"), chat2.instanceId);
|
|
295245
|
+
const priorSettings = chat2.settings ?? {};
|
|
295246
|
+
const wasClosed = priorSettings.closed === true || priorSettings.closeUntil != null;
|
|
295247
|
+
await services.chats.update(id, {
|
|
295248
|
+
settings: {
|
|
295249
|
+
...priorSettings,
|
|
295250
|
+
agentPaused: false,
|
|
295251
|
+
closed: false,
|
|
295252
|
+
closeUntil: null,
|
|
295253
|
+
closeOutcome: null,
|
|
295254
|
+
agentResumedAt: new Date().toISOString()
|
|
295255
|
+
}
|
|
295256
|
+
});
|
|
295257
|
+
return c.json({
|
|
295258
|
+
data: {
|
|
295259
|
+
chatId: id,
|
|
295260
|
+
reopened: true,
|
|
295261
|
+
wasClosed
|
|
295262
|
+
}
|
|
295263
|
+
});
|
|
295264
|
+
});
|
|
295155
295265
|
});
|
|
295156
295266
|
|
|
295157
295267
|
// ../api/src/routes/v2/context.ts
|
|
@@ -300017,6 +300127,38 @@ var init_media = __esm(() => {
|
|
|
300017
300127
|
});
|
|
300018
300128
|
});
|
|
300019
300129
|
|
|
300130
|
+
// ../api/src/routes/v2/_close-contact-config.ts
|
|
300131
|
+
function resolveCloseContactConfig(outcome, instanceSettings) {
|
|
300132
|
+
const override = instanceSettings?.closeContactConfig;
|
|
300133
|
+
const def = DEFAULT_CLOSE_CONTACT_CONFIG[outcome];
|
|
300134
|
+
if (!override || typeof override !== "object")
|
|
300135
|
+
return def;
|
|
300136
|
+
const ovr = override[outcome];
|
|
300137
|
+
if (!ovr || typeof ovr !== "object")
|
|
300138
|
+
return def;
|
|
300139
|
+
return {
|
|
300140
|
+
cooldownMs: typeof ovr.cooldownMs === "number" || ovr.cooldownMs === null ? ovr.cooldownMs : def.cooldownMs,
|
|
300141
|
+
escalationThreshold: typeof ovr.escalationThreshold === "number" || ovr.escalationThreshold === null ? ovr.escalationThreshold : def.escalationThreshold,
|
|
300142
|
+
escalationWindowMs: typeof ovr.escalationWindowMs === "number" || ovr.escalationWindowMs === null ? ovr.escalationWindowMs : def.escalationWindowMs
|
|
300143
|
+
};
|
|
300144
|
+
}
|
|
300145
|
+
function isHardTerminalOutcome(outcome) {
|
|
300146
|
+
return outcome === "won" || outcome === "lost";
|
|
300147
|
+
}
|
|
300148
|
+
var HOUR, DAY, DEFAULT_CLOSE_CONTACT_CONFIG;
|
|
300149
|
+
var init__close_contact_config = __esm(() => {
|
|
300150
|
+
HOUR = 60 * 60 * 1000;
|
|
300151
|
+
DAY = 24 * HOUR;
|
|
300152
|
+
DEFAULT_CLOSE_CONTACT_CONFIG = {
|
|
300153
|
+
won: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
|
|
300154
|
+
lost: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
|
|
300155
|
+
redirected_sac: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 7 * DAY },
|
|
300156
|
+
unqualified: { cooldownMs: 7 * DAY, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
|
|
300157
|
+
no_response: { cooldownMs: 48 * HOUR, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
|
|
300158
|
+
other: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 14 * DAY }
|
|
300159
|
+
};
|
|
300160
|
+
});
|
|
300161
|
+
|
|
300020
300162
|
// ../api/src/routes/v2/messages.ts
|
|
300021
300163
|
import { existsSync as existsSync8 } from "fs";
|
|
300022
300164
|
import { join as join24 } from "path";
|
|
@@ -300143,6 +300285,27 @@ async function resolveMessageFromRef(services, ref) {
|
|
|
300143
300285
|
}
|
|
300144
300286
|
return found;
|
|
300145
300287
|
}
|
|
300288
|
+
async function computeCloseContactTerminalState(db2, chatUuid, outcome, auditRowId) {
|
|
300289
|
+
const cfg = resolveCloseContactConfig(outcome, null);
|
|
300290
|
+
if (isHardTerminalOutcome(outcome)) {
|
|
300291
|
+
return { terminal: true, escalated: false, closeUntil: null };
|
|
300292
|
+
}
|
|
300293
|
+
if (cfg.escalationThreshold === null || cfg.escalationWindowMs === null) {
|
|
300294
|
+
const closeUntil2 = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
|
|
300295
|
+
return { terminal: false, escalated: false, closeUntil: closeUntil2 };
|
|
300296
|
+
}
|
|
300297
|
+
const windowStart = new Date(Date.now() - cfg.escalationWindowMs);
|
|
300298
|
+
const recent = await db2.select({ count: sql`count(*)::int` }).from(closeContactLogs).where(and2(eq(closeContactLogs.chatUuid, chatUuid), eq(closeContactLogs.outcome, outcome), gte(closeContactLogs.sentAt, windowStart)));
|
|
300299
|
+
const recentCount = Number(recent[0]?.count ?? 0);
|
|
300300
|
+
if (recentCount >= cfg.escalationThreshold) {
|
|
300301
|
+
if (auditRowId) {
|
|
300302
|
+
await db2.update(closeContactLogs).set({ escalated: true }).where(eq(closeContactLogs.id, auditRowId));
|
|
300303
|
+
}
|
|
300304
|
+
return { terminal: true, escalated: true, closeUntil: null };
|
|
300305
|
+
}
|
|
300306
|
+
const closeUntil = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
|
|
300307
|
+
return { terminal: false, escalated: false, closeUntil };
|
|
300308
|
+
}
|
|
300146
300309
|
async function verifyMessageInstanceOwnership(services, message2, instanceId) {
|
|
300147
300310
|
if (!message2.chatId)
|
|
300148
300311
|
return;
|
|
@@ -300156,19 +300319,21 @@ async function verifyMessageInstanceOwnership(services, message2, instanceId) {
|
|
|
300156
300319
|
});
|
|
300157
300320
|
}
|
|
300158
300321
|
}
|
|
300159
|
-
var log105, mediaDownloadLog, messagesRoutes, UUID_REGEX, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema13, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, sendHandoffSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
|
|
300322
|
+
var log105, mediaDownloadLog, messagesRoutes, UUID_REGEX, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema13, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, sendHandoffSchema, sendCloseContactSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
|
|
300160
300323
|
var init_messages5 = __esm(() => {
|
|
300161
300324
|
init_dist6();
|
|
300162
300325
|
init_src2();
|
|
300163
300326
|
init_src();
|
|
300164
300327
|
init_src5();
|
|
300165
300328
|
init_esm5();
|
|
300329
|
+
init_drizzle_orm();
|
|
300166
300330
|
init_dist2();
|
|
300167
300331
|
init_zod();
|
|
300168
300332
|
init_sentry_scrub();
|
|
300169
300333
|
init_date_query();
|
|
300170
300334
|
init_api_keys();
|
|
300171
300335
|
init_media_storage();
|
|
300336
|
+
init__close_contact_config();
|
|
300172
300337
|
log105 = createLogger("routes:messages");
|
|
300173
300338
|
mediaDownloadLog = createLogger("routes:messages:media-download");
|
|
300174
300339
|
messagesRoutes = new Hono2;
|
|
@@ -300330,6 +300495,15 @@ var init_messages5 = __esm(() => {
|
|
|
300330
300495
|
extraInfo: exports_external.string().optional().describe("Free-text briefing (legacy \u2014 prefer dadosLead)"),
|
|
300331
300496
|
handoffFields: exports_external.record(exports_external.unknown()).optional().describe("Structured fields for Gupshup flow variables (e.g. nome, cidade, temperatura_lead)")
|
|
300332
300497
|
});
|
|
300498
|
+
sendCloseContactSchema = exports_external.object({
|
|
300499
|
+
instanceId: exports_external.string().uuid().describe("Instance ID \u2014 close-contact native send is Gupshup-only in v1"),
|
|
300500
|
+
chatId: exports_external.string().min(1).describe("Chat DB UUID to mark as closed"),
|
|
300501
|
+
to: exports_external.string().min(1).describe("Recipient phone or platform ID"),
|
|
300502
|
+
text: exports_external.string().min(1).describe("Farewell message shown to the lead"),
|
|
300503
|
+
outcome: exports_external.enum(["won", "lost", "redirected_sac", "unqualified", "no_response", "other"]).describe("Drives terminal/cooldown/escalation logic and BI/audit trail"),
|
|
300504
|
+
reason: exports_external.string().optional().describe("Free-text rationale persisted in close_contact_logs"),
|
|
300505
|
+
closeFields: exports_external.record(exports_external.unknown()).optional().describe("Structured BI/CRM payload \u2014 forwarded to Gupshup native send when supported")
|
|
300506
|
+
});
|
|
300333
300507
|
messagesRoutes.get("/", zValidator("query", listQuerySchema13), async (c) => {
|
|
300334
300508
|
const query = c.req.valid("query");
|
|
300335
300509
|
const services = c.get("services");
|
|
@@ -301048,6 +301222,109 @@ var init_messages5 = __esm(() => {
|
|
|
301048
301222
|
}
|
|
301049
301223
|
}, 201);
|
|
301050
301224
|
});
|
|
301225
|
+
messagesRoutes.post("/send/close-contact", zValidator("json", sendCloseContactSchema), async (c) => {
|
|
301226
|
+
const data = c.req.valid("json");
|
|
301227
|
+
const services = c.get("services");
|
|
301228
|
+
const db2 = c.get("db");
|
|
301229
|
+
const channelRegistry2 = c.get("channelRegistry");
|
|
301230
|
+
checkInstanceAccess2(c.get("apiKey"), data.instanceId);
|
|
301231
|
+
const instance4 = await services.instances.getById(data.instanceId);
|
|
301232
|
+
if (!channelRegistry2) {
|
|
301233
|
+
throw new OmniError({
|
|
301234
|
+
code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
|
|
301235
|
+
message: "Channel registry not available",
|
|
301236
|
+
recoverable: false
|
|
301237
|
+
});
|
|
301238
|
+
}
|
|
301239
|
+
const plugin7 = channelRegistry2.get(instance4.channel);
|
|
301240
|
+
if (!plugin7) {
|
|
301241
|
+
throw new OmniError({
|
|
301242
|
+
code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
|
|
301243
|
+
message: `No plugin found for channel: ${instance4.channel}`,
|
|
301244
|
+
context: { channelType: instance4.channel },
|
|
301245
|
+
recoverable: false
|
|
301246
|
+
});
|
|
301247
|
+
}
|
|
301248
|
+
const resolvedTo = await resolveRecipient(data.to, instance4.channel, services);
|
|
301249
|
+
const hasNativeClose = plugin7.capabilities?.canCloseContact === true;
|
|
301250
|
+
const outcome = data.outcome;
|
|
301251
|
+
let channelSendResult = null;
|
|
301252
|
+
if (hasNativeClose) {
|
|
301253
|
+
const outgoingMessage = {
|
|
301254
|
+
to: resolvedTo,
|
|
301255
|
+
content: { type: "text", text: data.text },
|
|
301256
|
+
metadata: {
|
|
301257
|
+
isCloseContact: true,
|
|
301258
|
+
closeReason: data.reason,
|
|
301259
|
+
closeOutcome: outcome,
|
|
301260
|
+
closeFields: data.closeFields
|
|
301261
|
+
}
|
|
301262
|
+
};
|
|
301263
|
+
channelSendResult = await plugin7.sendMessage(data.instanceId, outgoingMessage);
|
|
301264
|
+
handleSendResult(channelSendResult, {
|
|
301265
|
+
channelType: instance4.channel,
|
|
301266
|
+
instanceId: data.instanceId,
|
|
301267
|
+
operation: "send close-contact"
|
|
301268
|
+
});
|
|
301269
|
+
}
|
|
301270
|
+
const [auditRow] = await db2.insert(closeContactLogs).values({
|
|
301271
|
+
instanceId: data.instanceId,
|
|
301272
|
+
chatUuid: data.chatId,
|
|
301273
|
+
chatId: resolvedTo,
|
|
301274
|
+
toPhone: resolvedTo,
|
|
301275
|
+
text: data.text,
|
|
301276
|
+
outcome,
|
|
301277
|
+
reason: data.reason ?? null,
|
|
301278
|
+
closeFields: data.closeFields ?? null,
|
|
301279
|
+
agentId: instance4.agentId ?? null,
|
|
301280
|
+
externalMessageId: channelSendResult?.messageId ?? null,
|
|
301281
|
+
escalated: false,
|
|
301282
|
+
sentAt: new Date,
|
|
301283
|
+
metadata: {
|
|
301284
|
+
instanceChannel: instance4.channel,
|
|
301285
|
+
channelCloseSupported: hasNativeClose
|
|
301286
|
+
}
|
|
301287
|
+
}).returning();
|
|
301288
|
+
const { terminal, escalated, closeUntil } = await computeCloseContactTerminalState(db2, data.chatId, outcome, auditRow?.id ?? null);
|
|
301289
|
+
const closedAt = new Date;
|
|
301290
|
+
await services.chats.update(data.chatId, {
|
|
301291
|
+
settings: {
|
|
301292
|
+
agentPaused: true,
|
|
301293
|
+
closed: terminal,
|
|
301294
|
+
closeUntil: closeUntil?.toISOString() ?? null,
|
|
301295
|
+
closeOutcome: outcome
|
|
301296
|
+
}
|
|
301297
|
+
});
|
|
301298
|
+
await services.followUpLifecycle.disarm({
|
|
301299
|
+
chatId: data.chatId,
|
|
301300
|
+
instanceId: data.instanceId,
|
|
301301
|
+
reason: "contact_closed"
|
|
301302
|
+
});
|
|
301303
|
+
if (services.eventBus) {
|
|
301304
|
+
const payload = {
|
|
301305
|
+
chatId: data.chatId,
|
|
301306
|
+
instanceId: data.instanceId,
|
|
301307
|
+
agentId: instance4.agentId ?? null,
|
|
301308
|
+
outcome,
|
|
301309
|
+
reason: data.reason ?? null,
|
|
301310
|
+
escalated: terminal,
|
|
301311
|
+
closedFields: data.closeFields ?? null,
|
|
301312
|
+
closedAt: closedAt.toISOString()
|
|
301313
|
+
};
|
|
301314
|
+
services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err) => log105.debug("Failed to publish chat.closed", { error: String(err) }));
|
|
301315
|
+
}
|
|
301316
|
+
return c.json({
|
|
301317
|
+
data: {
|
|
301318
|
+
messageId: channelSendResult?.messageId ?? null,
|
|
301319
|
+
status: "closed",
|
|
301320
|
+
terminal,
|
|
301321
|
+
closeUntil: closeUntil?.toISOString() ?? null,
|
|
301322
|
+
escalated,
|
|
301323
|
+
outcome,
|
|
301324
|
+
timestamp: channelSendResult?.timestamp ?? closedAt.getTime()
|
|
301325
|
+
}
|
|
301326
|
+
}, 201);
|
|
301327
|
+
});
|
|
301051
301328
|
messagesRoutes.get("/tts/voices", async (c) => {
|
|
301052
301329
|
const services = c.get("services");
|
|
301053
301330
|
const voices = await services.tts.listVoices();
|
|
@@ -304570,7 +304847,22 @@ async function setupFollowUpHooks(eventBus, services) {
|
|
|
304570
304847
|
retryDelayMs: 500,
|
|
304571
304848
|
startFrom: "new"
|
|
304572
304849
|
});
|
|
304573
|
-
|
|
304850
|
+
await eventBus.subscribe("chat.closed", async (event) => {
|
|
304851
|
+
const payload = event.payload;
|
|
304852
|
+
await services.followUpLifecycle.disarm({
|
|
304853
|
+
chatId: payload.chatId,
|
|
304854
|
+
instanceId: payload.instanceId,
|
|
304855
|
+
agentId: payload.agentId ?? null,
|
|
304856
|
+
reason: "contact_closed"
|
|
304857
|
+
});
|
|
304858
|
+
}, {
|
|
304859
|
+
durable: "follow-up-hooks-closed",
|
|
304860
|
+
queue: "follow-up-hooks",
|
|
304861
|
+
maxRetries: 2,
|
|
304862
|
+
retryDelayMs: 500,
|
|
304863
|
+
startFrom: "new"
|
|
304864
|
+
});
|
|
304865
|
+
log109.info("Follow-up hooks active (message.sent/received, chat.handoff_activated, chat.archived, chat.closed)");
|
|
304574
304866
|
} catch (error2) {
|
|
304575
304867
|
log109.error("Failed to set up follow-up hooks", { error: String(error2) });
|
|
304576
304868
|
}
|
|
@@ -309561,6 +309853,7 @@ var GUPSHUP_CAPABILITIES = {
|
|
|
309561
309853
|
canReplyToMessage: true,
|
|
309562
309854
|
canForwardMessage: false,
|
|
309563
309855
|
canHandoff: true,
|
|
309856
|
+
canCloseContact: true,
|
|
309564
309857
|
canSendContact: false,
|
|
309565
309858
|
canSendLocation: true,
|
|
309566
309859
|
canSendSticker: true,
|
|
@@ -310028,6 +310321,18 @@ function extractContent2(msg) {
|
|
|
310028
310321
|
return null;
|
|
310029
310322
|
}
|
|
310030
310323
|
|
|
310324
|
+
// ../channel-gupshup/src/senders/close-contact.ts
|
|
310325
|
+
var GUPSHUP_CLOSE_MSG_TYPE = "CLOSING";
|
|
310326
|
+
async function sendCloseContact(client, to, text, closeReason, closeOutcome, closeFields) {
|
|
310327
|
+
return client.send(to, {
|
|
310328
|
+
type: GUPSHUP_CLOSE_MSG_TYPE,
|
|
310329
|
+
text,
|
|
310330
|
+
close_reason: closeReason,
|
|
310331
|
+
close_outcome: closeOutcome,
|
|
310332
|
+
close_fields: closeFields
|
|
310333
|
+
});
|
|
310334
|
+
}
|
|
310335
|
+
|
|
310031
310336
|
// ../channel-gupshup/src/senders/handoff.ts
|
|
310032
310337
|
async function sendHandoff(client, to, text, dadosLead, motivoHandoff, handoffFields) {
|
|
310033
310338
|
return client.send(to, {
|
|
@@ -310100,6 +310405,12 @@ async function dispatchContent(client, dest, message2) {
|
|
|
310100
310405
|
const handoffFields = meta.handoffFields;
|
|
310101
310406
|
return sendHandoff(client, dest, content.text ?? "", dadosLead, motivoHandoff, handoffFields);
|
|
310102
310407
|
}
|
|
310408
|
+
if (meta?.isCloseContact === true) {
|
|
310409
|
+
const closeReason = meta.closeReason;
|
|
310410
|
+
const closeOutcome = meta.closeOutcome;
|
|
310411
|
+
const closeFields = meta.closeFields;
|
|
310412
|
+
return sendCloseContact(client, dest, content.text ?? "", closeReason, closeOutcome, closeFields);
|
|
310413
|
+
}
|
|
310103
310414
|
if (content.type === "text") {
|
|
310104
310415
|
return sendText(client, dest, content.text ?? "");
|
|
310105
310416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automagik/omni",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.260430.1",
|
|
4
4
|
"description": "LLM-optimized CLI for Omni",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,15 +50,15 @@
|
|
|
50
50
|
"qrcode-terminal": "^0.12.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@omni/api": "2.260429.
|
|
54
|
-
"@omni/channel-discord": "2.260429.
|
|
55
|
-
"@omni/channel-gupshup": "2.260429.
|
|
56
|
-
"@omni/channel-sdk": "2.260429.
|
|
57
|
-
"@omni/channel-slack": "2.260429.
|
|
58
|
-
"@omni/channel-telegram": "2.260429.
|
|
59
|
-
"@omni/channel-whatsapp": "2.260429.
|
|
60
|
-
"@omni/core": "2.260429.
|
|
61
|
-
"@omni/sdk": "2.260429.
|
|
53
|
+
"@omni/api": "2.260429.5",
|
|
54
|
+
"@omni/channel-discord": "2.260429.5",
|
|
55
|
+
"@omni/channel-gupshup": "2.260429.5",
|
|
56
|
+
"@omni/channel-sdk": "2.260429.5",
|
|
57
|
+
"@omni/channel-slack": "2.260429.5",
|
|
58
|
+
"@omni/channel-telegram": "2.260429.5",
|
|
59
|
+
"@omni/channel-whatsapp": "2.260429.5",
|
|
60
|
+
"@omni/core": "2.260429.5",
|
|
61
|
+
"@omni/sdk": "2.260429.5",
|
|
62
62
|
"@types/node": "^22.10.3",
|
|
63
63
|
"@types/qrcode-terminal": "^0.12.2",
|
|
64
64
|
"typescript": "^5.7.3"
|