From cd4b91a81eb86cdffc5b18e101cb6c67fbd2f20c Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 2 Jul 2026 12:09:22 +0800 Subject: [PATCH] ci: complete release pipeline Co-authored-by: Codex --- .github/workflows/ci.yml | 64 +++++ .github/workflows/release.yml | 145 ++++++----- .github/workflows/security-scan.yml | 64 +++++ README.md | 21 +- backend/build/VERSION | 1 - backend/build/build.rs | 20 +- backend/src/admin/monitoring/dashboard.rs | 59 ----- backend/src/admin/system/routes.rs | 36 ++- backend/tests/admin/system/mod.rs | 10 +- deploy/Dockerfile | 20 +- docs/architecture.md | 170 +++++++++++++ docs/project-layout-restructure.md | 237 ------------------ .../codex-proxy-rs/references/repo-guide.md | 14 +- 13 files changed, 459 insertions(+), 402 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/security-scan.yml delete mode 100644 backend/build/VERSION create mode 100644 docs/architecture.md delete mode 100644 docs/project-layout-restructure.md 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 需要和当前代码和真实命令保持一致。