11 KiB
语音转文字链路只读审计
审计日期: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 是音频通话,不应自动当作可转写语音消息。
实际副本表结构:
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 和来源字段供视觉/协议判断,而不是依赖模型自报“已经转写”。已有转写的文字可以按正常文本进入模型,同时在展示、归档中保留原媒体类型。纯截图分支仍不能读出音频内容。
最小实现与测试建议
- 在
WXWorkDB当前账号连接中按有效 server_id 查询一条缓存;类型仅限 4/16。表缺失、字段缺失、空串、非字符串/畸形值、无效 sid 均返回“未取得转写”,不把异常当成正文。保留原类型和消息 ID,增加voice_transcribed=true、transcription_source='wecom_cache'。 - 协议 eligibility 仅把“语音类型且可信转写非空”的项作为可回答内容。混合批次必须每条语音均有转写,任何剩余媒体继续人工处理;不得因为批次末尾是文本而丢掉前面的语音。
- 首次无转写时保留任务,后台按有限频率重新读目标 context,不阻塞 GUI/读库线程。
get_new_messages只按 message_table.rowid 推进,msg_voice2text 稍后更新不会发新消息事件,必须主动刷新待转写任务。重新读取后仍校验账号、会话、消息 ID、对方/我方方向及发送保护状态。 - 成功转写后通过现有
_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 查询;未测试真实语音识别准确率、客户端转录触发或模型回答效果。