@aster110/cc2wechat 1.0.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/.claude-plugin/plugin.json +6 -0
- package/.mcp.json +6 -0
- package/EXPERIENCE.md +148 -0
- package/README.md +66 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +230 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +123 -0
- package/dist/cli.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +412 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.js +61 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/dist/wechat-api.d.ts +33 -0
- package/dist/wechat-api.js +313 -0
- package/dist/wechat-api.js.map +1 -0
- package/package.json +28 -0
- package/skills/wechat-reply/SKILL.md +21 -0
- package/src/auth.ts +258 -0
- package/src/cli.ts +133 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/server.ts +479 -0
- package/src/store.ts +79 -0
- package/src/types.ts +144 -0
- package/src/wechat-api.ts +405 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GetUpdatesResp, GetConfigResp } from './types.js';
|
|
2
|
+
export interface QRCodeResponse {
|
|
3
|
+
qrcode: string;
|
|
4
|
+
qrcode_img_content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface QRStatusResponse {
|
|
7
|
+
status: 'wait' | 'scaned' | 'confirmed' | 'expired';
|
|
8
|
+
bot_token?: string;
|
|
9
|
+
ilink_bot_id?: string;
|
|
10
|
+
baseurl?: string;
|
|
11
|
+
ilink_user_id?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getQRCode(baseUrl?: string, botType?: string): Promise<QRCodeResponse>;
|
|
14
|
+
export declare function pollQRStatus(qrcode: string, baseUrl?: string): Promise<QRStatusResponse>;
|
|
15
|
+
export declare function getUpdates(token: string, buf: string, baseUrl?: string, timeoutMs?: number): Promise<GetUpdatesResp>;
|
|
16
|
+
export declare function sendMessage(token: string, to: string, text: string, contextToken: string, baseUrl?: string): Promise<void>;
|
|
17
|
+
export declare function sendTyping(token: string, userId: string, ticket: string, status?: number, baseUrl?: string): Promise<void>;
|
|
18
|
+
export declare function getConfig(token: string, userId: string, contextToken?: string, baseUrl?: string): Promise<GetConfigResp>;
|
|
19
|
+
/** AES-128-ECB encrypt (PKCS7 padding). */
|
|
20
|
+
export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
|
|
21
|
+
/** Compute AES-ECB ciphertext size (PKCS7 padding). */
|
|
22
|
+
export declare function aesEcbPaddedSize(plaintextSize: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Upload a local file to WeChat CDN and send it as a media message.
|
|
25
|
+
*/
|
|
26
|
+
export declare function uploadAndSendMedia(params: {
|
|
27
|
+
token: string;
|
|
28
|
+
toUser: string;
|
|
29
|
+
contextToken: string;
|
|
30
|
+
filePath: string;
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
cdnBaseUrl?: string;
|
|
33
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
5
|
+
const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
|
|
6
|
+
const CHANNEL_VERSION = '1.0.0';
|
|
7
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
8
|
+
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
9
|
+
const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
|
|
10
|
+
function buildBaseInfo() {
|
|
11
|
+
return { channel_version: CHANNEL_VERSION };
|
|
12
|
+
}
|
|
13
|
+
/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */
|
|
14
|
+
function randomWechatUin() {
|
|
15
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
16
|
+
return Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
17
|
+
}
|
|
18
|
+
function buildHeaders(token, body) {
|
|
19
|
+
const headers = {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
AuthorizationType: 'ilink_bot_token',
|
|
22
|
+
'X-WECHAT-UIN': randomWechatUin(),
|
|
23
|
+
};
|
|
24
|
+
if (token)
|
|
25
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
26
|
+
if (body)
|
|
27
|
+
headers['Content-Length'] = String(Buffer.byteLength(body, 'utf-8'));
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
function ensureTrailingSlash(url) {
|
|
31
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Common POST wrapper with timeout + abort.
|
|
35
|
+
*/
|
|
36
|
+
async function apiFetch(params) {
|
|
37
|
+
const base = ensureTrailingSlash(params.baseUrl ?? BASE_URL);
|
|
38
|
+
const url = new URL(params.endpoint, base);
|
|
39
|
+
const headers = buildHeaders(params.token, params.body);
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url.toString(), {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers,
|
|
46
|
+
body: params.body,
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
const rawText = await res.text();
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
53
|
+
}
|
|
54
|
+
return rawText;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const QR_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
62
|
+
export async function getQRCode(baseUrl, botType = '3') {
|
|
63
|
+
const base = ensureTrailingSlash(baseUrl ?? BASE_URL);
|
|
64
|
+
const url = new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, base);
|
|
65
|
+
const res = await fetch(url.toString());
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const body = await res.text().catch(() => '(unreadable)');
|
|
68
|
+
throw new Error(`Failed to fetch QR code: ${res.status} ${res.statusText} ${body}`);
|
|
69
|
+
}
|
|
70
|
+
return (await res.json());
|
|
71
|
+
}
|
|
72
|
+
export async function pollQRStatus(qrcode, baseUrl) {
|
|
73
|
+
const base = ensureTrailingSlash(baseUrl ?? BASE_URL);
|
|
74
|
+
const url = new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, base);
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => controller.abort(), QR_LONG_POLL_TIMEOUT_MS);
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(url.toString(), {
|
|
79
|
+
headers: { 'iLink-App-ClientVersion': '1' },
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
const body = await res.text().catch(() => '');
|
|
85
|
+
throw new Error(`QR status poll failed: ${res.status} ${body}`);
|
|
86
|
+
}
|
|
87
|
+
return (await res.json());
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
92
|
+
return { status: 'wait' };
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Message APIs
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export async function getUpdates(token, buf, baseUrl, timeoutMs) {
|
|
101
|
+
const timeout = timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
102
|
+
try {
|
|
103
|
+
const rawText = await apiFetch({
|
|
104
|
+
baseUrl,
|
|
105
|
+
endpoint: 'ilink/bot/getupdates',
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
get_updates_buf: buf ?? '',
|
|
108
|
+
base_info: buildBaseInfo(),
|
|
109
|
+
}),
|
|
110
|
+
token,
|
|
111
|
+
timeoutMs: timeout,
|
|
112
|
+
label: 'getUpdates',
|
|
113
|
+
});
|
|
114
|
+
return JSON.parse(rawText);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
118
|
+
return { ret: 0, msgs: [], get_updates_buf: buf };
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function sendMessage(token, to, text, contextToken, baseUrl) {
|
|
124
|
+
const body = {
|
|
125
|
+
msg: {
|
|
126
|
+
from_user_id: '',
|
|
127
|
+
to_user_id: to,
|
|
128
|
+
client_id: `wechat-cc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
129
|
+
message_type: 2, // BOT
|
|
130
|
+
message_state: 2, // FINISH
|
|
131
|
+
item_list: [{ type: 1, text_item: { text } }],
|
|
132
|
+
context_token: contextToken,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
await apiFetch({
|
|
136
|
+
baseUrl,
|
|
137
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
138
|
+
body: JSON.stringify({ ...body, base_info: buildBaseInfo() }),
|
|
139
|
+
token,
|
|
140
|
+
timeoutMs: DEFAULT_API_TIMEOUT_MS,
|
|
141
|
+
label: 'sendMessage',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export async function sendTyping(token, userId, ticket, status = 1, baseUrl) {
|
|
145
|
+
const body = {
|
|
146
|
+
ilink_user_id: userId,
|
|
147
|
+
typing_ticket: ticket,
|
|
148
|
+
status,
|
|
149
|
+
};
|
|
150
|
+
await apiFetch({
|
|
151
|
+
baseUrl,
|
|
152
|
+
endpoint: 'ilink/bot/sendtyping',
|
|
153
|
+
body: JSON.stringify({ ...body, base_info: buildBaseInfo() }),
|
|
154
|
+
token,
|
|
155
|
+
timeoutMs: DEFAULT_CONFIG_TIMEOUT_MS,
|
|
156
|
+
label: 'sendTyping',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export async function getConfig(token, userId, contextToken, baseUrl) {
|
|
160
|
+
const rawText = await apiFetch({
|
|
161
|
+
baseUrl,
|
|
162
|
+
endpoint: 'ilink/bot/getconfig',
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
ilink_user_id: userId,
|
|
165
|
+
context_token: contextToken,
|
|
166
|
+
base_info: buildBaseInfo(),
|
|
167
|
+
}),
|
|
168
|
+
token,
|
|
169
|
+
timeoutMs: DEFAULT_CONFIG_TIMEOUT_MS,
|
|
170
|
+
label: 'getConfig',
|
|
171
|
+
});
|
|
172
|
+
return JSON.parse(rawText);
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// CDN Upload & Media Send
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
/** AES-128-ECB encrypt (PKCS7 padding). */
|
|
178
|
+
export function encryptAesEcb(plaintext, key) {
|
|
179
|
+
const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
|
|
180
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
181
|
+
}
|
|
182
|
+
/** Compute AES-ECB ciphertext size (PKCS7 padding). */
|
|
183
|
+
export function aesEcbPaddedSize(plaintextSize) {
|
|
184
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
185
|
+
}
|
|
186
|
+
/** Determine media type from file extension: 1=IMAGE, 2=VIDEO, 3=FILE. */
|
|
187
|
+
function detectMediaType(filePath) {
|
|
188
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
189
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext))
|
|
190
|
+
return 1;
|
|
191
|
+
if (['.mp4', '.mov', '.avi', '.mkv'].includes(ext))
|
|
192
|
+
return 2;
|
|
193
|
+
return 3;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Upload a local file to WeChat CDN and send it as a media message.
|
|
197
|
+
*/
|
|
198
|
+
export async function uploadAndSendMedia(params) {
|
|
199
|
+
const { token, toUser, contextToken, filePath, baseUrl, cdnBaseUrl } = params;
|
|
200
|
+
// 1. Read file, compute rawsize + MD5
|
|
201
|
+
const fileData = fs.readFileSync(filePath);
|
|
202
|
+
const rawsize = fileData.length;
|
|
203
|
+
const rawfilemd5 = crypto.createHash('md5').update(fileData).digest('hex');
|
|
204
|
+
// 2. Generate random AES key (16 bytes)
|
|
205
|
+
const aeskey = crypto.randomBytes(16);
|
|
206
|
+
// 3. Detect media type
|
|
207
|
+
const mediaType = detectMediaType(filePath);
|
|
208
|
+
// 4. Encrypt
|
|
209
|
+
const ciphertext = encryptAesEcb(fileData, aeskey);
|
|
210
|
+
const ciphertextSize = ciphertext.length;
|
|
211
|
+
// 5. Get upload URL
|
|
212
|
+
const filekey = `wcc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${path.extname(filePath)}`;
|
|
213
|
+
const uploadUrlBody = JSON.stringify({
|
|
214
|
+
filekey,
|
|
215
|
+
media_type: mediaType,
|
|
216
|
+
to_user_id: toUser,
|
|
217
|
+
rawsize,
|
|
218
|
+
rawfilemd5,
|
|
219
|
+
filesize: ciphertextSize,
|
|
220
|
+
no_need_thumb: true,
|
|
221
|
+
aeskey: aeskey.toString('hex'),
|
|
222
|
+
base_info: buildBaseInfo(),
|
|
223
|
+
});
|
|
224
|
+
const uploadUrlRaw = await apiFetch({
|
|
225
|
+
baseUrl,
|
|
226
|
+
endpoint: 'ilink/bot/getuploadurl',
|
|
227
|
+
body: uploadUrlBody,
|
|
228
|
+
token,
|
|
229
|
+
timeoutMs: DEFAULT_API_TIMEOUT_MS,
|
|
230
|
+
label: 'getUploadUrl',
|
|
231
|
+
});
|
|
232
|
+
const uploadUrlResp = JSON.parse(uploadUrlRaw);
|
|
233
|
+
const uploadParam = uploadUrlResp.upload_param;
|
|
234
|
+
const serverFilekey = uploadUrlResp.filekey || filekey;
|
|
235
|
+
if (!uploadParam) {
|
|
236
|
+
throw new Error('getUploadUrl did not return upload_param');
|
|
237
|
+
}
|
|
238
|
+
// 6. Upload to CDN
|
|
239
|
+
const cdn = cdnBaseUrl ?? CDN_BASE_URL;
|
|
240
|
+
const uploadUrl = `${cdn}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(serverFilekey)}`;
|
|
241
|
+
const headers = buildHeaders(token);
|
|
242
|
+
headers['Content-Type'] = 'application/octet-stream';
|
|
243
|
+
headers['Content-Length'] = String(ciphertextSize);
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch(uploadUrl, {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers,
|
|
250
|
+
body: new Uint8Array(ciphertext),
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
});
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
const body = await res.text().catch(() => '');
|
|
256
|
+
throw new Error(`CDN upload failed: ${res.status} ${body}`);
|
|
257
|
+
}
|
|
258
|
+
const downloadParam = res.headers.get('x-encrypted-param');
|
|
259
|
+
if (!downloadParam) {
|
|
260
|
+
throw new Error('CDN upload did not return x-encrypted-param header');
|
|
261
|
+
}
|
|
262
|
+
// 7. Build media item and send message
|
|
263
|
+
const aesKeyBase64 = Buffer.from(aeskey.toString('hex')).toString('base64');
|
|
264
|
+
const mediaInfo = {
|
|
265
|
+
encrypt_query_param: downloadParam,
|
|
266
|
+
aes_key: aesKeyBase64,
|
|
267
|
+
encrypt_type: 1,
|
|
268
|
+
};
|
|
269
|
+
let mediaItem;
|
|
270
|
+
if (mediaType === 1) {
|
|
271
|
+
mediaItem = { type: 2, image_item: { media: mediaInfo, mid_size: ciphertextSize } };
|
|
272
|
+
}
|
|
273
|
+
else if (mediaType === 2) {
|
|
274
|
+
mediaItem = { type: 5, video_item: { media: mediaInfo, video_size: ciphertextSize } };
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
mediaItem = {
|
|
278
|
+
type: 4,
|
|
279
|
+
file_item: {
|
|
280
|
+
media: mediaInfo,
|
|
281
|
+
file_name: path.basename(filePath),
|
|
282
|
+
len: String(rawsize),
|
|
283
|
+
md5: rawfilemd5,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const msgBody = {
|
|
288
|
+
msg: {
|
|
289
|
+
from_user_id: '',
|
|
290
|
+
to_user_id: toUser,
|
|
291
|
+
client_id: `wcc-${Date.now()}`,
|
|
292
|
+
message_type: 2,
|
|
293
|
+
message_state: 2,
|
|
294
|
+
item_list: [mediaItem],
|
|
295
|
+
context_token: contextToken,
|
|
296
|
+
},
|
|
297
|
+
base_info: buildBaseInfo(),
|
|
298
|
+
};
|
|
299
|
+
await apiFetch({
|
|
300
|
+
baseUrl,
|
|
301
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
302
|
+
body: JSON.stringify(msgBody),
|
|
303
|
+
token,
|
|
304
|
+
timeoutMs: DEFAULT_API_TIMEOUT_MS,
|
|
305
|
+
label: 'sendMediaMessage',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=wechat-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wechat-api.js","sourceRoot":"","sources":["../src/wechat-api.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aster110/cc2wechat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WeChat channel for Claude Code — chat with Claude Code from WeChat via iLink Bot API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc2wechat": "dist/cli.js",
|
|
8
|
+
"wechat-cc-channel": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/server.ts",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
20
|
+
"qrcode-terminal": "^0.12.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"tsx": "^4.19.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wechat-auto-reply
|
|
3
|
+
description: Automatically reply to WeChat channel messages using the reply tool
|
|
4
|
+
autoTrigger: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WeChat Channel Auto-Reply
|
|
8
|
+
|
|
9
|
+
When you receive a message from the `wechat` channel (indicated by `← wechat-channel:` in the conversation), you MUST:
|
|
10
|
+
|
|
11
|
+
1. Process the message and formulate your response
|
|
12
|
+
2. **Always call the `reply` tool** to send your response back to WeChat
|
|
13
|
+
3. Include the `user_id` and `context_token` from the incoming message's metadata
|
|
14
|
+
4. Keep responses concise (under 500 characters when possible) since this is a chat app
|
|
15
|
+
|
|
16
|
+
Example flow:
|
|
17
|
+
- Receive: `← wechat-channel: 今天几月几号`
|
|
18
|
+
- Think: formulate answer
|
|
19
|
+
- Call: `reply` tool with `user_id`, `context_token`, and your `content`
|
|
20
|
+
|
|
21
|
+
IMPORTANT: Never just display the answer in the terminal. Always use the `reply` tool so the user gets the response on WeChat.
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { getQRCode, pollQRStatus } from './wechat-api.js';
|
|
4
|
+
|
|
5
|
+
const MAX_QR_REFRESH = 3;
|
|
6
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
7
|
+
const QR_WEB_PORT = 18891;
|
|
8
|
+
|
|
9
|
+
export interface LoginResult {
|
|
10
|
+
token: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Full QR login flow: fetch QR code, display in terminal, poll until confirmed.
|
|
17
|
+
* Writes to stderr so it doesn't interfere with stdio MCP transport.
|
|
18
|
+
*/
|
|
19
|
+
export async function loginWithQR(baseUrl?: string): Promise<LoginResult> {
|
|
20
|
+
let qrRefreshCount = 0;
|
|
21
|
+
|
|
22
|
+
while (qrRefreshCount < MAX_QR_REFRESH) {
|
|
23
|
+
qrRefreshCount++;
|
|
24
|
+
const qrResp = await getQRCode(baseUrl);
|
|
25
|
+
|
|
26
|
+
// Display QR code in terminal via stderr
|
|
27
|
+
process.stderr.write('\n--- Scan this QR code with WeChat ---\n');
|
|
28
|
+
try {
|
|
29
|
+
const qrterm = await import('qrcode-terminal');
|
|
30
|
+
// qrcode-terminal writes to stdout by default; we redirect
|
|
31
|
+
qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr: string) => {
|
|
32
|
+
process.stderr.write(qr + '\n');
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
process.stderr.write(`QR URL: ${qrResp.qrcode_img_content}\n`);
|
|
36
|
+
}
|
|
37
|
+
process.stderr.write('Waiting for scan...\n');
|
|
38
|
+
|
|
39
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
40
|
+
let scannedLogged = false;
|
|
41
|
+
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
const status = await pollQRStatus(qrResp.qrcode, baseUrl);
|
|
44
|
+
|
|
45
|
+
switch (status.status) {
|
|
46
|
+
case 'wait':
|
|
47
|
+
break;
|
|
48
|
+
case 'scaned':
|
|
49
|
+
if (!scannedLogged) {
|
|
50
|
+
process.stderr.write('Scanned! Confirm on your phone...\n');
|
|
51
|
+
scannedLogged = true;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'expired':
|
|
55
|
+
process.stderr.write(`QR expired (${qrRefreshCount}/${MAX_QR_REFRESH}), refreshing...\n`);
|
|
56
|
+
break;
|
|
57
|
+
case 'confirmed':
|
|
58
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
59
|
+
throw new Error('Login confirmed but missing bot_token or ilink_bot_id');
|
|
60
|
+
}
|
|
61
|
+
process.stderr.write('Login successful!\n');
|
|
62
|
+
return {
|
|
63
|
+
token: status.bot_token,
|
|
64
|
+
accountId: status.ilink_bot_id,
|
|
65
|
+
baseUrl: status.baseurl,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (status.status === 'expired') break; // break inner loop to refresh QR
|
|
70
|
+
|
|
71
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error('Login failed: QR code expired too many times');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Web-based QR login (opens browser, avoids stderr QR display issue in MCP)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function buildQRPage(qrUrl: string): string {
|
|
83
|
+
return `<!DOCTYPE html>
|
|
84
|
+
<html>
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="utf-8">
|
|
87
|
+
<title>WeChat Login - Claude Code</title>
|
|
88
|
+
<style>
|
|
89
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
90
|
+
body { background: #1a1a2e; color: #e0e0e0; display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
|
|
91
|
+
.container { text-align: center; }
|
|
92
|
+
h2 { font-size: 1.6rem; margin-bottom: 8px; }
|
|
93
|
+
.subtitle { color: #888; margin-bottom: 24px; }
|
|
94
|
+
#qr-container { margin: 0 auto 20px; }
|
|
95
|
+
#qr-img { width: 300px; height: 300px; border-radius: 12px; background: white; padding: 16px; }
|
|
96
|
+
#status { color: #888; font-size: 1.1rem; transition: color 0.3s; }
|
|
97
|
+
.success-box { background: #1e3a1e; border: 1px solid #5cb85c; border-radius: 12px; padding: 32px; margin-top: 16px; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div class="container">
|
|
102
|
+
<h2>WeChat × Claude Code</h2>
|
|
103
|
+
<p class="subtitle">Scan with WeChat to connect</p>
|
|
104
|
+
<div id="qr-container">
|
|
105
|
+
<img id="qr-img" src="${qrUrl}" onerror="this.alt='QR failed to load: ${qrUrl}'" />
|
|
106
|
+
</div>
|
|
107
|
+
<p id="status">Waiting for scan...</p>
|
|
108
|
+
</div>
|
|
109
|
+
<script>
|
|
110
|
+
const poll = setInterval(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch('/status');
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
const el = document.getElementById('status');
|
|
115
|
+
if (data.status === 'scanned') {
|
|
116
|
+
el.textContent = 'Scanned! Confirm on your phone...';
|
|
117
|
+
el.style.color = '#f0ad4e';
|
|
118
|
+
} else if (data.status === 'success') {
|
|
119
|
+
el.textContent = 'Connected!';
|
|
120
|
+
el.style.color = '#5cb85c';
|
|
121
|
+
document.getElementById('qr-container').style.display = 'none';
|
|
122
|
+
clearInterval(poll);
|
|
123
|
+
setTimeout(() => window.close(), 3000);
|
|
124
|
+
} else if (data.status === 'expired') {
|
|
125
|
+
el.textContent = 'QR expired, refreshing...';
|
|
126
|
+
el.style.color = '#d9534f';
|
|
127
|
+
try {
|
|
128
|
+
const r2 = await fetch('/qr-refresh');
|
|
129
|
+
const d2 = await r2.json();
|
|
130
|
+
if (d2.url) {
|
|
131
|
+
document.getElementById('qr-img').src = d2.url;
|
|
132
|
+
el.textContent = 'QR refreshed, scan again...';
|
|
133
|
+
el.style.color = '#888';
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
} else if (data.status === 'failed') {
|
|
137
|
+
el.textContent = 'Login failed: ' + (data.message || 'unknown error');
|
|
138
|
+
el.style.color = '#d9534f';
|
|
139
|
+
clearInterval(poll);
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
}, 2000);
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function openBrowser(url: string): void {
|
|
149
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
150
|
+
exec(`${cmd} ${url}`, () => {});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Web-based QR login: opens a local browser page with the QR code.
|
|
155
|
+
* Solves the MCP stderr capture issue where terminal QR codes are invisible.
|
|
156
|
+
*/
|
|
157
|
+
export async function loginWithQRWeb(baseUrl?: string): Promise<LoginResult> {
|
|
158
|
+
let currentStatus: 'waiting' | 'scanned' | 'expired' | 'success' | 'failed' = 'waiting';
|
|
159
|
+
let currentQrUrl = '';
|
|
160
|
+
let failMessage = '';
|
|
161
|
+
let qrRefreshCount = 0;
|
|
162
|
+
let currentQrCode = ''; // the qrcode token for polling
|
|
163
|
+
|
|
164
|
+
// Fetch initial QR code
|
|
165
|
+
const qrResp = await getQRCode(baseUrl);
|
|
166
|
+
currentQrUrl = qrResp.qrcode_img_content;
|
|
167
|
+
currentQrCode = qrResp.qrcode;
|
|
168
|
+
qrRefreshCount = 1;
|
|
169
|
+
|
|
170
|
+
// Start HTTP server
|
|
171
|
+
const httpServer = http.createServer((req, res) => {
|
|
172
|
+
if (req.url === '/' || req.url === '') {
|
|
173
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
174
|
+
res.end(buildQRPage(currentQrUrl));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (req.url === '/status') {
|
|
178
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
179
|
+
res.end(JSON.stringify({ status: currentStatus, message: failMessage }));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (req.url === '/qr-refresh') {
|
|
183
|
+
// The refresh is triggered by the polling loop setting status to 'expired',
|
|
184
|
+
// but the actual new QR is fetched there too. Just return current URL.
|
|
185
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
186
|
+
res.end(JSON.stringify({ url: currentQrUrl }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
res.writeHead(404);
|
|
190
|
+
res.end('Not found');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await new Promise<void>((resolve, reject) => {
|
|
194
|
+
httpServer.on('error', reject);
|
|
195
|
+
httpServer.listen(QR_WEB_PORT, () => resolve());
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
process.stderr.write(`[wechat-channel] QR login page at http://localhost:${QR_WEB_PORT}\n`);
|
|
199
|
+
openBrowser(`http://localhost:${QR_WEB_PORT}`);
|
|
200
|
+
|
|
201
|
+
// Poll for QR scan
|
|
202
|
+
try {
|
|
203
|
+
while (qrRefreshCount <= MAX_QR_REFRESH) {
|
|
204
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
205
|
+
|
|
206
|
+
while (Date.now() < deadline) {
|
|
207
|
+
const status = await pollQRStatus(currentQrCode, baseUrl);
|
|
208
|
+
|
|
209
|
+
switch (status.status) {
|
|
210
|
+
case 'wait':
|
|
211
|
+
currentStatus = 'waiting';
|
|
212
|
+
break;
|
|
213
|
+
case 'scaned':
|
|
214
|
+
currentStatus = 'scanned';
|
|
215
|
+
break;
|
|
216
|
+
case 'expired':
|
|
217
|
+
currentStatus = 'expired';
|
|
218
|
+
break;
|
|
219
|
+
case 'confirmed':
|
|
220
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
221
|
+
throw new Error('Login confirmed but missing bot_token or ilink_bot_id');
|
|
222
|
+
}
|
|
223
|
+
currentStatus = 'success';
|
|
224
|
+
// Give the browser a moment to show success
|
|
225
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
226
|
+
return {
|
|
227
|
+
token: status.bot_token,
|
|
228
|
+
accountId: status.ilink_bot_id,
|
|
229
|
+
baseUrl: status.baseurl,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (status.status === 'expired') break;
|
|
234
|
+
|
|
235
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Refresh QR
|
|
239
|
+
qrRefreshCount++;
|
|
240
|
+
if (qrRefreshCount <= MAX_QR_REFRESH) {
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
`[wechat-channel] QR expired (${qrRefreshCount - 1}/${MAX_QR_REFRESH}), refreshing...\n`,
|
|
243
|
+
);
|
|
244
|
+
const newQr = await getQRCode(baseUrl);
|
|
245
|
+
currentQrUrl = newQr.qrcode_img_content;
|
|
246
|
+
currentQrCode = newQr.qrcode;
|
|
247
|
+
currentStatus = 'waiting';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
currentStatus = 'failed';
|
|
252
|
+
failMessage = 'QR code expired too many times';
|
|
253
|
+
throw new Error('Login failed: QR code expired too many times');
|
|
254
|
+
} finally {
|
|
255
|
+
// Shut down HTTP server
|
|
256
|
+
httpServer.close();
|
|
257
|
+
}
|
|
258
|
+
}
|