
1. 从零搭一个能落地的 Android 记事本SQLite 建库与 AI 工具链统一配置做 Android 记事本这类小工具最容易被低估的就是数据层。界面画得快真正决定 App 能不能长期用的是 SQLite 建库、增删改查封装和版本升级这几件事。我见过太多 Demo 把 SQL 语句直接写在 Activity 里改个字段就要全局搜替换最后连自己都不敢动。这篇就按一个真实记事本 App 的路径走先用SQLiteOpenHelper把diary.db建起来再把 CRUD 封装成独立适配器最后把 AI 辅助编码工具的 endpoint 和 API Key 统一收到 TaoToken避免 Cline、Claude Code、Codex 各配一套 Key 的重复劳动。核心检索词先摆出来Android 记事本 SQLite 数据库实现本质是用SQLiteOpenHelper管理建库与版本用SQLiteDatabase执行增删改查用Cursor读取结果集。它适合谁适合刚学完四大组件、想做一个完整可运行小项目的 Android 初学者也适合想把 AI 编码工具链理顺的中级开发者。下面所有代码都可以直接复制进 Android Studio 跑数据库文件落在/data/data/包名/databases/diary.db用 Device File Explorer 就能看到。2. 前置准备TaoToken 统一 Key 与 Android 工程环境2.1 为什么要把 AI 工具的 Key 统一到 TaoToken写记事本的过程中你大概率会同时开几个 AI 辅助工具一个在 IDE 里补全 CRUD 代码一个在终端里跑 Agent 改 bug还有一个用来解释Cursor的移动逻辑。如果每个工具都单独填一家厂商的 Key配置散落在settings.json、auth.json、环境变量里换一次 Key 就要翻五个地方。TaoToken 的思路是提供一个统一的 OpenAI 兼容入口把 Base URL 指向同一个地址Key 也只维护一份。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时直接写这个根路径即可。2.2 拿 Key 与确认模型 ID登录后进入控制台在 API Keys 页面创建一个新 Key复制出来形如sk-开头的一串。这个 Key 就是后面所有工具共用的那一份。模型 ID 按你实际要用的填比如对话类、代码类各有对应名称配置时以控制台展示的为准不要凭记忆写。控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content2.3 Android 工程侧准备新建一个 Empty Views Activity 工程语言选 JavaminSdk 拉到 24 以上。在app/build.gradle里确认没有引入 Room 之类的 ORM这篇要手写原生 SQLite目的是让你看清每一层在干什么。建三个包com.jk.service放数据库帮助类和适配器com.jk放实体类com.jk.ui放 Activity。实体类Diary很简单四个字段加构造和 getter/setterpackage com.jk; public class Diary { private Integer id; private String title; private String body; private String time; public Diary() {} public Diary(Integer id, String title, String body, String time) { this.id id; this.title title; this.body body; this.time time; } public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getBody() { return body; } public void setBody(String body) { this.body body; } public String getTime() { return time; } public void setTime(String time) { this.time time; } }这一步别偷懒实体类字段和后面建表语句的列名要一一对应否则getColumnIndex会返回 -1读出来全是空值。3. 可复制配置SQLiteOpenHelper 建表与 CRUD 封装3.1 建库与版本管理SQLiteOpenHelper是 Android 提供的数据库管理基类onCreate只在数据库第一次创建时调用onUpgrade在版本号变大时调用。下面这份DbHelper直接可用数据库名diary.db版本 1package com.jk.service; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DbHelper extends SQLiteOpenHelper { public DbHelper(Context context) { super(context, diary.db, null, 1); } Override public void onCreate(SQLiteDatabase db) { String sql CREATE TABLE Diary ( diaryId integer primary key autoincrement, title varchar(20), body varchar(1000), time varchar(40)); db.execSQL(sql); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级时在这里写迁移逻辑比如加字段 // db.execSQL(ALTER TABLE Diary ADD body varchar(1000) null); } }注意onUpgrade里不要直接DROP TABLE再重建那会把用户写的日记全清掉。正确做法是按oldVersion逐级迁移比如从 1 升到 2 就加一列从 2 升到 3 再加索引。3.2 CRUD 适配器封装把增删改查集中到DiaryAdapterActivity 只调方法不碰 SQL。这里有个关键点getWritableDatabase()和getReadableDatabase()在磁盘没满时返回的是同一个对象但磁盘满时前者会抛异常后者会降级为只读。写操作统一用getWritableDatabase()读操作可以用getReadableDatabase()。package com.jk.service; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.jk.Diary; import java.util.ArrayList; import java.util.List; public class DiaryAdapter { private DbHelper dbOpenHelper; public DiaryAdapter(Context context) { dbOpenHelper new DbHelper(context); } public void save(Diary diary) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String sql insert into Diary(title,body,time) values(?,?,?); db.execSQL(sql, new Object[]{diary.getTitle(), diary.getBody(), diary.getTime()}); db.close(); } public void update(Diary diary) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String sql update Diary set title?,body? where diaryId?; db.execSQL(sql, new Object[]{diary.getTitle(), diary.getBody(), diary.getId()}); db.close(); } public void delete(Integer id) { SQLiteDatabase db dbOpenHelper.getWritableDatabase(); String sql delete from Diary where diaryId?; db.execSQL(sql, new Object[]{id}); db.close(); } public Diary find(Integer id) { SQLiteDatabase db dbOpenHelper.getReadableDatabase(); String sql select * from Diary where diaryId?; Cursor cursor db.rawQuery(sql, new String[]{id.toString()}); if (cursor.moveToFirst()) { int diaryId cursor.getInt(cursor.getColumnIndex(diaryId)); String title cursor.getString(cursor.getColumnIndex(title)); String body cursor.getString(cursor.getColumnIndex(body)); String time cursor.getString(cursor.getColumnIndex(time)); cursor.close(); db.close(); return new Diary(diaryId, title, body, time); } cursor.close(); db.close(); return null; } public ListDiary getScrollData(int offset, int maxResult) { ListDiary list new ArrayList(); SQLiteDatabase db dbOpenHelper.getReadableDatabase(); String sql select * from Diary limit ?,?; Cursor cursor db.rawQuery(sql, new String[]{ String.valueOf(offset), String.valueOf(maxResult)}); while (cursor.moveToNext()) { int diaryId cursor.getInt(cursor.getColumnIndex(diaryId)); String title cursor.getString(cursor.getColumnIndex(title)); String body cursor.getString(cursor.getColumnIndex(body)); String time cursor.getString(cursor.getColumnIndex(time)); list.add(new Diary(diaryId, title, body, time)); } cursor.close(); db.close(); return list; } public long getCount() { SQLiteDatabase db dbOpenHelper.getReadableDatabase(); Cursor cursor db.rawQuery(select count(*) from Diary, null); cursor.moveToFirst(); long count cursor.getLong(0); cursor.close(); db.close(); return count; } }用?占位符而不是字符串拼接是为了防止 SQL 注入也避免标题里带单引号时语句被截断。getScrollData里的limit ?,?第一个参数是偏移量第二个是每页条数做分页列表时直接传page * pageSize和pageSize。3.3 AI 工具统一 Key 的配置片段现在把 AI 编码工具的配置也统一到 TaoToken。以 Cline 的 MCP 配置为例在cline_mcp_settings.json里写{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { OPENAI_BASE_URL: https://taotoken.net/api, OPENAI_API_KEY: sk-你的Key, OPENAI_MODEL: 你的模型ID } } } }如果你用的是 Claude Code配置走settings.json把 Base URL 和 Key 填进对应字段Codex 则改auth.json。三件套永远是 Base URL、Key、Model ID缺一个都连不上。Base URL 写https://taotoken.net/api不要多加/v1之类的后缀具体以接入文档为准。4. 验证请求跑通数据库与 AI 连通性4.1 验证 SQLite 是否真的写进去了在 Activity 里调一次保存再查总数DiaryAdapter adapter new DiaryAdapter(this); Diary d new Diary(); d.setTitle(第一篇); d.setBody(今天开始用 SQLite 存日记); d.setTime(2025-01-01 10:00); adapter.save(d); long count adapter.getCount(); android.util.Log.d(DiaryTest, 当前记录数 count);运行后看 Logcat输出当前记录数1就说明建库、建表、插入、查询整条链路通了。再用 Android Studio 的 Device File Explorer 进/data/data/你的包名/databases/能看到diary.db和diary.db-journal说明文件确实落盘了。4.2 验证 TaoToken 连通性用 curl 发一个最小请求确认 Key 和地址没问题curl https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: 你的模型ID, messages: [{role: user, content: 只回复 ok}] }返回体里choices[0].message.content有内容就说明统一 Key 生效了。这一步过了再把同样的 Base URL 和 Key 填进 Cline、Claude Code就不用重复试错了。5. 本篇常见报错排查5.1 401 Unauthorized最常见的原因是 Key 复制时带了空格或者把Bearer前缀漏了。检查Authorization头是不是Bearer sk-xxx格式。还有一种情况是 Key 被禁用或额度用尽去控制台 API Keys 页面确认状态。5.2 local proxy failed这个报错通常出现在 AI 工具侧说明工具尝试走本地代理但没起来。检查你的工具配置里有没有多余的proxy字段把它删掉让请求直连https://taotoken.net/api。同时确认 Base URL 没有写成http://localhost:xxxx这种本地地址。5.3 reading choices 相关报错返回体解析失败报reading choices之类多半是响应不是标准 OpenAI 格式。先确认请求打到了正确的 endpoint路径是/api/chat/completions。如果模型 ID 写错服务端可能返回错误结构工具解析时就崩了。用第 4.2 节的 curl 先验证一遍能拿到标准响应再回工具里配。5.4 OAuth 相关报错有些工具默认走 OAuth 登录流程报OAuth token expired或类似信息。这类工具要切到 API Key 模式在配置里显式指定OPENAI_API_KEY不要让它去读本地缓存的登录态。Claude Code 和 Codex 都有对应的 Key 模式开关按接入文档改。5.5 SQLite 侧常见坑getColumnIndex返回 -1 导致读空值检查列名大小写是否和建表语句一致。Cursor用完不close会泄漏每个查询方法里都要关。db.close()调太早会导致后续操作报attempt to re-open an already-closed object确保在 Cursor 读完之后再关。6. 把工具链收拢到一处数据库这层跑通之后记事本的骨架就立住了。接下来加列表页、编辑页、时间格式化都是在这个 CRUD 封装上叠 UI。AI 工具那边把 Cline、Claude Code、Codex 的 Base URL 和 Key 都指向 TaoToken 同一份配置换 Key 时只改一处省下的时间够你多写两个功能。需要长期跑编码 Agent 的话可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想先验证模型对话效果用模型对话页https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入细节以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 在 API Keys 页面管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后留一个实操建议onUpgrade里写迁移逻辑时先在测试机上把版本号从 1 改到 2 跑一遍确认老数据还在再提交。数据库这东西改错了比 UI 崩了难恢复得多。