修了一夜桥,桥本来就在——微信文件直读链路打通记

修了一夜桥,桥本来就在——微信文件直读链路打通记

2026 年 10 月 17 日凌晨,我在微信里随手发了一份 markdown 文件给自己搭的 AI 助手。三十一秒后,它把全文读完、归档、解读完毕,答案就落在同一个对话框里。

这件事本身平平无奇——发给一个人类同事,本来就该是这样。平平无奇到让人想不起来:为了这三十秒,前一天夜里我们翻了协议、签了 COS、写了一个上传端点、被一个「自动化」方案带进沟里,最后在一行日志里发现,这座桥其实一直就在那里,亮着灯。

这篇文章记录这一夜。它不是教程,虽然教程该有的要素它都有(抓包、签名、端点设计)。它真正想说的是一件事:打通一条链路,最难的不是把断的地方接上,而是看清哪里才是断的地方。

一、那堵叫 100002 的墙

先交代背景。我的微信里住着一个 AI 助手(weclaw 编排的 ima 智能体,下文叫它小 I)。某天我发给它一篇文章的 markdown,想让它解读,它回我:附件读取报 100002。让它去知识库里搜,说搜不到。

第一反应当然是「哪里配错了」。鉴权?权限?代理?排查一圈,最后确认了一个令人无语的真相:这不是 bug,是设计。App 里上传的文件走 file_manager 通道,而这个通道对外部工具就没有留读取的门。100002 不是墙坏了——墙就是墙。

分辨「墙坏了」和「墙就是墙」很重要,前者要修,后者只能绕。我们的绕法很直接:正门不开,自己造一扇。

二、凌晨的协议考古

好在墙不是全封闭的。之前抓过 ima iOS App 的包,上传接口明摆着:POST /cgi-bin/file_manager/create_media。真正的考古从这里开始。

抓包揭示的第一个反直觉设计:create_media 只登记,不传字节。调用它,服务器一次性下发三样东西:一个 media_id(形如 markdown_cls1_…);一组腾讯 COS 临时凭证(bucket 在 ap-shanghai,有效期 12 小时);一个 pkb 签名读 URL(同样 12 小时)。

也就是说,文件字节根本不过 ima 的服务器,要自己签好名,PUT 进腾讯 COS。签名是 COS XML API 那套 q-sign-algorithm=sha1 三段式:KeyTime 算出 SignKey,再拿 StringToSign 算出 signature。文档看着吓人,实测有两个巨大的简化:q-header-list 置空、只签路径就够用;x-cos-security-token 当普通头带上即可。

第二个反直觉:没有 finalize 调用。PUT 成功即入库,不需要再通知 ima 一声「传完了」。干脆得让人不放心,回读验证了三遍才敢信。

第三个坑藏在回读上:pkb 的 URL 少任何一个 query 参数都不行——?t=…&sign=… 本身是签名的一部分,只取 pathname 就是 403 missing parameter t。错误报得很清楚,但第一次撞上还是会愣一下。

考古到最后还有一层体会:所有凭证的有效期都是 12 小时。COS 临时凭证如此,签名读 URL 也如此。这意味着这条链路造出来的每个文件链接都是「会过期的」——它天然是给「现在就要读」的场景用的,不是永久存储的凭证。想通这一点反而让人踏实:过期的设计逼你把临时阅读和长期归档分开,而这两件事本来就不该混为一谈。

三、造一座桥:POST /v1/files

考古完毕,把整条流程封装进我的 ima-proxy(本地 OpenAI 兼容代理),新增一个端点:

POST /v1/files  
{"filename": "xx.md", "content": "……全文……"}  
→ {"id": "markdown_cls1_…", "url": "https://pkb.ima….?t=…&sign=…", "bytes": 12032, "etag": "…"}  

为什么做成 OpenAI 风格?不是情怀,是生态:文件对象长成这个形状,所有讲 OpenAI 方言的工具都认得。返回的 url 是 12 小时免鉴权读链接,谁拿到都能直接 GET 全文。

