# wxmcp **Repository Path**: jimonik/wxmcp ## Basic Information - **Project Name**: wxmcp - **Description**: 微信的消息读取相关 mcp - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-10-14 - **Last Updated**: 2026-10-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # wxmcp macOS / Windows 微信 4.x 本地监听 MCP Server。历史和监听查询只读本地 SQLCipher 数据库及 WAL,不上传数据。Windows 复用相同的 MCP 工具定义和查询模型,首次初始化与跨版本适配见 [Windows 本地数据库接入](WINDOWS.md)。语音转写按数据库已有文字、内存二进制入口、HTTP API 的优先级处理;实验性语音能力目前仍以 macOS 说明为准。 项目定位为“微信任务控制中心”。Web 控制中心用于创建一体化编排动作:第一步选择要监听的微信会话,第二步将每条新文本消息交给 AI 执行。AI 运行时会加载本项目的 MCP Server,可按需查询联系人、群聊、会话历史和搜索消息。 ## 工作量概览 Skill 仓库中的 [skills/workload-overview](skills/workload-overview) 是工作量统计 Skill 的唯一最新版来源。提交和推送 MCP 项目时应同时提交该目录。 安装或更新到当前用户的 Codex: ```bash python scripts/sync_workload_skill.py ``` 仅检查 Codex 安装版是否与仓库一致: ```bash python scripts/sync_workload_skill.py --check ``` 修改 Skill 时先改仓库版本,再运行同步脚本;不要直接维护两份不同内容。 ## 本机 Web 控制台 后台服务启动后访问: ```text http://127.0.0.1:8766 ``` 也可以在项目目录执行: ```bash python3 automationctl.py open ``` 控制台目前提供: - 微信数据库、监听状态、规则数量和最近匹配概览; - 按名称、备注或内部 ID 搜索联系人和群聊; - 创建、编辑、启用和停用一体化编排动作; - 在编排第一步过滤并选择要监听的联系人或群聊; - 在第二步选择 AI 和常量提示词,项目路径等固定上下文直接写入提示词; - 新消息触发后,自动向 AI 开放微信 MCP 查询工具; - 查看和删除编排刷新、监听异常和 AI 任务记录。 Web 服务只允许绑定 `127.0.0.1`、`localhost` 或 `::1`,不会监听局域网地址。本地配置和运行记录保存在权限受限的 `.wechat-automation/` 目录中。 ## MCP 工具 | 工具 | 功能 | | --- | --- | | `search_contacts` | 按备注、昵称或内部 ID 搜索可监听联系人 | | `search_groups` | 按群名称或内部 ID 搜索可监听群聊 | | `list_recent_chats` | 按最近活动时间列出会话,可筛选联系人或群聊 | | `refresh_chats` | 刷新会话、联系人和消息分库索引,无需重启服务 | | `get_chat_history` | 分页读取指定会话的历史消息 | | `get_latest_messages` | 按会话名称或 ID 读取最新消息,重名返回候选,默认精简输出 | | `get_voice_transcript` | 读取微信自身“转文字”已保存的结果 | | `transcribe_voice` | 统一转写入口:数据库已有结果 → 内存二进制入口 → HTTP API | | `get_voice_transcription_status` | 查询实验性本地语音桥接状态及会话 ID | | `request_voice_transcription` | 显式确认后通过测试副本内存入口请求微信原生转写,默认关闭 | | `list_chat_images` | 列出会话中的图片消息、下载状态和本地图片容器 | | `get_image_file` | 按消息定位图片并还原为 AI 可直接读取的普通图片缓存 | | `search_messages` | 按关键词、会话及时间范围搜索本地文本消息 | | `get_contact` | 读取联系人或群聊的本地资料 | | `list_group_members` | 列出或搜索指定群聊的成员 | | `enable_monitoring` | 从当前最新消息建立基线并启用监听 | | `disable_monitoring` | 关闭指定对象监听并清理其待领取消息 | | `list_monitoring` | 查看所有监听对象及待领取消息数量 | | `get_new_messages` | 领取监听到的新增消息并推进持久游标 | | `check_local_setup` | 检查版本、权限、依赖和本地配置状态 | | `start_local_setup` | 经用户确认后启动本机初始化流程 | | `get_setup_status` | 查询初始化进度,不返回本地访问参数 | | `finish_local_setup` | 确认配置已保存并验证查询连接是否就绪 | | `reload_local_setup` | 无需重启宿主,加载保存的配置并恢复未就绪连接 | | `list_local_profiles` | 列出已保存配置的版本和账号指纹 | | `get_control_center_status` | 查看本机 Web 控制中心和后台服务状态 | | `list_orchestrations` | 查看当前的微信消息 AI 编排动作 | | `list_task_processors` | 查看可选 AI 的就绪状态 | | `list_automation_events` | 查看最近运行事件,不包含完整聊天正文 | 搜索结果会返回 `target_id`。启用和关闭监听必须使用该 ID,不能用可能重名的显示名称。 读取“某群最新消息”优先调用 `get_latest_messages(query="群名")`,无需开启监听。 引用回复会返回 `messageType="quote"`,`text` 是回复正文,`quote` 包含被引用原文的 `messageType`、`text`、`senderId`、`senderName`、`timestamp` 和字符串形式的 `serverId`;精简输出同样保留这些字段。内外层 `messageType` 使用同一套稳定名称:`text`、`image`、`voice`、`video`、`sticker`、`location`、`app_message`、`system` 或 `unknown`。历史查询和新增消息监听均支持。`quote.source="embedded_reference"` 表示读取引用消息内保存的原文快照,不代表重新查到了原始记录。被引用的图片等非文本内容返回 `text=null`、`contentReadable=false`,不暴露原始媒体 XML;嵌套引用最多解析八层。 名称精确匹配优先于包含匹配;重名时返回 `ambiguous` 和候选 ID,不擅自选择。 默认返回 10 条消息,包含带时区时间、发送者、消息类型和内容是否可读。 `compact=false` 可查看完整字段;历史接口保留原有字段和翻页游标。 语音标记为 `voice`,历史查询返回 `durationMs`(消息中存在有效时长时)和转写状态; 已转写的语音直接返回微信生成的文字。图片使用 `get_image_file` 读取。 查询仅覆盖本机已同步记录,不代表其他设备或云端的完整历史。 ### 微信原生语音转文字 统一入口为 `transcribe_voice(target_id, local_id, database, request_id)`,默认 `method="auto"`。三种方案按以下优先级执行,获得非空完成结果后立即返回: | 优先级 | 方案 | 行为与准备要求 | | --- | --- | --- | | 1 | `database`:读取数据库已有文字 | 正式版可用。调用 MCP 时提示用户在微信设置中开启“语音消息自动转文字”;微信保存文字后,MCP 直接读取,无需副本、进程附加或上传。 | | 2 | `native`:调用内存中的二进制转写入口 | 需提示用户先创建并登录受支持的微信测试副本,附加 LLDB、完成入口采样并启动桥接。调用微信自身转文字入口,再从数据库读取结果。 | | 3 | `api`:HTTP 接口 | 需提示用户先创建并登录受支持的微信测试副本,附加调试工具取得同账号鉴权。随后 Python 独立封包、上传 SILK、查询结果;不把接口文字写回微信数据库。 | 当前正式版微信 4.1.15 / Build 270135 的 Frida、LLDB 附加均被系统拒绝, 尚未从正式版获得有效的接口鉴权。因此目前正式版可直接使用方案 1;方案 2、3 的准备流程 使用调试副本。HTTP 请求本身不要求副本持续运行,但登录会话失效后需要重新采集鉴权。 `auto` 未传 `confirm=true` 时先只读数据库;有结果就返回,没有结果才提示确认后两种联网方案。 `confirm=true` 表示允许按顺序尝试后两种方案,仍然先查数据库。 也可显式指定 `method="database"`、`"native"` 或 `"api"`,只运行该方式;后两种需要确认。 原有 `request_voice_transcription` 保留为独立原生入口,`get_voice_transcript` 保留为只读入口。 不会自动创建副本、附加调试器、重签名、重启微信或重新登录。 MCP 返回 `settingsHint` 提醒开启自动转文字;`nativePreparationHint` 和 `apiPreparationHint` 明确提示副本准备要求。调用方应向用户展示适用提示,同次回复只提醒一次。 `method` 表示实际文字来源,`cached=true` 表示数据库已有文字,`attempts` 记录各级结果。 原生提交后若文字与原有记录相同,不能证明本次新转写,显式 `native` 不将其冒充新结果。 网络超时不等于服务端未处理;同一逻辑请求保持同一 `request_id`,服务进程存活期间去重 (最多 128 个请求,满后不淘汰未知提交)。确认前的只读预览不占用请求 ID,可以同 ID 确认后继续。 服务重启后去重记录不保留;原生未完成时用 `get_voice_transcript` 查询,不要换 ID 反复提交。 HTTP 方案使用本地已下载的 SILK,按会话、serverId、时间精确匹配媒体记录,不创建音频导出文件。 鉴权文件默认 `~/.wechat-voice/asr-auth.json`,可通过 `WECHAT_VOICE_API_AUTH` 指定; 必须属于当前用户且权限为 `600`,账号应与数据库一致。鉴权和音频不得提交到 Git。 创建并登录测试副本后,可用以下命令采集或刷新鉴权: ```bash python -m pip install frida python -m wxmcp.voice_api_auth # 如需明确进程:python -m wxmcp.voice_api_auth --pid <测试副本PID> ``` 鉴权采集校验 Mac Build 270135 模块 UUID,版本不符会拒绝使用偏移。 独立接口目前验证的是 SILK / 16 kHz、`mlextshort.weixin.qq.com` 的 `checkvoicetrans → uploadvoicefortrans → getvoicetransres` 链路。 同一 3159 字节样本接口返回「测试量。」,原生已有结果为「测试六。」:流程成功不保证识别准确。 自动模式优先返回数据库中的「测试六。」;显式接口模式按服务器实际结果返回,不替换成预期文字。 `get_voice_transcript(target_id, local_id, database)` 按历史结果的会话 ID、 `localId` 和 `database` 精确定位语音,读取微信保存在 `packed_info_data` 的转写文本。 当前解析规则已在 macOS 微信 4.1.15 本地记录验证:protobuf 字段 5 的嵌套字段 1 为状态(2 为完成),字段 2 为 UTF-8 文本。微信升级后格式可能变化; 无法解析时返回 `unsupported_data`,不猜测、不将其他字段当作正文。 历史查询和最新消息查询也会直接包含已有转写,标注 `transcriptionSource="wechat"`、 `transcriptionStatus="completed"` 和 `machineGenerated=true`。无保存结果时为 `not_available`,存在转写记录但未完成时为 `not_ready`。 不安装识别模型,不下载或上传音频,不修改微信数据库。 `get_voice_transcript` 只读已有结果,不会缓存空结果。历史、最新消息及监听结果发现 尚无文字结果的语音时,返回 `transcriptionHint`,提示用户在微信设置中查找并开启 语音消息自动转文字(名称和可用性以当前版本为准),待微信生成文字后重新查询。 同次回复只需提醒一次。只读语音工具和统一入口的 auto/database 模式即使已完成也通过 `settingsHint` 提醒自动转文字设置; 历史查询仍仅在缺少结果时追加提示。尚在处理中时提示稍后重试, 数据格式不支持时明确提示需要更新适配。 MCP 不包含自动点击、截图识别或语音识别模型,不代替用户操作微信设置。 读取语音文字不需要语音气泡出现在屏幕上,也不需要新增辅助功能或屏幕录制权限。 客户端原生转写可能使用微信服务,处理方式由微信决定。方言、人名和数字仍应核对原语音。 #### 实验性内存入口 MCP 接入 已接入本地 Unix socket 桥接,不包含自动点击或外部 ASR。当前仅支持 ARM64 微信 **4.1.15 / Build 270135**、已准备并登录的调试测试副本; 模块 UUID 必须为 `0C2F01C0-C197-3286-901D-70E1D007A1C0`。 不自动附加正式客户端、重签名、重启微信或降低系统安全设置。 版本升级后拒绝继续调用,需重新验证布局与调用约定。 准备要求: 1. 正式客户端与测试副本不能同时使用同一账号数据。测试副本可执行路径 必须位于项目 `.wechat-setup/voice-research.*/WeChat-voice-test.app/` 下。 这不是普通安装包即开即用功能;准备与登录需单独完成。 2. 从仓库根目录启动专用 LLDB 会话,导入 `wxmcp.voice_runtime`, 在受支持副本的原生入口通过 `observe_entry` 完成一次本进程校准, 取得真实处理器及线程。初始化采样可能需要一次人工原生转写操作; 后续 MCP 提交不显示或点击微信。旧进程指针或旧采样不能复用。 3. 暂停测试进程,移除全部校准断点,再在 LLDB 中显式启动桥接: ```python from wxmcp import voice_bridge b = voice_bridge.start( lldb.debugger, "/Users/<用户名>/.wechat-voice/bridge.sock", acknowledge_native_call=True, ) ``` 在 LLDB 提示符执行时使用 `script` 前缀,或在脚本中执行上述代码。 父目录须为当前用户所有、权限 `0700`(不存在时自动创建),socket 权限为 `0600`;路径宜短,避免 Unix socket 长度限制。 启动成功会恢复测试进程。桥接占用期间不要在同一 LLDB 中执行其他 调试命令;退出前先执行 `script b.close()`,若仍有请求执行则不要退出。 4. MCP 进程配置以下环境变量并重启;两边 socket 路径必须一致: ```json { "WECHAT_ENABLE_EXPERIMENTAL_VOICE": "1", "WECHAT_VOICE_BRIDGE_SOCKET": "/Users/<用户名>/.wechat-voice/bridge.sock" } ``` 调用顺序:搜索取得 `target_id` → 历史查询取得 `localId` / `database` → `get_voice_transcription_status()` 取得 `ready` 和 `session_id` → 调用 `request_voice_transcription(target_id, local_id, database, session_id, request_id, confirm=True)`。`request_id` 使用每次逻辑请求固定的 UUID 字符串; `wait_seconds` 默认 3,允许 0–10 秒,仅控制提交后的数据库回读等待。 工具标注为非只读、可能访问外部服务,宿主应先取得用户对转写的授权。 提交前校验数据库消息与内存对象的会话、类型、local/server ID、时间戳、 虚表、共享所有权及线程。仅处理已经加载且唯一的语音对象: `message_not_loaded` 不会自动打开群聊或加载历史; `ambiguous_message` 拒绝猜测。已有结果直接返回,不重复提交。 每次调用临时暂停进程,并在 `finally` 中尝试恢复; 若 `resume_failed`,需人工检查专用调试会话,不能假定微信仍正常运行。 `submitted` 仅表示原生入口返回,不表示转写完成。最终以 `get_voice_transcript` 的数据库结果为准。传输超时或调用异常返回 `outcome_unknown`,不能视为未执行,也不会自动重发。可查询数据库, 或使用原 `session_id` / `request_id` 获取缓存结果,不能换 ID 重试。 桥接按请求和消息双重去重;不确定调用后暂停新提交;每会话最多保留 128 个请求,不淘汰旧记录。重启桥接后缓存不持久化,不能借新会话 重新提交结果不确定的消息。 验证范围:自动化测试覆盖 MCP—真实本地 socket—模拟原生调用—结果回读、 去重、超时、权限和恢复异常;独立本机小程序验证了 LLDB 后台线程的 暂停、表达式调用和恢复。下文记录的微信原生入口单次 PoC 已成功, 但**新桥接尚未完成真实微信端到端复验**,不能作为生产可用承诺。 协议 API 暂不实现,研究证据与后续工作保留在下文。 #### 无界面触发研究记录 `scripts/inspect_voice_entrypoints.py` 仅静态读取客户端 ARM64 Mach-O, 查找转写字符串、相邻 ADRP/ADD 或 ADRP/LDR 引用、函数起点和直接 BL 调用。 不会加载执行客户端、附加进程、打开窗口、点击、读取账号凭据或发送网络请求。 扫描器是针对当前受信任客户端的研究辅助工具,不是完整反汇编器; 无匹配不代表不存在入口,匹配也不代表函数可直接调用。 在 4.1.15 / Build 270135 中,通过日志字符串引用定位并静态反汇编核对: | 字符串线索 | ARM64 未滑移函数地址 | | --- | --- | | `StartTranscribe` | `0x314f670` | | `RunViaRecognizeT`(字符串片段) | `0x3155a78` | | `UploadVoiceData` | `0x315706c` | | `GetTranscribeRes`(字符串片段) | `0x31582f4` | | `OnTranscribeProg`(字符串片段) | `0x315b680` | 已找到 `0x3148590` 内部对 `StartTranscribe` 候选函数的直接调用, 并找到上传候选函数对结果获取候选函数的直接调用。 函数名称是依据日志推断,不是导出符号或已验证的完整签名。 入口使用内部对象、状态字段和复杂参数;不能将其当作接收文件路径的 C API, 也不能仅凭地址从 MCP 安全调用。地址仅适用于上述 Build,运行时还涉及 ASLR。 静态分析阶段未验证后台主动转写;后续测试副本验证结果见下文。 尚未实现正式安装进程桥接;现有查询工具仍只读,测试副本桥接需单独启用。 后续需确定对象来源、参数布局、线程要求、任务生命周期及账号会话依赖, 再评估不修改正式客户端、不降低系统安全设置的验证方案。 进一步的静态追踪已定位消息处理器入口 `0x3141f80`: 其混淆日志解码为 `TranscribeVoiceMessage`,源码文件尾部为 `kernel/core/manager/message/handler/voice_message_handler.cc`。 解码依据是该函数中的逐字节减 `0x76` 后异或 16 字节循环键的指令; 不是凭相似字符串命名。当前 Build 的常量基址为 `0x88b6e70`: 函数名数据偏移 `0x441`、键偏移 `0x431`、长度 `0x17`; 源码路径数据偏移 `0x468`、键偏移 `0x458`、长度 `0x73`。 已核对的调用关系: ```text 0x41b7c88 -> 0x31e9b74 -> 0x31e9d50 -> 0x3141f80 (TranscribeVoiceMessage) 创建转写状态对象:0x3147fa8 -> 0x314f224 创建回调任务:虚表地址 0x9deb7f0 提交调度候选函数:0x453d344 ... 虚表 +0x30 -> 0x3148590 -> 0x314f670 (StartTranscribe) ``` 省略号表示尚未验证的调度执行路径,不是已完成动态调用。 `0x3141f80` 保存 x0–x3 参数并使用 x8 返回存储; x1 所指数据表现为共享所有权对象,x2/x3 表现为可调用包装对象。 `StartTranscribe` 将两个可调用对象移动到内部状态后调用 `0x3150c08`。 上述布局最初来自静态指令;后续已在单个测试进程中对照实测。 尚未建立适用于任意历史语音的稳定对象获取机制。 可使用 `--function 0x3141f80` 等参数继续追踪指定地址。 扫描器将直接 `BL` 调用与 `B` 分支候选分别输出;后者可能是尾调用、 转发跳板或普通跳转,不能直接当作已验证调用。 `dataPointerCandidates` 的 `low36_match_unvalidated_fixup` 仅表示低位匹配; 未解析 dyld fixup 链,不应将其视为已验证指针或用于运行时调用。 这些研究工具不附加进程、不实施注入,也不承诺正式客户端允许调试。 现有初始化脚本会退出/启动客户端并要求登录临时副本, 不满足本次“不显示微信”的要求,不能直接复用为转写方案。 2026-10-21 对当前正式客户端单独进行了一次附加能力检查(未调用函数、 未修改内存):LLDB 返回 `Not allowed to attach to process`, debugserver 日志显示 `task_for_pid` 失败。该安装启用了 Hardened Runtime; 未进一步确定具体拒绝策略,因此不声称修改某一个权限即可解决。 检查后客户端进程仍运行,未重新签名、重启或修改系统保护。 正式客户端动态验证受此限制,不能宣称已能从 MCP 操作原版进程。 #### 测试副本:隐藏窗口原生调用已通过 2026-10-21,经用户同意,正常退出正式客户端后创建并重新签名独立测试 应用副本,使用 LLDB 启动,由用户完成登录。副本并非独立账号沙箱: 它复用了本机账号数据,因此禁止与正式客户端同时运行。 未修改正式安装、未使用管理员权限、未改变 SIP 或其他系统保护。 实验分为两部分,不能混为一谈: 1. 界面对照采样:在目标群中用语音转文字操作核对真实对象、共享所有权 和线程。对照中的语音由微信正常转写,不是“无点击”验证。 2. 无点击验证:选择同群另一条数据库状态仍为 `not_available` 的语音, 隐藏测试窗口,核对会话、local ID、server ID、类型和对象虚表, 在已观察到的空闲 `coroutine` 工作线程中调用 `TranscribeVoiceMessage` 一次。调用返回后恢复执行,数据库状态变为 `completed`, 得到 27 字转写;检查时窗口仍隐藏。没有点击该条语音的转文字按钮, 没有调用外部 ASR,也没有直接写入数据库。 运行时核对的当前版本对象布局: | 对象 | 位置 | 含义 | | --- | --- | --- | | 消息 | `+0x0c` | 类型 34 | | 消息 | `+0xf4` | local ID | | 消息 | `+0xf8` | server ID | | 消息 | `+0x100` | 毫秒时间戳,**不是 server ID** | | 消息 | `+0x114` | 秒时间戳 | | 处理器 | 主/次虚表 | `slide+0x9deb4a8` / `slide+0x9deb5b8` | | 消息 | 虚表 | `slide+0x9eb04f0` | | 已验证分配形式的共享控制块 | 虚表 | `slide+0x9c92390` | `wxmcp/voice_runtime.py` 保存了受限扫描、断点观察和实验性单次提交 辅助函数,`scripts/voice_runtime_probe.py` 保留兼容导入。默认只读观察;`submit_transcription` 必须显式 `acknowledge_native_call=True`,并重新验证副本路径、模块 UUID、消息标识、 会话、虚表、共享所有权和空闲线程。调用有超时,不自动重试; 超时不能证明没有副作用。`submitted` 不代表完成,必须另查数据库确认。 停止/恢复进程由调用方负责,应在 `finally` 中恢复;不要复制旧进程地址。 **范围限制:**原生调用证据来自一个版本、一个测试进程中的概念验证; 上面的 MCP 封装仍属实验性能力。验证消息事先通过界面加载到内存;尚未验证无需打开群聊即可获取 任意历史消息对象。正式客户端不允许当前调试方式附加,后续仍需设计 明确授权的进程桥接、按需加载、生命周期管理和升级适配。不能据此宣称 “原版微信无需额外准备即可全自动后台转写”。 实验收尾已移除全部断点并分离调试器,退出测试进程,将测试应用改为 `.disabled` 后缀保留在私有忽略目录,再启动正式安装。 正式应用签名校验通过;主程序及 `Resources/wechat.dylib` 的 SHA-256 均与实验前一致。截图和临时副本未纳入版本控制。 #### 更新适配研究:从字符串自动定位服务入口 后续只读静态分析找到了更上层的链路(地址仍仅用于记录 Build 270135): ```text 明文 TranscribeVoice 的引用函数 0x78c054 -> 0x412eb70(两次读取服务成员,再 B 尾跳转) -> 0x41b6de4(按消息键查找/创建任务包装) 构造回调虚表 0x9e638b0,+0x30 指向 0x41b767c -> 0x41b767c(创建下一层回调并提交 0x453cce8) 构造回调虚表 0x9e63920,+0x30 指向 0x41b7c88 -> 0x41b7c88 -> 0x31e9b74 -> 0x31e9d50 -> 0x3141f80 ``` 虚表关系来自文件内低位指针匹配与构造指令的交叉核对,尚未动态验证 这一整条服务调用链;不是新增的后台转写成功案例。 `0x78c054` 的日志源码路径解码为 `ui/chat/detail/messages/viewmodel/chat_voice_item_view_model.cc`; `0x41b767c` 的路径解码为 `kernel/core/service/message/private/voice_transcribe_handler.cc`。 前者常量基址 `0x85377a0`,数据/键偏移 `0xe5/0xd5`,键长 16, 长度 `0x76`;后者基址 `0x89c7140`,数据/键偏移 `0xf7/0xe3`, 键长 20,解码 `0x74` 字节后指令另写 `c\0`。均使用减 `0x76` 后异或。 `inspect_voice_entrypoints.py` 现在无需传入地址即可沿 `TranscribeVoice` 字符串引用检查直接调用目标,识别 `LDR x9,[x0,#imm]; LDR x0,[x9,#imm]; B implementation` 结构, 输出 `serviceForwarderCandidates`。当前文件得到唯一候选: 转发入口 `0x412eb70`、实现 `0x41b6de4`、成员偏移 `0x1f8/0x8`, 与手工反汇编一致。算法未硬编码这些地址或成员偏移; 测试覆盖地址迁移、反向跳转以及错误寄存器/BL 的拒绝。 结构识别依然是假设驱动的候选发现,不能自动授权运行时调用; 换编译器、寄存器分配、内联策略或日志字符串都可能使其失效。 尚未取得第二个版本作兼容性验证。 **尚未找到“只传会话 ID 和消息 ID”的稳定触发接口。** 服务层 `0x41b6de4` 接收完整消息值对象并用 `0x395584` 复制, 不是单纯消息 ID;任务复用也不等同于从数据库按 ID 加载消息。 下游 `0x31e9b74 -> 0x39ee5e4` 从包装对象 `+0x238/+0x240` 取得已有核心消息共享指针,并非按 ID 查询。 更上层服务有望复用微信的任务调度,但消息获取、对象生命周期、 服务注册表访问和正式客户端的调用权限仍需解决。 此次研究未附加、重启或修改正式微信,未触发新的转写。 #### 只读导出本地语音 `python scripts/export_voice.py --chat filehelper --local-id 335 --database message_0.db --output /绝对路径/sample.silk` 脚本以消息的会话、服务器 ID 和时间定位 `media_*.db/VoiceInfo`,只读提取 已下载的 SILK 数据,拒绝歧义记录与未知格式,以 0600 权限新建文件且不覆盖已有文件。 它只导出音频,不调用识别服务,也不向微信数据库写入结果。 本机样本 335 已成功导出(2054 毫秒、3159 字节),并用于上文独立 Python 接口验证。 #### 协议 API 路线:静态路径与实际分支 同一 Build 的只读静态分析已从请求配置构造函数还原以下路径。 它们由多个常量片段拼接,并非完整明文字符串,单纯 `strings` 搜索会漏掉。 | 流程 | 请求路径 | 配置首字 / 函数 | | --- | --- | --- | | 上传语音 | `/cgi-bin/micromsg-bin/uploadvoicefortrans` | `547` / `0x31648dc` | | 查询转写结果 | `/cgi-bin/micromsg-bin/getvoicetransres` | `548` / `0x3167f60` | | RecognizeT 识别候选 | `/cgi-bin/micromsg-bin/voice_recognize` | `12220` / `0x3ca19e8` | 配置前四个 32 位字分别为 `(547,0,0,1)` 和 `(548,0,0,1)`。 首字是请求编号候选,尚未抓取实际网络包核对其线上含义。 `UploadVoiceData` 调用 `0x3158824`,继而构造回调和请求对象, 其配置来源可追至 `0x3166098 -> 0x31648dc`; 结果查询 `0x31582f4 -> 0x31590b4 -> 0x315adf0`, 经同类回调链到 `0x3169714 -> 0x3167f60`。 中间包含多级间接回调,这些是静态追踪,不是动态调用记录。 本机文件传输助手样本的动态观察命中 `TranscribeVoiceMessage -> StartTranscribe -> RunViaRecognizeT` 以及 `0x3cb5058 -> 0x3cb6914`,未命中上述旧上传/查询分支。 识别模块包含两个引擎:`0x3caa828` 和 `0x3cac6a4`;前者静态调用链 `0x3ca07bc -> 0x3ca19e8` 还原出 `voice_recognize`,配置为 `(12220,0,0,1)`。本机新增语音样本实际命中 `0x3cac6a4` 十二次,未命中 `0x3caa828` 或 `0x3ca19e8`;因此 `voice_recognize` 仍只是备用分支候选, 不能作为本次转写的已验证接口。 实际分支的静态调用链为 `0x3cac6a4 -> 0x3cad070 -> 0x2f4dc64`, 后者调用导出函数 `GetIlinkStreamContext` 并通过虚函数创建通道。 `0x3cad3dc` 通过通道虚表 `+0x38` 初始化请求,参数包含常量 `(3,0)`; `0x3cacf14` 通过虚表 `+0x58` 发送序列化数据。这里的参数含义仍待还原。 `GetIlinkStreamContext` 由 `ilink_wrapper.framework` 导出。 继续核对 `Contents/Resources` 中实际实现库(`Contents/Frameworks` 同名库含跳转入口)后, 通道虚表对应关系如下,地址均为 ARM64 库内偏移: | 调用 | wrapper 实现 | ilink_stream 导出 API | | --- | --- | --- | | context 虚表 `+0x10` | `0x123ac` | `IlinkStreamContextNoPB::NewIlinkStreamChannel(string const&)`,`0x74fd4` | | channel 虚表 `+0x38` | `0x12b50` | `IlinkStreamChannelNoPB::CreateStream(unsigned int,unsigned int,string const&)`,`0x7491c` | | channel 虚表 `+0x58` | `0x131bc` | `IlinkStreamChannelNoPB::SendStreamFragment(string const&)`,`0x74c74` | `ilink_stream.framework` 同时导出 `OnReceiveStreamFragmentEvent`、 `ConfigGlobalSession` 和 `WaitUntilGlobalSessionReadyAsync`;有 PB 版本的发送接口 接收 `ilink_stream::pb_export::UpStreamFragmentInfo`。这些名称可由本地 `nm` / `c++filt` 核对,无需获取账号数据。已有证据是引擎入口动态命中及其下游静态调用对应; 尚未动态确认该流式通道调用目标、还原其业务 payload 与会话初始化;上文已验证的独立 HTTP 实现走旧上传/查询接口,不是此流式通道。 脚本同时还原旧分支的八个 protobuf 类型名及序列化字段编号、wire type、 嵌套消息类型,并核对序列化虚表入口。字段名和完整语义尚未还原; 这些结构也不能直接套用于 RecognizeT 请求。 另在内置 XML 路由表发现 `voicetrans`,`reqid=381`、 `respid=1000000381`。该表也含其他/历史路由,不能仅凭存在就认定 当前转写使用该接口,也不能与上述配置编号混为一谈。 `scripts/inspect_voice_protocol.py` 保存当前版本的片段重建依据。 在文件模式 LLDB 中创建 ARM64 目标、导入脚本,调用 `inspect_voice_protocol.report(lldb.debugger)` 即可复现;拒绝有进程的目标 以及不匹配的模块 UUID。它是固定版本证据解码器,不是跨版本定位器、 网络客户端或 MCP 转写工具。 **独立 HTTP API 已验证,RecognizeT 流式通道仍未实现。** `voice_api.py` 和 `voice_mmtls.py` 实现旧分支的业务封包与 MMTLS, 使用测试副本采集的临时鉴权完成过单条语音识别。当前旧鉴权已失效, 正式版无法直接附加获取新鉴权,需按上文副本流程重新采集。 静态研究记录不代表当前客户端实际选择的流式协议也已实现;协议、鉴权及服务端策略仍可能变化。 工具失败返回 `error.code`、`error.message` 和 `error.recovery`,避免宿主隐藏异常后无从恢复。 `ready=true` 表示连接初始化已成功读取会话,并在存在会话时验证消息读取; `configuration_saved=true` 仅表示配置保存成功。已就绪连接重复加载不会丢弃监听队列。 新增联系人、群聊或消息分库后,先调用 `refresh_chats`,再重新搜索。刷新会话信息会保留已有监听的游标和过滤条件。 ### 历史和图片翻页 `get_chat_history` 和 `list_chat_images` 首次调用时省略游标。返回的 `next_cursor` 包含 `before_timestamp`、`before_database` 和 `before_local_id`;后续调用应同时原样传入这三个字段。例如: ```json { "target_id": "从搜索结果取得的 target_id", "limit": 50, "before_timestamp": 1792454400, "before_database": "message_0.db", "before_local_id": 12345 } ``` 完整游标按时间戳、分库名和消息本地 ID 排序,避免一秒内多条消息或跨分库时漏记录。旧的单独 `before_timestamp` 参数及 `next_before_timestamp` 返回值继续保留;仅按秒翻页可能跳过该秒内尚未返回的消息。空页返回 `next_cursor=null`。 `enable_monitoring` 支持以下过滤参数: - `incoming_only`:忽略本人发出的消息; - `keywords`:只领取正文包含任一关键词的消息; - `message_types`:只领取指定微信消息类型,例如文本类型 `1`。 微信全文索引依赖客户端私有的 `MMFtsTokenizer`,通用 SQLCipher 无法直接执行其 `MATCH` 查询。因此 `search_messages` 会在本地只读解压文本后匹配,并通过 `max_scan` 控制最多扫描条数。全局搜索会把预算分配到各个会话;需要完整搜索某个会话时应传入 `target_id` 和适当时间范围。 ## 安装 使用任意 Python 3.12+ 环境安装依赖;Conda、venv、uv 或系统 Python 均由使用者自行选择: ```bash cd /path/to/wxmcp python3 -m pip install -r requirements.txt ``` 服务同时兼容 MCP Python SDK 1.26+ 和 2.x;已有环境无需仅为导入路径差异强制升级到 2.x。 系统还需要 Homebrew SQLCipher: ```bash brew install sqlcipher ``` 新版图片转为普通 JPG 时还需要 FFmpeg: ```bash brew install ffmpeg ``` ## MCP 配置 参考 [mcp-config.example.json](examples/mcp-config.example.json)。stdio 启动命令为: ```bash python3 /path/to/wxmcp/mcp_server.py ``` 接入 MCP 客户端前可先运行诊断;它只输出版本、可读状态和会话数量,不输出联系人或消息: ```bash python3 mcp_server.py --check ``` 若提示无法读取微信容器,需要给实际启动 MCP 的程序授予 macOS“完全磁盘访问权限”。从终端运行时授权终端;由桌面 MCP 客户端启动时授权对应客户端。授权后应完全退出并重新打开该程序。 可用环境变量: | 环境变量 | 说明 | | --- | --- | | `WECHAT_KEY_FILE` | 经过 HMAC 验证的 `keys.json` 绝对路径 | | `WECHAT_DB_ROOT` | 可选;多账号时指定目标 `db_storage` | | `WECHAT_MCP_STATE` | 可选;监听游标状态文件路径 | 本项目默认从 `.wechat-keys/` 自动选择与当前账号及客户端版本匹配的本地配置。兼容旧式单层目录,也支持按账号保存: ```text .wechat-keys/4.1.15-build-270102/keys.json .wechat-keys/4.1.15-build-270135/account-<账号指纹>/keys.json ``` 同目录 `manifest.json` 记录捕获版本和已验证兼容版本。启动时会读取 `/Applications/WeChat.app/Contents/Info.plist`;当前版本不在兼容清单中时会拒绝启动,避免误用旧版捕获流程。 `.wechat-keys/` 已被 Git 忽略,目录权限应为 `0700`,密钥文件权限必须为 `0600`。密钥绑定具体账号及数据库,而非所有相同微信版本;换 Mac、换账号或更换数据库后仍需重新获取并验证。 ### 首次初始化 没有可用本地配置时,MCP 仍会正常启动,但联系人和消息工具会提示尚未初始化。MCP 客户端可依次调用: 1. `check_local_setup` 检查数据库权限、客户端版本和必要组件; 2. `start_local_setup(confirm=true)` 启动流程;启动前请保存微信中尚未发送的内容; 3. 如出现临时窗口,按界面提示完成本地确认; 4. 用 `get_setup_status` 查询进度,直到状态为 `completed`; 5. 调用 `finish_local_setup` 确认 `ready=true`。初始化完成会自动加载配置,无需重启宿主;加载失败时按错误提示修复,再调用 `reload_local_setup`。 初始化产物会保存到项目的 `.wechat-keys/` 私有目录。过程目录位于 `.wechat-setup/`,其中可能保留体积较大的临时运行副本,便于失败后排查;确认运行正常后可由用户自行移入废纸篓。上述目录均已被 Git 忽略,不应提交或分享。 若本机存在多个微信账号,需在 MCP 配置中通过 `WECHAT_DB_ROOT` 选择目标账号;不同账号分别完成初始化。自动选择会优先匹配账号指纹、已验证版本以及当前 Build,无法唯一确定时会要求设置 `WECHAT_KEY_FILE`。 ### 本地访问凭据的来源与版本维护 当前项目内的访问凭据来自此前在同一台设备、同一微信账号上完成的一次授权初始化;原始结果随后被复制到项目的私有版本目录中。 初始化过程使用独立的临时客户端副本,让客户端正常打开本账号的本地数据库,并对取得的候选访问参数逐库进行首页完整性校验。只有通过校验的结果才会保存。流程结束后临时副本会被停用,原始安装位置和系统安全设置保持不变。 这组结果最初在微信 `4.1.15 / Build 270102` 下生成,随后已在 `4.1.15 / Build 270135` 的当前数据库上验证可读,因此两个 Build 都记录在同目录的 `manifest.json` 中。版本号只是维护标记;实际访问参数仍与具体账号和数据库文件一一对应。 微信更新后按以下原则维护: 1. 先查看 `/Applications/WeChat.app/Contents/Info.plist` 中的版本和 Build,不直接假定旧结果兼容。 2. 保留旧版本目录,不覆盖或删除已有结果。 3. 在本人账号和本机上重新执行授权初始化,并要求所有目标数据库再次通过完整性校验。 4. 使用新的版本目录保存结果,例如 `.wechat-keys/4.2.0-build-xxxxxx/`。 5. 从 `examples/key-manifest.example.json` 创建同目录 `manifest.json`,填写实际初始化版本;只有完成真实数据库验证后,才将新 Build 加入 `verified_compatible_apps`。 6. 重启 MCP,确认联系人搜索、历史消息查询和“启用监听后无历史误报”测试通过,再投入使用。 把新结果放入项目时仅复制私有结果文件,并重新收紧权限: ```bash install -d -m 700 .wechat-keys/<版本-build> install -m 600 <本机初始化结果>/keys.json .wechat-keys/<版本-build>/keys.json ``` 若客户端升级后初始化或完整性校验不再通过,应视为数据格式或初始化路径发生变化,先更新适配并重新验证;不要绕过版本检查,也不要通过修改原始客户端、降低系统安全设置或使用管理员权限强行兼容。 ## 监听语义 - 启用监听时读取当前最大 `local_id` 作为基线,因此不会回放历史记录。 - MCP 进程每 800ms 在后台只读检查一次新增消息。 - 新消息正文只暂存在 MCP 进程内存,不另存成明文文件。 - `get_new_messages` 领取消息时先原子保存游标,再更新内存状态和移除已领取消息。保存失败会保留原游标和待领取队列,可在修复磁盘问题后重试。 - MCP 进程异常退出时,未领取消息会在下次启动后从数据库重新读取。 - 状态文件权限为 `0600`,只包含监听目标、显示名和游标,不包含密钥或聊天正文。 - 时间戳与本地 ID 乱序时,游标不会越过同一分库中尚未领取的较小 ID。此时重启可能重放空隙之后的已领取消息,消费者可按 `chatId + database + localId` 去重。 - 数据库凭据匹配仅读取所需的 16 字节文件头,避免每轮轮询读入整个数据库。 - 常规历史记录和监听结果会解码文本正文;图片、语音、文件等仍显示类型占位,图片的详细信息通过专用图片工具读取。 - 图片消息可通过 `list_chat_images` 和 `get_image_file` 解析。工具不会返回 CDN 访问参数或内容密钥;未落盘图片会报告远端档位是否存在,已落盘图片会返回只读容器路径。 ## 本地后台自动化 普通 stdio MCP 不能在 Codex 空闲时主动创建一个新对话轮次。需要即时触发时,可启用独立的 macOS LaunchAgent。它同时运行本地 Web 控制台和微信监听器,复用同一套只读数据库后端,但拥有独立游标,不依赖 `get_new_messages` 被反复调用。 默认示例配置位于 `examples/automation-rules.example.json`,初始编排列表为空。一条编排包含微信会话来源、AI 和常量提示词;项目路径可作为提示词的一部分。不存在额外的“指令匹配”层。初次启用时以当前最新消息建立基线,只处理之后到达的新文本消息。 初始化并检查规则: ```bash python3 automationctl.py init python3 automation_daemon.py --check ``` 安装为当前 macOS 用户的登录启动服务: ```bash python3 automationctl.py install python3 automationctl.py status ``` 停止或重新启动: ```bash python3 automationctl.py stop python3 automationctl.py restart ``` 实际编排保存在 `.wechat-automation/rules.json`,运行游标和日志也位于 `.wechat-automation/`,整个目录均被 Git 忽略。游标在 AI 任务提交前落盘,进程重启不会重复执行同一条消息。 LaunchAgent 直接运行安装命令所使用的 Python。macOS 可能要求在“系统设置 → 隐私与安全性 → 完全磁盘访问权限”中加入 `automationctl.py install` 输出的 Python 路径;授权后执行 `python3 automationctl.py restart`。可通过 `.wechat-automation/automation-error.log` 查看启动错误。 ## 安全边界 - 仅用于当前 macOS 用户本人有权访问的微信账号。 - 数据库通过 `SQLITE_OPEN_READONLY` 和 `PRAGMA query_only=ON` 打开。 - 不执行 `INSERT`、`UPDATE`、删除或 checkpoint。 - 密钥不会作为工具结果、日志或命令行参数返回。 - MCP 客户端及其宿主程序需要 macOS“完全磁盘访问权限”。 ## 验证 ```bash python3 -m compileall -q wxmcp scripts mcp_server.py automation_daemon.py automationctl.py wechat_service.py python3 -m unittest discover -s tests -v ``` 新增回归测试使用临时的合成 SQLite 数据和模拟监听后端,覆盖同秒跨分库翻页、游标落盘失败、时间戳乱序、过滤消息和会话刷新,不依赖真实聊天内容。 项目已使用官方 MCP Python SDK 的 stdio 客户端完成工具发现、搜索、启用监听、基线检查、领取消息和关闭监听的端到端测试。 更新后请重启 MCP 进程并刷新客户端工具列表,以加载 `refresh_chats` 和新的翻页参数。本轮完善微信的读取和监听能力;微信尚未提供文本或文件发送工具。 ## 代码目录与格式 ```text wxmcp/ Python 业务包(MCP、监听、Web 服务、AI 调度与任务存储) backend/ 微信只读数据库与图片处理 paths.py 统一项目根目录定位 scripts/initializer/ 本机初始化脚本及签名配置 web/static/ 控制台 HTML、CSS、JavaScript examples/ MCP、编排和版本清单配置示例 ``` 根目录的四个 Python 文件仅作为启动入口,原有 MCP 配置和 LaunchAgent 启动路径仍可使用。运行数据继续保存在项目根目录的 `.wechat-*` 私有目录中。 开发环境安装 `requirements-dev.txt` 后,可运行以下检查;去掉 `--check` 并将 Prettier 的 `--check` 换成 `--write` 即可格式化: ```bash python3 -m black --check wxmcp scripts *.py npx --yes prettier@3.6.2 --check 'web/static/*' ```