Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.github/
target/
data/
atp-data/
config/config.toml
.emrg/
docs/
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aitokenpool"
version = "0.6.5"
version = "0.6.6"
edition = "2021"
description = "AI Token 共享池 — 企业 key 池 + 公共共享市场"
license = "MIT"
Expand Down Expand Up @@ -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"] }
25 changes: 14 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 ----------
Expand All @@ -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"]
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 静态托管,无需单独部署前端。

Expand All @@ -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 单卷挂载:
>
> ```
> <ATP_DATA_DIR>/ # 默认 ./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 密钥,
> (或取消 <data-dir>/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):设置页「接入方式」展示的
Expand All @@ -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`

### ③ 登录账号

Expand Down Expand Up @@ -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 兼容模型列表,认证可选)
Expand Down
2 changes: 2 additions & 0 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# ============================================================
[server]
addr = "0.0.0.0:8080"
# 数据库路径由统一数据目录决定(rant 2026-08-19T20:53:23):<ATP_DATA_DIR>/aitokenpool.db。
# 此处的 db_path 已忽略(保留仅为兼容旧启动方式)。
db_path = "data/aitokenpool.db"
# 平台对外网关地址(不含 /v1 等路径):设置页「接入方式」端点由此拼接({public_url}/v1、{public_url}/anthropic)。
# 缺省 http://localhost:8080(dev 默认,与 addr 解耦——addr 是监听地址,public_url 是对外可达地址)。
Expand Down
14 changes: 9 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AITokenPool — Docker Compose 一键部署(v0.3.3)
# AITokenPool — Docker Compose 一键部署(v0.6.6)
#
# 快速开始:
# docker compose up -d --build
Expand All @@ -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
Expand Down
72 changes: 66 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,85 @@ 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,
/// 配置文件路径(可选;缺省 <data-dir>/config.toml)
#[arg(long)]
config: Option<String>,
}

/// 解析数据目录(绝对化 + 去除尾部斜杠),并自动创建目录结构
fn resolve_data_dir(dir: &str) -> anyhow::Result<std::path::PathBuf> {
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 显式指定 → 用之(不自动复制);否则 <data-dir>/config.toml(不存在则首次复制)
fn config_path(
data_dir: &std::path::Path,
explicit: Option<&str>,
) -> anyhow::Result<std::path::PathBuf> {
match explicit {
Some(p) => Ok(std::path::PathBuf::from(p)),
None => ensure_config(data_dir),
}
}

/// 首次启动:<data-dir>/config.toml 不存在 → 从仓库内置示例复制(rant 2026-08-19T20:53:23)
fn ensure_config(data_dir: &std::path::Path) -> anyhow::Result<std::path::PathBuf> {
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]
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();
Expand Down
Loading