@elizaos/plugin-wechat 2.0.0-alpha.537

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,75 @@
1
+ import type { ProxyClient } from "./proxy-client";
2
+
3
+ const DEFAULT_CHUNK_SIZE = 2000;
4
+
5
+ export interface ReplyDispatcherOptions {
6
+ client: ProxyClient;
7
+ chunkSize?: number;
8
+ }
9
+
10
+ export class ReplyDispatcher {
11
+ private readonly client: ProxyClient;
12
+ private readonly chunkSize: number;
13
+
14
+ constructor(options: ReplyDispatcherOptions) {
15
+ this.client = options.client;
16
+ this.chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
17
+ }
18
+
19
+ async sendText(to: string, text: string): Promise<void> {
20
+ const chunks = this.chunk(text);
21
+ for (const chunk of chunks) {
22
+ try {
23
+ await this.client.sendText(to, chunk);
24
+ } catch (err) {
25
+ console.error(`[wechat] Failed to send text to ${to}:`, err);
26
+ throw err;
27
+ }
28
+ }
29
+ }
30
+
31
+ async sendImage(
32
+ to: string,
33
+ imagePath: string,
34
+ caption?: string,
35
+ ): Promise<void> {
36
+ try {
37
+ await this.client.sendImage(to, imagePath, caption);
38
+ } catch (err) {
39
+ console.error(`[wechat] Failed to send image to ${to}:`, err);
40
+ throw err;
41
+ }
42
+ }
43
+
44
+ private chunk(text: string): string[] {
45
+ if (text.length <= this.chunkSize) {
46
+ return [text];
47
+ }
48
+
49
+ const chunks: string[] = [];
50
+ let remaining = text;
51
+
52
+ while (remaining.length > 0) {
53
+ if (remaining.length <= this.chunkSize) {
54
+ chunks.push(remaining);
55
+ break;
56
+ }
57
+
58
+ // Try to break at a newline
59
+ let breakAt = remaining.lastIndexOf("\n", this.chunkSize);
60
+ if (breakAt <= 0) {
61
+ // Try to break at a space
62
+ breakAt = remaining.lastIndexOf(" ", this.chunkSize);
63
+ }
64
+ if (breakAt <= 0) {
65
+ // Hard break
66
+ breakAt = this.chunkSize;
67
+ }
68
+
69
+ chunks.push(remaining.slice(0, breakAt));
70
+ remaining = remaining.slice(breakAt).trimStart();
71
+ }
72
+
73
+ return chunks;
74
+ }
75
+ }
@@ -0,0 +1,135 @@
1
+ import type { Content } from "@elizaos/core";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { deliverIncomingWechatMessage } from "./runtime-bridge";
4
+ import type { WechatMessageContext } from "./types";
5
+
6
+ function createMessage(
7
+ overrides: Partial<WechatMessageContext> = {},
8
+ ): WechatMessageContext {
9
+ return {
10
+ id: "wechat-msg-1",
11
+ type: "text",
12
+ sender: "wxid-alice",
13
+ recipient: "wxid-agent",
14
+ content: "hello agent",
15
+ timestamp: 123,
16
+ raw: { type: 60001 },
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function createRuntime(overrides: Record<string, unknown> = {}) {
22
+ return {
23
+ agentId: "00000000-0000-0000-0000-000000000001",
24
+ ensureConnection: vi.fn().mockResolvedValue(undefined),
25
+ createMemory: vi.fn().mockResolvedValue(undefined),
26
+ emitEvent: vi.fn().mockResolvedValue(undefined),
27
+ logger: {
28
+ warn: vi.fn(),
29
+ error: vi.fn(),
30
+ },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe("deliverIncomingWechatMessage", () => {
36
+ it("routes incoming messages through the runtime message API and reply callback", async () => {
37
+ const sendText = vi.fn().mockResolvedValue(undefined);
38
+ const sendMessage = vi.fn(
39
+ async (
40
+ _runtime: unknown,
41
+ _memory: unknown,
42
+ options?: {
43
+ onResponse?: (content: Content) => Promise<unknown>;
44
+ },
45
+ ) => {
46
+ await options?.onResponse?.({ text: "reply from agent" } as Content);
47
+ return { responseContent: { text: "reply from agent" } as Content };
48
+ },
49
+ );
50
+ const runtime = createRuntime({
51
+ elizaOS: { sendMessage },
52
+ });
53
+
54
+ await deliverIncomingWechatMessage({
55
+ runtime,
56
+ accountId: "main",
57
+ message: createMessage(),
58
+ sendText,
59
+ });
60
+
61
+ expect(runtime.ensureConnection).toHaveBeenCalledWith(
62
+ expect.objectContaining({
63
+ entityId: expect.any(String),
64
+ roomId: expect.any(String),
65
+ userName: "wxid-alice",
66
+ userId: "wxid-alice",
67
+ source: "wechat",
68
+ channelId: "wxid-alice",
69
+ worldName: "WeChat",
70
+ }),
71
+ );
72
+ expect(sendMessage).toHaveBeenCalledTimes(1);
73
+
74
+ const [, memory] = sendMessage.mock.calls[0];
75
+ expect(memory).toEqual(
76
+ expect.objectContaining({
77
+ content: expect.objectContaining({
78
+ text: "hello agent",
79
+ source: "wechat",
80
+ channelType: "DM",
81
+ }),
82
+ metadata: expect.objectContaining({
83
+ type: "message",
84
+ source: "wechat",
85
+ provider: "wechat",
86
+ fromId: "wxid-alice",
87
+ wechat: expect.objectContaining({
88
+ userId: "wxid-alice",
89
+ messageId: "wechat-msg-1",
90
+ }),
91
+ }),
92
+ }),
93
+ );
94
+
95
+ expect(sendText).toHaveBeenCalledTimes(1);
96
+ expect(sendText).toHaveBeenCalledWith(
97
+ "main",
98
+ "wxid-alice",
99
+ "reply from agent",
100
+ );
101
+ expect(runtime.createMemory).toHaveBeenCalledWith(
102
+ expect.objectContaining({
103
+ content: expect.objectContaining({
104
+ text: "reply from agent",
105
+ source: "wechat",
106
+ channelType: "DM",
107
+ }),
108
+ }),
109
+ "messages",
110
+ );
111
+ });
112
+
113
+ it("falls back to MESSAGE_RECEIVED events when no message pipeline is available", async () => {
114
+ const runtime = createRuntime();
115
+
116
+ await deliverIncomingWechatMessage({
117
+ runtime,
118
+ accountId: "main",
119
+ message: createMessage(),
120
+ sendText: vi.fn().mockResolvedValue(undefined),
121
+ });
122
+
123
+ expect(runtime.emitEvent).toHaveBeenCalledWith(["MESSAGE_RECEIVED"], {
124
+ runtime,
125
+ message: expect.objectContaining({
126
+ content: expect.objectContaining({
127
+ text: "hello agent",
128
+ source: "wechat",
129
+ }),
130
+ }),
131
+ callback: expect.any(Function),
132
+ source: "wechat",
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,259 @@
1
+ import { type Content, type Memory, stringToUuid } from "@elizaos/core";
2
+ import type { WechatMessageContext } from "./types";
3
+
4
+ type ResponseCallback = (content: Content) => Promise<Memory[]>;
5
+
6
+ type RuntimeLike = {
7
+ agentId?: string;
8
+ ensureConnection?: (details: Record<string, unknown>) => Promise<unknown>;
9
+ elizaOS?: {
10
+ sendMessage?: (
11
+ runtime: unknown,
12
+ message: Memory,
13
+ options?: { onResponse?: ResponseCallback },
14
+ ) => Promise<{ responseContent?: Content } | undefined>;
15
+ };
16
+ messageService?: {
17
+ handleMessage?: (
18
+ runtime: unknown,
19
+ message: Memory,
20
+ onResponse: ResponseCallback,
21
+ ) => Promise<{ responseContent?: Content } | undefined>;
22
+ };
23
+ emitEvent?: (events: string[], payload: unknown) => Promise<unknown>;
24
+ createMemory?: (memory: Memory, tableName: string) => Promise<unknown>;
25
+ logger?: {
26
+ warn?: (...args: unknown[]) => void;
27
+ error?: (...args: unknown[]) => void;
28
+ };
29
+ };
30
+
31
+ export interface IncomingWechatDeliveryOptions {
32
+ runtime: unknown;
33
+ accountId: string;
34
+ message: WechatMessageContext;
35
+ sendText: (accountId: string, to: string, text: string) => Promise<void>;
36
+ }
37
+
38
+ export async function deliverIncomingWechatMessage(
39
+ options: IncomingWechatDeliveryOptions,
40
+ ): Promise<void> {
41
+ const runtime = options.runtime as RuntimeLike;
42
+ const agentId =
43
+ typeof runtime.agentId === "string" && runtime.agentId.length > 0
44
+ ? runtime.agentId
45
+ : stringToUuid("wechat-agent");
46
+ const incomingMemory = buildIncomingMemory(
47
+ agentId,
48
+ options.accountId,
49
+ options.message,
50
+ );
51
+ const replyTarget = resolveReplyTarget(options.message);
52
+ let replyIndex = 0;
53
+ let replyDelivered = false;
54
+
55
+ const onResponse: ResponseCallback = async (content) => {
56
+ const replyText = extractReplyText(content);
57
+ if (!replyText) {
58
+ return [];
59
+ }
60
+
61
+ replyDelivered = true;
62
+ await options.sendText(options.accountId, replyTarget, replyText);
63
+
64
+ const replyMemory = buildReplyMemory(
65
+ agentId,
66
+ options.accountId,
67
+ options.message,
68
+ replyText,
69
+ replyIndex,
70
+ );
71
+ replyIndex += 1;
72
+
73
+ await runtime.createMemory?.(replyMemory, "messages");
74
+ return [replyMemory];
75
+ };
76
+
77
+ await runtime.ensureConnection?.({
78
+ entityId: incomingMemory.entityId,
79
+ roomId: incomingMemory.roomId,
80
+ worldId: stringToUuid(`wechat:world:${options.accountId}`),
81
+ userName: options.message.sender,
82
+ userId: options.message.sender,
83
+ name: options.message.sender,
84
+ source: "wechat",
85
+ type: getChannelType(options.message),
86
+ channelId: resolveChannelId(options.message),
87
+ worldName: "WeChat",
88
+ });
89
+
90
+ if (typeof runtime.elizaOS?.sendMessage === "function") {
91
+ const result = await runtime.elizaOS.sendMessage(
92
+ options.runtime,
93
+ incomingMemory,
94
+ { onResponse },
95
+ );
96
+ await maybeHandleResponseContent(result, replyDelivered, onResponse);
97
+ return;
98
+ }
99
+
100
+ if (typeof runtime.messageService?.handleMessage === "function") {
101
+ const result = await runtime.messageService.handleMessage(
102
+ options.runtime,
103
+ incomingMemory,
104
+ onResponse,
105
+ );
106
+ await maybeHandleResponseContent(result, replyDelivered, onResponse);
107
+ return;
108
+ }
109
+
110
+ if (typeof runtime.emitEvent === "function") {
111
+ await runtime.emitEvent(["MESSAGE_RECEIVED"], {
112
+ runtime: options.runtime,
113
+ message: incomingMemory,
114
+ callback: onResponse,
115
+ source: "wechat",
116
+ });
117
+ return;
118
+ }
119
+
120
+ runtime.logger?.warn?.(
121
+ "[wechat] No inbound runtime message pipeline is available",
122
+ );
123
+ }
124
+
125
+ function buildIncomingMemory(
126
+ agentId: string,
127
+ accountId: string,
128
+ message: WechatMessageContext,
129
+ ): Memory {
130
+ return {
131
+ id: stringToUuid(`wechat:incoming:${accountId}:${message.id}`),
132
+ agentId,
133
+ entityId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
134
+ roomId: stringToUuid(
135
+ `wechat:room:${accountId}:${resolveChannelId(message)}`,
136
+ ),
137
+ createdAt: message.timestamp,
138
+ content: {
139
+ text: message.content,
140
+ source: "wechat",
141
+ channelType: getChannelType(message),
142
+ metadata: {
143
+ accountId,
144
+ sender: message.sender,
145
+ recipient: message.recipient,
146
+ messageType: message.type,
147
+ threadId: message.threadId,
148
+ groupSubject: message.group?.subject,
149
+ imageUrl: message.imageUrl,
150
+ },
151
+ },
152
+ metadata: {
153
+ type: "message",
154
+ source: "wechat",
155
+ provider: "wechat",
156
+ timestamp: message.timestamp,
157
+ entityName: message.sender,
158
+ entityUserName: message.sender,
159
+ fromId: message.sender,
160
+ sourceId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
161
+ chatType: getChannelType(message),
162
+ messageIdFull: message.id,
163
+ sender: {
164
+ id: message.sender,
165
+ name: message.sender,
166
+ username: message.sender,
167
+ },
168
+ wechat: {
169
+ id: message.sender,
170
+ userId: message.sender,
171
+ username: message.sender,
172
+ userName: message.sender,
173
+ name: message.sender,
174
+ messageId: message.id,
175
+ accountId,
176
+ recipient: message.recipient,
177
+ threadId: message.threadId,
178
+ groupSubject: message.group?.subject,
179
+ },
180
+ },
181
+ } as Memory;
182
+ }
183
+
184
+ function buildReplyMemory(
185
+ agentId: string,
186
+ accountId: string,
187
+ message: WechatMessageContext,
188
+ text: string,
189
+ replyIndex: number,
190
+ ): Memory {
191
+ return {
192
+ id: stringToUuid(`wechat:reply:${accountId}:${message.id}:${replyIndex}`),
193
+ agentId,
194
+ entityId: agentId,
195
+ roomId: stringToUuid(
196
+ `wechat:room:${accountId}:${resolveChannelId(message)}`,
197
+ ),
198
+ createdAt: Date.now(),
199
+ content: {
200
+ text,
201
+ source: "wechat",
202
+ channelType: getChannelType(message),
203
+ inReplyTo: message.id,
204
+ metadata: {
205
+ accountId,
206
+ recipient: resolveReplyTarget(message),
207
+ },
208
+ },
209
+ metadata: {
210
+ type: "message",
211
+ source: "wechat",
212
+ provider: "wechat",
213
+ timestamp: Date.now(),
214
+ fromBot: true,
215
+ fromId: agentId,
216
+ sourceId: agentId,
217
+ chatType: getChannelType(message),
218
+ messageIdFull: `wechat:reply:${message.id}:${replyIndex}`,
219
+ wechat: {
220
+ accountId,
221
+ recipient: resolveReplyTarget(message),
222
+ threadId: message.threadId,
223
+ },
224
+ },
225
+ } as Memory;
226
+ }
227
+
228
+ function getChannelType(message: WechatMessageContext): "DM" | "GROUP" {
229
+ return message.group ? "GROUP" : "DM";
230
+ }
231
+
232
+ function resolveChannelId(message: WechatMessageContext): string {
233
+ return message.threadId ?? message.sender;
234
+ }
235
+
236
+ function resolveReplyTarget(message: WechatMessageContext): string {
237
+ return message.threadId ?? message.sender;
238
+ }
239
+
240
+ function extractReplyText(content: Content): string | null {
241
+ if (typeof content.text !== "string") {
242
+ return null;
243
+ }
244
+
245
+ const trimmed = content.text.trim();
246
+ return trimmed.length > 0 ? trimmed : null;
247
+ }
248
+
249
+ async function maybeHandleResponseContent(
250
+ result: { responseContent?: Content } | undefined,
251
+ replyDelivered: boolean,
252
+ onResponse: ResponseCallback,
253
+ ): Promise<void> {
254
+ if (replyDelivered || !result?.responseContent) {
255
+ return;
256
+ }
257
+
258
+ await onResponse(result.responseContent);
259
+ }
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ export type DeviceType = "ipad" | "mac";
2
+ export type LoginStatus = "waiting" | "need_verify" | "logged_in";
3
+
4
+ export interface WechatAccountConfig {
5
+ enabled?: boolean;
6
+ name?: string;
7
+ apiKey: string;
8
+ proxyUrl: string;
9
+ deviceType?: DeviceType;
10
+ webhookPort?: number;
11
+ webhookUrl?: string;
12
+ wcId?: string;
13
+ nickName?: string;
14
+ }
15
+
16
+ export interface WechatConfig {
17
+ enabled?: boolean;
18
+ apiKey?: string;
19
+ proxyUrl?: string;
20
+ webhookPort?: number;
21
+ deviceType?: DeviceType;
22
+ loginTimeoutMs?: number;
23
+ accounts?: Record<string, WechatAccountConfig>;
24
+ features?: {
25
+ images?: boolean;
26
+ groups?: boolean;
27
+ };
28
+ }
29
+
30
+ export interface ResolvedWechatAccount {
31
+ id: string;
32
+ apiKey: string;
33
+ proxyUrl: string;
34
+ deviceType: DeviceType;
35
+ webhookPort: number;
36
+ wcId?: string;
37
+ nickName?: string;
38
+ }
39
+
40
+ export type WechatMessageType =
41
+ | "text"
42
+ | "image"
43
+ | "video"
44
+ | "file"
45
+ | "voice"
46
+ | "unknown";
47
+
48
+ export interface WechatMessageContext {
49
+ id: string;
50
+ type: WechatMessageType;
51
+ sender: string;
52
+ recipient: string;
53
+ content: string;
54
+ timestamp: number;
55
+ threadId?: string;
56
+ group?: {
57
+ subject: string;
58
+ };
59
+ imageUrl?: string;
60
+ raw: unknown;
61
+ }
62
+
63
+ export interface AccountStatus {
64
+ valid: boolean;
65
+ wcId?: string;
66
+ loginState: LoginStatus;
67
+ nickName?: string;
68
+ tier?: string;
69
+ quota?: number;
70
+ }
71
+
72
+ export interface ProxyApiResponse<T> {
73
+ code: number;
74
+ message?: string;
75
+ data?: T;
76
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Display a QR code URL to the terminal.
3
+ * Prints the URL for the user to open in a browser.
4
+ * A vendored text-based QR renderer could be added here later.
5
+ */
6
+ export function displayQRUrl(url: string): void {
7
+ console.log("");
8
+ console.log("╔══════════════════════════════════════════╗");
9
+ console.log("║ Scan this QR code with WeChat to login ║");
10
+ console.log("╠══════════════════════════════════════════╣");
11
+ console.log(`║ ${url}`);
12
+ console.log("╚══════════════════════════════════════════╝");
13
+ console.log("");
14
+ console.log("Open the URL above in your browser to see the QR code.");
15
+ console.log("");
16
+ }