chatwoot-router 0.6.0 → 0.7.0

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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-10-07
11
+
12
+ ### Changed
13
+
14
+ - Jev sees an email's sender domain (`Email sender domain: mg1.substack.com`) before the customer's text, never the
15
+ address or name: a newsletter or notification sent through a mailing service is told from a person, so the spam
16
+ kind can end it. Other identifiers are redacted as before.
17
+
18
+ ### Fixed
19
+
20
+ - A ticket with no request yet waits for the customer to say more for 30 minutes at most, then the sweep routes it on
21
+ like any other ticket. An email that asked for nothing (a newsletter Jev did not call spam) stayed pending, with no
22
+ post and no owner, until someone noticed it.
23
+
10
24
  ## [0.6.0] - 2026-10-05
11
25
 
12
26
  ### Added
package/README.md CHANGED
@@ -12,12 +12,14 @@ is each routed account's native agent bot, connected to the inboxes it should ro
12
12
  1. A new ticket in a connected inbox is pending, so Chatwoot sends its events to the router's
13
13
  `/chatwoot/agent-bot` webhook. Each conversation is routed by a Durable Object of its own, so a slow request
14
14
  for one conversation never delays another.
15
- 2. The router reads the customer's first messages of this turn (redacted, at most three and 1,600 characters)
15
+ 2. The router reads the customer's first messages of this turn (redacted, at most three and 1,600 characters; an
16
+ email's sender domain leads them)
16
17
  and asks TypeSafe's System One API (`/v1/systemone`, model `routing.model`, Jev by default; `routing.endpoint`
17
18
  may point to another deployment) for the owner, topic, kind, and whether there is a request yet.
18
19
  3. It applies the topic and kind labels, sends the kind's canned reply if it has one (at most once per
19
20
  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. A resolved
21
+ people (`status=open`). A greeting with no request yet stays pending until the customer says more, for up to 30
22
+ minutes (an email that asks for nothing will not say more); then it is routed on. A resolved
21
23
  ticket that a customer message reopens keeps its owner: its kind may still end it (a thanks is resolved),
22
24
  otherwise it goes back to the owner.
23
25
  4. Anything uncertain hands off to people: an unclear owner, a failed or unconfirmed reply, a human replying,
@@ -149,6 +151,7 @@ the Router DO during rollback. If its state is lost or restored to an older reco
149
151
  and isolate conversations with uncertain attempts/turns for owner review; a currently empty history does not
150
152
  prove another send is safe. Existing pending conversations are reconciled; non-pending conversations stay with people. The user token must see all relevant inboxes.
151
153
 
152
- Redaction is best effort, not anonymization: other personal information can still reach TypeSafe. Logs contain
154
+ Redaction is best effort, not anonymization: other personal information can still reach TypeSafe, and an email's
155
+ sender domain is sent on purpose (never the address or name). Logs contain
153
156
  ids and outcomes, never message bodies or credentials. See [SECURITY.md](https://github.com/Phala-Network/chatwoot-workers/blob/main/SECURITY.md),
154
157
  [CONTRIBUTING.md](https://github.com/Phala-Network/chatwoot-workers/blob/main/CONTRIBUTING.md) and [CHANGELOG.md](https://github.com/Phala-Network/chatwoot-workers/blob/main/packages/chatwoot-router/CHANGELOG.md). Licensed under [MIT](https://github.com/Phala-Network/chatwoot-workers/blob/main/packages/chatwoot-router/LICENSE).
@@ -743,6 +743,7 @@ var NO_KIND = "none";
743
743
  var NO_KIND_CRITERION = "None of the other kinds.";
744
744
  var MAX_MESSAGES = 3;
745
745
  var MAX_TEXT = 1600;
746
+ var REQUEST_WAIT_SECONDS = 30 * 60;
746
747
  var REDACTIONS = [
747
748
  /(?<![\w.+-])[\w.+-]+@[\w.-]+\.[a-z]{2,}(?!\w)/gi,
748
749
  // email
@@ -835,17 +836,20 @@ async function routeConversation(ctx, accountId2, conversationId) {
835
836
  const initial = await snapshot();
836
837
  if (!initial) return;
837
838
  const identities = [initial.conversation.contact.name, initial.conversation.contact.email];
839
+ const senderDomain = initial.conversation.channel === "Channel::Email" ? initial.conversation.contact.email?.split("@")[1] : void 0;
838
840
  const inputs = (messages) => {
839
841
  const texts = messages.filter(isCustomer).flatMap((message) => {
840
- const text2 = sanitize(
842
+ const text3 = sanitize(
841
843
  [message.content_attributes?.email?.subject ?? "", messageContent(message)].join("\n"),
842
844
  identities
843
845
  );
844
- return text2.replaceAll("[REDACTED]", "").trim() ? [{ id: message.id, text: text2 }] : [];
846
+ return text3.replaceAll("[REDACTED]", "").trim() ? [{ id: message.id, text: text3 }] : [];
845
847
  }).slice(0, MAX_MESSAGES);
848
+ const text2 = texts.map((entry) => entry.text).join(" ");
846
849
  return {
847
850
  key: texts.map((entry) => entry.id).join(","),
848
- text: texts.map((entry) => entry.text).join(" ").slice(0, MAX_TEXT),
851
+ text: (senderDomain ? `Email sender domain: ${senderDomain}
852
+ ${text2}` : text2).slice(0, MAX_TEXT),
849
853
  count: texts.length
850
854
  };
851
855
  };
@@ -881,7 +885,9 @@ async function routeConversation(ctx, accountId2, conversationId) {
881
885
  let current = await fresh();
882
886
  if (current === "defer" || !current) return current;
883
887
  if (current.handoff) return handoff(current);
884
- if (!kind && decision.noRequest && input.count < MAX_MESSAGES && !current.conversation.assignee) return;
888
+ const lastSaid = Math.max(0, ...turn.messages.filter(isCustomer).map((message) => message.created_at ?? Infinity));
889
+ const waiting = Date.now() / 1e3 - lastSaid < REQUEST_WAIT_SECONDS;
890
+ if (!kind && decision.noRequest && input.count < MAX_MESSAGES && !current.conversation.assignee && waiting) return;
885
891
  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;
886
892
  const labels = [
887
893
  .../* @__PURE__ */ new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatwoot-router",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Route Chatwoot conversations with TypeSafe Jev on Cloudflare Workers.",
5
5
  "keywords": [
6
6
  "chatwoot",