架构设计
Obsidian Arc 以一个 Go 程序提供网页和 API,使用一个关系型数据库保存状态。前端在构建时嵌入程序,运行时不需要单独启动前端服务。
程序结构
浏览器 / API 客户端
↓
HTTP 路由、认证与请求记录
↓
模型权限与额度检查
↓
上游协议适配器 → 模型服务商
↓
对话保存与用量结算
↓
SQLite / PostgreSQL网页请求保存用户会话;兼容 API 由客户端维护上下文,不写入网页历史。两条路径共用模型权限与额度规则。
业务代码按领域放在 internal/ 下,由 internal/server/server.go 组装。模块按职责组织,不要求每个目录都具有同一套文件名。
依赖与部署
Go 的三个直接依赖用于 SQLite、PostgreSQL 和密码学。路由、日志与常用网络功能使用标准库,没有 ORM、消息队列或独立缓存服务。
SQLite 适合单机部署,PostgreSQL 可用于独立数据库或多个应用实例。切换数据库不会自动复制已有业务数据。
前端资源嵌入意味着升级界面需要重新构建程序。这样交付时只有一个应用产物,也避免前后端版本被单独替换后不匹配。
可移植的数据库查询
查询使用 ? 占位符,通过 database.Queryer 按驱动重绑定。实体标识使用 ULID,时间使用 epoch 毫秒。
迁移采用编号 SQL 文件,在启动时执行。%BLOB% 按数据库替换为相应二进制列类型。迁移不能依赖只有某个数据库提供的语法。
并发与短事务
先检查再写入的持久状态,需要让检查与修改在同一数据库事务的锁保护下完成。进程内互斥锁不能保护多个实例共同修改的数据。
按账号维护的约束可锁定用户行;全站约束可锁定一个固定的设置行。额度计数还使用原子更新返回更新后的数值。
SQLite 的写入由数据库串行协调,不能把这个方案理解成 SQLite 拥有 PostgreSQL 相同的行锁并行能力。
事务不跨越上游生成调用。调用前预留额度并提交,结束后再保存和结算,避免用一个数据库连接等待几分钟的回答。
取消与持久化
浏览器停止或断开连接会取消请求上下文,上游 HTTP 调用随之取消。已接收内容和用量通过脱离客户端取消的上下文处理,并在关键路径设置单独的时间上限。
这保证客户端离开后仍有机会保存结果,但无法保证进程被强制终止时仍然完成结算。上游是否停止收费也由其实际实现决定。
多实例边界
数据库事务保护持久状态,并不意味着程序内所有状态都自动共享:
- 业务设置在进程内缓存,当前没有跨实例自动刷新。
- 访客试用预算与部分频率、并发限制保存在内存,各实例分别计算。
- 后台巡检由各进程运行,需要考虑重复探测。
- 所有实例必须使用相同的凭据加密主密钥。
配置多个实例前应验证这些边界。项目尚未证明长期真实流量下的稳定性,不应把 CI 通过当作生产负载测试。
验证
make test 执行 Go 静态检查、格式检查、Go 测试、Vue 类型检查与前端测试。CI 还运行 PostgreSQL 测试及 Docker 启动检查。
构建体积属于测量结果。修改主应用的依赖或分包后,应重新测量,并同步更新仓库中的测量记录;文档站的构建产物与应用产物分别计算。