@axiom-lattice/gateway 2.1.97 → 2.1.99

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.
@@ -0,0 +1,162 @@
1
+ import { randomBytes } from "crypto";
2
+
3
+ const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
+
5
+ function getBaseUrl(): string {
6
+ return process.env.WECHAT_ILINK_BASE_URL || DEFAULT_BASE_URL;
7
+ }
8
+
9
+ function generateWechatUin(): string {
10
+ const buf = Buffer.alloc(4);
11
+ buf.writeUInt32BE(Math.floor(Math.random() * 0xffffffff), 0);
12
+ return buf.toString("base64url");
13
+ }
14
+
15
+ async function apiGet<T>(path: string, extraHeaders?: Record<string, string>): Promise<T> {
16
+ const url = `${getBaseUrl()}${path}`;
17
+ const headers: Record<string, string> = { ...extraHeaders };
18
+ const res = await fetch(url, { headers });
19
+ if (!res.ok) {
20
+ throw new Error(`iLink GET ${path} failed: ${res.status} ${res.statusText}`);
21
+ }
22
+ return res.json() as Promise<T>;
23
+ }
24
+
25
+ async function apiGetAuth<T>(path: string, botToken: string, extraHeaders?: Record<string, string>): Promise<T> {
26
+ const url = `${getBaseUrl()}${path}`;
27
+ const headers: Record<string, string> = {
28
+ "AuthorizationType": "ilink_bot_token",
29
+ "X-WECHAT-UIN": generateWechatUin(),
30
+ "Authorization": `Bearer ${botToken}`,
31
+ ...extraHeaders,
32
+ };
33
+ const res = await fetch(url, { headers });
34
+ if (!res.ok) {
35
+ throw new Error(`iLink GET ${path} failed: ${res.status} ${res.statusText}`);
36
+ }
37
+ return res.json() as Promise<T>;
38
+ }
39
+
40
+ async function apiPost<T>(path: string, body: unknown, botToken: string, signal?: AbortSignal): Promise<T> {
41
+ const url = `${getBaseUrl()}${path}`;
42
+ const headers: Record<string, string> = {
43
+ "Content-Type": "application/json",
44
+ "AuthorizationType": "ilink_bot_token",
45
+ "X-WECHAT-UIN": generateWechatUin(),
46
+ "Authorization": `Bearer ${botToken}`,
47
+ };
48
+
49
+ const res = await fetch(url, {
50
+ method: "POST",
51
+ headers,
52
+ body: JSON.stringify(body),
53
+ signal,
54
+ });
55
+ if (!res.ok) {
56
+ throw new Error(`iLink POST ${path} failed: ${res.status} ${res.statusText}`);
57
+ }
58
+ return res.json() as Promise<T>;
59
+ }
60
+
61
+ export async function getUpdates(
62
+ botToken: string,
63
+ syncBuffer: string,
64
+ signal?: AbortSignal,
65
+ ): Promise<{ msgs: Array<{
66
+ message_type?: number;
67
+ from_user_id?: string;
68
+ client_id?: string;
69
+ item_list?: Array<{ type?: number; text_item?: { text?: string } }>;
70
+ context_token?: string;
71
+ create_time_ms?: number;
72
+ }>; syncBuffer: string }> {
73
+ const data = await apiPost<{
74
+ ret?: number;
75
+ errcode?: number;
76
+ errmsg?: string;
77
+ msgs?: Array<{
78
+ message_type?: number;
79
+ from_user_id?: string;
80
+ client_id?: string;
81
+ item_list?: Array<{ type?: number; text_item?: { text?: string } }>;
82
+ context_token?: string;
83
+ create_time_ms?: number;
84
+ }>;
85
+ get_updates_buf?: string;
86
+ }>("/ilink/bot/getupdates", {
87
+ get_updates_buf: syncBuffer,
88
+ base_info: { channel_version: "1.0.2" },
89
+ }, botToken, signal);
90
+
91
+ // Check API-level error codes (HTTP 200 can still be an error)
92
+ if ((data.ret !== undefined && data.ret !== 0) || (data.errcode !== undefined && data.errcode !== 0)) {
93
+ throw new Error(`iLink getUpdates error: ret=${data.ret} errcode=${data.errcode} errmsg=${data.errmsg ?? ""}`);
94
+ }
95
+
96
+ return {
97
+ msgs: data.msgs ?? [],
98
+ syncBuffer: data.get_updates_buf ?? syncBuffer,
99
+ };
100
+ }
101
+
102
+ function generateClientId(): string {
103
+ return `axiom-wechat:${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
104
+ }
105
+
106
+ export async function sendMessage(
107
+ botToken: string,
108
+ toUserId: string,
109
+ text: string,
110
+ contextToken: string,
111
+ ): Promise<void> {
112
+ await apiPost<unknown>("/ilink/bot/sendmessage", {
113
+ msg: {
114
+ from_user_id: "",
115
+ to_user_id: toUserId,
116
+ client_id: generateClientId(),
117
+ message_type: 2, // MSG_TYPE_BOT
118
+ message_state: 2, // MSG_STATE_FINISH
119
+ item_list: [{ type: 1, text_item: { text } }],
120
+ context_token: contextToken,
121
+ },
122
+ base_info: { channel_version: "1.0.2" },
123
+ }, botToken);
124
+ }
125
+
126
+ export async function getQrCode(): Promise<{ qrcode: string; qrcodeImgUrl: string }> {
127
+ const data = await apiGet<{ qrcode?: string; qrcode_img_content?: string }>(
128
+ "/ilink/bot/get_bot_qrcode?bot_type=3",
129
+ );
130
+ if (!data.qrcode || !data.qrcode_img_content) {
131
+ throw new Error("Failed to get WeChat QR code");
132
+ }
133
+ return { qrcode: data.qrcode, qrcodeImgUrl: data.qrcode_img_content };
134
+ }
135
+
136
+ export async function getQrCodeStatus(
137
+ qrcode: string,
138
+ signal?: AbortSignal,
139
+ ): Promise<{
140
+ status: "wait" | "scaned" | "confirmed" | "expired";
141
+ botToken?: string;
142
+ uin?: string;
143
+ botId?: string;
144
+ baseUrl?: string;
145
+ }> {
146
+ const data = await apiGet<{
147
+ status?: string;
148
+ bot_token?: string;
149
+ ilink_bot_id?: string;
150
+ ilink_user_id?: string;
151
+ baseurl?: string;
152
+ }>(`/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, {
153
+ "iLink-App-ClientVersion": "1",
154
+ });
155
+ return {
156
+ status: (data.status as "wait" | "scaned" | "confirmed" | "expired") || "wait",
157
+ botToken: data.bot_token,
158
+ uin: data.ilink_user_id,
159
+ botId: data.ilink_bot_id,
160
+ baseUrl: data.baseurl,
161
+ };
162
+ }
@@ -16,7 +16,7 @@ jest.mock("@axiom-lattice/core", () => ({
16
16
  }));
