1. Android 短信备份与自定义:为什么需要 AI 辅助开发链路
做 Android 短信模块开发的同学大概都有体会:短信备份和短信自定义这两个功能,代码量不大,但坑特别碎。短信备份要处理content://sms/这个 URI 的查询、游标遍历、XML 序列化,还要考虑 Android 6.0 之后的运行时权限、Android 10 之后的存储分区;短信自定义则要往content://sms/里 insert 数据,模拟接收或发送记录,字段类型对不上就直接抛异常。
我在实际项目里写 demo13 的时候,光是短信备份的 XML 写入就调了三四轮——XmlSerializer的startDocument编码参数、Environment.getExternalStorageDirectory()在新版本上的废弃、cursor.getInt(0)和getLong(2)的类型错位,每一个都能让备份文件变成空壳。短信自定义那边更微妙,values.put("type", "1")传的是字符串,但底层期望的是 int,写进去之后短信列表里能看到,点开却闪退。
这些问题的共同点是:报错信息不直观,搜索引擎给的答案版本混杂。所以我开始把 AI 编码工具接进这个开发流程,用统一的 Key 通道来跑代码补全、报错解释和配置生成。这篇就围绕 TaoToken 的统一 API 通道,把短信备份与短信自定义的完整配置链路拆开讲,包括settings.json、config.toml、CC Switch 和 Cline 的配置片段,以及怎么验证配置真的生效。
适合谁看:正在写 Android 短信模块 demo、想用 AI 工具加速排障、但被多个模型 Key 管理搞烦的开发者。读完你能拿到一套可直接复制的配置骨架,以及短信模块从查询到写入的完整可运行代码。
2. TaoToken 前置:统一 Key 与 API 通道的定位
TaoToken 在这里扮演的角色是「统一入口」。你不需要为每个 AI 编码工具单独申请不同厂商的 Key,也不用在多个配置文件之间来回切换 base_url。它提供一套兼容 OpenAI 风格的 API 通道,编码工具只要支持自定义 base_url 和 api_key,就能接进来。
官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数,配置的时候直接写这个。
对短信模块开发来说,这个统一通道的价值在于:你在 Cline 里让它解释XmlSerializer的写入逻辑,在 CC Switch 里切换模型跑代码补全,在 Coding Plan 里做长期的 Agent 任务,用的都是同一套 Key。不用因为换个工具就重新配一遍环境。
需要先拿到 Key 的话,去控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。Key 生成后复制保存,后面所有配置都用它。
注意:Key 只显示一次,建议生成后立刻存到密码管理器里。配置文件中不要提交到 Git 仓库。
3. 可复制配置:settings.json 与 config.toml 骨架
这一节给的是可以直接抄的配置。分两部分:一部分是通用编辑器/插件的settings.json,另一部分是命令行工具的config.toml。
3.1 settings.json 骨架
很多 VS Code 系插件(包括 Cline)会读工作区或用户级的settings.json。下面这个骨架把 TaoToken 的 base_url 和 Key 占位符放进去,你只需要替换YOUR_TAOTOKEN_KEY。
{ "ai.provider": "openai-compatible", "ai.baseUrl": "https://taotoken.net/api", "ai.apiKey": "YOUR_TAOTOKEN_KEY", "ai.model": "claude-sonnet-4-20250514", "ai.timeout": 60000, "ai.maxTokens": 4096, "ai.temperature": 0.2, "android.sms.debug": true, "files.associations": { "*.xml": "xml" } }几个参数说明:baseUrl必须是https://taotoken.net/api,不要加尾部斜杠;model字段填你实际要用的模型标识,不同工具支持的模型名可能不同,以文档为准;temperature设低一点(0.2 左右)是因为代码补全和报错解释需要稳定输出,不需要发散。
3.2 config.toml 骨架
命令行类工具(比如一些 CLI Agent)用 TOML 格式。下面这个骨架覆盖了 provider、model、api_key 三个核心字段。
[provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key = "YOUR_TAOTOKEN_KEY" timeout_seconds = 60 [model] default = "claude-sonnet-4-20250514" fallback = "gpt-4o" max_tokens = 4096 temperature = 0.2 [project] name = "android-sms-demo13" language = "kotlin" context_files = [ "app/src/main/java/com/example/smsdemo/SmsBackupActivity.kt", "app/src/main/java/com/example/smsdemo/SmsCustomActivity.kt" ]context_files这个字段挺有用,它告诉 AI 工具优先读哪几个文件。短信备份和短信自定义的代码分别放在两个 Activity 里,把路径写进去,AI 在解释报错时就能直接定位到相关代码,不用你每次手动贴。
3.3 CC Switch 配置片段
CC Switch 用来在多个模型配置之间切换。它的配置文件通常是一个 JSON 数组,每个元素是一套配置。下面这段可以直接加到你的配置列表里。
{ "name": "TaoToken-SMS-Demo", "provider": "openai-compatible", "baseUrl": "https://taotoken.net/api", "apiKey": "YOUR_TAOTOKEN_KEY", "model": "claude-sonnet-4-20250514", "description": "Android短信备份与自定义开发专用" }切换的时候选中这个 name 就行。如果你同时还在做别的项目,可以再建一个配置项,用同一个 Key 但不同 model,互不干扰。
3.4 Cline 配置片段
Cline 是 VS Code 里的编码助手插件,它的配置入口在设置里选 API Provider 为 OpenAI Compatible,然后填 Base URL 和 API Key。对应的配置片段如下:
{ "cline.apiProvider": "openai", "cline.openAiBaseUrl": "https://taotoken.net/api", "cline.openAiApiKey": "YOUR_TAOTOKEN_KEY", "cline.openAiModelId": "claude-sonnet-4-20250514", "cline.customInstructions": "回答Android短信模块问题时,优先给出可运行的Kotlin代码,并标注API Level要求。" }customInstructions是我自己加的,让 Cline 在回答短信相关问题时直接给 Kotlin 代码而不是 Java 伪代码,省得来回转换。你可以按自己的技术栈改。
4. 短信备份与自定义的完整代码实现
配置好了之后,用 AI 辅助写代码的效率提升主要体现在排错和补全上。但前提是你自己得清楚正确的代码长什么样,不然 AI 给错了你也看不出来。这一节把短信备份和短信自定义的完整实现过一遍。
4.1 短信备份:从 URI 查询到 XML 序列化
短信的 ContentProvider URI 是content://sms/。查询的时候要指定列名,不然返回的游标列顺序不确定,getInt(0)这种硬编码索引很容易错位。
val mUri = Uri.parse("content://sms/") val mContentResolver = contentResolver val projection = arrayOf("_id", "address", "date", "type", "body") val cursor = mContentResolver.query(mUri, projection, null, null, null) val mList = mutableListOf<Message>() cursor?.use { c -> while (c.moveToNext()) { val msg = Message() msg.id = c.getInt(c.getColumnIndexOrThrow("_id")) msg.address = c.getString(c.getColumnIndexOrThrow("address")) msg.date = c.getLong(c.getColumnIndexOrThrow("date")) msg.type = c.getInt(c.getColumnIndexOrThrow("type")) msg.body = c.getString(c.getColumnIndexOrThrow("body")) mList.add(msg) } }这里用getColumnIndexOrThrow替代硬编码索引,是踩过坑之后的改法。之前用getInt(0),在某个厂商 ROM 上列顺序变了,备份出来的 address 全是 null。
序列化到 XML 的部分,XmlSerializer的用法如下:
private fun messageWrite(list: List<Message>) { try { val serializer = Xml.newSerializer() val file = File(getExternalFilesDir(null), "text.xml") FileOutputStream(file).use { fos -> serializer.setOutput(fos, "utf-8") serializer.startDocument("utf-8", true) serializer.startTag(null, "smss") for (msg in list) { serializer.startTag(null, "sms") serializer.attribute(null, "id", msg.id.toString()) serializer.startTag(null, "address") serializer.text(msg.address ?: "") serializer.endTag(null, "address") serializer.startTag(null, "date") serializer.text(msg.date.toString()) serializer.endTag(null, "date") serializer.startTag(null, "type") serializer.text(msg.type.toString()) serializer.endTag(null, "type") serializer.startTag(null, "body") serializer.text(msg.body ?: "") serializer.endTag(null, "body") serializer.endTag(null, "sms") } serializer.endTag(null, "smss") serializer.endDocument() } Toast.makeText(this, "备份成功", Toast.LENGTH_SHORT).show() } catch (e: Exception) { e.printStackTrace() } }注意getExternalFilesDir(null)替代了老的Environment.getExternalStorageDirectory(),后者在 Android 10 之后需要额外权限,而且路径行为不一致。用 app 专属目录不需要存储权限,省事。
4.2 短信自定义:往 sms 表插入记录
短信自定义的核心是往content://sms/里 insert 一条 ContentValues。字段类型要对:type是 int,date是 long。
val uri = Uri.parse("content://sms/") val values = ContentValues().apply { put("address", "95555") put("type", 1) put("body", "自定义短信接收") put("date", System.currentTimeMillis()) put("read", 0) put("seen", 0) } contentResolver.insert(uri, values)type传 1 表示接收,传 2 表示发送。read和seen设 0 表示未读未查看,这样插入后短信列表里会有未读标记,方便验证。
注意:从 Android 4.4 开始,只有默认短信应用才能往
content://sms/写入。如果你的 app 不是默认短信应用,insert 会静默失败或者抛 SecurityException。测试的时候要么把 app 设为默认短信应用,要么用模拟器上的系统短信应用权限。
4.3 权限声明
短信读取和写入需要运行时权限。在AndroidManifest.xml里声明:
<uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.WRITE_SMS" /> <uses-permission android:name="android.permission.RECEIVE_SMS" />运行时请求用ActivityResultContracts.RequestMultiplePermissions:
val launcher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result -> val allGranted = result.values.all { it } if (allGranted) { backupSms() } else { Toast.makeText(this, "需要短信权限才能备份", Toast.LENGTH_SHORT).show() } } launcher.launch(arrayOf( Manifest.permission.READ_SMS, Manifest.permission.WRITE_SMS ))5. 验证配置生效:从请求到结果确认
配置写完了不代表生效了。这一节给几个具体的验证步骤,确认 TaoToken 通道真的在工作。
5.1 用模型对话做连通性测试
最直接的方式是打开模型对话页面发一条测试请求:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。输入一段短信备份的代码,问它「这段代码在 Android 13 上会有什么问题」。如果返回了合理的分析(比如提到getExternalStorageDirectory废弃、需要READ_SMS权限),说明通道通了。
5.2 在 Cline 里跑一次真实补全
打开SmsBackupActivity.kt,把光标放在messageWrite函数末尾,触发 Cline 的代码补全。如果它给出的补全是基于你项目上下文的(比如引用了Message类、用了getExternalFilesDir),说明context_files配置生效了。
5.3 检查请求日志
TaoToken 控制台有请求日志页面,能看到每次调用的 model、token 消耗、响应时间。如果日志里有记录但编辑器里没反应,大概率是编辑器端的 base_url 写错了(比如多了斜杠或者少了/api)。
5.4 验证短信备份结果
代码跑通后,备份文件在getExternalFilesDir(null)/text.xml。用 Android Studio 的 Device File Explorer 打开这个路径,看 XML 内容是否完整。正常的备份文件结构如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <smss> <sms id="1"> <address>95555</address> <date>1710000000000</date> <type>1</type> <body>自定义短信接收</body> </sms> </smss>如果<address>是空的,回去检查getColumnIndexOrThrow("address")有没有拿到正确的列索引。
6. 本篇常见错排查
短信模块的报错有几个高频类型,这里集中列一下。
SecurityException: Permission Denial: reading com.android.providers.telephony.SmsProvider
权限没给全。除了READ_SMS,Android 10 之后读短信还需要READ_PHONE_STATE在某些 ROM 上才放行。另外检查 app 是不是默认短信应用,不是的话写入会被拒。
NullPointerException at cursor.getInt(0)
游标为空或者列索引越界。用cursor?.use {}包住,并且用getColumnIndexOrThrow替代硬编码索引。如果query返回 null,说明 URI 或者权限有问题。
备份文件写入成功但内容为空
XmlSerializer的startDocument和endDocument必须成对出现,中间所有startTag都要有对应的endTag。少一个endTag会导致序列化中断,文件里只有开头没有内容。
insert 返回 null 但没报错
contentResolver.insert返回 null 表示插入失败。常见原因是type字段传了字符串而不是 int,或者date字段缺失。把ContentValues里所有字段的类型检查一遍。
Cline 报 401 Unauthorized
Key 错了或者没填。去 API Keys 页面重新生成一个:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,然后更新settings.json里的ai.apiKey。
Cline 报 404 Not Found
base_url 写错了。正确写法是https://taotoken.net/api,不要加/v1或者尾部斜杠。有些工具会自动补/v1/chat/completions,所以 base 只需要到/api。
模型返回乱码或者截断
maxTokens设太小了。短信备份的 XML 序列化代码比较长,补全的时候如果maxTokens只有 1024,输出会被截断。调到 4096 试试。
7. 长期编码与 Agent 任务:Coding Plan 的接入
如果你不只是做 demo13 这一个模块,而是要把 AI 辅助开发长期用在 Android 项目上,可以考虑用 Coding Plan 来做 Agent 任务。它的定位是持续性的编码辅助,适合那种「帮我重构整个短信模块」「把 Java 代码迁移到 Kotlin」这类跨文件的任务。
接入地址:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。配置方式和前面一样,用同一个 Key 和 base_url,只是在任务类型上选 Agent 模式。
我自己的用法是:日常补全和报错解释走 Cline,跨文件的短信模块重构走 Coding Plan。两者共用一套 Key,不用来回切换。短信备份的 XML 序列化逻辑如果要改成 JSON 格式,直接让 Agent 读SmsBackupActivity.kt然后输出改造后的版本,比手动改快很多。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各工具的详细配置说明。Claude Code 相关的配置参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。
最后说一个实际经验:短信模块的调试,AI 能帮你解释报错和补全代码,但权限和默认短信应用这两个坑,它替代不了你在真机上的验证。配置跑通之后,拿一台 Android 10 以上的真机,把 app 设为默认短信应用,跑一遍备份和自定义插入,看短信列表里有没有出现那条「自定义短信接收」。这一步过了,整个链路才算真的通了。