@axiom-lattice/gateway 2.1.75 → 2.1.76
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 +14 -12
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +741 -401
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +689 -390
- package/dist/index.mjs.map +1 -1
- package/dist/sender-PX32VSHB.mjs +32 -0
- package/dist/sender-PX32VSHB.mjs.map +1 -0
- package/package.json +7 -6
- package/src/bindings/index.ts +14 -0
- package/src/channels/__tests__/routes.test.ts +23 -44
- package/src/channels/lark/LarkChannelAdapter.ts +75 -0
- package/src/channels/lark/__tests__/controller.test.ts +62 -196
- package/src/channels/lark/controller.ts +25 -167
- package/src/channels/lark/routes.ts +12 -111
- package/src/channels/registry.ts +20 -0
- package/src/channels/routes.ts +7 -4
- package/src/controllers/channel-bindings.ts +135 -0
- package/src/controllers/channel-installations.ts +6 -3
- package/src/index.ts +38 -2
- package/src/router/MessageContext.ts +14 -0
- package/src/router/MessageRouter.ts +201 -0
- package/src/router/middlewares/auditLogger.ts +34 -0
- package/src/router/middlewares/deduplication.ts +26 -0
- package/src/router/middlewares/index.ts +3 -0
- package/src/router/middlewares/rateLimit.ts +39 -0
- package/src/routes/channel-bindings.ts +11 -0
- package/src/routes/index.ts +50 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStoreLattice,
|
|
3
|
+
agentInstanceManager,
|
|
4
|
+
} from "@axiom-lattice/core";
|
|
5
|
+
import type {
|
|
6
|
+
InboundMessage,
|
|
7
|
+
DispatchResult,
|
|
8
|
+
BindingRegistry,
|
|
9
|
+
ChannelInstallationStore,
|
|
10
|
+
} from "@axiom-lattice/protocols";
|
|
11
|
+
import { ChannelAdapterRegistry } from "../channels/registry";
|
|
12
|
+
import type { MessageContext, MessageMiddleware } from "./MessageContext";
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
14
|
+
|
|
15
|
+
export class BindingNotFoundError extends Error {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "BindingNotFoundError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MessageRouterConfig {
|
|
23
|
+
middlewares: MessageMiddleware[];
|
|
24
|
+
bindingRegistry: BindingRegistry;
|
|
25
|
+
adapterRegistry: ChannelAdapterRegistry;
|
|
26
|
+
installationStore: ChannelInstallationStore;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MessageRouter {
|
|
30
|
+
private middlewares: MessageMiddleware[];
|
|
31
|
+
private bindingRegistry: BindingRegistry;
|
|
32
|
+
private adapterRegistry: ChannelAdapterRegistry;
|
|
33
|
+
private installationStore: ChannelInstallationStore;
|
|
34
|
+
|
|
35
|
+
constructor(config: MessageRouterConfig) {
|
|
36
|
+
this.middlewares = [...config.middlewares];
|
|
37
|
+
this.bindingRegistry = config.bindingRegistry;
|
|
38
|
+
this.adapterRegistry = config.adapterRegistry;
|
|
39
|
+
this.installationStore = config.installationStore;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
use(middleware: MessageMiddleware): void {
|
|
43
|
+
this.middlewares.push(middleware);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async dispatch(message: InboundMessage): Promise<DispatchResult> {
|
|
47
|
+
const ctx: MessageContext = {
|
|
48
|
+
inboundMessage: message,
|
|
49
|
+
metadata: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await this.runMiddlewares(ctx, async () => {
|
|
54
|
+
let binding = await this.bindingRegistry.resolve({
|
|
55
|
+
channel: message.channel,
|
|
56
|
+
senderId: message.sender.id,
|
|
57
|
+
channelInstallationId: message.channelInstallationId,
|
|
58
|
+
tenantId: message.tenantId,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!binding) {
|
|
62
|
+
const installation = await this.installationStore.getInstallationById(
|
|
63
|
+
message.channelInstallationId,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (installation?.rejectWhenNoBinding) {
|
|
67
|
+
throw new BindingNotFoundError(
|
|
68
|
+
`No binding for sender "${message.sender.id}" on channel "${message.channel}"`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (installation?.fallbackAgentId) {
|
|
73
|
+
binding = {
|
|
74
|
+
id: "fallback",
|
|
75
|
+
channel: message.channel,
|
|
76
|
+
channelInstallationId: message.channelInstallationId,
|
|
77
|
+
tenantId: message.tenantId,
|
|
78
|
+
senderId: message.sender.id,
|
|
79
|
+
agentId: installation.fallbackAgentId,
|
|
80
|
+
threadId: undefined,
|
|
81
|
+
threadMode: "fixed",
|
|
82
|
+
enabled: true,
|
|
83
|
+
createdAt: new Date(),
|
|
84
|
+
updatedAt: new Date(),
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
throw new BindingNotFoundError(
|
|
88
|
+
`No binding for sender "${message.sender.id}" and no fallback configured`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.binding = binding;
|
|
94
|
+
|
|
95
|
+
if (!binding.enabled) {
|
|
96
|
+
throw new BindingNotFoundError(
|
|
97
|
+
`Binding for sender "${message.sender.id}" is disabled`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let threadId = ctx.binding.threadId;
|
|
102
|
+
if (!threadId) {
|
|
103
|
+
const threadStore = getStoreLattice("default", "thread").store;
|
|
104
|
+
const newThreadId = randomUUID();
|
|
105
|
+
const newThread = await threadStore.createThread(
|
|
106
|
+
message.tenantId,
|
|
107
|
+
ctx.binding.agentId,
|
|
108
|
+
newThreadId,
|
|
109
|
+
{
|
|
110
|
+
metadata: {
|
|
111
|
+
channel: message.channel,
|
|
112
|
+
channelInstallationId: message.channelInstallationId,
|
|
113
|
+
senderId: message.sender.id,
|
|
114
|
+
bindingId: ctx.binding.id,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
threadId = newThread.id;
|
|
119
|
+
if (ctx.binding.id !== "fallback") {
|
|
120
|
+
await this.bindingRegistry.update(ctx.binding.id, { threadId });
|
|
121
|
+
ctx.binding.threadId = threadId;
|
|
122
|
+
} else {
|
|
123
|
+
ctx.binding.threadId = threadId;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const agent = agentInstanceManager.getAgent({
|
|
128
|
+
tenant_id: message.tenantId,
|
|
129
|
+
assistant_id: ctx.binding.agentId,
|
|
130
|
+
thread_id: threadId,
|
|
131
|
+
workspace_id: ctx.binding.workspaceId || "",
|
|
132
|
+
project_id: ctx.binding.projectId || "",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const invokeResult = await agent.invoke({
|
|
136
|
+
input: { message: message.content.text },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ctx.result = extractTextFromInvokeResult(invokeResult);
|
|
140
|
+
|
|
141
|
+
if (message.replyTarget) {
|
|
142
|
+
const adapter = this.adapterRegistry.get(message.replyTarget.adapterChannel);
|
|
143
|
+
if (adapter) {
|
|
144
|
+
const installation = await this.installationStore.getInstallationById(
|
|
145
|
+
message.channelInstallationId,
|
|
146
|
+
);
|
|
147
|
+
if (installation) {
|
|
148
|
+
await adapter.sendReply(
|
|
149
|
+
message.replyTarget,
|
|
150
|
+
{ text: ctx.result },
|
|
151
|
+
installation,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
bindingId: ctx.binding?.id,
|
|
161
|
+
threadId: ctx.binding?.threadId,
|
|
162
|
+
result: ctx.result,
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
ctx.error = error instanceof Error ? error : new Error(String(error));
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
bindingId: ctx.binding?.id,
|
|
169
|
+
threadId: ctx.binding?.threadId,
|
|
170
|
+
error: ctx.error,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async runMiddlewares(
|
|
176
|
+
ctx: MessageContext,
|
|
177
|
+
finalHandler: () => Promise<void>,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const dispatch = (index: number): Promise<void> => {
|
|
180
|
+
if (index >= this.middlewares.length) {
|
|
181
|
+
return finalHandler();
|
|
182
|
+
}
|
|
183
|
+
const middleware = this.middlewares[index];
|
|
184
|
+
return middleware(ctx, () => dispatch(index + 1));
|
|
185
|
+
};
|
|
186
|
+
return dispatch(0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractTextFromInvokeResult(result: unknown): string {
|
|
191
|
+
if (result && typeof result === "object" && "messages" in result) {
|
|
192
|
+
const messages = (result as { messages: Array<{ role: string; content: string }> }).messages;
|
|
193
|
+
if (Array.isArray(messages)) {
|
|
194
|
+
const aiMessages = messages.filter((m) => m.role === "ai");
|
|
195
|
+
if (aiMessages.length > 0) {
|
|
196
|
+
return aiMessages.map((m) => m.content).join("\n");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return JSON.stringify(result);
|
|
201
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MessageMiddleware } from "../MessageContext";
|
|
2
|
+
import { Logger } from "../../logger/Logger";
|
|
3
|
+
|
|
4
|
+
const logger = new Logger({ serviceName: "lattice/gateway/audit" });
|
|
5
|
+
|
|
6
|
+
export function createAuditLoggerMiddleware(): MessageMiddleware {
|
|
7
|
+
return async (ctx, next) => {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
try {
|
|
10
|
+
await next();
|
|
11
|
+
logger.info("message routed", {
|
|
12
|
+
event: "message:routed",
|
|
13
|
+
channel: ctx.inboundMessage.channel,
|
|
14
|
+
senderId: ctx.inboundMessage.sender.id,
|
|
15
|
+
agentId: ctx.binding?.agentId,
|
|
16
|
+
threadId: ctx.binding?.threadId,
|
|
17
|
+
duration: Date.now() - start,
|
|
18
|
+
status: "success",
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logger.error(
|
|
22
|
+
error instanceof Error ? error.message : String(error),
|
|
23
|
+
{
|
|
24
|
+
event: "message:error",
|
|
25
|
+
channel: ctx.inboundMessage.channel,
|
|
26
|
+
senderId: ctx.inboundMessage.sender.id,
|
|
27
|
+
duration: Date.now() - start,
|
|
28
|
+
status: "error",
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MessageMiddleware } from "../MessageContext";
|
|
2
|
+
|
|
3
|
+
const processedMessages = new Map<string, number>();
|
|
4
|
+
|
|
5
|
+
export function createDeduplicationMiddleware(ttlMs: number = 5 * 60 * 1000): MessageMiddleware {
|
|
6
|
+
return async (ctx, next) => {
|
|
7
|
+
const msg = ctx.inboundMessage;
|
|
8
|
+
const msgId = msg.content.metadata?.messageId as string | undefined;
|
|
9
|
+
const key = msgId
|
|
10
|
+
? `${msg.channel}:${msg.channelInstallationId}:${msgId}`
|
|
11
|
+
: `${msg.channel}:${msg.channelInstallationId}:${msg.sender.id}`;
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const lastProcessed = processedMessages.get(key);
|
|
14
|
+
if (lastProcessed && (now - lastProcessed) < ttlMs) return;
|
|
15
|
+
|
|
16
|
+
processedMessages.set(key, now);
|
|
17
|
+
|
|
18
|
+
if (processedMessages.size > 10000) {
|
|
19
|
+
const oldest = Array.from(processedMessages.entries())
|
|
20
|
+
.sort((a, b) => a[1] - b[1])[0];
|
|
21
|
+
if (oldest) processedMessages.delete(oldest[0]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await next();
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MessageMiddleware } from "../MessageContext";
|
|
2
|
+
|
|
3
|
+
export class RateLimitError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "RateLimitError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Counter { count: number; resetAt: number; }
|
|
11
|
+
const rateCounters = new Map<string, Counter>();
|
|
12
|
+
|
|
13
|
+
export function createRateLimitMiddleware(
|
|
14
|
+
maxRequests: number = 10,
|
|
15
|
+
windowMs: number = 60 * 1000,
|
|
16
|
+
maxEntries: number = 10000,
|
|
17
|
+
): MessageMiddleware {
|
|
18
|
+
return async (ctx, next) => {
|
|
19
|
+
const senderKey = `${ctx.inboundMessage.channel}:${ctx.inboundMessage.sender.id}`;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
let counter = rateCounters.get(senderKey);
|
|
22
|
+
if (!counter || now > counter.resetAt) {
|
|
23
|
+
counter = { count: 0, resetAt: now + windowMs };
|
|
24
|
+
}
|
|
25
|
+
counter.count++;
|
|
26
|
+
rateCounters.set(senderKey, counter);
|
|
27
|
+
|
|
28
|
+
if (rateCounters.size > maxEntries) {
|
|
29
|
+
const oldest = Array.from(rateCounters.entries())
|
|
30
|
+
.sort((a, b) => a[1].resetAt - b[1].resetAt)[0];
|
|
31
|
+
if (oldest) rateCounters.delete(oldest[0]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (counter.count > maxRequests) {
|
|
35
|
+
throw new RateLimitError(`Rate limit exceeded`);
|
|
36
|
+
}
|
|
37
|
+
await next();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import * as bindingsController from "../controllers/channel-bindings";
|
|
3
|
+
|
|
4
|
+
export function registerChannelBindingRoutes(app: FastifyInstance): void {
|
|
5
|
+
app.get("/api/channel-bindings", bindingsController.getBindingList);
|
|
6
|
+
app.get("/api/channel-bindings/resolve", bindingsController.resolveBinding);
|
|
7
|
+
app.post("/api/channel-bindings", bindingsController.createBinding);
|
|
8
|
+
app.get("/api/channel-bindings/:id", bindingsController.getBinding);
|
|
9
|
+
app.put("/api/channel-bindings/:id", bindingsController.updateBinding);
|
|
10
|
+
app.delete("/api/channel-bindings/:id", bindingsController.deleteBinding);
|
|
11
|
+
}
|
package/src/routes/index.ts
CHANGED
|
@@ -43,6 +43,9 @@ import { registerTenantRoutes } from "../controllers/tenants";
|
|
|
43
43
|
import { registerAuthRoutes } from "../controllers/auth";
|
|
44
44
|
import { registerChannelRoutes } from "../channels/routes";
|
|
45
45
|
import { registerChannelInstallationRoutes } from "./channel-installations";
|
|
46
|
+
import { registerChannelBindingRoutes } from "./channel-bindings";
|
|
47
|
+
import type { MessageRouter } from "../router/MessageRouter";
|
|
48
|
+
import type { ChannelInstallationStore } from "@axiom-lattice/protocols";
|
|
46
49
|
// import {
|
|
47
50
|
// getThreadStatusHandler,
|
|
48
51
|
// getAgentThreadsHandler,
|
|
@@ -52,7 +55,7 @@ import { registerChannelInstallationRoutes } from "./channel-installations";
|
|
|
52
55
|
// updateQueueConfigHandler,
|
|
53
56
|
// } from "../controllers/thread_status";
|
|
54
57
|
|
|
55
|
-
export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
58
|
+
export const registerLatticeRoutes = (app: FastifyInstance, channelDeps?: { router: MessageRouter; installationStore: ChannelInstallationStore }): void => {
|
|
56
59
|
// 运行路由
|
|
57
60
|
app.post<{
|
|
58
61
|
Body: any;
|
|
@@ -349,10 +352,55 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
|
349
352
|
allowTenantRegistration: process.env.ALLOW_TENANT_REGISTRATION !== "false",
|
|
350
353
|
});
|
|
351
354
|
|
|
352
|
-
registerChannelRoutes(app);
|
|
355
|
+
registerChannelRoutes(app, channelDeps);
|
|
353
356
|
|
|
354
357
|
registerChannelInstallationRoutes(app);
|
|
355
358
|
|
|
359
|
+
if (channelDeps) {
|
|
360
|
+
registerChannelBindingRoutes(app);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Direct inbound message dispatch
|
|
364
|
+
if (channelDeps?.router) {
|
|
365
|
+
app.post("/api/channels/inbound", async (request, reply) => {
|
|
366
|
+
try {
|
|
367
|
+
const router = channelDeps.router;
|
|
368
|
+
const msg = request.body as Record<string, unknown>;
|
|
369
|
+
|
|
370
|
+
if (!msg.channel || !msg.sender || !msg.content) {
|
|
371
|
+
reply.status(400).send({
|
|
372
|
+
success: false,
|
|
373
|
+
message: "Missing required fields: channel, sender, content",
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const inboundMessage = {
|
|
379
|
+
channel: msg.channel as string,
|
|
380
|
+
channelInstallationId: (msg.channelInstallationId || "") as string,
|
|
381
|
+
tenantId: (msg.tenantId || "default") as string,
|
|
382
|
+
sender: {
|
|
383
|
+
id: (msg.sender as Record<string, unknown>).id as string,
|
|
384
|
+
displayName: (msg.sender as Record<string, unknown>).displayName as string | undefined,
|
|
385
|
+
},
|
|
386
|
+
content: {
|
|
387
|
+
text: (msg.content as Record<string, unknown>).text as string,
|
|
388
|
+
},
|
|
389
|
+
replyTarget: msg.replyTarget as Record<string, unknown> | undefined,
|
|
390
|
+
} as Parameters<typeof router.dispatch>[0];
|
|
391
|
+
|
|
392
|
+
await router.dispatch(inboundMessage).catch((error) => {
|
|
393
|
+
console.error("Inbound dispatch error:", error);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
reply.status(200).send({ accepted: true });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error("Inbound route error:", error);
|
|
399
|
+
reply.status(500).send({ success: false, message: "Internal error" });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
356
404
|
// Workflow tracking routes
|
|
357
405
|
app.get(
|
|
358
406
|
"/api/workflows/definitions",
|