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.4.0...HEAD
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 (conversation.assignee) return;
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 && event.sender?.type === "contact" && event.id !== void 0 && event.conversation) {
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>;
@@ -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 qualifying public reply follows this message in Chatwoot. */
70
- answered?: boolean;
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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatwoot-router",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Route Chatwoot conversations with TypeSafe Jev on Cloudflare Workers.",
5
5
  "keywords": [
6
6
  "chatwoot",