diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..e0a7daff3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,64 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ backend:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt,clippy
+
+ - name: Cache Rust
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: backend -> target
+
+ - name: Rust format
+ run: cargo fmt --manifest-path backend/Cargo.toml --check
+
+ - name: Rust clippy
+ run: cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
+
+ - name: Rust tests
+ run: cargo test --manifest-path backend/Cargo.toml --test main --locked
+
+ frontend:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v6
+ with:
+ version: 11.7.0
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: frontend/pnpm-lock.yaml
+
+ - name: Install frontend dependencies
+ run: pnpm --dir frontend install --frozen-lockfile
+
+ - name: Frontend build
+ run: pnpm --dir frontend build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f9877dd53..9dc3834c7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,12 @@ on:
push:
tags:
- 'v*'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to release, for example v0.1.0'
+ required: true
+ type: string
permissions:
contents: write
@@ -19,88 +25,97 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
+ with:
+ ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
- name: Derive version
id: version
shell: bash
- run: |
- VERSION="${GITHUB_REF_NAME#v}"
- BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "build_time=${BUILD_TIME}" >> "$GITHUB_OUTPUT"
- echo "git_sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
-
- - name: Write VERSION file
- shell: bash
- run: printf '%s\n' '${{ steps.version.outputs.version }}' > backend/build/VERSION
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 24
-
- - name: Enable pnpm
- run: |
- corepack enable
- corepack prepare pnpm@11.7.0 --activate
-
- - name: Install frontend dependencies
- run: pnpm --dir frontend install --frozen-lockfile
-
- - name: Build frontend
- run: pnpm --dir frontend build
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
-
- - name: Cache Rust
- uses: Swatinem/rust-cache@v2
- with:
- workspaces: backend -> target
-
- - name: Build release binaries
env:
- CPR_VERSION: ${{ steps.version.outputs.version }}
- CPR_GIT_SHA: ${{ steps.version.outputs.git_sha }}
- CPR_BUILD_TIME: ${{ steps.version.outputs.build_time }}
- CPR_BUILD_TYPE: release
- run: cargo build --manifest-path backend/Cargo.toml --release --locked --bin codex-proxy-rs
+ RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
+ run: |
+ VERSION="${RELEASE_TAG#v}"
+ BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ GIT_SHA="$(git rev-parse HEAD)"
+ {
+ echo "tag=${RELEASE_TAG}"
+ echo "version=${VERSION}"
+ echo "build_time=${BUILD_TIME}"
+ echo "git_sha=${GIT_SHA}"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Verify Cargo package version
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.version }}
+ run: |
+ PACKAGE_VERSION="$(python3 - <<'PY'
+ import pathlib
+ import tomllib
+
+ manifest = tomllib.loads(pathlib.Path("backend/Cargo.toml").read_text())
+ print(manifest["package"]["version"])
+ PY
+ )"
+
+ if [ "${PACKAGE_VERSION}" != "${RELEASE_VERSION}" ]; then
+ echo "Tag version ${RELEASE_VERSION} does not match backend/Cargo.toml package version ${PACKAGE_VERSION}" >&2
+ exit 1
+ fi
+
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Setup Docker Buildx
+ uses: docker/setup-buildx-action@v4
- name: Package release assets
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
- PACKAGE_DIR="dist-package/codex-proxy-rs"
- mkdir -p "${PACKAGE_DIR}/web"
- cp backend/target/release/codex-proxy-rs "${PACKAGE_DIR}/"
- cp -r frontend/dist "${PACKAGE_DIR}/web/dist"
- tar -czf "codex-proxy-rs_${VERSION}_linux_x86_64.tar.gz" -C dist-package codex-proxy-rs
- sha256sum "codex-proxy-rs_${VERSION}_linux_x86_64.tar.gz" > checksums.txt
+ rm -rf dist-package checksums.txt
- - name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
+ docker buildx build \
+ --file deploy/Dockerfile \
+ --target release-asset \
+ --platform linux/amd64,linux/arm64 \
+ --output type=local,dest=dist-package \
+ --cache-from type=gha,scope=release \
+ --cache-to type=gha,mode=max,scope=release \
+ --build-arg CPR_VERSION="${VERSION}" \
+ --build-arg CPR_GIT_SHA="${{ steps.version.outputs.git_sha }}" \
+ --build-arg CPR_BUILD_TIME="${{ steps.version.outputs.build_time }}" \
+ --build-arg CPR_BUILD_TYPE=release \
+ .
+
+ find dist-package -type f -name 'codex-proxy-rs_*.tar.gz' -exec cp {} . \;
+ test -f "codex-proxy-rs_${VERSION}_linux_amd64.tar.gz"
+ test -f "codex-proxy-rs_${VERSION}_linux_arm64.tar.gz"
+ sha256sum "codex-proxy-rs_${VERSION}_linux_amd64.tar.gz" \
+ "codex-proxy-rs_${VERSION}_linux_arm64.tar.gz" > checksums.txt
- name: Login to GHCR
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push app image
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: .
file: deploy/Dockerfile
target: runtime
- platforms: linux/amd64
+ platforms: linux/amd64,linux/arm64
push: true
build-args: |
CPR_VERSION=${{ steps.version.outputs.version }}
CPR_GIT_SHA=${{ steps.version.outputs.git_sha }}
CPR_BUILD_TIME=${{ steps.version.outputs.build_time }}
CPR_BUILD_TYPE=release
+ cache-from: type=gha,scope=release
+ cache-to: type=gha,mode=max,scope=release
labels: |
org.opencontainers.image.title=Codex Proxy RS
org.opencontainers.image.version=${{ steps.version.outputs.version }}
@@ -109,12 +124,30 @@ jobs:
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.git_sha }}
- name: Publish GitHub Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
+ tag_name: ${{ steps.version.outputs.tag }}
generate_release_notes: true
+ body: |
+ ## Installation
+
+ **Docker:**
+ ```bash
+ # GitHub Container Registry
+ docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
+ ```
+
+ **Manual download:**
+ Download the appropriate archive for your platform from the assets below.
+
+ ## Documentation
+
+ - [GitHub Repository](https://github.com/${{ github.repository }})
+ - [Installation Guide](https://github.com/${{ github.repository }}/blob/main/deploy/README.md)
files: |
- codex-proxy-rs_${{ steps.version.outputs.version }}_linux_x86_64.tar.gz
+ codex-proxy-rs_${{ steps.version.outputs.version }}_linux_amd64.tar.gz
+ codex-proxy-rs_${{ steps.version.outputs.version }}_linux_arm64.tar.gz
checksums.txt
diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml
new file mode 100644
index 000000000..5286c55ef
--- /dev/null
+++ b/.github/workflows/security-scan.yml
@@ -0,0 +1,64 @@
+name: Security Scan
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ schedule:
+ - cron: '0 3 * * 1'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: security-scan-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ backend-security:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Rust
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: backend -> target
+
+ - name: Install cargo-audit
+ run: cargo install cargo-audit --locked
+
+ - name: Run cargo audit
+ working-directory: backend
+ run: cargo audit
+
+ frontend-security:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v6
+ with:
+ version: 11.7.0
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: frontend/pnpm-lock.yaml
+
+ - name: Install frontend dependencies
+ run: pnpm --dir frontend install --frozen-lockfile
+
+ - name: Run pnpm audit
+ run: pnpm --dir frontend audit --prod --audit-level=high
diff --git a/README.md b/README.md
index 4435165d4..2df18d823 100644
--- a/README.md
+++ b/README.md
@@ -103,24 +103,7 @@ logging:
## 架构
-```mermaid
-flowchart LR
- Client["客户端 / SDK"] -->|OpenAI API| Proxy["/v1/*"]
- Browser["浏览器"] -->|Web| Admin["/api/admin/*"]
-
- subgraph Gateway["codex-proxy-rs"]
- Proxy --> Auth["API Key 鉴权"]
- Auth --> Dispatch["调度层
选账号 · 限并发 · 亲和性"]
- Dispatch --> Transport["上游传输
HTTP SSE / WebSocket"]
- Dispatch --> Pool["账号池
内存状态 + SQLite"]
- Admin --> SQLite[("SQLite")]
- Pool --> SQLite
- end
-
- Transport --> Upstream["ChatGPT / Codex API"]
- Proxy -.->|静态资源| SPA["Vue 3 管理端"]
- Admin -.-> SPA
-```
+Codex Proxy RS 是单主服务架构:Rust 后端同时承载 OpenAI 兼容代理、管理端 API、SQLite 持久化、前端静态资源托管和在线更新。详细模块边界、请求链路、发布和更新流程见 [架构说明](docs/architecture.md)。
## 项目结构
@@ -131,7 +114,7 @@ backend/
build/ 构建脚本
frontend/ Vue 3 管理控制台
deploy/ Dockerfile、Compose、示例配置
-docs/ 设计与迁移文档
+docs/ 架构与维护文档
```
## 开发
diff --git a/backend/build/VERSION b/backend/build/VERSION
deleted file mode 100644
index 6e8bf73aa..000000000
--- a/backend/build/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.1.0
diff --git a/backend/build/build.rs b/backend/build/build.rs
index 3045e44d7..415071378 100644
--- a/backend/build/build.rs
+++ b/backend/build/build.rs
@@ -1,19 +1,14 @@
-use std::{env, fs};
-
-const VERSION_FILE: &str = "build/VERSION";
+use std::env;
fn main() {
- println!("cargo:rerun-if-changed={VERSION_FILE}");
println!("cargo:rerun-if-env-changed=CPR_VERSION");
println!("cargo:rerun-if-env-changed=CPR_GIT_SHA");
println!("cargo:rerun-if-env-changed=CPR_BUILD_TIME");
println!("cargo:rerun-if-env-changed=CPR_BUILD_TYPE");
- let version = version_env_value("CPR_VERSION")
- .or_else(version_file)
- .unwrap_or_else(|| {
- env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0-dev".to_string())
- });
+ let version = version_env_value("CPR_VERSION").unwrap_or_else(|| {
+ env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0-dev".to_string())
+ });
let git_sha = env_value("CPR_GIT_SHA").unwrap_or_else(|| "unknown".to_string());
let build_time = env_value("CPR_BUILD_TIME").unwrap_or_else(|| "unknown".to_string());
let build_type = env_value("CPR_BUILD_TYPE").unwrap_or_else(|| "source".to_string());
@@ -34,10 +29,3 @@ fn env_value(key: &str) -> Option {
fn version_env_value(key: &str) -> Option {
env_value(key).map(|value| value.trim_start_matches('v').to_string())
}
-
-fn version_file() -> Option {
- fs::read_to_string(VERSION_FILE)
- .ok()
- .map(|value| value.trim().trim_start_matches('v').to_string())
- .filter(|value| !value.is_empty())
-}
diff --git a/backend/src/admin/monitoring/dashboard.rs b/backend/src/admin/monitoring/dashboard.rs
index c8ea01a03..82bb63096 100644
--- a/backend/src/admin/monitoring/dashboard.rs
+++ b/backend/src/admin/monitoring/dashboard.rs
@@ -614,65 +614,6 @@ fn health_tone(bucket: UsageWindow, success_rate: f64, is_future: bool) -> char
}
}
-#[cfg(test)]
-mod tests {
- use super::{health_tone, UsageWindow};
-
- #[test]
- fn health_tone_should_classify_availability_levels() {
- assert_eq!(health_tone(UsageWindow::default(), 0.0, true), '0');
- assert_eq!(health_tone(UsageWindow::default(), 0.0, false), '1');
- assert_eq!(
- health_tone(
- UsageWindow {
- requests: 2,
- errors: 2,
- ..UsageWindow::default()
- },
- 0.0,
- false,
- ),
- '2'
- );
- assert_eq!(
- health_tone(
- UsageWindow {
- requests: 10,
- errors: 2,
- ..UsageWindow::default()
- },
- 80.0,
- false,
- ),
- '3'
- );
- assert_eq!(
- health_tone(
- UsageWindow {
- requests: 2,
- errors: 0,
- ..UsageWindow::default()
- },
- 100.0,
- false,
- ),
- '4'
- );
- assert_eq!(
- health_tone(
- UsageWindow {
- requests: 3,
- errors: 0,
- ..UsageWindow::default()
- },
- 100.0,
- false,
- ),
- '5'
- );
- }
-}
-
fn trend_summary(
kind: DashboardTrendKind,
points: &[DashboardTrendPointData],
diff --git a/backend/src/admin/system/routes.rs b/backend/src/admin/system/routes.rs
index 31d2c84d6..79e6e5d31 100644
--- a/backend/src/admin/system/routes.rs
+++ b/backend/src/admin/system/routes.rs
@@ -815,8 +815,8 @@ fn select_release_archive<'a>(
release: &'a GitHubRelease,
version: &str,
) -> Result<&'a GitHubAsset, AdminError> {
- let os = env::consts::OS;
- let arch = env::consts::ARCH;
+ let os_aliases = platform_os_aliases();
+ let arch_aliases = platform_arch_aliases();
let normalized = normalize_version_tag(version);
release
.assets
@@ -825,23 +825,47 @@ fn select_release_archive<'a>(
let name = asset.name.as_str();
name.contains(APP_BINARY_NAME)
&& name.contains(&normalized)
- && name.contains(os)
- && name.contains(arch)
+ && asset_matches_current_platform(name, os_aliases, arch_aliases)
&& !name.ends_with(".txt")
})
.or_else(|| {
release.assets.iter().find(|asset| {
let name = asset.name.as_str();
- name.contains(os) && name.contains(arch) && !name.ends_with(".txt")
+ asset_matches_current_platform(name, os_aliases, arch_aliases)
+ && !name.ends_with(".txt")
})
})
.ok_or_else(|| {
AdminError::conflict(format!(
- "No compatible release archive found for {os}/{arch}"
+ "No compatible release archive found for {}/{}",
+ env::consts::OS,
+ env::consts::ARCH
))
})
}
+fn platform_os_aliases() -> &'static [&'static str] {
+ match env::consts::OS {
+ "macos" => &["macos", "darwin"],
+ "windows" => &["windows", "win32"],
+ "linux" => &["linux"],
+ _ => &[env::consts::OS],
+ }
+}
+
+fn platform_arch_aliases() -> &'static [&'static str] {
+ match env::consts::ARCH {
+ "x86_64" => &["x86_64", "amd64"],
+ "aarch64" => &["aarch64", "arm64"],
+ _ => &[env::consts::ARCH],
+ }
+}
+
+fn asset_matches_current_platform(name: &str, os_aliases: &[&str], arch_aliases: &[&str]) -> bool {
+ os_aliases.iter().any(|os| name.contains(os))
+ && arch_aliases.iter().any(|arch| name.contains(arch))
+}
+
async fn download_file(
url: &str,
dest: &Path,
diff --git a/backend/tests/admin/system/mod.rs b/backend/tests/admin/system/mod.rs
index 1b153badf..b11f3422c 100644
--- a/backend/tests/admin/system/mod.rs
+++ b/backend/tests/admin/system/mod.rs
@@ -364,7 +364,7 @@ async fn mount_latest_release_with_archive(
let archive_name = format!(
"codex-proxy-rs_{version}_{}_{}.tar.gz",
std::env::consts::OS,
- std::env::consts::ARCH
+ release_arch()
);
let archive = release_archive(binary, index);
let checksum = format!(
@@ -406,6 +406,14 @@ async fn mount_latest_release_with_archive(
.await;
}
+fn release_arch() -> &'static str {
+ match std::env::consts::ARCH {
+ "x86_64" => "amd64",
+ "aarch64" => "arm64",
+ arch => arch,
+ }
+}
+
fn release_archive(binary: &str, index: &str) -> Vec {
let mut data = Vec::new();
{
diff --git a/deploy/Dockerfile b/deploy/Dockerfile
index cc0c60465..7167fc919 100644
--- a/deploy/Dockerfile
+++ b/deploy/Dockerfile
@@ -11,10 +11,12 @@ RUN pnpm build
FROM rust:1.95-bookworm AS backend-builder
WORKDIR /app/backend
+ARG TARGETARCH
ARG CPR_VERSION=
ARG CPR_GIT_SHA=unknown
ARG CPR_BUILD_TIME=unknown
ARG CPR_BUILD_TYPE=release
+ENV CARGO_TARGET_DIR=/app/backend/target/${TARGETARCH}
ENV CPR_VERSION=${CPR_VERSION}
ENV CPR_GIT_SHA=${CPR_GIT_SHA}
ENV CPR_BUILD_TIME=${CPR_BUILD_TIME}
@@ -27,7 +29,23 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/app/backend/target \
cargo build --release --locked --bin codex-proxy-rs \
- && cp /app/backend/target/release/codex-proxy-rs /tmp/codex-proxy-rs
+ && cp "${CARGO_TARGET_DIR}/release/codex-proxy-rs" /tmp/codex-proxy-rs
+
+FROM backend-builder AS release-asset-builder
+
+ARG TARGETOS
+ARG TARGETARCH
+
+WORKDIR /out
+RUN mkdir -p /tmp/release/codex-proxy-rs/web \
+ && cp /tmp/codex-proxy-rs /tmp/release/codex-proxy-rs/ \
+ && cp -r /app/backend/web/dist /tmp/release/codex-proxy-rs/web/dist \
+ && tar -czf "/out/codex-proxy-rs_${CPR_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz" \
+ -C /tmp/release codex-proxy-rs
+
+FROM scratch AS release-asset
+
+COPY --from=release-asset-builder /out /
FROM debian:bookworm-slim AS runtime
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 000000000..d8f592b52
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,170 @@
+# Codex Proxy RS 架构说明
+
+## 总览
+
+Codex Proxy RS 是一个单主服务应用:Rust 后端提供 OpenAI 兼容代理、管理端 API、SQLite 持久化、静态前端托管和在线更新能力;Vue 管理端作为 SPA 由同一个后端进程托管。
+
+```mermaid
+flowchart LR
+ Client["客户端 / SDK"] -->|"OpenAI API /v1/*"| Http["Axum HTTP Router"]
+ Browser["浏览器"] -->|"管理端 SPA"| Web["web 静态资源托管"]
+ Browser -->|"/api/admin/*"| Http
+
+ subgraph App["codex-proxy-rs 单主进程"]
+ Http --> Proxy["proxy
API Key 鉴权
OpenAI 兼容协议"]
+ Http --> Admin["admin
账号 / Key / 设置 / 监控 / 系统更新"]
+ Http --> Web
+
+ Proxy --> Dispatch["dispatch
账号选择 / 限流 / 亲和性 / 失败回退"]
+ Dispatch --> Protocol["upstream protocol
请求转换 / SSE 转换"]
+ Protocol --> Transport["upstream transport
HTTP SSE / WebSocket"]
+
+ Admin --> Runtime["runtime services
账号池 / 后台任务 / 更新状态"]
+ Dispatch --> Runtime
+ Runtime --> Store[("SQLite
账号 / 会话 / 使用记录 / 设置")]
+ Admin --> Store
+ end
+
+ Transport --> Upstream["ChatGPT / Codex Upstream"]
+ Web --> Frontend["frontend/dist
Vue 3 管理端"]
+
+ Release["GitHub Release
tar.gz + checksums"] --> Update["在线更新
下载 / 校验 / 替换 / 重启"]
+ Update --> Admin
+ GHCR["GHCR
linux/amd64 + linux/arm64"] --> Docker["Docker Compose
restart: unless-stopped"]
+```
+
+## 仓库边界
+
+- `backend/`:Rust/Axum 后端、SQLite schema、运行时服务、集成测试和 Cargo 构建脚本。
+- `frontend/`:Vue 3 管理端,使用 Vite、Tailwind v4、Pinia、Vue Router 和 ECharts。
+- `deploy/`:Dockerfile、Compose 文件、Docker ignore 文件和部署样例配置。
+- `docs/`:长期维护文档,只记录当前真实架构和约定,不保留迁移过程作为开发约束。
+- `skills/`:项目本地 Codex skill。
+
+## 后端结构
+
+后端入口是 `backend/src/main.rs`,公共库入口是 `backend/src/lib.rs`。运行时由 `runtime::bootstrap` 初始化,服务集合在 `runtime::services` 构造,并通过 `AppState` 注入到路由。
+
+主要模块:
+
+- `admin/`:管理端 API,包括认证、账号、API Key、监控、设置和系统更新。
+- `proxy/`:OpenAI 兼容代理入口、鉴权、请求转换和账号调度。
+- `upstream/`:ChatGPT/Codex 上游协议、传输层、账号模型、token 刷新、quota 刷新和 fingerprint。
+- `config/`:配置加载、运行时设置和配置类型。
+- `infra/`:数据库、日志、时间、格式化、JSON 和 schema。
+- `http/`:HTTP router、中间件和统一请求上下文。
+- `web/`:前端静态资源和 SPA fallback 托管。
+
+测试只放在 `backend/tests/`,不要在 `backend/src/` 中新增测试模块。
+
+## 数据和运行时
+
+后端以 SQLite 作为主要持久化存储。默认运行时数据位于 `.runtime/`,Docker 部署时通过 Compose 挂载到 `/app/data` 和 `/app/logs`。
+
+运行时状态分两类:
+
+- 持久化状态:账号、cookie、API Key、session、使用记录、时间桶、fingerprint、运行时设置等。
+- 内存状态:账号池、调度状态、运行中任务、SSE 更新日志广播、缓存的 release 检查结果等。
+
+运行时设置通过管理端 API 修改后写入数据库,并同步到内存服务。
+
+## 代理请求链路
+
+客户端请求进入 `/v1/*`:
+
+1. `proxy/auth` 校验客户端 API Key。
+2. `proxy/openai` 解析 OpenAI 兼容请求。
+3. `proxy/dispatch` 选择账号、处理限流、亲和性和失败回退。
+4. `upstream/protocol` 转换 Codex/ChatGPT 上游协议。
+5. `upstream/transport` 使用 HTTP SSE 或 WebSocket 访问上游。
+6. 响应被转换回 OpenAI 兼容格式,并记录使用数据。
+
+账号运行时状态由 SQLite 快照恢复,并在请求、刷新、quota 检查和失败回退时更新。
+
+## 管理端
+
+管理端 API 位于 `/api/admin/*`,使用 session cookie 保护。前端入口由后端托管,不单独部署 Node 服务。
+
+前端结构:
+
+- `frontend/src/api/modules`:管理端 API 客户端。
+- `frontend/src/components/base`:基础 UI 组件。
+- `frontend/src/layout`:主布局、侧边栏、系统更新入口和更新弹窗。
+- `frontend/src/views`:各业务页面。
+- `frontend/src/stores`:Pinia 状态。
+- `frontend/src/styles/tokens.css`:亮色/暗色主题 token。
+
+前端只展示后端返回的版本、状态和更新日志,不在前端做状态文案映射。
+
+## 版本和发布
+
+版本只有两个来源层级:
+
+- 源码默认版本:`backend/Cargo.toml` 的 `[package].version`。
+- 发布版本:Git tag 去掉 `v` 前缀后得到的版本。
+
+发布 workflow 会校验 tag 版本必须等于 `backend/Cargo.toml` 的 package version。发布构建时通过 `CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME` 和 `CPR_BUILD_TYPE=release` 注入编译期元数据。
+
+`backend/build/build.rs` 负责把这些值写入二进制。没有 `CPR_VERSION` 时,版本回落到 Cargo 的 `CARGO_PKG_VERSION`。不要重新引入独立 `VERSION` 文件。
+
+GitHub Release 产物:
+
+```text
+codex-proxy-rs__linux_amd64.tar.gz
+codex-proxy-rs__linux_arm64.tar.gz
+checksums.txt
+```
+
+GHCR 镜像发布为 `linux/amd64` 和 `linux/arm64` 多平台 manifest。
+
+## 在线更新
+
+在线更新由主服务自己处理,不依赖独立 updater sidecar。核心接口:
+
+- `GET /api/admin/system/version`
+- `GET /api/admin/system/check-updates`
+- `GET /api/admin/system/update-events`
+- `GET /api/admin/system/update-status`
+- `POST /api/admin/system/update`
+- `POST /api/admin/system/restart`
+
+更新流程:
+
+1. 后端检查 GitHub latest release。
+2. 按当前 OS/arch 选择匹配 asset,兼容 `amd64/x86_64` 和 `arm64/aarch64` 命名。
+3. 下载 release tar 包并用 `checksums.txt` 校验。
+4. 解压二进制和 `web/dist`。
+5. 替换当前二进制和前端静态资源。
+6. 前端通过 SSE 实时展示后端推送的中文更新日志。
+7. Docker 部署依靠 `restart: unless-stopped` 在自重启后拉起新进程。
+
+替换逻辑必须支持跨文件系统 fallback,因为容器文件系统里 `rename` 可能遇到 `Invalid cross-device link`。
+
+## Docker 部署
+
+Docker 构建入口是 `deploy/Dockerfile`,上下文保持仓库根目录。
+
+构建阶段:
+
+- `frontend-builder`:构建 Vue SPA。
+- `backend-builder`:构建 Rust release 二进制,并复制前端 dist。
+- `release-asset`:导出 GitHub Release tar 包。
+- `runtime`:最小运行镜像,基于 `debian:bookworm-slim`,只包含 CA、后端二进制和 `web/dist`。
+
+Compose 文件是 `deploy/docker-compose.yml`。示例配置只保留 `deploy/config.example.yaml`,用户复制为 `deploy/config.yaml` 后部署。
+
+## CI 和安全扫描
+
+常规 CI 位于 `.github/workflows/ci.yml`:
+
+- Rust fmt、clippy、集成测试。
+- 前端依赖安装和 build。
+- Compose 配置校验。
+
+安全扫描位于 `.github/workflows/security-scan.yml`:
+
+- `cargo audit` 扫描 Rust 依赖。
+- `pnpm audit --prod --audit-level=high` 扫描前端生产依赖。
+- 每周定时运行,同时覆盖 push 和 pull request。
+
+发布流程位于 `.github/workflows/release.yml`,只由 `v*` tag 或手动指定 tag 触发。
diff --git a/docs/project-layout-restructure.md b/docs/project-layout-restructure.md
deleted file mode 100644
index 46a9f8d43..000000000
--- a/docs/project-layout-restructure.md
+++ /dev/null
@@ -1,237 +0,0 @@
-# Project Layout Restructure
-
-## 背景
-
-项目目录已按 Sub2API 的边界收敛:
-
-- `backend/` 承载后端应用、后端构建元信息和后端测试。
-- `frontend/` 承载前端应用。
-- `deploy/` 承载 Docker、Compose、部署脚本和部署示例配置。
-
-根目录只保留仓库级入口和文档,不再承载后端源码、前端源码或具体部署实现。
-
-## 完成后结构
-
-```text
-codex-proxy-rs/
-├── backend/
-│ ├── Cargo.toml
-│ ├── Cargo.lock
-│ ├── build/
-│ │ ├── build.rs
-│ │ └── VERSION
-│ ├── rust-toolchain.toml
-│ ├── src/
-│ └── tests/
-├── frontend/
-│ ├── package.json
-│ ├── pnpm-lock.yaml
-│ ├── pnpm-workspace.yaml
-│ ├── vite.config.ts
-│ └── src/
-├── deploy/
-│ ├── Dockerfile
-│ ├── docker-compose.yml
-│ ├── config.example.yaml
-│ └── README.md
-├── docs/
-├── README.md
-└── .github/
-```
-
-说明:
-
-- `backend/build/VERSION` 是唯一的源码默认版本文件。它对应 Sub2API 的 `backend/cmd/server/VERSION`,但本项目的 Rust 入口仍保留在 `backend/src/main.rs`,所以版本元信息归入 `backend/build/`。
-- `backend/build/build.rs` 读取 `CPR_VERSION`,否则读取 `backend/build/VERSION`,最后才回落到 `CARGO_PKG_VERSION`。
-- `frontend/` 替代原 `web/`。前端只负责展示后端返回的版本、构建类型、部署类型和更新进度。
-- `deploy/` 替代根目录里的部署文件归属。根目录只保留仓库级入口和文档,不承载具体部署实现。
-- 测试代码继续放在测试目录,禁止放入 `backend/src`。
-
-## 路径映射
-
-| 原路径 | 新路径 | 备注 |
-| --- | --- | --- |
-| `src/` | `backend/src/` | Rust 后端源码。 |
-| `tests/` | `backend/tests/` | Rust 集成测试,仍在测试目录,不进入源码目录。 |
-| `Cargo.toml` | `backend/Cargo.toml` | 后端 package manifest。 |
-| `Cargo.lock` | `backend/Cargo.lock` | 后端锁文件,随后端移动。 |
-| `build.rs` | `backend/build/build.rs` | 后端构建脚本。 |
-| `VERSION` | `backend/build/VERSION` | 后端应用版本源。 |
-| `rust-toolchain.toml` | `backend/rust-toolchain.toml` | 后端工具链配置。 |
-| `web/` | `frontend/` | 前端应用。 |
-| `Dockerfile` | `deploy/Dockerfile` | 生产镜像构建入口。 |
-| `docker-compose.yml` | `deploy/docker-compose.yml` | Docker 部署入口。 |
-| `config.yaml` | `deploy/config.example.yaml` | 根目录不保留本地运行配置。 |
-
-## Cargo 组织方式
-
-优先采用 Sub2API 风格:后端是自包含子项目。
-
-```text
-backend/
-├── Cargo.toml
-├── Cargo.lock
-├── build/
-│ ├── build.rs
-│ └── VERSION
-├── src/
-└── tests/
-```
-
-后续命令从:
-
-```bash
-cargo test --test main admin::system --locked
-cargo clippy --all-targets --all-features --locked -- -D warnings
-```
-
-调整为:
-
-```bash
-cargo test --manifest-path backend/Cargo.toml --test main admin::system --locked
-cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
-```
-
-如果后续需要根目录统一调度,可以再加一个很薄的 root workspace `Cargo.toml`,但不要让根目录重新成为后端版本源。
-
-根目录 `target/` 不是项目构建目标目录。迁移后后端构建产物位于 `backend/target/`;如果本地编辑器或 rust-analyzer 在根目录生成 `target/flycheck*`,它只是本地检查临时产物,可以直接删除,且已被 `.gitignore` 忽略。
-
-## 前端迁移点
-
-`web/` 已迁移到 `frontend/`,同步点:
-
-- `.github/workflows/release.yml` 中所有 `pnpm --dir web ...` 改为 `pnpm --dir frontend ...`。
-- Dockerfile 中所有 `web/package.json`、`web/pnpm-lock.yaml`、`web/dist` 改为 `frontend/...`。
-- README、部署文档和开发命令里的 `web` 改为 `frontend`。
-- 前端 API 类型和更新弹窗不做业务重写,只做路径迁移。
-
-## 部署迁移点
-
-`deploy/` 负责部署相关文件:
-
-- `deploy/Dockerfile`
-- `deploy/docker-compose.yml`
-- `deploy/config.example.yaml`
-- `deploy/README.md`
-
-Docker 构建上下文仍建议使用仓库根目录:
-
-```bash
-docker build -f deploy/Dockerfile -t codex-proxy-rs .
-```
-
-这样 Dockerfile 可以同时访问 `backend/` 和 `frontend/`,避免把构建上下文拆散。
-
-Compose 文件迁移到 `deploy/` 后,默认挂载路径随 Compose 文件落在 `deploy/` 目录,保持部署文件和部署状态集中;需要接入外部配置或数据目录时,通过环境变量覆盖:
-
-```yaml
-services:
- codex-proxy-rs:
- volumes:
- - ${CPR_CONFIG_FILE:-./config.yaml}:/app/config.yaml:ro
- - ${CPR_DATA_DIR:-./data}:/app/data
- - ${CPR_LOG_DIR:-./logs}:/app/logs
-```
-
-使用 `deploy/config.example.yaml` 作为样例,运行时配置由用户复制为 `deploy/config.yaml` 或通过 `CPR_CONFIG_FILE` 挂载提供。
-
-## 发布链路调整
-
-发布版本必须只有一个来源:
-
-1. Git tag,例如 `v0.1.1`。
-2. CI 去掉前缀 `v`,得到 `0.1.1`。
-3. CI 写入 `backend/build/VERSION`。
-4. CI 构建后端时传入:
- - `CPR_VERSION=0.1.1`
- - `CPR_GIT_SHA=`
- - `CPR_BUILD_TIME=`
- - `CPR_BUILD_TYPE=release`
-5. `backend/build/build.rs` 将这些值注入二进制。
-6. 后端 `/api/admin/system/version` 和 `/api/admin/system/check-updates` 使用二进制内的版本值。
-7. 前端只展示后端返回的数据,不做版本映射。
-
-需要同步修改:
-
-- `.github/workflows/release.yml`
-- `deploy/Dockerfile`
-- `backend/build/build.rs`
-- `backend/src/admin/system/routes.rs`
-- `backend/tests/admin/system/mod.rs`
-
-## Dockerfile 调整
-
-目标 Dockerfile 阶段:
-
-```dockerfile
-FROM node:24-bookworm-slim AS frontend-builder
-WORKDIR /app/frontend
-COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./
-RUN corepack enable && pnpm install --frozen-lockfile
-COPY frontend ./
-RUN pnpm build
-
-FROM rust:1.95-bookworm AS backend-builder
-WORKDIR /app/backend
-COPY backend/Cargo.toml backend/Cargo.lock ./
-COPY backend/build ./build
-COPY backend/src ./src
-COPY --from=frontend-builder /app/frontend/dist ./web/dist
-RUN cargo build --release --locked --bin codex-proxy-rs
-
-FROM debian:bookworm-slim AS runtime
-WORKDIR /app
-ENV CPR_DEPLOYMENT_MODE=docker
-COPY --from=backend-builder /app/backend/target/release/codex-proxy-rs /usr/local/bin/codex-proxy-rs
-COPY --from=frontend-builder /app/frontend/dist ./web/dist
-CMD ["codex-proxy-rs"]
-```
-
-注意:
-
-- Docker runtime 镜像不设置 `CPR_VERSION`,避免运行时环境变量成为第二版本源。
-- Docker runtime 仍只包含主服务,不恢复 updater sidecar。
-- `deploy/Dockerfile` 的构建上下文应保持仓库根目录。
-
-## 迁移记录
-
-本次迁移执行项:
-
-1. 新建 `backend/`,用 `git mv` 移动 `src/`、`tests/`、`Cargo.toml`、`Cargo.lock`、`rust-toolchain.toml`,并将 `build.rs`、`VERSION` 归入 `backend/build/`。
-2. 新建 `frontend/`,用 `git mv web frontend`。
-3. 新建 `deploy/`,用 `git mv Dockerfile deploy/Dockerfile`、`git mv docker-compose.yml deploy/docker-compose.yml`,并移动部署样例配置。
-4. 修改 `.github/workflows/release.yml` 的工作目录、构建路径、打包路径和 Dockerfile 路径。
-5. 修改 `deploy/Dockerfile` 的 `COPY` 路径。
-6. 修改 `.dockerignore` 和 `.gitignore`,使用 `frontend/node_modules`、`frontend/dist`、`backend/target`。
-7. 修改文档和 README 中的开发、测试、构建命令。
-
-## 验收命令
-
-迁移完成后至少运行:
-
-```bash
-cargo test --manifest-path backend/Cargo.toml --test main admin::system --locked
-cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
-pnpm --dir frontend build
-docker build -f deploy/Dockerfile -t codex-proxy-rs:latest .
-docker compose -f deploy/docker-compose.yml config
-git diff --check
-```
-
-如果保留 root workspace,则还需要确认:
-
-```bash
-cargo metadata --format-version 1
-```
-
-输出中不能重新出现旧的 updater binary,也不能把根 `VERSION` 当成版本源。
-
-## 完成标准
-
-- 顶层不再有 `src/` 和 `web/`。
-- 后端源码、测试和版本源都归属 `backend/`。
-- 前端应用归属 `frontend/`。
-- Dockerfile、Compose 和部署样例归属 `deploy/`。
-- `backend/build/VERSION` 是唯一源码默认版本文件。
-- 发布 tag、release asset、Docker image label、后端 `/version`、前端更新弹窗显示同一个版本。
-- Docker 部署仍是单主服务模式,不恢复 updater sidecar。
diff --git a/skills/codex-proxy-rs/references/repo-guide.md b/skills/codex-proxy-rs/references/repo-guide.md
index 5c917771f..309413f9d 100644
--- a/skills/codex-proxy-rs/references/repo-guide.md
+++ b/skills/codex-proxy-rs/references/repo-guide.md
@@ -14,11 +14,11 @@
## 仓库结构
- `backend/`:Rust 1.95 Axum 后端,包含 SQLite 存储、代理调度、管理端 API、web 静态资源托管、在线更新逻辑。
-- `backend/build/`:构建脚本和 `VERSION`;release workflow 会写入 `backend/build/VERSION`。
+- `backend/build/`:Cargo 构建脚本,负责把版本、Git SHA、构建时间写入编译期环境变量。
- `backend/tests/`:集成测试和 fixtures。测试代码只放这里,禁止放进 `backend/src`。
- `frontend/`:Vue 3 管理端 SPA,使用 Vite 8、Tailwind v4、Pinia、Vue Router、Axios、lucide icons、ECharts。
- `deploy/`:Dockerfile、Dockerfile 专用 ignore 文件、Compose 文件、`config.example.yaml`。
-- `docs/`:只在用户要求时放设计说明或迁移文档。
+- `docs/`:只放长期维护文档,例如架构说明;不要保留过程型迁移文档作为开发约束。
- `skills/`:项目本地 Codex skills。
## 后端规范
@@ -89,13 +89,15 @@ docker compose -f deploy/docker-compose.yml up -d
- Release workflow:`.github/workflows/release.yml`。
- `v*` tag 触发发布。
-- release 时版本来源是去掉 `v` 前缀的 Git tag;workflow 会写入 `backend/build/VERSION`。
+- release 时版本来源是去掉 `v` 前缀的 Git tag;workflow 通过 `CPR_VERSION` build arg 写入编译期元数据。
+- 源码默认版本使用 `backend/Cargo.toml` 的 `[package].version`;不要重新引入独立 `VERSION` 文件。
- 运行时版本接口展示编译期元数据:`CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME`。
-- 当前发布的 Docker 平台是 `linux/amd64`,因为 release asset 目前只有 `linux_x86_64`。
+- 当前发布的 Docker 镜像平台是 `linux/amd64` 和 `linux/arm64`。
- Release asset 命名必须匹配在线更新选择器:
```text
-codex-proxy-rs__linux_x86_64.tar.gz
+codex-proxy-rs__linux_amd64.tar.gz
+codex-proxy-rs__linux_arm64.tar.gz
checksums.txt
```
@@ -173,4 +175,4 @@ docker compose -f deploy/docker-compose.yml config
- README、docs、发布和更新说明要和真实命令、真实行为一致。
- UI 保持当前安静、偏运维控制台的风格;避免营销式布局、卡片套卡片、图标过大、信息重复。
- 版本统一从发布 tag 到编译期元数据再到前端显示;不要引入第二个运行时版本源。
-- 历史迁移文档可能包含阶段性方案;实现和 README、workflow、Dockerfile 不一致时,以当前代码和真实命令为准。
+- 架构说明、README、workflow、Dockerfile 需要和当前代码和真实命令保持一致。