@elizaos/plugin-wechat 2.0.0-alpha.537 → 2.0.0-beta.1

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,89 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { Bot } from "./bot";
3
+ import { normalizePayload } from "./callback-server";
4
+ import type { ProxyClient } from "./proxy-client";
5
+ import { ReplyDispatcher } from "./reply-dispatcher";
6
+ import type { WechatMessageContext } from "./types";
7
+
8
+ describe("@elizaos/plugin-wechat", () => {
9
+ it("normalizes supported direct and group webhook payloads", () => {
10
+ expect(
11
+ normalizePayload({
12
+ data: {
13
+ type: 60001,
14
+ sender: "wxid_alice",
15
+ recipient: "wxid_bot",
16
+ content: "hello",
17
+ timestamp: 1_700_000_000,
18
+ msgId: "direct-1",
19
+ },
20
+ }),
21
+ ).toEqual(
22
+ expect.objectContaining({
23
+ id: "direct-1",
24
+ type: "text",
25
+ sender: "wxid_alice",
26
+ recipient: "wxid_bot",
27
+ content: "hello",
28
+ timestamp: 1_700_000_000,
29
+ threadId: undefined,
30
+ group: undefined,
31
+ }),
32
+ );
33
+
34
+ expect(
35
+ normalizePayload({
36
+ data: {
37
+ type: 80002,
38
+ sender: "12345@chatroom",
39
+ recipient: "wxid_bot",
40
+ imageUrl: "https://example.com/image.jpg",
41
+ roomName: "Team Chat",
42
+ timestamp: 1_700_000_001,
43
+ msgId: "group-1",
44
+ },
45
+ }),
46
+ ).toEqual(
47
+ expect.objectContaining({
48
+ id: "group-1",
49
+ type: "image",
50
+ threadId: "12345@chatroom",
51
+ group: { subject: "Team Chat" },
52
+ imageUrl: "https://example.com/image.jpg",
53
+ }),
54
+ );
55
+ });
56
+
57
+ it("deduplicates inbound messages before dispatching to runtime", () => {
58
+ const onMessage = vi.fn();
59
+ const bot = new Bot({ onMessage });
60
+ const message: WechatMessageContext = {
61
+ id: "msg-1",
62
+ type: "text",
63
+ sender: "wxid_alice",
64
+ recipient: "wxid_bot",
65
+ content: "hello",
66
+ timestamp: 1_700_000_000,
67
+ raw: {},
68
+ };
69
+
70
+ bot.handleIncoming(message);
71
+ bot.handleIncoming(message);
72
+ bot.stop();
73
+
74
+ expect(onMessage).toHaveBeenCalledTimes(1);
75
+ expect(onMessage).toHaveBeenCalledWith(message);
76
+ });
77
+
78
+ it("chunks long outgoing text through the proxy client", async () => {
79
+ const client = {
80
+ sendText: vi.fn(async () => undefined),
81
+ } as ProxyClient;
82
+ const dispatcher = new ReplyDispatcher({ client, chunkSize: 5 });
83
+
84
+ await dispatcher.sendText("wxid_alice", "hello world");
85
+
86
+ expect(client.sendText).toHaveBeenNthCalledWith(1, "wxid_alice", "hello");
87
+ expect(client.sendText).toHaveBeenNthCalledWith(2, "wxid_alice", "world");
88
+ });
89
+ });
package/src/index.ts CHANGED
@@ -1,4 +1,15 @@
1
+ import {
2
+ getConnectorAccountManager,
3
+ stringToUuid,
4
+ type Content,
5
+ type IAgentRuntime,
6
+ type Memory,
7
+ type MessageConnectorTarget,
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,335 @@ export interface Plugin {
37
48
  config: Record<string, unknown>,
38
49
  runtime: unknown,
39
50
  ) => Promise<void | (() => Promise<void>)>;
51
+ /**
52
+ * Declarative auto-enable conditions consumed by the runtime's
53
+ * plugin-auto-enable engine. Mirrors the shape on `@elizaos/core` Plugin.
54
+ */
55
+ autoEnable?: {
56
+ envKeys?: string[];
57
+ connectorKeys?: string[];
58
+ shouldEnable?: (
59
+ env: Record<string, string | undefined>,
60
+ config: Record<string, unknown>,
61
+ ) => boolean;
62
+ };
40
63
  }
