再次合并 master

合入 origin/master 最新后端、OSS 与认证链路调整。

保留本枝架构收口修改并合并 Hermes 决策记录。

通过 typecheck、编码检查、Spacetime schema guard 与 api-server cargo check。
This commit is contained in:
2026-06-07 22:52:45 +08:00
51 changed files with 786 additions and 1206 deletions

View File

@@ -982,6 +982,7 @@ pub(crate) fn attach_selected_puzzle_candidate_to_levels(
}
}
#[cfg(test)]
pub(crate) fn resolve_puzzle_initial_ui_background_prompt(
draft: &PuzzleResultDraftRecord,
target_level: &PuzzleDraftLevelRecord,
@@ -1047,6 +1048,7 @@ pub(crate) fn build_puzzle_ui_background_generation_prompt(
)
}
#[cfg(test)]
pub(crate) fn attach_puzzle_level_ui_background(
levels: &mut [PuzzleDraftLevelRecord],
level_id: &str,
@@ -1088,27 +1090,6 @@ pub(crate) fn attach_puzzle_level_asset_bundle(
level.ui_background_image_object_key = Some(generated.level_background.object_key);
}
pub(crate) async fn generate_puzzle_initial_ui_background_required(
state: &PuzzleApiState,
request_context: &RequestContext,
owner_user_id: &str,
session_id: &str,
draft: &PuzzleResultDraftRecord,
target_level: &PuzzleDraftLevelRecord,
) -> Result<(String, GeneratedPuzzleUiBackgroundResponse), AppError> {
let prompt = resolve_puzzle_initial_ui_background_prompt(draft, target_level);
let generated = generate_puzzle_ui_background_image(
state,
request_context,
owner_user_id,
session_id,
target_level.level_name.as_str(),
prompt.as_str(),
)
.await?;
Ok((prompt, generated))
}
pub(crate) async fn generate_puzzle_level_asset_bundle_required(
state: &PuzzleApiState,
request_context: &RequestContext,

View File

@@ -396,49 +396,6 @@ pub(super) fn map_puzzle_work_summary_response(
}
}
pub(super) fn map_puzzle_gallery_card_response(
state: &PuzzleApiState,
item: PuzzleGalleryCardRecord,
) -> PuzzleWorkSummaryResponse {
let author = resolve_puzzle_work_author_by_user_id(
state,
&item.owner_user_id,
Some(&item.author_display_name),
None,
);
PuzzleWorkSummaryResponse {
work_id: item.work_id,
profile_id: item.profile_id,
owner_user_id: item.owner_user_id,
source_session_id: item.source_session_id,
author_display_name: author.display_name,
work_title: item.work_title,
work_description: item.work_description,
level_name: item.level_name,
summary: item.summary,
theme_tags: item.theme_tags,
cover_image_src: item.cover_image_src,
cover_asset_id: item.cover_asset_id,
publication_status: item.publication_status,
updated_at: item.updated_at,
published_at: item.published_at,
play_count: item.play_count,
remix_count: item.remix_count,
like_count: item.like_count,
recent_play_count_7d: item.recent_play_count_7d,
point_incentive_total_half_points: item.point_incentive_total_half_points,
point_incentive_claimed_points: item.point_incentive_claimed_points,
point_incentive_total_points: item.point_incentive_total_half_points as f64 / 2.0,
point_incentive_claimable_points: item
.point_incentive_total_half_points
.saturating_div(2)
.saturating_sub(item.point_incentive_claimed_points),
publish_ready: item.publish_ready,
generation_status: item.generation_status,
levels: Vec::new(),
}
}
pub(super) fn map_public_work_puzzle_gallery_card_response(
state: &PuzzleApiState,
item: spacetime_client::PublicWorkGalleryEntryRecord,

View File

@@ -44,7 +44,6 @@ fn puzzle_vector_engine_create_request_never_embeds_reference_image() {
mime_type: "image/png".to_string(),
bytes_len: cursor.get_ref().len(),
bytes: cursor.into_inner(),
signed_read_url: None,
};
let body = build_puzzle_vector_engine_image_request_body(
@@ -197,15 +196,11 @@ fn puzzle_ui_spritesheet_postprocess_turns_green_screen_transparent() {
}
#[test]
fn puzzle_vector_engine_create_request_never_embeds_signed_reference_url() {
fn puzzle_vector_engine_create_request_never_embeds_reference_payload() {
let reference_image = PuzzleResolvedReferenceImage {
mime_type: "image/png".to_string(),
bytes_len: 4,
bytes: b"test".to_vec(),
signed_read_url: Some(
"https://oss.example/generated-puzzle-assets/reference.png?x-oss-signature=abc"
.to_string(),
),
};
let body = build_puzzle_vector_engine_image_request_body(
@@ -639,7 +634,6 @@ fn puzzle_uploaded_cover_can_reuse_resolved_history_image() {
mime_type: "image/png".to_string(),
bytes_len: 8,
bytes: b"pngbytes".to_vec(),
signed_read_url: None,
};
let downloaded = PuzzleDownloadedImage::from_resolved_reference_image(resolved);

View File

@@ -45,7 +45,6 @@ pub(crate) struct PuzzleResolvedReferenceImage {
pub(crate) mime_type: String,
pub(crate) bytes_len: usize,
pub(crate) bytes: Vec<u8>,
pub(crate) signed_read_url: Option<String>,
}
pub(crate) struct GeneratedPuzzleImageCandidate {
@@ -318,10 +317,10 @@ pub(crate) fn build_puzzle_downloaded_image_reference(
mime_type: image.mime_type.clone(),
bytes_len: image.bytes.len(),
bytes: image.bytes.clone(),
signed_read_url: None,
}
}
#[cfg(test)]
pub(crate) fn build_puzzle_vector_engine_image_request_body(
image_model: PuzzleImageModel,
prompt: &str,
@@ -330,7 +329,7 @@ pub(crate) fn build_puzzle_vector_engine_image_request_body(
candidate_count: u32,
reference_image: Option<&PuzzleResolvedReferenceImage>,
) -> Value {
let body = Map::from_iter([
let body = serde_json::Map::from_iter([
(
"model".to_string(),
Value::String(image_model.request_model_name().to_string()),
@@ -415,32 +414,6 @@ pub(crate) fn collect_puzzle_reference_image_sources(
sources
}
pub(crate) fn collect_legacy_puzzle_reference_image_sources(
legacy_reference_image_src: Option<&str>,
reference_image_srcs: &[String],
) -> Vec<String> {
let mut sources = Vec::new();
for source in legacy_reference_image_src
.into_iter()
.chain(reference_image_srcs.iter().map(String::as_str))
{
let normalized = source.trim();
if normalized.is_empty() {
continue;
}
if !sources
.iter()
.any(|existing: &String| existing == normalized)
{
sources.push(normalized.to_string());
}
if sources.len() >= PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT {
break;
}
}
sources
}
pub(crate) fn has_puzzle_reference_images(
legacy_reference_image_src: Option<&str>,
reference_image_srcs: &[String],
@@ -463,6 +436,7 @@ pub(crate) fn should_use_puzzle_reference_image_generation(
use_reference_image_generation && has_puzzle_reference_image(reference_image_src)
}
#[cfg(test)]
pub(crate) fn build_puzzle_vector_engine_prompt(prompt: &str, negative_prompt: &str) -> String {
let prompt = prompt.trim();
let negative_prompt = negative_prompt.trim();
@@ -525,7 +499,6 @@ pub(crate) async fn resolve_puzzle_reference_image(
mime_type: parsed.mime_type,
bytes_len,
bytes: parsed.bytes,
signed_read_url: None,
});
}
@@ -758,7 +731,6 @@ async fn download_signed_puzzle_reference_image(
mime_type,
bytes_len,
bytes: body.to_vec(),
signed_read_url: Some(signed_read_url),
})
}
@@ -1075,47 +1047,6 @@ pub(crate) fn decode_puzzle_base64(value: &str) -> Option<Vec<u8>> {
Some(output)
}
pub(crate) fn find_first_puzzle_string_by_key(payload: &Value, target_key: &str) -> Option<String> {
let mut results = Vec::new();
collect_puzzle_strings_by_key(payload, target_key, &mut results);
results.into_iter().next()
}
pub(crate) fn collect_puzzle_strings_by_key(
payload: &Value,
target_key: &str,
results: &mut Vec<String>,
) {
match payload {
Value::Array(entries) => {
for entry in entries {
collect_puzzle_strings_by_key(entry, target_key, results);
}
}
Value::Object(object) => {
for (key, value) in object {
if key == target_key {
collect_puzzle_string_values(value, results);
}
collect_puzzle_strings_by_key(value, target_key, results);
}
}
_ => {}
}
}
pub(crate) fn collect_puzzle_string_values(payload: &Value, results: &mut Vec<String>) {
match payload {
Value::String(text) => results.push(text.to_string()),
Value::Array(items) => {
for item in items {
collect_puzzle_string_values(item, results);
}
}
_ => {}
}
}
pub(crate) fn normalize_puzzle_downloaded_image_mime_type(content_type: &str) -> String {
let mime_type = content_type
.split(';')