Claude Code 配 TaoToken:ContentResolver.query 返回空 Cursor 的排查

Claude Code 配 TaoToken:ContentResolver.query 返回空 Cursor 的排查 ContentResolver.query() 从用户词典 Provider 取数时Cursor 可能因无匹配行而 count 为 0也可能因内部错误返回 null。复查这类代码可以交给 Claude Code而给 Claude Code 接模型通道可以用 TaoToken——先在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 创建 Key再把 Base URL 填成 https://taotoken.net/api。两种「空」的处理逻辑完全不同null 表示查询本身出错需要记日志count 为 0 表示查询正常但没有匹配词应该走提示用户重新输入的流程。很多代码只判了 mCursor ! null 就取数结果空 Cursor 时 UI 上没有反馈日志里也没有记录。这篇文章就顺着这条线把 ContentResolver.query 的复查流程和 Claude Code 接入 TaoToken 的配置一起讲透。1. ContentResolver.query() 返回的 Cursor 有两种「空」1.1 原始代码里 mCursor 的两个出口在用户词典 Provider 的示例里查询入口是 getContentResolver().query()它接收四个关键参数CONTENT_URI 指向 words 表mProjection 声明要返回的列mSelectionClause 和 mSelectionArgs 组成筛选条件。执行之后Cursor 对象可能落进两种状态这两种状态的处理方式完全不能混用。第一种是无匹配行。用户输入的单词在词典里不存在时Provider 返回的是一个没有数据的 CursormCursor.getCount() 等于 0。这种状态不算异常原文里也专门强调「这不能完全算是错误」界面提示应该给用户提供重新输入或插入新词的入口而不是记录一条错误日志。很多代码在这里会犯两个相反的错误要么完全忽略 count 判断让界面无反馈地空转要么把 count 为 0 当成异常用 Log.e 写日志导致日志平台被大量无效错误刷屏。第二种是内部错误。Provider 可能直接返回 null也可能抛出一个 Exception具体行为取决于 Provider 的实现。原文代码里专门写了 if (null mCursor) 这个分支注释里还提醒「此时不要使用游标你可能要调用 android.util.Log.e() 记录错误」。问题在于很多人抄这段示例时只把 else 分支的取数逻辑留下了null 判断和 getCount() 1 判断全被删掉等线上出现空 Cursor 才回头补这时候往往要靠日志才能定位到是哪一个 selection 导致的。这个场景非常适合让 Claude Code 参与复查它不需要连真机只需要把代码片段和相关 Contract 类贴进对话就能按 projection、selection、selectionArgs、结果处理四个层面对照分析指出哪个分支可能漏判、哪一行 selection 写法和 Contract 常量不一致。要让它介入先得把模型通道配好。1.2 复查代码时 Claude Code 能做什么ContentResolver.query 的问题往往不在语法上而在语义上projection 里的列名是不是 Contract 常量selection clause 是否是「列名 ?」的结构selectionArgs 数组里的值是否真的填充进去了这些点挨个扫过去不复杂但人眼复查时容易默认「代码能编译就没问题」。Claude Code 把代码读进来后会基于 Android 框架的查询语义逐项检查并在回答里明确标注「哪一行是原文示例中需要修正的」。例如原文示例里有一处 mSelectionClause ?这行代码实际上漏了列名正确写法应该是 UserDictionary.Words.WORD ?。如果照抄部分 Provider 会把 selection 解析成无法匹配任何数据的条件查询永远返回空 Cursor而你不会立刻想到是这里出的问题。这种细节人肉排查要花不少时间Claude Code 却能在拿到代码的第一时间指出来。提示原文示例把 query 放在 UI 线程只是为了讲清逻辑实际应用里应改用 CursorLoader 或协程 Dispatchers.IO。Claude Code 复查的是代码逻辑不是帮你决定线程模型。2. 先把 Claude Code 的 Base URL 指到 TaoToken2.1 创建 Key从控制台拿 YOUR_API_KEYClaude Code 要访问模型需要三个信息API Key、Base URL、模型 ID。打开 TaoToken 后注册登录进入控制台创建 API Key。创建出来的 Key 在配置里统一用 YOUR_API_KEY 占位你实际使用时替换成控制台里那串真实的字符。官网落地页和接口地址要分清给人点的页面是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 填进工具的接口地址是 https://taotoken.net/api末尾没有 /v1。模型 ID 这里先不给死值。TaoToken 模型广场上会列出当前可用的模型ID 以广场当时列表为准。很多教程喜欢把模型名写死在配置里结果模型下架或改名后又要重新查一遍文档从模型广场复制 ID 看起来多一步实际上最稳。控制台还能顺带看到 Key 的调用记录和用量方便后面核对 Claude Code 发起的请求是否成功计费。2.2 settings.json 里写入环境变量Claude Code 的配置入口是 ~/.claude/settings.json把环境变量写进 env 块。Base URL 注意填接口地址 https://taotoken.net/api不要加上 /v1也不要填成官网网页链接。网页链接是给人浏览器访问的接口地址是给 Claude Code 发请求用的两者混填最常见的错误就是 404 或 401。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: 模型 ID 以 TaoToken 模型广场为准 } }如果你不想改全局配置也可以在终端里临时导出一组环境变量效果相同只在当前终端窗口生效export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_AUTH_TOKENYOUR_API_KEY export ANTHROPIC_MODEL你的模型IDClaude Code 本身是命令行工具所以也可以直接用 TaoToken 的 CLI 起一个会话。先安装npm install -g taotoken/taotoken再指定 Key、接口地址和模型启动taotoken cc -k YOUR_API_KEY -u https://taotoken.net/api -m YOUR_MODEL_ID跑通之后Claude Code 里就可以正常对话了。原文里说「要从 Provider 获取数据应用需要对目标 Provider 具有读权限」对 Claude Code 来说这把 Key 和正确的 Base URL 就是它的「读权限」权限没配上后面所有排查都无从谈起。3. 交给 Claude Code 复查 projection、selection、selectionArgs3.1 原文三个变量最容易写错的点把原文那段用户词典查询代码交给 Claude Code 之前先说清这段代码的三个关键变量分别容易在哪里出错。mProjection 是「要返回哪些列」的数组。原文里用的是 UserDictionary.Words._ID、UserDictionary.Words.WORD、UserDictionary.Words.LOCALE 这些 Contract 常量。如果图省事直接写 word、locale 这种裸字符串编译期不会报错但查询结果里列的元信息可能与预期不符后续 getColumnIndex 返回 -1getString 就会抛 CursorIndexOutOfBoundsException。Claude Code 复查时会盯着这一点projection 中出现的每个列名是否都能在对应的 Contract 类里找到。mSelectionClause 是筛选条件。原文示例里的 mSelectionClause ? 其实少了一部分正确写法应该是 UserDictionary.Words.WORD ?。缺少列名时Provider 不知道要把这个参数跟哪一列比较结果往往是查询返回空 Cursor而且不会抛异常。这类问题最隐蔽因为它不报错只表现为「查不到数据」。Claude Code 按 SQL where 子句的结构去解析这行代码时能直接指出 where 条件不完整并给出修正建议。mSelectionArgs 是占位符对应的参数数组。当用户输入了搜索词mSearchString 会被放进 mSelectionArgs[0]当用户没有输入selection 设为 null查询返回所有行。这里最隐蔽的错误是用户输入为空时mSelectionArgs[0] 却残留了上一次查询的值导致下一次非空查询带上了旧参数。把这段代码贴给 Claude Code 时它通常会要求你检查 mSelectionArgs 的生命周期而不只是看查询那一刻的值。3.2 给 Claude Code 的复查提示与它该回什么把下面这段 prompt 贴给 Claude Code下面这段 ContentResolver.query 代码来自用户词典 Provider 的示例。请复查三件事 1. projection 的列名是否与 UserDictionary.Words Contract 常量一致有没有裸字符串 2. selection clause 是否正确写成 列名?selectionArgs 是否与占位符一一对应 3. 是否有把用户输入直接拼进 selection 的写法如果有指出 SQL 注入风险。 把发现的问题列成清单并说明每一行可能引发的运行时行为先不要给完整代码。Claude Code 返回时通常会给出类似下面的清单mSelectionClause 那行缺列名需要从 ? 改成 UserDictionary.Words.WORD ?mSelectionArgs 在 mSearchString 为空时要重置mProjection 建议全部换成 Contract 常量。如果它发现某处是 String mSelectionClause var mUserInput 这种拼接写法会专门标注 SQL 注入风险。注入风险是原文最强调的部分。用户输入 nothing; DROP TABLE *; 这类内容时如果被直接拼进 selection就可能被当成 SQL 语句执行Provider 的 SQLite 表可能被清空。使用 ? 占位符后用户输入被绑定为参数而不是 SQL 片段注入就无从谈起。Claude Code 能把所有可能出现字符串拼接的地方都扫出来比人工翻代码更快。注意一个边界Claude Code 在这里做的是「读代码、解释代码、改代码」它不会连接你的 Android 设备或模拟器也不会替你在工程里跑构建。它给出的修改意见需要你自己复制回 Android Studio编译运行后把报错或运行结果贴回对话再继续下一轮。这跟原文里「在实际应用中应该在另一个线程执行查询」是同一个道理工具负责给方案执行和验证始终在你手里。4. 空 Cursor 的兜底null 与 getCount()1 分开处理4.1 三个分支对应三种不同的用户现实原文把查询结果的处理分成了三种情况这三种情况对应完全不同的用户现实不能合并在一个 if 里。mCursor 为 null意味着 Provider 内部出错或者参数错到无法返回结果。这个分支要记录错误日志而且日志里要带上足够的上下文CONTENT_URI、mSelectionClause、mSelectionArgs否则报错后连复现条件都不知道。Claude Code 复查时会要求你把这三样东西都放进 Log.e 的参数里而不是只写一句 query failed。mCursor.getCount() 1说明查询正常执行了但没有行匹配。这不算异常应该用 Log.i 记录并在界面上提示用户没有找到匹配项或者给出插入新词和重新输入的入口。把空 Cursor 记成 error 是日志治理上的坏味道线上会充满这种无效错误真正的异常反而被淹没。正常分支也就是有数据的分支才进入 moveToNext() 循环取数。取数结束后还要考虑 Cursor 的关闭如果直接把 Cursor 交给 SimpleCursorAdapter 去显示不要提前 close因为适配器还要继续读它而且 Cursor 必须包含 _ID 列如果只是取一轮数据取完就 close最好放在 finally 里避免异常时泄漏。4.2 让 Claude Code 生成一份带日志的兜底模板把原文那段 if (null mCursor) / else if (mCursor.getCount() 1) / else 结构原样贴给 Claude Code然后提出两个要求第一补齐日志上下文第二区分错误和空结果。Claude Code 生成的版本大致应该是这样if (mCursor null) { Log.e(DictQuery, query failed: uri UserDictionary.Words.CONTENT_URI , selection mSelectionClause , args Arrays.toString(mSelectionArgs)); return; } if (mCursor.getCount() 1) { Log.i(DictQuery, no matching word for: mSearchString); // 提示用户重新输入或插入新词这里不是异常 return; } try { while (mCursor.moveToNext()) { int wordIndex mCursor.getColumnIndex(UserDictionary.Words.WORD); String word mCursor.getString(wordIndex); // 在这里处理收集到的单词 } } finally { mCursor.close(); }这套代码放到原文的语境里是成立的null 用 Log.e 记录错误count 为 0 用 Log.i 记录空结果finally 里关闭 Cursor 避免泄漏。Claude Code 生成后你仍然要在本地工程里编译验证再把它接入原有的 SimpleCursorAdapter 显示流程。若要让 Cursor 配合 ListView查询结果必须包含 _ID 列否则 SimpleCursorAdapter 找不到主键列会抛异常这一条原文提过Claude Code 复查时也会盯住不放。这些代码层面的细节适合让 AI 先扫一遍再由你在真机或模拟器上确认行为。Claude Code 不会替你把 APK 装到设备里也不会替你在 logcat 里翻日志它做的是把可能出错的位置标出来把更稳的写法给到你剩下的是你本地验证的功夫。5. 跑通之后去控制台对一下这次调用5.1 先测 Key 再回 Claude Code配置完成后建议先到 模型对话页 用同一把 Key 发一条测试消息。这个动作能快速确认 Key 和模型 ID 都没有问题避免在 Claude Code 里排查半天才发现是 Key 复制少了字符。测试通过后再回到 Claude Code 里提交第 3 节那段复查 prompt流程会顺畅很多。5.2 这几种报错不用重装环境变量如果 Claude Code 报 401 Unauthorized基本可以确定是 ANTHROPIC_AUTH_TOKEN 里的 Key 不对或者 Key 中间混入了空格。重新到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 的控制台复制检查两边字符是否完全一致不要手动敲 Key复制粘贴最稳。如果报 model not found 或类似错误说明当前模型 ID 已失效或不在模型广场列表里。不要照着旧教程里的模型名硬填去 TaoToken 模型广场复制最新列表里的 ID更新到 ANTHROPIC_MODEL 环境变量后重试。模型列表会调整写死某个模型名在切换时反而会再卡一次。如果连接时报错信息里出现了多个 /v1 或路径拼接异常检查 ANTHROPIC_BASE_URL 是否填成了带 /v1 的地址。正确的接口地址是 https://taotoken.net/api末尾不带 /v1官网网页链接也不能填进这个字段它只负责注册、创建 Key、看模型广场和用量不负责处理模型请求。5.3 下一步的入口排查完 ContentResolver.query 的空 Cursor 问题也确认了 Claude Code 能正常调用 TaoToken 之后后面几件事按需取用。长期写代码的话可以打开 Coding Plan 看套餐是否够用Key 的创建和管理在 控制台 API KeysClaude Code 的环境变量完整对照表在 接入文档 里需要时直接翻。这次接入的核心其实只做了一件事把 Claude Code 的模型访问通道切到 TaoToken然后让它在 ContentResolver.query 的代码复查里发挥该有的作用。工具不会替你运行 Android 工程但能帮你把 null、空 Cursor、selection 缺列名、SQL 注入这些坑一个个标出来剩下的编译、运行、验证还是在你的本地环境里完成。