From f6e8aa854e4d6a1fb4ce5573acb39d8ad9b19078 Mon Sep 17 00:00:00 2001 From: argszero Date: Thu, 20 Aug 2026 12:04:09 +0800 Subject: [PATCH] fix(db): seed_models sync-deletes config-removed models to avoid ghost market rows (v0.7.2) --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 3 +- src/db.rs | 67 ++++++++++++++++++++++++++++++++++++-- src/routes/admin_models.rs | 3 ++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e0fccd..28554fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aitokenpool" -version = "0.7.1" +version = "0.7.2" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index c9b43ec..577be35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aitokenpool" -version = "0.7.1" +version = "0.7.2" edition = "2021" description = "AI Token 共享池 — 企业 key 池 + 公共共享市场" license = "MIT" diff --git a/README.md b/README.md index e04df04..5a72343 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke ## 状态 -后端 + 前端已全部实现,当前版本 **v0.7.1**(config.toml 模型目录唯一真源): +后端 + 前端已全部实现,当前版本 **v0.7.2**(config.toml 模型目录唯一真源): - ✅ **P0-A(v0.2.0)**:后端骨架 + TOML 配置(`Config::validate`:points_per_unit>0 / plan→provider 存在 / endpoints≥1 / protocol 枚举)+ SQLite 数据层(幂等迁移,**生产空库不种任何种子数据**)+ 认证(argon2 + Bearer API Key)+ API Key 端点 - ✅ **P0-B(v0.2.1)**:网关转发(OpenAI Chat Completions + Anthropic Messages)+ 路由故障转移(粘性 / 健康冷却 / 3 次切换上限)+ 计量账本(点数计算、90/10 分成、事务性 settle) @@ -40,6 +40,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke - ✅ **v0.6.6**:**统一数据目录(rant 2026-08-19T20:53:23)**——`ATP_DATA_DIR`(默认 `./data`;`--data-dir` > env > 默认)下放 config.toml(首次自动复制示例)+ aitokenpool.db + logs/,目录自动创建;数据库路径统一由 data-dir 决定(config `db_path` 忽略);Docker 单卷挂载 `./atp-data:/data`(镜像内置示例配置,首启自动复制);.gitignore/.dockerignore 补 atp-data/;旧 data/ 迁移说明 - ✅ **v0.6.7**:**日志系统(rant 2026-08-19T20:54:26)**——log4rs 替换 env_logger:日志落盘 `/logs/aitokenpool.log` + stdout 双写;按大小滚动(`[log].max_file_size`,默认 10MB)+ `max_backups`(默认 7)自动清理旧日志;`[log]` 配置段(dir / level / file_pattern);Docker 日志随统一目录持久化 - ✅ **v0.7.0**:**缓存命中/未命中分开计费(rant 2026-08-20T10:17:27)**——usage 解析拆分缓存 token(openai `prompt_tokens_details.cached_tokens` / anthropic `cache_read_input_tokens` / responses `input_tokens_details.cached_tokens`);计费 = 未命中 × input_per_m + 命中 × cache_hit_input_per_m(缺省 0 = 命中免费);usage_records 新增 `cached_tokens` 列(迁移 v8);DeepSeek 官方 CNY 定价(deepseek-v4-flash 1.5/0.05/4.5、deepseek-v4-pro 4.5/0.15/13.5,空闲价);管理端模型表单可配缓存命中输入价;流式(SSE 转换 + 透传)同拆 +- ✅ **v0.7.2**:**seed 同步删除(rant 2026-08-20T11:58:33)**——启动 `seed_models` 在 upsert 之外同步删除 config 中已移除的模型行(config `[[models]]` 全量权威;市场不再显示幽灵模型;admin 运行时增删改下次启动被 config 覆盖/清掉,持久化自定义模型请写入 config.toml);新增测试 `seed_models_deletes_config_removed_rows`(125 passed) - ✅ **v0.7.1**:**简化模型配置(rant 2026-08-20T10:27:13)**——模型(厂商/价格/上下文/读图等)全部在 config.toml `[[models]]` 段定义(唯一真源),启动 upsert 进 DB;**移除 data/models.example.json 与 price_overrides 双层机制**(不再有「json 聚合源 + 官方价覆盖」设想);10 个模型迁入 config(DeepSeek 官方 CNY 价 flash 1.5/0.05/4.5、pro 4.5/0.15/13.5);data.js 游客兜底价对齐 config `ui/` 已由纯静态原型升级为**对接真实 API**(登录、钱包、市场、共享、交易、设置、管理、运营全部真实数据),由后端 `ServeDir` 静态托管,无需单独部署前端。 diff --git a/src/db.rs b/src/db.rs index 9c75c2b..f89cad5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -416,8 +416,10 @@ pub(crate) fn seed_test_users(conn: &Connection) -> Result<()> { Ok(()) } -/// models 表种子(启动时 upsert):config.toml [[models]] 唯一真源 +/// models 表种子(启动时同步):config.toml [[models]] 唯一真源 ///(rant 2026-08-20T10:27:13:移除 models.json + price_overrides 双层机制) +///(rant 2026-08-20T11:58:33:upsert 之外同步删除 config 已移除的模型,避免市场幽灵模型) +/// 语义:config 全量权威——启动后 models 表 = config [[models]](admin 运行时增改在下次启动被 config 覆盖/清掉) pub fn seed_models(conn: &Connection, cfg: &crate::config::Config) -> Result<()> { let mut n = 0u32; for m in &cfg.models { @@ -449,7 +451,34 @@ pub fn seed_models(conn: &Connection, cfg: &crate::config::Config) -> Result<()> )?; n += 1; } - log::info!("models seed:{n} 行 upsert(来源 config.toml [[models]])"); + // 同步删除 config 中已移除的模型(模型行无外键引用,usage_records/transactions 均按 model 字符串记) + let mut removed = 0u32; + let stale: Vec<(String, String)> = { + let mut stmt = conn + .prepare("SELECT provider, model FROM models") + .context("prepare models scan")?; + let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?; + let mut v = Vec::new(); + for row in rows { + let (p, m) = row?; + if !cfg + .models + .iter() + .any(|cm| cm.provider == p && cm.model == m) + { + v.push((p, m)); + } + } + v + }; + for (p, m) in &stale { + conn.execute( + "DELETE FROM models WHERE provider = ?1 AND model = ?2", + rusqlite::params![p, m], + )?; + removed += 1; + } + log::info!("models seed:{n} 行 upsert(来源 config.toml [[models]]),删除 {removed} 行 config 已移除模型"); Ok(()) } @@ -636,6 +665,40 @@ mod tests { let _ = std::fs::remove_file(p); } + #[test] + fn seed_models_deletes_config_removed_rows() { + // rant 2026-08-20T11:58:33:seed 只 upsert 不删除 → config 移除的模型残留 DB(市场幽灵模型) + let (conn, p) = tmp_db("models_sync"); + let cfg = crate::config::Config::load("config/config.example.toml").unwrap(); + seed_models(&conn, &cfg).expect("seed models"); + let before: i64 = conn + .query_row("SELECT COUNT(*) FROM models", [], |r| r.get(0)) + .unwrap(); + // 模拟历史残留:手工插入 config 中不存在的模型(旧版本遗留 / 曾手写 DB) + conn.execute( + "INSERT INTO models (provider, model, currency, input_per_m, output_per_m) \ + VALUES ('ghost-provider', 'ghost-model', 'USD', 1.0, 1.0)", + [], + ) + .unwrap(); + // 再 seed:ghost 行应被同步删除,数量回到 config 集合大小 + seed_models(&conn, &cfg).expect("二次 seed 应清理 ghost 行"); + let after: i64 = conn + .query_row("SELECT COUNT(*) FROM models", [], |r| r.get(0)) + .unwrap(); + assert_eq!(after, before, "config 已移除模型应被删除(无幽灵行)"); + let ghost: i64 = conn + .query_row( + "SELECT COUNT(*) FROM models WHERE provider = 'ghost-provider' AND model = 'ghost-model'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(ghost, 0, "ghost 模型行已删除"); + drop(conn); + let _ = std::fs::remove_file(p); + } + #[test] fn v2_migration_adds_available_and_note_columns() { // 模拟 v1 旧库:建表后不含 available_* / note 列 → migrate 幂等补列 diff --git a/src/routes/admin_models.rs b/src/routes/admin_models.rs index 6bea65f..e5924ff 100644 --- a/src/routes/admin_models.rs +++ b/src/routes/admin_models.rs @@ -7,6 +7,9 @@ //! - PATCH /api/admin/models/:id:部分更新(任意字段) //! - DELETE /api/admin/models/:id:直接删除(模型删除后该 model 的调用无价格行 → 0 计费,README 说明) //! - 权限:require_admin(role=admin,否则 403) +//! - 注意:config.toml `[[models]]` 为唯一真源——启动 seed_models 会同步删除 config 中已移除的 +//! 模型行(rant 2026-08-20T11:58:33),故 admin 运行时增删改在下次启动时会被 config 覆盖/清掉; +//! 持久化自定义模型请写入 config.toml。 use axum::extract::State; use axum::Json;