ZCode 开源后我拆了 4 个官方安装包:上传链路确实删了,那个开关却从没管过它

ZCode 开源后我拆了 4 个官方安装包:上传链路确实删了,那个开关却从没管过它

5600 颗星、1600 次 fork、两天——这是智谱把 ZCode 开源之后,GitHub 上给出的数字。而这款工具一周前被开发者抓包:登录状态下,它会把你本地整个代码仓库打包、加密、传到云端。

9 月 21 日,智谱在开源公告里说得很具体:v3.14.0 客户端已移除 Repo Wiki 功能,已切断本地仓库快照的生成与上传链路,并公布了信通院与绿盟的审计结论。

声明可以写得很干净,安装包不会。我把四个版本的 ZCode 从官方 CDN 下下来,解开内部的 app.asar,逐符号比了一遍。

一、三天之内发生了什么

时间 事件
9 月 18 日 开发者抓包:登录后 ZCode 静默打包本地工作区(含完整 .git 历史)加密上传至阿里云 OSS;官方当日致歉,称问题源于「代码库索引(Repo Wiki)」功能上线初期默认开启,已修复,并承诺开源代码库、引入第三方审查、给全体用户重置周额度
9 月 19 日 修复版 3.14.0 客户端发布
9 月 20 日 zai-org/ZCode 仓库建立
9 月 21 日 正式开源(Apache-2.0);公布信通院、绿盟审计结论;创始人唐杰实名投诉小红书网友「偷代码」传闻;社区发现开源版与官网版「不完全一致」

从曝光到开源,三天。速度值得肯定,但三天的信任重建只等于一份声明加一个仓库——能不能自证,要看你愿不愿意把安装包拆开看。

二、我把四个版本的安装包拆开比了一遍

官方说 v3.14.0 切断,我就把这条当假设去验。四个版本全部来自官方 CDN(cdn-zcode.z.ai),Linux arm64 的 AppImage,每个约 200 MB,解开 squashfs 后取出 resources/app.asar,再抽取桌面主进程的打包产物 out/host/index.js 逐符号计数。

符号(主机进程包内出现次数) 3.12.3(9/17 构建) 3.14.0(9/19) 3.14.1(9/20) 3.14.2(9/21)
RepoSnapshot(仓库快照子系统) 56 0 0 0
uploadCredential(上传凭证) 18 0 0 0
publicKeySpkiPem(服务端公钥) 2 0 0 0
keyWrapAlgorithm(信封加密算法) 2 0 0 0
encryptedSizeBytes(密文大小) 10 0 0 0
host/index.js 体积 2,588,119 B 1,496,653 B 1,497,846 B 1,497,917 B

结论与官方口径完全一致:事发前最后一版 3.12.3 里有一整套带 RepoSnapshot 前缀的代码,含 53 个具名函数;从 3.14.0 起,这些名字在客户端里一个都不剩,主进程包一次性瘦了约 42%(1.09 MB)。

同一套代码在命令行运行时里也被删了:3.12.3 的 zcode.cjs 有 3 处 RepoSnapshot,3.14.2 是 0。

服务端一侧也能对上一个便宜的证据。用 curl 直接打当时的上传凭证接口,今天两种方法都是 404;而同一台服务器上的 /api/v1/client/configs,GET 是正常的 200。这不是「全站 404」,是那条路由没了:

curl -s -o /dev/null -w '%{http_code}\n' https://zcode.z.ai/api/v1/snapshot/upload-credential   # 404
curl -s -o /dev/null -w '%{http_code}\n' https://zcode.z.ai/api/v1/client/configs              # 200

三、那段被删掉的代码,到底做了什么

更值得写清楚的是它原来的样子。废掉的 3.12.3 安装包本身还在 CDN 上,谁都能下——想自己复核的话,unsquashfs 解开 AppImage,用 asar 解析器取 out/host/index.js,搜 publicKeySpkiPem 就能看到这段代码。

流程和早先开发者还原的一致,但代码里还有几处此前没被提到的东西:

密钥由服务端掌控。客户端用随机 32 字节密钥 + 16 字节 nonce 走 AES-256-CTR 加密打包好的 tar.gz,再用服务端在凭证里下发的 RSA 公钥(SPKI)以 RSA-OAEP-SHA256 封装这把密钥,文件落盘为 repo-snapshot.tar.gz.enc。私钥从不出现在客户端,本地密文你自己解不开。

