mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
chore: prepare release cleanup
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
+13
-10
@@ -18,40 +18,43 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
||||
uses: dtolnay/rust-toolchain@1.95
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
|
||||
- name: Cache Rust
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
uses: Swatinem/rust-cache@v2.9.1
|
||||
with:
|
||||
workspaces: backend -> target
|
||||
|
||||
- name: Rust format
|
||||
run: cargo fmt --manifest-path backend/Cargo.toml --check
|
||||
working-directory: backend
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Rust clippy
|
||||
run: cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings
|
||||
working-directory: backend
|
||||
run: cargo clippy --all-targets --all-features --locked -- -D warnings
|
||||
|
||||
- name: Rust tests
|
||||
run: cargo test --manifest-path backend/Cargo.toml --test main --locked
|
||||
working-directory: backend
|
||||
run: cargo test --test main --locked
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 11.7.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
@@ -70,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Validate Docker Compose
|
||||
run: docker compose -f deploy/docker-compose.yml config
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
|
||||
fetch-depth: 0
|
||||
@@ -146,17 +146,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.release-info.outputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
version: 11.7.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
|
||||
- name: Upload frontend artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: frontend/dist
|
||||
@@ -188,12 +188,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.release-info.outputs.tag }}
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
cargo-${{ runner.os }}-${{ matrix.target }}-
|
||||
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: backend/web/dist
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
CPR_BUILD_TYPE: release
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/
|
||||
@@ -244,18 +244,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.release-info.outputs.tag }}
|
||||
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: dist/frontend
|
||||
|
||||
- name: Download binary artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: codex-proxy-rs-*
|
||||
path: dist/binaries
|
||||
@@ -340,12 +340,12 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Generate release asset provenance
|
||||
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: dist/release/*
|
||||
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-assets
|
||||
path: dist/release
|
||||
@@ -363,24 +363,24 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.release-info.outputs.tag }}
|
||||
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: dist/docker/web/dist
|
||||
|
||||
- name: Download Docker binary artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: codex-proxy-rs-linux-*
|
||||
path: dist/docker-artifacts
|
||||
|
||||
- name: Download release assets
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-assets
|
||||
path: dist/release
|
||||
@@ -422,16 +422,16 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@v4.1.2
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -439,7 +439,7 @@ jobs:
|
||||
|
||||
- name: Build and push app image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: deploy/Dockerfile
|
||||
@@ -463,7 +463,7 @@ jobs:
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.release-info.outputs.git_sha }}
|
||||
|
||||
- name: Generate container provenance attestation
|
||||
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
@@ -493,7 +493,7 @@ jobs:
|
||||
cosign sign --yes "${images[@]}"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ needs.release-info.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
||||
uses: dtolnay/rust-toolchain@1.95
|
||||
|
||||
- name: Cache Rust
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
uses: Swatinem/rust-cache@v2.9.1
|
||||
with:
|
||||
workspaces: backend -> target
|
||||
|
||||
@@ -43,15 +43,15 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 11.7.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Build runtime image
|
||||
run: docker build -f deploy/Dockerfile --target runtime -t codex-proxy-rs:security-scan .
|
||||
|
||||
- name: Scan runtime image
|
||||
uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: codex-proxy-rs:security-scan
|
||||
severity: HIGH,CRITICAL
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.95"
|
||||
profile = "minimal"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
@@ -1493,9 +1493,7 @@ fn openai_error_type_for_code(code: &str) -> String {
|
||||
|| code.contains("unauthorized")
|
||||
|| code.contains("invalid_api_key")
|
||||
|| code.contains("account_deactivated")
|
||||
{
|
||||
"invalid_request_error"
|
||||
} else if code.contains("model")
|
||||
|| code.contains("model")
|
||||
|| code.contains("invalid_request")
|
||||
|| code.contains("not_found")
|
||||
|| code.contains("context_window")
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
# Codex Proxy RS 项目审计结论
|
||||
|
||||
审计日期:2026-07-03
|
||||
|
||||
## 结论
|
||||
|
||||
项目整体工程质量高于普通自托管代理项目:Rust 后端边界清晰,热路径有较完整的集成测试,账号调度、限流、WebSocket fallback、SSE 终止补偿、使用记录裁剪等关键路径都有明确实现和测试支撑。首轮已完成弱默认密码、管理 Cookie、请求头边界、登录失败节流、账号导出空 ids 拦截和 Docker 默认部署面的加固。当前主要风险不在“代码不可运行”,而在敏感数据落盘形态、运行时任务生命周期、数据库升级机制和发布供应链。
|
||||
|
||||
上线前优先处理四类事项:
|
||||
|
||||
1. 安全默认值:弱默认管理员密码、管理会话 Cookie `Secure`、登录失败节流、登录审计落库、trusted proxy 显式配置、Docker 本机绑定、容器 healthcheck/rootless/security options 已完成首轮整改;管理端 CSP 已移除 `unsafe-inline`。
|
||||
2. 敏感数据:客户端 API Key、admin API Key、账号 access/refresh token、Cloudflare cookie 目前均明文存 SQLite,需要分层加密或不可逆哈希策略;该项按本轮决策暂缓,不与当前加固批次混做。
|
||||
3. 运维闭环:WebSocket 池已纳入 `TaskCoordinator` 统一 shutdown;构建与安全扫描已有 CI;本轮已清理当前 RustSec warning 来源。
|
||||
4. 在线更新供应链:系统更新接口有管理员鉴权、进程内锁、文件锁、下载大小上限、GitHub API base 信任边界、HTTPS/GitHub host 校验、强制 `checksums.txt` SHA256 校验和 tar 路径穿越检查;发布 workflow 已增加 release asset provenance、容器镜像 SBOM/provenance 和 cosign keyless 签名,运行时更新客户端仍需加入签名/provenance 验证。
|
||||
|
||||
## 审计范围
|
||||
|
||||
覆盖范围:
|
||||
|
||||
- 后端入口、运行时 bootstrap、服务组合、HTTP router。
|
||||
- 管理端认证、session、admin API key、client API key、账号管理和设置。
|
||||
- `/v1` 代理认证、OpenAI 兼容协议转换、调度、限流、WebSocket/SSE 路径。
|
||||
- SQLite schema、使用记录、敏感字段、配置加载和部署配置。
|
||||
- Vue 管理端路由守卫、API 请求层、登录流、构建配置。
|
||||
- 系统在线更新、回滚、自重启、实时更新日志和前端更新弹窗。
|
||||
- CI、安全扫描、Docker/Compose、发布/更新文档。
|
||||
|
||||
未做事项:
|
||||
|
||||
- 未做真实上游压测或长连接 soak test。
|
||||
- 未在本地实际执行容器镜像漏洞扫描;security workflow 已配置 runtime 镜像 Trivy 扫描。
|
||||
- 未做浏览器端 Playwright 可视化/交互回归。
|
||||
- 未实际触发 GitHub release workflow;本轮仅做 workflow 配置解析、action SHA pinning 校验和本地 Docker build 验证。
|
||||
|
||||
## 验证结果
|
||||
|
||||
已执行:
|
||||
|
||||
- `cargo fmt --manifest-path backend/Cargo.toml --check`:通过。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main config`:18 个相关测试通过,覆盖管理员默认密码校验、`CPR_CONFIG_FILE` 显式配置路径、`server.trusted_proxies` 配置解析和 `server` 未知字段拒绝。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main admin::auth`:9 个相关测试通过,覆盖管理端登录 Cookie、失败节流、成功/失败/限流登录审计落库。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main admin::accounts::exporting`:6 个相关测试通过,覆盖空 ids 拒绝导出、显式敏感导出确认、按 ids 导出和导出审计落库。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main admin::system`:18 个相关测试通过,覆盖更新缓存、更新成功、GitHub API base 信任边界、跨文件系统替换、缺失/不匹配 checksum 失败、非允许下载域名拒绝、HTTP 下载绕过开关失效、tar 路径穿越拒绝、替换失败回滚、stale 文件锁清理、优雅重启请求、更新状态和 SSE 认证。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main infra::storage_schema`:29 个相关测试通过,覆盖当前表结构、迁移记录、初始迁移不使用 `if not exists`、未版本化旧库拒绝和 SQLite 约束。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main runtime::`:16 个相关测试通过,覆盖 WebSocket pool shutdown 句柄、Services 暴露池句柄和后台任务关闭。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main proxy::`:150 个相关测试通过,覆盖 `/v1` client key 鉴权失败原因解析、对外统一 401、调度、WebSocket/SSE、usage logging 和 session affinity。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main http`:50 个相关测试通过,覆盖 `/healthz` SQLite 可用性、request id 校验、可信代理 IP/CIDR、转发 IP 头收紧、CSP 不含 `unsafe-inline`、HTTP trace query 参数值脱敏和未知 API 路径不落入 SPA fallback。
|
||||
- `docker compose -f deploy/docker-compose.yml config`:通过,确认本机端口绑定、healthcheck、非 root 用户、cap drop、命名卷和容器内 `CPR_CONFIG_FILE=/app/config.yaml`。
|
||||
- `docker build -f deploy/Dockerfile --target runtime -t codex-proxy-rs:runtime-check .`:通过,确认 `deploy/Dockerfile.dockerignore` 生效;本次 Docker 内前端构建未复现 Rolldown `INVALID_ANNOTATION` 警告。
|
||||
- `cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings`:通过。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main admin::keys`:9 个相关测试通过,覆盖 client key 创建、列表、更新、启停、删除和运行时鉴权缓存。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main admin::accounts::lifecycle`:12 个相关测试通过,覆盖账号创建、状态更新、刷新、健康检查和账号池同步。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main`:589 个测试全部通过。
|
||||
- `pnpm --dir frontend build`:通过;本次本地构建未复现 Rolldown `INVALID_ANNOTATION` 警告。
|
||||
- `pnpm --dir frontend audit --prod --audit-level=high`:通过,无 high 级以上已知漏洞。
|
||||
- `cargo audit`:通过,无漏洞或 allowed warnings;`serde_yml` 和 `validator_derive` 引入的 `proc-macro-error2` 已从依赖图移除。
|
||||
- `cargo tree --manifest-path backend/Cargo.toml -d`:直接 `sha2` 已收敛到现有 `0.10.9` 依赖线;仍存在多版本依赖,包括 `rand 0.9/0.10`、`getrandom 0.2/0.3/0.4`、`sqlx-sqlite` 重复树等。
|
||||
- `ruby -e 'require "yaml"; ...' .github/workflows/*.yml`:通过,确认 CI/security/release workflow YAML 可解析。
|
||||
- workflow action pinning 校验脚本:通过,`.github/workflows/*.yml` 内所有 `uses:` ref 均为 40 位 commit SHA;对应 SHA 使用 `git ls-remote` 从各 action 仓库解析。
|
||||
- `docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest .github/workflows/ci.yml .github/workflows/security-scan.yml .github/workflows/release.yml`:通过,无 workflow 语法/表达式问题输出。
|
||||
- CodeGraph 复核:系统更新、账号导出、客户端 API key、使用记录和前端更新流均按调用路径审计。
|
||||
- 横向扫描:前端未发现 `v-html` / `innerHTML` / `localStorage` / `sessionStorage` 命中;`any` 使用较多,主要集中在 API payload、图表、配额和错误处理。
|
||||
|
||||
## 已有强项
|
||||
|
||||
- 后端启用 `unsafe_code = "forbid"`,clippy 基线较严格,且当前全量 clippy 通过。
|
||||
- 代理调度路径对 quota exhausted、rate limit、expired、disabled、banned、Cloudflare challenge/path block、model unsupported 做了细分类和 fallback。
|
||||
- 流式响应路径在客户端取消、上游错误、正常完成时都释放账号槽位,并补齐 `response.failed` / `[DONE]`,降低悬挂连接和 OpenAI 协议不完整风险。
|
||||
- 使用记录默认不保存请求/响应体,只记录必要元数据;容量默认裁剪,避免无限增长。
|
||||
- SQLite schema 有较多约束和索引,测试覆盖了当前表结构、非法状态、负计数、运行时设置等。
|
||||
- 管理端静态资源带 CSP、`nosniff`、`x-frame-options`、`referrer-policy`,且 CSP 已限制 script/style 到 `self`,不再允许 `unsafe-inline`。
|
||||
- 管理端 session Cookie 现在包含 `Secure; HttpOnly; SameSite=Lax`。
|
||||
- 启动时会拒绝空密码、过短密码和常见弱默认管理员密码;Docker 示例配置不再提供可用弱密码。
|
||||
- CI 已覆盖 Rust fmt/clippy/test、前端 format/build、Compose config 和 runtime Docker 镜像构建;单独 security workflow 覆盖 cargo audit、pnpm audit 和 runtime 镜像 Trivy 扫描;当前 RustSec warning 已清零;workflow action 引用已 pin 到 commit SHA。
|
||||
- Docker 运行阶段已改为非 root 用户,Compose 默认仅绑定 `127.0.0.1:8080`,并包含 healthcheck、`no-new-privileges`、cap drop、`init`、停止宽限、pid/CPU/内存边界、`/tmp` tmpfs 和命名卷。
|
||||
- release workflow 会产出多平台二进制、Docker 镜像和 `checksums.txt`;release archives 会生成 GitHub artifact attestation,Docker 镜像会随 BuildKit 发布 SBOM/provenance attestations 并用 cosign keyless 签名;缺口转为运行时更新客户端验证这些证明。
|
||||
- 系统更新入口均要求 `require_admin_auth`;更新和回滚同时使用进程内 `SYSTEM_OPERATION_LOCK` 与文件锁,能防止常规并发更新。
|
||||
- 更新包处理有 500 MiB 下载上限、HTTPS/GitHub 下载域名白名单、tar 路径穿越拦截、跨文件系统移动 fallback、web/binary 备份与失败回滚。
|
||||
- 前端更新弹窗把 release notes 当文本渲染,没有用不可信 HTML;SSE/EventSource 使用 cookie session,并在弹窗关闭或组件卸载时断开。
|
||||
|
||||
## 关键证据索引
|
||||
|
||||
- `backend/src/config/types.rs`:`AdminConfig::default()` 的 `default_password` 为空;`validate_default_password()` 拒绝空值、短密码和常见弱口令。
|
||||
- `backend/src/config/loader.rs` / `backend/src/runtime/bootstrap.rs`:启动会优先读取 `CPR_CONFIG_FILE` 指向的配置文件,未设置时回退到当前目录 `config.yaml`;在没有管理员时用 `default_password` 创建默认管理员,创建前会先校验默认密码。
|
||||
- `backend/src/admin/auth/session.rs`:登录和登出 Cookie 字符串包含 `Secure; HttpOnly; SameSite=Lax`;登录成功、凭证失败、限流和内部错误会写入 `admin_auth` 审计记录,元数据只保留来源和是否提供用户名,不写入用户名、密码或 session id。
|
||||
- `backend/src/admin/auth/service.rs`:同一来源管理员登录失败达到 5 次后短期锁定,成功登录会清理该来源失败计数。
|
||||
- `backend/src/proxy/auth.rs`:`/v1` client API key 鉴权会区分 `missing_authorization`、`malformed_authorization`、`invalid_key_format`、`invalid_key` 和 `store_unavailable`,并写入结构化日志;对外仍返回统一 OpenAI `invalid_api_key`。
|
||||
- `backend/src/admin/keys/service.rs`:`AdminClientApiKey` 含完整 `key` 字段,SQLite `create` 直接写入完整 key,`list()` / `get()` 会把存储对象映射给管理端返回;运行时 cache 也以明文 key 为查找键。
|
||||
- `backend/src/admin/keys/routes.rs`:`ClientApiKeyData` 响应结构包含完整 `key`。
|
||||
- `backend/tests/admin/keys/store.rs`:测试显式断言数据库 `key` 字段和列表返回值等于创建出的完整 key,说明这是当前行为契约而不是偶然泄露。
|
||||
- `frontend/src/views/api-keys/composables/useApiKeyMutations.ts`:UI 会用 prefix 做遮罩展示,但后端响应、网络传输和前端内存里仍可能存在完整 key。
|
||||
- `backend/src/config/settings.rs`:`runtime_settings.admin_api_key` 保存完整 admin API key;校验用 constant-time compare,展示状态时才 masking。
|
||||
- `backend/src/admin/accounts/routes.rs` / `backend/src/admin/accounts/service/exporting.rs`:导出接口要求 `confirm=export_sensitive_accounts` 且 ids 非空;成功、缺少确认、空 ids、账号不存在和内部失败会写入 `admin_account_export` 审计记录;按 ids 导出时 `ExportedAccount::from` 仍会明文导出 access token 和 refresh token。
|
||||
- `backend/src/http/middleware/request_id.rs`:只接受长度不超过 128 且由字母、数字、`_`、`-`、`.` 组成的 `x-request-id`;`TrustedProxyConfig` 支持 IP/CIDR。未命中 trusted proxy 时会移除 `cf-connecting-ip`、`x-real-ip`、`x-forwarded-for` 并写入 TCP peer IP;命中 trusted proxy 时才从转发头提取客户端 IP,并统一折叠为规范化 `x-real-ip`。
|
||||
- `backend/src/web/headers.rs`:静态资源 CSP 当前为 `script-src 'self'` 和 `style-src 'self'`,已删除长期 `unsafe-inline` 依赖。
|
||||
- `backend/src/http/middleware/trace.rs`:日志 span 和请求日志保留 URI path 与 query 参数名、顺序和重复结构,但所有参数值均写成 `<redacted>`;无 `=` 的裸 query 段也会整体脱敏。
|
||||
- `backend/src/infra/database.rs` / `backend/src/infra/migrations/0001_initial.sql`:SQLite 初始化启用 WAL、foreign keys、busy timeout,并通过 `schema_migrations` 执行编号迁移;业务表 DDL 不再使用 `create table if not exists` 作为隐式升级机制。
|
||||
- `backend/src/admin/system/routes.rs`:`perform_update` 有管理员鉴权、mutex、文件锁、GitHub API base 校验、下载域名校验、强制 HTTPS 下载和强制 `checksums.txt` 校验;`CPR_UPDATE_ALLOW_INSECURE_DOWNLOADS=true` 不再放行非本地同源 HTTP 下载;checksum 校验仍来自同一 release;`restart` 现在请求运行时优雅关闭信号,不再直接 `std::process::exit(0)`。
|
||||
- `backend/src/http/router.rs`:`/healthz` 会执行 SQLite `select 1`,数据库不可用时返回 503。
|
||||
- `backend/src/admin/monitoring/usage_record_store.rs`:`capture_body=false` 时会移除请求/响应 body 字段,这是正确默认;`record_audit()` 用于安全审计事件,不受普通 usage logging 开关影响,但仍沿用容量裁剪和 body 捕获策略。
|
||||
- `deploy/Dockerfile`:runtime 阶段创建并使用 `cpr` 非 root 用户,内置 `/healthz` healthcheck。
|
||||
- `deploy/docker-compose.yml`:示例端口为 `127.0.0.1:8080:8080`,并配置 healthcheck、`no-new-privileges`、cap drop、非 root 用户和命名卷。
|
||||
- `deploy/Dockerfile.dockerignore`:Dockerfile 专用 ignore 已覆盖 build output、依赖缓存、本地配置、运行数据、工具状态,并保留 `dist/docker` 给 prebuilt runtime target 使用。
|
||||
- `.github/workflows/ci.yml` / `.github/workflows/security-scan.yml` / `.github/workflows/release.yml`:CI 已加入前端格式检查、Compose config、runtime 镜像构建和 Trivy 镜像扫描;workflow action 均已 pin 到 commit SHA;release workflow 的写权限已收敛到 publish job。
|
||||
- `.github/workflows/release.yml`:发布产物有 `checksums.txt`,release archives 会生成 GitHub artifact attestation,Docker 镜像发布启用 BuildKit SBOM/provenance,并通过 cosign keyless 签名;后续仍需让在线更新流程验证这些证明。
|
||||
- `frontend/src/composables/useSystemUpdate.ts`:EventSource 使用 `withCredentials: true`,错误处理和自动重连状态仍需测试。
|
||||
- `frontend/src/layout/components/SystemUpdateModal.vue`:release notes 和更新日志均使用文本插值/`pre`,未使用不可信 HTML 渲染。
|
||||
|
||||
## 加法清单
|
||||
|
||||
P0:
|
||||
|
||||
- 已增加生产启动保护:管理员默认密码为空、过短或属于常见弱口令时拒绝启动。
|
||||
- 已增加管理 Cookie 安全属性:登录和登出 Cookie 均包含 `Secure; HttpOnly; SameSite=Lax`。
|
||||
- 已收紧默认真实 IP 边界并落地 trusted proxy 显式配置:`server.trusted_proxies` 支持 IP/CIDR;未配置或 peer 未命中时入口会移除 `x-forwarded-for`、`x-real-ip`、`cf-connecting-ip` 并使用 TCP peer IP。
|
||||
- 增加敏感数据保护:client API key 改为只存哈希,admin API key 改为只存哈希,账号 token/cookie 至少用本机或配置密钥加密落盘;该项本轮暂缓。
|
||||
- 已增加登录防爆破和审计:管理员登录失败计数、短期锁定、成功/失败/限流审计落库已落地。
|
||||
- 已增加账号导出基础保护:空 ids 不再默认导出全部账号,接口要求显式 `confirm=export_sensitive_accounts`,并写入成功/失败审计记录;按 ids 导出仍会返回完整 access token 和 refresh token,后续应继续增加频率限制和可选加密导出包。
|
||||
- 增加在线更新签名验证:发布侧已生成 release asset provenance、镜像 SBOM/provenance 和 cosign keyless signature;运行时更新客户端还需要验证 release 包证明,不能只依赖同一 release 里的 `checksums.txt`。
|
||||
|
||||
P1:
|
||||
|
||||
- 已为 config loader 增加显式 `CPR_CONFIG_FILE` 支持,未设置时仍回退到运行目录 `config.yaml`。
|
||||
- 已为 `/v1` 鉴权增加可观测性:区分缺失 Authorization、Bearer 格式错误、client key 格式错误、无效 key、存储校验失败;对外仍统一 401。
|
||||
- 已为 request id 增加长度、字符集和生成策略校验;HTTP trace 日志会保留 query 参数名和结构,统一脱敏参数值,兼顾排查和泄露控制。
|
||||
- 已为反向代理部署增加 `server.trusted_proxies`,只有可信 peer 的转发 IP 头会被采信,采信后也只向业务层暴露规范化 `x-real-ip`。
|
||||
- 已引入 `schema_migrations` 迁移表和 `0001_initial.sql` 初始迁移,业务 schema 不再依赖 `create table if not exists`;后续 schema 变更必须显式增加编号迁移。
|
||||
- 已将 WebSocket pool shutdown 纳入 `TaskCoordinator`,不再只依赖弱引用后台维护任务自然退出。
|
||||
- 已增强 `/healthz` 和 Compose `healthcheck`;当前 healthz 会验证 SQLite 可用,后续可扩展到服务启动完成和前端 assets 存在。
|
||||
- 已补强 `deploy/Dockerfile.dockerignore`,排除 `.git`、`target`、`node_modules`、`frontend/dist`、本地数据、日志、配置密钥和临时文件。
|
||||
- 已完成 Docker 非 root 用户、`no-new-privileges`、capability drop、本机端口绑定、`init`、停止宽限、pid/CPU/内存边界和 `/tmp` tmpfs;`read_only` 根文件系统需和容器内自更新策略一起收口。
|
||||
- 系统更新测试矩阵已覆盖更新缓存、成功替换、跨文件系统替换、checksum 缺失失败、checksum 不匹配失败、非允许下载域名拒绝、HTTP 下载绕过开关失效、tar 路径穿越拒绝、替换失败回滚和 stale 文件锁清理;后续重点转向签名/provenance 验证。
|
||||
- 已增加 router 级测试,覆盖 `/api/admin/*` 不被 SPA fallback 吞掉、`/v1/*` 未知路径返回 JSON 404、正常静态资源和健康检查行为。
|
||||
|
||||
P2:
|
||||
|
||||
- 在现有维护文档中明确 fork 版 `tungstenite` / `tokio-tungstenite` 的更新责任。
|
||||
- 已增加 Docker runtime 镜像 Trivy 扫描到 security workflow。
|
||||
- 已增加前端格式检查到 CI。
|
||||
- 已增加 `CPR_GITHUB_API_BASE` 信任边界:生产仅允许干净的 `https://api.github.com/repos`,本地 loopback `/repos` 只保留给测试。
|
||||
- 已将 release workflow 写权限收敛到 publish job;已完成 GitHub Actions commit SHA pinning、release asset provenance、Docker 镜像 SBOM/provenance 和 cosign keyless 签名;仍需在线更新客户端消费并验证这些证明。
|
||||
|
||||
## 减法清单
|
||||
|
||||
P0:
|
||||
|
||||
- 已删除弱默认密码:代码里的 `admin/admin` 默认值和 `deploy/config.example.yaml` 中的 `123456` 示例不再作为可启动配置存在。
|
||||
- 删除明文 client API key 存储和列表返回完整 key 的长期能力。创建时只返回一次完整 key,后续只展示 prefix/masked key。
|
||||
- 删除“前端遮罩等于安全”的假设。当前遮罩只影响视觉展示,完整 key 仍通过 API 响应进入网络和前端运行时。
|
||||
- 删除 admin API key 明文存储。当前 constant-time 比较是好事,但明文落库仍是单点泄露。
|
||||
- 已删除“空 id 默认批量导出全部账号”。当前按 ids 导出仍包含完整 token,但服务端已要求显式确认并记录审计;后续继续做频率限制和可选加密导出包。
|
||||
|
||||
P1:
|
||||
|
||||
- 删除对生产配置“靠 README 提醒用户修改”的依赖,改成代码强制校验。
|
||||
- 删除前端 build 中的 Rolldown annotation 噪音来源。可先确认是否由 `treeshake.annotations: false`、VueUse 版本或 Vite/Rolldown 组合导致。
|
||||
- 已删除未必要的默认 root 运行;Docker builder 里直接 `pnpm install` 和 `cargo build` 仍属合理构建方式。
|
||||
- 已删除无 ignore 的全仓库 Docker build context 假设,当前由 `deploy/Dockerfile.dockerignore` 控制构建上下文。
|
||||
- 已删除“raw schema 可以自然升级”的假设。当前改为编号迁移文件,业务表和索引 DDL 不再使用 `if not exists` 兜底;新增列、约束变更、索引回填和数据修复都必须落到新 migration。
|
||||
- 已删除 CSP 的长期 `style-src 'unsafe-inline'` 依赖;当前 Tailwind/Vue 构建产物通过外链 CSS 加载,静态头使用 `style-src 'self'`。
|
||||
- 已删除在线更新的硬退出假设。`restart` 当前 500ms 后请求运行时优雅关闭信号,让 axum graceful shutdown 和 `TaskCoordinator` 接管后续关闭。
|
||||
- 已删除 `CPR_UPDATE_ALLOW_INSECURE_DOWNLOADS=true` 在生产可用的空间;除同源 loopback 测试下载外,release asset 和 checksum 下载均要求 HTTPS。
|
||||
|
||||
P2:
|
||||
|
||||
- 已删除 `serde_yml` dev dependency,配置测试改用项目自身 `config` YAML 解析路径;同时删除 `validator` derive 宏依赖,清掉 `proc-macro-error2` RustSec warning 来源。
|
||||
- 已将直接 `sha2` 依赖从 `0.11` 收敛到现有 `0.10.9` 依赖线,继续评估 `rand 0.9/0.10`、`getrandom` 多版本等剩余重复依赖,减少审计面和编译面。
|
||||
- 删除长期无用的构建/发布脚本兼容分支,前提是 release 流程已稳定且有测试覆盖。
|
||||
|
||||
## 分层发现
|
||||
|
||||
### 认证与会话
|
||||
|
||||
- 管理端 session 使用 Argon2 哈希密码、UUID session id、HttpOnly Cookie,基础方向正确。
|
||||
- Cookie 已包含 `Secure; HttpOnly; SameSite=Lax`;如果经反向代理终止 TLS,应把会覆盖或清理转发头的代理 IP/CIDR 写入 `server.trusted_proxies`。
|
||||
- 登录接口已加入同来源失败计数和短期锁定,暴力尝试成本已提高;成功、凭证失败、限流和内部错误均会写入 `usage_records` 的 `admin_auth` 审计事件。
|
||||
- 当前请求入口不再默认信任 `cf-connecting-ip`、`x-real-ip` 和 `x-forwarded-for`;只有 TCP peer 命中 `server.trusted_proxies` 时才采信转发头,否则统一使用 TCP peer IP。
|
||||
- `x-request-id` 已限制长度和字符集,非法值会被替换为服务端生成的新 request id。
|
||||
- 管理端 API key 从 `x-api-key` 读取,使用 constant-time 比较,但 key 明文存库。
|
||||
- `/v1` client API key 校验基于内存表,热路径性能好;鉴权失败原因已结构化记录但对外保持统一 401。明文 key 存库并在管理端元数据中长期可见,安全模型仍偏弱。
|
||||
- 管理端 client key 列表结构包含完整 `key` 字段,这意味着只要拿到管理端会话就能长期复制所有代理 key;建议只保留创建时一次性明文返回。
|
||||
|
||||
### 配置与部署
|
||||
|
||||
- `AppConfig` 使用 `deny_unknown_fields`,能阻止拼错字段静默生效。
|
||||
- 配置加载已支持 `CPR_CONFIG_FILE` 显式路径;未设置时仍回退到当前目录 `config.yaml`,本地直接运行需要注意工作目录。
|
||||
- `server.trusted_proxies` 默认为空列表,支持单个 IP 和 CIDR;默认会剥离客户端传入的转发 IP 头,反代部署需要显式配置可信 peer。
|
||||
- Docker Compose 默认暴露 `127.0.0.1:8080:8080`,避免直接监听宿主所有网卡。
|
||||
- runtime 镜像基于 `debian:bookworm-slim`,安装 CA 和 curl healthcheck,默认以 `cpr` 非 root 用户运行。
|
||||
- `deploy/Dockerfile.dockerignore` 会收窄 Dockerfile 构建上下文;由于 Dockerfile 位于 `deploy/`,这里使用 Dockerfile 专用 ignore,而不是根 `.dockerignore`。
|
||||
- Compose 已设置 `healthcheck`、`security_opt: no-new-privileges:true`、capability drop、非 root 用户、命名卷、`init`、停止宽限、pid/CPU/内存边界和 `/tmp` tmpfs;`read_only` 根文件系统仍需结合自更新路径评估。
|
||||
- runtime 镜像显式创建 `/app/data`、`/app/logs`、`/app/web` 并赋予非 root 用户;默认命名卷会继承容器目录属主。
|
||||
|
||||
### 数据库与升级
|
||||
|
||||
- SQLite 连接初始化会创建父目录、启用 WAL、foreign keys 和 5 秒 busy timeout,这些默认值适合单进程自托管场景。
|
||||
- schema 有表约束、索引和运行时设置表,测试也覆盖了当前 schema 的多类约束。
|
||||
- 当前初始化先确保 `schema_migrations` 元数据表存在,空库按顺序应用 `0001_initial.sql` 并记录版本 1;已有业务表但没有迁移记录的库会拒绝启动,避免把重复执行 schema 当成隐式升级。
|
||||
- 未来新增列、收紧约束、回填索引或迁移敏感字段加密形态时,需要新增编号 migration 并提升当前版本,否则启动会因未知迁移版本失败。
|
||||
|
||||
### 在线更新与发布供应链
|
||||
|
||||
- `version`、`check_updates`、`update_event_stream`、`perform_update`、`update_status`、`rollback`、`restart` 均要求管理员认证,这是正确边界。
|
||||
- `perform_update` 使用进程内 mutex 和文件锁,`OperationLock` 有 30 分钟 stale lock 处理,能处理大部分重复点击和崩溃残留。
|
||||
- 下载 URL 默认限制 HTTPS 且只允许 GitHub / objects.githubusercontent.com;`CPR_UPDATE_ALLOW_INSECURE_DOWNLOADS=true` 不再绕过 HTTP 拒绝,明显降低任意下载风险。
|
||||
- `extract_release_archive` 拒绝绝对路径、父目录和平台 prefix,避免 tar 包写出临时目录。
|
||||
- 当前已要求 release 必须提供 `checksums.txt`,缺失、不匹配或 checksum 下载失败时更新失败;生产 `CPR_GITHUB_API_BASE` 只能指向官方 GitHub API,避免 release 元数据来源被环境变量改到任意 endpoint。checksum 校验仍只是从同一个 release 下载文本并比对 SHA256,不能证明发布者身份;后续应验证 GitHub artifact attestation 或独立签名。
|
||||
- release workflow 会构建多平台二进制和 GHCR Docker 镜像,并发布 `checksums.txt`;release archives 已接入 GitHub artifact attestation,镜像已启用 SBOM/provenance attestations 和 cosign keyless signing。镜像漏洞扫描已在 security workflow 覆盖,不在 release 发布链路阻断。
|
||||
- GitHub Actions 依赖已从 tag ref 改为 commit SHA pinning,并保留原 tag 注释便于后续升级审计。
|
||||
- `CPR_GITHUB_API_BASE` 可由环境变量改写,但生产校验只允许不带 credentials/query/fragment 的 `https://api.github.com/repos`;本地 loopback `/repos` 仅用于测试。后续仍需要签名/provenance 兜底 GitHub release 资产本身的信任问题。
|
||||
- `replace_release_files` 先替换 web,再备份/替换 binary,并做失败回滚;实现有恢复意识,但不是严格事务,断电/进程 kill 仍可能留下 backup 与当前文件不一致的中间状态。
|
||||
- `restart` 使用 `tokio::spawn` 延迟 500ms 后请求运行时 shutdown signal,服务端会走 axum graceful shutdown 并在退出前关闭后台任务;仍依赖外部 supervisor 或 Docker restart policy 拉起新进程。
|
||||
|
||||
### 代理和并发
|
||||
|
||||
- 账号池用 `tokio::Mutex<AccountPool>` 串行化内存状态,逻辑简单可靠;调度计算在锁内,持久化在锁外,避免长时间持锁。
|
||||
- 流式响应的取消路径释放账号槽位,降低 stale slot 依赖。
|
||||
- `stale_slot_ttl` 仍是必要兜底,应补充测试覆盖 panic/abort 或 spawn 任务异常退出后的容量恢复。
|
||||
- WebSocket pool 有 idle maintenance、ping、liveness timeout 和 shutdown 方法;当前 Services 会保留 pool 句柄,`TaskCoordinator` 关闭时会调用 pool shutdown,让后续 acquire 绕过池并关闭 idle 连接。
|
||||
|
||||
### 数据与隐私
|
||||
|
||||
- 请求/响应体默认不落使用记录,这是正确默认。
|
||||
- `accounts.access_token`、`accounts.refresh_token`、`account_cookies.value`、`client_api_keys.key`、`runtime_settings.admin_api_key` 目前都是明文。
|
||||
- SQLite 文件一旦泄露,可直接获得上游账号 token、cookie 和代理 key;这是当前最大实际安全风险。
|
||||
- 账号导出现在要求显式 ids 和固定确认词,不再允许空 ids 导出全部;成功和失败路径会写入不含 token/email 的 `admin_account_export` 审计记录。但选中账号仍会把 `StoredAccount` 的 `access_token` 和 `refresh_token` 通过 `ExposeSecret` 转成明文 JSON。迁移能力还需要频率限制和可选加密导出包。
|
||||
- 使用记录默认移除 `body`、`rawBody`、`requestBody`、`responseBody`、`upstreamBody`,这是好的隐私默认;如果开启 capture body,需要在 UI 和配置层明确提示风险,并考虑字段级脱敏。
|
||||
|
||||
### 前端管理面
|
||||
|
||||
- 路由守卫会在首次进入非登录页时检查 session,基本闭环。
|
||||
- Axios 设置 `withCredentials: true`,和 cookie session 匹配。
|
||||
- API 错误对象未导出类型,调用处多用 `any`,前端错误处理和重构安全性一般。
|
||||
- 登录页和管理页依赖后端 session 状态,不把 token 放 localStorage,这是正确方向。
|
||||
- 未发现前端使用 `v-html` 或 `innerHTML` 渲染后端文本;release notes、日志和使用记录展示默认走文本插值。
|
||||
- API key 页面会用 prefix 生成 masked key 展示,这是好的交互;但后端列表响应仍包含完整 key,前端遮罩不能降低网络、浏览器内存、日志或调试工具里的暴露风险。
|
||||
- 系统更新 EventSource 使用 `withCredentials: true`,后端也要求 session;需要注意 SSE 长连接断线后的错误状态和自动重连策略,避免 UI 显示“断开”但底层浏览器仍在重连。
|
||||
- `useDownload` 会把 JSON 导出为本地下载,这本身合理;结合账号导出时需要把“下载文件就是完整密钥包”的风险在操作层显式化。
|
||||
|
||||
### CI 与供应链
|
||||
|
||||
- CI 基线足够覆盖主开发路径。
|
||||
- `cargo audit` 当前无漏洞或 allowed warnings;后续重点转向重复依赖收敛和 fork 依赖维护策略。
|
||||
- `Cargo.toml` 使用 forked `tungstenite` / `tokio-tungstenite`,这可能是必要工程选择,但应有版本更新和安全响应责任说明。
|
||||
- 前端 `pnpm audit --prod --audit-level=high` 通过;构建警告应尽快消除,避免真实构建问题被噪音淹没。
|
||||
- security workflow 已跑 cargo audit、pnpm audit 和 runtime 镜像 Trivy 扫描;GitHub Action 依赖已 pin 到 commit SHA;release asset provenance 和镜像签名已在发布链路配置,客户端验证仍未接入。
|
||||
- release workflow 默认只有 `contents: read`,只有 package-assets job 额外拥有 `id-token: write`/`attestations: write` 用于 release asset attestation,publish job 拥有 `contents: write`、`packages: write`、`id-token: write` 和 `attestations: write` 用于发版、镜像发布、provenance 与 cosign keyless signing。
|
||||
|
||||
## 优先级路线图
|
||||
|
||||
本周:
|
||||
|
||||
- 已强制拒绝弱默认管理员密码。
|
||||
- 已为生产态 Cookie 加 `Secure`。
|
||||
- 已把 Docker 示例密码改为空值并默认本地绑定。
|
||||
- 已补强 `deploy/Dockerfile.dockerignore`,先把构建上下文和本地敏感文件暴露面降下来。
|
||||
- 已落地 trusted proxy 显式配置,默认不再信任客户端传入的转发 IP 头。
|
||||
- 已删除管理端 CSP 的 `unsafe-inline`。
|
||||
- admin/client API key 改哈希存储设计方案定稿。
|
||||
- 已阻止空 id 默认导出全部账号,并补账号导出显式确认和审计日志。
|
||||
- 已把在线更新改为强制 checksum 存在,作为签名/provenance 上线前的临时底线。
|
||||
|
||||
下个迭代:
|
||||
|
||||
- client API key 存储改造:创建时返回一次,数据库只存 hash/prefix/masked。
|
||||
- admin API key 存储改造:数据库只存 hash。
|
||||
- 已补登录审计日志,覆盖成功、凭证失败和限流路径;账号导出高危操作也已接入独立审计类别。
|
||||
- 默认真实 IP 提取、trusted proxy 显式配置和 request id 校验已完成;后续如要支持代理链更复杂的信任策略,再单独扩展。
|
||||
- 已落地数据库 `schema_migrations` 表和首个编号迁移,后续 schema 变更必须走显式 migration。
|
||||
- WebSocket pool 已纳入统一 shutdown。
|
||||
- 在线更新客户端加入签名/provenance 验证;发布侧证明和镜像签名已配置,更新路径测试矩阵已覆盖主要失败分支。
|
||||
- 已增加 Docker 资源限制和镜像扫描;`read_only` 根文件系统需要先解决容器内自更新写二进制和 web 资产的问题。
|
||||
|
||||
后续:
|
||||
|
||||
- 账号 token/cookie 加密落盘。
|
||||
- 保持 `CPR_CONFIG_FILE` 部署文档和示例配置同步;未设置时仍回退到当前目录 `config.yaml`。
|
||||
- 依赖审计、镜像扫描、Action SHA pinning、发布侧 SBOM/provenance 和镜像签名已常态化;release 包签名/provenance 的运行时验证仍需落地。
|
||||
- 清理剩余重复依赖和前端 Rolldown warning。
|
||||
@@ -1,145 +0,0 @@
|
||||
# Codex Proxy RS 可维护性审计
|
||||
|
||||
审计日期:2026-07-03
|
||||
|
||||
## 定位
|
||||
|
||||
本文聚焦代码可维护性:可清理项、可抽象复用项、架构职责归类。安全加固相关结论见 `audit-2026-07-03.md`,本文不重复。
|
||||
|
||||
审计方法为签名采样 + 重复段抽查,未逐行通读,未跑构建。所有行号为审计当时快照,落地前需复核。
|
||||
|
||||
## 结论
|
||||
|
||||
后端约 39k 行 Rust,前端约 14k 行 Vue/TS。分层骨架清晰,是项目强项;主要问题集中在三类:跨文件逐字复制的私有 helper、dispatch/tasks 层的平行样板实现、以及少数超大文件把基础设施逻辑内联进 handler。
|
||||
|
||||
建议落地顺序:先做去重(helper 收敛,低风险)→ 再做后端复用抽象(dispatch 埋点样板、tasks 骨架泛型化)→ 最后做架构拆分(monitoring 重命名、system/responses 文件拆分,改动面大需单独 PR)。前端不做类型层改造:类型策略是尽量推导 + 简单声明,接口入参一律 `any`。当前已完成多批低风险减法:删除零引用时间 helper、合并 usage record trend 存储层重复方法,把三个 cleanup task 的重复调度 loop 收敛到共享骨架,抽出 transport 响应元数据 helper 供 HTTP SSE / WebSocket 共用,并把 dispatch usage 事件、RFC3339/耗时 helper、账号窗口重置规则、WebSocket event type 解析、monitoring metadata 提取、账号内 5xx retry、认证失败触发刷新、SSE failure 匹配、SSE 帧边界识别、非负展示数值转换、monitoring SQL select 列清单、usage record 账号邮箱映射和 system routes 错误映射样板收敛到共享实现。
|
||||
|
||||
本轮继续完成了此前剩余的高 ROI 后端项:admin 鉴权改为 `AdminAuth` extractor;`AdminUsageRecordService` 从 store 文件拆出;Responses live stream / SSE failure 分类拆到子模块,并把 live stream 首 token 计时复用协议 helper;system 自更新按 routes/updater/download/archive/release/state 拆分;accounts quota 展示视图抽到 `quota_view.rs`;账号与 client key 更新 payload 解析共用 helper;usage record 与 dashboard 时间桶费用计算下沉到 monitoring service helper,HTTP handler 只做汇总和展示格式化;monitoring 事件线与账号聚合线完成命名整理。
|
||||
|
||||
## 本轮落地验证
|
||||
|
||||
- `cargo check --manifest-path backend/Cargo.toml --all-targets --all-features --locked`:通过。
|
||||
- `cargo fmt --manifest-path backend/Cargo.toml --check`:通过。
|
||||
- `cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings`:通过。
|
||||
- `cargo test --manifest-path backend/Cargo.toml --test main --locked`:589 个测试全部通过。
|
||||
- `git diff --check`:通过。
|
||||
- `git diff --cached --check`:通过。
|
||||
- 前端测试未执行;本轮没有改前端,且本轮要求前端不用测试。
|
||||
|
||||
## 一、可清理
|
||||
|
||||
### 明确死代码
|
||||
|
||||
- 已删除 `backend/src/infra/time.rs` 中全库 0 引用的 `china_time`。
|
||||
- 已合并 `backend/src/admin/monitoring/usage_record_store.rs` 存储层的 `token_trend` / `latency_trend` 重复实现;service 层仍保留两个对外语义入口,但都调用同一个 `trend` 存储方法。
|
||||
|
||||
### 跨文件逐字复制的私有 helper
|
||||
|
||||
收敛到公共工具模块即可去重:
|
||||
|
||||
| helper | 重复位置 |
|
||||
|---|---|
|
||||
| `elapsed_millis_i64` | 已收敛到 `infra/time.rs`,dispatch、transport response meta 和 admin account lifecycle 共用 |
|
||||
| `parse_rfc3339` / `parse_optional_rfc3339` | 已收敛到 `infra/time.rs`,accounts store/pool、session affinity、usage record store 和 account usage store 共用 |
|
||||
| `metadata_string` / `metadata_i64` | 已收敛到 `admin/monitoring/usage_record_model.rs`;store 继续使用原始 i64,routes 使用非负 i64 展示 helper |
|
||||
| `update_first_token_ms` | 已收敛为 `upstream/protocol/responses.rs` 的 `update_first_response_event_ms`;transport response meta 与 responses live stream 共用 |
|
||||
| `is_rate_limit_header` | 已收敛到 `upstream/transport/response_meta.rs` |
|
||||
| `websocket_event_type` | 已将 `upstream/protocol/websocket.rs` 版本设为 `pub(crate)`,transport 直接复用 |
|
||||
| `trigger_refresh_after_auth_failure` | 已收敛到 `proxy/dispatch/auth_recovery.rs`,chat/responses/compact 共用 |
|
||||
| `reasoning_effort_from_request` | 已收敛到 `proxy/dispatch/usage_events.rs`,chat/responses 共用 |
|
||||
| turn_state / set_cookie / rate_limit header 抽取 | 已收敛到 `upstream/transport/response_meta.rs` |
|
||||
| SSE 分隔符查找 | 已收敛到 `upstream/protocol/sse.rs` 的 `sse_frame_separator` / `sse_frame_end`,chat stream translator 与 live responses tuple transformer 共用 |
|
||||
| `nonnegative_i64`(Option 版) vs `nonnegative_i64_to_u64` | 已新增 `infra/format.rs` 的 `optional_nonnegative_i64_to_u64`,usage record store 复用;账号 store 的同名本地 helper 已删除并复用 `infra::format::nonnegative_i64_to_u64` |
|
||||
|
||||
### 样板密集区(宏或裁剪中间层)
|
||||
|
||||
- `backend/src/admin/response.rs:1375-1473`:`AdminError` 的 18 个构造函数,每个都是 `Self::new(status, CODE, message)` 三行模板,可用声明宏压缩。
|
||||
- `backend/src/admin/monitoring/usage_record_store.rs`:`AdminUsageRecordService` 已拆到 `usage_record_service.rs`,store 文件回归 SQLite 存储和聚合 DTO;service 层仍保留对外语义入口与少量错误映射,后续若继续瘦身可让 store 直接返回 admin error。
|
||||
- 已将 `backend/src/admin/monitoring/account_usage_store.rs` 中 `LIST_USAGE_AFTER_CURSOR_SQL` 与 `LIST_USAGE_SQL` 重复的 23 列 select 收敛为 `LIST_USAGE_SELECT_SQL`,分页游标条件与普通列表共用同一个查询骨架。
|
||||
- 已将 `backend/src/admin/system/routes.rs` 中重复的 `map_err` 到 `AdminError` 收敛为本地 `internal_error_with` / `bad_request_with` / `bad_gateway_with` helper;带事件上报、动态 status/host 和纯 String 错误分支保留原写法。
|
||||
|
||||
### 前端
|
||||
|
||||
无孤儿文件,整体干净。可清理项是过重组件而非死代码:
|
||||
|
||||
- `frontend/src/views/settings/index.vue`(537 行):混杂管理员 API Key 管理、运行参数表单、模型别名编辑器三块独立业务,应拆为三个子组件,index 只做加载与组装。
|
||||
- `frontend/src/components/base/BaseTable/index.vue`(441 行):承担列渲染 + 分页 + 横向滚动同步 + 固定列阴影四类职责,横向滚动阴影逻辑可提为 `useHorizontalStickyShadow`,分页 footer 可拆为 `BaseTablePagination.vue`。
|
||||
|
||||
## 二、可抽象复用
|
||||
|
||||
### 后端(按 ROI 排序)
|
||||
|
||||
1. **dispatch 埋点样板(最高价值)**:已新增 `proxy/dispatch/usage_events.rs`,把 `record_response_event`、route/apiKind 元数据、identity 填充、reasoning effort 提取和 dispatch error 基础写入收敛为共享实现;chat/responses 仍保留各自错误分类和 live stream 专属记录逻辑。
|
||||
2. **runtime/tasks 骨架**:已把 `cookie_cleanup.rs`、`session_cleanup.rs`、`session_affinity_cleanup.rs` 的重复 select-loop 抽到 `runtime/tasks/cleanup.rs`,三个 public task 类型继续保留原来的 `start` / `cleanup_once_at` 入口。`token_refresh` / `quota_refresh` 的 start() 也是同一 select-loop 骨架;`model_refresh` 因首次拉取重试较特殊可不纳入。
|
||||
3. **transport header 抽取**:已将 turn_state / set_cookie / rate_limit / first_token / elapsed 收敛为 `upstream/transport/response_meta.rs` 与 `upstream/protocol/responses.rs` helper,HTTP SSE、WebSocket 与 dispatch live stream 共用。
|
||||
4. **admin 鉴权 extractor**:已新增 axum `FromRequestParts` extractor `AdminAuth`,管理端 routes 从手写 `require_admin_auth(&state, &headers).await?;` 改为 extractor 参数,去重且防漏写。
|
||||
5. **账号内 5xx retry 循环**:已新增 `retry_upstream_5xx<F, Fut, T>`,`proxy/dispatch/upstream.rs` 的 responses / stream / compact 三个账号内 5xx retry 路径共用同一 retry/backoff 循环,并保留原日志字段差异。
|
||||
6. **SSE 失败分类器**:已新增 `proxy/dispatch/responses/sse_failure.rs`,`proxy/dispatch/responses.rs` 的 model unsupported、history recovery、invalid reasoning replay、auth、quota、stream failure metadata/status 等分类规则移入子模块,同时保留 auth/quota 原有 code/message 匹配语义差异。
|
||||
7. 窗口重置判定:已新增 `upstream/accounts/window.rs`,`store.rs` 与 `pool.rs` 共用同一个漂移阈值纯函数。
|
||||
8. 请求解析:已新增 `admin/update_payload.rs`,`parse_account_update` 与 `parse_client_api_key_update` 复用同一个 label/status 更新 payload 解析 helper;账号侧仍保留自己的 label trim 规则。
|
||||
|
||||
### 前端(按 ROI 排序)
|
||||
|
||||
1. **不建立前端类型层(明确决策)**:不新增 `api/types.ts`、不为 `api/modules/*` 补 per-module response 类型。前端类型策略是尽量依赖类型推导 + 少量简单声明,接口入参一律用 `any`,避免大面积类型声明带来的维护负担。因此当前的 `params?: any` / 无返回类型 / `catch (error: any)` 属于有意选择,不作为待办;仅在推导能自然带出类型时受益,不主动补类型契约。
|
||||
2. **`useAsyncAction` / `useIdSet` / `useServerPagination`**:可从 `useAccountMutations.ts`(490 行)+ `useApiKeyMutations.ts`(219 行)消除大量重复:
|
||||
- `useIdSet()`:`run(id, fn)` 自动管理 in-flight Set(重复 3+ 次:refreshingAccountIds / refreshingQuotaAccountIds / updatingStatusAccountIds / updatingStatusKeyIds / savingLabelKeyIds)。
|
||||
- `useAsyncAction(fn, { errorText })`:统一 loading guard + toast,替换 ~13 处 `try/catch/finally` 样板。
|
||||
- `useServerPagination`(远端分页 + debounce search)与 `useClientPagination`:`useAccountFilters` / `useApiKeyFilters` / `useUsageFilters` 三份并存的 `page/pageSize/searchQuery` + `pageSizes:[10,20,50,100]` 逻辑。
|
||||
3. `utils/format.ts`:数字/token/成本/百分比缩写在 `views/usage/constants.ts:140-179/272-276`、`AccountOverviewCards.vue:42`、`AccountOverviewCard.vue:93` 等多处各自实现(K/M 缩写重复),统一为 formatNumber / formatCompact / formatCost / formatPercent。
|
||||
4. `utils/chart.ts`:`RequestTrendCard.vue`、`UsageInsightsGrid.vue`、`UsageRecordDetailModal.vue`、`MetricCard.vue` 各自构造 `EChartsOption`,`formatTooltip` 手写两份。提取共享 grid/axis/splitLine 默认样式 + tooltip helper。
|
||||
5. `constants/statusTone.ts`:`active/disabled/expired/banned -> success/warning/danger` 映射在 `AccountStatusBadge.vue`、`UsageStatusCodeBadge.vue`、`useDashboard.ts:366`、`useAccountConnectionTest.ts` 四处重复。
|
||||
6. `useModelField.ts`:`AccountEditModal.vue:50-57` 与 `ApiKeyCreateModal.vue:25-32` 逐字复制基于 `defineModel` 的 `formField(key)` 工厂。
|
||||
7. `BaseFormModal.vue`:多个 modal 共享 "BaseModal + 表单 + ghost 取消 / primary 提交 footer" 骨架,可封装 footer 双按钮 + `saving`/`close-disabled` 透传。
|
||||
|
||||
## 三、架构归类
|
||||
|
||||
### 分层骨架(清晰,项目强项)
|
||||
|
||||
```
|
||||
infra/ 平台原语(json 分页游标 / identity argon2+apikey / time / format / database / logging / paths)——无业务
|
||||
http/ 传输层(middleware: request_id + trace,顶层 router merge proxy/admin/assets)
|
||||
runtime/ 启动编排(bootstrap / services 依赖装配 / state / shutdown / tasks + coordinator 单一注册点)
|
||||
config/ loader 读文件 / types 结构+Default+validate / settings 运行时可变写回
|
||||
upstream/ 上游资源与传输:accounts(有状态池) · protocol(纯类型编解码,可测性好) · transport(实际 I/O)
|
||||
proxy/ 客户端协议与调度:openai(OpenAI 协议边界) · dispatch(编排核心) · auth/router 入口
|
||||
admin/ 管理端 API(auth / accounts / keys / monitoring / settings / system / response / router)
|
||||
web/ SPA 静态资源托管
|
||||
frontend/ api(唯一出口 request.ts) -> composables(全局 vs 就近) -> views / stores(仅 auth+ui,克制)
|
||||
```
|
||||
|
||||
前端分层克制得当:account/api-key/usage 列表数据刻意留在 view composable 内,未塞进 Pinia,是正确取舍。
|
||||
|
||||
### 职责混杂 / 需整改(按优先级)
|
||||
|
||||
1. **monitoring 命名混乱已整理**:事件线与账号聚合线已拆成明确命名:
|
||||
|
||||
| 文件 | 职责 | 数据线 |
|
||||
|---|---|---|
|
||||
| `usage_record_model.rs` | `UsageRecord` 模型 + `ResponseUsageRecord` 采集入参 | 逐条事件 |
|
||||
| `usage_record_routes.rs` | HTTP handler + 展示 DTO | 逐条事件 |
|
||||
| `usage_record_store.rs` | SQLite 事件存储 | 逐条事件 |
|
||||
| `usage_record_service.rs` | 事件 service + 费用明细视图 helper | 逐条事件 |
|
||||
| `account_usage_store.rs` | 账号累计用量 + 时间桶存储 | 账号聚合用量 |
|
||||
| `account_usage_service.rs` | 账号聚合用量 service + 时间桶费用视图 | 账号聚合用量 |
|
||||
|
||||
原 `usage_record.rs` / `usage_records.rs` 仅差一个 `s` 的问题已消除;聚合线也从 `usage_store.rs` / `service.rs` 改为 `account_usage_store.rs` / `account_usage_service.rs`。
|
||||
|
||||
2. **`admin/system/routes.rs` 职责过载已拆**:原 1600 行路由文件已拆为 `routes.rs` 入口、`updater.rs` 编排、`updater/download.rs` 下载与 checksum/URL 校验、`updater/archive.rs` 解压替换回滚、`updater/release.rs` Release 查询/缓存/asset 选择、`state.rs` 状态文件与锁。
|
||||
|
||||
3. **`proxy/dispatch/responses.rs` 已做第一轮拆分**:实时 SSE body 转发、tuple transformer、断流补尾移入 `responses/live_stream.rs`;SSE failure 分类、stream failure metadata/status 移入 `responses/sse_failure.rs`。主文件仍承担调度决策和埋点编排,后续如继续拆,可再把 event recording helpers 单独模块化。
|
||||
|
||||
4. **`admin/accounts/routes.rs` quota 展示逻辑已抽出**:`quota_window_*` 分组/排序/去重/标签逻辑移入 `admin/accounts/quota_view.rs`,routes 只调用 `quota_data` 和高用量判断。
|
||||
|
||||
5. **分层泄漏**:
|
||||
- `admin/monitoring/usage_record_routes.rs` 原 `account_email_map` 直接拿连接池拼 `QueryBuilder` 查 `accounts` 表;已下沉到 `AdminUsageRecordService` / `SqliteUsageRecordStore`,usage record routes 与 dashboard 共用 service 方法。
|
||||
- `upstream/accounts/store.rs`(1984 行,仓储层)与 `pool.rs`(1412 行,调度层)原先对"窗口重置/配额刷新"各有一套判定;本轮已将窗口重置漂移规则集中到 `upstream/accounts/window.rs`,配额刷新分支仍按各自职责保留。
|
||||
- transport 层原先自带一份平行的 WebSocket event type 解析;本轮已改为复用 protocol 层纯解析。后续仍可继续检查其他 transport/protocol 边界重复。
|
||||
|
||||
6. **`admin/monitoring/billing.rs` 计费位置已下沉一层**:`usage_record_routes.rs` 不再直接调用 `billing::calculate_cost_breakdown`,改由 `usage_record_service.rs` 的 `usage_record_cost_details` 返回费用明细;`dashboard.rs` 不再直接调用 billing,时间桶费用在 `AdminUsageService` 转换 `AdminUsageTimeBucketRecord` 时写入 `cost_usd`。handler 只做汇总和展示格式化。
|
||||
|
||||
7. **前端 `views/settings/index.vue`**:唯一未遵循 "index 组装 + 子组件" 约定的 view,三块业务全塞一个文件(见第一节)。`useDashboard.ts`(397 行)数据获取与视图模型构建混在一起,纯变换函数可抽到 `views/dashboard/transform.ts`。(前端 API 层普遍用 `any` 是刻意的类型策略,不视为需整改的分层问题。)
|
||||
|
||||
### 谨慎项(不建议粗暴重构)
|
||||
|
||||
- `upstream/protocol/websocket.rs`(1212 行)约 40 个 `*_invalid_required_fields` 校验函数:高度模式化但各校验不同字段,属 schema 校验的合理展开,测试覆盖概率高。可考虑字段规格表数据驱动,但收益/风险比一般,低优先级。
|
||||
- `AdminAccountService` + `AdminAccountServiceParts`(`accounts/service/mod.rs:39-90`):Parts 结构规避 clippy `too_many_arguments`,属可接受模式。
|
||||
@@ -1,138 +0,0 @@
|
||||
# Codex Proxy RS 协议保真度审计
|
||||
|
||||
审计日期:2026-07-03
|
||||
|
||||
## 定位
|
||||
|
||||
本文档聚焦 **OpenAI 兼容协议保真度**:`codex-proxy-rs` 把 Codex/ChatGPT 上游翻译成 OpenAI 兼容格式,翻译的完整度决定客户端 SDK 会不会异常。与安全向 `audit-2026-07-03.md`、可维护性向 `maintainability-audit-2026-07-03.md` 分开维护。
|
||||
|
||||
覆盖四个面:Chat Completions、Responses API、Models 与错误体、工具调用专项。
|
||||
|
||||
## 方法与基准
|
||||
|
||||
审计为**纯静态对拍**,未做真实上游抓包。基准有两类,可信度不同:
|
||||
|
||||
1. **desktop bundle 基准(强)**:从当日最新官方 `Codex.dmg`(`persistent.oaistatic.com/codex-app-prod/Codex.dmg`,构建时间 2026-07-03)解包出的 Electron `app.asar`,提取 `.vite/build/{main,src,worker}.js` 中的协议字符串。`worker.js` 是核心流处理器。
|
||||
2. **OpenAI 官方契约基准(中)**:我知识内的 OpenAI API 契约,用于错误体、Models 端点等 bundle 未覆盖处。
|
||||
3. **开源实现对照(辅助)**:本轮补充对照 `/home/zyy/桌面/Codes/sub2api` 与 `/home/zyy/桌面/Codes/CLIProxyAPI` 的 codegraph 结果。二者不能替代官方抓包,但可用于验证社区常见的 Chat Completions ⇄ Responses/Codex 转换形状。
|
||||
|
||||
### 基准的关键局限(务必先读)
|
||||
|
||||
**官方桌面端的上游请求体不由这些 JS bundle 构造**。JS 层只做两件事:
|
||||
|
||||
- SSE / 流解析(`worker.js` 的 `nh` 事件白名单)
|
||||
- OpenTelemetry 埋点(`gen_ai.request.*` / `gen_ai.usage.*` 是遥测键,不是发给上游的请求字段)
|
||||
|
||||
真正的 Responses 请求体由桌面端内嵌 spawn 的 Rust `codex` CLI 组装,不在解包产物内。因此:
|
||||
|
||||
- **SSE 事件面**:bundle 基准硬,结论可信。
|
||||
- **请求字段面**(`store` / `include` / `max_output_tokens` / `service_tier` / 工具 schema 等):官方真实取值**基准不确定**,只能核对"结构是否合理",无法核对"字段级取值是否与官方一致"。要彻底核对这几项需对官方 `codex` CLI 做真实上游抓包。
|
||||
|
||||
行号为审计时快照,复核时以实际代码为准。
|
||||
|
||||
## 确认的上游 SSE 事件全集
|
||||
|
||||
`worker.js` 的 `nh` 数组(事件白名单,官方原样透传给下游,仅用于遥测采集):
|
||||
|
||||
```
|
||||
response.created response.in_progress response.failed
|
||||
response.completed response.incomplete response.queued
|
||||
response.output_text.delta response.output_item.added
|
||||
response.function_call_arguments.delta
|
||||
response.function_call_arguments.done
|
||||
response.output_item.done
|
||||
```
|
||||
|
||||
附加已知事件(reasoning / content 系列):`response.reasoning_summary_text.delta/done`、`response.reasoning_text.delta`、`response.reasoning_summary_part.added`、`response.output_text.done`、`response.content_part.added`、`response.custom_tool_call_input.delta`。
|
||||
|
||||
端点分类:`responses` / `chat.completions` / `embeddings`。Provider 枚举:`Codex` / `ChatGPT`。
|
||||
|
||||
## 分级结论
|
||||
|
||||
本项目有两条语义完全不同的路径,保真度差异大:
|
||||
|
||||
- **`/v1/responses` 直通**(`TupleSseEventTransformer`):无 schema 时逐字节透传,接近官方透传语义,保真度高。
|
||||
- **`/v1/chat/completions` 转换**(`ChatCompletionStreamTranslator`):主动把 Codex Responses SSE 转成 `chat.completion.chunk`,`match` 未命中的事件 `_ => {}` 丢弃。**偏差集中在此路径。**
|
||||
|
||||
官方 11 个核心事件中本项目当前已识别 11 个;`response.incomplete` 已在本轮补齐。
|
||||
|
||||
### 🔴 严重(已读代码核实)
|
||||
|
||||
**1. `response.incomplete` 全路径终止处理(已修复)** — 可信度最高(基准硬 + 已核实)
|
||||
|
||||
- 基准:`response.incomplete` 在官方 `nh` 事件全集内,是合法终止事件(命中 `max_output_tokens`、内容策略等),官方按 `response.status` 采集 finishReasons 并原样透传。
|
||||
- 项目自身 `upstream/protocol/websocket.rs:235,725` 已在 **WebSocket 路径**完整识别该事件(能取 `incomplete_details/reason`),修复前 **HTTP SSE 路径漏了它**:
|
||||
- `proxy/openai/chat.rs` push_event 的 match 无 `response.incomplete` 分支 → 落入 `_ => {}` 丢弃。唯一发 `finish_reason` 和 `[DONE]` 的 `push_completed` 只在 `response.completed` 触发。
|
||||
- `proxy/dispatch/responses.rs:2389` `sse_body_has_terminal_event` 只认 `completed|failed|error`。
|
||||
- 触发场景:任意"未完成但正常终止"的响应(尤其 `max_output_tokens` 截断)。
|
||||
- 修复前后果:
|
||||
- chat 客户端:流无终止 chunk、无 `[DONE]`,流悬挂到超时。
|
||||
- responses 客户端:在合法 `response.incomplete` 后被追加一个伪造的 `response.failed`(stream_disconnected),出现双终止事件;后台把成功的截断响应误记为 502,污染用量/账号健康统计。
|
||||
- 修复状态(2026-07-03):已将 `response.incomplete` 纳入 Responses SSE 终止事件集;Chat Completions 流式/非流式转换会把 `max_output_tokens` 映射为 `finish_reason=length`,并正常输出终止 chunk / `[DONE]`;Responses live stream 不再为合法 incomplete 追加伪造 `response.failed`。
|
||||
|
||||
**2. Chat Completions 工具定义未从嵌套转扁平(已修复)** — 方向可信,已用开源实现辅助确认
|
||||
|
||||
- 基准:Codex 上游是 Responses API,函数工具应为扁平 `{type:"function", name, description, parameters}`;Chat 输入是嵌套 `{type:"function", function:{...}}`。二者不可互换。**注意**:工具 schema 的官方真实取值属"基准不确定"(不在 bundle 内),此条依据"Codex = Responses API"和开源实现对照修复,后续仍可用真实抓包复核。
|
||||
- 开源对照:
|
||||
- `sub2api` `convertChatToolsToResponses()`:把 Chat `tools[].function` 和 legacy `functions[]` 转为 Responses 扁平 `{type:"function", name, description, parameters, strict}`。
|
||||
- `CLIProxyAPI` `ConvertOpenAIRequestToCodex()`:把 Chat 函数工具扁平化,内置工具原样透传;函数型 `tool_choice` 从 `{type:"function", function:{name}}` 转为 `{type:"function", name}`。
|
||||
- 项目修复前:`proxy/openai/chat.rs` `codex_tools()` 的 `tools` 分支原样透传嵌套结构;legacy `functions` 还额外包一层 `{type:"function","function":function}`。
|
||||
- 修复状态(2026-07-03):Chat `tools[].function`、legacy `functions[]` 已转为 Responses 扁平函数工具,非 function 内置工具继续透传;函数型 `tool_choice` 已转为 `{type:"function", name}`。
|
||||
|
||||
### 🟡 中等
|
||||
|
||||
错误体:
|
||||
|
||||
- quota 耗尽 → HTTP 429(已修复):对齐 OpenAI SDK 兼容路径;`QuotaExhausted` 聚合路径现在返回 429,body 保持 `type/code=insufficient_quota`。对照:`CLIProxyAPI` 的 stream/HTTP error mapping 对 429 使用 `rate_limit_exceeded`,并把 quota 类错误放在 OpenAI 兼容错误码体系内;本项目 quota 用 429 + `insufficient_quota`。
|
||||
- 聚合错误 type/code 语义映射(已修复):quota/rate/auth/model unsupported 经账户池聚合路径时已分别映射为 `insufficient_quota`、`rate_limit_error/rate_limit_exceeded`、`invalid_request_error/invalid_api_key`、`invalid_request_error/model_not_found`。
|
||||
- 错误体缺 `param` 字段(`errors.rs:47-63`);多数 SDK 容忍。
|
||||
- Responses 非流式错误体形状(已修复):已去掉额外顶层 `"type":"error"`,返回标准 `{"error":{...}}`。对照:`sub2api` 与 `CLIProxyAPI` 普通 OpenAI 错误体均使用 `error` envelope。
|
||||
- Responses stream 聚合错误(已修复):`response.failed` 已复用 dispatch 语义 code,不再仅按 HTTP status 粗映射。
|
||||
- Chat stream 中途失败帧(已修复):已支持上游 `response.error`、标准 `{"error":{...}}` envelope,以及 `CLIProxyAPI` Responses stream error chunk 使用的顶层 `code/message` 结构;缺 `type` 时按 code 映射为 OpenAI error body;输出 `data: {"error":{type,code,message}}` 并追加 `[DONE]`。对照:`CLIProxyAPI` Chat stream terminal error 使用 `data: {"error":{...}}`;`sub2api` OpenAI Chat 路径保留 `event:error` legacy SSE,Responses 路径才合成 `response.failed`。
|
||||
|
||||
SSE / chat 转换:
|
||||
|
||||
- chat `response.incomplete` 已映射 `length`/`content_filter`;其它终止状态仍只有 `stop`/`tool_calls`,若上游未来在 `response.completed` 中携带非 stop 终止语义,需要继续扩展。
|
||||
- chat 转换 `response.reasoning_text.delta` 与 `response.custom_tool_call_input.delta` 已补齐;custom tool 增量按普通 function tool arguments delta 转为 Chat `tool_calls`。
|
||||
- chat 路径函数型 `tool_choice` 命名工具格式已转换为 Responses 期望的 `{type:function, name}`。
|
||||
- legacy `function_call` 回传 call_id 用 `fc_{name}` 伪造(`chat.rs:595`),与真实 call_id 对不上,多轮工具对话无法关联结果。
|
||||
- 流式 `output_item.done` 已补 function/custom tool 兜底;若上游没有单独发送 arguments delta,也能从完整 output item 生成 Chat `tool_calls` 参数。
|
||||
|
||||
Models 端点(结构层面与 OpenAI 契约高度一致):
|
||||
|
||||
- Models 冷启动/别名(已修复):`ModelCatalog::from_config()` 现在提供内置 Codex 模型兜底,`/v1/models` 首拉前不再为空;`/v1/models/{id}` 和 `/v1/models/{id}/info` 支持配置别名与已知后缀解析。对照:`sub2api` 有静态 DefaultModels,`CLIProxyAPI` 有 registry 内置模型和 alias。
|
||||
- `created` 全模型硬编码同一常量(`models.rs:15`),契约合法但无真实语义。
|
||||
|
||||
### 请求字段面(基准不确定,仅记录待抓包核对)
|
||||
|
||||
以下项结构合理,但官方真实取值不在 bundle 内,无法静态核对,留待真实抓包:
|
||||
|
||||
- `store` 默认 false 且始终序列化(`responses.rs:624`)。
|
||||
- `include` 自动注入 `reasoning.encrypted_content`(`responses.rs:585`)— 符合公开 Codex 惯例但 bundle 无字面量命中。
|
||||
- 不构造 `max_output_tokens`:客户端传入会被静默丢弃。本轮对照结论分裂:`sub2api` 会把 Chat `max_tokens`/`max_completion_tokens` 映射到 Responses `max_output_tokens`;`CLIProxyAPI` 明确注释 Codex 不支持并禁用该映射。暂不改,仍需真实抓包或产品决策。
|
||||
- `service_tier` 的 `fast`→`priority` 归一化(`responses.rs:597`)为本项目自定义。
|
||||
- 两条 reasoning 构造路径(`proxy/openai/responses.rs:358` vs `responses.rs:544`)在 effort 缺失时行为不完全一致。
|
||||
- `function_call_output` 无专门净化分支,靠 `_ => Some(object)` 原样透传。
|
||||
|
||||
## 优先级建议
|
||||
|
||||
1. **修 `response.incomplete`(🔴,基准硬,已完成)** — 两条 SSE 路径已纳入终止事件集,Chat Completions 已补 `length` finish reason 和 `[DONE]`。
|
||||
2. **chat 工具定义/`tool_choice` 转扁平(🔴,已完成)** — 已参考 sub2api 与 CLIProxyAPI 的 common path,并补充请求转换测试。
|
||||
3. **chat reasoning/custom tool delta 与 `output_item.done` 兜底(🟡,已完成)** — 已补 Chat 流式/非流式转换测试。
|
||||
4. **聚合错误 type/code 与 Responses stream code 映射(🟡,已完成)** — Chat/Responses JSON 与 Responses `response.failed` 已共享语义化错误 code。
|
||||
5. **quota→429、Chat stream terminal error、Responses 非流式错误体、Models 冷启动/别名(🟡,已完成)** — 已按 OpenAI SDK 兼容优先级和 sub2api/CLIProxyAPI 对照落地。
|
||||
6. 后续只剩低优先级项:错误体 `param` 字段、`created` 真实语义、legacy `function_call` call_id 关联等。
|
||||
|
||||
## 复现方法(解包基准)
|
||||
|
||||
```
|
||||
# 1. 下载当日 DMG
|
||||
curl -fL https://persistent.oaistatic.com/codex-app-prod/Codex.dmg -o Codex.dmg
|
||||
|
||||
# 2. 7z 解包(需 7-Zip 25.x,旧 p7zip 无法解 APFS DMG)
|
||||
7z x Codex.dmg -o dmg-extract # 注意路径含空格 "Codex Installer/"
|
||||
|
||||
# 3. 提取 app.asar(纯 node,@electron/asar 格式)
|
||||
# 协议逻辑在 dmg-extract/*/Codex.app/Contents/Resources/app.asar
|
||||
# 解出后核心文件:.vite/build/{main,src,worker}.js(webview/assets 全是 UI/i18n,无关)
|
||||
```
|
||||
@@ -49,7 +49,7 @@ function togglePasswordVisible(): void {
|
||||
as="form"
|
||||
:padded="false"
|
||||
variant="elevated"
|
||||
class="login-form relative grid min-h-129.25 w-[min(450px,100%)] gap-4 rounded-lg px-7.5 pt-6.5 pb-6 max-[560px]:min-h-auto max-[560px]:gap-4 max-[560px]:p-5.5"
|
||||
class="login-form relative grid min-h-129.25 w-[min(450px,100%)] gap-3 rounded-lg px-7.5 pt-6.5 pb-6 max-[560px]:min-h-auto max-[560px]:gap-3 max-[560px]:p-5.5"
|
||||
@submit.prevent="emit('submit')"
|
||||
>
|
||||
<div class="login-form-line" aria-hidden="true" />
|
||||
@@ -87,7 +87,7 @@ function togglePasswordVisible(): void {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="grid min-w-0 gap-2.5" aria-labelledby="login-title">
|
||||
<section class="grid min-w-0 gap-1.5" aria-labelledby="login-title">
|
||||
<h1
|
||||
id="login-title"
|
||||
class="m-0 text-[34px] leading-[1.02] font-semibold text-(--login-title) max-[560px]:text-[30px]"
|
||||
|
||||
Reference in New Issue
Block a user