AGC 作者预览切到 owner 媒体读路由

- resolve_preview_url 改走 /api/game-distribution/my-games/{gameId}/media/read-url,不再走素材库 /api/assets/read-url
- 新增 owner_media_read_url,复用 endpoint 的路径段转义并编码 objectKey
- 新增路由与编码单测,钉住不再回退素材库 ACL
This commit is contained in:
2026-10-07 10:27:03 +08:00
parent 069df704fd
commit 4cd7843134
@@ -585,11 +585,27 @@ fn clear_publication_binding(root: &Path) {
});
}
fn asset_read_url(api_base_url: &str, object_key: &str) -> Result<String, String> {
let mut url = Url::parse(&format!(
"{}/api/assets/read-url",
api_base_url.trim_end_matches('/')
))
/// owner 作用域媒体读地址:`/api/game-distribution/my-games/{gameId}/media/read-url`。
///
/// 不能再走 `/api/assets/read-url`:发布媒体是项目快照桶对象、不建 `asset_object`,素材库 ACL
/// 不会给它授权;owner 路由按「objectKey 命中调用者自己作品当前行的媒体」放行,未发布 / 被驳回
/// 的封面与截图也能预览。
fn owner_media_read_url(
snapshot: &PlatformSessionSnapshot,
game_id: &str,
object_key: &str,
) -> Result<String, String> {
let mut url = Url::parse(&endpoint(
snapshot,
&[
"api",
"game-distribution",
"my-games",
game_id,
"media",
"read-url",
],
)?)
.map_err(|_| "陶泥儿服务地址无效".to_string())?;
url.query_pairs_mut().append_pair("objectKey", object_key);
Ok(url.to_string())
@@ -599,13 +615,14 @@ fn asset_read_url(api_base_url: &str, object_key: &str) -> Result<String, String
async fn resolve_preview_url(
client: &reqwest::Client,
snapshot: &PlatformSessionSnapshot,
game_id: &str,
object_key: Option<&str>,
) -> Option<String> {
let object_key = object_key?.trim();
if object_key.is_empty() {
return None;
}
let read_url = asset_read_url(&snapshot.api_base_url, object_key).ok()?;
let read_url = owner_media_read_url(snapshot, game_id, object_key).ok()?;
match crate::assets::resolve_external_asset_signed_url(
client,
snapshot.access_token.as_str(),
@@ -797,7 +814,13 @@ async fn build_publication_draft(
let cover = if cover_asset_id.is_some() || cover_object_key.is_some() {
Some(GameDistributionPublicationImageDraft {
asset_id: normalize_asset_id(cover_asset_id),
preview_url: resolve_preview_url(client, snapshot, cover_object_key.as_deref()).await,
preview_url: resolve_preview_url(
client,
snapshot,
entry.id.as_str(),
cover_object_key.as_deref(),
)
.await,
})
} else {
None
@@ -806,7 +829,13 @@ async fn build_publication_draft(
for (asset_id, object_key) in screenshots {
screenshot_drafts.push(GameDistributionPublicationImageDraft {
asset_id: normalize_asset_id(asset_id),
preview_url: resolve_preview_url(client, snapshot, object_key.as_deref()).await,
preview_url: resolve_preview_url(
client,
snapshot,
entry.id.as_str(),
object_key.as_deref(),
)
.await,
});
}
GameDistributionPublicationDraft {
@@ -1499,6 +1528,33 @@ mod tests {
assert!(endpoint(&snapshot, &["api", "bad?query"]).is_err());
}
#[test]
fn owner_media_read_url_targets_owner_route_and_encodes_object_key() {
let snapshot = PlatformSessionSnapshot {
user_id: "user-1".to_string(),
access_token: "token".to_string(),
api_base_url: "https://dev.genarrative.world".to_string(),
identity_generation: 1,
revision: 1,
};
let url = owner_media_read_url(
&snapshot,
"game-1",
"agc/project-snapshots/v1/game-distribution/media/game-1/cover-1.png",
)
.expect("owner media read url");
assert!(url.starts_with(
"https://dev.genarrative.world/api/game-distribution/my-games/game-1/media/read-url?"
));
assert!(
url.contains("objectKey=agc%2Fproject-snapshots%2Fv1%2F"),
"objectKey 必须按查询参数编码:{url}"
);
// 作者预览不再回退到素材库 ACL 路由:发布媒体不是 asset_object。
assert!(!url.contains("/api/assets/read-url"));
assert!(owner_media_read_url(&snapshot, "bad/id", "cover.png").is_err());
}
#[test]
fn response_data_unwraps_v1_envelope_and_legacy_payload() {
assert_eq!(