Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	.hermes/shared-memory/project-overview.md
#	docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
#	scripts/dev.test.ts
#	server-rs/crates/api-server/src/creation_entry_config.rs
#	server-rs/crates/api-server/src/wooden_fish.rs
#	server-rs/crates/module-auth/src/lib.rs
#	server-rs/crates/spacetime-client/src/wooden_fish.rs
#	server-rs/crates/spacetime-module/src/auth/procedures.rs
#	src/components/custom-world-home/creationWorkShelf.ts
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/rpgEntryWorldPresentation.ts
#	src/services/miniGameDraftGenerationProgress.test.ts
#	src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
2026-06-04 11:24:14 +08:00
451 changed files with 18452 additions and 5266 deletions

View File

@@ -41,7 +41,10 @@ use crate::{
start_visual_novel_run, stream_visual_novel_action, stream_visual_novel_message,
submit_visual_novel_message, update_visual_novel_work,
},
wechat_pay::handle_wechat_pay_notify,
wechat_pay::{
handle_wechat_pay_notify, handle_wechat_virtual_payment_message_push_verify,
handle_wechat_virtual_payment_notify,
},
};
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
@@ -71,6 +74,11 @@ pub fn build_router(state: AppState) -> Router {
"/api/profile/recharge/wechat/notify",
post(handle_wechat_pay_notify),
)
.route(
"/api/profile/recharge/wechat/virtual-notify",
get(handle_wechat_virtual_payment_message_push_verify)
.post(handle_wechat_virtual_payment_notify),
)
.route(
"/api/runtime/sessions/{runtime_session_id}/inventory",
get(get_runtime_inventory_state).route_layer(middleware::from_fn_with_state(
@@ -511,6 +519,40 @@ mod tests {
.to_string()
}
/// 中文注释:后台路由测试通过真实登录流程取 token避免绕过鉴权中间件。
async fn read_admin_access_token(app: Router) -> String {
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/login")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"username": "root",
"password": "secret123"
})
.to_string(),
))
.expect("admin login request should build"),
)
.await
.expect("admin login request should succeed");
let body = response
.into_body()
.collect()
.await
.expect("admin login body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("admin login payload should be json");
payload["token"]
.as_str()
.expect("admin token should exist")
.to_string()
}
async fn password_login_request(
app: Router,
phone_number: &str,
@@ -699,7 +741,8 @@ mod tests {
let response = app
.oneshot(
Request::builder()
.uri("/api/runtime/puzzle/works")
.method("POST")
.uri("/api/runtime/puzzle/agent/sessions")
.body(Body::empty())
.expect("request should build"),
)
@@ -715,6 +758,31 @@ mod tests {
assert_eq!(body["error"]["details"]["creationTypeId"], "puzzle");
}
#[tokio::test]
async fn disabled_creation_entry_does_not_block_published_runtime_routes() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state.set_test_creation_entry_route_enabled("puzzle", false);
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/puzzle/runs")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_ne!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = read_json_response(response).await;
assert_ne!(
body["error"]["details"]["reason"],
"creation_entry_disabled"
);
}
#[tokio::test]
async fn disabled_visual_novel_creation_route_returns_service_unavailable() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -748,7 +816,7 @@ mod tests {
}
#[tokio::test]
async fn disabled_rpg_route_returns_service_unavailable() {
async fn disabled_rpg_creation_route_returns_service_unavailable() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state.set_test_creation_entry_route_enabled("rpg", false);
let app = build_router(state);
@@ -3973,6 +4041,91 @@ mod tests {
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
/// 中文注释:验证入口公告表单提交的 HTML 会保存进独立公告配置。
#[tokio::test]
async fn admin_creation_entry_banners_route_saves_html_form_payload() {
let mut config = AppConfig::default();
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let app = build_router(AppState::new(config).expect("state should build"));
let admin_token = read_admin_access_token(app.clone()).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/creation-entry/config/banners")
.header("authorization", format!("Bearer {admin_token}"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"eventBannersJson": serde_json::json!([
{
"title": "后台表单公告",
"htmlCode": "<section>入口公告 HTML</section>"
}
]).to_string()
})
.to_string(),
))
.expect("banners request should build"),
)
.await
.expect("banners request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("banners body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("banners payload should be json");
assert_eq!(payload["eventBanners"][0]["title"], "后台表单公告");
assert_eq!(payload["eventBanners"][0]["renderMode"], "html");
assert_eq!(
payload["eventBanners"][0]["htmlCode"],
"<section>入口公告 HTML</section>"
);
}
/// 中文注释:验证入口公告拒绝可执行脚本,避免后台配置变成不受控注入。
#[tokio::test]
async fn admin_creation_entry_banners_route_rejects_script_html() {
let mut config = AppConfig::default();
config.admin_username = Some("root".to_string());
config.admin_password = Some("secret123".to_string());
let app = build_router(AppState::new(config).expect("state should build"));
let admin_token = read_admin_access_token(app.clone()).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/admin/api/creation-entry/config/banners")
.header("authorization", format!("Bearer {admin_token}"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"eventBannersJson": serde_json::json!([
{
"title": "危险公告",
"htmlCode": "<script>alert(1)</script>"
}
]).to_string()
})
.to_string(),
))
.expect("banners request should build"),
)
.await
.expect("banners request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn admin_debug_http_can_probe_healthz_when_authenticated() {
let mut config = AppConfig::default();