@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.
Files changed (155) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +31 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,186 +0,0 @@
1
- import pino from "pino";
2
- import "pino-pretty";
3
- import "pino-roll";
4
- import { AsyncLocalStorage } from "async_hooks";
5
-
6
- export interface LoggerContext {
7
- "x-user-id"?: string;
8
- "x-tenant-id"?: string;
9
- "x-request-id"?: string;
10
- "x-task-id"?: string;
11
- "x-thread-id"?: string;
12
- }
13
-
14
- export interface LoggerOptions {
15
- name?: string;
16
- serviceName?: string;
17
- context?: LoggerContext;
18
- }
19
-
20
- /**
21
- * 单例的Pino日志工厂类,管理底层pino实例
22
- */
23
- class PinoLoggerFactory {
24
- private static instance: PinoLoggerFactory;
25
- private pinoLogger: pino.Logger;
26
-
27
- private constructor() {
28
- const isProd = process.env.NODE_ENV === "production";
29
-
30
- const loggerConfig: pino.LoggerOptions = {
31
- // 自定义时间戳格式
32
- timestamp: () => `,"@timestamp":"${new Date().toISOString()}"`,
33
-
34
- // 关闭默认的时间戳键
35
- base: {
36
- "@version": "1",
37
- app_name: "lattice",
38
- service_name: "lattice/graph-server",
39
- thread_name: "main",
40
- logger_name: "lattice-graph-logger",
41
- },
42
-
43
- formatters: {
44
- level: (label, number) => {
45
- return {
46
- level: label.toUpperCase(),
47
- level_value: number * 1000,
48
- };
49
- },
50
- },
51
- };
52
-
53
- // 生产环境使用文件日志
54
- if (isProd) {
55
- try {
56
- this.pinoLogger = pino(
57
- loggerConfig,
58
- pino.transport({
59
- target: "pino-roll",
60
- options: {
61
- file: "./logs/fin_ai_graph_server",
62
- frequency: "daily",
63
- mkdir: true,
64
- },
65
- })
66
- );
67
- } catch (error) {
68
- console.error(
69
- "无法初始化 pino-roll 日志记录器,回退到控制台日志",
70
- error
71
- );
72
- // 回退到开发环境的配置
73
- this.pinoLogger = pino({
74
- ...loggerConfig,
75
- transport: {
76
- target: "pino-pretty",
77
- options: {
78
- colorize: true,
79
- },
80
- },
81
- });
82
- }
83
- } else {
84
- // 开发环境使用格式化输出
85
- this.pinoLogger = pino({
86
- ...loggerConfig,
87
- transport: {
88
- target: "pino-pretty",
89
- options: {
90
- colorize: true,
91
- },
92
- },
93
- });
94
- }
95
- }
96
-
97
- public static getInstance(): PinoLoggerFactory {
98
- if (!PinoLoggerFactory.instance) {
99
- PinoLoggerFactory.instance = new PinoLoggerFactory();
100
- }
101
- return PinoLoggerFactory.instance;
102
- }
103
-
104
- public getPinoLogger(): pino.Logger {
105
- return this.pinoLogger;
106
- }
107
- }
108
-
109
- /**
110
- * Logger类,可以创建多个实例,每个实例有自己的上下文
111
- */
112
- export class Logger {
113
- private context: LoggerContext;
114
- private name: string;
115
- private serviceName: string;
116
-
117
- constructor(options?: LoggerOptions) {
118
- this.context = options?.context || {};
119
- this.name = options?.name || "lattice-graph-logger";
120
- this.serviceName = options?.serviceName || "lattice/graph-server";
121
- }
122
-
123
- /**
124
- * 获取合并了上下文的日志对象
125
- * @param additionalContext 额外的上下文数据
126
- * @returns 带有上下文的pino日志对象
127
- */
128
- private getContextualLogger(additionalContext?: object): pino.Logger {
129
- const pinoLogger = PinoLoggerFactory.getInstance().getPinoLogger();
130
-
131
- // 合并Logger实例的上下文和额外上下文
132
- const contextObj = {
133
- "x-user-id": this.context["x-user-id"] || "",
134
- "x-tenant-id": this.context["x-tenant-id"] || "",
135
- "x-request-id": this.context["x-request-id"] || "",
136
- "x-task-id": this.context["x-task-id"] || "",
137
- "x-thread-id": this.context["x-thread-id"] || "",
138
- service_name: this.serviceName,
139
- logger_name: this.name,
140
- ...additionalContext,
141
- };
142
-
143
- // 创建带有上下文的子日志记录器
144
- return pinoLogger.child(contextObj);
145
- }
146
-
147
- info(msg: string, obj?: object): void {
148
- this.getContextualLogger(obj).info(msg);
149
- }
150
-
151
- error(msg: string, obj?: object | Error): void {
152
- this.getContextualLogger(obj).error(msg);
153
- }
154
-
155
- warn(msg: string, obj?: object): void {
156
- this.getContextualLogger(obj).warn(msg);
157
- }
158
-
159
- debug(msg: string, obj?: object): void {
160
- this.getContextualLogger(obj).debug(msg);
161
- }
162
-
163
- /**
164
- * 更新Logger实例的上下文
165
- */
166
- updateContext(context: Partial<LoggerContext>): void {
167
- this.context = {
168
- ...this.context,
169
- ...context,
170
- };
171
- }
172
-
173
- /**
174
- * 创建一个新的Logger实例,继承当前Logger的上下文
175
- */
176
- child(options: Partial<LoggerOptions>): Logger {
177
- return new Logger({
178
- name: options.name || this.name,
179
- serviceName: options.serviceName || this.serviceName,
180
- context: {
181
- ...this.context,
182
- ...options.context,
183
- },
184
- });
185
- }
186
- }
@@ -1,14 +0,0 @@
1
- import type { InboundMessage, Binding } from "@axiom-lattice/protocols";
2
-
3
- export interface MessageContext {
4
- inboundMessage: InboundMessage;
5
- binding?: Binding;
6
- result?: string;
7
- error?: Error;
8
- metadata: Record<string, unknown>;
9
- }
10
-
11
- export type MessageMiddleware = (
12
- ctx: MessageContext,
13
- next: () => Promise<void>,
14
- ) => Promise<void>;
@@ -1,490 +0,0 @@
1
- import {
2
- getStoreLattice,
3
- agentInstanceManager,
4
- } from "@axiom-lattice/core";
5
- import type {
6
- InboundMessage,
7
- DispatchResult,
8
- BindingRegistry,
9
- Binding,
10
- ChannelInstallationStore,
11
- } from "@axiom-lattice/protocols";
12
- import { ChannelAdapterRegistry } from "../channels/registry";
13
- import type { MessageContext, MessageMiddleware } from "./MessageContext";
14
- import { randomUUID } from "crypto";
15
-
16
- export class BindingNotFoundError extends Error {
17
- constructor(message: string) {
18
- super(message);
19
- this.name = "BindingNotFoundError";
20
- }
21
- }
22
-
23
- /**
24
- * Configuration for {@link MessageRouter}.
25
- *
26
- * @param middlewares - Ordered middleware chain executed before dispatch
27
- * @param bindingRegistry - Resolves sender-to-agent bindings
28
- * @param adapterRegistry - Registry of {@link ChannelAdapter} implementations
29
- * @param installationStore - Persisted channel installation configs
30
- */
31
- export interface MessageRouterConfig {
32
- middlewares: MessageMiddleware[];
33
- bindingRegistry: BindingRegistry;
34
- adapterRegistry: ChannelAdapterRegistry;
35
- installationStore: ChannelInstallationStore;
36
- }
37
-
38
- /**
39
- * Core message router for external channel integration.
40
- *
41
- * Receives normalized {@link InboundMessage} objects from channel adapters,
42
- * resolves the target agent via {@link BindingRegistry}, manages thread
43
- * lifecycle, and sends AI responses back to the channel via
44
- * `ChannelAdapter.sendReply`.
45
- *
46
- * ## Reply flow
47
- *
48
- * The router uses a counter-based deduplication scheme on the internal
49
- * {@link Agent.reply:ready} event:
50
- *
51
- * 1. Before `agent.addMessage()`, it subscribes to `reply:ready` on the agent.
52
- * 2. Multiple concurrent dispatches on the same thread increment a counter;
53
- * only the first registers the `EventEmitter.once` listener.
54
- * 3. When `reply:ready` fires, the callback extracts the last AI message from
55
- * the LangGraph state and sends it via `ChannelAdapter.sendReply`.
56
- * 4. The counter is decremented; the subscription is only removed when the
57
- * counter reaches 0.
58
- * 5. Stale subscriptions are cleaned up after a 1-hour timeout.
59
- *
60
- * The `replyTarget` is carried through by injecting `_replyTarget` into
61
- * `custom_run_config` on {@link Agent.addMessage}, and retrieved from the
62
- * `reply:ready` event payload.
63
- *
64
- * @see {@link ChannelAdapter.sendReply}
65
- * @see {@link InboundMessage.replyTarget}
66
- */
67
- export class MessageRouter {
68
- private middlewares: MessageMiddleware[];
69
- private bindingRegistry: BindingRegistry;
70
- private adapterRegistry: ChannelAdapterRegistry;
71
- private installationStore: ChannelInstallationStore;
72
-
73
- /**
74
- * Tracks reply subscriptions per thread+channel to avoid duplicate
75
- * `subscribeOnce` registrations and ensure proper cleanup.
76
- *
77
- * Key format: `{threadId}:{adapterChannel}:reply`
78
- */
79
- private _replySubs: Map<string, { count: number; timer: NodeJS.Timeout }> = new Map();
80
-
81
- constructor(config: MessageRouterConfig) {
82
- this.middlewares = [...config.middlewares];
83
- this.bindingRegistry = config.bindingRegistry;
84
- this.adapterRegistry = config.adapterRegistry;
85
- this.installationStore = config.installationStore;
86
- }
87
-
88
- /**
89
- * Register an additional middleware at the end of the chain.
90
- *
91
- * @param middleware - A {@link MessageMiddleware} function
92
- */
93
- use(middleware: MessageMiddleware): void {
94
- this.middlewares.push(middleware);
95
- }
96
-
97
- /**
98
- * Dispatch an inbound channel message to the bound agent.
99
- *
100
- * Full pipeline: middleware chain → binding resolution → thread lifecycle
101
- * → agent.addMessage() → (async) reply via {@link ChannelAdapter.sendReply}.
102
- *
103
- * If the message has a {@link InboundMessage.replyTarget}, the router subscribes
104
- * to the agent's `reply:ready` event before enqueuing the message, and sends
105
- * the AI response back to the channel when it arrives.
106
- *
107
- * @param message - Normalized inbound message from a channel adapter
108
- * @returns {@link DispatchResult} with success status, bindingId, and threadId
109
- */
110
- async dispatch(message: InboundMessage): Promise<DispatchResult> {
111
- const ctx: MessageContext = {
112
- inboundMessage: message,
113
- metadata: {},
114
- };
115
-
116
- let binding: Binding | null = null;
117
- let threadId: string | undefined;
118
- let agentId: string | undefined;
119
-
120
- try {
121
- await this.runMiddlewares(ctx, async () => {
122
- // Resolve tenantId from installation if not provided
123
- const tenantId = message.tenantId
124
- || (await this.installationStore.getInstallationById(message.channelInstallationId))?.tenantId;
125
-
126
- if (!tenantId) {
127
- console.error({
128
- event: "dispatch:error",
129
- error: "tenantId missing",
130
- channel: message.channel,
131
- channelInstallationId: message.channelInstallationId,
132
- senderId: message.sender.id,
133
- }, "tenantId is required");
134
- throw new Error(
135
- "tenantId is required: provide it in the message or ensure the channelInstallation has a tenantId"
136
- );
137
- }
138
-
139
- console.log({ event: "dispatch:start", channel: message.channel, senderId: message.sender.id, tenantId }, "Message dispatch started");
140
-
141
- // 检查 adapter 是否支持自定义 thread 策略
142
- const adapter = this.adapterRegistry.get(message.channel);
143
- const hasAdapterThreadStrategy = !!adapter?.resolveThreadId;
144
-
145
- binding = await this.bindingRegistry.resolve({
146
- channel: message.channel,
147
- senderId: message.sender.id,
148
- channelInstallationId: message.channelInstallationId,
149
- tenantId,
150
- });
151
-
152
- const installation = await this.installationStore.getInstallationById(
153
- message.channelInstallationId,
154
- );
155
-
156
- if (!binding) {
157
- if (installation?.rejectWhenNoBinding) {
158
- console.warn({
159
- event: "dispatch:no_binding",
160
- channel: message.channel,
161
- senderId: message.sender.id,
162
- tenantId,
163
- channelInstallationId: message.channelInstallationId,
164
- }, "No binding found and rejectWhenNoBinding is enabled");
165
- throw new BindingNotFoundError(
166
- `No binding for sender "${message.sender.id}" on channel "${message.channel}"`,
167
- );
168
- }
169
-
170
- if (installation?.fallbackAgentId) {
171
- console.warn({
172
- event: "dispatch:fallback",
173
- channel: message.channel,
174
- senderId: message.sender.id,
175
- fallbackAgentId: installation.fallbackAgentId,
176
- }, "No binding found, falling back to fallbackAgentId");
177
- binding = {
178
- id: "fallback",
179
- channel: message.channel,
180
- channelInstallationId: message.channelInstallationId,
181
- tenantId,
182
- senderId: message.sender.id,
183
- agentId: installation.fallbackAgentId,
184
- threadId: undefined,
185
- threadMode: "fixed" as const,
186
- enabled: true,
187
- createdAt: new Date(),
188
- updatedAt: new Date(),
189
- };
190
- } else if (!hasAdapterThreadStrategy) {
191
- // 只有当 adapter 没有自定义 thread 策略且没有 fallback 时才报错
192
- console.error({
193
- event: "dispatch:no_fallback",
194
- channel: message.channel,
195
- senderId: message.sender.id,
196
- tenantId,
197
- }, "No binding found and no fallbackAgentId configured");
198
- throw new BindingNotFoundError(
199
- `No binding for sender "${message.sender.id}" and no fallback configured`,
200
- );
201
- }
202
- }
203
-
204
- ctx.binding = binding ?? undefined;
205
-
206
- if (binding) {
207
- console.log({
208
- event: "dispatch:binding",
209
- bindingId: binding.id,
210
- agentId: binding.agentId,
211
- threadId: binding.threadId,
212
- threadMode: binding.threadMode,
213
- workspaceId: binding.workspaceId,
214
- projectId: binding.projectId,
215
- }, "Binding resolved");
216
-
217
- if (!binding.enabled) {
218
- console.warn({
219
- event: "dispatch:binding_disabled",
220
- bindingId: binding.id,
221
- agentId: binding.agentId,
222
- senderId: message.sender.id,
223
- }, "Binding is disabled, rejecting message");
224
- throw new BindingNotFoundError(
225
- `Binding for sender "${message.sender.id}" is disabled`,
226
- );
227
- }
228
- }
229
-
230
- // 确定 agentId
231
- agentId = binding?.agentId ?? installation?.fallbackAgentId;
232
- if (!agentId) {
233
- throw new BindingNotFoundError(
234
- `No agent configured for sender "${message.sender.id}"`,
235
- );
236
- }
237
-
238
- // 优先使用 adapter 的自定义 thread 策略
239
- if (hasAdapterThreadStrategy) {
240
- const resolvedThreadId = await adapter!.resolveThreadId!(message, binding);
241
- threadId = resolvedThreadId;
242
- console.log({
243
- event: "dispatch:thread:adapter",
244
- threadId,
245
- channel: message.channel,
246
- adapterChannel: adapter!.channel,
247
- }, "Thread resolved by adapter strategy");
248
-
249
- // 确保 thread 存在(adapter 的策略可能有重复,例如同一天)
250
- const threadStore = getStoreLattice("default", "thread").store;
251
- try {
252
- await threadStore.createThread(
253
- tenantId!,
254
- agentId,
255
- threadId,
256
- {
257
- metadata: {
258
- channel: message.channel,
259
- channelInstallationId: message.channelInstallationId,
260
- senderId: message.sender.id,
261
- bindingId: binding?.id,
262
- ...(message.conversation ? {
263
- conversationId: message.conversation.id,
264
- conversationType: message.conversation.type,
265
- } : {}),
266
- },
267
- },
268
- );
269
- console.log({
270
- event: "dispatch:thread:adapter:created",
271
- threadId,
272
- }, "Thread created by adapter strategy");
273
- } catch {
274
- // Thread already exists from a previous message — 复用
275
- console.log({
276
- event: "dispatch:thread:adapter:reuse",
277
- threadId,
278
- }, "Thread already exists, reusing");
279
- }
280
- } else if (binding) {
281
- // 默认策略:per_conversation 总是创建新线程,fixed 复用已有线程
282
- if (binding.threadMode === "per_conversation") {
283
- threadId = undefined; // 创建新线程
284
- } else {
285
- threadId = binding.threadId; // 复用 fixed 线程
286
- }
287
- }
288
-
289
- if (!threadId) {
290
- const threadStore = getStoreLattice("default", "thread").store;
291
- const newThreadId = randomUUID();
292
- console.log({
293
- event: "dispatch:thread:create",
294
- agentId,
295
- newThreadId,
296
- tenantId: tenantId!,
297
- }, "Creating new thread for binding");
298
- const newThread = await threadStore.createThread(
299
- tenantId!,
300
- agentId,
301
- newThreadId,
302
- {
303
- metadata: {
304
- channel: message.channel,
305
- channelInstallationId: message.channelInstallationId,
306
- senderId: message.sender.id,
307
- bindingId: binding?.id,
308
- ...(message.conversation ? {
309
- conversationId: message.conversation.id,
310
- conversationType: message.conversation.type,
311
- } : {}),
312
- },
313
- },
314
- );
315
- threadId = newThread.id;
316
- if (binding && binding.id !== "fallback") {
317
- await this.bindingRegistry.update(binding.id, { threadId });
318
- binding.threadId = threadId;
319
- } else if (binding) {
320
- binding.threadId = threadId;
321
- }
322
- }
323
-
324
- console.log({
325
- event: "dispatch:agent",
326
- agentId,
327
- threadId,
328
- senderId: message.sender.id,
329
- contentLength: message.content.text.length,
330
- }, "Dispatching to agent");
331
-
332
- const agent = agentInstanceManager.getAgent({
333
- tenant_id: tenantId!,
334
- assistant_id: agentId,
335
- thread_id: threadId,
336
- workspace_id: binding?.workspaceId || "",
337
- project_id: binding?.projectId || "",
338
- });
339
-
340
- /**
341
- * Subscribe to reply:ready before addMessage to avoid a race with
342
- * an already-running queue processor.
343
- *
344
- * The `_replySubs` map uses a counter (not a boolean) so that multiple
345
- * concurrent dispatches on the same thread each get a callback invocation.
346
- * The `subscribeOnce` listener is only registered when the counter goes
347
- * from 0 → 1, and removed when it goes back to 0. A 1-hour timeout
348
- * protects against leaked subscriptions due to agent errors/aborts.
349
- */
350
- if (message.replyTarget) {
351
- const replySubKey = `${threadId}:${message.replyTarget.adapterChannel}:reply`;
352
- const adapter = this.adapterRegistry.get(message.replyTarget.adapterChannel);
353
- if (adapter) {
354
- const installation = await this.installationStore.getInstallationById(
355
- message.channelInstallationId,
356
- );
357
- if (installation) {
358
- const existing = this._replySubs.get(replySubKey);
359
- if (!existing || existing.count === 0) {
360
- // First subscriber for this thread+channel — register the event listener
361
- const timer = setTimeout(() => {
362
- // Cleanup if reply never fires (agent aborted, errored, etc.)
363
- const entry = this._replySubs.get(replySubKey);
364
- if (entry) {
365
- this._replySubs.delete(replySubKey);
366
- console.warn({
367
- event: "dispatch:reply:timeout",
368
- threadId,
369
- channel: message.replyTarget!.adapterChannel,
370
- }, "Reply subscription timed out — no reply:ready fired within 1h");
371
- }
372
- }, 3_600_000);
373
-
374
- this._replySubs.set(replySubKey, { count: 1, timer });
375
-
376
- console.log({
377
- event: "dispatch:reply:subscribed",
378
- threadId,
379
- channel: message.replyTarget.adapterChannel,
380
- senderId: message.sender.id,
381
- }, "Subscribed to reply:ready for thread");
382
-
383
- agent.subscribeOnce('reply:ready', (data: any) => {
384
- const messages = data.state?.values?.messages;
385
- const lastAI = messages
386
- ?.filter((m: any) => m.type === 'ai' || m.getType?.() === 'ai')
387
- .pop();
388
- const replyText = lastAI?.content ?? '';
389
-
390
- // Decrement counter; unsubscribe only when count reaches 0
391
- const entry = this._replySubs.get(replySubKey);
392
- if (entry) {
393
- entry.count--;
394
- if (entry.count <= 0) {
395
- clearTimeout(entry.timer);
396
- this._replySubs.delete(replySubKey);
397
- }
398
- }
399
-
400
- if (replyText) {
401
- console.log({
402
- event: "dispatch:reply:sending",
403
- threadId,
404
- channel: message.replyTarget!.adapterChannel,
405
- replyLength: replyText.length,
406
- }, "Sending channel reply");
407
- adapter.sendReply(message.replyTarget!, { text: replyText }, installation)
408
- .then(() => {
409
- console.log({
410
- event: "dispatch:reply:sent",
411
- threadId,
412
- channel: message.replyTarget!.adapterChannel,
413
- }, "Channel reply sent successfully");
414
- })
415
- .catch((err) => console.error({
416
- event: "dispatch:reply:failed",
417
- threadId,
418
- channel: message.replyTarget!.adapterChannel,
419
- error: err instanceof Error ? err.message : String(err),
420
- }, "Failed to send channel reply"));
421
- } else {
422
- console.warn({
423
- event: "dispatch:reply:empty",
424
- threadId,
425
- channel: message.replyTarget!.adapterChannel,
426
- }, "Agent produced no text output — skipping reply");
427
- }
428
- });
429
- } else {
430
- // Another dispatch already subscribed — increment counter
431
- existing.count++;
432
- console.log({
433
- event: "dispatch:reply:incremented",
434
- threadId,
435
- channel: message.replyTarget.adapterChannel,
436
- count: existing.count,
437
- }, "Incremented reply counter for thread (already subscribed)");
438
- }
439
- }
440
- }
441
- }
442
-
443
- const addResult = await agent.addMessage({
444
- input: { message: message.content.text },
445
- custom_run_config: message.replyTarget
446
- ? { ...(message.content.metadata || {}), _replyTarget: message.replyTarget }
447
- : (message.content.metadata || {}),
448
- });
449
-
450
- console.log({
451
- event: "dispatch:complete",
452
- agentId,
453
- threadId,
454
- messageId: (addResult as Record<string, unknown>)?.messageId,
455
- result: JSON.stringify(addResult),
456
- }, "Agent dispatch complete — messageId = " + ((addResult as Record<string, unknown>)?.messageId || "N/A"));
457
- });
458
-
459
- return {
460
- success: true,
461
- bindingId: ctx.binding?.id,
462
- threadId,
463
- result: ctx.result,
464
- };
465
- } catch (error) {
466
- ctx.error = error instanceof Error ? error : new Error(String(error));
467
- console.error({ event: "dispatch:error", error: ctx.error.message, channel: message.channel, senderId: message.sender.id }, "Message dispatch failed");
468
- return {
469
- success: false,
470
- bindingId: ctx.binding?.id,
471
- threadId: ctx.binding?.threadId,
472
- error: ctx.error,
473
- };
474
- }
475
- }
476
-
477
- private async runMiddlewares(
478
- ctx: MessageContext,
479
- finalHandler: () => Promise<void>,
480
- ): Promise<void> {
481
- const dispatch = (index: number): Promise<void> => {
482
- if (index >= this.middlewares.length) {
483
- return finalHandler();
484
- }
485
- const middleware = this.middlewares[index];
486
- return middleware(ctx, () => dispatch(index + 1));
487
- };
488
- return dispatch(0);
489
- }
490
- }