@paean-ai/wechat-mcp 0.1.0 → 0.2.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/README.md +102 -26
- package/dist/cli.js +359 -80
- package/dist/daemon/server.js +200 -14
- package/dist/index.d.ts +58 -5
- package/dist/index.js +115 -7
- package/package.json +2 -2
package/dist/daemon/server.js
CHANGED
|
@@ -221,18 +221,22 @@ var DaemonStore = class {
|
|
|
221
221
|
agents = /* @__PURE__ */ new Map();
|
|
222
222
|
defaultAgentId = null;
|
|
223
223
|
startedAt = Date.now();
|
|
224
|
-
registerAgent(agentId, name) {
|
|
224
|
+
registerAgent(agentId, name, mode = "poll", gatewayUrl, gatewayType) {
|
|
225
225
|
const existing = this.agents.get(agentId);
|
|
226
226
|
if (existing) return existing.registration;
|
|
227
227
|
const registration = {
|
|
228
228
|
agentId,
|
|
229
229
|
name: name || agentId,
|
|
230
|
+
mode,
|
|
231
|
+
gatewayUrl,
|
|
232
|
+
gatewayType,
|
|
230
233
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
231
234
|
};
|
|
232
235
|
this.agents.set(agentId, {
|
|
233
236
|
registration,
|
|
234
237
|
messages: [],
|
|
235
|
-
waiters: []
|
|
238
|
+
waiters: [],
|
|
239
|
+
conversationIds: /* @__PURE__ */ new Map()
|
|
236
240
|
});
|
|
237
241
|
if (!this.defaultAgentId) {
|
|
238
242
|
this.defaultAgentId = agentId;
|
|
@@ -256,6 +260,9 @@ var DaemonStore = class {
|
|
|
256
260
|
getAgents() {
|
|
257
261
|
return Array.from(this.agents.values()).map((s) => s.registration);
|
|
258
262
|
}
|
|
263
|
+
getAgent(agentId) {
|
|
264
|
+
return this.agents.get(agentId)?.registration ?? null;
|
|
265
|
+
}
|
|
259
266
|
getDefaultAgentId() {
|
|
260
267
|
return this.defaultAgentId;
|
|
261
268
|
}
|
|
@@ -265,6 +272,12 @@ var DaemonStore = class {
|
|
|
265
272
|
getUptime() {
|
|
266
273
|
return Date.now() - this.startedAt;
|
|
267
274
|
}
|
|
275
|
+
getConversationId(agentId, senderId) {
|
|
276
|
+
return this.agents.get(agentId)?.conversationIds.get(senderId);
|
|
277
|
+
}
|
|
278
|
+
setConversationId(agentId, senderId, convId) {
|
|
279
|
+
this.agents.get(agentId)?.conversationIds.set(senderId, convId);
|
|
280
|
+
}
|
|
268
281
|
/**
|
|
269
282
|
* Push a message to a specific agent's queue.
|
|
270
283
|
* If an agent has a waiting long-poll, resolve it immediately.
|
|
@@ -316,6 +329,18 @@ function parseMention(text) {
|
|
|
316
329
|
}
|
|
317
330
|
return { agent: match[1].toLowerCase(), rest: match[2] || "" };
|
|
318
331
|
}
|
|
332
|
+
function buildMessage(senderId, text, rawText, contextToken, targetAgent) {
|
|
333
|
+
return {
|
|
334
|
+
id: crypto2.randomBytes(8).toString("hex"),
|
|
335
|
+
senderId,
|
|
336
|
+
senderName: senderId.split("@")[0] || senderId,
|
|
337
|
+
text,
|
|
338
|
+
rawText,
|
|
339
|
+
contextToken,
|
|
340
|
+
targetAgent,
|
|
341
|
+
timestamp: Date.now()
|
|
342
|
+
};
|
|
343
|
+
}
|
|
319
344
|
function routeMessage(store2, senderId, text, contextToken) {
|
|
320
345
|
const { agent, rest } = parseMention(text);
|
|
321
346
|
let targetAgent = null;
|
|
@@ -328,20 +353,112 @@ function routeMessage(store2, senderId, text, contextToken) {
|
|
|
328
353
|
strippedText = text;
|
|
329
354
|
}
|
|
330
355
|
if (!targetAgent) return null;
|
|
331
|
-
const message =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
rawText: text,
|
|
337
|
-
contextToken,
|
|
338
|
-
targetAgent,
|
|
339
|
-
timestamp: Date.now()
|
|
340
|
-
};
|
|
356
|
+
const message = buildMessage(senderId, strippedText, text, contextToken, targetAgent);
|
|
357
|
+
const reg = store2.getAgent(targetAgent);
|
|
358
|
+
if (reg?.mode === "gateway") {
|
|
359
|
+
return message;
|
|
360
|
+
}
|
|
341
361
|
store2.pushMessage(targetAgent, message);
|
|
342
362
|
return message;
|
|
343
363
|
}
|
|
344
364
|
|
|
365
|
+
// src/daemon/adapters.ts
|
|
366
|
+
async function readSSEStream(body, handler) {
|
|
367
|
+
const reader = body.getReader();
|
|
368
|
+
const decoder = new TextDecoder();
|
|
369
|
+
let buffer = "";
|
|
370
|
+
while (true) {
|
|
371
|
+
const { done, value } = await reader.read();
|
|
372
|
+
if (done) break;
|
|
373
|
+
buffer += decoder.decode(value, { stream: true });
|
|
374
|
+
const lines = buffer.split("\n");
|
|
375
|
+
buffer = lines.pop() || "";
|
|
376
|
+
for (const line of lines) {
|
|
377
|
+
handler(line);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
var clawAdapter = {
|
|
382
|
+
name: "claw",
|
|
383
|
+
async send(gatewayUrl, message, signal, conversationId) {
|
|
384
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/api/chat`;
|
|
385
|
+
const body = { message };
|
|
386
|
+
if (conversationId) body.conversationId = conversationId;
|
|
387
|
+
const res = await fetch(url, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "Content-Type": "application/json" },
|
|
390
|
+
body: JSON.stringify(body),
|
|
391
|
+
signal
|
|
392
|
+
});
|
|
393
|
+
if (!res.ok || !res.body) {
|
|
394
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
395
|
+
}
|
|
396
|
+
let fullContent = "";
|
|
397
|
+
let returnedConversationId;
|
|
398
|
+
await readSSEStream(res.body, (line) => {
|
|
399
|
+
if (!line.startsWith("data: ")) return;
|
|
400
|
+
try {
|
|
401
|
+
const event = JSON.parse(line.slice(6));
|
|
402
|
+
switch (event.type) {
|
|
403
|
+
case "start":
|
|
404
|
+
if (event.conversationId) returnedConversationId = event.conversationId;
|
|
405
|
+
break;
|
|
406
|
+
case "content":
|
|
407
|
+
fullContent += event.text || "";
|
|
408
|
+
break;
|
|
409
|
+
case "done":
|
|
410
|
+
if (event.content && !fullContent) fullContent = event.content;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return { content: fullContent, conversationId: returnedConversationId };
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var openaiAdapter = {
|
|
420
|
+
name: "openai",
|
|
421
|
+
async send(gatewayUrl, message, signal) {
|
|
422
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/v1/chat/completions`;
|
|
423
|
+
const res = await fetch(url, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: { "Content-Type": "application/json" },
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
model: "default",
|
|
428
|
+
messages: [{ role: "user", content: message }],
|
|
429
|
+
stream: true
|
|
430
|
+
}),
|
|
431
|
+
signal
|
|
432
|
+
});
|
|
433
|
+
if (!res.ok || !res.body) {
|
|
434
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
435
|
+
}
|
|
436
|
+
let fullContent = "";
|
|
437
|
+
await readSSEStream(res.body, (line) => {
|
|
438
|
+
const trimmed = line.trim();
|
|
439
|
+
if (!trimmed.startsWith("data: ") || trimmed === "data: [DONE]") return;
|
|
440
|
+
try {
|
|
441
|
+
const chunk = JSON.parse(trimmed.slice(6));
|
|
442
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
443
|
+
if (delta?.content) {
|
|
444
|
+
fullContent += delta.content;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
return { content: fullContent };
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
function getAdapter(type) {
|
|
453
|
+
switch (type) {
|
|
454
|
+
case "openai":
|
|
455
|
+
return openaiAdapter;
|
|
456
|
+
case "claw":
|
|
457
|
+
default:
|
|
458
|
+
return clawAdapter;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
345
462
|
// src/daemon/server.ts
|
|
346
463
|
var store = new DaemonStore();
|
|
347
464
|
var activeAccount = null;
|
|
@@ -391,8 +508,19 @@ async function handleRequest(req, res) {
|
|
|
391
508
|
json(res, 400, { error: "agentId required" });
|
|
392
509
|
return;
|
|
393
510
|
}
|
|
394
|
-
const
|
|
395
|
-
|
|
511
|
+
const mode = body.mode || "poll";
|
|
512
|
+
if (mode === "gateway" && !body.gatewayUrl) {
|
|
513
|
+
json(res, 400, { error: "gatewayUrl required for gateway mode" });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const reg = store.registerAgent(
|
|
517
|
+
body.agentId,
|
|
518
|
+
body.name,
|
|
519
|
+
mode,
|
|
520
|
+
body.gatewayUrl,
|
|
521
|
+
body.gatewayType
|
|
522
|
+
);
|
|
523
|
+
log(`Agent registered: ${reg.agentId} (${reg.name}) mode=${reg.mode}${reg.gatewayUrl ? ` gateway=${reg.gatewayUrl}` : ""}`);
|
|
396
524
|
json(res, 200, reg);
|
|
397
525
|
return;
|
|
398
526
|
}
|
|
@@ -461,6 +589,58 @@ async function handleRequest(req, res) {
|
|
|
461
589
|
json(res, 500, { error: "internal error" });
|
|
462
590
|
}
|
|
463
591
|
}
|
|
592
|
+
async function bridgeToGateway(agentId, senderId, text, contextToken) {
|
|
593
|
+
const reg = store.getAgent(agentId);
|
|
594
|
+
if (!reg?.gatewayUrl) {
|
|
595
|
+
log(`Gateway bridge failed: no gatewayUrl for ${agentId}`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const adapter = getAdapter(reg.gatewayType || "claw");
|
|
599
|
+
const conversationId = store.getConversationId(agentId, senderId);
|
|
600
|
+
log(`Gateway bridge: ${agentId} \u2192 ${reg.gatewayUrl} (${adapter.name})`);
|
|
601
|
+
try {
|
|
602
|
+
const controller = new AbortController();
|
|
603
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
604
|
+
const result = await adapter.send(
|
|
605
|
+
reg.gatewayUrl,
|
|
606
|
+
text,
|
|
607
|
+
controller.signal,
|
|
608
|
+
conversationId
|
|
609
|
+
);
|
|
610
|
+
clearTimeout(timeout);
|
|
611
|
+
if (result.conversationId) {
|
|
612
|
+
store.setConversationId(agentId, senderId, result.conversationId);
|
|
613
|
+
}
|
|
614
|
+
if (result.content && activeAccount && contextToken) {
|
|
615
|
+
const preview = result.content.length > 80 ? result.content.slice(0, 80) + "..." : result.content;
|
|
616
|
+
log(`Gateway response: ${agentId} \u2192 ${senderId}: "${preview}"`);
|
|
617
|
+
for (let i = 0; i < result.content.length; i += MAX_WECHAT_MSG_LENGTH) {
|
|
618
|
+
await sendTextMessage(
|
|
619
|
+
activeAccount.baseUrl,
|
|
620
|
+
activeAccount.token,
|
|
621
|
+
senderId,
|
|
622
|
+
result.content.slice(i, i + MAX_WECHAT_MSG_LENGTH),
|
|
623
|
+
contextToken
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
629
|
+
log(`Gateway bridge error for ${agentId}: ${errMsg}`);
|
|
630
|
+
if (activeAccount && contextToken) {
|
|
631
|
+
try {
|
|
632
|
+
await sendTextMessage(
|
|
633
|
+
activeAccount.baseUrl,
|
|
634
|
+
activeAccount.token,
|
|
635
|
+
senderId,
|
|
636
|
+
`[${agentId}] Error: ${errMsg}`,
|
|
637
|
+
contextToken
|
|
638
|
+
);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
464
644
|
async function startPolling(account) {
|
|
465
645
|
let buf = loadSyncBuf();
|
|
466
646
|
let failures = 0;
|
|
@@ -501,6 +681,12 @@ async function startPolling(account) {
|
|
|
501
681
|
const routed = routeMessage(store, senderId, text, contextToken);
|
|
502
682
|
if (routed) {
|
|
503
683
|
log(`Message routed: from=${senderId} agent=${routed.targetAgent} text="${text.slice(0, 50)}..."`);
|
|
684
|
+
const reg = routed.targetAgent ? store.getAgent(routed.targetAgent) : null;
|
|
685
|
+
if (reg?.mode === "gateway") {
|
|
686
|
+
bridgeToGateway(routed.targetAgent, senderId, routed.text, contextToken).catch((err) => {
|
|
687
|
+
log(`Gateway bridge async error: ${err instanceof Error ? err.message : String(err)}`);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
504
690
|
} else {
|
|
505
691
|
log(`Message dropped (no agents): from=${senderId} text="${text.slice(0, 50)}..."`);
|
|
506
692
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -50,9 +50,29 @@ interface GetUpdatesResp {
|
|
|
50
50
|
get_updates_buf?: string;
|
|
51
51
|
longpolling_timeout_ms?: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* How messages are delivered to and processed by an agent:
|
|
55
|
+
*
|
|
56
|
+
* channel — Push via MCP notification (Claude Code's proprietary channel protocol).
|
|
57
|
+
* Messages appear in the agent's active conversation.
|
|
58
|
+
*
|
|
59
|
+
* gateway — Daemon forwards message to the agent's HTTP endpoint (POST /api/chat)
|
|
60
|
+
* and relays the SSE response back through WeChat. Like AnyClaw bridge.
|
|
61
|
+
*
|
|
62
|
+
* poll — Agent actively reads messages via the wechat_read_messages MCP tool.
|
|
63
|
+
* Fallback for agents that support neither push nor HTTP gateway.
|
|
64
|
+
*/
|
|
65
|
+
type AgentMode = "channel" | "gateway" | "poll";
|
|
66
|
+
/**
|
|
67
|
+
* For gateway-mode agents, which HTTP protocol the agent speaks.
|
|
68
|
+
*/
|
|
69
|
+
type GatewayType = "claw" | "openai";
|
|
53
70
|
interface AgentRegistration {
|
|
54
71
|
agentId: string;
|
|
55
72
|
name: string;
|
|
73
|
+
mode: AgentMode;
|
|
74
|
+
gatewayUrl?: string;
|
|
75
|
+
gatewayType?: GatewayType;
|
|
56
76
|
registeredAt: string;
|
|
57
77
|
}
|
|
58
78
|
interface RoutedMessage {
|
|
@@ -95,6 +115,14 @@ interface SendMessageResponse {
|
|
|
95
115
|
interface PollMessagesResponse {
|
|
96
116
|
messages: RoutedMessage[];
|
|
97
117
|
}
|
|
118
|
+
interface GatewaySendResult {
|
|
119
|
+
content: string;
|
|
120
|
+
conversationId?: string;
|
|
121
|
+
}
|
|
122
|
+
interface GatewayAdapter {
|
|
123
|
+
name: string;
|
|
124
|
+
send(gatewayUrl: string, message: string, signal?: AbortSignal, conversationId?: string): Promise<GatewaySendResult>;
|
|
125
|
+
}
|
|
98
126
|
|
|
99
127
|
/**
|
|
100
128
|
* Unified WeChat iLink API Client
|
|
@@ -157,25 +185,50 @@ declare class DaemonClient {
|
|
|
157
185
|
private request;
|
|
158
186
|
health(): Promise<boolean>;
|
|
159
187
|
status(): Promise<DaemonStatus>;
|
|
160
|
-
registerAgent(agentId: string, name?: string): Promise<AgentRegistration>;
|
|
188
|
+
registerAgent(agentId: string, name?: string, mode?: AgentMode, gatewayUrl?: string, gatewayType?: GatewayType): Promise<AgentRegistration>;
|
|
161
189
|
unregisterAgent(agentId: string): Promise<{
|
|
162
190
|
removed: boolean;
|
|
163
191
|
}>;
|
|
164
192
|
/**
|
|
165
193
|
* Long-poll for messages intended for this agent.
|
|
166
|
-
* Will block up to ~30s on the daemon side.
|
|
194
|
+
* Will block up to ~30s on the daemon side by default.
|
|
195
|
+
* Pass a custom timeoutMs for shorter waits (e.g. 500 for non-blocking read).
|
|
167
196
|
*/
|
|
168
|
-
pollMessages(agentId: string): Promise<RoutedMessage[]>;
|
|
197
|
+
pollMessages(agentId: string, clientTimeoutMs?: number): Promise<RoutedMessage[]>;
|
|
169
198
|
send(to: string, text: string, agentId: string): Promise<SendMessageResponse>;
|
|
170
199
|
getContacts(): Promise<WechatContact[]>;
|
|
171
200
|
}
|
|
172
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Gateway Adapters
|
|
204
|
+
*
|
|
205
|
+
* Protocol translators for different agent gateway types.
|
|
206
|
+
* Ported from AnyClaw bridge — each adapter knows how to POST a message
|
|
207
|
+
* to a local AI agent's HTTP endpoint and collect the response.
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Claw adapter (OpenClaw / 0claw / PaeanClaw / ZeroClaw / NanoClaw)
|
|
212
|
+
* All expose POST /api/chat with SSE response.
|
|
213
|
+
*/
|
|
214
|
+
declare const clawAdapter: GatewayAdapter;
|
|
215
|
+
/**
|
|
216
|
+
* OpenAI-compatible adapter
|
|
217
|
+
* For any endpoint that speaks POST /v1/chat/completions with SSE.
|
|
218
|
+
*/
|
|
219
|
+
declare const openaiAdapter: GatewayAdapter;
|
|
220
|
+
declare function getAdapter(type: GatewayType): GatewayAdapter;
|
|
221
|
+
|
|
173
222
|
/**
|
|
174
223
|
* Message Router
|
|
175
224
|
*
|
|
176
225
|
* Parses incoming WeChat messages for @agent_name mentions and routes
|
|
177
226
|
* them to the appropriate agent. Messages without a mention go to the
|
|
178
227
|
* default agent.
|
|
228
|
+
*
|
|
229
|
+
* For channel/poll-mode agents, messages are queued in the store.
|
|
230
|
+
* For gateway-mode agents, the message is returned without queuing —
|
|
231
|
+
* the daemon server handles the HTTP gateway bridge directly.
|
|
179
232
|
*/
|
|
180
233
|
|
|
181
234
|
/**
|
|
@@ -188,8 +241,8 @@ declare function parseMention(text: string): {
|
|
|
188
241
|
rest: string;
|
|
189
242
|
};
|
|
190
243
|
|
|
191
|
-
declare const PACKAGE_VERSION = "0.
|
|
244
|
+
declare const PACKAGE_VERSION = "0.2.0";
|
|
192
245
|
declare const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
193
246
|
declare const CONFIG_DIR: string;
|
|
194
247
|
|
|
195
|
-
export { type AccountData, type AgentRegistration, CONFIG_DIR, DEFAULT_BASE_URL, DaemonClient, type DaemonInfo, type DaemonStatus, type GetUpdatesResp, PACKAGE_VERSION, type PollMessagesResponse, type QRCodeResponse, type QRStatusResponse, type RoutedMessage, type SendMessageRequest, type SendMessageResponse, type WechatContact, type WeixinMessage, checkDaemon, ensureDaemon, extractTextFromMessage, fetchQRCode, getContactToken, getUpdates, loadContacts, loadCredentials, parseMention, pollQRStatus, removeCredentials, saveContact, saveCredentials, sendTextMessage, stopDaemon };
|
|
248
|
+
export { type AccountData, type AgentMode, type AgentRegistration, CONFIG_DIR, DEFAULT_BASE_URL, DaemonClient, type DaemonInfo, type DaemonStatus, type GatewayAdapter, type GatewaySendResult, type GatewayType, type GetUpdatesResp, PACKAGE_VERSION, type PollMessagesResponse, type QRCodeResponse, type QRStatusResponse, type RoutedMessage, type SendMessageRequest, type SendMessageResponse, type WechatContact, type WeixinMessage, checkDaemon, clawAdapter, ensureDaemon, extractTextFromMessage, fetchQRCode, getAdapter, getContactToken, getUpdates, loadContacts, loadCredentials, openaiAdapter, parseMention, pollQRStatus, removeCredentials, saveContact, saveCredentials, sendTextMessage, stopDaemon };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import crypto from "crypto";
|
|
|
4
4
|
// src/constants.ts
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
|
-
var PACKAGE_VERSION = "0.
|
|
7
|
+
var PACKAGE_VERSION = "0.2.0";
|
|
8
8
|
var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
9
9
|
var BOT_TYPE = "3";
|
|
10
10
|
var CHANNEL_VERSION = "0.1.0";
|
|
@@ -382,22 +382,30 @@ var DaemonClient = class {
|
|
|
382
382
|
async status() {
|
|
383
383
|
return this.request("GET", "/status");
|
|
384
384
|
}
|
|
385
|
-
async registerAgent(agentId, name) {
|
|
386
|
-
return this.request("POST", "/agents", {
|
|
385
|
+
async registerAgent(agentId, name, mode, gatewayUrl, gatewayType) {
|
|
386
|
+
return this.request("POST", "/agents", {
|
|
387
|
+
agentId,
|
|
388
|
+
name,
|
|
389
|
+
mode,
|
|
390
|
+
gatewayUrl,
|
|
391
|
+
gatewayType
|
|
392
|
+
});
|
|
387
393
|
}
|
|
388
394
|
async unregisterAgent(agentId) {
|
|
389
395
|
return this.request("DELETE", `/agents/${encodeURIComponent(agentId)}`);
|
|
390
396
|
}
|
|
391
397
|
/**
|
|
392
398
|
* Long-poll for messages intended for this agent.
|
|
393
|
-
* Will block up to ~30s on the daemon side.
|
|
399
|
+
* Will block up to ~30s on the daemon side by default.
|
|
400
|
+
* Pass a custom timeoutMs for shorter waits (e.g. 500 for non-blocking read).
|
|
394
401
|
*/
|
|
395
|
-
async pollMessages(agentId) {
|
|
402
|
+
async pollMessages(agentId, clientTimeoutMs) {
|
|
403
|
+
const path3 = clientTimeoutMs !== void 0 ? `/messages/${encodeURIComponent(agentId)}?timeout=${clientTimeoutMs}` : `/messages/${encodeURIComponent(agentId)}`;
|
|
396
404
|
const resp = await this.request(
|
|
397
405
|
"GET",
|
|
398
|
-
|
|
406
|
+
path3,
|
|
399
407
|
void 0,
|
|
400
|
-
35e3
|
|
408
|
+
Math.max(clientTimeoutMs ?? 35e3, 35e3)
|
|
401
409
|
);
|
|
402
410
|
return resp.messages;
|
|
403
411
|
}
|
|
@@ -410,6 +418,103 @@ var DaemonClient = class {
|
|
|
410
418
|
}
|
|
411
419
|
};
|
|
412
420
|
|
|
421
|
+
// src/daemon/adapters.ts
|
|
422
|
+
async function readSSEStream(body, handler) {
|
|
423
|
+
const reader = body.getReader();
|
|
424
|
+
const decoder = new TextDecoder();
|
|
425
|
+
let buffer = "";
|
|
426
|
+
while (true) {
|
|
427
|
+
const { done, value } = await reader.read();
|
|
428
|
+
if (done) break;
|
|
429
|
+
buffer += decoder.decode(value, { stream: true });
|
|
430
|
+
const lines = buffer.split("\n");
|
|
431
|
+
buffer = lines.pop() || "";
|
|
432
|
+
for (const line of lines) {
|
|
433
|
+
handler(line);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
var clawAdapter = {
|
|
438
|
+
name: "claw",
|
|
439
|
+
async send(gatewayUrl, message, signal, conversationId) {
|
|
440
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/api/chat`;
|
|
441
|
+
const body = { message };
|
|
442
|
+
if (conversationId) body.conversationId = conversationId;
|
|
443
|
+
const res = await fetch(url, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
446
|
+
body: JSON.stringify(body),
|
|
447
|
+
signal
|
|
448
|
+
});
|
|
449
|
+
if (!res.ok || !res.body) {
|
|
450
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
451
|
+
}
|
|
452
|
+
let fullContent = "";
|
|
453
|
+
let returnedConversationId;
|
|
454
|
+
await readSSEStream(res.body, (line) => {
|
|
455
|
+
if (!line.startsWith("data: ")) return;
|
|
456
|
+
try {
|
|
457
|
+
const event = JSON.parse(line.slice(6));
|
|
458
|
+
switch (event.type) {
|
|
459
|
+
case "start":
|
|
460
|
+
if (event.conversationId) returnedConversationId = event.conversationId;
|
|
461
|
+
break;
|
|
462
|
+
case "content":
|
|
463
|
+
fullContent += event.text || "";
|
|
464
|
+
break;
|
|
465
|
+
case "done":
|
|
466
|
+
if (event.content && !fullContent) fullContent = event.content;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
return { content: fullContent, conversationId: returnedConversationId };
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
var openaiAdapter = {
|
|
476
|
+
name: "openai",
|
|
477
|
+
async send(gatewayUrl, message, signal) {
|
|
478
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/v1/chat/completions`;
|
|
479
|
+
const res = await fetch(url, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: { "Content-Type": "application/json" },
|
|
482
|
+
body: JSON.stringify({
|
|
483
|
+
model: "default",
|
|
484
|
+
messages: [{ role: "user", content: message }],
|
|
485
|
+
stream: true
|
|
486
|
+
}),
|
|
487
|
+
signal
|
|
488
|
+
});
|
|
489
|
+
if (!res.ok || !res.body) {
|
|
490
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
491
|
+
}
|
|
492
|
+
let fullContent = "";
|
|
493
|
+
await readSSEStream(res.body, (line) => {
|
|
494
|
+
const trimmed = line.trim();
|
|
495
|
+
if (!trimmed.startsWith("data: ") || trimmed === "data: [DONE]") return;
|
|
496
|
+
try {
|
|
497
|
+
const chunk = JSON.parse(trimmed.slice(6));
|
|
498
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
499
|
+
if (delta?.content) {
|
|
500
|
+
fullContent += delta.content;
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
return { content: fullContent };
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
function getAdapter(type) {
|
|
509
|
+
switch (type) {
|
|
510
|
+
case "openai":
|
|
511
|
+
return openaiAdapter;
|
|
512
|
+
case "claw":
|
|
513
|
+
default:
|
|
514
|
+
return clawAdapter;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
413
518
|
// src/daemon/router.ts
|
|
414
519
|
import crypto2 from "crypto";
|
|
415
520
|
function parseMention(text) {
|
|
@@ -426,13 +531,16 @@ export {
|
|
|
426
531
|
DaemonClient,
|
|
427
532
|
PACKAGE_VERSION,
|
|
428
533
|
checkDaemon,
|
|
534
|
+
clawAdapter,
|
|
429
535
|
ensureDaemon,
|
|
430
536
|
extractTextFromMessage,
|
|
431
537
|
fetchQRCode,
|
|
538
|
+
getAdapter,
|
|
432
539
|
getContactToken,
|
|
433
540
|
getUpdates,
|
|
434
541
|
loadContacts,
|
|
435
542
|
loadCredentials,
|
|
543
|
+
openaiAdapter,
|
|
436
544
|
parseMention,
|
|
437
545
|
pollQRStatus,
|
|
438
546
|
removeCredentials,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paean-ai/wechat-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "WeChat MCP middleware — shared WeChat connection for multiple AI agents with @mention routing",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "WeChat MCP middleware — shared WeChat connection for multiple AI agents with @mention routing and multi-mode message delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"wechat-mcp": "dist/cli.js"
|