feat: checkpoint m5 and bootstrap m6 asset flow
This commit is contained in:
@@ -1,16 +1,26 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[cfg(test)]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use module_ai::{AiTaskService, InMemoryAiTaskStore};
|
||||
use module_auth::{
|
||||
AuthUserService, InMemoryAuthStore, PasswordEntryService, PhoneAuthService,
|
||||
RefreshSessionService, WechatAuthService, WechatAuthStateService,
|
||||
};
|
||||
use module_runtime::RuntimeSnapshotRecord;
|
||||
#[cfg(test)]
|
||||
use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros};
|
||||
use platform_auth::{
|
||||
JwtConfig, JwtError, RefreshCookieConfig, RefreshCookieError, RefreshCookieSameSite,
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmConfig, LlmError};
|
||||
use platform_oss::{OssClient, OssConfig, OssError};
|
||||
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig};
|
||||
use serde_json::Value;
|
||||
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::wechat_provider::{WechatProvider, build_wechat_provider};
|
||||
@@ -35,6 +45,9 @@ pub struct AppState {
|
||||
ai_task_service: AiTaskService,
|
||||
spacetime_client: SpacetimeClient,
|
||||
llm_client: Option<LlmClient>,
|
||||
#[cfg(test)]
|
||||
// 测试环境允许在未启动 SpacetimeDB 时,用内存快照兜底当前 runtime story 回归链。
|
||||
test_runtime_snapshot_store: Arc<Mutex<HashMap<String, RuntimeSnapshotRecord>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -98,6 +111,8 @@ impl AppState {
|
||||
ai_task_service,
|
||||
spacetime_client,
|
||||
llm_client,
|
||||
#[cfg(test)]
|
||||
test_runtime_snapshot_store: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,6 +168,162 @@ impl AppState {
|
||||
pub fn llm_client(&self) -> Option<&LlmClient> {
|
||||
self.llm_client.as_ref()
|
||||
}
|
||||
|
||||
pub async fn get_runtime_snapshot_record(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<RuntimeSnapshotRecord>, SpacetimeClientError> {
|
||||
match self
|
||||
.spacetime_client
|
||||
.get_runtime_snapshot(user_id.clone())
|
||||
.await
|
||||
{
|
||||
Ok(record) => {
|
||||
#[cfg(test)]
|
||||
if let Some(snapshot) = record.as_ref() {
|
||||
self.cache_test_runtime_snapshot(snapshot.clone());
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
#[cfg(test)]
|
||||
Err(_) => Ok(self.read_test_runtime_snapshot(user_id.as_str())),
|
||||
#[cfg(not(test))]
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put_runtime_snapshot_record(
|
||||
&self,
|
||||
user_id: String,
|
||||
saved_at_micros: i64,
|
||||
bottom_tab: String,
|
||||
game_state: Value,
|
||||
current_story: Option<Value>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeSnapshotRecord, SpacetimeClientError> {
|
||||
match self
|
||||
.spacetime_client
|
||||
.put_runtime_snapshot(
|
||||
user_id.clone(),
|
||||
saved_at_micros,
|
||||
bottom_tab.clone(),
|
||||
game_state.clone(),
|
||||
current_story.clone(),
|
||||
updated_at_micros,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => {
|
||||
#[cfg(test)]
|
||||
self.cache_test_runtime_snapshot(record.clone());
|
||||
Ok(record)
|
||||
}
|
||||
#[cfg(test)]
|
||||
Err(_) => {
|
||||
let snapshot = self.build_test_runtime_snapshot_record(
|
||||
user_id,
|
||||
saved_at_micros,
|
||||
bottom_tab,
|
||||
game_state,
|
||||
current_story,
|
||||
updated_at_micros,
|
||||
)?;
|
||||
self.cache_test_runtime_snapshot(snapshot.clone());
|
||||
Ok(snapshot)
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_runtime_snapshot_record(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<bool, SpacetimeClientError> {
|
||||
match self
|
||||
.spacetime_client
|
||||
.delete_runtime_snapshot(user_id.clone())
|
||||
.await
|
||||
{
|
||||
Ok(deleted) => {
|
||||
#[cfg(test)]
|
||||
if deleted {
|
||||
self.remove_test_runtime_snapshot(user_id.as_str());
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
#[cfg(test)]
|
||||
Err(_) => Ok(self
|
||||
.remove_test_runtime_snapshot(user_id.as_str())
|
||||
.is_some()),
|
||||
#[cfg(not(test))]
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl AppState {
|
||||
fn cache_test_runtime_snapshot(&self, record: RuntimeSnapshotRecord) {
|
||||
self.test_runtime_snapshot_store
|
||||
.lock()
|
||||
.expect("test runtime snapshot store should lock")
|
||||
.insert(record.user_id.clone(), record);
|
||||
}
|
||||
|
||||
fn read_test_runtime_snapshot(&self, user_id: &str) -> Option<RuntimeSnapshotRecord> {
|
||||
self.test_runtime_snapshot_store
|
||||
.lock()
|
||||
.expect("test runtime snapshot store should lock")
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn remove_test_runtime_snapshot(&self, user_id: &str) -> Option<RuntimeSnapshotRecord> {
|
||||
self.test_runtime_snapshot_store
|
||||
.lock()
|
||||
.expect("test runtime snapshot store should lock")
|
||||
.remove(user_id)
|
||||
}
|
||||
|
||||
fn build_test_runtime_snapshot_record(
|
||||
&self,
|
||||
user_id: String,
|
||||
saved_at_micros: i64,
|
||||
bottom_tab: String,
|
||||
game_state: Value,
|
||||
current_story: Option<Value>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeSnapshotRecord, SpacetimeClientError> {
|
||||
let previous = self.read_test_runtime_snapshot(user_id.as_str());
|
||||
let game_state_json = serde_json::to_string(&game_state).map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!("测试快照 game_state 序列化失败: {error}"))
|
||||
})?;
|
||||
let current_story_json = current_story
|
||||
.as_ref()
|
||||
.map(serde_json::to_string)
|
||||
.transpose()
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!("测试快照 current_story 序列化失败: {error}"))
|
||||
})?;
|
||||
|
||||
Ok(RuntimeSnapshotRecord {
|
||||
user_id,
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
saved_at: format_utc_micros(saved_at_micros),
|
||||
saved_at_micros,
|
||||
bottom_tab,
|
||||
game_state,
|
||||
current_story,
|
||||
game_state_json,
|
||||
current_story_json,
|
||||
created_at_micros: previous
|
||||
.as_ref()
|
||||
.map(|record| record.created_at_micros)
|
||||
.unwrap_or(updated_at_micros),
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AppStateInitError {
|
||||
|
||||
Reference in New Issue
Block a user