From 777e4f9cee3e4ce7d03a4528bca9c31d44200dff Mon Sep 17 00:00:00 2001 From: john-walks-slow Date: Tue, 29 Sep 2026 16:43:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(flow):=20=E4=BF=AE=E5=A4=8D=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=8F=96=E5=AA=92=E4=BD=93=E5=9C=B0=E5=9D=80=E7=94=A8?= =?UTF-8?q?=E9=94=99=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 视频生成成功但取媒体地址失败: ``` HTTP 502 {"error":{"code":502,"message":"视频生成成功但获取媒体地址失败: Flow frontend RPC rejected: rpc=as29s, code=[5]","status":"UNAVAILABLE"}} ``` 生成本身成功,挂在 as29s 这一步。 与 #182 那两处不同:这次参数结构没有错。as29s 的 argument 仍是 单元素 UUID 列表,bl / f.sid / source-path 也都与前端一致。错的是值—— as29s 按「生成操作 id」索引,而 _resolve_video_asset 的取值优先级把 mediaName 排在了第一位。 一个 operation 里同时存在两个不同 UUID: - mediaGenerationId / operation.name —— as29s 要的是这个 - mediaName —— 传它就 NOT_FOUND 2x2 组合实测(同一 media,仅换 id 与 source-path): | 传入 id | source-path | 结果 | |---|---|---| | mediaName | /project/{id} | code=[5] | | mediaName | /project/{id}/edit/{media} | code=[5] | | operation.name | /project/{id} | 返回 fifeUrl | | operation.name | /project/{id}/edit/{media} | 返回 fifeUrl | 可见 source-path 两种都通,唯一的变量就是 id。 附带收益:video_media_id 的兜底值也一并修正。返回的 URL 形如 /video/,故 extend:// 续写引用的正是该 id, 此前兜底会给出一个取不到媒体的 mediaName。 定位手法(可复用,无需重跑生成):把 operation dump 出来,拿到全部候选 UUID,再直接对 as29s 打点组合。比重跑一次视频生成快一个量级。 --- src/services/generation_handler.py | 8 ++-- tests/test_video_asset_resolution.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/test_video_asset_resolution.py diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index 28ee00d0..118dd722 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -1438,12 +1438,14 @@ async def _resolve_video_asset( str(operation.get("projectId") or video_info.get("projectId") or "").strip() or None ) + # as29s indexes by generation operation id, not mediaName; the two are + # distinct UUIDs and the wrong one comes back as code=[5] (NOT_FOUND). media_name = ( - operation.get("mediaName") - or video_info.get("mediaName") - or video_info.get("mediaGenerationId") + video_info.get("mediaGenerationId") or operation.get("name") or (operation.get("operation") or {}).get("name") + or operation.get("mediaName") + or video_info.get("mediaName") ) video_url = "" diff --git a/tests/test_video_asset_resolution.py b/tests/test_video_asset_resolution.py new file mode 100644 index 00000000..b590a560 --- /dev/null +++ b/tests/test_video_asset_resolution.py @@ -0,0 +1,61 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock + +from src.services.generation_handler import GenerationHandler + + +MEDIA_NAME = "4a7b00b4-a60f-44d2-a5fa-c0a4883c5fcc" +GENERATION_ID = "a1607a19-a932-4c31-bb39-b5f6283f1eee" +VIDEO_URL = f"https://flow-content.google/video/{GENERATION_ID}" + + +def _operation(): + return { + "status": "MEDIA_GENERATION_STATUS_SUCCESSFUL", + "projectId": "86955db9-5925-4e7b-b7ed-a3e9c5302c92", + "mediaName": MEDIA_NAME, + "name": GENERATION_ID, + "operation": { + "name": GENERATION_ID, + "metadata": { + "video": { + "mediaName": MEDIA_NAME, + "mediaGenerationId": GENERATION_ID, + "aspectRatio": "VIDEO_ASPECT_RATIO_LANDSCAPE", + } + }, + }, + } + + +class VideoAssetResolutionTests(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.handler = GenerationHandler.__new__(GenerationHandler) + self.handler.flow_client = MagicMock() + self.handler.flow_client.get_media_url_redirect = AsyncMock(return_value=VIDEO_URL) + + async def test_resolves_media_by_generation_id_not_media_name(self): + """as29s 按生成操作 id 索引;传 mediaName 会返回 code=[5]。""" + resolved = await self.handler._resolve_video_asset(MagicMock(), _operation()) + + self.handler.flow_client.get_media_url_redirect.assert_awaited_once() + _, passed_id = self.handler.flow_client.get_media_url_redirect.await_args.args + self.assertEqual(passed_id, GENERATION_ID) + self.assertNotEqual(passed_id, MEDIA_NAME) + self.assertEqual(resolved["video_url"], VIDEO_URL) + self.assertEqual(resolved["video_media_id"], GENERATION_ID) + + async def test_falls_back_to_operation_name_when_metadata_missing(self): + operation = _operation() + operation["operation"]["metadata"] = {} + operation.pop("name") + + resolved = await self.handler._resolve_video_asset(MagicMock(), operation) + + _, passed_id = self.handler.flow_client.get_media_url_redirect.await_args.args + self.assertEqual(passed_id, GENERATION_ID) + self.assertEqual(resolved["video_url"], VIDEO_URL) + + +if __name__ == "__main__": + unittest.main()