- 添加SQLite数据库文件及数据目录的忽略规则 - 将数据库连接地址更新为本地SQLite数据库文件路径 - 将数据库从 PostgreSQL 迁移至 SQLite,更新依赖、Drizzle 方言及数据类型映射,确保嵌入式环境兼容性并保持应用层代码不变。 - 将数据库方言从 PostgreSQL 更改为 SQLite。 - 将数据库依赖从 postgres 替换为 better-sqlite3,并添加 better-sqlite3 的类型定义。 - 修改数据库连接字符串验证规则,从URL格式验证改为非空字符串验证。 - 将数据库连接从 PostgreSQL 切换为 Better-SQLite3,并支持创建目录和内存数据库。 - 将 todo 表的 completed 字段从 PostgreSQL 的 boolean 类型改为 SQLite 的 boolean 模式整数类型。 - 将字段定义从 PostgreSQL 适配改为 SQLite 适配,使用文本类型 ID 和时间戳整数类型,并统一使用 UUIDv7 和当前时间作为默认值。 - 添加 better-sqlite3 及其类型定义,并引入相关依赖以支持其功能 - 添加 better-sqlite3 依赖并移除 postgres 依赖
16 KiB
AGENTS.md - AI Coding Agent Guidelines
本文档为 AI 编程助手提供此 TanStack Start 全栈项目的开发规范和指南。
项目概览
- 框架: TanStack Start (React SSR 框架,文件路由)
- 运行时: Bun
- 语言: TypeScript (strict mode, ESNext)
- 样式: Tailwind CSS v4
- 数据库: SQLite + Drizzle ORM
- 状态管理: TanStack Query
- 路由: TanStack Router (文件路由)
- RPC: ORPC (类型安全 RPC,契约优先)
- 构建工具: Vite + Turbo
- 代码质量: Biome (格式化 + Lint)
依赖管理
Bun Catalog 系统
项目使用 Bun Catalog 统一管理依赖版本(定义在根目录 package.json 的 catalog 字段)。
安装依赖的正确方式:
# ✅ 正确:使用 catalog: 前缀
bun add <package-name>@catalog:
# ❌ 错误:直接安装会绕过版本统一管理
bun add <package-name>@latest
示例:
# 添加 systeminformation 依赖到 packages/utils
cd packages/utils
bun add systeminformation@catalog:
# 添加 react 依赖到 apps/server
cd apps/server
bun add react@catalog:
为什么使用 Catalog:
- 确保 monorepo 中所有包使用相同版本
- 集中管理依赖版本,避免版本冲突
- 简化依赖升级(只需修改根 package.json)
添加新依赖的步骤:
- 在根目录
package.json的catalog字段添加依赖及版本 - 在目标包中使用
bun add <package>@catalog:安装
构建、Lint 和测试命令
开发
bun dev # 启动 Vite 开发服务器
bun db:studio # 打开 Drizzle Studio 数据库管理界面
构建
bun build # 构建 Vite 应用 (输出到 .output/)
bun compile # 编译为独立可执行文件 (使用 build.ts)
代码质量
bun typecheck # 运行 TypeScript 类型检查
bun fix # 运行 Biome 自动修复格式和 Lint 问题
biome check . # 检查但不自动修复
biome format --write . # 仅格式化代码
数据库
bun db:generate # 从 schema 生成迁移文件
bun db:migrate # 执行数据库迁移
bun db:push # 直接推送 schema 变更 (仅开发环境)
测试
注意: 当前未配置测试框架。添加测试时:
- 使用 Vitest 或 Bun 内置测试运行器
- 运行单个测试文件:
bun test path/to/test.ts - 运行特定测试:
bun test -t "测试名称模式"
代码风格指南
格式化 (Biome)
缩进: 2 空格 (不使用 tab)
换行符: LF (Unix 风格)
引号: 单引号 'string'
分号: 按需 (ASI - 自动分号插入)
箭头函数括号: 始终使用 (x) => x
示例:
const myFunc = (value: string) => {
return value.toUpperCase()
}
导入组织
Biome 自动组织导入。顺序:
- 外部依赖
- 内部导入 (使用
@/*别名) - 类型导入 (仅导入类型时使用
type关键字)
示例:
import { createFileRoute } from '@tanstack/react-router'
import { oc } from '@orpc/contract'
import { z } from 'zod'
import { db } from '@/db'
import { todoTable } from '@/db/schema'
import type { ReactNode } from 'react'
TypeScript
严格模式: 启用了额外的严格检查
strict: truenoUncheckedIndexedAccess: true- 数组/对象索引返回T | undefinednoImplicitOverride: truenoFallthroughCasesInSwitch: true
模块解析: bundler 模式 + verbatimModuleSyntax
- 导入时始终使用
.ts/.tsx扩展名 - 使用
@/*路径别名指向src/*
类型注解:
- 公共 API 的函数参数和返回类型必须注解
- 优先使用显式类型而非
any - 对象形状用
type,可扩展契约用interface - 不可变 props 使用
Readonly<T>
命名规范
- 文件: 工具函数用 kebab-case,组件用 PascalCase
utils.ts,todo.tsx,NotFound.tsx
- 路由: 遵循 TanStack Router 约定
routes/index.tsx→/routes/__root.tsx→ 根布局
- 组件: PascalCase 箭头函数 (Biome 规则
useArrowFunction强制) - 函数: camelCase
- 常量: 真常量用 UPPER_SNAKE_CASE,配置对象用 camelCase
- 类型/接口: PascalCase
React 模式
组件: 使用箭头函数
const MyComponent = ({ title }: { title: string }) => {
return <div>{title}</div>
}
路由: 使用 createFileRoute 定义路由
export const Route = createFileRoute('/')({
component: Home,
})
数据获取: 使用 TanStack Query hooks
useSuspenseQuery- 保证有数据useQuery- 数据可能为空
Props: 禁止直接修改 props (Biome 规则 noReactPropAssignments)
数据库 Schema (Drizzle)
- 在
src/server/db/schema/*.ts定义 schema - 从
src/server/db/schema/index.ts导出 - 使用
drizzle-orm/sqlite-core的 SQLite 类型 - 主键使用
uuidv7()(TEXT 存储) - 时间戳使用
integer({ mode: 'timestamp_ms' })(Unix 毫秒时间戳) - 始终包含
createdAt和updatedAt时间戳
示例:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { v7 as uuidv7 } from 'uuid'
export const myTable = sqliteTable('my_table', {
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()).$onUpdateFn(() => new Date()),
})
环境变量
- 使用
@t3-oss/env-core进行类型安全的环境变量验证 - 在
src/env.ts定义 schema - 服务端变量: 无前缀
- 客户端变量: 必须有
VITE_前缀 - 使用 Zod schema 验证
错误处理
- 异步操作使用 try-catch
- 抛出带有描述性消息的错误
- 用户界面错误优先使用 Result 类型或错误边界
- 适当记录错误 (避免记录敏感数据)
样式 (Tailwind CSS)
- 使用 Tailwind v4 工具类
- 通过
@/styles.css?url导入样式 - 优先使用组合而非自定义 CSS
- 响应式修饰符:
sm:,md:,lg: - UI 文本适当使用中文
目录结构
src/
├── components/ # 可复用 React 组件
├── db/
│ ├── schema/ # Drizzle schema 定义
│ └── index.ts # 数据库实例
├── integrations/ # 第三方集成 (TanStack Query/Router)
├── lib/ # 工具函数
├── orpc/ # ORPC (RPC 层)
│ ├── contracts/ # 契约定义 (input/output schemas)
│ ├── handlers/ # 服务端过程实现
│ ├── middlewares/ # 中间件 (如 DB provider)
│ ├── contract.ts # 契约聚合
│ ├── router.ts # 路由组合
│ ├── server.ts # 服务端实例
│ └── client.ts # 同构客户端
├── routes/ # TanStack Router 文件路由
│ ├── __root.tsx # 根布局
│ ├── index.tsx # 首页
│ └── api/rpc.$.ts # ORPC HTTP 端点
├── env.ts # 环境变量验证
└── router.tsx # 路由配置
重要提示
- 禁止 编辑
src/routeTree.gen.ts- 自动生成 - 禁止 提交
.env文件 - 使用.env.example作为模板 - 必须 在提交前运行
bun fix - 必须 使用
@/*路径别名而非相对导入 - 必须 利用 React Compiler (babel-plugin-react-compiler) - 避免手动 memoization
Git 工作流
- 按照上述风格指南进行修改
- 运行
bun fix自动格式化和 lint - 运行
bun typecheck确保类型安全 - 使用
bun dev本地测试变更 - 使用清晰的描述性消息提交
常见模式
创建 ORPC 过程
步骤 1: 定义契约 (src/orpc/contracts/my-feature.ts)
import { oc } from '@orpc/contract'
import { z } from 'zod'
export const myContract = {
get: oc.input(z.object({ id: z.uuid() })).output(mySchema),
create: oc.input(createSchema).output(mySchema),
}
步骤 2: 实现处理器 (src/orpc/handlers/my-feature.ts)
import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares'
export const get = os.myFeature.get
.use(dbProvider)
.handler(async ({ context, input }) => {
return await context.db.query.myTable.findFirst(...)
})
步骤 3: 注册到契约和路由
// src/orpc/contract.ts
export const contract = { myFeature: myContract }
// src/orpc/router.ts
import * as myFeature from './handlers/my-feature'
export const router = os.router({ myFeature })
步骤 4: 在组件中使用
import { orpc } from '@/orpc'
const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id }))
const mutation = useMutation(orpc.myFeature.create.mutationOptions())
已知问题与解决方案
构建问题
已解决: Vite 8.0.0-beta.10 已修复与 Nitro 插件的兼容性问题
- 当前版本: Vite 8.0.0-beta.10 + nitro-nightly@3.0.1-20260125
- 状态: 构建稳定,开发和生产环境均正常工作
依赖选择经验
ohash vs crypto.createHash
在实现硬件指纹功能时,曾误判 ohash 不适合用于硬件指纹识别。经深入研究发现:
事实:
ohash内部使用完整的 SHA-256 算法(256 位)- 输出 43 字符 Base64URL 编码(等价于 64 字符 Hex)
- 碰撞概率与
crypto.createHash('sha256')完全相同(2^128) - 自动处理对象序列化,代码更简洁
对比:
// ohash - 推荐用于对象哈希
import { hash } from 'ohash'
const fingerprint = hash(systemInfo) // 一行搞定
// crypto - 需要手动序列化
import { createHash } from 'node:crypto'
const fingerprint = createHash('sha256')
.update(JSON.stringify(systemInfo))
.digest('base64url')
结论:
- ✅
ohash完全适合硬件指纹场景(数据来自系统 API,非用户输入) - ✅ 两者安全性等价,选择取决于代码风格偏好
- ⚠️ ohash 文档警告的"序列化安全性"仅针对用户输入场景
经验教训:
- 不要仅凭名称("短哈希")判断库的实现
- 深入研究文档和源码再做技术决策
- 区分"用户输入场景"和"系统数据场景"的安全要求
缓存库选择:@isaacs/ttlcache
决策时间: 2026-01-26
背景: 硬件指纹功能最初使用手动实现的 TTL 缓存(module-level 变量 + 手动过期检查)。为提高代码可维护性,迁移到专业缓存库。
选型:
- 选择:
@isaacs/ttlcachev2.1.4 - 理由:
- 专为 TTL 场景优化,无需 LRU 追踪开销
- 零依赖,6M+ 周下载量
- 内置 TypeScript 类型
- 自动过期管理,无需手动定时器
- API 简洁:
new TTLCache({ ttl, max })
实现细节:
- 保留
inFlightPromise 模式用于并发请求去重(TTLCache 不提供此功能) - 使用单一缓存键
'fingerprint'(单服务器场景,opts 不影响输出) - 默认 TTL: 10 分钟(可通过
cacheTtlMs参数覆盖)
对比手动实现:
- ✅ 更少自定义代码
- ✅ 更清晰的 TTL 语义
- ✅ 经过充分测试的库
- ⚠️ 仍需手动处理并发去重
经验教训:
- 专业库不一定解决所有问题(如并发去重)
- 对于简单场景,手动实现 vs 库的选择主要取决于可维护性而非功能
PostgreSQL → SQLite 迁移
迁移时间: 2026-01-26
原因: 项目需要在嵌入环境中运行,无法安装 PostgreSQL
技术选择:
- 驱动:
better-sqlite3v11.8.1 (跨平台,成熟稳定,原生模块) - 类型定义:
@types/better-sqlite3v7.6.12 - 主键: TEXT 存储 UUIDv7 (保持全局唯一性,36 字符字符串)
- 时间戳: INTEGER 存储 Unix 毫秒时间戳 (
integer({ mode: 'timestamp_ms' })) - 布尔值: INTEGER 存储 0/1 (
integer({ mode: 'boolean' })) - 数据库文件:
./data/app.db
变更内容:
- 依赖:
postgres→better-sqlite3+@types/better-sqlite3 - Drizzle 方言:
dialect: 'postgresql'→dialect: 'sqlite' - Schema 导入:
drizzle-orm/pg-core→drizzle-orm/sqlite-core - 表定义:
pgTable→sqliteTable - 数据库连接:
drizzle-orm/postgres-js→drizzle-orm/better-sqlite3 - 数据类型映射:
- UUID:
uuid()→text()(使用uuidv7()生成) - 时间戳:
timestamp({ withTimezone: true })→integer({ mode: 'timestamp_ms' }) - 布尔值:
boolean()→integer({ mode: 'boolean' }) - 默认值:
defaultNow()→$defaultFn(() => new Date())
- UUID:
- 环境变量:
DATABASE_URL从 URL 格式改为文件路径 (./data/app.db) - 目录自动创建: 添加了数据库文件目录的自动创建逻辑
数据库连接层变更:
// Before (PostgreSQL)
import { drizzle } from 'drizzle-orm/postgres-js'
export const createDB = () =>
drizzle({
connection: { url: env.DATABASE_URL, prepare: true },
schema,
})
// After (SQLite)
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
export const createDB = () => {
const dbPath = env.DATABASE_URL
if (dbPath !== ':memory:') {
mkdirSync(dirname(dbPath), { recursive: true })
}
const sqlite = new Database(dbPath)
return drizzle(sqlite, { schema })
}
Schema 变更示例:
// Before (PostgreSQL)
import { pgTable, text, boolean, timestamp, uuid } from 'drizzle-orm/pg-core'
export const todoTable = pgTable('todo', {
id: uuid('id').primaryKey().$defaultFn(() => uuidv7()),
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
// After (SQLite)
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const todoTable = sqliteTable('todo', {
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
title: text('title').notNull(),
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
})
兼容性保证:
- ✅ 所有业务逻辑保持不变
- ✅ ORPC 契约和类型完全兼容
- ✅ 应用层代码无需修改
- ✅ Drizzle 自动处理类型转换 (Date ↔ number, boolean ↔ 0/1)
构建验证:
- ✅
bun typecheck- 类型检查通过 - ✅
bun fix- 代码质量检查通过 - ✅
bun run build- Vite 构建成功 - ✅
bun db:push- 数据库初始化成功 - ✅ SQLite 文件已创建 (
./data/app.db, 12KB)
注意事项:
- SQLite 是文件数据库,适合嵌入环境和开发环境
- 不支持多进程并发写入 (适合单应用实例)
- 性能足够支持中小型应用 (日请求 < 10万)
- 数据库文件和 WAL 文件 (
.db-shm,.db-wal) 已添加到.gitignore better-sqlite3是原生模块,需要匹配生产环境的 OS 和架构 (构建输出会提示)
Drizzle 时间戳模式:
timestamp_ms: Unix 毫秒时间戳 (推荐,精度更高)timestamp: Unix 秒时间戳- Drizzle 自动转换
Date对象 ↔ 数字
命令对比:
# 开发环境初始化 (推荐)
bun db:push
# 生产环境迁移
bun db:generate # 生成迁移文件
bun db:migrate # 执行迁移
# 数据库管理 UI
bun db:studio
Git 工作流要求
重要原则:保持代码仓库与文档同步
当遇到技术问题、做出架构决策、或发现重要经验时:
- 立即更新 AGENTS.md:记录问题、原因、解决方案
- 持续同步:每次重大变更后更新文档
- 版本关联:在文档中标注相关的库版本、commit hash
这确保未来的开发者(包括 AI 助手)能快速理解项目历史和技术选择。
最后更新: 2026-01-26 项目版本: 基于 package.json 依赖版本