@elizaos/plugin-wechat 2.0.3-beta.6 → 2.0.3-beta.7
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/dist/bot.d.ts +21 -0
- package/dist/callback-server.d.ts +16 -0
- package/dist/channel.d.ts +35 -0
- package/dist/connector-account-provider.d.ts +14 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1304 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy-client.d.ts +34 -0
- package/dist/reply-dispatcher.d.ts +13 -0
- package/dist/runtime-bridge.d.ts +8 -0
- package/dist/types.d.ts +64 -0
- package/dist/utils/qrcode.d.ts +6 -0
- package/package.json +4 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
getConnectorAccountManager,
|
|
4
|
+
stringToUuid as stringToUuid2
|
|
5
|
+
} from "@elizaos/core";
|
|
6
|
+
|
|
7
|
+
// src/bot.ts
|
|
8
|
+
var DEFAULT_DEDUP_WINDOW_MS = 30 * 60 * 1e3;
|
|
9
|
+
var DEDUP_MAX_ENTRIES = 1e3;
|
|
10
|
+
var DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
11
|
+
var Bot = class {
|
|
12
|
+
seen = /* @__PURE__ */ new Map();
|
|
13
|
+
onMessage;
|
|
14
|
+
featuresGroups;
|
|
15
|
+
featuresImages;
|
|
16
|
+
dedupWindowMs;
|
|
17
|
+
cleanupTimer = null;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.onMessage = options.onMessage;
|
|
20
|
+
this.featuresGroups = options.featuresGroups ?? true;
|
|
21
|
+
this.featuresImages = options.featuresImages ?? true;
|
|
22
|
+
this.dedupWindowMs = options.dedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS;
|
|
23
|
+
this.cleanupTimer = setInterval(
|
|
24
|
+
() => this.cleanup(),
|
|
25
|
+
DEDUP_CLEANUP_INTERVAL_MS
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
handleIncoming(message) {
|
|
29
|
+
if (this.isDuplicate(message.id)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (message.group && !this.featuresGroups) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (message.type === "image" && !this.featuresImages) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (message.type === "unknown") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
void Promise.resolve(this.onMessage(message)).catch((error) => {
|
|
42
|
+
console.error("[wechat] Failed to process inbound message:", error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
isDuplicate(messageId) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (this.seen.has(messageId)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (this.seen.size >= DEDUP_MAX_ENTRIES) {
|
|
51
|
+
this.cleanup();
|
|
52
|
+
}
|
|
53
|
+
this.seen.set(messageId, now);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
cleanup() {
|
|
57
|
+
const cutoff = Date.now() - this.dedupWindowMs;
|
|
58
|
+
for (const [id, ts] of this.seen) {
|
|
59
|
+
if (ts < cutoff) {
|
|
60
|
+
this.seen.delete(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
stop() {
|
|
65
|
+
if (this.cleanupTimer) {
|
|
66
|
+
clearInterval(this.cleanupTimer);
|
|
67
|
+
this.cleanupTimer = null;
|
|
68
|
+
}
|
|
69
|
+
this.seen.clear();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/callback-server.ts
|
|
74
|
+
import { timingSafeEqual } from "crypto";
|
|
75
|
+
import {
|
|
76
|
+
createServer
|
|
77
|
+
} from "http";
|
|
78
|
+
var WECHAT_TYPE_MAP = {
|
|
79
|
+
// Private message types
|
|
80
|
+
60001: { type: "text", scope: "private" },
|
|
81
|
+
60002: { type: "image", scope: "private" },
|
|
82
|
+
60003: { type: "voice", scope: "private" },
|
|
83
|
+
60004: { type: "video", scope: "private" },
|
|
84
|
+
60005: { type: "file", scope: "private" },
|
|
85
|
+
// Group message types
|
|
86
|
+
80001: { type: "text", scope: "group" },
|
|
87
|
+
80002: { type: "image", scope: "group" },
|
|
88
|
+
80003: { type: "voice", scope: "group" },
|
|
89
|
+
80004: { type: "video", scope: "group" },
|
|
90
|
+
80005: { type: "file", scope: "group" }
|
|
91
|
+
};
|
|
92
|
+
var DEFAULT_MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
93
|
+
async function startCallbackServer(options) {
|
|
94
|
+
const {
|
|
95
|
+
port,
|
|
96
|
+
accounts,
|
|
97
|
+
onMessage,
|
|
98
|
+
signal,
|
|
99
|
+
maxBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES
|
|
100
|
+
} = options;
|
|
101
|
+
const server = createServer((req, res) => {
|
|
102
|
+
const account = resolveWebhookAccount(req.url, accounts);
|
|
103
|
+
if (req.method !== "POST" || !account) {
|
|
104
|
+
res.writeHead(404);
|
|
105
|
+
res.end("Not Found");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const incomingKey = readHeaderValue(req.headers["x-api-key"]);
|
|
109
|
+
if (!incomingKey || !safeCompare(incomingKey, account.apiKey)) {
|
|
110
|
+
res.writeHead(401);
|
|
111
|
+
res.end("Unauthorized");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let body = "";
|
|
115
|
+
let bodyBytes = 0;
|
|
116
|
+
req.on("data", (chunk) => {
|
|
117
|
+
bodyBytes += chunk.length;
|
|
118
|
+
if (bodyBytes > maxBodyBytes) {
|
|
119
|
+
res.writeHead(413);
|
|
120
|
+
res.end("Payload Too Large");
|
|
121
|
+
req.destroy();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
body += chunk.toString();
|
|
125
|
+
});
|
|
126
|
+
req.on("end", () => {
|
|
127
|
+
if (res.writableEnded) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const payload = JSON.parse(body);
|
|
132
|
+
const message = normalizePayload(payload);
|
|
133
|
+
if (message) {
|
|
134
|
+
onMessage(account.accountId, message);
|
|
135
|
+
}
|
|
136
|
+
res.writeHead(200);
|
|
137
|
+
res.end("OK");
|
|
138
|
+
} catch {
|
|
139
|
+
res.writeHead(400);
|
|
140
|
+
res.end("Bad Request");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
req.on("error", () => {
|
|
144
|
+
if (res.writableEnded) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
res.writeHead(400);
|
|
148
|
+
res.end("Bad Request");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
const handleListening = () => {
|
|
153
|
+
server.off("error", handleError);
|
|
154
|
+
resolve();
|
|
155
|
+
};
|
|
156
|
+
const handleError = (error) => {
|
|
157
|
+
server.off("listening", handleListening);
|
|
158
|
+
reject(error);
|
|
159
|
+
};
|
|
160
|
+
server.once("listening", handleListening);
|
|
161
|
+
server.once("error", handleError);
|
|
162
|
+
server.listen(port);
|
|
163
|
+
});
|
|
164
|
+
const address = server.address();
|
|
165
|
+
const listeningPort = address?.port ?? port;
|
|
166
|
+
console.log(`[wechat] Webhook server listening on port ${listeningPort}`);
|
|
167
|
+
server.on("error", (err) => {
|
|
168
|
+
if (err.code === "EADDRINUSE") {
|
|
169
|
+
console.error(
|
|
170
|
+
`[wechat] Port ${listeningPort} already in use \u2014 webhook server failed to start`
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
console.error(`[wechat] Webhook server error:`, err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (signal) {
|
|
177
|
+
signal.addEventListener(
|
|
178
|
+
"abort",
|
|
179
|
+
() => {
|
|
180
|
+
void closeServer(server);
|
|
181
|
+
},
|
|
182
|
+
{ once: true }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
close: () => closeServer(server),
|
|
187
|
+
port: listeningPort
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function resolveWebhookAccount(rawUrl, accounts) {
|
|
191
|
+
if (!rawUrl) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const pathname = new URL(rawUrl, "http://localhost").pathname;
|
|
195
|
+
if (pathname === "/webhook/wechat" && accounts.length === 1) {
|
|
196
|
+
return accounts[0];
|
|
197
|
+
}
|
|
198
|
+
const match = /^\/webhook\/wechat\/([^/]+)$/.exec(pathname);
|
|
199
|
+
if (!match) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const accountId = decodeURIComponent(match[1]);
|
|
203
|
+
return accounts.find((account) => account.accountId === accountId) ?? null;
|
|
204
|
+
}
|
|
205
|
+
function readHeaderValue(value) {
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
return value[0];
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function safeCompare(a, b) {
|
|
212
|
+
const bufA = Buffer.from(a);
|
|
213
|
+
const bufB = Buffer.from(b);
|
|
214
|
+
if (bufA.length !== bufB.length) {
|
|
215
|
+
timingSafeEqual(bufA, bufA);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return timingSafeEqual(bufA, bufB);
|
|
219
|
+
}
|
|
220
|
+
function closeServer(server) {
|
|
221
|
+
if (!server.listening) {
|
|
222
|
+
return Promise.resolve();
|
|
223
|
+
}
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
server.close((error) => {
|
|
226
|
+
if (error) {
|
|
227
|
+
reject(error);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function normalizePayload(payload) {
|
|
235
|
+
const data = payload.data ?? (payload.content ? payload : null);
|
|
236
|
+
if (!data) {
|
|
237
|
+
console.warn("[wechat] Unrecognized webhook payload format");
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const typeCode = Number(data.type ?? data.msgType ?? 0);
|
|
241
|
+
const mapping = WECHAT_TYPE_MAP[typeCode];
|
|
242
|
+
let msgType = "unknown";
|
|
243
|
+
let scope = "private";
|
|
244
|
+
if (mapping) {
|
|
245
|
+
msgType = mapping.type;
|
|
246
|
+
scope = mapping.scope;
|
|
247
|
+
} else if (typeCode >= 60006 && typeCode <= 60010) {
|
|
248
|
+
msgType = "file";
|
|
249
|
+
scope = "private";
|
|
250
|
+
} else if (typeCode >= 80006 && typeCode <= 80010) {
|
|
251
|
+
msgType = "file";
|
|
252
|
+
scope = "group";
|
|
253
|
+
}
|
|
254
|
+
if (msgType === "unknown") {
|
|
255
|
+
console.warn(`[wechat] Unknown message type code: ${typeCode}`);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const sender = String(data.sender ?? data.from ?? "");
|
|
259
|
+
const recipient = String(data.recipient ?? data.to ?? "");
|
|
260
|
+
const content = String(data.content ?? data.text ?? "");
|
|
261
|
+
const timestamp = Number(data.timestamp ?? Date.now());
|
|
262
|
+
const msgId = String(data.msgId ?? data.id ?? `${sender}-${timestamp}`);
|
|
263
|
+
const isGroup = scope === "group" || sender.includes("@chatroom");
|
|
264
|
+
const threadId = isGroup ? String(data.roomId ?? data.threadId ?? sender) : void 0;
|
|
265
|
+
const groupSubject = isGroup ? String(data.roomName ?? data.groupName ?? threadId ?? "") : void 0;
|
|
266
|
+
const mediaTypes = /* @__PURE__ */ new Set(["image", "voice", "video", "file"]);
|
|
267
|
+
const hasMedia = mediaTypes.has(msgType);
|
|
268
|
+
const imageUrl = hasMedia ? String(data.imageUrl ?? data.mediaUrl ?? data.url ?? data.fileUrl ?? "") : void 0;
|
|
269
|
+
return {
|
|
270
|
+
id: msgId,
|
|
271
|
+
type: msgType,
|
|
272
|
+
sender,
|
|
273
|
+
recipient,
|
|
274
|
+
content,
|
|
275
|
+
timestamp,
|
|
276
|
+
threadId,
|
|
277
|
+
group: groupSubject ? { subject: groupSubject } : void 0,
|
|
278
|
+
imageUrl: imageUrl || void 0,
|
|
279
|
+
raw: payload
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/proxy-client.ts
|
|
284
|
+
var SUCCESS = 1e3;
|
|
285
|
+
var LOGIN_NEEDED = 1001;
|
|
286
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
287
|
+
var ProxyClient = class {
|
|
288
|
+
apiKey;
|
|
289
|
+
baseUrl;
|
|
290
|
+
accountId;
|
|
291
|
+
deviceType;
|
|
292
|
+
constructor(account) {
|
|
293
|
+
this.apiKey = account.apiKey;
|
|
294
|
+
this.baseUrl = normalizeProxyUrl(account.proxyUrl);
|
|
295
|
+
this.accountId = account.id;
|
|
296
|
+
this.deviceType = account.deviceType ?? "ipad";
|
|
297
|
+
}
|
|
298
|
+
async request(path, body) {
|
|
299
|
+
const url = `${this.baseUrl}${path}`;
|
|
300
|
+
const headers = {
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
"X-API-Key": this.apiKey,
|
|
303
|
+
"X-Account-ID": this.accountId,
|
|
304
|
+
"X-Device-Type": this.deviceType
|
|
305
|
+
};
|
|
306
|
+
let lastError;
|
|
307
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch(url, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers,
|
|
312
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
313
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
314
|
+
});
|
|
315
|
+
if (res.status === 429) {
|
|
316
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
317
|
+
const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** attempt, 8e3);
|
|
318
|
+
await res.text().catch(() => {
|
|
319
|
+
});
|
|
320
|
+
await sleep(delay);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const json = await res.json();
|
|
324
|
+
return json;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
327
|
+
const delay = Math.min(1e3 * 2 ** attempt, 8e3);
|
|
328
|
+
await sleep(delay);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw lastError ?? new Error(`Request failed after 3 attempts: ${path}`);
|
|
332
|
+
}
|
|
333
|
+
async getStatus() {
|
|
334
|
+
const res = await this.request("/api/status");
|
|
335
|
+
if (res.code === LOGIN_NEEDED) {
|
|
336
|
+
return {
|
|
337
|
+
valid: true,
|
|
338
|
+
loginState: "waiting"
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (res.code !== SUCCESS && res.code !== 1002) {
|
|
342
|
+
throw new Error(`getStatus failed: ${res.message ?? res.code}`);
|
|
343
|
+
}
|
|
344
|
+
return requireData(res, "getStatus");
|
|
345
|
+
}
|
|
346
|
+
async getQRCode() {
|
|
347
|
+
const res = await this.request("/api/qrcode");
|
|
348
|
+
if (res.code !== SUCCESS) {
|
|
349
|
+
throw new Error(`getQRCode failed: ${res.message ?? res.code}`);
|
|
350
|
+
}
|
|
351
|
+
return requireData(res, "getQRCode").qrCodeUrl;
|
|
352
|
+
}
|
|
353
|
+
async checkLogin() {
|
|
354
|
+
const res = await this.request("/api/check-login");
|
|
355
|
+
if (res.code !== SUCCESS && res.code !== 1002) {
|
|
356
|
+
throw new Error(`checkLogin failed: ${res.message ?? res.code}`);
|
|
357
|
+
}
|
|
358
|
+
return requireData(res, "checkLogin");
|
|
359
|
+
}
|
|
360
|
+
async sendText(to, text) {
|
|
361
|
+
const res = await this.request("/api/send-text", { to, text });
|
|
362
|
+
if (res.code === LOGIN_NEEDED) {
|
|
363
|
+
throw new LoginExpiredError();
|
|
364
|
+
}
|
|
365
|
+
if (res.code !== SUCCESS && res.code !== 1002) {
|
|
366
|
+
throw new Error(`sendText failed: ${res.message ?? res.code}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async sendImage(to, imagePath, text) {
|
|
370
|
+
const res = await this.request("/api/send-image", {
|
|
371
|
+
to,
|
|
372
|
+
imagePath,
|
|
373
|
+
text
|
|
374
|
+
});
|
|
375
|
+
if (res.code === LOGIN_NEEDED) {
|
|
376
|
+
throw new LoginExpiredError();
|
|
377
|
+
}
|
|
378
|
+
if (res.code !== SUCCESS && res.code !== 1002) {
|
|
379
|
+
throw new Error(`sendImage failed: ${res.message ?? res.code}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async getContacts() {
|
|
383
|
+
const res = await this.request("/api/contacts");
|
|
384
|
+
if (res.code !== SUCCESS) {
|
|
385
|
+
throw new Error(`getContacts failed: ${res.message ?? res.code}`);
|
|
386
|
+
}
|
|
387
|
+
return requireData(res, "getContacts");
|
|
388
|
+
}
|
|
389
|
+
async registerWebhook(url) {
|
|
390
|
+
const res = await this.request("/api/webhook/register", {
|
|
391
|
+
webhookUrl: url
|
|
392
|
+
});
|
|
393
|
+
if (res.code !== SUCCESS && res.code !== 1002) {
|
|
394
|
+
throw new Error(`registerWebhook failed: ${res.message ?? res.code}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
get needsLogin() {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var LoginExpiredError = class extends Error {
|
|
402
|
+
constructor() {
|
|
403
|
+
super("WeChat login expired \u2014 re-login required");
|
|
404
|
+
this.name = "LoginExpiredError";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function sleep(ms) {
|
|
408
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
409
|
+
}
|
|
410
|
+
function normalizeProxyUrl(proxyUrl) {
|
|
411
|
+
const parsed = new URL(proxyUrl);
|
|
412
|
+
if (parsed.protocol !== "https:") {
|
|
413
|
+
throw new Error("[wechat] proxyUrl must use https://");
|
|
414
|
+
}
|
|
415
|
+
if (parsed.username || parsed.password) {
|
|
416
|
+
throw new Error("[wechat] proxyUrl must not include credentials");
|
|
417
|
+
}
|
|
418
|
+
parsed.hash = "";
|
|
419
|
+
return parsed.toString().replace(/\/$/, "");
|
|
420
|
+
}
|
|
421
|
+
function requireData(response, action) {
|
|
422
|
+
if (response.data === void 0) {
|
|
423
|
+
throw new Error(`${action} failed: missing response data`);
|
|
424
|
+
}
|
|
425
|
+
return response.data;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/reply-dispatcher.ts
|
|
429
|
+
var DEFAULT_CHUNK_SIZE = 2e3;
|
|
430
|
+
var ReplyDispatcher = class {
|
|
431
|
+
client;
|
|
432
|
+
chunkSize;
|
|
433
|
+
constructor(options) {
|
|
434
|
+
this.client = options.client;
|
|
435
|
+
this.chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
436
|
+
}
|
|
437
|
+
async sendText(to, text) {
|
|
438
|
+
const chunks = this.chunk(text);
|
|
439
|
+
for (const chunk of chunks) {
|
|
440
|
+
try {
|
|
441
|
+
await this.client.sendText(to, chunk);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
console.error(`[wechat] Failed to send text to ${to}:`, err);
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async sendImage(to, imagePath, caption) {
|
|
449
|
+
try {
|
|
450
|
+
await this.client.sendImage(to, imagePath, caption);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
console.error(`[wechat] Failed to send image to ${to}:`, err);
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
chunk(text) {
|
|
457
|
+
if (text.length <= this.chunkSize) {
|
|
458
|
+
return [text];
|
|
459
|
+
}
|
|
460
|
+
const chunks = [];
|
|
461
|
+
let remaining = text;
|
|
462
|
+
while (remaining.length > 0) {
|
|
463
|
+
if (remaining.length <= this.chunkSize) {
|
|
464
|
+
chunks.push(remaining);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
let breakAt = remaining.lastIndexOf("\n", this.chunkSize);
|
|
468
|
+
if (breakAt <= 0) {
|
|
469
|
+
breakAt = remaining.lastIndexOf(" ", this.chunkSize);
|
|
470
|
+
}
|
|
471
|
+
if (breakAt <= 0) {
|
|
472
|
+
breakAt = this.chunkSize;
|
|
473
|
+
}
|
|
474
|
+
chunks.push(remaining.slice(0, breakAt));
|
|
475
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
476
|
+
}
|
|
477
|
+
return chunks;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/utils/qrcode.ts
|
|
482
|
+
function displayQRUrl(url) {
|
|
483
|
+
console.log("");
|
|
484
|
+
console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
485
|
+
console.log("\u2551 Scan this QR code with WeChat to login \u2551");
|
|
486
|
+
console.log("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
487
|
+
console.log(`\u2551 ${url}`);
|
|
488
|
+
console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
489
|
+
console.log("");
|
|
490
|
+
console.log("Open the URL above in your browser to see the QR code.");
|
|
491
|
+
console.log("");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/channel.ts
|
|
495
|
+
var HEALTH_CHECK_INTERVAL_MS = 6e4;
|
|
496
|
+
var LOGIN_POLL_INTERVAL_MS = 5e3;
|
|
497
|
+
var LOGIN_TIMEOUT_MS = 5 * 6e4;
|
|
498
|
+
var WechatChannel = class {
|
|
499
|
+
config;
|
|
500
|
+
onMessage;
|
|
501
|
+
accounts = /* @__PURE__ */ new Map();
|
|
502
|
+
callbackServers = [];
|
|
503
|
+
loginPromises = /* @__PURE__ */ new Map();
|
|
504
|
+
healthTimer = null;
|
|
505
|
+
abortController = null;
|
|
506
|
+
constructor(options) {
|
|
507
|
+
this.config = options.config;
|
|
508
|
+
this.onMessage = options.onMessage;
|
|
509
|
+
}
|
|
510
|
+
async start() {
|
|
511
|
+
this.abortController = new AbortController();
|
|
512
|
+
const resolved = this.resolveAccounts();
|
|
513
|
+
if (resolved.length === 0) {
|
|
514
|
+
console.warn("[wechat] No configured accounts found");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const webhookAccountsByPort = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const account of resolved) {
|
|
519
|
+
const existing = webhookAccountsByPort.get(account.webhookPort) ?? [];
|
|
520
|
+
existing.push({ accountId: account.id, apiKey: account.apiKey });
|
|
521
|
+
webhookAccountsByPort.set(account.webhookPort, existing);
|
|
522
|
+
}
|
|
523
|
+
for (const [webhookPort, accounts] of webhookAccountsByPort) {
|
|
524
|
+
try {
|
|
525
|
+
this.callbackServers.push(
|
|
526
|
+
await startCallbackServer({
|
|
527
|
+
port: webhookPort,
|
|
528
|
+
accounts,
|
|
529
|
+
onMessage: (accountId, msg) => this.routeIncoming(accountId, msg),
|
|
530
|
+
signal: this.abortController.signal
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
const accountIds = accounts.map((a) => a.accountId).join(", ");
|
|
535
|
+
console.error(
|
|
536
|
+
`[wechat] Failed to bind webhook server on port ${webhookPort} for accounts [${accountIds}]:`,
|
|
537
|
+
err
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const account of resolved) {
|
|
542
|
+
const client = new ProxyClient(account);
|
|
543
|
+
const dispatcher = new ReplyDispatcher({ client });
|
|
544
|
+
const bot = new Bot({
|
|
545
|
+
onMessage: (msg) => this.onMessage(account.id, msg),
|
|
546
|
+
featuresGroups: this.config.features?.groups,
|
|
547
|
+
featuresImages: this.config.features?.images
|
|
548
|
+
});
|
|
549
|
+
this.accounts.set(account.id, { client, dispatcher, bot });
|
|
550
|
+
await this.ensureLoggedIn(account.id, client);
|
|
551
|
+
const webhookUrl = `http://localhost:${account.webhookPort}/webhook/wechat/${account.id}`;
|
|
552
|
+
try {
|
|
553
|
+
await client.registerWebhook(webhookUrl);
|
|
554
|
+
console.log(
|
|
555
|
+
`[wechat] Account "${account.id}" registered webhook at ${webhookUrl}`
|
|
556
|
+
);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
console.error(
|
|
559
|
+
`[wechat] Failed to register webhook for "${account.id}":`,
|
|
560
|
+
err
|
|
561
|
+
);
|
|
562
|
+
throw new Error(
|
|
563
|
+
`Webhook registration failed for account "${account.id}": ${err instanceof Error ? err.message : String(err)}`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
this.healthTimer = setInterval(
|
|
568
|
+
() => this.healthCheck(),
|
|
569
|
+
HEALTH_CHECK_INTERVAL_MS
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
async stop() {
|
|
573
|
+
if (this.healthTimer) {
|
|
574
|
+
clearInterval(this.healthTimer);
|
|
575
|
+
this.healthTimer = null;
|
|
576
|
+
}
|
|
577
|
+
for (const [, { bot }] of this.accounts) {
|
|
578
|
+
bot.stop();
|
|
579
|
+
}
|
|
580
|
+
this.accounts.clear();
|
|
581
|
+
if (this.abortController) {
|
|
582
|
+
this.abortController.abort();
|
|
583
|
+
this.abortController = null;
|
|
584
|
+
}
|
|
585
|
+
const servers = this.callbackServers.splice(0);
|
|
586
|
+
await Promise.all(
|
|
587
|
+
servers.map((server) => server.close().catch(() => void 0))
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
async sendText(accountId, to, text) {
|
|
591
|
+
const entry = this.accounts.get(accountId);
|
|
592
|
+
if (!entry) throw new Error(`Unknown account: ${accountId}`);
|
|
593
|
+
try {
|
|
594
|
+
await entry.dispatcher.sendText(to, text);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (err instanceof LoginExpiredError) {
|
|
597
|
+
await this.ensureLoggedIn(accountId, entry.client);
|
|
598
|
+
await entry.dispatcher.sendText(to, text);
|
|
599
|
+
} else {
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async sendImage(accountId, to, imagePath, caption) {
|
|
605
|
+
const entry = this.accounts.get(accountId);
|
|
606
|
+
if (!entry) throw new Error(`Unknown account: ${accountId}`);
|
|
607
|
+
try {
|
|
608
|
+
await entry.dispatcher.sendImage(to, imagePath, caption);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
if (err instanceof LoginExpiredError) {
|
|
611
|
+
await this.ensureLoggedIn(accountId, entry.client);
|
|
612
|
+
await entry.dispatcher.sendImage(to, imagePath, caption);
|
|
613
|
+
} else {
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
getAccountIds() {
|
|
619
|
+
return Array.from(this.accounts.keys());
|
|
620
|
+
}
|
|
621
|
+
async listContacts(accountId) {
|
|
622
|
+
const entry = this.accounts.get(accountId);
|
|
623
|
+
if (!entry) throw new Error(`Unknown account: ${accountId}`);
|
|
624
|
+
return entry.client.getContacts();
|
|
625
|
+
}
|
|
626
|
+
routeIncoming(accountId, msg) {
|
|
627
|
+
const entry = this.accounts.get(accountId);
|
|
628
|
+
if (!entry) {
|
|
629
|
+
console.warn(
|
|
630
|
+
`[wechat] Received webhook for unknown account "${accountId}"`
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
entry.bot.handleIncoming(msg);
|
|
635
|
+
}
|
|
636
|
+
async ensureLoggedIn(accountId, client) {
|
|
637
|
+
const existing = this.loginPromises.get(accountId);
|
|
638
|
+
if (existing) {
|
|
639
|
+
return existing;
|
|
640
|
+
}
|
|
641
|
+
const promise = this.doLogin(accountId, client).finally(() => {
|
|
642
|
+
this.loginPromises.delete(accountId);
|
|
643
|
+
});
|
|
644
|
+
this.loginPromises.set(accountId, promise);
|
|
645
|
+
return promise;
|
|
646
|
+
}
|
|
647
|
+
async doLogin(accountId, client) {
|
|
648
|
+
const status = await client.getStatus();
|
|
649
|
+
if (status.loginState === "logged_in") {
|
|
650
|
+
console.log(
|
|
651
|
+
`[wechat] Account "${accountId}" logged in as ${status.nickName ?? status.wcId}`
|
|
652
|
+
);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
console.log(
|
|
656
|
+
`[wechat] Account "${accountId}" needs login \u2014 generating QR code...`
|
|
657
|
+
);
|
|
658
|
+
const qrUrl = await client.getQRCode();
|
|
659
|
+
displayQRUrl(qrUrl);
|
|
660
|
+
const timeoutMs = this.config.loginTimeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
661
|
+
const deadline = Date.now() + timeoutMs;
|
|
662
|
+
while (Date.now() < deadline) {
|
|
663
|
+
await sleep2(LOGIN_POLL_INTERVAL_MS);
|
|
664
|
+
if (this.abortController?.signal.aborted) {
|
|
665
|
+
throw new Error("Login aborted");
|
|
666
|
+
}
|
|
667
|
+
const result = await client.checkLogin();
|
|
668
|
+
if (result.status === "logged_in") {
|
|
669
|
+
console.log(
|
|
670
|
+
`[wechat] Account "${accountId}" logged in as ${result.nickName ?? result.wcId}`
|
|
671
|
+
);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (result.status === "need_verify") {
|
|
675
|
+
console.log(
|
|
676
|
+
`[wechat] Verification needed: ${result.verifyUrl ?? "check your phone"}`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
throw new Error(
|
|
681
|
+
`[wechat] Login timed out for account "${accountId}" after ${Math.round(timeoutMs / 1e3)} seconds`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
async healthCheck() {
|
|
685
|
+
for (const [accountId, { client }] of this.accounts) {
|
|
686
|
+
try {
|
|
687
|
+
const status = await client.getStatus();
|
|
688
|
+
if (status.loginState !== "logged_in") {
|
|
689
|
+
console.warn(
|
|
690
|
+
`[wechat] Account "${accountId}" login expired \u2014 attempting re-login`
|
|
691
|
+
);
|
|
692
|
+
await this.ensureLoggedIn(accountId, client);
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
console.error(`[wechat] Health check failed for "${accountId}":`, err);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
resolveAccounts() {
|
|
700
|
+
const accounts = [];
|
|
701
|
+
const rawPort = Number(process.env.ELIZA_WECHAT_WEBHOOK_PORT);
|
|
702
|
+
const envPort = Number.isFinite(rawPort) && rawPort > 0 ? rawPort : void 0;
|
|
703
|
+
const defaultPort = envPort ?? this.config.webhookPort ?? 18790;
|
|
704
|
+
const defaultDevice = this.config.deviceType ?? "ipad";
|
|
705
|
+
if (this.config.accounts) {
|
|
706
|
+
for (const [id, acc] of Object.entries(this.config.accounts)) {
|
|
707
|
+
if (acc.enabled === false) continue;
|
|
708
|
+
accounts.push({
|
|
709
|
+
id,
|
|
710
|
+
apiKey: acc.apiKey,
|
|
711
|
+
proxyUrl: acc.proxyUrl,
|
|
712
|
+
deviceType: acc.deviceType ?? defaultDevice,
|
|
713
|
+
webhookPort: acc.webhookPort ?? defaultPort,
|
|
714
|
+
wcId: acc.wcId,
|
|
715
|
+
nickName: acc.nickName
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
} else if (this.config.apiKey && this.config.proxyUrl) {
|
|
719
|
+
accounts.push({
|
|
720
|
+
id: "default",
|
|
721
|
+
apiKey: this.config.apiKey,
|
|
722
|
+
proxyUrl: this.config.proxyUrl,
|
|
723
|
+
deviceType: defaultDevice,
|
|
724
|
+
webhookPort: defaultPort
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return accounts;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
function sleep2(ms) {
|
|
731
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/connector-account-provider.ts
|
|
735
|
+
var WECHAT_PROVIDER_ID = "wechat";
|
|
736
|
+
var WECHAT_DEFAULT_ACCOUNT_ID = "default";
|
|
737
|
+
function getWechatConfig(runtime) {
|
|
738
|
+
const character = runtime.character?.settings;
|
|
739
|
+
return character?.connectors?.wechat ?? character?.wechat;
|
|
740
|
+
}
|
|
741
|
+
function listWechatAccounts(runtime) {
|
|
742
|
+
const config = getWechatConfig(runtime);
|
|
743
|
+
const result = [];
|
|
744
|
+
if (!config) {
|
|
745
|
+
const envApiKey = runtime.getSetting?.("WECHAT_API_KEY");
|
|
746
|
+
const envProxy = runtime.getSetting?.("WECHAT_PROXY_URL");
|
|
747
|
+
if (envApiKey?.trim() || envProxy?.trim()) {
|
|
748
|
+
result.push({
|
|
749
|
+
id: WECHAT_DEFAULT_ACCOUNT_ID,
|
|
750
|
+
enabled: true,
|
|
751
|
+
apiKeyConfigured: Boolean(envApiKey?.trim()),
|
|
752
|
+
proxyUrl: envProxy?.trim() || void 0
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
if (config.enabled === false) {
|
|
758
|
+
if (config.apiKey?.trim() || config.accounts) {
|
|
759
|
+
result.push({
|
|
760
|
+
id: WECHAT_DEFAULT_ACCOUNT_ID,
|
|
761
|
+
enabled: false,
|
|
762
|
+
apiKeyConfigured: Boolean(config.apiKey?.trim()),
|
|
763
|
+
proxyUrl: config.proxyUrl
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
if (config.apiKey?.trim()) {
|
|
769
|
+
result.push({
|
|
770
|
+
id: WECHAT_DEFAULT_ACCOUNT_ID,
|
|
771
|
+
enabled: true,
|
|
772
|
+
apiKeyConfigured: true,
|
|
773
|
+
proxyUrl: config.proxyUrl
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (config.accounts && typeof config.accounts === "object") {
|
|
777
|
+
for (const [id, account] of Object.entries(config.accounts)) {
|
|
778
|
+
if (!id) continue;
|
|
779
|
+
result.push({
|
|
780
|
+
id: id.trim().toLowerCase(),
|
|
781
|
+
enabled: account.enabled !== false,
|
|
782
|
+
apiKeyConfigured: Boolean(account.apiKey?.trim()),
|
|
783
|
+
proxyUrl: account.proxyUrl,
|
|
784
|
+
wcId: account.wcId,
|
|
785
|
+
nickName: account.nickName,
|
|
786
|
+
name: account.name
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
function toConnectorAccount(account) {
|
|
793
|
+
const now = Date.now();
|
|
794
|
+
return {
|
|
795
|
+
id: account.id,
|
|
796
|
+
provider: WECHAT_PROVIDER_ID,
|
|
797
|
+
label: account.name ?? account.nickName ?? account.id,
|
|
798
|
+
role: "AGENT",
|
|
799
|
+
purpose: ["messaging"],
|
|
800
|
+
accessGate: "open",
|
|
801
|
+
status: account.enabled && account.apiKeyConfigured ? "connected" : "disabled",
|
|
802
|
+
externalId: account.wcId || void 0,
|
|
803
|
+
displayHandle: account.nickName || void 0,
|
|
804
|
+
createdAt: now,
|
|
805
|
+
updatedAt: now,
|
|
806
|
+
metadata: {
|
|
807
|
+
proxyUrl: account.proxyUrl ?? "",
|
|
808
|
+
wcId: account.wcId ?? "",
|
|
809
|
+
nickName: account.nickName ?? ""
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function createWechatConnectorAccountProvider(runtime) {
|
|
814
|
+
return {
|
|
815
|
+
provider: WECHAT_PROVIDER_ID,
|
|
816
|
+
label: "WeChat",
|
|
817
|
+
listAccounts: async (_manager) => {
|
|
818
|
+
const accounts = listWechatAccounts(runtime);
|
|
819
|
+
return accounts.map(toConnectorAccount);
|
|
820
|
+
},
|
|
821
|
+
createAccount: async (input, _manager) => {
|
|
822
|
+
return {
|
|
823
|
+
...input,
|
|
824
|
+
provider: WECHAT_PROVIDER_ID,
|
|
825
|
+
role: input.role ?? "AGENT",
|
|
826
|
+
purpose: input.purpose ?? ["messaging"],
|
|
827
|
+
accessGate: input.accessGate ?? "open",
|
|
828
|
+
status: input.status ?? "pending"
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
patchAccount: async (_accountId, patch, _manager) => {
|
|
832
|
+
return { ...patch, provider: WECHAT_PROVIDER_ID };
|
|
833
|
+
},
|
|
834
|
+
deleteAccount: async (_accountId, _manager) => {
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/runtime-bridge.ts
|
|
840
|
+
import { stringToUuid } from "@elizaos/core";
|
|
841
|
+
async function deliverIncomingWechatMessage(options) {
|
|
842
|
+
const runtime = options.runtime;
|
|
843
|
+
const agentId = typeof runtime.agentId === "string" && runtime.agentId.length > 0 ? runtime.agentId : stringToUuid("wechat-agent");
|
|
844
|
+
const incomingMemory = buildIncomingMemory(
|
|
845
|
+
agentId,
|
|
846
|
+
options.accountId,
|
|
847
|
+
options.message
|
|
848
|
+
);
|
|
849
|
+
const replyTarget = resolveReplyTarget(options.message);
|
|
850
|
+
let replyIndex = 0;
|
|
851
|
+
let replyDelivered = false;
|
|
852
|
+
const onResponse = async (content) => {
|
|
853
|
+
const replyText = extractReplyText(content);
|
|
854
|
+
if (!replyText) {
|
|
855
|
+
return [];
|
|
856
|
+
}
|
|
857
|
+
replyDelivered = true;
|
|
858
|
+
await options.sendText(options.accountId, replyTarget, replyText);
|
|
859
|
+
const replyMemory = buildReplyMemory(
|
|
860
|
+
agentId,
|
|
861
|
+
options.accountId,
|
|
862
|
+
options.message,
|
|
863
|
+
replyText,
|
|
864
|
+
replyIndex
|
|
865
|
+
);
|
|
866
|
+
replyIndex += 1;
|
|
867
|
+
await runtime.createMemory?.(replyMemory, "messages");
|
|
868
|
+
return [replyMemory];
|
|
869
|
+
};
|
|
870
|
+
await runtime.ensureConnection?.({
|
|
871
|
+
entityId: incomingMemory.entityId,
|
|
872
|
+
roomId: incomingMemory.roomId,
|
|
873
|
+
worldId: stringToUuid(`wechat:world:${options.accountId}`),
|
|
874
|
+
userName: options.message.sender,
|
|
875
|
+
userId: options.message.sender,
|
|
876
|
+
name: options.message.sender,
|
|
877
|
+
source: "wechat",
|
|
878
|
+
type: getChannelType(options.message),
|
|
879
|
+
channelId: resolveChannelId(options.message),
|
|
880
|
+
worldName: "WeChat"
|
|
881
|
+
});
|
|
882
|
+
if (typeof runtime.elizaOS?.sendMessage === "function") {
|
|
883
|
+
const result = await runtime.elizaOS.sendMessage(
|
|
884
|
+
options.runtime,
|
|
885
|
+
incomingMemory,
|
|
886
|
+
{ onResponse }
|
|
887
|
+
);
|
|
888
|
+
await maybeHandleResponseContent(result, replyDelivered, onResponse);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (typeof runtime.messageService?.handleMessage === "function") {
|
|
892
|
+
const result = await runtime.messageService.handleMessage(
|
|
893
|
+
options.runtime,
|
|
894
|
+
incomingMemory,
|
|
895
|
+
onResponse
|
|
896
|
+
);
|
|
897
|
+
await maybeHandleResponseContent(result, replyDelivered, onResponse);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (typeof runtime.emitEvent === "function") {
|
|
901
|
+
await runtime.emitEvent(["MESSAGE_RECEIVED"], {
|
|
902
|
+
runtime: options.runtime,
|
|
903
|
+
message: incomingMemory,
|
|
904
|
+
callback: onResponse,
|
|
905
|
+
source: "wechat"
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
runtime.logger?.warn?.(
|
|
910
|
+
"[wechat] No inbound runtime message pipeline is available"
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
function buildIncomingMemory(agentId, accountId, message) {
|
|
914
|
+
return {
|
|
915
|
+
id: stringToUuid(`wechat:incoming:${accountId}:${message.id}`),
|
|
916
|
+
agentId,
|
|
917
|
+
entityId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
|
|
918
|
+
roomId: stringToUuid(
|
|
919
|
+
`wechat:room:${accountId}:${resolveChannelId(message)}`
|
|
920
|
+
),
|
|
921
|
+
createdAt: message.timestamp,
|
|
922
|
+
content: {
|
|
923
|
+
text: message.content,
|
|
924
|
+
source: "wechat",
|
|
925
|
+
channelType: getChannelType(message),
|
|
926
|
+
metadata: {
|
|
927
|
+
accountId,
|
|
928
|
+
sender: message.sender,
|
|
929
|
+
recipient: message.recipient,
|
|
930
|
+
messageType: message.type,
|
|
931
|
+
threadId: message.threadId,
|
|
932
|
+
groupSubject: message.group?.subject,
|
|
933
|
+
imageUrl: message.imageUrl
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
metadata: {
|
|
937
|
+
type: "message",
|
|
938
|
+
source: "wechat",
|
|
939
|
+
provider: "wechat",
|
|
940
|
+
timestamp: message.timestamp,
|
|
941
|
+
entityName: message.sender,
|
|
942
|
+
entityUserName: message.sender,
|
|
943
|
+
fromId: message.sender,
|
|
944
|
+
sourceId: stringToUuid(`wechat:entity:${accountId}:${message.sender}`),
|
|
945
|
+
chatType: getChannelType(message),
|
|
946
|
+
messageIdFull: message.id,
|
|
947
|
+
sender: {
|
|
948
|
+
id: message.sender,
|
|
949
|
+
name: message.sender,
|
|
950
|
+
username: message.sender
|
|
951
|
+
},
|
|
952
|
+
wechat: {
|
|
953
|
+
id: message.sender,
|
|
954
|
+
userId: message.sender,
|
|
955
|
+
username: message.sender,
|
|
956
|
+
userName: message.sender,
|
|
957
|
+
name: message.sender,
|
|
958
|
+
messageId: message.id,
|
|
959
|
+
accountId,
|
|
960
|
+
recipient: message.recipient,
|
|
961
|
+
threadId: message.threadId,
|
|
962
|
+
groupSubject: message.group?.subject
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function buildReplyMemory(agentId, accountId, message, text, replyIndex) {
|
|
968
|
+
return {
|
|
969
|
+
id: stringToUuid(`wechat:reply:${accountId}:${message.id}:${replyIndex}`),
|
|
970
|
+
agentId,
|
|
971
|
+
entityId: agentId,
|
|
972
|
+
roomId: stringToUuid(
|
|
973
|
+
`wechat:room:${accountId}:${resolveChannelId(message)}`
|
|
974
|
+
),
|
|
975
|
+
createdAt: Date.now(),
|
|
976
|
+
content: {
|
|
977
|
+
text,
|
|
978
|
+
source: "wechat",
|
|
979
|
+
channelType: getChannelType(message),
|
|
980
|
+
inReplyTo: message.id,
|
|
981
|
+
metadata: {
|
|
982
|
+
accountId,
|
|
983
|
+
recipient: resolveReplyTarget(message)
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
metadata: {
|
|
987
|
+
type: "message",
|
|
988
|
+
source: "wechat",
|
|
989
|
+
provider: "wechat",
|
|
990
|
+
timestamp: Date.now(),
|
|
991
|
+
fromBot: true,
|
|
992
|
+
fromId: agentId,
|
|
993
|
+
sourceId: agentId,
|
|
994
|
+
chatType: getChannelType(message),
|
|
995
|
+
messageIdFull: `wechat:reply:${message.id}:${replyIndex}`,
|
|
996
|
+
wechat: {
|
|
997
|
+
accountId,
|
|
998
|
+
recipient: resolveReplyTarget(message),
|
|
999
|
+
threadId: message.threadId
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function getChannelType(message) {
|
|
1005
|
+
return message.group ? "GROUP" : "DM";
|
|
1006
|
+
}
|
|
1007
|
+
function resolveChannelId(message) {
|
|
1008
|
+
return message.threadId ?? message.sender;
|
|
1009
|
+
}
|
|
1010
|
+
function resolveReplyTarget(message) {
|
|
1011
|
+
return message.threadId ?? message.sender;
|
|
1012
|
+
}
|
|
1013
|
+
function extractReplyText(content) {
|
|
1014
|
+
if (typeof content.text !== "string") {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
const trimmed = content.text.trim();
|
|
1018
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1019
|
+
}
|
|
1020
|
+
async function maybeHandleResponseContent(result, replyDelivered, onResponse) {
|
|
1021
|
+
if (replyDelivered || !result?.responseContent) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
await onResponse(result.responseContent);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/index.ts
|
|
1028
|
+
var WECHAT_PLUGIN_PACKAGE = "@elizaos/plugin-wechat";
|
|
1029
|
+
function isWechatConnectorConfigured(config) {
|
|
1030
|
+
if (!config || config.enabled === false) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
if (config.apiKey) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
const accounts = config.accounts;
|
|
1037
|
+
if (accounts && typeof accounts === "object") {
|
|
1038
|
+
return Object.values(
|
|
1039
|
+
accounts
|
|
1040
|
+
).some((account) => {
|
|
1041
|
+
if (account.enabled === false) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
return Boolean(account.apiKey);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
var channel = null;
|
|
1050
|
+
function readRuntimeSetting(runtime, key) {
|
|
1051
|
+
const value = runtime.getSetting?.(key);
|
|
1052
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1053
|
+
}
|
|
1054
|
+
function resolveWechatConfig(config, runtime) {
|
|
1055
|
+
const explicit = config?.connectors?.wechat;
|
|
1056
|
+
if (explicit) return explicit;
|
|
1057
|
+
const apiKey = readRuntimeSetting(runtime, "WECHAT_API_KEY");
|
|
1058
|
+
const proxyUrl = readRuntimeSetting(runtime, "WECHAT_PROXY_URL");
|
|
1059
|
+
if (!apiKey && !proxyUrl) return void 0;
|
|
1060
|
+
return {
|
|
1061
|
+
apiKey,
|
|
1062
|
+
proxyUrl
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function normalizeConnectorLimit(limit, fallback = 50) {
|
|
1066
|
+
if (!Number.isFinite(limit) || !limit || limit <= 0) {
|
|
1067
|
+
return fallback;
|
|
1068
|
+
}
|
|
1069
|
+
return Math.min(Math.floor(limit), 200);
|
|
1070
|
+
}
|
|
1071
|
+
function getConfiguredAccountIds(config) {
|
|
1072
|
+
if (config.accounts && typeof config.accounts === "object") {
|
|
1073
|
+
return Object.entries(config.accounts).filter(
|
|
1074
|
+
([, account]) => account.enabled !== false && Boolean(account.apiKey)
|
|
1075
|
+
).map(([id]) => id);
|
|
1076
|
+
}
|
|
1077
|
+
return config.apiKey ? ["default"] : [];
|
|
1078
|
+
}
|
|
1079
|
+
function resolveWechatAccountId(config, target) {
|
|
1080
|
+
const metadata = target?.metadata;
|
|
1081
|
+
const accountId = typeof metadata?.accountId === "string" && metadata.accountId.trim() ? metadata.accountId.trim() : void 0;
|
|
1082
|
+
if (accountId) {
|
|
1083
|
+
return accountId;
|
|
1084
|
+
}
|
|
1085
|
+
return channel?.getAccountIds()[0] ?? getConfiguredAccountIds(config)[0] ?? "default";
|
|
1086
|
+
}
|
|
1087
|
+
function wechatTarget(accountId, wxid, name, kind, score = 0.55) {
|
|
1088
|
+
return {
|
|
1089
|
+
target: {
|
|
1090
|
+
source: "wechat",
|
|
1091
|
+
channelId: wxid,
|
|
1092
|
+
roomId: stringToUuid2(`wechat:room:${accountId}:${wxid}`),
|
|
1093
|
+
metadata: { accountId }
|
|
1094
|
+
},
|
|
1095
|
+
label: name || wxid,
|
|
1096
|
+
kind,
|
|
1097
|
+
score,
|
|
1098
|
+
contexts: ["social", "connectors"],
|
|
1099
|
+
metadata: { accountId, wxid }
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
async function listWechatTargets(config) {
|
|
1103
|
+
if (!channel) {
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
const targets = [];
|
|
1107
|
+
for (const accountId of channel.getAccountIds()) {
|
|
1108
|
+
const contacts = await channel.listContacts(accountId).catch(() => null);
|
|
1109
|
+
if (!contacts) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
targets.push(
|
|
1113
|
+
...contacts.friends.map(
|
|
1114
|
+
(friend) => wechatTarget(accountId, friend.wxid, friend.name, "user")
|
|
1115
|
+
),
|
|
1116
|
+
...contacts.chatrooms.map(
|
|
1117
|
+
(chatroom) => wechatTarget(accountId, chatroom.wxid, chatroom.name, "group")
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (targets.length > 0) {
|
|
1122
|
+
return targets;
|
|
1123
|
+
}
|
|
1124
|
+
return getConfiguredAccountIds(config).map(
|
|
1125
|
+
(accountId) => wechatTarget(
|
|
1126
|
+
accountId,
|
|
1127
|
+
accountId,
|
|
1128
|
+
`WeChat account ${accountId}`,
|
|
1129
|
+
"user",
|
|
1130
|
+
0.25
|
|
1131
|
+
)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
function filterMemoriesByQuery(memories, query, limit) {
|
|
1135
|
+
const normalized = query.trim().toLowerCase();
|
|
1136
|
+
if (!normalized) {
|
|
1137
|
+
return memories.slice(0, limit);
|
|
1138
|
+
}
|
|
1139
|
+
return memories.filter((memory) => {
|
|
1140
|
+
const text = typeof memory.content?.text === "string" ? memory.content.text : "";
|
|
1141
|
+
return text.toLowerCase().includes(normalized);
|
|
1142
|
+
}).slice(0, limit);
|
|
1143
|
+
}
|
|
1144
|
+
function registerWechatMessageConnector(runtime, config) {
|
|
1145
|
+
const connectorRuntime = runtime;
|
|
1146
|
+
const sendHandler = async (_runtime, target, content) => {
|
|
1147
|
+
if (!channel) {
|
|
1148
|
+
throw new Error("[wechat] Channel is not available");
|
|
1149
|
+
}
|
|
1150
|
+
const text = typeof content.text === "string" ? content.text.trim() : "";
|
|
1151
|
+
if (!text) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const accountId = resolveWechatAccountId(config, target);
|
|
1155
|
+
const to = String(target.channelId ?? target.entityId ?? "").trim();
|
|
1156
|
+
if (!to) {
|
|
1157
|
+
throw new Error("[wechat] target is missing channelId/entityId");
|
|
1158
|
+
}
|
|
1159
|
+
await channel.sendText(accountId, to, text);
|
|
1160
|
+
};
|
|
1161
|
+
if (typeof connectorRuntime.registerMessageConnector === "function") {
|
|
1162
|
+
connectorRuntime.registerMessageConnector({
|
|
1163
|
+
source: "wechat",
|
|
1164
|
+
label: "WeChat",
|
|
1165
|
+
description: "WeChat connector for sending and reading stored DM/group messages.",
|
|
1166
|
+
capabilities: [
|
|
1167
|
+
"send_message",
|
|
1168
|
+
"resolve_targets",
|
|
1169
|
+
"list_rooms",
|
|
1170
|
+
"chat_context"
|
|
1171
|
+
],
|
|
1172
|
+
supportedTargetKinds: ["user", "group", "room"],
|
|
1173
|
+
contexts: ["social", "connectors"],
|
|
1174
|
+
resolveTargets: async (query) => {
|
|
1175
|
+
const normalized = query.trim().toLowerCase();
|
|
1176
|
+
return (await listWechatTargets(config)).map((target) => {
|
|
1177
|
+
const haystack = `${target.label ?? ""} ${target.target.channelId ?? ""}`.toLowerCase();
|
|
1178
|
+
return {
|
|
1179
|
+
...target,
|
|
1180
|
+
score: normalized && haystack.includes(normalized) ? 0.8 : target.score ?? 0.4
|
|
1181
|
+
};
|
|
1182
|
+
}).filter((target) => !normalized || (target.score ?? 0) >= 0.8).slice(0, 25);
|
|
1183
|
+
},
|
|
1184
|
+
listRecentTargets: async () => (await listWechatTargets(config)).slice(0, 10),
|
|
1185
|
+
listRooms: async () => listWechatTargets(config),
|
|
1186
|
+
fetchMessages: async (context, params) => {
|
|
1187
|
+
const limit = normalizeConnectorLimit(params?.limit);
|
|
1188
|
+
const target = params?.target ?? context.target;
|
|
1189
|
+
if (target?.roomId) {
|
|
1190
|
+
return context.runtime.getMemories({
|
|
1191
|
+
tableName: "messages",
|
|
1192
|
+
roomId: target.roomId,
|
|
1193
|
+
limit,
|
|
1194
|
+
orderBy: "createdAt",
|
|
1195
|
+
orderDirection: "desc"
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const targets = (await listWechatTargets(config)).slice(0, 10);
|
|
1199
|
+
const chunks = await Promise.all(
|
|
1200
|
+
targets.map((candidate) => candidate.target.roomId).filter((roomId) => Boolean(roomId)).map(
|
|
1201
|
+
(roomId) => context.runtime.getMemories({
|
|
1202
|
+
tableName: "messages",
|
|
1203
|
+
roomId,
|
|
1204
|
+
limit,
|
|
1205
|
+
orderBy: "createdAt",
|
|
1206
|
+
orderDirection: "desc"
|
|
1207
|
+
})
|
|
1208
|
+
)
|
|
1209
|
+
);
|
|
1210
|
+
return chunks.flat().sort((left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0)).slice(0, limit);
|
|
1211
|
+
},
|
|
1212
|
+
searchMessages: async (context, params) => {
|
|
1213
|
+
const limit = normalizeConnectorLimit(params.limit);
|
|
1214
|
+
const registration = connectorRuntime.getMessageConnectors?.().find((connector) => connector.source === "wechat");
|
|
1215
|
+
const messages = await registration?.fetchMessages?.(context, {
|
|
1216
|
+
target: params.target ?? context.target,
|
|
1217
|
+
limit: Math.max(limit, 100)
|
|
1218
|
+
}) ?? [];
|
|
1219
|
+
return filterMemoriesByQuery(messages, params.query, limit);
|
|
1220
|
+
},
|
|
1221
|
+
sendHandler
|
|
1222
|
+
});
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
connectorRuntime.registerSendHandler?.("wechat", sendHandler);
|
|
1226
|
+
}
|
|
1227
|
+
var wechatPlugin = {
|
|
1228
|
+
name: "wechat",
|
|
1229
|
+
description: "WeChat messaging via proxy API",
|
|
1230
|
+
// Self-declared auto-enable: activate when the "wechat" connector is
|
|
1231
|
+
// configured under config.connectors. The hardcoded CONNECTOR_PLUGINS map
|
|
1232
|
+
// in plugin-auto-enable-engine.ts still serves as a fallback.
|
|
1233
|
+
autoEnable: {
|
|
1234
|
+
connectorKeys: ["wechat"]
|
|
1235
|
+
},
|
|
1236
|
+
async init(config, runtime) {
|
|
1237
|
+
try {
|
|
1238
|
+
const manager = getConnectorAccountManager(runtime);
|
|
1239
|
+
manager.registerProvider(
|
|
1240
|
+
createWechatConnectorAccountProvider(runtime)
|
|
1241
|
+
);
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
console.warn(
|
|
1244
|
+
"[wechat] Failed to register provider with ConnectorAccountManager:",
|
|
1245
|
+
err instanceof Error ? err.message : String(err)
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const wechatConfig = resolveWechatConfig(config, runtime);
|
|
1249
|
+
if (!wechatConfig) {
|
|
1250
|
+
console.warn("[wechat] No wechat config found in connectors \u2014 skipping");
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (wechatConfig.enabled === false) {
|
|
1254
|
+
console.log("[wechat] Plugin disabled via config");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
channel = new WechatChannel({
|
|
1258
|
+
config: wechatConfig,
|
|
1259
|
+
onMessage: async (accountId, msg) => {
|
|
1260
|
+
await deliverIncomingWechatMessage({
|
|
1261
|
+
runtime,
|
|
1262
|
+
accountId,
|
|
1263
|
+
message: msg,
|
|
1264
|
+
sendText: async (replyAccountId, to, text) => {
|
|
1265
|
+
if (!channel) {
|
|
1266
|
+
throw new Error("[wechat] Channel is not available for replies");
|
|
1267
|
+
}
|
|
1268
|
+
await channel.sendText(replyAccountId, to, text);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
await channel.start();
|
|
1274
|
+
registerWechatMessageConnector(runtime, wechatConfig);
|
|
1275
|
+
console.log("[wechat] Plugin initialized");
|
|
1276
|
+
return async () => {
|
|
1277
|
+
if (channel) {
|
|
1278
|
+
await channel.stop();
|
|
1279
|
+
channel = null;
|
|
1280
|
+
console.log("[wechat] Plugin stopped");
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
},
|
|
1284
|
+
async dispose() {
|
|
1285
|
+
if (channel) {
|
|
1286
|
+
await channel.stop();
|
|
1287
|
+
channel = null;
|
|
1288
|
+
console.log("[wechat] Plugin disposed");
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
var index_default = wechatPlugin;
|
|
1293
|
+
export {
|
|
1294
|
+
Bot,
|
|
1295
|
+
ProxyClient,
|
|
1296
|
+
ReplyDispatcher,
|
|
1297
|
+
WECHAT_PLUGIN_PACKAGE,
|
|
1298
|
+
WechatChannel,
|
|
1299
|
+
index_default as default,
|
|
1300
|
+
deliverIncomingWechatMessage,
|
|
1301
|
+
isWechatConnectorConfigured,
|
|
1302
|
+
wechatPlugin
|
|
1303
|
+
};
|
|
1304
|
+
//# sourceMappingURL=index.js.map
|