Enrich external API failure audit metadata

This commit is contained in:
kdletters
2026-05-28 15:42:46 +08:00
parent 2cd2b9704b
commit f1fb92aa29
40 changed files with 315 additions and 152 deletions

View File

@@ -227,3 +227,31 @@ export function parseApiErrorMessage(rawText: string, fallbackMessage: string) {
return rawText.trim() || fallbackMessage; return rawText.trim() || fallbackMessage;
} }
export function appendApiErrorRequestId(
message: string,
requestId: string | null | undefined,
) {
const trimmedMessage = message.trim() || '请求失败';
const trimmedRequestId =
typeof requestId === 'string' && requestId.trim()
? requestId.trim()
: '';
if (!trimmedRequestId || trimmedMessage.includes(trimmedRequestId)) {
return trimmedMessage;
}
return `${trimmedMessage}requestId: ${trimmedRequestId}`;
}
export function parseApiErrorMessageWithRequestId(
rawText: string,
fallbackMessage: string,
requestId: string | null | undefined,
) {
return appendApiErrorRequestId(
parseApiErrorMessage(rawText, fallbackMessage),
requestId,
);
}

View File

@@ -325,11 +325,15 @@ fn validate_admin_work_visibility(
) -> Result<(String, String, bool), AppError> { ) -> Result<(String, String, bool), AppError> {
let source_type = payload.source_type.trim().to_string(); let source_type = payload.source_type.trim().to_string();
if source_type.is_empty() { if source_type.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("sourceType 不能为空")); return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("sourceType 不能为空")
);
} }
let profile_id = payload.profile_id.trim().to_string(); let profile_id = payload.profile_id.trim().to_string();
if profile_id.is_empty() { if profile_id.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("profileId 不能为空")); return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("profileId 不能为空")
);
} }
Ok((source_type, profile_id, payload.visible)) Ok((source_type, profile_id, payload.visible))
} }

View File

