@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/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ var init_constants = __esm({
|
|
|
48
48
|
"src/constants.ts"() {
|
|
49
49
|
"use strict";
|
|
50
50
|
PACKAGE_NAME = "@paean-ai/wechat-mcp";
|
|
51
|
-
PACKAGE_VERSION = "0.
|
|
51
|
+
PACKAGE_VERSION = "0.2.0";
|
|
52
52
|
DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
53
53
|
BOT_TYPE = "3";
|
|
54
54
|
CHANNEL_VERSION = "0.1.0";
|
|
@@ -522,22 +522,30 @@ var init_client = __esm({
|
|
|
522
522
|
async status() {
|
|
523
523
|
return this.request("GET", "/status");
|
|
524
524
|
}
|
|
525
|
-
async registerAgent(agentId, name) {
|
|
526
|
-
return this.request("POST", "/agents", {
|
|
525
|
+
async registerAgent(agentId, name, mode, gatewayUrl, gatewayType) {
|
|
526
|
+
return this.request("POST", "/agents", {
|
|
527
|
+
agentId,
|
|
528
|
+
name,
|
|
529
|
+
mode,
|
|
530
|
+
gatewayUrl,
|
|
531
|
+
gatewayType
|
|
532
|
+
});
|
|
527
533
|
}
|
|
528
534
|
async unregisterAgent(agentId) {
|
|
529
535
|
return this.request("DELETE", `/agents/${encodeURIComponent(agentId)}`);
|
|
530
536
|
}
|
|
531
537
|
/**
|
|
532
538
|
* Long-poll for messages intended for this agent.
|
|
533
|
-
* Will block up to ~30s on the daemon side.
|
|
539
|
+
* Will block up to ~30s on the daemon side by default.
|
|
540
|
+
* Pass a custom timeoutMs for shorter waits (e.g. 500 for non-blocking read).
|
|
534
541
|
*/
|
|
535
|
-
async pollMessages(agentId) {
|
|
542
|
+
async pollMessages(agentId, clientTimeoutMs) {
|
|
543
|
+
const path3 = clientTimeoutMs !== void 0 ? `/messages/${encodeURIComponent(agentId)}?timeout=${clientTimeoutMs}` : `/messages/${encodeURIComponent(agentId)}`;
|
|
536
544
|
const resp = await this.request(
|
|
537
545
|
"GET",
|
|
538
|
-
|
|
546
|
+
path3,
|
|
539
547
|
void 0,
|
|
540
|
-
35e3
|
|
548
|
+
Math.max(clientTimeoutMs ?? 35e3, 35e3)
|
|
541
549
|
);
|
|
542
550
|
return resp.messages;
|
|
543
551
|
}
|
|
@@ -567,8 +575,9 @@ function log(msg) {
|
|
|
567
575
|
process.stderr.write(`[wechat-mcp:mcp] ${msg}
|
|
568
576
|
`);
|
|
569
577
|
}
|
|
570
|
-
async function startMcpServer(
|
|
571
|
-
|
|
578
|
+
async function startMcpServer(opts) {
|
|
579
|
+
const { agentId, mode, gatewayUrl, gatewayType } = opts;
|
|
580
|
+
log(`Starting MCP server for agent "${agentId}" (mode=${mode})...`);
|
|
572
581
|
let daemonInfo;
|
|
573
582
|
try {
|
|
574
583
|
daemonInfo = await ensureDaemon();
|
|
@@ -579,30 +588,46 @@ async function startMcpServer(agentId) {
|
|
|
579
588
|
}
|
|
580
589
|
const client = new DaemonClient(daemonInfo.port);
|
|
581
590
|
try {
|
|
582
|
-
await client.registerAgent(agentId, agentId);
|
|
583
|
-
log(`Registered as agent "${agentId}"`);
|
|
591
|
+
await client.registerAgent(agentId, agentId, mode, gatewayUrl, gatewayType);
|
|
592
|
+
log(`Registered as agent "${agentId}" mode=${mode}`);
|
|
584
593
|
} catch (err) {
|
|
585
594
|
log(`Failed to register agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
586
595
|
process.exit(1);
|
|
587
596
|
}
|
|
597
|
+
const instructions = [
|
|
598
|
+
`You are connected to WeChat via the wechat-mcp middleware (agent: "${agentId}", mode: ${mode}).`
|
|
599
|
+
];
|
|
600
|
+
if (mode === "channel") {
|
|
601
|
+
instructions.push(
|
|
602
|
+
"Incoming WeChat messages arrive as channel notifications from this server.",
|
|
603
|
+
"Use the wechat_send tool to reply. Pass the sender_id from the incoming message."
|
|
604
|
+
);
|
|
605
|
+
} else if (mode === "poll") {
|
|
606
|
+
instructions.push(
|
|
607
|
+
"Use the wechat_read_messages tool to check for new incoming WeChat messages.",
|
|
608
|
+
"Use the wechat_send tool to reply. Pass the sender_id from the message.",
|
|
609
|
+
"Check for new messages periodically \u2014 they queue up until you read them."
|
|
610
|
+
);
|
|
611
|
+
} else if (mode === "gateway") {
|
|
612
|
+
instructions.push(
|
|
613
|
+
"The daemon automatically bridges incoming WeChat messages to your HTTP endpoint.",
|
|
614
|
+
"Use wechat_send to send proactive messages (not replies to incoming ones)."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
instructions.push(
|
|
618
|
+
"Messages are from real WeChat users. Respond in Chinese unless the user writes in another language.",
|
|
619
|
+
"Keep replies concise \u2014 WeChat is a chat app. Strip markdown formatting (WeChat doesn't render it)."
|
|
620
|
+
);
|
|
621
|
+
const capabilities = { tools: {} };
|
|
622
|
+
if (mode === "channel") {
|
|
623
|
+
capabilities.experimental = { "claude/channel": {} };
|
|
624
|
+
}
|
|
588
625
|
const mcp = new Server(
|
|
589
626
|
{ 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
|
-
}
|
|
627
|
+
{ capabilities, instructions: instructions.join("\n") }
|
|
603
628
|
);
|
|
604
|
-
mcp.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
605
|
-
tools
|
|
629
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
630
|
+
const tools = [
|
|
606
631
|
{
|
|
607
632
|
name: "wechat_send",
|
|
608
633
|
description: "Send a text message to a WeChat user via the shared WeChat connection",
|
|
@@ -637,8 +662,19 @@ async function startMcpServer(agentId) {
|
|
|
637
662
|
properties: {}
|
|
638
663
|
}
|
|
639
664
|
}
|
|
640
|
-
]
|
|
641
|
-
|
|
665
|
+
];
|
|
666
|
+
if (mode === "poll") {
|
|
667
|
+
tools.push({
|
|
668
|
+
name: "wechat_read_messages",
|
|
669
|
+
description: "Read pending WeChat messages addressed to this agent. Returns immediately with any queued messages (does not block).",
|
|
670
|
+
inputSchema: {
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return { tools };
|
|
677
|
+
});
|
|
642
678
|
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
643
679
|
const { name, arguments: args } = req.params;
|
|
644
680
|
if (name === "wechat_send") {
|
|
@@ -673,7 +709,7 @@ async function startMcpServer(agentId) {
|
|
|
673
709
|
if (name === "wechat_get_status") {
|
|
674
710
|
try {
|
|
675
711
|
const status = await client.status();
|
|
676
|
-
const agentList = status.agents.map((a) => ` - ${a.name} (${a.agentId})`).join("\n");
|
|
712
|
+
const agentList = status.agents.map((a) => ` - ${a.name} (${a.agentId}) [${a.mode}]${a.gatewayUrl ? ` \u2192 ${a.gatewayUrl}` : ""}`).join("\n");
|
|
677
713
|
const text = [
|
|
678
714
|
`WeChat connected: ${status.wechatConnected ? "yes" : "no"}`,
|
|
679
715
|
`Account: ${status.accountId || "N/A"}`,
|
|
@@ -686,48 +722,60 @@ ${agentList || " (none)"}`
|
|
|
686
722
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
687
723
|
}
|
|
688
724
|
}
|
|
725
|
+
if (name === "wechat_read_messages") {
|
|
726
|
+
try {
|
|
727
|
+
const messages = await client.pollMessages(agentId, 500);
|
|
728
|
+
if (messages.length === 0) {
|
|
729
|
+
return { content: [{ type: "text", text: "No new messages." }] };
|
|
730
|
+
}
|
|
731
|
+
const formatted = messages.map(
|
|
732
|
+
(m) => `[${new Date(m.timestamp).toLocaleTimeString()}] ${m.senderName} (${m.senderId}): ${m.text}`
|
|
733
|
+
).join("\n\n");
|
|
734
|
+
return { content: [{ type: "text", text: formatted }] };
|
|
735
|
+
} catch (err) {
|
|
736
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
689
739
|
throw new Error(`Unknown tool: ${name}`);
|
|
690
740
|
});
|
|
691
741
|
const transport = new StdioServerTransport();
|
|
692
742
|
await mcp.connect(transport);
|
|
693
743
|
log("MCP connection ready");
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
raw_text: msg.rawText,
|
|
711
|
-
timestamp: msg.timestamp
|
|
744
|
+
if (mode === "channel") {
|
|
745
|
+
const pollLoop = async () => {
|
|
746
|
+
while (true) {
|
|
747
|
+
try {
|
|
748
|
+
const messages = await client.pollMessages(agentId);
|
|
749
|
+
for (const msg of messages) {
|
|
750
|
+
log(`Channel push: from=${msg.senderId} text="${msg.text.slice(0, 50)}..."`);
|
|
751
|
+
try {
|
|
752
|
+
await mcp.notification({
|
|
753
|
+
method: "notifications/claude/channel",
|
|
754
|
+
params: {
|
|
755
|
+
content: msg.text,
|
|
756
|
+
meta: {
|
|
757
|
+
sender: msg.senderName,
|
|
758
|
+
sender_id: msg.senderId
|
|
759
|
+
}
|
|
712
760
|
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
761
|
+
});
|
|
762
|
+
} catch {
|
|
763
|
+
log(`Channel notification delivery failed for message ${msg.id}`);
|
|
764
|
+
}
|
|
717
765
|
}
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
768
|
+
if (!errMsg.includes("ECONNREFUSED")) {
|
|
769
|
+
log(`Poll error: ${errMsg}`);
|
|
770
|
+
}
|
|
771
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
718
772
|
}
|
|
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
773
|
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
774
|
+
};
|
|
775
|
+
pollLoop().catch((err) => {
|
|
776
|
+
log(`Fatal poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
731
779
|
const cleanup = async () => {
|
|
732
780
|
try {
|
|
733
781
|
await client.unregisterAgent(agentId);
|
|
@@ -757,18 +805,22 @@ var init_store = __esm({
|
|
|
757
805
|
agents = /* @__PURE__ */ new Map();
|
|
758
806
|
defaultAgentId = null;
|
|
759
807
|
startedAt = Date.now();
|
|
760
|
-
registerAgent(agentId, name) {
|
|
808
|
+
registerAgent(agentId, name, mode = "poll", gatewayUrl, gatewayType) {
|
|
761
809
|
const existing = this.agents.get(agentId);
|
|
762
810
|
if (existing) return existing.registration;
|
|
763
811
|
const registration = {
|
|
764
812
|
agentId,
|
|
765
813
|
name: name || agentId,
|
|
814
|
+
mode,
|
|
815
|
+
gatewayUrl,
|
|
816
|
+
gatewayType,
|
|
766
817
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
767
818
|
};
|
|
768
819
|
this.agents.set(agentId, {
|
|
769
820
|
registration,
|
|
770
821
|
messages: [],
|
|
771
|
-
waiters: []
|
|
822
|
+
waiters: [],
|
|
823
|
+
conversationIds: /* @__PURE__ */ new Map()
|
|
772
824
|
});
|
|
773
825
|
if (!this.defaultAgentId) {
|
|
774
826
|
this.defaultAgentId = agentId;
|
|
@@ -792,6 +844,9 @@ var init_store = __esm({
|
|
|
792
844
|
getAgents() {
|
|
793
845
|
return Array.from(this.agents.values()).map((s) => s.registration);
|
|
794
846
|
}
|
|
847
|
+
getAgent(agentId) {
|
|
848
|
+
return this.agents.get(agentId)?.registration ?? null;
|
|
849
|
+
}
|
|
795
850
|
getDefaultAgentId() {
|
|
796
851
|
return this.defaultAgentId;
|
|
797
852
|
}
|
|
@@ -801,6 +856,12 @@ var init_store = __esm({
|
|
|
801
856
|
getUptime() {
|
|
802
857
|
return Date.now() - this.startedAt;
|
|
803
858
|
}
|
|
859
|
+
getConversationId(agentId, senderId) {
|
|
860
|
+
return this.agents.get(agentId)?.conversationIds.get(senderId);
|
|
861
|
+
}
|
|
862
|
+
setConversationId(agentId, senderId, convId) {
|
|
863
|
+
this.agents.get(agentId)?.conversationIds.set(senderId, convId);
|
|
864
|
+
}
|
|
804
865
|
/**
|
|
805
866
|
* Push a message to a specific agent's queue.
|
|
806
867
|
* If an agent has a waiting long-poll, resolve it immediately.
|
|
@@ -854,6 +915,18 @@ function parseMention(text) {
|
|
|
854
915
|
}
|
|
855
916
|
return { agent: match[1].toLowerCase(), rest: match[2] || "" };
|
|
856
917
|
}
|
|
918
|
+
function buildMessage(senderId, text, rawText, contextToken, targetAgent) {
|
|
919
|
+
return {
|
|
920
|
+
id: crypto2.randomBytes(8).toString("hex"),
|
|
921
|
+
senderId,
|
|
922
|
+
senderName: senderId.split("@")[0] || senderId,
|
|
923
|
+
text,
|
|
924
|
+
rawText,
|
|
925
|
+
contextToken,
|
|
926
|
+
targetAgent,
|
|
927
|
+
timestamp: Date.now()
|
|
928
|
+
};
|
|
929
|
+
}
|
|
857
930
|
function routeMessage(store2, senderId, text, contextToken) {
|
|
858
931
|
const { agent, rest } = parseMention(text);
|
|
859
932
|
let targetAgent = null;
|
|
@@ -866,16 +939,11 @@ function routeMessage(store2, senderId, text, contextToken) {
|
|
|
866
939
|
strippedText = text;
|
|
867
940
|
}
|
|
868
941
|
if (!targetAgent) return null;
|
|
869
|
-
const message =
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
rawText: text,
|
|
875
|
-
contextToken,
|
|
876
|
-
targetAgent,
|
|
877
|
-
timestamp: Date.now()
|
|
878
|
-
};
|
|
942
|
+
const message = buildMessage(senderId, strippedText, text, contextToken, targetAgent);
|
|
943
|
+
const reg = store2.getAgent(targetAgent);
|
|
944
|
+
if (reg?.mode === "gateway") {
|
|
945
|
+
return message;
|
|
946
|
+
}
|
|
879
947
|
store2.pushMessage(targetAgent, message);
|
|
880
948
|
return message;
|
|
881
949
|
}
|
|
@@ -885,6 +953,109 @@ var init_router = __esm({
|
|
|
885
953
|
}
|
|
886
954
|
});
|
|
887
955
|
|
|
956
|
+
// src/daemon/adapters.ts
|
|
957
|
+
async function readSSEStream(body, handler) {
|
|
958
|
+
const reader = body.getReader();
|
|
959
|
+
const decoder = new TextDecoder();
|
|
960
|
+
let buffer = "";
|
|
961
|
+
while (true) {
|
|
962
|
+
const { done, value } = await reader.read();
|
|
963
|
+
if (done) break;
|
|
964
|
+
buffer += decoder.decode(value, { stream: true });
|
|
965
|
+
const lines = buffer.split("\n");
|
|
966
|
+
buffer = lines.pop() || "";
|
|
967
|
+
for (const line of lines) {
|
|
968
|
+
handler(line);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function getAdapter(type) {
|
|
973
|
+
switch (type) {
|
|
974
|
+
case "openai":
|
|
975
|
+
return openaiAdapter;
|
|
976
|
+
case "claw":
|
|
977
|
+
default:
|
|
978
|
+
return clawAdapter;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
var clawAdapter, openaiAdapter;
|
|
982
|
+
var init_adapters = __esm({
|
|
983
|
+
"src/daemon/adapters.ts"() {
|
|
984
|
+
"use strict";
|
|
985
|
+
clawAdapter = {
|
|
986
|
+
name: "claw",
|
|
987
|
+
async send(gatewayUrl, message, signal, conversationId) {
|
|
988
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/api/chat`;
|
|
989
|
+
const body = { message };
|
|
990
|
+
if (conversationId) body.conversationId = conversationId;
|
|
991
|
+
const res = await fetch(url, {
|
|
992
|
+
method: "POST",
|
|
993
|
+
headers: { "Content-Type": "application/json" },
|
|
994
|
+
body: JSON.stringify(body),
|
|
995
|
+
signal
|
|
996
|
+
});
|
|
997
|
+
if (!res.ok || !res.body) {
|
|
998
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
999
|
+
}
|
|
1000
|
+
let fullContent = "";
|
|
1001
|
+
let returnedConversationId;
|
|
1002
|
+
await readSSEStream(res.body, (line) => {
|
|
1003
|
+
if (!line.startsWith("data: ")) return;
|
|
1004
|
+
try {
|
|
1005
|
+
const event = JSON.parse(line.slice(6));
|
|
1006
|
+
switch (event.type) {
|
|
1007
|
+
case "start":
|
|
1008
|
+
if (event.conversationId) returnedConversationId = event.conversationId;
|
|
1009
|
+
break;
|
|
1010
|
+
case "content":
|
|
1011
|
+
fullContent += event.text || "";
|
|
1012
|
+
break;
|
|
1013
|
+
case "done":
|
|
1014
|
+
if (event.content && !fullContent) fullContent = event.content;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
return { content: fullContent, conversationId: returnedConversationId };
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
openaiAdapter = {
|
|
1024
|
+
name: "openai",
|
|
1025
|
+
async send(gatewayUrl, message, signal) {
|
|
1026
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/v1/chat/completions`;
|
|
1027
|
+
const res = await fetch(url, {
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
headers: { "Content-Type": "application/json" },
|
|
1030
|
+
body: JSON.stringify({
|
|
1031
|
+
model: "default",
|
|
1032
|
+
messages: [{ role: "user", content: message }],
|
|
1033
|
+
stream: true
|
|
1034
|
+
}),
|
|
1035
|
+
signal
|
|
1036
|
+
});
|
|
1037
|
+
if (!res.ok || !res.body) {
|
|
1038
|
+
throw new Error(`Gateway error: HTTP ${res.status}`);
|
|
1039
|
+
}
|
|
1040
|
+
let fullContent = "";
|
|
1041
|
+
await readSSEStream(res.body, (line) => {
|
|
1042
|
+
const trimmed = line.trim();
|
|
1043
|
+
if (!trimmed.startsWith("data: ") || trimmed === "data: [DONE]") return;
|
|
1044
|
+
try {
|
|
1045
|
+
const chunk = JSON.parse(trimmed.slice(6));
|
|
1046
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
1047
|
+
if (delta?.content) {
|
|
1048
|
+
fullContent += delta.content;
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
return { content: fullContent };
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
888
1059
|
// src/daemon/server.ts
|
|
889
1060
|
var server_exports2 = {};
|
|
890
1061
|
__export(server_exports2, {
|
|
@@ -936,8 +1107,19 @@ async function handleRequest(req, res) {
|
|
|
936
1107
|
json(res, 400, { error: "agentId required" });
|
|
937
1108
|
return;
|
|
938
1109
|
}
|
|
939
|
-
const
|
|
940
|
-
|
|
1110
|
+
const mode = body.mode || "poll";
|
|
1111
|
+
if (mode === "gateway" && !body.gatewayUrl) {
|
|
1112
|
+
json(res, 400, { error: "gatewayUrl required for gateway mode" });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const reg = store.registerAgent(
|
|
1116
|
+
body.agentId,
|
|
1117
|
+
body.name,
|
|
1118
|
+
mode,
|
|
1119
|
+
body.gatewayUrl,
|
|
1120
|
+
body.gatewayType
|
|
1121
|
+
);
|
|
1122
|
+
log2(`Agent registered: ${reg.agentId} (${reg.name}) mode=${reg.mode}${reg.gatewayUrl ? ` gateway=${reg.gatewayUrl}` : ""}`);
|
|
941
1123
|
json(res, 200, reg);
|
|
942
1124
|
return;
|
|
943
1125
|
}
|
|
@@ -1006,6 +1188,58 @@ async function handleRequest(req, res) {
|
|
|
1006
1188
|
json(res, 500, { error: "internal error" });
|
|
1007
1189
|
}
|
|
1008
1190
|
}
|
|
1191
|
+
async function bridgeToGateway(agentId, senderId, text, contextToken) {
|
|
1192
|
+
const reg = store.getAgent(agentId);
|
|
1193
|
+
if (!reg?.gatewayUrl) {
|
|
1194
|
+
log2(`Gateway bridge failed: no gatewayUrl for ${agentId}`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const adapter = getAdapter(reg.gatewayType || "claw");
|
|
1198
|
+
const conversationId = store.getConversationId(agentId, senderId);
|
|
1199
|
+
log2(`Gateway bridge: ${agentId} \u2192 ${reg.gatewayUrl} (${adapter.name})`);
|
|
1200
|
+
try {
|
|
1201
|
+
const controller = new AbortController();
|
|
1202
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
1203
|
+
const result = await adapter.send(
|
|
1204
|
+
reg.gatewayUrl,
|
|
1205
|
+
text,
|
|
1206
|
+
controller.signal,
|
|
1207
|
+
conversationId
|
|
1208
|
+
);
|
|
1209
|
+
clearTimeout(timeout);
|
|
1210
|
+
if (result.conversationId) {
|
|
1211
|
+
store.setConversationId(agentId, senderId, result.conversationId);
|
|
1212
|
+
}
|
|
1213
|
+
if (result.content && activeAccount && contextToken) {
|
|
1214
|
+
const preview = result.content.length > 80 ? result.content.slice(0, 80) + "..." : result.content;
|
|
1215
|
+
log2(`Gateway response: ${agentId} \u2192 ${senderId}: "${preview}"`);
|
|
1216
|
+
for (let i = 0; i < result.content.length; i += MAX_WECHAT_MSG_LENGTH) {
|
|
1217
|
+
await sendTextMessage(
|
|
1218
|
+
activeAccount.baseUrl,
|
|
1219
|
+
activeAccount.token,
|
|
1220
|
+
senderId,
|
|
1221
|
+
result.content.slice(i, i + MAX_WECHAT_MSG_LENGTH),
|
|
1222
|
+
contextToken
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1228
|
+
log2(`Gateway bridge error for ${agentId}: ${errMsg}`);
|
|
1229
|
+
if (activeAccount && contextToken) {
|
|
1230
|
+
try {
|
|
1231
|
+
await sendTextMessage(
|
|
1232
|
+
activeAccount.baseUrl,
|
|
1233
|
+
activeAccount.token,
|
|
1234
|
+
senderId,
|
|
1235
|
+
`[${agentId}] Error: ${errMsg}`,
|
|
1236
|
+
contextToken
|
|
1237
|
+
);
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1009
1243
|
async function startPolling(account) {
|
|
1010
1244
|
let buf = loadSyncBuf();
|
|
1011
1245
|
let failures = 0;
|
|
@@ -1046,6 +1280,12 @@ async function startPolling(account) {
|
|
|
1046
1280
|
const routed = routeMessage(store, senderId, text, contextToken);
|
|
1047
1281
|
if (routed) {
|
|
1048
1282
|
log2(`Message routed: from=${senderId} agent=${routed.targetAgent} text="${text.slice(0, 50)}..."`);
|
|
1283
|
+
const reg = routed.targetAgent ? store.getAgent(routed.targetAgent) : null;
|
|
1284
|
+
if (reg?.mode === "gateway") {
|
|
1285
|
+
bridgeToGateway(routed.targetAgent, senderId, routed.text, contextToken).catch((err) => {
|
|
1286
|
+
log2(`Gateway bridge async error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1049
1289
|
} else {
|
|
1050
1290
|
log2(`Message dropped (no agents): from=${senderId} text="${text.slice(0, 50)}..."`);
|
|
1051
1291
|
}
|
|
@@ -1111,6 +1351,7 @@ var init_server2 = __esm({
|
|
|
1111
1351
|
init_wechat_api();
|
|
1112
1352
|
init_store();
|
|
1113
1353
|
init_router();
|
|
1354
|
+
init_adapters();
|
|
1114
1355
|
store = new DaemonStore();
|
|
1115
1356
|
activeAccount = null;
|
|
1116
1357
|
wechatConnected = false;
|
|
@@ -1199,7 +1440,28 @@ WeChat connected successfully!`);
|
|
|
1199
1440
|
console.log(` Account ID: ${account.accountId}`);
|
|
1200
1441
|
console.log(` User ID: ${account.userId ?? "N/A"}
|
|
1201
1442
|
`);
|
|
1202
|
-
console.log("Add this to any agent's MCP config to connect
|
|
1443
|
+
console.log("Add this to any agent's MCP config to connect:\n");
|
|
1444
|
+
console.log(` Claude Code (channel mode \u2014 messages arrive automatically):`);
|
|
1445
|
+
console.log(` {`);
|
|
1446
|
+
console.log(` "mcpServers": {`);
|
|
1447
|
+
console.log(` "wechat": {`);
|
|
1448
|
+
console.log(` "command": "npx",`);
|
|
1449
|
+
console.log(` "args": ["@paean-ai/wechat-mcp", "serve", "--agent", "claude", "--mode", "channel"]`);
|
|
1450
|
+
console.log(` }`);
|
|
1451
|
+
console.log(` }`);
|
|
1452
|
+
console.log(` }
|
|
1453
|
+
`);
|
|
1454
|
+
console.log(` OpenClaw / other agents (gateway mode \u2014 auto-bridges to HTTP endpoint):`);
|
|
1455
|
+
console.log(` {`);
|
|
1456
|
+
console.log(` "mcpServers": {`);
|
|
1457
|
+
console.log(` "wechat": {`);
|
|
1458
|
+
console.log(` "command": "npx",`);
|
|
1459
|
+
console.log(` "args": ["@paean-ai/wechat-mcp", "serve", "--agent", "openclaw", "--mode", "gateway", "--gateway-url", "http://localhost:3000"]`);
|
|
1460
|
+
console.log(` }`);
|
|
1461
|
+
console.log(` }`);
|
|
1462
|
+
console.log(` }
|
|
1463
|
+
`);
|
|
1464
|
+
console.log(` Generic agent (poll mode \u2014 agent calls wechat_read_messages tool):`);
|
|
1203
1465
|
console.log(` {`);
|
|
1204
1466
|
console.log(` "mcpServers": {`);
|
|
1205
1467
|
console.log(` "wechat": {`);
|
|
@@ -1216,14 +1478,29 @@ WeChat connected successfully!`);
|
|
|
1216
1478
|
console.error("\nLogin timed out. Please try again.");
|
|
1217
1479
|
process.exit(1);
|
|
1218
1480
|
});
|
|
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) => {
|
|
1481
|
+
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)").option("-m, --mode <mode>", "Message delivery mode: channel, gateway, or poll (default: poll)", "poll").option("--gateway-url <url>", "Agent's HTTP endpoint URL (required for gateway mode)").option("--gateway-type <type>", "Gateway protocol: claw or openai (default: claw)", "claw").action(async (opts) => {
|
|
1220
1482
|
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
1221
1483
|
if (!loadCredentials2()) {
|
|
1222
1484
|
process.stderr.write("No WeChat credentials. Run `wechat-mcp setup` first.\n");
|
|
1223
1485
|
process.exit(1);
|
|
1224
1486
|
}
|
|
1487
|
+
const mode = opts.mode;
|
|
1488
|
+
if (!["channel", "gateway", "poll"].includes(mode)) {
|
|
1489
|
+
process.stderr.write(`Invalid mode "${opts.mode}". Must be: channel, gateway, or poll
|
|
1490
|
+
`);
|
|
1491
|
+
process.exit(1);
|
|
1492
|
+
}
|
|
1493
|
+
if (mode === "gateway" && !opts.gatewayUrl) {
|
|
1494
|
+
process.stderr.write("--gateway-url is required when --mode=gateway\n");
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1225
1497
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1226
|
-
await startMcpServer2(
|
|
1498
|
+
await startMcpServer2({
|
|
1499
|
+
agentId: opts.agent,
|
|
1500
|
+
mode,
|
|
1501
|
+
gatewayUrl: opts.gatewayUrl,
|
|
1502
|
+
gatewayType: opts.gatewayType
|
|
1503
|
+
});
|
|
1227
1504
|
});
|
|
1228
1505
|
program.command("daemon").description("Start the daemon process in foreground (usually auto-started)").action(async () => {
|
|
1229
1506
|
const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
|
|
@@ -1267,7 +1544,9 @@ program.command("status").description("Show daemon and WeChat connection status"
|
|
|
1267
1544
|
if (status.agents.length > 0) {
|
|
1268
1545
|
console.log(` Agents:`);
|
|
1269
1546
|
for (const a of status.agents) {
|
|
1270
|
-
|
|
1547
|
+
const modeStr = a.mode ? ` [${a.mode}]` : "";
|
|
1548
|
+
const gwStr = a.gatewayUrl ? ` \u2192 ${a.gatewayUrl}` : "";
|
|
1549
|
+
console.log(` - ${a.name} (${a.agentId})${modeStr}${gwStr}`);
|
|
1271
1550
|
}
|
|
1272
1551
|
} else {
|
|
1273
1552
|
console.log(` Agents: (none)`);
|