@qwen-code/channel-weixin 0.21.0-preview.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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * WeChat channel adapter for Qwen Code.
3
+ * Extends ChannelBase with WeChat iLink Bot API integration.
4
+ */
5
+ import { ChannelBase } from '@qwen-code/channel-base';
6
+ import type { ChannelConfig, ChannelBaseOptions, ChannelAgentBridge, ChannelTaskLifecycleEvent } from '@qwen-code/channel-base';
7
+ export declare class WeixinChannel extends ChannelBase {
8
+ private abortController;
9
+ private activeTypingChats;
10
+ private baseUrl;
11
+ private token;
12
+ constructor(name: string, config: ChannelConfig, bridge: ChannelAgentBridge, options?: ChannelBaseOptions);
13
+ connect(): Promise<void>;
14
+ protected onPromptStart(chatId: string): void;
15
+ protected onPromptEnd(chatId: string): void;
16
+ protected onTaskLifecycle(event: ChannelTaskLifecycleEvent): void;
17
+ private handleInboundWithMedia;
18
+ sendMessage(chatId: string, text: string): Promise<void>;
19
+ disconnect(): void;
20
+ private startTyping;
21
+ private stopTyping;
22
+ private setTyping;
23
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * WeChat channel adapter for Qwen Code.
3
+ * Extends ChannelBase with WeChat iLink Bot API integration.
4
+ */
5
+ import { mkdirSync, writeFileSync } from 'node:fs';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { basename, join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { ChannelBase, isTerminalTaskLifecycleType, } from '@qwen-code/channel-base';
10
+ import { loadAccount, DEFAULT_BASE_URL } from './accounts.js';
11
+ import { startPollLoop, getContextToken } from './monitor.js';
12
+ import { sendText, sendImage, detectImageMime } from './send.js';
13
+ import { downloadAndDecrypt } from './media.js';
14
+ import { getConfig, sendTyping, WeixinApiError } from './api.js';
15
+ import { TypingStatus } from './types.js';
16
+ /** In-memory typing ticket cache: userId -> typingTicket */
17
+ const typingTickets = new Map();
18
+ /** Escape special regex characters in a string. */
19
+ function escapeRegex(s) {
20
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
+ }
22
+ export class WeixinChannel extends ChannelBase {
23
+ abortController = null;
24
+ activeTypingChats = new Set();
25
+ baseUrl;
26
+ token = '';
27
+ constructor(name, config, bridge, options) {
28
+ super(name, config, bridge, options);
29
+ this.baseUrl =
30
+ config.baseUrl ||
31
+ DEFAULT_BASE_URL;
32
+ }
33
+ async connect() {
34
+ // Default channel instructions — always include image capability info
35
+ const imageInstructions = [
36
+ '',
37
+ 'If you created an image file (screenshot, chart, etc.), you can send it to the user by writing:',
38
+ '[IMAGE: /absolute/path/to/file.png]',
39
+ '',
40
+ 'The marker is stripped from text and the image is uploaded automatically.',
41
+ '',
42
+ 'CRITICAL: Only use real file paths. Do NOT write [IMAGE: ...] with:',
43
+ '- Example paths like /path/to/file or /tmp/cat.png',
44
+ '- Placeholder symbols like ...',
45
+ "- Paths that don't exist on disk",
46
+ ].join('\n');
47
+ if (!this.config.instructions) {
48
+ this.config.instructions = [
49
+ '## WeChat Channel',
50
+ '',
51
+ 'You are a concise coding assistant responding via WeChat.',
52
+ 'Keep responses under 500 characters. Use plain text only.',
53
+ '',
54
+ 'Users can also send you images.',
55
+ imageInstructions,
56
+ ].join('\n');
57
+ }
58
+ else if (!this.config.instructions.includes('[IMAGE:')) {
59
+ // Use a local copy to avoid mutating this.config.instructions on reconnect.
60
+ this.config.instructions =
61
+ this.config.instructions + '\n' + imageInstructions;
62
+ }
63
+ const account = loadAccount();
64
+ if (!account) {
65
+ throw new Error('WeChat account not configured. Run "qwen channel configure-weixin" first.');
66
+ }
67
+ this.token = account.token;
68
+ if (account.baseUrl) {
69
+ this.baseUrl = account.baseUrl;
70
+ }
71
+ this.abortController = new AbortController();
72
+ startPollLoop({
73
+ baseUrl: this.baseUrl,
74
+ token: this.token,
75
+ onMessage: async (msg) => {
76
+ const envelope = {
77
+ channelName: this.name,
78
+ senderId: msg.fromUserId,
79
+ senderName: msg.fromUserId,
80
+ chatId: msg.fromUserId,
81
+ text: msg.text,
82
+ isGroup: false,
83
+ isMentioned: false,
84
+ isReplyToBot: false,
85
+ referencedText: msg.refText,
86
+ };
87
+ this.handleInboundWithMedia(envelope, msg.image, msg.file).catch((err) => {
88
+ const errMsg = err instanceof Error ? err.message : JSON.stringify(err, null, 2);
89
+ process.stderr.write(`[Weixin:${this.name}] Error handling message: ${errMsg}\n`);
90
+ });
91
+ },
92
+ abortSignal: this.abortController.signal,
93
+ }).catch((err) => {
94
+ if (!this.abortController?.signal.aborted) {
95
+ process.stderr.write(`[Weixin:${this.name}] Poll loop error: ${err}\n`);
96
+ }
97
+ });
98
+ process.stderr.write(`[Weixin:${this.name}] Connected to WeChat (${this.baseUrl})\n`);
99
+ }
100
+ onPromptStart(chatId) {
101
+ this.startTyping(chatId);
102
+ }
103
+ onPromptEnd(chatId) {
104
+ this.stopTyping(chatId);
105
+ }
106
+ onTaskLifecycle(event) {
107
+ if (event.type === 'started') {
108
+ this.startTyping(event.chatId);
109
+ return;
110
+ }
111
+ if (isTerminalTaskLifecycleType(event.type)) {
112
+ this.stopTyping(event.chatId);
113
+ }
114
+ }
115
+ async handleInboundWithMedia(envelope, image, file) {
116
+ // Download image from CDN
117
+ if (image) {
118
+ try {
119
+ const imageData = await downloadAndDecrypt(image.encryptQueryParam, image.aesKey);
120
+ envelope.imageBase64 = imageData.toString('base64');
121
+ envelope.imageMimeType = detectImageMime(imageData);
122
+ }
123
+ catch (err) {
124
+ process.stderr.write(`[Weixin:${this.name}] Failed to download image: ${err instanceof Error ? err.message : err}\n`);
125
+ }
126
+ }
127
+ // Download file from CDN, save to temp dir
128
+ if (file) {
129
+ try {
130
+ const fileData = await downloadAndDecrypt(file.encryptQueryParam, file.aesKey);
131
+ const dir = join(tmpdir(), 'channel-files', randomUUID());
132
+ mkdirSync(dir, { recursive: true });
133
+ const filePath = join(dir, basename(file.fileName) || `file_${Date.now()}`);
134
+ writeFileSync(filePath, fileData);
135
+ envelope.attachments = [
136
+ {
137
+ type: 'file',
138
+ filePath,
139
+ mimeType: 'application/octet-stream',
140
+ fileName: file.fileName,
141
+ },
142
+ ];
143
+ }
144
+ catch (err) {
145
+ process.stderr.write(`[Weixin:${this.name}] Failed to download file: ${err instanceof Error ? err.message : err}\n`);
146
+ envelope.text = `(User sent a file "${file.fileName}" but download failed)`;
147
+ }
148
+ }
149
+ await super.handleInbound(envelope);
150
+ }
151
+ async sendMessage(chatId, text) {
152
+ const contextToken = getContextToken(chatId) || '';
153
+ // Parse [IMAGE: /path/to/file.png] markers from text.
154
+ // Strip code blocks first to avoid matching example syntax inside them.
155
+ const textWithoutCode = text
156
+ .replace(/```[\s\S]*?```/g, '')
157
+ .replace(/`[^`]*`/g, '');
158
+ // Extract image paths from code-free text.
159
+ const imageRegex = /\[IMAGE:\s*([^\]]+)\]/gi;
160
+ const parsedImages = [];
161
+ for (const m of textWithoutCode.matchAll(imageRegex)) {
162
+ const trimmed = m[1]?.trim();
163
+ if (trimmed)
164
+ parsedImages.push(trimmed);
165
+ }
166
+ // Only strip markers that were actually parsed (avoids silently
167
+ // removing [IMAGE:] inside code blocks from the displayed text).
168
+ let cleanedText = text;
169
+ for (const path of parsedImages) {
170
+ cleanedText = cleanedText.replace(new RegExp(`\\[IMAGE:\\s*${escapeRegex(path)}\\]`, 'gi'), '');
171
+ }
172
+ // Clean up double blank lines left by removed markers
173
+ cleanedText = cleanedText.replace(/\n{3,}/g, '\n\n').trim();
174
+ // Send text first if non-empty
175
+ if (cleanedText) {
176
+ await sendText({
177
+ to: chatId,
178
+ text: cleanedText,
179
+ baseUrl: this.baseUrl,
180
+ token: this.token,
181
+ contextToken,
182
+ });
183
+ }
184
+ // Send images
185
+ if (parsedImages.length) {
186
+ const workspaceDirs = [this.config.cwd];
187
+ for (const imagePath of parsedImages) {
188
+ try {
189
+ await sendImage({
190
+ to: chatId,
191
+ imagePath,
192
+ baseUrl: this.baseUrl,
193
+ token: this.token,
194
+ contextToken,
195
+ workspaceDirs,
196
+ });
197
+ }
198
+ catch (err) {
199
+ const status = err instanceof WeixinApiError ? err.status : 0;
200
+ const ret = err instanceof WeixinApiError ? err.ret : undefined;
201
+ const errcode = err instanceof WeixinApiError ? err.errcode : undefined;
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ process.stderr.write(`[Weixin:${this.name}] Failed to send image (status=${status} ret=${ret} errcode=${errcode}): ${msg}\n`);
204
+ try {
205
+ await sendText({
206
+ to: chatId,
207
+ text: '图片发送失败,请稍后重试',
208
+ baseUrl: this.baseUrl,
209
+ token: this.token,
210
+ contextToken,
211
+ });
212
+ }
213
+ catch (fallbackErr) {
214
+ process.stderr.write(`[Weixin:${this.name}] Fallback text also failed: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}\n`);
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ disconnect() {
221
+ if (this.abortController) {
222
+ this.abortController.abort();
223
+ this.abortController = null;
224
+ }
225
+ this.activeTypingChats.clear();
226
+ }
227
+ startTyping(chatId) {
228
+ if (this.activeTypingChats.has(chatId))
229
+ return;
230
+ this.activeTypingChats.add(chatId);
231
+ const controller = this.abortController;
232
+ void this.setTyping(chatId, true).then((started) => {
233
+ // Disconnect (or reconnect) raced the request — don't fire a
234
+ // post-disconnect setTyping(false).
235
+ if (controller !== this.abortController || controller?.signal.aborted) {
236
+ return;
237
+ }
238
+ if (!started) {
239
+ this.activeTypingChats.delete(chatId);
240
+ return;
241
+ }
242
+ if (!this.activeTypingChats.has(chatId)) {
243
+ void this.setTyping(chatId, false);
244
+ }
245
+ });
246
+ }
247
+ stopTyping(chatId) {
248
+ if (!this.activeTypingChats.delete(chatId))
249
+ return;
250
+ void this.setTyping(chatId, false);
251
+ }
252
+ async setTyping(userId, typing) {
253
+ try {
254
+ let ticket = typingTickets.get(userId);
255
+ if (!ticket) {
256
+ const contextToken = getContextToken(userId);
257
+ const config = await getConfig(this.baseUrl, this.token, userId, contextToken);
258
+ if (config.typing_ticket) {
259
+ ticket = config.typing_ticket;
260
+ typingTickets.set(userId, ticket);
261
+ }
262
+ }
263
+ if (!ticket)
264
+ return false;
265
+ await sendTyping(this.baseUrl, this.token, {
266
+ ilink_user_id: userId,
267
+ typing_ticket: ticket,
268
+ status: typing ? TypingStatus.TYPING : TypingStatus.CANCEL,
269
+ });
270
+ return true;
271
+ }
272
+ catch {
273
+ // Typing is best-effort — don't fail the message flow
274
+ return false;
275
+ }
276
+ }
277
+ }
278
+ //# sourceMappingURL=WeixinAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WeixinAdapter.js","sourceRoot":"","sources":["../src/WeixinAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Credential storage for WeChat account.
3
+ * Stores account data in ~/.qwen/channels/weixin/
4
+ */
5
+ export declare const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
6
+ export interface AccountData {
7
+ token: string;
8
+ baseUrl: string;
9
+ userId?: string;
10
+ savedAt: string;
11
+ }
12
+ export declare function getStateDir(): string;
13
+ export declare function loadAccount(): AccountData | null;
14
+ export declare function saveAccount(data: AccountData): void;
15
+ export declare function clearAccount(): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Credential storage for WeChat account.
3
+ * Stores account data in ~/.qwen/channels/weixin/
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, renameSync, } from 'node:fs';
6
+ import { randomBytes } from 'node:crypto';
7
+ import { join } from 'node:path';
8
+ import { getGlobalQwenDir } from '@qwen-code/channel-base';
9
+ export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
10
+ export function getStateDir() {
11
+ const dir = process.env['WEIXIN_STATE_DIR'] ||
12
+ join(getGlobalQwenDir(), 'channels', 'weixin');
13
+ if (!existsSync(dir)) {
14
+ mkdirSync(dir, { recursive: true });
15
+ }
16
+ return dir;
17
+ }
18
+ function accountPath() {
19
+ return join(getStateDir(), 'account.json');
20
+ }
21
+ export function loadAccount() {
22
+ const p = accountPath();
23
+ if (!existsSync(p))
24
+ return null;
25
+ try {
26
+ return JSON.parse(readFileSync(p, 'utf-8'));
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function saveAccount(data) {
33
+ const p = accountPath();
34
+ // The temp path must not be pre-creatable. A fixed `${p}.tmp` can be planted
35
+ // in advance by anyone able to write to the directory, and a plain write
36
+ // follows a symlink found there — which would send the token wherever it
37
+ // points. `randomBytes` rather than a `Date.now()`/pid/`Math.random()` suffix
38
+ // because those are guessable and V8's `Math.random` is not a CSPRNG; this
39
+ // matches `ChannelLoopStore`, the sibling store with the same requirement.
40
+ const tmp = `${p}.${randomBytes(6).toString('hex')}.tmp`;
41
+ try {
42
+ // `wx` is `O_CREAT|O_EXCL`: it refuses an existing path, and the kernel
43
+ // fails it on a symlink at the final component rather than following it.
44
+ // The unguessable name makes a plant unlikely; the flag makes it
45
+ // ineffective — so this does not rest on the name alone.
46
+ //
47
+ // `O_EXCL` also guarantees this call is the one that creates the file,
48
+ // which is what makes `mode` take effect: it is silently ignored for a path
49
+ // that already exists. Writing first and narrowing afterwards would leave
50
+ // the token readable at the umask default (0644 under the usual 022) for
51
+ // the window between the two calls.
52
+ writeFileSync(tmp, JSON.stringify(data, null, 2), {
53
+ encoding: 'utf-8',
54
+ mode: 0o600,
55
+ flag: 'wx',
56
+ });
57
+ // Rename carries the 0600 mode onto the destination, so an account.json
58
+ // written by an older version is narrowed rather than left as it was.
59
+ renameSync(tmp, p);
60
+ }
61
+ catch (e) {
62
+ try {
63
+ unlinkSync(tmp);
64
+ }
65
+ catch {
66
+ /* best-effort cleanup */
67
+ }
68
+ throw e;
69
+ }
70
+ }
71
+ export function clearAccount() {
72
+ const dir = getStateDir();
73
+ const p = join(dir, 'account.json');
74
+ if (existsSync(p)) {
75
+ unlinkSync(p);
76
+ }
77
+ // A save killed between the write and the rename leaves a temp file holding a
78
+ // live token. The name is never reused, so nothing else would ever remove it:
79
+ // without this sweep the credential outlives the logout meant to revoke it.
80
+ for (const f of readdirSync(dir)) {
81
+ if (f.startsWith('account.json.') && f.endsWith('.tmp')) {
82
+ try {
83
+ unlinkSync(join(dir, f));
84
+ }
85
+ catch {
86
+ /* best-effort cleanup */
87
+ }
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=accounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts.js","sourceRoot":"","sources":["../src/accounts.ts"],"names":[],"mappings":"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"}
package/dist/api.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * HTTP API wrapper for WeChat iLink Bot API.
3
+ */
4
+ import type { GetUpdatesResp, SendMessageReq, GetConfigResp, SendTypingReq, SendTypingResp } from './types.js';
5
+ /** Structured error from WeChat iLink Bot API. */
6
+ export declare class WeixinApiError extends Error {
7
+ /** HTTP status code (0 if network/timeout error). */
8
+ status: number;
9
+ /** API-level return code (ret field in response body). */
10
+ ret?: number;
11
+ /** API-level error code (errcode field in response body). */
12
+ errcode?: number;
13
+ constructor(message: string, status: number, ret?: number, errcode?: number);
14
+ }
15
+ export declare function buildHeaders(token?: string): Record<string, string>;
16
+ export declare function getUpdates(baseUrl: string, token: string, getUpdatesBuf: string, timeoutMs?: number, signal?: AbortSignal): Promise<GetUpdatesResp>;
17
+ export declare function sendMessage(baseUrl: string, token: string, msg: SendMessageReq['msg']): Promise<void>;
18
+ export declare function getConfig(baseUrl: string, token: string, userId: string, contextToken?: string): Promise<GetConfigResp>;
19
+ export declare function sendTyping(baseUrl: string, token: string, req: Omit<SendTypingReq, 'base_info'>): Promise<SendTypingResp>;
20
+ /**
21
+ * Request an upload URL and CDN credentials for media.
22
+ * @param aeskeyHex 16-byte AES key as 32-char hex string (e.g. "00112233445566778899aabbccddeeff")
23
+ * @returns Either the full CDN upload URL or the upload_param string
24
+ */
25
+ export declare function getUploadUrl(baseUrl: string, token: string, toUserId: string, filekey: string, rawsize: number, rawfilemd5: string, encryptedSize: number, aeskeyHex: string): Promise<string>;
26
+ /** Upload encrypted media to CDN.
27
+ * If urlOrParam is a full URL, use it directly (host must match).
28
+ * If it's just a param, construct the URL. */
29
+ export declare function uploadToCdn(urlOrParam: string, filekey: string, encryptedData: Buffer): Promise<string>;