@jaxonchenjc/claude-wechat-channel 0.1.0 → 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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +57 -81
- package/package.json +2 -3
- package/server.ts +38 -39
- package/skills/configure/SKILL.md +65 -46
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wechat",
|
|
3
3
|
"description": "WeChat channel for Claude Code \u2014 two-way messaging bridge via iLink Bot API. Login with QR code, receive and reply to WeChat messages from your Claude Code session.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wechat",
|
|
7
7
|
"weixin",
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# WeChat Channel for Claude Code
|
|
2
2
|
|
|
3
|
-
Two-way messaging bridge between WeChat and Claude Code via the iLink Bot API.
|
|
3
|
+
Two-way messaging bridge between WeChat and Claude Code via the iLink Bot API.
|
|
4
4
|
|
|
5
5
|
**Background**: Tencent released [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) — a WeChat channel for [OpenClaw](https://docs.openclaw.ai). This project brings the same capability to Claude Code, following the official [Telegram](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/telegram) and [Discord](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord) channel plugin patterns.
|
|
6
6
|
|
|
@@ -9,67 +9,51 @@ Two-way messaging bridge between WeChat and Claude Code via the iLink Bot API. S
|
|
|
9
9
|
- QR code login — scan with WeChat to connect
|
|
10
10
|
- Text, image, voice, file, and video message support
|
|
11
11
|
- Voice messages auto-transcribed (WeChat server-side STT)
|
|
12
|
-
- Image/file/video download
|
|
12
|
+
- Image/file/video CDN download with AES-128-ECB decryption
|
|
13
13
|
- File/image/video sending via CDN upload
|
|
14
|
-
- Typing indicator
|
|
15
|
-
- Long message auto-chunking
|
|
14
|
+
- Typing indicator
|
|
15
|
+
- Long message auto-chunking
|
|
16
|
+
- Poll cursor persistence (no message replay on restart)
|
|
16
17
|
|
|
17
18
|
## Prerequisites
|
|
18
19
|
|
|
19
|
-
- [Claude Code](https://claude.com/claude-code) v2.1.80
|
|
20
|
+
- [Claude Code](https://claude.com/claude-code) v2.1.80+
|
|
20
21
|
- [Bun](https://bun.sh) runtime
|
|
21
22
|
- A WeChat account
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### Option A: Plugin marketplace (recommended)
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# In Claude Code:
|
|
29
|
-
/plugin marketplace add jaxxjj/claude-wechat-channel
|
|
30
|
-
/plugin install wechat@claude-wechat-channel
|
|
31
|
-
```
|
|
24
|
+
## Quick Start
|
|
32
25
|
|
|
33
|
-
###
|
|
26
|
+
### 1. Install
|
|
34
27
|
|
|
35
28
|
```bash
|
|
36
|
-
|
|
29
|
+
claude --dangerously-skip-permissions --dangerously-load-development-channels plugin:wechat@claude-wechat-channel
|
|
37
30
|
```
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
Then inside Claude Code:
|
|
40
33
|
|
|
41
|
-
### Option C: Manual (clone)
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
git clone https://github.com/jaxxjj/claude-wechat-channel.git
|
|
45
|
-
cd claude-wechat-channel && bun install
|
|
46
|
-
# Add MCP server to ~/.claude.json manually (see below)
|
|
47
34
|
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### 1. Start Claude Code with the channel
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# If installed via marketplace:
|
|
55
|
-
claude --dangerously-load-development-channels plugin:wechat@claude-wechat-channel
|
|
56
|
-
|
|
57
|
-
# If installed via npm or manual:
|
|
58
|
-
claude --dangerously-load-development-channels server:wechat
|
|
35
|
+
/plugin marketplace add jaxxjj/claude-wechat-channel
|
|
36
|
+
/plugin install wechat@claude-wechat-channel
|
|
37
|
+
/reload-plugins
|
|
59
38
|
```
|
|
60
39
|
|
|
61
|
-
### 2.
|
|
40
|
+
### 2. Login
|
|
62
41
|
|
|
63
|
-
Inside Claude Code, run:
|
|
64
42
|
```
|
|
65
43
|
/wechat:configure login
|
|
66
44
|
```
|
|
67
45
|
|
|
68
|
-
|
|
46
|
+
Opens a QR code link — scan with WeChat. Credentials saved automatically.
|
|
47
|
+
|
|
48
|
+
### 3. Restart
|
|
49
|
+
|
|
50
|
+
Exit Claude Code, then:
|
|
69
51
|
|
|
70
|
-
|
|
52
|
+
```bash
|
|
53
|
+
claude --dangerously-skip-permissions --dangerously-load-development-channels plugin:wechat@claude-wechat-channel
|
|
54
|
+
```
|
|
71
55
|
|
|
72
|
-
|
|
56
|
+
Done. WeChat messages now arrive in your Claude Code session. Claude processes them and replies back through WeChat.
|
|
73
57
|
|
|
74
58
|
## How It Works
|
|
75
59
|
|
|
@@ -83,74 +67,66 @@ WeChat contact sends you a message
|
|
|
83
67
|
→ Reply appears in WeChat
|
|
84
68
|
```
|
|
85
69
|
|
|
86
|
-
Access is gated by WeChat's QR code login — only contacts of the logged-in account can send messages.
|
|
70
|
+
Access is gated by WeChat's QR code login — only contacts of the logged-in account can send messages.
|
|
87
71
|
|
|
88
72
|
## Supported Message Types
|
|
89
73
|
|
|
90
74
|
| Type | Inbound | Outbound |
|
|
91
75
|
|------|---------|----------|
|
|
92
|
-
| Text | Direct
|
|
93
|
-
| Image | CDN download + AES decrypt
|
|
94
|
-
| Voice | Server-side speech-to-text
|
|
95
|
-
| File (PDF, etc) | CDN download + AES decrypt
|
|
96
|
-
| Video | CDN download + AES decrypt
|
|
76
|
+
| Text | Direct | Auto-chunked |
|
|
77
|
+
| Image | CDN download + AES decrypt | CDN upload + encrypt |
|
|
78
|
+
| Voice | Server-side speech-to-text | — |
|
|
79
|
+
| File (PDF, etc) | CDN download + AES decrypt | CDN upload + encrypt |
|
|
80
|
+
| Video | CDN download + AES decrypt | CDN upload + encrypt |
|
|
97
81
|
|
|
98
|
-
##
|
|
82
|
+
## Alternative Install Methods
|
|
99
83
|
|
|
100
|
-
|
|
101
|
-
|------|-------------|
|
|
102
|
-
| `reply` | Send text + optional file attachments back to WeChat |
|
|
103
|
-
| `send_typing` | Show "typing..." indicator in WeChat |
|
|
84
|
+
### npm
|
|
104
85
|
|
|
105
|
-
|
|
86
|
+
```bash
|
|
87
|
+
npx @jaxonchenjc/claude-wechat-channel install
|
|
88
|
+
claude --dangerously-skip-permissions --dangerously-load-development-channels server:wechat
|
|
89
|
+
```
|
|
106
90
|
|
|
91
|
+
### Manual clone
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/jaxxjj/claude-wechat-channel.git
|
|
95
|
+
cd claude-wechat-channel && bun install
|
|
107
96
|
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
97
|
+
|
|
98
|
+
Add to `~/.claude.json` under `mcpServers`:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"wechat": {
|
|
103
|
+
"command": "bun",
|
|
104
|
+
"args": ["run", "--cwd", "/path/to/claude-wechat-channel", "--shell=bun", "--silent", "start"],
|
|
105
|
+
"type": "stdio"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
117
108
|
```
|
|
118
109
|
|
|
119
110
|
## State Files
|
|
120
111
|
|
|
121
112
|
```
|
|
122
113
|
~/.claude/channels/wechat/
|
|
123
|
-
├── account.json
|
|
124
|
-
|
|
114
|
+
├── account.json # Login credentials (chmod 600)
|
|
115
|
+
├── poll_cursor # Long-poll cursor (prevents replay on restart)
|
|
116
|
+
└── inbox/ # Downloaded media files
|
|
125
117
|
```
|
|
126
118
|
|
|
127
|
-
## Environment Variables
|
|
128
|
-
|
|
129
|
-
| Variable | Default | Description |
|
|
130
|
-
|----------|---------|-------------|
|
|
131
|
-
| `WECHAT_STATE_DIR` | `~/.claude/channels/wechat` | Override state directory |
|
|
132
|
-
|
|
133
|
-
## Differences from Telegram/Discord Plugins
|
|
134
|
-
|
|
135
|
-
| Aspect | Telegram/Discord | WeChat |
|
|
136
|
-
|--------|-----------------|--------|
|
|
137
|
-
| Authentication | Paste bot token | Scan QR code with phone |
|
|
138
|
-
| Access control | Pairing codes + allowlist | QR code login (contacts-only) |
|
|
139
|
-
| Bot identity | Separate bot account | Your own WeChat account |
|
|
140
|
-
| Message history | Telegram: none; Discord: fetch_messages | None |
|
|
141
|
-
| Media handling | Telegram: direct URL; Discord: attachment download | CDN + AES-128-ECB encryption |
|
|
142
|
-
|
|
143
119
|
## Limitations
|
|
144
120
|
|
|
145
121
|
- **Session expiry**: WeChat sessions may expire; re-run `/wechat:configure login`
|
|
146
122
|
- **Single session**: Only one Claude Code session can poll at a time
|
|
147
|
-
- **No message history**: Real-time only
|
|
148
|
-
- **
|
|
123
|
+
- **No message history**: Real-time only
|
|
124
|
+
- **Channels research preview**: Requires `--dangerously-load-development-channels` flag
|
|
149
125
|
|
|
150
126
|
## Credits
|
|
151
127
|
|
|
152
128
|
- WeChat iLink Bot API by Tencent
|
|
153
|
-
- Channel architecture inspired by [claude-plugins-official](https://github.com/anthropics/claude-plugins-official)
|
|
129
|
+
- Channel architecture inspired by [claude-plugins-official](https://github.com/anthropics/claude-plugins-official)
|
|
154
130
|
- CDN encryption protocol referenced from [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin)
|
|
155
131
|
|
|
156
132
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaxonchenjc/claude-wechat-channel",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "WeChat channel plugin for Claude Code — two-way messaging bridge via iLink Bot API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
"author": "jaxxjj",
|
|
30
30
|
"homepage": "https://github.com/jaxxjj/claude-wechat-channel",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
33
|
-
"qrcode-terminal": "^0.12.0"
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@biomejs/biome": "^2.4.8",
|
package/server.ts
CHANGED
|
@@ -32,6 +32,7 @@ const STATE_DIR = process.env.WECHAT_STATE_DIR ?? join(homedir(), '.claude', 'ch
|
|
|
32
32
|
const ENV_FILE = join(STATE_DIR, '.env')
|
|
33
33
|
const ACCOUNT_FILE = join(STATE_DIR, 'account.json')
|
|
34
34
|
const INBOX_DIR = join(STATE_DIR, 'inbox')
|
|
35
|
+
const CURSOR_FILE = join(STATE_DIR, 'poll_cursor')
|
|
35
36
|
|
|
36
37
|
// Load .env — plugin-spawned servers don't get an env block.
|
|
37
38
|
try {
|
|
@@ -130,6 +131,7 @@ async function apiCall(baseUrl: string, endpoint: string, body: unknown, token:
|
|
|
130
131
|
|
|
131
132
|
// ---------------------------------------------------------------------------
|
|
132
133
|
// Context token management — WeChat requires echoing context_token in replies
|
|
134
|
+
// In-memory cache as fallback; primary source is the tool parameter from Claude.
|
|
133
135
|
// ---------------------------------------------------------------------------
|
|
134
136
|
|
|
135
137
|
const contextTokens = new Map<string, string>()
|
|
@@ -142,6 +144,12 @@ function getContextToken(userId: string): string | undefined {
|
|
|
142
144
|
return contextTokens.get(userId)
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
function resolveContextToken(explicit: string | undefined, userId: string): string {
|
|
148
|
+
const token = explicit || getContextToken(userId)
|
|
149
|
+
if (!token) throw new Error(`No context_token for ${userId} — the user must message first`)
|
|
150
|
+
return token
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
// ---------------------------------------------------------------------------
|
|
146
154
|
// Typing indicator
|
|
147
155
|
// ---------------------------------------------------------------------------
|
|
@@ -170,11 +178,8 @@ async function sendTyping(account: WechatAccount, userId: string, status: 1 | 2
|
|
|
170
178
|
// Send message
|
|
171
179
|
// ---------------------------------------------------------------------------
|
|
172
180
|
|
|
173
|
-
async function sendMessage(account: WechatAccount, toUserId: string, text: string): Promise<{ messageId: string }> {
|
|
174
|
-
const contextToken =
|
|
175
|
-
if (!contextToken) {
|
|
176
|
-
throw new Error(`No context_token for ${toUserId} — the user must message first`)
|
|
177
|
-
}
|
|
181
|
+
async function sendMessage(account: WechatAccount, toUserId: string, text: string, explicitContextToken?: string): Promise<{ messageId: string }> {
|
|
182
|
+
const contextToken = resolveContextToken(explicitContextToken, toUserId)
|
|
178
183
|
|
|
179
184
|
const clientId = `claude-wechat-${randomBytes(8).toString('hex')}`
|
|
180
185
|
const res = await apiCall(account.baseUrl, 'ilink/bot/sendmessage', {
|
|
@@ -187,7 +192,7 @@ async function sendMessage(account: WechatAccount, toUserId: string, text: strin
|
|
|
187
192
|
message_state: 2, // FINISH
|
|
188
193
|
item_list: [{ type: 1, text_item: { text } }],
|
|
189
194
|
},
|
|
190
|
-
base_info: { channel_version: '0.0
|
|
195
|
+
base_info: { channel_version: '1.0.0' },
|
|
191
196
|
}, account.token) as Record<string, unknown>
|
|
192
197
|
|
|
193
198
|
return { messageId: String(res.message_id ?? clientId) }
|
|
@@ -258,12 +263,9 @@ async function sendFileMessage(
|
|
|
258
263
|
account: WechatAccount,
|
|
259
264
|
toUserId: string,
|
|
260
265
|
filePath: string,
|
|
261
|
-
|
|
266
|
+
explicitContextToken?: string,
|
|
262
267
|
): Promise<{ messageId: string }> {
|
|
263
|
-
const contextToken =
|
|
264
|
-
if (!contextToken) {
|
|
265
|
-
throw new Error(`No context_token for ${toUserId} — the user must message first`)
|
|
266
|
-
}
|
|
268
|
+
const contextToken = resolveContextToken(explicitContextToken, toUserId)
|
|
267
269
|
|
|
268
270
|
const plaintext = readFileSync(filePath)
|
|
269
271
|
const rawsize = plaintext.length
|
|
@@ -306,9 +308,7 @@ async function sendFileMessage(
|
|
|
306
308
|
mediaItem = { type: 4, file_item: { media: cdnMedia, file_name: fileName, len: String(rawsize) } }
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
const items: Record<string, unknown>[] = []
|
|
310
|
-
if (caption) items.push({ type: 1, text_item: { text: caption } })
|
|
311
|
-
items.push(mediaItem)
|
|
311
|
+
const items: Record<string, unknown>[] = [mediaItem]
|
|
312
312
|
|
|
313
313
|
let lastMessageId = '0'
|
|
314
314
|
for (const item of items) {
|
|
@@ -319,7 +319,7 @@ async function sendFileMessage(
|
|
|
319
319
|
context_token: contextToken, message_type: 2, message_state: 2,
|
|
320
320
|
item_list: [item],
|
|
321
321
|
},
|
|
322
|
-
base_info: { channel_version: '0.0
|
|
322
|
+
base_info: { channel_version: '1.0.0' },
|
|
323
323
|
}, account.token) as Record<string, unknown>
|
|
324
324
|
lastMessageId = String(res.message_id ?? res.msg_id ?? '0')
|
|
325
325
|
}
|
|
@@ -402,9 +402,9 @@ const mcp = new Server(
|
|
|
402
402
|
instructions: [
|
|
403
403
|
'The sender reads WeChat, not this session. Anything you want them to see must go through the reply tool — your transcript output never reaches their chat.',
|
|
404
404
|
'',
|
|
405
|
-
'Messages from WeChat arrive as <channel source="wechat" chat_id="..." message_id="..." user="..." ts="...">. If the tag has
|
|
405
|
+
'Messages from WeChat arrive as <channel source="wechat" chat_id="..." context_token="..." message_id="..." user="..." ts="...">. If the tag has image_path or attachment_path, use the Read tool directly on that file path — it handles images and PDFs natively, no external tools needed. Reply with the reply tool — pass chat_id and context_token back. The context_token is REQUIRED for delivery.',
|
|
406
406
|
'',
|
|
407
|
-
'reply accepts file paths (files: ["/abs/path.png"]) for attachments. Images send as photos; videos as video messages; other types as file attachments.
|
|
407
|
+
'reply accepts file paths (files: ["/abs/path.png"]) for attachments. Images send as photos; videos as video messages; other types as file attachments.',
|
|
408
408
|
'',
|
|
409
409
|
'WeChat has no history or search API — you only see messages as they arrive. If you need earlier context, ask the user to paste it or summarize.',
|
|
410
410
|
].join('\n'),
|
|
@@ -416,30 +416,20 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
416
416
|
{
|
|
417
417
|
name: 'reply',
|
|
418
418
|
description:
|
|
419
|
-
'Reply on WeChat. Pass chat_id from the inbound message.
|
|
419
|
+
'Reply on WeChat. Pass chat_id and context_token from the inbound message. context_token is required for delivery.',
|
|
420
420
|
inputSchema: {
|
|
421
421
|
type: 'object',
|
|
422
422
|
properties: {
|
|
423
423
|
chat_id: { type: 'string', description: 'WeChat user ID from the inbound <channel> tag' },
|
|
424
424
|
text: { type: 'string', description: 'Message text to send' },
|
|
425
|
+
context_token: { type: 'string', description: 'context_token from the inbound <channel> tag. Required for delivery.' },
|
|
425
426
|
files: {
|
|
426
427
|
type: 'array',
|
|
427
428
|
items: { type: 'string' },
|
|
428
429
|
description: 'Absolute file paths to attach. Images send as photos; videos as video messages; others as file attachments. Max 50MB each.',
|
|
429
430
|
},
|
|
430
431
|
},
|
|
431
|
-
required: ['chat_id', 'text'],
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
name: 'send_typing',
|
|
436
|
-
description: 'Show typing indicator in WeChat. Call before long operations.',
|
|
437
|
-
inputSchema: {
|
|
438
|
-
type: 'object',
|
|
439
|
-
properties: {
|
|
440
|
-
chat_id: { type: 'string', description: 'WeChat user ID' },
|
|
441
|
-
},
|
|
442
|
-
required: ['chat_id'],
|
|
432
|
+
required: ['chat_id', 'text', 'context_token'],
|
|
443
433
|
},
|
|
444
434
|
},
|
|
445
435
|
],
|
|
@@ -460,6 +450,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
460
450
|
case 'reply': {
|
|
461
451
|
const chat_id = args.chat_id as string
|
|
462
452
|
const text = args.text as string
|
|
453
|
+
const ct = args.context_token as string | undefined
|
|
463
454
|
const files = (args.files as string[] | undefined) ?? []
|
|
464
455
|
|
|
465
456
|
for (const f of files) {
|
|
@@ -477,7 +468,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
477
468
|
|
|
478
469
|
try {
|
|
479
470
|
for (const c of chunks) {
|
|
480
|
-
const result = await sendMessage(account, chat_id, c)
|
|
471
|
+
const result = await sendMessage(account, chat_id, c, ct)
|
|
481
472
|
sentIds.push(result.messageId)
|
|
482
473
|
}
|
|
483
474
|
} catch (err) {
|
|
@@ -486,7 +477,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
486
477
|
}
|
|
487
478
|
|
|
488
479
|
for (const f of files) {
|
|
489
|
-
const result = await sendFileMessage(account, chat_id, f)
|
|
480
|
+
const result = await sendFileMessage(account, chat_id, f, ct)
|
|
490
481
|
sentIds.push(result.messageId)
|
|
491
482
|
}
|
|
492
483
|
|
|
@@ -495,10 +486,6 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
495
486
|
: `sent ${sentIds.length} parts (ids: ${sentIds.join(', ')})`
|
|
496
487
|
return { content: [{ type: 'text', text: result }] }
|
|
497
488
|
}
|
|
498
|
-
case 'send_typing': {
|
|
499
|
-
await sendTyping(account, args.chat_id as string, 1)
|
|
500
|
-
return { content: [{ type: 'text', text: 'typing indicator sent' }] }
|
|
501
|
-
}
|
|
502
489
|
default:
|
|
503
490
|
return {
|
|
504
491
|
content: [{ type: 'text', text: `unknown tool: ${req.params.name}` }],
|
|
@@ -548,12 +535,17 @@ void (async () => {
|
|
|
548
535
|
|
|
549
536
|
process.stderr.write(`wechat channel: starting message polling\n`)
|
|
550
537
|
|
|
551
|
-
|
|
538
|
+
// Load persisted poll cursor — prevents message replay on restart
|
|
539
|
+
let cursor = ''
|
|
540
|
+
try {
|
|
541
|
+
cursor = readFileSync(CURSOR_FILE, 'utf8').trim()
|
|
542
|
+
process.stderr.write(`wechat channel: resumed poll cursor (${cursor.length} bytes)\n`)
|
|
543
|
+
} catch {}
|
|
552
544
|
|
|
553
545
|
for (let attempt = 1; !shuttingDown; ) {
|
|
554
546
|
try {
|
|
555
547
|
const res = await apiCall(account.baseUrl, 'ilink/bot/getupdates', {
|
|
556
|
-
get_updates_buf:
|
|
548
|
+
get_updates_buf: cursor,
|
|
557
549
|
}, account.token, 40000) as Record<string, unknown>
|
|
558
550
|
|
|
559
551
|
const errcode = res.errcode as number | undefined
|
|
@@ -568,7 +560,13 @@ void (async () => {
|
|
|
568
560
|
continue
|
|
569
561
|
}
|
|
570
562
|
|
|
571
|
-
if (res.get_updates_buf)
|
|
563
|
+
if (res.get_updates_buf) {
|
|
564
|
+
cursor = res.get_updates_buf as string
|
|
565
|
+
try {
|
|
566
|
+
mkdirSync(STATE_DIR, { recursive: true })
|
|
567
|
+
writeFileSync(CURSOR_FILE, cursor)
|
|
568
|
+
} catch {}
|
|
569
|
+
}
|
|
572
570
|
|
|
573
571
|
const msgs = (res.msgs ?? []) as WechatMessage[]
|
|
574
572
|
attempt = 1
|
|
@@ -618,6 +616,7 @@ void (async () => {
|
|
|
618
616
|
user_id: senderId,
|
|
619
617
|
ts,
|
|
620
618
|
}
|
|
619
|
+
if (msg.context_token) meta.context_token = msg.context_token
|
|
621
620
|
if (msg.message_id != null) meta.message_id = String(msg.message_id)
|
|
622
621
|
if (imagePath) meta.image_path = imagePath
|
|
623
622
|
if (attachmentPath) meta.attachment_path = attachmentPath
|
|
@@ -1,64 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configure
|
|
3
|
-
description: Set up the WeChat channel — scan QR code to log in. Use when the user asks to configure WeChat,
|
|
3
|
+
description: Set up the WeChat channel — scan QR code to log in. Use when the user asks to configure WeChat, login, check status, or reconnect.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Write
|
|
8
|
-
- Bash(ls *)
|
|
9
|
-
- Bash(mkdir *)
|
|
10
|
-
- Bash(chmod *)
|
|
11
8
|
- Bash(curl *)
|
|
9
|
+
- Bash(mkdir *)
|
|
12
10
|
---
|
|
13
11
|
|
|
14
12
|
# /wechat:configure — WeChat Channel Setup
|
|
15
13
|
|
|
16
|
-
Handles QR code login. WeChat access is gated by the QR code scan itself —
|
|
17
|
-
only contacts of the logged-in account can send messages. No separate
|
|
18
|
-
pairing or allowlist needed.
|
|
19
|
-
|
|
20
14
|
Arguments passed: `$ARGUMENTS`
|
|
21
15
|
|
|
22
16
|
---
|
|
23
17
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
### No args — status
|
|
27
|
-
|
|
28
|
-
1. **Account** — check `~/.claude/channels/wechat/account.json`. Show
|
|
29
|
-
logged-in/not-logged-in. If logged in, show the base URL (masked).
|
|
30
|
-
2. **What next**:
|
|
31
|
-
- Not logged in → *"Run `/wechat:configure login` to scan QR code."*
|
|
32
|
-
- Logged in → *"Ready. Messages from your WeChat contacts will reach
|
|
33
|
-
this session."*
|
|
34
|
-
|
|
35
|
-
### `login` — QR code login
|
|
36
|
-
|
|
37
|
-
1. `mkdir -p ~/.claude/channels/wechat`
|
|
38
|
-
2. GET `https://ilinkai.weixin.qq.com/ilink/bot/get_bot_qrcode?bot_type=3`
|
|
39
|
-
3. The response has `qrcode` (ID) and `qrcode_img_content` (URL).
|
|
40
|
-
Display the `qrcode_img_content` URL as a QR code in the terminal
|
|
41
|
-
using `npx qrcode-terminal "<url>"`, or show the URL directly.
|
|
42
|
-
4. Tell the user: *"Scan this QR code with WeChat on your phone."*
|
|
43
|
-
5. Poll status every 2-3 seconds until confirmed or timeout (5 minutes):
|
|
44
|
-
```
|
|
45
|
-
GET https://ilinkai.weixin.qq.com/ilink/bot/get_qrcode_status?qrcode=<qrcode>
|
|
46
|
-
```
|
|
47
|
-
Statuses: `wait`, `scaned`, `confirmed`, `expired`.
|
|
48
|
-
6. On `confirmed`, save to `~/.claude/channels/wechat/account.json`:
|
|
49
|
-
```json
|
|
50
|
-
{"token": "<bot_token>", "baseUrl": "<baseurl>"}
|
|
51
|
-
```
|
|
52
|
-
`chmod 600` the file.
|
|
53
|
-
7. Confirm: *"Logged in! Restart Claude Code to start receiving messages."*
|
|
54
|
-
|
|
55
|
-
### `logout` — remove credentials
|
|
56
|
-
|
|
57
|
-
Delete `~/.claude/channels/wechat/account.json`. Confirm.
|
|
18
|
+
## No args — status
|
|
58
19
|
|
|
59
|
-
|
|
20
|
+
1. Read `~/.claude/channels/wechat/account.json`. Show logged-in or not.
|
|
21
|
+
2. If logged in: *"Ready. Restart Claude Code to start receiving messages."*
|
|
22
|
+
3. If not: *"Run `/wechat:configure login` to connect."*
|
|
23
|
+
|
|
24
|
+
## `login`
|
|
25
|
+
|
|
26
|
+
### Step 1: Get QR code
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -s 'https://ilinkai.weixin.qq.com/ilink/bot/get_bot_qrcode?bot_type=3'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Response: `{"qrcode":"<token>","qrcode_img_content":"<url>","ret":0}`
|
|
33
|
+
|
|
34
|
+
Generate a QR image URL:
|
|
35
|
+
`https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=<url-encoded qrcode_img_content>`
|
|
36
|
+
|
|
37
|
+
Print ONLY the QR image URL. Tell the user: *"Open this link in a browser, then scan the QR code with WeChat."*
|
|
38
|
+
|
|
39
|
+
Then immediately start polling (don't wait for user to say they scanned).
|
|
40
|
+
|
|
41
|
+
IMPORTANT: In zsh, `status` is a read-only variable. Use `scan_status` or
|
|
42
|
+
another name when parsing the response.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
for i in $(seq 1 60); do
|
|
46
|
+
result=$(curl -s "https://ilinkai.weixin.qq.com/ilink/bot/get_qrcode_status?qrcode=<qrcode>")
|
|
47
|
+
scan_status=$(echo "$result" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
|
48
|
+
if [ "$scan_status" = "confirmed" ]; then echo "$result"; break; fi
|
|
49
|
+
if [ "$scan_status" = "expired" ]; then echo "expired"; break; fi
|
|
50
|
+
sleep 3
|
|
51
|
+
done
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Parse the result:
|
|
55
|
+
- `"status":"confirmed"` — success! Extract `bot_token`, `baseurl`
|
|
56
|
+
- `"status":"expired"` — tell user to run login again
|
|
57
|
+
|
|
58
|
+
### Step 3: Save credentials
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
mkdir -p ~/.claude/channels/wechat
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Write `~/.claude/channels/wechat/account.json`:
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"token": "<bot_token>",
|
|
68
|
+
"baseUrl": "<baseurl>"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then `chmod 600 ~/.claude/channels/wechat/account.json`.
|
|
73
|
+
|
|
74
|
+
Tell user:
|
|
75
|
+
```
|
|
76
|
+
✅ Logged in! Restart Claude Code with:
|
|
77
|
+
|
|
78
|
+
claude --dangerously-skip-permissions --dangerously-load-development-channels plugin:wechat@claude-wechat-channel
|
|
79
|
+
```
|
|
60
80
|
|
|
61
|
-
##
|
|
81
|
+
## `logout`
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
- The QR code expires after a few minutes. If it times out, re-run login.
|
|
83
|
+
Delete `~/.claude/channels/wechat/account.json`.
|