统一Rust格式化工具链与构建门禁
固定Rust 1.96.0与rustfmt格式配置 接入本地lint、Codex提交前检查和Jenkins构建门禁 提交server-rs全workspace一次性格式化基线 同步开发运维文档与production-ops防回退检查
This commit is contained in:
@@ -14,20 +14,40 @@ if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validationSteps = [
|
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',
|
label: 'TypeScript typecheck',
|
||||||
command: npmCommand,
|
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',
|
label: 'Admin web typecheck',
|
||||||
command: npmCommand,
|
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',
|
label: 'Rust api-server compile check',
|
||||||
command: 'cargo',
|
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) {
|
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 };
|
return { ok: false, status: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +126,15 @@ function extractShellCommand(input) {
|
|||||||
input?.command,
|
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) {
|
if (command) {
|
||||||
return 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)) {
|
if (Array.isArray(shellCommand)) {
|
||||||
return shellCommand.join(' ');
|
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`。
|
- 后端通用用户行为埋点统一通过 `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/`:
|
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` 刷新本地语义索引。
|
- `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`,不要提交。
|
- 如果某个 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 <crate> --manifest-path server-rs/Cargo.toml`
|
||||||
- `cargo test -p platform-image --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`
|
- `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||||
@@ -265,6 +266,12 @@ npm run spacetime:generate
|
|||||||
npm run check:spacetime-schema
|
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 生产构建都会执行同一检查,避免不同开发机或构建节点反复产生格式差异。
|
||||||
|
|
||||||
## 前端改动验收
|
## 前端改动验收
|
||||||
|
|
||||||
前端修改后,根据范围选择:
|
前端修改后,根据范围选择:
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ pipeline {
|
|||||||
echo "[api-build] 未找到 sccache,改用 rustc 直接构建。"
|
echo "[api-build] 未找到 sccache,改用 rustc 直接构建。"
|
||||||
unset RUSTC_WRAPPER
|
unset RUSTC_WRAPPER
|
||||||
fi
|
fi
|
||||||
|
npm run check:rustfmt
|
||||||
pingora_args=()
|
pingora_args=()
|
||||||
if [[ "${INCLUDE_PINGORA_GATEWAY:-false}" == "true" ]]; then
|
if [[ "${INCLUDE_PINGORA_GATEWAY:-false}" == "true" ]]; then
|
||||||
pingora_args+=(--include-pingora-gateway)
|
pingora_args+=(--include-pingora-gateway)
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ pipeline {
|
|||||||
echo "[stdb-build] 未找到可用 sccache,改用 rustc 直接构建。"
|
echo "[stdb-build] 未找到可用 sccache,改用 rustc 直接构建。"
|
||||||
unset RUSTC_WRAPPER
|
unset RUSTC_WRAPPER
|
||||||
fi
|
fi
|
||||||
|
npm run check:rustfmt
|
||||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||||
npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}"
|
npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}"
|
||||||
'
|
'
|
||||||
|
|||||||
+2
-1
@@ -31,6 +31,7 @@
|
|||||||
"preview": "node scripts/vite-cli.mjs preview",
|
"preview": "node scripts/vite-cli.mjs preview",
|
||||||
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||||
"check:encoding": "node scripts/check-encoding.mjs",
|
"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:spacetime-schema": "node scripts/check-spacetime-schema-guard.mjs",
|
||||||
"check:production-ops": "node scripts/check-production-ops-guardrails.mjs",
|
"check:production-ops": "node scripts/check-production-ops-guardrails.mjs",
|
||||||
"check:maintenance-page": "node scripts/check-maintenance-page.mjs",
|
"check:maintenance-page": "node scripts/check-maintenance-page.mjs",
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
"lint:guardrails": "npm run lint:eslint",
|
"lint:guardrails": "npm run lint:eslint",
|
||||||
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",
|
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",
|
||||||
"typecheck:guardrails": "npm run typecheck",
|
"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 .",
|
"lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.96.0"
|
||||||
|
profile = "minimal"
|
||||||
|
components = ["rustfmt"]
|
||||||
@@ -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';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
const checks = [
|
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',
|
file: 'server-rs/crates/spacetime-module/src/migration.rs',
|
||||||
includes:
|
includes:
|
||||||
|
|||||||
@@ -118,22 +118,36 @@ mod tests {
|
|||||||
error.message()
|
error.message()
|
||||||
);
|
);
|
||||||
let details = mapped.details().expect("details present");
|
let details = mapped.details().expect("details present");
|
||||||
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false));
|
assert_eq!(
|
||||||
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false));
|
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]
|
#[test]
|
||||||
fn upstream_transport_failure_maps_to_retryable_transport() {
|
fn upstream_transport_failure_maps_to_retryable_transport() {
|
||||||
let error =
|
let error = MattingError::upstream_transport_error(
|
||||||
MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false);
|
"通用抠图请求失败:dns error".to_string(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
let mapped = aliyun_matting_failure_to_app_error(&error, 12);
|
let mapped = aliyun_matting_failure_to_app_error(&error, 12);
|
||||||
|
|
||||||
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
|
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
|
||||||
let details = mapped.details().expect("details present");
|
let details = mapped.details().expect("details present");
|
||||||
// 无 HTTP 状态的传输层失败标记为可重试 transport 故障,且不带 upstreamStatus。
|
// 无 HTTP 状态的传输层失败标记为可重试 transport 故障,且不带 upstreamStatus。
|
||||||
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true));
|
assert_eq!(
|
||||||
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false));
|
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()));
|
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);
|
assert_eq!(mapped.status_code(), StatusCode::GATEWAY_TIMEOUT);
|
||||||
let details = mapped.details().expect("details present");
|
let details = mapped.details().expect("details present");
|
||||||
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(true));
|
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]
|
#[test]
|
||||||
@@ -159,7 +176,13 @@ mod tests {
|
|||||||
|
|
||||||
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
|
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
|
||||||
let details = mapped.details().expect("details present");
|
let details = mapped.details().expect("details present");
|
||||||
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false));
|
assert_eq!(
|
||||||
assert_eq!(details.get("upstreamStatus").and_then(|v| v.as_u64()), Some(429));
|
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,4 +1,4 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Extension, State},
|
extract::{Extension, State},
|
||||||
http::StatusCode,
|
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,
|
build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt,
|
||||||
};
|
};
|
||||||
pub(crate) use crate::prompt::character_visual::{
|
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]
|
EDITOR_SCREEN_BACKGROUND_COLORS[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn parse_editor_screen_background_color(
|
pub(crate) fn parse_editor_screen_background_color(
|
||||||
value: Option<&str>,
|
value: Option<&str>,
|
||||||
) -> Result<EditorScreenBackgroundColor, AppError> {
|
) -> Result<EditorScreenBackgroundColor, AppError> {
|
||||||
|
|||||||
@@ -177,16 +177,15 @@ pub(crate) async fn resolve_editor_screen_background_color(
|
|||||||
let mut last_error: Option<String> = None;
|
let mut last_error: Option<String> = None;
|
||||||
for attempt in 1..=EDITOR_SCREEN_BACKGROUND_DECISION_MAX_ATTEMPTS {
|
for attempt in 1..=EDITOR_SCREEN_BACKGROUND_DECISION_MAX_ATTEMPTS {
|
||||||
let user_message = match source_image_data_url {
|
let user_message = match source_image_data_url {
|
||||||
Some(image_url) => {
|
Some(image_url) => LlmMessage::user(user_prompt.as_str()).with_image_url(image_url),
|
||||||
LlmMessage::user(user_prompt.as_str()).with_image_url(image_url)
|
|
||||||
}
|
|
||||||
None => LlmMessage::user(user_prompt.as_str()),
|
None => LlmMessage::user(user_prompt.as_str()),
|
||||||
};
|
};
|
||||||
// 预算要够推理模型(如 gpt-5-mini)先花几百 token 推理、再吐 JSON 答案;
|
// 预算要够推理模型(如 gpt-5-mini)先花几百 token 推理、再吐 JSON 答案;
|
||||||
// 实测 low 档推理约 320~384 token,取 1024 留足余量。降级客户端遇 stop 提前结束,不会多花。
|
// 实测 low 档推理约 320~384 token,取 1024 留足余量。降级客户端遇 stop 提前结束,不会多花。
|
||||||
let mut request = LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message])
|
let mut request =
|
||||||
.with_max_tokens(1024)
|
LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message])
|
||||||
.with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS);
|
.with_max_tokens(1024)
|
||||||
|
.with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS);
|
||||||
if let Some(decision_model) = decision_model {
|
if let Some(decision_model) = decision_model {
|
||||||
// gpt-5-mini 是推理模型(有图视觉档 / 无图文本档均适用):走 Responses 协议并压到 low
|
// gpt-5-mini 是推理模型(有图视觉档 / 无图文本档均适用):走 Responses 协议并压到 low
|
||||||
// 推理档,否则默认档会把预算全烧在推理上、返回空答案。
|
// 推理档,否则默认档会把预算全烧在推理上、返回空答案。
|
||||||
@@ -296,8 +295,7 @@ async fn record_editor_screen_background_decision_llm_error(
|
|||||||
prompt_chars: usize,
|
prompt_chars: usize,
|
||||||
reference_image_count: 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::InvalidConfig(_) | LlmError::InvalidRequest(_) => return,
|
||||||
LlmError::Timeout { .. } => ("request_timeout", None, true, true, None, None),
|
LlmError::Timeout { .. } => ("request_timeout", None, true, true, None, None),
|
||||||
LlmError::Connectivity { message, .. } => (
|
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::EmptyResponse => ("missing_response", Some(200), false, false, None, None),
|
||||||
LlmError::StreamUnavailable => ("response_body", Some(200), false, true, None, None),
|
LlmError::StreamUnavailable => ("response_body", Some(200), false, true, None, None),
|
||||||
LlmError::Transport(message) => (
|
LlmError::Transport(message) => {
|
||||||
"transport",
|
("transport", None, false, true, Some(message.as_str()), None)
|
||||||
None,
|
}
|
||||||
false,
|
|
||||||
true,
|
|
||||||
Some(message.as_str()),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
record_editor_screen_background_decision_failure(
|
record_editor_screen_background_decision_failure(
|
||||||
audit,
|
audit,
|
||||||
@@ -754,8 +747,7 @@ mod tests {
|
|||||||
request_id: Some("request-1".to_string()),
|
request_id: Some("request-1".to_string()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let tracking =
|
let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
|
||||||
crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
|
|
||||||
|
|
||||||
assert_eq!(audit.provider, "vector-engine");
|
assert_eq!(audit.provider, "vector-engine");
|
||||||
assert_eq!(audit.endpoint, "https://vector.example/v1/responses");
|
assert_eq!(audit.endpoint, "https://vector.example/v1/responses");
|
||||||
@@ -809,8 +801,7 @@ mod tests {
|
|||||||
1,
|
1,
|
||||||
&ExternalApiAuditContext::default(),
|
&ExternalApiAuditContext::default(),
|
||||||
);
|
);
|
||||||
let tracking =
|
let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
|
||||||
crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
|
|
||||||
|
|
||||||
assert_eq!(audit.failure_stage, "request_timeout");
|
assert_eq!(audit.failure_stage, "request_timeout");
|
||||||
assert_eq!(audit.status_code, None);
|
assert_eq!(audit.status_code, None);
|
||||||
@@ -897,7 +888,8 @@ mod tests {
|
|||||||
let (k, v) = trimmed.split_once('=').unwrap();
|
let (k, v) = trimmed.split_once('=').unwrap();
|
||||||
let v = v.trim().trim_matches('"').trim_matches('\'');
|
let v = v.trim().trim_matches('"').trim_matches('\'');
|
||||||
// 先出现的文件优先(.env.local > .env.secrets.local > .env),与服务端 dotenv 顺序一致。
|
// 先出现的文件优先(.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())
|
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 image = RgbaImage::from_pixel(64, 64, Rgba([120, 180, 120, 255]));
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
image::DynamicImage::ImageRgba8(image)
|
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");
|
.expect("test image should encode");
|
||||||
format!(
|
format!(
|
||||||
"data:image/png;base64,{}",
|
"data:image/png;base64,{}",
|
||||||
@@ -966,7 +961,11 @@ mod tests {
|
|||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[live 无图] mode={:?} hex={} label={} attempts={} fallback={}",
|
"[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_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1000,7 +999,11 @@ mod tests {
|
|||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[live 有图] mode={:?} hex={} label={} attempts={} fallback={}",
|
"[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_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -203,11 +203,12 @@ impl ForegroundHistogram {
|
|||||||
.filter(|bin| bin.mass >= mass_floor)
|
.filter(|bin| bin.mass >= mass_floor)
|
||||||
.filter_map(|bin| {
|
.filter_map(|bin| {
|
||||||
let lab = bin.mean();
|
let lab = bin.mean();
|
||||||
(lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B)
|
(lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B).then(
|
||||||
.then(|| SkinReference {
|
|| SkinReference {
|
||||||
lab,
|
lab,
|
||||||
rgb: bin.mean_rgb(),
|
rgb: bin.mean_rgb(),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.max_by(|left, right| {
|
.max_by(|left, right| {
|
||||||
left.lab[0]
|
left.lab[0]
|
||||||
@@ -481,12 +482,18 @@ mod tests {
|
|||||||
let report = report_for(&transparent_image_with_center_block([250, 224, 200]), true);
|
let report = report_for(&transparent_image_with_center_block([250, 224, 200]), true);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
report.excluded.iter().any(|(color, _)| color.hex == "#FFD6C2"),
|
report
|
||||||
|
.excluded
|
||||||
|
.iter()
|
||||||
|
.any(|(color, _)| color.hex == "#FFD6C2"),
|
||||||
"肤色前景应剔除暖浅桃色,excluded: {}",
|
"肤色前景应剔除暖浅桃色,excluded: {}",
|
||||||
report.excluded_summary()
|
report.excluded_summary()
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
report.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"),
|
report
|
||||||
|
.excluded
|
||||||
|
.iter()
|
||||||
|
.any(|(color, _)| color.hex == "#FFF2A8"),
|
||||||
"肤色前景应剔除淡黄(Rule 2/3 关键新覆盖),excluded: {}",
|
"肤色前景应剔除淡黄(Rule 2/3 关键新覆盖),excluded: {}",
|
||||||
report.excluded_summary()
|
report.excluded_summary()
|
||||||
);
|
);
|
||||||
@@ -520,7 +527,10 @@ mod tests {
|
|||||||
|
|
||||||
let enabled = report_for(&image, true);
|
let enabled = report_for(&image, true);
|
||||||
assert!(
|
assert!(
|
||||||
enabled.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"),
|
enabled
|
||||||
|
.excluded
|
||||||
|
.iter()
|
||||||
|
.any(|(color, _)| color.hex == "#FFF2A8"),
|
||||||
"开启皮肤否决时淡黄应被剔除,excluded: {}",
|
"开启皮肤否决时淡黄应被剔除,excluded: {}",
|
||||||
enabled.excluded_summary()
|
enabled.excluded_summary()
|
||||||
);
|
);
|
||||||
@@ -542,7 +552,10 @@ mod tests {
|
|||||||
let report = report_for(&transparent_image_with_center_block([127, 179, 255]), false);
|
let report = report_for(&transparent_image_with_center_block([127, 179, 255]), false);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
report.excluded.iter().any(|(color, _)| color.hex == "#7FB3FF"),
|
report
|
||||||
|
.excluded
|
||||||
|
.iter()
|
||||||
|
.any(|(color, _)| color.hex == "#7FB3FF"),
|
||||||
"蓝色前景应剔除中度天蓝,excluded: {}",
|
"蓝色前景应剔除中度天蓝,excluded: {}",
|
||||||
report.excluded_summary()
|
report.excluded_summary()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -172,24 +172,14 @@ fn map_external_generation_job_status(
|
|||||||
100,
|
100,
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
"running" => (
|
"running" => (ExternalGenerationJobStatus::Running, "正在生成。", 35, None),
|
||||||
ExternalGenerationJobStatus::Running,
|
|
||||||
"正在生成。",
|
|
||||||
35,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
"failed" => (
|
"failed" => (
|
||||||
ExternalGenerationJobStatus::Failed,
|
ExternalGenerationJobStatus::Failed,
|
||||||
"生成失败。",
|
"生成失败。",
|
||||||
0,
|
0,
|
||||||
job.last_error_message.clone(),
|
job.last_error_message.clone(),
|
||||||
),
|
),
|
||||||
_ => (
|
_ => (ExternalGenerationJobStatus::Queued, "排队中。", 8, None),
|
||||||
ExternalGenerationJobStatus::Queued,
|
|
||||||
"排队中。",
|
|
||||||
8,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ExternalGenerationJobStatusRecord {
|
ExternalGenerationJobStatusRecord {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Extension, State},
|
extract::{Extension, State},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ mod admin;
|
|||||||
mod admin_accounts;
|
mod admin_accounts;
|
||||||
mod admin_recharge;
|
mod admin_recharge;
|
||||||
mod ai_generation_drafts;
|
mod ai_generation_drafts;
|
||||||
mod aliyun_matting;
|
|
||||||
mod ai_tasks;
|
mod ai_tasks;
|
||||||
|
mod aliyun_matting;
|
||||||
mod api_response;
|
mod api_response;
|
||||||
mod app;
|
mod app;
|
||||||
mod asset_billing;
|
mod asset_billing;
|
||||||
@@ -670,8 +670,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_recharge_expiration_listener_is_limited_to_http_roles() {
|
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(
|
||||||
assert!(should_start_profile_recharge_expiration_listener(ProcessRole::All));
|
ProcessRole::Api
|
||||||
|
));
|
||||||
|
assert!(should_start_profile_recharge_expiration_listener(
|
||||||
|
ProcessRole::All
|
||||||
|
));
|
||||||
assert!(!should_start_profile_recharge_expiration_listener(
|
assert!(!should_start_profile_recharge_expiration_listener(
|
||||||
ProcessRole::ExternalGenerationWorker
|
ProcessRole::ExternalGenerationWorker
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router, middleware,
|
Router, middleware,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use axum::http::{HeaderValue, StatusCode};
|
use axum::http::{HeaderValue, StatusCode};
|
||||||
use platform_auth::{AuthPlatformErrorKind, WechatProviderError};
|
use platform_auth::{AuthPlatformErrorKind, WechatProviderError};
|
||||||
use platform_llm::{LlmError, LlmErrorKind};
|
use platform_llm::{LlmError, LlmErrorKind};
|
||||||
use platform_oss::{OssError, OssErrorKind};
|
use platform_oss::{OssError, OssErrorKind};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
time::{Instant, SystemTime, UNIX_EPOCH},
|
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -626,14 +626,12 @@ impl AppState {
|
|||||||
let models = editor_generation_pricing_to_records(&next)?;
|
let models = editor_generation_pricing_to_records(&next)?;
|
||||||
let record = self
|
let record = self
|
||||||
.spacetime_client
|
.spacetime_client
|
||||||
.upsert_editor_generation_pricing_config(
|
.upsert_editor_generation_pricing_config(editor_generation_pricing_upsert_input(
|
||||||
editor_generation_pricing_upsert_input(
|
&self.config,
|
||||||
&self.config,
|
admin_user_id,
|
||||||
admin_user_id,
|
models,
|
||||||
models,
|
crate::editor_project::current_utc_micros(),
|
||||||
crate::editor_project::current_utc_micros(),
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| EditorGenerationPricingError::Persistence(error.to_string()))?;
|
.map_err(|error| EditorGenerationPricingError::Persistence(error.to_string()))?;
|
||||||
let pricing = editor_generation_pricing_from_record(record)?;
|
let pricing = editor_generation_pricing_from_record(record)?;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Extension, Query, State},
|
extract::{Extension, Query, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use platform_auth::{
|
use platform_auth::{
|
||||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
||||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
||||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_USER_INFO_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
Reference in New Issue
Block a user