从局域网另一台机器上 curl 过去实测:上传成功,media_id 回来,pkb URL 回读,内容一字不差。桥造好了,单向通车——任何机器上的任何 agent,一行 HTTP 就能把文件送进 ima 的文件管理器,还附赠一个可直接阅读的链接。

到这里一切顺利。顺利到很快就要犯一个形而上的错误。

四、走错的那条路:自动化的歧途

桥通了,下一步「集成方案」顺理成章地冒出来:在 Mac 上开一个监听目录,文件往里一丢,自动上传、自动推送微信通知。丝滑吧?全自动吧?

我回了三个字:「不太对。」然后补了一句:「应该上传直接在对话呈现结果。」

这句话值得单独一行。它推翻的不是方案细节,是整个形状。监听目录方案里,文件走的是旁路:丢文件夹、推送、确认,每一步都在对话之外完成,对话框里只剩一个结果的通知。而我从头到尾只想要一件事:在微信的对话里发文件,在同一块对话框里得到回应——像发给人一样。

差异的本质:方案要的是文件「到达」,我要的是对话「继续」。这两种形状的差别,放在人身上一试就懂:同事收到你发的文档,回一句「收到了,放在共享盘了,链接在此」,和直接在聊天里说「看完了,第二节有个问题」——前者是文件管理,后者是协作。旁路再顺滑,也是两个世界之间的穿梭;对话是单一世界,注意力从头到尾不离开那个输入框。产品的形状不由技术可能性决定,由用户的注意力住在哪里决定。

被毙掉方案之后,小 I 做了一件本该更早做的事:去读 weclaw 源码,看看文件消息到底是怎么递给 agent 的。

五、桥本来就在

然后就是这一夜最大的发现。

weclaw 处理带文件的消息时,对文本类附件干的事情简单粗暴:readFileContent 直接读文件,formatFileBlock 把全文塞进消息——

[文件: REA 逆向之道.md | 68269 字符 | /home/nanobot/…/acf3f61f-….md]  
````(全文)````  

也就是说,≤2MB 的文本文件(md/txt/json),内容本来就会原文注入对话,agent 不需要任何工具就能读到。超过 2MB 保首尾截断;pdf、docx、xlsx、pptx 有内置解析器。历史管理的设计也值得叫好:文件块在最近两轮对话里保持全文,之后再折叠——因为追问通常紧跟文件到达,折叠窗口太小,第二个问题就把全文弄丢了;窗口太大,多个大文件叠起来又把上下文烧穿,所以另有 3MB 的总预算兜底。这套参数显然是踩过坑的人调出来的。

本以为这只是个「理论上的存在」,结果日志把脸打得扎实:就在埋头造桥的那几分钟里——05:42,一个 24KB 的 json;05:44,一份 165KB 的书稿 md——两次 file content read,两次对话内直接解读成功,零工具调用。我根本没等它,随手发的两个测试文件,早就一路绿灯走通了。

那最初读不了的 article.md 呢?也复盘清楚了:它走的是 ima App 上传那条路,压根没经过 weclaw。我们对着一堵本来就是墙的墙修了一夜的桥,而旁边那条真正的通路一直亮着灯。

桥不算白造(下一节讲它的真用处),但「桥本来就在」这个事实,值得每个做集成的人刻在脑子里。

六、把桥放到该在的位置

原生的路当然有边界,边界之外才是新桥的岗位:

  1. 超长文件。>2MB 注入会截断,这时调 upload_file(把 /v1/files 下沉成了 ima-mcp 里的一个 MCP 工具)拿 12 小时免鉴权 read_url,再抓取全量,文件上限也放宽到 20MB。
  2. 反向通道。agent 自己产出的内容要「存进 ima」、手机 App 里能看到——这是原生注入完全覆盖不了的方向。

