统一Rust格式化工具链与构建门禁

固定Rust 1.96.0与rustfmt格式配置

接入本地lint、Codex提交前检查和Jenkins构建门禁

提交server-rs全workspace一次性格式化基线

同步开发运维文档与production-ops防回退检查
This commit is contained in:
2026-07-15 21:06:05 +08:00
parent 25f6027c4a
commit 271b153308
29 changed files with 269 additions and 147 deletions
+31 -6
View File
@@ -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(' ');
}
@@ -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`
编码检查:
@@ -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 <crate> --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 生产构建都会执行同一检查,避免不同开发机或构建节点反复产生格式差异。
## 前端改动验收
前端修改后,根据范围选择:
+1
View File
@@ -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)
@@ -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}"
'
+2 -1
View File
@@ -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 .",
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.96.0"
profile = "minimal"
components = ["rustfmt"]
+4
View File
@@ -0,0 +1,4 @@
edition = "2024"
max_width = 100
newline_style = "Unix"
use_small_heuristics = "Default"
@@ -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:
@@ -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)
);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use axum::{
use axum::{
Json,
extract::{Extension, State},
http::StatusCode,
@@ -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::{
@@ -1 +1 @@
pub(crate) use crate::prompt::agent_chat::*;
pub(crate) use crate::prompt::agent_chat::*;
@@ -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<EditorScreenBackgroundColor, AppError> {
@@ -177,16 +177,15 @@ pub(crate) async fn resolve_editor_screen_background_color(
let mut last_error: Option<String> = 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!(
@@ -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()
);
@@ -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 {
@@ -1,4 +1,4 @@
use axum::{
use axum::{
Json,
extract::{Extension, State},
};
+7 -3
View File
@@ -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
));
@@ -1,4 +1,4 @@
use axum::{
use axum::{
Router, middleware,
routing::{get, post},
};
@@ -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};
+1 -1
View File
@@ -1,4 +1,4 @@
use std::{
use std::{
collections::BTreeMap,
time::{Instant, SystemTime, UNIX_EPOCH},
};
+6 -8
View File
@@ -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)?;
@@ -1,4 +1,4 @@
use axum::{
use axum::{
Json,
extract::{Extension, Query, State},
http::{HeaderMap, StatusCode},
@@ -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,

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