diff --git a/.codex/hooks/pre-submit-compile-check.mjs b/.codex/hooks/pre-submit-compile-check.mjs index 97a5b305a..13f62b64b 100644 --- a/.codex/hooks/pre-submit-compile-check.mjs +++ b/.codex/hooks/pre-submit-compile-check.mjs @@ -14,20 +14,40 @@ if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) { } const validationSteps = [ + { + label: 'Rust format check', + command: npmCommand, + args: + process.platform === 'win32' + ? ['/d', '/s', '/c', 'npm run check:rustfmt'] + : ['run', 'check:rustfmt'], + }, { label: 'TypeScript typecheck', command: npmCommand, - args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'], + args: + process.platform === 'win32' + ? ['/d', '/s', '/c', 'npm run typecheck'] + : ['run', 'typecheck'], }, { label: 'Admin web typecheck', command: npmCommand, - args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'], + args: + process.platform === 'win32' + ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] + : ['run', 'admin-web:typecheck'], }, { label: 'Rust api-server compile check', command: 'cargo', - args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'], + args: [ + 'check', + '-p', + 'api-server', + '--manifest-path', + 'server-rs/Cargo.toml', + ], }, ]; @@ -66,7 +86,9 @@ function runStep(step) { } if (result.error) { - console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`); + console.error( + `[codex-hook] ${step.label} 启动失败:${result.error.message}`, + ); return { ok: false, status: 1 }; } @@ -104,12 +126,15 @@ function extractShellCommand(input) { input?.command, ]; - const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0); + const command = candidates.find( + (value) => typeof value === 'string' && value.trim().length > 0, + ); if (command) { return command; } - const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd; + const shellCommand = + input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd; if (Array.isArray(shellCommand)) { return shellCommand.join(' '); } diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index cdff5605e..eacc85123 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -173,6 +173,19 @@ RAG 主要供 Agent 检索项目上下文,开发者仍按 `AGENTS.md`、`docs/ ## 常用检查命令 +Rust 格式检查: + +```bash +npm run check:rustfmt +``` + +仓库通过根目录 `rust-toolchain.toml` 固定 Rust `1.96.0` 和 `rustfmt` 组件, +并通过 `rustfmt.toml` 固定 Edition 2024 格式口径。需要修复格式时运行: + +```bash +cargo fmt --all --manifest-path server-rs/Cargo.toml +``` + - 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。 编码检查: diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index db8dfdc3b..0699549eb 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -233,7 +233,7 @@ npm run codegraph:index Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`: -- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现编译错误会阻止本次提交。 +- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run check:rustfmt`、`npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现格式或编译错误会阻止本次提交。 - `PostToolUse` hook 会在 Codex 工具修改文件后运行 `node .codex/hooks/post-edit-codegraph-sync.mjs`,执行 `npm run codegraph:sync` 刷新本地语义索引。 - 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行 `node .codex/hooks/pre-submit-compile-check.mjs` 与 `node .codex/hooks/post-edit-codegraph-sync.mjs`;个人模型、token、MCP server 仍放在个人 `~/.codex/config.toml`,不要提交。 @@ -243,6 +243,7 @@ Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`: 后端代码修改后,按变更范围选择: +- `npm run check:rustfmt` - `cargo test -p --manifest-path server-rs/Cargo.toml` - `cargo test -p platform-image --manifest-path server-rs/Cargo.toml` - `cargo check -p api-server --manifest-path server-rs/Cargo.toml` @@ -265,6 +266,12 @@ npm run spacetime:generate npm run check:spacetime-schema ``` +仓库根目录的 `rust-toolchain.toml` 固定 Rust `1.96.0` 并要求 `rustfmt` 组件, +`rustfmt.toml` 固定 Edition 2024 的格式化口径。Rust 源码统一使用 +`cargo fmt --all --manifest-path server-rs/Cargo.toml` 格式化,并用 +`npm run check:rustfmt` 做只读校验;Codex 提交前门禁、API 生产构建和 +SpacetimeDB module 生产构建都会执行同一检查,避免不同开发机或构建节点反复产生格式差异。 + ## 前端改动验收 前端修改后,根据范围选择: diff --git a/jenkins/Jenkinsfile.production-api-build b/jenkins/Jenkinsfile.production-api-build index 31f67989a..a2de704af 100644 --- a/jenkins/Jenkinsfile.production-api-build +++ b/jenkins/Jenkinsfile.production-api-build @@ -125,6 +125,7 @@ pipeline { echo "[api-build] 未找到 sccache,改用 rustc 直接构建。" unset RUSTC_WRAPPER fi + npm run check:rustfmt pingora_args=() if [[ "${INCLUDE_PINGORA_GATEWAY:-false}" == "true" ]]; then pingora_args+=(--include-pingora-gateway) diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index 0f376c974..2383e2420 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -129,6 +129,7 @@ pipeline { echo "[stdb-build] 未找到可用 sccache,改用 rustc 直接构建。" unset RUSTC_WRAPPER fi + npm run check:rustfmt SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \ npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}" ' diff --git a/package.json b/package.json index d096f6700..06f38c247 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "preview": "node scripts/vite-cli.mjs preview", "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"", "check:encoding": "node scripts/check-encoding.mjs", + "check:rustfmt": "cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check", "check:spacetime-schema": "node scripts/check-spacetime-schema-guard.mjs", "check:production-ops": "node scripts/check-production-ops-guardrails.mjs", "check:maintenance-page": "node scripts/check-maintenance-page.mjs", @@ -84,7 +85,7 @@ "lint:guardrails": "npm run lint:eslint", "typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit", "typecheck:guardrails": "npm run typecheck", - "lint": "npm run check:encoding && npm run check:spacetime-schema && npm run check:production-ops && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck", + "lint": "npm run check:encoding && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:production-ops && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck", "lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..ecfb7c606 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.96.0" +profile = "minimal" +components = ["rustfmt"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..c0c5cfb55 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2024" +max_width = 100 +newline_style = "Unix" +use_small_heuristics = "Default" diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index c33b0b5ec..d46621969 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -3,6 +3,21 @@ import { readFileSync } from 'node:fs'; const checks = [ + { + file: 'package.json', + includes: '"check:rustfmt": "cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check"', + reason: '仓库必须保留统一、只读的 Rust workspace 格式检查入口。', + }, + { + file: 'jenkins/Jenkinsfile.production-api-build', + includes: 'npm run check:rustfmt', + reason: 'API 生产构建必须在编译前执行 Rust 格式检查。', + }, + { + file: 'jenkins/Jenkinsfile.production-stdb-module-build', + includes: 'npm run check:rustfmt', + reason: 'Stdb module 生产构建必须在编译前执行 Rust 格式检查。', + }, { file: 'server-rs/crates/spacetime-module/src/migration.rs', includes: diff --git a/server-rs/crates/api-server/src/aliyun_matting.rs b/server-rs/crates/api-server/src/aliyun_matting.rs index d3c684d57..60f5a9921 100644 --- a/server-rs/crates/api-server/src/aliyun_matting.rs +++ b/server-rs/crates/api-server/src/aliyun_matting.rs @@ -118,22 +118,36 @@ mod tests { error.message() ); let details = mapped.details().expect("details present"); - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false)); - assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(false) + ); + assert_eq!( + details.get("timeout").and_then(|v| v.as_bool()), + Some(false) + ); } } #[test] fn upstream_transport_failure_maps_to_retryable_transport() { - let error = - MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false); + let error = MattingError::upstream_transport_error( + "通用抠图请求失败:dns error".to_string(), + false, + ); let mapped = aliyun_matting_failure_to_app_error(&error, 12); assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped)); let details = mapped.details().expect("details present"); // 无 HTTP 状态的传输层失败标记为可重试 transport 故障,且不带 upstreamStatus。 - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + details.get("timeout").and_then(|v| v.as_bool()), + Some(false) + ); assert!(details.get("upstreamStatus").is_some_and(|v| v.is_null())); } @@ -146,7 +160,10 @@ mod tests { assert_eq!(mapped.status_code(), StatusCode::GATEWAY_TIMEOUT); let details = mapped.details().expect("details present"); assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(true)); - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(true) + ); } #[test] @@ -159,7 +176,13 @@ mod tests { assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped)); let details = mapped.details().expect("details present"); - assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false)); - assert_eq!(details.get("upstreamStatus").and_then(|v| v.as_u64()), Some(429)); + assert_eq!( + details.get("transport").and_then(|v| v.as_bool()), + Some(false) + ); + assert_eq!( + details.get("upstreamStatus").and_then(|v| v.as_u64()), + Some(429) + ); } } diff --git a/server-rs/crates/api-server/src/auth_me.rs b/server-rs/crates/api-server/src/auth_me.rs index 32cdc1a0d..bab8c4349 100644 --- a/server-rs/crates/api-server/src/auth_me.rs +++ b/server-rs/crates/api-server/src/auth_me.rs @@ -1,4 +1,4 @@ -use axum::{ +use axum::{ Json, extract::{Extension, State}, http::StatusCode, diff --git a/server-rs/crates/api-server/src/custom_world_asset_prompts.rs b/server-rs/crates/api-server/src/custom_world_asset_prompts.rs index 985a83447..f240d1758 100644 --- a/server-rs/crates/api-server/src/custom_world_asset_prompts.rs +++ b/server-rs/crates/api-server/src/custom_world_asset_prompts.rs @@ -1,4 +1,4 @@ -pub(crate) use crate::prompt::character_animation::{ +pub(crate) use crate::prompt::character_animation::{ build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt, }; pub(crate) use crate::prompt::character_visual::{ diff --git a/server-rs/crates/api-server/src/custom_world_rpg_draft_prompts.rs b/server-rs/crates/api-server/src/custom_world_rpg_draft_prompts.rs index 8ba9c49fb..f506f2034 100644 --- a/server-rs/crates/api-server/src/custom_world_rpg_draft_prompts.rs +++ b/server-rs/crates/api-server/src/custom_world_rpg_draft_prompts.rs @@ -1 +1 @@ -pub(crate) use crate::prompt::agent_chat::*; +pub(crate) use crate::prompt::agent_chat::*; diff --git a/server-rs/crates/api-server/src/editor_green_screen.rs b/server-rs/crates/api-server/src/editor_green_screen.rs index 5840533de..c13d3abc1 100644 --- a/server-rs/crates/api-server/src/editor_green_screen.rs +++ b/server-rs/crates/api-server/src/editor_green_screen.rs @@ -109,7 +109,6 @@ pub(crate) fn default_editor_screen_background_color() -> EditorScreenBackground EDITOR_SCREEN_BACKGROUND_COLORS[0] } - pub(crate) fn parse_editor_screen_background_color( value: Option<&str>, ) -> Result { diff --git a/server-rs/crates/api-server/src/editor_screen_background_decision.rs b/server-rs/crates/api-server/src/editor_screen_background_decision.rs index 77e3dcc2c..ac38816c9 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_decision.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_decision.rs @@ -177,16 +177,15 @@ pub(crate) async fn resolve_editor_screen_background_color( let mut last_error: Option = None; for attempt in 1..=EDITOR_SCREEN_BACKGROUND_DECISION_MAX_ATTEMPTS { let user_message = match source_image_data_url { - Some(image_url) => { - LlmMessage::user(user_prompt.as_str()).with_image_url(image_url) - } + Some(image_url) => LlmMessage::user(user_prompt.as_str()).with_image_url(image_url), None => LlmMessage::user(user_prompt.as_str()), }; // 预算要够推理模型(如 gpt-5-mini)先花几百 token 推理、再吐 JSON 答案; // 实测 low 档推理约 320~384 token,取 1024 留足余量。降级客户端遇 stop 提前结束,不会多花。 - let mut request = LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message]) - .with_max_tokens(1024) - .with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS); + let mut request = + LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message]) + .with_max_tokens(1024) + .with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS); if let Some(decision_model) = decision_model { // gpt-5-mini 是推理模型(有图视觉档 / 无图文本档均适用):走 Responses 协议并压到 low // 推理档,否则默认档会把预算全烧在推理上、返回空答案。 @@ -296,8 +295,7 @@ async fn record_editor_screen_background_decision_llm_error( prompt_chars: usize, reference_image_count: usize, ) { - let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error - { + let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error { LlmError::InvalidConfig(_) | LlmError::InvalidRequest(_) => return, LlmError::Timeout { .. } => ("request_timeout", None, true, true, None, None), LlmError::Connectivity { message, .. } => ( @@ -329,14 +327,9 @@ async fn record_editor_screen_background_decision_llm_error( ), LlmError::EmptyResponse => ("missing_response", Some(200), false, false, None, None), LlmError::StreamUnavailable => ("response_body", Some(200), false, true, None, None), - LlmError::Transport(message) => ( - "transport", - None, - false, - true, - Some(message.as_str()), - None, - ), + LlmError::Transport(message) => { + ("transport", None, false, true, Some(message.as_str()), None) + } }; record_editor_screen_background_decision_failure( audit, @@ -754,8 +747,7 @@ mod tests { request_id: Some("request-1".to_string()), }, ); - let tracking = - crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); + let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); assert_eq!(audit.provider, "vector-engine"); assert_eq!(audit.endpoint, "https://vector.example/v1/responses"); @@ -809,8 +801,7 @@ mod tests { 1, &ExternalApiAuditContext::default(), ); - let tracking = - crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); + let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit); assert_eq!(audit.failure_stage, "request_timeout"); assert_eq!(audit.status_code, None); @@ -897,7 +888,8 @@ mod tests { let (k, v) = trimmed.split_once('=').unwrap(); let v = v.trim().trim_matches('"').trim_matches('\''); // 先出现的文件优先(.env.local > .env.secrets.local > .env),与服务端 dotenv 顺序一致。 - map.entry(k.trim().to_string()).or_insert_with(|| v.to_string()); + map.entry(k.trim().to_string()) + .or_insert_with(|| v.to_string()); } } std::env::var(key).ok().or_else(|| map.get(key).cloned()) @@ -934,7 +926,10 @@ mod tests { let image = RgbaImage::from_pixel(64, 64, Rgba([120, 180, 120, 255])); let mut bytes = Vec::new(); image::DynamicImage::ImageRgba8(image) - .write_to(&mut std::io::Cursor::new(&mut bytes), image::ImageFormat::Png) + .write_to( + &mut std::io::Cursor::new(&mut bytes), + image::ImageFormat::Png, + ) .expect("test image should encode"); format!( "data:image/png;base64,{}", @@ -966,7 +961,11 @@ mod tests { eprintln!( "[live 无图] mode={:?} hex={} label={} attempts={} fallback={}", - decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback + decision.mode, + decision.color.hex, + decision.color.label, + decision.attempts, + decision.fallback ); assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto); assert!( @@ -1000,7 +999,11 @@ mod tests { eprintln!( "[live 有图] mode={:?} hex={} label={} attempts={} fallback={}", - decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback + decision.mode, + decision.color.hex, + decision.color.label, + decision.attempts, + decision.fallback ); assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto); assert!( diff --git a/server-rs/crates/api-server/src/editor_screen_background_filter.rs b/server-rs/crates/api-server/src/editor_screen_background_filter.rs index 59c70a521..d67d7bba0 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_filter.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_filter.rs @@ -203,11 +203,12 @@ impl ForegroundHistogram { .filter(|bin| bin.mass >= mass_floor) .filter_map(|bin| { let lab = bin.mean(); - (lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B) - .then(|| SkinReference { + (lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B).then( + || SkinReference { lab, rgb: bin.mean_rgb(), - }) + }, + ) }) .max_by(|left, right| { left.lab[0] @@ -481,12 +482,18 @@ mod tests { let report = report_for(&transparent_image_with_center_block([250, 224, 200]), true); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#FFD6C2"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFD6C2"), "肤色前景应剔除暖浅桃色,excluded: {}", report.excluded_summary() ); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFF2A8"), "肤色前景应剔除淡黄(Rule 2/3 关键新覆盖),excluded: {}", report.excluded_summary() ); @@ -520,7 +527,10 @@ mod tests { let enabled = report_for(&image, true); assert!( - enabled.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"), + enabled + .excluded + .iter() + .any(|(color, _)| color.hex == "#FFF2A8"), "开启皮肤否决时淡黄应被剔除,excluded: {}", enabled.excluded_summary() ); @@ -542,7 +552,10 @@ mod tests { let report = report_for(&transparent_image_with_center_block([127, 179, 255]), false); assert!( - report.excluded.iter().any(|(color, _)| color.hex == "#7FB3FF"), + report + .excluded + .iter() + .any(|(color, _)| color.hex == "#7FB3FF"), "蓝色前景应剔除中度天蓝,excluded: {}", report.excluded_summary() ); diff --git a/server-rs/crates/api-server/src/external_generation.rs b/server-rs/crates/api-server/src/external_generation.rs index def534cf9..e766bb669 100644 --- a/server-rs/crates/api-server/src/external_generation.rs +++ b/server-rs/crates/api-server/src/external_generation.rs @@ -172,24 +172,14 @@ fn map_external_generation_job_status( 100, None, ), - "running" => ( - ExternalGenerationJobStatus::Running, - "正在生成。", - 35, - None, - ), + "running" => (ExternalGenerationJobStatus::Running, "正在生成。", 35, None), "failed" => ( ExternalGenerationJobStatus::Failed, "生成失败。", 0, job.last_error_message.clone(), ), - _ => ( - ExternalGenerationJobStatus::Queued, - "排队中。", - 8, - None, - ), + _ => (ExternalGenerationJobStatus::Queued, "排队中。", 8, None), }; ExternalGenerationJobStatusRecord { diff --git a/server-rs/crates/api-server/src/login_options.rs b/server-rs/crates/api-server/src/login_options.rs index 19fe4bde1..f71f4d511 100644 --- a/server-rs/crates/api-server/src/login_options.rs +++ b/server-rs/crates/api-server/src/login_options.rs @@ -1,4 +1,4 @@ -use axum::{ +use axum::{ Json, extract::{Extension, State}, }; diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 8c6f4be3a..6ce33e2ce 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -4,8 +4,8 @@ mod admin; mod admin_accounts; mod admin_recharge; mod ai_generation_drafts; -mod aliyun_matting; mod ai_tasks; +mod aliyun_matting; mod api_response; mod app; mod asset_billing; @@ -670,8 +670,12 @@ mod tests { #[test] fn profile_recharge_expiration_listener_is_limited_to_http_roles() { - assert!(should_start_profile_recharge_expiration_listener(ProcessRole::Api)); - assert!(should_start_profile_recharge_expiration_listener(ProcessRole::All)); + assert!(should_start_profile_recharge_expiration_listener( + ProcessRole::Api + )); + assert!(should_start_profile_recharge_expiration_listener( + ProcessRole::All + )); assert!(!should_start_profile_recharge_expiration_listener( ProcessRole::ExternalGenerationWorker )); diff --git a/server-rs/crates/api-server/src/modules/auth.rs b/server-rs/crates/api-server/src/modules/auth.rs index fecbf4fe0..d8644873d 100644 --- a/server-rs/crates/api-server/src/modules/auth.rs +++ b/server-rs/crates/api-server/src/modules/auth.rs @@ -1,4 +1,4 @@ -use axum::{ +use axum::{ Router, middleware, routing::{get, post}, }; diff --git a/server-rs/crates/api-server/src/platform_errors.rs b/server-rs/crates/api-server/src/platform_errors.rs index 792fb8817..6da6f87c3 100644 --- a/server-rs/crates/api-server/src/platform_errors.rs +++ b/server-rs/crates/api-server/src/platform_errors.rs @@ -1,4 +1,4 @@ -use axum::http::{HeaderValue, StatusCode}; +use axum::http::{HeaderValue, StatusCode}; use platform_auth::{AuthPlatformErrorKind, WechatProviderError}; use platform_llm::{LlmError, LlmErrorKind}; use platform_oss::{OssError, OssErrorKind}; diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 13c042dde..50256c44c 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -1,4 +1,4 @@ -use std::{ +use std::{ collections::BTreeMap, time::{Instant, SystemTime, UNIX_EPOCH}, }; diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 231671080..62d067b4a 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -626,14 +626,12 @@ impl AppState { let models = editor_generation_pricing_to_records(&next)?; let record = self .spacetime_client - .upsert_editor_generation_pricing_config( - editor_generation_pricing_upsert_input( - &self.config, - admin_user_id, - models, - crate::editor_project::current_utc_micros(), - ), - ) + .upsert_editor_generation_pricing_config(editor_generation_pricing_upsert_input( + &self.config, + admin_user_id, + models, + crate::editor_project::current_utc_micros(), + )) .await .map_err(|error| EditorGenerationPricingError::Persistence(error.to_string()))?; let pricing = editor_generation_pricing_from_record(record)?; diff --git a/server-rs/crates/api-server/src/wechat/auth.rs b/server-rs/crates/api-server/src/wechat/auth.rs index b2571903c..7496ad4e1 100644 --- a/server-rs/crates/api-server/src/wechat/auth.rs +++ b/server-rs/crates/api-server/src/wechat/auth.rs @@ -1,4 +1,4 @@ -use axum::{ +use axum::{ Json, extract::{Extension, Query, State}, http::{HeaderMap, StatusCode}, diff --git a/server-rs/crates/api-server/src/wechat/provider.rs b/server-rs/crates/api-server/src/wechat/provider.rs index 94c3a117d..60722cb82 100644 --- a/server-rs/crates/api-server/src/wechat/provider.rs +++ b/server-rs/crates/api-server/src/wechat/provider.rs @@ -1,4 +1,4 @@ -use platform_auth::{ +use platform_auth::{ DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT, DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT, DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT, diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs index ba6214920..075d1a485 100644 --- a/server-rs/crates/api-server/src/wooden_fish.rs +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -1,4 +1,4 @@ -use std::{ +use std::{ collections::BTreeMap, time::{SystemTime, UNIX_EPOCH}, }; diff --git a/server-rs/crates/platform-matting/examples/segment_smoke.rs b/server-rs/crates/platform-matting/examples/segment_smoke.rs index c778b5f02..a0c817402 100644 --- a/server-rs/crates/platform-matting/examples/segment_smoke.rs +++ b/server-rs/crates/platform-matting/examples/segment_smoke.rs @@ -38,17 +38,16 @@ async fn main() { }); let input_bytes = std::fs::read(&input_path) .unwrap_or_else(|error| panic!("读取测试图片失败({input_path}):{error}")); - println!("[1/5] 已读取测试图片:{input_path}({} 字节)", input_bytes.len()); + println!( + "[1/5] 已读取测试图片:{input_path}({} 字节)", + input_bytes.len() + ); // SegmentCommonImage 要求分辨率低于 2000x2000,超限先等比缩小。 const MAX_EDGE: u32 = 1999; let decoded = image::load_from_memory(&input_bytes).expect("测试图片应可解码"); let input_bytes = if decoded.width() > MAX_EDGE || decoded.height() > MAX_EDGE { - let resized = decoded.resize( - MAX_EDGE, - MAX_EDGE, - image::imageops::FilterType::CatmullRom, - ); + let resized = decoded.resize(MAX_EDGE, MAX_EDGE, image::imageops::FilterType::CatmullRom); let mut buffer = std::io::Cursor::new(Vec::new()); resized .write_to(&mut buffer, image::ImageFormat::Png) @@ -70,8 +69,14 @@ async fn main() { // --- 调用通用抠图 --- // key 优先级:VIAPI 专用 → 官方 SDK 标准命名(#IMAGE_CALL)→ 短信 key 兜底。 let (matting_key_id, matting_key_secret) = [ - ("ALIYUN_IMAGESEG_ACCESS_KEY_ID", "ALIYUN_IMAGESEG_ACCESS_KEY_SECRET"), - ("ALIBABA_CLOUD_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_SECRET"), + ( + "ALIYUN_IMAGESEG_ACCESS_KEY_ID", + "ALIYUN_IMAGESEG_ACCESS_KEY_SECRET", + ), + ( + "ALIBABA_CLOUD_ACCESS_KEY_ID", + "ALIBABA_CLOUD_ACCESS_KEY_SECRET", + ), ("ALIYUN_SMS_ACCESS_KEY_ID", "ALIYUN_SMS_ACCESS_KEY_SECRET"), ] .iter() diff --git a/server-rs/crates/platform-matting/src/lib.rs b/server-rs/crates/platform-matting/src/lib.rs index 0df252889..854fafb19 100644 --- a/server-rs/crates/platform-matting/src/lib.rs +++ b/server-rs/crates/platform-matting/src/lib.rs @@ -162,9 +162,9 @@ pub struct UpstreamFailure { impl MattingError { pub fn message(&self) -> &str { match self { - Self::InvalidConfig(message) - | Self::InvalidRequest(message) - | Self::Sign(message) => message, + Self::InvalidConfig(message) | Self::InvalidRequest(message) | Self::Sign(message) => { + message + } Self::Upstream(failure) => &failure.message, } } @@ -270,7 +270,10 @@ impl MattingClient { } let mut form = BTreeMap::new(); - form.insert("Action".to_string(), SEGMENT_COMMON_IMAGE_ACTION.to_string()); + form.insert( + "Action".to_string(), + SEGMENT_COMMON_IMAGE_ACTION.to_string(), + ); form.insert("Format".to_string(), "json".to_string()); form.insert("Version".to_string(), IMAGESEG_API_VERSION.to_string()); form.insert("ImageURL".to_string(), image_url); @@ -451,17 +454,12 @@ impl MattingClient { } async fn download_result_image(&self, url: &str) -> Result, MattingError> { - let mut response = self - .client - .get(url) - .send() - .await - .map_err(|error| { - MattingError::upstream_transport_error( - describe_result_download_transport_error(&error), - error.is_timeout(), - ) - })?; + let mut response = self.client.get(url).send().await.map_err(|error| { + MattingError::upstream_transport_error( + describe_result_download_transport_error(&error), + error.is_timeout(), + ) + })?; let status = response.status(); if !status.is_success() { return Err(MattingError::upstream_http_error( @@ -501,9 +499,7 @@ impl MattingClient { content_type: &str, ) -> Result { if bytes.is_empty() { - return Err(MattingError::InvalidRequest( - "上传内容不能为空".to_string(), - )); + return Err(MattingError::InvalidRequest("上传内容不能为空".to_string())); } let sts = self.get_oss_sts_token().await?; let file_name = file_name.trim().trim_matches('/'); @@ -536,7 +532,8 @@ impl MattingClient { "PUT\n\n{content_type}\n{date}\nx-oss-security-token:{}\n/{VIAPI_TEMP_BUCKET}/{object_key}", sts.security_token ); - let signature = hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?; + let signature = + hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?; let authorization = format!("OSS {}:{}", sts.access_key_id, signature); let target_url = format!("https://{VIAPI_TEMP_OSS_HOST}/{object_key}"); @@ -625,7 +622,9 @@ impl MattingClient { format!( "GetOssStsToken 返回失败(HTTP {},Code={}):{}", http_status.as_u16(), - body.get("Code").and_then(|value| value.as_str()).unwrap_or("unknown"), + body.get("Code") + .and_then(|value| value.as_str()) + .unwrap_or("unknown"), body.get("Message") .and_then(|value| value.as_str()) .unwrap_or("unknown") @@ -762,10 +761,7 @@ fn encode_rgba_png(image: &image::RgbaImage) -> Result, MattingError> { image::ExtendedColorType::Rgba8, ) .map_err(|error| { - MattingError::upstream_response_error( - format!("编码抠图结果 PNG 失败:{error}"), - None, - ) + MattingError::upstream_response_error(format!("编码抠图结果 PNG 失败:{error}"), None) })?; Ok(encoded) } @@ -934,13 +930,7 @@ fn current_aliyun_timestamp() -> String { fn canonicalize_aliyun_form_params(params: &BTreeMap) -> String { params .iter() - .map(|(key, value)| { - format!( - "{}={}", - urlencoding_encode(key), - urlencoding_encode(value) - ) - }) + .map(|(key, value)| format!("{}={}", urlencoding_encode(key), urlencoding_encode(value))) .collect::>() .join("&") } @@ -1001,8 +991,10 @@ mod tests { #[test] fn upstream_transport_error_classifies_as_transport() { - let error = - MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false); + let error = MattingError::upstream_transport_error( + "通用抠图请求失败:dns error".to_string(), + false, + ); assert!(error.external_call_attempted()); assert!(error.is_transport()); assert!(!error.is_timeout()); @@ -1032,10 +1024,7 @@ mod tests { #[test] fn upstream_response_error_is_external_but_not_transport() { - let error = MattingError::upstream_response_error( - "抠图结果尺寸不一致".to_string(), - None, - ); + let error = MattingError::upstream_response_error("抠图结果尺寸不一致".to_string(), None); assert!(error.external_call_attempted()); assert!(!error.is_transport()); assert!(!error.is_timeout()); @@ -1130,13 +1119,31 @@ mod tests { let sanitized = sanitize_oss_upload_error_body(&body, token); // 敏感串全部消失:StringToSign 明文、其中的 token、十六进制、签名串、以及 Message 里的 token 明文。 - assert!(!sanitized.contains(token), "STS token 不能残留(含元素外的明文)"); - assert!(!sanitized.contains("x-oss-security-token:CAIS"), "StringToSign 明文不能残留"); - assert!(!sanitized.contains("sigSECRET"), "SignatureProvided 不能残留"); - assert!(!sanitized.contains("50 55 54 0a"), "StringToSignBytes 不能残留"); + assert!( + !sanitized.contains(token), + "STS token 不能残留(含元素外的明文)" + ); + assert!( + !sanitized.contains("x-oss-security-token:CAIS"), + "StringToSign 明文不能残留" + ); + assert!( + !sanitized.contains("sigSECRET"), + "SignatureProvided 不能残留" + ); + assert!( + !sanitized.contains("50 55 54 0a"), + "StringToSignBytes 不能残留" + ); // 可诊断信息保留。 - assert!(sanitized.contains("SignatureDoesNotMatch"), "OSS Code 应保留供诊断"); - assert!(sanitized.contains("[redacted]"), "签名材料元素应被脱敏为 [redacted]"); + assert!( + sanitized.contains("SignatureDoesNotMatch"), + "OSS Code 应保留供诊断" + ); + assert!( + sanitized.contains("[redacted]"), + "签名材料元素应被脱敏为 [redacted]" + ); } fn noise_image(width: u32, height: u32) -> image::DynamicImage { @@ -1151,10 +1158,12 @@ mod tests { #[test] fn normalize_keeps_small_image_and_outputs_png() { - let source = - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(200, 150, image::Rgba([10, 20, 30, 255]))); - let (bytes, dims) = - normalize_matting_input_png(&source).expect("normalize should succeed"); + let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( + 200, + 150, + image::Rgba([10, 20, 30, 255]), + )); + let (bytes, dims) = normalize_matting_input_png(&source).expect("normalize should succeed"); assert_eq!(dims, (200, 150), "小图不缩放,尺寸原样"); assert_eq!( @@ -1168,12 +1177,18 @@ mod tests { #[test] fn normalize_caps_oversized_edge_to_1999() { - let source = - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(2400, 1200, image::Rgba([0, 0, 0, 255]))); + let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( + 2400, + 1200, + image::Rgba([0, 0, 0, 255]), + )); let (_bytes, (width, height)) = normalize_matting_input_png(&source).expect("normalize should succeed"); - assert!(width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE, "两边都 ≤1999,实得 {width}x{height}"); + assert!( + width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE, + "两边都 ≤1999,实得 {width}x{height}" + ); assert_eq!(width.max(height), MAX_INPUT_EDGE, "最长边压到 1999"); } @@ -1189,7 +1204,10 @@ mod tests { let (bytes, (width, height)) = normalize_matting_input_png_within(&source, limit) .expect("limited normalize should succeed"); - assert!(width < 300 && height < 300, "应从 300x300 降尺寸,实得 {width}x{height}"); + assert!( + width < 300 && height < 300, + "应从 300x300 降尺寸,实得 {width}x{height}" + ); assert!( bytes.len() <= limit || width.min(height) <= MIN_INPUT_EDGE + 1, "编码 {} 字节应落在 {limit} 内(或已触最小边下限)", diff --git a/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs b/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs index 7bab87ddc..593f634f2 100644 --- a/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs +++ b/server-rs/crates/spacetime-client/src/profile_recharge_expiration.rs @@ -67,12 +67,10 @@ impl SpacetimeClient { send_connect_once(&connect_sender, Ok(())); }) .on_disconnect(move |_, error| { - let message = error - .map(|error| error.to_string()) - .unwrap_or_else(|| { - "SpacetimeDB profile recharge expiration subscription disconnected" - .to_string() - }); + let message = error.map(|error| error.to_string()).unwrap_or_else(|| { + "SpacetimeDB profile recharge expiration subscription disconnected" + .to_string() + }); send_connect_once( &disconnect_sender, Err(SpacetimeClientError::Procedure(message)),