@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.
@@ -1,159 +0,0 @@
1
- import { stringToUuid } from "@elizaos/core";
2
-
3
- //#region src/runtime-bridge.ts
4
- async function deliverIncomingWechatMessage(options) {
5
- const runtime = options.runtime;
6
- const agentId = typeof runtime.agentId === "string" && runtime.agentId.length > 0 ? runtime.agentId : stringToUuid("wechat-agent");
7
- const incomingMemory = buildIncomingMemory(agentId, options.accountId, options.message);
8
- const replyTarget = resolveReplyTarget(options.message);
9
- let replyIndex = 0;
10
- let replyDelivered = false;
11
- const onResponse = async (content) => {
12
- const replyText = extractReplyText(content);
13
- if (!replyText) return [];
14
- replyDelivered = true;
15
- await options.sendText(options.accountId, replyTarget, replyText);
16
- const replyMemory = buildReplyMemory(agentId, options.accountId, options.message, replyText, replyIndex);
17
- replyIndex += 1;
18
- await runtime.createMemory?.(replyMemory, "messages");
19
- return [replyMemory];
20
- };
21
- await runtime.ensureConnection?.({
22
- entityId: incomingMemory.entityId,
23
- roomId: incomingMemory.roomId,
24
- worldId: stringToUuid(`wechat:world:${options.accountId}`),
25
- userName: options.message.sender,
26
- userId: options.message.sender,
27
- name: options.message.sender,
28
- source: "wechat",
29
- type: getChannelType(options.message),
30
- channelId: resolveChannelId(options.message),
31
- worldName: "WeChat"
32
- });
33
- if (typeof runtime.elizaOS?.sendMessage === "function") {
34
- await maybeHandleResponseContent(await runtime.elizaOS.sendMessage(options.runtime, incomingMemory, { onResponse }), replyDelivered, onResponse);
35
- return;
36
- }
37
- if (typeof runtime.messageService?.handleMessage === "function") {
38
- await maybeHandleResponseContent(await runtime.messageService.handleMessage(options.runtime, incomingMemory, onResponse), replyDelivered, onResponse);
39
- return;
40
- }
41
- if (typeof runtime.emitEvent === "function") {
42
- await runtime.emitEvent(["MESSAGE_RECEIVED"], {
43
- runtime: options.runtime,
44
- message: incomingMemory,
45
- callback: onResponse,
46
- source: "wechat"
47
- });
48
- return;
49
- }
50
- runtime.logger?.warn?.("[wechat] No inbound runtime message pipeline is available");
51
- }
52
- function buildIncomingMemory(agentId, accountId, message) {
53
- return {
54
- id: stringToUuid(`wechat:incoming:${accountId}:${message.id}`),
55
- agentId,
56
- entityId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
57
- roomId: stringToUuid(`wechat:room:${accountId}:${resolveChannelId(message)}`),
58
- createdAt: message.timestamp,
59
- content: {
60
- text: message.content,
61
- source: "wechat",
62
- channelType: getChannelType(message),
63
- metadata: {
64
- accountId,
65
- sender: message.sender,
66
- recipient: message.recipient,
67
- messageType: message.type,
68
- threadId: message.threadId,
69
- groupSubject: message.group?.subject,
70
- imageUrl: message.imageUrl
71
- }
72
- },
73
- metadata: {
74
- type: "message",
75
- source: "wechat",
76
- provider: "wechat",
77
- timestamp: message.timestamp,
78
- entityName: message.sender,
79
- entityUserName: message.sender,
80
- fromId: message.sender,
81
- sourceId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
82
- chatType: getChannelType(message),
83
- messageIdFull: message.id,
84
- sender: {
85
- id: message.sender,
86
- name: message.sender,
87
- username: message.sender
88
- },
89
- wechat: {
90
- id: message.sender,
91
- userId: message.sender,
92
- username: message.sender,
93
- userName: message.sender,
94
- name: message.sender,
95
- messageId: message.id,
96
- accountId,
97
- recipient: message.recipient,
98
- threadId: message.threadId,
99
- groupSubject: message.group?.subject
100
- }
101
- }
102
- };
103
- }
104
- function buildReplyMemory(agentId, accountId, message, text, replyIndex) {
105
- return {
106
- id: stringToUuid(`wechat:reply:${accountId}:${message.id}:${replyIndex}`),
107
- agentId,
108
- entityId: agentId,
109
- roomId: stringToUuid(`wechat:room:${accountId}:${resolveChannelId(message)}`),
110
- createdAt: Date.now(),
111
- content: {
112
- text,
113
- source: "wechat",
114
- channelType: getChannelType(message),
115
- inReplyTo: message.id,
116
- metadata: {
117
- accountId,
118
- recipient: resolveReplyTarget(message)
119
- }
120
- },
121
- metadata: {
122
- type: "message",
123
- source: "wechat",
124
- provider: "wechat",
125
- timestamp: Date.now(),
126
- fromBot: true,
127
- fromId: agentId,
128
- sourceId: agentId,
129
- chatType: getChannelType(message),
130
- messageIdFull: `wechat:reply:${message.id}:${replyIndex}`,
131
- wechat: {
132
- accountId,
133
- recipient: resolveReplyTarget(message),
134
- threadId: message.threadId
135
- }
136
- }
137
- };
138
- }
139
- function getChannelType(message) {
140
- return message.group ? "GROUP" : "DM";
141
- }
142
- function resolveChannelId(message) {
143
- return message.threadId ?? message.sender;
144
- }
145
- function resolveReplyTarget(message) {
146
- return message.threadId ?? message.sender;
147
- }
148
- function extractReplyText(content) {
149
- if (typeof content.text !== "string") return null;
150
- const trimmed = content.text.trim();
151
- return trimmed.length > 0 ? trimmed : null;
152
- }
153
- async function maybeHandleResponseContent(result, replyDelivered, onResponse) {
154
- if (replyDelivered || !result?.responseContent) return;
155
- await onResponse(result.responseContent);
156
- }
157
-
158
- //#endregion
159
- export { deliverIncomingWechatMessage };
package/dist/types.d.ts DELETED
@@ -1,61 +0,0 @@
1
- //#region src/types.d.ts
2
- type DeviceType = "ipad" | "mac";
3
- type LoginStatus = "waiting" | "need_verify" | "logged_in";
4
- 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
- interface WechatConfig {
16
- enabled?: boolean;
17
- apiKey?: string;
18
- proxyUrl?: string;
19
- webhookPort?: number;
20
- deviceType?: DeviceType;
21
- loginTimeoutMs?: number;
22
- accounts?: Record<string, WechatAccountConfig>;
23
- features?: {
24
- images?: boolean;
25
- groups?: boolean;
26
- };
27
- }
28
- interface ResolvedWechatAccount {
29
- id: string;
30
- apiKey: string;
31
- proxyUrl: string;
32
- deviceType: DeviceType;
33
- webhookPort: number;
34
- wcId?: string;
35
- nickName?: string;
36
- }
37
- type WechatMessageType = "text" | "image" | "video" | "file" | "voice" | "unknown";
38
- interface WechatMessageContext {
39
- id: string;
40
- type: WechatMessageType;
41
- sender: string;
42
- recipient: string;
43
- content: string;
44
- timestamp: number;
45
- threadId?: string;
46
- group?: {
47
- subject: string;
48
- };
49
- imageUrl?: string;
50
- raw: unknown;
51
- }
52
- interface AccountStatus {
53
- valid: boolean;
54
- wcId?: string;
55
- loginState: LoginStatus;
56
- nickName?: string;
57
- tier?: string;
58
- quota?: number;
59
- }
60
- //#endregion
61
- export { AccountStatus, ResolvedWechatAccount, WechatConfig, WechatMessageContext };
@@ -1,20 +0,0 @@
1
- //#region src/utils/qrcode.ts
2
- /**
3
- * Display a QR code URL to the terminal.
4
- * Prints the URL for the user to open in a browser.
5
- * A vendored text-based QR renderer could be added here later.
6
- */
7
- function displayQRUrl(url) {
8
- console.log("");
9
- console.log("╔══════════════════════════════════════════╗");
10
- console.log("║ Scan this QR code with WeChat to login ║");
11
- console.log("╠══════════════════════════════════════════╣");
12
- console.log(`║ ${url}`);
13
- console.log("╚══════════════════════════════════════════╝");
14
- console.log("");
15
- console.log("Open the URL above in your browser to see the QR code.");
16
- console.log("");
17
- }
18
-
19
- //#endregion
20
- export { displayQRUrl };
@@ -1,190 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { startCallbackServer } from "./callback-server";
3
-
4
- const servers: Array<{ close: () => Promise<void> }> = [];
5
-
6
- afterEach(async () => {
7
- await Promise.all(servers.splice(0).map((server) => server.close()));
8
- });
9
-
10
- describe("startCallbackServer", () => {
11
- it("routes a webhook to the matching account and validates that account's API key", async () => {
12
- const onMessage = vi.fn();
13
- const server = await startCallbackServer({
14
- port: 0,
15
- accounts: [
16
- { accountId: "main", apiKey: "main-key" },
17
- { accountId: "secondary", apiKey: "secondary-key" },
18
- ],
19
- onMessage,
20
- });
21
- servers.push(server);
22
-
23
- const response = await fetch(
24
- `http://127.0.0.1:${server.port}/webhook/wechat/secondary`,
25
- {
26
- method: "POST",
27
- headers: {
28
- "content-type": "application/json",
29
- "x-api-key": "secondary-key",
30
- },
31
- body: JSON.stringify({
32
- type: 60001,
33
- sender: "wxid-alice",
34
- recipient: "wxid-agent",
35
- content: "hello",
36
- timestamp: 123,
37
- }),
38
- },
39
- );
40
-
41
- expect(response.status).toBe(200);
42
- expect(onMessage).toHaveBeenCalledWith(
43
- "secondary",
44
- expect.objectContaining({
45
- sender: "wxid-alice",
46
- content: "hello",
47
- }),
48
- );
49
- });
50
-
51
- it("rejects requests signed with another account's API key", async () => {
52
- const onMessage = vi.fn();
53
- const server = await startCallbackServer({
54
- port: 0,
55
- accounts: [
56
- { accountId: "main", apiKey: "main-key" },
57
- { accountId: "secondary", apiKey: "secondary-key" },
58
- ],
59
- onMessage,
60
- });
61
- servers.push(server);
62
-
63
- const response = await fetch(
64
- `http://127.0.0.1:${server.port}/webhook/wechat/secondary`,
65
- {
66
- method: "POST",
67
- headers: {
68
- "content-type": "application/json",
69
- "x-api-key": "main-key",
70
- },
71
- body: JSON.stringify({
72
- type: 60001,
73
- sender: "wxid-alice",
74
- recipient: "wxid-agent",
75
- content: "hello",
76
- timestamp: 123,
77
- }),
78
- },
79
- );
80
-
81
- expect(response.status).toBe(401);
82
- expect(onMessage).not.toHaveBeenCalled();
83
- });
84
-
85
- it("rejects oversized webhook payloads", async () => {
86
- const onMessage = vi.fn();
87
- const server = await startCallbackServer({
88
- port: 0,
89
- accounts: [{ accountId: "main", apiKey: "main-key" }],
90
- maxBodyBytes: 64,
91
- onMessage,
92
- });
93
- servers.push(server);
94
-
95
- const response = await fetch(
96
- `http://127.0.0.1:${server.port}/webhook/wechat/main`,
97
- {
98
- method: "POST",
99
- headers: {
100
- "content-type": "application/json",
101
- "x-api-key": "main-key",
102
- },
103
- body: JSON.stringify({
104
- type: 60001,
105
- sender: "wxid-alice",
106
- recipient: "wxid-agent",
107
- content: "x".repeat(512),
108
- timestamp: 123,
109
- }),
110
- },
111
- );
112
-
113
- expect(response.status).toBe(413);
114
- expect(onMessage).not.toHaveBeenCalled();
115
- });
116
-
117
- it("maps voice message type correctly", async () => {
118
- const onMessage = vi.fn();
119
- const server = await startCallbackServer({
120
- port: 0,
121
- accounts: [{ accountId: "main", apiKey: "key" }],
122
- onMessage,
123
- });
124
- servers.push(server);
125
-
126
- const response = await fetch(
127
- `http://127.0.0.1:${server.port}/webhook/wechat/main`,
128
- {
129
- method: "POST",
130
- headers: { "content-type": "application/json", "x-api-key": "key" },
131
- body: JSON.stringify({
132
- data: {
133
- type: 60003,
134
- sender: "wxid-alice",
135
- recipient: "wxid-agent",
136
- content: "",
137
- timestamp: 456,
138
- mediaUrl: "https://example.com/voice.amr",
139
- },
140
- }),
141
- },
142
- );
143
-
144
- expect(response.status).toBe(200);
145
- expect(onMessage).toHaveBeenCalledWith(
146
- "main",
147
- expect.objectContaining({
148
- type: "voice",
149
- imageUrl: "https://example.com/voice.amr",
150
- }),
151
- );
152
- });
153
-
154
- it("maps group video message type correctly", async () => {
155
- const onMessage = vi.fn();
156
- const server = await startCallbackServer({
157
- port: 0,
158
- accounts: [{ accountId: "main", apiKey: "key" }],
159
- onMessage,
160
- });
161
- servers.push(server);
162
-
163
- const response = await fetch(
164
- `http://127.0.0.1:${server.port}/webhook/wechat/main`,
165
- {
166
- method: "POST",
167
- headers: { "content-type": "application/json", "x-api-key": "key" },
168
- body: JSON.stringify({
169
- data: {
170
- type: 80004,
171
- sender: "room@chatroom",
172
- recipient: "wxid-agent",
173
- content: "",
174
- timestamp: 789,
175
- mediaUrl: "https://example.com/video.mp4",
176
- },
177
- }),
178
- },
179
- );
180
-
181
- expect(response.status).toBe(200);
182
- expect(onMessage).toHaveBeenCalledWith(
183
- "main",
184
- expect.objectContaining({
185
- type: "video",
186
- imageUrl: "https://example.com/video.mp4",
187
- }),
188
- );
189
- });
190
- });
@@ -1,121 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { WechatChannel } from "./channel";
3
- import type { WechatMessageContext } from "./types";
4
-
5
- function createConfig() {
6
- return {
7
- accounts: {
8
- main: {
9
- apiKey: "main-key",
10
- proxyUrl: "https://proxy.example.com",
11
- },
12
- secondary: {
13
- apiKey: "secondary-key",
14
- proxyUrl: "https://proxy.example.com",
15
- },
16
- },
17
- };
18
- }
19
-
20
- function createMessage(
21
- overrides: Partial<WechatMessageContext> = {},
22
- ): WechatMessageContext {
23
- return {
24
- id: "wechat-msg-1",
25
- type: "text",
26
- sender: "wxid-alice",
27
- recipient: "wxid-agent",
28
- content: "hello agent",
29
- timestamp: 123,
30
- raw: {},
31
- ...overrides,
32
- };
33
- }
34
-
35
- afterEach(() => {
36
- vi.useRealTimers();
37
- vi.restoreAllMocks();
38
- });
39
-
40
- describe("WechatChannel", () => {
41
- it("routes inbound messages to the matching account bot", () => {
42
- const channel = new WechatChannel({
43
- config: createConfig(),
44
- onMessage: vi.fn(),
45
- });
46
- const mainBot = { handleIncoming: vi.fn(), stop: vi.fn() };
47
- const secondaryBot = { handleIncoming: vi.fn(), stop: vi.fn() };
48
-
49
- (
50
- channel as unknown as {
51
- accounts: Map<
52
- string,
53
- { client: object; dispatcher: object; bot: typeof mainBot }
54
- >;
55
- routeIncoming: (accountId: string, msg: WechatMessageContext) => void;
56
- }
57
- ).accounts.set("main", {
58
- client: {},
59
- dispatcher: {},
60
- bot: mainBot,
61
- });
62
- (
63
- channel as unknown as {
64
- accounts: Map<
65
- string,
66
- { client: object; dispatcher: object; bot: typeof secondaryBot }
67
- >;
68
- }
69
- ).accounts.set("secondary", {
70
- client: {},
71
- dispatcher: {},
72
- bot: secondaryBot,
73
- });
74
-
75
- (
76
- channel as unknown as {
77
- routeIncoming: (accountId: string, msg: WechatMessageContext) => void;
78
- }
79
- ).routeIncoming("secondary", createMessage());
80
-
81
- expect(mainBot.handleIncoming).not.toHaveBeenCalled();
82
- expect(secondaryBot.handleIncoming).toHaveBeenCalledWith(
83
- expect.objectContaining({ id: "wechat-msg-1" }),
84
- );
85
- });
86
-
87
- it("times out login when QR verification never completes", async () => {
88
- vi.useFakeTimers();
89
- vi.spyOn(console, "log").mockImplementation(() => undefined);
90
-
91
- const channel = new WechatChannel({
92
- config: createConfig(),
93
- onMessage: vi.fn(),
94
- });
95
- const client = {
96
- getStatus: vi.fn().mockResolvedValue({
97
- valid: true,
98
- loginState: "waiting",
99
- }),
100
- getQRCode: vi.fn().mockResolvedValue("https://proxy.example.com/qr"),
101
- checkLogin: vi.fn().mockResolvedValue({ status: "waiting" }),
102
- };
103
-
104
- (
105
- channel as unknown as { abortController: AbortController | null }
106
- ).abortController = new AbortController();
107
-
108
- const loginPromise = (
109
- channel as unknown as {
110
- ensureLoggedIn: (
111
- accountId: string,
112
- client: typeof client,
113
- ) => Promise<void>;
114
- }
115
- ).ensureLoggedIn("main", client);
116
- const rejection = expect(loginPromise).rejects.toThrow(/timed out/i);
117
-
118
- await vi.advanceTimersByTimeAsync(5 * 60_000 + 5_000);
119
- await rejection;
120
- });
121
- });
@@ -1,24 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { describe, expect, it } from "vitest";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
- describe("proxy-client 429 body consumption", () => {
9
- it("consumes response body before retry on 429", () => {
10
- const source = readFileSync(
11
- path.join(__dirname, "proxy-client.ts"),
12
- "utf-8",
13
- );
14
-
15
- const idx = source.indexOf("res.status === 429");
16
- expect(idx).toBeGreaterThan(-1);
17
-
18
- const block = source.slice(idx, idx + 500);
19
- expect(block).toContain("res.text()");
20
- expect(block.indexOf("res.text()")).toBeLessThan(
21
- block.indexOf("continue;"),
22
- );
23
- });
24
- });
@@ -1,46 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { ProxyClient } from "./proxy-client";
3
-
4
- describe("ProxyClient", () => {
5
- afterEach(() => {
6
- vi.restoreAllMocks();
7
- });
8
-
9
- it("rejects insecure proxy URLs", () => {
10
- expect(
11
- () =>
12
- new ProxyClient({
13
- id: "main",
14
- apiKey: "main-key",
15
- proxyUrl: "http://127.0.0.1:8787",
16
- deviceType: "ipad",
17
- webhookPort: 18790,
18
- }),
19
- ).toThrow("proxyUrl must use https://");
20
- });
21
-
22
- it("sends X-Device-Type header in requests", async () => {
23
- const client = new ProxyClient({
24
- id: "test-account",
25
- apiKey: "test-key",
26
- proxyUrl: "https://proxy.example.com",
27
- deviceType: "mac",
28
- webhookPort: 18790,
29
- });
30
-
31
- let capturedHeaders: Record<string, string> = {};
32
- vi.spyOn(globalThis, "fetch").mockImplementation(async (_url, init) => {
33
- capturedHeaders = (init?.headers ?? {}) as Record<string, string>;
34
- return new Response(
35
- JSON.stringify({ code: 1000, data: { status: "logged_in" } }),
36
- { status: 200, headers: { "content-type": "application/json" } },
37
- );
38
- });
39
-
40
- await client.getStatus();
41
-
42
- expect(capturedHeaders["X-Device-Type"]).toBe("mac");
43
- expect(capturedHeaders["X-Account-ID"]).toBe("test-account");
44
- expect(capturedHeaders["X-API-Key"]).toBe("test-key");
45
- });
46
- });