chatwoot-router 0.4.0 → 0.5.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Route on a customer's message as it arrives. Chatwoot's message webhook describes a contact without a `type`
|
|
15
|
+
(`Contact#webhook_data`; users and bots have one), and the router required `type: "contact"`, so it ignored
|
|
16
|
+
every customer message and a new ticket waited for the five-minute sweep.
|
|
17
|
+
|
|
18
|
+
## [0.5.0] - 2026-10-05
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Route a resolved ticket that a customer message reopens. Chatwoot reopens it as pending in a bot inbox and keeps
|
|
23
|
+
its assignee, and the router left every pending ticket with a person assignee alone, so such a ticket stayed
|
|
24
|
+
pending: off people's open view and never handed back. Its turn is now routed with its owner kept: a kind with a
|
|
25
|
+
status still ends it (a thanks-only message can be resolved), and anything else, a greeting included, goes back
|
|
26
|
+
to the owner as open. Only another bot's pending tickets are left alone.
|
|
27
|
+
|
|
10
28
|
## [0.4.0] - 2026-10-04
|
|
11
29
|
|
|
12
30
|
### Changed
|
|
@@ -102,7 +120,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
120
|
`routing_kind`.
|
|
103
121
|
- The `chatwoot-router-store-config` command and `chatwoot-router/stored-config`, for a configuration in KV.
|
|
104
122
|
|
|
105
|
-
[Unreleased]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.
|
|
123
|
+
[Unreleased]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.5.1...HEAD
|
|
124
|
+
[0.5.1]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.5.0...chatwoot-router@0.5.1
|
|
125
|
+
[0.5.0]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.4.0...chatwoot-router@0.5.0
|
|
106
126
|
[0.4.0]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.3.0...chatwoot-router@0.4.0
|
|
107
127
|
[0.3.0]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.2.0...chatwoot-router@0.3.0
|
|
108
128
|
[0.2.0]: https://github.com/Phala-Network/chatwoot-workers/compare/chatwoot-router@0.1.0...chatwoot-router@0.2.0
|
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ is each routed account's native agent bot, connected to the inboxes it should ro
|
|
|
17
17
|
may point to another deployment) for the owner, topic, kind, and whether there is a request yet.
|
|
18
18
|
3. It applies the topic and kind labels, sends the kind's canned reply if it has one (at most once per
|
|
19
19
|
conversation), and ends the turn: the kind's resolved or snoozed status, the confident owner, or a handoff to
|
|
20
|
-
people (`status=open`). A greeting with no request yet stays pending until the customer says more.
|
|
20
|
+
people (`status=open`). A greeting with no request yet stays pending until the customer says more. A resolved
|
|
21
|
+
ticket that a customer message reopens keeps its owner: its kind may still end it (a thanks is resolved),
|
|
22
|
+
otherwise it goes back to the owner.
|
|
21
23
|
4. Anything uncertain hands off to people: an unclear owner, a failed or unconfirmed reply, a human replying,
|
|
22
24
|
three failed attempts. Disconnecting an inbox's bot hands its pending tickets to people.
|
|
23
25
|
5. A sweep every five minutes lists pending and open conversations, so a missed webhook is caught up and a bot
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export { ConfigError } from "../../../shared/config.ts";
|
|
3
2
|
export declare const configSchema: z.ZodObject<{
|
|
4
3
|
chatwoot: z.ZodObject<{
|
|
5
4
|
baseUrl: z.ZodURL;
|
|
@@ -39,3 +38,4 @@ export interface Settings {
|
|
|
39
38
|
}
|
|
40
39
|
export declare function parseSettings(rawConfig: unknown, rawSecrets: object): Settings;
|
|
41
40
|
export declare function buildSettings(config: Config, secrets: Secrets): Settings;
|
|
41
|
+
export {};
|
|
@@ -115,6 +115,11 @@ var ChatwootError = class extends Error {
|
|
|
115
115
|
this.conversationMissing = conversationMissing;
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
|
+
function snoozedUntil(conversation) {
|
|
119
|
+
const value = conversation.snoozed_until;
|
|
120
|
+
const ms = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
121
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
122
|
+
}
|
|
118
123
|
var text = z2.string().nullish();
|
|
119
124
|
var attachmentSchema = z2.object({
|
|
120
125
|
file_type: text,
|
|
@@ -433,6 +438,8 @@ function toRelayConversation(conversationId, conversation) {
|
|
|
433
438
|
},
|
|
434
439
|
assignee: assignee ? { id: assignee.id, name: assignee.name } : null,
|
|
435
440
|
assigneeType: meta?.assignee_type ?? null,
|
|
441
|
+
waitingSince: conversation.waiting_since || null,
|
|
442
|
+
snoozedUntil: snoozedUntil(conversation),
|
|
436
443
|
customAttributes: conversation.custom_attributes ?? {}
|
|
437
444
|
};
|
|
438
445
|
}
|
|
@@ -781,8 +788,7 @@ async function routeConversation(ctx, accountId2, conversationId) {
|
|
|
781
788
|
return;
|
|
782
789
|
}
|
|
783
790
|
const conversation = toRelayConversation(conversationId, raw);
|
|
784
|
-
if (
|
|
785
|
-
if (raw.meta?.assignee && (raw.meta.assignee_type !== "AgentBot" || raw.meta.assignee.id !== botId)) return;
|
|
791
|
+
if (raw.meta?.assignee_type === "AgentBot" && raw.meta.assignee?.id !== botId) return;
|
|
786
792
|
if ((await chatwoot.inboxBot(accountId2, raw.inbox_id))?.id !== botId) {
|
|
787
793
|
if (owns(raw)) {
|
|
788
794
|
const latest = await chatwoot.getConversation(accountId2, conversationId);
|
|
@@ -844,7 +850,7 @@ async function routeConversation(ctx, accountId2, conversationId) {
|
|
|
844
850
|
let current = await fresh();
|
|
845
851
|
if (current === "defer" || !current) return current;
|
|
846
852
|
if (current.handoff) return handoff(current);
|
|
847
|
-
if (!kind && decision.noRequest && input.count < MAX_MESSAGES) return;
|
|
853
|
+
if (!kind && decision.noRequest && input.count < MAX_MESSAGES && !current.conversation.assignee) return;
|
|
848
854
|
const topic = !kind?.status && decision.topic !== null && Object.hasOwn(routing.topics ?? {}, decision.topic) && decision.topicConfidence >= routing.minConfidence && current.conversation.labels.every((label) => Object.hasOwn(kinds, label)) ? decision.topic : null;
|
|
849
855
|
const labels = [
|
|
850
856
|
.../* @__PURE__ */ new Set([
|
|
@@ -1275,7 +1281,8 @@ function eventTarget(payload) {
|
|
|
1275
1281
|
...change && (change.previous_value === "pending" || change.current_value === "resolved" || change.current_value === "pending" && change.previous_value !== "resolved") && event.updated_at !== void 0 ? { transition: { status: change.current_value, at: event.updated_at } } : {}
|
|
1276
1282
|
};
|
|
1277
1283
|
}
|
|
1278
|
-
if (event.event === "message_created" && (event.message_type === "incoming" || event.message_type === 0) && !event.private &&
|
|
1284
|
+
if (event.event === "message_created" && (event.message_type === "incoming" || event.message_type === 0) && !event.private && // A contact's webhook data has no `type` (Contact#webhook_data); a user's and a bot's do.
|
|
1285
|
+
(event.sender?.type ?? "contact") === "contact" && event.id !== void 0 && event.conversation) {
|
|
1279
1286
|
return { accountId: event.account.id, conversationId: event.conversation.id };
|
|
1280
1287
|
}
|
|
1281
1288
|
return void 0;
|
|
@@ -12,11 +12,14 @@ export declare class ChatwootError extends Error {
|
|
|
12
12
|
* A conversation as returned by GET conversations/{id} and in the conversation list. The spec's
|
|
13
13
|
* `status` enum omits `snoozed`, which the API does return, so the relay treats it as a string.
|
|
14
14
|
*/
|
|
15
|
-
export type ChatwootConversation = components["schemas"]["conversation_show"] & {
|
|
15
|
+
export type ChatwootConversation = Omit<components["schemas"]["conversation_show"], "snoozed_until"> & {
|
|
16
16
|
meta?: {
|
|
17
17
|
assignee_type?: string | null;
|
|
18
18
|
};
|
|
19
|
+
snoozed_until?: string | number | null;
|
|
19
20
|
};
|
|
21
|
+
/** Unix seconds when a snoozed conversation reopens; null unless it is snoozed until a time. */
|
|
22
|
+
export declare function snoozedUntil(conversation: ChatwootConversation): number | null;
|
|
20
23
|
declare const messageSchema: z.ZodObject<{
|
|
21
24
|
id: z.ZodNumber;
|
|
22
25
|
conversation_id: z.ZodOptional<z.ZodNumber>;
|
package/lib/shared/types.d.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface RelayConversation {
|
|
|
51
51
|
};
|
|
52
52
|
/** Person only; agent bots are represented as unassigned. */
|
|
53
53
|
assignee?: RelayAssignee | null;
|
|
54
|
+
/** Unix seconds since the customer has waited for a reply (Chatwoot's `waiting_since`); null when they do not. */
|
|
55
|
+
waitingSince?: number | null;
|
|
56
|
+
/** Unix seconds when a snoozed conversation reopens; null when not snoozed or snoozed until the next reply. */
|
|
57
|
+
snoozedUntil?: number | null;
|
|
54
58
|
assigneeType?: string | null;
|
|
55
59
|
customAttributes: Record<string, unknown>;
|
|
56
60
|
}
|
|
@@ -66,8 +70,8 @@ export interface RelayMessage {
|
|
|
66
70
|
emailSubject?: string | null;
|
|
67
71
|
/** An automatic email reply (out of office, for example): relayed without notifications. */
|
|
68
72
|
autoReply?: boolean;
|
|
69
|
-
/** A
|
|
70
|
-
|
|
73
|
+
/** A public reply that answers the customer (see isAnsweringReply). */
|
|
74
|
+
answers?: boolean;
|
|
71
75
|
attachments: RelayAttachment[];
|
|
72
76
|
/** A bot's options to pick, cards, or articles, with a link each when they have one. */
|
|
73
77
|
items?: RelayItem[];
|