news 2026/9/17 0:30:41

openCursor 游标方向教程:用 TaoToken 让 Codex 走通 nextunique 示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
openCursor 游标方向教程:用 TaoToken 让 Codex 走通 nextunique 示例

openCursor() 的第二个参数 nextunique 到底有没有跳过重复记录?你把《JavaScript高级程序设计》里的游标方向示例抄进浏览器,Console 只给出一个 IDBRequest,肉眼很难判断 prev 和 prevunique 的差别。这篇就从这段不确定的输出说起:在 TaoToken 上打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建一把 Key,把 Codex 的 Base URL 指到 https://taotoken.net/api,让 Codex 帮你生成一份带逐条日志的 IndexedDB 测试页。你在本地浏览器跑完,把 Console 输出贴回对话,就能逐条核对游标方向参数有没有真的生效。

很多教程只告诉你“nextunique 会跳过重复”,却没有提醒你:在对象存储的主键游标上,主键本身就是唯一的,nextunique 和 next 跑出来的条数可能一模一样。真正能看出差异的地方,是把同样的方向参数放到username 索引上。下面按原文的节奏走一遍,从 openCursor 的方向参数到索引游标,再到 getKey、indexNames 和 deleteIndex,每一步都配上可运行的验证代码。

1. 从 nextunique 的 Console 输出说起:游标方向为什么“看起来没生效”

1.1 原文示例里最容易被忽略的两个参数位

openCursor() 的第一个参数是 IDBKeyRange 实例,第二个参数是表示方向的字符串。默认方向是 "next",游标从对象存储的第一条记录开始,每次调用 continue() 或 advance() 都往最后一条记录前进。如果传入 null 作为第一个参数,表示键范围覆盖所有值,不额外做范围过滤。

当对象存储里存在重复记录时,可以把第二个参数改成 "nextunique",让游标跳过重复项。也可以传入 "prev" 或 "prevunique" 创建反向游标,从最后一条记录往第一条记录移动。反向游标每次调用 continue() 或 advance() 都会在对象存储中反向移动。

问题在于,很多人抄完示例只看到request.onsuccess触发了一次,或者只打印了event.target.result,没有在回调里持续调用cursor.continue(),于是 next 和 nextunique 的输出看起来差不多。更隐蔽的坑是:你如果只在对象存储上测,主键是自增 id,每条记录的主键都不同,nextunique 根本没有重复主键可跳过。要验证“跳过重复”这个行为,必须把游标建在允许重复的索引键上。

1.2 为什么要把 Console 输出贴回 Codex,而不是让 Codex 直接跑

Codex 不能直接打开你本地的浏览器,也不能替你在页面上执行 IndexedDB 操作。它能做的是:根据你的描述生成测试页、解释每一行日志、对照 next 和 nextunique 的输出差异。你需要在本地浏览器里运行代码,把 Console 输出复制回对话,让 Codex 帮你核对结果。

这个分工很重要。TaoToken 负责让 Codex 的模型请求正常返回,不负责执行你的浏览器代码。你把 Codex 的 Base URL 填成https://taotoken.net/api,模型 ID 按模型广场当时的列表来选,配置完成后,Codex 就能正常生成和解释这段 IndexedDB 验证代码。

2. 把 Codex 的 config.toml 接到 TaoToken:拿 Key、填 Base URL、选模型

2.1 打开 TaoToken 创建 Key,并确认模型 ID

准备材料只有三样:一个可用的 Codex CLI、一把 TaoToken API Key、一个当前可用的模型 ID。打开 TaoToken 注册登录,进入控制台创建 API Key,把拿到的 Key 先记下来,后面配置里统一用YOUR_API_KEY占位。模型 ID 不要凭记忆写,也不要把网上看到的旧 ID 直接抄进来,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 里的模型广场当时列表为准。

Codex 的配置文件通常放在~/.codex/config.toml,Windows 下是%USERPROFILE%\.codex\config.toml。如果目录不存在就手动建一个。注意这里写的是 TOML,不是 JSON,也不是 Claude Code 那套ANTHROPIC_*环境变量。把 Codex 的配置项套成 Claude Code 的格式,是最常见的错误之一。

2.2 ~/.codex/config.toml 里的 model_provider 与 base_url

打开配置文件,写入下面这段。model_provider指向你自定义的 provider 名称,base_urlhttps://taotoken.net/api,末尾不要加/v1env_key是 Codex 去读取 API Key 的环境变量名,你可以自己起名,这里用TAOTOKEN_API_KEY

model_provider = "taotoken" model = "YOUR_MODEL_ID" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"

保存后设置环境变量。macOS 或 Linux:

export TAOTOKEN_API_KEY=YOUR_API_KEY codex

