Skip to content

Commit 09f4089

Browse files
author
linyuan.yang
committed
wechat
1 parent f494e42 commit 09f4089

2 files changed

Lines changed: 69 additions & 49 deletions

File tree

packages/channel.wechat/src/WechatService.ts

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./types";
1616
import type { WeixinMessage, WechatMessageItem, WechatCredentials, CDNMedia } from "./types";
1717

18+
1819
export interface WechatMessageArgs extends ChannelMessageArgs {
1920
messageId: number;
2021
fromUserId: string;
@@ -37,7 +38,6 @@ export class WechatService implements IChannelService {
3738
private _abortController?: AbortController;
3839
private _updatesBuf = "";
3940
private _lastMessageId = 0;
40-
/** Map of `botId:userId` → context_token, needed for typing and reply continuity */
4141
private _contextTokens = new Map<string, string>();
4242
private _botId: string;
4343

@@ -226,61 +226,80 @@ export class WechatService implements IChannelService {
226226
const items = msg.item_list ?? [];
227227
const parts: Array<{ type: string; text?: string; [key: string]: any }> = [];
228228
let hasImage = false;
229+
229230
for (const item of items) {
230-
switch (item.type) {
231-
case WechatMessageItemType.TEXT: parts.push({ type: 'text', text: item.text_item?.text ?? "" }); break;
232-
case WechatMessageItemType.VOICE: parts.push({ type: 'text', text: item.voice_item?.text ?? "" }); break;
233-
case WechatMessageItemType.FILE: {
234-
const fileContent = await this.extractFileContent(msg.message_id, item);
235-
parts.push({ type: 'text', text: fileContent });
236-
break;
237-
}
238-
case WechatMessageItemType.IMAGE: {
239-
const imagePart = await this.extractImagePart(msg.message_id, item);
240-
parts.push(imagePart);
241-
if (imagePart.type === 'image_url') hasImage = true;
242-
break;
231+
if (item.ref_msg?.message_item) {
232+
const refParts = await this.extractItemParts(msg.message_id, item.ref_msg.message_item, "引用");
233+
for (const rp of refParts) {
234+
parts.push(rp);
235+
if (rp.type === 'image_url') hasImage = true;
243236
}
244-
case WechatMessageItemType.VIDEO: parts.push({ type: 'text', text: "[视频]" }); break;
237+
}
238+
239+
const itemParts = await this.extractItemParts(msg.message_id, item);
240+
for (const p of itemParts) {
241+
parts.push(p);
242+
if (p.type === 'image_url') hasImage = true;
245243
}
246244
}
247-
// If no images, return plain string for backward compat
248245
if (!hasImage) return parts.map(p => p.text!).join("\n").trim();
249246
return parts;
250247
}
251248

252-
private async extractImagePart(messageId: number | undefined, item: WechatMessageItem): Promise<{ type: string; [key: string]: any }> {
253-
const media = item.image_item?.media;
254-
if (!media?.encrypt_query_param) {
255-
return { type: 'text', text: "[图片]" };
256-
}
257-
try {
258-
const buffer = await this.api.downloadFromCDN(media.encrypt_query_param, media.aes_key);
259-
const filePath = path.join(os.tmpdir(), `wechat_${messageId ?? Date.now()}_img`);
260-
await fs.writeFile(filePath, buffer);
261-
const dataUrl = await readImageAsDataUrl(filePath);
262-
return { type: 'image_url', image_url: { url: dataUrl } };
263-
} catch (e: any) {
264-
this.logger?.error(`Failed to download image: ${e.message}`);
265-
return { type: 'text', text: "[图片]" };
266-
}
267-
}
249+
private async extractItemParts(messageId: number | undefined, item: WechatMessageItem, label?: string): Promise<Array<{ type: string; [key: string]: any }>> {
250+
switch (item.type) {
251+
case WechatMessageItemType.TEXT: {
252+
const text = item.text_item?.text ?? "";
253+
return [{ type: 'text', text: label ? `[${label}: ${text}]` : text }];
254+
}
268255

269-
private async extractFileContent(messageId: number | undefined, item: WechatMessageItem): Promise<string> {
270-
const fileName = item.file_item?.file_name ?? "unknown_file";
271-
const media = item.file_item?.media;
272-
if (!media?.encrypt_query_param) {
273-
return `[文件: ${fileName}]`;
274-
}
275-
try {
276-
const buffer = await this.api.downloadFromCDN(media.encrypt_query_param, media.aes_key);
277-
const ext = path.extname(fileName);
278-
const filePath = path.join(os.tmpdir(), `wechat_${messageId ?? Date.now()}${ext}`);
279-
await fs.writeFile(filePath, buffer);
280-
return `[file: ${fileName}](${filePath})`;
281-
} catch (e: any) {
282-
this.logger?.error(`Failed to download file "${fileName}": ${e.message}`);
283-
return `[文件: ${fileName}]`;
256+
case WechatMessageItemType.VOICE: {
257+
const text = item.voice_item?.text ?? "";
258+
return [{ type: 'text', text: label ? `[${label}: ${text}]` : text }];
259+
}
260+
261+
case WechatMessageItemType.IMAGE: {
262+
const media = item.image_item?.media;
263+
const imgFallback = label ? `[${label}: 图片]` : "[图片]";
264+
if (!media?.encrypt_query_param) return [{ type: 'text', text: imgFallback }];
265+
try {
266+
const buffer = await this.api.downloadFromCDN(media.encrypt_query_param, media.aes_key);
267+
const filePath = path.join(os.tmpdir(), `wechat_${messageId ?? Date.now()}_img`);
268+
await fs.writeFile(filePath, buffer);
269+
const dataUrl = await readImageAsDataUrl(filePath);
270+
const parts: Array<{ type: string; [key: string]: any }> = [];
271+
if (label) parts.push({ type: 'text', text: imgFallback });
272+
parts.push({ type: 'image_url', image_url: { url: dataUrl } });
273+
return parts;
274+
} catch (e: any) {
275+
this.logger?.error(`Failed to download image: ${e.message}`);
276+
return [{ type: 'text', text: imgFallback }];
277+
}
278+
}
279+
280+
case WechatMessageItemType.FILE: {
281+
const fileName = item.file_item?.file_name ?? "unknown_file";
282+
const media = item.file_item?.media;
283+
const fileFallback = label ? `[${label}: 文件 ${fileName}]` : `[文件: ${fileName}]`;
284+
if (!media?.encrypt_query_param) return [{ type: 'text', text: fileFallback }];
285+
try {
286+
const buffer = await this.api.downloadFromCDN(media.encrypt_query_param, media.aes_key);
287+
const ext = path.extname(fileName);
288+
const filePath = path.join(os.tmpdir(), `wechat_${messageId ?? Date.now()}${ext}`);
289+
await fs.writeFile(filePath, buffer);
290+
const linkText = label ? `[${label}: file ${fileName}](${filePath})` : `[file: ${fileName}](${filePath})`;
291+
return [{ type: 'text', text: linkText }];
292+
} catch (e: any) {
293+
this.logger?.error(`Failed to download file "${fileName}": ${e.message}`);
294+
return [{ type: 'text', text: fileFallback }];
295+
}
296+
}
297+
298+
case WechatMessageItemType.VIDEO:
299+
return [{ type: 'text', text: label ? `[${label}: 视频]` : "[视频]" }];
300+
301+
default:
302+
return [];
284303
}
285304
}
286305

packages/channel.wechat/src/WechatSessionHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
2-
ChannelSessionHandler, SessionService, GlobalLoggerService, ToolApproval, createSendFileTool,
2+
ChannelSessionHandler, SessionService, GlobalLoggerService, ToolApproval,
3+
createSendFileTool,
34
type StructuredToolInterface,
45
type ChatMessage, type ChatToolCall, type AskToolParams, type MessageType,
56
type ChannelMessageArgs, type MessageContent,
@@ -58,7 +59,7 @@ export class WechatSessionHandler extends ChannelSessionHandler {
5859

5960
// --- Agent tools ---
6061

61-
static readonly SEND_FILE_PROMPT = "Send a local file to the current WeChat conversation. Use this tool to deliver any generated or exported file (documents, archives, reports, images, etc.) directly to the user via WeChat.";
62+
static readonly SEND_FILE_PROMPT = "Send a local file or image to the current WeChat conversation. Use this tool to deliver any generated or exported file (documents, archives, reports, images, charts, etc.) directly to the user via WeChat.";
6263

6364
buildAgentTools(args: ChannelMessageArgs): StructuredToolInterface[] {
6465
const userId = (args as WechatMessageArgs).fromUserId ?? args.sessionId;

0 commit comments

Comments
 (0)