@axiom-lattice/gateway 2.1.52 → 2.1.54
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +22 -0
- package/dist/index.js +717 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +713 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/__tests__/channel-installations.test.ts +199 -0
- package/src/channels/__tests__/routes.test.ts +71 -0
- package/src/channels/lark/README.md +187 -0
- package/src/channels/lark/__tests__/aggregator.test.ts +23 -0
- package/src/channels/lark/__tests__/controller.test.ts +270 -0
- package/src/channels/lark/__tests__/mapping-service.test.ts +118 -0
- package/src/channels/lark/__tests__/parser.test.ts +72 -0
- package/src/channels/lark/__tests__/sender.test.ts +37 -0
- package/src/channels/lark/__tests__/verification.test.ts +157 -0
- package/src/channels/lark/aggregator.ts +16 -0
- package/src/channels/lark/config.ts +44 -0
- package/src/channels/lark/controller.ts +189 -0
- package/src/channels/lark/mapping-service.ts +138 -0
- package/src/channels/lark/parser.ts +68 -0
- package/src/channels/lark/routes.ts +121 -0
- package/src/channels/lark/runner.ts +37 -0
- package/src/channels/lark/sender.ts +58 -0
- package/src/channels/lark/types.ts +33 -0
- package/src/channels/lark/verification.ts +67 -0
- package/src/channels/routes.ts +25 -0
- package/src/controllers/channel-installations.ts +354 -0
- package/src/controllers/threads.ts +8 -6
- package/src/routes/channel-installations.ts +33 -0
- package/src/routes/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.54",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@fastify/swagger": "^9.5.1",
|
|
26
26
|
"@fastify/swagger-ui": "^5.2.3",
|
|
27
27
|
"@fastify/websocket": "^11.0.1",
|
|
28
|
+
"@larksuiteoapi/node-sdk": "^1.47.0",
|
|
28
29
|
"@langchain/core": "1.1.30",
|
|
29
30
|
"@langchain/langgraph": "1.0.4",
|
|
30
31
|
"@supabase/supabase-js": "^2.49.1",
|
|
@@ -38,10 +39,10 @@
|
|
|
38
39
|
"pg": "^8.11.0",
|
|
39
40
|
"redis": "^5.0.1",
|
|
40
41
|
"uuid": "^9.0.1",
|
|
41
|
-
"@axiom-lattice/core": "2.1.
|
|
42
|
-
"@axiom-lattice/pg-stores": "1.0.
|
|
43
|
-
"@axiom-lattice/protocols": "2.1.
|
|
44
|
-
"@axiom-lattice/queue-redis": "1.0.
|
|
42
|
+
"@axiom-lattice/core": "2.1.48",
|
|
43
|
+
"@axiom-lattice/pg-stores": "1.0.38",
|
|
44
|
+
"@axiom-lattice/protocols": "2.1.26",
|
|
45
|
+
"@axiom-lattice/queue-redis": "1.0.25"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
|
2
|
+
import fastify from "fastify";
|
|
3
|
+
import { registerChannelInstallationRoutes } from "../routes/channel-installations";
|
|
4
|
+
|
|
5
|
+
// Set DATABASE_URL before importing controller
|
|
6
|
+
process.env.DATABASE_URL = "postgres://test:test@localhost:5432/test";
|
|
7
|
+
|
|
8
|
+
// Mock the store
|
|
9
|
+
const mockGetInstallationsByTenant = jest.fn();
|
|
10
|
+
const mockGetInstallationById = jest.fn();
|
|
11
|
+
const mockCreateInstallation = jest.fn();
|
|
12
|
+
const mockUpdateInstallation = jest.fn();
|
|
13
|
+
const mockDeleteInstallation = jest.fn();
|
|
14
|
+
|
|
15
|
+
jest.mock("@axiom-lattice/pg-stores", () => ({
|
|
16
|
+
PostgreSQLChannelInstallationStore: jest.fn().mockImplementation(() => ({
|
|
17
|
+
getInstallationsByTenant: mockGetInstallationsByTenant,
|
|
18
|
+
getInstallationById: mockGetInstallationById,
|
|
19
|
+
createInstallation: mockCreateInstallation,
|
|
20
|
+
updateInstallation: mockUpdateInstallation,
|
|
21
|
+
deleteInstallation: mockDeleteInstallation,
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe("Channel Installation API", () => {
|
|
26
|
+
let app: FastifyInstance;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// Reset mocks
|
|
30
|
+
mockGetInstallationsByTenant.mockReset();
|
|
31
|
+
mockGetInstallationById.mockReset();
|
|
32
|
+
mockCreateInstallation.mockReset();
|
|
33
|
+
mockUpdateInstallation.mockReset();
|
|
34
|
+
mockDeleteInstallation.mockReset();
|
|
35
|
+
|
|
36
|
+
app = fastify();
|
|
37
|
+
registerChannelInstallationRoutes(app);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
await app.close();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("GET /api/channel-installations", () => {
|
|
45
|
+
it("should return list of installations for tenant", async () => {
|
|
46
|
+
mockGetInstallationsByTenant.mockResolvedValueOnce([
|
|
47
|
+
{
|
|
48
|
+
id: "install-1",
|
|
49
|
+
tenantId: "tenant-a",
|
|
50
|
+
channel: "lark",
|
|
51
|
+
name: "Test Installation",
|
|
52
|
+
config: {
|
|
53
|
+
appId: "cli_test",
|
|
54
|
+
appSecret: "secret",
|
|
55
|
+
assistantId: "assistant-1",
|
|
56
|
+
mappingMode: "hybrid",
|
|
57
|
+
},
|
|
58
|
+
createdAt: new Date(),
|
|
59
|
+
updatedAt: new Date(),
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const response = await app.inject({
|
|
64
|
+
method: "GET",
|
|
65
|
+
url: "/api/channel-installations",
|
|
66
|
+
headers: {
|
|
67
|
+
"x-tenant-id": "tenant-a",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(response.statusCode).toBe(200);
|
|
72
|
+
const body = JSON.parse(response.body);
|
|
73
|
+
expect(body.success).toBe(true);
|
|
74
|
+
expect(body.data).toHaveProperty("records");
|
|
75
|
+
expect(body.data).toHaveProperty("total");
|
|
76
|
+
expect(body.data.records).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should filter by channel type", async () => {
|
|
80
|
+
const response = await app.inject({
|
|
81
|
+
method: "GET",
|
|
82
|
+
url: "/api/channel-installations?channel=lark",
|
|
83
|
+
headers: {
|
|
84
|
+
"x-tenant-id": "tenant-a",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(response.statusCode).toBe(200);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("GET /api/channel-installations/:installationId", () => {
|
|
93
|
+
it("should return installation by id", async () => {
|
|
94
|
+
const response = await app.inject({
|
|
95
|
+
method: "GET",
|
|
96
|
+
url: "/api/channel-installations/install-1",
|
|
97
|
+
headers: {
|
|
98
|
+
"x-tenant-id": "tenant-a",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Will return 404 since mock returns null, but route is accessible
|
|
103
|
+
expect([200, 404]).toContain(response.statusCode);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("POST /api/channel-installations", () => {
|
|
108
|
+
it("should create new installation", async () => {
|
|
109
|
+
mockCreateInstallation.mockResolvedValueOnce({
|
|
110
|
+
id: "new-install-id",
|
|
111
|
+
tenantId: "tenant-a",
|
|
112
|
+
channel: "lark",
|
|
113
|
+
name: "Test Lark Installation",
|
|
114
|
+
config: {
|
|
115
|
+
appId: "cli_test",
|
|
116
|
+
appSecret: "secret",
|
|
117
|
+
assistantId: "assistant-1",
|
|
118
|
+
mappingMode: "hybrid",
|
|
119
|
+
},
|
|
120
|
+
createdAt: new Date(),
|
|
121
|
+
updatedAt: new Date(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const response = await app.inject({
|
|
125
|
+
method: "POST",
|
|
126
|
+
url: "/api/channel-installations",
|
|
127
|
+
headers: {
|
|
128
|
+
"x-tenant-id": "tenant-a",
|
|
129
|
+
"content-type": "application/json",
|
|
130
|
+
},
|
|
131
|
+
payload: {
|
|
132
|
+
channel: "lark",
|
|
133
|
+
name: "Test Lark Installation",
|
|
134
|
+
config: {
|
|
135
|
+
appId: "cli_test",
|
|
136
|
+
appSecret: "secret",
|
|
137
|
+
assistantId: "assistant-1",
|
|
138
|
+
mappingMode: "hybrid",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(response.statusCode).toBe(201);
|
|
144
|
+
const body = JSON.parse(response.body);
|
|
145
|
+
expect(body.success).toBe(true);
|
|
146
|
+
expect(body.data).toHaveProperty("id");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should validate required fields", async () => {
|
|
150
|
+
const response = await app.inject({
|
|
151
|
+
method: "POST",
|
|
152
|
+
url: "/api/channel-installations",
|
|
153
|
+
headers: {
|
|
154
|
+
"x-tenant-id": "tenant-a",
|
|
155
|
+
"content-type": "application/json",
|
|
156
|
+
},
|
|
157
|
+
payload: {
|
|
158
|
+
// Missing channel and config
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(response.statusCode).toBe(400);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("PUT /api/channel-installations/:installationId", () => {
|
|
167
|
+
it("should update installation", async () => {
|
|
168
|
+
const response = await app.inject({
|
|
169
|
+
method: "PUT",
|
|
170
|
+
url: "/api/channel-installations/install-1",
|
|
171
|
+
headers: {
|
|
172
|
+
"x-tenant-id": "tenant-a",
|
|
173
|
+
"content-type": "application/json",
|
|
174
|
+
},
|
|
175
|
+
payload: {
|
|
176
|
+
name: "Updated Name",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Will return 404 since mock returns null, but route is accessible
|
|
181
|
+
expect([200, 404]).toContain(response.statusCode);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("DELETE /api/channel-installations/:installationId", () => {
|
|
186
|
+
it("should delete installation", async () => {
|
|
187
|
+
const response = await app.inject({
|
|
188
|
+
method: "DELETE",
|
|
189
|
+
url: "/api/channel-installations/install-1",
|
|
190
|
+
headers: {
|
|
191
|
+
"x-tenant-id": "tenant-a",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Will return 404 since mock returns null, but route is accessible
|
|
196
|
+
expect([200, 404]).toContain(response.statusCode);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fastify from "fastify";
|
|
2
|
+
import { registerChannelRoutes } from "../routes";
|
|
3
|
+
|
|
4
|
+
describe("registerChannelRoutes", () => {
|
|
5
|
+
const envBackup = { ...process.env };
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.env = { ...envBackup };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("registers the lark route dynamically", async () => {
|
|
12
|
+
const app = fastify();
|
|
13
|
+
registerChannelRoutes(app, {
|
|
14
|
+
lark: {
|
|
15
|
+
getInstallationConfig: jest.fn().mockResolvedValue({
|
|
16
|
+
installationId: "install-1",
|
|
17
|
+
tenantId: "tenant-a",
|
|
18
|
+
assistantId: "assistant-a",
|
|
19
|
+
appId: "cli_app_1",
|
|
20
|
+
appSecret: "secret",
|
|
21
|
+
verificationToken: "token-1",
|
|
22
|
+
mappingMode: "hybrid",
|
|
23
|
+
}),
|
|
24
|
+
parseRequestBody: jest.fn().mockReturnValue({
|
|
25
|
+
type: "url_verification",
|
|
26
|
+
challenge: "challenge-token",
|
|
27
|
+
token: "token-1",
|
|
28
|
+
}),
|
|
29
|
+
verifyParsedBody: jest.fn().mockReturnValue(true),
|
|
30
|
+
parseEvent: jest.fn(),
|
|
31
|
+
claimInboundReceipt: jest.fn(),
|
|
32
|
+
markInboundReceiptCompleted: jest.fn(),
|
|
33
|
+
markInboundReceiptFailed: jest.fn(),
|
|
34
|
+
resolveThread: jest.fn(),
|
|
35
|
+
runAgentAndCollectText: jest.fn(),
|
|
36
|
+
sendTextReply: jest.fn(),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const response = await app.inject({
|
|
41
|
+
method: "POST",
|
|
42
|
+
url: "/api/channels/lark/installations/install-1/events",
|
|
43
|
+
payload: {
|
|
44
|
+
challenge: "challenge-token",
|
|
45
|
+
type: "url_verification",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(response.statusCode).toBe(200);
|
|
50
|
+
expect(response.json()).toEqual({ challenge: "challenge-token" });
|
|
51
|
+
|
|
52
|
+
await app.close();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("skips lark route registration when lark config is disabled", async () => {
|
|
56
|
+
process.env.LARK_ENABLED = "false";
|
|
57
|
+
|
|
58
|
+
const app = fastify();
|
|
59
|
+
registerChannelRoutes(app);
|
|
60
|
+
|
|
61
|
+
const response = await app.inject({
|
|
62
|
+
method: "POST",
|
|
63
|
+
url: "/api/channels/lark/installations/install-1/events",
|
|
64
|
+
payload: {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(response.statusCode).toBe(404);
|
|
68
|
+
|
|
69
|
+
await app.close();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Lark Channel Ingress
|
|
2
|
+
|
|
3
|
+
This module adds Feishu/Lark message ingress to the existing `@axiom-lattice/gateway` service.
|
|
4
|
+
|
|
5
|
+
## What It Supports
|
|
6
|
+
|
|
7
|
+
- inbound Lark event callbacks
|
|
8
|
+
- URL verification challenge
|
|
9
|
+
- plain and encrypted callback payloads
|
|
10
|
+
- v1 and v2 verification token extraction
|
|
11
|
+
- text-message parsing
|
|
12
|
+
- thread mapping to existing Axiom threads
|
|
13
|
+
- reactive text replies sent through the official Lark SDK
|
|
14
|
+
|
|
15
|
+
## Current Scope
|
|
16
|
+
|
|
17
|
+
This is the MVP scope:
|
|
18
|
+
|
|
19
|
+
- one gateway deployment can handle multiple tenant-scoped Lark installations
|
|
20
|
+
- only text messages are supported
|
|
21
|
+
- only reactive replies are supported
|
|
22
|
+
- identity mapping is persisted in PostgreSQL
|
|
23
|
+
|
|
24
|
+
## Environment Variables
|
|
25
|
+
|
|
26
|
+
Required when `LARK_ENABLED=true`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
LARK_ENABLED=true
|
|
30
|
+
|
|
31
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/axiom_lattice
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Lark app credentials are no longer loaded from global environment variables. They are stored per installation in PostgreSQL.
|
|
35
|
+
|
|
36
|
+
## Route
|
|
37
|
+
|
|
38
|
+
The webhook route is mounted on the existing gateway service:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
POST /api/channels/lark/installations/:installationId/events
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Start The Gateway
|
|
45
|
+
|
|
46
|
+
Development:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm --filter @axiom-lattice/gateway dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Production:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm --filter @axiom-lattice/gateway build
|
|
56
|
+
pnpm --filter @axiom-lattice/gateway start
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Feishu/Lark Console Setup
|
|
60
|
+
|
|
61
|
+
In the Feishu developer console:
|
|
62
|
+
|
|
63
|
+
1. Create or open an enterprise self-built app.
|
|
64
|
+
2. Go to the event subscription / callback configuration page.
|
|
65
|
+
3. Set the request URL to:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
https://your-domain.com/api/channels/lark/installations/<installationId>/events
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
4. Create a matching installation record in PostgreSQL for that tenant. The installation config must include:
|
|
72
|
+
|
|
73
|
+
- appId
|
|
74
|
+
- appSecret
|
|
75
|
+
- Verification Token
|
|
76
|
+
- Encrypt Key
|
|
77
|
+
- assistantId
|
|
78
|
+
- mappingMode
|
|
79
|
+
|
|
80
|
+
5. Configure the same values in the Feishu console:
|
|
81
|
+
|
|
82
|
+
- Verification Token
|
|
83
|
+
- Encrypt Key
|
|
84
|
+
|
|
85
|
+
6. Subscribe to the message event:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
im.message.receive_v1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Callback Handling
|
|
92
|
+
|
|
93
|
+
This implementation handles the normal Feishu event callback flow, not the message-card callback signature flow.
|
|
94
|
+
|
|
95
|
+
Supported verification behavior:
|
|
96
|
+
|
|
97
|
+
- v1 callback token: `body.token`
|
|
98
|
+
- v2 callback token: `body.header.token`
|
|
99
|
+
- encrypted envelope: `body.encrypt`
|
|
100
|
+
|
|
101
|
+
If the callback is encrypted, the gateway decrypts it before:
|
|
102
|
+
|
|
103
|
+
- checking the verification token
|
|
104
|
+
- handling URL verification
|
|
105
|
+
- parsing the event body
|
|
106
|
+
|
|
107
|
+
## Identity Mapping
|
|
108
|
+
|
|
109
|
+
The mapping mode controls how Lark conversations map to Axiom threads.
|
|
110
|
+
|
|
111
|
+
### `user`
|
|
112
|
+
|
|
113
|
+
- one Lark user maps to one Axiom thread
|
|
114
|
+
- useful for personal assistant behavior
|
|
115
|
+
|
|
116
|
+
### `group`
|
|
117
|
+
|
|
118
|
+
- one Lark chat maps to one Axiom thread
|
|
119
|
+
- useful for shared team assistant behavior
|
|
120
|
+
|
|
121
|
+
### `hybrid`
|
|
122
|
+
|
|
123
|
+
- direct chats use user isolation
|
|
124
|
+
- group chats use group isolation
|
|
125
|
+
|
|
126
|
+
This is the recommended default because it matches normal user expectations.
|
|
127
|
+
|
|
128
|
+
Each installation also binds the request to:
|
|
129
|
+
|
|
130
|
+
- one tenant
|
|
131
|
+
- one default assistant
|
|
132
|
+
- optional workspace and project IDs
|
|
133
|
+
|
|
134
|
+
## Message Flow
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
Lark callback
|
|
138
|
+
-> gateway route
|
|
139
|
+
-> decrypt / verify
|
|
140
|
+
-> parse text event
|
|
141
|
+
-> claim inbound receipt
|
|
142
|
+
-> resolve or create thread mapping
|
|
143
|
+
-> run assistant on existing gateway/core pipeline
|
|
144
|
+
-> aggregate assistant text
|
|
145
|
+
-> send reply through official Lark SDK
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Local Debugging
|
|
149
|
+
|
|
150
|
+
To test locally, the Feishu callback URL must be reachable from Feishu.
|
|
151
|
+
|
|
152
|
+
Typical options:
|
|
153
|
+
|
|
154
|
+
- a public dev domain
|
|
155
|
+
- an internal network tunnel
|
|
156
|
+
|
|
157
|
+
Example callback URL:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
https://your-tunnel.example.com/api/channels/lark/installations/<installationId>/events
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## What To Check When It Fails
|
|
164
|
+
|
|
165
|
+
1. `LARK_ENABLED` is set to `true`
|
|
166
|
+
2. `DATABASE_URL` is set and reachable
|
|
167
|
+
3. the installation record exists in `lattice_channel_installations`
|
|
168
|
+
4. the installation `appId` / `appSecret` are correct
|
|
169
|
+
5. the installation `verificationToken` matches the Feishu console value
|
|
170
|
+
6. the installation `encryptKey` matches the Feishu console value
|
|
171
|
+
7. the callback URL is publicly reachable
|
|
172
|
+
8. the Feishu app is subscribed to `im.message.receive_v1`
|
|
173
|
+
|
|
174
|
+
## Data Written
|
|
175
|
+
|
|
176
|
+
This module writes to PostgreSQL tables created by the gateway migration path:
|
|
177
|
+
|
|
178
|
+
- `lattice_channel_installations`
|
|
179
|
+
- `channel_identity_mappings`
|
|
180
|
+
- `channel_inbound_message_receipts`
|
|
181
|
+
|
|
182
|
+
## Known Limits
|
|
183
|
+
|
|
184
|
+
- text only
|
|
185
|
+
- one default assistant per installation
|
|
186
|
+
- no proactive outbound messaging workflow yet
|
|
187
|
+
- no card/file/image handling yet
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MessageChunkTypes } from "@axiom-lattice/protocols";
|
|
2
|
+
import { aggregateLarkReply } from "../aggregator";
|
|
3
|
+
|
|
4
|
+
describe("aggregateLarkReply", () => {
|
|
5
|
+
it("joins ai chunks for the requested message id", () => {
|
|
6
|
+
const result = aggregateLarkReply("msg-1", [
|
|
7
|
+
{ type: MessageChunkTypes.AI, data: { id: "msg-1", content: "Hel" } },
|
|
8
|
+
{ type: MessageChunkTypes.AI, data: { id: "msg-2", content: "skip" } },
|
|
9
|
+
{ type: MessageChunkTypes.TOOL, data: { id: "msg-1", content: "ignore" } },
|
|
10
|
+
{ type: MessageChunkTypes.AI, data: { id: "msg-1", content: "lo" } },
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
expect(result).toBe("Hello");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns an empty string when no ai content exists for the message", () => {
|
|
17
|
+
const result = aggregateLarkReply("msg-1", [
|
|
18
|
+
{ type: MessageChunkTypes.MESSAGE_COMPLETED, data: { id: "msg-1" } },
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
expect(result).toBe("");
|
|
22
|
+
});
|
|
23
|
+
});
|