@elizaos/plugin-wechat 2.0.0-alpha.537 → 2.0.3-beta.5
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/LICENSE +21 -0
- package/README.md +65 -10
- package/auto-enable.ts +21 -0
- package/package.json +36 -10
- package/src/channel.ts +13 -0
- package/src/connector-account-provider.test.ts +44 -0
- package/src/connector-account-provider.ts +169 -0
- package/src/index.test.ts +89 -0
- package/src/index.ts +341 -2
- package/src/types.ts +3 -3
- package/dist/bot.d.ts +0 -25
- package/dist/bot.js +0 -49
- package/dist/callback-server.js +0 -207
- package/dist/channel.d.ts +0 -28
- package/dist/channel.js +0 -194
- package/dist/index.d.ts +0 -173
- package/dist/index.js +0 -833
- package/dist/proxy-client.d.ts +0 -35
- package/dist/proxy-client.js +0 -117
- package/dist/reply-dispatcher.d.ts +0 -17
- package/dist/reply-dispatcher.js +0 -47
- package/dist/runtime-bridge.d.ts +0 -12
- package/dist/runtime-bridge.js +0 -159
- package/dist/types.d.ts +0 -61
- package/dist/utils/qrcode.js +0 -20
- package/src/callback-server.test.ts +0 -190
- package/src/channel.test.ts +0 -121
- package/src/proxy-client-429.test.ts +0 -24
- package/src/proxy-client.test.ts +0 -46
- package/src/runtime-bridge.test.ts +0 -135
package/src/index.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Content,
|
|
3
|
+
getConnectorAccountManager,
|
|
4
|
+
type IAgentRuntime,
|
|
5
|
+
type Memory,
|
|
6
|
+
type MessageConnectorTarget,
|
|
7
|
+
stringToUuid,
|
|
8
|
+
type TargetInfo,
|
|
9
|
+
type UUID,
|
|
10
|
+
} from "@elizaos/core";
|
|
1
11
|
import { WechatChannel } from "./channel";
|
|
12
|
+
import { createWechatConnectorAccountProvider } from "./connector-account-provider";
|
|
2
13
|
import { deliverIncomingWechatMessage } from "./runtime-bridge";
|
|
3
14
|
import type { WechatConfig, WechatMessageContext } from "./types";
|
|
4
15
|
|
|
@@ -37,17 +48,337 @@ export interface Plugin {
|
|
|
37
48
|
config: Record<string, unknown>,
|
|
38
49
|
runtime: unknown,
|
|
39
50
|
) => Promise<void | (() => Promise<void>)>;
|
|
51
|
+
dispose?: () => Promise<void> | void;
|
|
52
|
+
/**
|
|
53
|
+
* Declarative auto-enable conditions consumed by the runtime's
|
|
54
|
+
* plugin-auto-enable engine. Mirrors the shape on `@elizaos/core` Plugin.
|
|
55
|
+
*/
|
|
56
|
+
autoEnable?: {
|
|
57
|
+
envKeys?: string[];
|
|
58
|
+
connectorKeys?: string[];
|
|
59
|
+
shouldEnable?: (
|
|
60
|
+
env: Record<string, string | undefined>,
|
|
61
|
+
config: Record<string, unknown>,
|
|
62
|
+
) => boolean;
|
|
63
|
+
};
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
let channel: WechatChannel | null = null;
|
|
43
67
|
|
|
68
|
+
type RuntimeWithWechatConnector = {
|
|
69
|
+
registerMessageConnector?: (registration: Record<string, unknown>) => void;
|
|
70
|
+
getMessageConnectors?: () => Array<{
|
|
71
|
+
source?: string;
|
|
72
|
+
fetchMessages?: (
|
|
73
|
+
context: { runtime: IAgentRuntime; target?: TargetInfo },
|
|
74
|
+
params?: WechatConnectorReadParams,
|
|
75
|
+
) => Promise<Memory[]>;
|
|
76
|
+
}>;
|
|
77
|
+
registerSendHandler?: (
|
|
78
|
+
source: string,
|
|
79
|
+
handler: (
|
|
80
|
+
runtime: IAgentRuntime,
|
|
81
|
+
target: TargetInfo,
|
|
82
|
+
content: Content,
|
|
83
|
+
) => Promise<void>,
|
|
84
|
+
) => void;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type WechatConnectorReadParams = {
|
|
88
|
+
target?: TargetInfo;
|
|
89
|
+
limit?: number;
|
|
90
|
+
query?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function readRuntimeSetting(runtime: unknown, key: string): string | undefined {
|
|
94
|
+
const value = (
|
|
95
|
+
runtime as { getSetting?: (setting: string) => unknown }
|
|
96
|
+
).getSetting?.(key);
|
|
97
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveWechatConfig(
|
|
101
|
+
config: Record<string, unknown>,
|
|
102
|
+
runtime: unknown,
|
|
103
|
+
): WechatConfig | undefined {
|
|
104
|
+
const explicit = (config as { connectors?: { wechat?: WechatConfig } })
|
|
105
|
+
?.connectors?.wechat;
|
|
106
|
+
if (explicit) return explicit;
|
|
107
|
+
const apiKey = readRuntimeSetting(runtime, "WECHAT_API_KEY");
|
|
108
|
+
const proxyUrl = readRuntimeSetting(runtime, "WECHAT_PROXY_URL");
|
|
109
|
+
if (!apiKey && !proxyUrl) return undefined;
|
|
110
|
+
return {
|
|
111
|
+
apiKey,
|
|
112
|
+
proxyUrl,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeConnectorLimit(
|
|
117
|
+
limit: number | undefined,
|
|
118
|
+
fallback = 50,
|
|
119
|
+
): number {
|
|
120
|
+
if (!Number.isFinite(limit) || !limit || limit <= 0) {
|
|
121
|
+
return fallback;
|
|
122
|
+
}
|
|
123
|
+
return Math.min(Math.floor(limit), 200);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getConfiguredAccountIds(config: WechatConfig): string[] {
|
|
127
|
+
if (config.accounts && typeof config.accounts === "object") {
|
|
128
|
+
return Object.entries(config.accounts)
|
|
129
|
+
.filter(
|
|
130
|
+
([, account]) => account.enabled !== false && Boolean(account.apiKey),
|
|
131
|
+
)
|
|
132
|
+
.map(([id]) => id);
|
|
133
|
+
}
|
|
134
|
+
return config.apiKey ? ["default"] : [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveWechatAccountId(
|
|
138
|
+
config: WechatConfig,
|
|
139
|
+
target?: TargetInfo,
|
|
140
|
+
): string {
|
|
141
|
+
const metadata = (
|
|
142
|
+
target as (TargetInfo & { metadata?: Record<string, unknown> }) | undefined
|
|
143
|
+
)?.metadata;
|
|
144
|
+
const accountId =
|
|
145
|
+
typeof metadata?.accountId === "string" && metadata.accountId.trim()
|
|
146
|
+
? metadata.accountId.trim()
|
|
147
|
+
: undefined;
|
|
148
|
+
if (accountId) {
|
|
149
|
+
return accountId;
|
|
150
|
+
}
|
|
151
|
+
return (
|
|
152
|
+
channel?.getAccountIds()[0] ??
|
|
153
|
+
getConfiguredAccountIds(config)[0] ??
|
|
154
|
+
"default"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wechatTarget(
|
|
159
|
+
accountId: string,
|
|
160
|
+
wxid: string,
|
|
161
|
+
name: string | undefined,
|
|
162
|
+
kind: "user" | "group",
|
|
163
|
+
score = 0.55,
|
|
164
|
+
): MessageConnectorTarget {
|
|
165
|
+
return {
|
|
166
|
+
target: {
|
|
167
|
+
source: "wechat",
|
|
168
|
+
channelId: wxid,
|
|
169
|
+
roomId: stringToUuid(`wechat:room:${accountId}:${wxid}`) as UUID,
|
|
170
|
+
metadata: { accountId },
|
|
171
|
+
} as TargetInfo,
|
|
172
|
+
label: name || wxid,
|
|
173
|
+
kind,
|
|
174
|
+
score,
|
|
175
|
+
contexts: ["social", "connectors"],
|
|
176
|
+
metadata: { accountId, wxid },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function listWechatTargets(
|
|
181
|
+
config: WechatConfig,
|
|
182
|
+
): Promise<MessageConnectorTarget[]> {
|
|
183
|
+
if (!channel) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const targets: MessageConnectorTarget[] = [];
|
|
187
|
+
for (const accountId of channel.getAccountIds()) {
|
|
188
|
+
const contacts = await channel.listContacts(accountId).catch(() => null);
|
|
189
|
+
if (!contacts) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
targets.push(
|
|
193
|
+
...contacts.friends.map((friend) =>
|
|
194
|
+
wechatTarget(accountId, friend.wxid, friend.name, "user"),
|
|
195
|
+
),
|
|
196
|
+
...contacts.chatrooms.map((chatroom) =>
|
|
197
|
+
wechatTarget(accountId, chatroom.wxid, chatroom.name, "group"),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (targets.length > 0) {
|
|
202
|
+
return targets;
|
|
203
|
+
}
|
|
204
|
+
return getConfiguredAccountIds(config).map((accountId) =>
|
|
205
|
+
wechatTarget(
|
|
206
|
+
accountId,
|
|
207
|
+
accountId,
|
|
208
|
+
`WeChat account ${accountId}`,
|
|
209
|
+
"user",
|
|
210
|
+
0.25,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function filterMemoriesByQuery(
|
|
216
|
+
memories: Memory[],
|
|
217
|
+
query: string,
|
|
218
|
+
limit: number,
|
|
219
|
+
): Memory[] {
|
|
220
|
+
const normalized = query.trim().toLowerCase();
|
|
221
|
+
if (!normalized) {
|
|
222
|
+
return memories.slice(0, limit);
|
|
223
|
+
}
|
|
224
|
+
return memories
|
|
225
|
+
.filter((memory) => {
|
|
226
|
+
const text =
|
|
227
|
+
typeof memory.content?.text === "string" ? memory.content.text : "";
|
|
228
|
+
return text.toLowerCase().includes(normalized);
|
|
229
|
+
})
|
|
230
|
+
.slice(0, limit);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function registerWechatMessageConnector(
|
|
234
|
+
runtime: unknown,
|
|
235
|
+
config: WechatConfig,
|
|
236
|
+
): void {
|
|
237
|
+
const connectorRuntime = runtime as RuntimeWithWechatConnector;
|
|
238
|
+
const sendHandler = async (
|
|
239
|
+
_runtime: IAgentRuntime,
|
|
240
|
+
target: TargetInfo,
|
|
241
|
+
content: Content,
|
|
242
|
+
): Promise<void> => {
|
|
243
|
+
if (!channel) {
|
|
244
|
+
throw new Error("[wechat] Channel is not available");
|
|
245
|
+
}
|
|
246
|
+
const text = typeof content.text === "string" ? content.text.trim() : "";
|
|
247
|
+
if (!text) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const accountId = resolveWechatAccountId(config, target);
|
|
251
|
+
const to = String(target.channelId ?? target.entityId ?? "").trim();
|
|
252
|
+
if (!to) {
|
|
253
|
+
throw new Error("[wechat] target is missing channelId/entityId");
|
|
254
|
+
}
|
|
255
|
+
await channel.sendText(accountId, to, text);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (typeof connectorRuntime.registerMessageConnector === "function") {
|
|
259
|
+
connectorRuntime.registerMessageConnector({
|
|
260
|
+
source: "wechat",
|
|
261
|
+
label: "WeChat",
|
|
262
|
+
description:
|
|
263
|
+
"WeChat connector for sending and reading stored DM/group messages.",
|
|
264
|
+
capabilities: [
|
|
265
|
+
"send_message",
|
|
266
|
+
"resolve_targets",
|
|
267
|
+
"list_rooms",
|
|
268
|
+
"chat_context",
|
|
269
|
+
],
|
|
270
|
+
supportedTargetKinds: ["user", "group", "room"],
|
|
271
|
+
contexts: ["social", "connectors"],
|
|
272
|
+
resolveTargets: async (query: string) => {
|
|
273
|
+
const normalized = query.trim().toLowerCase();
|
|
274
|
+
return (await listWechatTargets(config))
|
|
275
|
+
.map((target) => {
|
|
276
|
+
const haystack =
|
|
277
|
+
`${target.label ?? ""} ${target.target.channelId ?? ""}`.toLowerCase();
|
|
278
|
+
return {
|
|
279
|
+
...target,
|
|
280
|
+
score:
|
|
281
|
+
normalized && haystack.includes(normalized)
|
|
282
|
+
? 0.8
|
|
283
|
+
: (target.score ?? 0.4),
|
|
284
|
+
};
|
|
285
|
+
})
|
|
286
|
+
.filter((target) => !normalized || (target.score ?? 0) >= 0.8)
|
|
287
|
+
.slice(0, 25);
|
|
288
|
+
},
|
|
289
|
+
listRecentTargets: async () =>
|
|
290
|
+
(await listWechatTargets(config)).slice(0, 10),
|
|
291
|
+
listRooms: async () => listWechatTargets(config),
|
|
292
|
+
fetchMessages: async (
|
|
293
|
+
context: { runtime: IAgentRuntime; target?: TargetInfo },
|
|
294
|
+
params?: WechatConnectorReadParams,
|
|
295
|
+
) => {
|
|
296
|
+
const limit = normalizeConnectorLimit(params?.limit);
|
|
297
|
+
const target = params?.target ?? context.target;
|
|
298
|
+
if (target?.roomId) {
|
|
299
|
+
return context.runtime.getMemories({
|
|
300
|
+
tableName: "messages",
|
|
301
|
+
roomId: target.roomId,
|
|
302
|
+
limit,
|
|
303
|
+
orderBy: "createdAt",
|
|
304
|
+
orderDirection: "desc",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const targets = (await listWechatTargets(config)).slice(0, 10);
|
|
308
|
+
const chunks = await Promise.all(
|
|
309
|
+
targets
|
|
310
|
+
.map((candidate) => candidate.target.roomId)
|
|
311
|
+
.filter((roomId): roomId is UUID => Boolean(roomId))
|
|
312
|
+
.map((roomId) =>
|
|
313
|
+
context.runtime.getMemories({
|
|
314
|
+
tableName: "messages",
|
|
315
|
+
roomId,
|
|
316
|
+
limit,
|
|
317
|
+
orderBy: "createdAt",
|
|
318
|
+
orderDirection: "desc",
|
|
319
|
+
}),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
return chunks
|
|
323
|
+
.flat()
|
|
324
|
+
.sort((left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0))
|
|
325
|
+
.slice(0, limit);
|
|
326
|
+
},
|
|
327
|
+
searchMessages: async (
|
|
328
|
+
context: { runtime: IAgentRuntime; target?: TargetInfo },
|
|
329
|
+
params: WechatConnectorReadParams & { query: string },
|
|
330
|
+
) => {
|
|
331
|
+
const limit = normalizeConnectorLimit(params.limit);
|
|
332
|
+
const registration = connectorRuntime
|
|
333
|
+
.getMessageConnectors?.()
|
|
334
|
+
.find((connector) => connector.source === "wechat") as
|
|
335
|
+
| {
|
|
336
|
+
fetchMessages?: (
|
|
337
|
+
context: { runtime: IAgentRuntime; target?: TargetInfo },
|
|
338
|
+
params?: WechatConnectorReadParams,
|
|
339
|
+
) => Promise<Memory[]>;
|
|
340
|
+
}
|
|
341
|
+
| undefined;
|
|
342
|
+
const messages =
|
|
343
|
+
(await registration?.fetchMessages?.(context, {
|
|
344
|
+
target: params.target ?? context.target,
|
|
345
|
+
limit: Math.max(limit, 100),
|
|
346
|
+
})) ?? [];
|
|
347
|
+
return filterMemoriesByQuery(messages, params.query, limit);
|
|
348
|
+
},
|
|
349
|
+
sendHandler,
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
connectorRuntime.registerSendHandler?.("wechat", sendHandler);
|
|
355
|
+
}
|
|
356
|
+
|
|
44
357
|
const wechatPlugin: Plugin = {
|
|
45
358
|
name: "wechat",
|
|
46
359
|
description: "WeChat messaging via proxy API",
|
|
47
360
|
|
|
361
|
+
// Self-declared auto-enable: activate when the "wechat" connector is
|
|
362
|
+
// configured under config.connectors. The hardcoded CONNECTOR_PLUGINS map
|
|
363
|
+
// in plugin-auto-enable-engine.ts still serves as a fallback.
|
|
364
|
+
autoEnable: {
|
|
365
|
+
connectorKeys: ["wechat"],
|
|
366
|
+
},
|
|
367
|
+
|
|
48
368
|
async init(config: Record<string, unknown>, runtime: unknown) {
|
|
49
|
-
|
|
50
|
-
|
|
369
|
+
try {
|
|
370
|
+
const manager = getConnectorAccountManager(runtime as IAgentRuntime);
|
|
371
|
+
manager.registerProvider(
|
|
372
|
+
createWechatConnectorAccountProvider(runtime as IAgentRuntime),
|
|
373
|
+
);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.warn(
|
|
376
|
+
"[wechat] Failed to register provider with ConnectorAccountManager:",
|
|
377
|
+
err instanceof Error ? err.message : String(err),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const wechatConfig = resolveWechatConfig(config, runtime);
|
|
51
382
|
|
|
52
383
|
if (!wechatConfig) {
|
|
53
384
|
console.warn("[wechat] No wechat config found in connectors — skipping");
|
|
@@ -77,6 +408,7 @@ const wechatPlugin: Plugin = {
|
|
|
77
408
|
});
|
|
78
409
|
|
|
79
410
|
await channel.start();
|
|
411
|
+
registerWechatMessageConnector(runtime, wechatConfig);
|
|
80
412
|
console.log("[wechat] Plugin initialized");
|
|
81
413
|
|
|
82
414
|
// Return cleanup function
|
|
@@ -88,6 +420,13 @@ const wechatPlugin: Plugin = {
|
|
|
88
420
|
}
|
|
89
421
|
};
|
|
90
422
|
},
|
|
423
|
+
async dispose() {
|
|
424
|
+
if (channel) {
|
|
425
|
+
await channel.stop();
|
|
426
|
+
channel = null;
|
|
427
|
+
console.log("[wechat] Plugin disposed");
|
|
428
|
+
}
|
|
429
|
+
},
|
|
91
430
|
};
|
|
92
431
|
|
|
93
432
|
export default wechatPlugin;
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type DeviceType = "ipad" | "mac";
|
|
2
|
+
type LoginStatus = "waiting" | "need_verify" | "logged_in";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface WechatAccountConfig {
|
|
5
5
|
enabled?: boolean;
|
|
6
6
|
name?: string;
|
|
7
7
|
apiKey: string;
|
package/dist/bot.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { WechatMessageContext } from "./types.js";
|
|
2
|
-
|
|
3
|
-
//#region src/bot.d.ts
|
|
4
|
-
interface BotOptions {
|
|
5
|
-
onMessage: (msg: WechatMessageContext) => void | Promise<void>;
|
|
6
|
-
featuresGroups?: boolean;
|
|
7
|
-
featuresImages?: boolean;
|
|
8
|
-
/** Deduplication window in milliseconds. Defaults to 30 minutes. */
|
|
9
|
-
dedupWindowMs?: number;
|
|
10
|
-
}
|
|
11
|
-
declare class Bot {
|
|
12
|
-
private readonly seen;
|
|
13
|
-
private readonly onMessage;
|
|
14
|
-
private readonly featuresGroups;
|
|
15
|
-
private readonly featuresImages;
|
|
16
|
-
private readonly dedupWindowMs;
|
|
17
|
-
private cleanupTimer;
|
|
18
|
-
constructor(options: BotOptions);
|
|
19
|
-
handleIncoming(message: WechatMessageContext): void;
|
|
20
|
-
private isDuplicate;
|
|
21
|
-
private cleanup;
|
|
22
|
-
stop(): void;
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
|
-
export { Bot };
|
package/dist/bot.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
//#region src/bot.ts
|
|
2
|
-
const DEFAULT_DEDUP_WINDOW_MS = 1800 * 1e3;
|
|
3
|
-
const DEDUP_MAX_ENTRIES = 1e3;
|
|
4
|
-
const DEDUP_CLEANUP_INTERVAL_MS = 300 * 1e3;
|
|
5
|
-
var Bot = class {
|
|
6
|
-
seen = /* @__PURE__ */ new Map();
|
|
7
|
-
onMessage;
|
|
8
|
-
featuresGroups;
|
|
9
|
-
featuresImages;
|
|
10
|
-
dedupWindowMs;
|
|
11
|
-
cleanupTimer = null;
|
|
12
|
-
constructor(options) {
|
|
13
|
-
this.onMessage = options.onMessage;
|
|
14
|
-
this.featuresGroups = options.featuresGroups ?? true;
|
|
15
|
-
this.featuresImages = options.featuresImages ?? true;
|
|
16
|
-
this.dedupWindowMs = options.dedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS;
|
|
17
|
-
this.cleanupTimer = setInterval(() => this.cleanup(), DEDUP_CLEANUP_INTERVAL_MS);
|
|
18
|
-
}
|
|
19
|
-
handleIncoming(message) {
|
|
20
|
-
if (this.isDuplicate(message.id)) return;
|
|
21
|
-
if (message.group && !this.featuresGroups) return;
|
|
22
|
-
if (message.type === "image" && !this.featuresImages) return;
|
|
23
|
-
if (message.type === "unknown") return;
|
|
24
|
-
Promise.resolve(this.onMessage(message)).catch((error) => {
|
|
25
|
-
console.error("[wechat] Failed to process inbound message:", error);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
isDuplicate(messageId) {
|
|
29
|
-
const now = Date.now();
|
|
30
|
-
if (this.seen.has(messageId)) return true;
|
|
31
|
-
if (this.seen.size >= DEDUP_MAX_ENTRIES) this.cleanup();
|
|
32
|
-
this.seen.set(messageId, now);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
cleanup() {
|
|
36
|
-
const cutoff = Date.now() - this.dedupWindowMs;
|
|
37
|
-
for (const [id, ts] of this.seen) if (ts < cutoff) this.seen.delete(id);
|
|
38
|
-
}
|
|
39
|
-
stop() {
|
|
40
|
-
if (this.cleanupTimer) {
|
|
41
|
-
clearInterval(this.cleanupTimer);
|
|
42
|
-
this.cleanupTimer = null;
|
|
43
|
-
}
|
|
44
|
-
this.seen.clear();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
//#endregion
|
|
49
|
-
export { Bot };
|
package/dist/callback-server.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
|
-
|
|
4
|
-
//#region src/callback-server.ts
|
|
5
|
-
const WECHAT_TYPE_MAP = {
|
|
6
|
-
60001: {
|
|
7
|
-
type: "text",
|
|
8
|
-
scope: "private"
|
|
9
|
-
},
|
|
10
|
-
60002: {
|
|
11
|
-
type: "image",
|
|
12
|
-
scope: "private"
|
|
13
|
-
},
|
|
14
|
-
60003: {
|
|
15
|
-
type: "voice",
|
|
16
|
-
scope: "private"
|
|
17
|
-
},
|
|
18
|
-
60004: {
|
|
19
|
-
type: "video",
|
|
20
|
-
scope: "private"
|
|
21
|
-
},
|
|
22
|
-
60005: {
|
|
23
|
-
type: "file",
|
|
24
|
-
scope: "private"
|
|
25
|
-
},
|
|
26
|
-
80001: {
|
|
27
|
-
type: "text",
|
|
28
|
-
scope: "group"
|
|
29
|
-
},
|
|
30
|
-
80002: {
|
|
31
|
-
type: "image",
|
|
32
|
-
scope: "group"
|
|
33
|
-
},
|
|
34
|
-
80003: {
|
|
35
|
-
type: "voice",
|
|
36
|
-
scope: "group"
|
|
37
|
-
},
|
|
38
|
-
80004: {
|
|
39
|
-
type: "video",
|
|
40
|
-
scope: "group"
|
|
41
|
-
},
|
|
42
|
-
80005: {
|
|
43
|
-
type: "file",
|
|
44
|
-
scope: "group"
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const DEFAULT_MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
48
|
-
async function startCallbackServer(options) {
|
|
49
|
-
const { port, accounts, onMessage, signal, maxBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES } = options;
|
|
50
|
-
const server = createServer((req, res) => {
|
|
51
|
-
const account = resolveWebhookAccount(req.url, accounts);
|
|
52
|
-
if (req.method !== "POST" || !account) {
|
|
53
|
-
res.writeHead(404);
|
|
54
|
-
res.end("Not Found");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const incomingKey = readHeaderValue(req.headers["x-api-key"]);
|
|
58
|
-
if (!incomingKey || !safeCompare(incomingKey, account.apiKey)) {
|
|
59
|
-
res.writeHead(401);
|
|
60
|
-
res.end("Unauthorized");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
let body = "";
|
|
64
|
-
let bodyBytes = 0;
|
|
65
|
-
req.on("data", (chunk) => {
|
|
66
|
-
bodyBytes += chunk.length;
|
|
67
|
-
if (bodyBytes > maxBodyBytes) {
|
|
68
|
-
res.writeHead(413);
|
|
69
|
-
res.end("Payload Too Large");
|
|
70
|
-
req.destroy();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
body += chunk.toString();
|
|
74
|
-
});
|
|
75
|
-
req.on("end", () => {
|
|
76
|
-
if (res.writableEnded) return;
|
|
77
|
-
try {
|
|
78
|
-
const message = normalizePayload(JSON.parse(body));
|
|
79
|
-
if (message) onMessage(account.accountId, message);
|
|
80
|
-
res.writeHead(200);
|
|
81
|
-
res.end("OK");
|
|
82
|
-
} catch {
|
|
83
|
-
res.writeHead(400);
|
|
84
|
-
res.end("Bad Request");
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
req.on("error", () => {
|
|
88
|
-
if (res.writableEnded) return;
|
|
89
|
-
res.writeHead(400);
|
|
90
|
-
res.end("Bad Request");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
await new Promise((resolve, reject) => {
|
|
94
|
-
const handleListening = () => {
|
|
95
|
-
server.off("error", handleError);
|
|
96
|
-
resolve();
|
|
97
|
-
};
|
|
98
|
-
const handleError = (error) => {
|
|
99
|
-
server.off("listening", handleListening);
|
|
100
|
-
reject(error);
|
|
101
|
-
};
|
|
102
|
-
server.once("listening", handleListening);
|
|
103
|
-
server.once("error", handleError);
|
|
104
|
-
server.listen(port);
|
|
105
|
-
});
|
|
106
|
-
const listeningPort = server.address()?.port ?? port;
|
|
107
|
-
console.log(`[wechat] Webhook server listening on port ${listeningPort}`);
|
|
108
|
-
server.on("error", (err) => {
|
|
109
|
-
if (err.code === "EADDRINUSE") console.error(`[wechat] Port ${listeningPort} already in use — webhook server failed to start`);
|
|
110
|
-
else console.error(`[wechat] Webhook server error:`, err);
|
|
111
|
-
});
|
|
112
|
-
if (signal) signal.addEventListener("abort", () => {
|
|
113
|
-
closeServer(server);
|
|
114
|
-
}, { once: true });
|
|
115
|
-
return {
|
|
116
|
-
close: () => closeServer(server),
|
|
117
|
-
port: listeningPort
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
function resolveWebhookAccount(rawUrl, accounts) {
|
|
121
|
-
if (!rawUrl) return null;
|
|
122
|
-
const pathname = new URL(rawUrl, "http://localhost").pathname;
|
|
123
|
-
if (pathname === "/webhook/wechat" && accounts.length === 1) return accounts[0];
|
|
124
|
-
const match = /^\/webhook\/wechat\/([^/]+)$/.exec(pathname);
|
|
125
|
-
if (!match) return null;
|
|
126
|
-
const accountId = decodeURIComponent(match[1]);
|
|
127
|
-
return accounts.find((account) => account.accountId === accountId) ?? null;
|
|
128
|
-
}
|
|
129
|
-
function readHeaderValue(value) {
|
|
130
|
-
if (Array.isArray(value)) return value[0];
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
function safeCompare(a, b) {
|
|
134
|
-
const bufA = Buffer.from(a);
|
|
135
|
-
const bufB = Buffer.from(b);
|
|
136
|
-
if (bufA.length !== bufB.length) {
|
|
137
|
-
timingSafeEqual(bufA, bufA);
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
return timingSafeEqual(bufA, bufB);
|
|
141
|
-
}
|
|
142
|
-
function closeServer(server) {
|
|
143
|
-
if (!server.listening) return Promise.resolve();
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
server.close((error) => {
|
|
146
|
-
if (error) {
|
|
147
|
-
reject(error);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
resolve();
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function normalizePayload(payload) {
|
|
155
|
-
const data = payload.data ?? (payload.content ? payload : null);
|
|
156
|
-
if (!data) {
|
|
157
|
-
console.warn("[wechat] Unrecognized webhook payload format");
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
const typeCode = Number(data.type ?? data.msgType ?? 0);
|
|
161
|
-
const mapping = WECHAT_TYPE_MAP[typeCode];
|
|
162
|
-
let msgType = "unknown";
|
|
163
|
-
let scope = "private";
|
|
164
|
-
if (mapping) {
|
|
165
|
-
msgType = mapping.type;
|
|
166
|
-
scope = mapping.scope;
|
|
167
|
-
} else if (typeCode >= 60006 && typeCode <= 60010) {
|
|
168
|
-
msgType = "file";
|
|
169
|
-
scope = "private";
|
|
170
|
-
} else if (typeCode >= 80006 && typeCode <= 80010) {
|
|
171
|
-
msgType = "file";
|
|
172
|
-
scope = "group";
|
|
173
|
-
}
|
|
174
|
-
if (msgType === "unknown") {
|
|
175
|
-
console.warn(`[wechat] Unknown message type code: ${typeCode}`);
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const sender = String(data.sender ?? data.from ?? "");
|
|
179
|
-
const recipient = String(data.recipient ?? data.to ?? "");
|
|
180
|
-
const content = String(data.content ?? data.text ?? "");
|
|
181
|
-
const timestamp = Number(data.timestamp ?? Date.now());
|
|
182
|
-
const msgId = String(data.msgId ?? data.id ?? `${sender}-${timestamp}`);
|
|
183
|
-
const isGroup = scope === "group" || sender.includes("@chatroom");
|
|
184
|
-
const threadId = isGroup ? String(data.roomId ?? data.threadId ?? sender) : void 0;
|
|
185
|
-
const groupSubject = isGroup ? String(data.roomName ?? data.groupName ?? threadId ?? "") : void 0;
|
|
186
|
-
const imageUrl = new Set([
|
|
187
|
-
"image",
|
|
188
|
-
"voice",
|
|
189
|
-
"video",
|
|
190
|
-
"file"
|
|
191
|
-
]).has(msgType) ? String(data.imageUrl ?? data.mediaUrl ?? data.url ?? data.fileUrl ?? "") : void 0;
|
|
192
|
-
return {
|
|
193
|
-
id: msgId,
|
|
194
|
-
type: msgType,
|
|
195
|
-
sender,
|
|
196
|
-
recipient,
|
|
197
|
-
content,
|
|
198
|
-
timestamp,
|
|
199
|
-
threadId,
|
|
200
|
-
group: groupSubject ? { subject: groupSubject } : void 0,
|
|
201
|
-
imageUrl: imageUrl || void 0,
|
|
202
|
-
raw: payload
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
//#endregion
|
|
207
|
-
export { startCallbackServer };
|
package/dist/channel.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { WechatConfig, WechatMessageContext } from "./types.js";
|
|
2
|
-
|
|
3
|
-
//#region src/channel.d.ts
|
|
4
|
-
interface ChannelOptions {
|
|
5
|
-
config: WechatConfig;
|
|
6
|
-
onMessage: (accountId: string, msg: WechatMessageContext) => void | Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
declare class WechatChannel {
|
|
9
|
-
private readonly config;
|
|
10
|
-
private readonly onMessage;
|
|
11
|
-
private readonly accounts;
|
|
12
|
-
private readonly callbackServers;
|
|
13
|
-
private readonly loginPromises;
|
|
14
|
-
private healthTimer;
|
|
15
|
-
private abortController;
|
|
16
|
-
constructor(options: ChannelOptions);
|
|
17
|
-
start(): Promise<void>;
|
|
18
|
-
stop(): Promise<void>;
|
|
19
|
-
sendText(accountId: string, to: string, text: string): Promise<void>;
|
|
20
|
-
sendImage(accountId: string, to: string, imagePath: string, caption?: string): Promise<void>;
|
|
21
|
-
private routeIncoming;
|
|
22
|
-
private ensureLoggedIn;
|
|
23
|
-
private doLogin;
|
|
24
|
-
private healthCheck;
|
|
25
|
-
private resolveAccounts;
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
|
-
export { WechatChannel };
|