41
64
 
42
65
  let channel: WechatChannel | null = null;
43
66
 
67
+ type RuntimeWithWechatConnector = {
68
+ registerMessageConnector?: (registration: Record<string, unknown>) => void;
69
+ getMessageConnectors?: () => Array<{
70
+ source?: string;
71
+ fetchMessages?: (
72
+ context: { runtime: IAgentRuntime; target?: TargetInfo },
73
+ params?: WechatConnectorReadParams,
74
+ ) => Promise<Memory[]>;
75
+ }>;
76
+ registerSendHandler?: (
77
+ source: string,
78
+ handler: (
79
+ runtime: IAgentRuntime,
80
+ target: TargetInfo,
81
+ content: Content,
82
+ ) => Promise<void>,
83
+ ) => void;
84
+ };
85
+
86
+ type WechatConnectorReadParams = {
87
+ target?: TargetInfo;
88
+ limit?: number;
89
+ query?: string;
90
+ };
91
+
92
+ function readRuntimeSetting(runtime: unknown, key: string): string | undefined {
93
+ const value = (runtime as { getSetting?: (setting: string) => unknown })
94
+ .getSetting?.(key);
95
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
96
+ }
97
+
98
+ function resolveWechatConfig(
99
+ config: Record<string, unknown>,
100
+ runtime: unknown,
101
+ ): WechatConfig | undefined {
102
+ const explicit = (config as { connectors?: { wechat?: WechatConfig } })
103
+ ?.connectors?.wechat;
104
+ if (explicit) return explicit;
105
+ const apiKey = readRuntimeSetting(runtime, "WECHAT_API_KEY");
106
+ const proxyUrl = readRuntimeSetting(runtime, "WECHAT_PROXY_URL");
107
+ if (!apiKey && !proxyUrl) return undefined;
108
+ return {
109
+ apiKey,
110
+ proxyUrl,
111
+ };
112
+ }
113
+
114
+ function normalizeConnectorLimit(
115
+ limit: number | undefined,
116
+ fallback = 50,
117
+ ): number {
118
+ if (!Number.isFinite(limit) || !limit || limit <= 0) {
119
+ return fallback;
120
+ }
121
+ return Math.min(Math.floor(limit), 200);
122
+ }
123
+
124
+ function getConfiguredAccountIds(config: WechatConfig): string[] {
125
+ if (config.accounts && typeof config.accounts === "object") {
126
+ return Object.entries(config.accounts)
127
+ .filter(
128
+ ([, account]) => account.enabled !== false && Boolean(account.apiKey),
129
+ )
130
+ .map(([id]) => id);
131
+ }
132
+ return config.apiKey ? ["default"] : [];
133
+ }
134
+
135
+ function resolveWechatAccountId(
136
+ config: WechatConfig,
137
+ target?: TargetInfo,
138
+ ): string {
139
+ const metadata = (
140
+ target as (TargetInfo & { metadata?: Record<string, unknown> }) | undefined
141
+ )?.metadata;
142
+ const accountId =
143
+ typeof metadata?.accountId === "string" && metadata.accountId.trim()
144
+ ? metadata.accountId.trim()
145
+ : undefined;
146
+ if (accountId) {
147
+ return accountId;
148
+ }
149
+ return (
150
+ channel?.getAccountIds()[0] ??
151
+ getConfiguredAccountIds(config)[0] ??
152
+ "default"
153
+ );
154
+ }
155
+
156
+ function wechatTarget(
157
+ accountId: string,
158
+ wxid: string,
159
+ name: string | undefined,
160
+ kind: "user" | "group",
161
+ score = 0.55,
162
+ ): MessageConnectorTarget {
163
+ return {
164
+ target: {
165
+ source: "wechat",
166
+ channelId: wxid,
167
+ roomId: stringToUuid(`wechat:room:${accountId}:${wxid}`) as UUID,
168
+ metadata: { accountId },
169
+ } as TargetInfo,
170
+ label: name || wxid,
171
+ kind,
172
+ score,
173
+ contexts: ["social", "connectors"],
174
+ metadata: { accountId, wxid },
175
+ };
176
+ }
177
+
178
+ async function listWechatTargets(
179
+ config: WechatConfig,
180
+ ): Promise<MessageConnectorTarget[]> {
181
+ if (!channel) {
182
+ return [];
183
+ }
184
+ const targets: MessageConnectorTarget[] = [];
185
+ for (const accountId of channel.getAccountIds()) {
186
+ const contacts = await channel.listContacts(accountId).catch(() => null);
187
+ if (!contacts) {
188
+ continue;
189
+ }
190
+ targets.push(
191
+ ...contacts.friends.map((friend) =>
192
+ wechatTarget(accountId, friend.wxid, friend.name, "user"),
193
+ ),
194
+ ...contacts.chatrooms.map((chatroom) =>
195
+ wechatTarget(accountId, chatroom.wxid, chatroom.name, "group"),
196
+ ),
197
+ );
198
+ }
199
+ if (targets.length > 0) {
200
+ return targets;
201
+ }
202
+ return getConfiguredAccountIds(config).map((accountId) =>
203
+ wechatTarget(
204
+ accountId,
205
+ accountId,
206
+ `WeChat account ${accountId}`,
207
+ "user",
208
+ 0.25,
209
+ ),
210
+ );
211
+ }
212
+
213
+ function filterMemoriesByQuery(
214
+ memories: Memory[],
215
+ query: string,
216
+ limit: number,
217
+ ): Memory[] {
218
+ const normalized = query.trim().toLowerCase();
219
+ if (!normalized) {
220
+ return memories.slice(0, limit);
221
+ }
222
+ return memories
223
+ .filter((memory) => {
224
+ const text =
225
+ typeof memory.content?.text === "string" ? memory.content.text : "";
226
+ return text.toLowerCase().includes(normalized);
227
+ })
228
+ .slice(0, limit);
229
+ }
230
+
231
+ function registerWechatMessageConnector(
232
+ runtime: unknown,
233
+ config: WechatConfig,
234
+ ): void {
235
+ const connectorRuntime = runtime as RuntimeWithWechatConnector;
236
+ const sendHandler = async (
237
+ _runtime: IAgentRuntime,
238
+ target: TargetInfo,
239
+ content: Content,
240
+ ): Promise<void> => {
241
+ if (!channel) {
242
+ throw new Error("[wechat] Channel is not available");
243
+ }
244
+ const text = typeof content.text === "string" ? content.text.trim() : "";
245
+ if (!text) {
246
+ return;
247
+ }
248
+ const accountId = resolveWechatAccountId(config, target);
249
+ const to = String(target.channelId ?? target.entityId ?? "").trim();
250
+ if (!to) {
251
+ throw new Error("[wechat] target is missing channelId/entityId");
252
+ }
253
+ await channel.sendText(accountId, to, text);
254
+ };
255
+
256
+ if (typeof connectorRuntime.registerMessageConnector === "function") {
257
+ connectorRuntime.registerMessageConnector({
258
+ source: "wechat",
259
+ label: "WeChat",
260
+ description:
261
+ "WeChat connector for sending and reading stored DM/group messages.",
262
+ capabilities: [
263
+ "send_message",
264
+ "resolve_targets",
265
+ "list_rooms",
266
+ "chat_context",
267
+ ],
268
+ supportedTargetKinds: ["user", "group", "room"],
269
+ contexts: ["social", "connectors"],
270
+ resolveTargets: async (query: string) => {
271
+ const normalized = query.trim().toLowerCase();
272
+ return (await listWechatTargets(config))
273
+ .map((target) => {
274
+ const haystack =
275
+ `${target.label ?? ""} ${target.target.channelId ?? ""}`.toLowerCase();
276
+ return {
277
+ ...target,
278
+ score:
279
+ normalized && haystack.includes(normalized)
280
+ ? 0.8
281
+ : (target.score ?? 0.4),
282
+ };
283
+ })
284
+ .filter((target) => !normalized || (target.score ?? 0) >= 0.8)
285
+ .slice(0, 25);
286
+ },
287
+ listRecentTargets: async () =>
288
+ (await listWechatTargets(config)).slice(0, 10),
289
+ listRooms: async () => listWechatTargets(config),
290
+ fetchMessages: async (
291
+ context: { runtime: IAgentRuntime; target?: TargetInfo },
292
+ params?: WechatConnectorReadParams,
293
+ ) => {
294
+ const limit = normalizeConnectorLimit(params?.limit);
295
+ const target = params?.target ?? context.target;
296
+ if (target?.roomId) {
297
+ return context.runtime.getMemories({
298
+ tableName: "messages",
299
+ roomId: target.roomId,
300
+ limit,
301
+ orderBy: "createdAt",
302
+ orderDirection: "desc",
303
+ });
304
+ }
305
+ const targets = (await listWechatTargets(config)).slice(0, 10);
306
+ const chunks = await Promise.all(
307
+ targets
308
+ .map((candidate) => candidate.target.roomId)
309
+ .filter((roomId): roomId is UUID => Boolean(roomId))
310
+ .map((roomId) =>
311
+ context.runtime.getMemories({
312
+ tableName: "messages",
313
+ roomId,
314
+ limit,
315
+ orderBy: "createdAt",
316
+ orderDirection: "desc",
317
+ }),
318
+ ),
319
+ );
320
+ return chunks
321
+ .flat()
322
+ .sort((left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0))
323
+ .slice(0, limit);
324
+ },
325
+ searchMessages: async (
326
+ context: { runtime: IAgentRuntime; target?: TargetInfo },
327
+ params: WechatConnectorReadParams & { query: string },
328
+ ) => {
329
+ const limit = normalizeConnectorLimit(params.limit);
330
+ const registration = connectorRuntime
331
+ .getMessageConnectors?.()
332
+ .find((connector) => connector.source === "wechat") as
333
+ | {
334
+ fetchMessages?: (
335
+ context: { runtime: IAgentRuntime; target?: TargetInfo },
336
+ params?: WechatConnectorReadParams,
337
+ ) => Promise<Memory[]>;
338
+ }
339
+ | undefined;
340
+ const messages =
341
+ (await registration?.fetchMessages?.(context, {
342
+ target: params.target ?? context.target,
343
+ limit: Math.max(limit, 100),
344
+ })) ?? [];
345
+ return filterMemoriesByQuery(messages, params.query, limit);
346
+ },
347
+ sendHandler,
348
+ });
349
+ return;
350
+ }
351
+
352
+ connectorRuntime.registerSendHandler?.("wechat", sendHandler);
353
+ }
354
+
44
355
  const wechatPlugin: Plugin = {
45
356
  name: "wechat",
46
357
  description: "WeChat messaging via proxy API",
47
358
 
359
+ // Self-declared auto-enable: activate when the "wechat" connector is
360
+ // configured under config.connectors. The hardcoded CONNECTOR_PLUGINS map
361
+ // in plugin-auto-enable-engine.ts still serves as a fallback.
362
+ autoEnable: {
363
+ connectorKeys: ["wechat"],
364
+ },
365
+
48
366
  async init(config: Record<string, unknown>, runtime: unknown) {
49
- const wechatConfig = (config as { connectors?: { wechat?: WechatConfig } })
50
- ?.connectors?.wechat;
367
+ try {
368
+ const manager = getConnectorAccountManager(runtime as IAgentRuntime);
369
+ manager.registerProvider(
370
+ createWechatConnectorAccountProvider(runtime as IAgentRuntime),
371
+ );
372
+ } catch (err) {
373
+ console.warn(
374
+ "[wechat] Failed to register provider with ConnectorAccountManager:",
375
+ err instanceof Error ? err.message : String(err),
376
+ );
377
+ }
378
+
379
+ const wechatConfig = resolveWechatConfig(config, runtime);
51
380
 
52
381
  if (!wechatConfig) {
53
382
  console.warn("[wechat] No wechat config found in connectors — skipping");
@@ -77,6 +406,7 @@ const wechatPlugin: Plugin = {
77
406
  });
78
407
 
79
408
  await channel.start();
409
+ registerWechatMessageConnector(runtime, wechatConfig);
80
410
  console.log("[wechat] Plugin initialized");
81
411
 
82
412
  // Return cleanup function
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 };