@paean-ai/wechat-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/cli.js +1341 -0
- package/dist/daemon/server.js +565 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +443 -0
- package/package.json +66 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/constants.ts
|
|
13
|
+
var constants_exports = {};
|
|
14
|
+
__export(constants_exports, {
|
|
15
|
+
BACKOFF_DELAY_MS: () => BACKOFF_DELAY_MS,
|
|
16
|
+
BOT_TYPE: () => BOT_TYPE,
|
|
17
|
+
CHANNEL_VERSION: () => CHANNEL_VERSION,
|
|
18
|
+
CONFIG_DIR: () => CONFIG_DIR,
|
|
19
|
+
CONTACTS_FILE: () => CONTACTS_FILE,
|
|
20
|
+
CREDENTIALS_FILE: () => CREDENTIALS_FILE,
|
|
21
|
+
DAEMON_DEFAULT_PORT: () => DAEMON_DEFAULT_PORT,
|
|
22
|
+
DAEMON_HOST: () => DAEMON_HOST,
|
|
23
|
+
DAEMON_INFO_FILE: () => DAEMON_INFO_FILE,
|
|
24
|
+
DAEMON_LONG_POLL_MS: () => DAEMON_LONG_POLL_MS,
|
|
25
|
+
DAEMON_PID_FILE: () => DAEMON_PID_FILE,
|
|
26
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
27
|
+
LONG_POLL_TIMEOUT_MS: () => LONG_POLL_TIMEOUT_MS,
|
|
28
|
+
MAX_CONSECUTIVE_FAILURES: () => MAX_CONSECUTIVE_FAILURES,
|
|
29
|
+
MAX_WECHAT_MSG_LENGTH: () => MAX_WECHAT_MSG_LENGTH,
|
|
30
|
+
MESSAGE_QUEUE_MAX_PER_AGENT: () => MESSAGE_QUEUE_MAX_PER_AGENT,
|
|
31
|
+
MSG_ITEM_TEXT: () => MSG_ITEM_TEXT,
|
|
32
|
+
MSG_ITEM_VOICE: () => MSG_ITEM_VOICE,
|
|
33
|
+
MSG_STATE_FINISH: () => MSG_STATE_FINISH,
|
|
34
|
+
MSG_TYPE_BOT: () => MSG_TYPE_BOT,
|
|
35
|
+
MSG_TYPE_USER: () => MSG_TYPE_USER,
|
|
36
|
+
PACKAGE_NAME: () => PACKAGE_NAME,
|
|
37
|
+
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
38
|
+
QR_LOGIN_TIMEOUT_MS: () => QR_LOGIN_TIMEOUT_MS,
|
|
39
|
+
QR_POLL_TIMEOUT_MS: () => QR_POLL_TIMEOUT_MS,
|
|
40
|
+
RETRY_DELAY_MS: () => RETRY_DELAY_MS,
|
|
41
|
+
SEND_TIMEOUT_MS: () => SEND_TIMEOUT_MS,
|
|
42
|
+
SYNC_BUF_FILE: () => SYNC_BUF_FILE
|
|
43
|
+
});
|
|
44
|
+
import path from "path";
|
|
45
|
+
import os from "os";
|
|
46
|
+
var PACKAGE_NAME, PACKAGE_VERSION, DEFAULT_BASE_URL, BOT_TYPE, CHANNEL_VERSION, MSG_TYPE_USER, MSG_TYPE_BOT, MSG_STATE_FINISH, MSG_ITEM_TEXT, MSG_ITEM_VOICE, LONG_POLL_TIMEOUT_MS, QR_POLL_TIMEOUT_MS, QR_LOGIN_TIMEOUT_MS, SEND_TIMEOUT_MS, MAX_CONSECUTIVE_FAILURES, BACKOFF_DELAY_MS, RETRY_DELAY_MS, DAEMON_LONG_POLL_MS, DAEMON_DEFAULT_PORT, DAEMON_HOST, CONFIG_DIR, CREDENTIALS_FILE, SYNC_BUF_FILE, CONTACTS_FILE, DAEMON_PID_FILE, DAEMON_INFO_FILE, MAX_WECHAT_MSG_LENGTH, MESSAGE_QUEUE_MAX_PER_AGENT;
|
|
47
|
+
var init_constants = __esm({
|
|
48
|
+
"src/constants.ts"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
PACKAGE_NAME = "@paean-ai/wechat-mcp";
|
|
51
|
+
PACKAGE_VERSION = "0.1.0";
|
|
52
|
+
DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
53
|
+
BOT_TYPE = "3";
|
|
54
|
+
CHANNEL_VERSION = "0.1.0";
|
|
55
|
+
MSG_TYPE_USER = 1;
|
|
56
|
+
MSG_TYPE_BOT = 2;
|
|
57
|
+
MSG_STATE_FINISH = 2;
|
|
58
|
+
MSG_ITEM_TEXT = 1;
|
|
59
|
+
MSG_ITEM_VOICE = 3;
|
|
60
|
+
LONG_POLL_TIMEOUT_MS = 35e3;
|
|
61
|
+
QR_POLL_TIMEOUT_MS = 35e3;
|
|
62
|
+
QR_LOGIN_TIMEOUT_MS = 48e4;
|
|
63
|
+
SEND_TIMEOUT_MS = 15e3;
|
|
64
|
+
MAX_CONSECUTIVE_FAILURES = 3;
|
|
65
|
+
BACKOFF_DELAY_MS = 3e4;
|
|
66
|
+
RETRY_DELAY_MS = 2e3;
|
|
67
|
+
DAEMON_LONG_POLL_MS = 3e4;
|
|
68
|
+
DAEMON_DEFAULT_PORT = 0;
|
|
69
|
+
DAEMON_HOST = "127.0.0.1";
|
|
70
|
+
CONFIG_DIR = path.join(os.homedir(), ".wechat-mcp");
|
|
71
|
+
CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
72
|
+
SYNC_BUF_FILE = path.join(CONFIG_DIR, "sync_buf.txt");
|
|
73
|
+
CONTACTS_FILE = path.join(CONFIG_DIR, "contacts.json");
|
|
74
|
+
DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
75
|
+
DAEMON_INFO_FILE = path.join(CONFIG_DIR, "daemon.json");
|
|
76
|
+
MAX_WECHAT_MSG_LENGTH = 2048;
|
|
77
|
+
MESSAGE_QUEUE_MAX_PER_AGENT = 200;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/wechat-api.ts
|
|
82
|
+
var wechat_api_exports = {};
|
|
83
|
+
__export(wechat_api_exports, {
|
|
84
|
+
extractTextFromMessage: () => extractTextFromMessage,
|
|
85
|
+
fetchQRCode: () => fetchQRCode,
|
|
86
|
+
generateClientId: () => generateClientId,
|
|
87
|
+
getUpdates: () => getUpdates,
|
|
88
|
+
pollQRStatus: () => pollQRStatus,
|
|
89
|
+
sendTextMessage: () => sendTextMessage
|
|
90
|
+
});
|
|
91
|
+
import crypto from "crypto";
|
|
92
|
+
function randomWechatUin() {
|
|
93
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
94
|
+
return Buffer.from(String(uint32), "utf-8").toString("base64");
|
|
95
|
+
}
|
|
96
|
+
function buildHeaders(token, body) {
|
|
97
|
+
const headers = {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
AuthorizationType: "ilink_bot_token",
|
|
100
|
+
"X-WECHAT-UIN": randomWechatUin()
|
|
101
|
+
};
|
|
102
|
+
if (body) {
|
|
103
|
+
headers["Content-Length"] = String(Buffer.byteLength(body, "utf-8"));
|
|
104
|
+
}
|
|
105
|
+
if (token?.trim()) {
|
|
106
|
+
headers.Authorization = `Bearer ${token.trim()}`;
|
|
107
|
+
}
|
|
108
|
+
return headers;
|
|
109
|
+
}
|
|
110
|
+
function normalizeBaseUrl(baseUrl) {
|
|
111
|
+
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
112
|
+
}
|
|
113
|
+
async function apiFetch(baseUrl, endpoint, body, token, timeoutMs = 15e3) {
|
|
114
|
+
const url = new URL(endpoint, normalizeBaseUrl(baseUrl)).toString();
|
|
115
|
+
const headers = buildHeaders(token, body);
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers,
|
|
122
|
+
body,
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
const text = await res.text();
|
|
127
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
|
|
128
|
+
return text;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function fetchQRCode(baseUrl) {
|
|
135
|
+
const url = new URL(
|
|
136
|
+
`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(BOT_TYPE)}`,
|
|
137
|
+
normalizeBaseUrl(baseUrl)
|
|
138
|
+
);
|
|
139
|
+
const res = await fetch(url.toString());
|
|
140
|
+
if (!res.ok) throw new Error(`QR fetch failed: ${res.status}`);
|
|
141
|
+
return await res.json();
|
|
142
|
+
}
|
|
143
|
+
async function pollQRStatus(baseUrl, qrcode) {
|
|
144
|
+
const url = new URL(
|
|
145
|
+
`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
146
|
+
normalizeBaseUrl(baseUrl)
|
|
147
|
+
);
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(url.toString(), {
|
|
152
|
+
headers: { "iLink-App-ClientVersion": "1" },
|
|
153
|
+
signal: controller.signal
|
|
154
|
+
});
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
if (!res.ok) throw new Error(`QR status failed: ${res.status}`);
|
|
157
|
+
return await res.json();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
161
|
+
return { status: "wait" };
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function getUpdates(baseUrl, token, getUpdatesBuf) {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await apiFetch(
|
|
169
|
+
baseUrl,
|
|
170
|
+
"ilink/bot/getupdates",
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
get_updates_buf: getUpdatesBuf,
|
|
173
|
+
base_info: { channel_version: CHANNEL_VERSION }
|
|
174
|
+
}),
|
|
175
|
+
token,
|
|
176
|
+
LONG_POLL_TIMEOUT_MS
|
|
177
|
+
);
|
|
178
|
+
return JSON.parse(raw);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
181
|
+
return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function generateClientId() {
|
|
187
|
+
return `wechat-mcp:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
188
|
+
}
|
|
189
|
+
async function sendTextMessage(baseUrl, token, to, text, contextToken) {
|
|
190
|
+
const clientId = generateClientId();
|
|
191
|
+
await apiFetch(
|
|
192
|
+
baseUrl,
|
|
193
|
+
"ilink/bot/sendmessage",
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
msg: {
|
|
196
|
+
from_user_id: "",
|
|
197
|
+
to_user_id: to,
|
|
198
|
+
client_id: clientId,
|
|
199
|
+
message_type: MSG_TYPE_BOT,
|
|
200
|
+
message_state: MSG_STATE_FINISH,
|
|
201
|
+
item_list: [{ type: MSG_ITEM_TEXT, text_item: { text } }],
|
|
202
|
+
context_token: contextToken
|
|
203
|
+
},
|
|
204
|
+
base_info: { channel_version: CHANNEL_VERSION }
|
|
205
|
+
}),
|
|
206
|
+
token,
|
|
207
|
+
SEND_TIMEOUT_MS
|
|
208
|
+
);
|
|
209
|
+
return clientId;
|
|
210
|
+
}
|
|
211
|
+
function extractTextFromMessage(msg) {
|
|
212
|
+
if (!msg.item_list?.length) return "";
|
|
213
|
+
for (const item of msg.item_list) {
|
|
214
|
+
if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
|
|
215
|
+
const text = item.text_item.text;
|
|
216
|
+
const ref = item.ref_msg;
|
|
217
|
+
if (!ref) return text;
|
|
218
|
+
const parts = [];
|
|
219
|
+
if (ref.title) parts.push(ref.title);
|
|
220
|
+
if (!parts.length) return text;
|
|
221
|
+
return `[Quote: ${parts.join(" | ")}]
|
|
222
|
+
${text}`;
|
|
223
|
+
}
|
|
224
|
+
if (item.type === MSG_ITEM_VOICE && item.voice_item?.text) {
|
|
225
|
+
return item.voice_item.text;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
var init_wechat_api = __esm({
|
|
231
|
+
"src/wechat-api.ts"() {
|
|
232
|
+
"use strict";
|
|
233
|
+
init_constants();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// src/credentials.ts
|
|
238
|
+
var credentials_exports = {};
|
|
239
|
+
__export(credentials_exports, {
|
|
240
|
+
getContactToken: () => getContactToken,
|
|
241
|
+
loadContacts: () => loadContacts,
|
|
242
|
+
loadCredentials: () => loadCredentials,
|
|
243
|
+
loadDaemonInfo: () => loadDaemonInfo,
|
|
244
|
+
loadDaemonPid: () => loadDaemonPid,
|
|
245
|
+
loadSyncBuf: () => loadSyncBuf,
|
|
246
|
+
removeCredentials: () => removeCredentials,
|
|
247
|
+
removeDaemonInfo: () => removeDaemonInfo,
|
|
248
|
+
saveContact: () => saveContact,
|
|
249
|
+
saveCredentials: () => saveCredentials,
|
|
250
|
+
saveDaemonInfo: () => saveDaemonInfo,
|
|
251
|
+
saveDaemonPid: () => saveDaemonPid,
|
|
252
|
+
saveSyncBuf: () => saveSyncBuf
|
|
253
|
+
});
|
|
254
|
+
import fs from "fs";
|
|
255
|
+
function ensureDir() {
|
|
256
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
function loadCredentials() {
|
|
259
|
+
try {
|
|
260
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) return null;
|
|
261
|
+
return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function saveCredentials(data) {
|
|
267
|
+
ensureDir();
|
|
268
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
269
|
+
try {
|
|
270
|
+
fs.chmodSync(CREDENTIALS_FILE, 384);
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function removeCredentials() {
|
|
275
|
+
for (const file of [CREDENTIALS_FILE, SYNC_BUF_FILE, CONTACTS_FILE]) {
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function loadSyncBuf() {
|
|
283
|
+
try {
|
|
284
|
+
if (fs.existsSync(SYNC_BUF_FILE)) {
|
|
285
|
+
return fs.readFileSync(SYNC_BUF_FILE, "utf-8");
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
function saveSyncBuf(buf) {
|
|
292
|
+
try {
|
|
293
|
+
ensureDir();
|
|
294
|
+
fs.writeFileSync(SYNC_BUF_FILE, buf, "utf-8");
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function loadContacts() {
|
|
299
|
+
try {
|
|
300
|
+
if (!fs.existsSync(CONTACTS_FILE)) return [];
|
|
301
|
+
return JSON.parse(fs.readFileSync(CONTACTS_FILE, "utf-8"));
|
|
302
|
+
} catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function saveContact(userId, contextToken) {
|
|
307
|
+
const contacts = loadContacts();
|
|
308
|
+
const displayName = userId.split("@")[0] || userId;
|
|
309
|
+
const idx = contacts.findIndex((c) => c.userId === userId);
|
|
310
|
+
const entry = {
|
|
311
|
+
userId,
|
|
312
|
+
contextToken,
|
|
313
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
314
|
+
displayName
|
|
315
|
+
};
|
|
316
|
+
if (idx >= 0) contacts[idx] = entry;
|
|
317
|
+
else contacts.push(entry);
|
|
318
|
+
try {
|
|
319
|
+
ensureDir();
|
|
320
|
+
fs.writeFileSync(CONTACTS_FILE, JSON.stringify(contacts, null, 2), "utf-8");
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function getContactToken(userId) {
|
|
325
|
+
const contacts = loadContacts();
|
|
326
|
+
const match = contacts.find(
|
|
327
|
+
(c) => c.userId === userId || c.displayName === userId
|
|
328
|
+
);
|
|
329
|
+
return match?.contextToken ?? null;
|
|
330
|
+
}
|
|
331
|
+
function loadDaemonInfo() {
|
|
332
|
+
try {
|
|
333
|
+
if (!fs.existsSync(DAEMON_INFO_FILE)) return null;
|
|
334
|
+
return JSON.parse(fs.readFileSync(DAEMON_INFO_FILE, "utf-8"));
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function saveDaemonInfo(info) {
|
|
340
|
+
ensureDir();
|
|
341
|
+
fs.writeFileSync(DAEMON_INFO_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
342
|
+
}
|
|
343
|
+
function removeDaemonInfo() {
|
|
344
|
+
for (const file of [DAEMON_INFO_FILE, DAEMON_PID_FILE]) {
|
|
345
|
+
try {
|
|
346
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function saveDaemonPid(pid) {
|
|
352
|
+
ensureDir();
|
|
353
|
+
fs.writeFileSync(DAEMON_PID_FILE, String(pid), "utf-8");
|
|
354
|
+
}
|
|
355
|
+
function loadDaemonPid() {
|
|
356
|
+
try {
|
|
357
|
+
if (!fs.existsSync(DAEMON_PID_FILE)) return null;
|
|
358
|
+
const raw = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
|
|
359
|
+
const pid = parseInt(raw, 10);
|
|
360
|
+
return isNaN(pid) ? null : pid;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
var init_credentials = __esm({
|
|
366
|
+
"src/credentials.ts"() {
|
|
367
|
+
"use strict";
|
|
368
|
+
init_constants();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// src/daemon/lifecycle.ts
|
|
373
|
+
var lifecycle_exports = {};
|
|
374
|
+
__export(lifecycle_exports, {
|
|
375
|
+
checkDaemon: () => checkDaemon,
|
|
376
|
+
ensureDaemon: () => ensureDaemon,
|
|
377
|
+
startDaemonProcess: () => startDaemonProcess,
|
|
378
|
+
stopDaemon: () => stopDaemon
|
|
379
|
+
});
|
|
380
|
+
import { spawn } from "child_process";
|
|
381
|
+
import path2 from "path";
|
|
382
|
+
import { fileURLToPath } from "url";
|
|
383
|
+
function getDaemonScript() {
|
|
384
|
+
const thisFile = typeof __filename !== "undefined" ? __filename : fileURLToPath(import.meta.url);
|
|
385
|
+
return path2.resolve(path2.dirname(thisFile), "..", "daemon", "server.js");
|
|
386
|
+
}
|
|
387
|
+
function isProcessAlive(pid) {
|
|
388
|
+
try {
|
|
389
|
+
process.kill(pid, 0);
|
|
390
|
+
return true;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function healthCheck(port) {
|
|
396
|
+
try {
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
399
|
+
const res = await fetch(`http://${DAEMON_HOST}:${port}/health`, {
|
|
400
|
+
signal: controller.signal
|
|
401
|
+
});
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
if (!res.ok) return false;
|
|
404
|
+
const data = await res.json();
|
|
405
|
+
return data.ok === true;
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function checkDaemon() {
|
|
411
|
+
const info = loadDaemonInfo();
|
|
412
|
+
if (!info) return null;
|
|
413
|
+
if (!isProcessAlive(info.pid)) {
|
|
414
|
+
removeDaemonInfo();
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const healthy = await healthCheck(info.port);
|
|
418
|
+
if (!healthy) {
|
|
419
|
+
removeDaemonInfo();
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return info;
|
|
423
|
+
}
|
|
424
|
+
async function startDaemonProcess() {
|
|
425
|
+
const script = getDaemonScript();
|
|
426
|
+
const child = spawn(process.execPath, [script], {
|
|
427
|
+
detached: true,
|
|
428
|
+
stdio: "ignore",
|
|
429
|
+
env: { ...process.env }
|
|
430
|
+
});
|
|
431
|
+
child.unref();
|
|
432
|
+
const deadline = Date.now() + 15e3;
|
|
433
|
+
while (Date.now() < deadline) {
|
|
434
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
435
|
+
const info = loadDaemonInfo();
|
|
436
|
+
if (info && isProcessAlive(info.pid)) {
|
|
437
|
+
const healthy = await healthCheck(info.port);
|
|
438
|
+
if (healthy) return info;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw new Error("Daemon failed to start within 15 seconds");
|
|
442
|
+
}
|
|
443
|
+
async function ensureDaemon() {
|
|
444
|
+
const existing = await checkDaemon();
|
|
445
|
+
if (existing) return existing;
|
|
446
|
+
return startDaemonProcess();
|
|
447
|
+
}
|
|
448
|
+
async function stopDaemon() {
|
|
449
|
+
const pid = loadDaemonPid();
|
|
450
|
+
if (!pid) return false;
|
|
451
|
+
if (!isProcessAlive(pid)) {
|
|
452
|
+
removeDaemonInfo();
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
process.kill(pid, "SIGTERM");
|
|
457
|
+
const deadline = Date.now() + 5e3;
|
|
458
|
+
while (Date.now() < deadline) {
|
|
459
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
460
|
+
if (!isProcessAlive(pid)) break;
|
|
461
|
+
}
|
|
462
|
+
removeDaemonInfo();
|
|
463
|
+
return true;
|
|
464
|
+
} catch {
|
|
465
|
+
removeDaemonInfo();
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
var init_lifecycle = __esm({
|
|
470
|
+
"src/daemon/lifecycle.ts"() {
|
|
471
|
+
"use strict";
|
|
472
|
+
init_credentials();
|
|
473
|
+
init_constants();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/client/index.ts
|
|
478
|
+
var client_exports = {};
|
|
479
|
+
__export(client_exports, {
|
|
480
|
+
DaemonClient: () => DaemonClient
|
|
481
|
+
});
|
|
482
|
+
var DaemonClient;
|
|
483
|
+
var init_client = __esm({
|
|
484
|
+
"src/client/index.ts"() {
|
|
485
|
+
"use strict";
|
|
486
|
+
init_constants();
|
|
487
|
+
DaemonClient = class {
|
|
488
|
+
baseUrl;
|
|
489
|
+
constructor(port) {
|
|
490
|
+
this.baseUrl = `http://${DAEMON_HOST}:${port}`;
|
|
491
|
+
}
|
|
492
|
+
async request(method, path3, body, timeoutMs = 5e3) {
|
|
493
|
+
const controller = new AbortController();
|
|
494
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
495
|
+
try {
|
|
496
|
+
const res = await fetch(`${this.baseUrl}${path3}`, {
|
|
497
|
+
method,
|
|
498
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
499
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
500
|
+
signal: controller.signal
|
|
501
|
+
});
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
const data = await res.json();
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
const errMsg = data.error || `HTTP ${res.status}`;
|
|
506
|
+
throw new Error(errMsg);
|
|
507
|
+
}
|
|
508
|
+
return data;
|
|
509
|
+
} catch (err) {
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async health() {
|
|
515
|
+
try {
|
|
516
|
+
const data = await this.request("GET", "/health");
|
|
517
|
+
return data.ok === true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async status() {
|
|
523
|
+
return this.request("GET", "/status");
|
|
524
|
+
}
|
|
525
|
+
async registerAgent(agentId, name) {
|
|
526
|
+
return this.request("POST", "/agents", { agentId, name });
|
|
527
|
+
}
|
|
528
|
+
async unregisterAgent(agentId) {
|
|
529
|
+
return this.request("DELETE", `/agents/${encodeURIComponent(agentId)}`);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Long-poll for messages intended for this agent.
|
|
533
|
+
* Will block up to ~30s on the daemon side.
|
|
534
|
+
*/
|
|
535
|
+
async pollMessages(agentId) {
|
|
536
|
+
const resp = await this.request(
|
|
537
|
+
"GET",
|
|
538
|
+
`/messages/${encodeURIComponent(agentId)}`,
|
|
539
|
+
void 0,
|
|
540
|
+
35e3
|
|
541
|
+
);
|
|
542
|
+
return resp.messages;
|
|
543
|
+
}
|
|
544
|
+
async send(to, text, agentId) {
|
|
545
|
+
return this.request("POST", "/send", { to, text, agentId });
|
|
546
|
+
}
|
|
547
|
+
async getContacts() {
|
|
548
|
+
const data = await this.request("GET", "/contacts");
|
|
549
|
+
return data.contacts;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// src/mcp/server.ts
|
|
556
|
+
var server_exports = {};
|
|
557
|
+
__export(server_exports, {
|
|
558
|
+
startMcpServer: () => startMcpServer
|
|
559
|
+
});
|
|
560
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
561
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
562
|
+
import {
|
|
563
|
+
ListToolsRequestSchema,
|
|
564
|
+
CallToolRequestSchema
|
|
565
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
566
|
+
function log(msg) {
|
|
567
|
+
process.stderr.write(`[wechat-mcp:mcp] ${msg}
|
|
568
|
+
`);
|
|
569
|
+
}
|
|
570
|
+
async function startMcpServer(agentId) {
|
|
571
|
+
log(`Starting MCP server for agent "${agentId}"...`);
|
|
572
|
+
let daemonInfo;
|
|
573
|
+
try {
|
|
574
|
+
daemonInfo = await ensureDaemon();
|
|
575
|
+
log(`Daemon ready on port ${daemonInfo.port}`);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
log(`Failed to start daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
const client = new DaemonClient(daemonInfo.port);
|
|
581
|
+
try {
|
|
582
|
+
await client.registerAgent(agentId, agentId);
|
|
583
|
+
log(`Registered as agent "${agentId}"`);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
log(`Failed to register agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const mcp = new Server(
|
|
589
|
+
{ name: PACKAGE_NAME, version: PACKAGE_VERSION },
|
|
590
|
+
{
|
|
591
|
+
capabilities: {
|
|
592
|
+
tools: {}
|
|
593
|
+
},
|
|
594
|
+
instructions: [
|
|
595
|
+
`You are connected to WeChat via the wechat-mcp middleware (agent: "${agentId}").`,
|
|
596
|
+
"Incoming WeChat messages arrive as notifications from this server.",
|
|
597
|
+
"Use the wechat_send tool to reply. You MUST pass the sender_id from the incoming message.",
|
|
598
|
+
"Messages are from real WeChat users. Respond in Chinese unless the user writes in another language.",
|
|
599
|
+
"Keep replies concise \u2014 WeChat is a chat app. Strip markdown formatting (WeChat doesn't render it).",
|
|
600
|
+
"Use wechat_get_contacts to see who has messaged before."
|
|
601
|
+
].join("\n")
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
605
|
+
tools: [
|
|
606
|
+
{
|
|
607
|
+
name: "wechat_send",
|
|
608
|
+
description: "Send a text message to a WeChat user via the shared WeChat connection",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: {
|
|
612
|
+
to: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "Target user ID (xxx@im.wechat format) from a previous message's sender_id"
|
|
615
|
+
},
|
|
616
|
+
text: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "Plain-text message to send (no markdown \u2014 WeChat doesn't render it)"
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
required: ["to", "text"]
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "wechat_get_contacts",
|
|
626
|
+
description: "List known WeChat contacts who have previously messaged the bot",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: "object",
|
|
629
|
+
properties: {}
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "wechat_get_status",
|
|
634
|
+
description: "Get WeChat connection status and list of registered agents",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: "object",
|
|
637
|
+
properties: {}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}));
|
|
642
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
643
|
+
const { name, arguments: args } = req.params;
|
|
644
|
+
if (name === "wechat_send") {
|
|
645
|
+
const { to, text } = args;
|
|
646
|
+
if (!to || !text) {
|
|
647
|
+
return { content: [{ type: "text", text: "Error: 'to' and 'text' are required" }] };
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const result = await client.send(to, text, agentId);
|
|
651
|
+
if (result.success) {
|
|
652
|
+
return { content: [{ type: "text", text: "Message sent" }] };
|
|
653
|
+
}
|
|
654
|
+
return { content: [{ type: "text", text: `Send failed: ${result.error}` }] };
|
|
655
|
+
} catch (err) {
|
|
656
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (name === "wechat_get_contacts") {
|
|
660
|
+
try {
|
|
661
|
+
const contacts = await client.getContacts();
|
|
662
|
+
if (contacts.length === 0) {
|
|
663
|
+
return { content: [{ type: "text", text: "No contacts yet. Users must message the bot first." }] };
|
|
664
|
+
}
|
|
665
|
+
const lines = contacts.map(
|
|
666
|
+
(c) => `${c.displayName || c.userId} (${c.userId}) \u2014 last seen: ${c.lastSeen}`
|
|
667
|
+
);
|
|
668
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
669
|
+
} catch (err) {
|
|
670
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (name === "wechat_get_status") {
|
|
674
|
+
try {
|
|
675
|
+
const status = await client.status();
|
|
676
|
+
const agentList = status.agents.map((a) => ` - ${a.name} (${a.agentId})`).join("\n");
|
|
677
|
+
const text = [
|
|
678
|
+
`WeChat connected: ${status.wechatConnected ? "yes" : "no"}`,
|
|
679
|
+
`Account: ${status.accountId || "N/A"}`,
|
|
680
|
+
`Uptime: ${Math.round(status.uptime / 1e3)}s`,
|
|
681
|
+
`Registered agents:
|
|
682
|
+
${agentList || " (none)"}`
|
|
683
|
+
].join("\n");
|
|
684
|
+
return { content: [{ type: "text", text }] };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
690
|
+
});
|
|
691
|
+
const transport = new StdioServerTransport();
|
|
692
|
+
await mcp.connect(transport);
|
|
693
|
+
log("MCP connection ready");
|
|
694
|
+
const pollLoop = async () => {
|
|
695
|
+
while (true) {
|
|
696
|
+
try {
|
|
697
|
+
const messages = await client.pollMessages(agentId);
|
|
698
|
+
for (const msg of messages) {
|
|
699
|
+
log(`Forwarding message: from=${msg.senderId} text="${msg.text.slice(0, 50)}..."`);
|
|
700
|
+
try {
|
|
701
|
+
await mcp.notification({
|
|
702
|
+
method: "notifications/message",
|
|
703
|
+
params: {
|
|
704
|
+
level: "info",
|
|
705
|
+
data: {
|
|
706
|
+
type: "wechat_message",
|
|
707
|
+
sender: msg.senderName,
|
|
708
|
+
sender_id: msg.senderId,
|
|
709
|
+
text: msg.text,
|
|
710
|
+
raw_text: msg.rawText,
|
|
711
|
+
timestamp: msg.timestamp
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
} catch {
|
|
716
|
+
log(`Notification delivery failed for message ${msg.id}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch (err) {
|
|
720
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
721
|
+
if (!errMsg.includes("ECONNREFUSED")) {
|
|
722
|
+
log(`Poll error: ${errMsg}`);
|
|
723
|
+
}
|
|
724
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
pollLoop().catch((err) => {
|
|
729
|
+
log(`Fatal poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
730
|
+
});
|
|
731
|
+
const cleanup = async () => {
|
|
732
|
+
try {
|
|
733
|
+
await client.unregisterAgent(agentId);
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
process.exit(0);
|
|
737
|
+
};
|
|
738
|
+
process.on("SIGINT", cleanup);
|
|
739
|
+
process.on("SIGTERM", cleanup);
|
|
740
|
+
}
|
|
741
|
+
var init_server = __esm({
|
|
742
|
+
"src/mcp/server.ts"() {
|
|
743
|
+
"use strict";
|
|
744
|
+
init_constants();
|
|
745
|
+
init_lifecycle();
|
|
746
|
+
init_client();
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// src/daemon/store.ts
|
|
751
|
+
var DaemonStore;
|
|
752
|
+
var init_store = __esm({
|
|
753
|
+
"src/daemon/store.ts"() {
|
|
754
|
+
"use strict";
|
|
755
|
+
init_constants();
|
|
756
|
+
DaemonStore = class {
|
|
757
|
+
agents = /* @__PURE__ */ new Map();
|
|
758
|
+
defaultAgentId = null;
|
|
759
|
+
startedAt = Date.now();
|
|
760
|
+
registerAgent(agentId, name) {
|
|
761
|
+
const existing = this.agents.get(agentId);
|
|
762
|
+
if (existing) return existing.registration;
|
|
763
|
+
const registration = {
|
|
764
|
+
agentId,
|
|
765
|
+
name: name || agentId,
|
|
766
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
767
|
+
};
|
|
768
|
+
this.agents.set(agentId, {
|
|
769
|
+
registration,
|
|
770
|
+
messages: [],
|
|
771
|
+
waiters: []
|
|
772
|
+
});
|
|
773
|
+
if (!this.defaultAgentId) {
|
|
774
|
+
this.defaultAgentId = agentId;
|
|
775
|
+
}
|
|
776
|
+
return registration;
|
|
777
|
+
}
|
|
778
|
+
unregisterAgent(agentId) {
|
|
779
|
+
const state = this.agents.get(agentId);
|
|
780
|
+
if (!state) return false;
|
|
781
|
+
for (const w of state.waiters) {
|
|
782
|
+
clearTimeout(w.timer);
|
|
783
|
+
w.resolve([]);
|
|
784
|
+
}
|
|
785
|
+
this.agents.delete(agentId);
|
|
786
|
+
if (this.defaultAgentId === agentId) {
|
|
787
|
+
const first = this.agents.keys().next();
|
|
788
|
+
this.defaultAgentId = first.done ? null : first.value;
|
|
789
|
+
}
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
getAgents() {
|
|
793
|
+
return Array.from(this.agents.values()).map((s) => s.registration);
|
|
794
|
+
}
|
|
795
|
+
getDefaultAgentId() {
|
|
796
|
+
return this.defaultAgentId;
|
|
797
|
+
}
|
|
798
|
+
hasAgent(agentId) {
|
|
799
|
+
return this.agents.has(agentId);
|
|
800
|
+
}
|
|
801
|
+
getUptime() {
|
|
802
|
+
return Date.now() - this.startedAt;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Push a message to a specific agent's queue.
|
|
806
|
+
* If an agent has a waiting long-poll, resolve it immediately.
|
|
807
|
+
*/
|
|
808
|
+
pushMessage(agentId, message) {
|
|
809
|
+
const state = this.agents.get(agentId);
|
|
810
|
+
if (!state) return false;
|
|
811
|
+
if (state.waiters.length > 0) {
|
|
812
|
+
const waiter = state.waiters.shift();
|
|
813
|
+
clearTimeout(waiter.timer);
|
|
814
|
+
waiter.resolve([message]);
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
state.messages.push(message);
|
|
818
|
+
if (state.messages.length > MESSAGE_QUEUE_MAX_PER_AGENT) {
|
|
819
|
+
state.messages.shift();
|
|
820
|
+
}
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Drain all queued messages for an agent, or wait up to timeoutMs
|
|
825
|
+
* for new ones to arrive.
|
|
826
|
+
*/
|
|
827
|
+
async pollMessages(agentId, timeoutMs) {
|
|
828
|
+
const state = this.agents.get(agentId);
|
|
829
|
+
if (!state) return [];
|
|
830
|
+
if (state.messages.length > 0) {
|
|
831
|
+
const msgs = state.messages.splice(0);
|
|
832
|
+
return msgs;
|
|
833
|
+
}
|
|
834
|
+
return new Promise((resolve) => {
|
|
835
|
+
const timer = setTimeout(() => {
|
|
836
|
+
const idx = state.waiters.findIndex((w) => w.resolve === resolve);
|
|
837
|
+
if (idx >= 0) state.waiters.splice(idx, 1);
|
|
838
|
+
resolve([]);
|
|
839
|
+
}, timeoutMs);
|
|
840
|
+
state.waiters.push({ resolve, timer });
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// src/daemon/router.ts
|
|
848
|
+
import crypto2 from "crypto";
|
|
849
|
+
function parseMention(text) {
|
|
850
|
+
const trimmed = text.trimStart();
|
|
851
|
+
const match = trimmed.match(/^@(\S+)\s*([\s\S]*)$/);
|
|
852
|
+
if (!match) {
|
|
853
|
+
return { agent: null, rest: text };
|
|
854
|
+
}
|
|
855
|
+
return { agent: match[1].toLowerCase(), rest: match[2] || "" };
|
|
856
|
+
}
|
|
857
|
+
function routeMessage(store2, senderId, text, contextToken) {
|
|
858
|
+
const { agent, rest } = parseMention(text);
|
|
859
|
+
let targetAgent = null;
|
|
860
|
+
let strippedText = text;
|
|
861
|
+
if (agent && store2.hasAgent(agent)) {
|
|
862
|
+
targetAgent = agent;
|
|
863
|
+
strippedText = rest || text;
|
|
864
|
+
} else {
|
|
865
|
+
targetAgent = store2.getDefaultAgentId();
|
|
866
|
+
strippedText = text;
|
|
867
|
+
}
|
|
868
|
+
if (!targetAgent) return null;
|
|
869
|
+
const message = {
|
|
870
|
+
id: crypto2.randomBytes(8).toString("hex"),
|
|
871
|
+
senderId,
|
|
872
|
+
senderName: senderId.split("@")[0] || senderId,
|
|
873
|
+
text: strippedText,
|
|
874
|
+
rawText: text,
|
|
875
|
+
contextToken,
|
|
876
|
+
targetAgent,
|
|
877
|
+
timestamp: Date.now()
|
|
878
|
+
};
|
|
879
|
+
store2.pushMessage(targetAgent, message);
|
|
880
|
+
return message;
|
|
881
|
+
}
|
|
882
|
+
var init_router = __esm({
|
|
883
|
+
"src/daemon/router.ts"() {
|
|
884
|
+
"use strict";
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// src/daemon/server.ts
|
|
889
|
+
var server_exports2 = {};
|
|
890
|
+
__export(server_exports2, {
|
|
891
|
+
startDaemon: () => startDaemon
|
|
892
|
+
});
|
|
893
|
+
import http from "http";
|
|
894
|
+
function log2(msg) {
|
|
895
|
+
process.stderr.write(`[wechat-mcp:daemon] ${msg}
|
|
896
|
+
`);
|
|
897
|
+
}
|
|
898
|
+
async function readBody(req) {
|
|
899
|
+
const chunks = [];
|
|
900
|
+
for await (const chunk of req) {
|
|
901
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
902
|
+
}
|
|
903
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
904
|
+
}
|
|
905
|
+
function json(res, status, data) {
|
|
906
|
+
const body = JSON.stringify(data);
|
|
907
|
+
res.writeHead(status, {
|
|
908
|
+
"Content-Type": "application/json",
|
|
909
|
+
"Content-Length": Buffer.byteLength(body)
|
|
910
|
+
});
|
|
911
|
+
res.end(body);
|
|
912
|
+
}
|
|
913
|
+
async function handleRequest(req, res) {
|
|
914
|
+
const url = new URL(req.url || "/", `http://${DAEMON_HOST}`);
|
|
915
|
+
const method = req.method?.toUpperCase() || "GET";
|
|
916
|
+
const path3 = url.pathname;
|
|
917
|
+
try {
|
|
918
|
+
if (method === "GET" && path3 === "/health") {
|
|
919
|
+
json(res, 200, { ok: true, pid: process.pid });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (method === "GET" && path3 === "/status") {
|
|
923
|
+
const status = {
|
|
924
|
+
running: true,
|
|
925
|
+
wechatConnected,
|
|
926
|
+
accountId: activeAccount?.accountId,
|
|
927
|
+
agents: store.getAgents(),
|
|
928
|
+
uptime: store.getUptime()
|
|
929
|
+
};
|
|
930
|
+
json(res, 200, status);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (method === "POST" && path3 === "/agents") {
|
|
934
|
+
const body = JSON.parse(await readBody(req));
|
|
935
|
+
if (!body.agentId) {
|
|
936
|
+
json(res, 400, { error: "agentId required" });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const reg = store.registerAgent(body.agentId, body.name);
|
|
940
|
+
log2(`Agent registered: ${reg.agentId} (${reg.name})`);
|
|
941
|
+
json(res, 200, reg);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (method === "DELETE" && path3.startsWith("/agents/")) {
|
|
945
|
+
const agentId = decodeURIComponent(path3.slice("/agents/".length));
|
|
946
|
+
const removed = store.unregisterAgent(agentId);
|
|
947
|
+
if (removed) log2(`Agent unregistered: ${agentId}`);
|
|
948
|
+
json(res, 200, { removed });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (method === "GET" && path3.startsWith("/messages/")) {
|
|
952
|
+
const agentId = decodeURIComponent(path3.slice("/messages/".length));
|
|
953
|
+
if (!store.hasAgent(agentId)) {
|
|
954
|
+
json(res, 404, { error: "agent not registered" });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const messages = await store.pollMessages(agentId, DAEMON_LONG_POLL_MS);
|
|
958
|
+
const resp = { messages };
|
|
959
|
+
json(res, 200, resp);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (method === "POST" && path3 === "/send") {
|
|
963
|
+
const body = JSON.parse(await readBody(req));
|
|
964
|
+
if (!body.to || !body.text) {
|
|
965
|
+
json(res, 400, { success: false, error: "to and text required" });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!activeAccount) {
|
|
969
|
+
json(res, 503, { success: false, error: "WeChat not connected" });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const contextToken = getContactToken(body.to);
|
|
973
|
+
if (!contextToken) {
|
|
974
|
+
json(res, 404, {
|
|
975
|
+
success: false,
|
|
976
|
+
error: `No context token for "${body.to}". User must message the bot first.`
|
|
977
|
+
});
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
for (let i = 0; i < body.text.length; i += MAX_WECHAT_MSG_LENGTH) {
|
|
982
|
+
await sendTextMessage(
|
|
983
|
+
activeAccount.baseUrl,
|
|
984
|
+
activeAccount.token,
|
|
985
|
+
body.to,
|
|
986
|
+
body.text.slice(i, i + MAX_WECHAT_MSG_LENGTH),
|
|
987
|
+
contextToken
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
json(res, 200, { success: true });
|
|
991
|
+
} catch (err) {
|
|
992
|
+
json(res, 500, {
|
|
993
|
+
success: false,
|
|
994
|
+
error: err instanceof Error ? err.message : String(err)
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (method === "GET" && path3 === "/contacts") {
|
|
1000
|
+
json(res, 200, { contacts: loadContacts() });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
json(res, 404, { error: "not found" });
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
log2(`HTTP error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1006
|
+
json(res, 500, { error: "internal error" });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function startPolling(account) {
|
|
1010
|
+
let buf = loadSyncBuf();
|
|
1011
|
+
let failures = 0;
|
|
1012
|
+
if (buf) log2(`Restored sync state (${buf.length} bytes)`);
|
|
1013
|
+
log2("Listening for WeChat messages...");
|
|
1014
|
+
wechatConnected = true;
|
|
1015
|
+
while (true) {
|
|
1016
|
+
try {
|
|
1017
|
+
const resp = await getUpdates(account.baseUrl, account.token, buf);
|
|
1018
|
+
const isError = resp.ret !== void 0 && resp.ret !== 0 || resp.errcode !== void 0 && resp.errcode !== 0;
|
|
1019
|
+
if (isError) {
|
|
1020
|
+
failures++;
|
|
1021
|
+
log2(`getUpdates error: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""}`);
|
|
1022
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
1023
|
+
wechatConnected = false;
|
|
1024
|
+
failures = 0;
|
|
1025
|
+
await new Promise((r) => setTimeout(r, BACKOFF_DELAY_MS));
|
|
1026
|
+
} else {
|
|
1027
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1028
|
+
}
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
failures = 0;
|
|
1032
|
+
wechatConnected = true;
|
|
1033
|
+
if (resp.get_updates_buf) {
|
|
1034
|
+
buf = resp.get_updates_buf;
|
|
1035
|
+
saveSyncBuf(buf);
|
|
1036
|
+
}
|
|
1037
|
+
for (const msg of resp.msgs ?? []) {
|
|
1038
|
+
if (msg.message_type !== MSG_TYPE_USER) continue;
|
|
1039
|
+
const text = extractTextFromMessage(msg);
|
|
1040
|
+
if (!text) continue;
|
|
1041
|
+
const senderId = msg.from_user_id ?? "unknown";
|
|
1042
|
+
const contextToken = msg.context_token ?? "";
|
|
1043
|
+
if (contextToken) {
|
|
1044
|
+
saveContact(senderId, contextToken);
|
|
1045
|
+
}
|
|
1046
|
+
const routed = routeMessage(store, senderId, text, contextToken);
|
|
1047
|
+
if (routed) {
|
|
1048
|
+
log2(`Message routed: from=${senderId} agent=${routed.targetAgent} text="${text.slice(0, 50)}..."`);
|
|
1049
|
+
} else {
|
|
1050
|
+
log2(`Message dropped (no agents): from=${senderId} text="${text.slice(0, 50)}..."`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
failures++;
|
|
1055
|
+
log2(`Poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1056
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
1057
|
+
wechatConnected = false;
|
|
1058
|
+
failures = 0;
|
|
1059
|
+
await new Promise((r) => setTimeout(r, BACKOFF_DELAY_MS));
|
|
1060
|
+
} else {
|
|
1061
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function startDaemon() {
|
|
1067
|
+
const account = loadCredentials();
|
|
1068
|
+
if (!account) {
|
|
1069
|
+
log2("No credentials found. Run `wechat-mcp setup` first.");
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
activeAccount = account;
|
|
1073
|
+
const server = http.createServer((req, res) => {
|
|
1074
|
+
handleRequest(req, res).catch(() => {
|
|
1075
|
+
if (!res.headersSent) json(res, 500, { error: "internal error" });
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
await new Promise((resolve, reject) => {
|
|
1079
|
+
server.listen(0, DAEMON_HOST, () => resolve());
|
|
1080
|
+
server.on("error", reject);
|
|
1081
|
+
});
|
|
1082
|
+
const addr = server.address();
|
|
1083
|
+
if (!addr || typeof addr === "string") {
|
|
1084
|
+
log2("Failed to get server address");
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
const port = addr.port;
|
|
1088
|
+
saveDaemonPid(process.pid);
|
|
1089
|
+
saveDaemonInfo({ pid: process.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1090
|
+
log2(`Daemon running on ${DAEMON_HOST}:${port} (PID ${process.pid})`);
|
|
1091
|
+
log2(`Account: ${account.accountId}`);
|
|
1092
|
+
const cleanup = () => {
|
|
1093
|
+
log2("Shutting down...");
|
|
1094
|
+
removeDaemonInfo();
|
|
1095
|
+
server.close();
|
|
1096
|
+
process.exit(0);
|
|
1097
|
+
};
|
|
1098
|
+
process.on("SIGINT", cleanup);
|
|
1099
|
+
process.on("SIGTERM", cleanup);
|
|
1100
|
+
startPolling(account).catch((err) => {
|
|
1101
|
+
log2(`Fatal polling error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1102
|
+
cleanup();
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
var store, activeAccount, wechatConnected;
|
|
1106
|
+
var init_server2 = __esm({
|
|
1107
|
+
"src/daemon/server.ts"() {
|
|
1108
|
+
"use strict";
|
|
1109
|
+
init_constants();
|
|
1110
|
+
init_credentials();
|
|
1111
|
+
init_wechat_api();
|
|
1112
|
+
init_store();
|
|
1113
|
+
init_router();
|
|
1114
|
+
store = new DaemonStore();
|
|
1115
|
+
activeAccount = null;
|
|
1116
|
+
wechatConnected = false;
|
|
1117
|
+
if (process.argv[1] && (process.argv[1].endsWith("daemon/server.js") || process.argv[1].endsWith("daemon/server.ts"))) {
|
|
1118
|
+
startDaemon();
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// src/cli.ts
|
|
1124
|
+
init_constants();
|
|
1125
|
+
import { Command } from "commander";
|
|
1126
|
+
var program = new Command();
|
|
1127
|
+
program.name("wechat-mcp").description("WeChat MCP middleware \u2014 shared WeChat connection for multiple AI agents").version(PACKAGE_VERSION);
|
|
1128
|
+
program.command("setup").description("Authenticate with WeChat via QR code scan").option("-f, --force", "Re-authenticate even if already logged in").action(async (opts) => {
|
|
1129
|
+
const { fetchQRCode: fetchQRCode2, pollQRStatus: pollQRStatus2 } = await Promise.resolve().then(() => (init_wechat_api(), wechat_api_exports));
|
|
1130
|
+
const { loadCredentials: loadCredentials2, saveCredentials: saveCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1131
|
+
const existing = loadCredentials2();
|
|
1132
|
+
if (existing && !opts.force) {
|
|
1133
|
+
console.log(`Existing WeChat account: ${existing.accountId}`);
|
|
1134
|
+
console.log(`Saved at: ${existing.savedAt}
|
|
1135
|
+
`);
|
|
1136
|
+
const readline = await import("readline");
|
|
1137
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1138
|
+
const answer = await new Promise((resolve) => {
|
|
1139
|
+
rl.question("Re-authenticate? (y/N) ", resolve);
|
|
1140
|
+
});
|
|
1141
|
+
rl.close();
|
|
1142
|
+
if (answer.toLowerCase() !== "y") {
|
|
1143
|
+
console.log("Keeping existing credentials.");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
console.log("Fetching WeChat login QR code...\n");
|
|
1148
|
+
const qrResp = await fetchQRCode2(DEFAULT_BASE_URL);
|
|
1149
|
+
try {
|
|
1150
|
+
const qrterm = await import("qrcode-terminal");
|
|
1151
|
+
await new Promise((resolve) => {
|
|
1152
|
+
qrterm.default.generate(
|
|
1153
|
+
qrResp.qrcode_img_content,
|
|
1154
|
+
{ small: true },
|
|
1155
|
+
(qr) => {
|
|
1156
|
+
console.log(qr);
|
|
1157
|
+
resolve();
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
});
|
|
1161
|
+
} catch {
|
|
1162
|
+
console.log(`QR code URL: ${qrResp.qrcode_img_content}
|
|
1163
|
+
`);
|
|
1164
|
+
}
|
|
1165
|
+
console.log("Scan the QR code with WeChat...\n");
|
|
1166
|
+
const deadline = Date.now() + QR_LOGIN_TIMEOUT_MS;
|
|
1167
|
+
let scannedPrinted = false;
|
|
1168
|
+
while (Date.now() < deadline) {
|
|
1169
|
+
const status = await pollQRStatus2(DEFAULT_BASE_URL, qrResp.qrcode);
|
|
1170
|
+
switch (status.status) {
|
|
1171
|
+
case "wait":
|
|
1172
|
+
process.stdout.write(".");
|
|
1173
|
+
break;
|
|
1174
|
+
case "scaned":
|
|
1175
|
+
if (!scannedPrinted) {
|
|
1176
|
+
console.log("\nScanned! Please confirm on your phone...");
|
|
1177
|
+
scannedPrinted = true;
|
|
1178
|
+
}
|
|
1179
|
+
break;
|
|
1180
|
+
case "expired":
|
|
1181
|
+
console.error("\nQR code expired. Please run setup again.");
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
break;
|
|
1184
|
+
case "confirmed": {
|
|
1185
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
1186
|
+
console.error("\nLogin failed: server did not return complete info.");
|
|
1187
|
+
process.exit(1);
|
|
1188
|
+
}
|
|
1189
|
+
const account = {
|
|
1190
|
+
token: status.bot_token,
|
|
1191
|
+
baseUrl: status.baseurl || DEFAULT_BASE_URL,
|
|
1192
|
+
accountId: status.ilink_bot_id,
|
|
1193
|
+
userId: status.ilink_user_id,
|
|
1194
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1195
|
+
};
|
|
1196
|
+
saveCredentials2(account);
|
|
1197
|
+
console.log(`
|
|
1198
|
+
WeChat connected successfully!`);
|
|
1199
|
+
console.log(` Account ID: ${account.accountId}`);
|
|
1200
|
+
console.log(` User ID: ${account.userId ?? "N/A"}
|
|
1201
|
+
`);
|
|
1202
|
+
console.log("Add this to any agent's MCP config to connect:");
|
|
1203
|
+
console.log(` {`);
|
|
1204
|
+
console.log(` "mcpServers": {`);
|
|
1205
|
+
console.log(` "wechat": {`);
|
|
1206
|
+
console.log(` "command": "npx",`);
|
|
1207
|
+
console.log(` "args": ["@paean-ai/wechat-mcp", "serve", "--agent", "my-agent"]`);
|
|
1208
|
+
console.log(` }`);
|
|
1209
|
+
console.log(` }`);
|
|
1210
|
+
console.log(` }`);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1215
|
+
}
|
|
1216
|
+
console.error("\nLogin timed out. Please try again.");
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
});
|
|
1219
|
+
program.command("serve").description("Start MCP stdio server for an agent (used in MCP config)").requiredOption("-a, --agent <id>", "Agent identifier (e.g. claude, openclaw, paean)").action(async (opts) => {
|
|
1220
|
+
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1221
|
+
if (!loadCredentials2()) {
|
|
1222
|
+
process.stderr.write("No WeChat credentials. Run `wechat-mcp setup` first.\n");
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1226
|
+
await startMcpServer2(opts.agent);
|
|
1227
|
+
});
|
|
1228
|
+
program.command("daemon").description("Start the daemon process in foreground (usually auto-started)").action(async () => {
|
|
1229
|
+
const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
|
|
1230
|
+
await startDaemon2();
|
|
1231
|
+
});
|
|
1232
|
+
program.command("stop").description("Stop the running daemon").action(async () => {
|
|
1233
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
1234
|
+
const stopped = await stopDaemon2();
|
|
1235
|
+
if (stopped) {
|
|
1236
|
+
console.log("Daemon stopped.");
|
|
1237
|
+
} else {
|
|
1238
|
+
console.log("No daemon was running.");
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
program.command("status").description("Show daemon and WeChat connection status").action(async () => {
|
|
1242
|
+
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1243
|
+
const { checkDaemon: checkDaemon2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
1244
|
+
const account = loadCredentials2();
|
|
1245
|
+
console.log("WeChat MCP Status\n");
|
|
1246
|
+
if (account) {
|
|
1247
|
+
console.log(` WeChat: Logged in`);
|
|
1248
|
+
console.log(` Account ID: ${account.accountId}`);
|
|
1249
|
+
console.log(` User ID: ${account.userId ?? "N/A"}`);
|
|
1250
|
+
console.log(` Saved at: ${account.savedAt}`);
|
|
1251
|
+
} else {
|
|
1252
|
+
console.log(` WeChat: Not logged in`);
|
|
1253
|
+
console.log(` Run: wechat-mcp setup
|
|
1254
|
+
`);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
console.log();
|
|
1258
|
+
const daemonInfo = await checkDaemon2();
|
|
1259
|
+
if (daemonInfo) {
|
|
1260
|
+
console.log(` Daemon: Running (PID ${daemonInfo.pid}, port ${daemonInfo.port})`);
|
|
1261
|
+
try {
|
|
1262
|
+
const { DaemonClient: DaemonClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1263
|
+
const client = new DaemonClient2(daemonInfo.port);
|
|
1264
|
+
const status = await client.status();
|
|
1265
|
+
console.log(` Connected: ${status.wechatConnected ? "Yes" : "No"}`);
|
|
1266
|
+
console.log(` Uptime: ${Math.round(status.uptime / 1e3)}s`);
|
|
1267
|
+
if (status.agents.length > 0) {
|
|
1268
|
+
console.log(` Agents:`);
|
|
1269
|
+
for (const a of status.agents) {
|
|
1270
|
+
console.log(` - ${a.name} (${a.agentId})`);
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
console.log(` Agents: (none)`);
|
|
1274
|
+
}
|
|
1275
|
+
} catch {
|
|
1276
|
+
console.log(` Status: Unable to query daemon`);
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(` Daemon: Not running`);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
program.command("contacts").description("List known WeChat contacts").action(async () => {
|
|
1283
|
+
const { loadContacts: loadContacts2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1284
|
+
const contacts = loadContacts2();
|
|
1285
|
+
if (contacts.length === 0) {
|
|
1286
|
+
console.log("No contacts yet. Start the daemon and receive messages first.");
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
console.log(`Known WeChat contacts (${contacts.length}):
|
|
1290
|
+
`);
|
|
1291
|
+
for (const c of contacts) {
|
|
1292
|
+
console.log(` ${c.displayName ?? c.userId}`);
|
|
1293
|
+
console.log(` User ID: ${c.userId}`);
|
|
1294
|
+
console.log(` Last seen: ${c.lastSeen}
|
|
1295
|
+
`);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
program.command("send").description("Send a one-shot message to a WeChat user").requiredOption("--to <userId>", "Target user ID or display name").requiredOption("--text <message>", "Message text to send").action(async (opts) => {
|
|
1299
|
+
const { loadCredentials: loadCredentials2, getContactToken: getContactToken2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1300
|
+
const { sendTextMessage: sendTextMessage2 } = await Promise.resolve().then(() => (init_wechat_api(), wechat_api_exports));
|
|
1301
|
+
const { MAX_WECHAT_MSG_LENGTH: MAX_WECHAT_MSG_LENGTH2 } = await Promise.resolve().then(() => (init_constants(), constants_exports));
|
|
1302
|
+
const account = loadCredentials2();
|
|
1303
|
+
if (!account) {
|
|
1304
|
+
console.error("No WeChat credentials. Run `wechat-mcp setup` first.");
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
const contextToken = getContactToken2(opts.to);
|
|
1308
|
+
if (!contextToken) {
|
|
1309
|
+
console.error(`No context token for "${opts.to}". User must message the bot first.`);
|
|
1310
|
+
console.error("Run `wechat-mcp contacts` to see known contacts.");
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
const text = String(opts.text);
|
|
1314
|
+
try {
|
|
1315
|
+
for (let i = 0; i < text.length; i += MAX_WECHAT_MSG_LENGTH2) {
|
|
1316
|
+
await sendTextMessage2(
|
|
1317
|
+
account.baseUrl,
|
|
1318
|
+
account.token,
|
|
1319
|
+
opts.to,
|
|
1320
|
+
text.slice(i, i + MAX_WECHAT_MSG_LENGTH2),
|
|
1321
|
+
contextToken
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
console.log(`Message sent to ${opts.to}`);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
console.error(`Failed to send: ${err instanceof Error ? err.message : String(err)}`);
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
program.command("logout").description("Remove WeChat credentials and stop daemon").action(async () => {
|
|
1331
|
+
const { loadCredentials: loadCredentials2, removeCredentials: removeCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1332
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
1333
|
+
if (!loadCredentials2()) {
|
|
1334
|
+
console.log("No credentials found. Already logged out.");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
await stopDaemon2();
|
|
1338
|
+
removeCredentials2();
|
|
1339
|
+
console.log("Credentials removed and daemon stopped.");
|
|
1340
|
+
});
|
|
1341
|
+
program.parse();
|