@axiom-lattice/gateway 2.1.96 → 2.1.98
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 +21 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.98",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"redis": "^5.0.1",
|
|
41
41
|
"uuid": "^9.0.1",
|
|
42
42
|
"zod": "3.25.76",
|
|
43
|
-
"@axiom-lattice/agent-eval": "2.1.
|
|
44
|
-
"@axiom-lattice/core": "2.1.
|
|
45
|
-
"@axiom-lattice/pg-stores": "1.0.
|
|
46
|
-
"@axiom-lattice/protocols": "2.1.
|
|
47
|
-
"@axiom-lattice/queue-redis": "1.0.
|
|
43
|
+
"@axiom-lattice/agent-eval": "2.1.80",
|
|
44
|
+
"@axiom-lattice/core": "2.1.86",
|
|
45
|
+
"@axiom-lattice/pg-stores": "1.0.77",
|
|
46
|
+
"@axiom-lattice/protocols": "2.1.44",
|
|
47
|
+
"@axiom-lattice/queue-redis": "1.0.43"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/jest": "^29.5.14",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
|
|
69
69
|
"start": "node dist/index.js",
|
|
70
70
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --sourcemap",
|
|
71
|
-
"lint": "tsc --noEmit",
|
|
71
|
+
"lint": "eslint src && tsc --noEmit",
|
|
72
72
|
"test": "jest",
|
|
73
73
|
"test:watch": "jest --watch",
|
|
74
74
|
"test:coverage": "jest --coverage"
|
|
@@ -194,4 +194,24 @@ export const larkChannelAdapter: ChannelAdapter<LarkChannelInstallationConfig> =
|
|
|
194
194
|
activeConnections.set(installationId, client);
|
|
195
195
|
logger.info("Lark WS client connected", { installationId });
|
|
196
196
|
},
|
|
197
|
+
|
|
198
|
+
async disconnect(installationId: string): Promise<void> {
|
|
199
|
+
const client = activeConnections.get(installationId);
|
|
200
|
+
if (!client) {
|
|
201
|
+
logger.warn("Lark WS not connected, nothing to disconnect", { installationId });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.info("Lark WS client disconnecting", { installationId });
|
|
206
|
+
try {
|
|
207
|
+
client.close({ force: true });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logger.error("Lark WS client stop error", {
|
|
210
|
+
installationId,
|
|
211
|
+
error: err instanceof Error ? err.message : String(err),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
activeConnections.delete(installationId);
|
|
215
|
+
logger.info("Lark WS client disconnected", { installationId });
|
|
216
|
+
},
|
|
197
217
|
};
|
package/src/channels/routes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import { registerLarkChannelRoutes } from "./lark/routes";
|
|
3
|
+
import { registerWechatChannelRoutes } from "./wechat/routes";
|
|
3
4
|
import type { MessageRouter } from "../router/MessageRouter";
|
|
4
5
|
import type { ChannelInstallationStore } from "@axiom-lattice/protocols";
|
|
5
6
|
|
|
@@ -15,6 +16,7 @@ type ChannelRouteRegistrar = (
|
|
|
15
16
|
|
|
16
17
|
const channelRouteRegistrars: ChannelRouteRegistrar[] = [
|
|
17
18
|
(app, deps) => registerLarkChannelRoutes(app, deps),
|
|
19
|
+
(app, _deps) => registerWechatChannelRoutes(app),
|
|
18
20
|
];
|
|
19
21
|
|
|
20
22
|
export function registerChannelRoutes(
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelAdapter,
|
|
4
|
+
InboundMessage,
|
|
5
|
+
OutboundMessage,
|
|
6
|
+
ReplyTarget,
|
|
7
|
+
ChannelInstallation,
|
|
8
|
+
} from "@axiom-lattice/protocols";
|
|
9
|
+
import type { WechatChannelInstallationConfig } from "@axiom-lattice/protocols";
|
|
10
|
+
import { getUpdates, sendMessage } from "./wechat-client";
|
|
11
|
+
import { setContextToken, getContextToken, deleteContextToken } from "./context-store";
|
|
12
|
+
import { Logger } from "../../logger/Logger";
|
|
13
|
+
|
|
14
|
+
const logger = new Logger({ serviceName: "lattice/gateway/wechat" });
|
|
15
|
+
|
|
16
|
+
const wechatConfigSchema = z.object({
|
|
17
|
+
botToken: z.string(),
|
|
18
|
+
uin: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
22
|
+
const BASE_RECONNECT_DELAY_MS = 1_000;
|
|
23
|
+
const HEARTBEAT_INTERVAL_MS = 60_000;
|
|
24
|
+
const CHANNEL_VERSION = "1.0.2";
|
|
25
|
+
|
|
26
|
+
// ─── Message type constants ───────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const MSG_TYPE_USER = 1;
|
|
29
|
+
const MSG_ITEM_TEXT = 1;
|
|
30
|
+
|
|
31
|
+
// ─── Polling connection state ─────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
interface PollingState {
|
|
34
|
+
installationId: string;
|
|
35
|
+
abortController: AbortController;
|
|
36
|
+
lastActivity: number;
|
|
37
|
+
heartbeatTimer: NodeJS.Timeout;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const activeConnections = new Map<string, PollingState>();
|
|
41
|
+
|
|
42
|
+
// ─── Message dedup ────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const seenClientIds = new Set<string>();
|
|
45
|
+
|
|
46
|
+
function addToDedup(clientId: string): boolean {
|
|
47
|
+
if (!clientId || seenClientIds.has(clientId)) return false;
|
|
48
|
+
seenClientIds.add(clientId);
|
|
49
|
+
if (seenClientIds.size > 1000) {
|
|
50
|
+
const first = seenClientIds.values().next().value;
|
|
51
|
+
if (first !== undefined) seenClientIds.delete(first);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Text extraction from iLink message ───────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function extractText(msg: { item_list?: Array<{ type?: number; text_item?: { text?: string } }> }): string | null {
|
|
59
|
+
if (!msg.item_list?.length) return null;
|
|
60
|
+
for (const item of msg.item_list) {
|
|
61
|
+
if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
|
|
62
|
+
return item.text_item.text;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Adapter ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export const wechatChannelAdapter: ChannelAdapter<WechatChannelInstallationConfig> = {
|
|
71
|
+
channel: "wechat",
|
|
72
|
+
|
|
73
|
+
configSchema: wechatConfigSchema,
|
|
74
|
+
|
|
75
|
+
async receive(
|
|
76
|
+
rawPayload: unknown,
|
|
77
|
+
installation: ChannelInstallation<WechatChannelInstallationConfig>,
|
|
78
|
+
): Promise<InboundMessage | null> {
|
|
79
|
+
const msg = rawPayload as {
|
|
80
|
+
from_user_id?: string;
|
|
81
|
+
client_id?: string;
|
|
82
|
+
message_type?: number;
|
|
83
|
+
item_list?: Array<{ type?: number; text_item?: { text?: string } }>;
|
|
84
|
+
context_token?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Only process user messages (type 1), skip bot echoes (type 2)
|
|
88
|
+
if (msg.message_type !== MSG_TYPE_USER) return null;
|
|
89
|
+
|
|
90
|
+
const senderId = msg.from_user_id;
|
|
91
|
+
if (!senderId) return null;
|
|
92
|
+
|
|
93
|
+
const text = extractText(msg);
|
|
94
|
+
if (!text) return null;
|
|
95
|
+
|
|
96
|
+
// Cache context token for reply
|
|
97
|
+
if (msg.context_token) {
|
|
98
|
+
setContextToken(senderId, msg.context_token);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
channel: "wechat",
|
|
103
|
+
channelInstallationId: installation.id,
|
|
104
|
+
tenantId: installation.tenantId,
|
|
105
|
+
sender: {
|
|
106
|
+
id: senderId,
|
|
107
|
+
displayName: senderId.split("@")[0],
|
|
108
|
+
},
|
|
109
|
+
content: {
|
|
110
|
+
text,
|
|
111
|
+
},
|
|
112
|
+
conversation: {
|
|
113
|
+
id: senderId,
|
|
114
|
+
type: "direct",
|
|
115
|
+
},
|
|
116
|
+
replyTarget: {
|
|
117
|
+
adapterChannel: "wechat",
|
|
118
|
+
channelInstallationId: installation.id,
|
|
119
|
+
rawTarget: { senderId },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async sendReply(
|
|
125
|
+
replyTarget: ReplyTarget,
|
|
126
|
+
message: OutboundMessage,
|
|
127
|
+
installation: ChannelInstallation<WechatChannelInstallationConfig>,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const senderId = replyTarget.rawTarget.senderId as string;
|
|
130
|
+
const contextToken = getContextToken(senderId);
|
|
131
|
+
|
|
132
|
+
if (!contextToken) {
|
|
133
|
+
logger.warn("WeChat context token expired, cannot send reply", {
|
|
134
|
+
installationId: installation.id,
|
|
135
|
+
senderId,
|
|
136
|
+
});
|
|
137
|
+
deleteContextToken(senderId);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { botToken } = installation.config;
|
|
142
|
+
await sendMessage(botToken, senderId, message.text, contextToken);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
resolveThreadId(message: InboundMessage, binding: unknown): string {
|
|
146
|
+
const date = new Date().toISOString().split("T")[0];
|
|
147
|
+
const agentId = (binding as { agentId: string }).agentId;
|
|
148
|
+
return `wechat:dm:${message.sender.id}:${agentId}:${date}`;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async connect(
|
|
152
|
+
installation: ChannelInstallation<WechatChannelInstallationConfig>,
|
|
153
|
+
deps?: unknown,
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const { id: installationId, tenantId, config } = installation;
|
|
156
|
+
|
|
157
|
+
if (!config.botToken) {
|
|
158
|
+
logger.warn("WeChat installation missing botToken, skipping", { installationId });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (activeConnections.has(installationId)) {
|
|
163
|
+
logger.warn("WeChat polling already running for installation, skipping", { installationId });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
logger.info("WeChat polling starting", { installationId, tenantId });
|
|
168
|
+
|
|
169
|
+
const abortController = new AbortController();
|
|
170
|
+
|
|
171
|
+
const heartbeatTimer = setInterval(() => {
|
|
172
|
+
const state2 = activeConnections.get(installationId);
|
|
173
|
+
if (!state2) {
|
|
174
|
+
clearInterval(heartbeatTimer);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const elapsed = Date.now() - state2.lastActivity;
|
|
178
|
+
if (elapsed > HEARTBEAT_INTERVAL_MS * 2) {
|
|
179
|
+
logger.error("WeChat polling heartbeat lost — no activity", {
|
|
180
|
+
installationId,
|
|
181
|
+
elapsedMs: elapsed,
|
|
182
|
+
});
|
|
183
|
+
state2.abortController.abort();
|
|
184
|
+
}
|
|
185
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
186
|
+
|
|
187
|
+
const state: PollingState = {
|
|
188
|
+
installationId,
|
|
189
|
+
abortController,
|
|
190
|
+
lastActivity: Date.now(),
|
|
191
|
+
heartbeatTimer,
|
|
192
|
+
};
|
|
193
|
+
activeConnections.set(installationId, state);
|
|
194
|
+
|
|
195
|
+
const router = (deps as { router?: { dispatch(msg: InboundMessage): Promise<unknown> } })?.router;
|
|
196
|
+
|
|
197
|
+
let syncBuffer = "";
|
|
198
|
+
let reconnectDelay = BASE_RECONNECT_DELAY_MS;
|
|
199
|
+
|
|
200
|
+
const scheduleNextPoll = (): void => {
|
|
201
|
+
poll().catch((err) => {
|
|
202
|
+
logger.error("WeChat poll iteration crashed", {
|
|
203
|
+
installationId,
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
});
|
|
206
|
+
const currentState = activeConnections.get(installationId);
|
|
207
|
+
if (currentState && !currentState.abortController.signal.aborted) {
|
|
208
|
+
setTimeout(scheduleNextPoll, reconnectDelay);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const poll = async (): Promise<void> => {
|
|
214
|
+
const currentState = activeConnections.get(installationId);
|
|
215
|
+
if (!currentState || currentState.abortController.signal.aborted) {
|
|
216
|
+
logger.info("WeChat polling aborted", { installationId });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const result = await getUpdates(
|
|
222
|
+
config.botToken,
|
|
223
|
+
syncBuffer,
|
|
224
|
+
currentState.abortController.signal,
|
|
225
|
+
);
|
|
226
|
+
syncBuffer = result.syncBuffer;
|
|
227
|
+
reconnectDelay = BASE_RECONNECT_DELAY_MS;
|
|
228
|
+
currentState.lastActivity = Date.now();
|
|
229
|
+
|
|
230
|
+
if (result.msgs.length > 0) {
|
|
231
|
+
logger.info("WeChat poll received messages", {
|
|
232
|
+
installationId,
|
|
233
|
+
count: result.msgs.length,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const rawMsg of result.msgs) {
|
|
238
|
+
// Only user messages, skip bot echoes and non-text
|
|
239
|
+
if (rawMsg.message_type !== MSG_TYPE_USER) continue;
|
|
240
|
+
|
|
241
|
+
const text = extractText(rawMsg);
|
|
242
|
+
if (!text) continue;
|
|
243
|
+
|
|
244
|
+
if (!addToDedup((rawMsg as { client_id?: string }).client_id ?? "")) continue;
|
|
245
|
+
|
|
246
|
+
const inbound = await wechatChannelAdapter.receive(
|
|
247
|
+
rawMsg,
|
|
248
|
+
installation as ChannelInstallation<WechatChannelInstallationConfig>,
|
|
249
|
+
);
|
|
250
|
+
if (inbound && router) {
|
|
251
|
+
const dispatchResult = await router.dispatch(inbound);
|
|
252
|
+
if (!(dispatchResult as { success?: boolean }).success) {
|
|
253
|
+
logger.warn("WeChat dispatch failed", {
|
|
254
|
+
installationId,
|
|
255
|
+
error: (dispatchResult as { error?: { message?: string } }).error?.message,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
262
|
+
logger.info("WeChat poll aborted by signal", { installationId });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
logger.error("WeChat poll error", {
|
|
267
|
+
installationId,
|
|
268
|
+
error: err instanceof Error ? err.message : String(err),
|
|
269
|
+
});
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
|
|
271
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
scheduleNextPoll();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
scheduleNextPoll();
|
|
278
|
+
logger.info("WeChat polling started", { installationId });
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async disconnect(installationId: string): Promise<void> {
|
|
282
|
+
const state = activeConnections.get(installationId);
|
|
283
|
+
if (!state) {
|
|
284
|
+
logger.warn("WeChat polling not running, nothing to disconnect", { installationId });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
logger.info("WeChat polling disconnecting", { installationId });
|
|
289
|
+
state.abortController.abort();
|
|
290
|
+
clearInterval(state.heartbeatTimer);
|
|
291
|
+
activeConnections.delete(installationId);
|
|
292
|
+
logger.info("WeChat polling disconnected", { installationId });
|
|
293
|
+
},
|
|
294
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface ContextTokenEntry {
|
|
2
|
+
token: string;
|
|
3
|
+
senderId: string;
|
|
4
|
+
updatedAt: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
8
|
+
const store = new Map<string, ContextTokenEntry>();
|
|
9
|
+
const timers = new Map<string, NodeJS.Timeout>();
|
|
10
|
+
|
|
11
|
+
export function getContextToken(senderId: string): string | undefined {
|
|
12
|
+
const entry = store.get(senderId);
|
|
13
|
+
if (!entry) return undefined;
|
|
14
|
+
if (Date.now() - entry.updatedAt > TOKEN_TTL_MS) {
|
|
15
|
+
deleteContextToken(senderId);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return entry.token;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setContextToken(senderId: string, token: string): void {
|
|
22
|
+
// Clear existing timer
|
|
23
|
+
const existingTimer = timers.get(senderId);
|
|
24
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
25
|
+
|
|
26
|
+
store.set(senderId, { token, senderId, updatedAt: Date.now() });
|
|
27
|
+
|
|
28
|
+
// Auto-cleanup after TTL
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
store.delete(senderId);
|
|
31
|
+
timers.delete(senderId);
|
|
32
|
+
}, TOKEN_TTL_MS);
|
|
33
|
+
timers.set(senderId, timer);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function deleteContextToken(senderId: string): void {
|
|
37
|
+
store.delete(senderId);
|
|
38
|
+
const timer = timers.get(senderId);
|
|
39
|
+
if (timer) {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
timers.delete(senderId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getContextTokenStoreSize(): number {
|
|
46
|
+
return store.size;
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { getQrCode, getQrCodeStatus } from "./wechat-client";
|
|
3
|
+
import type { QrSession } from "./types";
|
|
4
|
+
|
|
5
|
+
const qrSessions = new Map<string, QrSession>();
|
|
6
|
+
const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
|
|
8
|
+
function cleanupExpiredSessions(): void {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, session] of qrSessions) {
|
|
11
|
+
if (now - session.createdAt > SESSION_TTL_MS) {
|
|
12
|
+
qrSessions.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function handleGetQrCode(
|
|
18
|
+
_request: FastifyRequest,
|
|
19
|
+
reply: FastifyReply,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
cleanupExpiredSessions();
|
|
23
|
+
const { qrcode, qrcodeImgUrl } = await getQrCode();
|
|
24
|
+
qrSessions.set(qrcode, { qrcode, createdAt: Date.now() });
|
|
25
|
+
reply.send({ success: true, data: { qrcode, qrcodeImgUrl } });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
reply.status(500).send({
|
|
28
|
+
success: false,
|
|
29
|
+
message: err instanceof Error ? err.message : "Failed to get QR code",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleGetQrCodeStatus(
|
|
35
|
+
request: FastifyRequest,
|
|
36
|
+
reply: FastifyReply,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const { qrcode } = request.query as { qrcode?: string };
|
|
39
|
+
if (!qrcode) {
|
|
40
|
+
reply.status(400).send({ success: false, message: "qrcode is required" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const session = qrSessions.get(qrcode);
|
|
45
|
+
if (!session) {
|
|
46
|
+
reply.status(404).send({ success: false, message: "Session not found or expired" });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await getQrCodeStatus(qrcode);
|
|
52
|
+
reply.send({ success: true, data: result });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
reply.status(500).send({
|
|
55
|
+
success: false,
|
|
56
|
+
message: err instanceof Error ? err.message : "Failed to check QR status",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { handleGetQrCode, handleGetQrCodeStatus } from "./controller";
|
|
3
|
+
|
|
4
|
+
export function registerWechatChannelRoutes(app: FastifyInstance): void {
|
|
5
|
+
app.get("/api/channels/wechat/setup/qrcode", handleGetQrCode);
|
|
6
|
+
app.get("/api/channels/wechat/setup/status", handleGetQrCodeStatus);
|
|
7
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface WechatInstallationConfig {
|
|
2
|
+
botToken: string;
|
|
3
|
+
uin?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ─── iLink message types ─────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const MSG_TYPE_USER = 1;
|
|
9
|
+
export const MSG_TYPE_BOT = 2;
|
|
10
|
+
export const MSG_STATE_FINISH = 2;
|
|
11
|
+
export const MSG_ITEM_TEXT = 1;
|
|
12
|
+
|
|
13
|
+
export interface TextItem {
|
|
14
|
+
text?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MessageItem {
|
|
18
|
+
type?: number;
|
|
19
|
+
text_item?: TextItem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WechatMessage {
|
|
23
|
+
from_user_id?: string;
|
|
24
|
+
to_user_id?: string;
|
|
25
|
+
client_id?: string;
|
|
26
|
+
message_type?: number;
|
|
27
|
+
message_state?: number;
|
|
28
|
+
item_list?: MessageItem[];
|
|
29
|
+
context_token?: string;
|
|
30
|
+
create_time_ms?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GetUpdatesResp {
|
|
34
|
+
ret?: number;
|
|
35
|
+
errcode?: number;
|
|
36
|
+
errmsg?: string;
|
|
37
|
+
msgs?: WechatMessage[];
|
|
38
|
+
get_updates_buf?: string;
|
|
39
|
+
longpolling_timeout_ms?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── QR login types ───────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface WechatQrCodeResponse {
|
|
45
|
+
qrcode?: string;
|
|
46
|
+
qrcode_img_content?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WechatQrCodeStatusResponse {
|
|
51
|
+
status: "wait" | "scaned" | "confirmed" | "expired";
|
|
52
|
+
bot_token?: string;
|
|
53
|
+
ilink_bot_id?: string;
|
|
54
|
+
ilink_user_id?: string;
|
|
55
|
+
baseurl?: string;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface QrSession {
|
|
60
|
+
qrcode: string;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
}
|