weixin-mcp 1.7.3 → 1.7.5

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.en.md CHANGED
@@ -91,6 +91,22 @@ npx weixin-mcp send abc12 "hello"
91
91
  # ✓ Resolved "abc12" → abc123xyz456@im.wechat
92
92
  ```
93
93
 
94
+ ### Send Images/Files/Videos
95
+
96
+ ```bash
97
+ # Send image
98
+ npx weixin-mcp send abc12 --image /path/to/photo.jpg
99
+
100
+ # Send file
101
+ npx weixin-mcp send abc12 --file /path/to/document.pdf
102
+
103
+ # Send video
104
+ npx weixin-mcp send abc12 --video /path/to/video.mp4
105
+
106
+ # With caption
107
+ npx weixin-mcp send abc12 --image /path/to/photo.jpg --caption "Check this out"
108
+ ```
109
+
94
110
  ---
95
111
 
96
112
  ## 🔧 MCP Tools
@@ -143,6 +159,41 @@ Priority: `$WEIXIN_MCP_DIR` > `~/.openclaw/openclaw-weixin/` > `~/.weixin-mcp/`
143
159
 
144
160
  ---
145
161
 
162
+ ## 🔀 Multi-Instance Mode
163
+
164
+ One bot = one WeChat account. For multiple accounts, run instances in different directories:
165
+
166
+ ```bash
167
+ # Instance A (port 3001)
168
+ WEIXIN_MCP_DIR=~/.weixin-mcp-a npx weixin-mcp login
169
+ WEIXIN_MCP_DIR=~/.weixin-mcp-a npx weixin-mcp start --port 3001
170
+
171
+ # Instance B (port 3002)
172
+ WEIXIN_MCP_DIR=~/.weixin-mcp-b npx weixin-mcp login
173
+ WEIXIN_MCP_DIR=~/.weixin-mcp-b npx weixin-mcp start --port 3002
174
+ ```
175
+
176
+ Claude Desktop multi-account config:
177
+
178
+ ```json
179
+ {
180
+ "mcpServers": {
181
+ "weixin-personal": {
182
+ "command": "npx",
183
+ "args": ["weixin-mcp", "start", "--port", "3001"],
184
+ "env": { "WEIXIN_MCP_DIR": "/Users/you/.weixin-mcp-personal" }
185
+ },
186
+ "weixin-work": {
187
+ "command": "npx",
188
+ "args": ["weixin-mcp", "start", "--port", "3002"],
189
+ "env": { "WEIXIN_MCP_DIR": "/Users/you/.weixin-mcp-work" }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ ---
196
+
146
197
  ## 🔗 Related Projects
147
198
 
148
199
  - [OpenClaw](https://github.com/anthropics/openclaw) — AI Agent Infrastructure
package/README.md CHANGED
@@ -91,6 +91,22 @@ npx weixin-mcp send abc12 "hello"
91
91
  # ✓ Resolved "abc12" → abc123xyz456@im.wechat
92
92
  ```
93
93
 
94
+ ### 发送图片/文件/视频
95
+
96
+ ```bash
97
+ # 发送图片
98
+ npx weixin-mcp send abc12 --image /path/to/photo.jpg
99
+
100
+ # 发送文件
101
+ npx weixin-mcp send abc12 --file /path/to/document.pdf
102
+
103
+ # 发送视频
104
+ npx weixin-mcp send abc12 --video /path/to/video.mp4
105
+
106
+ # 带文字说明
107
+ npx weixin-mcp send abc12 --image /path/to/photo.jpg --caption "看看这张图"
108
+ ```
109
+
94
110
  ---
95
111
 
96
112
  ## 🔧 MCP 工具
@@ -143,6 +159,41 @@ npx weixin-mcp start --webhook http://your-server/weixin-hook
143
159
 
144
160
  ---
145
161
 
162
+ ## 🔀 多实例模式
163
+
164
+ 一个 bot = 一个微信账号。需要多账号时,用不同目录运行多个实例:
165
+
166
+ ```bash
167
+ # 实例 A(端口 3001)
168
+ WEIXIN_MCP_DIR=~/.weixin-mcp-a npx weixin-mcp login
169
+ WEIXIN_MCP_DIR=~/.weixin-mcp-a npx weixin-mcp start --port 3001
170
+
171
+ # 实例 B(端口 3002)
172
+ WEIXIN_MCP_DIR=~/.weixin-mcp-b npx weixin-mcp login
173
+ WEIXIN_MCP_DIR=~/.weixin-mcp-b npx weixin-mcp start --port 3002
174
+ ```
175
+
176
+ Claude Desktop 配置多账号:
177
+
178
+ ```json
179
+ {
180
+ "mcpServers": {
181
+ "weixin-personal": {
182
+ "command": "npx",
183
+ "args": ["weixin-mcp", "start", "--port", "3001"],
184
+ "env": { "WEIXIN_MCP_DIR": "/Users/you/.weixin-mcp-personal" }
185
+ },
186
+ "weixin-work": {
187
+ "command": "npx",
188
+ "args": ["weixin-mcp", "start", "--port", "3002"],
189
+ "env": { "WEIXIN_MCP_DIR": "/Users/you/.weixin-mcp-work" }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ ---
196
+
146
197
  ## 🔗 相关项目
147
198
 
148
199
  - [OpenClaw](https://github.com/anthropics/openclaw) — AI Agent 基础设施
package/dist/api.d.ts CHANGED
@@ -53,3 +53,13 @@ export declare function sendFileMessage(to: string, uploaded: UploadedMedia, tok
53
53
  * Send a video message using a previously uploaded file.
54
54
  */
55
55
  export declare function sendVideoMessage(to: string, uploaded: UploadedMedia, token: string, baseUrl: string, contextToken?: string, caption?: string): Promise<void>;
56
+ export type MediaSendType = "image" | "file" | "video";
57
+ export declare function sendMediaMessage(opts: {
58
+ to: string;
59
+ mediaType: MediaSendType;
60
+ uploaded: UploadedMedia;
61
+ caption?: string;
62
+ token: string;
63
+ baseUrl: string;
64
+ contextToken?: string;
65
+ }): Promise<void>;
package/dist/api.js CHANGED
@@ -249,3 +249,15 @@ export async function sendVideoMessage(to, uploaded, token, baseUrl, contextToke
249
249
  }, token, baseUrl);
250
250
  }
251
251
  }
252
+ export async function sendMediaMessage(opts) {
253
+ switch (opts.mediaType) {
254
+ case "image":
255
+ return sendImageMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
256
+ case "file":
257
+ return sendFileMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
258
+ case "video":
259
+ return sendVideoMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
260
+ default:
261
+ throw new Error(`Unknown media type: ${opts.mediaType}`);
262
+ }
263
+ }
package/dist/cli.js CHANGED
@@ -122,7 +122,10 @@ Commands:
122
122
  contacts Show contact book (users who messaged the bot)
123
123
  update Check and install latest version
124
124
  --version / -v Print version
125
- send <userId> <text> Send a message from CLI
125
+ send <userId> <text> Send a text message
126
+ send <userId> --image <path> [--caption <text>] Send an image
127
+ send <userId> --file <path> [--caption <text>] Send a file
128
+ send <userId> --video <path> [--caption <text>] Send a video
126
129
  poll [--watch|-w] [--reset] Poll messages once, or watch continuously
127
130
  accounts [list] List all accounts
128
131
  accounts remove <id> Remove an account
package/dist/messaging.js CHANGED
@@ -6,7 +6,8 @@
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { ACCOUNTS_DIR } from "./paths.js";
9
- import { DEFAULT_BASE_URL, sendTextMessage, getUpdates, loadCursor, saveCursor, } from "./api.js";
9
+ import { DEFAULT_BASE_URL, sendTextMessage, sendMediaMessage, getUpdates, loadCursor, saveCursor, } from "./api.js";
10
+ import { uploadMedia } from "./cdn.js";
10
11
  import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
11
12
  /** Resolve a short/partial userId to a full one from contacts. */
12
13
  function resolveUserId(input) {
@@ -52,26 +53,94 @@ function formatMsg(msg) {
52
53
  const prefix = msgType === 1 ? "← " : "→ "; // incoming vs outgoing
53
54
  return `${prefix}${from.slice(0, 20)}: ${parts.join(" ") || "(empty)"}`;
54
55
  }
56
+ function parseCliSendArgs(args) {
57
+ const opts = { to: "" };
58
+ let i = 0;
59
+ // First arg is always <to>
60
+ if (args[i] && !args[i].startsWith("--")) {
61
+ opts.to = args[i++];
62
+ }
63
+ while (i < args.length) {
64
+ const arg = args[i];
65
+ if (arg === "--image" && args[i + 1]) {
66
+ opts.image = args[++i];
67
+ }
68
+ else if (arg === "--file" && args[i + 1]) {
69
+ opts.file = args[++i];
70
+ }
71
+ else if (arg === "--video" && args[i + 1]) {
72
+ opts.video = args[++i];
73
+ }
74
+ else if (arg === "--caption" && args[i + 1]) {
75
+ opts.caption = args[++i];
76
+ }
77
+ else if (!arg.startsWith("--")) {
78
+ // Collect remaining as text
79
+ opts.text = args.slice(i).join(" ");
80
+ break;
81
+ }
82
+ i++;
83
+ }
84
+ return opts;
85
+ }
55
86
  export async function cliSend(args) {
56
- const [to, ...textParts] = args;
57
- if (!to || textParts.length === 0) {
58
- console.error("Usage: npx weixin-mcp send <userId> <message text>");
87
+ const opts = parseCliSendArgs(args);
88
+ if (!opts.to) {
89
+ console.error(`Usage: npx weixin-mcp send <userId> <text>
90
+ npx weixin-mcp send <userId> --image <path> [--caption <text>]
91
+ npx weixin-mcp send <userId> --file <path> [--caption <text>]
92
+ npx weixin-mcp send <userId> --video <path> [--caption <text>]`);
59
93
  process.exit(1);
60
94
  }
61
- const text = textParts.join(" ");
62
- const resolvedTo = resolveUserId(to);
63
- if (resolvedTo !== to)
64
- console.log(`Resolved "${to}" → ${resolvedTo}`);
95
+ const resolvedTo = resolveUserId(opts.to);
96
+ if (resolvedTo !== opts.to)
97
+ console.log(`Resolved "${opts.to}" → ${resolvedTo}`);
65
98
  const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
66
- process.stdout.write(`Sending to ${resolvedTo}... `);
67
- const result = await sendTextMessage(resolvedTo, text, token, baseUrl);
68
- const ret = result?.ret ?? result?.errcode;
69
- if (ret === 0 || ret === undefined) {
99
+ // Determine what to send
100
+ const mediaPath = opts.image || opts.file || opts.video;
101
+ const mediaType = opts.image ? "image" : opts.file ? "file" : opts.video ? "video" : null;
102
+ if (mediaPath && mediaType) {
103
+ // Check file exists
104
+ if (!fs.existsSync(mediaPath)) {
105
+ console.error(`File not found: ${mediaPath}`);
106
+ process.exit(1);
107
+ }
108
+ process.stdout.write(`Uploading ${mediaType}... `);
109
+ const uploaded = await uploadMedia({
110
+ source: mediaPath,
111
+ mediaType,
112
+ toUserId: resolvedTo,
113
+ token: token,
114
+ baseUrl,
115
+ });
116
+ console.log("✅");
117
+ process.stdout.write(`Sending ${mediaType} to ${resolvedTo}... `);
118
+ await sendMediaMessage({
119
+ to: resolvedTo,
120
+ mediaType,
121
+ uploaded,
122
+ caption: opts.caption,
123
+ token: token,
124
+ baseUrl,
125
+ });
70
126
  console.log("✅ Sent");
71
127
  }
128
+ else if (opts.text) {
129
+ // Text message
130
+ process.stdout.write(`Sending to ${resolvedTo}... `);
131
+ const result = await sendTextMessage(resolvedTo, opts.text, token, baseUrl);
132
+ const ret = result?.ret ?? result?.errcode;
133
+ if (ret === 0 || ret === undefined) {
134
+ console.log("✅ Sent");
135
+ }
136
+ else {
137
+ console.log(`❌ Failed (ret=${ret})`);
138
+ console.log(JSON.stringify(result, null, 2));
139
+ }
140
+ }
72
141
  else {
73
- console.log(`❌ Failed (ret=${ret})`);
74
- console.log(JSON.stringify(result, null, 2));
142
+ console.error("Nothing to send. Provide text or --image/--file/--video");
143
+ process.exit(1);
75
144
  }
76
145
  }
77
146
  export async function cliPoll(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "MCP server for WeChat — send text, images, files, videos; receive via webhook; works with Claude Desktop, Cursor, OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/api.ts CHANGED
@@ -355,3 +355,28 @@ export async function sendVideoMessage(
355
355
  );
356
356
  }
357
357
  }
358
+
359
+ // ── Unified media sender for CLI ───────────────────────────────────────────
360
+
361
+ export type MediaSendType = "image" | "file" | "video";
362
+
363
+ export async function sendMediaMessage(opts: {
364
+ to: string;
365
+ mediaType: MediaSendType;
366
+ uploaded: UploadedMedia;
367
+ caption?: string;
368
+ token: string;
369
+ baseUrl: string;
370
+ contextToken?: string;
371
+ }) {
372
+ switch (opts.mediaType) {
373
+ case "image":
374
+ return sendImageMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
375
+ case "file":
376
+ return sendFileMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
377
+ case "video":
378
+ return sendVideoMessage(opts.to, opts.uploaded, opts.token, opts.baseUrl, opts.contextToken, opts.caption);
379
+ default:
380
+ throw new Error(`Unknown media type: ${opts.mediaType}`);
381
+ }
382
+ }
package/src/cli.ts CHANGED
@@ -124,7 +124,10 @@ Commands:
124
124
  contacts Show contact book (users who messaged the bot)
125
125
  update Check and install latest version
126
126
  --version / -v Print version
127
- send <userId> <text> Send a message from CLI
127
+ send <userId> <text> Send a text message
128
+ send <userId> --image <path> [--caption <text>] Send an image
129
+ send <userId> --file <path> [--caption <text>] Send a file
130
+ send <userId> --video <path> [--caption <text>] Send a video
128
131
  poll [--watch|-w] [--reset] Poll messages once, or watch continuously
129
132
  accounts [list] List all accounts
130
133
  accounts remove <id> Remove an account
package/src/messaging.ts CHANGED
@@ -10,10 +10,12 @@ import { ACCOUNTS_DIR } from "./paths.js";
10
10
  import {
11
11
  DEFAULT_BASE_URL,
12
12
  sendTextMessage,
13
+ sendMediaMessage,
13
14
  getUpdates,
14
15
  loadCursor,
15
16
  saveCursor,
16
17
  } from "./api.js";
18
+ import { uploadMedia, type MediaType } from "./cdn.js";
17
19
  import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
18
20
 
19
21
  /** Resolve a short/partial userId to a full one from contacts. */
@@ -59,25 +61,104 @@ function formatMsg(msg: Record<string, unknown>): string {
59
61
  return `${prefix}${from.slice(0, 20)}: ${parts.join(" ") || "(empty)"}`;
60
62
  }
61
63
 
64
+ interface SendOptions {
65
+ to: string;
66
+ text?: string;
67
+ image?: string;
68
+ file?: string;
69
+ video?: string;
70
+ caption?: string;
71
+ }
72
+
73
+ function parseCliSendArgs(args: string[]): SendOptions {
74
+ const opts: SendOptions = { to: "" };
75
+ let i = 0;
76
+
77
+ // First arg is always <to>
78
+ if (args[i] && !args[i].startsWith("--")) {
79
+ opts.to = args[i++];
80
+ }
81
+
82
+ while (i < args.length) {
83
+ const arg = args[i];
84
+ if (arg === "--image" && args[i + 1]) {
85
+ opts.image = args[++i];
86
+ } else if (arg === "--file" && args[i + 1]) {
87
+ opts.file = args[++i];
88
+ } else if (arg === "--video" && args[i + 1]) {
89
+ opts.video = args[++i];
90
+ } else if (arg === "--caption" && args[i + 1]) {
91
+ opts.caption = args[++i];
92
+ } else if (!arg.startsWith("--")) {
93
+ // Collect remaining as text
94
+ opts.text = args.slice(i).join(" ");
95
+ break;
96
+ }
97
+ i++;
98
+ }
99
+ return opts;
100
+ }
101
+
62
102
  export async function cliSend(args: string[]) {
63
- const [to, ...textParts] = args;
64
- if (!to || textParts.length === 0) {
65
- console.error("Usage: npx weixin-mcp send <userId> <message text>");
103
+ const opts = parseCliSendArgs(args);
104
+
105
+ if (!opts.to) {
106
+ console.error(`Usage: npx weixin-mcp send <userId> <text>
107
+ npx weixin-mcp send <userId> --image <path> [--caption <text>]
108
+ npx weixin-mcp send <userId> --file <path> [--caption <text>]
109
+ npx weixin-mcp send <userId> --video <path> [--caption <text>]`);
66
110
  process.exit(1);
67
111
  }
68
- const text = textParts.join(" ");
69
- const resolvedTo = resolveUserId(to);
70
- if (resolvedTo !== to) console.log(`Resolved "${to}" → ${resolvedTo}`);
112
+
113
+ const resolvedTo = resolveUserId(opts.to);
114
+ if (resolvedTo !== opts.to) console.log(`Resolved "${opts.to}" → ${resolvedTo}`);
71
115
  const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
72
-
73
- process.stdout.write(`Sending to ${resolvedTo}... `);
74
- const result = await sendTextMessage(resolvedTo, text, token!, baseUrl) as Record<string, unknown>;
75
- const ret = result?.ret ?? result?.errcode;
76
- if (ret === 0 || ret === undefined) {
116
+
117
+ // Determine what to send
118
+ const mediaPath = opts.image || opts.file || opts.video;
119
+ const mediaType: MediaType | null = opts.image ? "image" : opts.file ? "file" : opts.video ? "video" : null;
120
+
121
+ if (mediaPath && mediaType) {
122
+ // Check file exists
123
+ if (!fs.existsSync(mediaPath)) {
124
+ console.error(`File not found: ${mediaPath}`);
125
+ process.exit(1);
126
+ }
127
+
128
+ process.stdout.write(`Uploading ${mediaType}... `);
129
+ const uploaded = await uploadMedia({
130
+ source: mediaPath,
131
+ mediaType,
132
+ toUserId: resolvedTo,
133
+ token: token!,
134
+ baseUrl,
135
+ });
136
+ console.log("✅");
137
+
138
+ process.stdout.write(`Sending ${mediaType} to ${resolvedTo}... `);
139
+ await sendMediaMessage({
140
+ to: resolvedTo,
141
+ mediaType,
142
+ uploaded,
143
+ caption: opts.caption,
144
+ token: token!,
145
+ baseUrl,
146
+ });
77
147
  console.log("✅ Sent");
148
+ } else if (opts.text) {
149
+ // Text message
150
+ process.stdout.write(`Sending to ${resolvedTo}... `);
151
+ const result = await sendTextMessage(resolvedTo, opts.text, token!, baseUrl) as Record<string, unknown>;
152
+ const ret = result?.ret ?? result?.errcode;
153
+ if (ret === 0 || ret === undefined) {
154
+ console.log("✅ Sent");
155
+ } else {
156
+ console.log(`❌ Failed (ret=${ret})`);
157
+ console.log(JSON.stringify(result, null, 2));
158
+ }
78
159
  } else {
79
- console.log(`❌ Failed (ret=${ret})`);
80
- console.log(JSON.stringify(result, null, 2));
160
+ console.error("Nothing to send. Provide text or --image/--file/--video");
161
+ process.exit(1);
81
162
  }
82
163
  }
83
164