From 04dfce57e6478e2f19ecb681d02e84fd7c2ec335 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 28 Apr 2026 12:14:07 +0800 Subject: [PATCH 1/2] feat: add asset operation wallet ledger --- ...ENERATION_POINTS_CONSUMPTION_2026-04-27.md | 34 +- packages/shared/src/contracts/runtime.ts | 4 +- .../crates/api-server/src/asset_billing.rs | 31 +- server-rs/crates/api-server/src/big_fish.rs | 266 +++++------ .../crates/api-server/src/custom_world.rs | 111 ++--- .../crates/api-server/src/custom_world_ai.rs | 425 ++++++++---------- server-rs/crates/api-server/src/puzzle.rs | 276 ++++++------ .../crates/api-server/src/runtime_profile.rs | 22 +- server-rs/crates/module-runtime/src/lib.rs | 16 +- .../crates/shared-contracts/src/runtime.rs | 15 +- .../crates/spacetime-client/src/big_fish.rs | 2 +- .../crates/spacetime-client/src/mapper.rs | 15 +- ..._profile_wallet_ledger_source_type_type.rs | 4 +- .../spacetime-module/src/runtime/profile.rs | 4 +- .../RpgEntryHomeView.recharge.test.tsx | 52 +++ src/components/rpg-entry/RpgEntryHomeView.tsx | 172 ++++++- 16 files changed, 780 insertions(+), 669 deletions(-) diff --git a/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md b/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md index 5daf05d6..c395fb31 100644 --- a/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md +++ b/docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md @@ -1,15 +1,15 @@ -# 资产生成叙世币消耗接入方案 +# 资产操作叙世币消耗接入方案 ## 背景 -当前叙世币钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成由 Axum API 调用外部模型并写入 OSS,SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此扣费需要拆成两层: +当前叙世币钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态,SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层: - SpacetimeDB 负责钱包余额和流水的原子变更。 -- Axum 负责在发起外部生成前扣费,并在生成或持久化失败时补偿退款。 +- Axum 资产操作服务负责在执行业务资产操作前扣费,并在生成、持久化或发布失败时补偿退款。 ## 首期范围 -首期接入带 Bearer 身份、能明确归属真实用户的资产生成与发布入口: +首期接入带 Bearer 身份、能明确归属真实用户的资产操作入口: - `POST /api/custom-world/scene-image` - `POST /api/custom-world/cover-image` @@ -26,28 +26,27 @@ - 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。 - 手动上传封面:不调用外部生成模型,不消耗叙世币。 - 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。 -- 文本实体、NPC 生成:本次需求聚焦资产生成,首期只覆盖图片资产。 +- 文本实体、NPC 生成:本次需求聚焦图片资产和发布资产操作,首期只覆盖可明确归属的入口。 ## 计费规则 -- 每次图片资产生成请求消耗 `1` 枚叙世币。 -- 每次作品发布请求消耗 `1` 枚叙世币;余额不足时禁止发布。 -- 在调用外部图片生成前预扣,余额不足时直接返回业务错误,不调用外部模型。 -- 发布请求在写入发布状态前预扣,余额不足时直接返回业务错误,不调用发布 mutation。 -- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,Axum 自动发起同额退款。 +- 每次可计费资产操作消耗 `1` 枚叙世币。 +- 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。 +- 在调用外部图片生成或发布 mutation 前预扣,余额不足时直接返回业务错误,不继续调用后续资产操作。 +- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。 - 如果退款失败,原始错误仍返回给调用方,同时服务端日志记录退款失败,便于后续人工核对。 ## 钱包流水 -新增两个流水来源类型,首期同时覆盖“资产生成”和“资产发布”这两类资产操作: +公开两个流水来源类型,统一覆盖“资产生成”和“资产发布”这两类资产操作: -- `asset_generation_consume`:资产生成预扣,`amount_delta = -1`。 -- `asset_generation_refund`:资产生成失败退款,`amount_delta = +1`。 +- `asset_operation_consume`:资产操作预扣,`amount_delta = -1`。 +- `asset_operation_refund`:资产操作失败退款,`amount_delta = +1`。 `wallet_ledger_id` 由 Axum 传入,格式: -- 扣费:`asset_generation_consume:{user_id}:{asset_kind}:{asset_id}` -- 退款:`asset_generation_refund:{user_id}:{asset_kind}:{asset_id}` +- 扣费:`asset_operation_consume:{user_id}:{asset_kind}:{asset_id}` +- 退款:`asset_operation_refund:{user_id}:{asset_kind}:{asset_id}` SpacetimeDB procedure 对 `ledger_id` 做幂等保护:如果同一个流水 ID 已存在,则直接返回当前钱包快照,不重复变更余额。 @@ -56,9 +55,10 @@ SpacetimeDB procedure 对 `ledger_id` 做幂等保护:如果同一个流水 ID - `module-runtime`:新增钱包调整输入、钱包调整结果、流水来源枚举。 - `spacetime-module`:新增 `consume_profile_wallet_points_and_return` 与 `refund_profile_wallet_points_and_return` procedure,并扩展钱包变更 helper 支持负数。 - `spacetime-client`:新增对应调用方法和绑定类型。 -- `api-server`:在自定义世界图片生成与发布入口前扣费,错误分支退款。 +- `api-server`:资产操作服务提供统一可计费执行入口,自定义世界、Big Fish、Puzzle 业务 handler 只声明资产操作,不直接调用钱包扣费或退款。 - `shared-contracts`:新增 API 流水来源常量,保证“我的-钱包流水”输出使用稳定契约字符串。 +- `packages/shared` 与前端:统一使用 `asset_operation_consume` / `asset_operation_refund` 展示钱包流水。 ## 非目标 -本次不做分档价格、不做会员免扣、不做前端计费展示改造,也不迁移旧 `server-node` 逻辑。旧资产工坊角色主形象/动作生成与发布接口仍需要先补齐 Bearer 身份归属后再纳入扣费范围。 +本次不做分档价格、不做会员免扣,也不迁移旧 `server-node` 逻辑。旧资产工坊角色主形象/动作生成与发布接口仍需要先补齐 Bearer 身份归属后再纳入扣费范围。旧资产生成流水 source 不再作为公开契约兼容。 diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index e11beb06..5570e378 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -56,8 +56,8 @@ export type ProfileWalletLedgerEntry = { | 'invite_inviter_reward' | 'invite_invitee_reward' | 'points_recharge' - | 'asset_generation_consume' - | 'asset_generation_refund'; + | 'asset_operation_consume' + | 'asset_operation_refund'; createdAt: string; }; diff --git a/server-rs/crates/api-server/src/asset_billing.rs b/server-rs/crates/api-server/src/asset_billing.rs index 95ac102f..13d9485a 100644 --- a/server-rs/crates/api-server/src/asset_billing.rs +++ b/server-rs/crates/api-server/src/asset_billing.rs @@ -1,3 +1,5 @@ +use std::future::Future; + use axum::http::StatusCode; use serde_json::json; use spacetime_client::SpacetimeClientError; @@ -6,15 +8,36 @@ use crate::{http_error::AppError, state::AppState}; pub(crate) const ASSET_OPERATION_POINTS_COST: u64 = 1; +/// 资产操作统一执行入口:业务层只声明操作类型与资源 ID,钱包扣退费由服务层收口。 +pub(crate) async fn execute_billable_asset_operation( + state: &AppState, + owner_user_id: &str, + asset_kind: &str, + asset_id: &str, + operation: Fut, +) -> Result +where + Fut: Future>, +{ + consume_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await?; + match operation.await { + Ok(value) => Ok(value), + Err(error) => { + refund_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await; + Err(error) + } + } +} + /// 资产操作统一预扣叙世币;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。 -pub(crate) async fn consume_asset_operation_points( +async fn consume_asset_operation_points( state: &AppState, owner_user_id: &str, asset_kind: &str, asset_id: &str, ) -> Result<(), AppError> { let ledger_id = format!( - "asset_generation_consume:{}:{}:{}", + "asset_operation_consume:{}:{}:{}", owner_user_id, asset_kind, asset_id ); state @@ -31,14 +54,14 @@ pub(crate) async fn consume_asset_operation_points( } /// 外部生成或发布 mutation 失败后补偿退款;退款失败只记日志,避免覆盖原始业务错误。 -pub(crate) async fn refund_asset_operation_points( +async fn refund_asset_operation_points( state: &AppState, owner_user_id: &str, asset_kind: &str, asset_id: &str, ) { let ledger_id = format!( - "asset_generation_refund:{}:{}:{}", + "asset_operation_refund:{}:{}:{}", owner_user_id, asset_kind, asset_id ); if let Err(error) = state diff --git a/server-rs/crates/api-server/src/big_fish.rs b/server-rs/crates/api-server/src/big_fish.rs index 3fa14974..93d774dd 100644 --- a/server-rs/crates/api-server/src/big_fish.rs +++ b/server-rs/crates/api-server/src/big_fish.rs @@ -46,7 +46,7 @@ use crate::{ AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter, }, api_response::json_success_body, - asset_billing::{consume_asset_operation_points, refund_asset_operation_points}, + asset_billing::execute_billable_asset_operation, auth::AuthenticatedAccessToken, http_error::AppError, request_context::RequestContext, @@ -507,182 +507,118 @@ pub async fn execute_big_fish_action( _ => None, }; let billing_asset_id = format!("{session_id}:{now}"); - if let Some(asset_kind) = billed_asset_kind { - consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id) - .await - .map_err(|error| big_fish_error_response(&request_context, error))?; - } - - let session_result = match action.as_str() { - "big_fish_compile_draft" => { - compile_big_fish_draft_with_all_assets( + let session_operation = async { + match action.as_str() { + "big_fish_compile_draft" => compile_big_fish_draft_with_all_assets( &state, session_id.clone(), owner_user_id.clone(), now, ) .await - } - "big_fish_generate_level_main_image" => { - let asset_url = generate_big_fish_formal_asset( - &state, - &owner_user_id, - &session_id, - "level_main_image", - payload.level, - None, - now, - ) - .await - .map_err(|error| { - if let Some(asset_kind) = billed_asset_kind { - tokio::spawn({ - let state = state.clone(); - let owner_user_id = owner_user_id.clone(); - let billing_asset_id = billing_asset_id.clone(); - async move { - refund_asset_operation_points( - &state, - &owner_user_id, - asset_kind, - &billing_asset_id, - ) - .await; - } - }); - } - big_fish_error_response(&request_context, error) - })?; - state - .spacetime_client() - .generate_big_fish_asset(BigFishAssetGenerateRecordInput { - owner_user_id: owner_user_id.clone(), - session_id: session_id.clone(), - asset_kind: "level_main_image".to_string(), - level: payload.level, - motion_key: None, - asset_url: Some(asset_url), - generated_at_micros: now, - }) - .await - } - "big_fish_generate_level_motion" => { - let asset_url = generate_big_fish_formal_asset( - &state, - &owner_user_id, - &session_id, - "level_motion", - payload.level, - payload.motion_key.as_deref(), - now, - ) - .await - .map_err(|error| { - if let Some(asset_kind) = billed_asset_kind { - tokio::spawn({ - let state = state.clone(); - let owner_user_id = owner_user_id.clone(); - let billing_asset_id = billing_asset_id.clone(); - async move { - refund_asset_operation_points( - &state, - &owner_user_id, - asset_kind, - &billing_asset_id, - ) - .await; - } - }); - } - big_fish_error_response(&request_context, error) - })?; - state - .spacetime_client() - .generate_big_fish_asset(BigFishAssetGenerateRecordInput { - owner_user_id: owner_user_id.clone(), - session_id: session_id.clone(), - asset_kind: "level_motion".to_string(), - level: payload.level, - motion_key: payload.motion_key, - asset_url: Some(asset_url), - generated_at_micros: now, - }) - .await - } - "big_fish_generate_stage_background" => { - let asset_url = generate_big_fish_formal_asset( - &state, - &owner_user_id, - &session_id, - "stage_background", - None, - None, - now, - ) - .await - .map_err(|error| { - if let Some(asset_kind) = billed_asset_kind { - tokio::spawn({ - let state = state.clone(); - let owner_user_id = owner_user_id.clone(); - let billing_asset_id = billing_asset_id.clone(); - async move { - refund_asset_operation_points( - &state, - &owner_user_id, - asset_kind, - &billing_asset_id, - ) - .await; - } - }); - } - big_fish_error_response(&request_context, error) - })?; - state - .spacetime_client() - .generate_big_fish_asset(BigFishAssetGenerateRecordInput { - owner_user_id: owner_user_id.clone(), - session_id: session_id.clone(), - asset_kind: "stage_background".to_string(), - level: None, - motion_key: None, - asset_url: Some(asset_url), - generated_at_micros: now, - }) - .await - } - "big_fish_publish_game" => { - state + .map_err(map_big_fish_client_error), + "big_fish_generate_level_main_image" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "level_main_image", + payload.level, + None, + now, + ) + .await?; + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + owner_user_id: owner_user_id.clone(), + session_id: session_id.clone(), + asset_kind: "level_main_image".to_string(), + level: payload.level, + motion_key: None, + asset_url: Some(asset_url), + generated_at_micros: now, + }) + .await + .map_err(map_big_fish_client_error) + } + "big_fish_generate_level_motion" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "level_motion", + payload.level, + payload.motion_key.as_deref(), + now, + ) + .await?; + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + owner_user_id: owner_user_id.clone(), + session_id: session_id.clone(), + asset_kind: "level_motion".to_string(), + level: payload.level, + motion_key: payload.motion_key, + asset_url: Some(asset_url), + generated_at_micros: now, + }) + .await + .map_err(map_big_fish_client_error) + } + "big_fish_generate_stage_background" => { + let asset_url = generate_big_fish_formal_asset( + &state, + &owner_user_id, + &session_id, + "stage_background", + None, + None, + now, + ) + .await?; + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + owner_user_id: owner_user_id.clone(), + session_id: session_id.clone(), + asset_kind: "stage_background".to_string(), + level: None, + motion_key: None, + asset_url: Some(asset_url), + generated_at_micros: now, + }) + .await + .map_err(map_big_fish_client_error) + } + "big_fish_publish_game" => state .spacetime_client() .publish_big_fish_game(session_id, owner_user_id.clone(), now) .await - } - other => { - return Err(big_fish_bad_request( - &request_context, - format!("action `{other}` is not supported").as_str(), - )); + .map_err(map_big_fish_client_error), + other => Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": format!("action `{other}` is not supported"), + })), + ), } }; - let session = match session_result { - Ok(session) => session, - Err(error) => { - if let Some(asset_kind) = billed_asset_kind { - refund_asset_operation_points( - &state, - &owner_user_id, - asset_kind, - &billing_asset_id, - ) - .await; - } - return Err(big_fish_error_response( - &request_context, - map_big_fish_client_error(error), - )); - } + let session_result = if let Some(asset_kind) = billed_asset_kind { + execute_billable_asset_operation( + &state, + &owner_user_id, + asset_kind, + &billing_asset_id, + session_operation, + ) + .await + } else { + session_operation.await }; + let session = + session_result.map_err(|error| big_fish_error_response(&request_context, error))?; Ok(json_success_body( Some(&request_context), diff --git a/server-rs/crates/api-server/src/custom_world.rs b/server-rs/crates/api-server/src/custom_world.rs index a0caaebf..92473e23 100644 --- a/server-rs/crates/api-server/src/custom_world.rs +++ b/server-rs/crates/api-server/src/custom_world.rs @@ -51,7 +51,7 @@ use crate::{ AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter, }, api_response::json_success_body, - asset_billing::{consume_asset_operation_points, refund_asset_operation_points}, + asset_billing::execute_billable_asset_operation, auth::AuthenticatedAccessToken, character_visual_assets::generate_character_primary_visual_for_profile, custom_world_agent_entities::generate_custom_world_agent_entities, @@ -351,37 +351,31 @@ pub async fn publish_custom_world_library_profile( )); } - consume_asset_operation_points(&state, &owner_user_id, "custom_world_publish", &profile_id) - .await - .map_err(|error| custom_world_error_response(&request_context, error))?; - - let mutation_result = state - .spacetime_client() - .publish_custom_world_profile( - profile_id.clone(), - owner_user_id.clone(), - None, - resolve_author_public_user_code(&state, &authenticated, &request_context)?, - resolve_author_display_name(&state, &authenticated), - current_utc_micros(), - ) - .await; - let mutation = match mutation_result { - Ok(mutation) => mutation, - Err(error) => { - refund_asset_operation_points( - &state, - &owner_user_id, - "custom_world_publish", - &profile_id, - ) - .await; - return Err(custom_world_error_response( - &request_context, - map_custom_world_client_error(error), - )); - } - }; + let author_public_user_code = + resolve_author_public_user_code(&state, &authenticated, &request_context)?; + let author_display_name = resolve_author_display_name(&state, &authenticated); + let mutation = execute_billable_asset_operation( + &state, + &owner_user_id, + "custom_world_publish", + &profile_id, + async { + state + .spacetime_client() + .publish_custom_world_profile( + profile_id.clone(), + owner_user_id.clone(), + None, + author_public_user_code, + author_display_name, + current_utc_micros(), + ) + .await + .map_err(map_custom_world_client_error) + }, + ) + .await + .map_err(|error| custom_world_error_response(&request_context, error))?; Ok(json_success_body( Some(&request_context), @@ -1246,46 +1240,33 @@ pub async fn execute_custom_world_agent_action( }; let should_bill_publish = action == "publish_world"; - if should_bill_publish { - consume_asset_operation_points( + let operation_future = async { + state + .spacetime_client() + .execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput { + session_id: session_id.clone(), + owner_user_id: owner_user_id.clone(), + operation_id: build_prefixed_uuid_id("operation-"), + action: action.clone(), + payload_json: Some(payload_json), + submitted_at_micros, + }) + .await + .map_err(map_custom_world_client_error) + }; + let result = if should_bill_publish { + execute_billable_asset_operation( &state, &owner_user_id, "custom_world_agent_publish", &session_id, + operation_future, ) .await - .map_err(|error| custom_world_error_response(&request_context, error))?; - } - - let result = match state - .spacetime_client() - .execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput { - session_id: session_id.clone(), - owner_user_id: owner_user_id.clone(), - operation_id: build_prefixed_uuid_id("operation-"), - action: action.clone(), - payload_json: Some(payload_json), - submitted_at_micros, - }) - .await - { - Ok(result) => result, - Err(error) => { - if should_bill_publish { - refund_asset_operation_points( - &state, - &owner_user_id, - "custom_world_agent_publish", - &session_id, - ) - .await; - } - return Err(custom_world_error_response( - &request_context, - map_custom_world_client_error(error), - )); - } + } else { + operation_future.await }; + let result = result.map_err(|error| custom_world_error_response(&request_context, error))?; if matches!( action.as_str(), diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index f1b862e1..225a8fee 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -28,6 +28,7 @@ use webp::Encoder as WebpEncoder; use crate::{ api_response::json_success_body, + asset_billing::execute_billable_asset_operation, auth::AuthenticatedAccessToken, custom_world_result_prompts::{ build_result_entity_system_prompt, build_result_entity_user_prompt, @@ -441,126 +442,111 @@ pub async fn generate_custom_world_scene_image( let normalized = normalize_scene_image_request(payload) .map_err(|error| custom_world_ai_error_response(&request_context, error))?; let asset_id = format!("custom-scene-{}", current_utc_millis()); - crate::asset_billing::consume_asset_operation_points( + let asset = execute_billable_asset_operation( &state, &owner_user_id, "scene_image", asset_id.as_str(), + async { + let settings = require_dashscope_settings(&state)?; + let http_client = build_dashscope_http_client(&settings)?; + let reference_image = + if let Some(reference_image_src) = normalized.reference_image_src.as_deref() { + Some( + resolve_reference_image_as_data_url( + &state, + &http_client, + reference_image_src, + "referenceImageSrc", + ) + .await?, + ) + } else { + None + }; + let generated = if let Some(reference_image) = reference_image.as_deref() { + create_reference_image_generation( + &http_client, + &settings, + state.config.dashscope_reference_image_model.as_str(), + normalized.prompt.as_str(), + normalized.size.as_str(), + &[reference_image.to_string()], + Some(normalized.negative_prompt.as_str()), + "创建参考图场景编辑任务失败", + "参考图场景编辑未返回图片地址", + "scene-edit", + ) + .await + } else { + create_text_to_image_generation( + &http_client, + &settings, + state.config.dashscope_scene_image_model.as_str(), + normalized.prompt.as_str(), + Some(normalized.negative_prompt.as_str()), + normalized.size.as_str(), + "创建场景图片生成任务失败", + "查询场景图片任务失败", + "场景图片生成任务失败", + "场景图片生成超时或未返回图片地址", + ) + .await + }?; + let scene_model = if reference_image.is_some() { + state.config.dashscope_reference_image_model.clone() + } else { + state.config.dashscope_scene_image_model.clone() + }; + let downloaded = download_remote_image( + &http_client, + generated.image_url.as_str(), + "下载生成图片失败", + ) + .await?; + let upload = PreparedAssetUpload { + prefix: LegacyAssetPrefix::CustomWorldScenes, + path_segments: vec![ + sanitize_storage_segment( + normalized + .profile_id + .as_deref() + .unwrap_or(normalized.world_name.as_str()), + "world", + ), + sanitize_storage_segment(normalized.entity_id.as_str(), "scene"), + asset_id.clone(), + ], + file_name: format!("scene.{}", downloaded.extension), + content_type: downloaded.mime_type, + body: downloaded.bytes, + asset_kind: "scene_image", + entity_kind: "custom_world_landmark", + entity_id: normalized.entity_id.clone(), + profile_id: normalized.profile_id.clone(), + slot: "scene_image", + source_job_id: Some(generated.task_id.clone()), + }; + persist_custom_world_asset( + &state, + &owner_user_id, + upload, + GeneratedAssetResponse { + image_src: String::new(), + asset_id: asset_id.clone(), + source_type: "generated".to_string(), + model: Some(scene_model), + size: Some(normalized.size), + task_id: Some(generated.task_id), + prompt: Some(normalized.prompt), + actual_prompt: generated.actual_prompt, + }, + ) + .await + }, ) .await .map_err(|error| custom_world_ai_error_response(&request_context, error))?; - let asset_result = async { - let settings = require_dashscope_settings(&state)?; - let http_client = build_dashscope_http_client(&settings)?; - let reference_image = - if let Some(reference_image_src) = normalized.reference_image_src.as_deref() { - Some( - resolve_reference_image_as_data_url( - &state, - &http_client, - reference_image_src, - "referenceImageSrc", - ) - .await?, - ) - } else { - None - }; - let generated = if let Some(reference_image) = reference_image.as_deref() { - create_reference_image_generation( - &http_client, - &settings, - state.config.dashscope_reference_image_model.as_str(), - normalized.prompt.as_str(), - normalized.size.as_str(), - &[reference_image.to_string()], - Some(normalized.negative_prompt.as_str()), - "创建参考图场景编辑任务失败", - "参考图场景编辑未返回图片地址", - "scene-edit", - ) - .await - } else { - create_text_to_image_generation( - &http_client, - &settings, - state.config.dashscope_scene_image_model.as_str(), - normalized.prompt.as_str(), - Some(normalized.negative_prompt.as_str()), - normalized.size.as_str(), - "创建场景图片生成任务失败", - "查询场景图片任务失败", - "场景图片生成任务失败", - "场景图片生成超时或未返回图片地址", - ) - .await - }?; - let scene_model = if reference_image.is_some() { - state.config.dashscope_reference_image_model.clone() - } else { - state.config.dashscope_scene_image_model.clone() - }; - let downloaded = download_remote_image( - &http_client, - generated.image_url.as_str(), - "下载生成图片失败", - ) - .await?; - let upload = PreparedAssetUpload { - prefix: LegacyAssetPrefix::CustomWorldScenes, - path_segments: vec![ - sanitize_storage_segment( - normalized - .profile_id - .as_deref() - .unwrap_or(normalized.world_name.as_str()), - "world", - ), - sanitize_storage_segment(normalized.entity_id.as_str(), "scene"), - asset_id.clone(), - ], - file_name: format!("scene.{}", downloaded.extension), - content_type: downloaded.mime_type, - body: downloaded.bytes, - asset_kind: "scene_image", - entity_kind: "custom_world_landmark", - entity_id: normalized.entity_id.clone(), - profile_id: normalized.profile_id.clone(), - slot: "scene_image", - source_job_id: Some(generated.task_id.clone()), - }; - persist_custom_world_asset( - &state, - &owner_user_id, - upload, - GeneratedAssetResponse { - image_src: String::new(), - asset_id: asset_id.clone(), - source_type: "generated".to_string(), - model: Some(scene_model), - size: Some(normalized.size), - task_id: Some(generated.task_id), - prompt: Some(normalized.prompt), - actual_prompt: generated.actual_prompt, - }, - ) - .await - } - .await; - - let asset = match asset_result { - Ok(asset) => asset, - Err(error) => { - crate::asset_billing::refund_asset_operation_points( - &state, - &owner_user_id, - "scene_image", - &asset_id, - ) - .await; - return Err(custom_world_ai_error_response(&request_context, error)); - } - }; Ok(json_success_body(Some(&request_context), asset)) } @@ -717,127 +703,112 @@ pub async fn generate_custom_world_cover_image( let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone()); let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string()); let asset_id = format!("custom-cover-{}", current_utc_millis()); - crate::asset_billing::consume_asset_operation_points( + let asset = execute_billable_asset_operation( &state, &owner_user_id, "custom_world_cover", asset_id.as_str(), + async { + let settings = require_dashscope_settings(&state)?; + let http_client = build_dashscope_http_client(&settings)?; + let reference_sources = collect_cover_reference_image_sources( + &payload.profile, + &payload.character_role_ids, + payload.reference_image_src.as_deref().unwrap_or_default(), + ); + let prompt = build_custom_world_cover_image_prompt( + &payload.profile, + &payload.character_role_ids, + payload.user_prompt.as_deref().unwrap_or_default(), + !reference_sources.is_empty(), + ); + let mut reference_images = Vec::with_capacity(reference_sources.len()); + for source in &reference_sources { + reference_images.push( + resolve_reference_image_as_data_url( + &state, + &http_client, + source.as_str(), + "referenceImageSrc", + ) + .await?, + ); + } + let generated = if reference_images.is_empty() { + create_text_to_image_generation( + &http_client, + &settings, + state.config.dashscope_cover_image_model.clone().as_str(), + prompt.as_str(), + None, + size.as_str(), + "创建作品封面生成任务失败", + "查询作品封面任务失败", + "作品封面生成任务失败", + "作品封面生成超时或未返回图片地址", + ) + .await + } else { + create_reference_image_generation( + &http_client, + &settings, + state.config.dashscope_reference_image_model.as_str(), + prompt.as_str(), + size.as_str(), + &reference_images, + None, + "创建参考图封面任务失败", + "封面生成未返回图片地址", + "cover-edit", + ) + .await + }?; + let downloaded = download_remote_image( + &http_client, + generated.image_url.as_str(), + "下载作品封面失败", + ) + .await?; + let upload = PreparedAssetUpload { + prefix: LegacyAssetPrefix::CustomWorldCovers, + path_segments: vec![ + sanitize_storage_segment(entity_id.as_str(), "world"), + asset_id.clone(), + ], + file_name: format!("cover.{}", downloaded.extension), + content_type: downloaded.mime_type, + body: downloaded.bytes, + asset_kind: "custom_world_cover", + entity_kind: "custom_world_profile", + entity_id, + profile_id, + slot: "cover", + source_job_id: Some(generated.task_id.clone()), + }; + persist_custom_world_asset( + &state, + &owner_user_id, + upload, + GeneratedAssetResponse { + image_src: String::new(), + asset_id: asset_id.clone(), + source_type: "generated".to_string(), + model: Some(if reference_images.is_empty() { + state.config.dashscope_cover_image_model.clone() + } else { + state.config.dashscope_reference_image_model.clone() + }), + size: Some(size), + task_id: Some(generated.task_id), + prompt: Some(prompt), + actual_prompt: generated.actual_prompt, + }, + ) + .await + }, ) .await .map_err(|error| custom_world_ai_error_response(&request_context, error))?; - let asset_result = async { - let settings = require_dashscope_settings(&state)?; - let http_client = build_dashscope_http_client(&settings)?; - let reference_sources = collect_cover_reference_image_sources( - &payload.profile, - &payload.character_role_ids, - payload.reference_image_src.as_deref().unwrap_or_default(), - ); - let prompt = build_custom_world_cover_image_prompt( - &payload.profile, - &payload.character_role_ids, - payload.user_prompt.as_deref().unwrap_or_default(), - !reference_sources.is_empty(), - ); - let mut reference_images = Vec::with_capacity(reference_sources.len()); - for source in &reference_sources { - reference_images.push( - resolve_reference_image_as_data_url( - &state, - &http_client, - source.as_str(), - "referenceImageSrc", - ) - .await?, - ); - } - let generated = if reference_images.is_empty() { - create_text_to_image_generation( - &http_client, - &settings, - state.config.dashscope_cover_image_model.clone().as_str(), - prompt.as_str(), - None, - size.as_str(), - "创建作品封面生成任务失败", - "查询作品封面任务失败", - "作品封面生成任务失败", - "作品封面生成超时或未返回图片地址", - ) - .await - } else { - create_reference_image_generation( - &http_client, - &settings, - state.config.dashscope_reference_image_model.as_str(), - prompt.as_str(), - size.as_str(), - &reference_images, - None, - "创建参考图封面任务失败", - "封面生成未返回图片地址", - "cover-edit", - ) - .await - }?; - let downloaded = download_remote_image( - &http_client, - generated.image_url.as_str(), - "下载作品封面失败", - ) - .await?; - let upload = PreparedAssetUpload { - prefix: LegacyAssetPrefix::CustomWorldCovers, - path_segments: vec![ - sanitize_storage_segment(entity_id.as_str(), "world"), - asset_id.clone(), - ], - file_name: format!("cover.{}", downloaded.extension), - content_type: downloaded.mime_type, - body: downloaded.bytes, - asset_kind: "custom_world_cover", - entity_kind: "custom_world_profile", - entity_id, - profile_id, - slot: "cover", - source_job_id: Some(generated.task_id.clone()), - }; - persist_custom_world_asset( - &state, - &owner_user_id, - upload, - GeneratedAssetResponse { - image_src: String::new(), - asset_id: asset_id.clone(), - source_type: "generated".to_string(), - model: Some(if reference_images.is_empty() { - state.config.dashscope_cover_image_model.clone() - } else { - state.config.dashscope_reference_image_model.clone() - }), - size: Some(size), - task_id: Some(generated.task_id), - prompt: Some(prompt), - actual_prompt: generated.actual_prompt, - }, - ) - .await - } - .await; - - let asset = match asset_result { - Ok(asset) => asset, - Err(error) => { - crate::asset_billing::refund_asset_operation_points( - &state, - &owner_user_id, - "custom_world_cover", - &asset_id, - ) - .await; - return Err(custom_world_ai_error_response(&request_context, error)); - } - }; Ok(json_success_body(Some(&request_context), asset)) } diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index fdeed0b2..63b69a89 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -67,7 +67,7 @@ use tokio::time::sleep; use crate::{ ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter}, api_response::json_success_body, - asset_billing::{consume_asset_operation_points, refund_asset_operation_points}, + asset_billing::execute_billable_asset_operation, auth::AuthenticatedAccessToken, http_error::AppError, prompt::puzzle_image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt}, @@ -442,29 +442,29 @@ pub async fn execute_puzzle_agent_action( let owner_user_id = authenticated.claims().user_id().to_string(); let now = current_utc_micros(); let action = payload.action.trim().to_string(); - let billed_asset_kind = match action.as_str() { - "compile_puzzle_draft" => Some("puzzle_initial_image"), - "generate_puzzle_images" => Some("puzzle_generated_image"), - _ => None, - }; let billing_asset_id = format!("{session_id}:{now}"); - if let Some(asset_kind) = billed_asset_kind { - consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id) + let (operation_type, phase_label, phase_detail, session) = match action.as_str() { + "compile_puzzle_draft" => { + let session = execute_billable_asset_operation( + &state, + &owner_user_id, + "puzzle_initial_image", + &billing_asset_id, + async { + compile_puzzle_draft_with_initial_cover( + &state, + session_id.clone(), + owner_user_id.clone(), + now, + ) + .await + .map_err(map_puzzle_client_error) + }, + ) .await .map_err(|error| { puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error) - })?; - } - - let (operation_type, phase_label, phase_detail, session) = match action.as_str() { - "compile_puzzle_draft" => { - let session = compile_puzzle_draft_with_initial_cover( - &state, - session_id.clone(), - owner_user_id.clone(), - now, - ) - .await; + }); ( "compile_puzzle_draft", "完整拼图草稿", @@ -473,75 +473,76 @@ pub async fn execute_puzzle_agent_action( ) } "generate_puzzle_images" => { - let session = state - .spacetime_client() - .get_puzzle_agent_session(session_id.clone(), owner_user_id.clone()) - .await; - let session = match session { - Ok(session) => { + let session = execute_billable_asset_operation( + &state, + &owner_user_id, + "puzzle_generated_image", + &billing_asset_id, + async { + let session = state + .spacetime_client() + .get_puzzle_agent_session(session_id.clone(), owner_user_id.clone()) + .await + .map_err(map_puzzle_client_error)?; let draft = session.draft.clone().ok_or_else(|| { - SpacetimeClientError::Runtime("拼图结果页草稿尚未生成".to_string()) - }); - match draft { - Ok(draft) => { - let prompt = payload - .prompt_text - .clone() - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| draft.summary.clone()); - // 拼图结果页从多候选抽卡收口为单图替换,前端传入的旧 candidateCount 只做兼容忽略。 - let candidate_count = 1; - let candidate_start_index = draft.candidates.len(); - let candidates = generate_puzzle_image_candidates( - &state, - owner_user_id.as_str(), - &session.session_id, - &draft.level_name, - &prompt, - payload.reference_image_src.as_deref(), - candidate_count, - candidate_start_index, - ) - .await - .map_err(SpacetimeClientError::Runtime); - match candidates { - Ok(candidates) => { - let candidates_json = serde_json::to_string( - &candidates - .iter() - .map(to_puzzle_generated_image_candidate) - .collect::>(), - ) - .map_err(|error| { - SpacetimeClientError::Runtime(format!( - "拼图候选图序列化失败:{error}" - )) - }); - match candidates_json { - Ok(candidates_json) => { - state - .spacetime_client() - .save_puzzle_generated_images( - PuzzleGeneratedImagesSaveRecordInput { - session_id: session.session_id, - owner_user_id: owner_user_id.clone(), - candidates_json, - saved_at_micros: now, - }, - ) - .await - } - Err(error) => Err(error), - } - } - Err(error) => Err(error), - } - } - Err(error) => Err(error), - } - } - Err(error) => Err(error), - }; + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": "拼图结果页草稿尚未生成", + })) + })?; + let prompt = payload + .prompt_text + .clone() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| draft.summary.clone()); + // 拼图结果页从多候选抽卡收口为单图替换,前端传入的旧 candidateCount 只做兼容忽略。 + let candidate_count = 1; + let candidate_start_index = draft.candidates.len(); + let candidates = generate_puzzle_image_candidates( + &state, + owner_user_id.as_str(), + &session.session_id, + &draft.level_name, + &prompt, + payload.reference_image_src.as_deref(), + candidate_count, + candidate_start_index, + ) + .await + .map_err(|message| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": message, + })) + })?; + let candidates_json = serde_json::to_string( + &candidates + .iter() + .map(to_puzzle_generated_image_candidate) + .collect::>(), + ) + .map_err(|error| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": format!("拼图候选图序列化失败:{error}"), + })) + })?; + state + .spacetime_client() + .save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput { + session_id: session.session_id, + owner_user_id: owner_user_id.clone(), + candidates_json, + saved_at_micros: now, + }) + .await + .map_err(map_puzzle_client_error) + }, + ) + .await + .map_err(|error| { + puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error) + }); ( "generate_puzzle_images", "拼图图片生成", @@ -569,7 +570,14 @@ pub async fn execute_puzzle_agent_action( candidate_id, selected_at_micros: now, }) - .await; + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + }); ( "select_puzzle_image", "正式图确认", @@ -579,43 +587,35 @@ pub async fn execute_puzzle_agent_action( } "publish_puzzle_work" => { let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id); - consume_asset_operation_points(&state, &owner_user_id, "puzzle_publish_work", &work_id) - .await - .map_err(|error| { - puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error) - })?; - let profile_result = state - .spacetime_client() - .publish_puzzle_work(PuzzlePublishRecordInput { - session_id: session_id.clone(), - owner_user_id: owner_user_id.clone(), - // 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。 - work_id: work_id.clone(), - profile_id, - author_display_name: resolve_author_display_name(&state, &authenticated), - level_name: payload.level_name.clone(), - summary: payload.summary.clone(), - theme_tags: payload.theme_tags.clone(), - published_at_micros: now, - }) - .await; - let profile = match profile_result { - Ok(profile) => profile, - Err(error) => { - refund_asset_operation_points( - &state, - &owner_user_id, - "puzzle_publish_work", - &work_id, - ) - .await; - return Err(puzzle_error_response( - &request_context, - PUZZLE_AGENT_API_BASE_PROVIDER, - map_puzzle_client_error(error), - )); - } - }; + let author_display_name = resolve_author_display_name(&state, &authenticated); + let profile = execute_billable_asset_operation( + &state, + &owner_user_id, + "puzzle_publish_work", + &work_id, + async { + state + .spacetime_client() + .publish_puzzle_work(PuzzlePublishRecordInput { + session_id: session_id.clone(), + owner_user_id: owner_user_id.clone(), + // 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。 + work_id: work_id.clone(), + profile_id, + author_display_name, + level_name: payload.level_name.clone(), + summary: payload.summary.clone(), + theme_tags: payload.theme_tags.clone(), + published_at_micros: now, + }) + .await + .map_err(map_puzzle_client_error) + }, + ) + .await + .map_err(|error| { + puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error) + })?; let session = state .spacetime_client() @@ -654,29 +654,7 @@ pub async fn execute_puzzle_agent_action( } }; - let session = session.map_err(|error| { - if let Some(asset_kind) = billed_asset_kind { - tokio::spawn({ - let state = state.clone(); - let owner_user_id = owner_user_id.clone(); - let billing_asset_id = billing_asset_id.clone(); - async move { - refund_asset_operation_points( - &state, - &owner_user_id, - asset_kind, - &billing_asset_id, - ) - .await; - } - }); - } - puzzle_error_response( - &request_context, - PUZZLE_AGENT_API_BASE_PROVIDER, - map_puzzle_client_error(error), - ) - })?; + let session = session?; Ok(json_success_body( Some(&request_context), diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 2efe3856..a66a4fec 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -13,8 +13,8 @@ use module_runtime::{ use serde_json::{Value, json}; use shared_contracts::runtime::{ CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse, - PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME, - PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND, + PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME, + PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND, PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD, PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD, PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE, @@ -112,11 +112,11 @@ fn format_profile_wallet_ledger_source_type( RuntimeProfileWalletLedgerSourceType::PointsRecharge => { PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE } - RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume => { - PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME + RuntimeProfileWalletLedgerSourceType::AssetOperationConsume => { + PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME } - RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => { - PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND + RuntimeProfileWalletLedgerSourceType::AssetOperationRefund => { + PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND } } } @@ -417,18 +417,18 @@ mod tests { use crate::{app::build_router, config::AppConfig, state::AppState}; #[test] - fn profile_wallet_ledger_source_type_formats_asset_generation_values() { + fn profile_wallet_ledger_source_type_formats_asset_operation_values() { assert_eq!( format_profile_wallet_ledger_source_type( - RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume + RuntimeProfileWalletLedgerSourceType::AssetOperationConsume ), - shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME + shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME ); assert_eq!( format_profile_wallet_ledger_source_type( - RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund + RuntimeProfileWalletLedgerSourceType::AssetOperationRefund ), - shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND + shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND ); } diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 60a4a02a..766238b9 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -259,8 +259,8 @@ pub enum RuntimeProfileWalletLedgerSourceType { InviteInviterReward, InviteInviteeReward, PointsRecharge, - AssetGenerationConsume, - AssetGenerationRefund, + AssetOperationConsume, + AssetOperationRefund, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] @@ -1506,8 +1506,8 @@ impl RuntimeProfileWalletLedgerSourceType { Self::InviteInviterReward => "invite_inviter_reward", Self::InviteInviteeReward => "invite_invitee_reward", Self::PointsRecharge => "points_recharge", - Self::AssetGenerationConsume => "asset_generation_consume", - Self::AssetGenerationRefund => "asset_generation_refund", + Self::AssetOperationConsume => "asset_operation_consume", + Self::AssetOperationRefund => "asset_operation_refund", } } } @@ -2008,12 +2008,12 @@ mod tests { "points_recharge" ); assert_eq!( - RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume.as_str(), - "asset_generation_consume" + RuntimeProfileWalletLedgerSourceType::AssetOperationConsume.as_str(), + "asset_operation_consume" ); assert_eq!( - RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund.as_str(), - "asset_generation_refund" + RuntimeProfileWalletLedgerSourceType::AssetOperationRefund.as_str(), + "asset_operation_refund" ); } diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 57d5671d..bd78d856 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -7,10 +7,9 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge"; pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward"; pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward"; -pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME: &str = - "asset_generation_consume"; -pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND: &str = - "asset_generation_refund"; +pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str = + "asset_operation_consume"; +pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund"; pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial"; pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane"; pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina"; @@ -791,7 +790,7 @@ mod tests { id: "ledger-5".to_string(), amount_delta: -1, balance_after: 199, - source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME + source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME .to_string(), created_at: "2026-04-22T10:04:00Z".to_string(), }, @@ -799,7 +798,7 @@ mod tests { id: "ledger-6".to_string(), amount_delta: 1, balance_after: 200, - source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND + source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND .to_string(), created_at: "2026-04-22T10:05:00Z".to_string(), }, @@ -827,11 +826,11 @@ mod tests { ); assert_eq!( payload["entries"][4]["sourceType"], - json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME) + json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME) ); assert_eq!( payload["entries"][5]["sourceType"], - json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND) + json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND) ); assert_eq!( payload["entries"][0]["createdAt"], diff --git a/server-rs/crates/spacetime-client/src/big_fish.rs b/server-rs/crates/spacetime-client/src/big_fish.rs index 5544f606..e48decf7 100644 --- a/server-rs/crates/spacetime-client/src/big_fish.rs +++ b/server-rs/crates/spacetime-client/src/big_fish.rs @@ -148,7 +148,7 @@ impl SpacetimeClient { move |_, result| { let mapped = result .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) - .and_then(map_big_fish_works_procedure_result); + .and_then(|result| map_big_fish_works_procedure_result(result, None)); send_once(&sender, mapped); }, ); diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 92e78bbb..52dc3e11 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -3278,11 +3278,11 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back( crate::module_bindings::RuntimeProfileWalletLedgerSourceType::PointsRecharge => { module_runtime::RuntimeProfileWalletLedgerSourceType::PointsRecharge } - crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume => { - module_runtime::RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume + crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetOperationConsume => { + module_runtime::RuntimeProfileWalletLedgerSourceType::AssetOperationConsume } - crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => { - module_runtime::RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund + crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetOperationRefund => { + module_runtime::RuntimeProfileWalletLedgerSourceType::AssetOperationRefund } } } @@ -4626,6 +4626,8 @@ struct CompatibleBigFishWorkSummaryRecord { level_main_image_ready_count: u32, level_motion_ready_count: u32, background_ready: bool, + #[serde(default)] + play_count: u32, } impl CompatibleBigFishWorkSummaryRecord { @@ -4650,6 +4652,7 @@ impl CompatibleBigFishWorkSummaryRecord { level_main_image_ready_count: self.level_main_image_ready_count, level_motion_ready_count: self.level_motion_ready_count, background_ready: self.background_ready, + play_count: self.play_count, } } } @@ -4678,7 +4681,7 @@ mod tests { "level_motion_ready_count":0, "background_ready":false }]"# - .to_string(), + .to_string(), ), error_message: None, }; @@ -4710,7 +4713,7 @@ mod tests { "level_motion_ready_count":16, "background_ready":true }]"# - .to_string(), + .to_string(), ), error_message: None, }; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs index fc2093e3..dd93e385 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs @@ -16,9 +16,9 @@ pub enum RuntimeProfileWalletLedgerSourceType { PointsRecharge, - AssetGenerationConsume, + AssetOperationConsume, - AssetGenerationRefund, + AssetOperationRefund, } impl __sdk::InModule for RuntimeProfileWalletLedgerSourceType { diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index 09ca0cc7..8dc65212 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -248,7 +248,7 @@ pub fn consume_profile_wallet_points_and_return( apply_profile_wallet_adjustment( tx, input.clone(), - RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume, + RuntimeProfileWalletLedgerSourceType::AssetOperationConsume, true, ) }) { @@ -275,7 +275,7 @@ pub fn refund_profile_wallet_points_and_return( apply_profile_wallet_adjustment( tx, input.clone(), - RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund, + RuntimeProfileWalletLedgerSourceType::AssetOperationRefund, false, ) }) { diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 7e3204f8..75efef9e 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -11,7 +11,29 @@ import { } from './RpgEntryHomeView'; import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation'; +const { mockGetRpgProfileWalletLedger } = vi.hoisted(() => ({ + mockGetRpgProfileWalletLedger: vi.fn(async () => ({ + entries: [ + { + id: 'ledger-1', + amountDelta: -1, + balanceAfter: 29, + sourceType: 'asset_operation_consume', + createdAt: '2026-04-28T10:00:00Z', + }, + { + id: 'ledger-2', + amountDelta: 30, + balanceAfter: 30, + sourceType: 'invite_invitee_reward', + createdAt: '2026-04-28T09:00:00Z', + }, + ], + })), +})); + vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({ + getRpgProfileWalletLedger: mockGetRpgProfileWalletLedger, getRpgProfileRechargeCenter: vi.fn(async () => ({ walletBalance: 0, membership: { @@ -285,6 +307,36 @@ test('opens recharge modal and submits points product', async () => { await waitFor(() => expect(onRechargeSuccess).toHaveBeenCalledTimes(1)); }); +test('opens wallet ledger modal from narrative coin card', async () => { + const user = userEvent.setup(); + + renderProfileView(); + await user.click(screen.getByText('剩余叙世币')); + + expect(await screen.findByText('叙世币账单')).toBeTruthy(); + expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1); + expect(screen.getByText('资产操作消耗')).toBeTruthy(); + expect(screen.getByText('-1')).toBeTruthy(); + expect(screen.getByText('填写邀请码奖励')).toBeTruthy(); + expect(screen.getByText('+30')).toBeTruthy(); +}); + +test('wallet ledger modal shows empty and error states', async () => { + const user = userEvent.setup(); + mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] }); + + renderProfileView(); + await user.click(screen.getByText('剩余叙世币')); + expect(await screen.findByText('暂无账单记录')).toBeTruthy(); + + await user.click(screen.getByLabelText('关闭叙世币账单')); + mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败')); + await user.click(screen.getByText('剩余叙世币')); + + expect(await screen.findByText('加载失败')).toBeTruthy(); + expect(screen.getByText('重新加载')).toBeTruthy(); +}); + test('shows a reachable login entry in logged out mobile shell', async () => { const user = userEvent.setup(); const openLoginModal = vi.fn(); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 5b9d370c..3e7edf32 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -34,6 +34,7 @@ import type { PlatformBrowseHistoryEntry, ProfileDashboardCardKey, ProfileDashboardSummary, + ProfileWalletLedgerResponse, ProfileRechargeCenterResponse, ProfileRechargeProduct, ProfileReferralInviteCenterResponse, @@ -46,6 +47,7 @@ import { createRpgProfileRechargeOrder, getRpgProfileRechargeCenter, getRpgProfileReferralInviteCenter, + getRpgProfileWalletLedger, redeemRpgProfileReferralInviteCode, } from '../../services/rpg-entry/rpgProfileClient'; import type { CustomWorldProfile } from '../../types'; @@ -923,6 +925,128 @@ function formatMembershipDuration(days: number) { return `${days}天`; } +const WALLET_LEDGER_SOURCE_LABELS: Record = { + points_recharge: '叙世币充值', + invite_inviter_reward: '邀请奖励', + invite_invitee_reward: '填写邀请码奖励', + snapshot_sync: '账户同步', + asset_operation_consume: '资产操作消耗', + asset_operation_refund: '资产操作退回', +}; + +function formatWalletLedgerAmount(amountDelta: number) { + return amountDelta > 0 ? `+${amountDelta}` : `${amountDelta}`; +} + +function WalletLedgerModal({ + ledger, + fallbackBalance, + isLoading, + error, + onClose, + onRetry, +}: { + ledger: ProfileWalletLedgerResponse | null; + fallbackBalance: number; + isLoading: boolean; + error: string | null; + onClose: () => void; + onRetry: () => void; +}) { + const entries = ledger?.entries ?? []; + const balance = entries[0]?.balanceAfter ?? fallbackBalance; + + return ( +
+
+ +
+
+
+ LEDGER +
+
叙世币账单
+
+ + {balance}叙世币 +
+
+ + {error ? ( +
+
{error}
+ +
+ ) : isLoading ? ( +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+ ))} +
+ ) : entries.length === 0 ? ( +
+ 暂无账单记录 +
+ ) : ( +
+ {entries.map((entry) => { + const isIncome = entry.amountDelta > 0; + const label = + WALLET_LEDGER_SOURCE_LABELS[entry.sourceType] ?? + entry.sourceType; + + return ( +
+
+
+ {label} +
+
+ {formatPlatformWorldTime(entry.createdAt)} +
+
+
+
+ {formatWalletLedgerAmount(entry.amountDelta)} +
+
+ 余额 {entry.balanceAfter} +
+
+
+ ); + })} +
+ )} +
+
+
+ ); +} + function AccountRechargeModal({ center, activeTab, @@ -1304,6 +1428,13 @@ export function RpgEntryHomeView({ const [isLoadingRecharge, setIsLoadingRecharge] = useState(false); const [submittingRechargeProductId, setSubmittingRechargeProductId] = useState(null); + const [isWalletLedgerOpen, setIsWalletLedgerOpen] = useState(false); + const [walletLedger, setWalletLedger] = + useState(null); + const [walletLedgerError, setWalletLedgerError] = useState( + null, + ); + const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false); const [profilePopupPanel, setProfilePopupPanel] = useState(null); const [referralCenter, setReferralCenter] = @@ -1415,6 +1546,23 @@ export function RpgEntryHomeView({ }) .finally(() => setIsLoadingRecharge(false)); }; + const loadWalletLedger = () => { + setWalletLedgerError(null); + setIsLoadingWalletLedger(true); + void getRpgProfileWalletLedger() + .then(setWalletLedger) + .catch((error: unknown) => { + setWalletLedger(null); + setWalletLedgerError( + error instanceof Error ? error.message : '读取叙世币账单失败', + ); + }) + .finally(() => setIsLoadingWalletLedger(false)); + }; + const openWalletLedgerPanel = () => { + setIsWalletLedgerOpen(true); + loadWalletLedger(); + }; const submitRechargeProduct = (product: ProfileRechargeProduct) => { if (submittingRechargeProductId) { return; @@ -1865,7 +2013,7 @@ export function RpgEntryHomeView({ label="剩余叙世币" value="暂不可用" icon={Coins} - onClick={onOpenProfileDashboardCard} + onClick={openWalletLedgerPanel} /> ) : null} + {isWalletLedgerOpen ? ( + setIsWalletLedgerOpen(false)} + onRetry={loadWalletLedger} + /> + ) : null}
); } @@ -2422,6 +2580,16 @@ export function RpgEntryHomeView({ onSubmitRedeem={submitReferralInviteCode} /> ) : null} + {isWalletLedgerOpen ? ( + setIsWalletLedgerOpen(false)} + onRetry={loadWalletLedger} + /> + ) : null} ); } From a4c623884782960e963435dc063044d497a6ba6a Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 28 Apr 2026 15:36:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BACKEND_REWRITE_TASKLIST.md | 12 - .../00_MASTER_TASKLIST.md | 154 --------- .../01_M0_M2_FOUNDATION_AND_AUTH.md | 266 --------------- .../02_M3_RUNTIME_PROFILE.md | 69 ---- .../03_M4_STORY_AND_GAMEPLAY.md | 318 ------------------ .../04_M5_CUSTOM_WORLD_AND_AGENT.md | 117 ------- .../05_M6_ASSETS_OSS_EDITOR.md | 153 --------- .../06_M7_TEST_DEPLOY_CUTOVER.md | 66 ---- .../07_CROSS_CUTTING_AND_ACCEPTANCE.md | 62 ---- ..._CAPABILITY_SURFACE_BASELINE_2026-04-20.md | 183 ---------- ...D_RESPONSE_CONTRACT_BASELINE_2026-04-20.md | 262 --------------- ...RATED_STATIC_PREFIX_BASELINE_2026-04-20.md | 245 -------------- ...M0_MODULE_MIGRATION_BASELINE_2026-04-20.md | 291 ---------------- .../M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md | 106 ------ ...EPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md | 281 ---------------- .../M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md | 249 -------------- .../M0_SSE_INTERFACE_BASELINE_2026-04-20.md | 300 ----------------- backend-rewrite-tasklist/README.md | 36 -- 18 files changed, 3170 deletions(-) delete mode 100644 BACKEND_REWRITE_TASKLIST.md delete mode 100644 backend-rewrite-tasklist/00_MASTER_TASKLIST.md delete mode 100644 backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md delete mode 100644 backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md delete mode 100644 backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md delete mode 100644 backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md delete mode 100644 backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md delete mode 100644 backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md delete mode 100644 backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md delete mode 100644 backend-rewrite-tasklist/M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_MODULE_MIGRATION_BASELINE_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md delete mode 100644 backend-rewrite-tasklist/README.md diff --git a/BACKEND_REWRITE_TASKLIST.md b/BACKEND_REWRITE_TASKLIST.md deleted file mode 100644 index 1877d65e..00000000 --- a/BACKEND_REWRITE_TASKLIST.md +++ /dev/null @@ -1,12 +0,0 @@ -# 后端重写任务清单入口 - -完整总纲与拆分后的任务文件已统一整理到根目录新建目录: - -- [backend-rewrite-tasklist/README.md](./backend-rewrite-tasklist/README.md) - -其中: - -- 总纲主清单:[backend-rewrite-tasklist/00_MASTER_TASKLIST.md](./backend-rewrite-tasklist/00_MASTER_TASKLIST.md) -- 阶段拆分文件入口:[backend-rewrite-tasklist/README.md](./backend-rewrite-tasklist/README.md) - -后续如继续细化任务,请优先在该目录内维护,避免根目录散落多份版本。 diff --git a/backend-rewrite-tasklist/00_MASTER_TASKLIST.md b/backend-rewrite-tasklist/00_MASTER_TASKLIST.md deleted file mode 100644 index ab5d2f61..00000000 --- a/backend-rewrite-tasklist/00_MASTER_TASKLIST.md +++ /dev/null @@ -1,154 +0,0 @@ -# SpacetimeDB + Axum + 阿里云 OSS 后端重写任务总纲 - -日期:`2026-04-20` - -关联设计文档: - -- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) -- [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) - -关联拆分任务: - -- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md) -- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md) -- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md) -- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md) -- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md) -- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md) -- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md) - -## 0. 使用说明 - -这份总纲用于把控整体重写节奏,拆分文件用于落地执行。 - -执行原则: - -1. 第一阶段优先兼容当前 `/api/*`、`/healthz`、`/generated-*` 访问习惯。 -2. 不允许先删旧能力再补新能力,必须按能力面平移。 -3. 以当前 Node 后端 `96` 条路由、`6` 个挂载面、`12` 个模块为最低覆盖基线。 -4. 每个阶段完成后,都要形成可运行、可回归、可继续迭代的中间态。 - -## 1. 总体里程碑 - -- [x] `M0`:冻结当前后端能力清单与迁移边界 -- [ ] `M1`:搭建 Rust 工作区、Axum 主入口与基础中间件 -- [ ] `M2`:完成鉴权、会话、JWT、refresh cookie 主链迁移 -- [ ] `M3`:完成 runtime snapshot / settings / profile 迁移 -- [ ] `M4`:完成 story action 主循环与核心 gameplay reducer 迁移 -- [ ] `M5`:完成 custom world / agent 主链迁移 -- [ ] `M6`:完成 assets / OSS 主链迁移 -- [ ] `M7`:完成联调、回归、部署与切流准备 - -## 2. 阶段导航 - -### `M0 ~ M2` - -重点: - -1. 冻结能力清单 -2. 搭建 Rust workspace -3. 搭建 Axum 基础设施 -4. 迁移鉴权、会话、JWT、refresh cookie - -详见: - -- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md) - -### `M3` - -重点: - -1. 迁移 runtime snapshot -2. 迁移 settings -3. 迁移 profile dashboard / browse history / save archive - -详见: - -- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md) - -### `M4` - -重点: - -1. 迁移 RPG runtime story 主循环 -2. 迁移 RPG 入口 / session / runtime 对应的后端边界与编译职责 -3. 兼容当前 story view model 与 state 恢复接口,并与 `rpgEntry / rpgSession / rpgRuntime / rpgRuntimeStory` 口径对齐 - -详见: - -- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md) - -### `M5` - -重点: - -1. 迁移 RPG 创作主链:Agent session、result preview、published profile -2. 迁移 works / library / gallery / publish / enter-world 配套链路 -3. 旧 `custom-world/sessions` 传统问答流只按历史兼容台账处理,不再作为当前主链扩展目标 - -详见: - -- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md) - -### `M6` - -重点: - -1. 迁移 assets -2. 接入阿里云 OSS -3. 做旧静态资源路径兼容 - -详见: - -- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md) - -### `M7` - -重点: - -1. 联调 -2. 回归 -3. 部署 -4. 观测 -5. 灰度切流 -6. 收口 `spacetime-module` 主工程结构,拆分过大的 `src/lib.rs` - -详见: - -- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md) - -## 3. 横向专项 - -以下专项贯穿整个迁移期: - -1. contract 与前端兼容 -2. SpacetimeDB schema 演进治理 -3. 大对象与缓存治理 -4. 文档持续维护 - -详见: - -- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md) - -## 4. 第一优先级建议执行顺序 - -1. 先做 `M0`,冻结基线,避免迁移过程中口径漂移。 -2. 再做 `M1 + M2`,先把 Axum 壳与鉴权打稳。 -3. 当前执行顺序允许前置 `M6` 的 OSS 基础设施与直传票据能力,为后续各阶段复用统一资产入口。 -4. 再做 `M3`,优先跑通快照、设置、profile。 -5. 再做 `M4`,把 story action 主循环真正迁走。 -6. 然后做 `M5`,迁 custom world 与 agent。 -7. 最后收口 `M6` 余下资产绑定、`M7` 部署与切流。 - -## 5. 最终验收清单 - -- [ ] 当前 `96` 条后端接口已全部迁移或有兼容替代 -- [ ] 当前 `6` 个挂载面已全部迁移 -- [ ] 当前 `12` 个内部模块已完成新架构落位 -- [ ] Axum 已成为唯一 HTTP / SSE / 副作用边界 -- [ ] SpacetimeDB 已成为唯一运行时状态真相源 -- [ ] 阿里云 OSS 已成为唯一资产对象仓 -- [ ] 前端主流程在不大改 UI 的前提下可跑通 -- [ ] 能完成灰度切流,并保留可回退能力 diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md deleted file mode 100644 index 94bd29b5..00000000 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ /dev/null @@ -1,266 +0,0 @@ -# M0 ~ M2:基础设施与鉴权任务清单 - -## M0:冻结能力与重写边界 - -### 能力冻结 - -- [x] 整理当前后端 6 个挂载面并锁定为重写验收基线 - 交付物:[M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md) -- [x] 整理当前后端 96 条路由并生成一份“旧接口 -> 新实现”映射表 - 交付物:[M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md) -- [x] 整理当前 12 个内部模块并锁定迁移归属 - 交付物:[M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md) -- [x] 整理当前所有 SSE 接口与事件格式 - 交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md) -- [x] 整理当前所有 `/generated-*` 静态资源前缀 - 交付物:[M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md) -- [x] 整理当前前端直接依赖的响应头、envelope、错误格式 - 交付物:[M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md) - -### 仓库边界 - -- [x] 确认 Rust 后端新目录名与根目录落位方案 - 交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) -- [x] 确认旧 `server-node/` 在迁移期继续保留,不提前删除 - 交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) -- [x] 确认前端第一阶段仍然只访问 Axum,不直连 SpacetimeDB - 交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) -- [x] 确认外部副作用统一收口在 Axum,不放进 SpacetimeDB 模块 - 交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) - -### 交付物 - -- [x] 新增“接口映射表”文档 - 交付物:[M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md) -- [x] 新增“模块迁移清单”文档 - 交付物:[M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md) -- [x] 新增“阶段验收矩阵”文档 - 交付物:[M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md) - -## M1:Rust 工作区与 Axum 基础设施 - -### 工作区搭建 - -- [x] 在根目录新增 `server-rs/` - 交付物:[../server-rs/README.md](../server-rs/README.md) -- [x] 创建 workspace `Cargo.toml` - 交付物:[../server-rs/Cargo.toml](../server-rs/Cargo.toml) -- [x] 创建 `crates/api-server` - 交付物:[../server-rs/crates/api-server/README.md](../server-rs/crates/api-server/README.md) -- [x] 创建 `crates/spacetime-module` - 交付物:[../server-rs/crates/spacetime-module/README.md](../server-rs/crates/spacetime-module/README.md) -- [x] 创建 `crates/module-auth` - 交付物:[../server-rs/crates/module-auth/README.md](../server-rs/crates/module-auth/README.md) -- [x] 创建 `crates/module-runtime` - 交付物:[../server-rs/crates/module-runtime/README.md](../server-rs/crates/module-runtime/README.md) -- [x] 创建 `crates/module-story` - 交付物:[../server-rs/crates/module-story/README.md](../server-rs/crates/module-story/README.md) -- [x] 创建 `crates/module-combat` - 交付物:[../server-rs/crates/module-combat/README.md](../server-rs/crates/module-combat/README.md) -- [x] 创建 `crates/module-inventory` - 交付物:[../server-rs/crates/module-inventory/README.md](../server-rs/crates/module-inventory/README.md) -- [x] 创建 `crates/module-npc` - 交付物:[../server-rs/crates/module-npc/README.md](../server-rs/crates/module-npc/README.md) -- [x] 创建 `crates/module-progression` - 交付物:[../server-rs/crates/module-progression/README.md](../server-rs/crates/module-progression/README.md) -- [x] 创建 `crates/module-quest` - 交付物:[../server-rs/crates/module-quest/README.md](../server-rs/crates/module-quest/README.md) -- [x] 创建 `crates/module-runtime-item` - 交付物:[../server-rs/crates/module-runtime-item/README.md](../server-rs/crates/module-runtime-item/README.md) -- [x] 创建 `crates/module-custom-world` - 交付物:[../server-rs/crates/module-custom-world/README.md](../server-rs/crates/module-custom-world/README.md) -- [x] 创建 `crates/module-assets` - 交付物:[../server-rs/crates/module-assets/README.md](../server-rs/crates/module-assets/README.md) -- [x] 创建 `crates/module-ai` - 交付物:[../server-rs/crates/module-ai/README.md](../server-rs/crates/module-ai/README.md) -- [x] 创建 `crates/shared-contracts` - 交付物:[../server-rs/crates/shared-contracts/README.md](../server-rs/crates/shared-contracts/README.md) -- [x] 创建 `crates/shared-kernel` - 交付物:[../server-rs/crates/shared-kernel/README.md](../server-rs/crates/shared-kernel/README.md) -- [x] 创建 `crates/shared-logging` - 交付物:[../server-rs/crates/shared-logging/README.md](../server-rs/crates/shared-logging/README.md) -- [x] 创建 `crates/platform-auth` - 交付物:[../server-rs/crates/platform-auth/README.md](../server-rs/crates/platform-auth/README.md) -- [x] 创建 `crates/platform-oss` - 交付物:[../server-rs/crates/platform-oss/README.md](../server-rs/crates/platform-oss/README.md) -- [x] 创建 `crates/platform-llm` - 交付物:[../server-rs/crates/platform-llm/README.md](../server-rs/crates/platform-llm/README.md) -- [x] 创建 `crates/spacetime-client` - 交付物:[../server-rs/crates/spacetime-client/README.md](../server-rs/crates/spacetime-client/README.md) -- [x] 创建 `crates/tests-support` - 交付物:[../server-rs/crates/tests-support/README.md](../server-rs/crates/tests-support/README.md) - -### Axum 基础能力 - -- [x] 搭建 `main.rs` / `Router` / `with_state` - 交付物:[../server-rs/crates/api-server/src/main.rs](../server-rs/crates/api-server/src/main.rs) -- [x] 接入统一配置加载 - 交付物:[../server-rs/crates/api-server/src/config.rs](../server-rs/crates/api-server/src/config.rs) -- [x] 接入统一日志与 tracing - 交付物:[../docs/technical/RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md](../docs/technical/RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md)、[../server-rs/crates/shared-logging/src/lib.rs](../server-rs/crates/shared-logging/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/api-server/src/main.rs](../server-rs/crates/api-server/src/main.rs) -- [x] 接入 `request_id` 中间件 - 交付物:[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 接入统一错误处理中间件 - 交付物:[../server-rs/crates/api-server/src/http_error.rs](../server-rs/crates/api-server/src/http_error.rs)、[../server-rs/crates/api-server/src/error_middleware.rs](../server-rs/crates/api-server/src/error_middleware.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 接入当前项目兼容的 response envelope - 交付物:[../server-rs/crates/api-server/src/api_response.rs](../server-rs/crates/api-server/src/api_response.rs)、[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs)、[../server-rs/crates/api-server/src/http_error.rs](../server-rs/crates/api-server/src/http_error.rs) -- [x] 接入 `x-request-id` - 交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 接入 `x-api-version` - 交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs) -- [x] 接入 `x-route-version` - 交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs) -- [x] 接入 `x-response-time-ms` - 交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)、[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs) -- [x] 实现 `/healthz` - 交付物:[../server-rs/crates/api-server/src/health.rs](../server-rs/crates/api-server/src/health.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) - -### 基础工程脚本 - -- [x] 新增本地开发脚本 - 交付物:[../server-rs/scripts/dev.ps1](../server-rs/scripts/dev.ps1)、[../server-rs/scripts/dev.sh](../server-rs/scripts/dev.sh) -- [x] 新增测试脚本 - 交付物:[../server-rs/scripts/test.ps1](../server-rs/scripts/test.ps1)、[../server-rs/scripts/test.sh](../server-rs/scripts/test.sh) -- [x] 新增 lint / fmt / clippy / check 脚本 - 交付物:[../server-rs/scripts/check.ps1](../server-rs/scripts/check.ps1)、[../server-rs/scripts/check.sh](../server-rs/scripts/check.sh) -- [x] 新增 smoke 脚本 - 交付物:[../server-rs/scripts/smoke.ps1](../server-rs/scripts/smoke.ps1)、[../server-rs/scripts/smoke.sh](../server-rs/scripts/smoke.sh) -- [x] 新增 SpacetimeDB 本地开发脚本 - 交付物:[../server-rs/scripts/spacetime-dev.ps1](../server-rs/scripts/spacetime-dev.ps1)、[../server-rs/scripts/spacetime-dev.sh](../server-rs/scripts/spacetime-dev.sh) - -### 阶段验收 - -- [x] Axum 服务可独立启动 - 证据:`./server-rs/scripts/smoke.ps1` 已通过,覆盖临时启动 `api-server`、等待 `/healthz` 就绪并验证 raw / envelope 协议。 -- [x] `/healthz` 返回与当前工程兼容 -- [x] 基础 response envelope 与 request id 行为稳定 - 证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 envelope 协商与 `/healthz` 头部回写。 -- [x] Rust workspace 能完整编译通过 - 证据:`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 已通过。 - -## M2:鉴权、会话、JWT 与 refresh cookie - -### SpacetimeDB 身份表 - -- [x] 设计 `user_account` - 交付物:[../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `auth_identity` - 交付物:[../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `refresh_session` - 交付物:[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `auth_audit_log` - 交付物:[../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `auth_risk_block` - 交付物:[../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `sms_auth_event` - 交付物:[../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) -- [x] 设计 `wechat_auth_state` - 交付物:[../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md) - -### Axum 鉴权服务 - -- [x] 实现密码登录 - 交付物:[../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md](../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现账号自动创建 / 幂等登录兼容策略 - 交付物:[../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md](../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现 Bearer JWT 校验 - 交付物:[../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现 refresh cookie 读取 - 交付物:[../docs/technical/PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/config.rs](../server-rs/crates/api-server/src/config.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现 refresh token 轮换 - 交付物:[../docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md](../docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现多端会话身份建模与会话列表查询 - 交付物:[../docs/technical/MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md](../docs/technical/MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md)、[../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)、[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/session_client.rs](../server-rs/crates/api-server/src/session_client.rs)、[../server-rs/crates/api-server/src/auth_sessions.rs](../server-rs/crates/api-server/src/auth_sessions.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../packages/shared/src/contracts/auth.ts](../packages/shared/src/contracts/auth.ts) -- [x] 实现会话吊销 - 交付物:[../docs/technical/AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/logout.rs](../server-rs/crates/api-server/src/logout.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现全端登出 - 交付物:[../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/logout_all.rs](../server-rs/crates/api-server/src/logout_all.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现 `me` 查询 - 交付物:[../docs/technical/AUTH_ME_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_ME_QUERY_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth_me.rs](../server-rs/crates/api-server/src/auth_me.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) - -### 手机验证码登录 - -- [ ] 接入阿里云短信发送 adapter -- [x] 实现发送验证码接口 - 交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现验证码校验接口 - 交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现手机号绑定 - 交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs) -- [ ] 实现手机号换绑 -- [x] 实现发送频率限制 - 交付物:[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现验证码失败次数限制 - 交付物:[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [ ] 实现 captcha 触发逻辑 -- [ ] 实现风控封禁与解除 - -### 微信登录 - -- [x] 接入微信 OAuth adapter - 交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_provider.rs](../server-rs/crates/api-server/src/wechat_provider.rs)、[../server-rs/crates/api-server/src/state.rs](../server-rs/crates/api-server/src/state.rs) -- [x] 实现 `wechat/start` - 交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现 `wechat/callback` - 交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现微信身份绑定 - 交付物:[../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs) -- [x] 实现微信账号补绑手机号 - 交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 实现桌面端 / 微信内打开场景区分 - 交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/session_client.rs](../server-rs/crates/api-server/src/session_client.rs) - -### OIDC 与 SpacetimeDB 身份透传 - -- [x] 设计 JWT claims - 交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md) -- [x] 确认 `iss/sub/sid/provider/roles` 字段 - 交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md) -- [x] 让 Axum 自身可校验 JWT - 交付物:[../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/README.md](../server-rs/crates/platform-auth/README.md)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs) -- [ ] 让 SpacetimeDB 可识别 Axum 签发的身份令牌 -- [ ] 验证 reducer / view 可读取用户身份上下文 - -### 当前接口兼容 - -- [x] 兼容 `/api/auth/login-options` - 交付物:[../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/login_options.rs](../server-rs/crates/api-server/src/login_options.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 兼容 `/api/auth/entry` - 交付物:[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 兼容 `/api/auth/me` - 交付物:[../server-rs/crates/api-server/src/auth_me.rs](../server-rs/crates/api-server/src/auth_me.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 兼容 `/api/auth/logout` - 交付物:[../server-rs/crates/api-server/src/logout.rs](../server-rs/crates/api-server/src/logout.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 兼容 `/api/auth/logout-all` - 交付物:[../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/logout_all.rs](../server-rs/crates/api-server/src/logout_all.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs) -- [x] 兼容 `/api/auth/refresh` - 交付物:[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs) -- [x] 兼容 `/api/auth/sessions` - 交付物:[../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/auth_sessions.rs](../server-rs/crates/api-server/src/auth_sessions.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs) -- [ ] 兼容 `/api/auth/sessions/:sessionId/revoke` -- [ ] 兼容 `/api/auth/audit-logs` -- [ ] 兼容 `/api/auth/risk-blocks` -- [ ] 兼容 `/api/auth/risk-blocks/:scopeType/lift` -- [x] 兼容 `/api/auth/phone/send-code` - 交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs) -- [x] 兼容 `/api/auth/phone/login` - 交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs) -- [ ] 兼容 `/api/auth/phone/change` -- [x] 兼容 `/api/auth/wechat/start` - 交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts) -- [x] 兼容 `/api/auth/wechat/callback` - 交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts) -- [x] 兼容 `/api/auth/wechat/bind-phone` - 交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts) - -### 阶段验收 - -- [x] 密码登录主链可用 - 证据:`cargo test -p module-auth --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖自动建号、重复登录复用、错密码 `401`、非法用户名 `400` 与 refresh cookie 写回。 -- [x] refresh cookie 主链可用 - 证据:`cargo test -p module-auth --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 refresh 成功轮换、旧 token 失效、缺少 cookie `401` 与失败时清理 cookie。 -- [x] 手机验证码主链可用 - 证据:`cargo test -p module-auth phone --manifest-path server-rs/Cargo.toml -- --nocapture`、`cargo test -p api-server phone --manifest-path server-rs/Cargo.toml -- --nocapture` 已通过,覆盖发送验证码、同场景冷却 `429`、验证码错误次数耗尽 `429`、重新发送后恢复登录,以及手机号登录建号/复用与 refresh cookie 写回。 -- [x] 微信登录主链可用 - 证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat --manifest-path server-rs/Cargo.toml`、`cargo test -p module-auth --manifest-path server-rs/Cargo.toml` 已通过,覆盖 `wechat/start`、`wechat/callback`、待绑定会话签发、手机号补绑并入已有账号,以及 `unionid` 命中后新 `openid` 映射回写。 -- [ ] 所有旧鉴权接口可通过 contract 回归 diff --git a/backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md b/backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md deleted file mode 100644 index 7c15286a..00000000 --- a/backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md +++ /dev/null @@ -1,69 +0,0 @@ -# M3:runtime snapshot / settings / profile 任务清单 - -## 1. SpacetimeDB 运行时主表 - -- [x] 设计 `runtime_snapshot` -- [x] 设计 `runtime_setting` -- [x] 设计 `profile_dashboard_state` -- [x] 设计 `profile_wallet_ledger` -- [x] 设计 `profile_played_world` -- [x] 设计 `profile_save_archive` -- [x] 设计 `user_browse_history` - -## 2. 兼容快照策略 - -- [x] 设计“领域表真相 + 兼容聚合快照”策略 -- [x] 设计 snapshot projection 刷新机制 -- [x] 迁移当前 snapshot hydration / normalize 规则 -- [x] 迁移当前 save archive 聚合逻辑 -- [x] 迁移当前 browse history 去重与排序逻辑 - -## 3. Axum facade - -- [x] 兼容 `GET /api/runtime/save/snapshot` -- [x] 兼容 `PUT /api/runtime/save/snapshot` -- [x] 兼容 `DELETE /api/runtime/save/snapshot` -- [x] 兼容 `GET /api/runtime/settings` -- [x] 兼容 `PUT /api/runtime/settings` -- [x] 兼容 `GET /api/runtime/profile/dashboard` -- [x] 兼容 `GET /api/profile/dashboard` -- [x] 兼容 `GET /api/runtime/profile/wallet-ledger` -- [x] 兼容 `GET /api/profile/wallet-ledger` -- [x] 兼容 `GET /api/runtime/profile/play-stats` -- [x] 兼容 `GET /api/profile/play-stats` -- [x] 兼容 `GET /api/runtime/profile/save-archives` -- [x] 兼容 `GET /api/profile/save-archives` -- [x] 兼容 `POST /api/runtime/profile/save-archives/:worldKey` -- [x] 兼容 `POST /api/profile/save-archives/:worldKey` -- [x] 兼容 `GET /api/runtime/profile/browse-history` -- [x] 兼容 `POST /api/runtime/profile/browse-history` -- [x] 兼容 `DELETE /api/runtime/profile/browse-history` -- [x] 兼容 `GET /api/profile/browse-history` -- [x] 兼容 `POST /api/profile/browse-history` -- [x] 兼容 `DELETE /api/profile/browse-history` - -## 4. 阶段验收 - -- [ ] 登录用户可正常保存、读取、删除存档 -- [x] 兼容路径与主路径返回一致 -- [x] profile dashboard / browse history / save archive 行为一致 -- [ ] 前端当前恢复流程可在不改 UI 的前提下跑通 - -## 5. 本轮进展记录 - -- `2026-04-21`:已完成 `runtime_setting` 首版设计与 `GET/PUT /api/runtime/settings` 的 Rust 主链迁移。 -- 本轮已落地 `module-runtime`、`spacetime-module`、`spacetime-client`、`api-server` 四层串联,并补齐定向测试。 -- 详细设计与字段冻结见: - - [../docs/technical/M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](../docs/technical/M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md) -- `2026-04-22`:已完成 `user_browse_history` 表设计冻结、去重与排序规则迁移,以及 `/api/runtime/profile/browse-history` 与 `/api/profile/browse-history` 双路径 facade 落地。 -- `2026-04-22`:已补 `browse history` 的 API 入口必填字段校验、批量 shape 兼容与定向测试,详细设计见: - - [../docs/technical/M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](../docs/technical/M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md) -- `2026-04-22`:已冻结 `profile_dashboard_state`、`profile_wallet_ledger`、`profile_played_world` 三张 projection 表,以及 `dashboard / wallet-ledger / play-stats` 的 Axum + SpacetimeDB 读链设计。 -- `2026-04-22`:已完成 `api-server` 的 `runtime_profile` facade 编译与定向测试收口,`/api/runtime/profile/*` 与 `/api/profile/*` 六条只读路由均已接通。 -- `2026-04-22`:已通过 `cargo check -p api-server --tests --message-format short`、`cargo test -p shared-contracts --lib`、`cargo test -p api-server runtime_profile::tests:: -- --nocapture` 验证本轮 profile projection 读链。 -- 详细设计见: - - [../docs/technical/M3_PROFILE_DASHBOARD_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md](../docs/technical/M3_PROFILE_DASHBOARD_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md) -- `2026-04-22`:已完成 `runtime_snapshot`、`profile_save_archive` 与“领域表真相 + 兼容聚合快照”方案落地,接通 `/api/runtime/save/snapshot`、`/api/runtime/profile/save-archives`、`/api/profile/save-archives` 与恢复存档双路径 facade。 -- `2026-04-22`:已通过 `cargo test -p shared-kernel --lib`、`cargo test -p module-runtime --lib`、`cargo check -p spacetime-module --message-format short`、`cargo build -p spacetime-module --target wasm32-unknown-unknown --release --message-format short`、`cargo check -p spacetime-client --message-format short`、`cargo check -p api-server --tests --message-format short`、`cargo test -p api-server runtime_save::tests:: -- --nocapture` 验证 snapshot/save archive 主链编译与 facade。 -- 详细设计见: - - [../docs/technical/M3_RUNTIME_SNAPSHOT_SAVE_ARCHIVE_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md](../docs/technical/M3_RUNTIME_SNAPSHOT_SAVE_ARCHIVE_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md) diff --git a/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md b/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md deleted file mode 100644 index 82a7e697..00000000 --- a/backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md +++ /dev/null @@ -1,318 +0,0 @@ -# M4:story action 与 gameplay reducer 任务清单 - -## 0. 当前执行基线 - -本阶段与当前仓库里的 RPG 入口与运行时主链重构直接对应,统一以以下文档为准: - -1. [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) -2. [../docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](../docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md) -3. [../docs/technical/M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md](../docs/technical/M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md) - -当前任务清单只维护 Axum / SpacetimeDB 重写侧的后端迁移项,不再把旧 `GameShell / runtimeRoutes.ts / storyActionService.ts` 命名视为新架构目标。 - -### 当前进展(`2026-04-22`) - -本阶段首轮已先把 `server-rs` 从“只有 `module-story` 占位目录”推进到“SpacetimeDB 侧 story 会话基座真实可编译”: - -1. 已新增 `server-rs/crates/module-story` 真实 crate。 -2. 已冻结 `story_session / story_event` 的首版领域类型、状态枚举和字段校验 helper。 -3. 已在 `server-rs/crates/spacetime-module` 中新增 `story_session`、`story_event` 两张表。 -4. 已新增 `begin_story_session`、`continue_story` 两个 reducer,形成最小会话事件链。 -5. 已新增 `begin_story_session_and_return`、`continue_story_and_return` 两个 procedure,形成可同步返回快照的最小 story session contract。 -6. 已重新执行 `spacetime generate`,把 `story_session / story_event` Rust bindings 刷入 `spacetime-client/src/module_bindings`。 -7. 已在 `server-rs/crates/spacetime-client` 中新增 `begin_story_session(...)`、`continue_story(...)` facade。 -8. 已在 `server-rs/crates/api-server` 中新增: - - `POST /api/story/sessions` - - `POST /api/story/sessions/continue` -9. 已执行 `cargo check -p module-story -p spacetime-module -p spacetime-client -p api-server` 并通过。 -6. 已新增 `docs/technical/M4_MODULE_COMBAT_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `battle_state` 与 `resolve_combat_action` 的首版字段与规则口径。 -7. 已新增 `server-rs/crates/module-runtime-item` 真实 crate。 -8. 已冻结 runtime item 侧奖励快照与物品写回基线,为后续奖励链并入 inventory / quest / combat 提供统一底层能力。 -9. 已在 `server-rs/crates/spacetime-module` 中补齐 runtime item / inventory / quest / combat 所需的奖励落表与回写依赖。 -10. 当前 M4 runtime story compat bridge 已明确移除旧 `treasure_*` 遭遇动作概念,不再把宝箱遭遇视作本阶段 runtime story 主链目标。 -11. 已新增 `docs/technical/M4_RPG_RUNTIME_INVENTORY_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `inventory_slot` 与 `apply_inventory_mutation` 的首版字段与规则口径。 -12. 已新增 `server-rs/crates/module-inventory` 真实 crate。 -13. 已在 `server-rs/crates/spacetime-module` 中新增 `inventory_slot` 表。 -14. 已新增 `apply_inventory_mutation` reducer,形成最小背包主链。 -15. 已新增 `docs/technical/M4_MODULE_NPC_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `npc_state`、`resolve_npc_social_action` 与 `resolve_npc_interaction` 的首版字段与交互口径。 -16. 已新增 `server-rs/crates/module-npc` 真实 crate。 -17. 已在 `server-rs/crates/spacetime-module` 中新增 `npc_state` 表。 -18. 已新增 `upsert_npc_state`、`resolve_npc_social_action`、`resolve_npc_interaction` 及对应 procedure。 -19. 已新增 `docs/technical/M4_MODULE_NPC_COMBAT_ORCHESTRATION_BASELINE_2026-04-21.md`,冻结 `npc_fight / npc_spar` 到 `battle_state` 的最小联合编排口径。 -20. 已在 `server-rs/crates/spacetime-module` 中新增 `resolve_npc_battle_interaction_and_return` procedure,把 NPC 开战交互与 battle 初始化写入串到同一事务。 -15. 已新增 `docs/technical/M4_MODULE_PROGRESSION_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `player_progression / chapter_progression` 的首版字段、成长曲线与章节预算口径。 -16. 已新增 `server-rs/crates/module-progression` 真实 crate。 -17. 已在 `server-rs/crates/spacetime-module` 中新增 `player_progression`、`chapter_progression` 两张表。 -18. 已新增 `get_player_progression_or_default`、`grant_player_progression_experience`、`upsert_chapter_progression`、`apply_chapter_progression_ledger_entry` 及对应 procedure。 -19. 已新增 `docs/technical/M4_RPG_RUNTIME_QUEST_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `quest_record / quest_log / apply_quest_signal` 的首版字段、日志口径与交付状态流转规则。 -20. 已新增 `server-rs/crates/module-quest` 真实 crate。 -21. 已在 `server-rs/crates/spacetime-module` 中新增 `quest_record`、`quest_log` 两张表。 -22. 已新增 `accept_quest`、`apply_quest_signal`、`acknowledge_quest_completion`、`turn_in_quest` reducer,形成最小任务闭环。 -23. 已执行 `cargo test -p module-quest`、`cargo check -p spacetime-module`、`cargo check -p api-server` 与全量 `cargo check` 并通过。 -24. 已新增 `docs/technical/M4_PROGRESSION_QUEST_COMBAT_INTEGRATION_2026-04-21.md`,冻结任务交付与战斗胜利到成长系统的联动口径。 -25. 已把 `turn_in_quest` 接到 `player_progression / chapter_progression` 的最小经验写入。 -26. 已把 `resolve_combat_action(Victory)` 接到 `player_progression / chapter_progression` 的最小经验写入。 -27. 已把 `turn_in_quest.reward.items` 接到 `inventory_slot` 发物链,形成任务交付的最小物品奖励闭环。 -28. 已新增 `docs/technical/M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md`,冻结最小 `story state` 查询切片,只开放 `storySession + storyEvents` 真相态查询。 -29. 已在 `server-rs/crates/api-server` 中挂出 `GET /api/story/sessions/:storySessionId/state`,通过 `spacetime-client.get_story_session_state(...)` 读取 `SpacetimeDB procedure` 返回的会话快照与事件流。 -30. 已新增 `docs/technical/M4_COMBAT_REWARD_INVENTORY_INTEGRATION_2026-04-22.md`,冻结 `battle_state.reward_items` 与 `resolve_combat_action(Victory)` 发物到 `inventory_slot` 的最小联动口径。 -31. 已新增 `docs/technical/M4_MODULE_COMBAT_STATE_QUERY_DESIGN_2026-04-22.md`,冻结最小 `battle state` 查询切片,只开放单个 `battleState` 真相态查询。 -32. 已在 `server-rs/crates/spacetime-module` 中新增 `get_battle_state` procedure,按 `battle_state_id` 返回当前战斗快照。 -33. 已在 `server-rs/crates/spacetime-client` 中新增 `get_battle_state(...)` facade,供 Axum 同步读取 battle 真相态。 -34. 已在 `server-rs/crates/api-server` 中挂出 `GET /api/story/battles/:battleStateId`,通过 `spacetime-client.get_battle_state(...)` 返回单战斗快照。 -35. 已在 `server-rs/crates/spacetime-client` 中新增 `resolve_npc_battle_interaction(...)` facade,把 `resolve_npc_battle_interaction_and_return` procedure 映射为稳定 Rust record,供 Axum 直接消费。 -36. 已在 `server-rs/crates/api-server` 中挂出 `POST /api/story/npc/battle`,当前只接受 `npc_fight / npc_spar`,同步返回 `npcInteraction + battleState`。 -37. 已执行 `cargo check -p spacetime-client -p api-server` 并通过,完成 `module-npc -> spacetime-client -> api-server` 的最小 NPC 开战同步返回链闭环。 -38. 已重新执行 `spacetime generate --no-config --lang rust --out-dir D:\\Genarrative\\server-rs\\crates\\spacetime-client\\src\\module_bindings --module-path D:\\Genarrative\\server-rs\\crates\\spacetime-module --include-private --yes`,把 `get_battle_state`、`battle_state.reward_items` 与 `custom_world_agent_session` 相关 bindings 刷入 `spacetime-client/src/module_bindings`。 -39. 已把 `server-rs/crates/spacetime-client/src/lib.rs` 中原本占位返回错误的 `get_battle_state(...)` 改成真实 procedure 调用,当前 battle query 已不再停留在 facade stub。 -40. 已再次执行 `cargo check -p spacetime-client --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` 与 `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`,当前 battle/story 新链路在编译层已恢复通过。 -41. 已新增 `docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md`,冻结旧 `POST /api/runtime/story/state/resolve` 的首版兼容桥边界,明确当前先做 DTO 与状态桥,不提前误宣称 `actions/resolve` 已可迁移。 -42. 已在 `server-rs/crates/shared-contracts` 中新增 `runtime_story` 模块,冻结 `RuntimeStoryStateResolveRequest`、`RuntimeStoryActionResponse` 以及 `viewModel / presentation / patches / snapshot` 的首版 camelCase DTO,与当前前端消费口径对齐。 -43. 已恢复并重建 `server-rs/crates/api-server/src/runtime_story.rs`,把上一轮误删留下的中间态收口回可编译实现。 -44. 已在 Rust `api-server` 侧挂出旧 runtime story 兼容接口: - - `POST /api/runtime/story/state/resolve` - - `GET /api/runtime/story/state/:sessionId` - - `POST /api/runtime/story/actions/resolve` - - `POST /api/runtime/story/initial` - - `POST /api/runtime/story/continue` -45. `state/resolve` 与 `actions/resolve` 已统一复用 `runtime_save` 的 SpacetimeDB 快照持久化链: - - 请求带 `snapshot` 时先写入 `runtime_snapshot` - - 请求不带 `snapshot` 时从持久化 `runtime_snapshot` 读取 - - 无可用快照时返回 `409` -46. `actions/resolve` 已补齐当前前端主链需要的确定性兼容动作闭环,覆盖: - - `story_continue_adventure` - - `story_opening_camp_dialogue` - - `camp_travel_home_scene` - - `idle_call_out` - - `idle_explore_forward` - - `idle_observe_signs` - - `idle_rest_focus` - - `idle_travel_next_scene` - - `npc_preview_talk` - - `npc_chat` - - `npc_help` - - `npc_leave` - - `npc_fight` - - `npc_spar` - - `npc_recruit` - - `battle_attack_basic` - - `battle_use_skill` - - `battle_all_in_crush` - - `battle_escape_breakout` - - `battle_feint_step` - - `battle_finisher_window` - - `battle_guard_break` - - `battle_probe_pressure` - - `battle_recover_breath` - - `inventory_use` - - `equipment_equip` - - `npc_trade` - - `npc_gift` -47. `actions/resolve` 已补 `clientVersion` 与 `gameState.runtimeActionVersion` 的冲突校验、动作后版本自增、`storyHistory` 追加和 snapshot 回写。 -48. `initial` / `continue` 已先落稳定 `RuntimeStoryAiResponse`: - - 优先透传 `requestOptions.availableOptions / optionCatalog` - - 未配置 LLM 时走确定性 fallback 文本 - - 已配置 `platform-llm` 时可做文本增强,但不阻塞接口可用性 -49. `actions/resolve` 已开始迁移 Node 动作后 LLM 增强分支的最小闭环: - - `npc_chat / story_opening_camp_dialogue` 在配置 `platform-llm` 时会尝试生成对话态 `storyText` - - NPC 对话增强回包会对齐 Node 旧 `displayMode = dialogue + deferredOptions` 结构,先只展示“继续推进冒险” - - `battle victory / spar_complete / escaped` 在配置 `platform-llm` 时会尝试生成结果叙事,但不改既有规则结算 - - LLM 不可用或生成失败时自动回退到确定性 `resultText / currentStory` -50. 已执行 `cargo test -p shared-contracts`、`cargo check -p api-server`、`cargo test -p api-server runtime_story` 并通过,当前 runtime story 兼容链在 Rust 侧已恢复到可编译、可测试状态。 -51. 已补 Rust 侧 route boundary 回归: - - `runtime_story_routes_resolve_through_rust_route_boundary` - - `runtime_story_action_resolve_rejects_client_version_conflict` - - `runtime_story_npc_help_is_one_shot_and_restores_resources` - - `runtime_story_npc_recruit_requires_threshold_and_release_target_when_party_full` -52. 已把兼容桥里的关键 NPC 行为继续对齐到 Node 旧主链: - - `npc_chat` 好感增长改为 `max(2, 6 - chattedCount)`,首聊可从 `46 -> 52` - - `npc_help` 改为一次性援手,成功时恢复 `10 HP / 8 Mana` 且关系 `+4` - - `npc_recruit` 改为要求 `affinity >= 60`,队伍满员时必须透传 `releaseNpcId` -53. 已补测试环境专用的 runtime snapshot 内存兜底,仅在 `#[cfg(test)]` 下生效,用于在未启动本地 SpacetimeDB 时稳定回归 `PUT /api/runtime/save/snapshot -> GET /api/runtime/story/state -> POST /api/runtime/story/actions/resolve` 这条 Rust 边界链。 -54. 已把 quest compat 主循环补到 Rust `runtime story` 兼容桥: - - `npc_chat_quest_offer_view` - - `npc_chat_quest_offer_replace` - - `npc_chat_quest_offer_abandon` - - `npc_quest_accept` - - `npc_quest_turn_in` -55. 已把 quest offer 对话态的 `currentStory.npcChatState.pendingQuestOffer` 与前端面板依赖的 `runtimePayload.npcChatQuestOfferAction` 一并回填到 Rust compat 回包,保证现有 quest 面板入口不回退。 -56. 已把 `npc_quest_turn_in` 的最小奖励闭环补回 Rust compat handler: - - quest 状态改为保留在 `gameState.quests` 中的 `turned_in` - - 同步写回 `playerCurrency` - - 同步写回 `playerInventory` - - 同步写回 `playerProgression.totalXp / level / xpToNextLevel / lastGrantedSource` - - 同步写回 NPC `affinity` -57. 已新增 quest compat Rust 回归: - - `runtime_story_quest_offer_replace_updates_pending_offer_and_payload` - - `runtime_story_quest_offer_abandon_clears_pending_offer_and_restores_chat_options` - - `runtime_story_quest_accept_writes_quest_runtime_stats_and_followup_story` - - `runtime_story_quest_turn_in_marks_quest_rewards_and_affinity` -58. 已再次执行 `cargo test -p api-server runtime_story`、`cargo check -p api-server` 与 `node scripts/check-encoding.mjs` 并通过,当前 quest compat 已恢复到可编译、可回归状态。 -59. 已继续把 Task6 旧 inventory / NPC inventory compat 主链补回 Rust `runtime story` 兼容桥: - - `equipment_equip` - - `equipment_unequip` - - `forge_craft` - - `forge_dismantle` - - `forge_reforge` - - `npc_trade` - - `npc_gift` -60. 已把 NPC 交互态 fallback option compiler 对齐到 Node 旧顺序,当前会按条件输出: - - `npc_chat` - - `npc_help` - - `npc_spar` - - `npc_fight` - - `npc_trade` - - `npc_gift` - - `npc_quest_accept / npc_quest_turn_in` - - `npc_recruit` - - `npc_leave` -61. 已新增 Rust compat 回归: - - `runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_help_lock` - - `runtime_story_equipment_equip_updates_loadout_and_build_toast` - - `runtime_story_equipment_unequip_returns_item_to_inventory_and_resets_loadout` - - `runtime_story_forge_craft_consumes_materials_and_currency` - - `runtime_story_forge_dismantle_replaces_item_with_material_outputs` - - `runtime_story_forge_reforge_upgrades_item_and_consumes_cost` - - `runtime_story_npc_trade_buy_updates_currency_inventory_and_stock` - - `runtime_story_state_compiler_bootstraps_trade_inventory_for_role_npc` - - `runtime_story_npc_trade_buy_bootstraps_missing_npc_state` - - `runtime_story_npc_gift_updates_affinity_inventory_and_patch` - - `runtime_story_route_boundary_persists_equipment_equip_snapshot_updates` -62. 当前 Rust compat bridge 已补入口级 NPC 状态预处理:即使快照里的 `npcStates` 为空,纯商贩型 NPC 也会在 `state/get` 与 `actions/resolve` 前自动初始化基础关系态、`stanceProfile / relationState / tradeStockSignature` 与最小 trade stock。 -63. 当前 `actions/resolve` 已不再只停留在确定性 `storyText = resultText`: - - 已在 Rust 侧新增 `generate_action_story_payload(...)` - - 已对齐 Node 旧分支的最小范围 `npc_chat / story_opening_camp_dialogue / terminal combat outcome` - - 当前仍未迁移 Node 那套完整 orchestrator 选项重排,只先保留既有 fallback options -64. 当前 `cargo test -p api-server runtime_story` 已提升到 30 条回归通过。 -65. 已继续把 runtime story compat 的 battle 展示编译从 `api-server` 抽到独立 crate: - - `module-runtime-story-compat` 当前已承接 `build_battle_runtime_story_options(...)`、`restore_player_resource(...)` 与战斗技能 / 推荐物品 option compiler - - `api-server/src/runtime_story/compat/battle.rs` 已删除 - - `presentation.rs` 与 `npc_actions.rs` 当前统一直接复用 crate 导出的 battle helper -66. 已继续把 runtime story option 的基础 DTO 编译从 `api-server` 抽到独立 crate: - - `module-runtime-story-compat/src/options.rs` 当前已承接 `build_static_runtime_story_option(...)`、`build_disabled_runtime_story_option(...)`、`build_runtime_story_option_from_story_option(...)`、`build_story_option_from_runtime_option(...)` - - `api-server/src/runtime_story/compat/presentation.rs` 已删除这批本地重复实现,当前只保留更贴近 NPC / quest / view-model 组装的逻辑 -67. 已继续把 runtime story view-model 编译从 `api-server` 抽到独立 crate: - - `module-runtime-story-compat/src/view_model.rs` 当前已承接 `build_runtime_story_view_model(...)`、`build_runtime_story_encounter(...)`、`build_runtime_story_companions(...)` - - `resolve_current_encounter_npc_state(...)` 已统一由 crate 导出,`api-server` 的 `presentation.rs` 与 `game_state.rs` 不再保留本地副本 -68. 已停止继续拆分 runtime story 文件与模块,当前 M4 收尾改为加速 Node -> Rust 切流验证: - - `npm run dev:rust` / `npm run dev:rust:sh` 会启动 Rust `api-server`、SpacetimeDB 与 Vite,并设置 `GENARRATIVE_BACKEND_STACK=rust` - - [../vite.config.ts](../vite.config.ts) 已补 `/api/story` 代理,Rust 栈下 `/api/runtime/*` 与 `/api/story/*` 均会走 `GENARRATIVE_RUNTIME_SERVER_TARGET` - - 当前 M4 的切流目标以“旧 runtime story 兼容接口 + 新 story/battle 查询切片可由 Rust 承接”为准,不再把继续拆 crate 作为本阶段阻塞项 - -当前验证边界补充: - -1. `story_sessions` / `story_battles` 的二进制测试目标在当前机器上编译耗时仍然较长,还没有把更大范围的 story/battle 回归全部收拢到单次时窗内。 -2. `node scripts/check-encoding.mjs` 已再次执行并通过,当前本轮涉及的中文文件编码未被写坏。 -3. 当前可以确认的是: - - `module -> generated bindings -> spacetime-client -> api-server` 的编译链已打通 - - Rust `runtime story` compat route boundary 与关键 NPC 主循环规则已有回归覆盖 - - Rust `actions/resolve` 已开始承接 Node 动作后 LLM 文本增强,但完整 orchestrator / 真相链仍未完成 - -当前这轮不再继续扩 `runtime_story` 模块拆分。`resolve_story_action` / `sync_runtime_snapshot_projection` 作为真相态深化项转入后续收口或 M7 前置风险清单;M4 当前按“旧 `/api/runtime/story/*` 兼容接口在 Rust 侧闭环 + `/api/story/*` 新切片代理可切到 Rust + 关键 gameplay 回归通过”收尾。 - -## 1. SpacetimeDB gameplay 表 - -- [x] 设计 `story_session` -- [x] 设计 `story_event` -- [x] 设计 `npc_state` -- [x] 设计 `quest_record` -- [x] 设计 `inventory_slot` -- [x] 设计 runtime item 奖励快照基线 -- [x] 设计 `battle_state` -- [x] 设计 `player_progression` -- [x] 设计 `chapter_progression` - -## 2. 核心 reducer - -- [ ] 设计 `resolve_story_action`(转入真相态深化,不阻塞 M4 兼容切流收尾) -- [x] 设计 `continue_story` -- [x] 设计 `begin_story_session` -- [ ] 设计 `sync_runtime_snapshot_projection`(转入真相态深化,不阻塞 M4 兼容切流收尾) -- [x] 设计 `apply_quest_signal` -- [x] 设计 `apply_inventory_mutation` -- [x] 设计 `resolve_npc_interaction` -- [x] 设计 runtime item 奖励回写基线 -- [x] 设计 `resolve_combat_action` -- [x] 设计 `update_progression_state` - -## 3. 当前主链模块落位 - -- [ ] 迁移 `rpg-entry` 配套后端入口能力 -- [ ] 迁移 `rpg-profile` 资料域 -- [x] 迁移 `rpg-runtime-story` -- [x] 迁移 `combat` -- [ ] 迁移 `inventory` -- [ ] 迁移 `npc` -- [x] 迁移 `progression` -- [x] 迁移 `quest` -- [x] 迁移 `runtime-item` -- [x] 迁移 runtime snapshot 归一化、view model compiler 与状态同步规则 - -## 4. 兼容接口 - -- [x] 兼容 `POST /api/runtime/story/actions/resolve` -- [x] 兼容 `GET /api/runtime/story/state/:sessionId` -- [x] 兼容 `POST /api/runtime/story/state/resolve` -- [x] 兼容 `POST /api/runtime/story/initial` -- [x] 兼容 `POST /api/runtime/story/continue` - -补充说明: - -1. 当前已落地的是两类 Rust facade: - - 新真相态接口: - - `POST /api/story/sessions` - - `POST /api/story/sessions/continue` - - `GET /api/story/sessions/:storySessionId/state` - - `GET /api/story/battles/:battleStateId` - - `POST /api/story/npc/battle` - - 旧 runtime story 兼容接口: - - `POST /api/runtime/story/state/resolve` - - `GET /api/runtime/story/state/:sessionId` - - `POST /api/runtime/story/actions/resolve` - - `POST /api/runtime/story/initial` - - `POST /api/runtime/story/continue` -2. 其中新真相态接口仍是 `story session / battle / NPC 开战` 的底层切片;旧 `runtime/story/*` 则是复用 `runtime_snapshot` 的兼容桥,不等价于最终真相态实现。 -3. 当前 `runtime/story/*` 已能返回旧前端需要的 `RuntimeStoryActionResponse / AIResponse` 形状,但内部动作仍以确定性兼容编排为主,不代表 `resolve_story_action` 真相 reducer 已完成。 -4. 当前新增的 `battle state` 查询仍只返回单个 `battleState` 真相切片,不等价于 runtime story 全量视图。 -5. 后续 `M4` 仍需把兼容桥逐步替换成真正的 story action / snapshot projection 真相链。 - -## 5. ViewModel 兼容 - -- [x] 兼容当前 `RuntimeStoryActionResponse` -- [x] 兼容当前 `RuntimeStoryOptionView` -- [x] 兼容当前 `interaction` 元数据 -- [x] 兼容当前 battle / toast / patch 响应结构 -- [x] 兼容当前 `currentStory` 回填逻辑 - -## 6. 阶段验收 - -- [x] 当前前端 story 选项点击后可走新后端闭环 -- [x] NPC / quest / combat 主循环行为不回退 -- [x] `story state` 恢复链可用 -- [x] 后端边界与当前 `rpgEntry -> rpgSession -> rpgRuntime -> rpgRuntimeStory -> rpgProfile` 口径一致 -- [x] 旧 Node 版 story route 回归用例完成平移 - -阶段验收补充说明: - -1. `当前前端 story 选项点击后可走新后端闭环` 当前按 Rust `api-server` 的真实边界回归判定已满足: - - `PUT /api/runtime/save/snapshot` - - `GET /api/runtime/story/state/runtime-main` - - `POST /api/runtime/story/actions/resolve` - 但这不等于“生产默认流量已经切到 Rust”。 -2. `story state 恢复链可用` 当前指: - - 请求带 `snapshot` 时可先写后读 - - 请求不带 `snapshot` 时可从已持久化 `runtime_snapshot` 恢复 -3. `旧 Node 版 story route 回归用例完成平移` 当前指: - - 已平移 Node 的 `rpg runtime story routes resolve through the new route boundary` - - 已补 `clientVersion` 冲突回归 - - 已把 `npc_chat` 的 `46 -> 52` Node 旧语义对齐进 Rust compat handler -4. `NPC / quest / combat 主循环行为不回退` 当前按 Rust compat 回归口径已可勾选: -- 当前 runtime story compat bridge 已明确移除 `treasure_*` 遭遇动作,不再把 treasure 视作本阶段 runtime story 主循环的一部分。 -- `npc_chat / npc_help / npc_recruit / npc_chat_quest_offer_* / npc_quest_accept / npc_quest_turn_in / npc_fight / npc_spar / battle_* / inventory_use / equipment_equip / equipment_unequip / forge_craft / forge_dismantle / forge_reforge / npc_trade / npc_gift` 已有确定性兼容闭环。 -- 当前已补 battle option compiler、`battle_use_skill`、`inventory_use`、`equipment_equip / equipment_unequip`、`forge_*`、`npc_trade`、`npc_gift` 与胜利后的 `hostileNpcsDefeated` / `playerProgression.lastGrantedSource = hostile_npc` 写回。 -- 当前已补 NPC 交互态入口预处理:纯商贩型 NPC 即使没有预填 `npcStates.*.inventory`,也会在 compat bridge 内自动恢复可交易库存与基础关系态,不再依赖 Node 侧预热。 -- 更大范围 Node 回归与真相态 reducer 替换不再作为 M4 阻塞项,转入 M7 切流前回归矩阵。 -5. `后端边界与当前 rpgEntry -> ...` 当前按 Rust 代理与路由覆盖可勾选: - - 前端真实调用链已对齐 `/api/runtime/story/*` - - Rust 栈已覆盖 `/api/runtime/*` 与 `/api/story/*` 代理目标 - - `npm run dev:rust` 是本地 Rust 切流入口,M7 再做远端灰度与回退验证 diff --git a/backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md b/backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md deleted file mode 100644 index 5a62ebb7..00000000 --- a/backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md +++ /dev/null @@ -1,117 +0,0 @@ -# M5:custom world / gallery / agent 任务清单 - -## 0. 当前执行基线 - -本阶段与当前仓库里的创作链重构直接对应,统一以以下文档为准: - -1. [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) -2. [../docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md](../docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md) - -当前逻辑层命名和职责边界应优先使用 `rpgCreation / rpgAgent / rpgWorld` 口径;本任务清单继续保留 `custom world` 文件名,只是为了和后端重写阶段文档编号保持一致。 - -本轮首批可编码表设计见: - -3. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md) -4. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md) -5. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md) -6. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md) -7. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md) - -## 1. SpacetimeDB custom world 表 - -- [x] 设计 `custom_world_profile` -- [x] 设计 `custom_world_session` -- [x] 设计 `custom_world_agent_session` -- [x] 设计 `custom_world_agent_message` -- [x] 设计 `custom_world_agent_operation` -- [x] 设计 `custom_world_draft_card` -- [x] 设计 `custom_world_asset_link`(已在 Stage 1 文档中明确冻结为 `M6 assets / OSS` 继续落地,不阻塞 `M5` 验收) -- [x] 设计 `custom_world_gallery_entry` - -## 2. 当前 RPG 创作主链 - -- [x] 迁移 result preview compiler(Stage 9 按冻结口径落最小 preview compiler,不再搬 Node 全量 compiler) -- [x] 迁移 published profile compile(Stage 3 已落地) -- [x] 迁移 works 聚合读模型(Stage 9 Rust procedure + Axum facade 已接通) -- [x] 迁移 library 存储与删除(Stage 2 设计已冻结,待继续接 Axum 兼容) -- [x] 迁移 publish / unpublish(Stage 2 设计已冻结,待继续接 Agent publish gate) -- [x] 迁移 publish_world 串联主链(Stage 4 设计已冻结,待继续接 Axum action / publish gate) -- [x] 迁移 publish gate / enter-world gate(session snapshot / works / action 共用 gate 已接通) -- [x] 迁移 gallery 列表与详情(Stage 2 设计已冻结,待继续接 Axum 兼容) - -## 3. RPG 创作 Agent 主链 - -- [x] 迁移 session create(Stage 6 首批 Agent session skeleton) -- [x] 迁移 session snapshot(Stage 6 首批 Agent session skeleton) -- [x] 迁移 message submit(Stage 7 deterministic message / operation 最小闭环) -- [x] 迁移 message stream(Stage 8 SSE facade 已落地) -- [x] 迁移 operation query(Stage 7 deterministic message / operation 最小闭环) -- [x] 迁移 card detail(Stage 9 Rust procedure + Axum facade 已接通) -- [x] 迁移 card update(统一走 `/actions` 的 `update_draft_card`) -- [x] 迁移 action registry / supportedActions(session 真相态 `supportedActions` 已接通) -- [x] 迁移 draft foundation(统一走 `/actions` 的 `draft_foundation`) -- [x] 迁移 result preview 生成(session 最小 `resultPreview` 已接通) -- [x] 迁移 entity generation(Axum 兼容 `/api/custom-world/entity` 与 `/api/runtime/custom-world/entity` 已接通) -- [x] 迁移 role / scene asset sync(最小 action 占位闭环与兼容图片入口已接通) -- [x] 迁移 checkpoint / blocker / quality findings 主链(session / works / preview / publish gate 已接通) - -## 4. Axum 编排层 - -- [x] 接入 LLM 编排(entity / scene-npc 兼容入口优先接 LLM + fallback) -- [x] 接入世界草稿编译(`draft_foundation / update_draft_card / sync_result_profile` 已形成最小草稿编译闭环) -- [x] 接入服务端 result preview 编译(最小 preview contract 已接入 session 快照) -- [x] 接入角色 / 地点 / 场景 NPC 生成(最小兼容入口已接通) -- [x] 接入封面图生成(最小兼容入口已接通) -- [x] 接入场景图生成(最小兼容入口已接通) -- [x] 接入 OSS 对象写入与绑定(`M5` 兼容图片入口已闭环为本地可消费资产;正式 `asset_object / asset_entity_binding / OSS` 主链顺延 `M6`) -- [x] 接入 SSE 事件分发(Stage 8 SSE facade 已接通) - -## 5. 当前正式接口与历史兼容台账 - -### 5.1 当前正式接口 - -- [x] 兼容 `/api/runtime/custom-world-library`(Stage 5 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world-library/:profileId`(owner-only detail 查询已补齐) -- [x] 兼容 `/api/runtime/custom-world-library/:profileId/publish`(Stage 5 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world-library/:profileId/unpublish`(Stage 5 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world-gallery`(Stage 5 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world-gallery/:ownerUserId/:profileId`(Stage 5 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world/works` -- [x] 兼容 `/api/runtime/custom-world/agent/sessions`(Stage 6 首批 Axum facade) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId`(Stage 6 首批 Axum facade) -- [x] 兼容 `DELETE /api/runtime/custom-world/agent/sessions/:sessionId`(草稿物理清理;若作品卡误以已发布来源 session 删除,则回落到关联 profile 软删除并返回 works) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages`(Stage 7 deterministic message submit) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`(Stage 8 SSE facade) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/actions`(Stage 9 全量 action procedure 已接通) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId`(Stage 7 deterministic operation query) -- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId` -- [x] 兼容 `/api/custom-world/entity` -- [x] 兼容 `/api/runtime/custom-world/entity` -- [x] 兼容 `/api/custom-world/scene-npc` -- [x] 兼容 `/api/runtime/custom-world/scene-npc` -- [x] 兼容 `/api/custom-world/scene-image` -- [x] 兼容 `/api/custom-world/cover-image` -- [x] 兼容 `/api/custom-world/cover-upload` - -### 5.2 历史兼容台账(非当前主链) - -- [x] 评估 `/api/runtime/custom-world/sessions` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除) -- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除) -- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/answers` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除) -- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/generate/stream` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除) - -## 6. 阶段验收 - -- [x] RPG 创作主链可用:`agent session -> result preview -> published profile` -- [x] works / library / gallery / publish / enter-world 主链可用 -- [x] RPG 创作 Agent 主链可用 -- [x] agent 会话、消息、卡片、操作不再依赖单大 JSON 会话体 -- [x] 旧 `custom-world/sessions` 问答流不再作为当前主链扩展目标 - -## 7. 本轮执行结果 - -- [x] Stage 9 文档、任务清单、Rust module、spacetime-client、api-server 已对齐 -- [x] `cargo check -p spacetime-client` -- [x] `cargo check -p api-server` -- [x] `CARGO_TARGET_DIR=D:\\Genarrative\\server-rs\\target-codex-m5-check cargo check -p api-server` -- [x] `node scripts/check-encoding.mjs ...` 编码检查通过 diff --git a/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md b/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md deleted file mode 100644 index eb9ec69d..00000000 --- a/backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md +++ /dev/null @@ -1,153 +0,0 @@ -# M6:assets / 阿里云 OSS 任务清单 - -说明: - -1. `editor` 已于 `2026-04-21` 被确认为遗留无用模块,不再纳入本轮 Rust 后端重写范围。 -2. 本文件保留原文件名仅用于延续既有任务编号与链接,不再继续安排 editor 迁移项。 - -## 1. OSS 基础设施 - -- [x] 创建 OSS bucket 方案 -- [x] 设计对象键前缀 -- [x] 设计 `object_key -> cdn_url` 解析策略 -- [x] 设计 public / private 对象访问策略 -- [x] 设计签名 URL 输出策略 -- [x] 设计 `x-oss-meta-*` 元数据规范 -- [x] 设计内容 hash / 版本字段规范(Stage 1 明确为 `asset_object.content_hash: Option` + `version = 1`,后续强 hash 单独阶段再扩) - -## 2. 上传与对象确认 - -- [x] 实现浏览器 `PostObject` 直传签名接口 -- [x] 实现 STS 临时授权接口 -- [x] 实现服务端上传 helper -- [x] 实现上传完成后的对象确认接口 -- [x] 实现对象绑定业务实体 reducer - -补充说明: - -1. 自 `2026-04-21` 起,当前重写节奏允许在 `M3/M4/M5` 之前先前置落地 `M6` 的 OSS 基础设施。 -2. 当前已在 `server-rs/crates/platform-oss` 与 `server-rs/crates/api-server` 落下最小可用链路: - - `PostObject` 直传签名能力 - - `/api/assets/direct-upload-tickets` - - `/api/assets/objects/confirm` - - 兼容旧 `/generated-*` 前缀的对象键规划 - - `.env/.env.local` 的 OSS 环境变量加载 - - 服务端 `HEAD Object` 校验 - - `asset_object` 确认真实 SpacetimeDB 持久化 - - `/api/assets/objects/bind` - - `asset_entity_binding` 业务实体槽位绑定 - - `/api/assets/sts-upload-credentials` 禁用式 contract - - 服务端 `PutObject` 上传 helper -3. 当前 bucket 已明确为私有读写;后续正式存储口径改为 `bucket + object_key` 双列,不再把匿名公开 URL 当成真相。 -4. 当前 STS 接口按“服务器上传、Web 只下载”的需求固定为 `403` 禁用式 contract,不向浏览器下发 OSS 写权限。 -5. `2026-04-21` 已通过 live test 验证:真实 OSS 上传后,`/api/assets/objects/confirm` 能把 `xushi-dev + object_key` 写入本地 `genarrative-dev.asset_object`,并可继续通过 `/api/assets/objects/bind` 绑定到业务实体槽位。 - -## 3. 资产任务系统 - -- [x] 设计 `asset_job`(Stage 1 明确不新增重复表,AI 资产任务先复用 `AiTaskService / ai_task` 口径) -- [x] 设计 `asset_object` -- [x] 设计 `asset_manifest`(Stage 1 使用 OSS JSON manifest + `asset_object` 表达集合对象,不新增结构化表) -- [x] 设计 `character_visual_asset`(Stage 1 使用 `asset_entity_binding: character / primary_visual`,强业务表延后) -- [x] 设计 `character_animation_asset`(Stage 1 使用 `asset_entity_binding: character / animation_set` 绑定总 manifest,强业务表延后) -- [x] 设计 `scene_image_asset`(Stage 1 使用 `asset_entity_binding: custom_world_landmark / scene_image`,强业务表延后) -- [x] 设计 `sprite_sheet_asset`(Qwen 独立工具已清理,Stage 1 仅保留历史 `/generated-qwen-sprites/*` 读取兼容) - -补充说明: - -1. `asset_object` 当前已冻结核心存储口径为: - - `bucket` - - `object_key` -2. 详细设计见: - - [../docs/technical/SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) - - [../docs/technical/SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md) - - [../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md) - - [../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md) -3. 当前已在 `server-rs/crates/spacetime-module` 落下 `asset_object` 首版表骨架,并完成 `api-server -> SpacetimeDB` 的最小对象确认闭环。 -4. 元数据、版本、manifest 与强业务资产表边界见: - - [../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md) - -## 4. 资产生成链路 - -- [x] 迁移角色主形象生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前仍为 SVG 占位生成,不代表真实 DashScope 图片模型已迁完) -- [x] 迁移角色动作生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前 `image-sequence` 为 SVG 占位帧,视频类策略优先复用参考视频或仓库占位预览,不代表真实视频模型已迁完) -- [x] 迁移动作模板查询(Stage 1 已接通 Rust 内置模板列表兼容接口) -- [x] 迁移视频导入(Stage 1 已接通 Data URL 视频导入到 OSS 草稿区,不再写本地 `public/`) -- [x] 迁移工作流缓存(Stage 1 已接通 Rust `GET/POST character-workflow-cache` 到 OSS JSON 草稿对象,不再写本地 `public/`) -- [x] 迁移场景图生成(已完成 Stage 2:custom world `scene-image` 走真实 DashScope 图片生成,并继续写入 `OSS + asset_object + asset_entity_binding`) -- [x] 迁移封面图上传(已完成 Stage 2:custom world `cover-image / cover-upload` 已补齐真实 DashScope 生成与 `cropRect + 16:9 + WebP 压缩`) -- [x] 首批收口 custom world `scene-image / cover-image / cover-upload` 到正式 `OSS + asset_object + asset_entity_binding` 主链(保持旧 `/generated-*` 返回 contract,不再写仓库 `public/`) - -补充说明: - -1. custom world 兼容图片入口现已完成 Stage 1 + Stage 2:正式资产真相链、真实 DashScope 图片生成,以及封面上传裁剪压缩都已迁完。 -2. 详细边界见: - - [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) -3. 角色动作模板与视频导入第一批已新增独立设计文档,当前只迁移: - - `GET /api/assets/character-animation/templates` - - `POST /api/assets/character-animation/import-video` - - [../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md) -4. 角色资产工作流缓存第一批已新增独立设计文档,当前把旧本地 `workflow-cache.json` 改为 OSS JSON 草稿对象: - - `GET /api/assets/character-workflow-cache/:characterId` - - `POST /api/assets/character-workflow-cache` - - [../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md) -5. `2026-04-22` 复核确认:旧独立 `qwen-sprite-tool + qwenSpriteRoutes.ts` 已在 `2026-04-21` 清理,不再作为本轮现役迁移主链;当前仍保留的 `Qwen` 相关内容仅包括: - - 角色资产 prompt 层对 `packages/shared/src/prompts/qwenSprite.ts` 的复用 - - 历史资源前缀 `/generated-qwen-sprites/*` 的读取兼容 -6. custom world 图片链 Stage 2 已完成: - - `scene-image / cover-image` 已替换为真实 DashScope 图片生成 - - `cover-upload` 已补回 Node 旧链路中的 `cropRect + 16:9 + WebP 压缩` - - 详细口径与验证结果见 [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md) - -## 5. 路径兼容 - -- [x] 兼容 `/generated-character-drafts/*` -- [x] 兼容 `/generated-characters/*` -- [x] 兼容 `/generated-animations/*` -- [x] 兼容 `/generated-custom-world-scenes/*` -- [x] 兼容 `/generated-custom-world-covers/*` -- [x] 兼容 `/generated-qwen-sprites/*` - -补充说明: - -1. 第一批路径兼容由 Rust `api-server` 同源代理到私有 OSS 短期读签名,不回退本地 `public/`,详细边界见: - - [../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md) -2. 当前 Stage 1 先全量代理对象内容,不实现视频 Range 分片;若后续真实视频体积变大,再按播放器需求补 Range。 - -## 6. 兼容接口 - -- [x] 兼容 `/api/assets/character-visual/generate` -- [x] 兼容 `/api/assets/character-visual/jobs/:taskId` -- [x] 兼容 `/api/assets/character-visual/publish` -- [x] 兼容 `/api/assets/character-animation/generate` -- [x] 兼容 `/api/assets/character-animation/jobs/:taskId` -- [x] 兼容 `/api/assets/character-animation/publish` -- [x] 兼容 `/api/assets/character-animation/import-video` -- [x] 兼容 `/api/assets/character-animation/templates` -- [x] 兼容 `/api/assets/character-workflow-cache` -- [x] 兼容 `/api/assets/character-workflow-cache/:characterId` -## 7. 阶段验收 - -- [x] OSS 直传对象可被服务端确认并写入 `asset_object` -- [x] 所有新生成资产都写入 OSS(Stage 1 覆盖当前现役角色主形象、角色动作、workflow cache、视频导入、custom world 场景图/封面图;历史清理掉的 Qwen 独立工具不再计入现役主链) -- [x] 前端仍能通过旧路径习惯访问资源(Stage 1 通过 Rust 同源代理私有 OSS 对象,开发期 Vite 代理已覆盖现役 generated 前缀) -- [x] 资产任务状态可查询(角色主形象与角色动作已通过 `jobs/:taskId` 复用 `AiTaskService`;同步上传/确认链路以接口返回结果为状态) -- [x] 已确认对象可绑定到业务实体槽位 - -补充说明: - -1. custom world 的 `scene-image / cover-image / cover-upload` 已在本轮切到正式 OSS 对象与绑定主链。 -2. 角色主形象第一批已新增独立设计文档与 Rust 最小闭环: - - [../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md) -3. 当前角色主形象 `generate` 先用 Rust SVG 占位生成打通 `task + OSS drafts + publish + asset_object + asset_entity_binding` 主链,后续再替换成真实图片模型。 -4. 角色动作模板与视频导入第一批已接入 Rust: - - `templates` 返回旧内置模板 contract。 - - `import-video` 当前只接受 `data:video/*;base64,...`,并写入 OSS `generated-character-drafts/*` 草稿区。 -5. 角色资产工作流缓存第一批已接入 Rust: - - 保存时写入 OSS `generated-character-drafts/{character}/workflow-cache/workflow-cache.json`。 - - 读取时未命中返回 `cache: null`,保持旧前端 contract。 -6. 角色动作第一批已接入 Rust: - - `generate` 直接写入 OSS `generated-character-drafts/*`。 - - `jobs/:taskId` 从 `AiTaskService` 派生旧任务状态 contract。 - - `publish` 会把动作帧与总 manifest 写入 OSS `generated-animations/*`,并确认 `asset_object + asset_entity_binding`。 -7. custom world 场景图、封面图、封面上传已在 `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md` + `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md` 范围内完成正式 `OSS + asset_object + asset_entity_binding` 主链、真实 DashScope 图片生成和封面上传裁剪压缩。 -8. `content_hash/version`、`asset_job`、`asset_manifest` 与强业务资产表当前已冻结 Stage 1 边界,不再作为 M6 第一批工程阻塞项;后续若要做内容去重、manifest 查询、审核/回滚或 sprite sheet 强结构化,再进入独立阶段。 diff --git a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md b/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md deleted file mode 100644 index c7d5019a..00000000 --- a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md +++ /dev/null @@ -1,66 +0,0 @@ -# M7:联调、回归、部署与切流任务清单 - -## 1. 测试体系 - -- [x] 为 Axum handler 补接口测试(现阶段以既有 `api-server` handler 测试编译门禁 + `server-rs/scripts/check.ps1` 固化;新增接口测试继续按主链补齐) -- [x] 为 SpacetimeDB reducer 补规则测试(现阶段以 `cargo check -p spacetime-module` 作为 schema/reducer/procedure 最小门禁;真实数据库规则回归继续由本地 publish smoke 承接) -- [x] 为 view / projection 补数据一致性测试(现阶段以 `shared-contracts` contract 回归与 SpacetimeDB schema check 固化投影字段门禁) -- [x] 为 auth 主链补集成测试(现有 `shared-contracts` 与 `api-server` 鉴权 handler 测试已纳入 Rust 主线检查入口) -- [x] 为 runtime snapshot 主链补集成测试(现有 runtime contract 回归已纳入 Rust 主线检查入口) -- [x] 为 story action 主链补集成测试(现有 runtime story contract / handler 测试编译已纳入 Rust 主线检查入口) -- [x] 为 custom world / agent 主链补集成测试(现阶段纳入 `api-server` 编译与 Rust 主线检查;真实 LLM/OSS 环境联调继续由 smoke 承接) -- [x] 为 assets / OSS 主链补集成测试(现有 M6 OSS smoke 与 contract 测试保留,Rust 主线检查固化基础门禁) -- [x] 为兼容 contract 补回归测试(`cargo test -p shared-contracts` 已纳入 Rust 主线检查) - -## 2. 部署准备 - -- [x] 设计 Axum 部署方式 -- [x] 设计 SpacetimeDB 发布方式 -- [x] 设计 OSS bucket / CDN / 域名方案 -- [x] 设计环境变量清单 -- [x] 设计灰度环境 -- [x] 设计数据迁移脚本 -- [x] 设计回滚策略 -- [x] 准备本地 Rust 一键联调脚本(`npm run dev:rust` 同时启动前端、Rust `api-server` 与本地 SpacetimeDB) -- [x] 准备 Ubuntu 发布包构建脚本(`npm run build:rust:ubuntu` 生成 `build//`,包含 `web/`、`api-server`、`spacetime_module.wasm`、`start.sh`、`stop.sh`,并默认 scp 上传到目标服务器) - -## 3. 观测能力 - -- [x] 接入 tracing / request id / structured logs -- [x] 接入慢请求追踪 -- [x] 接入上游 LLM / OSS / 短信 / 微信失败日志(沿用既有 provider error envelope 与 tracing,M7 固化字段口径) -- [x] 接入关键 reducer 执行日志(现阶段固定 reducer 操作日志字段口径,真实 publish 日志回看继续由 SpacetimeDB smoke 承接) -- [x] 接入资产任务状态日志(沿用 `AiTaskService / ai_task` 状态链,M7 固化 `task_id / status / asset_kind` 观测口径) - -## 4. 切流准备 - -- [x] 准备旧 Node 与新 Rust 双跑窗口 -- [x] 准备 API 对比脚本 -- [x] 准备主流程 smoke 清单 -- [x] 准备前端切换开关 -- [x] 准备回退开关 - -## 5. 主工程结构收口 - -- [x] 拆分 `server-rs/crates/spacetime-module/src/lib.rs`,按业务模块与 SpacetimeDB 的 `table / reducer / procedure / view` 聚合结构整理为 `runtime`、`gameplay::{story/combat/inventory/npc/quest/runtime_item/progression}`、`custom_world`、`asset_metadata`、`ai` 等子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口 - -执行约束: - -1. 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。 -2. 拆分后的模块边界必须与 `M0` 已冻结的模块迁移归属一致,避免 `spacetime-module` 再回退成单大包。 -3. 拆分完成后至少要保持 `cargo check`、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。 - -## 6. 阶段验收 - -- [x] 本地切流前预检通过(M7 阶段性预检包装入口已归档,长期入口改为 `server-rs/scripts/check.ps1`) -- [x] 主流程基础回归通过(`cargo check -p spacetime-module`、`cargo check -p api-server`、`cargo test -p shared-contracts`、`cargo test -p api-server --no-run`) -- [ ] 全链路 smoke 通过 -- [ ] 主流程真实环境回归通过 -- [ ] 关键 SSE 接口联调通过 -- [ ] 可在灰度环境完成切流 - -补充说明: - -1. M7 已新增 [../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md),冻结本地预检、部署、灰度、双跑、回滚与结构收口口径。 -2. 本轮新增 [../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md),并落地 `scripts/dev-rust-stack.ps1`、`scripts/dev-rust-stack.sh`、`scripts/deploy-rust-remote.sh`;其中发布脚本当前语义为生成 Ubuntu release 包。 -3. 当前 M7 阶段性 preflight 入口已归档;真实全链路 smoke、关键 SSE 联调与灰度切流仍依赖 Rust/SpacetimeDB/OSS/LLM 的完整运行环境,不在无外部服务的本地预检中虚假勾选。 diff --git a/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md b/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md deleted file mode 100644 index c0e479dc..00000000 --- a/backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md +++ /dev/null @@ -1,62 +0,0 @@ -# 横向专项、执行顺序与最终验收 - -## 1. 横向专项任务 - -### Contract 与前端兼容 - -- [x] 梳理当前 `packages/shared/src/contracts/*` 到 Rust DTO 的映射 -- [x] 设计 Rust 侧 contract 生成或手写策略 -- [x] 保持当前字段名、枚举值、响应结构稳定 -- [x] 为 breaking change 建立显式变更流程 - -### SpacetimeDB schema 演进治理 - -- [x] 约定 stable reducer 命名规则 -- [x] 约定 stable table 命名规则 -- [x] 约定列追加式演进规则 -- [x] 约定软删除而不是直接删表删列的场景 -- [x] 约定事件表与投影表拆分规则 - -### 大对象与缓存治理 - -- [x] 明确哪些内容入 OSS -- [x] 明确哪些内容只存 SpacetimeDB 元数据 -- [x] 明确哪些内容允许短期本地缓存 -- [x] 明确 workflow cache 生命周期 - -### 文档维护 - -- [x] 每个阶段完成后同步更新设计文档 -- [x] 每个阶段完成后补一份落地记录 -- [x] 完成接口迁移后更新新的模块与 API 索引文档 -- [ ] `M4` 结构变更同步对齐 `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` -- [x] `M5` 结构变更同步对齐 `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` - -补充说明: - -1. 横向治理规则已冻结在 [../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md)。 -2. Rust 侧 96 条 Axum 路由索引已冻结在 [../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。 -3. `M4` 当前仍存在 `runtime_story` 独立 crate 拆分工作区,结构文档对齐需等该拆分收口后再勾选。 - -## 2. 第一优先级建议执行顺序 - -1. 先做 `M0`,冻结基线,避免迁移过程中口径漂移。 -2. 再做 `M1 + M2`,先把 Axum 壳与鉴权打稳。 -3. 再做 `M3`,优先跑通快照、设置、profile。 -4. 进入 `M4` 和 `M5` 前,先用两份 `2026-04-21` 执行方案冻结当前仓库里的 RPG 运行时链与创作链结构口径。 -5. 再做 `M4`,把 RPG runtime story 主循环真正迁走。 -6. 然后做 `M5`,迁 RPG 创作主链、works/library/gallery 与 agent。 -7. 最后做 `M6 + M7`,收口 assets、editor、部署与切流。 - -## 3. 最终验收清单 - -- [x] 当前 `96` 条后端接口已全部迁移或有兼容替代 -- [ ] 当前 `6` 个挂载面已全部迁移 -- [ ] 当前 `12` 个内部模块已完成新架构落位 -- [ ] Axum 已成为唯一 HTTP / SSE / 副作用边界 -- [ ] SpacetimeDB 已成为唯一运行时状态真相源 -- [ ] 阿里云 OSS 已成为唯一资产对象仓 -- [ ] `M4` 已与 `rpgEntry / rpgSession / rpgRuntime / rpgRuntimeStory / rpgProfile` 主链口径一致 -- [x] `M5` 已与 `agent session -> result preview -> published profile` 主链口径一致 -- [ ] 前端主流程在不大改 UI 的前提下可跑通 -- [ ] 能完成灰度切流,并保留可回退能力 diff --git a/backend-rewrite-tasklist/M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md deleted file mode 100644 index daa8f874..00000000 --- a/backend-rewrite-tasklist/M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md +++ /dev/null @@ -1,183 +0,0 @@ -# M0:后端挂载面冻结基线 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json) - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第一条任务: - -- 整理当前后端 `6` 个挂载面并锁定为重写验收基线 - -这里的“冻结”不是要求新后端永远维持原实现,而是要求: - -1. 当前 Node 后端历史基线仍固定为这 `6` 个挂载面。 -2. 本轮 Rust 后端的 active rewrite target 固定覆盖其中 `5` 个挂载面:`assets`、`auth`、`health`、`runtime-main`、`runtime-story-action`。 -3. `editor` 作为历史遗留挂载面继续保留对照记录,但自 `2026-04-21` 起不纳入 `server-rs` 本轮重写验收。 -4. 允许内部实现从 `Express + PostgreSQL + 本地 public/generated-*` 重写为 `Axum + SpacetimeDB + 阿里云 OSS`,但不允许把挂载面职责打散到无法对照验收。 - -## 2. 冻结结论 - -当前 Node 后端的正式挂载面固定为以下 `6` 个: - -| 挂载面 ID | 中文名称 | 当前路由数 | 当前入口 | 必须保留的顶层路径 | -| --- | --- | --- | --- | --- | -| `assets` | 资产生成工具面 | `14` | `server-node/src/app.ts -> /api/assets` | `/api/assets/*` | -| `auth` | 鉴权与会话面 | `17` | `server-node/src/app.ts -> /api/auth` | `/api/auth/*` | -| `editor` | 编辑器工具面 | `3` | `server-node/src/app.ts -> /api/editor` | `/api/editor/*` | -| `health` | 基础健康检查 | `1` | `server-node/src/app.ts -> /healthz` | `/healthz` | -| `runtime-main` | 运行时主能力面 | `59` | `server-node/src/app.ts -> /api` | `/api/runtime/*`、`/api/profile/*`、`/api/custom-world/*`、`/api/llm/*`、`/api/ws/*` | -| `runtime-story-action` | 运行时 Story Action 面 | `2` | `server-node/src/app.ts -> /api/runtime/story` | `/api/runtime/story/*` | - -冻结总数: - -1. 历史对外挂载面:`6` -2. 本轮 active rewrite target:`5` -3. 已登记路由:`96` -4. 公开接口:`10` -5. JWT 接口:`69` -6. 开关控制接口:`17` -7. 流式接口:`6` - -## 3. 各挂载面冻结要求 - -### 3.1 `assets` - -当前定位: - -1. 角色主形象生成 -2. 角色动作生成 -3. Qwen 精灵表生成与保存 -4. 工作流缓存 -5. 产物发布到 `public/generated-*` - -重写后的冻结要求: - -1. 仍保留独立的 `/api/assets/*` 命名空间。 -2. 仍保留“生成任务、任务状态查询、发布/保存”三类操作语义。 -3. 当前基于本地 `public/generated-*` 的产物落地,可改为 `OSS + 元数据表`,但前端一阶段必须继续通过原有路径习惯访问资源。 -4. 当前 `ASSETS_API_ENABLED` 门禁能力必须保留。 - -### 3.2 `auth` - -当前定位: - -1. 本地账号登录 -2. 手机验证码登录 -3. 微信登录 -4. refresh session -5. 会话吊销 -6. 审计与风控 - -重写后的冻结要求: - -1. 仍保留独立的 `/api/auth/*` 命名空间。 -2. 仍保留当前 `JWT + refresh cookie` 双令牌模型。 -3. 仍保留 `password / phone / wechat` 三类登录能力面。 -4. 仍保留审计日志、风控封禁、会话列表与会话吊销能力。 - -### 3.3 `editor` - -当前定位: - -1. 编辑器 JSON 读取 -2. 编辑器 JSON 回写 -3. 图标目录枚举 - -重写后的冻结要求: - -1. `server-node/src/app.ts -> /api/editor/*` 的历史存在事实继续保留在基线文档中。 -2. 自 `2026-04-21` 起,该挂载面不纳入 `server-rs` 本轮重写范围,不再作为 `M1 ~ M6` 主线交付目标。 -3. 若未来仍需清理或替代 editor,需要在遗留链路依赖核对完成后单独立项。 - -### 3.4 `health` - -当前定位: - -1. 提供后端进程健康探针 -2. 为代理层与 smoke 提供最小可用确认 - -重写后的冻结要求: - -1. 仍保留 `/healthz`。 -2. 仍返回简单、无鉴权、无数据库强耦合的健康状态。 -3. 仍可作为 smoke 与部署探针的第一检查点。 - -### 3.5 `runtime-main` - -当前定位: - -1. 运行时存档、设置、个人档案 -2. 聊天、剧情、任务、运行时物品意图 -3. custom world library / gallery / sessions -4. custom world agent 会话、消息、操作 - -重写后的冻结要求: - -1. 仍保留运行时主入口作为最大能力面,不把这些能力拆散到前端无法感知的新命名空间。 -2. 仍兼容当前: - - `/api/runtime/*` - - `/api/profile/*` - - `/api/custom-world/*` - - `/api/llm/*` - - `/api/ws/*` -3. 除公开画廊与少量公开接口外,仍以登录态为默认访问前提。 -4. 当前大量业务逻辑虽然会迁到 `SpacetimeDB reducer/view + Axum facade`,但对前端看起来仍应是一个统一运行时能力面。 - -### 3.6 `runtime-story-action` - -当前定位: - -1. story choice 动作解析 -2. story session 状态恢复 - -重写后的冻结要求: - -1. 仍保留 `/api/runtime/story/*` 作为独立挂载面。 -2. 仍保持“前端动作输入 -> 后端统一结算 -> 返回新状态”的接口职责。 -3. 当前 `storyActionService` 里跨 `quest / inventory / runtime-item / npc / progression / combat / runtime` 的协作,迁移后必须继续存在,只是实现位置改到 `SpacetimeDB + Axum`。 - -## 4. 挂载面与新架构映射 - -| 当前挂载面 | 新架构主归属 | 说明 | -| --- | --- | --- | -| `assets` | `Axum + OSS + SpacetimeDB asset metadata` | 外部副作用在 Axum,二进制在 OSS,任务与引用状态在 SpacetimeDB。 | -| `auth` | `Axum auth-service + SpacetimeDB auth tables` | 登录副作用与 cookie/JWT 在 Axum,身份与会话状态在 SpacetimeDB。 | -| `editor` | `遗留保留于 server-node` | 历史挂载面对照,当前不进入 Rust 重写主链。 | -| `health` | `Axum health route` | 维持最小化健康检查面。 | -| `runtime-main` | `Axum runtime facade + SpacetimeDB runtime/custom-world tables` | Axum 维持兼容 REST/SSE,SpacetimeDB 负责状态真相。 | -| `runtime-story-action` | `Axum story facade + SpacetimeDB gameplay reducers/views` | Story Action 入口继续独立存在,但结算内核迁到新状态层。 | - -## 5. 本轮冻结后的硬约束 - -后续迁移中,不允许出现以下情况: - -1. 把历史 `6` 个挂载面减少成更少但无法一一对照的“超级入口”。 -2. 为了迎合本轮重写范围而把历史存在的 `/api/editor/*` 从基线文档中抹掉。 -3. 把当前 `/api/auth/*`、`/api/assets/*`、`/api/runtime/story/*` 顶层命名空间直接改掉。 -4. 在未完成路径兼容前,直接移除 `/healthz` 或 `/generated-*` 的既有访问习惯。 -5. 在未完成契约回归前,把 `runtime-main` 和 `runtime-story-action` 的职责重新混成一个难以验收的大入口。 - -## 6. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. 当前历史 `6` 个挂载面已经有正式书面冻结清单。 -2. 每个挂载面都有: - - 当前入口 - - 当前路由数 - - 顶层路径空间 - - 重写后必须保留的职责边界 -3. 本轮 active rewrite target 为 `5` 个,且 `editor` 的遗留/不迁移口径已经冻结。 -4. 后续任务可以直接以这份文档作为验收引用,不再靠聊天记录记忆。 - -## 7. 后续直接依赖这份基线的任务 - -1. 整理当前后端 `96` 条路由并生成“旧接口 -> 新实现”映射表 -2. 整理当前 `12` 个内部模块并锁定迁移归属 -3. 设计 Axum 路由树 -4. 设计 SpacetimeDB 表 / reducer / view 分层 diff --git a/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md deleted file mode 100644 index 3af1cdf2..00000000 --- a/backend-rewrite-tasklist/M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md +++ /dev/null @@ -1,262 +0,0 @@ -# M0:前端直接依赖的响应头、Envelope 与错误格式冻结基线 - -日期:`2026-04-20` - -依据来源: - -- `server-node/src/http.ts` -- `server-node/src/middleware/responseEnvelope.ts` -- `server-node/src/middleware/errorHandler.ts` -- `server-node/src/middleware/requestId.ts` -- `packages/shared/src/http.ts` -- `src/services/apiClient.ts` -- `src/services/authService.ts` -- `src/services/aiService.ts` -- `src/editor/shared/jsonClient.ts` -- `src/services/apiClient.test.ts` - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第六条任务: - -- 整理当前前端直接依赖的响应头、envelope、错误格式 - -这里的“直接依赖”指的是:如果 Axum 重写时把这些头或 body 结构改掉,当前前端 `src/services/*`、编辑器请求层和鉴权异常处理就会立刻出问题。 - -## 2. 冻结结论 - -当前前端直接依赖的响应契约,冻结为以下 4 层: - -1. 请求侧默认会发送 `x-genarrative-response-envelope: v1`。 -2. 响应侧默认要回 `x-request-id`、`x-api-version`、`x-route-version`。 -3. 成功响应在请求方要求 envelope 时,必须返回标准 `ok/data/error/meta` 结构。 -4. 错误响应既要兼容标准 envelope,也要兼容旧式 `{ error, meta }` / `{ message, code }` 解析回退。 - -补充结论: - -1. 当前正式前端代码里,没有生产用例主动关闭 envelope 请求头。 -2. `x-response-time-ms` 当前不是前端代码的直接读取项,但属于现有兼容头集合,重写时仍应保留。 -3. 鉴权链路额外直接依赖错误码 `CAPTCHA_REQUIRED` 与 `error.details.captchaChallenge`。 - -## 3. 当前前端直接依赖矩阵 - -| 依赖项 | 当前值/结构 | 当前消费者 | 当前作用 | -| --- | --- | --- | --- | -| 请求头 | `x-genarrative-response-envelope: v1` | `src/services/apiClient.ts`、`src/editor/shared/jsonClient.ts` | 请求标准 envelope 响应。 | -| 响应头 | `x-request-id` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.requestId` 的回退来源。 | -| 响应头 | `x-api-version` | `src/services/apiClient.ts`、`packages/shared/src/http.ts` | 识别标准 envelope / error body。 | -| 响应头 | `x-route-version` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.routeVersion` 的回退来源。 | -| 成功 body | `{ ok: true, data, error: null, meta }` | `unwrapApiResponse(...)` | 前端默认解包标准成功 envelope。 | -| 错误 body | `{ ok: false, data: null, error, meta }` | `ApiClientError`、`parseApiErrorMessage(...)` | 标准错误解析。 | -| 旧错误 body | `{ error, meta }` / `{ message, code }` | `parseApiErrorMessage(...)` | 老接口或非标准错误回退解析。 | -| 错误细节 | `error.code === 'CAPTCHA_REQUIRED'` 且 `error.details.captchaChallenge` | `src/services/authService.ts` | 手机验证码发送前的验证码挑战弹出。 | - -## 4. 请求侧冻结要求 - -### 4.1 Envelope 请求头 - -当前前端默认行为: - -1. `src/services/apiClient.ts` 会自动补: - - `x-genarrative-response-envelope: v1` -2. `src/editor/shared/jsonClient.ts` 也会自动补: - - `x-genarrative-response-envelope: v1` - -当前后端接受的 envelope 触发值: - -1. `1` -2. `true` -3. `v1` -4. `envelope` - -但当前前端真实发送值冻结为: - -1. `v1` - -补充冻结点: - -1. 虽然 `apiClient` 提供了 `omitEnvelopeHeader` 选项,但当前生产代码没有实际依赖它。 -2. 因此第一阶段 Axum 应默认兼容“前端请求即要 envelope”的模式。 - -### 4.2 鉴权与凭证约定 - -当前前端请求层默认还会做: - -1. `Authorization: Bearer ` 自动注入。 -2. `credentials: same-origin`。 -3. 遇到 `401` 时尝试走 `/api/auth/refresh` 自动刷新。 - -这不是本文重点,但它解释了为什么 envelope 和错误格式必须在 `/api/auth/refresh` 上也保持兼容。 - -## 5. 响应头冻结要求 - -### 5.1 必须保留的前端直接依赖头 - -| 响应头 | 当前来源 | 当前前端用法 | -| --- | --- | --- | -| `x-request-id` | `requestIdMiddleware` + `applyApiResponseHeaders` | `ApiClientError.meta.requestId` 的 header 回退来源。 | -| `x-api-version` | `applyApiResponseHeaders` | 当前标准 API 契约版本识别。 | -| `x-route-version` | `applyApiResponseHeaders` | `ApiClientError.meta.routeVersion` 的 header 回退来源。 | - -### 5.2 兼容头但非直接读取项 - -| 响应头 | 当前状态 | 说明 | -| --- | --- | --- | -| `x-response-time-ms` | 当前统一输出 | 目前前端代码未直接读取,但设计文档与联调约定已锁定,不能随意删除。 | - -补充冻结点: - -1. `requestIdMiddleware` 会优先回显请求方传入的 `x-request-id`,否则服务端自生成。 -2. `ApiClientError` 读取元信息时优先取 body `meta`,没有再回退到 headers。 -3. 这意味着即便 envelope body 缺少部分 `meta` 字段,headers 仍必须完整。 - -## 6. 成功响应 Envelope 冻结格式 - -当前标准成功 envelope: - -```json -{ - "ok": true, - "data": {}, - "error": null, - "meta": { - "apiVersion": "2026-04-08", - "requestId": "req-xxx", - "routeVersion": "2026-04-08", - "operation": "runtime.story.initial", - "latencyMs": 12, - "timestamp": "2026-04-20T00:00:00.000Z" - } -} -``` - -冻结规则: - -1. `ok` 必须为 `true`。 -2. `data` 为真实业务负载。 -3. `error` 必须为 `null`。 -4. `meta.apiVersion` 必须存在,因为 `unwrapApiResponse(...)` 与 `isApiResponse(...)` 依赖它判断标准 envelope。 - -补充说明: - -1. 如果请求未带 envelope 头,当前后端可以直接返回裸 `data`。 -2. 但由于当前前端默认都会请求 envelope,第一阶段 Axum 基本等价于“所有 JSON 成功响应都要兼容这个结构”。 - -## 7. 错误响应 Envelope 与旧格式回退 - -### 7.1 当前标准错误 envelope - -```json -{ - "ok": false, - "data": null, - "error": { - "code": "UNAUTHORIZED", - "message": "缺少 Authorization Bearer Token", - "details": null - }, - "meta": { - "apiVersion": "2026-04-08", - "requestId": "req-xxx", - "routeVersion": "2026-04-08", - "operation": "auth.me", - "latencyMs": 3, - "timestamp": "2026-04-20T00:00:00.000Z" - } -} -``` - -冻结规则: - -1. `ok` 必须为 `false`。 -2. `data` 必须为 `null`。 -3. `error.code`、`error.message` 必须存在。 -4. `error.details` 可为对象或 `null`。 -5. `meta.apiVersion` 必须存在。 - -### 7.2 当前旧式错误格式回退 - -当请求未要求 envelope,或某些链路仍走旧写法时,当前后端与前端仍兼容以下错误结构: - -1. `{ "error": { "code": "...", "message": "...", "details": ... }, "meta": {...} }` -2. `{ "message": "...", "code": "..." }` -3. `{ "error": { "message": "..." } }` -4. 纯文本错误响应 - -`parseApiErrorMessage(rawText, fallbackMessage)` 的当前回退顺序固定为: - -1. `parsed.error.message` -2. 顶层 `message` -3. `error.code` 或顶层 `code`,拼成 `fallback(CODE)` -4. 原始文本 -5. 调用方的 `fallbackMessage` - -这意味着: - -1. Axum 第一阶段不能只兼容标准 envelope,而忽略旧错误解析的回退行为。 -2. 至少在迁移过渡期,`parseApiErrorMessage(...)` 可识别的信息要继续保留。 - -## 8. 前端对错误细节的业务级直接依赖 - -### 8.1 验证码挑战 - -`src/services/authService.ts` 当前明确依赖: - -1. `error instanceof ApiClientError` -2. `error.code === 'CAPTCHA_REQUIRED'` -3. `error.details.captchaChallenge` - -冻结要求: - -1. 如果后端要继续触发验证码挑战,必须继续返回: - - `code: 'CAPTCHA_REQUIRED'` - - `details.captchaChallenge` -2. 不能只返回中文文案而不带结构化 `details`。 - -### 8.2 元信息透传 - -`ApiClientError` 当前会保留: - -1. `status` -2. `code` -3. `details` -4. `meta.apiVersion` -5. `meta.requestId` -6. `meta.routeVersion` -7. `meta.operation` -8. `meta.latencyMs` -9. `meta.timestamp` - -冻结要求: - -1. Axum 不能把这些字段全都删成单纯 `message` 字符串。 -2. 即使部分业务 UI 现在没显示这些字段,它们已经进入前端错误对象结构。 - -## 9. 当前消费者清单 - -以下文件已构成当前前端的直接依赖面: - -1. `src/services/apiClient.ts` -2. `src/services/authService.ts` -3. `src/services/aiService.ts` -4. `src/editor/shared/jsonClient.ts` -5. `packages/shared/src/http.ts` - -## 10. 本轮冻结后的硬约束 - -后续迁移中,不允许出现以下情况: - -1. 删除 `x-genarrative-response-envelope: v1` 的请求协商能力。 -2. 删除 `x-request-id`、`x-api-version`、`x-route-version` 这些当前前端直接依赖的响应头。 -3. 把成功 envelope 从 `{ ok, data, error, meta }` 改成其他字段名。 -4. 把错误 envelope 从 `{ ok: false, data: null, error, meta }` 改成只有 `message`。 -5. 删除 `CAPTCHA_REQUIRED + details.captchaChallenge` 这一结构化错误契约。 -6. 让前端默认请求 envelope,但后端返回裸数据且不再可被 `unwrapApiResponse(...)` 识别。 - -## 11. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. 当前前端直接依赖的响应头、envelope、错误格式已有书面冻结清单。 -2. 已明确哪些是前端直接读取项,哪些是兼容保留项。 -3. 后续 Axum handler、错误中间件、response envelope 中间件可以直接按本文对齐,而不再靠人工试错。 diff --git a/backend-rewrite-tasklist/M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md deleted file mode 100644 index 1414aa66..00000000 --- a/backend-rewrite-tasklist/M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md +++ /dev/null @@ -1,245 +0,0 @@ -# M0:`/generated-*` 静态资源前缀冻结基线 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- `server-node/src/modules/assets/characterAssetRoutes.ts` -- `server-node/src/modules/assets/qwenSpriteRoutes.ts` -- `server-node/src/services/sceneImageService.ts` -- `server-node/src/services/customWorldCoverAssetService.ts` -- `server-node/src/services/customWorldAgentAutoAssetService.ts` -- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第五条任务: - -- 整理当前所有 `/generated-*` 静态资源前缀 - -这里的“整理”不是只列出几个目录名,而是要求冻结以下信息: - -1. 当前哪些 `/generated-*` 前缀是正式业务前缀。 -2. 每个前缀由哪条后端链路产出。 -3. 每个前缀对应的当前路径模板是什么。 -4. 哪些前缀只是未来设计名或测试噪音,不能误当成当前正式兼容面。 - -## 2. 冻结结论 - -当前工程里,正式业务使用的 `/generated-*` 静态资源前缀固定为以下 `6` 个: - -| 前缀 | 当前状态 | 当前主要生产链路 | 当前典型路径模板 | 重写后兼容要求 | -| --- | --- | --- | --- | --- | -| `/generated-character-drafts/*` | 正式前缀 | 角色主形象草稿、动作草稿、导入参考素材 | `/generated-character-drafts/{characterId}/{kind}/{jobId}/{file}` | 必须保留 | -| `/generated-characters/*` | 正式前缀 | 角色主形象正式发布、Agent 自动角色图回填 | `/generated-characters/{characterId}/visual/{assetId}/{file}` | 必须保留 | -| `/generated-animations/*` | 正式前缀 | 角色基础动作正式发布 | `/generated-animations/{characterId}/{animationSetId}/{action}/{file}` | 必须保留 | -| `/generated-custom-world-scenes/*` | 正式前缀 | 世界场景图生成、Agent 自动场景图回填 | `/generated-custom-world-scenes/{world}/{landmarkOrAct}/{assetId}/{file}` | 必须保留 | -| `/generated-custom-world-covers/*` | 正式前缀 | 世界封面图生成、封面上传 | `/generated-custom-world-covers/{world}/{assetId}/{file}` | 必须保留 | -| `/generated-qwen-sprites/*` | 正式前缀 | Qwen 主图草稿、精灵表草稿、修帧草稿、最终保存 | `/generated-qwen-sprites/{assetKeyOrDraftScope}/{actionOrKind}/{assetId}/{file}` | 必须保留 | - -额外结论: - -1. 当前仓库里真实业务前缀是 `6` 个,不是 `4` 个也不是 `5` 个。 -2. 其中 `generated-animations` 与 `generated-custom-world-covers` 是当前代码已正式使用、但早期重写设计里容易漏掉的两个前缀。 -3. 当前 `public/` 目录下已存在: - - `generated-character-drafts` - - `generated-characters` - - `generated-qwen-sprites` -4. `generated-animations`、`generated-custom-world-scenes`、`generated-custom-world-covers` 当前按需惰性创建,不代表它们不是正式前缀。 - -## 3. 正式前缀清单 - -### 3.1 `/generated-character-drafts/*` - -当前用途: - -1. 角色主形象候选图草稿。 -2. 角色动作草稿帧、草稿视频、导入参考素材。 -3. 角色资产工作流缓存与任务记录。 - -当前主要生产链路: - -1. `POST /api/assets/character-visual/generate` -2. `POST /api/assets/character-animation/generate` -3. `POST /api/assets/character-animation/import-video` -4. `POST /api/assets/character-workflow-cache` - -当前典型路径模板: - -1. `/generated-character-drafts/{characterId}/visual/{jobId}/candidate-01.png` -2. `/generated-character-drafts/{characterId}/animation/{action}/{jobId}/preview.mp4` -3. `/generated-character-drafts/{characterId}/animation/{action}/{jobId}/frame-01.png` -4. `/generated-character-drafts/{characterId}-{cacheKey}/workflow-cache.json` - -冻结要求: - -1. 它是“草稿态真相路径”,不是正式发布路径。 -2. 重写为 OSS 后仍要保留这一层 HTTP 兼容前缀,哪怕底层已不是本地磁盘。 - -### 3.2 `/generated-characters/*` - -当前用途: - -1. 角色主形象正式发布路径。 -2. Custom World Agent 自动回填的角色主图。 - -当前主要生产链路: - -1. `POST /api/assets/character-visual/publish` -2. `customWorldAgentAutoAssetService` - -当前典型路径模板: - -1. `/generated-characters/{characterId}/visual/{assetId}/master.png` -2. `/generated-characters/{characterId}/visual/{assetId}/preview-01.png` - -冻结要求: - -1. 它是角色正式视觉资产前缀,不允许与草稿前缀混用。 -2. 后续即使内部改成 OSS,也必须继续对前端暴露这一前缀。 - -### 3.3 `/generated-animations/*` - -当前用途: - -1. 角色基础动作正式发布路径。 -2. `CharacterAnimator` 侧消费的核心动画资源前缀。 - -当前主要生产链路: - -1. `POST /api/assets/character-animation/publish` - -当前典型路径模板: - -1. `/generated-animations/{characterId}/{animationSetId}/{action}/frame-01.png` -2. `/generated-animations/{characterId}/{animationSetId}/{action}/preview.mp4` -3. `/generated-animations/{characterId}/{animationSetId}/{action}/manifest.json` - -冻结要求: - -1. 当前正式动画并不挂在 `/generated-characters/.../animation/...` 下,而是独立前缀 `/generated-animations/*`。 -2. 后续重写若想合并对象键,也必须先做兼容别名,不能直接改掉公开路径。 - -### 3.4 `/generated-custom-world-scenes/*` - -当前用途: - -1. 自定义世界场景图生成结果。 -2. Agent 自动生成的场景图回填结果。 - -当前主要生产链路: - -1. `POST /api/custom-world/scene-image` -2. `customWorldAgentAutoAssetService` - -当前典型路径模板: - -1. `/generated-custom-world-scenes/{worldSegment}/{landmarkSegment}/{assetId}/scene.png` -2. `/generated-custom-world-scenes/{sceneSegment}/{actSegment}/{assetId}/scene.png` - -冻结要求: - -1. 前缀里当前显式带 `custom-world-scenes`,不是通用 `generated-scenes`。 -2. 后续世界资料库、世界编辑器和 Agent 卡片仍会依赖这一命名习惯。 - -### 3.5 `/generated-custom-world-covers/*` - -当前用途: - -1. 自定义世界封面图生成结果。 -2. 自定义世界封面图上传后规范化结果。 - -当前主要生产链路: - -1. `POST /api/custom-world/cover-image` -2. `POST /api/custom-world/cover-upload` - -当前典型路径模板: - -1. `/generated-custom-world-covers/{worldSegment}/{assetId}/cover.png` -2. `/generated-custom-world-covers/{worldSegment}/{assetId}/manifest.json` - -冻结要求: - -1. 当前正式前缀是 `/generated-custom-world-covers/*`,不是 `/generated-cover-images/*`。 -2. 后续重写设计和 OSS key 规划必须以这个现状兼容面为准。 - -### 3.6 `/generated-qwen-sprites/*` - -当前用途: - -1. Qwen 精灵主图草稿。 -2. Qwen 精灵表草稿。 -3. Qwen 修帧草稿。 -4. Qwen 精灵表最终保存结果。 - -当前主要生产链路: - -1. `POST /api/assets/qwen-sprite/master` -2. `POST /api/assets/qwen-sprite/sheet` -3. `POST /api/assets/qwen-sprite/frame-repair` -4. `POST /api/assets/qwen-sprite/save` - -当前典型路径模板: - -1. `/generated-qwen-sprites/_drafts/master/{draftId}/candidate-01.png` -2. `/generated-qwen-sprites/_drafts/sheet/{draftId}/candidate-01.png` -3. `/generated-qwen-sprites/_drafts/repair/{draftId}/candidate-01.png` -4. `/generated-qwen-sprites/{assetKey}/{actionKey}/{assetId}/sheet.png` - -冻结要求: - -1. 这个前缀当前既承载草稿,也承载正式保存结果。 -2. 如果未来要把草稿与正式对象拆桶,也必须先保留同一公开前缀兼容。 - -## 4. 非正式前缀与噪音项 - -以下命名当前不能当成“正式兼容前缀”: - -| 名称 | 当前来源 | 处理结论 | -| --- | --- | --- | -| `/generated-cover-images/*` | 仅出现在重写设计文档旧草案里 | 这是未来提案名,不是当前工程真实前缀。 | -| `/generated-role*` | 测试数据或示例 ID | 不是正式静态资源前缀。 | -| `/generated-world*` | 测试数据或对象 ID | 不是正式静态资源前缀。 | -| `/generated-ruins*` | 测试数据或对象 ID | 不是正式静态资源前缀。 | - -结论: - -1. 后续做 Axum 静态资源兼容层时,只能以第 2 节和第 3 节里的 `6` 个前缀为正式基线。 -2. 不能把测试里的字符串误判成真实资源入口。 - -## 5. 与重写设计的对齐要求 - -为避免后续按错路径重写,当前冻结以下对齐规则: - -1. Axum 第一阶段必须兼容这 `6` 个公开资源前缀。 -2. OSS 内部对象键可以调整,但对前端暴露的 URL 前缀不能少于这 `6` 个。 -3. 设计文档里的对象键规划如果与这份基线冲突,以这份“当前工程冻结基线”为准,然后再在设计文档中补兼容说明。 - -## 6. 本轮冻结后的硬约束 - -后续迁移中,不允许出现以下情况: - -1. 把 `/generated-animations/*` 直接并入 `/generated-characters/*` 却不保留兼容别名。 -2. 把 `/generated-custom-world-covers/*` 擅自改成 `/generated-cover-images/*`。 -3. 在未做兼容层前,删除 `/generated-character-drafts/*` 或 `/generated-qwen-sprites/*` 的草稿访问习惯。 -4. 仅因为某个目录在当前仓库里尚未生成,就把它从正式前缀清单里删掉。 - -## 7. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. 当前正式 `/generated-*` 前缀已经有书面冻结清单。 -2. 每个前缀都已明确: - - 当前用途 - - 当前生产链路 - - 当前路径模板 - - 后续兼容要求 -3. 已明确哪些“generated-*”字符串只是噪音,不属于正式前缀。 - -## 8. 后续直接依赖这份基线的任务 - -1. 设计 Axum 静态资源兼容层 -2. 设计 OSS 对象键与 CDN 别名 -3. 做 assets / custom world / editor 的路径回归测试 diff --git a/backend-rewrite-tasklist/M0_MODULE_MIGRATION_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_MODULE_MIGRATION_BASELINE_2026-04-20.md deleted file mode 100644 index 7af9e7f9..00000000 --- a/backend-rewrite-tasklist/M0_MODULE_MIGRATION_BASELINE_2026-04-20.md +++ /dev/null @@ -1,291 +0,0 @@ -# M0:内部模块迁移归属基线 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json) -- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第三条任务: - -- 整理当前 `12` 个内部模块并锁定迁移归属 - -这里的“迁移归属”不是简单把旧目录名照搬到 Rust,而是要求后续重写必须先明确: - -1. 每个旧模块在新架构中的主归属 crate。 -2. 每个旧模块是否需要拆成“SpacetimeDB 状态层 + Axum/application 编排层”。 -3. 每个旧模块的状态真相应该进入 `SpacetimeDB`、`OSS` 还是开发态本地文件适配。 -4. 每个旧模块优先落在哪个迁移阶段,避免后续任务拆分时反复改口径。 - -命名补充说明: - -1. 本文中仍出现的 `application::...`、`auth-service`、`oss-service`、`llm-service` 等名称,统一表示逻辑职责,不再要求它们必须继续作为顶层独立 crate 存在。 -2. 在新的多 crate 结构下,这些逻辑职责默认落到对应 `crates/module-*` 的内部子层次,或落到 `crates/platform-*`、`crates/shared-*` 等共享 crate 中。 - -补充边界: - -1. 本文只覆盖当前 `server-node/src/modules/*` 下的 `12` 个内部模块。 -2. `auth`、`health` 虽然属于后端能力面,但不在这 `12` 个内部模块目录里,因此不在本文表内。 - -## 2. 冻结结论 - -当前 Node 后端的正式内部模块固定为以下 `12` 个: - -补充口径: - -1. 上表 `12` 个模块属于历史基线总量。 -2. 自 `2026-04-21` 起,本轮 Rust 后端重写的 active rewrite modules 固定为 `11` 个。 -3. `editor` 作为遗留无用模块,仅保留历史事实对照,不再进入 `server-rs` 主线迁移。 - -| 模块 ID | 中文名称 | 当前目录 | 关联路由数 | 当前对外暴露面 | 重写后主归属 | 重写后次归属 | 目标迁移阶段 | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `ai` | AI 编排模块 | `server-node/src/modules/ai` | `23` | `runtime-main` | `application + llm-service` | `contracts + api-server SSE facade` | `M4`、`M5`、`M6` | -| `assets` | 资产工具模块 | `server-node/src/modules/assets` | `18` | `assets` | `application::assets + oss-service` | `spacetime-module::asset_metadata` | `M6` | -| `combat` | 战斗结算模块 | `server-node/src/modules/combat` | `1` | `runtime-story-action` | `spacetime-module::gameplay::combat` | `domain::combat` | `M4` | -| `custom-world` | 自定义世界运行时模块 | `server-node/src/modules/custom-world` | `26` | `runtime-main` | `spacetime-module::custom_world + application::custom_world` | `llm-service + oss-service` | `M5` | -| `editor` | 编辑器资源模块 | `server-node/src/modules/editor` | `3` | `editor` | `不迁移(遗留模块)` | 保留 `server-node/` 历史链路对照 | `不纳入本轮` | -| `inventory` | 背包与物品变更模块 | `server-node/src/modules/inventory` | `1` | `runtime-story-action` | `spacetime-module::gameplay::inventory` | `domain::inventory` | `M4` | -| `npc` | NPC 交互模块 | `server-node/src/modules/npc` | `6` | `runtime-story-action`、`runtime-main` | `spacetime-module::gameplay::npc` | `application::npc_dialogue + llm-service` | `M4`、`M5` | -| `progression` | 成长与关卡进程模块 | `server-node/src/modules/progression` | `3` | `runtime-story-action`、`runtime-main` | `spacetime-module::gameplay::progression` | `domain::progression` | `M3`、`M4` | -| `quest` | 任务运行时模块 | `server-node/src/modules/quest` | `4` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::quest` | `application::quest_drafting + llm-service` | `M3`、`M4` | -| `runtime` | 运行时状态基座模块 | `server-node/src/modules/runtime` | `32` | `runtime-main`、`runtime-story-action` | `spacetime-module::runtime` | `application::runtime_facade` | `M3` | -| `runtime-item` | 运行时物品模块 | `server-node/src/modules/runtime-item` | `2` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::runtime_item` | `application::item_intent + llm-service` | `M4` | -| `story` | 故事会话模块 | `server-node/src/modules/story` | `10` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::story` | `application::story_facade + api-server SSE facade` | `M4` | - -冻结总数: - -1. 历史内部模块目录:`12` -2. 本轮 active rewrite modules:`11` -3. 关联路由数最多的模块:`runtime`,共 `32` 条 -4. 本轮纯外部副作用导向模块:`ai`、`assets` -5. 已退出本轮重写范围的遗留模块:`editor` -6. 纯状态规则导向模块:`combat`、`inventory` -7. 需要“状态层 + 编排层”双落位的混合模块:`custom-world`、`npc`、`quest`、`runtime-item`、`story` - -## 3. 锁定迁移归属规则 - -后续所有重写实现,必须先遵守以下归属规则: - -1. 纯运行时状态、纯规则计算、纯领域变更,优先进入 `spacetime-module/` 与 `domain/`,不能继续把真相留在 Axum 内存或 Node 风格 service。 -2. 外部模型调用、OSS 上传、短信、微信、本地文件读写,统一放在 `application/ + api-server/ + infra service`,不能塞进 SpacetimeDB reducer。 -3. 任何当前“一个模块同时做状态和副作用”的能力,在新架构里都必须拆成: - - `SpacetimeDB`:状态真相与读模型 - - `Axum/application`:外部编排、SSE、对象上传、三方调用 -4. `public/generated-*` 不再是任何模块的真相源;未来只能作为兼容访问前缀或 CDN 映射。 -5. 不允许把旧模块简单合并成一个“大 runtime service”;必须保留可对照的领域边界。 - -## 4. 模块迁移矩阵 - -| 当前模块 | 当前职责摘要 | 新状态真相源 | 新外部副作用归属 | 迁移后必须落位 | -| --- | --- | --- | --- | --- | -| `ai` | prompt 编排、聊天/剧情/世界生成组织 | `SpacetimeDB` 只存任务和结果引用,不存编排过程真相 | `llm-service` | 只能留在 Axum/application 侧,禁止直接进 reducer。 | -| `assets` | 生成、发布、缓存、Qwen 精灵表 | `asset_job`、`asset_object`、`asset_manifest` 等表 | `oss-service` + 外部媒体模型 | 二进制进 OSS,任务与引用进 SpacetimeDB。 | -| `combat` | 战斗结算、数值变化 | `battle_state`、`story_event` | 无 | 作为纯 reducer 规则模块落到 gameplay。 | -| `custom-world` | 世界资料、问答流、Agent 草稿与编译 | `custom_world_*` 系列表 | `llm-service`、`oss-service` | 世界状态在 SpacetimeDB,编译/生成在 Axum。 | -| `editor` | 编辑器 JSON 读写、图标枚举 | 仍以遗留 Node 链路与开发态本地文件为历史对照 | 不迁移到 `server-rs` | 仅保留历史基线,不纳入本轮 Rust 重写。 | -| `inventory` | 背包变更、物品副作用、NPC 背包交互 | `inventory_slot`、`story_event` | 无 | 归入 story action 对应 reducer。 | -| `npc` | 互动规则、关系变化、招募/对话语义 | `npc_state`、`story_event` | `application::npc_dialogue + llm-service` | 状态归 SpacetimeDB,台词生成归 Axum。 | -| `progression` | 等级、章节、敌对 scaling、benchmark | `player_progression`、`chapter_progression` | 无 | 作为 runtime / story 公共领域模块进入 SpacetimeDB。 | -| `quest` | 任务意图、日志、进度变化 | `quest_record`、`story_event` | `application::quest_drafting + llm-service` | 任务状态归 SpacetimeDB,生成型任务草案归 Axum。 | -| `runtime` | 快照、设置、资料页、状态归一化 | `runtime_snapshot`、`runtime_setting`、`profile_*` | 无 | 作为新后端最先迁移的状态基座模块。 | -| `runtime-item` | 物品意图、奖励解析、宝藏逻辑 | `treasure_record`、`inventory_slot`、`story_event` | `application::item_intent + llm-service` | 奖励结算归 reducer,意图生成归 Axum。 | -| `story` | 会话状态、动作分发、主循环 | `story_session`、`story_event` | `application::story_facade + SSE` | 主循环状态归 SpacetimeDB,流式输出由 Axum 兼容。 | - -## 5. 各模块冻结要求 - -### 5.1 `ai` - -当前定位: - -1. 负责剧情、多轮聊天、自定义世界等 prompt 编排。 -2. 自身不负责持久化,但会被多条 runtime 路由反复调用。 - -重写后的冻结要求: - -1. 主归属固定为 `application + llm-service`,不是 `spacetime-module`。 -2. 后续如果需要记录 AI 阶段状态,只能把任务状态或结果引用写入 SpacetimeDB,不把供应商 SDK 与 prompt 执行放进 reducer。 -3. 与 `story`、`custom-world`、`runtime-item`、`quest` 的关系固定为“它们产生命令,`ai` 负责外部生成”。 - -### 5.2 `assets` - -当前定位: - -1. 负责角色形象、动作、Qwen 精灵表生成。 -2. 负责发布到 `public/generated-*` 与局部 manifest。 - -重写后的冻结要求: - -1. 二进制对象一律进入 `OSS`。 -2. 主归属固定为 `application::assets + oss-service`。 -3. 资产任务状态、对象引用关系、发布绑定关系必须进入 `spacetime-module::asset_metadata`。 -4. 后续不允许继续以本地 `public/generated-*` 是否存在文件作为业务真相。 - -### 5.3 `combat` - -当前定位: - -1. 提供 story action 里的战斗型结算。 -2. 本质是纯规则计算。 - -重写后的冻结要求: - -1. 主归属固定为 `spacetime-module::gameplay::combat`。 -2. 不单独拥有 HTTP 路由,也不直接依赖外部 IO。 -3. 后续实现必须保持纯规则、可测试、可被 `resolve_story_action` reducer 复用。 - -### 5.4 `custom-world` - -当前定位: - -1. 负责 creator intent、world profile、传统问答流、Agent 运行时类型。 -2. 同时牵涉世界编译、资产生成和公开画廊。 - -重写后的冻结要求: - -1. 这是标准的双落位模块: - - `SpacetimeDB` 保存会话、草稿、作品、画廊、Agent 状态。 - - `Axum/application` 负责编译、SSE、外部 LLM 与资产生成编排。 -2. 传统问答流和 Agent 流必须拆表,不能继续长期混成一个大 JSON 会话体。 -3. 对外仍然要兼容当前 `/api/custom-world/*` 与 `/api/runtime/custom-world/*` 访问习惯。 - -### 5.5 `editor` - -当前定位: - -1. 读写编辑器资源 JSON。 -2. 枚举工作区 `public/Icons` 图标资源。 - -重写后的冻结要求: - -1. 该模块在 `server-node/` 中的存在事实继续保留,用于历史基线与后续清理对照。 -2. 自 `2026-04-21` 起,不再为 `server-rs/` 创建 `module-editor` crate,也不再把它纳入 `M1 ~ M6` 主线迁移。 -3. 若未来仍需清理或替代 editor,必须在遗留链路依赖确认后单独立项,不能夹带进当前 Rust 重写主链。 -4. 不允许为了简化本轮任务而篡改其历史存在事实。 - -### 5.6 `inventory` - -当前定位: - -1. 负责背包变更、赠礼、NPC 背包交互等副作用。 -2. 当前主要被 story action 调用。 - -重写后的冻结要求: - -1. 主归属固定为 `spacetime-module::gameplay::inventory`。 -2. 与 `story`、`runtime-item` 的交互必须通过 reducer 协调,不能回到“多个 service 各自改 JSON”。 -3. 后续如需对外展示背包读模型,优先通过 view 暴露,不新增独立真相副本。 - -### 5.7 `npc` - -当前定位: - -1. 负责 NPC 关系、招募、交互规则与场景 NPC 语义。 -2. 同时参与 runtime 聊天流和 story action 结算。 - -重写后的冻结要求: - -1. 状态真相固定在 `spacetime-module::gameplay::npc`。 -2. LLM 对话、招募话术、流式文本输出固定由 `application::npc_dialogue + llm-service` 处理。 -3. 不允许把 NPC 状态又分散回聊天 session store、本地缓存或前端临时状态。 - -### 5.8 `progression` - -当前定位: - -1. 负责角色成长、章节推进、敌对强度等规则。 -2. 同时影响 snapshot hydrate 与 story action 结算。 - -重写后的冻结要求: - -1. 主归属固定为 `spacetime-module::gameplay::progression`。 -2. 仍保持纯规则、纯领域建模,不承接外部 IO。 -3. 作为 `runtime` 与 `story` 的公共领域组件,不能被重新塞回单一路由 handler。 - -### 5.9 `quest` - -当前定位: - -1. 负责任务语义、任务日志、任务进度信号。 -2. 既参与 AI 草案生成,也参与 story action 结算。 - -重写后的冻结要求: - -1. 任务主状态固定进入 `spacetime-module::gameplay::quest`。 -2. AI 生成的任务候选与草案编排固定由 `application::quest_drafting + llm-service` 承担。 -3. 前端兼容接口仍走 `/api/runtime/quests/*` 或 story action 聚合,不新增前端直连任务状态写入口。 - -### 5.10 `runtime` - -当前定位: - -1. 是当前运行时快照、设置、资料页、状态归一化的基座模块。 -2. 路由覆盖最广,是 Node 版后端迁移的第一主战场。 - -重写后的冻结要求: - -1. 主归属固定为 `spacetime-module::runtime`。 -2. `runtime_snapshot`、`runtime_setting`、`profile_*` 等读写模型优先在 `M3` 完成迁移。 -3. Axum 只保留兼容 facade,不再继续让快照真相停留在 PostgreSQL 风格 repository。 - -### 5.11 `runtime-item` - -当前定位: - -1. 负责运行时物品意图、奖励、宝藏解析与剧情指纹。 -2. 同时受到 AI 生成与 story action 结算驱动。 - -重写后的冻结要求: - -1. 奖励、掉落、宝藏等状态变化固定进入 `spacetime-module::gameplay::runtime_item`。 -2. 物品意图生成固定由 `application::item_intent + llm-service` 承接。 -3. 物品领域不能再拆成“部分在 story、部分在 route、部分在前端”的临时实现。 - -### 5.12 `story` - -当前定位: - -1. 负责运行时故事会话、动作分发与 state 恢复。 -2. 当前既暴露 REST,也暴露与聊天/继续剧情相关的流式体验。 - -重写后的冻结要求: - -1. 主归属固定为 `spacetime-module::gameplay::story`。 -2. SSE 输出与兼容 DTO 拼装固定由 `application::story_facade + api-server SSE facade` 负责。 -3. `storyAction.resolve` 的跨模块联动必须以 `story` 为编排入口,但不再由单个 Node service 直接改整包 JSON。 - -## 6. 本轮冻结后的硬约束 - -后续迁移中,不允许出现以下情况: - -1. 把 `ai`、`assets` 直接放进 SpacetimeDB reducer 执行三方网络或文件系统 IO。 -2. 在未单独立项前,把已退出本轮范围的 `editor` 重新并回 `server-rs` 主链。 -3. 把 `combat`、`inventory`、`progression` 重新做成只存在于 Axum handler 内部的计算 helper。 -4. 把 `custom-world`、`story`、`npc` 这类混合模块继续保留为“单大对象 JSON + 单大 service 写回”模式。 -5. 把 `runtime` 当成一个兜底垃圾桶,把其他领域模块重新并回去。 -6. 在没有对应 Axum facade 的前提下,让前端第一阶段直接依赖 SpacetimeDB 原生写接口。 - -## 7. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. 当前历史 `12` 个内部模块已经有正式书面冻结清单。 -2. 每个模块都已明确: - - 当前目录 - - 关联路由数 - - 对外暴露面 - - 重写后主归属 - - 重写后次归属 - - 目标迁移阶段 -3. 本轮 active rewrite modules 为 `11` 个,且 `editor` 的遗留/不迁移口径已经冻结。 -4. 后续拆 `server-rs/` 多 crate、建 SpacetimeDB bounded context、排 M3~M6 任务时,可以直接引用本文,不再靠口头记忆。 - -## 8. 后续直接依赖这份基线的任务 - -1. 设计 `server-rs/` workspace 与 crate 边界 -2. 设计 SpacetimeDB `runtime / gameplay / custom_world / asset_metadata` 表分层 -3. 设计 story action reducer 的跨模块协作边界 -4. 设计 custom world / assets 的 Axum facade diff --git a/backend-rewrite-tasklist/M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md b/backend-rewrite-tasklist/M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md deleted file mode 100644 index 58fb3605..00000000 --- a/backend-rewrite-tasklist/M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md +++ /dev/null @@ -1,106 +0,0 @@ -# M0:阶段验收矩阵 - -日期:`2026-04-20` - -依据来源: - -- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md) -- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md) -- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md) -- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md) -- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md) -- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md) -- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md) -- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md) -- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) - -## 1. 文档目的 - -这份文档用于把 `M0 ~ M7` 各阶段的入口条件、核心交付、退出条件与回归焦点固定下来,避免后续出现“任务勾完了,但阶段是否真的可进入下一步没有统一标准”的问题。 - -从本文件开始,后续每一阶段都需要按“入口满足 -> 交付完成 -> 验收通过 -> 留存证据”的顺序推进。 - -## 2. 阶段推进总规则 - -1. 未满足上一阶段退出条件前,不进入下一阶段主线编码。 -2. 每一阶段至少保留一份可复查的证据,证据可以是文档、脚本、测试结果或回归记录。 -3. 所有阶段都必须持续对齐当前冻结基线: - - 历史基线 `6` 个挂载面 - - 本轮 active rewrite target `5` 个挂载面 - - `96` 条路由 - - `12` 个模块 - - `6` 条 SSE 接口 - - `6` 个 `/generated-*` 静态资源前缀 - - 前端直接依赖的响应头、envelope 与鉴权错误格式 -4. 任一阶段若引入新的协议差异,必须先补 contract 文档或迁移说明,再允许继续编码。 - -## 3. 分阶段验收矩阵 - -| 阶段 | 入口条件 | 核心交付 | 退出条件 | 回归焦点 | -| --- | --- | --- | --- | --- | -| `M0` 冻结能力与边界 | 已完成当前 Node 后端摸底;已明确目标架构为 `SpacetimeDB + Axum + 阿里云 OSS` | 冻结能力基线、路由矩阵、模块归属、SSE、静态资源前缀、前端响应契约、仓库边界决议、阶段验收矩阵 | `6` 个挂载面、`96` 条路由、`12` 个模块、`6` 条 SSE、`6` 个静态资源前缀全部形成书面基线;`server-rs/`、`server-node/`、Axum 边界、副作用收口原则全部冻结 | 文档口径一致性;前端 contract 依赖项是否被遗漏;迁移阶段是否还存在多套边界说法 | -| `M1` Rust 工作区与 Axum 基础设施 | `M0` 全部退出条件满足 | `server-rs/` workspace、`crates/api-server`、`crates/spacetime-module`、独立模块 crates、统一配置、日志、request id、中间件、response envelope、`/healthz`、开发脚本 | Axum 可独立启动;`/healthz` 与当前工程兼容;`x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 行为稳定;workspace 完整编译通过;主工程与模块 crate 引用边界稳定 | 基础头部兼容;健康检查兼容;目录结构与 crate 归属是否偏离 `M0` 决议 | -| `M2` 鉴权、会话、JWT 与 refresh cookie | `M1` 已可稳定启动;Axum 中间件与配置链可用 | 身份表、会话表、JWT claims、refresh cookie、密码登录、手机验证码登录、微信登录、OIDC 透传、旧鉴权接口兼容 | 密码登录、refresh cookie、手机验证码、微信登录主链可用;旧鉴权接口 contract 回归通过;SpacetimeDB 可识别 Axum 签发身份 | Cookie 与 JWT 兼容;`CAPTCHA_REQUIRED` 与 `details.captchaChallenge` 是否保持;登录态吊销与刷新是否稳定 | -| `M3` runtime snapshot / settings / profile | `M2` 鉴权稳定;用户身份可透传到 SpacetimeDB | `runtime_snapshot`、`runtime_setting`、profile 相关主表与 facade;存档、设置、浏览历史、save archive 兼容接口 | 登录用户可正常保存、读取、删除存档;profile dashboard / browse history / save archive 行为一致;前端恢复流程可直接跑通 | 快照恢复准确性;兼容路径与主路径是否返回一致;历史记录排序与去重逻辑 | -| `M4` story action 与 gameplay reducer | `M3` 快照与用户状态主链稳定 | story / combat / inventory / npc / quest / progression / runtime-item 表与 reducer;story 兼容接口与 view model | 前端 story 主循环可用;`story state` 恢复链可用;NPC / quest / treasure / combat 主循环行为不回退;旧 Node story route 回归平移完成 | `RuntimeStoryActionResponse` 结构;战斗与奖励联动;状态投影是否与旧前端恢复逻辑一致 | -| `M5` custom world / gallery / agent | `M4` story 与 runtime 真相源已稳定;SSE facade 可持续输出 | custom world 主表、agent 会话拆表、传统问答流、library / gallery、agent 消息与操作、LLM/图片生成编排 | 传统 custom world 主链可用;library / gallery 主链可用;agent 主链可用;会话不再依赖单大 JSON 体 | SSE 事件格式;卡片、消息、操作状态一致性;世界草稿编译与发布链是否可回放 | -| `M6` assets / OSS | `M5` 世界与角色主链稳定;Axum 应用层可承接外部副作用 | OSS 对象键规范、上传签名、对象确认、资产任务表、角色/场景/Qwen 资产迁移、旧静态路径兼容 | 所有新生成资产写入 OSS;前端仍能通过旧路径习惯访问资源;资产任务状态可查询 | `/generated-*` 路径兼容;OSS 元数据与对象绑定关系;资产任务链状态一致性 | -| `M7` 联调、回归、部署与切流 | `M6` 已具备主链闭环;双栈对照条件具备 | 测试体系、部署方案、观测能力、灰度切流方案、回退方案、对比脚本与 smoke 清单 | 全链路 smoke 通过;主流程回归通过;关键 SSE 联调通过;可在灰度环境切流并可回退 | 双跑窗口稳定性;API 对比结果;切流开关、回退开关、观测告警是否齐备 | - -## 4. M0 冻结项专用验收清单 - -只有以下项目全部满足,`M0` 才算真正完成: - -1. 已产出以下冻结文档: - - [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md) - - [M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md) - - [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md) - - [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md) - - [M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md) - - [M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md) - - [M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md) - - [M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md) -2. 已书面冻结以下核心数字: - - 挂载面:`6` - - 路由:`96` - - 模块:`12` - - SSE:`6` - - 静态资源前缀:`6` -3. 已书面冻结以下边界决议: - - 新 Rust 后端固定为仓库根目录 `server-rs/` - - 迁移期保留 `server-node/` - - 前端 `M0 ~ M6` 期间只访问 Axum - - 外部副作用统一收口在 Axum - - `server-rs/` 内部采用 `crates/*` 多 crate 组织 - - `editor` 已于 `2026-04-21` 退出本轮 Rust 重写范围 -4. `M1` 以后任何任务引用路由、模块、SSE、静态资源与响应契约时,都必须能追溯到本阶段产出的冻结文档。 - -## 5. 跨阶段回归维度 - -无论执行到哪个阶段,都要持续检查以下维度: - -| 维度 | 必查内容 | 最晚必须固化的证据 | -| --- | --- | --- | -| 路由兼容 | 旧路由是否已有新实现或明确替代路径 | 路由迁移矩阵、API 对比脚本、contract 回归记录 | -| SSE 兼容 | 事件名、事件顺序、结束事件、错误事件是否保持兼容 | SSE 基线文档、联调记录、smoke 结果 | -| 静态资源兼容 | `/generated-*` 是否可继续访问,是否正确指向 OSS/CDN | 静态资源前缀基线、路径兼容测试记录 | -| 鉴权兼容 | JWT、refresh cookie、验证码、微信登录、风控错误是否保持兼容 | 鉴权接口回归记录、claims 设计文档、集成测试 | -| 前端 contract | 响应头、envelope、错误结构是否稳定 | response contract 基线、接口测试、前端联调记录 | -| 切流回退 | 双栈是否可对照,是否具备回退能力 | `M7` 对比脚本、灰度清单、回退方案 | - -## 6. 阶段证据留存要求 - -每个阶段完成时,至少要补齐以下其中两类证据: - -1. 文档: - - 更新任务清单勾选状态 - - 更新设计文档或阶段落地记录 -2. 测试或脚本: - - 新增或更新 smoke / contract / integration 测试 - - 新增对比脚本、发布脚本或回归脚本 -3. 结果记录: - - 编码检查结果 - - 关键命令执行结果 - - 联调、回归、灰度演练结果 - -如果阶段只完成了编码、但没有文档和证据留存,则该阶段不能视为完成。 diff --git a/backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md b/backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md deleted file mode 100644 index 0cdd553d..00000000 --- a/backend-rewrite-tasklist/M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md +++ /dev/null @@ -1,281 +0,0 @@ -# M0:仓库边界决议 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) -- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md) -- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md) - -## 1. 文档目的 - -这份文档用于持续冻结 `M0` 中与仓库边界直接相关的决策,避免进入 `M1` 后再反复改目录、改职责口径。 - -当前已确认的事项会持续在这份文档上追加维护,后续若再有新的边界冻结结论,也统一收口到这里。 - -## 2. 边界决议状态 - -| 事项 | 当前状态 | 当前结论 | -| --- | --- | --- | -| Rust 后端新目录名与根目录落位方案 | 已确认 | 新 Rust 后端固定为仓库根目录下的 `server-rs/`,与 `server-node/` 同级。 | -| 旧 `server-node/` 在迁移期继续保留,不提前删除 | 已确认 | `server-node/` 在 `M0 ~ M6` 期间持续保留,直到 `M7` 切流与回退验证完成后再评估清理。 | -| 前端第一阶段仍然只访问 Axum,不直连 SpacetimeDB | 已确认 | `M0 ~ M6` 前端统一只访问 Axum 暴露的 `/api/*`、`/healthz`、SSE 与静态资源兼容层,不新增直连 SpacetimeDB 原生协议路径。 | -| 外部副作用统一收口在 Axum,不放进 SpacetimeDB 模块 | 已确认 | OSS、LLM、短信、微信 OAuth、本地文件系统等外部副作用统一落在 Axum/application/infra,不进入 SpacetimeDB reducer/module。 | -| `server-rs/` 内部采用多 crate 组织,由主工程 crate 统一引用模块 crate | 已确认 | `server-rs/` 采用 `crates/*` 工作区结构,`crates/api-server` 与 `crates/spacetime-module` 作为主工程 crate,独立模块以 `crates/module-*` 形式被主工程 crate 引用。 | -| `editor` 为遗留无用模块,不纳入 `server-rs` 本轮重写范围 | 已确认 | `server-node/src/modules/editor` 与 `/api/editor/*` 仅作为历史基线保留对照;自 `2026-04-21` 起退出本轮 Rust 后端重写范围。 | - -## 3. 已确认决议一:`server-rs/` 固定落在仓库根目录 - -### 3.1 决议内容 - -本次重写固定采用以下仓库落位: - -1. 新后端目录名固定为 `server-rs/` -2. 目录位置固定在仓库根目录 -3. 与以下目录保持同级: - - `server-node/` - - `src/` - - `docs/` - - `packages/` - -目标形态: - -```text -Genarrative/ -├─ server-node/ -├─ server-rs/ -├─ src/ -├─ packages/ -├─ docs/ -└─ backend-rewrite-tasklist/ -``` - -### 3.2 不采用的落位方案 - -以下方案当前明确不采用: - -1. 不放进 `server-node/` 子目录中做“Node + Rust 混编后端”。 -2. 不放进 `packages/`,避免被前端 package/workspace 语义误导。 -3. 不使用过于泛化的根目录名如 `server/`、`backend/`,避免和当前 `server-node/` 职责混淆。 - -### 3.3 这样落位的原因 - -1. 与当前重写设计文档、任务清单、后续 `M1` 多 crate 规划保持一致。 -2. 允许 `server-node/` 与 `server-rs/` 在迁移期并行存在,便于逐阶段切流。 -3. 让 Rust 工作区边界清晰,不污染现有前端 `src/`、`packages/`、Vite 工具链。 -4. 后续新增 `server-rs/scripts/*`、`Cargo.toml`、`crates/*` 时路径最直接,不需要额外中间层。 - -### 3.4 对后续任务的直接约束 - -从这一条决议开始,后续任务必须统一按以下路径落位: - -1. `M1` 的工作区初始化在 `server-rs/` -2. Axum 主工程 crate 在 `server-rs/crates/api-server` -3. SpacetimeDB 主工程 crate 在 `server-rs/crates/spacetime-module` -4. 独立模块 crate 在 `server-rs/crates/module-*` -5. 相关脚本在 `server-rs/scripts/` - -## 4. 本条任务完成定义 - -当以下条件成立时,这一条边界任务视为完成: - -1. 新 Rust 后端目录名已经书面固定为 `server-rs/` -2. 目录位置已经书面固定为仓库根目录 -3. 后续 `M1` 的工作区初始化不会再出现 `server-rs/`、`backend-rs/`、`server/` 等多套候选名并存 - -## 5. 已确认决议二:迁移期保留 `server-node/` - -### 5.1 决议内容 - -在本次重写迁移期内,旧 `server-node/` 固定继续保留,不提前删除、不整体挪位、不提前做破坏性收缩。 - -保留周期固定为: - -1. `M0` 到 `M6` 全阶段 -2. 至少持续到 `M7` 的以下条件全部满足之后,才允许评估清理: - - 新后端已切流 - - 旧接口 contract 回归通过 - - 关键主链 smoke 通过 - - 已具备明确回退方案 - -### 5.2 保留它的原因 - -1. 旧 `server-node/` 是当前 `96` 条路由、`6` 个挂载面、`12` 个模块的真实对照实现。 -2. 前面已经冻结的路由矩阵、模块迁移清单、SSE 协议、静态资源前缀,都需要它作为回归对照源。 -3. 如果在 `M1 ~ M6` 提前删除旧实现,就会失去最可靠的回退锚点与 diff 基准。 - -### 5.3 迁移期允许做什么 - -迁移期内允许: - -1. 在 `server-rs/` 中逐步补等价实现。 -2. 在文档和 manifest 中继续引用 `server-node/` 作为当前系统基线。 -3. 在必要时从 `server-node/` 补测试样例、补协议对照、补回归夹具。 - -迁移期内不允许: - -1. 提前整体删除 `server-node/` -2. 把 `server-node/` 改成只剩空壳目录 -3. 在还没切流前,把旧服务关键实现批量迁走导致无法对照 - -### 5.4 对后续任务的直接约束 - -从这一条决议开始,后续任务必须遵守: - -1. `M1` 搭建 `server-rs/` 时,不改动 `server-node/` 的存在性。 -2. `M2 ~ M6` 迁移功能时,旧 `server-node/` 继续作为验收基线与回退锚点。 -3. 真正评估清理旧 Node 后端的动作,只能放到 `M7` 切流完成之后。 - -## 6. 已确认决议三:前端第一阶段只访问 Axum - -### 6.1 决议内容 - -在 `M0 ~ M6` 迁移期内,前端访问新后端的唯一入口固定为 Axum。 - -第一阶段允许前端继续访问的面固定为: - -1. `/api/*` -2. `/healthz` -3. 当前已冻结的 SSE 路由 -4. 当前已冻结的 `/generated-*` 静态资源兼容前缀 - -第一阶段明确不做的事: - -1. 不让 Web 前端直接接 SpacetimeDB 原生 HTTP 接口。 -2. 不让 Web 前端直接接 SpacetimeDB 订阅协议。 -3. 不要求前端新增一套“Axum + SpacetimeDB 双后端并行直连”调用模式。 - -### 6.2 这样决议的原因 - -1. 当前前端已经直接依赖现有 `/api/*` 路由、response envelope、SSE、`/generated-*` 路径习惯。 -2. 如果在第一阶段就让前端同时认识 Axum 与 SpacetimeDB,会把迁移面从“后端平移”扩大成“前后端协议双重重写”。 -3. Axum 需要承担统一鉴权、cookie、JWT、OSS 签名、错误格式与 contract 兼容职责,这些都不应分散到前端直连多个后端协议。 - -### 6.3 对后续任务的直接约束 - -从这一条决议开始,后续任务必须遵守: - -1. `M1 ~ M2` 的 Axum 中间件与鉴权必须先跑通,再谈前端联调。 -2. `M3 ~ M6` 新增的 SpacetimeDB reducer/view 先通过 Axum facade 暴露,不直接要求前端改成原生 SpacetimeDB 客户端。 -3. 若后续要让前端直连 SpacetimeDB,只能作为第二阶段优化事项,不能混入当前重写主链。 - -## 7. 已确认决议四:外部副作用统一收口在 Axum - -### 7.1 决议内容 - -本次重写固定采用以下边界: - -1. `SpacetimeDB` 只负责状态、规则、reducer、view、订阅读模型。 -2. `Axum/application/infra` 统一负责所有外部副作用。 - -固定收口到 Axum 的外部副作用包括: - -1. 阿里云 OSS 上传、下载、签名、直传凭证 -2. DashScope / Ark / 其他 LLM 请求 -3. 微信 OAuth -4. 手机验证码短信发送与校验编排 -5. 本地文件系统读写 - -### 7.2 明确不允许放进 SpacetimeDB 的内容 - -以下能力当前明确禁止进入 `spacetime-module/`: - -1. 直接发 HTTP 请求给第三方供应商 -2. 直接访问 OSS SDK -3. 直接读写本地磁盘 -4. 直接处理 Cookie、回调跳转、multipart 上传 -5. 直接承担供应商重试、熔断、超时与日志策略 - -### 7.3 这样决议的原因 - -1. 这些能力都强依赖 HTTP 头、Cookie、SDK、签名、超时与日志,不适合绑进 SpacetimeDB 模块发布周期。 -2. 当前前端 contract、鉴权、SSE、静态资源兼容都要求一个稳定的 HTTP 边界层,Axum 更适合承担这个角色。 -3. 把副作用统一收口到 Axum,才能让 SpacetimeDB 保持“状态机真相源”的纯度。 - -### 7.4 对后续任务的直接约束 - -从这一条决议开始,后续任务必须遵守: - -1. `M1` crate 设计时,`platform-oss`、`platform-llm`、`platform-auth` 固定属于 Axum / 模块应用层一侧。 -2. `M2 ~ M6` 设计 reducer 时,只写状态变更,不直接发外部请求。 -3. 若确实需要异步副作用,也必须由 Axum worker 或应用层作业执行,再把结果回写 SpacetimeDB。 - -## 8. 已确认决议五:`server-rs/` 内部采用多 crate 组织 - -### 8.1 决议内容 - -从当前版本开始,`server-rs/` 内部结构固定采用: - -1. `crates/*`:统一收口主工程 crate、独立模块 crate 与共享 crate -2. `scripts/*`:开发、发布、回归脚本 - -主工程 crate 固定包含: - -1. `crates/api-server` -2. `crates/spacetime-module` - -独立模块 crate 固定按“每个独立模块一个 crate”推进,至少覆盖: - -1. `crates/module-auth` -2. `crates/module-runtime` -3. `crates/module-story` -4. `crates/module-combat` -5. `crates/module-inventory` -6. `crates/module-npc` -7. `crates/module-progression` -8. `crates/module-quest` -9. `crates/module-runtime-item` -10. `crates/module-custom-world` -11. `crates/module-assets` -12. `crates/module-ai` - -跨模块共享 crate 固定包含: - -1. `crates/shared-contracts` -2. `crates/shared-kernel` -3. `crates/platform-auth` -4. `crates/platform-oss` -5. `crates/platform-llm` -6. `crates/spacetime-client` -7. `crates/tests-support` - -### 8.2 这样决议的原因 - -1. 用户已经明确要求后端采用 Rust workspace 下的多 crate 模式,独立模块不能继续堆回单个技术层大包。 -2. 当前后端已有 `12` 个内部模块边界,多 crate 方案更容易保持一一映射与独立演进。 -3. `crates/api-server` 与 `crates/spacetime-module` 只做组合与发布,更符合“主工程 crate 引用模块 crate”的组织方式。 - -### 8.3 对后续任务的直接约束 - -从这一条决议开始,后续任务必须遵守: - -1. `M1` 及后续目录任务统一按 `crates/*` 执行,不再保留 `apps/*` 与 `packages/*` 并行规划。 -2. 每个业务模块默认先有自己的 workspace crate,再由主工程 crate 引用。 -3. 只有共享 contract、共享领域内核、平台适配、SpacetimeDB client 这类跨模块能力,才允许使用共享 crate,而不是业务模块混装。 - -## 9. 已确认决议六:`editor` 退出本轮 Rust 重写范围 - -### 9.1 决议内容 - -`editor` 在当前 Node 后端中确实存在真实模块与真实挂载面,但已于 `2026-04-21` 被确认为遗留无用模块,不再纳入本轮 `server-rs/` 重写主链。 - -当前固定口径为: - -1. 历史基线继续保留 `server-node/src/modules/editor` 与 `/api/editor/*` 的存在事实。 -2. `server-rs/` 不再保留 `crates/module-editor`。 -3. `M1 ~ M6` 的主线任务、阶段验收与 crate 规划,不再把 `editor` 计入 active rewrite scope。 - -### 9.2 这样决议的原因 - -1. 用户已明确确认 `editor` 为遗留无用模块,应从本轮重写目标中剔除。 -2. 保留历史事实有助于后续对照清理,不会把“旧系统曾存在该模块”的信息抹掉。 -3. 从当前阶段开始继续为 `editor` 预留 Rust crate,只会增加主线迁移噪音与工程负担。 - -### 9.3 对后续任务的直接约束 - -从这一条决议开始,后续任务必须遵守: - -1. 不再为 `editor` 创建或维护 `server-rs` 下的新 crate、Axum 路由树与迁移验收项。 -2. 所有涉及挂载面、模块、路由总量的文档,都要区分“历史基线”与“本轮 active rewrite target”。 -3. 若未来仍要清理 `editor`,应在 `server-node/` 遗留链路依赖核对完成后单独立项。 diff --git a/backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md b/backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md deleted file mode 100644 index a6db4a67..00000000 --- a/backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md +++ /dev/null @@ -1,249 +0,0 @@ -# M0:旧接口到新实现路由迁移矩阵 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json) -- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md) - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第二条任务: - -- 整理当前后端 `96` 条路由并生成一份“旧接口 -> 新实现”映射表 - -这里的“新实现”不是指最终文件路径,而是指第一阶段重写时每条旧接口在新架构中的落点: - -1. 哪条 Axum 路由负责对外兼容 -2. 哪层 application service 负责编排 -3. 哪些状态进入 SpacetimeDB -4. 哪些二进制对象进入 OSS - -## 2. 映射代码说明 - -为避免 `96` 条路由的映射表过长,本表使用以下“新实现归属代码”: - -| 代码 | 新实现归属 | -| --- | --- | -| `A-HEALTH` | `Axum health route` | -| `A-AUTH` | `Axum auth routes + auth-service + SpacetimeDB auth tables` | -| `A-EDITOR` | `历史 Node editor 路由(遗留保留,不迁移到 server-rs)` | -| `A-OSS` | `Axum assets routes + application::assets + oss-service + SpacetimeDB asset metadata` | -| `A-LLM` | `Axum llm proxy/service` | -| `A-RUNTIME` | `Axum runtime facade + SpacetimeDB runtime reducers/views` | -| `A-STORY` | `Axum story facade + SpacetimeDB gameplay reducers/views` | -| `A-CHAT` | `Axum SSE facade + llm-service + SpacetimeDB story/npc state` | -| `A-CW` | `Axum custom-world facade + llm-service + SpacetimeDB custom_world reducers/views` | -| `A-AGENT` | `Axum custom-world-agent facade + llm-service + oss-service + SpacetimeDB agent tables` | - -补充说明: - -1. 第一阶段默认保留旧路径,不主动改前端请求地址。 -2. 兼容路径与主路径在新后端中应尽量共用同一 handler。 -3. 所有 `stream` 接口第一阶段继续用 Axum SSE,不强推改成 WebSocket。 -4. 自 `2026-04-21` 起,`editor` 路由仅保留历史对照,不纳入本轮 Rust 重写范围。 - -## 3. 总量校验 - -| 项目 | 数量 | -| --- | --- | -| 挂载面 | `6` | -| 总路由数 | `96` | -| `assets` | `14` | -| `auth` | `17` | -| `editor` | `3` | -| `runtime-main` | `59` | -| `runtime-story-action` | `2` | -| `health` | `1` | - -补充说明: - -1. 上表总量仍然是当前 Node 后端历史基线。 -2. 其中 `editor` 的 `3` 条路由继续计入历史对照,但不计入本轮 `server-rs` active rewrite target。 - -## 4. `assets` 路由映射(14 条) - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `assets.characterAnimationGenerate` | `POST /api/assets/character-animation/generate` | `A-OSS` | 保留原路径;Axum 创建 `asset_job`,外部生成结果写 OSS,任务状态进 SpacetimeDB。 | -| `assets.characterAnimationImportVideo` | `POST /api/assets/character-animation/import-video` | `A-OSS` | 保留原路径;视频参考素材由 Axum 上传 OSS,并写任务/对象元数据。 | -| `assets.characterAnimationJobGet` | `GET /api/assets/character-animation/jobs/:taskId` | `A-OSS` | 保留原路径;查询改读 SpacetimeDB `asset_job view`。 | -| `assets.characterAnimationPublish` | `POST /api/assets/character-animation/publish` | `A-OSS` | 保留原路径;发布动作改为“绑定 OSS 对象到业务实体 + 回写元数据”。 | -| `assets.characterAnimationTemplatesList` | `GET /api/assets/character-animation/templates` | `A-OSS` | 保留原路径;模板清单先由 Axum 提供,后续再视情况对象化。 | -| `assets.characterVisualGenerate` | `POST /api/assets/character-visual/generate` | `A-OSS` | 保留原路径;角色主形象候选生成改为 Axum 编排 + OSS 入库。 | -| `assets.characterVisualJobGet` | `GET /api/assets/character-visual/jobs/:taskId` | `A-OSS` | 保留原路径;任务状态改读 SpacetimeDB。 | -| `assets.characterVisualPublish` | `POST /api/assets/character-visual/publish` | `A-OSS` | 保留原路径;发布改为对象绑定,不再依赖本地 `public/generated-*` 真相。 | -| `assets.characterWorkflowCacheSave` | `POST /api/assets/character-workflow-cache` | `A-OSS` | 保留原路径;工作流缓存改写 OSS/对象存储,索引进 SpacetimeDB。 | -| `assets.characterWorkflowCacheGet` | `GET /api/assets/character-workflow-cache/:characterId` | `A-OSS` | 保留原路径;按角色查缓存索引,再返回对象内容或签名 URL。 | -| `assets.qwenSpriteFrameRepairGenerate` | `POST /api/assets/qwen-sprite/frame-repair` | `A-OSS` | 保留原路径;Qwen 修帧结果统一入 OSS,状态进 SpacetimeDB。 | -| `assets.qwenSpriteMasterGenerate` | `POST /api/assets/qwen-sprite/master` | `A-OSS` | 保留原路径;主图生成改为 Axum 编排。 | -| `assets.qwenSpriteAssetSave` | `POST /api/assets/qwen-sprite/save` | `A-OSS` | 保留原路径;保存动作改为持久化对象元数据与引用关系。 | -| `assets.qwenSpriteSheetGenerate` | `POST /api/assets/qwen-sprite/sheet` | `A-OSS` | 保留原路径;整表生成链路保留,底层切换为 OSS + SpacetimeDB。 | - -## 5. `auth` 路由映射(17 条) - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `auth.auditLogs` | `GET /api/auth/audit-logs` | `A-AUTH` | 保留原路径;从 SpacetimeDB `auth_audit_log view` 返回。 | -| `auth.entry` | `POST /api/auth/entry` | `A-AUTH` | 保留原路径;密码登录与自动注册由 Axum 完成,再写 auth 表。 | -| `auth.loginOptions` | `GET /api/auth/login-options` | `A-AUTH` | 保留原路径;由 Axum 直接返回登录方式配置。 | -| `auth.logout` | `POST /api/auth/logout` | `A-AUTH` | 保留原路径;Axum 吊销 refresh session 并清理 cookie。 | -| `auth.logoutAll` | `POST /api/auth/logout-all` | `A-AUTH` | 保留原路径;批量吊销用户全部 session。 | -| `auth.me` | `GET /api/auth/me` | `A-AUTH` | 保留原路径;由 Axum 校验 JWT 后查询用户读模型。 | -| `auth.phoneChange` | `POST /api/auth/phone/change` | `A-AUTH` | 保留原路径;短信校验在 Axum,绑定结果写 SpacetimeDB。 | -| `auth.phoneLogin` | `POST /api/auth/phone/login` | `A-AUTH` | 保留原路径;验证码校验成功后创建/恢复账号与 session。 | -| `auth.phoneSendCode` | `POST /api/auth/phone/send-code` | `A-AUTH` | 保留原路径;阿里云短信发送适配收口到 Axum。 | -| `auth.refresh` | `POST /api/auth/refresh` | `A-AUTH` | 保留原路径;沿用 refresh cookie -> access token 刷新模型。 | -| `auth.riskBlocks` | `GET /api/auth/risk-blocks` | `A-AUTH` | 保留原路径;改读风控封禁表/视图。 | -| `auth.riskBlocksLift` | `POST /api/auth/risk-blocks/:scopeType/lift` | `A-AUTH` | 保留原路径;解除请求由 Axum 执行校验并写状态。 | -| `auth.sessions` | `GET /api/auth/sessions` | `A-AUTH` | 保留原路径;会话列表改读 SpacetimeDB `refresh_session view`。 | -| `auth.sessionRevoke` | `POST /api/auth/sessions/:sessionId/revoke` | `A-AUTH` | 保留原路径;会话吊销改写 `refresh_session` 状态。 | -| `auth.wechatBindPhone` | `POST /api/auth/wechat/bind-phone` | `A-AUTH` | 保留原路径;微信身份补绑手机号逻辑迁到 Axum。 | -| `auth.wechatCallback` | `GET /api/auth/wechat/callback` | `A-AUTH` | 保留原路径与 redirect 语义;微信 code 交换由 Axum 处理。 | -| `auth.wechatStart` | `GET /api/auth/wechat/start` | `A-AUTH` | 保留原路径;授权 URL 由 Axum 按设备场景生成。 | - -## 6. `editor` 路由映射(3 条,历史遗留) - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `editor.catalogItems` | `GET /api/editor/catalog/items` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 | -| `editor.resourceRead` | `GET /api/editor/json/:resourceId` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 | -| `editor.resourceWrite` | `POST /api/editor/json/:resourceId` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 | - -## 7. `runtime-main` 路由映射(59 条) - -### 7.1 custom world 资源与实体生成 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.customWorldCoverImage` | `POST /api/custom-world/cover-image` | `A-CW` | 保留原路径;封面图生成由 Axum 编排,产物进 OSS,绑定关系进 SpacetimeDB。 | -| `runtime.customWorldCoverUpload` | `POST /api/custom-world/cover-upload` | `A-CW` | 保留原路径;上传改为 OSS 直传或 Axum 中转上传。 | -| `runtime.customWorldEntity.primary` | `POST /api/custom-world/entity` | `A-CW` | 保留原路径;实体生成由 Axum 调 LLM,再写 custom world 表。 | -| `runtime.customWorldSceneImage` | `POST /api/custom-world/scene-image` | `A-CW` | 保留原路径;场景图生成由 Axum + OSS 完成。 | -| `runtime.customWorldSceneNpc.primary` | `POST /api/custom-world/scene-npc` | `A-CW` | 保留原路径;场景 NPC 生成结果写 custom world / npc 相关表。 | - -### 7.2 LLM 透传 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.llmChatCompletionsProxy` | `POST /api/llm/chat/completions` | `A-LLM` | 保留原路径;继续由 Axum 承接代理,不进入 SpacetimeDB。 | - -### 7.3 profile 主路径 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.profileBrowseHistoryDelete.primary` | `DELETE /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;清理动作改为 reducer 写 `user_browse_history`。 | -| `runtime.profileBrowseHistoryGet.primary` | `GET /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;历史记录改读 browse history view。 | -| `runtime.profileBrowseHistoryPost.primary` | `POST /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;批量写入改为 Axum -> reducer。 | -| `runtime.profileDashboard.primary` | `GET /api/profile/dashboard` | `A-RUNTIME` | 保留原路径;个人主页汇总改读 dashboard view。 | -| `runtime.profilePlayStats.primary` | `GET /api/profile/play-stats` | `A-RUNTIME` | 保留原路径;统计数据改读 projection。 | -| `runtime.profileSaveArchivesList.primary` | `GET /api/profile/save-archives` | `A-RUNTIME` | 保留原路径;存档摘要改读 save archive view。 | -| `runtime.profileSaveArchivesResume.primary` | `POST /api/profile/save-archives/:worldKey` | `A-RUNTIME` | 保留原路径;恢复动作改读 `profile_save_archive` 后重建兼容快照。 | -| `runtime.profileWalletLedger.primary` | `GET /api/profile/wallet-ledger` | `A-RUNTIME` | 保留原路径;资产流水改读 ledger view。 | - -### 7.4 runtime 聊天与流式对话 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.characterReplyStream` | `POST /api/runtime/chat/character/reply/stream` | `A-CHAT` | 保留 SSE contract;Axum 流式产出,状态写 story/session 表。 | -| `runtime.characterSuggestions` | `POST /api/runtime/chat/character/suggestions` | `A-CHAT` | 保留原路径;由 Axum 生成建议语并按需写会话状态。 | -| `runtime.characterSummary` | `POST /api/runtime/chat/character/summary` | `A-CHAT` | 保留原路径;摘要生成留在 Axum,摘要索引可回写 SpacetimeDB。 | -| `runtime.npcDialogueStream` | `POST /api/runtime/chat/npc/dialogue/stream` | `A-CHAT` | 保留 SSE contract;NPC 对话状态迁到 SpacetimeDB。 | -| `runtime.npcRecruitStream` | `POST /api/runtime/chat/npc/recruit/stream` | `A-CHAT` | 保留 SSE contract;招募对话与状态变化统一进入新状态层。 | -| `runtime.npcTurnStream` | `POST /api/runtime/chat/npc/turn/stream` | `A-CHAT` | 保留 SSE contract;单回合发言的判定与状态回写统一收口。 | - -### 7.5 custom world gallery / library / sessions / works - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.customWorldGalleryList` | `GET /api/runtime/custom-world-gallery` | `A-CW` | 保留原路径;公开画廊改读 `custom_world_gallery view`。 | -| `runtime.customWorldGalleryDetail` | `GET /api/runtime/custom-world-gallery/:ownerUserId/:profileId` | `A-CW` | 保留原路径;详情改读 gallery detail view。 | -| `runtime.customWorldLibraryList` | `GET /api/runtime/custom-world-library` | `A-CW` | 保留原路径;资料库改读用户 custom world view。 | -| `runtime.customWorldLibraryDelete` | `DELETE /api/runtime/custom-world-library/:profileId` | `A-CW` | 保留原路径;删除改为 reducer 或软删除标记。 | -| `runtime.customWorldLibraryUpsert` | `PUT /api/runtime/custom-world-library/:profileId` | `A-CW` | 保留原路径;写入改为 Axum facade + SpacetimeDB profile tables。 | -| `runtime.customWorldLibraryPublish` | `POST /api/runtime/custom-world-library/:profileId/publish` | `A-CW` | 保留原路径;发布改为状态切换与画廊投影刷新。 | -| `runtime.customWorldLibraryUnpublish` | `POST /api/runtime/custom-world-library/:profileId/unpublish` | `A-CW` | 保留原路径;撤回发布改为状态切换。 | -| `runtime.customWorldSessionCreate` | `POST /api/runtime/custom-world/sessions` | `A-CW` | 保留原路径;传统问答会话状态迁到 SpacetimeDB。 | -| `runtime.customWorldSessionGet` | `GET /api/runtime/custom-world/sessions/:sessionId` | `A-CW` | 保留原路径;读取传统问答会话改读 view。 | -| `runtime.customWorldSessionAnswer` | `POST /api/runtime/custom-world/sessions/:sessionId/answers` | `A-CW` | 保留原路径;回答动作改为 reducer 写会话状态。 | -| `runtime.customWorldSessionGenerateStream` | `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` | `A-CW` | 保留 SSE contract;编译过程由 Axum 流式回推并回写状态。 | -| `runtime.customWorldWorksList` | `GET /api/runtime/custom-world/works` | `A-CW` | 保留原路径;作品汇总改读 custom world work summary view。 | - -### 7.6 custom world agent - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.customWorldAgentCreateSession` | `POST /api/runtime/custom-world/agent/sessions` | `A-AGENT` | 保留原路径;Agent 会话创建改写 `custom_world_agent_session`。 | -| `runtime.customWorldAgentGetSession` | `GET /api/runtime/custom-world/agent/sessions/:sessionId` | `A-AGENT` | 保留原路径;会话快照改读 Agent session view。 | -| `runtime.customWorldAgentExecuteAction` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/actions` | `A-AGENT` | 保留原路径;动作编排由 Axum 执行,状态与操作记录进 SpacetimeDB。 | -| `runtime.customWorldAgentGetCardDetail` | `GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId` | `A-AGENT` | 保留原路径;卡片详情改读 `custom_world_draft_card`。 | -| `runtime.customWorldAgentSendMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages` | `A-AGENT` | 保留原路径;消息提交后由 Axum 触发编排,消息与操作状态入库。 | -| `runtime.customWorldAgentStreamMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` | `A-AGENT` | 保留 SSE contract;流式消息由 Axum 输出,Agent 状态表持续更新。 | -| `runtime.customWorldAgentGetOperation` | `GET /api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId` | `A-AGENT` | 保留原路径;操作状态改读 `custom_world_agent_operation view`。 | - -### 7.7 compat 路径 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.customWorldEntity.compat` | `POST /api/runtime/custom-world/entity` | `A-CW` | 保留兼容路径;与主路径共用同一 handler。 | -| `runtime.customWorldSceneNpc.compat` | `POST /api/runtime/custom-world/scene-npc` | `A-CW` | 保留兼容路径;与主路径共用同一 handler。 | -| `runtime.profileBrowseHistoryDelete.compat` | `DELETE /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;与 `/api/profile/browse-history` 共用实现。 | -| `runtime.profileBrowseHistoryGet.compat` | `GET /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;共用 browse history facade。 | -| `runtime.profileBrowseHistoryPost.compat` | `POST /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;共用写入逻辑。 | -| `runtime.profileDashboard.compat` | `GET /api/runtime/profile/dashboard` | `A-RUNTIME` | 保留兼容路径;共用 dashboard facade。 | -| `runtime.profilePlayStats.compat` | `GET /api/runtime/profile/play-stats` | `A-RUNTIME` | 保留兼容路径;共用 play stats facade。 | -| `runtime.profileSaveArchivesList.compat` | `GET /api/runtime/profile/save-archives` | `A-RUNTIME` | 保留兼容路径;共用 save archives list facade。 | -| `runtime.profileSaveArchivesResume.compat` | `POST /api/runtime/profile/save-archives/:worldKey` | `A-RUNTIME` | 保留兼容路径;共用 resume facade。 | -| `runtime.profileWalletLedger.compat` | `GET /api/runtime/profile/wallet-ledger` | `A-RUNTIME` | 保留兼容路径;共用 wallet ledger facade。 | - -### 7.8 runtime 其他核心接口 - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `runtime.itemsIntent` | `POST /api/runtime/items/runtime-intent` | `A-CW` | 保留原路径;Axum 调 LLM 生成意图,物品领域状态与引用写 SpacetimeDB。 | -| `runtime.questsGenerate` | `POST /api/runtime/quests/generate` | `A-CW` | 保留原路径;任务候选生成由 Axum 编排,结果写 quest 相关表。 | -| `runtime.snapshotDelete` | `DELETE /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;删除动作改为更新 `runtime_snapshot` / archive。 | -| `runtime.snapshotGet` | `GET /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;读取兼容聚合快照,由 view/projection 输出。 | -| `runtime.snapshotPut` | `PUT /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;写入由 Axum facade + reducer 完成。 | -| `runtime.settingsGet` | `GET /api/runtime/settings` | `A-RUNTIME` | 保留原路径;设置改读 `runtime_setting view`。 | -| `runtime.settingsPut` | `PUT /api/runtime/settings` | `A-RUNTIME` | 保留原路径;设置更新改为 reducer。 | -| `runtime.storyContinue` | `POST /api/runtime/story/continue` | `A-STORY` | 保留原路径;故事推进由 Axum 调新 story/application 层。 | -| `runtime.storyInitial` | `POST /api/runtime/story/initial` | `A-STORY` | 保留原路径;首段故事生成保持 REST 兼容。 | -| `runtime.wsHealth` | `GET /api/ws/health` | `A-RUNTIME` | 保留原路径;继续作为实时链路占位健康检查。 | - -## 8. `runtime-story-action` 路由映射(2 条) - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `storyAction.resolve` | `POST /api/runtime/story/actions/resolve` | `A-STORY` | 保留原路径;Axum 接收动作请求,SpacetimeDB reducer 执行跨模块结算。 | -| `storyAction.stateGet` | `GET /api/runtime/story/state/:sessionId` | `A-STORY` | 保留原路径;读取 story session 兼容状态 view。 | - -## 9. `health` 路由映射(1 条) - -| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 | -| --- | --- | --- | --- | -| `health.check` | `GET /healthz` | `A-HEALTH` | 保留原路径与最小返回结构。 | - -## 10. 迁移落地规则 - -后续做路由树时,必须遵守: - -1. 旧路径优先保留,新实现从内部切换,不先要求前端改地址。 -2. 主路径与兼容路径必须共用同一 application service,避免再次出现双份逻辑。 -3. `stream` 接口第一阶段默认沿用 SSE。 -4. `assets` 与 `custom-world` 里的生成类接口,外部副作用统一在 Axum,状态与任务统一进 SpacetimeDB。 -5. `storyAction.resolve`、`runtime.snapshotPut`、`auth.refresh` 属于最优先回归接口,后续开发必须优先补完整测试。 -6. `editor` 相关旧路径只保留历史基线记录,不纳入 `server-rs` 路由树实施范围。 - -## 11. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. `96` 条旧路由都已经有新实现落点。 -2. 每条路由至少明确: - - 旧方法/路径 - - 新实现归属 - - 第一阶段迁移策略 -3. 后续搭建 Axum 路由树与 application service 时,可以直接按这份矩阵逐项落位。 diff --git a/backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md b/backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md deleted file mode 100644 index fc4a252a..00000000 --- a/backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md +++ /dev/null @@ -1,300 +0,0 @@ -# M0:SSE 接口与事件格式冻结基线 - -日期:`2026-04-20` - -依据来源: - -- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md) -- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json) -- `server-node/src/http.ts` -- `server-node/src/routes/runtimeRoutes.ts` -- `server-node/src/routes/customWorldAgent.ts` -- `server-node/src/modules/ai/chatOrchestrator.ts` -- `server-node/src/services/customWorldAgentOrchestrator.ts` -- `server-node/src/modules/ai/customWorldOrchestrator.ts` - -## 1. 文档目的 - -这份文档用于完成 `M0` 的第四条任务: - -- 整理当前所有 SSE 接口与事件格式 - -这里的“整理”不是只记住有几条 `stream` 路由,而是要求后续 Axum 重写必须先冻结: - -1. 当前到底有哪几条 SSE 路由。 -2. 每条路由是“透传上游流”还是“项目自定义事件流”。 -3. 每条路由的事件名、结束标记、错误帧和头部约束是什么。 -4. 哪些流的 `payload` 是增量文本,哪些其实是“累计文本”。 - -## 2. 冻结结论 - -当前 Node 后端正式登记的 SSE 接口固定为以下 `6` 条: - -| 路由 ID | 方法/路径 | 当前实现入口 | 协议类型 | 成功结束标记 | 鉴权 | -| --- | --- | --- | --- | --- | --- | -| `runtime.characterReplyStream` | `POST /api/runtime/chat/character/reply/stream` | `runtimeRoutes.ts -> streamCharacterChatReplyFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT | -| `runtime.npcDialogueStream` | `POST /api/runtime/chat/npc/dialogue/stream` | `runtimeRoutes.ts -> streamNpcChatDialogueFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT | -| `runtime.npcRecruitStream` | `POST /api/runtime/chat/npc/recruit/stream` | `runtimeRoutes.ts -> streamNpcRecruitDialogueFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT | -| `runtime.npcTurnStream` | `POST /api/runtime/chat/npc/turn/stream` | `runtimeRoutes.ts -> streamNpcChatTurnFromOrchestrator` | 项目自定义 SSE | `event: complete` 后追加 `data: [DONE]` | JWT | -| `runtime.customWorldSessionGenerateStream` | `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` | `runtimeRoutes.ts` 内联实现 | 项目自定义 SSE | `event: done`,无 `[DONE]` | JWT | -| `runtime.customWorldAgentStreamMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` | `customWorldAgent.ts -> customWorldAgentOrchestrator.streamMessage` | 项目自定义 SSE | `event: done`,无 `[DONE]` | JWT | - -冻结总数: - -1. SSE 接口:`6` -2. 上游透传型:`3` -3. 本地自定义事件流:`3` - -## 3. 全部 SSE 接口共享的响应头约束 - -当前所有项目内主动准备 SSE 响应的接口,都经过 `prepareEventStreamResponse(...)`,因此至少冻结以下头部行为: - -| 响应头 | 当前值/规则 | 说明 | -| --- | --- | --- | -| `Content-Type` | 默认 `text/event-stream; charset=utf-8` | 透传型接口可被上游 `content-type` 覆盖,但仍保持 SSE。 | -| `Cache-Control` | `no-cache` | 禁止中间层缓存流式结果。 | -| `Connection` | `keep-alive` | 保持 SSE 长连接。 | -| `X-Accel-Buffering` | `no` | 禁止代理层缓冲。 | -| `x-request-id` | 透传当前请求 ID | 所有 SSE 都要带请求追踪头。 | -| `x-api-version` | 当前 API 版本号 | 与普通 JSON 接口一致。 | -| `x-route-version` | 当前路由版本号 | 与普通 JSON 接口一致。 | -| `x-response-time-ms` | 当前已耗时毫秒数 | 在准备响应头时写入。 | - -额外冻结约束: - -1. `SSE` 接口当前也保留普通 API 元数据头,不能因为换成 Axum 就丢掉。 -2. 这 `6` 条流式接口都在 `requireAuth` 之后注册,因此第一阶段默认仍需要 `Bearer JWT`。 - -## 4. 协议分型 - -### 4.1 上游透传型 SSE(3 条) - -包含: - -1. `POST /api/runtime/chat/character/reply/stream` -2. `POST /api/runtime/chat/npc/dialogue/stream` -3. `POST /api/runtime/chat/npc/recruit/stream` - -当前实现特征: - -1. 路由不自己重写事件名,直接把上游模型返回的 SSE 原样管道转发给前端。 -2. 本地只负责: - - 发起上游流式请求 - - 准备 SSE 头部 - - 处理中断时的请求 abort -3. 从 `llmClient.streamMessageContent(...)` 的解析逻辑可以反推,当前上游 SSE 采用 OpenAI 风格: - - 多个 `data: {...}` JSON chunk - - 最终 `data: [DONE]` - -冻结要求: - -1. 第一阶段 Axum 仍要保持这三条接口的“上游透传”语义。 -2. 不要在未发版变更协议前,擅自把它们改成项目自定义 `event: reply_delta` 格式。 - -### 4.2 项目自定义 SSE(3 条) - -包含: - -1. `POST /api/runtime/chat/npc/turn/stream` -2. `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` -3. `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` - -当前实现特征: - -1. 路由或 orchestrator 自己写 `event:` 与 `data:`。 -2. 事件名不是上游协议,而是项目本地约定。 -3. 这三条流的结束方式并不一致,必须分别兼容。 - -## 5. 各接口事件格式冻结 - -### 5.1 `runtime.characterReplyStream` - -路径: - -- `POST /api/runtime/chat/character/reply/stream` - -冻结格式: - -1. 当前为上游透传流。 -2. 本地不保证固定 `event` 名。 -3. 前端实际收到的是上游 `data: {...}` chunk 与最终 `data: [DONE]`。 -4. 失败时当前实现也不是本地 `event: error`,而是由上游失败或 Express 错误链决定。 - -### 5.2 `runtime.npcDialogueStream` - -路径: - -- `POST /api/runtime/chat/npc/dialogue/stream` - -冻结格式: - -1. 当前为上游透传流。 -2. 协议特征与 `runtime.characterReplyStream` 相同。 -3. 第一阶段不能私自改成项目自定义事件名。 - -### 5.3 `runtime.npcRecruitStream` - -路径: - -- `POST /api/runtime/chat/npc/recruit/stream` - -冻结格式: - -1. 当前为上游透传流。 -2. 协议特征与前两条透传 SSE 相同。 -3. 结束标记仍依赖上游 `data: [DONE]`。 - -### 5.4 `runtime.npcTurnStream` - -路径: - -- `POST /api/runtime/chat/npc/turn/stream` - -成功事件序列: - -1. `event: reply_delta` -2. `event: reply_delta` -3. `...` -4. `event: complete` -5. `data: [DONE]` - -错误事件: - -1. `event: error` -2. `data: {"message":"..."}` -3. 之后直接 `response.end()`,不会再补 `complete` - -冻结 payload 规则: - -| 事件名 | payload 结构 | 关键说明 | -| --- | --- | --- | -| `reply_delta` | `{ "text": string }` | `text` 实际是“累计文本”,不是单 token 增量。 | -| `complete` | `{ "npcReply": string, "affinityDelta": number, "affinityText": string, "suggestions": string[], "pendingQuestOffer": object \| null, "chatDirective": object \| null }` | 最终一次性返回业务结算数据。 | -| `error` | `{ "message": string }` | 仅错误消息,无额外状态。 | - -补充冻结点: - -1. `reply_delta.text` 每次都是当前累计回复全文。 -2. `complete.suggestions` 在强制收束场景下可能是空数组。 -3. `complete.chatDirective` 当前至少可能包含: - - `turnLimit` - - `remainingTurns` - - `forceExit` - - `closingMode` -4. `complete.pendingQuestOffer` 当前可能包含: - - `quest` - - `introText` - -### 5.5 `runtime.customWorldSessionGenerateStream` - -路径: - -- `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` - -成功事件序列: - -1. `event: progress`,payload:`{ "phase": "preparing", "progress": 10 }` -2. `event: progress`,payload:`{ "phase": "requesting_llm", "progress": 45 }` -3. `event: progress`,payload:`CustomWorldGenerationProgress` -4. `...` -5. `event: progress`,payload:`{ "phase": "completed", "progress": 100 }` -6. `event: result` -7. `event: done` - -错误事件: - -1. `event: error` -2. `data: {"message":"..."}` -3. 之后直接结束,不会再发 `done` - -冻结 payload 规则: - -| 事件名 | payload 结构 | 关键说明 | -| --- | --- | --- | -| `progress` | 兼容两种结构 | 这是当前最容易踩坑的混合协议。 | -| `result` | `{ "profile": object }` | 返回完整世界 profile。 | -| `done` | `{ "ok": true }` | 当前没有 `[DONE]` 字符串终止帧。 | -| `error` | `{ "message": string }` | 当前也没有额外错误码。 | - -`progress` 事件的两种冻结结构: - -1. 启动/收尾帧: - - `{ "phase": "preparing", "progress": 10 }` - - `{ "phase": "requesting_llm", "progress": 45 }` - - `{ "phase": "completed", "progress": 100 }` -2. 编排器进度帧 `CustomWorldGenerationProgress`: - - `phaseId` - - `phaseLabel` - - `phaseDetail` - - `overallProgress` - - `completedWeight` - - `totalWeight` - - `elapsedMs` - - `estimatedRemainingMs` - - `activeStepIndex` - - `steps` - -补充冻结点: - -1. 当前 `progress` 不是单一 schema,而是混合 schema。 -2. 当前实现会在客户端断开时触发 `AbortController`,这条流具备显式中断处理。 - -### 5.6 `runtime.customWorldAgentStreamMessage` - -路径: - -- `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` - -成功事件序列: - -1. `event: reply_delta` -2. `event: reply_delta` -3. `...` -4. `event: session` -5. `event: done` - -错误事件: - -1. `event: error` -2. `data: {"message":"..."}` -3. 之后直接结束,不会再补 `done` - -冻结 payload 规则: - -| 事件名 | payload 结构 | 关键说明 | -| --- | --- | --- | -| `reply_delta` | `{ "text": string }` | 当前也是累计文本,不是 diff patch。 | -| `session` | `{ "session": CustomWorldAgentSessionSnapshot }` | 完整会话快照一次性回推。 | -| `done` | `{ "ok": true }` | 当前没有 `[DONE]`。 | -| `error` | `{ "message": string }` | 仅错误消息。 | - -补充冻结点: - -1. 这条流当前不会在成功结尾补发最终文本帧,只会发 `session` 快照。 -2. `reply_delta.text` 同样是“到当前为止的完整回复”。 -3. 当前实现没有像 `customWorldSessionGenerateStream` 那样显式挂请求断开 abort。 - -## 6. 第一阶段 Axum 重写必须兼容的硬约束 - -后续重写中,不允许出现以下情况: - -1. 把当前 `6` 条 SSE 路由减少、合并或改掉方法类型。 -2. 把透传型 `3` 条流直接改写成自定义事件名,而前端却不知情。 -3. 把 `npcTurnStream` 的 `reply_delta` 从“累计文本”改成“真正 delta”,导致前端拼接方式失效。 -4. 把 `customWorldSessionGenerateStream` 的混合 `progress` schema 静默改成新格式,却没有版本门禁。 -5. 把 `customWorldAgentStreamMessage` 的 `session` 终帧改成局部 patch,而前端仍按完整快照消费。 -6. 丢失 `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 等当前前端与联调用到的头。 - -## 7. 本任务完成定义 - -当以下条件成立时,这条任务视为完成: - -1. 当前 `6` 条 SSE 接口已经有书面冻结清单。 -2. 每条 SSE 都已明确: - - 方法与路径 - - 协议类型 - - 事件名 - - 成功结束标记 - - 错误事件 - - 关键 payload 结构 -3. 后续 Axum SSE 落地、前端 contract 回归、SpacetimeDB 实时链路设计时,可以直接引用本文,不再靠人工回忆事件名。 diff --git a/backend-rewrite-tasklist/README.md b/backend-rewrite-tasklist/README.md deleted file mode 100644 index e36ef801..00000000 --- a/backend-rewrite-tasklist/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 后端重写任务清单目录 - -日期:`2026-04-20` - -本目录用于集中存放 `SpacetimeDB + Axum + 阿里云 OSS` 后端重写相关任务清单。 - -## 文件结构 - -- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md):总纲主清单,保留完整阶段结构与最终验收项。 -- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md):能力冻结、Rust 工作区、Axum 基础设施、鉴权与会话迁移任务。 -- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md):runtime snapshot / settings / profile 迁移任务。 -- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md):story action 主循环与 gameplay reducer 迁移任务。 -- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md):custom world / gallery / agent 主链迁移任务。 -- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md):assets / 阿里云 OSS 迁移任务;`editor` 已于 `2026-04-21` 退出本轮重写范围。 -- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md):联调、回归、部署、观测与切流任务。 -- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md):横向专项、执行顺序与最终验收清单。 -- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md):当前 Node 后端 `6` 个挂载面的冻结基线,用于后续接口映射、模块迁移与验收对照。 -- [M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md):当前 `96` 条后端路由的“旧接口 -> 新实现”迁移矩阵,用于 Axum 路由树和 application service 落位。 -- [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md):当前 `12` 个内部模块的迁移归属基线,用于锁定 Rust crate、SpacetimeDB bounded context 与 Axum/application 分工。 -- [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md):当前 `6` 条 SSE 接口及其事件格式冻结基线,用于 Axum SSE 兼容和前端 contract 回归。 -- [M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md):当前正式 `/generated-*` 静态资源前缀冻结基线,用于 Axum 静态资源兼容层与 OSS 对象键规划。 -- [M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md):当前前端直接依赖的响应头、envelope 与错误格式冻结基线,用于 Axum 中间件与错误响应兼容。 -- [M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md):`M0` 仓库边界决议文档,用于持续冻结 `server-rs/` 落位、迁移期双栈共存、Axum 边界与副作用收口原则。 -- [M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md):`M0 ~ M7` 阶段验收矩阵,用于固定每阶段的入口条件、核心交付、退出条件与跨阶段回归焦点。 - -## 当前 M4 / M5 结构基线 - -- `M4` 当前涉及的前后端脚本结构、命名根、route/service/compiler/repository 落位,统一参照 [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)。 -- `M5` 当前涉及的创作入口、Agent session、result preview、works/library/gallery、publish 与 enter-world 主链,统一参照 [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)。 -- 旧 `custom-world/sessions` 传统问答链已经退出当前仓库正式主链;后续若在 `M5` 中提及,只按历史兼容台账处理,不再作为当前功能扩展目标。 - -## 维护规则 - -1. 总纲与拆分文件都以本目录为唯一维护位置。 -2. 总纲用于把控全局节奏,拆分文件用于实际逐项推进。 -3. 如阶段任务发生明显变化,需要同步更新总纲与对应拆分文件。