上传走 OSS 表单直传。凭证接口是 GET /api/v1/snapshot/upload-credential,返回 OSS 的 host、path、policy、x-oss-signature、x-oss-credential、x-oss-security-token、max_size,以及一个回执用的 callback。客户端组 multipart 表单,把密文以字段名 repo-snapshot.tar.gz.enc POST 上去,并在回执字段里带上 sessionId、queryId、requestId、failureCount、captureStage、historyRoundCount 等归因信息。

.git 被显式豁免了所有过滤规则。文件收集器里确实有一长串排除表:node_modules 等依赖目录、缓存、构建产物、符号链接、二进制文件、超过 1 MiB 的大文件,以及名字像密钥的文件(.env、.npmrc、id_rsa、.pem、.key、.p12、.pfx,以及文件名里含 token 或 secret 的)。但只要路径里出现 .git,函数直接返回「包含」,大小、二进制、密钥名三道检查一道都不跑。所以历史上被删掉的配置文件、早年的测试密钥、内部域名,全都随 .git 目录一起进了压缩包——过滤器拦得住当前目录里的 .env,拦不住提交历史里的 .env。

除了代码,还顺走了你的全局 Agent 配置。快照里有一个 extra 包,采集函数名字就叫 collectRepoSnapshotGlobalConfigs,内容包括:家目录下的全局 AGENTS.md(超过 20 MiB 才截断,截断标记写的是 ...[repo-snapshot-global-configs truncated])、用户级 MCP 服务配置(含启动命令、环境变量、请求头)、用户级技能与命令清单、hooks(含要执行的命令与参数)、memory 内容、子代理定义、插件清单,以及 17 项行为设置的当前值。

那个开关,从来没有管过它。客户端里有个设置项叫 repoSnapshotIndexingEnabled,界面上对应「仓库快照索引」。它在整个主进程包里只出现两次:一次是用户改动它时打个「用户已配置」的标记,一次是作为设置值被塞进上面那个 extra 包。没有一处代码读它来决定要不要采集、要不要上传。早先开发者说「没找到开关控制上传的逻辑」,我这次是从代码层面对上了。

它在你按回车之前就开始干活。捕获函数有两个入口:发送 prompt 之前(captureBeforePrompt,标记 captureStage: "prompt",prompt 正文一起进归因字段),以及任务完成之后。此外本地还有配额管理:单份密文上限默认 2 GiB,最多同时存在 3 份、保留 2 份,磁盘预算 6 GiB——这解释了为什么有人的 ~/.zcode 会悄悄涨到几百兆。

四、开源的那份,和你装的那份

开源当天,社区就发现 GitHub 上的代码和官网下载版不完全一样,「公关式开源」的质疑随之而来。这一条需要拆开说,因为两件事被混在了一起。

第一件,争议代码。我把开源仓库全库搜了一遍:RepoSnapshot、repoSnapshot、snapshot/upload-credential,全部零命中。开源版里唯一带 upload-credential 的地方是反馈附件的上传,那件事写在 NOTICE.md 的对外请求清单里,是用户主动提交工单才触发的。我又抽查了几个只可能来自同一份源码的字符串(例如主进程里那句「任务终态未读裁决」),开源仓库与线上 3.14.x 能对上——修复后的代码,两边是同源的。所以就「你审查的是哪一份 ZCode」这个问题,答案比社区猜测的更清楚:争议中的那段,两份里都没有。

第二件,商业功能。仓库自己的 NOTICE.md 第 70 行写得很直白:「受第三方版权、许可及再分发条件等约束,不承诺提供官方产品的全部功能及活动政策」。开源版少掉的是额度活动一类的商业化能力,官网仍在单独分发完整客户端。这属于「开源不等于免费送全部权益」的常规操作,但它确实意味着仓库不能替代对线上产品的审计——静态代码能证明「没有这段」,证明不了「线上跑的那份是什么」。