下沉时补了两个工程细节:Bearer key 落盘独立文件、权限 0600(密钥不进代码、不进日志);全链路 standalone 跑通一轮才注册进工具表。system_prompt 里同步写清分工:正文直读为主,upload_file 只管截断兜底和存档两个场景,并明令禁止对附件路径走知识库检索——那是 100002 的老家。

七、为什么不直接用现成的轮子

复盘时被问得最多的问题是:这么常见的需求,怎么会有现成方案?答案是有,但每个都差一口气:

  • 微信文件助手能传文件,但那是个私人网盘,文件进了助手就到不了 agent 手里;
  • 企业微信 / 公众号机器人有文件消息 API,可那意味着换一个账号体系——我的 agent 住在个人微信里,靠它才有「随手发」的松弛感;
  • 知识库导入(RAG)看起来最像正解:文件进了索引,agent 能检索。但检索给的是碎片,不是全文,而且 file_manager 上传的东西根本不进索引——最初那个 100002 正是从「以为进了知识库」来的。想要全文还得另走一遍读取,绕回原点;
  • 再造一个 App / 网页入口当然最自由,但那等于让用户离开微信——恰恰违背了初心。

每条现成的路都能解决「文件传过去」,唯独解决不了「在同一个对话框里发、在同一个对话框里答」。有时候自造轮子的原因不是轮子不存在,是所有现成轮子的轴距都差半寸。

八、打通之后的世界

早上六点,我往微信里发了一轮「压力测试」。日志后来把那二十分钟拼成一张全景图:

  • 发一份 49KB 的 md:内容直注入,31 秒后归档加解读完毕;
  • 发一份 1.9KB 的短文:17 秒,直接读,顺手纠正了文中两处过时的说法;
  • 发一张图:vision 描述注入,30 秒看图作答;
  • 说「写三千字 ima 使用心得」:43 秒,联网搜完素材直接成文;
  • 说「发博客」:意图路由自动切到发布 agent,34 秒后文章上线;
  • 问 ima 问问的用量限制:44 秒,多源检索梳理完毕。

四条链路——文件直读、vision、意图路由、联网搜索——各就各位,没有一条需要人肉中转。像清晨的路口,绿灯一亮,车流自己开始走。

那个画面值得记住。因为凌晨三点的时候,我们还在对着一堵墙研究砖缝。

九、沉淀

一夜下来,四条心得,按说服力排序:

**第一,最好的集成是不集成。**动手造轮子之前,先把要接的系统源码读一遍。文件直读功能在 weclaw 里一直存在,只是没人告诉我,而我们默认它不存在。默认,是做集成的人最贵的资产。这一夜造出来的桥并非无用——超长文件和反向存档两个岗位它至今在岗——但它的价值是在「发现原生通路」之后才被正确定位的,顺序反了就会变成重复建设。

**第二,先分清「坏了」和「设计如此」。**100002 不是待修的 bug,是待绕的墙——但前提是判断准确。把「设计如此」当 bug 修,会修出一堆补丁;把「坏了」当设计绕,会白绕很远。判断的办法说穿了不值钱:找到返回这个错误码的那段代码或那条文档,看它是 if 出来的还是 throw 出来的。

第三,一句纠正胜过十页 PRD。「不太对,应该上传直接在对话呈现结果」——十几个字,推翻一个方案、指明一个形状。需求文档写得再细,也不如用户看着你的方案说「不对」的那一刻信息量大。做个人工具链的额外优势是:说「不对」的人和验收的人是同一个,反馈回路短到几乎没有延迟。

**第四,日志不说谎。**Agent 的回复可以客气、可以含糊,日志行不会。file content read: 165436 bytes 这一行比任何流畅的对话都诚实——它证明了桥早已通车,也证明了造桥的那几个小时里,真正的通路一直在运转。排障时对话要看,但定案要靠日志。

下一次再想「给这条链路加个功能」的时候,我会先去看一眼日志。

说不定,桥又已经在了。