@axiom-lattice/core 2.1.75 → 2.1.77
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/README.md +340 -99
- package/dist/index.d.mts +558 -52
- package/dist/index.d.ts +558 -52
- package/dist/index.js +470 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +468 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,149 +1,390 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @axiom-lattice/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Core agent framework providing lattice managers for models, tools, agents, memory, stores, and more.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Store Configuration
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 工具格子管理 (ToolLatticeManager)
|
|
9
|
-
- 代理格子管理 (AgentLatticeManager)
|
|
10
|
-
- 记忆格子管理 (MemoryLatticeManager)
|
|
11
|
-
- 流式缓冲管理 (ChunkBufferLatticeManager)
|
|
12
|
-
- DeepAgent 实现
|
|
13
|
-
- 工具类和辅助函数
|
|
7
|
+
### `configureStores`
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
Unified store registration that replaces the manual `new → initialize → remove → register` boilerplate.
|
|
10
|
+
Handles `StoreLatticeManager`, `ScheduleLatticeManager`, and `MemoryLatticeManager` through a single call.
|
|
16
11
|
|
|
17
|
-
```
|
|
18
|
-
|
|
12
|
+
```typescript
|
|
13
|
+
import { configureStores } from "@axiom-lattice/core";
|
|
14
|
+
import { createPgStoreConfig } from "@axiom-lattice/pg-stores";
|
|
15
|
+
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
|
|
16
|
+
|
|
17
|
+
// One connection string creates all PG store instances
|
|
18
|
+
const stores = createPgStoreConfig(process.env.DATABASE_URL);
|
|
19
|
+
|
|
20
|
+
// One call registers all stores across three managers
|
|
21
|
+
await configureStores({
|
|
22
|
+
...stores,
|
|
23
|
+
checkpoint: PostgresSaver.fromConnString(process.env.DATABASE_URL),
|
|
24
|
+
}, { autoDisposeStores: true });
|
|
19
25
|
```
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
### Supported store keys
|
|
28
|
+
|
|
29
|
+
| Key | Registered via |
|
|
30
|
+
|-----|---------------|
|
|
31
|
+
| `thread`, `assistant`, `workspace`, `project`, `database`, `metrics`, `mcp`, `user`, `tenant`, `userTenantLink`, `threadMessageQueue`, `workflowTracking`, `eval`, `channelInstallation`, `channelBinding`, `skill` | `StoreLatticeManager` |
|
|
32
|
+
| `schedule` | `ScheduleLatticeManager` (auto-configured as POSTGRES) |
|
|
33
|
+
| `checkpoint` | `MemoryLatticeManager` (accepts any CheckpointSaver) |
|
|
34
|
+
|
|
35
|
+
### Behavior
|
|
36
|
+
|
|
37
|
+
For each store entry:
|
|
38
|
+
|
|
39
|
+
1. Calls `store.initialize()` if the method exists (skipped if it requires arguments)
|
|
40
|
+
2. Removes any existing `"default"` registration
|
|
41
|
+
3. Registers the new store under `"default"`
|
|
42
|
+
4. Tracks `store.dispose` for cleanup
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
### Options
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface ConfigureStoresOptions {
|
|
48
|
+
autoDisposeStores?: boolean; // Register SIGINT/SIGTERM handlers for cleanup
|
|
49
|
+
customStores?: Record<string, object>; // Register custom store types
|
|
50
|
+
}
|
|
25
51
|
```
|
|
26
52
|
|
|
27
|
-
|
|
53
|
+
Returns a `dispose` function for manual cleanup regardless of `autoDisposeStores`.
|
|
54
|
+
|
|
55
|
+
### Custom stores
|
|
56
|
+
|
|
57
|
+
Register custom store implementations that follow the same `initialize()` / `dispose()` pattern:
|
|
28
58
|
|
|
29
|
-
```
|
|
30
|
-
|
|
59
|
+
```ts
|
|
60
|
+
interface MyCustomStore {
|
|
61
|
+
getData(): Promise<string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class PostgreSQLMyCustomStore implements MyCustomStore {
|
|
65
|
+
async initialize() { /* run migrations */ }
|
|
66
|
+
async dispose() { /* close pool */ }
|
|
67
|
+
async getData() { return "from pg"; }
|
|
68
|
+
}
|
|
31
69
|
```
|
|
32
70
|
|
|
33
|
-
|
|
71
|
+
```ts
|
|
72
|
+
await configureStores({
|
|
73
|
+
thread: new PostgreSQLThreadStore(opts),
|
|
74
|
+
}, {
|
|
75
|
+
customStores: {
|
|
76
|
+
myCustom: new PostgreSQLMyCustomStore({ poolConfig }),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
34
79
|
|
|
35
|
-
|
|
80
|
+
// Consume via StoreLatticeManager
|
|
81
|
+
const { store } = getStoreLattice("default", "myCustom");
|
|
82
|
+
const customStore = store as MyCustomStore;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Consumer pattern
|
|
86
|
+
|
|
87
|
+
Components that need stores read them directly from `StoreLatticeManager` — no `setConfigStore` required:
|
|
36
88
|
|
|
37
89
|
```typescript
|
|
38
|
-
|
|
90
|
+
// SqlDatabaseManager reads database configs lazily from the store lattice
|
|
91
|
+
const db = await sqlDatabaseManager.getDatabase(tenantId, "main-db");
|
|
39
92
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
93
|
+
// MetricsServerManager loads configs on first access
|
|
94
|
+
const client = await metricsServerManager.getClient(tenantId, "prometheus");
|
|
95
|
+
const servers = await metricsServerManager.getServerKeys(tenantId);
|
|
42
96
|
```
|
|
43
97
|
|
|
44
|
-
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Other Lattice Managers
|
|
101
|
+
|
|
102
|
+
### Model Lattice
|
|
45
103
|
|
|
46
104
|
```typescript
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
ttl: 30 * 60 * 1000, // 30 minutes
|
|
55
|
-
cleanupInterval: 5 * 60 * 1000 // Clean every 5 minutes (optional)
|
|
105
|
+
import { registerModelLattice } from "@axiom-lattice/core";
|
|
106
|
+
|
|
107
|
+
registerModelLattice("default", {
|
|
108
|
+
model: "gpt-4o",
|
|
109
|
+
provider: "openai",
|
|
110
|
+
streaming: true,
|
|
111
|
+
apiKeyEnvName: "OPENAI_API_KEY",
|
|
56
112
|
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### ChunkBuffer
|
|
57
116
|
|
|
58
|
-
|
|
59
|
-
|
|
117
|
+
```typescript
|
|
118
|
+
import { InMemoryChunkBuffer, registerChunkBuffer } from "@axiom-lattice/core";
|
|
119
|
+
|
|
120
|
+
const buffer = new InMemoryChunkBuffer({ ttl: 30 * 60 * 1000 });
|
|
121
|
+
registerChunkBuffer("default", buffer);
|
|
122
|
+
```
|
|
60
123
|
|
|
61
|
-
|
|
62
|
-
await buffer.addChunk('thread-123', 'msg-1', 'Hello ');
|
|
63
|
-
await buffer.addChunk('thread-123', 'msg-1', 'world!');
|
|
64
|
-
await buffer.addChunk('thread-123', 'msg-2', ' How are you?');
|
|
124
|
+
## Directory Structure
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
126
|
+
- `src/store_lattice/` — Store lattice manager + configureStores
|
|
127
|
+
- `src/model_lattice/` — LLM provider abstractions
|
|
128
|
+
- `src/tool_lattice/` — Tool implementations (SQL, metrics, etc.)
|
|
129
|
+
- `src/agent_lattice/` — Agent definitions and builders
|
|
130
|
+
- `src/memory_lattice/` — Context/memory management
|
|
131
|
+
- `src/schedule_lattice/` — Scheduled task management
|
|
132
|
+
- `src/sandbox_lattice/` — Code execution sandbox providers
|
|
69
133
|
|
|
70
|
-
|
|
71
|
-
const isActive = await buffer.isThreadActive('thread-123'); // true
|
|
134
|
+
---
|
|
72
135
|
|
|
73
|
-
|
|
74
|
-
await buffer.completeThread('thread-123');
|
|
136
|
+
## Custom Middleware Registry
|
|
75
137
|
|
|
76
|
-
|
|
77
|
-
const stats = buffer.getStats();
|
|
78
|
-
console.log(stats);
|
|
138
|
+
Applications can write their own middleware and register it at runtime without modifying core source code. The registered middleware can then be enabled via agent config stored in the database.
|
|
79
139
|
|
|
80
|
-
|
|
81
|
-
|
|
140
|
+
### Quick start
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
|
|
144
|
+
|
|
145
|
+
CustomMiddlewareRegistry.register("my-logger", (config) =>
|
|
146
|
+
createMiddleware({
|
|
147
|
+
name: "MyLogger",
|
|
148
|
+
wrapModelCall: async (request, handler) => {
|
|
149
|
+
console.log(`[${config.logLevel}] Model call`);
|
|
150
|
+
return handler(request);
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
);
|
|
82
154
|
```
|
|
83
155
|
|
|
84
|
-
|
|
156
|
+
### Database config format
|
|
157
|
+
|
|
158
|
+
In your agent's `graphDefinition.middleware` array, add a `"custom"` type entry:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"id": "mw-001",
|
|
163
|
+
"type": "custom",
|
|
164
|
+
"name": "Audit Logger",
|
|
165
|
+
"description": "Logs model calls for audit",
|
|
166
|
+
"enabled": true,
|
|
167
|
+
"config": {
|
|
168
|
+
"key": "my-logger",
|
|
169
|
+
"logLevel": "debug"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `config.key` must match the key passed to `register()`. All other config fields are forwarded to your factory function.
|
|
85
175
|
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
176
|
+
### Full example — permission-check middleware
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
|
|
180
|
+
|
|
181
|
+
CustomMiddlewareRegistry.register("permission-check", (config) =>
|
|
182
|
+
createMiddleware({
|
|
183
|
+
name: "PermissionCheck",
|
|
184
|
+
wrapToolCall: async (request, handler) => {
|
|
185
|
+
const toolName = request.toolCall?.name;
|
|
186
|
+
if (toolName && !config.allowedTools.includes(toolName)) {
|
|
187
|
+
return { content: `Tool "${toolName}" is not permitted.` };
|
|
188
|
+
}
|
|
189
|
+
return handler(request);
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Database config:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"id": "perm-001",
|
|
200
|
+
"type": "custom",
|
|
201
|
+
"name": "Tool Permissions",
|
|
202
|
+
"description": "Restrict tool access",
|
|
203
|
+
"enabled": true,
|
|
204
|
+
"config": {
|
|
205
|
+
"key": "permission-check",
|
|
206
|
+
"allowedTools": ["read_file", "write_file"]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
94
210
|
|
|
95
|
-
|
|
211
|
+
### API reference
|
|
96
212
|
|
|
97
|
-
|
|
213
|
+
| Method | Signature | Description |
|
|
214
|
+
|--------|-----------|-------------|
|
|
215
|
+
| `register` | `(key: string, factory: (config: Record<string, any>) => AgentMiddleware \| Promise<AgentMiddleware>) => void` | Register a factory by key |
|
|
216
|
+
| `unregister` | `(key: string) => boolean` | Remove a registration |
|
|
217
|
+
| `get` | `(key: string) => AgentMiddlewareFactory \| undefined` | Look up a factory |
|
|
218
|
+
| `has` | `(key: string) => boolean` | Check if key is registered |
|
|
219
|
+
| `list` | `() => string[]` | Get all registered keys |
|
|
98
220
|
|
|
99
|
-
###
|
|
221
|
+
### Important
|
|
100
222
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- 懒清理(Lazy Cleanup): 访问时自动清除过期 thread
|
|
106
|
-
- 可选周期清理: 后台定时器定期清理过期 thread
|
|
107
|
-
5. **TTL Auto-Extension**: 有新 chunk 加入时自动延长 TTL
|
|
223
|
+
- Register factories **before** building agents (typically at app startup)
|
|
224
|
+
- Duplicate keys overwrite previous registrations
|
|
225
|
+
- Unregistered keys are skipped with a console warning at build time
|
|
226
|
+
- Factory functions receive the raw `config` object (minus `key`) — validate it yourself
|
|
108
227
|
|
|
109
|
-
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## External Channel Integration
|
|
231
|
+
|
|
232
|
+
core provides the storage infrastructure and binding system that external channel adapters (Lark, Slack, Email, etc.) depend on. The actual HTTP routes and message dispatch live in `packages/gateway`, but all persistence and sender resolution are handled here.
|
|
233
|
+
|
|
234
|
+
### What core provides
|
|
235
|
+
|
|
236
|
+
| Component | Location | Purpose |
|
|
237
|
+
|-----------|----------|---------|
|
|
238
|
+
| `InMemoryChannelInstallationStore` | `store_lattice/` | In-memory `ChannelInstallationStore` implementation |
|
|
239
|
+
| `InMemoryBindingStore` | `store_lattice/` | In-memory `BindingRegistry` implementation |
|
|
240
|
+
| `BindingRegistryHolder` | `bindings_lattice/` | Global accessor for the active `BindingRegistry` |
|
|
241
|
+
| `manage_binding` tool | `tool_lattice/manage_binding/` | Agent tool for CRUD on sender-to-agent bindings |
|
|
242
|
+
|
|
243
|
+
### Architecture overview
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
External Platform (Lark, Slack, Email)
|
|
247
|
+
→ Gateway HTTP route (packages/gateway)
|
|
248
|
+
→ ChannelAdapter.receive(rawPayload) → InboundMessage
|
|
249
|
+
→ MessageRouter.dispatch() (packages/gateway)
|
|
250
|
+
→ BindingRegistry.resolve() ←── core
|
|
251
|
+
→ Thread creation / reuse ←── core
|
|
252
|
+
→ Agent.addMessage() ←── core
|
|
253
|
+
→ ChannelAdapter.sendReply() (packages/gateway)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Agents remain **channel-agnostic** — they only see standard thread/message execution.
|
|
257
|
+
|
|
258
|
+
### Setup for custom channel development
|
|
259
|
+
|
|
260
|
+
#### 1. Register channel stores
|
|
110
261
|
|
|
111
262
|
```typescript
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
getChunks(threadId) // Get all chunks for a thread
|
|
115
|
-
getAccumulatedContent(threadId) // Get concatenated content
|
|
116
|
-
getChunksByMessageId(threadId, messageId) // Get chunks for specific message
|
|
263
|
+
import { configureStores } from "@axiom-lattice/core";
|
|
264
|
+
import { PostgreSQLChannelInstallationStore, PostgreSQLBindingStore } from "@axiom-lattice/pg-stores";
|
|
117
265
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
266
|
+
await configureStores({
|
|
267
|
+
channelInstallation: new PostgreSQLChannelInstallationStore({ pool }),
|
|
268
|
+
channelBinding: new PostgreSQLBindingStore({ pool }),
|
|
269
|
+
// ... other stores
|
|
270
|
+
});
|
|
271
|
+
```
|
|
123
272
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
273
|
+
#### 2. Set the global BindingRegistry
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { setBindingRegistry } from "@axiom-lattice/core";
|
|
128
277
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
getAllThreads() // Get all thread IDs
|
|
132
|
-
getStats() // Get buffer statistics
|
|
278
|
+
const bindingStore = getStoreLattice("default", "channelBinding").store;
|
|
279
|
+
setBindingRegistry(bindingStore);
|
|
133
280
|
```
|
|
134
281
|
|
|
135
|
-
|
|
282
|
+
#### 3. Implement ChannelAdapter (in gateway)
|
|
136
283
|
|
|
137
284
|
```typescript
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
285
|
+
import type { ChannelAdapter, InboundMessage, OutboundMessage, ReplyTarget } from "@axiom-lattice/protocols";
|
|
286
|
+
import { z } from "zod";
|
|
287
|
+
|
|
288
|
+
const slackConfigSchema = z.object({
|
|
289
|
+
botToken: z.string(),
|
|
290
|
+
signingSecret: z.string(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export const slackAdapter: ChannelAdapter = {
|
|
294
|
+
channel: "slack",
|
|
295
|
+
configSchema: slackConfigSchema,
|
|
296
|
+
|
|
297
|
+
async receive(rawPayload, installation): Promise<InboundMessage | null> {
|
|
298
|
+
// 1. Parse Slack-specific event
|
|
299
|
+
// 2. Return normalized InboundMessage or null to ignore
|
|
300
|
+
return {
|
|
301
|
+
channel: "slack",
|
|
302
|
+
channelInstallationId: installation.id,
|
|
303
|
+
tenantId: installation.tenantId,
|
|
304
|
+
sender: { id: userId, displayName: userName },
|
|
305
|
+
content: { text: messageText },
|
|
306
|
+
replyTarget: {
|
|
307
|
+
adapterChannel: "slack",
|
|
308
|
+
channelInstallationId: installation.id,
|
|
309
|
+
rawTarget: { channelId, threadTs },
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async sendReply(replyTarget, message, installation): Promise<void> {
|
|
315
|
+
// Use Slack API to send reply
|
|
316
|
+
// replyTarget.rawTarget contains channel-specific context
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### 4. Register routes and adapter (in gateway)
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// packages/gateway/src/channels/registry.ts
|
|
325
|
+
adapterRegistry.register(slackAdapter);
|
|
326
|
+
|
|
327
|
+
// packages/gateway/src/channels/routes.ts
|
|
328
|
+
app.post("/api/channels/slack/installations/:id/events", async (req, reply) => {
|
|
329
|
+
const installation = await installationStore.getInstallationById(req.params.id);
|
|
330
|
+
const message = await slackAdapter.receive(req.body, installation);
|
|
331
|
+
if (message) await messageRouter.dispatch(message);
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Binding resolution flow
|
|
336
|
+
|
|
337
|
+
When `MessageRouter.dispatch()` receives an `InboundMessage`:
|
|
338
|
+
|
|
339
|
+
1. Calls `BindingRegistry.resolve({ channel, senderId, channelInstallationId, tenantId })`
|
|
340
|
+
2. If **no binding found**:
|
|
341
|
+
- If `installation.rejectWhenNoBinding` → throw `BindingNotFoundError`
|
|
342
|
+
- If `installation.fallbackAgentId` → create temporary fallback binding
|
|
343
|
+
3. If **binding disabled** → reject
|
|
344
|
+
4. **Thread resolution** (based on `binding.threadMode`):
|
|
345
|
+
- `"fixed"` → reuse `binding.threadId`
|
|
346
|
+
- `"per_conversation"` → always create new thread
|
|
347
|
+
5. Execute agent via `agent.addMessage()`
|
|
348
|
+
|
|
349
|
+
### Agent-side binding management
|
|
350
|
+
|
|
351
|
+
Agents can manage bindings dynamically via the `manage_binding` tool:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Agent can call this tool to:
|
|
355
|
+
// - list_installations: List available channel installations
|
|
356
|
+
// - create: Bind a sender to an agent (channel, senderId, agentId)
|
|
357
|
+
// - update: Change agent or threadMode for a binding
|
|
358
|
+
// - delete: Remove a binding
|
|
359
|
+
// - list: List all bindings with optional filters
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Key types (from @axiom-lattice/protocols)
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
interface ChannelAdapter<TConfig = unknown> {
|
|
366
|
+
readonly channel: string;
|
|
367
|
+
readonly configSchema: z.ZodSchema<TConfig>;
|
|
368
|
+
receive(rawPayload: unknown, installation: ChannelInstallation): Promise<InboundMessage | null>;
|
|
369
|
+
sendReply(replyTarget: ReplyTarget, message: OutboundMessage, installation: ChannelInstallation): Promise<void>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interface Binding {
|
|
373
|
+
id: string;
|
|
374
|
+
channel: string;
|
|
375
|
+
channelInstallationId: string;
|
|
376
|
+
tenantId: string;
|
|
377
|
+
senderId: string;
|
|
378
|
+
agentId: string;
|
|
379
|
+
threadMode: "fixed" | "per_conversation";
|
|
380
|
+
enabled: boolean;
|
|
141
381
|
}
|
|
142
382
|
```
|
|
143
383
|
|
|
144
|
-
###
|
|
384
|
+
### Reference implementation
|
|
145
385
|
|
|
146
|
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
386
|
+
See `packages/gateway/src/channels/lark/` for a complete production channel adapter including:
|
|
387
|
+
- Webhook verification and decryption
|
|
388
|
+
- Event parsing and normalization
|
|
389
|
+
- Thread mapping (`user` / `group` / `hybrid` modes)
|
|
390
|
+
- Reply delivery via official SDK
|