From 44b8c6cdcac86e9a4cf7b2bce87cc823f4ecbaa2 Mon Sep 17 00:00:00 2001 From: argszero Date: Wed, 19 Aug 2026 21:25:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20unified=20data=20dir=20ATP=5FDA?= =?UTF-8?q?TA=5FDIR=20=E2=80=94=20config/db/logs=20under=20one=20dir=20(v0?= =?UTF-8?q?.6.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rant 2026-08-19T20:53:23: put config, db and logs in one directory so docker compose mounts a single volume. - main.rs: --data-dir / env ATP_DATA_DIR (default ./data); creates the dir + logs/ on start; config path = /config.toml (auto-copied from config.example.toml on first start; --config still honored); db path always /aitokenpool.db (config db_path ignored) - config: clap 'env' feature enabled for ATP_DATA_DIR - Dockerfile: ATP_DATA_DIR=/data, CMD --data-dir /data, image ships /config/config.example.toml for first-start copy - docker-compose: single volume ./atp-data:/data (config/db/logs inside) - .gitignore/.dockerignore: atp-data/ + config/config.toml - README: unified dir layout, docker mount example, legacy migration note - verified: first start creates config.toml+db+logs; second start reuses both (no admin rebuild, config hash unchanged); healthz 200 --- .dockerignore | 1 + .gitignore | 5 +++ Cargo.lock | 2 +- Cargo.toml | 4 +-- Dockerfile | 25 +++++++------ README.md | 26 +++++++++++--- config/config.example.toml | 2 ++ docker-compose.yml | 14 +++++--- src/main.rs | 72 ++++++++++++++++++++++++++++++++++---- 9 files changed, 121 insertions(+), 30 deletions(-) diff --git a/.dockerignore b/.dockerignore index 10a1222..6c8ec24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .github/ target/ data/ +atp-data/ config/config.toml .emrg/ docs/ diff --git a/.gitignore b/.gitignore index 1535f3e..7778c67 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ # macOS .DS_Store + +# 统一数据目录(rant 2026-08-19T20:53:23):config.toml + aitokenpool.db + logs/ +/atp-data +# 旧版数据目录(v0.6.6 前:db 在 data/) /data +/config/config.toml diff --git a/Cargo.lock b/Cargo.lock index b78c91a..47ea300 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "aitokenpool" -version = "0.6.5" +version = "0.6.6" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 073d6a8..6f238d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aitokenpool" -version = "0.6.5" +version = "0.6.6" edition = "2021" description = "AI Token 共享池 — 企业 key 池 + 公共共享市场" license = "MIT" @@ -46,7 +46,7 @@ argon2 = "0.5" sha2 = "0.10" hex = "0.4" rand = "0.8" -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } # 注册邮箱验证码发信(rant 2026-08-19T14:36:19:方案 B 完整验证码) lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "rustls-tls", "hostname", "pool"] } diff --git a/Dockerfile b/Dockerfile index c567a04..6dbcf1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,15 @@ -# AITokenPool — multi-stage release image (v0.3.3) +# AITokenPool — multi-stage release image (v0.6.6) # -# Build: docker build -t aitokenpool:0.3.3 . -# Run: docker run -p 8080:8080 -v "$PWD/data:/data" \ +# Build: docker build -t aitokenpool:0.6.6 . +# Run: docker run -p 8080:8080 -v "$PWD/atp-data:/data" \ # -e ATP_MASTER_KEY="$(openssl rand -hex 32)" \ -# aitokenpool:0.3.3 +# aitokenpool:0.6.6 # # Notes: -# - runtime WORKDIR is / so the relative paths in config.example.toml work: -# db_path = "data/aitokenpool.db" -> /data/aitokenpool.db (mounted volume) -# ServeDir "ui" -> /ui (copied below) +# - unified data dir (rant 2026-08-19T20:53:23): ATP_DATA_DIR=/data holds +# config.toml + aitokenpool.db + logs/ — mount ONE volume. +# - first start copies /config/config.example.toml → /data/config.toml +# if missing (main.rs ensure_config). # - master_key comes from env ATP_MASTER_KEY (32-byte hex), crypto reads env first. # ---------- builder ---------- @@ -34,12 +35,14 @@ RUN apt-get update \ WORKDIR / COPY --from=builder /build/target/release/aitokenpool /usr/local/bin/aitokenpool COPY ui /ui -COPY config/config.example.toml /etc/aitokenpool/config.toml +# 内置示例配置:首次启动时由 main.rs 复制到 /data/config.toml(若不存在) +COPY config/config.example.toml /config/config.example.toml -RUN mkdir -p /data /etc/aitokenpool \ - && chown -R 10001:10001 /data /etc/aitokenpool +RUN mkdir -p /data /config \ + && chown -R 10001:10001 /data USER 10001 +ENV ATP_DATA_DIR=/data EXPOSE 8080 VOLUME ["/data"] -CMD ["aitokenpool", "--config", "/etc/aitokenpool/config.toml"] +CMD ["aitokenpool", "--data-dir", "/data"] diff --git a/README.md b/README.md index 1d00334..d0d34ed 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke - ✅ **v0.6.3**:**接入方式 URL 配置化(rant 2026-08-19T20:37:37)**——设置页「接入方式」端点不再硬编码域名:新增 `[server].public_url` 配置(缺省 `http://localhost:8080`)+ `GET /api/config` 下发;前端从配置拼接 `{public_url}/v1`、`{public_url}/anthropic`,取不到配置时回退同源 origin - ✅ **v0.6.4**:**管理员模型信息 CRUD(rant 2026-08-19T20:40:29)**——models 表补 context_length / max_output / vision / cache_hit_input_per_m(迁移 v7,幂等)+ seed 从 models.example.json 写入;新增 `GET|POST /api/admin/models` + `PATCH|DELETE /api/admin/models/:id`(admin 权限,唯一冲突 409,删除后按 0 计费);管理视图「模型管理」tab(搜索/新增/编辑/删除,行内表单 + 二次确认,中英 i18n);`GET /api/models` 市场列表补新字段(读图/上下文真实值) - ✅ **v0.6.5**:**全站时区修复(rant 2026-08-19T20:45:32 BUG)**——后端所有返回 JSON 的时间字段统一转 UTC ISO 带 Z(`2026-08-19T12:00:00Z`;交易/共享/API Key/部门/加额/模型列表全量,`utc_iso()` 序列化);前端 `timeAgo()` 按 UTC 解析(兼容旧格式视为 UTC)、仪表盘 sparkline 日期按 UTC 转本地归天、绝对时间 title 本地化显示——消费后交易记录不再显示「8小时前」,跨天不错位 +- ✅ **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/ 迁移说明 `ui/` 已由纯静态原型升级为**对接真实 API**(登录、钱包、市场、共享、交易、设置、管理、运营全部真实数据),由后端 `ServeDir` 静态托管,无需单独部署前端。 @@ -45,12 +46,24 @@ AITokenPool 是一个开源的 **AI Token 共享平台**:企业版(内部 ke ### ① 本地运行(cargo) ```bash -cp config/config.example.toml config/config.toml # 首次 -cargo run # http://localhost:8080/ +cargo run # 首次自动建 ./data/ + 复制配置,http://localhost:8080/ ``` +> **统一数据目录**(rant 2026-08-19T20:53:23):配置 / 数据库 / 日志放在同一个目录,方便 Docker 单卷挂载: +> +> ``` +> / # 默认 ./data;--data-dir 或 env ATP_DATA_DIR 覆盖 +> ├── config.toml # 配置(首次启动自动从 config.example.toml 复制) +> ├── aitokenpool.db # SQLite 数据库 +> └── logs/ # 运行日志 +> ``` +> +> 启动:`cargo run -- --data-dir ./my-data`(或 `ATP_DATA_DIR=./my-data cargo run`)。二次启动复用同目录 +> (config/db 不重建,日志追加)。数据库路径统一由 data-dir 决定(config 里 `db_path` 已忽略)。 +> 旧版(v0.6.6 前)`data/aitokenpool.db` 与 `config/config.toml` 迁移:直接拷贝到新数据目录即可。 + > ⚠️ **首次启动即应配置主密钥**(用于加密上游 key):`export ATP_MASTER_KEY=$(openssl rand -hex 32)` -> (或取消 config/config.toml `[server].master_key` 注释)。未配置时使用随机 dev 密钥, +> (或取消 /config.toml `[server].master_key` 注释)。未配置时使用随机 dev 密钥, > **重启后已上架的 key 密文无法解密 → 全部 503**(rant 2026-08-18T16:14:21 Bug 3)。 > **对外网关地址(`[server].public_url`)**(rant 2026-08-19T20:37:37):设置页「接入方式」展示的 @@ -74,7 +87,10 @@ open http://localhost:8080/ # 浏览器访问 > docker compose up -d > ``` > -> 未设置时使用 dev 默认值,仅适合本地试用(重启后已加密的上游 key 不可解密)。数据持久化在 `./data/`(SQLite)。 +> 未设置时使用 dev 默认值,仅适合本地试用(重启后已加密的上游 key 不可解密)。数据持久化在统一数据目录 +> `./atp-data/`(挂载到容器 `/data`):`config.toml`(首次自动复制)+ `aitokenpool.db` + `logs/` 全在其中, +> 改配置 / 备份 / 迁移只操作这一个目录(rant 2026-08-19T20:53:23)。手动运行: +> `docker run -p 8080:8080 -v "$PWD/atp-data:/data" -e ATP_MASTER_KEY=$(openssl rand -hex 32) ghcr.io/argszero/aitokenpool:latest` ### ③ 登录账号 @@ -111,7 +127,7 @@ open http://localhost:8080/ # 浏览器访问 ## API 端点(Bearer 认证) -- `GET /healthz` → `{"status":"ok","version":"0.6.5"}` +- `GET /healthz` → `{"status":"ok","version":"0.6.6"}` - `POST /api/auth/login` → `{api_key}`;`POST /api/auth/change-password`(改密);`POST /api/auth/register|verify|resend-code`(注册+邮箱验证);`GET /api/me` → `{id,email,name,role}`;`GET /api/config` → `{public_url}`(接入端点 base,rant 2026-08-19T20:37:37) - `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/config/config.example.toml b/config/config.example.toml index 943ef4d..ef90766 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -11,6 +11,8 @@ # ============================================================ [server] addr = "0.0.0.0:8080" +# 数据库路径由统一数据目录决定(rant 2026-08-19T20:53:23):/aitokenpool.db。 +# 此处的 db_path 已忽略(保留仅为兼容旧启动方式)。 db_path = "data/aitokenpool.db" # 平台对外网关地址(不含 /v1 等路径):设置页「接入方式」端点由此拼接({public_url}/v1、{public_url}/anthropic)。 # 缺省 http://localhost:8080(dev 默认,与 addr 解耦——addr 是监听地址,public_url 是对外可达地址)。 diff --git a/docker-compose.yml b/docker-compose.yml index 87fe312..032fed5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# AITokenPool — Docker Compose 一键部署(v0.3.3) +# AITokenPool — Docker Compose 一键部署(v0.6.6) # # 快速开始: # docker compose up -d --build @@ -8,19 +8,23 @@ # 生成:openssl rand -hex 32 # 注入:export ATP_MASTER_KEY=$(openssl rand -hex 32) && docker compose up -d # 未设置时使用示例默认值(dev 模式,重启后已加密的上游 key 不可解密,仅用于本地试用)。 +# +# 统一数据目录(rant 2026-08-19T20:53:23):只挂载一个 /data 卷, +# config.toml(首次启动自动从内置示例复制)+ aitokenpool.db + logs/ 全在 ./atp-data/ 下。 services: aitokenpool: build: . - image: aitokenpool:0.3.3 + image: aitokenpool:0.6.6 ports: - "8080:8080" volumes: - # SQLite 数据持久化(config.example.toml 中 db_path = "data/aitokenpool.db", - # 容器内 WORKDIR=/,相对路径解析为 /data/aitokenpool.db) - - ./data:/data + # 统一数据目录:config / db / logs 全在这里(服务端 --data-dir /data) + - ./atp-data:/data environment: # 主密钥(32 字节 hex;env 优先于 config [server].master_key) - ATP_MASTER_KEY=${ATP_MASTER_KEY:-dev-master-key-请替换} + # 统一数据目录(与 Dockerfile CMD --data-dir /data 一致) + - ATP_DATA_DIR=/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"] interval: 10s diff --git a/src/main.rs b/src/main.rs index 2cd7c80..7a014d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,9 +35,57 @@ use clap::Parser; about = "AI Token 共享池 — 企业 key 池 + 公共共享市场" )] struct Args { - /// 配置文件路径(默认 config/config.toml) - #[arg(long, default_value = "config/config.toml")] - config: String, + /// 统一数据目录(rant 2026-08-19T20:53:23):config.toml + aitokenpool.db + logs/ 都在其下。 + /// 优先级:--data-dir > env ATP_DATA_DIR > 默认 ./data + #[arg(long, env = "ATP_DATA_DIR", default_value = "./data")] + data_dir: String, + /// 配置文件路径(可选;缺省 /config.toml) + #[arg(long)] + config: Option, +} + +/// 解析数据目录(绝对化 + 去除尾部斜杠),并自动创建目录结构 +fn resolve_data_dir(dir: &str) -> anyhow::Result { + let p = std::path::Path::new(dir); + let abs = if p.is_absolute() { + p.to_path_buf() + } else { + std::env::current_dir()?.join(p) + }; + std::fs::create_dir_all(&abs)?; + std::fs::create_dir_all(abs.join("logs"))?; + Ok(abs) +} + +/// 配置路径:--config 显式指定 → 用之(不自动复制);否则 /config.toml(不存在则首次复制) +fn config_path( + data_dir: &std::path::Path, + explicit: Option<&str>, +) -> anyhow::Result { + match explicit { + Some(p) => Ok(std::path::PathBuf::from(p)), + None => ensure_config(data_dir), + } +} + +/// 首次启动:/config.toml 不存在 → 从仓库内置示例复制(rant 2026-08-19T20:53:23) +fn ensure_config(data_dir: &std::path::Path) -> anyhow::Result { + let target = data_dir.join("config.toml"); + if target.exists() { + return Ok(target); + } + let example = "config/config.example.toml"; + if !std::path::Path::new(example).exists() { + // 运行目录没有示例(如已安装的二进制)→ 生成最小可用配置 + log::warn!("示例配置 {example} 不存在,生成最小配置 {target:?}"); + let min = "[server]\naddr = \"0.0.0.0:8080\"\ndb_path = \"aitokenpool.db\"\n"; + std::fs::write(&target, min)?; + return Ok(target); + } + std::fs::copy(example, &target) + .map_err(|e| anyhow::anyhow!("复制示例配置到 {target:?} 失败: {e}"))?; + log::info!("首次启动:已从 {example} 复制配置到 {target:?}"); + Ok(target) } #[tokio::main] @@ -45,15 +93,27 @@ async fn main() -> anyhow::Result<()> { env_logger::init(); let args = Args::parse(); - let cfg = match config::Config::load(&args.config) { + // 统一数据目录(rant 2026-08-19T20:53:23):config/db/logs 同目录,方便 Docker 单 volume 挂载 + let data_dir = resolve_data_dir(&args.data_dir)?; + let cfg_path = config_path(&data_dir, args.config.as_deref())?; + + let mut cfg = match config::Config::load(cfg_path.to_str().unwrap_or("config.toml")) { Ok(c) => c, Err(e) => { eprintln!("加载配置失败: {e}"); - eprintln!("提示: 请先复制示例配置:"); - eprintln!(" cp config/config.example.toml config/config.toml"); + eprintln!("提示: 请先复制示例配置到数据目录:"); + eprintln!( + " cp config/config.example.toml {}/config.toml", + data_dir.display() + ); std::process::exit(1); } }; + // 数据库路径统一由 data-dir 决定(配置里 db_path 忽略;config.example 的 data/ 前缀也失效) + cfg.server.db_path = data_dir + .join("aitokenpool.db") + .to_string_lossy() + .into_owned(); let addr = cfg.server.addr.clone(); let db_path = cfg.server.db_path.clone();