diff --git a/Cargo.lock b/Cargo.lock index f6f3d28..89be089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "aitokenpool" -version = "0.5.1" +version = "0.5.2" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 5489031..38c1ede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aitokenpool" -version = "0.5.1" +version = "0.5.2" edition = "2021" description = "AI Token 共享池 — 企业 key 池 + 公共共享市场" license = "MIT" diff --git a/README.md b/README.md index c80a163..f3e35a6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke ## 状态 -后端 + 前端已全部实现,当前版本 **v0.5.1**(102/102 测试全绿): +后端 + 前端已全部实现,当前版本 **v0.5.2**(102/102 测试全绿): - ✅ **P0-A(v0.2.0)**:后端骨架 + TOML 配置(`Config::validate`:points_per_unit>0 / plan→provider 存在 / endpoints≥1 / protocol 枚举)+ SQLite 数据层(幂等迁移 v4 + demo/admin/ops 种子)+ 认证(argon2 + Bearer API Key)+ API Key 端点 - ✅ **P0-B(v0.2.1)**:网关转发(OpenAI Chat Completions + Anthropic Messages)+ 路由故障转移(粘性 / 健康冷却 / 3 次切换上限)+ 计量账本(点数计算、90/10 分成、事务性 settle) @@ -30,6 +30,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke - ✅ **P3-A 补充(v0.4.1)**:`GET /v1/models` OpenAI 兼容模型列表(认证可选,带 Bearer 附加 available_keys;`/models` 别名) - ✅ **P3-B(v0.5.0)**:流式 SSE 跨协议转换——openai/anthropic/responses 任意协议的 `stream:true` 请求可转发到任意协议上游,响应流事件实时互转(openai delta ↔ anthropic content_block_delta ↔ responses output_text.delta,含工具调用/thinking 增量与流内 usage 计量;responses→anthropic 流式暂延后) - ✅ **发布(v0.5.1)**:GitHub Actions Docker 发布——`main` push / `v*` tag 自动构建推送 GHCR 镜像 `ghcr.io/argszero/aitokenpool`(buildx + gha 缓存) +- ✅ **v0.5.2(bugfix)**:修复 3 个时间敏感测试——测试硬编码日期(2026-08-18)跨天后导致赠送过期断言失败,改为 SQLite 动态日期(`datetime('now')` / `strftime('%Y-%m-%d 23:59:59', 'now')`),测试不再随日期推移周期失败 `ui/` 已由纯静态原型升级为**对接真实 API**(登录、钱包、市场、共享、交易、设置、管理、运营全部真实数据),由后端 `ServeDir` 静态托管,无需单独部署前端。 @@ -74,7 +75,7 @@ open http://localhost:8080/ # 浏览器访问 ## API 端点(Bearer 认证) -- `GET /healthz` → `{"status":"ok","version":"0.5.1"}` +- `GET /healthz` → `{"status":"ok","version":"0.5.2"}` - `POST /api/auth/login` → `{api_key}`;`GET /api/me` → `{id,email,name,role}` - `POST|GET /api/api-keys`(key 脱敏 `atk_live_****xxxx`);`DELETE /api/api-keys/:id`(撤销) - `POST /v1/chat/completions` / `POST /anthropic/v1/messages` / `POST /v1/responses`(网关,三协议互转,非流式 + 流式 SSE 跨协议转换);`GET /v1/models`(OpenAI 兼容模型列表,认证可选) diff --git a/src/billing.rs b/src/billing.rs index f0871a9..87cf43f 100644 --- a/src/billing.rs +++ b/src/billing.rs @@ -312,7 +312,7 @@ mod tests { // 消费者 user_id=1:赠送 1 点(当天 23:59:59 过期)+ 永久 10 点 conn.execute( "INSERT INTO gift_grants (user_id, amount, granted_at, expires_at, status) \ - VALUES (1, 1, '2026-08-18 10:00:00', '2026-08-18 23:59:59', 'active')", + VALUES (1, 1, datetime('now'), strftime('%Y-%m-%d 23:59:59', 'now'), 'active')", [], ) .unwrap(); @@ -394,7 +394,7 @@ mod tests { // 一笔已过期(昨天)的赠送:settle 前惰性清理 → 只扣永久 conn.execute( "INSERT INTO gift_grants (user_id, amount, granted_at, expires_at, status) \ - VALUES (1, 1, '2026-08-17 10:00:00', '2026-08-17 23:59:59', 'active')", + VALUES (1, 1, datetime('now', '-1 day'), strftime('%Y-%m-%d 23:59:59', 'now', '-1 day'), 'active')", [], ) .unwrap(); diff --git a/src/gift.rs b/src/gift.rs index 983d56a..923c40d 100644 --- a/src/gift.rs +++ b/src/gift.rs @@ -157,10 +157,20 @@ mod tests { id } + /// 相对当前时间的动态时间戳(避免测试硬编码日期跨天后失效) + fn ts(conn: &Connection, days: i64) -> String { + conn.query_row( + "SELECT datetime('now', ?1)", + [format!("{days} days")], + |r| r.get(0), + ) + .unwrap() + } + #[test] fn first_gift_granted_with_today_expiry() { let (conn, p) = tmp_db("g1"); - let uid = register(&conn, "u1@t.local", "2026-08-18 10:00:00"); // 今天注册 + let uid = register(&conn, "u1@t.local", &ts(&conn, 0)); // 今天注册 let granted = ensure_daily_gift(&conn, uid).unwrap(); assert!(granted, "注册当天应补发 1 点"); let (bal, gift): (f64, f64) = conn @@ -192,7 +202,7 @@ mod tests { #[test] fn same_day_no_duplicate_gift() { let (conn, p) = tmp_db("g2"); - let uid = register(&conn, "u2@t.local", "2026-08-18 10:00:00"); + let uid = register(&conn, "u2@t.local", &ts(&conn, 0)); assert!(ensure_daily_gift(&conn, uid).unwrap()); assert!(!ensure_daily_gift(&conn, uid).unwrap(), "同天不重复赠送"); let n: i64 = conn @@ -211,7 +221,7 @@ mod tests { fn outside_window_no_gift() { let (conn, p) = tmp_db("g3"); // 11 天前注册 → 超出 10 天窗口 - let uid = register(&conn, "u3@t.local", "2026-08-07 09:00:00"); + let uid = register(&conn, "u3@t.local", &ts(&conn, -11)); let granted = ensure_daily_gift(&conn, uid).unwrap(); assert!(!granted, "第 11 天起不再赠送"); let n: i64 = conn @@ -229,11 +239,11 @@ mod tests { #[test] fn expired_gift_cleaned_lazily() { let (conn, p) = tmp_db("g4"); - let uid = register(&conn, "u4@t.local", "2026-08-18 10:00:00"); + let uid = register(&conn, "u4@t.local", &ts(&conn, 0)); assert!(ensure_daily_gift(&conn, uid).unwrap()); // 手工把 expires_at 改成过去 → 惰性清理应标记 expired 并扣 gift_balance conn.execute( - "UPDATE gift_grants SET expires_at = '2026-08-01 00:00:00' WHERE user_id = ?1", + "UPDATE gift_grants SET expires_at = datetime('now', '-3 days') WHERE user_id = ?1", [uid], ) .unwrap(); @@ -261,17 +271,17 @@ mod tests { #[test] fn deduct_gift_first_oldest_expiry() { let (conn, p) = tmp_db("g5"); - let uid = register(&conn, "u5@t.local", "2026-08-18 10:00:00"); + let uid = register(&conn, "u5@t.local", &ts(&conn, 0)); // 两笔 active 赠送:今天到期 1 点、明天到期 1 点(最早到期先扣) conn.execute( "INSERT INTO gift_grants (user_id, amount, granted_at, expires_at, status) \ - VALUES (?1, 1, '2026-08-18 10:00:00', '2026-08-18 23:59:59', 'active')", + VALUES (?1, 1, datetime('now'), strftime('%Y-%m-%d 23:59:59', 'now'), 'active')", [uid], ) .unwrap(); conn.execute( "INSERT INTO gift_grants (user_id, amount, granted_at, expires_at, status) \ - VALUES (?1, 1, '2026-08-18 10:00:00', '2026-08-19 23:59:59', 'active')", + VALUES (?1, 1, datetime('now'), strftime('%Y-%m-%d 23:59:59', 'now', '+1 day'), 'active')", [uid], ) .unwrap(); @@ -311,10 +321,10 @@ mod tests { #[test] fn deduct_overflow_falls_to_permanent() { let (conn, p) = tmp_db("g6"); - let uid = register(&conn, "u6@t.local", "2026-08-18 10:00:00"); + let uid = register(&conn, "u6@t.local", &ts(&conn, 0)); conn.execute( "INSERT INTO gift_grants (user_id, amount, granted_at, expires_at, status) \ - VALUES (?1, 1, '2026-08-18 10:00:00', '2026-08-18 23:59:59', 'active')", + VALUES (?1, 1, datetime('now'), strftime('%Y-%m-%d 23:59:59', 'now'), 'active')", [uid], ) .unwrap();