顺便说三个开源仓库的硬数据:6,973 个文件、14 个包、84.3 万行 TypeScript;NOTICE.md 27.7 KB,第三方声明 THIRD-PARTY-NOTICES.md 1.98 MB;2 个 commit、0 个 tag、0 个 release。星标 5,596、fork 1,597——fork 比例 28%,远高于正常仓库的 10% 到 15%,一部分是真想改,一部分可能只是想在自己账号下留个副本。

还有一个细节:仓库的 Issues 是关着的(GitHub API 返回 has_issues: false),PR 也是 0 条。官方在公告里说「把代码交给社区监督,欢迎开发者持续检查和反馈」——但反馈入口目前不在这个仓库里。

五、还没被验证的三件事

一、服务端的行为只能靠审计背书。信通院与绿盟的结论都指向同一个点:zcode-prod 的阿里云 OSS 存储桶「云端零数据」、全部对象与桶本身已删除。这对用户是好消息,但两份报告本身没有公开原文,外界看到的是官方转述的结论。国内机构做审计、三天出结论,效率和可信度都摆在那,唯一缺的是可复查的过程。

二、旧安装包还在。事发前的 3.12.3(9 月 17 日构建,199,710,914 字节)今天仍然能从官方 CDN 直接下载。这一点对想自己复核的人是便利,对没开自动更新的用户是风险——修复只在客户端,装了老版本的人不会因为服务端下线而变安全。

三、本地历史删不干净。无论客户端怎么改,已经被打包上传过的内容、以及 .git 里那些「以前删掉的密钥」,都不因为这次整改而变得不存在。涉及生产凭据的仓库,该轮换的密钥还是要轮换。

我的判断

这件事到目前为止,是一次兑现得比较扎实的信任修复:客户端删干净了,而且删得可以被任何人从安装包层面验证;服务端数据删除了,有第三方机构背书;开源把代码摆上了台面。

但信任重建有三条腿,现在只稳了一条。代码可自证,数据删除靠背书,长期制度还只是一句承诺——「常态化安全漏洞机制」怎么运转、反馈入口在哪、下次出事多久公布,都还没有可检验的样本。开源第一天就把 Issues 关掉,恰恰是这种落差最直观的注脚。

给正在用或者准备用的人三句话:

  1. 确认客户端版本不低于 3.14.0,这是官方口径里切断上传链路的分界,我的比对结果与之一致。
  1. 想自己复核,检查 ~/.zcode/v2/checkpoints 目录里有没有几百兆的密文文件;顺手看一眼 ~/.zcode 的总占用。
  1. 在用过老版本的机器上,把 .git 历史里出现过的密钥当作已泄露处理——文件名过滤挡不住提交历史。

—

相关阅读:

关注 AI 商业快讯,每天一篇 AI 热点深度解读。

ChatGPT 跨站追踪实证:一枚 Cookie 挂满一年,936 个广告像素把你在别处买的东西送回 OpenAI

ChatGPT 跨站追踪实证:一枚 Cookie 挂满一年,936 个广告像素把你在别处买的东西送回 OpenAI

一枚 Cookie,名字叫 __obi,挂在 .openai.com 域下,有效期整整一年。它的响应头里写着 SameSite=None——这是让浏览器在跨站请求里也把它带上的配置。而在同一批请求里,OpenAI 的其他 Cookie 全被浏览器拦了下来,只有它被放行。

9 月 20 日,独立研究者 Buchodi 发布了一份逆向分析:任何在 ChatGPT 上投过广告的公司,只要在自己站点装上 OpenAI 的追踪代码,你在这家网站上看过的商品、读过的文章、填过的表单,就会连着一枚能对上你 ChatGPT 账号的标识,回到 OpenAI 手里。这份分析当天冲上 Hacker News 头版,我记录到的数据是 698 分、371 条评论。

三步:从你的 ChatGPT 账号,到别人家的网站

第一步发生在你打开 chatgpt.com 的那一刻。浏览器生成 16 字节随机数,调用 POST /backend-api/bazaar/obi/sync-token;后端签回一枚 RS256 的 JWT,60 秒后过期,里面把 sub(账号标识)和 obi(22 字符标识)钉在一起。bzr 是 bazaar 的缩写,OpenAI 内部对广告平台的叫法。

第二步是换 Cookie。客户端拿着这枚令牌,跨站 POST 给 bzr.openai.com/v1/obi/sync,服务器回种 __obi:域为 .openai.com、HttpOnly、Secure、有效期一年、SameSite=None。研究者贴出的响应头原文如此。

