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 +14 -0
- package/README.md +6 -3
- package/lib/packages/chatwoot-router/src/index.js +10 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
842
|
+
const text3 = sanitize(
|
|
841
843
|
[message.content_attributes?.email?.subject ?? "", messageContent(message)].join("\n"),
|
|
842
844
|
identities
|
|
843
845
|
);
|
|
844
|
-
return
|
|
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:
|
|
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
|
-
|
|
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([
|