1
This commit is contained in:
@@ -10,11 +10,11 @@
|
||||
2. `SpacetimeDB` 状态机模块
|
||||
3. `阿里云 OSS` 资产接入与应用层编排
|
||||
|
||||
该目录固定放在仓库根目录,与 `src/`、`docs/` 同级。旧 `server-node/` 已进入分批删除流程,后续只可通过历史提交或迁移文档追溯。
|
||||
该目录固定放在仓库根目录,与 `src/`、`docs/` 同级。旧 `server-node/` 已完成物理删除,后续只可通过历史提交或迁移文档追溯。
|
||||
|
||||
## 2. 当前阶段说明
|
||||
|
||||
当前目录已经完成以下三十八项初始化:
|
||||
当前目录已经完成以下三十七项初始化:
|
||||
|
||||
1. 为新后端预留正式目录并把路径固定到仓库结构中。
|
||||
2. 创建虚拟 workspace `Cargo.toml`,后续 crate 会逐项挂入。
|
||||
@@ -52,8 +52,7 @@
|
||||
34. 创建 `scripts/spacetime-dev.ps1`,固定 Windows 本地 SpacetimeDB 启动入口。
|
||||
35. 创建 `scripts/spacetime-dev.sh`,固定 Unix-like 本地 SpacetimeDB 启动入口。
|
||||
36. 创建 `scripts/oss-smoke.ps1`,固定 Windows 本地阿里云 OSS 真实联调入口。
|
||||
37. 创建 `scripts/m7-preflight.ps1`,固定 M7 切流前 Rust 后端预检入口。
|
||||
38. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关。
|
||||
37. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关。
|
||||
|
||||
后续任务会继续在本目录内按顺序补齐:
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, State},
|
||||
http::{HeaderName, HeaderValue, StatusCode, header},
|
||||
http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest};
|
||||
@@ -115,44 +114,47 @@ async fn read_legacy_generated_asset(
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.cloned();
|
||||
let bytes = upstream_response
|
||||
.error_for_status()
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": format!("读取 OSS 旧 generated 资源失败:{error}"),
|
||||
}))
|
||||
})?
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": format!("读取 OSS 旧 generated 资源内容失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let mut response = Response::builder()
|
||||
.status(status)
|
||||
.header(header::CACHE_CONTROL, CACHE_CONTROL_VALUE)
|
||||
.header(
|
||||
HeaderName::from_static(ASSET_OBJECT_KEY_HEADER),
|
||||
HeaderValue::from_str(object_key.as_str()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "legacy-generated-assets",
|
||||
"message": format!("构造资源响应头失败:{error}"),
|
||||
}))
|
||||
})?,
|
||||
);
|
||||
if let Some(content_type) = content_type {
|
||||
response = response.header(header::CONTENT_TYPE, content_type);
|
||||
if !status.is_success() {
|
||||
return Err(map_legacy_generated_upstream_status(status, object_key));
|
||||
}
|
||||
|
||||
response.body(Body::from(bytes)).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "legacy-generated-assets",
|
||||
"message": format!("构造资源响应失败:{error}"),
|
||||
let bytes = upstream_response.bytes().await.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"message": format!("读取 OSS 旧 generated 资源内容失败:{error}"),
|
||||
}))
|
||||
})
|
||||
})?;
|
||||
|
||||
// 旧 generated 路径会被 <img> / <video> 直接消费,成功分支必须返回原始二进制体。
|
||||
// 这里显式组装 HeaderMap 并设置长度,避免代理层把已成功读取的 OSS 对象变成空响应。
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static(CACHE_CONTROL_VALUE),
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static(ASSET_OBJECT_KEY_HEADER),
|
||||
HeaderValue::from_str(object_key.as_str()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "legacy-generated-assets",
|
||||
"message": format!("构造资源响应头失败:{error}"),
|
||||
}))
|
||||
})?,
|
||||
);
|
||||
headers.insert(
|
||||
header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(bytes.len().to_string().as_str()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "legacy-generated-assets",
|
||||
"message": format!("构造资源长度响应头失败:{error}"),
|
||||
}))
|
||||
})?,
|
||||
);
|
||||
if let Some(content_type) = content_type {
|
||||
headers.insert(header::CONTENT_TYPE, content_type);
|
||||
}
|
||||
|
||||
Ok((status, headers, bytes).into_response())
|
||||
}
|
||||
|
||||
fn build_generated_object_key(prefix: LegacyAssetPrefix, path: &str) -> Result<String, AppError> {
|
||||
@@ -189,6 +191,25 @@ fn map_legacy_generated_oss_error(error: platform_oss::OssError) -> AppError {
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_legacy_generated_upstream_status(
|
||||
status: reqwest::StatusCode,
|
||||
object_key: String,
|
||||
) -> AppError {
|
||||
let mapped_status = match status {
|
||||
reqwest::StatusCode::NOT_FOUND => StatusCode::NOT_FOUND,
|
||||
reqwest::StatusCode::FORBIDDEN | reqwest::StatusCode::UNAUTHORIZED => {
|
||||
StatusCode::BAD_GATEWAY
|
||||
}
|
||||
_ => StatusCode::BAD_GATEWAY,
|
||||
};
|
||||
|
||||
AppError::from_status(mapped_status).with_details(json!({
|
||||
"provider": "aliyun-oss",
|
||||
"objectKey": object_key,
|
||||
"upstreamStatus": status.as_u16(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1971,15 +1971,25 @@ mod tests {
|
||||
|
||||
assert_eq!(point_products.len(), 6);
|
||||
assert_eq!(point_products[0].product_id, "points_60");
|
||||
assert_eq!(point_products[0].title, "60叙世币");
|
||||
assert_eq!(point_products[0].price_cents, 600);
|
||||
assert_eq!(point_products[0].bonus_points, 60);
|
||||
assert_eq!(point_products[0].description, "首充送60叙世币");
|
||||
assert_eq!(point_products[5].product_id, "points_3280");
|
||||
assert_eq!(point_products[5].price_cents, 32800);
|
||||
assert_eq!(point_products[5].bonus_points, 3280);
|
||||
assert_eq!(point_products[5].description, "首充送3280叙世币");
|
||||
assert_eq!(membership_products.len(), 3);
|
||||
assert_eq!(membership_products[0].title, "月卡");
|
||||
assert_eq!(membership_products[0].price_cents, 2800);
|
||||
assert_eq!(membership_products[2].duration_days, 365);
|
||||
|
||||
let benefits = runtime_profile_membership_benefits();
|
||||
assert!(
|
||||
benefits
|
||||
.iter()
|
||||
.any(|benefit| benefit.benefit_name == "免叙世币回合数")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -807,7 +807,12 @@ mod tests {
|
||||
json!("2026-05-25T10:00:00Z")
|
||||
);
|
||||
assert_eq!(payload["pointProducts"][0]["productId"], json!("points_60"));
|
||||
assert_eq!(payload["pointProducts"][0]["title"], json!("60叙世币"));
|
||||
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
|
||||
assert_eq!(
|
||||
payload["pointProducts"][0]["description"],
|
||||
json!("首充送60叙世币")
|
||||
);
|
||||
assert_eq!(payload["hasPointsRecharged"], json!(false));
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,9 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
||||
.filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||
})
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -251,6 +253,24 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSession) -> bool {
|
||||
if big_fish_session_has_direct_work_content(row) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db.big_fish_agent_message().iter().any(|message| {
|
||||
message.session_id == row.session_id
|
||||
&& matches!(message.role, BigFishAgentMessageRole::User)
|
||||
})
|
||||
}
|
||||
|
||||
fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool {
|
||||
// 助手欢迎语和默认 anchorPack 只是工作台初始状态,不应被当成草稿作品。
|
||||
!row.seed_text.trim().is_empty()
|
||||
|| row.draft_json.is_some()
|
||||
|| row.stage == BigFishCreationStage::Published
|
||||
}
|
||||
|
||||
pub(crate) fn delete_big_fish_work_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishWorkDeleteInput,
|
||||
@@ -687,3 +707,53 @@ pub(crate) fn append_big_fish_system_message(
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn build_test_big_fish_session(
|
||||
seed_text: &str,
|
||||
draft_json: Option<&str>,
|
||||
stage: BigFishCreationStage,
|
||||
) -> BigFishCreationSession {
|
||||
BigFishCreationSession {
|
||||
session_id: "big-fish-session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
seed_text: seed_text.to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 20,
|
||||
stage,
|
||||
anchor_pack_json: "{}".to_string(),
|
||||
draft_json: draft_json.map(str::to_string),
|
||||
asset_coverage_json: "{}".to_string(),
|
||||
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||
publish_ready: false,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_direct_work_content_ignores_empty_created_session() {
|
||||
let empty_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::CollectingAnchors);
|
||||
let seeded_session = build_test_big_fish_session(
|
||||
"想做深海吞噬成长",
|
||||
None,
|
||||
BigFishCreationStage::CollectingAnchors,
|
||||
);
|
||||
let drafted_session = build_test_big_fish_session(
|
||||
"",
|
||||
Some(r#"{"title":"深海吞噬"}"#),
|
||||
BigFishCreationStage::DraftReady,
|
||||
);
|
||||
let published_session =
|
||||
build_test_big_fish_session("", None, BigFishCreationStage::Published);
|
||||
|
||||
assert!(!big_fish_session_has_direct_work_content(&empty_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&seeded_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&drafted_session));
|
||||
assert!(big_fish_session_has_direct_work_content(&published_session));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = custom_world_profile,
|
||||
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
||||
@@ -1457,10 +1459,14 @@ fn list_custom_world_work_snapshots(
|
||||
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut active_agent_session_ids = HashSet::new();
|
||||
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.stage != RpgAgentStage::Published
|
||||
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||
}) {
|
||||
active_agent_session_ids.insert(session.session_id.clone());
|
||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
||||
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
||||
@@ -1504,6 +1510,7 @@ fn list_custom_world_work_snapshots(
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||
.filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids))
|
||||
{
|
||||
items.push(CustomWorldWorkSummarySnapshot {
|
||||
work_id: format!("published:{}", profile.profile_id),
|
||||
@@ -1558,6 +1565,63 @@ fn list_custom_world_work_snapshots(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_custom_world_agent_session_work(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
if custom_world_agent_session_has_direct_work_content(session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||
message.session_id == session.session_id && matches!(message.role, RpgAgentMessageRole::User)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.any(|card| card.session_id == session.session_id)
|
||||
}
|
||||
|
||||
fn custom_world_agent_session_has_direct_work_content(
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||
!session.seed_text.trim().is_empty()
|
||||
|| matches!(
|
||||
session.stage,
|
||||
RpgAgentStage::ObjectRefining
|
||||
| RpgAgentStage::VisualRefining
|
||||
| RpgAgentStage::LongTailReview
|
||||
| RpgAgentStage::ReadyToPublish
|
||||
| RpgAgentStage::Published
|
||||
)
|
||||
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.as_ref()
|
||||
.is_some_and(|profile| !profile.is_empty())
|
||||
}
|
||||
|
||||
fn should_include_custom_world_profile_work(
|
||||
row: &CustomWorldProfile,
|
||||
active_agent_session_ids: &HashSet<String>,
|
||||
) -> bool {
|
||||
// 已发布 profile 是正式作品;即使来源会话还存在,也必须保留独立入口。
|
||||
if row.publication_status == CustomWorldPublicationStatus::Published {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未发布 profile 若来源于仍可继续聊天的 Agent 会话,只是同一草稿的编译产物,
|
||||
// works 里保留 agent_session 即可,避免草稿分组显示两份同名作品。
|
||||
row.source_agent_session_id
|
||||
.as_ref()
|
||||
.map_or(true, |session_id| {
|
||||
!active_agent_session_ids.contains(session_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_custom_world_agent_session_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldAgentSessionGetInput,
|
||||
@@ -3710,7 +3774,7 @@ fn parse_json_array_or_empty(raw: &str) -> Vec<JsonValue> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||
payload.get(array_key).and_then(JsonValue::as_array).and_then(|values| values.first()).and_then(JsonValue::as_str)
|
||||
.or_else(|| payload.get(scalar_key).and_then(JsonValue::as_str))
|
||||
.map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned)
|
||||
|
||||
@@ -2930,7 +2930,9 @@ fn list_custom_world_work_snapshots(
|
||||
let mut active_agent_session_ids = HashSet::new();
|
||||
|
||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
||||
row.owner_user_id == input.owner_user_id
|
||||
&& row.stage != RpgAgentStage::Published
|
||||
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||
}) {
|
||||
active_agent_session_ids.insert(session.session_id.clone());
|
||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||
@@ -3031,6 +3033,44 @@ fn list_custom_world_work_snapshots(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn should_include_custom_world_agent_session_work(
|
||||
ctx: &ReducerContext,
|
||||
session: &CustomWorldAgentSession,
|
||||
) -> bool {
|
||||
if custom_world_agent_session_has_direct_work_content(session) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||
message.session_id == session.session_id
|
||||
&& matches!(message.role, RpgAgentMessageRole::User)
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.custom_world_draft_card()
|
||||
.iter()
|
||||
.any(|card| card.session_id == session.session_id)
|
||||
}
|
||||
|
||||
fn custom_world_agent_session_has_direct_work_content(session: &CustomWorldAgentSession) -> bool {
|
||||
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||
!session.seed_text.trim().is_empty()
|
||||
|| matches!(
|
||||
session.stage,
|
||||
RpgAgentStage::ObjectRefining
|
||||
| RpgAgentStage::VisualRefining
|
||||
| RpgAgentStage::LongTailReview
|
||||
| RpgAgentStage::ReadyToPublish
|
||||
| RpgAgentStage::Published
|
||||
)
|
||||
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||
.as_ref()
|
||||
.is_some_and(|profile| !profile.is_empty())
|
||||
}
|
||||
|
||||
fn should_include_custom_world_profile_work(
|
||||
row: &CustomWorldProfile,
|
||||
active_agent_session_ids: &HashSet<String>,
|
||||
@@ -6250,25 +6290,25 @@ fn build_npc_state_snapshot_from_row(row: &NpcState) -> NpcStateSnapshot {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||
let session = CustomWorldAgentSession {
|
||||
fn build_test_custom_world_agent_session(
|
||||
seed_text: &str,
|
||||
stage: RpgAgentStage,
|
||||
draft_profile_json: Option<&str>,
|
||||
) -> CustomWorldAgentSession {
|
||||
CustomWorldAgentSession {
|
||||
session_id: "session-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
seed_text: "seed".to_string(),
|
||||
current_turn: 1,
|
||||
progress_percent: 100,
|
||||
stage: RpgAgentStage::ObjectRefining,
|
||||
seed_text: seed_text.to_string(),
|
||||
current_turn: 0,
|
||||
progress_percent: 0,
|
||||
stage,
|
||||
focus_card_id: None,
|
||||
anchor_content_json: "{}".to_string(),
|
||||
creator_intent_json: None,
|
||||
creator_intent_readiness_json: "{}".to_string(),
|
||||
anchor_pack_json: None,
|
||||
lock_state_json: None,
|
||||
draft_profile_json: Some(
|
||||
r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#
|
||||
.to_string(),
|
||||
),
|
||||
draft_profile_json: draft_profile_json.map(str::to_string),
|
||||
last_assistant_reply: None,
|
||||
publish_gate_json: None,
|
||||
result_preview_json: None,
|
||||
@@ -6280,7 +6320,16 @@ mod tests {
|
||||
checkpoints_json: "[]".to_string(),
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||
let session = build_test_custom_world_agent_session(
|
||||
"seed",
|
||||
RpgAgentStage::ObjectRefining,
|
||||
Some(r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_stable_agent_draft_profile_id(&session),
|
||||
@@ -6288,6 +6337,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_world_agent_session_direct_work_content_ignores_empty_created_session() {
|
||||
let empty_session =
|
||||
build_test_custom_world_agent_session("", RpgAgentStage::CollectingIntent, Some("{}"));
|
||||
let seeded_session = build_test_custom_world_agent_session(
|
||||
"想做一个海雾群岛",
|
||||
RpgAgentStage::CollectingIntent,
|
||||
Some("{}"),
|
||||
);
|
||||
let drafted_session =
|
||||
build_test_custom_world_agent_session("", RpgAgentStage::ObjectRefining, Some("{}"));
|
||||
let profile_session = build_test_custom_world_agent_session(
|
||||
"",
|
||||
RpgAgentStage::CollectingIntent,
|
||||
Some(r#"{"worldHook":"海雾会吞掉记错航线的人。"}"#),
|
||||
);
|
||||
|
||||
assert!(!custom_world_agent_session_has_direct_work_content(
|
||||
&empty_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&seeded_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&drafted_session,
|
||||
));
|
||||
assert!(custom_world_agent_session_has_direct_work_content(
|
||||
&profile_session,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_agent_draft_profile_candidate_requires_same_owner_active_draft_and_session() {
|
||||
let matching = CustomWorldProfile {
|
||||
|
||||
@@ -473,7 +473,8 @@ fn create_puzzle_agent_session_tx(
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
seed_text: input.seed_text.clone(),
|
||||
current_turn: 1,
|
||||
progress_percent: 18,
|
||||
// 中文注释:欢迎语和初始锚点推断不计入创作进度,新会话必须从 0% 开始。
|
||||
progress_percent: 0,
|
||||
stage: PuzzleAgentStage::CollectingAnchors,
|
||||
anchor_pack_json: serialize_json(&anchor_pack),
|
||||
draft_json: None,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("h")]
|
||||
[switch]$Help,
|
||||
[switch]$RunSmoke,
|
||||
[switch]$RunSpacetimeBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Usage {
|
||||
@(
|
||||
'Usage:',
|
||||
' ./server-rs/scripts/m7-preflight.ps1',
|
||||
' ./server-rs/scripts/m7-preflight.ps1 -RunSmoke',
|
||||
' ./server-rs/scripts/m7-preflight.ps1 -RunSpacetimeBuild',
|
||||
'',
|
||||
'Notes:',
|
||||
' 1. Run M7 cutover preflight checks for Rust backend',
|
||||
' 2. Default checks are non-destructive and do not publish or clear SpacetimeDB data',
|
||||
' 3. -RunSmoke starts a temporary api-server and verifies /healthz contract',
|
||||
' 4. -RunSpacetimeBuild requires spacetime CLI and only builds the module'
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Write-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$serverRsDir = Split-Path -Parent $scriptDir
|
||||
$repoRoot = Split-Path -Parent $serverRsDir
|
||||
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
|
||||
$modulePath = Join-Path $serverRsDir "crates\spacetime-module"
|
||||
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
throw "Missing server-rs/Cargo.toml, cannot start M7 preflight."
|
||||
}
|
||||
|
||||
Write-Host "[m7:preflight] repo root: $repoRoot"
|
||||
Write-Host "[m7:preflight] server-rs: $serverRsDir"
|
||||
|
||||
Push-Location $serverRsDir
|
||||
try {
|
||||
Write-Host "[m7:preflight] step: cargo check -p spacetime-module"
|
||||
cargo check -p spacetime-module --manifest-path $manifestPath
|
||||
|
||||
Write-Host "[m7:preflight] step: cargo check -p api-server"
|
||||
cargo check -p api-server --manifest-path $manifestPath
|
||||
|
||||
Write-Host "[m7:preflight] step: cargo test -p shared-contracts"
|
||||
cargo test -p shared-contracts --manifest-path $manifestPath
|
||||
|
||||
if ($RunSpacetimeBuild) {
|
||||
$spacetimeCommand = Get-Command spacetime -ErrorAction SilentlyContinue
|
||||
if ($null -eq $spacetimeCommand) {
|
||||
throw "Missing spacetime CLI, cannot run spacetime build."
|
||||
}
|
||||
|
||||
Write-Host "[m7:preflight] step: spacetime build --debug"
|
||||
Push-Location $modulePath
|
||||
try {
|
||||
& $spacetimeCommand.Source build --debug
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
if ($RunSmoke) {
|
||||
Write-Host "[m7:preflight] step: server-rs smoke"
|
||||
& (Join-Path $serverRsDir "scripts\smoke.ps1")
|
||||
}
|
||||
|
||||
Write-Host "[m7:preflight] all checks passed"
|
||||
Reference in New Issue
Block a user