第三步发生在广告主的网站上。装了 OpenAI 测量像素的页面,会把 __obi 随请求一起送回 OpenAI——包括加载 bzrcdn.openai.com/sdk/oaiq.min.js 这个脚本本身的请求。也就是说,页面一打开、OpenAI 的代码还没运行,标识就已经送出去了。

环节 具体动作 关键参数
发证 chatgpt.com 调 backend-api/bazaar/obi/sync-token JWT,60 秒过期
换 Cookie 跨站 POST bzr.openai.com/v1/obi/sync 一年有效期,SameSite=None
回传 广告主页面加载像素脚本、上报事件 请求自动带上 __obi

研究者给出的观测规模:几个月收集到的流量覆盖 936 个广告像素、1,029 个域名;在他自己手机上,同一个 __obi 被 Chewy、Wayfair、ThriftBooks、Eventbrite、HelloFresh、Coursera、SeatGeek 等 12 个商业站点送回 OpenAI,每一次都被服务端以 202 收下。他还解码了 932 枚同步令牌,其中 736 枚对应登录账号,196 枚是匿名主体——没登录也不影响,匿名标识同样能稳定存在 27 天以上。

我把那 82 KB 的追踪脚本下下来读了

不带 ChatGPT 账号,也能核验其中一大半。我从广告主被官方要求加载的那个地址把脚本抓了下来:bzrcdn.openai.com/sdk/oaiq.min.js,82,799 字节,里面写死的版本号是 0.1.41、类型 oaiq-web。研究者文章里引用的行为,来自更早的 0.1.31 时代。

这段代码里有四件事值得单独说。

先说第一件:它不只会收广告主给的数据,还会自己去页面上找。身份来源被分成四类:in(广告主主动传入)、fm(表单字段)、ht(渲染出来的页面文字)、js(标签管理器总线)。代码会去读 window.dataLayer、adobeDataLayer,甚至解析 gtm.js 的 URL 参数,找出改过名字的 GTM 容器。研究者统计的抓取记录里,从页面上扒来的身份比广告主主动交出来的更多——685 条对 255 条。

*姓名仍在自动采集清单里。* 代码中的身份定义表写着:email、phone、firstName、lastName 四项都是 automatic: true,只有 externalId 是 false。地理字段 country、city、region、postal_code 不进哈希,直接明文发送。研究者称采集范围在 8 月 27 日收窄过一轮,但从 0.1.41 的代码看,姓和名依然在自动匹配集合中。

再往下看:它自己划的红线很细。代码里有一张排除名单正则:密码、一次性验证码、CVV、卡号、社保号、生日、病历、诊断、法院案件、罪名都不碰;同时排除的还有礼物收件人、捐赠人、第三方、客服、推荐人、卖家——也就是别人的身份信息。这份名单说明 OpenAI 清楚哪些字段是雷区,边界画得很明确。

最后一件,开关不在广告主手里。每个像素会去 bzrcdn.openai.com/pixel-config/v1/ 加像素 ID 取一份配置,其中有一个布尔字段 automatic_advanced_matching_enabled——自动匹配是否开启,由 OpenAI 的广告后台按账户下发。

我还直接对着端点发了请求。带 Origin: https://chatgpt.com 时,预检返回 204,响应头里 access-control-allow-origin 回显 chatgpt.com、access-control-allow-credentials: true,也就是允许带凭证的跨站调用;换成别的 Origin,直接返回 403 Origin is not allowed.。令牌不对时是 401 Invalid or expired sync token.,内容类型不对则提示必须是 text/plain。一个只对自家前端开放、还要求带凭证的绑定接口,正是把账号身份写进跨站 Cookie 所必需的那一环。

官方文档和 Cookie 政策,写的都是另一套

OpenAI 自己的测量像素文档里,「给用户数据」是广告主主动做的事:可选地在初始化时传入 user 对象,邮箱与手机号按规则规范化后做 SHA-256,用来提高转化匹配率。文档没有说这套 SDK 会自己从表单和页面文字里抓。

再看 Cookie 政策。OpenAI 官网中文版的 Cookie 列表里,「分析型 Cookie」一栏只有一行:来源 OpenAI、名称 __obi、时长一年、用途「分析」、域 chatgpt.com 与 openai.com。营销绩效那一栏里排的是 LinkedIn 等第三方。换句话说,这枚承担跨站身份绑定的 Cookie,官方登记的身份是「分析型」。

研究者 9 月 14 日给 OpenAI 的 press 与 privacy 邮箱发过邮件,问了两个问题:为什么 __obi 被归类为分析 Cookie;只同意分析、拒绝营销的用户,是不是依然会收到它。回复来自客服支持:已收到,会转内部复核。两个问题都没有正面回答。

广告业务比这套机制跑得更快

把时间线摊开,这套东西为什么存在就不难理解了:

时间 事件
2026 年 2 月 9 日 ChatGPT 面向美国 Free 与 Go 用户开始展示广告
2026 年 4 月 30 日 免费用户的营销 Cookie 默认开启,WIRED 实测两个账号均为默认开
2026 年 5 月 5 日 自助广告管理平台向全美企业开放,取消 20 万美元投放门槛
2026 年 8 月 31 日 OpenAI 宣布 ChatGPT Ads 年化收入运行率达 10 亿美元,上线不足 200 天

按 OpenAI 8 月 31 日官方公告的口径:数万家广告主在使用,业务覆盖 40 多个国家,ChatGPT 周活跃用户超过 10 亿;「Pixel 和转化 API 已成为衡量与优化的重要基础」;并强调广告主无权访问用户的私人对话。据投资人披露的口径,OpenAI 2026 年广告收入目标 25 亿美元,2030 年 1000 亿美元。

广告主确实拿不到你的对话,但「衡量转化」需要知道看过广告的人有没有在别处下单——__obi 就是这条链路里的中间件。它把「在 ChatGPT 里看过一则广告」和「在 Chewy 上买了一次狗粮」接在了同一个人身上。

它证明了什么,没证明什么

边界要说清楚,这决定了你该有多紧张。

已被独立核验的:机制与代码(我下载脚本逐段读的)、绑定端点的访问控制(我实测的)、Cookie 分类(OpenAI 官方政策页原文)。研究者实测、我未能复现的:__obi 真的随广告主页面的请求回传。我没有登录态的 ChatGPT 账号和安卓 Chrome 环境,这一步没跑通。仍是推断的:OpenAI 服务端是否把回传落到你的账号上——观测到的只有 202(已接受),研究者本人在文末写明「这个 join 没有被观测到」。

受影响的人也没那么多:观测环境是安卓版 Chrome;iOS 上所有浏览器跑的都是 WebKit,Safari 的智能防跟踪拦第三方 Cookie,机制不生效;桌面 Chrome 未测;大约每 5 次 ChatGPT 会话才下发一次令牌。

放在广告行业里,这套机制与 Meta Pixel、Google 标签是同构的:登录态、第三方 Cookie、站外转化归因。不一样的地方在于它跑在一个 AI 对话产品上——人们会告诉 ChatGPT 一些不会发朋友圈的事,而这个产品正在越来越多地替人做决定。研究者的原话是:机制本身是标准广告技术,没有先例的是它跑在 AI 聊天产品上。

想关的话,路径在 ChatGPT 的设置 → 数据控制,那里有分析和营销两个独立开关,可以只留一个。另外,广告主自己也看不到 __obi,它在广告主脚本读不到的域上——所以别指望商家能替你把这件事关掉。

我的判断是:这不是漏洞,是选择。OpenAI 在 Cookie 政策里如实登记了这枚 Cookie,也在代码里划了红线,但它没有把「分析型 Cookie 会被用来做跨站身份绑定」这件事讲明白。在广告收入年化冲上 10 亿美元的当口,讲明白的代价,大概比划一条红线要高。

相关阅读:OpenAI推出自助广告管理工具:AI广告投放如何变现? | OpenAI 与微软未删节文件曝光:Copilot 把纽约时报点击率打掉 93%,内部称抓取是「史上最大劳动盗窃」 | 三人用 Claude 黑进 OpenAI 内部代码库:一个图片上传漏洞,72 小时打通到代码提交

关注 AI商业快讯,每天一篇 AI 热点深度解读。