@@ -658,7 +658,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn spacetime_unavailable_router_returns_service_unavailable_for_requests() { async fn spacetime_unavailable_router_returns_service_unavailable_for_requests() {
let app = build_spacetime_unavailable_router("SpacetimeDB 启动恢复认证快照超时".to_string()); let app =
build_spacetime_unavailable_router("SpacetimeDB 启动恢复认证快照超时".to_string());
let response = app let response = app
.oneshot( .oneshot(

View File

@@ -311,6 +311,7 @@ pub async fn generate_bark_battle_image_asset(
async { async {
generate_and_persist_bark_battle_image_asset( generate_and_persist_bark_battle_image_asset(
&state, &state,
&request_context,
&owner_user_id, &owner_user_id,
&slot, &slot,
draft_id.as_deref(), draft_id.as_deref(),
@@ -1197,6 +1198,7 @@ fn bark_battle_sanitize_path_segment(value: &str, fallback: &str) -> String {
async fn generate_and_persist_bark_battle_image_asset( async fn generate_and_persist_bark_battle_image_asset(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
slot: &BarkBattleAssetSlot, slot: &BarkBattleAssetSlot,
draft_id: Option<&str>, draft_id: Option<&str>,
@@ -1205,6 +1207,7 @@ async fn generate_and_persist_bark_battle_image_asset(
size: &str, size: &str,
) -> Result<BarkBattleGeneratedImageAsset, AppError> { ) -> Result<BarkBattleGeneratedImageAsset, AppError> {
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
&request_context,
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
Some(draft_id.unwrap_or(asset_id).to_string()), Some(draft_id.unwrap_or(asset_id).to_string()),
); );

View File

@@ -95,8 +95,12 @@ pub async fn generate_character_visual(
let result = async { let result = async {
let settings = require_openai_image_settings(&state)? let settings = require_openai_image_settings(&state)?
.with_external_api_audit_context(Some(owner_user_id.clone()), Some(character_id.clone())) .with_external_api_audit_context(
.with_external_api_audit_request_id(Some(request_context.request_id().to_string())); &request_context,
Some(owner_user_id.clone()),
Some(character_id.clone()),
)
;
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
state state
@@ -320,7 +324,7 @@ pub(crate) async fn generate_character_primary_visual_for_profile(
&model, &model,
&prompt, &prompt,
)?; )?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?.with_external_api_audit_actor(
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
Some(character_id.clone()), Some(character_id.clone()),
); );

View File

@@ -555,10 +555,10 @@ pub async fn generate_custom_world_scene_image(
async { async {
let settings = require_openai_image_settings(&state)? let settings = require_openai_image_settings(&state)?
.with_external_api_audit_context( .with_external_api_audit_context(
&request_context,
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
normalized.profile_id.clone(), normalized.profile_id.clone(),
) );
.with_external_api_audit_request_id(Some(request_context.request_id().to_string()));
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let reference_image = let reference_image =
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() { if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
@@ -680,7 +680,7 @@ pub(crate) async fn generate_custom_world_scene_image_for_profile(
}), }),
}; };
let normalized = normalize_scene_image_request(payload)?; let normalized = normalize_scene_image_request(payload)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?.with_external_api_audit_actor(
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
normalized.profile_id.clone(), normalized.profile_id.clone(),
); );
@@ -1021,10 +1021,10 @@ pub async fn generate_custom_world_opening_cg(
async { async {
let image_settings = require_openai_image_settings(&state)? let image_settings = require_openai_image_settings(&state)?
.with_external_api_audit_context( .with_external_api_audit_context(
&request_context,
Some(owner_user_id.clone()), Some(owner_user_id.clone()),
normalized.profile_id.clone(), normalized.profile_id.clone(),
) );
.with_external_api_audit_request_id(Some(request_context.request_id().to_string()));
let image_http_client = build_openai_image_http_client(&image_settings)?; let image_http_client = build_openai_image_http_client(&image_settings)?;
let video_settings = require_ark_video_settings(&state)?; let video_settings = require_ark_video_settings(&state)?;
let video_http_client = build_upstream_http_client(video_settings.request_timeout_ms)?; let video_http_client = build_upstream_http_client(video_settings.request_timeout_ms)?;

View File

@@ -8,10 +8,7 @@ pub(super) async fn generate_opening_cg_storyboard(
normalized: &NormalizedOpeningCgRequest, normalized: &NormalizedOpeningCgRequest,
reference_images: &[String], reference_images: &[String],
) -> Result<GeneratedOpeningCgStoryboard, AppError> { ) -> Result<GeneratedOpeningCgStoryboard, AppError> {
let audit_settings = settings.clone().with_external_api_audit_context( let audit_settings = settings.clone();
Some(owner_user_id.to_string()),
normalized.profile_id.clone(),
);
let generated = create_openai_image_generation( let generated = create_openai_image_generation(
http_client, http_client,
&audit_settings, &audit_settings,

View File

@@ -260,13 +260,28 @@ fn build_external_api_failure_metadata(failure: &ExternalApiFailureDraft) -> Val
if let Some(image_model) = failure.image_model { if let Some(image_model) = failure.image_model {
metadata["imageModel"] = json!(image_model); metadata["imageModel"] = json!(image_model);
} }
if let Some(user_id) = failure.user_id.as_deref().map(str::trim).filter(|value| !value.is_empty()) { if let Some(user_id) = failure
.user_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
metadata["userId"] = json!(truncate_field(user_id, 1_000)); metadata["userId"] = json!(truncate_field(user_id, 1_000));
} }
if let Some(profile_id) = failure.profile_id.as_deref().map(str::trim).filter(|value| !value.is_empty()) { if let Some(profile_id) = failure
.profile_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
metadata["profileId"] = json!(truncate_field(profile_id, 1_000)); metadata["profileId"] = json!(truncate_field(profile_id, 1_000));
} }
if let Some(request_id) = failure.request_id.as_deref().map(str::trim).filter(|value| !value.is_empty()) { if let Some(request_id) = failure
.request_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
metadata["requestId"] = json!(truncate_field(request_id, 1_000)); metadata["requestId"] = json!(truncate_field(request_id, 1_000));
} }
if let Some(source) = failure if let Some(source) = failure

View File

@@ -416,14 +416,14 @@ async fn maybe_generate_jump_hop_assets(
.map(|settings| { .map(|settings| {
settings settings
.with_external_api_audit_context( .with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
Some(profile_id.clone()), Some(profile_id.clone()),
) )
.with_external_api_audit_request_id(Some(request_context.request_id().to_string()))
}) })
.map_err(|error| { .map_err(|error| {
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error) jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
})?; })?;
let http_client = build_openai_image_http_client(&settings).map_err(|error| { let http_client = build_openai_image_http_client(&settings).map_err(|error| {
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error) jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
})?; })?;

View File

@@ -172,7 +172,9 @@ async fn run_server(config: AppConfig) -> Result<(), io::Error> {
build_spacetime_unavailable_router(message) build_spacetime_unavailable_router(message)
} }
Err(error) => { Err(error) => {
return Err(std::io::Error::other(format!("初始化应用状态失败:{error}"))); return Err(std::io::Error::other(format!(
"初始化应用状态失败:{error}"
)));
} }
}; };

View File

@@ -701,6 +701,7 @@ pub async fn generate_match3d_cover_image(
.await?; .await?;
let generated_cover = generate_match3d_cover_image_asset( let generated_cover = generate_match3d_cover_image_asset(
&state, &state,
&request_context,
&context.owner_user_id, &context.owner_user_id,
context.session_id.as_str(), context.session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -772,6 +773,7 @@ pub async fn generate_match3d_background_image_for_work(
async { async {
let generated_background = generate_match3d_background_image( let generated_background = generate_match3d_background_image(
&state, &state,
&request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
session_id.as_str(), session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -883,6 +885,7 @@ pub async fn generate_match3d_container_image_for_work(
async { async {
let generated_container = generate_match3d_container_image( let generated_container = generate_match3d_container_image(
&state, &state,
&request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
session_id.as_str(), session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),

View File

@@ -202,6 +202,7 @@ async fn generate_match3d_item_image_assets_in_batches(
async move { async move {
let material_sheet = generate_match3d_material_sheet_from_level_scene( let material_sheet = generate_match3d_material_sheet_from_level_scene(
state, state,
request_context,
owner_user_id, owner_user_id,
session_id, session_id,
profile_id, profile_id,
@@ -747,16 +748,19 @@ pub(super) struct Match3DSlicedItemImage {
async fn generate_match3d_material_sheet_from_level_scene( async fn generate_match3d_material_sheet_from_level_scene(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
config: &Match3DConfigJson, config: &Match3DConfigJson,
background_asset: Option<&Match3DGeneratedBackgroundAsset>, background_asset: Option<&Match3DGeneratedBackgroundAsset>,
) -> Result<Match3DMaterialSheet, AppError> { ) -> Result<Match3DMaterialSheet, AppError> {
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?
Some(owner_user_id.to_string()), .with_external_api_audit_context(
Some(profile_id.to_string()), request_context,
); Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let prompt = build_match3d_item_spritesheet_prompt(); let prompt = build_match3d_item_spritesheet_prompt();
let reference = load_match3d_level_scene_reference_image(state, background_asset).await?; let reference = load_match3d_level_scene_reference_image(state, background_asset).await?;

View File

@@ -214,6 +214,7 @@ pub(super) async fn ensure_match3d_background_asset(
let generated_background = generate_match3d_level_asset_bundle( let generated_background = generate_match3d_level_asset_bundle(
state, state,
request_context,
owner_user_id, owner_user_id,
session_id, session_id,
profile_id, profile_id,
@@ -260,6 +261,7 @@ pub(super) async fn resolve_or_generate_match3d_level_asset_bundle(
}; };
generate_match3d_level_asset_bundle( generate_match3d_level_asset_bundle(
state, state,
request_context,
owner_user_id, owner_user_id,
session_id, session_id,
profile_id, profile_id,
@@ -292,6 +294,7 @@ pub(super) fn build_match3d_item_slug(item_id: &str, item_name: &str) -> String
pub(super) async fn generate_match3d_cover_image_asset( pub(super) async fn generate_match3d_cover_image_asset(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -301,10 +304,12 @@ pub(super) async fn generate_match3d_cover_image_asset(
reference_image_srcs: Vec<String>, reference_image_srcs: Vec<String>,
) -> Result<Match3DAssetUpload, AppError> { ) -> Result<Match3DAssetUpload, AppError> {
require_match3d_oss_client(state)?; require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?
Some(owner_user_id.to_string()), .with_external_api_audit_context(
Some(profile_id.to_string()), request_context,
); Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let cover_prompt = build_match3d_cover_generation_prompt(config, prompt); let cover_prompt = build_match3d_cover_generation_prompt(config, prompt);
let generated = if let Some(uploaded_image) = resolve_match3d_reference_image_for_edit( let generated = if let Some(uploaded_image) = resolve_match3d_reference_image_for_edit(
@@ -425,6 +430,7 @@ pub(super) fn build_match3d_cover_reference_generation_prompt(
pub(super) async fn generate_match3d_background_image( pub(super) async fn generate_match3d_background_image(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -433,6 +439,7 @@ pub(super) async fn generate_match3d_background_image(
) -> Result<Match3DGeneratedBackgroundAsset, AppError> { ) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
generate_match3d_level_asset_bundle( generate_match3d_level_asset_bundle(
state, state,
request_context,
owner_user_id, owner_user_id,
session_id, session_id,
profile_id, profile_id,
@@ -444,6 +451,7 @@ pub(super) async fn generate_match3d_background_image(
pub(super) async fn generate_match3d_level_asset_bundle( pub(super) async fn generate_match3d_level_asset_bundle(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -451,10 +459,12 @@ pub(super) async fn generate_match3d_level_asset_bundle(
prompt: &str, prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> { ) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
require_match3d_oss_client(state)?; require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?
Some(owner_user_id.to_string()), .with_external_api_audit_context(
Some(profile_id.to_string()), request_context,
); Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let level_scene_prompt = build_match3d_level_scene_generation_prompt(config); let level_scene_prompt = build_match3d_level_scene_generation_prompt(config);
@@ -589,6 +599,7 @@ pub(super) async fn generate_match3d_level_asset_bundle(
pub(super) async fn generate_match3d_container_image( pub(super) async fn generate_match3d_container_image(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -596,10 +607,12 @@ pub(super) async fn generate_match3d_container_image(
prompt: &str, prompt: &str,
) -> Result<Match3DGeneratedBackgroundAsset, AppError> { ) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
require_match3d_oss_client(state)?; require_match3d_oss_client(state)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?
Some(owner_user_id.to_string()), .with_external_api_audit_context(
Some(profile_id.to_string()), request_context,
); Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let reference_image = load_match3d_container_reference_image()?; let reference_image = load_match3d_container_reference_image()?;
let container_prompt = build_match3d_container_generation_prompt(config, prompt); let container_prompt = build_match3d_container_generation_prompt(config, prompt);

View File

@@ -16,6 +16,7 @@ use crate::{
record_external_api_failure, record_external_api_failure,
}, },
http_error::AppError, http_error::AppError,
request_context::RequestContext,
state::AppState, state::AppState,
tracking::record_external_generation_run_after_success, tracking::record_external_generation_run_after_success,
}; };
@@ -258,7 +259,7 @@ pub(crate) fn build_openai_image_request_body(
} }
impl OpenAiImageSettings { impl OpenAiImageSettings {
pub(crate) fn with_external_api_audit_context( pub(crate) fn with_external_api_audit_actor(
mut self, mut self,
user_id: Option<String>, user_id: Option<String>,
profile_id: Option<String>, profile_id: Option<String>,
@@ -268,8 +269,15 @@ impl OpenAiImageSettings {
self self
} }
pub(crate) fn with_external_api_audit_request_id(mut self, request_id: Option<String>) -> Self { pub(crate) fn with_external_api_audit_context(
self.external_api_audit_request_id = request_id; mut self,
request_context: &RequestContext,
user_id: Option<String>,
profile_id: Option<String>,
) -> Self {
self.external_api_audit_user_id = user_id;
self.external_api_audit_profile_id = profile_id;
self.external_api_audit_request_id = Some(request_context.request_id().to_string());
self self
} }

View File

@@ -269,10 +269,10 @@ pub(crate) async fn generate_puzzle_ui_background_image(
) -> Result<GeneratedPuzzleUiBackgroundResponse, AppError> { ) -> Result<GeneratedPuzzleUiBackgroundResponse, AppError> {
let settings = require_openai_image_settings(state.root_state())? let settings = require_openai_image_settings(state.root_state())?
.with_external_api_audit_context( .with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
Some(session_id.to_string()), Some(session_id.to_string()),
) );
.with_external_api_audit_request_id(Some(request_context.request_id().to_string()));
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let generated = create_openai_image_generation( let generated = create_openai_image_generation(
&http_client, &http_client,
@@ -311,7 +311,11 @@ pub(crate) async fn generate_puzzle_level_asset_bundle(
puzzle_image: &PuzzleDownloadedImage, puzzle_image: &PuzzleDownloadedImage,
) -> Result<GeneratedPuzzleLevelAssetBundle, AppError> { ) -> Result<GeneratedPuzzleLevelAssetBundle, AppError> {
let settings = require_puzzle_vector_engine_settings(state)? let settings = require_puzzle_vector_engine_settings(state)?
.with_external_api_audit_request_id(Some(request_context.request_id().to_string())); .with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()),
Some(session_id.to_string()),
);
let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?; let http_client = build_puzzle_image_http_client(state, PuzzleImageModel::GptImage2)?;
let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image); let puzzle_reference = build_puzzle_downloaded_image_reference(puzzle_image);
let scene_generated = create_puzzle_vector_engine_image_generation( let scene_generated = create_puzzle_vector_engine_image_generation(

View File

@@ -109,13 +109,19 @@ impl PuzzleVectorEngineSettings {
} }
} }
pub(crate) fn with_external_api_audit_request_id( pub(crate) fn with_external_api_audit_context(
mut self, mut self,
request_id: Option<String>, request_context: &RequestContext,
user_id: Option<String>,
profile_id: Option<String>,
) -> Self { ) -> Self {
self.external_api_audit_request_id = request_id; self.external_api_audit_user_id = user_id;
self.external_api_audit_profile_id = profile_id;
self.external_api_audit_request_id =
Some(request_context.request_id().to_string());
self self
} }
} }
pub(crate) struct ParsedPuzzleImageDataUrl { pub(crate) struct ParsedPuzzleImageDataUrl {

View File

@@ -62,6 +62,7 @@ pub(super) async fn generate_square_hole_visual_assets_for_session(
_ => Some( _ => Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
&owner_user_id, &owner_user_id,
&session_id, &session_id,
profile_id.as_str(), profile_id.as_str(),
@@ -90,6 +91,7 @@ pub(super) async fn generate_square_hole_visual_assets_for_session(
_ => Some( _ => Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
&owner_user_id, &owner_user_id,
&session_id, &session_id,
profile_id.as_str(), profile_id.as_str(),
@@ -118,6 +120,7 @@ pub(super) async fn generate_square_hole_visual_assets_for_session(
option.image_src = Some( option.image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
&owner_user_id, &owner_user_id,
&session_id, &session_id,
profile_id.as_str(), profile_id.as_str(),
@@ -145,6 +148,7 @@ pub(super) async fn generate_square_hole_visual_assets_for_session(
option.image_src = Some( option.image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
&owner_user_id, &owner_user_id,
&session_id, &session_id,
profile_id.as_str(), profile_id.as_str(),
@@ -252,6 +256,7 @@ pub(super) async fn regenerate_square_hole_visual_asset_for_work(
work.cover_image_src = Some( work.cover_image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
synthetic_session_id.as_str(), synthetic_session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -271,6 +276,7 @@ pub(super) async fn regenerate_square_hole_visual_asset_for_work(
work.background_image_src = Some( work.background_image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
synthetic_session_id.as_str(), synthetic_session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -301,6 +307,7 @@ pub(super) async fn regenerate_square_hole_visual_asset_for_work(
option.image_src = Some( option.image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
synthetic_session_id.as_str(), synthetic_session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -331,6 +338,7 @@ pub(super) async fn regenerate_square_hole_visual_asset_for_work(
option.image_src = Some( option.image_src = Some(
generate_square_hole_image_data_url( generate_square_hole_image_data_url(
state, state,
request_context,
owner_user_id.as_str(), owner_user_id.as_str(),
synthetic_session_id.as_str(), synthetic_session_id.as_str(),
profile_id.as_str(), profile_id.as_str(),
@@ -380,6 +388,7 @@ pub(super) async fn regenerate_square_hole_visual_asset_for_work(
async fn generate_square_hole_image_data_url( async fn generate_square_hole_image_data_url(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -389,10 +398,12 @@ async fn generate_square_hole_image_data_url(
size: &str, size: &str,
failure_context: &str, failure_context: &str,
) -> Result<String, AppError> { ) -> Result<String, AppError> {
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?
Some(owner_user_id.to_string()), .with_external_api_audit_context(
Some(profile_id.to_string()), request_context,
); Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?; let http_client = build_openai_image_http_client(&settings)?;
let generated = create_openai_image_generation( let generated = create_openai_image_generation(
&http_client, &http_client,

View File

@@ -1042,7 +1042,9 @@ impl fmt::Display for AppStateInitError {
match self { match self {
Self::Jwt(error) => write!(f, "{error}"), Self::Jwt(error) => write!(f, "{error}"),
Self::RefreshCookie(error) => write!(f, "{error}"), Self::RefreshCookie(error) => write!(f, "{error}"),
Self::AuthStore(error) | Self::DependencyUnavailable(error) | Self::WechatPay(error) => { Self::AuthStore(error)
| Self::DependencyUnavailable(error)
| Self::WechatPay(error) => {
write!(f, "{error}") write!(f, "{error}")
} }
Self::SmsProvider(error) => write!(f, "{error}"), Self::SmsProvider(error) => write!(f, "{error}"),

View File

@@ -526,6 +526,7 @@ async fn maybe_generate_hit_object_asset(
let generated = generate_wooden_fish_image_assets( let generated = generate_wooden_fish_image_assets(
state, state,
request_context,
owner_user_id, owner_user_id,
session_id, session_id,
profile_id.as_str(), profile_id.as_str(),
@@ -659,6 +660,7 @@ struct WoodenFishGeneratedImageAssets {
async fn generate_wooden_fish_image_assets( async fn generate_wooden_fish_image_assets(
state: &AppState, state: &AppState,
request_context: &RequestContext,
owner_user_id: &str, owner_user_id: &str,
session_id: &str, session_id: &str,
profile_id: &str, profile_id: &str,
@@ -666,6 +668,7 @@ async fn generate_wooden_fish_image_assets(
hit_object_reference_image_src: Option<&str>, hit_object_reference_image_src: Option<&str>,
) -> Result<WoodenFishGeneratedImageAssets, AppError> { ) -> Result<WoodenFishGeneratedImageAssets, AppError> {
let settings = require_openai_image_settings(state)?.with_external_api_audit_context( let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
request_context,
Some(owner_user_id.to_string()), Some(owner_user_id.to_string()),
Some(profile_id.to_string()), Some(profile_id.to_string()),
); );

View File

@@ -98,7 +98,10 @@ pub fn should_rebind_orphan_work_owner(
return false; return false;
} }
!matches!(auth_user_service.get_user_by_id(&owner_user_id), Ok(Some(_))) !matches!(
auth_user_service.get_user_by_id(&owner_user_id),
Ok(Some(_))
)
} }
#[cfg(test)] #[cfg(test)]
@@ -137,6 +140,9 @@ mod tests {
assert!(should_rebind_orphan_work_owner(&service, "")); assert!(should_rebind_orphan_work_owner(&service, ""));
assert!(should_rebind_orphan_work_owner(&service, "user_missing")); assert!(should_rebind_orphan_work_owner(&service, "user_missing"));
assert!(!should_rebind_orphan_work_owner(&service, ORPHAN_WORK_OWNER_USER_ID)); assert!(!should_rebind_orphan_work_owner(
&service,
ORPHAN_WORK_OWNER_USER_ID
));
} }
} }

View File

@@ -807,12 +807,8 @@ impl AuthUserService {
display_name: &str, display_name: &str,
public_user_code: &str, public_user_code: &str,
) -> Result<AuthUser, PasswordEntryError> { ) -> Result<AuthUser, PasswordEntryError> {
self.store.ensure_orphan_work_owner_user( self.store
user_id, .ensure_orphan_work_owner_user(user_id, username, display_name, public_user_code)
username,
display_name,
public_user_code,
)
} }
pub fn get_user_by_id(&self, user_id: &str) -> Result<Option<AuthUser>, LogoutError> { pub fn get_user_by_id(&self, user_id: &str) -> Result<Option<AuthUser>, LogoutError> {
@@ -1019,18 +1015,14 @@ impl InMemoryAuthStore {
display_name: &str, display_name: &str,
public_user_code: &str, public_user_code: &str,
) -> Result<AuthUser, PasswordEntryError> { ) -> Result<AuthUser, PasswordEntryError> {
let user_id = normalize_required_string(user_id).ok_or_else(|| { let user_id = normalize_required_string(user_id)
PasswordEntryError::Store("孤儿作品占位用户 id 不能为空".to_string()) .ok_or_else(|| PasswordEntryError::Store("孤儿作品占位用户 id 不能为空".to_string()))?;
})?; let username = normalize_required_string(username)
let username = normalize_required_string(username).ok_or_else(|| { .ok_or_else(|| PasswordEntryError::Store("孤儿作品占位用户名不能为空".to_string()))?;
PasswordEntryError::Store("孤儿作品占位用户名不能为空".to_string()) let display_name = normalize_required_string(display_name)
})?; .ok_or_else(|| PasswordEntryError::Store("孤儿作品占位展示名不能为空".to_string()))?;
let display_name = normalize_required_string(display_name).ok_or_else(|| { let public_user_code = normalize_required_string(public_user_code)
PasswordEntryError::Store("孤儿作品占位展示名不能为空".to_string()) .ok_or_else(|| PasswordEntryError::Store("孤儿作品占位陶泥号不能为空".to_string()))?;
})?;
let public_user_code = normalize_required_string(public_user_code).ok_or_else(|| {
PasswordEntryError::Store("孤儿作品占位陶泥号不能为空".to_string())
})?;
let mut state = self let mut state = self
.inner .inner

View File

@@ -6,18 +6,17 @@ mod mapper;
mod telemetry; mod telemetry;
use mapper::*; use mapper::*;
pub use mapper::{ pub use mapper::{
AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord, AdminWorkVisibilityRecord, AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord,
AiTextChunkRecord, BarkBattleDraftConfigRecord, BarkBattleRunRecord, AiTaskStageRecord, AiTextChunkRecord, BarkBattleDraftConfigRecord, BarkBattleRunRecord,
BarkBattleRuntimeConfigRecord, BattleStateRecord, BigFishAgentMessageRecord, BarkBattleRuntimeConfigRecord, BattleStateRecord, BigFishAgentMessageRecord,
BigFishAnchorItemRecord, BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord, BigFishAssetCoverageRecord,
BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord,
BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput, BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput,
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput, BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput,
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput, BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
AdminWorkVisibilityRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord,
BigFishRuntimeRunRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
BigFishVector2Record, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, CreationEntryConfigRecord,
CreationEntryConfigRecord,
CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput,
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput,
CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput,

View File

@@ -115,9 +115,8 @@ pub use self::puzzle::{
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
}; };
pub use self::runtime::{ pub use self::runtime::{
AdminWorkVisibilityRecord, AdminWorkVisibilityRecord, BigFishGameDraftRecord, BigFishRuntimeEntityRecord,
BigFishGameDraftRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord, CreationEntryConfigRecord,
BigFishRuntimeRunRecord, CreationEntryConfigRecord,
}; };
pub use self::runtime_profile::{ pub use self::runtime_profile::{
SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleRunRecord, SquareHoleDropConfirmationRecord, SquareHoleDropFeedbackRecord, SquareHoleRunRecord,

View File

@@ -97,14 +97,15 @@ impl SpacetimeClient {
.into(); .into();
self.call_after_connect("admin_list_work_visibility", move |connection, sender| { self.call_after_connect("admin_list_work_visibility", move |connection, sender| {
connection connection.procedures().admin_list_work_visibility_then(
.procedures() procedure_input,
.admin_list_work_visibility_then(procedure_input, move |_, result| { move |_, result| {
let mapped = result let mapped = result
.map_err(SpacetimeClientError::from_sdk_error) .map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_admin_work_visibility_list_procedure_result); .and_then(map_admin_work_visibility_list_procedure_result);
send_once(&sender, mapped); send_once(&sender, mapped);
}); },
);
}) })
.await .await
} }
@@ -126,19 +127,17 @@ impl SpacetimeClient {
.map_err(SpacetimeClientError::validation_failed)? .map_err(SpacetimeClientError::validation_failed)?
.into(); .into();
self.call_after_connect( self.call_after_connect("admin_update_work_visibility", move |connection, sender| {
"admin_update_work_visibility", connection.procedures().admin_update_work_visibility_then(
move |connection, sender| { procedure_input,
connection move |_, result| {
.procedures() let mapped = result
.admin_update_work_visibility_then(procedure_input, move |_, result| { .map_err(SpacetimeClientError::from_sdk_error)
let mapped = result .and_then(map_admin_work_visibility_procedure_result);
.map_err(SpacetimeClientError::from_sdk_error) send_once(&sender, mapped);
.and_then(map_admin_work_visibility_procedure_result); },
send_once(&sender, mapped); );
}); })
},
)
.await .await
} }

View File

@@ -66,7 +66,10 @@ fn upsert_auth_snapshot_row(
.find(&snapshot_id) .find(&snapshot_id)
.is_some() .is_some()
{ {
ctx.db.auth_store_snapshot().snapshot_id().delete(&snapshot_id); ctx.db
.auth_store_snapshot()
.snapshot_id()
.delete(&snapshot_id);
} }
ctx.db.auth_store_snapshot().insert(AuthStoreSnapshot { ctx.db.auth_store_snapshot().insert(AuthStoreSnapshot {
@@ -106,7 +109,10 @@ fn auth_store_snapshot_wechat_row_id(provider_uid: &str, user_id: &str) -> Strin
} }
fn auth_store_snapshot_union_row_id(union_id: &str, user_id: &str) -> String { fn auth_store_snapshot_union_row_id(union_id: &str, user_id: &str) -> String {
prefixed_snapshot_id(AUTH_STORE_SNAPSHOT_UNION_PREFIX, &format!("{union_id}|{user_id}")) prefixed_snapshot_id(
AUTH_STORE_SNAPSHOT_UNION_PREFIX,
&format!("{union_id}|{user_id}"),
)
} }
fn snapshot_has_user_rows(snapshot: &PersistentAuthStoreSnapshot) -> bool { fn snapshot_has_user_rows(snapshot: &PersistentAuthStoreSnapshot) -> bool {
@@ -202,13 +208,7 @@ fn import_auth_store_snapshot_json_value_tx(
for stored_user in parsed.users_by_username.into_values() { for stored_user in parsed.users_by_username.into_values() {
let user = stored_user.user; let user = stored_user.user;
let user_id = user.id.clone(); let user_id = user.id.clone();
if ctx if ctx.db.user_account().user_id().find(&user_id).is_some() {
.db
.user_account()
.user_id()
.find(&user_id)
.is_some()
{
ctx.db.user_account().user_id().delete(&user_id); ctx.db.user_account().user_id().delete(&user_id);
} }
ctx.db.user_account().insert(UserAccount { ctx.db.user_account().insert(UserAccount {
@@ -644,10 +644,7 @@ mod tests {
PersistentAuthStoreSnapshot { PersistentAuthStoreSnapshot {
next_user_id: 43, next_user_id: 43,
users_by_username: std::collections::HashMap::from([( users_by_username: std::collections::HashMap::from([("phone_42".to_string(), user)]),
"phone_42".to_string(),
user,
)]),
phone_to_user_id: std::collections::HashMap::from([( phone_to_user_id: std::collections::HashMap::from([(
"+8613800008000".to_string(), "+8613800008000".to_string(),
"user_00000042".to_string(), "user_00000042".to_string(),

View File

@@ -2603,7 +2603,7 @@ fn is_same_agent_draft_profile_candidate(
) -> bool { ) -> bool {
row.owner_user_id == owner_user_id row.owner_user_id == owner_user_id
&& row.deleted_at.is_none() && row.deleted_at.is_none()
&& row.visible && row.visible
&& row.publication_status == CustomWorldPublicationStatus::Draft && row.publication_status == CustomWorldPublicationStatus::Draft
&& row.source_agent_session_id.as_deref() == Some(source_agent_session_id) && row.source_agent_session_id.as_deref() == Some(source_agent_session_id)
} }

View File

@@ -16,7 +16,7 @@ use crate::jump_hop::tables::{
jump_hop_agent_session, jump_hop_event, jump_hop_runtime_run, jump_hop_work_profile, jump_hop_agent_session, jump_hop_event, jump_hop_runtime_run, jump_hop_work_profile,
}; };
use crate::match3d::tables::{ use crate::match3d::tables::{
match3d_agent_message, match3d_agent_session, match3d_runtime_run, match_3_d_work_profile, match_3_d_work_profile, match3d_agent_message, match3d_agent_session, match3d_runtime_run,
}; };
use crate::puzzle::{ use crate::puzzle::{
puzzle_agent_message, puzzle_agent_session, puzzle_event, puzzle_leaderboard_entry, puzzle_agent_message, puzzle_agent_session, puzzle_event, puzzle_leaderboard_entry,

View File

@@ -1,13 +1,13 @@
pub mod analytics_date_dimension;
mod admin_work_visibility; mod admin_work_visibility;
pub mod analytics_date_dimension;
mod browse_history; mod browse_history;
pub mod creation_entry_config; pub mod creation_entry_config;
mod profile; mod profile;
mod settings; mod settings;
mod snapshots; mod snapshots;
pub use analytics_date_dimension::*;
pub use admin_work_visibility::*; pub use admin_work_visibility::*;
pub use analytics_date_dimension::*;
pub use browse_history::*; pub use browse_history::*;
pub use creation_entry_config::*; pub use creation_entry_config::*;
pub use profile::*; pub use profile::*;

View File

@@ -1,5 +1,5 @@
use crate::*;
use crate::puzzle::{PuzzleWorkProfileRow, puzzle_work_profile}; use crate::puzzle::{PuzzleWorkProfileRow, puzzle_work_profile};
use crate::*;
use module_custom_world::CustomWorldPublicationStatus; use module_custom_world::CustomWorldPublicationStatus;
use module_puzzle::PuzzlePublicationStatus; use module_puzzle::PuzzlePublicationStatus;
@@ -93,7 +93,9 @@ fn update_work_visibility_tx(
update_wooden_fish_work_visibility(ctx, &profile_id, input.visible) update_wooden_fish_work_visibility(ctx, &profile_id, input.visible)
} }
SOURCE_TYPE_MATCH3D => update_match3d_work_visibility(ctx, &profile_id, input.visible), SOURCE_TYPE_MATCH3D => update_match3d_work_visibility(ctx, &profile_id, input.visible),
SOURCE_TYPE_SQUARE_HOLE => update_square_hole_work_visibility(ctx, &profile_id, input.visible), SOURCE_TYPE_SQUARE_HOLE => {
update_square_hole_work_visibility(ctx, &profile_id, input.visible)
}
SOURCE_TYPE_VISUAL_NOVEL => { SOURCE_TYPE_VISUAL_NOVEL => {
update_visual_novel_work_visibility(ctx, &profile_id, input.visible) update_visual_novel_work_visibility(ctx, &profile_id, input.visible)
} }
@@ -158,7 +160,9 @@ fn puzzle_work_visibility_snapshot(row: &PuzzleWorkProfileRow) -> AdminWorkVisib
subtitle: "拼图关卡".to_string(), subtitle: "拼图关卡".to_string(),
cover_image_src: row.cover_image_src.clone(), cover_image_src: row.cover_image_src.clone(),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -234,7 +238,9 @@ fn custom_world_work_visibility_snapshot(row: &CustomWorldProfile) -> AdminWorkV
subtitle: row.subtitle.clone(), subtitle: row.subtitle.clone(),
cover_image_src: row.cover_image_src.clone(), cover_image_src: row.cover_image_src.clone(),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -287,7 +293,9 @@ fn jump_hop_work_visibility_snapshot(row: &JumpHopWorkProfileRow) -> AdminWorkVi
subtitle: "跳一跳".to_string(), subtitle: "跳一跳".to_string(),
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()), cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -342,7 +350,9 @@ fn wooden_fish_work_visibility_snapshot(
subtitle: "敲木鱼".to_string(), subtitle: "敲木鱼".to_string(),
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()), cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -395,7 +405,9 @@ fn match3d_work_visibility_snapshot(row: &Match3DWorkProfileRow) -> AdminWorkVis
subtitle: "抓大鹅".to_string(), subtitle: "抓大鹅".to_string(),
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()), cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -450,7 +462,9 @@ fn square_hole_work_visibility_snapshot(
subtitle: "方洞挑战".to_string(), subtitle: "方洞挑战".to_string(),
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()), cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -505,7 +519,9 @@ fn visual_novel_work_visibility_snapshot(
subtitle: "视觉小说".to_string(), subtitle: "视觉小说".to_string(),
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()), cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
visible: row.visible, visible: row.visible,
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()), published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
updated_at_micros: sort_time, updated_at_micros: sort_time,
} }
} }
@@ -544,10 +560,10 @@ fn update_big_fish_work_visibility(
Ok(snapshot) Ok(snapshot)
} }
fn big_fish_work_visibility_snapshot( fn big_fish_work_visibility_snapshot(row: &BigFishCreationSession) -> AdminWorkVisibilitySnapshot {
row: &BigFishCreationSession, let published_at = row
) -> AdminWorkVisibilitySnapshot { .published_at
let published_at = row.published_at.map(|value| value.to_micros_since_unix_epoch()); .map(|value| value.to_micros_since_unix_epoch());
let updated_at = timestamp_sort_micros(row.published_at, row.updated_at); let updated_at = timestamp_sort_micros(row.published_at, row.updated_at);
AdminWorkVisibilitySnapshot { AdminWorkVisibilitySnapshot {
source_type: SOURCE_TYPE_BIG_FISH.to_string(), source_type: SOURCE_TYPE_BIG_FISH.to_string(),

View File

@@ -3,6 +3,7 @@ import {
postApiJson, postApiJson,
} from '../../editor/shared/editorApiClient'; } from '../../editor/shared/editorApiClient';
import { import {
appendApiErrorRequestId,
fetchJson, fetchJson,
parseApiErrorMessage, parseApiErrorMessage,
} from '../../editor/shared/jsonClient'; } from '../../editor/shared/jsonClient';
@@ -265,7 +266,10 @@ export async function putCharacterRoleAssetWorkflow(
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'), appendApiErrorRequestId(
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
response.headers.get('x-request-id'),
),
); );
} }

View File

@@ -1,4 +1,9 @@
import { fetchJson, parseApiErrorMessage, saveJsonObject } from './jsonClient'; import {
appendApiErrorRequestId,
fetchJson,
parseApiErrorMessage,
saveJsonObject,
} from './jsonClient';
export const EDITOR_API_BASE_PATH = '/api/editor'; export const EDITOR_API_BASE_PATH = '/api/editor';
export const ASSETS_API_BASE_PATH = '/api/assets'; export const ASSETS_API_BASE_PATH = '/api/assets';
@@ -69,7 +74,7 @@ export async function postApiJson<T>(
const responseText = await response.text(); const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
} }
return responseText ? (JSON.parse(responseText) as T) : ({} as T); return responseText ? (JSON.parse(responseText) as T) : ({} as T);

View File

@@ -1,6 +1,7 @@
import { import {
API_RESPONSE_ENVELOPE_HEADER, API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION, API_RESPONSE_ENVELOPE_VERSION,
appendApiErrorRequestId,
parseApiErrorMessage, parseApiErrorMessage,
unwrapApiResponse, unwrapApiResponse,
} from '../../../packages/shared/src/http'; } from '../../../packages/shared/src/http';
@@ -17,7 +18,15 @@ export async function fetchJson<T>(
const responseText = await response.text(); const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, `${fallbackMessage}: ${response.status}`)); throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(
responseText,
`${fallbackMessage}: ${response.status}`,
),
response.headers.get('x-request-id'),
),
);
} }
return responseText return responseText
@@ -41,8 +50,13 @@ export async function saveJsonObject(
const responseText = await response.text(); const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, fallbackMessage),
response.headers.get('x-request-id'),
),
);
} }
} }
export { parseApiErrorMessage }; export { appendApiErrorRequestId, parseApiErrorMessage };

View File

@@ -14,7 +14,10 @@ import type {
GenerateCustomWorldProfileInput, GenerateCustomWorldProfileInput,
GenerateCustomWorldProfileOptions, GenerateCustomWorldProfileOptions,
} from '../../packages/shared/src/contracts/runtime'; } from '../../packages/shared/src/contracts/runtime';
import { parseApiErrorMessage } from '../../packages/shared/src/http'; import {
appendApiErrorRequestId,
parseApiErrorMessage,
} from '../../packages/shared/src/http';
import type { import type {
AIResponse, AIResponse,
Character, Character,
@@ -93,7 +96,12 @@ async function requestPlainTextStream(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, '流式请求失败')); throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, '流式请求失败'),
response.headers.get('x-request-id'),
),
);
} }
if (!response.body) { if (!response.body) {
@@ -488,7 +496,12 @@ export async function streamNpcChatTurn(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, 'NPC 聊天续写失败')); throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, 'NPC 聊天续写失败'),
response.headers.get('x-request-id'),
),
);
} }
if (!response.body) { if (!response.body) {

View File

@@ -547,6 +547,7 @@ describe('apiClient', () => {
routeVersion: 'runtime.v2', routeVersion: 'runtime.v2',
}, },
}); });
expect((capturedError as Error).message).toContain('requestId: req-body');
}); });
it('uses api error details.message as ApiClientError message', async () => { it('uses api error details.message as ApiClientError message', async () => {

View File

@@ -671,9 +671,14 @@ async function buildApiClientError(
) { ) {
const responseText = await response.text(); const responseText = await response.text();
const parsedError = parseApiErrorShape(responseText); const parsedError = parseApiErrorShape(responseText);
const requestId =
parsedError?.meta.requestId ??
response.headers.get(REQUEST_ID_HEADER) ??
undefined;
const baseMessage = parseApiErrorMessage(responseText, fallbackMessage);
return new ApiClientError({ return new ApiClientError({
message: parseApiErrorMessage(responseText, fallbackMessage), message: requestId ? `${baseMessage}requestId: ${requestId}` : baseMessage,
status: response.status, status: response.status,
code: parsedError?.code ?? `HTTP_${response.status || 0}`, code: parsedError?.code ?? `HTTP_${response.status || 0}`,
details: parsedError?.details ?? null, details: parsedError?.details ?? null,
@@ -682,10 +687,7 @@ async function buildApiClientError(
parsedError?.meta.apiVersion ?? parsedError?.meta.apiVersion ??
response.headers.get(API_VERSION_HEADER) ?? response.headers.get(API_VERSION_HEADER) ??
API_VERSION, API_VERSION,
requestId: requestId,
parsedError?.meta.requestId ??
response.headers.get(REQUEST_ID_HEADER) ??
undefined,
routeVersion: routeVersion:
parsedError?.meta.routeVersion ?? parsedError?.meta.routeVersion ??
response.headers.get(ROUTE_VERSION_HEADER) ?? response.headers.get(ROUTE_VERSION_HEADER) ??

View File

@@ -1,4 +1,7 @@
import { parseApiErrorMessage } from '../../packages/shared/src/http'; import {
appendApiErrorRequestId,
parseApiErrorMessage,
} from '../../packages/shared/src/http';
import { import {
ApiClientError, ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS, BACKGROUND_AUTH_REQUEST_OPTIONS,
@@ -338,7 +341,12 @@ export async function readAssetBytes(
if (!response.ok) { if (!response.ok) {
const message = await response const message = await response
.text() .text()
.then((text) => parseApiErrorMessage(text, '读取资源内容失败')) .then((text) =>
appendApiErrorRequestId(
parseApiErrorMessage(text, '读取资源内容失败'),
response.headers.get('x-request-id'),
),
)
.catch(() => ''); .catch(() => '');
throw new Error(message || '读取资源内容失败'); throw new Error(message || '读取资源内容失败');
} }

View File

@@ -1,4 +1,4 @@
import { parseApiErrorMessage } from '../../../packages/shared/src/http'; import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes'; import type { TextStreamOptions } from '../aiTypes';
import { import {
type ApiRetryOptions, type ApiRetryOptions,
@@ -64,7 +64,7 @@ async function openCreationAgentSsePost(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
} }
if (!response.body) { if (!response.body) {

View File

@@ -7,7 +7,7 @@ import type {
CreativeDraftEditStreamRequest, CreativeDraftEditStreamRequest,
StreamCreativeAgentMessageRequest, StreamCreativeAgentMessageRequest,
} from '../../../packages/shared/src/contracts/creativeAgent'; } from '../../../packages/shared/src/contracts/creativeAgent';
import { parseApiErrorMessage } from '../../../packages/shared/src/http'; import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes'; import type { TextStreamOptions } from '../aiTypes';
import { fetchWithApiAuth, requestJson } from '../apiClient'; import { fetchWithApiAuth, requestJson } from '../apiClient';
import { import {
@@ -42,7 +42,7 @@ async function openCreativeAgentSsePost(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
} }
if (!response.body) { if (!response.body) {

View File

@@ -1,4 +1,4 @@
import { parseApiErrorMessage } from '../../../packages/shared/src/http'; import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import { fetchWithApiAuth, requestJson } from '../apiClient'; import { fetchWithApiAuth, requestJson } from '../apiClient';
export async function requestRpgCreationPostJson<T>( export async function requestRpgCreationPostJson<T>(
@@ -32,7 +32,7 @@ export async function openRpgCreationSsePost(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
} }
if (!response.body) { if (!response.body) {

View File

@@ -16,7 +16,7 @@ import type {
VisualNovelStartRunRequest, VisualNovelStartRunRequest,
VisualNovelWorksResponse, VisualNovelWorksResponse,
} from '../../../packages/shared/src/contracts/visualNovel'; } from '../../../packages/shared/src/contracts/visualNovel';
import { parseApiErrorMessage } from '../../../packages/shared/src/http'; import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes'; import type { TextStreamOptions } from '../aiTypes';
import { import {
type ApiRetryOptions, type ApiRetryOptions,
@@ -100,7 +100,7 @@ async function openVisualNovelRuntimeSsePost(
if (!response.ok) { if (!response.ok) {
const responseText = await response.text(); const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage)); throw new Error(appendApiErrorRequestId(parseApiErrorMessage(responseText, fallbackMessage), response.headers.get('x-request-id')));
} }
if (!response.body) { if (!response.body) {