@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.
- package/.turbo/turbo-build.log +18 -14
- package/CHANGELOG.md +22 -0
- package/dist/WechatChannelAdapter-QQYOHZTL.mjs +249 -0
- package/dist/WechatChannelAdapter-QQYOHZTL.mjs.map +1 -0
- package/dist/chunk-6CUQGDJI.mjs +238 -0
- package/dist/chunk-6CUQGDJI.mjs.map +1 -0
- package/dist/index.js +828 -355
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -145
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/channels/lark/LarkChannelAdapter.ts +20 -0
- package/src/channels/routes.ts +2 -0
- package/src/channels/wechat/WechatChannelAdapter.ts +294 -0
- package/src/channels/wechat/context-store.ts +47 -0
- package/src/channels/wechat/controller.ts +59 -0
- package/src/channels/wechat/routes.ts +7 -0
- package/src/channels/wechat/types.ts +62 -0
- package/src/channels/wechat/wechat-client.ts +162 -0
- package/src/controllers/__tests__/run.test.ts +1 -1
- package/src/controllers/__tests__/tasks.test.ts +6 -6
- package/src/controllers/channel-installations.ts +52 -0
- package/src/controllers/database-configs.ts +1 -1
- package/src/controllers/metrics-configs.ts +1 -1
- package/src/controllers/workspace.ts +2 -2
- package/src/index.ts +9 -5
|
@@ -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
|
+
}
|
|
@@ -19,12 +19,12 @@ jest.mock('@axiom-lattice/core', () => ({
|
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
21
|
describe('Tasks Controller', () => {
|
|
22
|
-
let listTasks:
|
|
23
|
-
let getTask:
|
|
24
|
-
let createTask:
|
|
25
|
-
let updateTask:
|
|
26
|
-
let deleteTask:
|
|
27
|
-
let completeTask:
|
|
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) {
|
|
@@ -461,7 +461,7 @@ export class WorkspaceController {
|
|
|
461
461
|
const inferredContentType = this.getMimeType(filename);
|
|
462
462
|
|
|
463
463
|
try {
|
|
464
|
-
|
|
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_
|
|
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
|
-
|
|
295
|
-
|
|
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
|