refactor: migrate backend to Rust 1.97 and edition 2024

Unify the local, CI, release, and container toolchains on Rust 1.97.

Move update configuration, release caching, and event delivery into service-owned state, apply Edition 2024 idioms, and refresh repository ignore rules and documentation.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-07-12 19:10:56 +08:00
co-authored by Codex
parent 26cf170f33
commit 1e99c5037d
160 changed files with 1726 additions and 1482 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
warnings = "deny"
+2 -3
View File
@@ -24,7 +24,6 @@ jobs:
CARGO_TERM_COLOR: always
CPR_TEST_DATABASE_URL: postgres://codex_proxy:codex_proxy@127.0.0.1:5432/codex_proxy
CPR_TEST_REDIS_URL: redis://127.0.0.1:6379
RUSTFLAGS: -D warnings
services:
postgres:
image: postgres:18-bookworm@sha256:d9c83446333daec3f0588cc709adb80c26090b7f9f0f7ec8d43c243385d79818
@@ -56,7 +55,7 @@ jobs:
ref: ${{ inputs.ref }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
with:
components: rustfmt,clippy
@@ -71,7 +70,7 @@ jobs:
- name: Rust clippy
working-directory: backend
run: cargo clippy --all-targets --all-features --locked -- -D warnings
run: cargo clippy --all-targets --all-features --locked
- name: Rust tests
working-directory: backend
+2 -2
View File
@@ -205,7 +205,7 @@ jobs:
-v "$HOME/.cargo:/cargo" \
-v "$PWD/..:/workspace" \
-w /workspace/backend \
rust:1.95-bookworm@sha256:6258907abe69656e41cd992e0b705cdcfabcbbe3db374f92ed2d47121282d4a1 \
rust:1.97-bookworm@sha256:7d0723df719e7f213b69dc7c8c595985c3f4b060cfbee4f7bc0e347a86fe3b6a \
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
working-directory: backend
@@ -239,7 +239,7 @@ jobs:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
with:
targets: ${{ matrix.target }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@eac0f66a48bc4b70a10b9acd4c4e930f835d95ff # 1.95
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
- name: Cache Rust
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
+36 -25
View File
@@ -1,43 +1,54 @@
# Rust build output.
# Rust
/backend/target/
/target/
/dist/
/package/
/checksums.txt
/codex-proxy-rs_*.tar.gz
/codex-proxy-rs_*.zip
*.profraw
*.profdata
# Frontend dependencies and build output.
# Frontend
/frontend/node_modules/
/frontend/dist/
.pnpm-store
/frontend/.vite/
/.pnpm-store/
/frontend/.pnpm-store/
*.tsbuildinfo
# Application runtime state.
*.runtime
# Release artifacts
/dist/
# Runtime data
/.runtime/
*.sqlite
*.sqlite3
*.sqlite-*
*.db
*.db-shm
*.db-wal
*.log
# Local configuration.
/local.yaml
/local.yml
/.env
# Local configuration and secrets
.env
.env.*
!.env.example
!**/.env.example
/.envrc
/config.yaml
/backend/config.yaml
/deploy/.env
# Local workspaces and tooling state.
# Editors and operating systems
/.idea/
/.vscode/
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# Local tools and worktrees
/.codegraph/
/.codex-ws-audit/
/.agents/
/.reasonix/
/.worktrees/
/worktrees/
/.codex-ws-audit/
/.codegraph/
/.cargo/*
!/.cargo/config.toml
*.tmp
*.swp
*~
*.codegraph
/.agents/
/.cargo/
/.reasonix/
+1 -1
View File
@@ -2,7 +2,7 @@
<h1>Codex Proxy RS</h1>
<p>OpenAI Responses 兼容的 Codex 多账号网关</p>
<p>
<a href="https://www.rust-lang.org/"><img alt="Rust 1.95" src="https://img.shields.io/badge/Rust-1.95-000?style=flat-square&amp;logo=rust" /></a>
<a href="https://www.rust-lang.org/"><img alt="Rust 1.97" src="https://img.shields.io/badge/Rust-1.97-000?style=flat-square&amp;logo=rust" /></a>
<a href="https://github.com/tokio-rs/axum"><img alt="Axum 0.8" src="https://img.shields.io/badge/Axum-0.8-2f7d95?style=flat-square" /></a>
<a href="https://vuejs.org/"><img alt="Vue 3.5" src="https://img.shields.io/badge/Vue-3.5-42b883?style=flat-square&amp;logo=vuedotjs&amp;logoColor=white" /></a>
<a href="https://vite.dev/"><img alt="Vite 8" src="https://img.shields.io/badge/Vite-8-646cff?style=flat-square&amp;logo=vite&amp;logoColor=white" /></a>
+2 -2
View File
@@ -1,9 +1,9 @@
[package]
name = "codex-proxy-rs"
edition = "2021"
edition = "2024"
license = "MIT"
publish = false
rust-version = "1.95"
rust-version = "1.97"
[dependencies]
argon2 = "0.5.3"
+2 -2
View File
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.95"
channel = "1.97"
components = ["clippy", "rustfmt"]
profile = "minimal"
components = ["rustfmt", "clippy"]
@@ -33,12 +33,12 @@ pub async fn health_check_accounts(
"stagger_ms must be between 500 and 30000",
));
}
if let Some(concurrency) = payload.concurrency {
if !(1..=10).contains(&concurrency) {
return Err(AdminError::bad_request(
"concurrency must be between 1 and 10",
));
}
if let Some(concurrency) = payload.concurrency
&& !(1..=10).contains(&concurrency)
{
return Err(AdminError::bad_request(
"concurrency must be between 1 and 10",
));
}
match state
.services
+8 -8
View File
@@ -1,14 +1,14 @@
//! 账号管理 HTTP 处理器。
use axum::{
Json,
body::Body,
extract::{Query, State},
http::{
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
StatusCode,
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
},
response::{IntoResponse, Response},
Json,
};
use std::collections::{HashMap, HashSet};
@@ -17,22 +17,22 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
api::AppState,
api::admin::{
response::{
parse_editable_update, AdminEnvelope, AdminError, AdminResponse, BatchDeleteData,
EditableUpdateMessages, PageMeta, ADMIN_OK_CODE, ADMIN_OK_MESSAGE,
ADMIN_OK_CODE, ADMIN_OK_MESSAGE, AdminEnvelope, AdminError, AdminResponse,
BatchDeleteData, EditableUpdateMessages, PageMeta, parse_editable_update,
},
session::AdminAuth,
},
api::AppState,
fleet::{
account::AccountStatus,
manage::{
quota_view::{
quota_data, AccountQuotaData, AccountQuotaUsageWindow, AccountQuotaWindowLocalUsage,
},
AccountHealthCheck, AccountManageError, AccountRefreshOutcome, AccountRefreshResult,
AccountUpdate, ManagedAccount, OAuthExchangeInput,
quota_view::{
AccountQuotaData, AccountQuotaUsageWindow, AccountQuotaWindowLocalUsage, quota_data,
},
},
refresh::token_refresh_status_eligible,
},
+9 -10
View File
@@ -1,21 +1,21 @@
//! 管理端认证路由。
use axum::{
extract::State,
http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Extension, Json,
extract::State,
http::{HeaderMap, HeaderValue, StatusCode, header::SET_COOKIE},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use crate::{
api::{
AppState,
admin::{
response::{AdminEnvelope, AdminError, AdminResponse},
session::{admin_session_cookie, ADMIN_SESSION_COOKIE},
session::{ADMIN_SESSION_COOKIE, admin_session_cookie},
},
middleware::request_id::ClientIp,
AppState,
},
auth::types::SessionError,
infra::time::china_rfc3339,
@@ -107,15 +107,14 @@ pub(crate) async fn logout(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Response, AdminError> {
if let Some(session_id) = admin_session_cookie(&headers) {
if let Err(error) = state
if let Some(session_id) = admin_session_cookie(&headers)
&& let Err(error) = state
.services
.admin_sessions
.delete_session(&session_id)
.await
{
tracing::warn!(error = %error, "failed to revoke admin session during logout");
}
{
tracing::warn!(error = %error, "failed to revoke admin session during logout");
}
let mut response = AdminResponse::new(
+6 -6
View File
@@ -12,15 +12,15 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
api::AppState,
api::admin::{
response::{AdminEnvelope, AdminError, AdminResponse},
session::AdminAuth,
usage_routes::{
usage_cost_details, usage_record_models, usage_token_details,
UsageRecordCostDetailsData, UsageRecordTokenDetailsData,
UsageRecordCostDetailsData, UsageRecordTokenDetailsData, usage_cost_details,
usage_record_models, usage_token_details,
},
},
api::AppState,
fleet::{
account::{Account, AccountStatus},
pool::AccountCapacitySummary,
@@ -36,13 +36,13 @@ use crate::{
telemetry::{
account_usage::query::AccountUsageTimeBucket,
dashboard::{
dashboard_cards, dashboard_health_timeline_data, dashboard_trend_data,
DashboardAccountCounts, DashboardCardsData, DashboardHealthTimelineData,
DashboardTrendData, DashboardTrendKind,
DashboardTrendData, DashboardTrendKind, dashboard_cards,
dashboard_health_timeline_data, dashboard_trend_data,
},
usage::{
query::{UsageQueryFilter, UsageRecordAccountUsage},
types::{metadata_string, UsageRecord},
types::{UsageRecord, metadata_string},
},
},
upstream::openai::fingerprint::Fingerprint,
+5 -5
View File
@@ -1,23 +1,23 @@
//! 管理端 v1 接口访问 Key 处理器(列表、创建、生命周期)。
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
api::AppState,
api::admin::response::{
parse_editable_update, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse,
BatchDeleteData, EditableUpdateMessages,
AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse, BatchDeleteData,
EditableUpdateMessages, parse_editable_update,
},
api::admin::session::AdminAuth,
api::AppState,
infra::{
json::{clamp_limit, clamp_page, NumberedPage},
json::{NumberedPage, clamp_limit, clamp_page},
time::{china_relative_time_str, china_rfc3339_str},
},
keys::types::{KeyManageError, ManagedClientApiKey},
+2 -2
View File
@@ -8,13 +8,13 @@ use axum::{
use serde::{Deserialize, Serialize};
use crate::{
api::AppState,
api::admin::{
response::{AdminError, AdminPageEnvelope, AdminResponse},
session::AdminAuth,
},
api::AppState,
infra::{
json::{clamp_limit, clamp_page, NumberedPage},
json::{NumberedPage, clamp_limit, clamp_page},
time::china_datetime,
},
telemetry::{ops::query::OpsQueryError, ops::store::OpsErrorFilter, ops::types::OpsErrorLog},
+2 -2
View File
@@ -1,10 +1,10 @@
//! 管理端响应封装与 From 转换。
use crate::infra::json::{total_pages, NumberedPage};
use crate::infra::json::{NumberedPage, total_pages};
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
+2 -2
View File
@@ -1,11 +1,11 @@
//! 管理端路由。
use axum::{
http::{header, HeaderValue},
Router,
http::{HeaderValue, header},
middleware,
response::Response,
routing::{get, post},
Router,
};
use crate::api::AppState;
+2 -2
View File
@@ -2,10 +2,10 @@
use axum::{
extract::FromRequestParts,
http::{request::Parts, HeaderMap},
http::{HeaderMap, request::Parts},
};
use crate::api::{admin::response::AdminError, AppState};
use crate::api::{AppState, admin::response::AdminError};
pub(super) const ADMIN_SESSION_COOKIE: &str = "cpr_admin_session";
+1 -1
View File
@@ -6,11 +6,11 @@ use serde_json::Value;
use std::collections::BTreeMap;
use crate::{
api::AppState,
api::admin::{
response::{AdminEnvelope, AdminError, AdminResponse},
session::AdminAuth,
},
api::AppState,
settings::{SettingsError, SettingsPatch, SettingsSnapshot, SettingsValidationError},
};
+46 -17
View File
@@ -6,24 +6,20 @@ use axum::{
extract::{Json, Query, State},
http::StatusCode,
response::{
sse::{Event, KeepAlive, Sse},
IntoResponse,
sse::{Event, KeepAlive, Sse},
},
};
use futures::Stream;
use serde::Deserialize;
use crate::{
api::AppState,
api::admin::{
response::{AdminEnvelope, AdminError, AdminResponse},
session::AdminAuth,
},
api::AppState,
update::{
service::{self, RestartAction},
state::operation_id,
types::UpdateError,
},
update::{service::RestartAction, state::operation_id, types::UpdateError},
};
#[derive(Debug, Deserialize)]
@@ -38,20 +34,30 @@ pub(crate) struct UpdateRequest {
target_version: String,
}
pub(crate) async fn version(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
pub(crate) async fn version(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<impl IntoResponse, AdminError> {
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(service::version_data().await),
AdminEnvelope::ok(state.services.system_update.version_data().await),
))
}
pub(crate) async fn update_detail(
State(state): State<AppState>,
_auth: AdminAuth,
Query(query): Query<UpdateDetailQuery>,
) -> Result<impl IntoResponse, AdminError> {
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(service::update_detail(query.refresh.unwrap_or(false)).await),
AdminEnvelope::ok(
state
.services
.system_update
.update_detail(query.refresh.unwrap_or(false))
.await,
),
))
}
@@ -59,7 +65,7 @@ pub(crate) async fn update_event_stream(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, AdminError> {
let receiver = service::subscribe_update_events();
let receiver = state.services.system_update.subscribe_update_events();
let shutdown = state.services.process_control.subscribe_shutdown();
let stream = futures::stream::unfold(
(receiver, shutdown, false),
@@ -90,11 +96,15 @@ pub(crate) async fn update_event_stream(
}
pub(crate) async fn perform_update(
State(state): State<AppState>,
_auth: AdminAuth,
payload: Option<Json<UpdateRequest>>,
) -> Result<impl IntoResponse, AdminError> {
let target = payload.map(|Json(payload)| payload.target_version);
let result = service::perform_update(target)
let result = state
.services
.system_update
.perform_update(target)
.await
.map_err(update_error)?;
Ok(AdminResponse::new(
@@ -103,16 +113,31 @@ pub(crate) async fn perform_update(
))
}
pub(crate) async fn update_status(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
let status = service::update_status().map_err(update_error)?;
pub(crate) async fn update_status(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<impl IntoResponse, AdminError> {
let status = state
.services
.system_update
.update_status()
.map_err(update_error)?;
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(status),
))
}
pub(crate) async fn rollback(_auth: AdminAuth) -> Result<impl IntoResponse, AdminError> {
let operation_id = service::rollback().await.map_err(update_error)?;
pub(crate) async fn rollback(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<impl IntoResponse, AdminError> {
let operation_id = state
.services
.system_update
.rollback()
.await
.map_err(update_error)?;
Ok(AdminResponse::new(
StatusCode::OK,
AdminEnvelope::ok(serde_json::json!({
@@ -127,7 +152,11 @@ pub(crate) async fn restart(
State(state): State<AppState>,
_auth: AdminAuth,
) -> Result<impl IntoResponse, AdminError> {
let plan = service::restart_plan().map_err(update_error)?;
let plan = state
.services
.system_update
.restart_plan()
.map_err(update_error)?;
let message = plan.message;
let process_control = state.services.process_control.clone();
tokio::spawn(async move {
+6 -6
View File
@@ -10,26 +10,26 @@ use axum::{
use serde::{Deserialize, Serialize};
use crate::{
api::AppState,
api::admin::{
response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse},
session::AdminAuth,
},
api::AppState,
infra::{
format::{
format_compact_number, format_cost, format_duration_ms, format_duration_ms_f64,
format_multiplier, format_number, format_percent, format_token_price, format_tokens,
},
json::{clamp_limit, clamp_page, NumberedPage},
json::{NumberedPage, clamp_limit, clamp_page},
time::china_datetime,
},
telemetry::{
usage::query::{
usage_record_cost_details, UsageQueryError, UsageQueryFilter, UsageRecordBreakdown,
UsageRecordEndpointSource, UsageRecordModelSource, UsageRecordSummary,
UsageRecordTrendPoint,
UsageQueryError, UsageQueryFilter, UsageRecordBreakdown, UsageRecordEndpointSource,
UsageRecordModelSource, UsageRecordSummary, UsageRecordTrendPoint,
usage_record_cost_details,
},
usage::types::{metadata_string, UsageRecord},
usage::types::{UsageRecord, metadata_string},
},
};
+1 -1
View File
@@ -4,10 +4,10 @@ use std::{
};
use axum::{
Json, Router,
body::Body,
http::{Request, StatusCode},
response::{IntoResponse, Response},
Json, Router,
};
use serde_json::json;
use tower::service_fn;
+1 -1
View File
@@ -2,7 +2,7 @@
//!
//! 提供 Bearer token 提取与客户端 API key 校验。
use axum::http::{header::AUTHORIZATION, HeaderMap};
use axum::http::{HeaderMap, header::AUTHORIZATION};
use crate::api::AppState;
+5 -7
View File
@@ -1,11 +1,11 @@
//! OpenAI 路由共享错误响应。
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::{
dispatch::errors::{DispatchFailureClass, ResponseDispatchError},
@@ -117,12 +117,10 @@ pub fn responses_dispatch_error_response(error: ResponseDispatchError) -> Respon
pub fn responses_dispatch_error_response_ref(error: &ResponseDispatchError) -> Response {
if let ResponseDispatchError::Upstream(CodexClientError::Upstream { status, body, .. }) = error
&& status.is_client_error()
&& let Ok(body) = serde_json::from_str::<Value>(body)
{
if status.is_client_error() {
if let Ok(body) = serde_json::from_str::<Value>(body) {
return (*status, Json(body)).into_response();
}
}
return (*status, Json(body)).into_response();
}
match error {
+3 -3
View File
@@ -1,15 +1,15 @@
//! OpenAI 模型列表以及模型处理器。
use axum::{
Json,
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::{
api::client::auth::authorize_client_api_key, api::AppState, models::catalog::ModelCatalog,
api::AppState, api::client::auth::authorize_client_api_key, models::catalog::ModelCatalog,
};
use super::errors::{missing_client_api_key_response, model_not_found_response};
+5 -9
View File
@@ -2,17 +2,17 @@
//! 做最小 patch 后交给调度层,请求语义原样透传上游。
use axum::{
Extension, Json,
body::{Body, Bytes},
extract::State,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
Extension, Json,
};
use serde_json::{Map, Value};
use crate::{
api::middleware::request_id::{ClientIp, RequestId},
api::AppState,
api::middleware::request_id::{ClientIp, RequestId},
dispatch::{errors::ResponseDispatchError, service::ResponseDispatchStream},
upstream::openai::protocol::{
responses::{CodexCompactRequest, CodexResponsesRequest},
@@ -449,15 +449,11 @@ pub(crate) fn user_agent_from_headers(headers: &HeaderMap) -> Option<String> {
}
fn openai_subagent_from_headers(headers: &HeaderMap) -> Option<String> {
header_string(headers, OPENAI_SUBAGENT_HEADER).and_then(|value| {
if matches!(
header_string(headers, OPENAI_SUBAGENT_HEADER).filter(|value| {
matches!(
value.as_str(),
"review" | "compact" | "memory_consolidation" | "collab_spawn"
) {
Some(value)
} else {
None
}
)
})
}
+1 -1
View File
@@ -1,9 +1,9 @@
//! OpenAI 兼容 API 路由。
use axum::{
Router,
extract::DefaultBodyLimit,
routing::{get, post},
Router,
};
use crate::api::AppState;
+1 -1
View File
@@ -5,8 +5,8 @@
use axum::{
body::Body,
http::{
header::{CACHE_CONTROL, CONTENT_TYPE},
StatusCode,
header::{CACHE_CONTROL, CONTENT_TYPE},
},
response::{IntoResponse, Response},
};
+1 -1
View File
@@ -4,7 +4,7 @@ use std::net::{IpAddr, SocketAddr};
use std::sync::LazyLock;
use axum::{
extract::{connect_info::ConnectInfo, Request},
extract::{Request, connect_info::ConnectInfo},
http::{HeaderMap, HeaderValue},
middleware::Next,
response::Response,
+1 -1
View File
@@ -10,7 +10,7 @@ use tower_http::{
classify::{ServerErrorsAsFailures, ServerErrorsFailureClass, SharedClassifier},
trace::TraceLayer,
};
use tracing::{debug, info, warn, Span};
use tracing::{Span, debug, info, warn};
use crate::api::middleware::request_id::{ClientIp, RequestId};
+3 -1
View File
@@ -6,7 +6,7 @@ use std::{
};
use async_trait::async_trait;
use axum::{extract::State, http::StatusCode, middleware, routing::get, Router};
use axum::{Router, extract::State, http::StatusCode, middleware, routing::get};
use crate::{
api::{
@@ -26,6 +26,7 @@ use crate::{
account_usage::query::AccountUsageQueryService, ops::query::OpsQueryService,
usage::query::UsageQueryService,
},
update::service::SystemUpdateService,
upstream::openai::{fingerprint::RuntimeFingerprint, token_client::OpenAiTokenClient},
};
@@ -49,6 +50,7 @@ pub struct ApiServices {
pub session_affinity: Arc<SessionAffinityService>,
pub fingerprint: RuntimeFingerprint,
pub process_control: Arc<dyn ProcessControl>,
pub system_update: Arc<SystemUpdateService>,
}
/// 由 bootstrap 实现的进程生命周期控制端口。
+1 -1
View File
@@ -1,6 +1,6 @@
use std::{collections::BTreeMap, env, path::Path};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::{Deserialize, Serialize};
use url::Url;
+3 -3
View File
@@ -1,11 +1,11 @@
use futures::TryStreamExt;
use sqlx::{types::Json, Postgres, Row, SqlitePool, Transaction};
use sqlx::{Postgres, Row, SqlitePool, Transaction, types::Json};
use crate::infra::identity::hash_credential;
use super::{
parse_cookie_expiration, parse_json, parse_optional_json, parse_optional_timestamp,
parse_timestamp, ImportSqliteError, ImportSqliteReport,
ImportSqliteError, ImportSqliteReport, parse_cookie_expiration, parse_json,
parse_optional_json, parse_optional_timestamp, parse_timestamp,
};
pub(super) async fn import_core_tables(
+1 -1
View File
@@ -8,8 +8,8 @@ use std::{collections::BTreeMap, fmt, path::Path};
use chrono::{DateTime, Utc};
use serde_json::Value;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
PgPool, Postgres, SqlitePool, Transaction,
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
};
use thiserror::Error;
@@ -1,9 +1,9 @@
use futures::TryStreamExt;
use serde_json::Value;
use sqlx::{types::Json, Postgres, Row, SqlitePool, Transaction};
use sqlx::{Postgres, Row, SqlitePool, Transaction, types::Json};
use super::{
normalized_dimension, parse_json, parse_timestamp, ImportSqliteError, ImportSqliteReport,
ImportSqliteError, ImportSqliteReport, normalized_dimension, parse_json, parse_timestamp,
};
pub(super) async fn import_telemetry_tables(
+11 -6
View File
@@ -26,7 +26,7 @@ use crate::{
infra::{
database::connect,
identity::AccountPseudonymizer,
logging::{init_tracing, LogError, LogGuard, RotationConfig, TracingConfig},
logging::{LogError, LogGuard, RotationConfig, TracingConfig, init_tracing},
paths::{ensure_data_dir, load_or_create_identity_secret},
redis::RedisConnection,
},
@@ -36,7 +36,7 @@ use crate::{
store::{ModelSnapshotStore, RedisModelSnapshotStore},
types::ModelConfig,
},
settings::{service::SettingsService, SettingsSnapshot},
settings::{SettingsSnapshot, service::SettingsService},
telemetry::{
account_usage::query::AccountUsageQueryService,
account_usage::store::{AccountUsageStore, PgAccountUsageStore},
@@ -45,14 +45,15 @@ use crate::{
ops::store::PgOpsErrorLogStore,
recorder::Recorder,
usage::query::UsageQueryService,
usage::store::{PgUsageRecordStore, DEFAULT_USAGE_RECORD_CAPTURE_BODY},
usage::store::{DEFAULT_USAGE_RECORD_CAPTURE_BODY, PgUsageRecordStore},
},
update::service::SystemUpdateService,
upstream::openai::{
fingerprint::{Fingerprint, PgFingerprintStore, RuntimeFingerprint},
token_client::{default_openai_token_client, OpenAiTokenClient, TokenClientConfig},
token_client::{OpenAiTokenClient, TokenClientConfig, default_openai_token_client},
transport::{
build_reqwest_client, tls::CustomCaError, CodexBackendClient, CodexModelCatalogClient,
CodexWebSocketPool, CodexWebSocketPoolConfig,
CodexBackendClient, CodexModelCatalogClient, CodexWebSocketPool,
CodexWebSocketPoolConfig, build_reqwest_client, tls::CustomCaError,
},
},
};
@@ -117,6 +118,7 @@ pub struct Services {
pub websocket_pool: Option<Arc<CodexWebSocketPool>>,
pub fingerprint: RuntimeFingerprint,
pub account_pseudonymizer: Arc<AccountPseudonymizer>,
pub system_update: Arc<SystemUpdateService>,
pub background_tasks: BackgroundTaskStores,
pub(crate) account_pool_static: AccountPoolStaticSettings,
}
@@ -304,6 +306,7 @@ impl Services {
account_identity,
cloudflare: cloudflare_recovery,
}));
let system_update = Arc::new(SystemUpdateService::from_env());
Ok(Self {
database,
@@ -327,6 +330,7 @@ impl Services {
websocket_pool,
fingerprint,
account_pseudonymizer,
system_update,
background_tasks: stores,
account_pool_static,
})
@@ -362,6 +366,7 @@ impl From<&Services> for crate::api::router::ApiServices {
session_affinity: services.session_affinity.clone(),
fingerprint: services.fingerprint.clone(),
process_control: Arc::new(crate::bootstrap::shutdown::RuntimeProcessControl),
system_update: services.system_update.clone(),
}
}
}
+1 -1
View File
@@ -3,8 +3,8 @@
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
OnceLock,
atomic::{AtomicBool, Ordering},
},
};
+1 -1
View File
@@ -7,7 +7,7 @@ use tracing::{debug, info, warn};
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
};
/// 支持按时间清理过期记录的存储。
+2 -2
View File
@@ -7,8 +7,8 @@ use tokio::task::JoinHandle;
use super::{
cookie_cleanup::CookieCleanupTask,
fingerprint_update::{
start_fingerprint_update_task, CODEX_DESKTOP_APPCAST_URL,
DEFAULT_EXTRACTED_FINGERPRINT_PATH,
CODEX_DESKTOP_APPCAST_URL, DEFAULT_EXTRACTED_FINGERPRINT_PATH,
start_fingerprint_update_task,
},
model_refresh::ModelRefreshTask,
quota_refresh::QuotaRefreshTask,
@@ -5,12 +5,12 @@ use std::path::PathBuf;
use tracing::{info, warn};
use crate::upstream::openai::fingerprint::{
PgFingerprintStore, RuntimeFingerprint, UpdateChecker, APPCAST_POLL_INTERVAL,
APPCAST_POLL_INTERVAL, PgFingerprintStore, RuntimeFingerprint, UpdateChecker,
};
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
};
/// Codex Desktop 官方 appcast 地址。
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::models::service::{
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner, PeriodicTaskStartup},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, PeriodicTaskStartup, spawn_periodic_task},
};
/// 模型刷新任务接线器。
+1 -1
View File
@@ -19,7 +19,7 @@ use crate::{
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
};
/// 主动配额刷新后台任务。
@@ -13,7 +13,7 @@ use crate::{
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner, TaskFuture},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, TaskFuture, spawn_periodic_task},
};
const RETENTION_TRIM_INTERVAL_SECS: u64 = 60 * 60;
+1 -1
View File
@@ -10,7 +10,7 @@ use crate::upstream::openai::token_client::TokenRefresher;
use super::{
coordinator::SchedulerHandle,
periodic::{spawn_periodic_task, PeriodicTaskConfig, PeriodicTaskRunner},
periodic::{PeriodicTaskConfig, PeriodicTaskRunner, spawn_periodic_task},
};
const DEFAULT_INTERVAL_SECS: u64 = 60;
+5 -6
View File
@@ -10,7 +10,7 @@ use crate::{
},
upstream::openai::protocol::{
events::TokenUsage,
responses::{completed_response_metadata, CodexResponsesRequest},
responses::{CodexResponsesRequest, completed_response_metadata},
},
};
@@ -146,12 +146,11 @@ fn build_variant_identity(request: &CodexResponsesRequest) -> Option<String> {
if let Some(window_id) = non_empty_str(request.codex_window_id.as_deref()) {
parts.push(format!("window:{window_id}"));
}
if request.explicit_prompt_cache_key
|| non_empty_str(request.client_conversation_id.as_deref()).is_some()
if (request.explicit_prompt_cache_key
|| non_empty_str(request.client_conversation_id.as_deref()).is_some())
&& let Some(anchor) = derive_stable_conversation_key(request)
{
if let Some(anchor) = derive_stable_conversation_key(request) {
parts.push(format!("anchor:{anchor}"));
}
parts.push(format!("anchor:{anchor}"));
}
(!parts.is_empty()).then(|| parts.join("\0"))
+1 -1
View File
@@ -1,7 +1,7 @@
//! Shared upstream error classification for dispatch routes.
use chrono::{DateTime, Duration, Utc};
use serde_json::{json, Map, Value};
use serde_json::{Map, Value, json};
use thiserror::Error;
use crate::{
+15 -15
View File
@@ -1,19 +1,19 @@
use std::time::Instant;
use serde_json::{json, Map, Value};
use serde_json::{Map, Value, json};
use crate::{
dispatch::errors::{
backend_transport_name, insert_dispatch_error_metadata,
insert_upstream_diagnostics_metadata, upstream_error_http_status, DispatchErrorMetadata,
DispatchErrorMetadata, backend_transport_name, insert_dispatch_error_metadata,
insert_upstream_diagnostics_metadata, upstream_error_http_status,
},
infra::time::elapsed_millis_i64,
telemetry::{
ops::types::OpsErrorLog,
recorder::{
enrich_event_route_metadata, enrich_usage_record_identity,
event_kind as response_event_kind, reasoning_effort_from_request,
record_dispatch_error_event, DispatchErrorLogRecord, Recorder,
DispatchErrorLogRecord, Recorder, enrich_event_route_metadata,
enrich_usage_record_identity, event_kind as response_event_kind,
reasoning_effort_from_request, record_dispatch_error_event,
},
usage::types::UsageRecordLevel,
},
@@ -28,10 +28,10 @@ use crate::{
use super::{
errors::{
dispatch_error_metadata, enrich_response_dispatch_error_metadata, ResponseDispatchError,
ResponseDispatchError, dispatch_error_metadata, enrich_response_dispatch_error_metadata,
},
stream::{
live::{latest_response_id, LiveResponseStreamContext},
live::{LiveResponseStreamContext, latest_response_id},
sse_failure::{status_code_for_stream_failure, stream_failure_metadata},
trace::{ResponseDispatchAttempt, ResponseDispatchTrace},
},
@@ -236,13 +236,13 @@ pub(super) async fn record_prefetched_response_stream_failure_event(
event.model = Some(record.model.to_string());
event.status_code = Some(event_status_code);
event.latency_ms = Some(elapsed_millis_i64(record.started_at));
if !record.rate_limit_headers.is_empty() {
if let Some(object) = metadata.as_object_mut() {
object.insert(
"rateLimitHeaders".to_string(),
serde_json::json!(record.rate_limit_headers),
);
}
if !record.rate_limit_headers.is_empty()
&& let Some(object) = metadata.as_object_mut()
{
object.insert(
"rateLimitHeaders".to_string(),
serde_json::json!(record.rate_limit_headers),
);
}
event.metadata = metadata;
if let Err(error) = record.recorder.record_error(event).await {
@@ -12,9 +12,9 @@ use crate::{
recovery::{
auth::{auth_failure_account_status, is_auth_upstream_error},
cloudflare::{
cloudflare_challenge_error_message, cloudflare_path_block_error_message,
is_cloudflare_challenge_upstream_error, is_cloudflare_path_block_upstream_error,
CloudflareRecovery,
CloudflareRecovery, cloudflare_challenge_error_message,
cloudflare_path_block_error_message, is_cloudflare_challenge_upstream_error,
is_cloudflare_path_block_upstream_error,
},
exhaustion::AccountExhaustionTracker,
},
@@ -26,7 +26,7 @@ use crate::{
fleet::{account::AccountStatus, pool::AccountPoolService},
upstream::openai::{
protocol::responses::ResponsesSseFailure,
transport::{is_banned_upstream_error, CodexClientError},
transport::{CodexClientError, is_banned_upstream_error},
},
};
+1 -1
View File
@@ -2,7 +2,7 @@
use crate::{
fleet::account::AccountStatus,
upstream::openai::transport::{is_banned_auth_signal, CodexClientError},
upstream::openai::transport::{CodexClientError, is_banned_auth_signal},
};
pub(crate) fn is_auth_upstream_error(error: &CodexClientError) -> bool {
+1 -1
View File
@@ -5,9 +5,9 @@ use serde_json::Value;
use crate::{
dispatch::affinity::{
SessionAffinityEntry, SessionAffinityService,
service::{MAX_REPLAY_DEPTH, MAX_REPLAY_SESSION_BYTES, MAX_REPLAY_SNAPSHOT_BYTES},
types::ResponseReplaySnapshot,
SessionAffinityEntry, SessionAffinityService,
},
upstream::openai::protocol::responses::{CodexResponsesRequest, StreamCommitPolicy},
};
+11 -11
View File
@@ -12,7 +12,7 @@ use std::{pin::Pin, sync::Arc, time::Instant};
use bytes::Bytes;
use chrono::Utc;
use futures::stream::Stream;
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::{
dispatch::{
@@ -31,9 +31,9 @@ use crate::{
history::HistoryRecoveryPlan,
},
upstream_call::{
create_response_with_account_retrying_5xx, verify_acquired_quota_if_required,
AccountUpstreamContext, QuotaVerificationContext, QuotaVerificationDecision,
QUOTA_VERIFY_LIMIT_REACHED_MESSAGE,
AccountUpstreamContext, QUOTA_VERIFY_LIMIT_REACHED_MESSAGE, QuotaVerificationContext,
QuotaVerificationDecision, create_response_with_account_retrying_5xx,
verify_acquired_quota_if_required,
},
},
fleet::{
@@ -42,16 +42,16 @@ use crate::{
},
models::service::ModelService,
telemetry::{
recorder::{reasoning_effort_from_request, record_response_event, Recorder},
recorder::{Recorder, reasoning_effort_from_request, record_response_event},
usage::types::ResponseUsageRecord,
},
upstream::openai::{
protocol::{
events::TokenUsage,
responses::{response_from_codex_sse, CodexResponsesRequest, CollectedResponse},
responses::{CodexResponsesRequest, CollectedResponse, response_from_codex_sse},
},
transport::{
backend_transport_for_response_request, CodexBackendClient, CodexBackendResponse,
CodexBackendClient, CodexBackendResponse, backend_transport_for_response_request,
},
},
};
@@ -59,11 +59,11 @@ use crate::{
use super::{
errors::{ResponseDispatchError, ResponseDispatchStreamError},
recording::{
insert_response_status_metadata, insert_response_trace_metadata,
insert_response_upstream_diagnostics, insert_websocket_pool_decision,
record_response_dispatch_error_event, record_response_upstream_error_event,
ResponseDispatchErrorDetails, ResponseDispatchErrorEventRecord,
ResponseUpstreamErrorEventRecord,
ResponseUpstreamErrorEventRecord, insert_response_status_metadata,
insert_response_trace_metadata, insert_response_upstream_diagnostics,
insert_websocket_pool_decision, record_response_dispatch_error_event,
record_response_upstream_error_event,
},
stream::{
sse_failure::is_history_recovery_sse_failure,
+9 -9
View File
@@ -7,14 +7,14 @@ use crate::{
affinity::prepare_variant_identity,
attempts::AccountAttemptLedger,
errors::{
backend_transport_name, is_continuation_busy_error, is_history_recovery_upstream_error,
is_retryable_account_transport_error, upstream_error_body,
upstream_error_set_cookie_headers, ResponseDispatchError,
ResponseDispatchError, backend_transport_name, is_continuation_busy_error,
is_history_recovery_upstream_error, is_retryable_account_transport_error,
upstream_error_body, upstream_error_set_cookie_headers,
},
recording::{
record_prefetched_response_stream_failure_event, record_response_upstream_error_event,
ResponseDispatchErrorDetails, ResponseStreamFailureEventRecord,
ResponseUpstreamErrorEventRecord,
ResponseUpstreamErrorEventRecord, record_prefetched_response_stream_failure_event,
record_response_upstream_error_event,
},
recovery::{
account_failure::{isolate_rotatable_account_failure, isolate_sse_account_failure},
@@ -23,7 +23,7 @@ use crate::{
},
service::{ResponseDispatchService, ResponseDispatchStream},
stream::{
live::{spawn_live_response_stream, LiveResponseStreamContext},
live::{LiveResponseStreamContext, spawn_live_response_stream},
prefetch::prefetch_first_sse_chunk,
sse_failure::{
first_sse_failure, is_history_recovery_sse_failure, sse_failure_error_body,
@@ -31,9 +31,9 @@ use crate::{
trace::ResponseDispatchTrace,
},
upstream_call::{
create_response_stream_with_account_retrying_5xx, verify_acquired_quota_if_required,
AccountUpstreamContext, QuotaVerificationContext, QuotaVerificationDecision,
QUOTA_VERIFY_LIMIT_REACHED_MESSAGE,
AccountUpstreamContext, QUOTA_VERIFY_LIMIT_REACHED_MESSAGE, QuotaVerificationContext,
QuotaVerificationDecision, create_response_stream_with_account_retrying_5xx,
verify_acquired_quota_if_required,
},
},
fleet::pool::AccountAcquireRequest,
+8 -9
View File
@@ -6,14 +6,14 @@ use std::{
};
use bytes::Bytes;
use futures::{stream::Stream, StreamExt};
use futures::{StreamExt, stream::Stream};
use serde_json::Value;
use tokio::sync::{mpsc, oneshot};
use crate::{
dispatch::{
affinity::{resolve::record_response_affinity, SessionAffinityService},
errors::{backend_transport_name, ResponseDispatchStreamError},
affinity::{SessionAffinityService, resolve::record_response_affinity},
errors::{ResponseDispatchStreamError, backend_transport_name},
recording::{
insert_first_token_ms, live_response_rate_limit_headers, live_response_turn_state,
record_live_response_stream_event,
@@ -28,13 +28,13 @@ use crate::{
protocol::{
events::extract_sse_usage,
responses::{
CodexResponsesRequest, CollectedResponse,
reconvert_responses_sse_event_tuple_values, response_from_codex_sse,
response_sse_event_is_terminal, update_first_response_output_ms,
CodexResponsesRequest, CollectedResponse,
},
sse::{
encode_sse_event, parse_sse_events, response_failed_sse_event_with_id,
sse_body_has_done, sse_frame_end, DONE_SSE_FRAME,
DONE_SSE_FRAME, encode_sse_event, parse_sse_events,
response_failed_sse_event_with_id, sse_body_has_done, sse_frame_end,
},
},
transport::{
@@ -47,9 +47,8 @@ use crate::{
use super::{
sse_failure::{
status_code_for_stream_failure, stream_failure_metadata, stream_failure_source,
synthetic_stream_disconnected_detail, STREAM_DISCONNECTED_CODE,
STREAM_DISCONNECTED_MESSAGE,
STREAM_DISCONNECTED_CODE, STREAM_DISCONNECTED_MESSAGE, status_code_for_stream_failure,
stream_failure_metadata, stream_failure_source, synthetic_stream_disconnected_detail,
},
trace::ResponseDispatchAttempt,
};
+1 -1
View File
@@ -2,7 +2,7 @@ use bytes::Bytes;
use futures::StreamExt;
use crate::upstream::openai::{
protocol::sse::{sse_frame_end, SseError},
protocol::sse::{SseError, sse_frame_end},
transport::CodexBackendSseStream,
};
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::{
upstream::openai::{
protocol::{
events::TokenUsage,
responses::{response_from_codex_sse, CollectedResponse, ResponsesSseFailure},
responses::{CollectedResponse, ResponsesSseFailure, response_from_codex_sse},
sse::SseError,
},
transport::is_banned_auth_signal,
+14 -14
View File
@@ -3,29 +3,30 @@
use std::{future::Future, time::Instant};
use chrono::Utc;
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::{
dispatch::{
affinity::{AccountIdentityService, AccountScopedIdentity},
attempts::AccountAttemptLedger,
errors::{
is_model_unsupported_upstream_error, is_quota_exhausted_upstream_error,
is_rate_limit_upstream_error, is_retryable_account_transport_error,
is_retryable_upstream_5xx_error, rate_limit_cooldown_until, upstream_error_body,
upstream_error_http_status, upstream_error_set_cookie_headers, ResponseDispatchError,
ResponseDispatchError, is_model_unsupported_upstream_error,
is_quota_exhausted_upstream_error, is_rate_limit_upstream_error,
is_retryable_account_transport_error, is_retryable_upstream_5xx_error,
rate_limit_cooldown_until, upstream_error_body, upstream_error_http_status,
upstream_error_set_cookie_headers,
},
recording::{
insert_response_status_metadata, insert_response_trace_metadata,
insert_response_upstream_diagnostics, record_response_dispatch_error_event,
ResponseDispatchErrorEventRecord,
ResponseDispatchErrorEventRecord, insert_response_status_metadata,
insert_response_trace_metadata, insert_response_upstream_diagnostics,
record_response_dispatch_error_event,
},
recovery::{
auth::{auth_failure_account_status, is_auth_upstream_error},
cloudflare::{
cloudflare_challenge_error_message, cloudflare_path_block_error_message,
is_cloudflare_challenge_upstream_error, is_cloudflare_path_block_upstream_error,
CloudflareRecovery,
CloudflareRecovery, cloudflare_challenge_error_message,
cloudflare_path_block_error_message, is_cloudflare_challenge_upstream_error,
is_cloudflare_path_block_upstream_error,
},
exhaustion::AccountExhaustionTracker,
},
@@ -50,9 +51,8 @@ use crate::{
responses::{CodexCompactRequest, CodexResponsesRequest},
},
transport::{
is_banned_upstream_error, CodexBackendClient, CodexBackendResponse,
CodexBackendStreamingResponse, CodexClientError, CodexCompactResponse,
CodexRequestContext,
CodexBackendClient, CodexBackendResponse, CodexBackendStreamingResponse,
CodexClientError, CodexCompactResponse, CodexRequestContext, is_banned_upstream_error,
},
},
};
+1 -1
View File
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
use serde_json::{Map, Value};
use crate::fleet::account::AccountStatus;
use crate::fleet::refresh::{jwt_expiry, JwtExpiry};
use crate::fleet::refresh::{JwtExpiry, jwt_expiry};
/// 账号导入条目。
#[derive(Debug, Clone)]
+1 -1
View File
@@ -1,8 +1,8 @@
use secrecy::ExposeSecret;
use super::{
types::{ExportedAccount, ExportedAccounts},
AccountManageError, AccountManageService,
types::{ExportedAccount, ExportedAccounts},
};
use crate::{fleet::store::StoredAccount, infra::time::china_rfc3339_str};
+9 -10
View File
@@ -2,12 +2,12 @@ use chrono::Utc;
use secrecy::SecretString;
use super::{
types::{
import_quota_plan_type, import_status_from_usage_error, import_usage_plan_type,
import_usage_string, AccountManageError, ImportSupplementalAccountInfo,
ImportSupplementalNeeds, ImportedAccountState, ImportedAccounts, ResolvedImportTokens,
},
AccountManageService,
types::{
AccountManageError, ImportSupplementalAccountInfo, ImportSupplementalNeeds,
ImportedAccountState, ImportedAccounts, ResolvedImportTokens, import_quota_plan_type,
import_status_from_usage_error, import_usage_plan_type, import_usage_string,
},
};
impl AccountManageService {
@@ -304,15 +304,14 @@ impl AccountManageService {
let chatgpt_account_id = crate::fleet::import::normalize_nonempty_str(account_id);
let chatgpt_user_id = crate::fleet::import::normalize_nonempty_str(user_id);
if let Some(chatgpt_account_id) = chatgpt_account_id {
if let Some(existing) = self
if let Some(chatgpt_account_id) = chatgpt_account_id
&& let Some(existing) = self
.store
.find_by_chatgpt_identity(chatgpt_account_id, chatgpt_user_id)
.await
.map_err(|_| AccountManageError::Inspect)?
{
return Ok(existing.id);
}
{
return Ok(existing.id);
}
Ok(provided_id.unwrap_or_else(|| crate::fleet::import::normalized_account_id(None)))
+7 -7
View File
@@ -1,10 +1,10 @@
use std::{
collections::{hash_map::DefaultHasher, HashSet},
collections::{HashSet, hash_map::DefaultHasher},
hash::{Hash, Hasher},
};
use chrono::{DateTime, Duration, Utc};
use futures::{stream, StreamExt};
use futures::{StreamExt, stream};
use secrecy::{ExposeSecret, SecretString};
use tokio::time::sleep;
use uuid::Uuid;
@@ -16,12 +16,12 @@ use crate::{
};
use super::{
types::{
parse_account_status, stored_to_admin_metadata, AccountHealthCheck, AccountManageError,
AccountRefreshOutcome, AccountRefreshResult, AccountUpdate, BatchDeleteAccounts,
ManagedAccount, ManualCreateTokens,
},
AccountManageService,
types::{
AccountHealthCheck, AccountManageError, AccountRefreshOutcome, AccountRefreshResult,
AccountUpdate, BatchDeleteAccounts, ManagedAccount, ManualCreateTokens,
parse_account_status, stored_to_admin_metadata,
},
};
const ADMIN_REFRESH_LEASE_TTL_SECONDS: i64 = 5 * 60;
+2 -2
View File
@@ -1,6 +1,6 @@
use std::{collections::HashMap, fmt::Write as _, sync::Arc};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Duration, Utc};
use rand::Rng;
use reqwest::{Client, StatusCode, Url};
@@ -9,8 +9,8 @@ use sha2::{Digest, Sha256};
use tokio::sync::Mutex;
use super::{
types::{AccountManageError, OAuthAuthorizeResult, OAuthExchangeInput},
AccountManageService,
types::{AccountManageError, OAuthAuthorizeResult, OAuthExchangeInput},
};
const OPENAI_OAUTH_AUTHORIZE_ENDPOINT: &str = "https://auth.openai.com/oauth/authorize";
+20 -20
View File
@@ -3,7 +3,7 @@ use std::{convert::Infallible, pin::Pin};
use bytes::Bytes;
use futures::{Stream, StreamExt};
use secrecy::ExposeSecret;
use serde_json::{json, Value};
use serde_json::{Value, json};
use tokio::sync::mpsc;
use crate::{
@@ -12,15 +12,15 @@ use crate::{
upstream::openai::{
protocol::{
responses::{CodexResponsesRequest, ResponsesSseFailure},
sse::{encode_sse_event, parse_sse_events, SseEvent},
sse::{SseEvent, encode_sse_event, parse_sse_events},
},
transport::{
is_banned_auth_signal, is_banned_upstream_error, CodexClientError, CodexRequestContext,
CodexClientError, CodexRequestContext, is_banned_auth_signal, is_banned_upstream_error,
},
},
};
use super::{types::AccountManageError, AccountManageService};
use super::{AccountManageService, types::AccountManageError};
pub(super) type AccountTestStream = Pin<Box<dyn Stream<Item = Result<Bytes, Infallible>> + Send>>;
@@ -221,14 +221,14 @@ impl AccountManageService {
}
}
if matches!(status, AccountStatus::Expired | AccountStatus::Banned) {
if let Err(error) = self.store.set_next_refresh_at(account_id, None).await {
tracing::warn!(
account_id,
error = %error,
"failed to clear token refresh schedule after connection test"
);
}
if matches!(status, AccountStatus::Expired | AccountStatus::Banned)
&& let Err(error) = self.store.set_next_refresh_at(account_id, None).await
{
tracing::warn!(
account_id,
error = %error,
"failed to clear token refresh schedule after connection test"
);
}
self.sync_account_pool_best_effort(account_id, "connection test")
.await;
@@ -297,10 +297,10 @@ async fn process_upstream_test_stream(
}
}
if !buffer.trim().is_empty() {
if let Some(outcome) = process_sse_frame(tx, &buffer).await {
return outcome;
}
if !buffer.trim().is_empty()
&& let Some(outcome) = process_sse_frame(tx, &buffer).await
{
return outcome;
}
AccountTestOutcome::error("Stream ended before response.completed", None)
@@ -332,10 +332,10 @@ async fn process_sse_event(
};
match value.get("type").and_then(Value::as_str) {
Some("response.output_text.delta") => {
if let Some(delta) = value.get("delta").and_then(Value::as_str) {
if !delta.is_empty() {
send_test_event(tx, json!({ "type": "content", "text": delta })).await;
}
if let Some(delta) = value.get("delta").and_then(Value::as_str)
&& !delta.is_empty()
{
send_test_event(tx, json!({ "type": "content", "text": delta })).await;
}
None
}
+1 -1
View File
@@ -1,6 +1,6 @@
use secrecy::ExposeSecret;
use super::{types::AccountManageError, AccountManageService};
use super::{AccountManageService, types::AccountManageError};
impl AccountManageService {
async fn usage_cookie_header(&self, account_id: &str) -> Option<String> {
+19 -21
View File
@@ -13,10 +13,10 @@ use indexmap::IndexMap;
use thiserror::Error;
use crate::fleet::quota::{quota_snapshot_limit_reached, quota_snapshot_reset_at};
use crate::fleet::refresh::{jwt_expiry, JwtExpiry};
use crate::fleet::refresh::{JwtExpiry, jwt_expiry};
use crate::fleet::scheduler::{
candidates::{self, CandidateFilter, CandidateRequest},
AccountScheduler, ScoreWeights,
candidates::{self, CandidateFilter, CandidateRequest},
};
use crate::fleet::store::AccountStore;
use crate::fleet::{
@@ -27,7 +27,7 @@ use crate::telemetry::account_usage::store::{
AccountUsageDelta, AccountUsageSnapshot, AccountUsageStore, AccountUsageWindow,
};
use crate::upstream::openai::protocol::events::{
parse_rate_limit_headers, rate_limit_quota, TokenUsage as CodexTokenUsage,
TokenUsage as CodexTokenUsage, parse_rate_limit_headers, rate_limit_quota,
};
pub use crate::fleet::scheduler::RotationStrategy;
@@ -457,21 +457,20 @@ impl AccountPoolService {
"failed to persist refreshed runtime account state"
);
}
if refreshed.sync_usage_window {
if let Err(error) = self
if refreshed.sync_usage_window
&& let Err(error) = self
.usage_store
.sync_runtime_window(
&refreshed.account.id,
account_usage_window(&refreshed.account),
)
.await
{
tracing::error!(
account_id = %refreshed.account.id,
error = %error,
"failed to persist refreshed runtime account usage window"
);
}
{
tracing::error!(
account_id = %refreshed.account.id,
error = %error,
"failed to persist refreshed runtime account usage window"
);
}
}
}
@@ -641,18 +640,17 @@ impl AccountPoolService {
}
};
if let Some(reset_at) = reset_at {
if let Err(error) = self
if let Some(reset_at) = reset_at
&& let Err(error) = self
.usage_store
.sync_rate_limit_window(account_id, reset_at, limit_window_seconds)
.await
{
tracing::error!(
account_id,
error = %error,
"failed to persist verified quota window"
);
}
{
tracing::error!(
account_id,
error = %error,
"failed to persist verified quota window"
);
}
let in_memory = {
+4 -5
View File
@@ -451,13 +451,12 @@ impl AccountPool {
});
let mut account_ids = Vec::with_capacity(ordered.len());
if let Some(preferred_account_id) = request.preferred_account_id.as_deref() {
if let Some(index) = ordered
if let Some(preferred_account_id) = request.preferred_account_id.as_deref()
&& let Some(index) = ordered
.iter()
.position(|account| account.id == preferred_account_id)
{
account_ids.push(ordered.remove(index).id);
}
{
account_ids.push(ordered.remove(index).id);
}
account_ids.extend(ordered.into_iter().map(|account| account.id));
AccountCandidateSnapshotWithStatusRefresh {
+1 -1
View File
@@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use chrono::Utc;
use thiserror::Error;
use tokio::time::{sleep, Instant};
use tokio::time::{Instant, sleep};
use tracing::warn;
use crate::fleet::{
+1 -1
View File
@@ -3,7 +3,7 @@
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde_json::{json, Value};
use serde_json::{Value, json};
const MONTH_WINDOW_MINUTES: u64 = 43_200;
const MONTH_WINDOW_SECONDS: u64 = 2_592_000;
+2 -2
View File
@@ -7,10 +7,10 @@ use std::{
time::Duration as StdDuration,
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Duration, TimeZone, Utc};
use serde_json::{Map, Value};
use tokio::sync::{watch, Semaphore};
use tokio::sync::{Semaphore, watch};
use crate::fleet::account::{Account, AccountStatus};
use crate::upstream::openai::token_client::{RefreshFailure, TokenPair, TokenRefresher};
+6 -6
View File
@@ -5,8 +5,8 @@ use std::{
future::Future,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex as StdMutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
@@ -30,12 +30,12 @@ use crate::upstream::openai::token_client::TokenRefresher;
use super::{
lease::{RedisRefreshLeaseStore, RedisRefreshLeaseStoreError},
policy::{
default_refresh_retry_delays, duration_until, is_permanent_refresh_failure_status,
jittered_refresh_at, jwt_expiration, same_refresh_time, scheduled_at_from_delay,
stable_jittered_duration, token_refresh_status_eligible, RefreshError, RefreshPolicy,
RefreshScheduler, RuntimeRefreshPolicy, TokenTimerSummary,
PERMANENT_FAILURE_CONFIRMATION_THRESHOLD, RECOVERY_DELAY_JITTER, RECOVERY_DELAY_SECONDS,
RETRY_DELAY_JITTER,
RETRY_DELAY_JITTER, RefreshError, RefreshPolicy, RefreshScheduler, RuntimeRefreshPolicy,
TokenTimerSummary, default_refresh_retry_delays, duration_until,
is_permanent_refresh_failure_status, jittered_refresh_at, jwt_expiration,
same_refresh_time, scheduled_at_from_delay, stable_jittered_duration,
token_refresh_status_eligible,
},
};
+2 -6
View File
@@ -6,8 +6,8 @@
//! 对齐 sub2api 的设计,避免选择热路径上的锁与 DB 往返。
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
/// EWMA 平滑系数(新样本权重)。与 sub2api 一致取 0.2。
const EWMA_ALPHA: f64 = 0.2;
@@ -59,11 +59,7 @@ impl AccountFeedback {
/// 读取 EWMA 样本;哨兵(NaN)返回 `None`。
fn load_sample(bits: &AtomicU64) -> Option<f64> {
let value = f64::from_bits(bits.load(Ordering::Relaxed));
if value.is_nan() {
None
} else {
Some(value)
}
if value.is_nan() { None } else { Some(value) }
}
/// 无锁 CAS 更新 EWMA:首个样本直接落值,其后按 alpha 混合。
@@ -12,7 +12,7 @@ use std::cmp::Ordering;
use crate::fleet::account::Account;
use crate::fleet::scheduler::{
compare_last_used, compare_window_reset, order_by, select_by, SelectionInput,
SelectionInput, compare_last_used, compare_window_reset, order_by, select_by,
};
/// QuotaResetPriority 策略选择。
@@ -11,7 +11,7 @@
use crate::fleet::account::Account;
use crate::fleet::scheduler::feedback::{FeedbackSample, FeedbackStats};
use crate::fleet::scheduler::{account_window_token_count, SelectionInput};
use crate::fleet::scheduler::{SelectionInput, account_window_token_count};
/// 配额封顶账号的惩罚分。远大于各因子加权和的理论上限(各因子 ∈ [0,1],权重和通常为
/// 个位数),保证未封顶账号总是优先于封顶账号。
@@ -202,11 +202,11 @@ impl NormalizationContext {
max_window_cached = max_window_cached.max(account.window_cached_tokens as f64);
max_window_requests = max_window_requests.max(account.window_request_count as f64);
if let Some(ttft) = sample.ttft_ms {
if ttft > 0.0 {
min_ttft_ms = min_ttft_ms.min(ttft);
max_ttft_ms = max_ttft_ms.max(ttft);
}
if let Some(ttft) = sample.ttft_ms
&& ttft > 0.0
{
min_ttft_ms = min_ttft_ms.min(ttft);
max_ttft_ms = max_ttft_ms.max(ttft);
}
if let Some(reset_at) = account.window_reset_at {
@@ -6,7 +6,7 @@
use std::cmp::Ordering;
use crate::fleet::account::Account;
use crate::fleet::scheduler::{compare_last_used, SelectionInput};
use crate::fleet::scheduler::{SelectionInput, compare_last_used};
/// 选中最近使用过的账号(`last_used_at` 最大者)。
///
+2 -2
View File
@@ -8,7 +8,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Row};
use thiserror::Error;
use crate::fleet::account::{Account, AccountStatus};
use crate::infra::json::{page_offset, NumberedPage};
use crate::infra::json::{NumberedPage, page_offset};
mod queries;
mod rows;
@@ -249,7 +249,7 @@ pub trait AccountStore: Send + Sync + 'static {
/// 更新账号状态。
async fn set_status(&self, account_id: &str, status: AccountStatus)
-> AccountStoreResult<bool>;
-> AccountStoreResult<bool>;
/// 读取账号当前配额 JSON。
async fn get_quota_json(&self, account_id: &str) -> AccountStoreResult<Option<String>>;
+2 -2
View File
@@ -2,7 +2,7 @@
use secrecy::SecretString;
use serde_json::Value;
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
use sqlx::{PgPool, Postgres, QueryBuilder, Row, postgres::PgRow};
use crate::{
fleet::{
@@ -13,9 +13,9 @@ use crate::{
};
use super::{
queries::{GET_POOL_ACCOUNT_SQL, LIST_POOL_ACCOUNTS_SQL},
AccountQuotaSnapshot, AccountStoreError, PgAccountStore, PgAccountStoreError,
PgAccountStoreResult, StoredAccount, StoredAccountMetadata,
queries::{GET_POOL_ACCOUNT_SQL, LIST_POOL_ACCOUNTS_SQL},
};
pub(super) async fn list_pool_accounts(
+1 -1
View File
@@ -3,7 +3,7 @@
use std::time::Duration;
use sha2::{Digest, Sha256};
use sqlx::{postgres::PgPoolOptions, PgPool, Postgres, Row, Transaction};
use sqlx::{PgPool, Postgres, Row, Transaction, postgres::PgPoolOptions};
const MIGRATIONS: &[Migration] = &[Migration {
version: 1,
+2 -2
View File
@@ -1,10 +1,10 @@
//! 身份认证相关原语:管理员密码哈希与 API Key 生成。
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use hmac::{Hmac, KeyInit, Mac};
use rand::Rng;
use sha2::{Digest, Sha256};
+11 -9
View File
@@ -16,10 +16,10 @@ use chrono::{DateTime, Duration, NaiveDate, Utc};
use thiserror::Error;
use tracing_appender::non_blocking::{ErrorCounter, NonBlockingBuilder, WorkerGuard};
use tracing_subscriber::{
EnvFilter,
fmt::{self as tracing_fmt, format::Writer, time::FormatTime},
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter,
};
use crate::infra::time::{china_date, china_rfc3339_millis};
@@ -459,14 +459,16 @@ impl DroppedLogMonitor {
let (shutdown_tx, shutdown_rx) = mpsc::channel();
let handle = thread::Builder::new()
.name("tracing-drop-monitor".to_string())
.spawn(move || loop {
match shutdown_rx.recv_timeout(DROP_MONITOR_INTERVAL) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
report_dropped_logs(&mut counters, &mut io::stderr().lock());
break;
}
Err(mpsc::RecvTimeoutError::Timeout) => {
report_dropped_logs(&mut counters, &mut io::stderr().lock());
.spawn(move || {
loop {
match shutdown_rx.recv_timeout(DROP_MONITOR_INTERVAL) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
report_dropped_logs(&mut counters, &mut io::stderr().lock());
break;
}
Err(mpsc::RecvTimeoutError::Timeout) => {
report_dropped_logs(&mut counters, &mut io::stderr().lock());
}
}
}
})?;
+1 -1
View File
@@ -1,6 +1,6 @@
//! Redis 运行态连接与键前缀。
use redis::{aio::ConnectionManager, AsyncCommands};
use redis::{AsyncCommands, aio::ConnectionManager};
/// Redis 运行态连接。
#[derive(Clone)]
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::infra::json::NumberedPage;
use super::{
store::PgClientKeyStore,
types::{
parse_client_key_status, BatchDeleteClientApiKeys, KeyManageError, ManagedClientApiKey,
BatchDeleteClientApiKeys, KeyManageError, ManagedClientApiKey, parse_client_key_status,
},
};
+1 -1
View File
@@ -3,8 +3,8 @@
use std::{
collections::BTreeMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
+1 -1
View File
@@ -9,7 +9,7 @@ use uuid::Uuid;
use crate::infra::{
identity::generate_client_api_key,
json::{page_offset, NumberedPage},
json::{NumberedPage, page_offset},
};
const CLIENT_KEY_SELECT_SQL: &str = r"select
+1 -1
View File
@@ -3,7 +3,7 @@
use std::{collections::BTreeMap, sync::Arc};
use subtle::ConstantTimeEq;
use tokio::sync::{watch, Mutex};
use tokio::sync::{Mutex, watch};
use crate::infra::identity::{generate_admin_api_key, hash_credential};
+1 -1
View File
@@ -3,7 +3,7 @@
use std::collections::BTreeMap;
use chrono::Utc;
use sqlx::{postgres::PgRow, PgPool, Row};
use sqlx::{PgPool, Row, postgres::PgRow};
use super::types::{ManagementApiKeyStatus, SettingsError, SettingsSnapshot};
+1 -1
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
use sqlx::{PgPool, Postgres, QueryBuilder, Row, postgres::PgRow};
use thiserror::Error;
use crate::infra::format::nonnegative_i64_to_u64;
+1 -1
View File
@@ -5,7 +5,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder, Row};
use thiserror::Error;
use crate::{
infra::json::{page_offset, NumberedPage},
infra::json::{NumberedPage, page_offset},
telemetry::{buckets::store::PgRequestBucketStore, ops::types::OpsErrorLog},
};
+9 -9
View File
@@ -12,8 +12,8 @@ use crate::{
usage::{
store::{PgUsageRecordStore, PgUsageRecordStoreError},
types::{
metadata_i64, metadata_service_tier, metadata_string as metadata_string_any,
ResponseUsageRecord, UsageRecord,
ResponseUsageRecord, UsageRecord, metadata_i64, metadata_service_tier,
metadata_string as metadata_string_any,
},
},
},
@@ -150,13 +150,13 @@ pub(crate) async fn record_response_event(record: ResponseUsageRecord<'_>) {
record.reasoning_effort,
record.service_tier,
);
if !record.rate_limit_headers.is_empty() {
if let Some(object) = metadata.as_object_mut() {
object.insert(
"rateLimitHeaders".to_string(),
serde_json::json!(record.rate_limit_headers),
);
}
if !record.rate_limit_headers.is_empty()
&& let Some(object) = metadata.as_object_mut()
{
object.insert(
"rateLimitHeaders".to_string(),
serde_json::json!(record.rate_limit_headers),
);
}
lift_success_fact_fields(&mut event, &mut metadata);
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
telemetry::{
billing,
usage::store::{
push_filter, PgUsageRecordStore, PgUsageRecordStoreResult, UsageRecordFilter,
PgUsageRecordStore, PgUsageRecordStoreResult, UsageRecordFilter, push_filter,
},
usage::types::UsageRecord,
},
+7 -11
View File
@@ -1,10 +1,10 @@
//! PostgreSQL 成功使用事实存储。
use super::query::{
count_usage_records, usage_account_usage, usage_breakdown, usage_record_account_email_map,
usage_record_from_row, usage_summary, usage_trend, UsageRecordAccountUsage,
UsageRecordBreakdown, UsageRecordEndpointSource, UsageRecordModelSource, UsageRecordSummary,
UsageRecordTrendPoint,
UsageRecordAccountUsage, UsageRecordBreakdown, UsageRecordEndpointSource,
UsageRecordModelSource, UsageRecordSummary, UsageRecordTrendPoint, count_usage_records,
usage_account_usage, usage_breakdown, usage_record_account_email_map, usage_record_from_row,
usage_summary, usage_trend,
};
use std::collections::HashMap;
@@ -14,7 +14,7 @@ use sqlx::{PgPool, Postgres, QueryBuilder};
use thiserror::Error;
use crate::{
infra::json::{page_offset, NumberedPage},
infra::json::{NumberedPage, page_offset},
telemetry::{buckets::store::PgRequestBucketStore, usage::types::UsageRecord},
};
@@ -221,12 +221,8 @@ insert into usage_records (
source: UsageRecordModelSource,
) -> PgUsageRecordStoreResult<Vec<UsageRecordBreakdown>> {
let expression = match source {
UsageRecordModelSource::Requested => {
"coalesce(nullif(requested_model, ''), model)"
}
UsageRecordModelSource::Upstream => {
"coalesce(nullif(upstream_model, ''), model)"
}
UsageRecordModelSource::Requested => "coalesce(nullif(requested_model, ''), model)",
UsageRecordModelSource::Upstream => "coalesce(nullif(upstream_model, ''), model)",
UsageRecordModelSource::Mapping => {
"coalesce(nullif(requested_model, ''), model) || ' -> ' || coalesce(nullif(upstream_model, ''), model)"
}
+16 -16
View File
@@ -10,7 +10,7 @@ use tar::Archive;
use crate::update::types::UpdateError;
use super::service::{internal_error, internal_error_with, SystemUpdateConfig, APP_BINARY_NAME};
use super::service::{APP_BINARY_NAME, SystemUpdateConfig, internal_error, internal_error_with};
#[derive(Debug)]
pub(super) struct ExtractedRelease {
@@ -125,22 +125,22 @@ pub(super) fn replace_release_files(
};
let binary_backup = backup_path_for(exe_path);
if binary_backup.exists() {
if let Err(error) = fs::remove_file(&binary_backup) {
let mut rollback_errors = Vec::new();
if web_replaced {
collect_rollback_error(
&mut rollback_errors,
"restore web assets",
restore_dir(web_dist_dir, &web_backup),
);
}
return Err(update_error_with_rollback(
"Failed to remove old binary backup",
error,
rollback_errors,
));
if binary_backup.exists()
&& let Err(error) = fs::remove_file(&binary_backup)
{
let mut rollback_errors = Vec::new();
if web_replaced {
collect_rollback_error(
&mut rollback_errors,
"restore web assets",
restore_dir(web_dist_dir, &web_backup),
);
}
return Err(update_error_with_rollback(
"Failed to remove old binary backup",
error,
rollback_errors,
));
}
if let Err(error) = move_file(exe_path, &binary_backup) {
let mut rollback_errors = Vec::new();
+5 -4
View File
@@ -12,14 +12,15 @@ use sha2::{Digest, Sha256};
use crate::update::types::UpdateError;
use super::service::{
bad_gateway_with, bad_request_with, emit_update_event, format_bytes, internal_error_with,
UpdateLogLevel,
UpdateEventSender, UpdateLogLevel, bad_gateway_with, bad_request_with, format_bytes,
internal_error_with,
};
#[derive(Debug, Clone, Copy)]
#[derive(Clone, Copy)]
pub(super) struct DownloadProgress<'a> {
pub operation_id: &'a str,
pub total_size: u64,
pub events: &'a UpdateEventSender,
}
pub(super) async fn download_file(
@@ -65,7 +66,7 @@ pub(super) async fn download_file(
if progress.total_size > 0
&& (percent >= next_progress || downloaded >= progress.total_size)
{
emit_update_event(
progress.events.emit(
UpdateLogLevel::Info,
Some(progress.operation_id),
Some("download"),
+25 -16
View File
@@ -13,13 +13,11 @@ use crate::update::types::UpdateError;
use super::{
download::validate_github_api_base,
service::{build_type_label, deployment_mode_label, SystemUpdateConfig, APP_BINARY_NAME},
service::{APP_BINARY_NAME, SystemUpdateConfig, build_type_label, deployment_mode_label},
};
const UPDATE_CACHE_TTL: Duration = Duration::from_secs(20 * 60);
static RELEASE_CACHE: Mutex<Option<CachedUpdateInfo>> = Mutex::const_new(None);
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct UpdateInfoData {
@@ -45,6 +43,18 @@ struct CachedUpdateInfo {
cached_at: Instant,
}
pub(super) struct ReleaseCache {
entry: Mutex<Option<CachedUpdateInfo>>,
}
impl Default for ReleaseCache {
fn default() -> Self {
Self {
entry: Mutex::const_new(None),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub(super) struct GitHubRelease {
pub tag_name: String,
@@ -64,6 +74,7 @@ pub(super) struct GitHubAsset {
}
pub(super) async fn check_latest_release(
cache: &ReleaseCache,
config: &SystemUpdateConfig,
force: bool,
) -> UpdateInfoData {
@@ -103,19 +114,17 @@ pub(super) async fn check_latest_release(
}
let cache_key = config.release_cache_key();
if !force {
if let Some(info) = cached_release_info(&cache_key).await {
return info;
}
if !force && let Some(info) = cached_release_info(cache, &cache_key).await {
return info;
}
match fetch_latest_release(&config.github_api_base, repository).await {
Ok(release) => {
let info = update_info_from_release(config, release);
cache_release_info(cache_key, &info).await;
cache_release_info(cache, cache_key, &info).await;
info
}
Err(error) => cached_release_info(&cache_key)
Err(error) => cached_release_info(cache, &cache_key)
.await
.unwrap_or_else(|| UpdateInfoData {
current_version: config.version.clone(),
@@ -224,24 +233,24 @@ pub(super) fn select_release_archive<'a>(
})
}
async fn cached_release_info(cache_key: &str) -> Option<UpdateInfoData> {
async fn cached_release_info(cache: &ReleaseCache, cache_key: &str) -> Option<UpdateInfoData> {
let mut info = {
let cache = RELEASE_CACHE.lock().await;
let cached = cache.as_ref()?;
let entry = cache.entry.lock().await;
let cached = entry.as_ref()?;
if cached.key != cache_key || cached.cached_at.elapsed() > UPDATE_CACHE_TTL {
return None;
}
let info = cached.info.clone();
drop(cache);
drop(entry);
info
};
info.cached = true;
Some(info)
}
async fn cache_release_info(cache_key: String, info: &UpdateInfoData) {
let mut cache = RELEASE_CACHE.lock().await;
*cache = Some(CachedUpdateInfo {
async fn cache_release_info(cache: &ReleaseCache, cache_key: String, info: &UpdateInfoData) {
let mut entry = cache.entry.lock().await;
*entry = Some(CachedUpdateInfo {
key: cache_key,
info: info.clone(),
cached_at: Instant::now(),
+386 -361
View File
@@ -4,26 +4,25 @@ use std::{
env, fmt, fs, io,
path::{Path, PathBuf},
process::Command,
sync::OnceLock,
};
use chrono::Utc;
use serde::Serialize;
use tokio::sync::{broadcast, Mutex};
use tokio::sync::{Mutex, broadcast};
use super::{
archive::{extract_release_archive, replace_release_files, rollback_release_update},
download::{
download_file, validate_download_url, validate_github_api_base, verify_checksum,
DownloadProgress,
DownloadProgress, download_file, validate_download_url, validate_github_api_base,
verify_checksum,
},
release::{
check_latest_release, fetch_latest_release, normalize_version_tag, select_release_archive,
update_info_from_release, GitHubRelease, UpdateInfoData,
GitHubRelease, ReleaseCache, UpdateInfoData, check_latest_release, fetch_latest_release,
normalize_version_tag, select_release_archive, update_info_from_release,
},
state::{
finish_operation, operation_id, read_state, set_operation_running, OperationLock,
SystemOperationKind, SystemUpdateStatusData,
OperationLock, SystemOperationKind, SystemUpdateStatusData, finish_operation, operation_id,
read_state, set_operation_running,
},
types::UpdateError,
};
@@ -35,9 +34,6 @@ const MAX_DOWNLOAD_SIZE: u64 = 500 * 1024 * 1024;
const RESTART_DELAY_ENV: &str = "CPR_RESTART_DELAY_MS";
const REPLACEMENT_START_DELAY_MS: &str = "1200";
static SYSTEM_OPERATION_LOCK: Mutex<()> = Mutex::const_new(());
static UPDATE_EVENT_SENDER: OnceLock<broadcast::Sender<SystemUpdateEvent>> = OnceLock::new();
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct VersionData {
@@ -96,145 +92,170 @@ impl SystemUpdateEvent {
}
#[derive(Debug, Clone)]
pub(super) struct SystemUpdateConfig {
pub(super) version: String,
pub(super) git_sha: String,
pub(super) build_time: String,
pub(super) deployment_mode: String,
pub(super) build_type: String,
pub(super) update_channel: String,
pub(super) update_repository: Option<String>,
pub(super) github_api_base: String,
pub(super) executable_path: Option<PathBuf>,
pub(super) web_dist_dir: PathBuf,
pub(super) update_state_file: PathBuf,
pub(super) update_lock_file: PathBuf,
pub(super) update_temp_dir: PathBuf,
pub struct SystemUpdateConfig {
pub version: String,
pub git_sha: String,
pub build_time: String,
pub deployment_mode: String,
pub build_type: String,
pub update_channel: String,
pub update_repository: Option<String>,
pub github_api_base: String,
pub executable_path: Option<PathBuf>,
pub web_dist_dir: PathBuf,
pub update_state_file: PathBuf,
pub update_lock_file: PathBuf,
pub update_temp_dir: PathBuf,
pub self_restart_enabled: bool,
}
pub(crate) async fn version_data() -> VersionData {
let config = SystemUpdateConfig::from_env();
let update_info = check_latest_release(&config, false).await;
config.version_data(&update_info)
pub struct SystemUpdateService {
config: SystemUpdateConfig,
operation_lock: Mutex<()>,
release_cache: ReleaseCache,
events: UpdateEventSender,
}
pub(crate) async fn update_detail(refresh: bool) -> UpdateInfoData {
let config = SystemUpdateConfig::from_env();
check_latest_release(&config, refresh).await
}
pub(crate) fn subscribe_update_events() -> broadcast::Receiver<SystemUpdateEvent> {
update_event_sender().subscribe()
}
pub(crate) async fn perform_update(
target_version: Option<String>,
) -> Result<UpdateStartedData, UpdateError> {
let _guard = SYSTEM_OPERATION_LOCK
.try_lock()
.map_err(|_| UpdateError::conflict("System update already running"))?;
let config = SystemUpdateConfig::from_env();
if let Some(reason) = config.update_support_error() {
emit_terminal_update_event(
UpdateLogLevel::Error,
None,
Some("preflight"),
reason.clone(),
);
return Err(UpdateError::conflict(reason));
impl SystemUpdateService {
pub fn new(config: SystemUpdateConfig) -> Self {
Self {
config,
operation_lock: Mutex::const_new(()),
release_cache: ReleaseCache::default(),
events: UpdateEventSender::default(),
}
}
let confirmed_target_version = confirmed_update_target(target_version)?;
emit_update_event(
UpdateLogLevel::Info,
None,
Some("release"),
"正在获取最新 Release 信息",
);
let release = fetch_latest_release(
&config.github_api_base,
config
.update_repository
.as_deref()
.ok_or_else(|| UpdateError::conflict("Update checks require CPR_UPDATE_REPOSITORY"))?,
)
.await
.map_err(|error| {
emit_terminal_update_event(UpdateLogLevel::Error, None, Some("release"), error.clone());
UpdateError::bad_gateway(error)
})?;
let info = update_info_from_release(&config, release.clone());
if info.latest_version != confirmed_target_version {
let message = format!(
"远端最新版本已变更为 v{},请重新检查并确认",
info.latest_version
);
emit_terminal_update_event(
UpdateLogLevel::Warning,
pub(crate) fn from_env() -> Self {
Self::new(SystemUpdateConfig::from_env())
}
pub(crate) async fn version_data(&self) -> VersionData {
let update_info = check_latest_release(&self.release_cache, &self.config, false).await;
self.config.version_data(&update_info)
}
pub(crate) async fn update_detail(&self, refresh: bool) -> UpdateInfoData {
check_latest_release(&self.release_cache, &self.config, refresh).await
}
pub(crate) fn subscribe_update_events(&self) -> broadcast::Receiver<SystemUpdateEvent> {
self.events.subscribe()
}
pub(crate) async fn perform_update(
&self,
target_version: Option<String>,
) -> Result<UpdateStartedData, UpdateError> {
let _guard = self
.operation_lock
.try_lock()
.map_err(|_| UpdateError::conflict("System update already running"))?;
let config = &self.config;
if let Some(reason) = config.update_support_error() {
self.events.emit_terminal(
UpdateLogLevel::Error,
None,
Some("preflight"),
reason.clone(),
);
return Err(UpdateError::conflict(reason));
}
let confirmed_target_version = confirmed_update_target(target_version)?;
self.events.emit(
UpdateLogLevel::Info,
None,
Some("release"),
message.clone(),
"正在获取最新 Release 信息",
);
return Err(UpdateError::conflict(message));
}
if !info.has_update {
emit_terminal_update_event(
UpdateLogLevel::Warning,
None,
Some("release"),
"当前版本已是最新",
);
return Err(UpdateError::conflict("Already up to date"));
}
let target_version = info.latest_version.clone();
let operation_id = operation_id("update");
let lock = OperationLock::acquire(&config.update_lock_file)?;
set_operation_running(
&config.update_state_file,
&operation_id,
SystemOperationKind::Update,
Some(&target_version),
)?;
let release = fetch_latest_release(
&config.github_api_base,
config.update_repository.as_deref().ok_or_else(|| {
UpdateError::conflict("Update checks require CPR_UPDATE_REPOSITORY")
})?,
)
.await
.map_err(|error| {
self.events
.emit_terminal(UpdateLogLevel::Error, None, Some("release"), error.clone());
UpdateError::bad_gateway(error)
})?;
let info = update_info_from_release(config, release.clone());
if info.latest_version != confirmed_target_version {
let message = format!(
"远端最新版本已变更为 v{},请重新检查并确认",
info.latest_version
);
self.events.emit_terminal(
UpdateLogLevel::Warning,
None,
Some("release"),
message.clone(),
);
return Err(UpdateError::conflict(message));
}
if !info.has_update {
self.events.emit_terminal(
UpdateLogLevel::Warning,
None,
Some("release"),
"当前版本已是最新",
);
return Err(UpdateError::conflict("Already up to date"));
}
let target_version = info.latest_version.clone();
let operation_id = operation_id("update");
let lock = OperationLock::acquire(&config.update_lock_file)?;
set_operation_running(
&config.update_state_file,
&operation_id,
SystemOperationKind::Update,
Some(&target_version),
)?;
emit_update_event(
UpdateLogLevel::Info,
Some(&operation_id),
Some("prepare"),
format!("准备更新到 v{target_version}"),
);
let result = perform_release_update(&config, &release, &target_version, &operation_id).await;
if let Err(error) = &result {
emit_terminal_update_event(
UpdateLogLevel::Error,
self.events.emit(
UpdateLogLevel::Info,
Some(&operation_id),
Some("failed"),
error.to_string(),
Some("prepare"),
format!("准备更新到 v{target_version}"),
);
} else {
emit_terminal_update_event(
UpdateLogLevel::Success,
Some(&operation_id),
Some("done"),
"更新文件已替换,等待服务重启生效",
let result = self
.perform_release_update(&release, &target_version, &operation_id)
.await;
if let Err(error) = &result {
self.events.emit_terminal(
UpdateLogLevel::Error,
Some(&operation_id),
Some("failed"),
error.to_string(),
);
} else {
self.events.emit_terminal(
UpdateLogLevel::Success,
Some(&operation_id),
Some("done"),
"更新文件已替换,等待服务重启生效",
);
}
finish_operation(
&config.update_state_file,
&operation_id,
SystemOperationKind::Update,
result.as_ref().ok().map(|_| target_version.clone()),
result.as_ref().err().map(ToString::to_string),
);
}
finish_operation(
&config.update_state_file,
&operation_id,
SystemOperationKind::Update,
result.as_ref().ok().map(|_| target_version.clone()),
result.as_ref().err().map(ToString::to_string),
);
drop(lock);
result?;
drop(lock);
result?;
Ok(UpdateStartedData {
operation_id,
deployment_mode: config.deployment_mode,
message: "更新完成,请重启服务。".to_string(),
need_restart: true,
target_version,
})
Ok(UpdateStartedData {
operation_id,
deployment_mode: config.deployment_mode.clone(),
message: "更新完成,请重启服务。".to_string(),
need_restart: true,
target_version,
})
}
}
fn confirmed_update_target(target_version: Option<String>) -> Result<String, UpdateError> {
@@ -248,51 +269,52 @@ fn confirmed_update_target(target_version: Option<String>) -> Result<String, Upd
Ok(target_version)
}
pub(crate) fn update_status() -> Result<SystemUpdateStatusData, UpdateError> {
let config = SystemUpdateConfig::from_env();
read_state(&config.update_state_file)
}
pub(crate) async fn rollback() -> Result<String, UpdateError> {
let _guard = SYSTEM_OPERATION_LOCK
.try_lock()
.map_err(|_| UpdateError::conflict("系统操作正在执行中"))?;
let config = SystemUpdateConfig::from_env();
if let Some(reason) = config.update_support_error() {
return Err(UpdateError::conflict(reason));
impl SystemUpdateService {
pub(crate) fn update_status(&self) -> Result<SystemUpdateStatusData, UpdateError> {
read_state(&self.config.update_state_file)
}
let operation_id = operation_id("rollback");
let lock = OperationLock::acquire(&config.update_lock_file)?;
set_operation_running(
&config.update_state_file,
&operation_id,
SystemOperationKind::Rollback,
None,
)?;
let result = rollback_release_update(&config).await;
finish_operation(
&config.update_state_file,
&operation_id,
SystemOperationKind::Rollback,
None,
result.as_ref().err().map(ToString::to_string),
);
drop(lock);
result?;
pub(crate) async fn rollback(&self) -> Result<String, UpdateError> {
let _guard = self
.operation_lock
.try_lock()
.map_err(|_| UpdateError::conflict("系统操作正在执行中"))?;
let config = &self.config;
if let Some(reason) = config.update_support_error() {
return Err(UpdateError::conflict(reason));
}
Ok(operation_id)
}
let operation_id = operation_id("rollback");
let lock = OperationLock::acquire(&config.update_lock_file)?;
set_operation_running(
&config.update_state_file,
&operation_id,
SystemOperationKind::Rollback,
None,
)?;
let result = rollback_release_update(config).await;
finish_operation(
&config.update_state_file,
&operation_id,
SystemOperationKind::Rollback,
None,
result.as_ref().err().map(ToString::to_string),
);
drop(lock);
result?;
pub(crate) fn restart_plan() -> Result<RestartPlan, UpdateError> {
if env_string("CPR_ENABLE_SELF_RESTART").as_deref() != Some("true") {
return Err(UpdateError::conflict(
"自重启未启用,请设置 CPR_ENABLE_SELF_RESTART=true",
));
Ok(operation_id)
}
let config = SystemUpdateConfig::from_env();
schedule_restart(&config)
pub(crate) fn restart_plan(&self) -> Result<RestartPlan, UpdateError> {
if !self.config.self_restart_enabled {
return Err(UpdateError::conflict(
"自重启未启用,请设置 CPR_ENABLE_SELF_RESTART=true",
));
}
schedule_restart(&self.config)
}
}
pub(crate) struct RestartPlan {
@@ -369,6 +391,7 @@ impl SystemUpdateConfig {
update_state_file: state_file,
update_lock_file,
update_temp_dir,
self_restart_enabled: env_string("CPR_ENABLE_SELF_RESTART").as_deref() == Some("true"),
}
}
@@ -443,127 +466,131 @@ pub(super) fn bad_gateway_with<E: fmt::Display>(
move |error| UpdateError::bad_gateway(format!("{context}: {error}"))
}
async fn perform_release_update(
config: &SystemUpdateConfig,
release: &GitHubRelease,
version: &str,
operation_id: &str,
) -> Result<(), UpdateError> {
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("asset"),
"正在选择匹配当前平台的更新包",
);
let archive = select_release_archive(release, version)?;
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("asset"),
format!(
"已选择更新包 {} ({})",
archive.name,
format_bytes(archive.size)
),
);
impl SystemUpdateService {
async fn perform_release_update(
&self,
release: &GitHubRelease,
version: &str,
operation_id: &str,
) -> Result<(), UpdateError> {
let config = &self.config;
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("asset"),
"正在选择匹配当前平台的更新包",
);
let archive = select_release_archive(release, version)?;
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("asset"),
format!(
"已选择更新包 {} ({})",
archive.name,
format_bytes(archive.size)
),
);
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("verify"),
"正在校验下载地址",
);
validate_download_url(&archive.browser_download_url, &config.github_api_base)?;
if archive.size > MAX_DOWNLOAD_SIZE {
return Err(UpdateError::bad_request("Release archive is too large"));
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("verify"),
"正在校验下载地址",
);
validate_download_url(&archive.browser_download_url, &config.github_api_base)?;
if archive.size > MAX_DOWNLOAD_SIZE {
return Err(UpdateError::bad_request("Release archive is too large"));
}
let checksum = release
.assets
.iter()
.find(|asset| asset.name == "checksums.txt")
.ok_or_else(|| UpdateError::bad_gateway("Release checksums.txt is required"))?;
validate_download_url(&checksum.browser_download_url, &config.github_api_base)?;
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("prepare"),
"正在创建临时更新目录",
);
fs::create_dir_all(&config.update_temp_dir)
.map_err(internal_error_with("Failed to prepare update temp dir"))?;
let temp_dir = fs::canonicalize(&config.update_temp_dir)
.and_then(|dir| tempfile_dir_in(&dir, ".codex-proxy-rs-update-"))
.map_err(internal_error_with("Failed to create update temp dir"))?;
let archive_path = temp_dir.join(&archive.name);
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("download"),
"开始下载更新包",
);
download_file(
&archive.browser_download_url,
&archive_path,
MAX_DOWNLOAD_SIZE,
Some(DownloadProgress {
operation_id,
total_size: archive.size,
events: &self.events,
}),
)
.await?;
self.events.emit(
UpdateLogLevel::Success,
Some(operation_id),
Some("download"),
"更新包下载完成",
);
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("checksum"),
"正在校验 checksum",
);
verify_checksum(&archive_path, &archive.name, &checksum.browser_download_url).await?;
self.events.emit(
UpdateLogLevel::Success,
Some(operation_id),
Some("checksum"),
"checksum 校验通过",
);
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("extract"),
"正在解压更新包",
);
let extracted = extract_release_archive(&archive_path, &temp_dir)?;
self.events.emit(
UpdateLogLevel::Success,
Some(operation_id),
Some("extract"),
"更新包解压完成",
);
self.events.emit(
UpdateLogLevel::Info,
Some(operation_id),
Some("replace"),
"正在替换应用文件",
);
let exe_path = config.executable_path()?;
replace_release_files(&exe_path, &config.web_dist_dir, extracted)?;
self.events.emit(
UpdateLogLevel::Success,
Some(operation_id),
Some("replace"),
"应用文件替换完成",
);
let _ = fs::remove_dir_all(temp_dir);
Ok(())
}
let checksum = release
.assets
.iter()
.find(|asset| asset.name == "checksums.txt")
.ok_or_else(|| UpdateError::bad_gateway("Release checksums.txt is required"))?;
validate_download_url(&checksum.browser_download_url, &config.github_api_base)?;
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("prepare"),
"正在创建临时更新目录",
);
fs::create_dir_all(&config.update_temp_dir)
.map_err(internal_error_with("Failed to prepare update temp dir"))?;
let temp_dir = fs::canonicalize(&config.update_temp_dir)
.and_then(|dir| tempfile_dir_in(&dir, ".codex-proxy-rs-update-"))
.map_err(internal_error_with("Failed to create update temp dir"))?;
let archive_path = temp_dir.join(&archive.name);
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("download"),
"开始下载更新包",
);
download_file(
&archive.browser_download_url,
&archive_path,
MAX_DOWNLOAD_SIZE,
Some(DownloadProgress {
operation_id,
total_size: archive.size,
}),
)
.await?;
emit_update_event(
UpdateLogLevel::Success,
Some(operation_id),
Some("download"),
"更新包下载完成",
);
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("checksum"),
"正在校验 checksum",
);
verify_checksum(&archive_path, &archive.name, &checksum.browser_download_url).await?;
emit_update_event(
UpdateLogLevel::Success,
Some(operation_id),
Some("checksum"),
"checksum 校验通过",
);
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("extract"),
"正在解压更新包",
);
let extracted = extract_release_archive(&archive_path, &temp_dir)?;
emit_update_event(
UpdateLogLevel::Success,
Some(operation_id),
Some("extract"),
"更新包解压完成",
);
emit_update_event(
UpdateLogLevel::Info,
Some(operation_id),
Some("replace"),
"正在替换应用文件",
);
let exe_path = config.executable_path()?;
replace_release_files(&exe_path, &config.web_dist_dir, extracted)?;
emit_update_event(
UpdateLogLevel::Success,
Some(operation_id),
Some("replace"),
"应用文件替换完成",
);
let _ = fs::remove_dir_all(temp_dir);
Ok(())
}
fn tempfile_dir_in(parent: &Path, prefix: &str) -> io::Result<PathBuf> {
@@ -598,90 +625,88 @@ fn env_string(key: &str) -> Option<String> {
.filter(|value| !value.is_empty())
}
fn runtime_build_info_override(key: &str) -> Option<String> {
if cfg!(debug_assertions) {
env_string(key)
} else {
None
}
}
fn build_version() -> String {
runtime_build_info_override("CPR_VERSION").unwrap_or_else(|| {
option_env!("CPR_VERSION")
.unwrap_or(env!("CARGO_PKG_VERSION"))
.to_string()
})
option_env!("CPR_VERSION")
.unwrap_or(env!("CARGO_PKG_VERSION"))
.to_string()
}
fn build_git_sha() -> String {
runtime_build_info_override("CPR_GIT_SHA")
.unwrap_or_else(|| option_env!("CPR_GIT_SHA").unwrap_or("unknown").to_string())
option_env!("CPR_GIT_SHA").unwrap_or("unknown").to_string()
}
fn build_time() -> String {
runtime_build_info_override("CPR_BUILD_TIME").unwrap_or_else(|| {
option_env!("CPR_BUILD_TIME")
.unwrap_or("unknown")
.to_string()
})
option_env!("CPR_BUILD_TIME")
.unwrap_or("unknown")
.to_string()
}
fn build_type() -> String {
runtime_build_info_override("CPR_BUILD_TYPE").unwrap_or_else(|| {
option_env!("CPR_BUILD_TYPE")
.unwrap_or("source")
.to_string()
})
option_env!("CPR_BUILD_TYPE")
.unwrap_or("source")
.to_string()
}
fn update_event_sender() -> &'static broadcast::Sender<SystemUpdateEvent> {
UPDATE_EVENT_SENDER.get_or_init(|| {
pub(super) struct UpdateEventSender {
sender: broadcast::Sender<SystemUpdateEvent>,
}
impl Default for UpdateEventSender {
fn default() -> Self {
let (sender, _receiver) = broadcast::channel(256);
sender
})
Self { sender }
}
}
pub(super) fn emit_update_event(
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
) {
emit_update_event_with_terminal(level, operation_id, step, message, false);
}
impl UpdateEventSender {
fn subscribe(&self) -> broadcast::Receiver<SystemUpdateEvent> {
self.sender.subscribe()
}
fn emit_terminal_update_event(
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
) {
emit_update_event_with_terminal(level, operation_id, step, message, true);
}
pub(super) fn emit(
&self,
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
) {
self.emit_with_terminal(level, operation_id, step, message, false);
}
fn emit_update_event_with_terminal(
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
terminal: bool,
) {
let now = Utc::now();
let event = SystemUpdateEvent {
id: format!(
"update-log-{}",
now.timestamp_nanos_opt()
.unwrap_or_else(|| now.timestamp_millis())
),
operation_id: operation_id.map(ToString::to_string),
level,
step: step.map(ToString::to_string),
message: message.into(),
terminal,
at: now.to_rfc3339(),
};
let _ = update_event_sender().send(event);
fn emit_terminal(
&self,
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
) {
self.emit_with_terminal(level, operation_id, step, message, true);
}
fn emit_with_terminal(
&self,
level: UpdateLogLevel,
operation_id: Option<&str>,
step: Option<&str>,
message: impl Into<String>,
terminal: bool,
) {
let now = Utc::now();
let event = SystemUpdateEvent {
id: format!(
"update-log-{}",
now.timestamp_nanos_opt()
.unwrap_or_else(|| now.timestamp_millis())
),
operation_id: operation_id.map(ToString::to_string),
level,
step: step.map(ToString::to_string),
message: message.into(),
terminal,
at: now.to_rfc3339(),
};
let _ = self.sender.send(event);
}
}
fn is_false(value: &bool) -> bool {
+4 -4
View File
@@ -204,10 +204,10 @@ impl OperationLock {
impl Drop for OperationLock {
fn drop(&mut self) {
if let Err(error) = fs::remove_file(&self.path) {
if error.kind() != io::ErrorKind::NotFound {
eprintln!("failed to remove update lock: {error}");
}
if let Err(error) = fs::remove_file(&self.path)
&& error.kind() != io::ErrorKind::NotFound
{
eprintln!("failed to remove update lock: {error}");
}
}
}
+12 -13
View File
@@ -1,9 +1,9 @@
use std::{collections::BTreeMap, time::Duration};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::upstream::openai::protocol::sse::{parse_sse_events, SseError};
use crate::upstream::openai::protocol::sse::{SseError, parse_sse_events};
/// 从 Codex/OpenAI usage 结构中提取出的标准化 token 用量。
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -356,17 +356,16 @@ pub fn rate_limit_quota(
})
.collect::<Vec<_>>();
if let Some(existing_quota) = existing_quota {
if let Some(existing_snapshots) = existing_quota.get("snapshots").and_then(Value::as_array)
{
for snapshot in existing_snapshots {
if !rate_limits
.limits
.keys()
.any(|limit_id| quota_snapshot_matches_limit(snapshot, limit_id))
{
snapshots.push(snapshot.clone());
}
if let Some(existing_quota) = existing_quota
&& let Some(existing_snapshots) = existing_quota.get("snapshots").and_then(Value::as_array)
{
for snapshot in existing_snapshots {
if !rate_limits
.limits
.keys()
.any(|limit_id| quota_snapshot_matches_limit(snapshot, limit_id))
{
snapshots.push(snapshot.clone());
}
}
}
@@ -7,7 +7,7 @@ use crate::{
infra::time::elapsed_millis_i64,
upstream::openai::protocol::{
schema::reconvert_tuple_values,
sse::{parse_sse_events, SseError, SseEvent},
sse::{SseError, SseEvent, parse_sse_events},
},
};
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::upstream::openai::protocol::responses::{
http_sse_fallback_allowed, transport_for_request, CodexResponsesRequest, CodexTransport,
CodexResponsesRequest, CodexTransport, http_sse_fallback_allowed, transport_for_request,
};
use crate::upstream::openai::protocol::sse::encode_sse_event;
+13 -13
View File
@@ -11,21 +11,21 @@ use async_trait::async_trait;
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use reqwest::{
header::{
HeaderMap, HeaderName, HeaderValue, ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE,
COOKIE, RETRY_AFTER, USER_AGENT,
},
Client, Response as ReqwestResponse, StatusCode,
header::{
ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, COOKIE, HeaderMap, HeaderName,
HeaderValue, RETRY_AFTER, USER_AGENT,
},
};
use serde_json::{map::Map, Value};
use serde_json::{Value, map::Map};
use thiserror::Error;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use crate::upstream::openai::fingerprint::{Fingerprint, RuntimeFingerprint};
use crate::upstream::openai::protocol::events::{extract_sse_usage, retry_after_seconds_from_body};
use crate::upstream::openai::protocol::responses::{
http_sse_fallback_allowed, transport_for_request, CodexCompactRequest, CodexResponsesRequest,
CodexTransport,
CodexCompactRequest, CodexResponsesRequest, CodexTransport, http_sse_fallback_allowed,
transport_for_request,
};
use crate::upstream::openai::protocol::sse::SseError;
use crate::upstream::openai::protocol::websocket::{
@@ -33,20 +33,20 @@ use crate::upstream::openai::protocol::websocket::{
};
use super::diagnostics::CodexUpstreamDiagnostics;
use super::endpoints::{endpoint_url, CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH};
use super::endpoints::{CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH, endpoint_url};
use super::headers::{
build_ordered_codex_base_headers, insert_optional_header, insert_ordered_headers,
websocket_header_pairs,
};
use super::response_meta;
use super::tls::{build_reqwest_client_with_custom_ca, custom_ca_env_cache_key, CustomCaError};
use super::tls::{CustomCaError, build_reqwest_client_with_custom_ca, custom_ca_env_cache_key};
use super::websocket::{
execute_response_create_request_stream_with_pool, execute_response_create_request_with_pool,
write_websocket_audit_artifact_from_env, CodexWebSocketConnection, CodexWebSocketExchangeError,
CodexWebSocketRateLimitHeaderUpdates, CodexWebSocketTurnStateUpdate,
CodexWebSocketConnection, CodexWebSocketExchangeError, CodexWebSocketRateLimitHeaderUpdates,
CodexWebSocketTurnStateUpdate, execute_response_create_request_stream_with_pool,
execute_response_create_request_with_pool, write_websocket_audit_artifact_from_env,
};
use super::websocket_pool::{
CodexWebSocketPool, CodexWebSocketPoolKey, WebSocketPoolDecision, DEFAULT_INITIAL_EVENT_TIMEOUT,
CodexWebSocketPool, CodexWebSocketPoolKey, DEFAULT_INITIAL_EVENT_TIMEOUT, WebSocketPoolDecision,
};
// ---------------------------------------------------------------------------
+5 -5
View File
@@ -13,17 +13,17 @@ pub(crate) mod websocket_pump;
pub use self::{
client::{
backend_transport_for_response_request, build_reqwest_client, is_banned_auth_signal,
is_banned_upstream_error, CodexBackendClient, CodexBackendResponse, CodexBackendSseStream,
CodexBackendClient, CodexBackendResponse, CodexBackendSseStream,
CodexBackendStreamingResponse, CodexBackendTransport, CodexClientError, CodexClientResult,
CodexCompactResponse, CodexModelCatalogClient, CodexModelCatalogClientError,
CodexModelCatalogRequest, CodexRateLimitHeaderUpdates, CodexRequestContext,
CodexResponseMetadata, CodexTurnStateUpdate,
CodexResponseMetadata, CodexTurnStateUpdate, backend_transport_for_response_request,
build_reqwest_client, is_banned_auth_signal, is_banned_upstream_error,
},
diagnostics::CodexUpstreamDiagnostics,
endpoints::{
endpoint_request_path, endpoint_url, usage_endpoint_urls, CODEX_RESPONSES_COMPACT_PATH,
CODEX_RESPONSES_PATH, CODEX_USAGE_API_PATH, CODEX_USAGE_PATH, WHAM_USAGE_PATH,
CODEX_RESPONSES_COMPACT_PATH, CODEX_RESPONSES_PATH, CODEX_USAGE_API_PATH, CODEX_USAGE_PATH,
WHAM_USAGE_PATH, endpoint_request_path, endpoint_url, usage_endpoint_urls,
},
headers::{
build_codex_base_headers, build_codex_headers, build_ordered_codex_base_headers,
+1 -1
View File
@@ -2,8 +2,8 @@ use std::{env, fs, io, path::PathBuf, sync::Arc};
use rustls::{ClientConfig, RootCertStore};
use rustls_pki_types::{
pem::{self, PemObject, SectionKind},
CertificateDer,
pem::{self, PemObject, SectionKind},
};
use thiserror::Error;
@@ -4,10 +4,11 @@ use serde_json::Value;
use crate::upstream::openai::protocol::events::retry_after_seconds_from_body;
use super::{
CodexBackendClient, CodexClientError, CodexClientResult, CodexRequestContext,
CodexUpstreamDiagnostics,
client::{read_capped_error_body, retry_after_seconds, truncate_for_error},
endpoints::usage_endpoint_urls,
response_meta, CodexBackendClient, CodexClientError, CodexClientResult, CodexRequestContext,
CodexUpstreamDiagnostics,
response_meta,
};
impl CodexBackendClient {

Some files were not shown because too many files have changed in this diff Show More