Skip to content

fix: 适配 MoviePilot v3 的统一响应 envelope(仪表盘数值为 0 / 详情页仍 422) - #98

Open
sokouu87 wants to merge 1 commit into
singleton-altman:masterfrom
sokouu87:fix/v3-response-envelope
Open

fix: 适配 MoviePilot v3 的统一响应 envelope(仪表盘数值为 0 / 详情页仍 422)#98
sokouu87 wants to merge 1 commit into
singleton-altman:masterfrom
sokouu87:fix/v3-response-envelope

Conversation

@sokouu87

Copy link
Copy Markdown

#97#97 修好了媒体身份契约,但用户在真实 v3 上实测仍然:详情页 422、仪表盘所有数值显示 0
根因是另一个 #97 完全没覆盖的破坏性变更。

根因

v3 用 ResponseAPIRouter 把几乎所有 JSON 响应统一包装成 envelope:

{"success": true, "message": "", "data": <原来的裸响应>}

对比 v2 与 v3 的 openapi.json266 个端点从裸响应变成了 envelope。实测(携带合法 token):

GET /api/v1/dashboard/cpu      → {"success":true,"message":"","data":7.1}
GET /api/v1/dashboard/network  → {"success":true,"message":"","data":[43885,44009]}
GET /api/v1/media/source       → {"success":true,"message":"","data":[{...}]}
GET /api/v1/media/550?media_source=themoviedb&type_name=电影
                               → {"success":true,"message":"","data":{"title":"搏击俱乐部",...}}

网页端正常,是因为前端 src/api/client.ts 里有统一的 envelope 拦截器;App 没有这一层。

两个现象由此而来:

  1. 仪表盘全 0 —— dashboard/* 在 v2 返回裸 float / 裸 List / 裸对象,App 把 Map 当数值解析,全部回落默认值。
  2. 详情页仍 422 —— fix: 兼容 MoviePilot v3 媒体身份契约,修复 v3 服务端下详情页 422 #97 的版本探测判据是「/api/v1/media/source 返回 200 且 body 是 List」,
    而这个探测端点自身也被包成了 envelope(Map),判据永不成立 → 探测回落 v2 → 继续按 v2 形态请求详情。

改动

只有 5 个文件,解包逻辑集中在 ApiClient 的一处拦截器里,业务层零散落:

data 类型 处理 原因
null 保持原 envelope 操作型接口(Response_NoneType_),现有代码正是读 success 判断成败
Map 合并原始字段,再 putIfAbsentsuccess / message / data 让期待裸对象、读 success、读 data.id 三类调用方同时可用;业务同名字段优先,不被 envelope 覆盖
List 解包成裸 List 期待裸列表的调用方占绝大多数
标量 解包 例如 dashboard/cpu7.1
  • 仅在探测到 v3 时介入,v2 全程不解包,行为零变更。
  • 版本探测请求带 skipV3EnvelopeUnwrap 标记跳过解包(避免自我递归),判据放宽为「200 且能取到来源列表」。
  • 用 openapi 交叉核对了全部读 success 的调用点,修正三处「v3 返回 List 而代码读 success」的:
    message/agent/sessionssite/userdata/{site_id}dashboard/memory(v3 由 [used, usage] 变为对象),
    均改为兼容两种形态。

验证

flutter analyze:0 error,改动文件无新增 warning。

另外写了一个联机集成测试,直接打真实 v3 服务端,用 App 自己的解析链路(normalizeMediaJson +
MediaDetail.fromJson + StatisticModel + SubscribeItem + HttpPathBuilderUtil)逐项断言,
10 项全部通过

详情 OK: 绿灯军团 (2026) seasons=1 prefix=tmdb
统计 OK: movie=1996 tv=310 episode=10207
CPU OK: 7.9  网络 OK: [24621, 24885]
订阅 OK: 无敌少侠 tmdbid=95557 mediaid=tmdb:95557
推荐项 path OK: douban:38581618
旧形态对照 OK: 422 {success: false, message: 请求参数不正确,
                   data: [{location: [query, media_source], message: Field required}]}

最后一条是回归对照:旧形态请求确实仍返回 422,证明 v3 分支走的是新形态。

测试文件是临时联机验证用的(依赖真实服务端与 token),没有包含在这个 PR 里。

🤖 Generated with Claude Code

v3 用 ResponseAPIRouter 把几乎所有 JSON 响应统一包装成
{"success": ..., "message": ..., "data": <原始响应>},共有 266 个端点
从 v2 的裸响应变成了 envelope。App 没有对应的解包层,导致:

- 仪表盘所有数值显示 0:dashboard/* 在 v2 返回裸 float / 裸 List / 裸对象,
  v3 变成 envelope,App 直接把 Map 当数值解析,全部回落默认值。
- 媒体详情页仍然 422:版本探测请求 /api/v1/media/source 的判据是
  「200 且响应是 List」,而该端点自身也被包成了 envelope,判据永不成立,
  探测结果回落 v2,于是继续按 v2 形态请求详情。

改动:

- ApiClient 增加唯一一处 v3 envelope 解包拦截器。data 为 null 时保持原
  envelope(操作型接口依赖 success 判断成败);data 为 Map 时合并原始字段
  并按需补 success / message / data(业务同名字段优先,不被覆盖);
  data 为 List 或标量时直接解包。仅在探测到 v3 时介入,v2 全程不解包。
- 版本探测请求跳过解包并放宽判据,避免自我递归与误判。
- 修正三处「v3 返回 List 而 App 读 success」的调用点,使其兼容两种形态。

联机验证(真实 v3 服务端):媒体详情、季列表、订阅状态与订阅列表、
仪表盘 CPU / 网络 / 统计、推荐项详情 path 构造共 10 项断言全部通过;
旧形态请求作为对照仍返回 422。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant