
1. 从一次打包失误说起source map 还原 TypeScript 工程结构到底能做什么Claude Code 源码泄露这件事真正值得技术人关注的不是八卦而是它把一个平时藏在构建产物里的东西推到了台前source map。51 万行 TypeScript、1900 多个文件能从一个 57MB 的.map文件里被还原出来靠的就是 source map 里记录的映射关系。这篇文章不聊谁对谁错只聊一件事——如果你手上有一个 source map 文件怎么把它还原成可读的 TypeScript 工程目录以及从还原出来的结构里能看出一个大型 AI 编程工具是怎么组织代码的。source map 本质上是一张“翻译对照表”。打包工具把几十上百个.ts文件压缩、合并、混淆成一个.js文件时会同时生成一个.map文件里面用 VLQ 编码记录了“压缩后第几行第几列”对应“原始文件第几行第几列”。浏览器开发者工具能让你在调试时看到原始代码靠的就是它。反过来说只要拿到这个.map就能反向把原始文件结构和内容大致拼回来。这件事对普通开发者有什么用三个场景。第一你在研究某个前端或 Node 工具的架构官方没开源但发布包里不小心带了 map你就能还原出它的模块划分。第二你自己团队的项目某次发布后发现 map 被误打包进去了你需要快速评估泄露了什么。第三你想学习大型 TypeScript 工程怎么分层——路由、工具调用、沙箱、上下文管理这些模块是怎么拆目录的。Claude Code 这次泄露之所以被反复讨论就是因为它是运行在用户本地的客户端工具核心逻辑全在代码里还原出来的目录结构本身就是一份架构教材。需要先明确一点本文所有操作都基于“你合法持有的 source map 文件”用于学习工程结构和排查自己项目的打包问题。不要用它去还原别人的闭源商业代码再分发那是另一回事。下面从工具准备开始一步步走完还原流程。2. 还原前的准备TaoToken 接入与 source map 解析环境搭建要跟做后面的步骤你需要一个能跑 Node 脚本的环境以及一个能调用模型帮你批量分析还原后代码的入口。前者用本地 Node 就行后者我用 TaoToken 来做因为它把多家模型的调用统一成一个 OpenAI 兼容接口脚本里换模型只改一个字符串省得为每个模型单独写适配。先说环境。Node 建议 18 以上npm或pnpm都行。核心依赖是两个source-map这个库用来解析 VLQ 映射jridgewell/trace-mapping是它的现代替代品速度更快我实测下来大文件用后者更稳。安装命令mkdir sm-restore cd sm-restore npm init -y npm install jridgewell/trace-mapping source-map然后是 TaoToken 的接入。它的 API 地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions格式。你需要先在控制台创建一个 API Key地址在https://taotoken.net/console/api-keys。拿到 Key 之后把它写进环境变量别硬编码在脚本里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api模型 ID 这块要注意TaoToken 上不同模型的 ID 不一样你在模型列表里选一个适合代码分析的比如带长上下文能力的型号把它的 ID 记下来后面脚本里要用。如果你不确定选哪个可以先去模型对话页面试一下地址是https://taotoken.net/models直接对话感受一下不同模型对代码的理解能力再决定用哪个跑批量分析。为什么还原 source map 还要接模型因为还原出来的 1900 多个文件你不可能一个个手读。你需要模型帮你做三件事按目录归类模块、提取每个模块的导出接口、画出调用链。这些用脚本调模型批量处理比人肉快得多。TaoToken 在这里的价值是一个 Key 打通多个模型你可以在同一个还原流程里用便宜模型做粗分类用强模型做深度调用链分析成本可控。再准备一个目录放还原产物mkdir -p output/sources output/reportsoutput/sources放还原出来的.ts文件output/reports放模型生成的分析报告。到这里环境就齐了下一节进入真正的解析步骤。3. 可复制的 source map 解析配置从 .map 到 TypeScript 目录树这一节是核心给你能直接跑的脚本和配置。整个还原分三步读 map、提取 sourcesContent、按原始路径写文件。先看 map 文件的结构。一个标准的 source map 长这样{ version: 3, file: cli.js, sources: [../src/index.ts, ../src/agent/loop.ts, ../src/tools/bash.ts], sourcesContent: [import ..., export ..., ...], names: [], mappings: AAAA,IAAM,... }关键字段是sources和sourcesContent。sources是原始文件的相对路径数组sourcesContent是对应的原始文件内容数组。如果打包工具把sourcesContent也写进去了很多工具默认会写那你根本不用解析mappings那串 VLQ直接按sources的路径把sourcesContent写出来就行。Claude Code 这次泄露的 map 就带了完整的sourcesContent所以还原门槛极低。写一个还原脚本restore.jsconst fs require(fs); const path require(path); const MAP_FILE process.argv[2] || ./input.map; const OUT_DIR process.argv[3] || ./output/sources; const raw fs.readFileSync(MAP_FILE, utf-8); const map JSON.parse(raw); if (!map.sources || !map.sourcesContent) { console.error(该 map 不含 sourcesContent需要走 mappings 解析路径); process.exit(1); } let written 0; map.sources.forEach((src, i) { const content map.sourcesContent[i]; if (content null) return; // 去掉开头的 ../ 防止写出到上级目录 const safe src.replace(/^(\.\.\/)/, ); const target path.join(OUT_DIR, safe); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content, utf-8); written; }); console.log(还原完成共写出 ${written} 个文件到 ${OUT_DIR});跑起来node restore.js ./claude-code.map ./output/sources如果 map 里没有sourcesContent就得用jridgewell/trace-mapping配合原始.js文件反解。这种情况复杂一些需要逐行解析mappings把每个映射段对应的原始位置找出来再从原始.js里截取对应代码。大部分现代打包工具都会带sourcesContent所以先检查这个字段有就直接用上面的脚本。还原完之后你会得到一个和原始工程几乎一致的目录树。用tree看一眼tree -L 3 output/sources典型的大型 AI 编程工具目录大概长这样src/ ├── agent/ # 主循环、任务调度 ├── tools/ # 工具调用实现bash、read、write、edit ├── context/ # 上下文管理与压缩 ├── sandbox/ # 代码执行沙箱 ├── api/ # 模型接口封装 ├── ui/ # 终端交互 └── utils/ # 通用工具这个结构本身就是设计密码agent和tools分离说明工具调用是插件式的context单独成模块说明上下文管理是核心难点sandbox独立说明执行安全被当作一等公民。你还原自己的项目时也可以对照这个分层看自己的目录是不是把职责混在一起了。如果你想把还原后的代码喂给模型做批量分析可以用 TaoToken 的接口写个批处理脚本。配置片段如下注意 Base URL 和 Key 的写法const BASE_URL process.env.TAOTOKEN_BASE_URL; // https://taotoken.net/api const API_KEY process.env.TAOTOKEN_API_KEY; const MODEL_ID 你选的模型ID; async function analyze(filePath, code) { const res await fetch(${BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY} }, body: JSON.stringify({ model: MODEL_ID, messages: [ { role: system, content: 你是代码架构分析助手输出该文件的职责、导出接口、依赖模块。 }, { role: user, content: 文件${filePath}\n\n${code} } ] }) }); const data await res.json(); return data.choices[0].message.content; }这个脚本对每个还原出的文件调一次模型把结果写进output/reports。1900 个文件全跑一遍成本不低建议先按目录抽样比如每个模块挑 2 到 3 个代表文件跑出模块级理解后再决定要不要全量。4. 验证还原结果用调用链请求确认模块关系是否真实还原出文件不等于理解架构你得验证模块之间的调用关系是不是真的。这一步用“调用链请求”来做从入口文件出发顺着 import 关系往下追看能不能串起一条完整的执行路径。先找入口。看package.json的bin字段或main字段通常指向dist/cli.js或src/index.ts。还原后的源码里入口文件一般会 import 主 agent 循环。写个脚本提取 import 关系const fs require(fs); const path require(path); function extractImports(file) { const code fs.readFileSync(file, utf-8); const re /import\s.*?from\s[](.?)[]/g; const deps []; let m; while ((m re.exec(code)) ! null) { if (m[1].startsWith(.)) deps.push(m[1]); } return deps; } function walk(entry, seen new Set()) { if (seen.has(entry)) return; seen.add(entry); const deps extractImports(entry); console.log(entry, -, deps.join(, )); deps.forEach(d { const resolved path.resolve(path.dirname(entry), d); const candidates [resolved .ts, resolved /index.ts, resolved]; for (const c of candidates) { if (fs.existsSync(c)) { walk(c, seen); break; } } }); } walk(./output/sources/src/index.ts);跑出来你会看到一条从入口到工具调用的链路比如index.ts - agent/loop.ts - tools/registry.ts - tools/bash.ts。这条链就是“调用链”。验证它是否真实有两个办法。第一个办法是静态验证检查每个被 import 的模块是否真的导出了被使用的符号。比如loop.ts里import { runBash } from ../tools/bash你就去bash.ts里看有没有export function runBash。有说明还原完整没有说明还原时丢了内容或者路径映射有偏差。第二个办法是动态验证把还原出的代码在本地跑一个最小用例。比如只加载tools/bash.ts调用它的导出函数看能不能正常执行一条echo hello。这一步能验证还原出的代码语法是否完整、依赖是否齐全。注意别直接跑主循环那会真的去调模型接口浪费额度。用模型辅助验证调用链也很有效。把入口文件和它直接 import 的几个文件一起发给模型问它“这条调用链的职责边界在哪里哪个模块负责安全校验”。模型的回答能帮你快速定位关键模块。这里用 TaoToken 的模型对话入口试最方便https://taotoken.net/models把代码贴进去直接问不用写脚本。验证通过后你会对这套架构有一个从“目录好看”到“真的能跑通”的认知升级。这一步别省很多人还原完文件就停了结果对架构的理解全是猜的。5. 还原过程中常见的报错与排查401、local proxy failed、reading choices 怎么处理这一节列几个我实际踩过的坑都是还原和分析流程里高频出现的报错给你对照排查。报错一401 Unauthorized。调 TaoToken 接口时最常见。原因通常是 Key 没传对。检查三处环境变量TAOTOKEN_API_KEY是否真的 export 了echo $TAOTOKEN_API_KEY看有没有值请求头是不是Authorization: Bearer sk-xxx注意 Bearer 后面有空格Key 是不是在控制台被禁用或额度耗尽。如果用的是https://taotoken.net/api这个 Base URL路径要拼成/v1/chat/completions别漏了/v1。报错二local proxy failed或连接超时。这个一般是你本地网络环境或代理配置导致的。检查你的 shell 里有没有设置HTTP_PROXY、HTTPS_PROXY这类环境变量如果有脚本请求会走那个代理代理不通就报这个错。临时清掉unset HTTP_PROXY HTTPS_PROXY再跑一次。另外确认你的 Base URL 写的是https://taotoken.net/api不要自己加端口或改成 http。报错三Cannot read properties of undefined (reading choices)。这是解析响应时data.choices为 undefined。原因通常是接口返回了错误对象而不是正常响应比如{error: {message: ...}}。排查方法在const data await res.json()后面先打印console.log(JSON.stringify(data))看实际返回了什么。常见触发原因是模型 ID 写错了或者请求体里messages格式不对。模型 ID 一定要用你在模型列表里看到的准确字符串大小写敏感。报错四还原出的文件是空的或乱码。说明sourcesContent里对应项是 null或者编码不是 utf-8。先检查 map 文件本身是不是完整的 JSON用node -e JSON.parse(require(fs).readFileSync(x.map))验证。如果sourcesContent大量为 null说明打包时没写入内容只能走 mappings 反解路径那就需要原始.js文件配合复杂度上一个台阶。报错五OAuth相关报错。如果你在还原后的代码里看到 OAuth 流程并且想本地跑通登录逻辑可能会遇到 token 交换失败。这类报错通常是回调地址或 client id 不匹配导致的属于业务逻辑问题不是还原问题。排查时先确认你跑的是还原代码还是官方代码别把两边的配置混用。报错六ENOENT: no such file or directory。写文件时路径不存在。上面的还原脚本里已经用fs.mkdirSync(..., { recursive: true })处理了如果你自己改脚本漏了这行就会报。另外注意sources里的路径可能带../直接拼会写到输出目录外面脚本里的replace(/^(\.\.\/)/, )就是干这个的。排查这类问题的通用思路先看报错原文定位是网络层、鉴权层还是数据层网络层查代理和环境变量鉴权层查 Key 和请求头数据层打印原始响应。别一上来就改代码先确认输入是什么。6. 把还原流程用起来从架构学习到 Coding Plan 的落地路径还原 source map 这件事学一次就能复用。你可以把它变成一个固定流程拿到 map → 跑还原脚本 → 提取 import 关系 → 模型批量分析 → 输出架构报告。这套流程不仅能用来研究别人的工具更能用来审计自己团队的发布产物——每次发版前跑一遍确认没有 map 被误打包进去。如果你打算长期做这类代码分析和 AI 编程工具的架构研究单次调用模型的方式会比较零散。TaoToken 的 Coding Plan 更适合这种持续性的编码和分析场景地址在https://taotoken.net/coding-plan它把额度打包成订阅制你跑批量分析脚本时不用每次算 token 成本心里有底。我试过用它跑一个 200 文件的批量架构分析流程很顺不用中途担心额度。具体落地建议分三步。第一步把本文的还原脚本存成自己的工具加上参数校验和日志以后任何 map 文件拖进来就能还原。第二步把模型分析脚本改成可配置的模型 ID、并发数、输出格式都从配置文件读这样换模型不用改代码。第三步把还原和分析串成一条命令比如npm run analyze -- ./xxx.map一键出报告。最后提醒一句还原出来的代码只用于学习和排查自己的项目别拿去二次分发或做成产品。source map 泄露本身是发布流程的失误你能从中学到的是“发布前检查清单”该加哪几项——比如构建产物里不能有.map、.npmignore要写对、CI 里加一步扫描。这些才是这次事件对普通开发者最实际的提醒。把流程跑通比看一百篇解读都有用。