@axiom-lattice/gateway 2.1.99 → 2.1.101
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 +16 -16
- package/CHANGELOG.md +23 -0
- package/dist/{WechatChannelAdapter-QQYOHZTL.mjs → WechatChannelAdapter-WSDKR4OA.mjs} +23 -17
- package/dist/WechatChannelAdapter-WSDKR4OA.mjs.map +1 -0
- package/dist/index.js +36 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/channels/wechat/WechatChannelAdapter.ts +3 -3
- package/src/channels/wechat/__tests__/context-store.test.ts +148 -0
- package/src/channels/wechat/context-store.ts +22 -15
- package/src/controllers/workspace.ts +16 -9
- package/dist/WechatChannelAdapter-QQYOHZTL.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.101",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,9 +40,9 @@
|
|
|
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.
|
|
43
|
+
"@axiom-lattice/agent-eval": "2.1.83",
|
|
44
|
+
"@axiom-lattice/core": "2.1.89",
|
|
45
|
+
"@axiom-lattice/pg-stores": "1.0.80",
|
|
46
46
|
"@axiom-lattice/protocols": "2.1.44",
|
|
47
47
|
"@axiom-lattice/queue-redis": "1.0.43"
|
|
48
48
|
},
|
|
@@ -95,7 +95,7 @@ export const wechatChannelAdapter: ChannelAdapter<WechatChannelInstallationConfi
|
|
|
95
95
|
|
|
96
96
|
// Cache context token for reply
|
|
97
97
|
if (msg.context_token) {
|
|
98
|
-
setContextToken(senderId, msg.context_token);
|
|
98
|
+
setContextToken(installation.id, senderId, msg.context_token);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
return {
|
|
@@ -127,14 +127,14 @@ export const wechatChannelAdapter: ChannelAdapter<WechatChannelInstallationConfi
|
|
|
127
127
|
installation: ChannelInstallation<WechatChannelInstallationConfig>,
|
|
128
128
|
): Promise<void> {
|
|
129
129
|
const senderId = replyTarget.rawTarget.senderId as string;
|
|
130
|
-
const contextToken = getContextToken(senderId);
|
|
130
|
+
const contextToken = getContextToken(installation.id, senderId);
|
|
131
131
|
|
|
132
132
|
if (!contextToken) {
|
|
133
133
|
logger.warn("WeChat context token expired, cannot send reply", {
|
|
134
134
|
installationId: installation.id,
|
|
135
135
|
senderId,
|
|
136
136
|
});
|
|
137
|
-
deleteContextToken(senderId);
|
|
137
|
+
deleteContextToken(installation.id, senderId);
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setContextToken,
|
|
3
|
+
getContextToken,
|
|
4
|
+
deleteContextToken,
|
|
5
|
+
getContextTokenStoreSize,
|
|
6
|
+
} from "../context-store";
|
|
7
|
+
|
|
8
|
+
describe("context-store", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.useFakeTimers();
|
|
11
|
+
// Clear all tokens by deleting known keys (no public clear API)
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.useRealTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("setContextToken / getContextToken", () => {
|
|
19
|
+
it("stores and retrieves a token keyed by installationId + senderId", () => {
|
|
20
|
+
setContextToken("inst-1", "user-a@im.wechat", "token-abc");
|
|
21
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("token-abc");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns undefined for unknown installationId + senderId", () => {
|
|
25
|
+
expect(getContextToken("inst-1", "unknown@im.wechat")).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns undefined when senderId matches but installationId differs", () => {
|
|
29
|
+
setContextToken("inst-1", "user-a@im.wechat", "token-abc");
|
|
30
|
+
expect(getContextToken("inst-2", "user-a@im.wechat")).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("isolation between installations", () => {
|
|
35
|
+
it("does not collide when different installations use the same senderId", () => {
|
|
36
|
+
setContextToken("inst-1", "user-x@im.wechat", "tok-111");
|
|
37
|
+
setContextToken("inst-2", "user-x@im.wechat", "tok-222");
|
|
38
|
+
|
|
39
|
+
expect(getContextToken("inst-1", "user-x@im.wechat")).toBe("tok-111");
|
|
40
|
+
expect(getContextToken("inst-2", "user-x@im.wechat")).toBe("tok-222");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("isolates tokens between multiple installations with multiple users", () => {
|
|
44
|
+
setContextToken("inst-1", "alice@im.wechat", "a1");
|
|
45
|
+
setContextToken("inst-1", "bob@im.wechat", "b1");
|
|
46
|
+
setContextToken("inst-2", "alice@im.wechat", "a2");
|
|
47
|
+
setContextToken("inst-2", "charlie@im.wechat", "c2");
|
|
48
|
+
|
|
49
|
+
expect(getContextToken("inst-1", "alice@im.wechat")).toBe("a1");
|
|
50
|
+
expect(getContextToken("inst-1", "bob@im.wechat")).toBe("b1");
|
|
51
|
+
expect(getContextToken("inst-2", "alice@im.wechat")).toBe("a2");
|
|
52
|
+
expect(getContextToken("inst-2", "charlie@im.wechat")).toBe("c2");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("overwrite", () => {
|
|
57
|
+
it("overwrites existing token for the same installationId + senderId", () => {
|
|
58
|
+
setContextToken("inst-1", "user-a@im.wechat", "old-token");
|
|
59
|
+
setContextToken("inst-1", "user-a@im.wechat", "new-token");
|
|
60
|
+
|
|
61
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("new-token");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not affect other senderIds in the same installation", () => {
|
|
65
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-a");
|
|
66
|
+
setContextToken("inst-1", "user-b@im.wechat", "tok-b");
|
|
67
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-a-v2");
|
|
68
|
+
|
|
69
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-a-v2");
|
|
70
|
+
expect(getContextToken("inst-1", "user-b@im.wechat")).toBe("tok-b");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("deleteContextToken", () => {
|
|
75
|
+
it("removes a specific token", () => {
|
|
76
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-a");
|
|
77
|
+
deleteContextToken("inst-1", "user-a@im.wechat");
|
|
78
|
+
|
|
79
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("only removes the targeted key, leaving others intact", () => {
|
|
83
|
+
setContextToken("inst-1", "alice@im.wechat", "tok-1");
|
|
84
|
+
setContextToken("inst-1", "bob@im.wechat", "tok-2");
|
|
85
|
+
setContextToken("inst-2", "alice@im.wechat", "tok-3");
|
|
86
|
+
|
|
87
|
+
deleteContextToken("inst-1", "alice@im.wechat");
|
|
88
|
+
|
|
89
|
+
expect(getContextToken("inst-1", "alice@im.wechat")).toBeUndefined();
|
|
90
|
+
expect(getContextToken("inst-1", "bob@im.wechat")).toBe("tok-2");
|
|
91
|
+
expect(getContextToken("inst-2", "alice@im.wechat")).toBe("tok-3");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("is a no-op for unknown keys", () => {
|
|
95
|
+
expect(() => deleteContextToken("inst-1", "no-such@im.wechat")).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("TTL expiry", () => {
|
|
100
|
+
it("returns undefined after TTL has passed", () => {
|
|
101
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-a");
|
|
102
|
+
|
|
103
|
+
// Advance time past 24h TTL
|
|
104
|
+
jest.advanceTimersByTime(25 * 60 * 60 * 1000);
|
|
105
|
+
|
|
106
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns token within TTL window", () => {
|
|
110
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-a");
|
|
111
|
+
|
|
112
|
+
// Advance time to just under 24h
|
|
113
|
+
jest.advanceTimersByTime(23 * 60 * 60 * 1000);
|
|
114
|
+
|
|
115
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-a");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("resets TTL when overwritten", () => {
|
|
119
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-v1");
|
|
120
|
+
jest.advanceTimersByTime(23 * 60 * 60 * 1000);
|
|
121
|
+
|
|
122
|
+
// Overwrite resets the TTL
|
|
123
|
+
setContextToken("inst-1", "user-a@im.wechat", "tok-v2");
|
|
124
|
+
jest.advanceTimersByTime(23 * 60 * 60 * 1000);
|
|
125
|
+
|
|
126
|
+
// Original TTL + 23h would have expired, but overwrite reset it
|
|
127
|
+
expect(getContextToken("inst-1", "user-a@im.wechat")).toBe("tok-v2");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("getContextTokenStoreSize", () => {
|
|
132
|
+
it("returns the number of stored entries", () => {
|
|
133
|
+
const startSize = getContextTokenStoreSize();
|
|
134
|
+
|
|
135
|
+
setContextToken("sz-1", "a@im.wechat", "x");
|
|
136
|
+
expect(getContextTokenStoreSize()).toBe(startSize + 1);
|
|
137
|
+
|
|
138
|
+
setContextToken("sz-1", "b@im.wechat", "y");
|
|
139
|
+
expect(getContextTokenStoreSize()).toBe(startSize + 2);
|
|
140
|
+
|
|
141
|
+
setContextToken("sz-2", "a@im.wechat", "z");
|
|
142
|
+
expect(getContextTokenStoreSize()).toBe(startSize + 3);
|
|
143
|
+
|
|
144
|
+
deleteContextToken("sz-1", "a@im.wechat");
|
|
145
|
+
expect(getContextTokenStoreSize()).toBe(startSize + 2);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
interface ContextTokenEntry {
|
|
2
2
|
token: string;
|
|
3
|
+
installationId: string;
|
|
3
4
|
senderId: string;
|
|
4
5
|
updatedAt: number;
|
|
5
6
|
}
|
|
@@ -8,37 +9,43 @@ const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
8
9
|
const store = new Map<string, ContextTokenEntry>();
|
|
9
10
|
const timers = new Map<string, NodeJS.Timeout>();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
function makeKey(installationId: string, senderId: string): string {
|
|
13
|
+
return `${installationId}::${senderId}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getContextToken(installationId: string, senderId: string): string | undefined {
|
|
17
|
+
const key = makeKey(installationId, senderId);
|
|
18
|
+
const entry = store.get(key);
|
|
13
19
|
if (!entry) return undefined;
|
|
14
20
|
if (Date.now() - entry.updatedAt > TOKEN_TTL_MS) {
|
|
15
|
-
deleteContextToken(senderId);
|
|
21
|
+
deleteContextToken(installationId, senderId);
|
|
16
22
|
return undefined;
|
|
17
23
|
}
|
|
18
24
|
return entry.token;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
export function setContextToken(senderId: string, token: string): void {
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
export function setContextToken(installationId: string, senderId: string, token: string): void {
|
|
28
|
+
const key = makeKey(installationId, senderId);
|
|
29
|
+
|
|
30
|
+
const existingTimer = timers.get(key);
|
|
24
31
|
if (existingTimer) clearTimeout(existingTimer);
|
|
25
32
|
|
|
26
|
-
store.set(
|
|
33
|
+
store.set(key, { token, installationId, senderId, updatedAt: Date.now() });
|
|
27
34
|
|
|
28
|
-
// Auto-cleanup after TTL
|
|
29
35
|
const timer = setTimeout(() => {
|
|
30
|
-
store.delete(
|
|
31
|
-
timers.delete(
|
|
36
|
+
store.delete(key);
|
|
37
|
+
timers.delete(key);
|
|
32
38
|
}, TOKEN_TTL_MS);
|
|
33
|
-
timers.set(
|
|
39
|
+
timers.set(key, timer);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
export function deleteContextToken(senderId: string): void {
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
export function deleteContextToken(installationId: string, senderId: string): void {
|
|
43
|
+
const key = makeKey(installationId, senderId);
|
|
44
|
+
store.delete(key);
|
|
45
|
+
const timer = timers.get(key);
|
|
39
46
|
if (timer) {
|
|
40
47
|
clearTimeout(timer);
|
|
41
|
-
timers.delete(
|
|
48
|
+
timers.delete(key);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -373,13 +373,17 @@ export class WorkspaceController {
|
|
|
373
373
|
const filename = this.getFilenameFromPath(resolvedPath);
|
|
374
374
|
const isBinary = this.isBinaryContentType(filename);
|
|
375
375
|
|
|
376
|
-
//
|
|
376
|
+
// Use volume backend when available; fall back to sandbox
|
|
377
377
|
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
|
|
378
378
|
|
|
379
379
|
let buf: Buffer;
|
|
380
|
-
if (volumeBackend
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
if (volumeBackend) {
|
|
381
|
+
if (isBinary && volumeBackend.readBinary) {
|
|
382
|
+
buf = await volumeBackend.readBinary(resolvedPath);
|
|
383
|
+
} else {
|
|
384
|
+
const fileData = await volumeBackend.readRaw(resolvedPath);
|
|
385
|
+
buf = Buffer.from(fileData.content.join("\n"), "utf-8");
|
|
386
|
+
}
|
|
383
387
|
} else {
|
|
384
388
|
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
385
389
|
buf = await sandbox.file.downloadFile({ file: resolvedPath });
|
|
@@ -445,14 +449,17 @@ export class WorkspaceController {
|
|
|
445
449
|
const filename = this.getFilenameFromPath(resolvedPath);
|
|
446
450
|
const isBinary = this.isBinaryContentType(filename);
|
|
447
451
|
|
|
448
|
-
//
|
|
449
|
-
// (volume backend's readRaw API is text-based and corrupts binary data)
|
|
452
|
+
// Use volume backend when available; fall back to sandbox
|
|
450
453
|
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
|
|
451
454
|
|
|
452
455
|
let buf: Buffer;
|
|
453
|
-
if (volumeBackend
|
|
454
|
-
|
|
455
|
-
|
|
456
|
+
if (volumeBackend) {
|
|
457
|
+
if (isBinary && volumeBackend.readBinary) {
|
|
458
|
+
buf = await volumeBackend.readBinary(resolvedPath);
|
|
459
|
+
} else {
|
|
460
|
+
const fileData = await volumeBackend.readRaw(resolvedPath);
|
|
461
|
+
buf = Buffer.from(fileData.content.join("\n"), "utf-8");
|
|
462
|
+
}
|
|
456
463
|
} else {
|
|
457
464
|
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
458
465
|
buf = await sandbox.file.downloadFile({ file: resolvedPath });
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/channels/wechat/WechatChannelAdapter.ts","../src/channels/wechat/context-store.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type {\n ChannelAdapter,\n InboundMessage,\n OutboundMessage,\n ReplyTarget,\n ChannelInstallation,\n} from \"@axiom-lattice/protocols\";\nimport type { WechatChannelInstallationConfig } from \"@axiom-lattice/protocols\";\nimport { getUpdates, sendMessage } from \"./wechat-client\";\nimport { setContextToken, getContextToken, deleteContextToken } from \"./context-store\";\nimport { Logger } from \"../../logger/Logger\";\n\nconst logger = new Logger({ serviceName: \"lattice/gateway/wechat\" });\n\nconst wechatConfigSchema = z.object({\n botToken: z.string(),\n uin: z.string().optional(),\n});\n\nconst MAX_RECONNECT_DELAY_MS = 30_000;\nconst BASE_RECONNECT_DELAY_MS = 1_000;\nconst HEARTBEAT_INTERVAL_MS = 60_000;\nconst CHANNEL_VERSION = \"1.0.2\";\n\n// ─── Message type constants ───────────────────────────────────────────────\n\nconst MSG_TYPE_USER = 1;\nconst MSG_ITEM_TEXT = 1;\n\n// ─── Polling connection state ─────────────────────────────────────────────\n\ninterface PollingState {\n installationId: string;\n abortController: AbortController;\n lastActivity: number;\n heartbeatTimer: NodeJS.Timeout;\n}\n\nconst activeConnections = new Map<string, PollingState>();\n\n// ─── Message dedup ────────────────────────────────────────────────────────\n\nconst seenClientIds = new Set<string>();\n\nfunction addToDedup(clientId: string): boolean {\n if (!clientId || seenClientIds.has(clientId)) return false;\n seenClientIds.add(clientId);\n if (seenClientIds.size > 1000) {\n const first = seenClientIds.values().next().value;\n if (first !== undefined) seenClientIds.delete(first);\n }\n return true;\n}\n\n// ─── Text extraction from iLink message ───────────────────────────────────\n\nfunction extractText(msg: { item_list?: Array<{ type?: number; text_item?: { text?: string } }> }): string | null {\n if (!msg.item_list?.length) return null;\n for (const item of msg.item_list) {\n if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {\n return item.text_item.text;\n }\n }\n return null;\n}\n\n// ─── Adapter ──────────────────────────────────────────────────────────────\n\nexport const wechatChannelAdapter: ChannelAdapter<WechatChannelInstallationConfig> = {\n channel: \"wechat\",\n\n configSchema: wechatConfigSchema,\n\n async receive(\n rawPayload: unknown,\n installation: ChannelInstallation<WechatChannelInstallationConfig>,\n ): Promise<InboundMessage | null> {\n const msg = rawPayload as {\n from_user_id?: string;\n client_id?: string;\n message_type?: number;\n item_list?: Array<{ type?: number; text_item?: { text?: string } }>;\n context_token?: string;\n };\n\n // Only process user messages (type 1), skip bot echoes (type 2)\n if (msg.message_type !== MSG_TYPE_USER) return null;\n\n const senderId = msg.from_user_id;\n if (!senderId) return null;\n\n const text = extractText(msg);\n if (!text) return null;\n\n // Cache context token for reply\n if (msg.context_token) {\n setContextToken(senderId, msg.context_token);\n }\n\n return {\n channel: \"wechat\",\n channelInstallationId: installation.id,\n tenantId: installation.tenantId,\n sender: {\n id: senderId,\n displayName: senderId.split(\"@\")[0],\n },\n content: {\n text,\n },\n conversation: {\n id: senderId,\n type: \"direct\",\n },\n replyTarget: {\n adapterChannel: \"wechat\",\n channelInstallationId: installation.id,\n rawTarget: { senderId },\n },\n };\n },\n\n async sendReply(\n replyTarget: ReplyTarget,\n message: OutboundMessage,\n installation: ChannelInstallation<WechatChannelInstallationConfig>,\n ): Promise<void> {\n const senderId = replyTarget.rawTarget.senderId as string;\n const contextToken = getContextToken(senderId);\n\n if (!contextToken) {\n logger.warn(\"WeChat context token expired, cannot send reply\", {\n installationId: installation.id,\n senderId,\n });\n deleteContextToken(senderId);\n return;\n }\n\n const { botToken } = installation.config;\n await sendMessage(botToken, senderId, message.text, contextToken);\n },\n\n resolveThreadId(message: InboundMessage, binding: unknown): string {\n const date = new Date().toISOString().split(\"T\")[0];\n const agentId = (binding as { agentId: string }).agentId;\n return `wechat:dm:${message.sender.id}:${agentId}:${date}`;\n },\n\n async connect(\n installation: ChannelInstallation<WechatChannelInstallationConfig>,\n deps?: unknown,\n ): Promise<void> {\n const { id: installationId, tenantId, config } = installation;\n\n if (!config.botToken) {\n logger.warn(\"WeChat installation missing botToken, skipping\", { installationId });\n return;\n }\n\n if (activeConnections.has(installationId)) {\n logger.warn(\"WeChat polling already running for installation, skipping\", { installationId });\n return;\n }\n\n logger.info(\"WeChat polling starting\", { installationId, tenantId });\n\n const abortController = new AbortController();\n\n const heartbeatTimer = setInterval(() => {\n const state2 = activeConnections.get(installationId);\n if (!state2) {\n clearInterval(heartbeatTimer);\n return;\n }\n const elapsed = Date.now() - state2.lastActivity;\n if (elapsed > HEARTBEAT_INTERVAL_MS * 2) {\n logger.error(\"WeChat polling heartbeat lost — no activity\", {\n installationId,\n elapsedMs: elapsed,\n });\n state2.abortController.abort();\n }\n }, HEARTBEAT_INTERVAL_MS);\n\n const state: PollingState = {\n installationId,\n abortController,\n lastActivity: Date.now(),\n heartbeatTimer,\n };\n activeConnections.set(installationId, state);\n\n const router = (deps as { router?: { dispatch(msg: InboundMessage): Promise<unknown> } })?.router;\n\n let syncBuffer = \"\";\n let reconnectDelay = BASE_RECONNECT_DELAY_MS;\n\n const scheduleNextPoll = (): void => {\n poll().catch((err) => {\n logger.error(\"WeChat poll iteration crashed\", {\n installationId,\n error: err instanceof Error ? err.message : String(err),\n });\n const currentState = activeConnections.get(installationId);\n if (currentState && !currentState.abortController.signal.aborted) {\n setTimeout(scheduleNextPoll, reconnectDelay);\n }\n });\n };\n\n const poll = async (): Promise<void> => {\n const currentState = activeConnections.get(installationId);\n if (!currentState || currentState.abortController.signal.aborted) {\n logger.info(\"WeChat polling aborted\", { installationId });\n return;\n }\n\n try {\n const result = await getUpdates(\n config.botToken,\n syncBuffer,\n currentState.abortController.signal,\n );\n syncBuffer = result.syncBuffer;\n reconnectDelay = BASE_RECONNECT_DELAY_MS;\n currentState.lastActivity = Date.now();\n\n if (result.msgs.length > 0) {\n logger.info(\"WeChat poll received messages\", {\n installationId,\n count: result.msgs.length,\n });\n }\n\n for (const rawMsg of result.msgs) {\n // Only user messages, skip bot echoes and non-text\n if (rawMsg.message_type !== MSG_TYPE_USER) continue;\n\n const text = extractText(rawMsg);\n if (!text) continue;\n\n if (!addToDedup((rawMsg as { client_id?: string }).client_id ?? \"\")) continue;\n\n const inbound = await wechatChannelAdapter.receive(\n rawMsg,\n installation as ChannelInstallation<WechatChannelInstallationConfig>,\n );\n if (inbound && router) {\n const dispatchResult = await router.dispatch(inbound);\n if (!(dispatchResult as { success?: boolean }).success) {\n logger.warn(\"WeChat dispatch failed\", {\n installationId,\n error: (dispatchResult as { error?: { message?: string } }).error?.message,\n });\n }\n }\n }\n } catch (err) {\n if (err instanceof Error && err.name === \"AbortError\") {\n logger.info(\"WeChat poll aborted by signal\", { installationId });\n return;\n }\n\n logger.error(\"WeChat poll error\", {\n installationId,\n error: err instanceof Error ? err.message : String(err),\n });\n await new Promise((resolve) => setTimeout(resolve, reconnectDelay));\n reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);\n }\n\n scheduleNextPoll();\n };\n\n scheduleNextPoll();\n logger.info(\"WeChat polling started\", { installationId });\n },\n\n async disconnect(installationId: string): Promise<void> {\n const state = activeConnections.get(installationId);\n if (!state) {\n logger.warn(\"WeChat polling not running, nothing to disconnect\", { installationId });\n return;\n }\n\n logger.info(\"WeChat polling disconnecting\", { installationId });\n state.abortController.abort();\n clearInterval(state.heartbeatTimer);\n activeConnections.delete(installationId);\n logger.info(\"WeChat polling disconnected\", { installationId });\n },\n};\n","interface ContextTokenEntry {\n token: string;\n senderId: string;\n updatedAt: number;\n}\n\nconst TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\nconst store = new Map<string, ContextTokenEntry>();\nconst timers = new Map<string, NodeJS.Timeout>();\n\nexport function getContextToken(senderId: string): string | undefined {\n const entry = store.get(senderId);\n if (!entry) return undefined;\n if (Date.now() - entry.updatedAt > TOKEN_TTL_MS) {\n deleteContextToken(senderId);\n return undefined;\n }\n return entry.token;\n}\n\nexport function setContextToken(senderId: string, token: string): void {\n // Clear existing timer\n const existingTimer = timers.get(senderId);\n if (existingTimer) clearTimeout(existingTimer);\n\n store.set(senderId, { token, senderId, updatedAt: Date.now() });\n\n // Auto-cleanup after TTL\n const timer = setTimeout(() => {\n store.delete(senderId);\n timers.delete(senderId);\n }, TOKEN_TTL_MS);\n timers.set(senderId, timer);\n}\n\nexport function deleteContextToken(senderId: string): void {\n store.delete(senderId);\n const timer = timers.get(senderId);\n if (timer) {\n clearTimeout(timer);\n timers.delete(senderId);\n }\n}\n\nexport function getContextTokenStoreSize(): number {\n return store.size;\n}\n"],"mappings":"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","names":[]}
|