1. 从 JSON 深拷贝翻车说起:循环引用为什么必须换 WeakMap
如果你写过前端数据缓存、状态快照或者配置合并,大概率用过JSON.parse(JSON.stringify(obj))这行“万能深拷贝”。它确实简单,一行搞定,但它丢的东西比你想象的多:函数、Symbol、undefined全部消失,Date会变成字符串,原型链直接断掉,最要命的是遇到循环引用会当场抛TypeError: Converting circular structure to JSON。
我最近在做一个带父子互相引用的树形结构,节点里node.parent指回父节点,父节点children又指向子节点。用 JSON 版深拷贝直接报错,换成基础递归版又因为没做缓存陷入死循环。真正能扛住这种场景的,是带WeakMap的增强版递归:用一个hash记录“原对象 → 拷贝对象”的映射,遇到已经拷贝过的对象直接返回缓存,循环引用自然被解开。
问题在于,我想让 Codex 帮我把这段 WeakMap 版深拷贝按原文逻辑完整跑一遍、验证copied.self === copied是否返回true,结果 Codex 里没有可用的模型通道,请求发不出去。这篇就记录我怎么用 TaoToken 给 Codex 接上模型通道,然后让 Codex 把 JSON 版改写成 WeakMap 版并实际执行验证的完整过程。适合正在用 Codex、Cursor 这类编码工具,但卡在模型配置上的同学。
2. 给 Codex 接上模型通道:TaoToken 的 Key 与 Base URL 怎么填
先说清楚定位:TaoToken 在这里解决的是Codex 没有可用模型通道的问题,它不替代你的深拷贝函数,也不改你的业务代码。你要做的只是把模型请求指向一个可用的入口,让 Codex 能正常发请求、拿回复。
你需要两样东西:
- 一个 API Key:在 TaoToken 后台创建,形如
sk-开头的一串字符。 - 一个 Base URL:
https://taotoken.net/api,注意这里不带任何查询参数,就是干净的接口根地址。
注册和创建 Key 的入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end 。进去之后在控制台里找到 API Keys 页面,新建一个 Key,复制出来先存好,后面填进 Codex 配置要用。
注意:Key 只在创建时完整显示一次,关掉页面就看不到了,建议先粘到本地临时文件里。
这一步的关键认知是:Codex 本身是个客户端,它需要一个兼容的模型服务端点。你把 Base URL 和 Key 配对填对,Codex 才有“嘴”去说话。填错任何一个,表现都是请求失败或 401,而不是代码逻辑问题。
3. 可复制配置:把 Key 和 Base URL 写进 Codex
Codex 的模型配置一般通过环境变量或配置文件注入。下面给你两种常见写法,按你实际用的版本选一种即可。
3.1 环境变量方式
在终端里设置(Linux/macOS):
export OPENAI_API_KEY="sk-你的TaoToken密钥" export OPENAI_BASE_URL="https://taotoken.net/api"Windows PowerShell:
$env:OPENAI_API_KEY="sk-你的TaoToken密钥" $env:OPENAI_BASE_URL="https://taotoken.net/api"设完之后,重新打开一个终端窗口让变量生效,再启动 Codex。环境变量的好处是不用改配置文件,缺点是每次新开终端都要重设,适合临时验证。
3.2 配置文件方式
如果你希望长期使用,写进 Codex 的配置文件更稳。以常见的config文件为例:
model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"然后在环境变量里只放 Key:
export TAOTOKEN_API_KEY="sk-你的TaoToken密钥"这样配置和密钥分离,配置文件可以进版本库,密钥留在本地环境变量里,安全一些。
3.3 参数对照表
| 配置项 | 填写值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 接口根地址,不带 UTM 参数 |
| API Key | sk-开头字符串 | 在 TaoToken 控制台创建 |
| 环境变量名 | OPENAI_API_KEY或自定义 | 需与配置文件里的env_key一致 |
| 模型名 | 按 Codex 默认或指定 | 保持客户端默认即可 |
填完之后,Codex 的模型通道就算接上了。接下来才是真正有意思的部分:让它跑 WeakMap 版深拷贝。
4. 让 Codex 跑通 WeakMap 版深拷贝并验证循环引用
4.1 给 Codex 的提示词
我直接把下面这段丢给 Codex,让它按原文增强版逻辑改写并执行:
请把下面这个 JSON 版深拷贝改写成带 WeakMap 的增强版递归深拷贝, 要求: 1. 用 WeakMap 缓存已拷贝对象,处理循环引用; 2. 保留 Date、RegExp、Set、Map、Symbol 属性的处理; 3. 用 Object.create(Object.getPrototypeOf(obj)) 保留原型链; 4. 写完后构造一个循环引用对象 obj.self = obj, 执行深拷贝并打印 copied.self === copied 的结果。 原始 JSON 版: function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); }4.2 Codex 产出的 WeakMap 版代码
Codex 返回的核心实现如下,我贴出来你可以直接复制:
function deepCopy(obj, hash = new WeakMap()) { // 基本类型和 null 直接返回 if (obj === null || typeof obj !== 'object') return obj; // 命中缓存说明是循环引用,直接返回已拷贝对象 if (hash.has(obj)) return hash.get(obj); // Date if (obj instanceof Date) return new Date(obj); // RegExp if (obj instanceof RegExp) return new RegExp(obj); // Set if (obj instanceof Set) { const copy = new Set(); hash.set(obj, copy); obj.forEach(value => copy.add(deepCopy(value, hash))); return copy; } // Map if (obj instanceof Map) { const copy = new Map(); hash.set(obj, copy); obj.forEach((value, key) => copy.set(key, deepCopy(value, hash))); return copy; } // 数组或普通对象,保留原型链 const copy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj)); hash.set(obj, copy); // Symbol 属性 const symKeys = Object.getOwnPropertySymbols(obj); for (const symKey of symKeys) { copy[symKey] = deepCopy(obj[symKey], hash); } // 普通属性 for (const key in obj) { if (obj.hasOwnProperty(key)) { copy[key] = deepCopy(obj[key], hash); } } return copy; }4.3 验证循环引用
构造一个自引用对象,跑一遍:
const obj = { a: 1 }; obj.self = obj; // 循环引用 const copied = deepCopy(obj); console.log(copied.self === copied); // true console.log(copied.a); // 1 console.log(copied === obj); // false,确实是新对象关键看第一行输出:copied.self === copied返回true,说明循环引用被正确解开,拷贝出来的对象自己指向自己,而不是指向原对象。同时copied === obj是false,证明这是独立的新对象,修改copied.a不会影响obj.a。
4.4 顺手对比 JSON 版的表现
同样的对象喂给 JSON 版:
const obj = { a: 1 }; obj.self = obj; try { const bad = JSON.parse(JSON.stringify(obj)); console.log(bad.self === bad); } catch (e) { console.log('JSON 版报错:', e.message); }输出会是JSON 版报错:Converting circular structure to JSON。这一对比就把 WeakMap 版的价值讲清楚了:不是它更快,而是它能处理 JSON 版根本处理不了的循环引用。
5. 本篇常见错排查:Key、Base URL 与请求失败
跑不通的时候,先别怀疑深拷贝代码,九成问题出在模型通道配置上。按下面顺序排查。
5.1 请求返回 401 或鉴权失败
最常见的原因是 Key 没填对或没生效。检查三点:Key 是不是完整复制了(有没有漏掉尾部字符);环境变量名和配置文件里的env_key是否一致;设置完环境变量后有没有重开终端。如果用的是配置文件方式,确认TAOTOKEN_API_KEY这个变量在当前 shell 里能echo出来。
5.2 Base URL 写错导致连不上
Base URL 必须是https://taotoken.net/api,不要多加斜杠、不要带查询参数、不要写成别的路径。有些同学习惯性在后面补/v1,这要看客户端约定,Codex 默认按根地址拼接,多写反而会 404。如果你不确定,先用最干净的根地址试。
5.3 Codex 没反应或一直转圈
先确认模型通道是否真的通了。可以单独发一个最小请求验证,比如让 Codex 只回复“ok”。如果最小请求都失败,说明是通道问题,不是深拷贝代码问题。这时候回到第 3 节重新核对 Key 和 Base URL。
5.4 代码跑出来copied.self === copied是 false
如果模型通道正常、代码也执行了,但结果是false,那多半是 WeakMap 缓存没生效。检查hash.set(obj, copy)是否在递归子属性之前执行——必须在进入子属性递归前就把当前对象的拷贝存进缓存,否则循环引用时缓存里还没有这个对象,就会无限递归或拷贝出错误结构。上面 Codex 产出的代码里,hash.set(obj, copy)就放在处理 Symbol 和普通属性之前,顺序是对的。
5.5 验证用量:后台看到一次成功调用
跑通之后,回到 TaoToken 后台的用量或调用记录页面,应该能看到刚才那次请求的成功记录。这一步很关键:它证明你的 Key 配对成功、Base URL 正确、Codex 确实通过这个通道发出了请求并拿到了回复。看到这条记录,就说明整条链路是通的,后面再跑更复杂的深拷贝场景也不用担心通道问题。
6. 后续怎么用:把通道固定下来,专注写代码
这次验证下来,我的建议是把 TaoToken 的 Key 和 Base URL 固定写进 Codex 配置,而不是每次临时设环境变量。通道稳定之后,你就可以把精力放在真正的问题上,比如让 Codex 继续帮你处理structuredClone不支持函数的场景、或者给深拷贝加上自定义类的clone方法。
如果你也在用 Codex 做长期编码或 Agent 类任务,可以考虑 Coding Plan 这类按周期计费的方式,比每次单独配 Key 更省心:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。需要管理多个 Key 或查看调用明细,直接进控制台:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。想先手动验证模型回复是否正常,用模型对话页面发一条测试消息最快:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。创建和管理 Key 在 API Keys 页面:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。接入细节和参数说明看文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
回到深拷贝本身:JSON 版适合简单数据对象,一旦涉及循环引用、函数、Symbol 或原型链,就老老实实上 WeakMap 增强版。而让 Codex 帮你改写和验证的前提,是先把模型通道配通——这一步用 TaoToken 的 Key 加 Base URL 就能解决,后台看到成功调用记录,就说明你配对