Update spacetime-client bindings and frontend

Large update across server and web clients: regenerated/added many spacetime-client module bindings and input types (including new delete/work_delete input types and numerous procedure/reducer files), updates to server-rs API modules (bark_battle, jump_hop, wooden_fish, auth, module-runtime and shared contracts), and fixes in module-runtime behavior and domain logic. Frontend changes include new/updated components and tests (creative audio helpers, bark-battle/jump-hop/wooden-fish clients and views, unified generation pages, RPG entry views, and runtime shells), plus CSS and service updates. Documentation and operational notes updated (.hermes pitfalls and multiple PRD/docs) to cover daily-task refresh, banner asset fallback, recommend-key bug, and other platform behaviors. Tests and verification steps added/updated alongside these changes.
This commit is contained in:
2026-06-04 22:44:19 +08:00
parent 2678954627
commit 27b30f974b
326 changed files with 4374 additions and 2539 deletions

View File

@@ -57,10 +57,14 @@ pub struct AuthUser {
pub display_name: String,
#[serde(default)]
pub avatar_url: Option<String>,
#[serde(default)]
pub phone_number: Option<String>,
pub phone_number_masked: Option<String>,
pub login_method: AuthLoginMethod,
pub binding_status: AuthBindingStatus,
pub wechat_bound: bool,
#[serde(default)]
pub wechat_account: Option<String>,
pub token_version: u64,
#[serde(default)]
pub created_at: String,

View File

@@ -97,6 +97,24 @@ struct StoredWechatIdentity {
session_key: Option<String>,
}
fn hydrate_private_auth_fields(
state: &InMemoryAuthStoreState,
stored_user: &StoredPasswordUser,
) -> StoredPasswordUser {
let mut hydrated = stored_user.clone();
if hydrated.user.phone_number.is_none() {
hydrated.user.phone_number = hydrated.phone_number.clone();
}
if hydrated.user.wechat_account.is_none() {
hydrated.user.wechat_account = state
.wechat_identity_by_provider_uid
.values()
.find(|identity| identity.user_id == hydrated.user.id)
.map(|identity| identity.provider_uid.clone());
}
hydrated
}
#[derive(Clone, Debug)]
pub struct PasswordEntryService {
store: InMemoryAuthStore,
@@ -1037,7 +1055,7 @@ impl InMemoryAuthStore {
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == user_id)
.cloned())
.map(|stored_user| hydrate_private_auth_fields(&state, stored_user)))
}
fn ensure_orphan_work_owner_user(
@@ -1077,10 +1095,12 @@ impl InMemoryAuthStore {
username: username.clone(),
display_name,
avatar_url: None,
phone_number: None,
phone_number_masked: None,
login_method: AuthLoginMethod::Password,
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
wechat_account: None,
token_version: 1,
created_at,
};
@@ -1111,7 +1131,7 @@ impl InMemoryAuthStore {
.users_by_username
.values()
.find(|stored_user| stored_user.user.public_user_code == public_user_code)
.cloned())
.map(|stored_user| hydrate_private_auth_fields(&state, stored_user)))
}
fn find_by_phone_number(
@@ -1129,7 +1149,7 @@ impl InMemoryAuthStore {
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == *user_id)
.cloned())
.map(|stored_user| hydrate_private_auth_fields(&state, stored_user)))
}
fn find_by_phone_number_for_password(
@@ -1147,7 +1167,7 @@ impl InMemoryAuthStore {
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == *user_id)
.cloned())
.map(|stored_user| hydrate_private_auth_fields(&state, stored_user)))
}
fn update_user_profile(
@@ -1217,10 +1237,12 @@ impl InMemoryAuthStore {
username: username.clone(),
display_name,
avatar_url: None,
phone_number: Some(phone_number.e164.clone()),
phone_number_masked: Some(phone_number.masked_national_number.clone()),
login_method: AuthLoginMethod::Phone,
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
wechat_account: None,
token_version: 1,
created_at,
};
@@ -1276,10 +1298,12 @@ impl InMemoryAuthStore {
username: username.clone(),
display_name,
avatar_url: None,
phone_number: Some(phone_number.e164.clone()),
phone_number_masked: Some(phone_number.masked_national_number.clone()),
login_method: AuthLoginMethod::Password,
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
wechat_account: None,
token_version: 1,
created_at,
};
@@ -1326,16 +1350,19 @@ impl InMemoryAuthStore {
.unwrap_or("微信旅人")
.to_string();
let username = build_wechat_username(&display_name, &profile.provider_uid);
let provider_uid = normalize_required_string(&profile.provider_uid).unwrap_or_default();
let user = AuthUser {
id: user_id.clone(),
public_user_code,
username: username.clone(),
display_name,
avatar_url: avatar_url.clone(),
phone_number: None,
phone_number_masked: None,
login_method: AuthLoginMethod::Wechat,
binding_status: AuthBindingStatus::PendingBindPhone,
wechat_bound: true,
wechat_account: Some(provider_uid.clone()),
token_version: 1,
created_at,
};
@@ -1350,7 +1377,7 @@ impl InMemoryAuthStore {
);
let identity = StoredWechatIdentity {
user_id: user_id.clone(),
provider_uid: normalize_required_string(&profile.provider_uid).unwrap_or_default(),
provider_uid,
provider_union_id: normalize_optional_string(profile.provider_union_id),
display_name: normalize_optional_string(profile.display_name),
avatar_url,
@@ -1388,7 +1415,7 @@ impl InMemoryAuthStore {
.values()
.find(|stored_user| stored_user.user.id == *user_id)
{
return Ok(Some(stored.user.clone()));
return Ok(Some(hydrate_private_auth_fields(&state, stored).user));
}
let Some(identity) = state
@@ -1401,7 +1428,7 @@ impl InMemoryAuthStore {
.users_by_username
.values()
.find(|stored_user| stored_user.user.id == identity.user_id)
.map(|stored| stored.user.clone()))
.map(|stored| hydrate_private_auth_fields(&state, stored).user))
}
fn get_wechat_identity_by_user_id(
@@ -1490,6 +1517,7 @@ impl InMemoryAuthStore {
{
stored_user.user.display_name = display_name.to_string();
}
stored_user.user.wechat_account = Some(next_provider_uid.clone());
stored_user.user.clone()
};
self.persist_wechat_state(&state)?;
@@ -1724,6 +1752,7 @@ impl InMemoryAuthStore {
.find(|identity| identity.user_id == pending_user_id)
.cloned()
.ok_or(PhoneAuthError::UserStateMismatch)?;
let pending_wechat_account = pending_wechat_identity.provider_uid.clone();
let pending_username = state
.users_by_username
@@ -1752,6 +1781,10 @@ impl InMemoryAuthStore {
.find(|stored| stored.user.id == target_user_id)
.ok_or(PhoneAuthError::UserNotFound)?;
target_user.user.wechat_bound = true;
target_user.user.wechat_account = Some(pending_wechat_account);
if target_user.user.phone_number.is_none() {
target_user.user.phone_number = target_user.phone_number.clone();
}
let next_user = target_user.user.clone();
self.persist_phone_state(&state)?;
@@ -1761,15 +1794,24 @@ impl InMemoryAuthStore {
state
.phone_to_user_id
.insert(phone_number.e164.clone(), pending_user_id.to_string());
let bound_wechat_account = state
.wechat_identity_by_provider_uid
.values()
.find(|identity| identity.user_id == pending_user_id)
.map(|identity| identity.provider_uid.clone());
let stored_user = state
.users_by_username
.values_mut()
.find(|stored| stored.user.id == pending_user_id)
.ok_or(PhoneAuthError::UserNotFound)?;
stored_user.user.phone_number = Some(phone_number.e164.clone());
stored_user.user.phone_number_masked = Some(phone_number.masked_national_number.clone());
stored_user.user.binding_status = AuthBindingStatus::Active;
stored_user.user.wechat_bound = true;
if stored_user.user.wechat_account.is_none() {
stored_user.user.wechat_account = bound_wechat_account;
}
stored_user.phone_number = Some(phone_number.e164);
let next_user = stored_user.user.clone();
self.persist_phone_state(&state)?;