@aluria/wechat-ai-api 1.0.0 → 1.0.1

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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/WechatAiApi.ts","../src/core/CryptoUtils.ts","../src/constants.ts","../src/types.ts","../src/core/utils.ts","../src/core/WeChatCore.ts","../src/managers/AuthManager.ts","../src/managers/MessageManager.ts","../src/managers/CdnManager.ts","../src/core/SilkConverter.ts","../src/managers/MessageParser.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\n\nimport { WeChatCore } from \"./core/WeChatCore.ts\";\nimport { AuthManager } from \"./managers/AuthManager.ts\";\nimport { MessageManager } from \"./managers/MessageManager.ts\";\nimport { CdnManager } from \"./managers/CdnManager.ts\";\nimport { MessageParser } from \"./managers/MessageParser.ts\";\nimport { mergeObjects } from \"./core/utils.ts\";\nimport {\n DEFAULT_CLIENT_CONFIG,\n DEFAULT_LOGIN_OPTIONS,\n DEFAULT_REQUEST_TIMEOUT_MS,\n LONG_POLLING_TIMEOUT_MS,\n POLLING_ERROR_RETRY_DELAY_MS,\n} from \"./constants.ts\";\nimport type {\n LoginCredentials,\n LoginOptions,\n PollingResult,\n WeChatApiEventMap,\n WeChatClientConfig,\n} from \"./types.ts\";\n\nexport class WeChatApi extends EventEmitter<WeChatApiEventMap> {\n public readonly core: WeChatCore;\n public readonly auth: AuthManager;\n public readonly messages: MessageManager;\n public readonly parser: MessageParser;\n public readonly cdn: CdnManager;\n\n // 内部缓存当前的登录凭证\n private currentCredentials: LoginCredentials | null = null;\n\n private isPolling: boolean = false;\n private syncBuf: string = \"\"; // 极其重要:状态同步游标\n\n constructor(config: Partial<WeChatClientConfig> = DEFAULT_CLIENT_CONFIG) {\n super(); // 初始化 EventEmitter\n\n // 合并默认配置和用户配置\n const mergedConfig = mergeObjects(DEFAULT_CLIENT_CONFIG, config);\n\n // 初始化网络底层\n this.core = new WeChatCore(mergedConfig);\n\n // 挂载领域模块\n this.auth = new AuthManager(this.core);\n this.cdn = new CdnManager(this.core);\n this.messages = new MessageManager(this.core, this.cdn);\n this.parser = new MessageParser(this.cdn, mergedConfig);\n\n // 如果初始化时直接传入了 token,先暂存一份不完整的凭证\n if (mergedConfig.token) {\n this.currentCredentials = {\n token: mergedConfig.token,\n baseUrl: mergedConfig.baseUrl,\n accountId: \"\", // 初始化时可能未知\n userId: \"\", // 初始化时可能未知\n };\n }\n }\n\n // ==========================================\n // 1. 登录模块 (支持无脑调用 & 深度定制)\n // ==========================================\n\n /**\n * 启动扫码登录流程。\n * @param options 可选。如果为空,将使用默认的控制台交互体验。\n */\n public async login(\n options: Partial<LoginOptions> = DEFAULT_LOGIN_OPTIONS,\n ): Promise<LoginCredentials> {\n // 默认的“无脑”交互实现\n const defaultOptions = mergeObjects(DEFAULT_LOGIN_OPTIONS, options);\n\n // 执行底层的登录逻辑\n const result = await this.auth.login(defaultOptions);\n\n // 登录成功后,缓存在 Bot 实例中,方便随时导出\n this.currentCredentials = result;\n\n this.emit(\"login\", this.currentCredentials); // 触发全局登录事件\n return this.currentCredentials;\n }\n\n // ==========================================\n // 2. 凭证管理模块 (导入 / 导出 / 验证)\n // ==========================================\n\n /**\n * 导出当前的登录凭证。\n * 开发者拿到后可以自行保存到本地文件或数据库中。\n */\n public exportCredentials(): LoginCredentials | null {\n if (!this.currentCredentials || !this.currentCredentials.token) {\n return null;\n }\n // 返回一个深拷贝,防止外部修改污染内部状态\n return { ...this.currentCredentials };\n }\n\n /**\n * 加载现有的登录凭证。\n * 适用于程序重启后,免扫码直接恢复状态。\n */\n public loadCredentials(credentials: LoginCredentials): void {\n this.currentCredentials = { ...credentials };\n\n // 同步给底层 HTTP 引擎\n this.core.setToken(credentials.token);\n this.core.setBaseUrl(credentials.baseUrl);\n this.core.setUserId(credentials.userId);\n\n console.log(\n `[WeChatApi] 成功加载凭证 (AccountID: ${credentials.accountId})`,\n );\n }\n\n /**\n * 验证当前加载的凭证是否仍然有效。\n * @returns boolean 是否有效\n */\n public async verifyCredentials(): Promise<boolean> {\n if (!this.currentCredentials || !this.currentCredentials.token) {\n return false;\n }\n\n try {\n // 使用 getconfig 作为一个轻量级的 Ping 探针\n const response = await this.core.request<any>(\"ilink/bot/getconfig\", {\n method: \"POST\",\n body: {\n ilink_user_id: this.currentCredentials.userId,\n },\n timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (response.ret !== undefined && response.ret !== 0) {\n console.warn(\n `[WeChatApi] 凭证已失效 (Server returned: ${\n response.errcode || response.ret\n })`,\n );\n return false;\n }\n\n if (response.errcode !== undefined && response.errcode !== 0) {\n console.warn(\n `[WeChatApi] 凭证已失效 (Server returned errcode: ${response.errcode}): ${response.errmsg}`,\n );\n return false;\n }\n\n return true;\n } catch (error) {\n console.error(\n \"[WeChatApi] 凭证验证失败,网络异常或已过期:\",\n (error as Error).message,\n );\n return false;\n }\n }\n\n // ==========================================\n // 轮询守护进程 (Daemon Loop)\n // ==========================================\n\n /**\n * 启动长轮询,监听新消息\n */\n public async startPolling() {\n if (this.isPolling) {\n console.warn(\"[WeChatApi] 轮询已经在运行中,请勿重复启动\");\n return;\n }\n\n if (!this.currentCredentials?.token) {\n throw new Error(\"[WeChatApi] 无法启动轮询:尚未登录或未加载凭证\");\n }\n\n this.isPolling = true;\n console.log(\"[WeChatApi] 🚀 轮询守护进程已启动,正在监听新消息...\");\n\n while (this.isPolling) {\n try {\n // 核心:发起长轮询请求。这里会挂起长达 35 秒,直到有新消息或超时\n const response = await this.core.request<PollingResult>(\n \"ilink/bot/getupdates\",\n {\n method: \"POST\",\n body: { get_updates_buf: this.syncBuf },\n timeoutMs: LONG_POLLING_TIMEOUT_MS, // 长轮询标准超时时间\n },\n );\n\n // 1. 检查服务端返回的错误码 (容错处理:成功时字段可能被省略)\n const hasErrorRet = response.ret !== undefined && response.ret !== 0;\n const hasErrcode = response.errcode !== undefined &&\n response.errcode !== 0;\n\n // 1. 检查服务端返回的错误码 (例如 -14 代表登录失效)\n if (hasErrorRet || hasErrcode) {\n this.isPolling = false;\n console.log(response);\n this.emit(\n \"error\",\n new Error(`会话已失效或服务端报错 (errcode: ${response.errcode})`),\n );\n console.error(`[WeChatApi] ❌ 轮询异常中止:${response.errmsg}`);\n break;\n }\n\n // 2. 更新同步游标 (无论有没有新消息,都要更新,否则会死循环拉取旧消息)\n if (response.get_updates_buf) {\n this.syncBuf = response.get_updates_buf;\n }\n\n // 3. 解析并分发新消息\n if (response.msgs && response.msgs.length > 0) {\n for (const rawMsg of response.msgs) {\n const parsedMsgs = await this.parser.parse(rawMsg);\n if (!parsedMsgs || parsedMsgs.length === 0) continue;\n\n // 触发全局通用消息事件\n for (const msg of parsedMsgs) {\n this.emit(\"message\", msg);\n if (msg.msgTypeStr !== \"unknown\") {\n this.emit(msg.msgTypeStr, msg as any);\n }\n }\n }\n }\n } catch (error: any) {\n // 4. 处理客户端超时 (正常现象,继续轮询)\n if (error.name === \"AbortError\" || error.message.includes(\"timeout\")) {\n // 仅仅是 35 秒内没人发消息而已,什么都不用做,继续进入下一个 while 循环\n continue;\n }\n\n // 处理真实的网络异常 (断网等),退避 3 秒后重试,防止 CPU 满载\n console.error(\n `[WeChatApi] ⚠️ 轮询遇到网络异常,3秒后重试: ${error.message}`,\n );\n await new Promise((resolve) =>\n setTimeout(resolve, POLLING_ERROR_RETRY_DELAY_MS)\n );\n }\n }\n\n console.log(\"[WeChatApi] 🛑 轮询守护进程已停止。\");\n }\n\n /**\n * 停止长轮询\n */\n public stopPolling() {\n this.isPolling = false;\n }\n}\n","// src/core/CryptoUtils.ts\n\nimport {\n createCipheriv,\n createDecipheriv,\n createHash,\n randomBytes,\n} from \"node:crypto\";\nimport { AES_BLOCK_SIZE, WECHAT_AES_ALGO } from \"../constants.ts\";\n\nexport class CryptoUtils {\n public static md5(buffer: Buffer): string {\n return createHash(\"md5\").update(buffer).digest(\"hex\");\n }\n\n public static generateRandomKey(length: number = 16): Buffer {\n return randomBytes(length);\n }\n\n public static getPaddedSize(plaintextSize: number): number {\n return Math.ceil((plaintextSize + 1) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE;\n }\n\n public static aesEcbEncrypt(plaintext: Buffer, key: Buffer): Buffer {\n if (key.length !== AES_BLOCK_SIZE) {\n throw new Error(\n `[CryptoUtils] AES-128 密钥长度必须为 ${AES_BLOCK_SIZE} 字节,当前为 ${key.length} 字节`,\n );\n }\n const cipher = createCipheriv(WECHAT_AES_ALGO, key, null);\n return Buffer.concat([cipher.update(plaintext), cipher.final()]);\n }\n\n public static aesEcbDecrypt(ciphertext: Buffer, key: Buffer): Buffer {\n if (key.length !== AES_BLOCK_SIZE) {\n throw new Error(\n `[CryptoUtils] AES-128 密钥长度必须为 ${AES_BLOCK_SIZE} 字节,当前为 ${key.length} 字节`,\n );\n }\n const decipher = createDecipheriv(WECHAT_AES_ALGO, key, null);\n return Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n }\n\n /**\n * 生成去重的客户端消息 ID\n * 格式: prefix:timestamp-randomHex\n */\n public static generateClientId(): string {\n const randomHex = randomBytes(4).toString(\"hex\");\n return `wechat-bot:${Date.now()}-${randomHex}`;\n }\n\n /** 生成随机的 X-WECHAT-UIN (4字节随机数 -> uint32 -> base64) */\n public static randomWechatUin(): string {\n const uint32 = randomBytes(4).readUInt32BE(0);\n return Buffer.from(String(uint32), \"utf-8\").toString(\"base64\");\n }\n}\n","// src/constants.ts\n\nimport type {\n CdnManagerConfig,\n LoginOptions,\n SendMessageOptions,\n WeChatClientConfig,\n} from \"./types.ts\";\n\n// ====== Universal Constants ======\n\nexport const WECHAT_DEFAULT_BASE_URL = \"https://ilinkai.weixin.qq.com\";\nexport const WECHAT_DEFAULT_CDN_URL = \"https://cdn.weixin.qq.com\";\n\n// 获取二维码的普通 HTTP 请求超时 (10秒)\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 10000;\n// 微信极其经典的长轮询 (Long-Polling) 超时基准线 (35秒)\nexport const LONG_POLLING_TIMEOUT_MS = 35000;\n// 轮询接口返回错误时的重试间隔 (3秒)\nexport const POLLING_ERROR_RETRY_DELAY_MS = 3000;\n// 获取上传门票的接口超时时间 (15秒)\nexport const DEFAULT_API_TIMEOUT_MS = 15000;\n// 二维码硬性失效的兜底时间 (5分钟)\nexport const QR_CODE_EXPIRATION_MS = 5 * 60 * 1000;\n// 轮询失败或异常时的退避时间 (1秒 防御性延迟)\nexport const BACKOFF_BASE_MS = 1000;\n// 获取二维码超时的重试次数\nexport const DEFAULT_LOGIN_MAX_RETRIES = 3;\n\n// AES-128 的密钥长度和块大小(Block Size)。\nexport const AES_BLOCK_SIZE = 16;\n// 微信 CDN 加密算法的硬性规定。\nexport const WECHAT_AES_ALGO = \"aes-128-ecb\";\n// 协议层魔数。微信用 1 来代表特定的 CDN 加密方式(极大概率指代 AES-128-ECB)\nexport const WECHAT_CDN_ENCRYPT_TYPE = 1;\n\n// 微信语音的标准采样率。虽然现在是定义在文件顶部的常量,但它属于协议级常量。\nexport const WECHAT_SILK_SAMPLE_RATE = 24000;\n\nexport const WECHAT_HTTP_HEADERS = {\n AUTH_TYPE: \"AuthorizationType\",\n UIN: \"X-WECHAT-UIN\",\n APP_ID: \"iLink-App-Id\",\n CLIENT_VERSION: \"iLink-App-ClientVersion\",\n CONTENT_TYPE: \"Content-Type\",\n AUTHORIZATION: \"Authorization\",\n ERROR_MSG: \"x-error-message\",\n ENCRYPTED_PARAM: \"x-encrypted-param\",\n} as const;\n\nexport const WECHAT_PROTOCOL = {\n AUTH_TYPE_VALUE: \"ilink_bot_token\",\n CONTENT_TYPE_JSON: \"application/json\",\n DEFAULT_TIMEOUT_MS: 30000,\n} as const;\n\n// ====== default options =======\n\nexport const DEFAULT_CLIENT_CONFIG: WeChatClientConfig = {\n appId: \"bot\",\n version: \"2.1.1\",\n baseUrl: WECHAT_DEFAULT_BASE_URL,\n botType: \"3\",\n autoDownloadMedia: true,\n};\n\nexport const DEFAULT_CDN_CONFIG: CdnManagerConfig = {\n maxRetry: 3,\n cdnBaseUrl: WECHAT_DEFAULT_CDN_URL,\n apiTimeoutMs: DEFAULT_API_TIMEOUT_MS,\n backoffBaseMs: BACKOFF_BASE_MS,\n};\n\nexport const DEFAULT_LOGIN_OPTIONS: LoginOptions = {\n maxRetries: DEFAULT_LOGIN_MAX_RETRIES,\n pollTimeoutMs: LONG_POLLING_TIMEOUT_MS,\n onQrCode: (qrInfo) => {\n console.log(\"\\n==========================================\");\n console.log(\"请在浏览器中打开以下链接,并使用微信扫码:\");\n console.log(qrInfo.qrcodeUrl);\n console.log(\"==========================================\\n\");\n },\n onStatusChange: (payload) => {\n switch (payload.status) {\n case \"wait\":\n console.log(\"等待登录...\"); // 简易的 loading 动画\n break;\n case \"scaned\":\n console.log(\"\\n👀 已扫码,请在手机微信上点击确认登录...\");\n break;\n case \"confirmed\":\n console.log(\n \"\\n已链接, 但需手动在微信发送第一条消息后, bot 才能正常回复\",\n );\n break;\n case \"scaned_but_redirect\":\n console.log(`\\n[系统] ${payload.message}`);\n break;\n }\n },\n};\n\nexport const DEFAULT_SEND_OPTIONS: SendMessageOptions = {\n contextToken: undefined,\n userId: undefined,\n caption: undefined,\n};\n","// src/types.ts\n\nexport interface WeChatClientConfig {\n /** 你的应用 ID (对应原 config.json 的 ilink_appid) */\n appId: string;\n /** 客户端版本号字符串,如 \"1.0.11\" */\n version: string;\n /** 基础 API 地址,通常为 https://ilinkai.weixin.qq.com */\n baseUrl: string;\n /** 默认机器人类型, 为 \"3\" */\n botType: string;\n /** 机器人的登录凭证,登录前可为空 */\n token?: string;\n /** 收到图片/文件/视频时,是否在后台自动下载解密到内存中?默认 true */\n autoDownloadMedia: boolean;\n /** 用户 ID */\n userId?: string;\n}\n\nexport interface CdnManagerConfig {\n // CDN 物理传输极易受到网络波动影响,固定死 3 次在网络差的环境下可能不够,在极速环境下又显得多余\n maxRetry: number;\n // \"https://cdn.weixin.qq.com\";\n cdnBaseUrl: string;\n apiTimeoutMs: number;\n backoffBaseMs: number;\n}\n\nexport interface SendMessageOptions {\n /** * 上下文 Token。用于在特定的会话上下文中回复消息\n */\n contextToken?: string;\n /** * 接收消息的用户 ID (微信 ID),目前仅对自身账号有效,如果不提供,SDK 会尝试使用登录用户的 ID 作为默认值\n */\n userId?: string;\n /** * 媒体附件的文字说明。\n * 注意:微信不支持图文混合在同一个 Item 中,\n * 如果提供此参数,SDK 会先发送一条文本消息,紧接着发送媒体消息。\n */\n caption?: string;\n}\n\n/** 核心引擎接收的请求参数 */\nexport interface CoreRequestOptions {\n method: \"GET\" | \"POST\";\n /** 纯业务请求体,无需包含 base_info */\n body?: Record<string, any>;\n /** 超时时间 (毫秒) */\n timeoutMs: number;\n}\n\nexport interface QrCodeInfo {\n qrcodeId: string;\n qrcodeUrl: string; // 用于生成图片的原始字符串/链接\n}\n\nexport interface LoginCredentials {\n token: string;\n baseUrl: string;\n accountId: string;\n userId: string;\n}\n\nexport type LogStatusChangePayload =\n | {\n status: LoginStatus.WAIT | LoginStatus.SCANNED | LoginStatus.EXPIRED;\n message: string;\n }\n | {\n status: LoginStatus.REDIRECT;\n message: string;\n redirect_host: string; // 服务器要求重定向的新 host\n }\n | {\n status: LoginStatus.CONFIRMED;\n message: string;\n bot_token: string; // 登录成功后返回的 token\n baseurl?: string; // 登录成功后返回的 baseUrl(如果有的话)\n ilink_bot_id: string; // 登录成功后返回的 bot 账号 ID\n ilink_user_id: string; // 登录成功后返回的用户 ID\n };\n\nexport interface LoginOptions {\n /** 当获取到新二维码时触发 (如果二维码过期会自动刷新并再次触发) */\n onQrCode: (qrInfo: QrCodeInfo) => void;\n /** 当轮询状态改变时触发 (可选) */\n onStatusChange: (payload: LogStatusChangePayload) => void;\n /** 二维码过期后最大重试次数,默认 3 */\n maxRetries: number;\n /** 单次长轮询的超时时间,默认 35000ms */\n pollTimeoutMs: number;\n}\n\nexport interface SendResult {\n clientId: string;\n response: any;\n}\n\n/** 内部流转的 CDN 文件凭据 (CdnManager 产出,MessageManager 消费) */\nexport interface CdnFileTicket {\n filekey: string;\n aeskeyHex: string; // 给 MessageManager 拼 JSON 用的 hex\n aeskeyBuffer: Buffer; // 未来下载解密用的 buffer\n fileSizePlain: number; // 明文大小\n fileSizeCipher: number; // 加密后大小\n encryptedQueryParam: string; // 核心下载参数\n}\n\n/** 统一的 CDN 下载票据 (抹平了图片、文件、视频的差异) */\nexport interface CdnDownloadTicket {\n fullUrl?: string; // 优先级 1\n encryptedQueryParam?: string; // 优先级 2\n aesKeyBase64?: string; // 密钥 (已统一转为 base64 处理好的)\n isPlain: boolean; // 是否是明文传输 (针对某些没有 aes_key 的图片)\n originalFileName?: string; // 针对文件\n}\n\nexport type ItemTypeStr =\n | \"text\"\n | \"image\"\n | \"video\"\n | \"file\"\n | \"voice\"\n | \"unknown\";\n\n// 对外暴露的消息对象结构\nexport interface WeChatIncomingMessage<\n T extends RawMessageItemBase = RawMessageItem,\n> {\n messageId: string;\n seq: number;\n fromUserId: string;\n toUserId: string;\n timestamp: number;\n contextToken: string; // 回复消息时必须带上这个字段\n msgType: T[\"type\"];\n msgTypeStr: ItemTypeStr; // 方便使用者直接判断类型的字符串版本\n /** 纯文本内容 */\n text?: string;\n /** 文件名 */\n fileName?: string;\n /**\n * 获取媒体文件的二进制 Buffer。\n * 如果开启了 autoDownloadMedia,此方法瞬间返回内存中的 Buffer。\n * 如果关闭了,此方法会发起网络请求下载并解密,然后缓存。\n */\n getBuffer?: () => Promise<Buffer | null>;\n /** 获取语音消息的二进制 Buffer, 这是原始的 SILK 编码 */\n getVoiceBuffer?: () => Promise<Buffer | null>;\n /**\n * 快捷方法:将媒体文件保存到本地磁盘。\n * @param savePath 指定绝对或相对路径\n */\n saveToFile?: (savePath: string) => Promise<string>;\n /** 原始底层票据 */\n mediaTicket?: CdnDownloadTicket;\n /** 消息在同一 seq 中的索引,方便开发者处理多 Item 的情况 */\n index: number;\n /** 原始消息载荷,给高级玩家使用 */\n raw: RawMessage<T>;\n}\n\nexport const enum MessageType {\n NONE = 0,\n USER = 1,\n BOT = 2,\n}\n\nexport const enum MessageItemType {\n NONE = 0,\n TEXT = 1,\n IMAGE = 2,\n VOICE = 3,\n FILE = 4,\n VIDEO = 5,\n}\n\nexport const enum MessageState {\n NEW = 0,\n GENERATING = 1,\n FINISH = 2,\n}\n\nexport const enum UploadMediaType {\n IMAGE = 1,\n VIDEO = 2,\n FILE = 3,\n VOICE = 4,\n}\n\nexport interface PollingResult {\n ret?: number | -2;\n errcode?: number;\n errmsg?: string;\n get_updates_buf?: string;\n msgs?: RawMessage[];\n}\n\nexport interface RawMessage<T extends RawMessageItemBase = RawMessageItem> {\n seq: number;\n message_id: number;\n from_user_id: string;\n to_user_id: string;\n client_id: string;\n create_time_ms: number;\n update_time_ms: number;\n delete_time_ms: number | 0;\n session_id: string | \"\";\n group_id: string | \"\";\n message_type: MessageType;\n message_state: MessageState;\n item_list: T[];\n context_token: string;\n}\n\nexport type RawMessageItem =\n | RawMessageTextItem\n | RawMessageFileItem\n | RawMessageImageItem\n | RawMessageVideoItem\n | RawMessageVoiceItem;\n\nexport interface RawMessageItemBase {\n type: MessageItemType;\n create_time_ms: number;\n update_time_ms: number;\n is_completed: boolean;\n}\n\nexport interface RawMessageTextItem extends RawMessageItemBase {\n type: MessageItemType.TEXT;\n text_item: { text: string };\n}\n\nexport interface RawMessageFileItem extends RawMessageItemBase {\n type: MessageItemType.FILE;\n file_item: {\n media: RawMessageMedia;\n file_name: string;\n md5: string;\n len: string;\n };\n}\n\nexport interface RawMessageImageItem extends RawMessageItemBase {\n type: MessageItemType.IMAGE;\n image_item: {\n url: string;\n aeskey: string;\n media: RawMessageMedia;\n mid_size: number;\n thumb_size: number;\n thumb_height: number;\n thumb_width: number;\n hd_size: number;\n };\n}\n\nexport interface RawMessageVideoItem extends RawMessageItemBase {\n type: MessageItemType.VIDEO;\n video_item: {\n media: RawMessageMedia;\n video_size: number;\n play_length: number;\n video_md5: string;\n thumb_media: RawMessageMedia;\n thumb_size: number;\n thumb_height: number;\n thumb_width: number;\n };\n}\n\nexport interface RawMessageVoiceItem extends RawMessageItemBase {\n type: MessageItemType.VOICE;\n voice_item: {\n media: RawMessageMedia;\n encode_type: number | 4;\n bits_per_sample: number | 16;\n sample_rate: number | 16000;\n playtime: number;\n text: string | \"\";\n };\n}\n\nexport interface RawMessageMedia {\n encrypt_query_param: string;\n aes_key: string;\n full_url: string;\n}\n\nexport interface WeChatApiEventMap {\n login: [credentials: LoginCredentials];\n message: [msg: WeChatIncomingMessage];\n text: [msg: WeChatIncomingMessage<RawMessageTextItem>];\n file: [msg: WeChatIncomingMessage<RawMessageFileItem>];\n image: [msg: WeChatIncomingMessage<RawMessageImageItem>];\n video: [msg: WeChatIncomingMessage<RawMessageVideoItem>];\n voice: [msg: WeChatIncomingMessage<RawMessageVoiceItem>];\n error: [error: Error];\n}\n\nexport const enum LoginStatus {\n WAIT = \"wait\",\n SCANNED = \"scaned\", // 保持和微信接口错别字一致\n REDIRECT = \"scaned_but_redirect\",\n CONFIRMED = \"confirmed\",\n EXPIRED = \"expired\",\n}\n","// src/core/utils.ts\nimport {\n type ItemTypeStr,\n MessageItemType,\n type RawMessageItem,\n} from \"../types.ts\";\n\nexport function mergeObjects<T>(ref: T, ...sources: Partial<T>[]): T {\n const target = { ...ref };\n for (const source of sources) {\n for (const key in source) {\n if (source[key] !== undefined) {\n target[key] = source[key] as T[Extract<keyof T, string>];\n }\n }\n }\n return target;\n}\n\nexport function getItemType(item: MessageItemType): ItemTypeStr {\n switch (item) {\n case MessageItemType.TEXT:\n return \"text\";\n case MessageItemType.IMAGE:\n return \"image\";\n case MessageItemType.VIDEO:\n return \"video\";\n case MessageItemType.FILE:\n return \"file\";\n case MessageItemType.VOICE:\n return \"voice\";\n default:\n return \"unknown\";\n }\n}\n\n/** 将 \"2.1.1\" 等版本号转换为 API 要求的数字位运算格式 */\nexport function buildClientVersion(version: string): number {\n const parts = version.split(\".\").map((p) => parseInt(p, 10));\n const major = parts[0] || 0;\n const minor = parts[1] || 0;\n const patch = parts[2] || 0;\n return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);\n}\n\nexport function getFileImageItem(item: RawMessageItem) {\n if (\"file_item\" in item) return item.file_item;\n if (\"image_item\" in item) return item.image_item;\n if (\"voice_item\" in item) return item.voice_item;\n if (\"video_item\" in item) return item.video_item;\n}\n\nexport function isItemWithMedia(\n item: MessageItemType,\n): item is\n | MessageItemType.FILE\n | MessageItemType.IMAGE\n | MessageItemType.VOICE\n | MessageItemType.VIDEO {\n switch (item) {\n case MessageItemType.IMAGE:\n case MessageItemType.VOICE:\n case MessageItemType.FILE:\n case MessageItemType.VIDEO:\n return true;\n case MessageItemType.NONE:\n case MessageItemType.TEXT:\n default:\n return false;\n }\n}\n","// src/core/WeChatCore.ts\nimport { CryptoUtils } from \"./CryptoUtils.ts\";\nimport { buildClientVersion, mergeObjects } from \"./utils.ts\";\nimport type { CoreRequestOptions, WeChatClientConfig } from \"../types.ts\";\nimport {\n DEFAULT_CLIENT_CONFIG,\n WECHAT_HTTP_HEADERS,\n WECHAT_PROTOCOL,\n} from \"../constants.ts\";\n\nexport class WeChatCore {\n private config: WeChatClientConfig;\n private clientVersionInt: number;\n\n constructor(config: WeChatClientConfig) {\n this.config = mergeObjects(DEFAULT_CLIENT_CONFIG, config);\n // 预计算版本号整数,避免每次发请求都算一遍\n this.clientVersionInt = buildClientVersion(config.version);\n }\n\n // ==========================================\n // 状态修改器 (Mutators)\n // ==========================================\n\n /** 设置或更新登录凭证 */\n public setToken(token: string): void {\n this.config.token = token;\n }\n\n /** 更新基础网关 URL (用于处理 IDC 重定向) */\n public setBaseUrl(baseUrl: string): void {\n this.config.baseUrl = baseUrl;\n }\n\n public setUserId(userId: string): void {\n this.config.userId = userId;\n }\n\n /** 获取当前的 BaseUrl (供某些需要拼接绝对路径的特殊场景使用) */\n public getBaseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** 获取当前的机器人类型 */\n public getBotType(): string {\n return this.config.botType;\n }\n\n /** 获取当前的用户 ID */\n public getUserId(): string | undefined {\n return this.config.userId;\n }\n\n // ==========================================\n // 核心请求发射器\n // ==========================================\n\n /**\n * 统一的 HTTP 请求方法\n * @param endpoint 接口路径,例如 \"ilink/bot/sendmessage\"\n * @param options 请求配置 (方法、请求体、超时设置)\n */\n public async request<T>(\n endpoint: string,\n options: CoreRequestOptions,\n ): Promise<T> {\n // 1. URL 拼接处理\n const base = this.config.baseUrl.endsWith(\"/\")\n ? this.config.baseUrl\n : `${this.config.baseUrl}/`;\n const url = new URL(endpoint, base);\n\n // 2. 构造最终的请求体 (如果是 POST,自动注入 base_info)\n let finalBodyString: string | undefined = undefined;\n\n if (options.method === \"POST\" && options.body) {\n // 浅拷贝一层,防止污染调用方传入的原始对象\n const payload = { ...options.body };\n payload.base_info = { channel_version: this.config.version };\n finalBodyString = JSON.stringify(payload);\n } else if (options.method === \"POST\" && !options.body) {\n // 即使没有具体业务参数,POST 请求通常也需要 base_info\n finalBodyString = JSON.stringify({\n base_info: { channel_version: this.config.version },\n });\n }\n\n // 3. 构造请求头\n const headers = this.buildHeaders(finalBodyString);\n\n // 4. 设置超时控制器\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);\n\n // 5. 执行请求\n try {\n const response = await fetch(url.toString(), {\n method: options.method,\n headers: headers,\n body: finalBodyString,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n const rawText = await response.text();\n\n // 非 200 状态码直接抛出异常,交给上层业务处理\n if (!response.ok) {\n throw new Error(\n `WeChat API Error [${options.method} ${endpoint}] HTTP ${response.status}: ${rawText}`,\n );\n }\n\n // 如果有返回值则解析,无返回值(如 sendmessage)则返回空对象\n return rawText ? JSON.parse(rawText) as T : ({} as T);\n } catch (err) {\n clearTimeout(timeoutId);\n // 将底层的网络异常原样抛出,如果是 AbortError (超时),上层(如长轮询)可以特别捕获它\n throw err;\n }\n }\n\n // ==========================================\n // 私有辅助方法\n // ==========================================\n\n /** 构造标准请求头 */\n private buildHeaders(bodyString?: string): Record<string, string> {\n const headers: Record<string, string> = {\n [WECHAT_HTTP_HEADERS.CONTENT_TYPE]: WECHAT_PROTOCOL.CONTENT_TYPE_JSON,\n [WECHAT_HTTP_HEADERS.AUTH_TYPE]: WECHAT_PROTOCOL.AUTH_TYPE_VALUE,\n [WECHAT_HTTP_HEADERS.UIN]: CryptoUtils.randomWechatUin(),\n [WECHAT_HTTP_HEADERS.APP_ID]: this.config.appId,\n [WECHAT_HTTP_HEADERS.CLIENT_VERSION]: String(this.clientVersionInt),\n };\n\n if (bodyString) {\n headers[\"Content-Length\"] = String(\n Buffer.byteLength(bodyString, \"utf-8\"),\n );\n }\n\n if (this.config.token?.trim()) {\n headers[\"Authorization\"] = `Bearer ${this.config.token.trim()}`;\n }\n\n return headers;\n }\n}\n","// src/managers/AuthManager.ts\nimport {\n BACKOFF_BASE_MS,\n DEFAULT_LOGIN_MAX_RETRIES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n LONG_POLLING_TIMEOUT_MS,\n QR_CODE_EXPIRATION_MS,\n} from \"../constants.ts\";\nimport { WeChatCore } from \"../core/WeChatCore.ts\";\nimport {\n type LoginCredentials,\n type LoginOptions,\n LoginStatus,\n type QrCodeInfo,\n} from \"../types.ts\";\n\nexport class AuthManager {\n private core: WeChatCore;\n\n constructor(core: WeChatCore) {\n this.core = core;\n }\n\n // ==========================================\n // 细粒度 API: 第一步 - 获取二维码\n // ==========================================\n public async getQrCode(): Promise<QrCodeInfo> {\n // 强制使用默认的登录网关获取二维码\n this.core.setBaseUrl(this.core.getBaseUrl()); // 触发内部状态更新,确保使用默认 BaseUrl\n\n const response = await this.core.request<any>(\n `ilink/bot/get_bot_qrcode?bot_type=${this.core.getBotType()}`,\n { method: \"GET\", timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS },\n );\n\n return {\n qrcodeId: response.qrcode,\n qrcodeUrl: response.qrcode_img_content,\n };\n }\n\n // ==========================================\n // 细粒度 API: 第二步 - 单次轮询状态\n // ==========================================\n public async pollLoginStatus(\n qrcodeId: string,\n timeoutMs = LONG_POLLING_TIMEOUT_MS,\n ): Promise<{ status: LoginStatus; data?: any }> {\n try {\n const response = await this.core.request<any>(\n `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcodeId)}`,\n { method: \"GET\", timeoutMs },\n );\n return { status: response.status, data: response };\n } catch (err: any) {\n // 区分普通网络错误和长轮询的正常超时 (AbortError)\n if (err.name === \"AbortError\") {\n return { status: LoginStatus.WAIT }; // 超时视为继续等待\n }\n throw err;\n }\n }\n\n // ==========================================\n // 粗粒度 API: 一键自动托管登录\n // ==========================================\n public async login(options: LoginOptions): Promise<LoginCredentials> {\n const maxRetries = options.maxRetries ?? DEFAULT_LOGIN_MAX_RETRIES;\n let currentRetry = 0;\n\n while (currentRetry <= maxRetries) {\n // 1. 获取二维码\n const qrInfo = await this.getQrCode();\n options.onQrCode(qrInfo);\n\n // 2. 开始持续轮询该二维码\n // 设置一个总超时,防止死循环 (比如 5 分钟二维码一定会过期)\n const qrDeadline = Date.now() + QR_CODE_EXPIRATION_MS;\n\n while (Date.now() < qrDeadline) {\n const { status, data } = await this.pollLoginStatus(\n qrInfo.qrcodeId,\n options.pollTimeoutMs,\n );\n\n switch (status) {\n case LoginStatus.WAIT:\n options.onStatusChange?.({\n status: LoginStatus.WAIT,\n message: \"等待扫码...\",\n });\n break;\n\n case LoginStatus.SCANNED:\n options.onStatusChange?.({\n status: LoginStatus.SCANNED,\n message: \"已扫码,请在手机端确认...\",\n });\n break;\n\n case LoginStatus.REDIRECT:\n if (data.redirect_host) {\n const newBaseUrl = `https://${data.redirect_host}`;\n this.core.setBaseUrl(newBaseUrl); // 关键:更新底层引擎的 BaseUrl\n options.onStatusChange?.({\n status: LoginStatus.REDIRECT,\n message: `服务器要求重定向至: ${newBaseUrl}`,\n redirect_host: data.redirect_host,\n });\n }\n break;\n\n case LoginStatus.CONFIRMED:\n options.onStatusChange?.({\n status: LoginStatus.CONFIRMED,\n message: \"登录成功!\",\n bot_token: data.bot_token,\n baseurl: data.baseurl,\n ilink_bot_id: data.ilink_bot_id,\n ilink_user_id: data.ilink_user_id,\n });\n\n // 关键:自动将获取到的凭据注入到底层引擎中\n this.core.setToken(data.bot_token);\n this.core.setUserId(data.ilink_user_id);\n if (data.baseurl) {\n this.core.setBaseUrl(data.baseurl);\n }\n\n // 返回凭据给开发者,以便他们持久化存储\n return {\n token: data.bot_token,\n baseUrl: data.baseurl || this.core.getBaseUrl(),\n accountId: data.ilink_bot_id,\n userId: data.ilink_user_id,\n };\n\n case LoginStatus.EXPIRED:\n options.onStatusChange?.({\n status: LoginStatus.EXPIRED,\n message: \"二维码已过期,正在重新获取...\",\n });\n // 跳出内层轮询,触发外层循环重新获取二维码\n break;\n\n default:\n // 未知状态,稍作延迟后继续尝试\n await new Promise((res) => setTimeout(res, BACKOFF_BASE_MS));\n }\n\n if (status === LoginStatus.EXPIRED) {\n break; // 跳出内层 while 循环\n }\n\n // 防御性延迟,避免请求过于频繁\n await new Promise((res) => setTimeout(res, BACKOFF_BASE_MS));\n }\n\n currentRetry++;\n }\n\n throw new Error(\n `登录失败:二维码已过期并超过最大重试次数 (${maxRetries}次)。`,\n );\n }\n}\n","// src/managers/MessageManager.ts\n\nimport { readFile } from \"node:fs/promises\";\nimport { basename } from \"node:path\";\n\nimport { mergeObjects } from \"../core/utils.ts\";\nimport {\n DEFAULT_API_TIMEOUT_MS,\n DEFAULT_SEND_OPTIONS,\n WECHAT_CDN_ENCRYPT_TYPE,\n} from \"../constants.ts\";\nimport { CryptoUtils } from \"../core/CryptoUtils.ts\";\nimport {\n MessageItemType,\n MessageState,\n MessageType,\n type SendMessageOptions,\n type SendResult,\n UploadMediaType,\n} from \"../types.ts\";\nimport type { WeChatCore } from \"../core/WeChatCore.ts\";\nimport type { CdnManager } from \"./CdnManager.ts\";\n\nexport class MessageManager {\n private core: WeChatCore;\n private cdn: CdnManager;\n\n constructor(core: WeChatCore, cdn: CdnManager) {\n this.core = core;\n this.cdn = cdn;\n }\n\n // ==========================================\n // 公开 API: 发送纯文本\n // ==========================================\n\n public async sendText(\n text: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const textItem = {\n type: MessageItemType.TEXT,\n text_item: { text },\n };\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendRawItem(\n textItem,\n mergedOptions.userId!,\n mergedOptions.contextToken,\n );\n }\n\n // ==========================================\n // 公开 API: 发送媒体文件\n // ==========================================\n\n public async sendImage(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.IMAGE,\n mergedOptions,\n );\n }\n\n public async sendVideo(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.VIDEO,\n mergedOptions,\n );\n }\n\n /** - 仅允许发送文件, 发送**图片或视频**会导致发送失败!\n * - 发送图片和视频应使用 `sendImage` 或 `sendVideo` */\n public async sendFile(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.FILE,\n mergedOptions,\n );\n }\n\n // ==========================================\n // 私有核心流水线\n // ==========================================\n\n /**\n * 统一的媒体发送工作流:读取本地文件 -> 上传 CDN -> (可选发送 Caption) -> 发送媒体消息\n */\n private async _sendMediaWorkflow(\n filePath: string,\n mediaType: UploadMediaType,\n options: SendMessageOptions,\n ): Promise<SendResult> {\n // 1. 读取本地文件为 Buffer\n const buffer = await readFile(filePath);\n const fileName = basename(filePath);\n\n // 2. 调用 CdnManager 上传至微信服务器,获取票据\n const ticket = await this.cdn.uploadBuffer(\n buffer,\n options.userId!,\n mediaType,\n );\n\n // 3. 如果用户传了 caption,先发送一条纯文本消息\n if (options.caption) {\n await this.sendText(options.caption, {\n contextToken: options.contextToken,\n });\n }\n\n // 4. 组装媒体 Item 载荷\n const mediaObj = {\n encrypt_query_param: ticket.encryptedQueryParam,\n aes_key: Buffer.from(ticket.aeskeyHex).toString(\"base64\"), // JSON 中要求 base64 格式\n encrypt_type: WECHAT_CDN_ENCRYPT_TYPE,\n };\n\n let messageItem: any = {};\n\n switch (mediaType) {\n case UploadMediaType.IMAGE:\n messageItem = {\n type: MessageItemType.IMAGE,\n image_item: { media: mediaObj, mid_size: ticket.fileSizeCipher },\n };\n break;\n case UploadMediaType.VIDEO:\n messageItem = {\n type: MessageItemType.VIDEO,\n video_item: { media: mediaObj, video_size: ticket.fileSizeCipher },\n };\n break;\n case UploadMediaType.FILE:\n messageItem = {\n type: MessageItemType.FILE,\n file_item: {\n media: mediaObj,\n file_name: fileName,\n len: String(ticket.fileSizePlain),\n }, // 注意 len 是明文大小的字符串\n };\n break;\n default:\n throw new Error(`[MessageManager] 不支持的媒体类型: ${mediaType}`);\n }\n\n // 5. 将组装好的媒体 Item 发送出去\n return this._sendRawItem(\n messageItem,\n options.userId!,\n options.contextToken,\n );\n }\n\n /**\n * 最底层的发包函数,将组装好的 Item 包装成完整的微信请求体并 POST\n */\n private async _sendRawItem(\n itemObj: any,\n userId: string,\n contextToken?: string,\n ): Promise<SendResult> {\n const clientId = CryptoUtils.generateClientId();\n\n const requestBody: any = {\n msg: {\n from_user_id: \"\", // 留空,微信网关会自动通过 Bearer Token 识别机器人身份\n to_user_id: userId,\n client_id: clientId,\n message_type: MessageType.BOT,\n message_state: MessageState.FINISH,\n context_token: contextToken,\n item_list: [itemObj],\n },\n };\n\n const response = await this.core.request(\"ilink/bot/sendmessage\", {\n method: \"POST\",\n body: requestBody,\n timeoutMs: DEFAULT_API_TIMEOUT_MS,\n });\n\n return { clientId, response };\n }\n}\n","// src/managers/CdnManager.ts\nimport { WeChatCore } from \"../core/WeChatCore.ts\";\nimport { CryptoUtils } from \"../core/CryptoUtils.ts\";\nimport {\n AES_BLOCK_SIZE,\n DEFAULT_CDN_CONFIG,\n WECHAT_HTTP_HEADERS,\n} from \"../constants.ts\";\nimport type {\n CdnDownloadTicket,\n CdnFileTicket,\n CdnManagerConfig,\n UploadMediaType,\n} from \"../types.ts\";\nimport { mergeObjects } from \"../core/utils.ts\";\n\nexport class CdnManager {\n private core: WeChatCore;\n private config: CdnManagerConfig;\n\n constructor(\n core: WeChatCore,\n config: Partial<CdnManagerConfig> = DEFAULT_CDN_CONFIG,\n ) {\n this.core = core;\n this.config = mergeObjects(DEFAULT_CDN_CONFIG, config);\n }\n\n // ==========================================\n // 核心流水线:上传二进制流到 CDN\n // ==========================================\n\n /**\n * 将任意 Buffer 上传至微信 CDN,并返回发送消息所需的凭证\n * @param buffer 文件的二进制原始数据\n * @param toUserId 接收人的微信 ID (CDN 强制要求绑定)\n * @param mediaType 媒体类型 (IMAGE, VIDEO, FILE, VOICE)\n */\n public async uploadBuffer(\n buffer: Buffer,\n toUserId: string,\n mediaType: UploadMediaType,\n ): Promise<CdnFileTicket> {\n // --------------------------------------------------\n // Step 1: 准备元数据与加密密钥\n // --------------------------------------------------\n const rawsize = buffer.length;\n const rawfilemd5 = CryptoUtils.md5(buffer);\n const filesize = CryptoUtils.getPaddedSize(rawsize); // AES 加密后的对齐大小\n\n // 微信要求 aeskey 和 filekey 都是 16 字节\n const filekey = CryptoUtils.generateRandomKey(AES_BLOCK_SIZE).toString(\n \"hex\",\n );\n const aeskeyBuffer = CryptoUtils.generateRandomKey(AES_BLOCK_SIZE);\n const aeskeyHex = aeskeyBuffer.toString(\"hex\");\n\n // --------------------------------------------------\n // Step 2: 访问控制面,申请上传门票 (Upload URL)\n // --------------------------------------------------\n const uploadUrlResp = await this.core.request<any>(\n \"ilink/bot/getuploadurl\",\n {\n method: \"POST\",\n body: {\n filekey,\n media_type: mediaType,\n to_user_id: toUserId,\n rawsize,\n rawfilemd5,\n filesize,\n no_need_thumb: true, // 📝 [待重构]: 暂时忽略缩略图逻辑,全部设为 true\n aeskey: aeskeyHex,\n },\n timeoutMs: this.config.apiTimeoutMs,\n },\n );\n\n // 解析出真实的 CDN 地址\n // 微信有时候返回完整的 URL (upload_full_url),有时候只返回 query 参数 (upload_param)\n let cdnUrl = \"\";\n if (uploadUrlResp.upload_full_url?.trim()) {\n cdnUrl = uploadUrlResp.upload_full_url.trim();\n } else if (uploadUrlResp.upload_param) {\n cdnUrl = `${this.config.cdnBaseUrl}/upload?encrypted_query_param=${\n encodeURIComponent(uploadUrlResp.upload_param)\n }&filekey=${encodeURIComponent(filekey)}`;\n } else {\n throw new Error(\n `[CdnManager] 获取上传地址失败,API 响应缺少 full_url 或 upload_param`,\n );\n }\n\n // --------------------------------------------------\n // Step 3: 数据面加密\n // --------------------------------------------------\n const ciphertextBuffer = CryptoUtils.aesEcbEncrypt(buffer, aeskeyBuffer);\n\n // --------------------------------------------------\n // Step 4: 物理传输至 CDN (带重试机制)\n // --------------------------------------------------\n const encryptedQueryParam = await this.postToCdn(ciphertextBuffer, cdnUrl);\n\n // --------------------------------------------------\n // Step 5: 组装凭证交付给 MessageManager\n // --------------------------------------------------\n return {\n filekey,\n aeskeyHex,\n aeskeyBuffer,\n fileSizePlain: rawsize,\n fileSizeCipher: filesize,\n encryptedQueryParam,\n };\n }\n\n // ==========================================\n // 私有发包器:专门应对奇葩的 CDN 接口\n // ==========================================\n\n /**\n * 将密文 POST 到 CDN,并从 Response Header 中抠出下载参数。\n * 包含 4xx 直接报错、5xx 重试的逻辑。\n */\n private async postToCdn(ciphertext: Buffer, cdnUrl: string): Promise<string> {\n const MAX_RETRIES = this.config.maxRetry;\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(cdnUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/octet-stream\" },\n body: new Uint8Array(ciphertext),\n });\n\n // 1. 客户端错误 (4xx):比如鉴权失败、参数错误,重试也没用,直接抛出\n if (res.status >= 400 && res.status < 500) {\n const errMsg = res.headers.get(WECHAT_HTTP_HEADERS.ERROR_MSG) ??\n (await res.text());\n throw new Error(`[CdnClientError] HTTP ${res.status}: ${errMsg}`);\n }\n\n // 2. 服务端错误 (5xx):比如网关超时,尝试重试\n if (res.status !== 200) {\n const errMsg = res.headers.get(WECHAT_HTTP_HEADERS.ERROR_MSG) ??\n `HTTP ${res.status}`;\n throw new Error(`[CdnServerError]: ${errMsg}`);\n }\n\n // 3. 成功!从 Header 中寻找那把“钥匙”\n const downloadParam = res.headers.get(\n WECHAT_HTTP_HEADERS.ENCRYPTED_PARAM,\n );\n if (!downloadParam) {\n throw new Error(\n `[CdnManager] 上传成功,但响应头中缺少 ${WECHAT_HTTP_HEADERS.ENCRYPTED_PARAM}`,\n );\n }\n\n return downloadParam;\n } catch (err: any) {\n lastError = err;\n\n // 如果是 4xx 错误,立刻中断循环,不进行无意义的重试\n if (err.message && err.message.includes(\"[CdnClientError]\")) {\n throw err;\n }\n\n // 否则等待一会儿继续重试 (简易退避)\n if (attempt < MAX_RETRIES) {\n await new Promise((resolve) =>\n setTimeout(resolve, this.config.backoffBaseMs * attempt)\n );\n }\n }\n }\n\n throw new Error(\n `[CdnManager] CDN 上传失败,已重试 ${MAX_RETRIES} 次。最后错误: ${\n (lastError as Error)?.message\n }`,\n );\n }\n\n // ==========================================\n // 核心流水线:从 CDN 下载并解密\n // ==========================================\n\n /**\n * 唯一对内暴露的下载引擎\n * 负责拉取字节流并根据协议规范进行解密\n */\n public async downloadBuffer(ticket: CdnDownloadTicket): Promise<Buffer> {\n // 1. 确定最终的下载 URL (优先使用 fullUrl)\n let url = \"\";\n if (ticket.fullUrl) {\n url = ticket.fullUrl;\n } else if (ticket.encryptedQueryParam) {\n // 如果没有 fullUrl,就按照规则拼接\n url = `${this.config.cdnBaseUrl}/download?encrypted_query_param=${\n encodeURIComponent(ticket.encryptedQueryParam)\n }`;\n } else {\n throw new Error(\n \"[CdnManager] 下载失败:缺少 fullUrl 和 encryptedQueryParam\",\n );\n }\n\n // 2. 发起网络请求,拉取原始的 ArrayBuffer\n const encryptedBuffer = await this.fetchCdnBytes(url);\n\n // 3. 明文回退:如果协议表明这是明文传输(没有密钥),直接返回\n if (ticket.isPlain || !ticket.aesKeyBase64) {\n return encryptedBuffer;\n }\n\n // 4. 解析变态的微信 AES 密钥\n const aesKey = this.parseWechatAesKey(ticket.aesKeyBase64);\n\n // 5. 使用密码学工具箱进行 AES-128-ECB 解密\n return CryptoUtils.aesEcbDecrypt(encryptedBuffer, aesKey);\n }\n\n // ==========================================\n // 私有辅助方法\n // ==========================================\n\n /**\n * 处理微信极其不一致的密钥编码:\n * 情况A: base64( raw 16 bytes )\n * 情况B: base64( hex string of 16 bytes )\n */\n private parseWechatAesKey(aesKeyBase64: string): Buffer {\n const decoded = Buffer.from(aesKeyBase64, \"base64\");\n\n // 如果解密出来正好是 16 字节的二进制,直接用\n if (decoded.length === 16) {\n return decoded;\n }\n\n // 如果解密出来是 32 个字符,并且全是十六进制字符,说明它被二次 Hex 编码了\n if (\n decoded.length === 32 &&\n /^[0-9a-fA-F]{32}$/i.test(decoded.toString(\"ascii\"))\n ) {\n return Buffer.from(decoded.toString(\"ascii\"), \"hex\");\n }\n\n throw new Error(\n `[CdnManager] 无法解析的 AES 密钥格式 (Base64=\"${aesKeyBase64}\")`,\n );\n }\n\n /**\n * 原生 fetch 拉取二进制字节流\n */\n private async fetchCdnBytes(url: string): Promise<Buffer> {\n const res = await fetch(url);\n if (!res.ok) {\n const body = await res.text().catch(() => \"(unreadable)\");\n throw new Error(\n `[CdnManager] CDN 下载网络错误 HTTP ${res.status}: ${body}`,\n );\n }\n return Buffer.from(await res.arrayBuffer());\n }\n}\n","// src/core/SilkConverter.ts\n\nimport { WECHAT_SILK_SAMPLE_RATE } from \"../constants.ts\";\n\n/**\n * Wrap raw pcm_s16le bytes in a WAV container.\n * Mono channel, 16-bit signed little-endian.\n */\nfunction pcmBytesToWav(pcm: Uint8Array, sampleRate: number): Buffer {\n const pcmBytes = pcm.byteLength;\n const totalSize = 44 + pcmBytes;\n const buf = Buffer.allocUnsafe(totalSize);\n let offset = 0;\n\n buf.write(\"RIFF\", offset);\n offset += 4;\n buf.writeUInt32LE(totalSize - 8, offset);\n offset += 4;\n buf.write(\"WAVE\", offset);\n offset += 4;\n\n buf.write(\"fmt \", offset);\n offset += 4;\n buf.writeUInt32LE(16, offset);\n offset += 4; // fmt chunk size\n buf.writeUInt16LE(1, offset);\n offset += 2; // PCM format\n buf.writeUInt16LE(1, offset);\n offset += 2; // mono\n buf.writeUInt32LE(sampleRate, offset);\n offset += 4;\n buf.writeUInt32LE(sampleRate * 2, offset);\n offset += 4; // byte rate (mono 16-bit)\n buf.writeUInt16LE(2, offset);\n offset += 2; // block align\n buf.writeUInt16LE(16, offset);\n offset += 2; // bits per sample\n\n buf.write(\"data\", offset);\n offset += 4;\n buf.writeUInt32LE(pcmBytes, offset);\n offset += 4;\n\n Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).copy(buf, offset);\n\n return buf;\n}\n\nexport async function silkToWav(\n silkBuffer: Buffer,\n sampleRate: number = WECHAT_SILK_SAMPLE_RATE,\n): Promise<Buffer | null> {\n try {\n const { decode } = await import(\"silk-wasm\");\n const result = await decode(silkBuffer, sampleRate);\n const wav = pcmBytesToWav(result.data, sampleRate);\n return wav;\n } catch (err) {\n return null;\n }\n}\n","// src/managers/MessageParser.ts\n\nimport type { CdnManager } from \"./CdnManager.ts\";\nimport {\n type CdnDownloadTicket,\n MessageItemType,\n type RawMessage,\n type RawMessageItem,\n type WeChatClientConfig,\n type WeChatIncomingMessage,\n} from \"../types.ts\";\nimport { silkToWav } from \"../core/SilkConverter.ts\";\nimport { writeFile } from \"node:fs/promises\";\nimport {\n getFileImageItem,\n getItemType,\n isItemWithMedia,\n mergeObjects,\n} from \"../core/utils.ts\";\n\nexport class MessageParser {\n private cdn: CdnManager;\n private config: WeChatClientConfig;\n\n constructor(cdn: CdnManager, config: WeChatClientConfig) {\n this.cdn = cdn;\n this.config = config;\n }\n\n /**\n * 将微信底层的复杂 JSON 扁平化为开发者友好的对象\n */\n public async parse(raw: RawMessage): Promise<WeChatIncomingMessage[] | null> {\n // 过滤掉系统消息或无内容的空包\n if (!raw.item_list || raw.item_list.length === 0) return [];\n\n const results: WeChatIncomingMessage[] = [];\n\n // 提取公共的信封数据\n const shared_data: WeChatIncomingMessage = {\n messageId: String(raw.message_id),\n seq: raw.seq,\n fromUserId: raw.from_user_id,\n toUserId: raw.to_user_id,\n timestamp: raw.create_time_ms,\n contextToken: raw.context_token,\n msgType: MessageItemType.TEXT,\n msgTypeStr: \"unknown\",\n index: 0,\n raw: raw,\n };\n\n for (let i = 0; i < raw.item_list.length; i++) {\n const item = raw.item_list[i];\n const msgCopy = mergeObjects(shared_data, {\n index: i,\n msgType: item.type,\n msgTypeStr: getItemType(item.type),\n });\n\n // 1. 先处理文本消息,提取纯文本内容\n if (item.type === MessageItemType.TEXT) {\n msgCopy.text = item.text_item.text;\n }\n if (!isItemWithMedia(item.type)) {\n results.push(msgCopy);\n continue; // 如果不是媒体消息,直接进入下一轮循环\n }\n\n // 2. 媒体消息需要提取下载票据,并提供 getBuffer 方法\n let cachedBuffer: Buffer | null = null;\n const ticket = this._extractDownloadTicket(item);\n msgCopy.fileName = ticket?.originalFileName;\n msgCopy.mediaTicket = ticket;\n msgCopy.getBuffer = async () => {\n if (cachedBuffer) return cachedBuffer;\n if (!ticket) return null;\n // 调用 CDN 管理器下载\n cachedBuffer = await this.cdn.downloadBuffer(ticket);\n return cachedBuffer;\n };\n msgCopy.saveToFile = async (savePath: string) => {\n const buf = await msgCopy.getBuffer!();\n if (!buf) throw new Error(\"无媒体内容可保存\");\n await writeFile(savePath, buf);\n return savePath;\n };\n\n // 3. 语音消息需要特殊处理,提供一个额外的方法获取原始 SILK 编码的 Buffer\n if (msgCopy.msgType === MessageItemType.VOICE) {\n msgCopy.getVoiceBuffer = msgCopy.getBuffer; // 语音消息的原始 Buffer 是 SILK 编码\n let pcmCachedBuffer: Buffer | null = null;\n msgCopy.getBuffer = async () => {\n if (pcmCachedBuffer) return pcmCachedBuffer;\n const silk_buffer = await msgCopy.getVoiceBuffer!();\n if (!silk_buffer) return null;\n pcmCachedBuffer = await silkToWav(silk_buffer);\n return pcmCachedBuffer;\n };\n }\n\n // 4. 如果开启了自动下载,就在抛出事件前,在后台先下载好!\n if (this.config.autoDownloadMedia !== false && ticket) {\n try {\n msgCopy.getBuffer(); // 不 await,后台下载,事件照常触发\n } catch (err: any) {\n console.error(`自动下载媒体失败: ${err.message}`);\n }\n }\n results.push(msgCopy);\n }\n return results;\n }\n\n /**\n * 从原始 JSON 中提取标准化的 CDN 下载票据\n */\n private _extractDownloadTicket(\n item: RawMessageItem,\n ): CdnDownloadTicket | undefined {\n const file_item = getFileImageItem(item);\n if (!file_item || !file_item.media) return undefined;\n const media = file_item.media;\n\n // 如果连基本的下载参数都没有,直接放弃\n if (!media.encrypt_query_param && !media.full_url) return undefined;\n\n // 2. 抹平 AES 密钥的特权差异\n let aesKeyBase64 = media.aes_key;\n\n // ⚠️ 协议特例:图片类型有时会把密钥放在外层,而且是 Hex 格式\n if (\n item.type === MessageItemType.IMAGE && \"aeskey\" in file_item &&\n file_item.aeskey\n ) {\n // 统一转成 Base64,方便 CdnManager 统一处理\n aesKeyBase64 = Buffer.from(file_item.aeskey, \"hex\").toString(\"base64\");\n }\n\n // 3. 判断是否为明文传输 (某些情况下微信图片没有 aes_key)\n const isPlain = !aesKeyBase64;\n\n return {\n fullUrl: media.full_url,\n encryptedQueryParam: media.encrypt_query_param,\n aesKeyBase64,\n isPlain,\n originalFileName: \"file_name\" in file_item\n ? file_item.file_name\n : undefined,\n };\n }\n}\n"],"mappings":"AAAA,OAAS,gBAAAA,OAAoB,SCE7B,OACE,kBAAAC,EACA,oBAAAC,EACA,cAAAC,EACA,eAAAC,MACK,SCIA,IAAMC,EAA0B,gCAC1BC,EAAyB,4BAoB/B,IAAMC,EAAkB,cAOxB,IAAMC,EAAsB,CACjC,UAAW,oBACX,IAAK,eACL,OAAQ,eACR,eAAgB,0BAChB,aAAc,eACd,cAAe,gBACf,UAAW,kBACX,gBAAiB,mBACnB,EAEaC,EAAkB,CAC7B,gBAAiB,kBACjB,kBAAmB,mBACnB,mBAAoB,GACtB,EAIaC,EAA4C,CACvD,MAAO,MACP,QAAS,QACT,QAASC,EACT,QAAS,IACT,kBAAmB,EACrB,EAEaC,EAAuC,CAClD,SAAU,EACV,WAAYC,EACZ,aAAc,KACd,cAAe,GACjB,EAEaC,EAAsC,CACjD,WAAY,EACZ,cAAe,KACf,SAAWC,GAAW,CACpB,QAAQ,IAAI;AAAA,2CAA8C,EAC1D,QAAQ,IAAI,gIAAuB,EACnC,QAAQ,IAAIA,EAAO,SAAS,EAC5B,QAAQ,IAAI;AAAA,CAA8C,CAC5D,EACA,eAAiBC,GAAY,CAC3B,OAAQA,EAAQ,OAAQ,CACtB,IAAK,OACH,QAAQ,IAAI,6BAAS,EACrB,MACF,IAAK,SACH,QAAQ,IAAI;AAAA,oHAA2B,EACvC,MACF,IAAK,YACH,QAAQ,IACN;AAAA,yJACF,EACA,MACF,IAAK,sBACH,QAAQ,IAAI;AAAA,iBAAUA,EAAQ,OAAO,EAAE,EACvC,KACJ,CACF,CACF,EAEaC,EAA2C,CACtD,aAAc,OACd,OAAQ,OACR,QAAS,MACX,EDhGO,IAAMC,EAAN,KAAkB,CACvB,OAAc,IAAIC,EAAwB,CACxC,OAAOC,EAAW,KAAK,EAAE,OAAOD,CAAM,EAAE,OAAO,KAAK,CACtD,CAEA,OAAc,kBAAkBE,EAAiB,GAAY,CAC3D,OAAOC,EAAYD,CAAM,CAC3B,CAEA,OAAc,cAAcE,EAA+B,CACzD,OAAO,KAAK,MAAMA,EAAgB,GAAK,EAAc,EAAI,EAC3D,CAEA,OAAc,cAAcC,EAAmBC,EAAqB,CAClE,GAAIA,EAAI,SAAW,GACjB,MAAM,IAAI,MACR,oEAAiC,EAAc,yCAAWA,EAAI,MAAM,eACtE,EAEF,IAAMC,EAASC,EAAeC,EAAiBH,EAAK,IAAI,EACxD,OAAO,OAAO,OAAO,CAACC,EAAO,OAAOF,CAAS,EAAGE,EAAO,MAAM,CAAC,CAAC,CACjE,CAEA,OAAc,cAAcG,EAAoBJ,EAAqB,CACnE,GAAIA,EAAI,SAAW,GACjB,MAAM,IAAI,MACR,oEAAiC,EAAc,yCAAWA,EAAI,MAAM,eACtE,EAEF,IAAMK,EAAWC,EAAiBH,EAAiBH,EAAK,IAAI,EAC5D,OAAO,OAAO,OAAO,CAACK,EAAS,OAAOD,CAAU,EAAGC,EAAS,MAAM,CAAC,CAAC,CACtE,CAMA,OAAc,kBAA2B,CACvC,IAAME,EAAYV,EAAY,CAAC,EAAE,SAAS,KAAK,EAC/C,MAAO,cAAc,KAAK,IAAI,CAAC,IAAIU,CAAS,EAC9C,CAGA,OAAc,iBAA0B,CACtC,IAAMC,EAASX,EAAY,CAAC,EAAE,aAAa,CAAC,EAC5C,OAAO,OAAO,KAAK,OAAOW,CAAM,EAAG,OAAO,EAAE,SAAS,QAAQ,CAC/D,CACF,EEoPO,IAAWC,OAChBA,EAAA,KAAO,OACPA,EAAA,QAAU,SACVA,EAAA,SAAW,sBACXA,EAAA,UAAY,YACZA,EAAA,QAAU,UALMA,OAAA,ICtSX,SAASC,EAAgBC,KAAWC,EAA0B,CACnE,IAAMC,EAAS,CAAE,GAAGF,CAAI,EACxB,QAAWG,KAAUF,EACnB,QAAWG,KAAOD,EACZA,EAAOC,CAAG,IAAM,SAClBF,EAAOE,CAAG,EAAID,EAAOC,CAAG,GAI9B,OAAOF,CACT,CAEO,SAASG,EAAYC,EAAoC,CAC9D,OAAQA,EAAM,CACZ,OACE,MAAO,OACT,OACE,MAAO,QACT,OACE,MAAO,QACT,OACE,MAAO,OACT,OACE,MAAO,QACT,QACE,MAAO,SACX,CACF,CAGO,SAASC,EAAmBC,EAAyB,CAC1D,IAAMC,EAAQD,EAAQ,MAAM,GAAG,EAAE,IAAKE,GAAM,SAASA,EAAG,EAAE,CAAC,EACrDC,EAAQF,EAAM,CAAC,GAAK,EACpBG,EAAQH,EAAM,CAAC,GAAK,EACpBI,EAAQJ,EAAM,CAAC,GAAK,EAC1B,OAASE,EAAQ,MAAS,IAAQC,EAAQ,MAAS,EAAMC,EAAQ,GACnE,CAEO,SAASC,EAAiBR,EAAsB,CACrD,GAAI,cAAeA,EAAM,OAAOA,EAAK,UACrC,GAAI,eAAgBA,EAAM,OAAOA,EAAK,WACtC,GAAI,eAAgBA,EAAM,OAAOA,EAAK,WACtC,GAAI,eAAgBA,EAAM,OAAOA,EAAK,UACxC,CAEO,SAASS,EACdT,EAKwB,CACxB,OAAQA,EAAM,CACZ,OACA,OACA,OACA,OACE,MAAO,GACT,OACA,OACA,QACE,MAAO,EACX,CACF,CC5DO,IAAMU,EAAN,KAAiB,CACd,OACA,iBAER,YAAYC,EAA4B,CACtC,KAAK,OAASC,EAAaC,EAAuBF,CAAM,EAExD,KAAK,iBAAmBG,EAAmBH,EAAO,OAAO,CAC3D,CAOO,SAASI,EAAqB,CACnC,KAAK,OAAO,MAAQA,CACtB,CAGO,WAAWC,EAAuB,CACvC,KAAK,OAAO,QAAUA,CACxB,CAEO,UAAUC,EAAsB,CACrC,KAAK,OAAO,OAASA,CACvB,CAGO,YAAqB,CAC1B,OAAO,KAAK,OAAO,OACrB,CAGO,YAAqB,CAC1B,OAAO,KAAK,OAAO,OACrB,CAGO,WAAgC,CACrC,OAAO,KAAK,OAAO,MACrB,CAWA,MAAa,QACXC,EACAC,EACY,CAEZ,IAAMC,EAAO,KAAK,OAAO,QAAQ,SAAS,GAAG,EACzC,KAAK,OAAO,QACZ,GAAG,KAAK,OAAO,OAAO,IACpBC,EAAM,IAAI,IAAIH,EAAUE,CAAI,EAG9BE,EAEJ,GAAIH,EAAQ,SAAW,QAAUA,EAAQ,KAAM,CAE7C,IAAMI,EAAU,CAAE,GAAGJ,EAAQ,IAAK,EAClCI,EAAQ,UAAY,CAAE,gBAAiB,KAAK,OAAO,OAAQ,EAC3DD,EAAkB,KAAK,UAAUC,CAAO,CAC1C,MAAWJ,EAAQ,SAAW,QAAU,CAACA,EAAQ,OAE/CG,EAAkB,KAAK,UAAU,CAC/B,UAAW,CAAE,gBAAiB,KAAK,OAAO,OAAQ,CACpD,CAAC,GAIH,IAAME,EAAU,KAAK,aAAaF,CAAe,EAG3CG,EAAa,IAAI,gBACjBC,EAAY,WAAW,IAAMD,EAAW,MAAM,EAAGN,EAAQ,SAAS,EAGxE,GAAI,CACF,IAAMQ,EAAW,MAAM,MAAMN,EAAI,SAAS,EAAG,CAC3C,OAAQF,EAAQ,OAChB,QAASK,EACT,KAAMF,EACN,OAAQG,EAAW,MACrB,CAAC,EAED,aAAaC,CAAS,EACtB,IAAME,EAAU,MAAMD,EAAS,KAAK,EAGpC,GAAI,CAACA,EAAS,GACZ,MAAM,IAAI,MACR,qBAAqBR,EAAQ,MAAM,IAAID,CAAQ,UAAUS,EAAS,MAAM,KAAKC,CAAO,EACtF,EAIF,OAAOA,EAAU,KAAK,MAAMA,CAAO,EAAU,CAAC,CAChD,OAASC,EAAK,CACZ,mBAAaH,CAAS,EAEhBG,CACR,CACF,CAOQ,aAAaC,EAA6C,CAChE,IAAMN,EAAkC,CACtC,CAACO,EAAoB,YAAY,EAAGC,EAAgB,kBACpD,CAACD,EAAoB,SAAS,EAAGC,EAAgB,gBACjD,CAACD,EAAoB,GAAG,EAAGE,EAAY,gBAAgB,EACvD,CAACF,EAAoB,MAAM,EAAG,KAAK,OAAO,MAC1C,CAACA,EAAoB,cAAc,EAAG,OAAO,KAAK,gBAAgB,CACpE,EAEA,OAAID,IACFN,EAAQ,gBAAgB,EAAI,OAC1B,OAAO,WAAWM,EAAY,OAAO,CACvC,GAGE,KAAK,OAAO,OAAO,KAAK,IAC1BN,EAAQ,cAAmB,UAAU,KAAK,OAAO,MAAM,KAAK,CAAC,IAGxDA,CACT,CACF,ECpIO,IAAMU,EAAN,KAAkB,CACf,KAER,YAAYC,EAAkB,CAC5B,KAAK,KAAOA,CACd,CAKA,MAAa,WAAiC,CAE5C,KAAK,KAAK,WAAW,KAAK,KAAK,WAAW,CAAC,EAE3C,IAAMC,EAAW,MAAM,KAAK,KAAK,QAC/B,qCAAqC,KAAK,KAAK,WAAW,CAAC,GAC3D,CAAE,OAAQ,MAAO,UAAW,GAA2B,CACzD,EAEA,MAAO,CACL,SAAUA,EAAS,OACnB,UAAWA,EAAS,kBACtB,CACF,CAKA,MAAa,gBACXC,EACAC,EAAY,KACkC,CAC9C,GAAI,CACF,IAAMF,EAAW,MAAM,KAAK,KAAK,QAC/B,sCAAsC,mBAAmBC,CAAQ,CAAC,GAClE,CAAE,OAAQ,MAAO,UAAAC,CAAU,CAC7B,EACA,MAAO,CAAE,OAAQF,EAAS,OAAQ,KAAMA,CAAS,CACnD,OAASG,EAAU,CAEjB,GAAIA,EAAI,OAAS,aACf,MAAO,CAAE,aAAyB,EAEpC,MAAMA,CACR,CACF,CAKA,MAAa,MAAMC,EAAkD,CACnE,IAAMC,EAAaD,EAAQ,YAAc,EACrCE,EAAe,EAEnB,KAAOA,GAAgBD,GAAY,CAEjC,IAAME,EAAS,MAAM,KAAK,UAAU,EACpCH,EAAQ,SAASG,CAAM,EAIvB,IAAMC,EAAa,KAAK,IAAI,EAAI,IAEhC,KAAO,KAAK,IAAI,EAAIA,GAAY,CAC9B,GAAM,CAAE,OAAAC,EAAQ,KAAAC,CAAK,EAAI,MAAM,KAAK,gBAClCH,EAAO,SACPH,EAAQ,aACV,EAEA,OAAQK,EAAQ,CACd,WACEL,EAAQ,iBAAiB,CACvB,cACA,QAAS,6BACX,CAAC,EACD,MAEF,aACEA,EAAQ,iBAAiB,CACvB,gBACA,QAAS,uEACX,CAAC,EACD,MAEF,0BACE,GAAIM,EAAK,cAAe,CACtB,IAAMC,EAAa,WAAWD,EAAK,aAAa,GAChD,KAAK,KAAK,WAAWC,CAAU,EAC/BP,EAAQ,iBAAiB,CACvB,6BACA,QAAS,2DAAcO,CAAU,GACjC,cAAeD,EAAK,aACtB,CAAC,CACH,CACA,MAEF,gBACE,OAAAN,EAAQ,iBAAiB,CACvB,mBACA,QAAS,iCACT,UAAWM,EAAK,UAChB,QAASA,EAAK,QACd,aAAcA,EAAK,aACnB,cAAeA,EAAK,aACtB,CAAC,EAGD,KAAK,KAAK,SAASA,EAAK,SAAS,EACjC,KAAK,KAAK,UAAUA,EAAK,aAAa,EAClCA,EAAK,SACP,KAAK,KAAK,WAAWA,EAAK,OAAO,EAI5B,CACL,MAAOA,EAAK,UACZ,QAASA,EAAK,SAAW,KAAK,KAAK,WAAW,EAC9C,UAAWA,EAAK,aAChB,OAAQA,EAAK,aACf,EAEF,cACEN,EAAQ,iBAAiB,CACvB,iBACA,QAAS,mFACX,CAAC,EAED,MAEF,QAEE,MAAM,IAAI,QAASQ,GAAQ,WAAWA,EAAK,GAAe,CAAC,CAC/D,CAEA,GAAIH,IAAW,UACb,MAIF,MAAM,IAAI,QAASG,GAAQ,WAAWA,EAAK,GAAe,CAAC,CAC7D,CAEAN,GACF,CAEA,MAAM,IAAI,MACR,6HAAyBD,CAAU,eACrC,CACF,CACF,ECnKA,OAAS,YAAAQ,MAAgB,cACzB,OAAS,YAAAC,MAAgB,OAoBlB,IAAMC,EAAN,KAAqB,CAClB,KACA,IAER,YAAYC,EAAkBC,EAAiB,CAC7C,KAAK,KAAOD,EACZ,KAAK,IAAMC,CACb,CAMA,MAAa,SACXC,EACAC,EAAuCC,EAClB,CACrB,IAAMC,EAAW,CACf,OACA,UAAW,CAAE,KAAAH,CAAK,CACpB,EACMI,EAAgBC,EAAaH,EAAsB,CACvD,OAAQ,KAAK,KAAK,UAAU,CAC9B,EAAGD,CAAO,EACV,OAAO,KAAK,aACVE,EACAC,EAAc,OACdA,EAAc,YAChB,CACF,CAMA,MAAa,UACXE,EACAL,EAAuCC,EAClB,CACrB,IAAME,EAAgBC,EAAaH,EAAsB,CACvD,OAAQ,KAAK,KAAK,UAAU,CAC9B,EAAGD,CAAO,EACV,OAAO,KAAK,mBACVK,IAEAF,CACF,CACF,CAEA,MAAa,UACXE,EACAL,EAAuCC,EAClB,CACrB,IAAME,EAAgBC,EAAaH,EAAsB,CACvD,OAAQ,KAAK,KAAK,UAAU,CAC9B,EAAGD,CAAO,EACV,OAAO,KAAK,mBACVK,IAEAF,CACF,CACF,CAIA,MAAa,SACXE,EACAL,EAAuCC,EAClB,CACrB,IAAME,EAAgBC,EAAaH,EAAsB,CACvD,OAAQ,KAAK,KAAK,UAAU,CAC9B,EAAGD,CAAO,EACV,OAAO,KAAK,mBACVK,IAEAF,CACF,CACF,CASA,MAAc,mBACZE,EACAC,EACAN,EACqB,CAErB,IAAMO,EAAS,MAAMC,EAASH,CAAQ,EAChCI,EAAWC,EAASL,CAAQ,EAG5BM,EAAS,MAAM,KAAK,IAAI,aAC5BJ,EACAP,EAAQ,OACRM,CACF,EAGIN,EAAQ,SACV,MAAM,KAAK,SAASA,EAAQ,QAAS,CACnC,aAAcA,EAAQ,YACxB,CAAC,EAIH,IAAMY,EAAW,CACf,oBAAqBD,EAAO,oBAC5B,QAAS,OAAO,KAAKA,EAAO,SAAS,EAAE,SAAS,QAAQ,EACxD,aAAc,CAChB,EAEIE,EAAmB,CAAC,EAExB,OAAQP,EAAW,CACjB,OACEO,EAAc,CACZ,OACA,WAAY,CAAE,MAAOD,EAAU,SAAUD,EAAO,cAAe,CACjE,EACA,MACF,OACEE,EAAc,CACZ,OACA,WAAY,CAAE,MAAOD,EAAU,WAAYD,EAAO,cAAe,CACnE,EACA,MACF,OACEE,EAAc,CACZ,OACA,UAAW,CACT,MAAOD,EACP,UAAWH,EACX,IAAK,OAAOE,EAAO,aAAa,CAClC,CACF,EACA,MACF,QACE,MAAM,IAAI,MAAM,sEAA8BL,CAAS,EAAE,CAC7D,CAGA,OAAO,KAAK,aACVO,EACAb,EAAQ,OACRA,EAAQ,YACV,CACF,CAKA,MAAc,aACZc,EACAC,EACAC,EACqB,CACrB,IAAMC,EAAWC,EAAY,iBAAiB,EAExCC,EAAmB,CACvB,IAAK,CACH,aAAc,GACd,WAAYJ,EACZ,UAAWE,EACX,eACA,gBACA,cAAeD,EACf,UAAW,CAACF,CAAO,CACrB,CACF,EAEMM,EAAW,MAAM,KAAK,KAAK,QAAQ,wBAAyB,CAChE,OAAQ,OACR,KAAMD,EACN,UAAW,IACb,CAAC,EAED,MAAO,CAAE,SAAAF,EAAU,SAAAG,CAAS,CAC9B,CACF,EC9LO,IAAMC,EAAN,KAAiB,CACd,KACA,OAER,YACEC,EACAC,EAAoCC,EACpC,CACA,KAAK,KAAOF,EACZ,KAAK,OAASG,EAAaD,EAAoBD,CAAM,CACvD,CAYA,MAAa,aACXG,EACAC,EACAC,EACwB,CAIxB,IAAMC,EAAUH,EAAO,OACjBI,EAAaC,EAAY,IAAIL,CAAM,EACnCM,EAAWD,EAAY,cAAcF,CAAO,EAG5CI,EAAUF,EAAY,kBAAkB,EAAc,EAAE,SAC5D,KACF,EACMG,EAAeH,EAAY,kBAAkB,EAAc,EAC3DI,EAAYD,EAAa,SAAS,KAAK,EAKvCE,EAAgB,MAAM,KAAK,KAAK,QACpC,yBACA,CACE,OAAQ,OACR,KAAM,CACJ,QAAAH,EACA,WAAYL,EACZ,WAAYD,EACZ,QAAAE,EACA,WAAAC,EACA,SAAAE,EACA,cAAe,GACf,OAAQG,CACV,EACA,UAAW,KAAK,OAAO,YACzB,CACF,EAIIE,EAAS,GACb,GAAID,EAAc,iBAAiB,KAAK,EACtCC,EAASD,EAAc,gBAAgB,KAAK,UACnCA,EAAc,aACvBC,EAAS,GAAG,KAAK,OAAO,UAAU,iCAChC,mBAAmBD,EAAc,YAAY,CAC/C,YAAY,mBAAmBH,CAAO,CAAC,OAEvC,OAAM,IAAI,MACR,8HACF,EAMF,IAAMK,EAAmBP,EAAY,cAAcL,EAAQQ,CAAY,EAKjEK,EAAsB,MAAM,KAAK,UAAUD,EAAkBD,CAAM,EAKzE,MAAO,CACL,QAAAJ,EACA,UAAAE,EACA,aAAAD,EACA,cAAeL,EACf,eAAgBG,EAChB,oBAAAO,CACF,CACF,CAUA,MAAc,UAAUC,EAAoBH,EAAiC,CAC3E,IAAMI,EAAc,KAAK,OAAO,SAC5BC,EAEJ,QAASC,EAAU,EAAGA,GAAWF,EAAaE,IAC5C,GAAI,CACF,IAAMC,EAAM,MAAM,MAAMP,EAAQ,CAC9B,OAAQ,OACR,QAAS,CAAE,eAAgB,0BAA2B,EACtD,KAAM,IAAI,WAAWG,CAAU,CACjC,CAAC,EAGD,GAAII,EAAI,QAAU,KAAOA,EAAI,OAAS,IAAK,CACzC,IAAMC,EAASD,EAAI,QAAQ,IAAIE,EAAoB,SAAS,GACzD,MAAMF,EAAI,KAAK,EAClB,MAAM,IAAI,MAAM,yBAAyBA,EAAI,MAAM,KAAKC,CAAM,EAAE,CAClE,CAGA,GAAID,EAAI,SAAW,IAAK,CACtB,IAAMC,EAASD,EAAI,QAAQ,IAAIE,EAAoB,SAAS,GAC1D,QAAQF,EAAI,MAAM,GACpB,MAAM,IAAI,MAAM,qBAAqBC,CAAM,EAAE,CAC/C,CAGA,IAAME,EAAgBH,EAAI,QAAQ,IAChCE,EAAoB,eACtB,EACA,GAAI,CAACC,EACH,MAAM,IAAI,MACR,yFAA6BD,EAAoB,eAAe,EAClE,EAGF,OAAOC,CACT,OAASC,EAAU,CAIjB,GAHAN,EAAYM,EAGRA,EAAI,SAAWA,EAAI,QAAQ,SAAS,kBAAkB,EACxD,MAAMA,EAIJL,EAAUF,GACZ,MAAM,IAAI,QAASQ,GACjB,WAAWA,EAAS,KAAK,OAAO,cAAgBN,CAAO,CACzD,CAEJ,CAGF,MAAM,IAAI,MACR,qEAA6BF,CAAW,0CACrCC,GAAqB,OACxB,EACF,CACF,CAUA,MAAa,eAAeQ,EAA4C,CAEtE,IAAIC,EAAM,GACV,GAAID,EAAO,QACTC,EAAMD,EAAO,gBACJA,EAAO,oBAEhBC,EAAM,GAAG,KAAK,OAAO,UAAU,mCAC7B,mBAAmBD,EAAO,mBAAmB,CAC/C,OAEA,OAAM,IAAI,MACR,4FACF,EAIF,IAAME,EAAkB,MAAM,KAAK,cAAcD,CAAG,EAGpD,GAAID,EAAO,SAAW,CAACA,EAAO,aAC5B,OAAOE,EAIT,IAAMC,EAAS,KAAK,kBAAkBH,EAAO,YAAY,EAGzD,OAAOnB,EAAY,cAAcqB,EAAiBC,CAAM,CAC1D,CAWQ,kBAAkBC,EAA8B,CACtD,IAAMC,EAAU,OAAO,KAAKD,EAAc,QAAQ,EAGlD,GAAIC,EAAQ,SAAW,GACrB,OAAOA,EAIT,GACEA,EAAQ,SAAW,IACnB,qBAAqB,KAAKA,EAAQ,SAAS,OAAO,CAAC,EAEnD,OAAO,OAAO,KAAKA,EAAQ,SAAS,OAAO,EAAG,KAAK,EAGrD,MAAM,IAAI,MACR,qFAAwCD,CAAY,IACtD,CACF,CAKA,MAAc,cAAcH,EAA8B,CACxD,IAAMP,EAAM,MAAM,MAAMO,CAAG,EAC3B,GAAI,CAACP,EAAI,GAAI,CACX,IAAMY,EAAO,MAAMZ,EAAI,KAAK,EAAE,MAAM,IAAM,cAAc,EACxD,MAAM,IAAI,MACR,8DAAgCA,EAAI,MAAM,KAAKY,CAAI,EACrD,CACF,CACA,OAAO,OAAO,KAAK,MAAMZ,EAAI,YAAY,CAAC,CAC5C,CACF,ECnQA,SAASa,EAAcC,EAAiBC,EAA4B,CAClE,IAAMC,EAAWF,EAAI,WACfG,EAAY,GAAKD,EACjBE,EAAM,OAAO,YAAYD,CAAS,EACpCE,EAAS,EAEb,OAAAD,EAAI,MAAM,OAAQC,CAAM,EACxBA,GAAU,EACVD,EAAI,cAAcD,EAAY,EAAGE,CAAM,EACvCA,GAAU,EACVD,EAAI,MAAM,OAAQC,CAAM,EACxBA,GAAU,EAEVD,EAAI,MAAM,OAAQC,CAAM,EACxBA,GAAU,EACVD,EAAI,cAAc,GAAIC,CAAM,EAC5BA,GAAU,EACVD,EAAI,cAAc,EAAGC,CAAM,EAC3BA,GAAU,EACVD,EAAI,cAAc,EAAGC,CAAM,EAC3BA,GAAU,EACVD,EAAI,cAAcH,EAAYI,CAAM,EACpCA,GAAU,EACVD,EAAI,cAAcH,EAAa,EAAGI,CAAM,EACxCA,GAAU,EACVD,EAAI,cAAc,EAAGC,CAAM,EAC3BA,GAAU,EACVD,EAAI,cAAc,GAAIC,CAAM,EAC5BA,GAAU,EAEVD,EAAI,MAAM,OAAQC,CAAM,EACxBA,GAAU,EACVD,EAAI,cAAcF,EAAUG,CAAM,EAClCA,GAAU,EAEV,OAAO,KAAKL,EAAI,OAAQA,EAAI,WAAYA,EAAI,UAAU,EAAE,KAAKI,EAAKC,CAAM,EAEjED,CACT,CAEA,eAAsBE,EACpBC,EACAN,EAAqB,KACG,CACxB,GAAI,CACF,GAAM,CAAE,OAAAO,CAAO,EAAI,KAAM,QAAO,WAAW,EACrCC,EAAS,MAAMD,EAAOD,EAAYN,CAAU,EAElD,OADYF,EAAcU,EAAO,KAAMR,CAAU,CAEnD,MAAc,CACZ,OAAO,IACT,CACF,CChDA,OAAS,aAAAS,OAAiB,cAQnB,IAAMC,EAAN,KAAoB,CACjB,IACA,OAER,YAAYC,EAAiBC,EAA4B,CACvD,KAAK,IAAMD,EACX,KAAK,OAASC,CAChB,CAKA,MAAa,MAAMC,EAA0D,CAE3E,GAAI,CAACA,EAAI,WAAaA,EAAI,UAAU,SAAW,EAAG,MAAO,CAAC,EAE1D,IAAMC,EAAmC,CAAC,EAGpCC,EAAqC,CACzC,UAAW,OAAOF,EAAI,UAAU,EAChC,IAAKA,EAAI,IACT,WAAYA,EAAI,aAChB,SAAUA,EAAI,WACd,UAAWA,EAAI,eACf,aAAcA,EAAI,cAClB,UACA,WAAY,UACZ,MAAO,EACP,IAAKA,CACP,EAEA,QAASG,EAAI,EAAGA,EAAIH,EAAI,UAAU,OAAQG,IAAK,CAC7C,IAAMC,EAAOJ,EAAI,UAAUG,CAAC,EACtBE,EAAUC,EAAaJ,EAAa,CACxC,MAAOC,EACP,QAASC,EAAK,KACd,WAAYG,EAAYH,EAAK,IAAI,CACnC,CAAC,EAMD,GAHIA,EAAK,OAAS,IAChBC,EAAQ,KAAOD,EAAK,UAAU,MAE5B,CAACI,EAAgBJ,EAAK,IAAI,EAAG,CAC/BH,EAAQ,KAAKI,CAAO,EACpB,QACF,CAGA,IAAII,EAA8B,KAC5BC,EAAS,KAAK,uBAAuBN,CAAI,EAkB/C,GAjBAC,EAAQ,SAAWK,GAAQ,iBAC3BL,EAAQ,YAAcK,EACtBL,EAAQ,UAAY,SACdI,IACCC,GAELD,EAAe,MAAM,KAAK,IAAI,eAAeC,CAAM,EAC5CD,GAHa,MAKtBJ,EAAQ,WAAa,MAAOM,GAAqB,CAC/C,IAAMC,EAAM,MAAMP,EAAQ,UAAW,EACrC,GAAI,CAACO,EAAK,MAAM,IAAI,MAAM,kDAAU,EACpC,aAAMC,GAAUF,EAAUC,CAAG,EACtBD,CACT,EAGIN,EAAQ,UAAY,EAAuB,CAC7CA,EAAQ,eAAiBA,EAAQ,UACjC,IAAIS,EAAiC,KACrCT,EAAQ,UAAY,SAAY,CAC9B,GAAIS,EAAiB,OAAOA,EAC5B,IAAMC,EAAc,MAAMV,EAAQ,eAAgB,EAClD,OAAKU,GACLD,EAAkB,MAAME,EAAUD,CAAW,EACtCD,GAFkB,IAG3B,CACF,CAGA,GAAI,KAAK,OAAO,oBAAsB,IAASJ,EAC7C,GAAI,CACFL,EAAQ,UAAU,CACpB,OAASY,EAAU,CACjB,QAAQ,MAAM,qDAAaA,EAAI,OAAO,EAAE,CAC1C,CAEFhB,EAAQ,KAAKI,CAAO,CACtB,CACA,OAAOJ,CACT,CAKQ,uBACNG,EAC+B,CAC/B,IAAMc,EAAYC,EAAiBf,CAAI,EACvC,GAAI,CAACc,GAAa,CAACA,EAAU,MAAO,OACpC,IAAME,EAAQF,EAAU,MAGxB,GAAI,CAACE,EAAM,qBAAuB,CAACA,EAAM,SAAU,OAGnD,IAAIC,EAAeD,EAAM,QAIvBhB,EAAK,OAAS,GAAyB,WAAYc,GACnDA,EAAU,SAGVG,EAAe,OAAO,KAAKH,EAAU,OAAQ,KAAK,EAAE,SAAS,QAAQ,GAIvE,IAAMI,EAAU,CAACD,EAEjB,MAAO,CACL,QAASD,EAAM,SACf,oBAAqBA,EAAM,oBAC3B,aAAAC,EACA,QAAAC,EACA,iBAAkB,cAAeJ,EAC7BA,EAAU,UACV,MACN,CACF,CACF,EVjIO,IAAMK,EAAN,cAAwBC,EAAgC,CAC7C,KACA,KACA,SACA,OACA,IAGR,mBAA8C,KAE9C,UAAqB,GACrB,QAAkB,GAE1B,YAAYC,EAAsCC,EAAuB,CACvE,MAAM,EAGN,IAAMC,EAAeC,EAAaF,EAAuBD,CAAM,EAG/D,KAAK,KAAO,IAAII,EAAWF,CAAY,EAGvC,KAAK,KAAO,IAAIG,EAAY,KAAK,IAAI,EACrC,KAAK,IAAM,IAAIC,EAAW,KAAK,IAAI,EACnC,KAAK,SAAW,IAAIC,EAAe,KAAK,KAAM,KAAK,GAAG,EACtD,KAAK,OAAS,IAAIC,EAAc,KAAK,IAAKN,CAAY,EAGlDA,EAAa,QACf,KAAK,mBAAqB,CACxB,MAAOA,EAAa,MACpB,QAASA,EAAa,QACtB,UAAW,GACX,OAAQ,EACV,EAEJ,CAUA,MAAa,MACXO,EAAiCC,EACN,CAE3B,IAAMC,EAAiBR,EAAaO,EAAuBD,CAAO,EAG5DG,EAAS,MAAM,KAAK,KAAK,MAAMD,CAAc,EAGnD,YAAK,mBAAqBC,EAE1B,KAAK,KAAK,QAAS,KAAK,kBAAkB,EACnC,KAAK,kBACd,CAUO,mBAA6C,CAClD,MAAI,CAAC,KAAK,oBAAsB,CAAC,KAAK,mBAAmB,MAChD,KAGF,CAAE,GAAG,KAAK,kBAAmB,CACtC,CAMO,gBAAgBC,EAAqC,CAC1D,KAAK,mBAAqB,CAAE,GAAGA,CAAY,EAG3C,KAAK,KAAK,SAASA,EAAY,KAAK,EACpC,KAAK,KAAK,WAAWA,EAAY,OAAO,EACxC,KAAK,KAAK,UAAUA,EAAY,MAAM,EAEtC,QAAQ,IACN,gEAAkCA,EAAY,SAAS,GACzD,CACF,CAMA,MAAa,mBAAsC,CACjD,GAAI,CAAC,KAAK,oBAAsB,CAAC,KAAK,mBAAmB,MACvD,MAAO,GAGT,GAAI,CAEF,IAAMC,EAAW,MAAM,KAAK,KAAK,QAAa,sBAAuB,CACnE,OAAQ,OACR,KAAM,CACJ,cAAe,KAAK,mBAAmB,MACzC,EACA,UAAW,GACb,CAAC,EAED,OAAIA,EAAS,MAAQ,QAAaA,EAAS,MAAQ,GACjD,QAAQ,KACN,gEACEA,EAAS,SAAWA,EAAS,GAC/B,GACF,EACO,IAGLA,EAAS,UAAY,QAAaA,EAAS,UAAY,GACzD,QAAQ,KACN,wEAA+CA,EAAS,OAAO,MAAMA,EAAS,MAAM,EACtF,EACO,IAGF,EACT,OAASC,EAAO,CACd,eAAQ,MACN,0GACCA,EAAgB,OACnB,EACO,EACT,CACF,CASA,MAAa,cAAe,CAC1B,GAAI,KAAK,UAAW,CAClB,QAAQ,KAAK,wGAA6B,EAC1C,MACF,CAEA,GAAI,CAAC,KAAK,oBAAoB,MAC5B,MAAM,IAAI,MAAM,oHAA+B,EAMjD,IAHA,KAAK,UAAY,GACjB,QAAQ,IAAI,iIAAqC,EAE1C,KAAK,WACV,GAAI,CAEF,IAAMD,EAAW,MAAM,KAAK,KAAK,QAC/B,uBACA,CACE,OAAQ,OACR,KAAM,CAAE,gBAAiB,KAAK,OAAQ,EACtC,UAAW,IACb,CACF,EAGME,EAAcF,EAAS,MAAQ,QAAaA,EAAS,MAAQ,EAC7DG,EAAaH,EAAS,UAAY,QACtCA,EAAS,UAAY,EAGvB,GAAIE,GAAeC,EAAY,CAC7B,KAAK,UAAY,GACjB,QAAQ,IAAIH,CAAQ,EACpB,KAAK,KACH,QACA,IAAI,MAAM,gFAAyBA,EAAS,OAAO,GAAG,CACxD,EACA,QAAQ,MAAM,gEAAwBA,EAAS,MAAM,EAAE,EACvD,KACF,CAQA,GALIA,EAAS,kBACX,KAAK,QAAUA,EAAS,iBAItBA,EAAS,MAAQA,EAAS,KAAK,OAAS,EAC1C,QAAWI,KAAUJ,EAAS,KAAM,CAClC,IAAMK,EAAa,MAAM,KAAK,OAAO,MAAMD,CAAM,EACjD,GAAI,GAACC,GAAcA,EAAW,SAAW,GAGzC,QAAWC,KAAOD,EAChB,KAAK,KAAK,UAAWC,CAAG,EACpBA,EAAI,aAAe,WACrB,KAAK,KAAKA,EAAI,WAAYA,CAAU,CAG1C,CAEJ,OAASL,EAAY,CAEnB,GAAIA,EAAM,OAAS,cAAgBA,EAAM,QAAQ,SAAS,SAAS,EAEjE,SAIF,QAAQ,MACN,6GAAkCA,EAAM,OAAO,EACjD,EACA,MAAM,IAAI,QAASM,GACjB,WAAWA,EAAS,GAA4B,CAClD,CACF,CAGF,QAAQ,IAAI,oFAA2B,CACzC,CAKO,aAAc,CACnB,KAAK,UAAY,EACnB,CACF","names":["EventEmitter","createCipheriv","createDecipheriv","createHash","randomBytes","WECHAT_DEFAULT_BASE_URL","WECHAT_DEFAULT_CDN_URL","WECHAT_AES_ALGO","WECHAT_HTTP_HEADERS","WECHAT_PROTOCOL","DEFAULT_CLIENT_CONFIG","WECHAT_DEFAULT_BASE_URL","DEFAULT_CDN_CONFIG","WECHAT_DEFAULT_CDN_URL","DEFAULT_LOGIN_OPTIONS","qrInfo","payload","DEFAULT_SEND_OPTIONS","CryptoUtils","buffer","createHash","length","randomBytes","plaintextSize","plaintext","key","cipher","createCipheriv","WECHAT_AES_ALGO","ciphertext","decipher","createDecipheriv","randomHex","uint32","LoginStatus","mergeObjects","ref","sources","target","source","key","getItemType","item","buildClientVersion","version","parts","p","major","minor","patch","getFileImageItem","isItemWithMedia","WeChatCore","config","mergeObjects","DEFAULT_CLIENT_CONFIG","buildClientVersion","token","baseUrl","userId","endpoint","options","base","url","finalBodyString","payload","headers","controller","timeoutId","response","rawText","err","bodyString","WECHAT_HTTP_HEADERS","WECHAT_PROTOCOL","CryptoUtils","AuthManager","core","response","qrcodeId","timeoutMs","err","options","maxRetries","currentRetry","qrInfo","qrDeadline","status","data","newBaseUrl","res","readFile","basename","MessageManager","core","cdn","text","options","DEFAULT_SEND_OPTIONS","textItem","mergedOptions","mergeObjects","filePath","mediaType","buffer","readFile","fileName","basename","ticket","mediaObj","messageItem","itemObj","userId","contextToken","clientId","CryptoUtils","requestBody","response","CdnManager","core","config","DEFAULT_CDN_CONFIG","mergeObjects","buffer","toUserId","mediaType","rawsize","rawfilemd5","CryptoUtils","filesize","filekey","aeskeyBuffer","aeskeyHex","uploadUrlResp","cdnUrl","ciphertextBuffer","encryptedQueryParam","ciphertext","MAX_RETRIES","lastError","attempt","res","errMsg","WECHAT_HTTP_HEADERS","downloadParam","err","resolve","ticket","url","encryptedBuffer","aesKey","aesKeyBase64","decoded","body","pcmBytesToWav","pcm","sampleRate","pcmBytes","totalSize","buf","offset","silkToWav","silkBuffer","decode","result","writeFile","MessageParser","cdn","config","raw","results","shared_data","i","item","msgCopy","mergeObjects","getItemType","isItemWithMedia","cachedBuffer","ticket","savePath","buf","writeFile","pcmCachedBuffer","silk_buffer","silkToWav","err","file_item","getFileImageItem","media","aesKeyBase64","isPlain","WeChatApi","EventEmitter","config","DEFAULT_CLIENT_CONFIG","mergedConfig","mergeObjects","WeChatCore","AuthManager","CdnManager","MessageManager","MessageParser","options","DEFAULT_LOGIN_OPTIONS","defaultOptions","result","credentials","response","error","hasErrorRet","hasErrcode","rawMsg","parsedMsgs","msg","resolve"]}
1
+ {"version":3,"sources":["../src/WechatAiApi.ts","../src/core/CryptoUtils.ts","../src/constants.ts","../src/types.ts","../src/core/utils.ts","../src/core/WeChatCore.ts","../src/managers/AuthManager.ts","../src/managers/MessageManager.ts","../src/managers/CdnManager.ts","../src/core/SilkConverter.ts","../src/managers/MessageParser.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\n\nimport { WeChatCore } from \"./core/WeChatCore.ts\";\nimport { AuthManager } from \"./managers/AuthManager.ts\";\nimport { MessageManager } from \"./managers/MessageManager.ts\";\nimport { CdnManager } from \"./managers/CdnManager.ts\";\nimport { MessageParser } from \"./managers/MessageParser.ts\";\nimport { mergeObjects } from \"./core/utils.ts\";\nimport {\n DEFAULT_CLIENT_CONFIG,\n DEFAULT_LOGIN_OPTIONS,\n DEFAULT_REQUEST_TIMEOUT_MS,\n LONG_POLLING_TIMEOUT_MS,\n POLLING_ERROR_RETRY_DELAY_MS,\n} from \"./constants.ts\";\nimport type {\n LoginCredentials,\n LoginOptions,\n PollingResult,\n WeChatApiEventMap,\n WeChatClientConfig,\n} from \"./types.ts\";\n\nexport class WeChatApi extends EventEmitter<WeChatApiEventMap> {\n public readonly core: WeChatCore;\n public readonly auth: AuthManager;\n public readonly messages: MessageManager;\n public readonly parser: MessageParser;\n public readonly cdn: CdnManager;\n\n // 内部缓存当前的登录凭证\n private currentCredentials: LoginCredentials | null = null;\n\n private isPolling: boolean = false;\n private syncBuf: string = \"\"; // 极其重要:状态同步游标\n\n constructor(config: Partial<WeChatClientConfig> = DEFAULT_CLIENT_CONFIG) {\n super(); // 初始化 EventEmitter\n\n // 合并默认配置和用户配置\n const mergedConfig = mergeObjects(DEFAULT_CLIENT_CONFIG, config);\n\n // 初始化网络底层\n this.core = new WeChatCore(mergedConfig);\n\n // 挂载领域模块\n this.auth = new AuthManager(this.core);\n this.cdn = new CdnManager(this.core);\n this.messages = new MessageManager(this.core, this.cdn);\n this.parser = new MessageParser(this.cdn, mergedConfig);\n\n // 如果初始化时直接传入了 token,先暂存一份不完整的凭证\n if (mergedConfig.token) {\n this.currentCredentials = {\n token: mergedConfig.token,\n baseUrl: mergedConfig.baseUrl,\n accountId: \"\", // 初始化时可能未知\n userId: \"\", // 初始化时可能未知\n };\n }\n }\n\n // ==========================================\n // 1. 登录模块 (支持无脑调用 & 深度定制)\n // ==========================================\n\n /**\n * 启动扫码登录流程。\n * @param options 可选。如果为空,将使用默认的控制台交互体验。\n */\n public async login(\n options: Partial<LoginOptions> = DEFAULT_LOGIN_OPTIONS,\n ): Promise<LoginCredentials> {\n // 默认的“无脑”交互实现\n const defaultOptions = mergeObjects(DEFAULT_LOGIN_OPTIONS, options);\n\n // 执行底层的登录逻辑\n const result = await this.auth.login(defaultOptions);\n\n // 登录成功后,缓存在 Bot 实例中,方便随时导出\n this.currentCredentials = result;\n\n this.emit(\"login\", this.currentCredentials); // 触发全局登录事件\n return this.currentCredentials;\n }\n\n // ==========================================\n // 2. 凭证管理模块 (导入 / 导出 / 验证)\n // ==========================================\n\n /**\n * 导出当前的登录凭证。\n * 开发者拿到后可以自行保存到本地文件或数据库中。\n */\n public exportCredentials(): LoginCredentials | null {\n if (!this.currentCredentials || !this.currentCredentials.token) {\n return null;\n }\n // 返回一个深拷贝,防止外部修改污染内部状态\n return { ...this.currentCredentials };\n }\n\n /**\n * 加载现有的登录凭证。\n * 适用于程序重启后,免扫码直接恢复状态。\n */\n public loadCredentials(credentials: LoginCredentials): void {\n this.currentCredentials = { ...credentials };\n\n // 同步给底层 HTTP 引擎\n this.core.setToken(credentials.token);\n this.core.setBaseUrl(credentials.baseUrl);\n this.core.setUserId(credentials.userId);\n\n console.log(\n `[WeChatApi] 成功加载凭证 (AccountID: ${credentials.accountId})`,\n );\n }\n\n /**\n * 验证当前加载的凭证是否仍然有效。\n * @returns boolean 是否有效\n */\n public async verifyCredentials(): Promise<boolean> {\n if (!this.currentCredentials || !this.currentCredentials.token) {\n return false;\n }\n\n try {\n // 使用 getconfig 作为一个轻量级的 Ping 探针\n const response = await this.core.request<any>(\"ilink/bot/getconfig\", {\n method: \"POST\",\n body: {\n ilink_user_id: this.currentCredentials.userId,\n },\n timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,\n });\n\n if (response.ret !== undefined && response.ret !== 0) {\n console.warn(\n `[WeChatApi] 凭证已失效 (Server returned: ${\n response.errcode || response.ret\n })`,\n );\n return false;\n }\n\n if (response.errcode !== undefined && response.errcode !== 0) {\n console.warn(\n `[WeChatApi] 凭证已失效 (Server returned errcode: ${response.errcode}): ${response.errmsg}`,\n );\n return false;\n }\n\n return true;\n } catch (error) {\n console.error(\n \"[WeChatApi] 凭证验证失败,网络异常或已过期:\",\n (error as Error).message,\n );\n return false;\n }\n }\n\n // ==========================================\n // 轮询守护进程 (Daemon Loop)\n // ==========================================\n\n /**\n * 启动长轮询,监听新消息\n */\n public async startPolling() {\n if (this.isPolling) {\n console.warn(\"[WeChatApi] 轮询已经在运行中,请勿重复启动\");\n return;\n }\n\n if (!this.currentCredentials?.token) {\n throw new Error(\"[WeChatApi] 无法启动轮询:尚未登录或未加载凭证\");\n }\n\n this.isPolling = true;\n console.log(\"[WeChatApi] 🚀 轮询守护进程已启动,正在监听新消息...\");\n\n while (this.isPolling) {\n try {\n // 核心:发起长轮询请求。这里会挂起长达 35 秒,直到有新消息或超时\n const response = await this.core.request<PollingResult>(\n \"ilink/bot/getupdates\",\n {\n method: \"POST\",\n body: { get_updates_buf: this.syncBuf },\n timeoutMs: LONG_POLLING_TIMEOUT_MS, // 长轮询标准超时时间\n },\n );\n\n // 1. 检查服务端返回的错误码 (容错处理:成功时字段可能被省略)\n const hasErrorRet = response.ret !== undefined && response.ret !== 0;\n const hasErrcode = response.errcode !== undefined &&\n response.errcode !== 0;\n\n // 1. 检查服务端返回的错误码 (例如 -14 代表登录失效)\n if (hasErrorRet || hasErrcode) {\n this.isPolling = false;\n console.log(response);\n this.emit(\n \"error\",\n new Error(`会话已失效或服务端报错 (errcode: ${response.errcode})`),\n );\n console.error(`[WeChatApi] ❌ 轮询异常中止:${response.errmsg}`);\n break;\n }\n\n // 2. 更新同步游标 (无论有没有新消息,都要更新,否则会死循环拉取旧消息)\n if (response.get_updates_buf) {\n this.syncBuf = response.get_updates_buf;\n }\n\n // 3. 解析并分发新消息\n if (response.msgs && response.msgs.length > 0) {\n for (const rawMsg of response.msgs) {\n const parsedMsgs = await this.parser.parse(rawMsg);\n if (!parsedMsgs || parsedMsgs.length === 0) continue;\n\n // 触发全局通用消息事件\n for (const msg of parsedMsgs) {\n this.emit(\"message\", msg);\n if (msg.msgTypeStr !== \"unknown\") {\n this.emit(msg.msgTypeStr, msg as any);\n }\n }\n }\n }\n } catch (error: any) {\n // 4. 处理客户端超时 (正常现象,继续轮询)\n if (error.name === \"AbortError\" || error.message.includes(\"timeout\")) {\n // 仅仅是 35 秒内没人发消息而已,什么都不用做,继续进入下一个 while 循环\n continue;\n }\n\n // 处理真实的网络异常 (断网等),退避 3 秒后重试,防止 CPU 满载\n console.error(\n `[WeChatApi] ⚠️ 轮询遇到网络异常,3秒后重试: ${error.message}`,\n );\n await new Promise((resolve) =>\n setTimeout(resolve, POLLING_ERROR_RETRY_DELAY_MS)\n );\n }\n }\n\n console.log(\"[WeChatApi] 🛑 轮询守护进程已停止。\");\n }\n\n /**\n * 停止长轮询\n */\n public stopPolling() {\n this.isPolling = false;\n }\n}\n","// src/core/CryptoUtils.ts\n\nimport {\n createCipheriv,\n createDecipheriv,\n createHash,\n randomBytes,\n} from \"node:crypto\";\nimport { AES_BLOCK_SIZE, WECHAT_AES_ALGO } from \"../constants.ts\";\n\nexport class CryptoUtils {\n public static md5(buffer: Buffer): string {\n return createHash(\"md5\").update(buffer).digest(\"hex\");\n }\n\n public static generateRandomKey(length: number = 16): Buffer {\n return randomBytes(length);\n }\n\n public static getPaddedSize(plaintextSize: number): number {\n return Math.ceil((plaintextSize + 1) / AES_BLOCK_SIZE) * AES_BLOCK_SIZE;\n }\n\n public static aesEcbEncrypt(plaintext: Buffer, key: Buffer): Buffer {\n if (key.length !== AES_BLOCK_SIZE) {\n throw new Error(\n `[CryptoUtils] AES-128 密钥长度必须为 ${AES_BLOCK_SIZE} 字节,当前为 ${key.length} 字节`,\n );\n }\n const cipher = createCipheriv(WECHAT_AES_ALGO, key, null);\n return Buffer.concat([cipher.update(plaintext), cipher.final()]);\n }\n\n public static aesEcbDecrypt(ciphertext: Buffer, key: Buffer): Buffer {\n if (key.length !== AES_BLOCK_SIZE) {\n throw new Error(\n `[CryptoUtils] AES-128 密钥长度必须为 ${AES_BLOCK_SIZE} 字节,当前为 ${key.length} 字节`,\n );\n }\n const decipher = createDecipheriv(WECHAT_AES_ALGO, key, null);\n return Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n }\n\n /**\n * 生成去重的客户端消息 ID\n * 格式: prefix:timestamp-randomHex\n */\n public static generateClientId(): string {\n const randomHex = randomBytes(4).toString(\"hex\");\n return `wechat-bot:${Date.now()}-${randomHex}`;\n }\n\n /** 生成随机的 X-WECHAT-UIN (4字节随机数 -> uint32 -> base64) */\n public static randomWechatUin(): string {\n const uint32 = randomBytes(4).readUInt32BE(0);\n return Buffer.from(String(uint32), \"utf-8\").toString(\"base64\");\n }\n}\n","// src/constants.ts\n\nimport type {\n CdnManagerConfig,\n LoginOptions,\n SendMessageOptions,\n WeChatClientConfig,\n} from \"./types.ts\";\n\n// ====== Universal Constants ======\n\nexport const WECHAT_DEFAULT_BASE_URL = \"https://ilinkai.weixin.qq.com\";\nexport const WECHAT_DEFAULT_CDN_URL = \"https://cdn.weixin.qq.com\";\n\n// 获取二维码的普通 HTTP 请求超时 (10秒)\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 10000;\n// 微信极其经典的长轮询 (Long-Polling) 超时基准线 (35秒)\nexport const LONG_POLLING_TIMEOUT_MS = 35000;\n// 轮询接口返回错误时的重试间隔 (3秒)\nexport const POLLING_ERROR_RETRY_DELAY_MS = 3000;\n// 获取上传门票的接口超时时间 (15秒)\nexport const DEFAULT_API_TIMEOUT_MS = 15000;\n// 二维码硬性失效的兜底时间 (5分钟)\nexport const QR_CODE_EXPIRATION_MS = 5 * 60 * 1000;\n// 轮询失败或异常时的退避时间 (1秒 防御性延迟)\nexport const BACKOFF_BASE_MS = 1000;\n// 获取二维码超时的重试次数\nexport const DEFAULT_LOGIN_MAX_RETRIES = 3;\n\n// AES-128 的密钥长度和块大小(Block Size)。\nexport const AES_BLOCK_SIZE = 16;\n// 微信 CDN 加密算法的硬性规定。\nexport const WECHAT_AES_ALGO = \"aes-128-ecb\";\n// 协议层魔数。微信用 1 来代表特定的 CDN 加密方式(极大概率指代 AES-128-ECB)\nexport const WECHAT_CDN_ENCRYPT_TYPE = 1;\n\n// 微信语音的标准采样率。虽然现在是定义在文件顶部的常量,但它属于协议级常量。\nexport const WECHAT_SILK_SAMPLE_RATE = 24000;\n\nexport const WECHAT_HTTP_HEADERS = {\n AUTH_TYPE: \"AuthorizationType\",\n UIN: \"X-WECHAT-UIN\",\n APP_ID: \"iLink-App-Id\",\n CLIENT_VERSION: \"iLink-App-ClientVersion\",\n CONTENT_TYPE: \"Content-Type\",\n AUTHORIZATION: \"Authorization\",\n ERROR_MSG: \"x-error-message\",\n ENCRYPTED_PARAM: \"x-encrypted-param\",\n} as const;\n\nexport const WECHAT_PROTOCOL = {\n AUTH_TYPE_VALUE: \"ilink_bot_token\",\n CONTENT_TYPE_JSON: \"application/json\",\n DEFAULT_TIMEOUT_MS: 30000,\n} as const;\n\n// ====== default options =======\n\nexport const DEFAULT_CLIENT_CONFIG: WeChatClientConfig = {\n appId: \"bot\",\n version: \"2.1.1\",\n baseUrl: WECHAT_DEFAULT_BASE_URL,\n botType: \"3\",\n autoDownloadMedia: true,\n};\n\nexport const DEFAULT_CDN_CONFIG: CdnManagerConfig = {\n maxRetry: 3,\n cdnBaseUrl: WECHAT_DEFAULT_CDN_URL,\n apiTimeoutMs: DEFAULT_API_TIMEOUT_MS,\n backoffBaseMs: BACKOFF_BASE_MS,\n};\n\nexport const DEFAULT_LOGIN_OPTIONS: LoginOptions = {\n maxRetries: DEFAULT_LOGIN_MAX_RETRIES,\n pollTimeoutMs: LONG_POLLING_TIMEOUT_MS,\n onQrCode: (qrInfo) => {\n console.log(\"\\n==========================================\");\n console.log(\"请在浏览器中打开以下链接,并使用微信扫码:\");\n console.log(qrInfo.qrcodeUrl);\n console.log(\"==========================================\\n\");\n },\n onStatusChange: (payload) => {\n switch (payload.status) {\n case \"wait\":\n console.log(\"等待登录...\"); // 简易的 loading 动画\n break;\n case \"scaned\":\n console.log(\"\\n👀 已扫码,请在手机微信上点击确认登录...\");\n break;\n case \"confirmed\":\n console.log(\n \"\\n已链接, 但需手动在微信发送第一条消息后, bot 才能正常回复\",\n );\n break;\n case \"scaned_but_redirect\":\n console.log(`\\n[系统] ${payload.message}`);\n break;\n }\n },\n};\n\nexport const DEFAULT_SEND_OPTIONS: SendMessageOptions = {\n contextToken: undefined,\n userId: undefined,\n caption: undefined,\n};\n","// src/types.ts\n\nexport interface WeChatClientConfig {\n /** 你的应用 ID (对应原 config.json 的 ilink_appid) */\n appId: string;\n /** 客户端版本号字符串,如 \"1.0.11\" */\n version: string;\n /** 基础 API 地址,通常为 https://ilinkai.weixin.qq.com */\n baseUrl: string;\n /** 默认机器人类型, 为 \"3\" */\n botType: string;\n /** 机器人的登录凭证,登录前可为空 */\n token?: string;\n /** 收到图片/文件/视频时,是否在后台自动下载解密到内存中?默认 true */\n autoDownloadMedia: boolean;\n /** 用户 ID */\n userId?: string;\n}\n\nexport interface CdnManagerConfig {\n // CDN 物理传输极易受到网络波动影响,固定死 3 次在网络差的环境下可能不够,在极速环境下又显得多余\n maxRetry: number;\n // \"https://cdn.weixin.qq.com\";\n cdnBaseUrl: string;\n apiTimeoutMs: number;\n backoffBaseMs: number;\n}\n\nexport interface SendMessageOptions {\n /** * 上下文 Token。用于在特定的会话上下文中回复消息\n */\n contextToken?: string;\n /** * 接收消息的用户 ID (微信 ID),目前仅对自身账号有效,如果不提供,SDK 会尝试使用登录用户的 ID 作为默认值\n */\n userId?: string;\n /** * 媒体附件的文字说明。\n * 注意:微信不支持图文混合在同一个 Item 中,\n * 如果提供此参数,SDK 会先发送一条文本消息,紧接着发送媒体消息。\n */\n caption?: string;\n}\n\n/** 核心引擎接收的请求参数 */\nexport interface CoreRequestOptions {\n method: \"GET\" | \"POST\";\n /** 纯业务请求体,无需包含 base_info */\n body?: Record<string, any>;\n /** 超时时间 (毫秒) */\n timeoutMs: number;\n}\n\nexport interface QrCodeInfo {\n qrcodeId: string;\n qrcodeUrl: string; // 用于生成图片的原始字符串/链接\n}\n\nexport interface LoginCredentials {\n token: string;\n baseUrl: string;\n accountId: string;\n userId: string;\n}\n\nexport type LogStatusChangePayload =\n | {\n status: LoginStatus.WAIT | LoginStatus.SCANNED | LoginStatus.EXPIRED;\n message: string;\n }\n | {\n status: LoginStatus.REDIRECT;\n message: string;\n redirect_host: string; // 服务器要求重定向的新 host\n }\n | {\n status: LoginStatus.CONFIRMED;\n message: string;\n bot_token: string; // 登录成功后返回的 token\n baseurl?: string; // 登录成功后返回的 baseUrl(如果有的话)\n ilink_bot_id: string; // 登录成功后返回的 bot 账号 ID\n ilink_user_id: string; // 登录成功后返回的用户 ID\n };\n\nexport interface LoginOptions {\n /** 当获取到新二维码时触发 (如果二维码过期会自动刷新并再次触发) */\n onQrCode: (qrInfo: QrCodeInfo) => void;\n /** 当轮询状态改变时触发 (可选) */\n onStatusChange: (payload: LogStatusChangePayload) => void;\n /** 二维码过期后最大重试次数,默认 3 */\n maxRetries: number;\n /** 单次长轮询的超时时间,默认 35000ms */\n pollTimeoutMs: number;\n}\n\nexport interface SendResult {\n clientId: string;\n response: any;\n}\n\n/** 内部流转的 CDN 文件凭据 (CdnManager 产出,MessageManager 消费) */\nexport interface CdnFileTicket {\n filekey: string;\n aeskeyHex: string; // 给 MessageManager 拼 JSON 用的 hex\n aeskeyBuffer: Buffer; // 未来下载解密用的 buffer\n fileSizePlain: number; // 明文大小\n fileSizeCipher: number; // 加密后大小\n encryptedQueryParam: string; // 核心下载参数\n}\n\n/** 统一的 CDN 下载票据 (抹平了图片、文件、视频的差异) */\nexport interface CdnDownloadTicket {\n fullUrl?: string; // 优先级 1\n encryptedQueryParam?: string; // 优先级 2\n aesKeyBase64?: string; // 密钥 (已统一转为 base64 处理好的)\n isPlain: boolean; // 是否是明文传输 (针对某些没有 aes_key 的图片)\n originalFileName?: string; // 针对文件\n}\n\nexport type ItemTypeStr =\n | \"text\"\n | \"image\"\n | \"video\"\n | \"file\"\n | \"voice\"\n | \"unknown\";\n\n// 对外暴露的消息对象结构\nexport interface WeChatIncomingMessage<\n T extends RawMessageItemBase = RawMessageItem,\n> {\n messageId: string;\n seq: number;\n fromUserId: string;\n toUserId: string;\n timestamp: number;\n contextToken: string; // 回复消息时必须带上这个字段\n msgType: T[\"type\"];\n msgTypeStr: ItemTypeStr; // 方便使用者直接判断类型的字符串版本\n /** 纯文本内容 */\n text?: string;\n /** 文件名 */\n fileName?: string;\n /**\n * 获取媒体文件的二进制 Buffer。\n * 如果开启了 autoDownloadMedia,此方法瞬间返回内存中的 Buffer。\n * 如果关闭了,此方法会发起网络请求下载并解密,然后缓存。\n */\n getBuffer?: () => Promise<Buffer | null>;\n /** 获取语音消息的二进制 Buffer, 这是原始的 SILK 编码 */\n getVoiceBuffer?: () => Promise<Buffer | null>;\n /**\n * 快捷方法:将媒体文件保存到本地磁盘。\n * @param savePath 指定绝对或相对路径\n */\n saveToFile?: (savePath: string) => Promise<string>;\n /** 原始底层票据 */\n mediaTicket?: CdnDownloadTicket;\n /** 消息在同一 seq 中的索引,方便开发者处理多 Item 的情况 */\n index: number;\n /** 原始消息载荷,给高级玩家使用 */\n raw: RawMessage<T>;\n}\n\nexport const enum MessageType {\n NONE = 0,\n USER = 1,\n BOT = 2,\n}\n\nexport const enum MessageItemType {\n NONE = 0,\n TEXT = 1,\n IMAGE = 2,\n VOICE = 3,\n FILE = 4,\n VIDEO = 5,\n}\n\nexport const enum MessageState {\n NEW = 0,\n GENERATING = 1,\n FINISH = 2,\n}\n\nexport const enum UploadMediaType {\n IMAGE = 1,\n VIDEO = 2,\n FILE = 3,\n VOICE = 4,\n}\n\nexport interface PollingResult {\n ret?: number | -2;\n errcode?: number;\n errmsg?: string;\n get_updates_buf?: string;\n msgs?: RawMessage[];\n}\n\nexport interface RawMessage<T extends RawMessageItemBase = RawMessageItem> {\n seq: number;\n message_id: number;\n from_user_id: string;\n to_user_id: string;\n client_id: string;\n create_time_ms: number;\n update_time_ms: number;\n delete_time_ms: number | 0;\n session_id: string | \"\";\n group_id: string | \"\";\n message_type: MessageType;\n message_state: MessageState;\n item_list: T[];\n context_token: string;\n}\n\nexport type RawMessageItem =\n | RawMessageTextItem\n | RawMessageFileItem\n | RawMessageImageItem\n | RawMessageVideoItem\n | RawMessageVoiceItem;\n\nexport interface RawMessageItemBase {\n type: MessageItemType;\n create_time_ms: number;\n update_time_ms: number;\n is_completed: boolean;\n}\n\nexport interface RawMessageTextItem extends RawMessageItemBase {\n type: MessageItemType.TEXT;\n text_item: { text: string };\n}\n\nexport interface RawMessageFileItem extends RawMessageItemBase {\n type: MessageItemType.FILE;\n file_item: {\n media: RawMessageMedia;\n file_name: string;\n md5: string;\n len: string;\n };\n}\n\nexport interface RawMessageImageItem extends RawMessageItemBase {\n type: MessageItemType.IMAGE;\n image_item: {\n url: string;\n aeskey: string;\n media: RawMessageMedia;\n mid_size: number;\n thumb_size: number;\n thumb_height: number;\n thumb_width: number;\n hd_size: number;\n };\n}\n\nexport interface RawMessageVideoItem extends RawMessageItemBase {\n type: MessageItemType.VIDEO;\n video_item: {\n media: RawMessageMedia;\n video_size: number;\n play_length: number;\n video_md5: string;\n thumb_media: RawMessageMedia;\n thumb_size: number;\n thumb_height: number;\n thumb_width: number;\n };\n}\n\nexport interface RawMessageVoiceItem extends RawMessageItemBase {\n type: MessageItemType.VOICE;\n voice_item: {\n media: RawMessageMedia;\n encode_type: number | 4;\n bits_per_sample: number | 16;\n sample_rate: number | 16000;\n playtime: number;\n text: string | \"\";\n };\n}\n\nexport interface RawMessageMedia {\n encrypt_query_param: string;\n aes_key: string;\n full_url: string;\n}\n\nexport interface WeChatApiEventMap {\n login: [credentials: LoginCredentials];\n message: [msg: WeChatIncomingMessage];\n text: [msg: WeChatIncomingMessage<RawMessageTextItem>];\n file: [msg: WeChatIncomingMessage<RawMessageFileItem>];\n image: [msg: WeChatIncomingMessage<RawMessageImageItem>];\n video: [msg: WeChatIncomingMessage<RawMessageVideoItem>];\n voice: [msg: WeChatIncomingMessage<RawMessageVoiceItem>];\n error: [error: Error];\n}\n\nexport const enum LoginStatus {\n WAIT = \"wait\",\n SCANNED = \"scaned\", // 保持和微信接口错别字一致\n REDIRECT = \"scaned_but_redirect\",\n CONFIRMED = \"confirmed\",\n EXPIRED = \"expired\",\n}\n","// src/core/utils.ts\nimport {\n type ItemTypeStr,\n MessageItemType,\n type RawMessageItem,\n} from \"../types.ts\";\n\nexport function mergeObjects<T>(ref: T, ...sources: Partial<T>[]): T {\n const target = { ...ref };\n for (const source of sources) {\n for (const key in source) {\n if (source[key] !== undefined) {\n target[key] = source[key] as T[Extract<keyof T, string>];\n }\n }\n }\n return target;\n}\n\nexport function getItemType(item: MessageItemType): ItemTypeStr {\n switch (item) {\n case MessageItemType.TEXT:\n return \"text\";\n case MessageItemType.IMAGE:\n return \"image\";\n case MessageItemType.VIDEO:\n return \"video\";\n case MessageItemType.FILE:\n return \"file\";\n case MessageItemType.VOICE:\n return \"voice\";\n default:\n return \"unknown\";\n }\n}\n\n/** 将 \"2.1.1\" 等版本号转换为 API 要求的数字位运算格式 */\nexport function buildClientVersion(version: string): number {\n const parts = version.split(\".\").map((p) => parseInt(p, 10));\n const major = parts[0] || 0;\n const minor = parts[1] || 0;\n const patch = parts[2] || 0;\n return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);\n}\n\nexport function getFileImageItem(item: RawMessageItem) {\n if (\"file_item\" in item) return item.file_item;\n if (\"image_item\" in item) return item.image_item;\n if (\"voice_item\" in item) return item.voice_item;\n if (\"video_item\" in item) return item.video_item;\n}\n\nexport function isItemWithMedia(\n item: MessageItemType,\n): item is\n | MessageItemType.FILE\n | MessageItemType.IMAGE\n | MessageItemType.VOICE\n | MessageItemType.VIDEO {\n switch (item) {\n case MessageItemType.IMAGE:\n case MessageItemType.VOICE:\n case MessageItemType.FILE:\n case MessageItemType.VIDEO:\n return true;\n case MessageItemType.NONE:\n case MessageItemType.TEXT:\n default:\n return false;\n }\n}\n","// src/core/WeChatCore.ts\nimport { CryptoUtils } from \"./CryptoUtils.ts\";\nimport { buildClientVersion, mergeObjects } from \"./utils.ts\";\nimport type { CoreRequestOptions, WeChatClientConfig } from \"../types.ts\";\nimport {\n DEFAULT_CLIENT_CONFIG,\n WECHAT_HTTP_HEADERS,\n WECHAT_PROTOCOL,\n} from \"../constants.ts\";\n\nexport class WeChatCore {\n private config: WeChatClientConfig;\n private clientVersionInt: number;\n\n constructor(config: WeChatClientConfig) {\n this.config = mergeObjects(DEFAULT_CLIENT_CONFIG, config);\n // 预计算版本号整数,避免每次发请求都算一遍\n this.clientVersionInt = buildClientVersion(config.version);\n }\n\n // ==========================================\n // 状态修改器 (Mutators)\n // ==========================================\n\n /** 设置或更新登录凭证 */\n public setToken(token: string): void {\n this.config.token = token;\n }\n\n /** 更新基础网关 URL (用于处理 IDC 重定向) */\n public setBaseUrl(baseUrl: string): void {\n this.config.baseUrl = baseUrl;\n }\n\n public setUserId(userId: string): void {\n this.config.userId = userId;\n }\n\n /** 获取当前的 BaseUrl (供某些需要拼接绝对路径的特殊场景使用) */\n public getBaseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** 获取当前的机器人类型 */\n public getBotType(): string {\n return this.config.botType;\n }\n\n /** 获取当前的用户 ID */\n public getUserId(): string | undefined {\n return this.config.userId;\n }\n\n // ==========================================\n // 核心请求发射器\n // ==========================================\n\n /**\n * 统一的 HTTP 请求方法\n * @param endpoint 接口路径,例如 \"ilink/bot/sendmessage\"\n * @param options 请求配置 (方法、请求体、超时设置)\n */\n public async request<T>(\n endpoint: string,\n options: CoreRequestOptions,\n ): Promise<T> {\n // 1. URL 拼接处理\n const base = this.config.baseUrl.endsWith(\"/\")\n ? this.config.baseUrl\n : `${this.config.baseUrl}/`;\n const url = new URL(endpoint, base);\n\n // 2. 构造最终的请求体 (如果是 POST,自动注入 base_info)\n let finalBodyString: string | undefined = undefined;\n\n if (options.method === \"POST\" && options.body) {\n // 浅拷贝一层,防止污染调用方传入的原始对象\n const payload = { ...options.body };\n payload.base_info = { channel_version: this.config.version };\n finalBodyString = JSON.stringify(payload);\n } else if (options.method === \"POST\" && !options.body) {\n // 即使没有具体业务参数,POST 请求通常也需要 base_info\n finalBodyString = JSON.stringify({\n base_info: { channel_version: this.config.version },\n });\n }\n\n // 3. 构造请求头\n const headers = this.buildHeaders(finalBodyString);\n\n // 4. 设置超时控制器\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);\n\n // 5. 执行请求\n try {\n const response = await fetch(url.toString(), {\n method: options.method,\n headers: headers,\n body: finalBodyString,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n const rawText = await response.text();\n\n // 非 200 状态码直接抛出异常,交给上层业务处理\n if (!response.ok) {\n throw new Error(\n `WeChat API Error [${options.method} ${endpoint}] HTTP ${response.status}: ${rawText}`,\n );\n }\n\n // 如果有返回值则解析,无返回值(如 sendmessage)则返回空对象\n return rawText ? JSON.parse(rawText) as T : ({} as T);\n } catch (err) {\n clearTimeout(timeoutId);\n // 将底层的网络异常原样抛出,如果是 AbortError (超时),上层(如长轮询)可以特别捕获它\n throw err;\n }\n }\n\n // ==========================================\n // 私有辅助方法\n // ==========================================\n\n /** 构造标准请求头 */\n private buildHeaders(bodyString?: string): Record<string, string> {\n const headers: Record<string, string> = {\n [WECHAT_HTTP_HEADERS.CONTENT_TYPE]: WECHAT_PROTOCOL.CONTENT_TYPE_JSON,\n [WECHAT_HTTP_HEADERS.AUTH_TYPE]: WECHAT_PROTOCOL.AUTH_TYPE_VALUE,\n [WECHAT_HTTP_HEADERS.UIN]: CryptoUtils.randomWechatUin(),\n [WECHAT_HTTP_HEADERS.APP_ID]: this.config.appId,\n [WECHAT_HTTP_HEADERS.CLIENT_VERSION]: String(this.clientVersionInt),\n };\n\n if (bodyString) {\n headers[\"Content-Length\"] = String(\n Buffer.byteLength(bodyString, \"utf-8\"),\n );\n }\n\n if (this.config.token?.trim()) {\n headers[\"Authorization\"] = `Bearer ${this.config.token.trim()}`;\n }\n\n return headers;\n }\n}\n","// src/managers/AuthManager.ts\nimport {\n BACKOFF_BASE_MS,\n DEFAULT_LOGIN_MAX_RETRIES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n LONG_POLLING_TIMEOUT_MS,\n QR_CODE_EXPIRATION_MS,\n} from \"../constants.ts\";\nimport { WeChatCore } from \"../core/WeChatCore.ts\";\nimport {\n type LoginCredentials,\n type LoginOptions,\n LoginStatus,\n type QrCodeInfo,\n} from \"../types.ts\";\n\nexport class AuthManager {\n private core: WeChatCore;\n\n constructor(core: WeChatCore) {\n this.core = core;\n }\n\n // ==========================================\n // 细粒度 API: 第一步 - 获取二维码\n // ==========================================\n public async getQrCode(): Promise<QrCodeInfo> {\n // 强制使用默认的登录网关获取二维码\n this.core.setBaseUrl(this.core.getBaseUrl()); // 触发内部状态更新,确保使用默认 BaseUrl\n\n const response = await this.core.request<any>(\n `ilink/bot/get_bot_qrcode?bot_type=${this.core.getBotType()}`,\n { method: \"GET\", timeoutMs: DEFAULT_REQUEST_TIMEOUT_MS },\n );\n\n return {\n qrcodeId: response.qrcode,\n qrcodeUrl: response.qrcode_img_content,\n };\n }\n\n // ==========================================\n // 细粒度 API: 第二步 - 单次轮询状态\n // ==========================================\n public async pollLoginStatus(\n qrcodeId: string,\n timeoutMs = LONG_POLLING_TIMEOUT_MS,\n ): Promise<{ status: LoginStatus; data?: any }> {\n try {\n const response = await this.core.request<any>(\n `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcodeId)}`,\n { method: \"GET\", timeoutMs },\n );\n return { status: response.status, data: response };\n } catch (err: any) {\n // 区分普通网络错误和长轮询的正常超时 (AbortError)\n if (err.name === \"AbortError\") {\n return { status: LoginStatus.WAIT }; // 超时视为继续等待\n }\n throw err;\n }\n }\n\n // ==========================================\n // 粗粒度 API: 一键自动托管登录\n // ==========================================\n public async login(options: LoginOptions): Promise<LoginCredentials> {\n const maxRetries = options.maxRetries ?? DEFAULT_LOGIN_MAX_RETRIES;\n let currentRetry = 0;\n\n while (currentRetry <= maxRetries) {\n // 1. 获取二维码\n const qrInfo = await this.getQrCode();\n options.onQrCode(qrInfo);\n\n // 2. 开始持续轮询该二维码\n // 设置一个总超时,防止死循环 (比如 5 分钟二维码一定会过期)\n const qrDeadline = Date.now() + QR_CODE_EXPIRATION_MS;\n\n while (Date.now() < qrDeadline) {\n const { status, data } = await this.pollLoginStatus(\n qrInfo.qrcodeId,\n options.pollTimeoutMs,\n );\n\n switch (status) {\n case LoginStatus.WAIT:\n options.onStatusChange?.({\n status: LoginStatus.WAIT,\n message: \"等待扫码...\",\n });\n break;\n\n case LoginStatus.SCANNED:\n options.onStatusChange?.({\n status: LoginStatus.SCANNED,\n message: \"已扫码,请在手机端确认...\",\n });\n break;\n\n case LoginStatus.REDIRECT:\n if (data.redirect_host) {\n const newBaseUrl = `https://${data.redirect_host}`;\n this.core.setBaseUrl(newBaseUrl); // 关键:更新底层引擎的 BaseUrl\n options.onStatusChange?.({\n status: LoginStatus.REDIRECT,\n message: `服务器要求重定向至: ${newBaseUrl}`,\n redirect_host: data.redirect_host,\n });\n }\n break;\n\n case LoginStatus.CONFIRMED:\n options.onStatusChange?.({\n status: LoginStatus.CONFIRMED,\n message: \"登录成功!\",\n bot_token: data.bot_token,\n baseurl: data.baseurl,\n ilink_bot_id: data.ilink_bot_id,\n ilink_user_id: data.ilink_user_id,\n });\n\n // 关键:自动将获取到的凭据注入到底层引擎中\n this.core.setToken(data.bot_token);\n this.core.setUserId(data.ilink_user_id);\n if (data.baseurl) {\n this.core.setBaseUrl(data.baseurl);\n }\n\n // 返回凭据给开发者,以便他们持久化存储\n return {\n token: data.bot_token,\n baseUrl: data.baseurl || this.core.getBaseUrl(),\n accountId: data.ilink_bot_id,\n userId: data.ilink_user_id,\n };\n\n case LoginStatus.EXPIRED:\n options.onStatusChange?.({\n status: LoginStatus.EXPIRED,\n message: \"二维码已过期,正在重新获取...\",\n });\n // 跳出内层轮询,触发外层循环重新获取二维码\n break;\n\n default:\n // 未知状态,稍作延迟后继续尝试\n await new Promise((res) => setTimeout(res, BACKOFF_BASE_MS));\n }\n\n if (status === LoginStatus.EXPIRED) {\n break; // 跳出内层 while 循环\n }\n\n // 防御性延迟,避免请求过于频繁\n await new Promise((res) => setTimeout(res, BACKOFF_BASE_MS));\n }\n\n currentRetry++;\n }\n\n throw new Error(\n `登录失败:二维码已过期并超过最大重试次数 (${maxRetries}次)。`,\n );\n }\n}\n","// src/managers/MessageManager.ts\n\nimport { readFile } from \"node:fs/promises\";\nimport { basename } from \"node:path\";\n\nimport { mergeObjects } from \"../core/utils.ts\";\nimport {\n DEFAULT_API_TIMEOUT_MS,\n DEFAULT_SEND_OPTIONS,\n WECHAT_CDN_ENCRYPT_TYPE,\n} from \"../constants.ts\";\nimport { CryptoUtils } from \"../core/CryptoUtils.ts\";\nimport {\n MessageItemType,\n MessageState,\n MessageType,\n type SendMessageOptions,\n type SendResult,\n UploadMediaType,\n} from \"../types.ts\";\nimport type { WeChatCore } from \"../core/WeChatCore.ts\";\nimport type { CdnManager } from \"./CdnManager.ts\";\n\nexport class MessageManager {\n private core: WeChatCore;\n private cdn: CdnManager;\n\n constructor(core: WeChatCore, cdn: CdnManager) {\n this.core = core;\n this.cdn = cdn;\n }\n\n // ==========================================\n // 公开 API: 发送纯文本\n // ==========================================\n\n public async sendText(\n text: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const textItem = {\n type: MessageItemType.TEXT,\n text_item: { text },\n };\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendRawItem(\n textItem,\n mergedOptions.userId!,\n mergedOptions.contextToken,\n );\n }\n\n // ==========================================\n // 公开 API: 发送媒体文件\n // ==========================================\n\n public async sendImage(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.IMAGE,\n mergedOptions,\n );\n }\n\n public async sendVideo(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.VIDEO,\n mergedOptions,\n );\n }\n\n /** - 仅允许发送文件, 发送**图片或视频**会导致发送失败!\n * - 发送图片和视频应使用 `sendImage` 或 `sendVideo` */\n public async sendFile(\n filePath: string,\n options: Partial<SendMessageOptions> = DEFAULT_SEND_OPTIONS,\n ): Promise<SendResult> {\n const mergedOptions = mergeObjects(DEFAULT_SEND_OPTIONS, {\n userId: this.core.getUserId(),\n }, options);\n return this._sendMediaWorkflow(\n filePath,\n UploadMediaType.FILE,\n mergedOptions,\n );\n }\n\n // ==========================================\n // 私有核心流水线\n // ==========================================\n\n /**\n * 统一的媒体发送工作流:读取本地文件 -> 上传 CDN -> (可选发送 Caption) -> 发送媒体消息\n */\n private async _sendMediaWorkflow(\n filePath: string,\n mediaType: UploadMediaType,\n options: SendMessageOptions,\n ): Promise<SendResult> {\n // 1. 读取本地文件为 Buffer\n const buffer = await readFile(filePath);\n const fileName = basename(filePath);\n\n // 2. 调用 CdnManager 上传至微信服务器,获取票据\n const ticket = await this.cdn.uploadBuffer(\n buffer,\n options.userId!,\n mediaType,\n );\n\n // 3. 如果用户传了 caption,先发送一条纯文本消息\n if (options.caption) {\n await this.sendText(options.caption, {\n contextToken: options.contextToken,\n });\n }\n\n // 4. 组装媒体 Item 载荷\n const mediaObj = {\n encrypt_query_param: ticket.encryptedQueryParam,\n aes_key: Buffer.from(ticket.aeskeyHex).toString(\"base64\"), // JSON 中要求 base64 格式\n encrypt_type: WECHAT_CDN_ENCRYPT_TYPE,\n };\n\n let messageItem: any = {};\n\n switch (mediaType) {\n case UploadMediaType.IMAGE:\n messageItem = {\n type: MessageItemType.IMAGE,\n image_item: { media: mediaObj, mid_size: ticket.fileSizeCipher },\n };\n break;\n case UploadMediaType.VIDEO:\n messageItem = {\n type: MessageItemType.VIDEO,\n video_item: { media: mediaObj, video_size: ticket.fileSizeCipher },\n };\n break;\n case UploadMediaType.FILE:\n messageItem = {\n type: MessageItemType.FILE,\n file_item: {\n media: mediaObj,\n file_name: fileName,\n len: String(ticket.fileSizePlain),\n }, // 注意 len 是明文大小的字符串\n };\n break;\n default:\n throw new Error(`[MessageManager] 不支持的媒体类型: ${mediaType}`);\n }\n\n // 5. 将组装好的媒体 Item 发送出去\n return this._sendRawItem(\n messageItem,\n options.userId!,\n options.contextToken,\n );\n }\n\n /**\n * 最底层的发包函数,将组装好的 Item 包装成完整的微信请求体并 POST\n */\n private async _sendRawItem(\n itemObj: any,\n userId: string,\n contextToken?: string,\n ): Promise<SendResult> {\n const clientId = CryptoUtils.generateClientId();\n\n const requestBody: any = {\n msg: {\n from_user_id: \"\", // 留空,微信网关会自动通过 Bearer Token 识别机器人身份\n to_user_id: userId,\n client_id: clientId,\n message_type: MessageType.BOT,\n message_state: MessageState.FINISH,\n context_token: contextToken,\n item_list: [itemObj],\n },\n };\n\n const response = await this.core.request(\"ilink/bot/sendmessage\", {\n method: \"POST\",\n body: requestBody,\n timeoutMs: DEFAULT_API_TIMEOUT_MS,\n });\n\n return { clientId, response };\n }\n}\n","// src/managers/CdnManager.ts\nimport { WeChatCore } from \"../core/WeChatCore.ts\";\nimport { CryptoUtils } from \"../core/CryptoUtils.ts\";\nimport {\n AES_BLOCK_SIZE,\n DEFAULT_CDN_CONFIG,\n WECHAT_HTTP_HEADERS,\n} from \"../constants.ts\";\nimport type {\n CdnDownloadTicket,\n CdnFileTicket,\n CdnManagerConfig,\n UploadMediaType,\n} from \"../types.ts\";\nimport { mergeObjects } from \"../core/utils.ts\";\n\nexport class CdnManager {\n private core: WeChatCore;\n private config: CdnManagerConfig;\n\n constructor(\n core: WeChatCore,\n config: Partial<CdnManagerConfig> = DEFAULT_CDN_CONFIG,\n ) {\n this.core = core;\n this.config = mergeObjects(DEFAULT_CDN_CONFIG, config);\n }\n\n // ==========================================\n // 核心流水线:上传二进制流到 CDN\n // ==========================================\n\n /**\n * 将任意 Buffer 上传至微信 CDN,并返回发送消息所需的凭证\n * @param buffer 文件的二进制原始数据\n * @param toUserId 接收人的微信 ID (CDN 强制要求绑定)\n * @param mediaType 媒体类型 (IMAGE, VIDEO, FILE, VOICE)\n */\n public async uploadBuffer(\n buffer: Buffer,\n toUserId: string,\n mediaType: UploadMediaType,\n ): Promise<CdnFileTicket> {\n // --------------------------------------------------\n // Step 1: 准备元数据与加密密钥\n // --------------------------------------------------\n const rawsize = buffer.length;\n const rawfilemd5 = CryptoUtils.md5(buffer);\n const filesize = CryptoUtils.getPaddedSize(rawsize); // AES 加密后的对齐大小\n\n // 微信要求 aeskey 和 filekey 都是 16 字节\n const filekey = CryptoUtils.generateRandomKey(AES_BLOCK_SIZE).toString(\n \"hex\",\n );\n const aeskeyBuffer = CryptoUtils.generateRandomKey(AES_BLOCK_SIZE);\n const aeskeyHex = aeskeyBuffer.toString(\"hex\");\n\n // --------------------------------------------------\n // Step 2: 访问控制面,申请上传门票 (Upload URL)\n // --------------------------------------------------\n const uploadUrlResp = await this.core.request<any>(\n \"ilink/bot/getuploadurl\",\n {\n method: \"POST\",\n body: {\n filekey,\n media_type: mediaType,\n to_user_id: toUserId,\n rawsize,\n rawfilemd5,\n filesize,\n no_need_thumb: true, // 📝 [待重构]: 暂时忽略缩略图逻辑,全部设为 true\n aeskey: aeskeyHex,\n },\n timeoutMs: this.config.apiTimeoutMs,\n },\n );\n\n // 解析出真实的 CDN 地址\n // 微信有时候返回完整的 URL (upload_full_url),有时候只返回 query 参数 (upload_param)\n let cdnUrl = \"\";\n if (uploadUrlResp.upload_full_url?.trim()) {\n cdnUrl = uploadUrlResp.upload_full_url.trim();\n } else if (uploadUrlResp.upload_param) {\n cdnUrl = `${this.config.cdnBaseUrl}/upload?encrypted_query_param=${\n encodeURIComponent(uploadUrlResp.upload_param)\n }&filekey=${encodeURIComponent(filekey)}`;\n } else {\n throw new Error(\n `[CdnManager] 获取上传地址失败,API 响应缺少 full_url 或 upload_param`,\n );\n }\n\n // --------------------------------------------------\n // Step 3: 数据面加密\n // --------------------------------------------------\n const ciphertextBuffer = CryptoUtils.aesEcbEncrypt(buffer, aeskeyBuffer);\n\n // --------------------------------------------------\n // Step 4: 物理传输至 CDN (带重试机制)\n // --------------------------------------------------\n const encryptedQueryParam = await this.postToCdn(ciphertextBuffer, cdnUrl);\n\n // --------------------------------------------------\n // Step 5: 组装凭证交付给 MessageManager\n // --------------------------------------------------\n return {\n filekey,\n aeskeyHex,\n aeskeyBuffer,\n fileSizePlain: rawsize,\n fileSizeCipher: filesize,\n encryptedQueryParam,\n };\n }\n\n // ==========================================\n // 私有发包器:专门应对奇葩的 CDN 接口\n // ==========================================\n\n /**\n * 将密文 POST 到 CDN,并从 Response Header 中抠出下载参数。\n * 包含 4xx 直接报错、5xx 重试的逻辑。\n */\n private async postToCdn(ciphertext: Buffer, cdnUrl: string): Promise<string> {\n const MAX_RETRIES = this.config.maxRetry;\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(cdnUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/octet-stream\" },\n body: new Uint8Array(ciphertext),\n });\n\n // 1. 客户端错误 (4xx):比如鉴权失败、参数错误,重试也没用,直接抛出\n if (res.status >= 400 && res.status < 500) {\n const errMsg = res.headers.get(WECHAT_HTTP_HEADERS.ERROR_MSG) ??\n (await res.text());\n throw new Error(`[CdnClientError] HTTP ${res.status}: ${errMsg}`);\n }\n\n // 2. 服务端错误 (5xx):比如网关超时,尝试重试\n if (res.status !== 200) {\n const errMsg = res.headers.get(WECHAT_HTTP_HEADERS.ERROR_MSG) ??\n `HTTP ${res.status}`;\n throw new Error(`[CdnServerError]: ${errMsg}`);\n }\n\n // 3. 成功!从 Header 中寻找那把“钥匙”\n const downloadParam = res.headers.get(\n WECHAT_HTTP_HEADERS.ENCRYPTED_PARAM,\n );\n if (!downloadParam) {\n throw new Error(\n `[CdnManager] 上传成功,但响应头中缺少 ${WECHAT_HTTP_HEADERS.ENCRYPTED_PARAM}`,\n );\n }\n\n return downloadParam;\n } catch (err: any) {\n lastError = err;\n\n // 如果是 4xx 错误,立刻中断循环,不进行无意义的重试\n if (err.message && err.message.includes(\"[CdnClientError]\")) {\n throw err;\n }\n\n // 否则等待一会儿继续重试 (简易退避)\n if (attempt < MAX_RETRIES) {\n await new Promise((resolve) =>\n setTimeout(resolve, this.config.backoffBaseMs * attempt)\n );\n }\n }\n }\n\n throw new Error(\n `[CdnManager] CDN 上传失败,已重试 ${MAX_RETRIES} 次。最后错误: ${\n (lastError as Error)?.message\n }`,\n );\n }\n\n // ==========================================\n // 核心流水线:从 CDN 下载并解密\n // ==========================================\n\n /**\n * 唯一对内暴露的下载引擎\n * 负责拉取字节流并根据协议规范进行解密\n */\n public async downloadBuffer(ticket: CdnDownloadTicket): Promise<Buffer> {\n // 1. 确定最终的下载 URL (优先使用 fullUrl)\n let url = \"\";\n if (ticket.fullUrl) {\n url = ticket.fullUrl;\n } else if (ticket.encryptedQueryParam) {\n // 如果没有 fullUrl,就按照规则拼接\n url = `${this.config.cdnBaseUrl}/download?encrypted_query_param=${\n encodeURIComponent(ticket.encryptedQueryParam)\n }`;\n } else {\n throw new Error(\n \"[CdnManager] 下载失败:缺少 fullUrl 和 encryptedQueryParam\",\n );\n }\n\n // 2. 发起网络请求,拉取原始的 ArrayBuffer\n const encryptedBuffer = await this.fetchCdnBytes(url);\n\n // 3. 明文回退:如果协议表明这是明文传输(没有密钥),直接返回\n if (ticket.isPlain || !ticket.aesKeyBase64) {\n return encryptedBuffer;\n }\n\n // 4. 解析变态的微信 AES 密钥\n const aesKey = this.parseWechatAesKey(ticket.aesKeyBase64);\n\n // 5. 使用密码学工具箱进行 AES-128-ECB 解密\n return CryptoUtils.aesEcbDecrypt(encryptedBuffer, aesKey);\n }\n\n // ==========================================\n // 私有辅助方法\n // ==========================================\n\n /**\n * 处理微信极其不一致的密钥编码:\n * 情况A: base64( raw 16 bytes )\n * 情况B: base64( hex string of 16 bytes )\n */\n private parseWechatAesKey(aesKeyBase64: string): Buffer {\n const decoded = Buffer.from(aesKeyBase64, \"base64\");\n\n // 如果解密出来正好是 16 字节的二进制,直接用\n if (decoded.length === 16) {\n return decoded;\n }\n\n // 如果解密出来是 32 个字符,并且全是十六进制字符,说明它被二次 Hex 编码了\n if (\n decoded.length === 32 &&\n /^[0-9a-fA-F]{32}$/i.test(decoded.toString(\"ascii\"))\n ) {\n return Buffer.from(decoded.toString(\"ascii\"), \"hex\");\n }\n\n throw new Error(\n `[CdnManager] 无法解析的 AES 密钥格式 (Base64=\"${aesKeyBase64}\")`,\n );\n }\n\n /**\n * 原生 fetch 拉取二进制字节流\n */\n private async fetchCdnBytes(url: string): Promise<Buffer> {\n const res = await fetch(url);\n if (!res.ok) {\n const body = await res.text().catch(() => \"(unreadable)\");\n throw new Error(\n `[CdnManager] CDN 下载网络错误 HTTP ${res.status}: ${body}`,\n );\n }\n return Buffer.from(await res.arrayBuffer());\n }\n}\n","// src/core/SilkConverter.ts\n\nimport { WECHAT_SILK_SAMPLE_RATE } from \"../constants.ts\";\n\n/**\n * Wrap raw pcm_s16le bytes in a WAV container.\n * Mono channel, 16-bit signed little-endian.\n */\nfunction pcmBytesToWav(pcm: Uint8Array, sampleRate: number): Buffer {\n const pcmBytes = pcm.byteLength;\n const totalSize = 44 + pcmBytes;\n const buf = Buffer.allocUnsafe(totalSize);\n let offset = 0;\n\n buf.write(\"RIFF\", offset);\n offset += 4;\n buf.writeUInt32LE(totalSize - 8, offset);\n offset += 4;\n buf.write(\"WAVE\", offset);\n offset += 4;\n\n buf.write(\"fmt \", offset);\n offset += 4;\n buf.writeUInt32LE(16, offset);\n offset += 4; // fmt chunk size\n buf.writeUInt16LE(1, offset);\n offset += 2; // PCM format\n buf.writeUInt16LE(1, offset);\n offset += 2; // mono\n buf.writeUInt32LE(sampleRate, offset);\n offset += 4;\n buf.writeUInt32LE(sampleRate * 2, offset);\n offset += 4; // byte rate (mono 16-bit)\n buf.writeUInt16LE(2, offset);\n offset += 2; // block align\n buf.writeUInt16LE(16, offset);\n offset += 2; // bits per sample\n\n buf.write(\"data\", offset);\n offset += 4;\n buf.writeUInt32LE(pcmBytes, offset);\n offset += 4;\n\n Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).copy(buf, offset);\n\n return buf;\n}\n\nexport async function silkToWav(\n silkBuffer: Buffer,\n sampleRate: number = WECHAT_SILK_SAMPLE_RATE,\n): Promise<Buffer | null> {\n try {\n const { decode } = await import(\"silk-wasm\");\n const result = await decode(silkBuffer, sampleRate);\n const wav = pcmBytesToWav(result.data, sampleRate);\n return wav;\n } catch (err) {\n return null;\n }\n}\n","// src/managers/MessageParser.ts\n\nimport type { CdnManager } from \"./CdnManager.ts\";\nimport {\n type CdnDownloadTicket,\n MessageItemType,\n type RawMessage,\n type RawMessageItem,\n type WeChatClientConfig,\n type WeChatIncomingMessage,\n} from \"../types.ts\";\nimport { silkToWav } from \"../core/SilkConverter.ts\";\nimport { writeFile } from \"node:fs/promises\";\nimport {\n getFileImageItem,\n getItemType,\n isItemWithMedia,\n mergeObjects,\n} from \"../core/utils.ts\";\n\nexport class MessageParser {\n private cdn: CdnManager;\n private config: WeChatClientConfig;\n\n constructor(cdn: CdnManager, config: WeChatClientConfig) {\n this.cdn = cdn;\n this.config = config;\n }\n\n /**\n * 将微信底层的复杂 JSON 扁平化为开发者友好的对象\n */\n public async parse(raw: RawMessage): Promise<WeChatIncomingMessage[] | null> {\n // 过滤掉系统消息或无内容的空包\n if (!raw.item_list || raw.item_list.length === 0) return [];\n\n const results: WeChatIncomingMessage[] = [];\n\n // 提取公共的信封数据\n const shared_data: WeChatIncomingMessage = {\n messageId: String(raw.message_id),\n seq: raw.seq,\n fromUserId: raw.from_user_id,\n toUserId: raw.to_user_id,\n timestamp: raw.create_time_ms,\n contextToken: raw.context_token,\n msgType: MessageItemType.TEXT,\n msgTypeStr: \"unknown\",\n index: 0,\n raw: raw,\n };\n\n for (let i = 0; i < raw.item_list.length; i++) {\n const item = raw.item_list[i];\n const msgCopy = mergeObjects(shared_data, {\n index: i,\n msgType: item.type,\n msgTypeStr: getItemType(item.type),\n });\n\n // 1. 先处理文本消息,提取纯文本内容\n if (item.type === MessageItemType.TEXT) {\n msgCopy.text = item.text_item.text;\n }\n if (!isItemWithMedia(item.type)) {\n results.push(msgCopy);\n continue; // 如果不是媒体消息,直接进入下一轮循环\n }\n\n // 2. 媒体消息需要提取下载票据,并提供 getBuffer 方法\n let cachedBuffer: Buffer | null = null;\n const ticket = this._extractDownloadTicket(item);\n msgCopy.fileName = ticket?.originalFileName;\n msgCopy.mediaTicket = ticket;\n msgCopy.getBuffer = async () => {\n if (cachedBuffer) return cachedBuffer;\n if (!ticket) return null;\n // 调用 CDN 管理器下载\n cachedBuffer = await this.cdn.downloadBuffer(ticket);\n return cachedBuffer;\n };\n msgCopy.saveToFile = async (savePath: string) => {\n const buf = await msgCopy.getBuffer!();\n if (!buf) throw new Error(\"无媒体内容可保存\");\n await writeFile(savePath, buf);\n return savePath;\n };\n\n // 3. 语音消息需要特殊处理,提供一个额外的方法获取原始 SILK 编码的 Buffer\n if (msgCopy.msgType === MessageItemType.VOICE) {\n msgCopy.getVoiceBuffer = msgCopy.getBuffer; // 语音消息的原始 Buffer 是 SILK 编码\n let pcmCachedBuffer: Buffer | null = null;\n msgCopy.getBuffer = async () => {\n if (pcmCachedBuffer) return pcmCachedBuffer;\n const silk_buffer = await msgCopy.getVoiceBuffer!();\n if (!silk_buffer) return null;\n pcmCachedBuffer = await silkToWav(silk_buffer);\n return pcmCachedBuffer;\n };\n }\n\n // 4. 如果开启了自动下载,就在抛出事件前,在后台先下载好!\n if (this.config.autoDownloadMedia !== false && ticket) {\n try {\n msgCopy.getBuffer(); // 不 await,后台下载,事件照常触发\n } catch (err: any) {\n console.error(`自动下载媒体失败: ${err.message}`);\n }\n }\n results.push(msgCopy);\n }\n return results;\n }\n\n /**\n * 从原始 JSON 中提取标准化的 CDN 下载票据\n */\n private _extractDownloadTicket(\n item: RawMessageItem,\n ): CdnDownloadTicket | undefined {\n const file_item = getFileImageItem(item);\n if (!file_item || !file_item.media) return undefined;\n const media = file_item.media;\n\n // 如果连基本的下载参数都没有,直接放弃\n if (!media.encrypt_query_param && !media.full_url) return undefined;\n\n // 2. 抹平 AES 密钥的特权差异\n let aesKeyBase64 = media.aes_key;\n\n // ⚠️ 协议特例:图片类型有时会把密钥放在外层,而且是 Hex 格式\n if (\n item.type === MessageItemType.IMAGE && \"aeskey\" in file_item &&\n file_item.aeskey\n ) {\n // 统一转成 Base64,方便 CdnManager 统一处理\n aesKeyBase64 = Buffer.from(file_item.aeskey, \"hex\").toString(\"base64\");\n }\n\n // 3. 判断是否为明文传输 (某些情况下微信图片没有 aes_key)\n const isPlain = !aesKeyBase64;\n\n return {\n fullUrl: media.full_url,\n encryptedQueryParam: media.encrypt_query_param,\n aesKeyBase64,\n isPlain,\n originalFileName: \"file_name\" in file_item\n ? file_item.file_name\n : undefined,\n };\n }\n}\n"],"mappings":"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","names":["LoginStatus"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aluria/wechat-ai-api",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "一个优雅、轻量、开箱即用的个人微信通知/机器人 SDK",
5
5
  "keywords": [
6
6
  "wechat",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/你的用户名/wechat-ai-api.git"
15
+ "url": "https://github.com/Wu-Yijun/wechat-ai-api.git"
16
16
  },
17
17
  "license": "MIT",
18
18
  "author": "Aluria",