# 语音转文字链路只读审计 审计日期:2026-09-18。范围:`C:/wechat_rpa` 协议与归档代码、`C:/kefu/wechat_rpa` 视觉模式代码,以及已有解密 SQLite 副本的 schema 和计数。未修改产品、未启动原生接口、未触发解密或转录、未下载模型、未打开音频、未读取登录凭证、未查询或输出消息与转写正文。 ## 结论 现有软件具备读取企微本地转写缓存的能力,但该能力只接入了归档导出,没有接入客服回复。安装端现有副本里 5 条语音全部已有对应缓存,可在不新增服务的前提下将这些转写接到问答模型。**这不等于具备主动转录任意新语音的能力**:没有发现可调用的 voice2text RPC,也没有自动右键“转文字”的视觉操作;离线 ASR 是未安装依赖、可能下载模型的导出兜底。 建议先统一消息解析层读取经过账号与消息关联校验的缓存,并让协议模式只放行已经取得转写的语音;对尚无转写的消息设置可重查的等待状态。若要求每条未缓存语音自动识别,还需要另行实现并验证客户端转录触发或本地 ASR,不能把本次缓存接入描述为完整的主动语音识别。 ## 已有函数与数据契约 | 位置 | 现状与可复用能力 | | --- | --- | | `archive_exporter/wxwork_voice2text.py:20` `load_voice2text` | 遍历解密的 `message.db`,读取 `msg_voice2text.message_id,text`;返回 `(user_dir, str(message_id)) -> text`。仅读取已经存在的转写,不触发客户端转录。 | | 同文件 `:56` `decode_silk_to_wav` | 通过 `pilk.silk_to_wav(..., rate=24000)` 解码,TypeError 时回退 `pilk.decode`。 | | 同文件 `:73` `asr_missing` | 对 `.silk/.amr` 的未缓存项实例化 `faster_whisper.WhisperModel(model_size='base', device='cpu', compute_type='int8')`,再 `transcribe(...,language='zh',vad_filter=True)`。首次可能下载模型。 | | `archive_exporter/wxwork_export_final.py:591` | 导出时自动加载缓存;只有 `voice_asr=True` 才运行离线兜底。`:599` 的 `_get_voice_text` 同时允许账号键与全局 `('',sid)`。 | | `archive_auto_backup.py:1085` `_normalize` / `:1286` | 增量归档预先取得 `voice_texts`,匹配 server_id 后传给 `_semantic_content`,以转写替换归档正文。没有给回复链路回填。 | | `wxwork_db.py:984` `_parse_message` | 当前只解析 `message_table.content`;无可读内容时产生 `[语音]` 占位符,没有查询 `msg_voice2text`。 | | `wxwork_db.py:961` `_read_conversation_context` | 各行经过 `_parse_message` 后组合 `messages`、`last_message` 和 `text`,是两种回复模式共享的合适接入位置。 | | `reply_database.py:158` `get_conversation_context_by_id` | 协议按 account+conv_id 读取;保留失败,不回退剪贴板。GUI/视觉读库通过后台线程队列,默认等待 0.35 秒。不要在这一读库线程做长耗时 ASR。 | | `protocol_engine.py:35` `_is_text_message` | 只允许类型 0/1/2。仅将语音 `content` 换成文字还不够,类型 4/16 仍被转人工。 | | `protocol_engine.py:65` `_unanswered_manual_reason` | 检查当前未回复批次,任一非文本即阻断。应保留对没有可靠转写的语音、图片、文件和混合未解析媒体的限制。 | | `protocol_engine.py:593` `_generate` | 将 `model_context_text(context)` 交给 `ai_chat.get_ai_reply` 或 `model_router.answer`,可沿原有上下文、知识库和裁判链路处理转写,不需要另建问答出口。 | 语音类型在 `wxwork_db.MSG_TYPE_MAP` 和归档映射中为 **4、16**。注意类型 69 是音频通话,不应自动当作可转写语音消息。 实际副本表结构: ```text msg_voice2text: message_id INTEGER PRIMARY KEY voice_id TEXT text TEXT collapse INTEGER voice_silent_transfer INTEGER ``` 已验证关联为:同一账号数据库中,`msg_voice2text.message_id = message_table.server_id`。**不是** `message_table.message_id`。`voice_silent_transfer` 的业务语义没有通过接口或客户端实验核实,不应仅凭字段名作为“转录成功”标志。 消息身份应继续保留 `account`、`conv_id`、`sender_id`、`server_id`、`rowid` 和 `dedup_key`。缓存表没有账号列,账号隔离来自选中的数据库连接;先核对消息属于该账号与目标会话,再使用其有效 server_id 查询。不能遍历所有账号数据库找同 sid 的任意转写。 ## 本机副本统计 由同目录 `audit_voice_schema_counts.py` 生成 `voice-schema-counts.json`。只查询 schema、COUNT 和是否非空;未 SELECT 正文。账号使用匿名编号。 | 副本位置(有消息账号) | 消息行 | 语音行 | 对方语音 | 非空转写 | 按 server_id 匹配语音 | | --- | ---: | ---: | ---: | ---: | ---: | | 源目录 `wxwork_reply_cache` | 657 | 3 | 3 | 3 | 3 | | 源目录 `archive_auto_backup/decrypted` | 657 | 3 | 3 | 3 | 3 | | 源目录 `wxwork_decrypted` | 654 | 3 | 3 | 3 | 3 | | 安装端 `wxwork_reply_cache` | 706 | 5 | 3 | 5 | 5 | | 安装端 `archive_auto_backup/decrypted` | 706 | 5 | 3 | 5 | 5 | 这些是同一账号的重复副本,不能相加。另一个匿名账号的已有副本均为零消息。所有发现的语音均为类型 16,均为 M:/S: 单聊。 审计连接使用 `mode=ro&immutable=1`、`PRAGMA query_only=ON`,不创建 journal/SHM;因此表格是现存解密副本主文件的快照,不包含未合入副本的 WAL,不能代表实时客户端完整数据。没有扫描原企微 Data/Cache 目录,也没有调用 `active_context`、`load_keys` 或解密入口。 ## 原生能力、音频路径与身份边界 `wecom_native_sender.py` 的 `NativeClient.validate`(`:177`)核对指定客户端构建、消息服务与登录 owner、account UID、发送入口;`NativeClient.send` / `NativeSender.send` 只构造并发送文本。`wecom_native_stub.py` 仅有 send、send_with_options、cleanup。没有语音上传、下载或转录调用。`wecom_native_protocol.py` 只是原生试验响应解析和历史结果展示,不提供 voice2text。不能把已有发消息身份验证等同于获得某个未实现语音 RPC 的权限。 现有本机缓存读取不需要新的接口 token,但必须复用当前账号既有、经过验证的读库连接与客户端身份,不读取其他账号、全局 sid 或猜测对端。此审计没有读取密钥内容。 `archive_exporter/wxwork_export_media.py` 的代码约定:原始媒体位于所选账号 `Cache/Voice`,注释说明为按 UUID 命名的 SILK v3;`extract_media_refs` 从消息二进制提取 UUID/URL/文件名,`build_cache_index(account_dir)` 建立该账号缓存索引,`match_media` 按 UUID、文件名、模糊文件名依次尝试。`export_media` 返回按裸 server_id 键控的字典,path 常为相对导出目录的路径。 **没有检验真实音频文件格式或可访问性**,上述格式和目录是代码声明,不能作为已完成实机音频验证。在线问答若走音频路径,至少要求当前账号缓存根路径包含校验、精确 UUID 关联、长度/格式上限与文件变化检测;不要继承“模糊文件名即关联”的归档便利逻辑,也不要自动抓取消息中任意 URL。 离线导出兜底不能原样当在线识别服务: - `media_map` 只有 server_id,`asr_missing` 查“任意账号相同 sid”并写 `('',sid)`;有跨账号碰撞风险。 - `export_media` 返回相对 out_dir 的 path,但 `asr_missing` 直接将它交给 decoder,缺少明确根目录解析。 - `.amr` 与 `.silk` 均走名为 `decode_silk_to_wav` 的同一分支;格式兼容和回退结果是否为有效 WAV 尚未测试。 - `asr_missing` 的日志包含转写前 50 字,不应直接进入在线生产诊断日志。 - 构建 Python 3.12 的 `pilk`、`faster_whisper`、`ctranslate2`、`av` 均不存在;requirements 也未列这些依赖。不能承诺当前 EXE 已有离线 ASR。 ## 视觉模式 源目录 `wechat_bot.py:6455` `_database_chat_text` 已优先从共享消息库取得 context.text,并核对当前标题、目标账号/会话、期望 dedup_key 与时效。因此统一解析层增加缓存转写可复用于视觉模式,不需要重新识别窗口。 `wechat_bot.py:15584` 的纯未转写语音分支直接请求客户补文字;没有自动右键、菜单定位或“转文字”点击实现。`ai_chat.py:531` 只过滤“转文字/正在识别/失败”等 UI 文案,不是转录功能。`:1953` 的视觉回复遇到语音会返回 `VISION_VOICE_NEEDS_TEXT`,只根据已经存在的可见文字决定转写标志;不会从波形、时长推断声音。 接入后应传递可信的 `voice_transcribed` 和来源字段供视觉/协议判断,而不是依赖模型自报“已经转写”。已有转写的文字可以按正常文本进入模型,同时在展示、归档中保留原媒体类型。纯截图分支仍不能读出音频内容。 ## 最小实现与测试建议 1. 在 `WXWorkDB` 当前账号连接中按有效 server_id 查询一条缓存;类型仅限 4/16。表缺失、字段缺失、空串、非字符串/畸形值、无效 sid 均返回“未取得转写”,不把异常当成正文。保留原类型和消息 ID,增加 `voice_transcribed=true`、`transcription_source='wecom_cache'`。 2. 协议 eligibility 仅把“语音类型且可信转写非空”的项作为可回答内容。混合批次必须每条语音均有转写,任何剩余媒体继续人工处理;不得因为批次末尾是文本而丢掉前面的语音。 3. 首次无转写时保留任务,后台按有限频率重新读目标 context,不阻塞 GUI/读库线程。`get_new_messages` 只按 message_table.rowid 推进,msg_voice2text 稍后更新不会发新消息事件,必须主动刷新待转写任务。重新读取后仍校验账号、会话、消息 ID、对方/我方方向及发送保护状态。 4. 成功转写后通过现有 `_generate` 进入上下文、知识库、裁判与人工审核流程。超时、错误、缺少主动转录能力时清楚显示原因,保留可重试状态;不能用占位符或猜测文本生成并自动发送。 建议最少覆盖以下隔离回归(未在本次只读任务中实现或执行): - 两个账号相同 server_id 返回各自转写,账号A绝不使用账号B或全局缓存。 - 相同本地 message_id、不同 server_id,以 server_id 关联;sid为空/0及不匹配返回未转写。 - 类型4与16成功、类型69不冒充语音;保持原 content_type、方向、dedup_key。 - 缺表、空白/状态文案/畸形转写、读库失败不产生可回答文本。 - 单语音、连续两语音、文本+语音与语音+图片批次;缺任何一条转写不部分自动回复。 - 新语音先到、转写后到且 message_table 没新增 rowid,待转写任务能恢复且仅生成一次。 - 等待时收到新消息、人工回复、切换账号、撤销关系或停止监听,旧任务不错误继续。 - AI 调用捕获合成输入,断言包含合成转写和正确历史,保留原知识库调用路径;不发原始媒体二进制或占位符。 - 视觉数据库优先路径使用可信转写;纯截图且无转写继续安全阻断。 - 所有测试临时库、合成转写、模型/发送 mock;无真实转录、下载、网络或客户发送。 本次执行验证仅包括代码静态审计、依赖存在性检查和 SQLite schema/aggregate 查询;未测试真实语音识别准确率、客户端转录触发或模型回答效果。