@automagik/omni 2.260429.5 → 2.260430.2
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 +353 -11
- 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.2",
|
|
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.2",
|
|
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(),
|
|
@@ -240539,7 +240566,7 @@ async function verifySignature3(opts) {
|
|
|
240539
240566
|
if (!ok) {
|
|
240540
240567
|
return { status: "invalid", reason: "signature does not verify under registered pubkey" };
|
|
240541
240568
|
}
|
|
240542
|
-
return { status: "verified", hostId: host.id };
|
|
240569
|
+
return { status: "verified", hostId: host.id, hostScopes: host.scopes };
|
|
240543
240570
|
}
|
|
240544
240571
|
function pathFromRequest(url) {
|
|
240545
240572
|
return `${url.pathname}${url.search}`;
|
|
@@ -240576,7 +240603,7 @@ var init_genie_signature = __esm(() => {
|
|
|
240576
240603
|
now: Date.now(),
|
|
240577
240604
|
findHost: async (id) => {
|
|
240578
240605
|
const host = await services.genieHosts.findById(id);
|
|
240579
|
-
return host ? { id: host.id, pubkey: host.pubkey, revokedAt: host.revokedAt } : null;
|
|
240606
|
+
return host ? { id: host.id, pubkey: host.pubkey, revokedAt: host.revokedAt, scopes: host.scopes ?? ["*"] } : null;
|
|
240580
240607
|
}
|
|
240581
240608
|
});
|
|
240582
240609
|
if (outcome.status === "invalid") {
|
|
@@ -240595,6 +240622,9 @@ var init_genie_signature = __esm(() => {
|
|
|
240595
240622
|
}
|
|
240596
240623
|
if (outcome.status === "verified" && outcome.hostId) {
|
|
240597
240624
|
c.set("signedBy", outcome.hostId);
|
|
240625
|
+
if (outcome.hostScopes) {
|
|
240626
|
+
c.set("signedByScopes", outcome.hostScopes);
|
|
240627
|
+
}
|
|
240598
240628
|
services.genieHosts.touchLastSeen(outcome.hostId).catch((err) => {
|
|
240599
240629
|
log58.warn("touchLastSeen failed (non-fatal)", { hostId: outcome.hostId, err: String(err) });
|
|
240600
240630
|
});
|
|
@@ -241314,9 +241344,12 @@ var init_scope_enforcer = __esm(() => {
|
|
|
241314
241344
|
}
|
|
241315
241345
|
const method = c.req.method.toUpperCase();
|
|
241316
241346
|
const path3 = c.req.path;
|
|
241347
|
+
const signedBy = c.get("signedBy");
|
|
241348
|
+
const signedByScopes = c.get("signedByScopes");
|
|
241317
241349
|
const wildcard = ApiKeyService.scopeAllows(apiKey.scopes, "*");
|
|
241350
|
+
let requiredScope;
|
|
241318
241351
|
if (!wildcard) {
|
|
241319
|
-
|
|
241352
|
+
requiredScope = findRequiredScope(method, path3);
|
|
241320
241353
|
if (!requiredScope) {
|
|
241321
241354
|
log60.warn(`DENIED: key=${apiKey.id} route=${method} ${path3} required=UNMAPPED`);
|
|
241322
241355
|
return c.json({
|
|
@@ -241336,6 +241369,31 @@ var init_scope_enforcer = __esm(() => {
|
|
|
241336
241369
|
}, 403);
|
|
241337
241370
|
}
|
|
241338
241371
|
}
|
|
241372
|
+
if (signedBy && signedByScopes) {
|
|
241373
|
+
const hostWildcard = ApiKeyService.scopeAllows(signedByScopes, "*");
|
|
241374
|
+
if (!hostWildcard) {
|
|
241375
|
+
const needed = requiredScope ?? findRequiredScope(method, path3);
|
|
241376
|
+
if (!needed) {
|
|
241377
|
+
log60.warn(`DENIED: signedBy=${signedBy} route=${method} ${path3} required=UNMAPPED`);
|
|
241378
|
+
return c.json({
|
|
241379
|
+
error: {
|
|
241380
|
+
code: "FORBIDDEN",
|
|
241381
|
+
message: "Insufficient permissions. Route not mapped in scope policy."
|
|
241382
|
+
}
|
|
241383
|
+
}, 403);
|
|
241384
|
+
}
|
|
241385
|
+
if (!ApiKeyService.scopeAllows(signedByScopes, needed)) {
|
|
241386
|
+
log60.warn(`DENIED: signedBy=${signedBy} route=${method} ${path3} required=${needed} host-scopes=${signedByScopes.join(",")}`);
|
|
241387
|
+
return c.json({
|
|
241388
|
+
error: {
|
|
241389
|
+
code: "FORBIDDEN",
|
|
241390
|
+
message: `Insufficient permissions for signing host. Required scope: ${needed}`,
|
|
241391
|
+
host: signedBy
|
|
241392
|
+
}
|
|
241393
|
+
}, 403);
|
|
241394
|
+
}
|
|
241395
|
+
}
|
|
241396
|
+
}
|
|
241339
241397
|
const body = await safeReadJsonBody(c);
|
|
241340
241398
|
const targets = extractLockTargets(method, path3, body);
|
|
241341
241399
|
const instanceResult = enforceInstanceAllowlist(apiKey, targets.instance);
|
|
@@ -284644,6 +284702,55 @@ async function dispatchToAgent(services, instance4, msgs, triggerType, channel4,
|
|
|
284644
284702
|
});
|
|
284645
284703
|
await dispatchViaLegacy(services, instance4, msgs, triggerType, channel4, chatId, senderId, personId, senderName, traceId, perThreadExtraContext, senderAgentId);
|
|
284646
284704
|
}
|
|
284705
|
+
async function applyCloseContactGate(services, chatRecordId, instanceId, chatId, chatSettings) {
|
|
284706
|
+
if (chatSettings?.closed === true) {
|
|
284707
|
+
log94.debug("Chat closed (terminal), skipping dispatch", {
|
|
284708
|
+
instanceId,
|
|
284709
|
+
chatId,
|
|
284710
|
+
outcome: chatSettings.closeOutcome ?? null
|
|
284711
|
+
});
|
|
284712
|
+
return "skip";
|
|
284713
|
+
}
|
|
284714
|
+
if (!chatSettings?.closeUntil)
|
|
284715
|
+
return "pass";
|
|
284716
|
+
const closeUntilMs = new Date(chatSettings.closeUntil).getTime();
|
|
284717
|
+
if (!Number.isFinite(closeUntilMs))
|
|
284718
|
+
return "pass";
|
|
284719
|
+
if (Date.now() < closeUntilMs) {
|
|
284720
|
+
log94.debug("Chat in close cooldown, skipping dispatch", {
|
|
284721
|
+
instanceId,
|
|
284722
|
+
chatId,
|
|
284723
|
+
closeUntil: chatSettings.closeUntil,
|
|
284724
|
+
outcome: chatSettings.closeOutcome ?? null
|
|
284725
|
+
});
|
|
284726
|
+
return "skip";
|
|
284727
|
+
}
|
|
284728
|
+
if (!chatRecordId)
|
|
284729
|
+
return "pass";
|
|
284730
|
+
try {
|
|
284731
|
+
await services.chats.update(chatRecordId, {
|
|
284732
|
+
settings: {
|
|
284733
|
+
...chatSettings,
|
|
284734
|
+
agentPaused: false,
|
|
284735
|
+
closeUntil: null,
|
|
284736
|
+
agentResumedAt: new Date().toISOString()
|
|
284737
|
+
}
|
|
284738
|
+
});
|
|
284739
|
+
log94.info("Close cooldown expired, agent reopened", {
|
|
284740
|
+
instanceId,
|
|
284741
|
+
chatId,
|
|
284742
|
+
closeOutcome: chatSettings.closeOutcome ?? null
|
|
284743
|
+
});
|
|
284744
|
+
return "reopened";
|
|
284745
|
+
} catch (err) {
|
|
284746
|
+
log94.warn("Failed to flip close cooldown state, deferring dispatch", {
|
|
284747
|
+
instanceId,
|
|
284748
|
+
chatId,
|
|
284749
|
+
error: String(err)
|
|
284750
|
+
});
|
|
284751
|
+
return "skip";
|
|
284752
|
+
}
|
|
284753
|
+
}
|
|
284647
284754
|
async function processAgentResponse(services, instance4, messages4, triggerType, db2, eventBus) {
|
|
284648
284755
|
const firstMessage = messages4[0];
|
|
284649
284756
|
if (!firstMessage)
|
|
@@ -284664,7 +284771,12 @@ async function processAgentResponse(services, instance4, messages4, triggerType,
|
|
|
284664
284771
|
}
|
|
284665
284772
|
const chatRecord = await services.chats.findByExternalIdSmart(instance4.id, chatId);
|
|
284666
284773
|
const chatSettings = chatRecord?.settings;
|
|
284667
|
-
const
|
|
284774
|
+
const gateResult = await applyCloseContactGate(services, chatRecord?.id ?? null, instance4.id, chatId, chatSettings);
|
|
284775
|
+
if (gateResult === "skip") {
|
|
284776
|
+
ackHandle.remove();
|
|
284777
|
+
return;
|
|
284778
|
+
}
|
|
284779
|
+
const isAgentPaused = chatSettings?.agentPaused === true && gateResult !== "reopened";
|
|
284668
284780
|
if (isAgentPaused) {
|
|
284669
284781
|
log94.debug("Agent paused (handoff active), skipping dispatch", { instanceId: instance4.id, chatId });
|
|
284670
284782
|
ackHandle.remove();
|
|
@@ -287129,7 +287241,8 @@ function createServices(db2, eventBus) {
|
|
|
287129
287241
|
consumerOffsets: new ConsumerOffsetService(db2),
|
|
287130
287242
|
followUpLifecycle: new FollowUpLifecycleService(db2, eventBus),
|
|
287131
287243
|
followUpSweeper: new FollowUpSweeperService(db2, eventBus),
|
|
287132
|
-
genieHosts: new GenieHostsService(db2)
|
|
287244
|
+
genieHosts: new GenieHostsService(db2),
|
|
287245
|
+
eventBus
|
|
287133
287246
|
};
|
|
287134
287247
|
}
|
|
287135
287248
|
var init_services = __esm(() => {
|
|
@@ -295152,6 +295265,34 @@ var init_chats2 = __esm(() => {
|
|
|
295152
295265
|
} catch {}
|
|
295153
295266
|
return c.json({ success: true, sessionId, sessionStrategy });
|
|
295154
295267
|
});
|
|
295268
|
+
chatsRoutes.post("/:id/reopen-contact", async (c) => {
|
|
295269
|
+
const id = c.req.param("id");
|
|
295270
|
+
const services = c.get("services");
|
|
295271
|
+
const chat2 = await services.chats.getById(id);
|
|
295272
|
+
if (!chat2)
|
|
295273
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
295274
|
+
if (chat2.instanceId)
|
|
295275
|
+
checkInstanceAccess(c.get("apiKey"), chat2.instanceId);
|
|
295276
|
+
const priorSettings = chat2.settings ?? {};
|
|
295277
|
+
const wasClosed = priorSettings.closed === true || priorSettings.closeUntil != null;
|
|
295278
|
+
await services.chats.update(id, {
|
|
295279
|
+
settings: {
|
|
295280
|
+
...priorSettings,
|
|
295281
|
+
agentPaused: false,
|
|
295282
|
+
closed: false,
|
|
295283
|
+
closeUntil: null,
|
|
295284
|
+
closeOutcome: null,
|
|
295285
|
+
agentResumedAt: new Date().toISOString()
|
|
295286
|
+
}
|
|
295287
|
+
});
|
|
295288
|
+
return c.json({
|
|
295289
|
+
data: {
|
|
295290
|
+
chatId: id,
|
|
295291
|
+
reopened: true,
|
|
295292
|
+
wasClosed
|
|
295293
|
+
}
|
|
295294
|
+
});
|
|
295295
|
+
});
|
|
295155
295296
|
});
|
|
295156
295297
|
|
|
295157
295298
|
// ../api/src/routes/v2/context.ts
|
|
@@ -300017,6 +300158,38 @@ var init_media = __esm(() => {
|
|
|
300017
300158
|
});
|
|
300018
300159
|
});
|
|
300019
300160
|
|
|
300161
|
+
// ../api/src/routes/v2/_close-contact-config.ts
|
|
300162
|
+
function resolveCloseContactConfig(outcome, instanceSettings) {
|
|
300163
|
+
const override = instanceSettings?.closeContactConfig;
|
|
300164
|
+
const def = DEFAULT_CLOSE_CONTACT_CONFIG[outcome];
|
|
300165
|
+
if (!override || typeof override !== "object")
|
|
300166
|
+
return def;
|
|
300167
|
+
const ovr = override[outcome];
|
|
300168
|
+
if (!ovr || typeof ovr !== "object")
|
|
300169
|
+
return def;
|
|
300170
|
+
return {
|
|
300171
|
+
cooldownMs: typeof ovr.cooldownMs === "number" || ovr.cooldownMs === null ? ovr.cooldownMs : def.cooldownMs,
|
|
300172
|
+
escalationThreshold: typeof ovr.escalationThreshold === "number" || ovr.escalationThreshold === null ? ovr.escalationThreshold : def.escalationThreshold,
|
|
300173
|
+
escalationWindowMs: typeof ovr.escalationWindowMs === "number" || ovr.escalationWindowMs === null ? ovr.escalationWindowMs : def.escalationWindowMs
|
|
300174
|
+
};
|
|
300175
|
+
}
|
|
300176
|
+
function isHardTerminalOutcome(outcome) {
|
|
300177
|
+
return outcome === "won" || outcome === "lost";
|
|
300178
|
+
}
|
|
300179
|
+
var HOUR, DAY, DEFAULT_CLOSE_CONTACT_CONFIG;
|
|
300180
|
+
var init__close_contact_config = __esm(() => {
|
|
300181
|
+
HOUR = 60 * 60 * 1000;
|
|
300182
|
+
DAY = 24 * HOUR;
|
|
300183
|
+
DEFAULT_CLOSE_CONTACT_CONFIG = {
|
|
300184
|
+
won: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
|
|
300185
|
+
lost: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
|
|
300186
|
+
redirected_sac: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 7 * DAY },
|
|
300187
|
+
unqualified: { cooldownMs: 7 * DAY, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
|
|
300188
|
+
no_response: { cooldownMs: 48 * HOUR, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
|
|
300189
|
+
other: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 14 * DAY }
|
|
300190
|
+
};
|
|
300191
|
+
});
|
|
300192
|
+
|
|
300020
300193
|
// ../api/src/routes/v2/messages.ts
|
|
300021
300194
|
import { existsSync as existsSync8 } from "fs";
|
|
300022
300195
|
import { join as join24 } from "path";
|
|
@@ -300143,6 +300316,27 @@ async function resolveMessageFromRef(services, ref) {
|
|
|
300143
300316
|
}
|
|
300144
300317
|
return found;
|
|
300145
300318
|
}
|
|
300319
|
+
async function computeCloseContactTerminalState(db2, chatUuid, outcome, auditRowId) {
|
|
300320
|
+
const cfg = resolveCloseContactConfig(outcome, null);
|
|
300321
|
+
if (isHardTerminalOutcome(outcome)) {
|
|
300322
|
+
return { terminal: true, escalated: false, closeUntil: null };
|
|
300323
|
+
}
|
|
300324
|
+
if (cfg.escalationThreshold === null || cfg.escalationWindowMs === null) {
|
|
300325
|
+
const closeUntil2 = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
|
|
300326
|
+
return { terminal: false, escalated: false, closeUntil: closeUntil2 };
|
|
300327
|
+
}
|
|
300328
|
+
const windowStart = new Date(Date.now() - cfg.escalationWindowMs);
|
|
300329
|
+
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)));
|
|
300330
|
+
const recentCount = Number(recent[0]?.count ?? 0);
|
|
300331
|
+
if (recentCount >= cfg.escalationThreshold) {
|
|
300332
|
+
if (auditRowId) {
|
|
300333
|
+
await db2.update(closeContactLogs).set({ escalated: true }).where(eq(closeContactLogs.id, auditRowId));
|
|
300334
|
+
}
|
|
300335
|
+
return { terminal: true, escalated: true, closeUntil: null };
|
|
300336
|
+
}
|
|
300337
|
+
const closeUntil = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
|
|
300338
|
+
return { terminal: false, escalated: false, closeUntil };
|
|
300339
|
+
}
|
|
300146
300340
|
async function verifyMessageInstanceOwnership(services, message2, instanceId) {
|
|
300147
300341
|
if (!message2.chatId)
|
|
300148
300342
|
return;
|
|
@@ -300156,19 +300350,21 @@ async function verifyMessageInstanceOwnership(services, message2, instanceId) {
|
|
|
300156
300350
|
});
|
|
300157
300351
|
}
|
|
300158
300352
|
}
|
|
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;
|
|
300353
|
+
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
300354
|
var init_messages5 = __esm(() => {
|
|
300161
300355
|
init_dist6();
|
|
300162
300356
|
init_src2();
|
|
300163
300357
|
init_src();
|
|
300164
300358
|
init_src5();
|
|
300165
300359
|
init_esm5();
|
|
300360
|
+
init_drizzle_orm();
|
|
300166
300361
|
init_dist2();
|
|
300167
300362
|
init_zod();
|
|
300168
300363
|
init_sentry_scrub();
|
|
300169
300364
|
init_date_query();
|
|
300170
300365
|
init_api_keys();
|
|
300171
300366
|
init_media_storage();
|
|
300367
|
+
init__close_contact_config();
|
|
300172
300368
|
log105 = createLogger("routes:messages");
|
|
300173
300369
|
mediaDownloadLog = createLogger("routes:messages:media-download");
|
|
300174
300370
|
messagesRoutes = new Hono2;
|
|
@@ -300330,6 +300526,15 @@ var init_messages5 = __esm(() => {
|
|
|
300330
300526
|
extraInfo: exports_external.string().optional().describe("Free-text briefing (legacy \u2014 prefer dadosLead)"),
|
|
300331
300527
|
handoffFields: exports_external.record(exports_external.unknown()).optional().describe("Structured fields for Gupshup flow variables (e.g. nome, cidade, temperatura_lead)")
|
|
300332
300528
|
});
|
|
300529
|
+
sendCloseContactSchema = exports_external.object({
|
|
300530
|
+
instanceId: exports_external.string().uuid().describe("Instance ID \u2014 close-contact native send is Gupshup-only in v1"),
|
|
300531
|
+
chatId: exports_external.string().min(1).describe("Chat DB UUID to mark as closed"),
|
|
300532
|
+
to: exports_external.string().min(1).describe("Recipient phone or platform ID"),
|
|
300533
|
+
text: exports_external.string().min(1).describe("Farewell message shown to the lead"),
|
|
300534
|
+
outcome: exports_external.enum(["won", "lost", "redirected_sac", "unqualified", "no_response", "other"]).describe("Drives terminal/cooldown/escalation logic and BI/audit trail"),
|
|
300535
|
+
reason: exports_external.string().optional().describe("Free-text rationale persisted in close_contact_logs"),
|
|
300536
|
+
closeFields: exports_external.record(exports_external.unknown()).optional().describe("Structured BI/CRM payload \u2014 forwarded to Gupshup native send when supported")
|
|
300537
|
+
});
|
|
300333
300538
|
messagesRoutes.get("/", zValidator("query", listQuerySchema13), async (c) => {
|
|
300334
300539
|
const query = c.req.valid("query");
|
|
300335
300540
|
const services = c.get("services");
|
|
@@ -301048,6 +301253,109 @@ var init_messages5 = __esm(() => {
|
|
|
301048
301253
|
}
|
|
301049
301254
|
}, 201);
|
|
301050
301255
|
});
|
|
301256
|
+
messagesRoutes.post("/send/close-contact", zValidator("json", sendCloseContactSchema), async (c) => {
|
|
301257
|
+
const data = c.req.valid("json");
|
|
301258
|
+
const services = c.get("services");
|
|
301259
|
+
const db2 = c.get("db");
|
|
301260
|
+
const channelRegistry2 = c.get("channelRegistry");
|
|
301261
|
+
checkInstanceAccess2(c.get("apiKey"), data.instanceId);
|
|
301262
|
+
const instance4 = await services.instances.getById(data.instanceId);
|
|
301263
|
+
if (!channelRegistry2) {
|
|
301264
|
+
throw new OmniError({
|
|
301265
|
+
code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
|
|
301266
|
+
message: "Channel registry not available",
|
|
301267
|
+
recoverable: false
|
|
301268
|
+
});
|
|
301269
|
+
}
|
|
301270
|
+
const plugin7 = channelRegistry2.get(instance4.channel);
|
|
301271
|
+
if (!plugin7) {
|
|
301272
|
+
throw new OmniError({
|
|
301273
|
+
code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
|
|
301274
|
+
message: `No plugin found for channel: ${instance4.channel}`,
|
|
301275
|
+
context: { channelType: instance4.channel },
|
|
301276
|
+
recoverable: false
|
|
301277
|
+
});
|
|
301278
|
+
}
|
|
301279
|
+
const resolvedTo = await resolveRecipient(data.to, instance4.channel, services);
|
|
301280
|
+
const hasNativeClose = plugin7.capabilities?.canCloseContact === true;
|
|
301281
|
+
const outcome = data.outcome;
|
|
301282
|
+
let channelSendResult = null;
|
|
301283
|
+
if (hasNativeClose) {
|
|
301284
|
+
const outgoingMessage = {
|
|
301285
|
+
to: resolvedTo,
|
|
301286
|
+
content: { type: "text", text: data.text },
|
|
301287
|
+
metadata: {
|
|
301288
|
+
isCloseContact: true,
|
|
301289
|
+
closeReason: data.reason,
|
|
301290
|
+
closeOutcome: outcome,
|
|
301291
|
+
closeFields: data.closeFields
|
|
301292
|
+
}
|
|
301293
|
+
};
|
|
301294
|
+
channelSendResult = await plugin7.sendMessage(data.instanceId, outgoingMessage);
|
|
301295
|
+
handleSendResult(channelSendResult, {
|
|
301296
|
+
channelType: instance4.channel,
|
|
301297
|
+
instanceId: data.instanceId,
|
|
301298
|
+
operation: "send close-contact"
|
|
301299
|
+
});
|
|
301300
|
+
}
|
|
301301
|
+
const [auditRow] = await db2.insert(closeContactLogs).values({
|
|
301302
|
+
instanceId: data.instanceId,
|
|
301303
|
+
chatUuid: data.chatId,
|
|
301304
|
+
chatId: resolvedTo,
|
|
301305
|
+
toPhone: resolvedTo,
|
|
301306
|
+
text: data.text,
|
|
301307
|
+
outcome,
|
|
301308
|
+
reason: data.reason ?? null,
|
|
301309
|
+
closeFields: data.closeFields ?? null,
|
|
301310
|
+
agentId: instance4.agentId ?? null,
|
|
301311
|
+
externalMessageId: channelSendResult?.messageId ?? null,
|
|
301312
|
+
escalated: false,
|
|
301313
|
+
sentAt: new Date,
|
|
301314
|
+
metadata: {
|
|
301315
|
+
instanceChannel: instance4.channel,
|
|
301316
|
+
channelCloseSupported: hasNativeClose
|
|
301317
|
+
}
|
|
301318
|
+
}).returning();
|
|
301319
|
+
const { terminal, escalated, closeUntil } = await computeCloseContactTerminalState(db2, data.chatId, outcome, auditRow?.id ?? null);
|
|
301320
|
+
const closedAt = new Date;
|
|
301321
|
+
await services.chats.update(data.chatId, {
|
|
301322
|
+
settings: {
|
|
301323
|
+
agentPaused: true,
|
|
301324
|
+
closed: terminal,
|
|
301325
|
+
closeUntil: closeUntil?.toISOString() ?? null,
|
|
301326
|
+
closeOutcome: outcome
|
|
301327
|
+
}
|
|
301328
|
+
});
|
|
301329
|
+
await services.followUpLifecycle.disarm({
|
|
301330
|
+
chatId: data.chatId,
|
|
301331
|
+
instanceId: data.instanceId,
|
|
301332
|
+
reason: "contact_closed"
|
|
301333
|
+
});
|
|
301334
|
+
if (services.eventBus) {
|
|
301335
|
+
const payload = {
|
|
301336
|
+
chatId: data.chatId,
|
|
301337
|
+
instanceId: data.instanceId,
|
|
301338
|
+
agentId: instance4.agentId ?? null,
|
|
301339
|
+
outcome,
|
|
301340
|
+
reason: data.reason ?? null,
|
|
301341
|
+
escalated: terminal,
|
|
301342
|
+
closedFields: data.closeFields ?? null,
|
|
301343
|
+
closedAt: closedAt.toISOString()
|
|
301344
|
+
};
|
|
301345
|
+
services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err) => log105.debug("Failed to publish chat.closed", { error: String(err) }));
|
|
301346
|
+
}
|
|
301347
|
+
return c.json({
|
|
301348
|
+
data: {
|
|
301349
|
+
messageId: channelSendResult?.messageId ?? null,
|
|
301350
|
+
status: "closed",
|
|
301351
|
+
terminal,
|
|
301352
|
+
closeUntil: closeUntil?.toISOString() ?? null,
|
|
301353
|
+
escalated,
|
|
301354
|
+
outcome,
|
|
301355
|
+
timestamp: channelSendResult?.timestamp ?? closedAt.getTime()
|
|
301356
|
+
}
|
|
301357
|
+
}, 201);
|
|
301358
|
+
});
|
|
301051
301359
|
messagesRoutes.get("/tts/voices", async (c) => {
|
|
301052
301360
|
const services = c.get("services");
|
|
301053
301361
|
const voices = await services.tts.listVoices();
|
|
@@ -304570,7 +304878,22 @@ async function setupFollowUpHooks(eventBus, services) {
|
|
|
304570
304878
|
retryDelayMs: 500,
|
|
304571
304879
|
startFrom: "new"
|
|
304572
304880
|
});
|
|
304573
|
-
|
|
304881
|
+
await eventBus.subscribe("chat.closed", async (event) => {
|
|
304882
|
+
const payload = event.payload;
|
|
304883
|
+
await services.followUpLifecycle.disarm({
|
|
304884
|
+
chatId: payload.chatId,
|
|
304885
|
+
instanceId: payload.instanceId,
|
|
304886
|
+
agentId: payload.agentId ?? null,
|
|
304887
|
+
reason: "contact_closed"
|
|
304888
|
+
});
|
|
304889
|
+
}, {
|
|
304890
|
+
durable: "follow-up-hooks-closed",
|
|
304891
|
+
queue: "follow-up-hooks",
|
|
304892
|
+
maxRetries: 2,
|
|
304893
|
+
retryDelayMs: 500,
|
|
304894
|
+
startFrom: "new"
|
|
304895
|
+
});
|
|
304896
|
+
log109.info("Follow-up hooks active (message.sent/received, chat.handoff_activated, chat.archived, chat.closed)");
|
|
304574
304897
|
} catch (error2) {
|
|
304575
304898
|
log109.error("Failed to set up follow-up hooks", { error: String(error2) });
|
|
304576
304899
|
}
|
|
@@ -309561,6 +309884,7 @@ var GUPSHUP_CAPABILITIES = {
|
|
|
309561
309884
|
canReplyToMessage: true,
|
|
309562
309885
|
canForwardMessage: false,
|
|
309563
309886
|
canHandoff: true,
|
|
309887
|
+
canCloseContact: true,
|
|
309564
309888
|
canSendContact: false,
|
|
309565
309889
|
canSendLocation: true,
|
|
309566
309890
|
canSendSticker: true,
|
|
@@ -310028,6 +310352,18 @@ function extractContent2(msg) {
|
|
|
310028
310352
|
return null;
|
|
310029
310353
|
}
|
|
310030
310354
|
|
|
310355
|
+
// ../channel-gupshup/src/senders/close-contact.ts
|
|
310356
|
+
var GUPSHUP_CLOSE_MSG_TYPE = "CLOSING";
|
|
310357
|
+
async function sendCloseContact(client, to, text, closeReason, closeOutcome, closeFields) {
|
|
310358
|
+
return client.send(to, {
|
|
310359
|
+
type: GUPSHUP_CLOSE_MSG_TYPE,
|
|
310360
|
+
text,
|
|
310361
|
+
close_reason: closeReason,
|
|
310362
|
+
close_outcome: closeOutcome,
|
|
310363
|
+
close_fields: closeFields
|
|
310364
|
+
});
|
|
310365
|
+
}
|
|
310366
|
+
|
|
310031
310367
|
// ../channel-gupshup/src/senders/handoff.ts
|
|
310032
310368
|
async function sendHandoff(client, to, text, dadosLead, motivoHandoff, handoffFields) {
|
|
310033
310369
|
return client.send(to, {
|
|
@@ -310100,6 +310436,12 @@ async function dispatchContent(client, dest, message2) {
|
|
|
310100
310436
|
const handoffFields = meta.handoffFields;
|
|
310101
310437
|
return sendHandoff(client, dest, content.text ?? "", dadosLead, motivoHandoff, handoffFields);
|
|
310102
310438
|
}
|
|
310439
|
+
if (meta?.isCloseContact === true) {
|
|
310440
|
+
const closeReason = meta.closeReason;
|
|
310441
|
+
const closeOutcome = meta.closeOutcome;
|
|
310442
|
+
const closeFields = meta.closeFields;
|
|
310443
|
+
return sendCloseContact(client, dest, content.text ?? "", closeReason, closeOutcome, closeFields);
|
|
310444
|
+
}
|
|
310103
310445
|
if (content.type === "text") {
|
|
310104
310446
|
return sendText(client, dest, content.text ?? "");
|
|
310105
310447
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automagik/omni",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.260430.2",
|
|
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.
|
|
54
|
-
"@omni/channel-discord": "2.
|
|
55
|
-
"@omni/channel-gupshup": "2.
|
|
56
|
-
"@omni/channel-sdk": "2.
|
|
57
|
-
"@omni/channel-slack": "2.
|
|
58
|
-
"@omni/channel-telegram": "2.
|
|
59
|
-
"@omni/channel-whatsapp": "2.
|
|
60
|
-
"@omni/core": "2.
|
|
61
|
-
"@omni/sdk": "2.
|
|
53
|
+
"@omni/api": "2.260430.1",
|
|
54
|
+
"@omni/channel-discord": "2.260430.1",
|
|
55
|
+
"@omni/channel-gupshup": "2.260430.1",
|
|
56
|
+
"@omni/channel-sdk": "2.260430.1",
|
|
57
|
+
"@omni/channel-slack": "2.260430.1",
|
|
58
|
+
"@omni/channel-telegram": "2.260430.1",
|
|
59
|
+
"@omni/channel-whatsapp": "2.260430.1",
|
|
60
|
+
"@omni/core": "2.260430.1",
|
|
61
|
+
"@omni/sdk": "2.260430.1",
|
|
62
62
|
"@types/node": "^22.10.3",
|
|
63
63
|
"@types/qrcode-terminal": "^0.12.2",
|
|
64
64
|
"typescript": "^5.7.3"
|