Windows PowerShell:

$env:TAOTOKEN_API_KEY="YOUR_API_KEY" codex

这里有一个容易混淆的点:官网落地页是https://taotoken.net/?utm_source=taotoken_aicg_blog_end,用来注册、创建 Key、看模型广场和用量;填进 Codex 的 Base URL 是https://taotoken.net/api,不要把 UTM 参数加到 API 地址上,也不要写成https://taotoken.net/api/v1

2.3 启动后先问一个 IndexedDB 小问题

配置保存后,运行codex,先问一个和本篇直接相关的问题:“在 IndexedDB 里,store.openCursor(null, "nextunique") 和 index.openCursor(null, "nextunique") 的输出有什么差别?”如果 Codex 能正常返回解释,说明 TaoToken 通道和模型 ID 都填对了。

如果报 401,优先检查TAOTOKEN_API_KEY环境变量是否真的在当前终端生效,以及 Key 是否复制完整。如果报 404 或提示找不到接口,检查base_url是不是多写了/v1或结尾多了斜杠。如果提示模型不存在,回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场重新确认模型 ID。排障顺序不要反过来,先确认通道通,再让 Codex 生成代码。

3. 让 Codex 生成一份 users 测试页:next 与 nextunique 的日志对照

3.1 给 Codex 的提示词和单文件 HTML 骨架

在 Codex 里发一条尽量具体的提示词,让它生成一个单文件 HTML,要求包含对象存储游标和索引游标两组测试:

“请生成一个单文件 HTML,使用 IndexedDB 创建 users 对象存储,主键 id 自增,并在 username 上创建非唯一索引。插入 5 条记录,其中 alice 和 bob 各出现两次。分别用 store.openCursor(null, direction) 和 index.openCursor(null, direction) 遍历,direction 依次为 next、nextunique、prev、prevunique。每次 onsuccess 打印当前游标的方向、索引键、主键和整条记录。最后把所有 Console 输出整理成表格。”

Codex 返回代码后,不要直接全信。重点看三个地方:索引的 unique 是否为 false、回调里有没有cursor.continue()、索引游标里是否同时打印了cursor.keycursor.primaryKey。下面是一份可以直接复制到本地 HTML 文件的版本,你可以用它对照 Codex 生成的结果。

<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>IndexedDB openCursor 方向验证</title> </head> <body> <script> const DB_NAME = "cursor-direction-demo"; const DB_VERSION = 1; const STORE = "users"; function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE)) { const store = db.createObjectStore(STORE, { keyPath: "id", autoIncrement: true }); store.createIndex("username", "username", { unique: false }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } function seed(db) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); const store = tx.objectStore(STORE); store.clear(); [ { username: "alice", age: 20 }, { username: "bob", age: 21 }, { username: "alice", age: 22 }, { username: "carol", age: 23 }, { username: "bob", age: 24 } ].forEach((row) => store.add(row)); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function walkStore(db, direction) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readonly"); const store = tx.objectStore(STORE); const req = store.openCursor(null, direction); const log = []; req.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) { console.log(`[对象存储 ${direction}] 结束,共 ${log.length} 条`, log); resolve(log); return; } const row = { 主键: cursor.key, username: cursor.value.username, age: cursor.value.age }; log.push(row); console.log(`[对象存储 ${direction}]`, row); cursor.continue(); }; req.onerror = () => reject(req.error); }); } function walkIndex(db, direction) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readonly"); const store = tx.objectStore(STORE); const index = store.index("username"); const req = index.openCursor(null, direction); const log = []; req.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) { console.log(`[索引 username ${direction}] 结束,共 ${log.length} 条`, log); resolve(log); return; } const row = { 索引键: cursor.key, 主键: cursor.primaryKey, username: cursor.value.username, age: cursor.value.age }; log.push(row); console.log(`[索引 username ${direction}]`, row); cursor.continue(); }; req.onerror = () => reject(req.error); }); } (async () => { const db = await openDB(); await seed(db); for (const direction of ["next", "nextunique", "prev", "prevunique"]) { await walkStore(db, direction); } for (const direction of ["next", "nextunique", "prev", "prevunique"]) { await walkIndex(db, direction); } })().catch((err) => console.error(err)); </script> </body> </html>

3.2 本地跑完把日志贴回 Codex,逐行核对

把上面代码保存为cursor.html,用浏览器打开,按 F12 打开 DevTools 的 Console 面板,刷新页面。你会看到两组日志:对象存储游标和索引游标。对象存储的 next 输出 5 条,nextunique 大概率也是 5 条,因为主键 id 是自增的,没有重复主键可以跳。prev 和 prevunique 同样是 5 条,只是顺序反过来。

索引游标就不一样了。索引 username 上存在重复值,next 输出 5 条,nextunique 只输出 3 条:alice、bob、carol 各取第一次出现的那条记录。prev 输出 5 条反向记录,prevunique 也输出 3 条,但取的是每个 username 最后出现的那条记录,顺序是 carol、bob、alice。把这两组日志复制回 Codex,让它逐行解释哪一条被跳过、为什么被跳过,比你自己盯着IDBRequest猜要快得多。

这里也解释了一个常见疑问:为什么原文在对象存储上写openCursor(null, "nextunique"),你照抄后却看不出跳过效果。不是参数没生效,而是对象存储的主键本身唯一,nextunique 没有重复主键可跳。把同样的方向参数放到允许重复的索引上,差异才会出现在日志里。

3.3 对象存储游标与索引游标的 key/value 差异

在对象存储游标里,cursor.key是主键,cursor.value是整条记录对象。在索引游标里,cursor.key是索引键,cursor.primaryKey是主键,cursor.value仍然是整条记录。很多示例只打印cursor.value,所以你看不到索引键的变化,也就无法判断 nextunique 到底跳过了哪个 username。

把打印语句拆开,是验证游标方向最省事的办法。对象存储游标打印cursor.keycursor.value.username;索引游标打印cursor.keycursor.primaryKeycursor.value.username。两边同时跑 next 和 nextunique,对照条数和顺序,结论就出来了。

4. 索引上的游标:createIndex、index.openCursor 与 openKeyCursor 怎么配合方向参数

4.1 createIndex 的三个参数与 unique 的坑

原文用store.createIndex("username", "username", { unique: true })创建索引,并说明第一个参数是索引名称,第二个参数是索引属性的路径,第三个参数是包含unique的 options 对象。如果你只是学索引创建,unique: true没问题;但如果你想验证 nextunique 跳过重复索引键,就千万不能把 username 索引设成唯一,否则插入重复 username 时会直接抛出 ConstraintError,游标连打开的机会都没有。

所以本篇测试页里用的是{ unique: false },故意让 username 可以重复,这样才能在索引游标上看到 nextunique 和 next 的条数差异。等你验证完,再按真实业务决定是否需要唯一索引。真实项目里,用户 ID 做主键、用户名做唯一索引是很常见的组合,但测试游标方向时不要把这个顺序搞反。

4.2 index.openCursor 的 result.key 是索引键

在对象存储上调用index("username")可以得到一个 IDBIndex 实例,在它上面调用openCursor(),参数与对象存储上的openCursor()完全一样。最大的不同是:event.result.key保存的是索引键,而不是主键。也就是说,索引游标遍历时,cursor.key是 username,cursor.primaryKey才是原来的自增 id。

这一点直接影响你对 nextunique 的判断。对象存储游标看主键去重,索引游标看索引键去重。你在 username 索引上跑index.openCursor(null, "nextunique"),看到 alice 只出现一次、bob 只出现一次,就说明方向参数真的在索引键上生效了。

4.3 openKeyCursor 只返回主键,不返回整条记录

IDBIndex 还提供了openKeyCursor(),参数与openCursor()一样。它的特点是:event.result.key是索引键,event.result.value是主键,而不是整条记录。也就是说,它只告诉你“这个索引键对应哪条主键”,不把整条记录读出来。当你只需要主键列表,或者只需要确认游标方向时,openKeyCursor 比 openCursor 更轻。

可以在测试页里追加一个walkKeyIndex函数,用index.openKeyCursor(null, direction)遍历,打印cursor.keycursor.value。你会看到 index.openCursor 的cursor.value是完整记录,而 index.openKeyCursor 的cursor.value是主键。两者在 nextunique 下跳过的重复项一致,只是返回内容不同。

5. get、getKey、indexNames 与 deleteIndex:补全验证清单

5.1 index.get 与 index.getKey 的返回值差异

除了游标,索引还支持用get()getKey()按索引键取单条记录。index.get("007")会创建一个新请求,成功时event.target.result是整条记录;index.getKey("007")同样创建新请求,但event.target.result是主键,而不是整条记录。原文里提到event.target.result.value在主键查询场景下才是用户 ID,这一点在签名或只取主键时很有用。

把这两个方法加进验证清单:先确认索引里存在某个 username,再用 get 取整条记录,用 getKey 取主键,对比控制台输出。如果 getKey 返回的是主键而不是记录对象,说明你用的确实是 getKey,而不是把 get 的返回误当成主键。

5.2 用 indexNames 查看对象存储上已有哪些索引

对象存储自身有一个indexNames属性,保存着与之相关索引的名称。可以写一个循环,把每个索引的名称、keyPath 和 unique 都打印出来。原文里那段for (let indexName in indexNames)的思路是对的,只是真实项目里建议用Array.from(store.indexNames)forEach,避免把原型链上的属性也遍历进来。

const tx = db.transaction("users", "readonly"); const store = tx.objectStore("users"); Array.from(store.indexNames).forEach((name) => { const index = store.index(name); console.log({ name: index.name, keyPath: index.keyPath, unique: index.unique }); });

IDBIndex 的namekeyPathobjectStoreunique四个属性,刚好能帮你确认索引是否按预期创建。尤其unique,如果你发现测试页里 username 索引是 true,那就解释了你为什么插不进重复数据,而不是游标方向没生效。

5.3 deleteIndex 之后游标还能不能跑

在对象存储上调用deleteIndex("username")并传入索引名称,可以删除索引。删除索引不会影响对象存储里的数据,这个操作也没有回调。你可以先跑一遍索引游标,确认 nextunique 输出了 3 条;然后删掉 username 索引,再用store.index("username")就会直接抛错,而不是返回空游标。这时只能重新建索引,原来基于该索引的游标请求也不会继续。

所以验证顺序建议是:先建索引并插入重复数据,跑完 next、nextunique、prev、prevunique 四组日志,把输出贴回 Codex 确认;再考虑是否删除索引或调整 unique 设置。不要一边验证一边删索引,否则你会分不清是参数没生效,还是索引已经被移除了。

6. 这次 Codex 调用记上了吗:回控制台看用量与下一步

配置保存后,先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。如果你打算让 Codex 长期帮你解释 IndexedDB 输出、对照游标日志,可以打开 Coding Plan 看套餐是否够用;Key 在 控制台 API Keys 创建,模型广场也在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 里。

接下来可以试一个更小的练习:把 prevunique 在 username 索引上的输出单独贴给 Codex,让它标出每条记录属于哪个 username、取的是该 username 的第几次出现。再把index.openKeyCursor(null, "nextunique")的输出贴进去,对比cursor.value为什么从整条记录变成了主键。两轮下来,openCursor 的第二个参数就不再是靠猜的了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 0:25:35

DeskcommCRM:融合桌面端与通信的轻量级客户管理工具实践

1. 项目概述1.1 DeskcommCRM 到底是做什么的先说说我为什么会对 DeskcommCRM 感兴趣。做销售或者做客户运营的朋友应该都有这种感觉&#xff0c;每天的工作有一大半时间浪费在“切换工具”上&#xff1a;客户资料在Excel里&#xff0c;聊天记录在IM里&#xff0c;跟进任务在备忘…

作者头像 李华
网站建设 2026/9/17 0:18:18

OpenMontage:面向AI Agent的视频生产协议栈

1. OpenMontage不是另一个AI视频工具&#xff0c;而是Agent范式在媒体生产中的首次工程落地OpenMontage这个名字刚出现时&#xff0c;我第一反应是“又一个开源视频剪辑库”——毕竟montage在法语里就是“剪辑”的意思&#xff0c;加上Open前缀&#xff0c;很容易让人联想到Ope…

作者头像 李华
网站建设 2026/9/17 0:14:32

Kaimal谱风时程生成:湍流积分尺度校准与空间相干性实现

简介&#xff1a;本资源是一份面向土木工程、风工程及结构动力学方向高校师生与工程师的MATLAB脉动风模拟工具包&#xff0c;聚焦大跨度桥梁抗风设计中的关键环节——基于Kaimal谱的脉动风时程生成。它解决了实际工程中缺乏轻量、可复现、参数可调的风谱建模脚本的问题&#xf…

作者头像 李华
网站建设 2026/9/17 0:14:01

I2C多设备可靠通信实战:总线缓冲器与开关协同设计

1. 为什么“连接多个I2C设备”这件事&#xff0c;比教科书里写的难十倍&#xff1f;你手头有一块STM32开发板&#xff0c;接了个OLED屏&#xff08;地址0x3C&#xff09;&#xff0c;又加了个温湿度传感器&#xff08;0x40&#xff09;&#xff0c;再插上一个EEPROM&#xff08…

作者头像 李华
网站建设 2026/9/17 0:10:37

WorkBuddy Enterprise:企业级Agent协同工作流落地实践

1. 这不是又一个“AI平台”宣传页&#xff0c;而是一套可落地的企业级Agent协同工作流WorkBuddy Enterprise这个名字听起来像某个SaaS产品的标准命名——但如果你真去翻过腾讯云ADP&#xff08;Application Development Platform&#xff09;前沿部署工程师的实操笔记、看过虾哥…

作者头像 李华