17
17
 
18
18
  describe("recoverRun", () => {
19
- let recoverRun: Function;
19
+ let recoverRun: (...args: unknown[]) => unknown;
20
20
 
21
21
  beforeEach(async () => {
22
22
  jest.clearAllMocks();
@@ -19,12 +19,12 @@ jest.mock('@axiom-lattice/core', () => ({
19
19
  }));
20
20
 
21
21
  describe('Tasks Controller', () => {
22
- let listTasks: Function;
23
- let getTask: Function;
24
- let createTask: Function;
25
- let updateTask: Function;
26
- let deleteTask: Function;
27
- let completeTask: Function;
22
+ let listTasks: (...args: unknown[]) => unknown;
23
+ let getTask: (...args: unknown[]) => unknown;
24
+ let createTask: (...args: unknown[]) => unknown;
25
+ let updateTask: (...args: unknown[]) => unknown;
26
+ let deleteTask: (...args: unknown[]) => unknown;
27
+ let completeTask: (...args: unknown[]) => unknown;
28
28
 
29
29
  const mockReply = () =>
30
30
  ({
@@ -7,6 +7,23 @@ import {
7
7
  UpdateChannelInstallationRequest,
8
8
  ChannelInstallationType,
9
9
  } from "@axiom-lattice/protocols";
10
+ import type { ChannelAdapterRegistry } from "../channels/registry";
11
+
12
+ /**
13
+ * Controller-scoped deps for channel lifecycle management.
14
+ * Set by the gateway during route registration so that
15
+ * enable/disable/deletion can start/stop live connections.
16
+ */
17
+ let _adapterRegistry: ChannelAdapterRegistry | undefined;
18
+ let _router: { dispatch(msg: unknown): Promise<unknown> } | undefined;
19
+
20
+ export function setChannelControllerDeps(deps: {
21
+ adapterRegistry: ChannelAdapterRegistry;
22
+ router: { dispatch(msg: unknown): Promise<unknown> };
23
+ }): void {
24
+ _adapterRegistry = deps.adapterRegistry;
25
+ _router = deps.router;
26
+ }
10
27
 
11
28
  /**
12
29
  * Channel Installation Controller
@@ -212,6 +229,18 @@ export async function createChannelInstallation(
212
229
  body,
213
230
  );
214
231
 
232
+ // Start live connection for channels that need it (e.g. WeChat polling, Lark WS)
233
+ if (body.enabled !== false && _adapterRegistry) {
234
+ const adapter = _adapterRegistry.get(body.channel);
235
+ if (adapter?.connect) {
236
+ try {
237
+ await adapter.connect(installation, { router: _router });
238
+ } catch (err) {
239
+ console.error("Failed to start channel connection after creation:", err);
240
+ }
241
+ }
242
+ }
243
+
215
244
  reply.code(201);
216
245
  return {
217
246
  success: true,
@@ -286,6 +315,17 @@ export async function updateChannelInstallation(
286
315
  };
287
316
  }
288
317
 
318
+ // Handle enable/disable — start/stop live connections
319
+ if (body.enabled !== undefined && body.enabled !== existing.enabled) {
320
+ const adapter = _adapterRegistry?.get(existing.channel);
321
+ if (body.enabled && adapter?.connect) {
322
+ // Re-connect: pass router deps so the adapter can dispatch messages
323
+ await adapter.connect(installation, { router: _router });
324
+ } else if (!body.enabled && adapter?.disconnect) {
325
+ await adapter.disconnect(installationId);
326
+ }
327
+ }
328
+
289
329
  return {
290
330
  success: true,
291
331
  message: "Channel installation updated successfully",
@@ -333,6 +373,18 @@ export async function deleteChannelInstallation(
333
373
  };
334
374
  }
335
375
 
376
+ // Disconnect live connection before removing from DB
377
+ if (_adapterRegistry) {
378
+ const adapter = _adapterRegistry.get(existing.channel);
379
+ if (adapter?.disconnect) {
380
+ try {
381
+ await adapter.disconnect(installationId);
382
+ } catch (err) {
383
+ console.error("Failed to disconnect channel adapter:", err);
384
+ }
385
+ }
386
+ }
387
+
336
388
  const deleted = await store.deleteInstallation(tenantId, installationId);
337
389
 
338
390
  if (!deleted) {
@@ -374,7 +374,7 @@ export async function testDatabaseConnection(
374
374
  try {
375
375
  await db.disconnect();
376
376
  await sqlDatabaseManager.removeDatabase(tenantId, testKey);
377
- } catch {}
377
+ } catch { /* cleanup error, ignore */ }
378
378
 
379
379
  return {
380
380
  success: true,
@@ -556,7 +556,7 @@ export async function testMetricsServerConnection(
556
556
  // Cleanup on error
557
557
  try {
558
558
  metricsServerManager.removeServer(tenantId, testKey);
559
- } catch {}
559
+ } catch { /* cleanup error, ignore */ }
560
560
 
561
561
  return {
562
562
  success: true,
@@ -461,7 +461,7 @@ export class WorkspaceController {
461
461
  const inferredContentType = this.getMimeType(filename);
462
462
 
463
463
  try {
464
- let contentType = inferredContentType;
464
+ const contentType = inferredContentType;
465
465
 
466
466
  // Inject AI2APP context script for HTML files (sandbox storage)
467
467
  const isHtml = contentType?.toLowerCase().includes("text/html") ||
@@ -635,7 +635,7 @@ export class WorkspaceController {
635
635
  ? pathEntry
636
636
  : undefined;
637
637
 
638
- if (pathValue && !/^[a-zA-Z0-9_./~\-]+$/.test(pathValue)) {
638
+ if (pathValue && !/^[a-zA-Z0-9_./~-]+$/.test(pathValue)) {
639
639
  return reply
640
640
  .status(400)
641
641
  .send({ success: false, error: "Invalid path parameter" });
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { createDeduplicationMiddleware, createRateLimitMiddleware, createAuditLo
14
14
  import { setBindingRegistry, setMenuRegistry } from "@axiom-lattice/core";
15
15
  import { larkChannelAdapter } from "./channels/lark/LarkChannelAdapter";
16
16
  import { extractUserFromAuthHeader } from "./controllers/auth";
17
+ import { setChannelControllerDeps } from "./controllers/channel-installations";
17
18
  import { configureSwagger } from "./swagger";
18
19
  import {
19
20
  setQueueServiceType,
@@ -289,13 +290,12 @@ const start = async (config?: LatticeGatewayConfig) => {
289
290
  let channelDeps: { router: MessageRouter; installationStore: ChannelInstallationStore } | undefined;
290
291
  const adapterRegistry = new ChannelAdapterRegistry();
291
292
  adapterRegistry.register(larkChannelAdapter);
293
+ const { wechatChannelAdapter } = await import("./channels/wechat/WechatChannelAdapter");
294
+ adapterRegistry.register(wechatChannelAdapter);
292
295
  try {
293
296
  const { getStoreLattice: getStore } = await import("@axiom-lattice/core");
294
- let bindingStore: BindingRegistry;
295
- let installationStore: ChannelInstallationStore;
296
-
297
- bindingStore = getStore("default", "channelBinding").store as BindingRegistry;
298
- installationStore = getStore("default", "channelInstallation").store as ChannelInstallationStore;
297
+ const bindingStore = getStore("default", "channelBinding").store as BindingRegistry;
298
+ const installationStore = getStore("default", "channelInstallation").store as ChannelInstallationStore;
299
299
 
300
300
  setBindingRegistry(bindingStore);
301
301
 
@@ -339,6 +339,10 @@ const start = async (config?: LatticeGatewayConfig) => {
339
339
  }
340
340
 
341
341
  // Register all routes (channel routes only active if channelDeps is set)
342
+ // Wire controller deps so enable/disable/delete can manage live connections
343
+ if (channelDeps?.router) {
344
+ setChannelControllerDeps({ adapterRegistry, router: channelDeps.router });
345
+ }
342
346
  registerLatticeRoutes(app, channelDeps);
343
347
 
344
348
  // Register sandbox manager if not already registered