@axiom-lattice/gateway 3.0.2 → 3.0.3
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/dist/index.js +31 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -6
- package/.eslintrc.json +0 -22
- package/.turbo/turbo-build.log +0 -46
- package/AGENTS.md +0 -112
- package/CHANGELOG.md +0 -1190
- package/LOGGER_CONFIG.md +0 -135
- package/RESUME_STREAM_CONTENT_BASED.md +0 -326
- package/RESUME_STREAM_README.md +0 -388
- package/jest.config.js +0 -72
- package/public/sdk/README.md +0 -695
- package/public/sdk/dashboard-engine-skill.md +0 -1122
- package/public/sdk/dashboard-single-file-spec.md +0 -357
- package/public/sdk/data-query-sdk-skill.md +0 -307
- package/public/sdk/data-query-sdk.d.ts +0 -252
- package/public/sdk/data-query-sdk.js +0 -970
- package/public/sdk/occupancy-dashboard.html +0 -363
- package/public/sdk/test-dashboard.html +0 -690
- package/scripts/seed-lark-installation.ts +0 -54
- package/src/__tests__/__mocks__/e2b.ts +0 -1
- package/src/__tests__/a2a.test.ts +0 -428
- package/src/__tests__/channel-installations.test.ts +0 -199
- package/src/__tests__/data-query.test.ts +0 -77
- package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
- package/src/__tests__/setup.ts +0 -5
- package/src/__tests__/workspace.test.ts +0 -400
- package/src/channels/__tests__/routes.test.ts +0 -50
- package/src/channels/lark/LarkChannelAdapter.ts +0 -217
- package/src/channels/lark/README.md +0 -187
- package/src/channels/lark/__tests__/controller.test.ts +0 -136
- package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
- package/src/channels/lark/__tests__/parser.test.ts +0 -72
- package/src/channels/lark/__tests__/sender.test.ts +0 -37
- package/src/channels/lark/__tests__/verification.test.ts +0 -157
- package/src/channels/lark/controller.ts +0 -48
- package/src/channels/lark/mapping-service.ts +0 -138
- package/src/channels/lark/parser.ts +0 -68
- package/src/channels/lark/routes.ts +0 -22
- package/src/channels/lark/sender.ts +0 -58
- package/src/channels/lark/types.ts +0 -20
- package/src/channels/lark/verification.ts +0 -32
- package/src/channels/registry.ts +0 -20
- package/src/channels/routes.ts +0 -30
- package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
- package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
- package/src/channels/wechat/context-store.ts +0 -54
- package/src/channels/wechat/controller.ts +0 -59
- package/src/channels/wechat/routes.ts +0 -7
- package/src/channels/wechat/types.ts +0 -62
- package/src/channels/wechat/wechat-client.ts +0 -162
- package/src/config/local-a2a-templates.ts +0 -59
- package/src/config.ts +0 -124
- package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
- package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
- package/src/controllers/__tests__/mime.test.ts +0 -74
- package/src/controllers/__tests__/resources.test.ts +0 -122
- package/src/controllers/__tests__/run.test.ts +0 -115
- package/src/controllers/__tests__/tasks.test.ts +0 -215
- package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
- package/src/controllers/agent_task.ts +0 -72
- package/src/controllers/assistant.ts +0 -490
- package/src/controllers/auth.ts +0 -524
- package/src/controllers/channel-bindings.ts +0 -135
- package/src/controllers/channel-installations.ts +0 -409
- package/src/controllers/collections.ts +0 -176
- package/src/controllers/config.ts +0 -126
- package/src/controllers/connections.ts +0 -181
- package/src/controllers/data-query.ts +0 -236
- package/src/controllers/database-configs.ts +0 -440
- package/src/controllers/embeddings.ts +0 -22
- package/src/controllers/entries.ts +0 -167
- package/src/controllers/eval.ts +0 -513
- package/src/controllers/export-import.ts +0 -166
- package/src/controllers/health.ts +0 -36
- package/src/controllers/local-a2a-templates.ts +0 -18
- package/src/controllers/local-a2a.ts +0 -149
- package/src/controllers/mcp-configs.ts +0 -673
- package/src/controllers/memory.ts +0 -257
- package/src/controllers/menu-items.ts +0 -114
- package/src/controllers/metrics-configs.ts +0 -1061
- package/src/controllers/middleware-types.ts +0 -15
- package/src/controllers/models.ts +0 -199
- package/src/controllers/personal-assistant.ts +0 -191
- package/src/controllers/resources.ts +0 -477
- package/src/controllers/run.ts +0 -338
- package/src/controllers/sandbox.ts +0 -255
- package/src/controllers/schedules.ts +0 -336
- package/src/controllers/skills.ts +0 -256
- package/src/controllers/tasks.ts +0 -213
- package/src/controllers/tenants.ts +0 -121
- package/src/controllers/thread_status.ts +0 -66
- package/src/controllers/threads.ts +0 -260
- package/src/controllers/tools.ts +0 -139
- package/src/controllers/users.ts +0 -135
- package/src/controllers/workflow-tracking.ts +0 -693
- package/src/controllers/workspace.ts +0 -879
- package/src/export_registrations/a2a-api-key.registration.ts +0 -89
- package/src/export_registrations/agent.registration.ts +0 -80
- package/src/export_registrations/binding.registration.ts +0 -106
- package/src/export_registrations/channel-installation.registration.ts +0 -88
- package/src/export_registrations/collection.registration.ts +0 -85
- package/src/export_registrations/connection.registration.ts +0 -97
- package/src/export_registrations/database-config.registration.ts +0 -94
- package/src/export_registrations/eval.registration.ts +0 -334
- package/src/export_registrations/index.ts +0 -31
- package/src/export_registrations/mcp-config.registration.ts +0 -97
- package/src/export_registrations/menu.registration.ts +0 -91
- package/src/export_registrations/metrics-config.registration.ts +0 -94
- package/src/export_registrations/skill.registration.ts +0 -88
- package/src/export_registrations/task.registration.ts +0 -99
- package/src/index.ts +0 -497
- package/src/logger/Logger.ts +0 -186
- package/src/router/MessageContext.ts +0 -14
- package/src/router/MessageRouter.ts +0 -490
- package/src/router/middlewares/auditLogger.ts +0 -34
- package/src/router/middlewares/deduplication.ts +0 -38
- package/src/router/middlewares/index.ts +0 -3
- package/src/router/middlewares/rateLimit.ts +0 -46
- package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
- package/src/routes/a2a-bridge.ts +0 -266
- package/src/routes/a2a-test-connection.ts +0 -126
- package/src/routes/a2a.ts +0 -851
- package/src/routes/channel-bindings.ts +0 -11
- package/src/routes/channel-installations.ts +0 -33
- package/src/routes/index.ts +0 -640
- package/src/routes/menu-items.ts +0 -10
- package/src/routes/resource-routes.ts +0 -35
- package/src/routes/stt.ts +0 -102
- package/src/schemas/data-query.ts +0 -69
- package/src/schemas/index.ts +0 -852
- package/src/services/ExportImportService.ts +0 -296
- package/src/services/__tests__/ExportImportService.test.ts +0 -130
- package/src/services/__tests__/eval-runner.test.ts +0 -131
- package/src/services/__tests__/local-a2a.test.ts +0 -124
- package/src/services/agent_task_consumer.ts +0 -348
- package/src/services/collection_service.ts +0 -258
- package/src/services/eval-runner.ts +0 -283
- package/src/services/local-a2a.ts +0 -354
- package/src/services/queue_service.ts +0 -104
- package/src/services/sandbox_service.ts +0 -223
- package/src/services/skill_service.ts +0 -97
- package/src/services/supabase.ts +0 -60
- package/src/swagger.ts +0 -77
- package/src/types/index.ts +0 -118
- package/src/types/koa.d.ts +0 -7
- package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
- package/src/utils/__tests__/path-containment.test.ts +0 -35
- package/src/utils/a2a-url.ts +0 -20
- package/src/utils/local-a2a-runtime.ts +0 -107
- package/src/utils/mime.ts +0 -44
- package/src/utils/path-containment.ts +0 -38
- package/tsconfig.json +0 -26
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type {
|
|
3
|
-
ChannelAdapter,
|
|
4
|
-
InboundMessage,
|
|
5
|
-
OutboundMessage,
|
|
6
|
-
ReplyTarget,
|
|
7
|
-
ChannelInstallation,
|
|
8
|
-
} from "@axiom-lattice/protocols";
|
|
9
|
-
import type { LarkChannelInstallationConfig } from "@axiom-lattice/protocols";
|
|
10
|
-
import { parseLarkMessageEvent } from "./parser";
|
|
11
|
-
import { Logger } from "../../logger/Logger";
|
|
12
|
-
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
13
|
-
|
|
14
|
-
const logger = new Logger({ serviceName: "lattice/gateway/lark" });
|
|
15
|
-
|
|
16
|
-
// ─── WS connection state ──────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const activeConnections = new Map<string, Lark.WSClient>();
|
|
19
|
-
|
|
20
|
-
// ─── WS message parsing (shared with lark-ws) ─────────────────────────────
|
|
21
|
-
|
|
22
|
-
interface LarkWSMessageEvent {
|
|
23
|
-
sender?: { sender_id?: { open_id?: string } };
|
|
24
|
-
message?: {
|
|
25
|
-
message_id?: string;
|
|
26
|
-
chat_id?: string;
|
|
27
|
-
chat_type?: string;
|
|
28
|
-
message_type?: string;
|
|
29
|
-
content?: string;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseTextContent(content: string | undefined): string | null {
|
|
34
|
-
if (!content) return null;
|
|
35
|
-
try {
|
|
36
|
-
const parsed = JSON.parse(content) as { text?: string };
|
|
37
|
-
return typeof parsed.text === "string" ? parsed.text : null;
|
|
38
|
-
} catch { return null; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeChatType(chatType: string | undefined): "direct" | "group" {
|
|
42
|
-
return chatType === "p2p" ? "direct" : "group";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function wsEventToInbound(
|
|
46
|
-
event: LarkWSMessageEvent,
|
|
47
|
-
installationId: string,
|
|
48
|
-
tenantId: string,
|
|
49
|
-
): InboundMessage | null {
|
|
50
|
-
const messageId = event.message?.message_id;
|
|
51
|
-
const chatId = event.message?.chat_id;
|
|
52
|
-
const openId = event.sender?.sender_id?.open_id;
|
|
53
|
-
if (!messageId || !chatId || !openId) return null;
|
|
54
|
-
if (event.message?.message_type !== "text") return null;
|
|
55
|
-
|
|
56
|
-
const text = parseTextContent(event.message.content);
|
|
57
|
-
if (!text) return null;
|
|
58
|
-
|
|
59
|
-
const chatType = normalizeChatType(event.message.chat_type);
|
|
60
|
-
return {
|
|
61
|
-
channel: "lark",
|
|
62
|
-
channelInstallationId: installationId,
|
|
63
|
-
tenantId,
|
|
64
|
-
sender: { id: openId, displayName: undefined },
|
|
65
|
-
content: { text, metadata: { chatId, chatType, messageId } },
|
|
66
|
-
conversation: { id: chatId, type: chatType },
|
|
67
|
-
replyTarget: {
|
|
68
|
-
adapterChannel: "lark",
|
|
69
|
-
channelInstallationId: installationId,
|
|
70
|
-
rawTarget: { chatId, messageId, chatType },
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ─── Adapter ──────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export const larkConfigSchema = z.object({
|
|
78
|
-
appId: z.string(),
|
|
79
|
-
appSecret: z.string(),
|
|
80
|
-
verificationToken: z.string().optional(),
|
|
81
|
-
encryptKey: z.string().optional(),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
export const larkChannelAdapter: ChannelAdapter<LarkChannelInstallationConfig> = {
|
|
85
|
-
channel: "lark",
|
|
86
|
-
|
|
87
|
-
configSchema: larkConfigSchema,
|
|
88
|
-
|
|
89
|
-
async receive(
|
|
90
|
-
rawPayload: unknown,
|
|
91
|
-
installation: ChannelInstallation<LarkChannelInstallationConfig>,
|
|
92
|
-
): Promise<InboundMessage | null> {
|
|
93
|
-
const event = parseLarkMessageEvent(rawPayload);
|
|
94
|
-
if (!event) return null;
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
channel: "lark",
|
|
98
|
-
channelInstallationId: installation.id,
|
|
99
|
-
tenantId: installation.tenantId,
|
|
100
|
-
sender: { id: event.openId, displayName: undefined },
|
|
101
|
-
content: {
|
|
102
|
-
text: event.text,
|
|
103
|
-
metadata: { chatId: event.chatId, chatType: event.chatType, messageId: event.messageId },
|
|
104
|
-
},
|
|
105
|
-
conversation: { id: event.chatId, type: event.chatType },
|
|
106
|
-
replyTarget: {
|
|
107
|
-
adapterChannel: "lark",
|
|
108
|
-
channelInstallationId: installation.id,
|
|
109
|
-
rawTarget: { chatId: event.chatId, messageId: event.messageId, chatType: event.chatType },
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
async sendReply(
|
|
115
|
-
replyTarget: ReplyTarget,
|
|
116
|
-
message: OutboundMessage,
|
|
117
|
-
installation: ChannelInstallation<LarkChannelInstallationConfig>,
|
|
118
|
-
): Promise<void> {
|
|
119
|
-
const { createLarkSender } = await import("./sender");
|
|
120
|
-
const sender = await createLarkSender(installation.config);
|
|
121
|
-
await sender.sendTextReply({
|
|
122
|
-
chatId: replyTarget.rawTarget.chatId as string,
|
|
123
|
-
text: message.text,
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
resolveThreadId(message: InboundMessage, binding: unknown): string {
|
|
128
|
-
const date = new Date().toISOString().split("T")[0];
|
|
129
|
-
const chatType = message.conversation?.type === "direct" ? "dm" : "group";
|
|
130
|
-
const agentId = (binding as { agentId: string }).agentId;
|
|
131
|
-
if (chatType === "dm") {
|
|
132
|
-
return `lark:dm:${message.sender.id}:${agentId}:${date}`;
|
|
133
|
-
}
|
|
134
|
-
return `lark:group:${message.conversation?.id ?? "unknown"}:${agentId}:${date}`;
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
async connect(
|
|
138
|
-
installation: ChannelInstallation<LarkChannelInstallationConfig>,
|
|
139
|
-
deps?: unknown,
|
|
140
|
-
): Promise<void> {
|
|
141
|
-
const { id: installationId, tenantId, config } = installation;
|
|
142
|
-
|
|
143
|
-
if (!config.appId || !config.appSecret) {
|
|
144
|
-
logger.warn("Lark installation missing credentials, skipping", { installationId });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (activeConnections.has(installationId)) {
|
|
149
|
-
logger.warn("Lark WS already connected for installation, skipping", { installationId });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
logger.info("Lark WS client starting", { installationId, tenantId });
|
|
154
|
-
|
|
155
|
-
const router = (deps as { router?: { dispatch(msg: InboundMessage): Promise<unknown> } })?.router;
|
|
156
|
-
|
|
157
|
-
const eventDispatcher = new Lark.EventDispatcher({}).register({
|
|
158
|
-
"im.message.receive_v1": async (data: LarkWSMessageEvent) => {
|
|
159
|
-
try {
|
|
160
|
-
const inbound = wsEventToInbound(data, installationId, tenantId);
|
|
161
|
-
if (!inbound) return;
|
|
162
|
-
|
|
163
|
-
logger.info("Lark WS message received", {
|
|
164
|
-
installationId,
|
|
165
|
-
senderId: inbound.sender.id,
|
|
166
|
-
chatId: data.message?.chat_id,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (router) {
|
|
170
|
-
const result = await router.dispatch(inbound);
|
|
171
|
-
if (!(result as { success?: boolean }).success) {
|
|
172
|
-
logger.warn("Lark WS dispatch failed", {
|
|
173
|
-
installationId,
|
|
174
|
-
error: (result as { error?: { message?: string } }).error?.message,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} catch (err) {
|
|
179
|
-
logger.error("Lark WS event handler error", {
|
|
180
|
-
installationId,
|
|
181
|
-
error: err instanceof Error ? err.message : String(err),
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const client = new Lark.WSClient({
|
|
188
|
-
appId: config.appId,
|
|
189
|
-
appSecret: config.appSecret,
|
|
190
|
-
loggerLevel: Lark.LoggerLevel.info,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await client.start({ eventDispatcher });
|
|
194
|
-
activeConnections.set(installationId, client);
|
|
195
|
-
logger.info("Lark WS client connected", { installationId });
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
async disconnect(installationId: string): Promise<void> {
|
|
199
|
-
const client = activeConnections.get(installationId);
|
|
200
|
-
if (!client) {
|
|
201
|
-
logger.warn("Lark WS not connected, nothing to disconnect", { installationId });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
logger.info("Lark WS client disconnecting", { installationId });
|
|
206
|
-
try {
|
|
207
|
-
client.close({ force: true });
|
|
208
|
-
} catch (err) {
|
|
209
|
-
logger.error("Lark WS client stop error", {
|
|
210
|
-
installationId,
|
|
211
|
-
error: err instanceof Error ? err.message : String(err),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
activeConnections.delete(installationId);
|
|
215
|
-
logger.info("Lark WS client disconnected", { installationId });
|
|
216
|
-
},
|
|
217
|
-
};
|
|
@@ -1,187 +0,0 @@
|
|
|
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
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import fastify from "fastify";
|
|
2
|
-
import { createLarkEventHandler } from "../controller";
|
|
3
|
-
|
|
4
|
-
const installation = {
|
|
5
|
-
id: "install-1",
|
|
6
|
-
tenantId: "tenant-a",
|
|
7
|
-
channel: "lark" as const,
|
|
8
|
-
config: { appId: "cli_app_1", appSecret: "secret" },
|
|
9
|
-
enabled: true,
|
|
10
|
-
rejectWhenNoBinding: false,
|
|
11
|
-
createdAt: new Date(),
|
|
12
|
-
updatedAt: new Date(),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
describe("createLarkEventHandler", () => {
|
|
16
|
-
it("returns 404 when installation is not found", async () => {
|
|
17
|
-
const app = fastify();
|
|
18
|
-
const handler = createLarkEventHandler({
|
|
19
|
-
installationStore: {
|
|
20
|
-
getInstallationById: jest.fn().mockResolvedValue(null),
|
|
21
|
-
} as any,
|
|
22
|
-
router: { dispatch: jest.fn() } as any,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
app.post(
|
|
26
|
-
"/api/channels/lark/installations/:installationId/events",
|
|
27
|
-
handler,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const response = await app.inject({
|
|
31
|
-
method: "POST",
|
|
32
|
-
url: "/api/channels/lark/installations/install-1/events",
|
|
33
|
-
payload: {},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(response.statusCode).toBe(404);
|
|
37
|
-
expect(response.json()).toEqual({ success: false, message: "Installation not found" });
|
|
38
|
-
|
|
39
|
-
await app.close();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("returns 404 when installation channel is not lark", async () => {
|
|
43
|
-
const app = fastify();
|
|
44
|
-
const handler = createLarkEventHandler({
|
|
45
|
-
installationStore: {
|
|
46
|
-
getInstallationById: jest.fn().mockResolvedValue({ ...installation, channel: "slack" }),
|
|
47
|
-
} as any,
|
|
48
|
-
router: { dispatch: jest.fn() } as any,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
app.post(
|
|
52
|
-
"/api/channels/lark/installations/:installationId/events",
|
|
53
|
-
handler,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const response = await app.inject({
|
|
57
|
-
method: "POST",
|
|
58
|
-
url: "/api/channels/lark/installations/install-1/events",
|
|
59
|
-
payload: {},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
expect(response.statusCode).toBe(404);
|
|
63
|
-
|
|
64
|
-
await app.close();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("returns 200 for non-message events (e.g. url verification)", async () => {
|
|
68
|
-
const app = fastify();
|
|
69
|
-
const handler = createLarkEventHandler({
|
|
70
|
-
installationStore: {
|
|
71
|
-
getInstallationById: jest.fn().mockResolvedValue(installation),
|
|
72
|
-
} as any,
|
|
73
|
-
router: { dispatch: jest.fn() } as any,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
app.post(
|
|
77
|
-
"/api/channels/lark/installations/:installationId/events",
|
|
78
|
-
handler,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const response = await app.inject({
|
|
82
|
-
method: "POST",
|
|
83
|
-
url: "/api/channels/lark/installations/install-1/events",
|
|
84
|
-
payload: { type: "url_verification", challenge: "challenge-token" },
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(response.statusCode).toBe(200);
|
|
88
|
-
|
|
89
|
-
await app.close();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("dispatches valid message events via router", async () => {
|
|
93
|
-
const app = fastify();
|
|
94
|
-
const dispatch = jest.fn().mockResolvedValue({ success: true });
|
|
95
|
-
|
|
96
|
-
const handler = createLarkEventHandler({
|
|
97
|
-
installationStore: {
|
|
98
|
-
getInstallationById: jest.fn().mockResolvedValue(installation),
|
|
99
|
-
} as any,
|
|
100
|
-
router: { dispatch } as any,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
app.post(
|
|
104
|
-
"/api/channels/lark/installations/:installationId/events",
|
|
105
|
-
handler,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
const response = await app.inject({
|
|
109
|
-
method: "POST",
|
|
110
|
-
url: "/api/channels/lark/installations/install-1/events",
|
|
111
|
-
payload: {
|
|
112
|
-
header: { event_type: "im.message.receive_v1" },
|
|
113
|
-
event: {
|
|
114
|
-
sender: { sender_id: { open_id: "ou_1" } },
|
|
115
|
-
message: {
|
|
116
|
-
message_id: "om_1",
|
|
117
|
-
chat_id: "oc_1",
|
|
118
|
-
chat_type: "p2p",
|
|
119
|
-
message_type: "text",
|
|
120
|
-
content: '{"text":"hello"}',
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(response.statusCode).toBe(200);
|
|
127
|
-
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(dispatch.mock.calls[0][0]).toMatchObject({
|
|
129
|
-
channel: "lark",
|
|
130
|
-
channelInstallationId: "install-1",
|
|
131
|
-
tenantId: "tenant-a",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await app.close();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { ThreadStore } from "@axiom-lattice/protocols";
|
|
2
|
-
import { createChannelThreadMappingService } from "../mapping-service";
|
|
3
|
-
|
|
4
|
-
describe("createChannelThreadMappingService", () => {
|
|
5
|
-
it("uses a user subject key for direct chats in hybrid mode", async () => {
|
|
6
|
-
const mappingStore = {
|
|
7
|
-
getMappingBySubject: jest.fn().mockResolvedValue(null),
|
|
8
|
-
createMapping: jest.fn().mockResolvedValue({ threadId: "thread-1" }),
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const threadStore: Pick<ThreadStore, "createThread"> = {
|
|
12
|
-
createThread: jest.fn().mockResolvedValue({ id: "thread-1" }),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const service = createChannelThreadMappingService({
|
|
16
|
-
mappingStore,
|
|
17
|
-
threadStore,
|
|
18
|
-
uuid: () => "thread-1",
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const result = await service.getOrCreateThread({
|
|
22
|
-
channel: "lark",
|
|
23
|
-
channelAppId: "cli_app_1",
|
|
24
|
-
tenantId: "tenant-a",
|
|
25
|
-
assistantId: "assistant-a",
|
|
26
|
-
mappingMode: "hybrid",
|
|
27
|
-
openId: "ou_1",
|
|
28
|
-
chatId: "oc_1",
|
|
29
|
-
chatType: "direct",
|
|
30
|
-
messageId: "om_1",
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
expect(mappingStore.getMappingBySubject).toHaveBeenCalledWith(
|
|
34
|
-
expect.objectContaining({
|
|
35
|
-
externalSubjectKey:
|
|
36
|
-
"lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_1",
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
expect(result.threadId).toBe("thread-1");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("reuses an existing mapping when one already exists", async () => {
|
|
43
|
-
const mappingStore = {
|
|
44
|
-
getMappingBySubject: jest.fn().mockResolvedValue({ threadId: "thread-existing" }),
|
|
45
|
-
createMapping: jest.fn(),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const threadStore: Pick<ThreadStore, "createThread"> = {
|
|
49
|
-
createThread: jest.fn(),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const service = createChannelThreadMappingService({
|
|
53
|
-
mappingStore,
|
|
54
|
-
threadStore,
|
|
55
|
-
uuid: () => "thread-new",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const result = await service.getOrCreateThread({
|
|
59
|
-
channel: "lark",
|
|
60
|
-
channelAppId: "cli_app_1",
|
|
61
|
-
tenantId: "tenant-a",
|
|
62
|
-
assistantId: "assistant-a",
|
|
63
|
-
mappingMode: "group",
|
|
64
|
-
openId: "ou_1",
|
|
65
|
-
chatId: "oc_group_1",
|
|
66
|
-
chatType: "group",
|
|
67
|
-
messageId: "om_2",
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result.threadId).toBe("thread-existing");
|
|
71
|
-
expect(threadStore.createThread).not.toHaveBeenCalled();
|
|
72
|
-
expect(mappingStore.createMapping).not.toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("re-reads the canonical mapping when createMapping hits a unique conflict", async () => {
|
|
76
|
-
const mappingStore = {
|
|
77
|
-
getMappingBySubject: jest
|
|
78
|
-
.fn()
|
|
79
|
-
.mockResolvedValueOnce(null)
|
|
80
|
-
.mockResolvedValueOnce({ threadId: "thread-canonical" }),
|
|
81
|
-
createMapping: jest.fn().mockRejectedValue(
|
|
82
|
-
Object.assign(new Error("duplicate key value violates unique constraint"), {
|
|
83
|
-
code: "23505",
|
|
84
|
-
}),
|
|
85
|
-
),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const threadStore: Pick<ThreadStore, "createThread" | "deleteThread"> = {
|
|
89
|
-
createThread: jest.fn().mockResolvedValue({ id: "thread-new" }),
|
|
90
|
-
deleteThread: jest.fn().mockResolvedValue(true),
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const service = createChannelThreadMappingService({
|
|
94
|
-
mappingStore,
|
|
95
|
-
threadStore,
|
|
96
|
-
uuid: () => "thread-new",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const result = await service.getOrCreateThread({
|
|
100
|
-
channel: "lark",
|
|
101
|
-
channelAppId: "cli_app_1",
|
|
102
|
-
tenantId: "tenant-a",
|
|
103
|
-
assistantId: "assistant-a",
|
|
104
|
-
mappingMode: "hybrid",
|
|
105
|
-
openId: "ou_1",
|
|
106
|
-
chatId: "oc_1",
|
|
107
|
-
chatType: "direct",
|
|
108
|
-
messageId: "om_1",
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(result.threadId).toBe("thread-canonical");
|
|
112
|
-
expect(mappingStore.getMappingBySubject).toHaveBeenCalledTimes(2);
|
|
113
|
-
expect(threadStore.deleteThread).toHaveBeenCalledWith(
|
|
114
|
-
"tenant-a",
|
|
115
|
-
"thread-new",
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
});
|