docs(runbook): file manager v2 backend gaps G1-G9

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:08:18 +08:00
1 parent 13132e7ad8
commit f0b3a10564
1 file changed
+14 -2
+14 -2
View File
@@ -37,12 +37,24 @@
| scrape | 仅有 active 资产的作品;入队 `refreshMetadata`(字段锁优先,不动路径),返回 `lockedFields`;`writeNfo/images` 目前返回 `not_implemented` |
| download | `GET /files/download?root=&path=&inline=`:仅管理员会话(API Key 含 admin scope 也 403);只允许 NFO/字幕/图片(视频 403),≤ `FILES_DOWNLOAD_MAX_BYTES`(默认 20 MiB,超出 413 `too_large:`);路径守卫同其它接口,打开前再 realpath 复核、按文件句柄 stat;流式返回(非 JSON 信封),`inline=true` 仅对图片生效(SVG 一律附件),带 `nosniff` + `CSP sandbox`;写审计 `files.download` |
## 文件管理 v2 后端缺口(W9-H,`docs/design/file-manager-v2.md` §15)
| # | 接口 | 要点 |
|---|---|---|
| G1 | `POST /files/entity-from-candidate {provider:"tmdb", externalId, kind:"movie"\|"tv"}`;`/files/organize` 项 `external`(与 `mediaEntityId` 二选一,都给/都不给 400) | 取 TMDB 详情建 `media_entity` + `external_identity`(accepted,`evidence_level=manual`,IMDb 同时登记)+ 别名;剧集按 TMDB 季表建季/集单元(provisional,单季 ≤200 集)。幂等:同 TMDB id 已有 accepted 映射直接复用(跟随合并),并发冲突重试一次。organize 预览阶段就地建(同一请求内同一候选只建一次),组里 `entity{provider,externalId,created,title}`。TMDB 未配置 400、无此条目 404、上游失败 502(`upstream:`)。服务在 `sources-identity/service/entity-from-candidate.ts`(`createEntityFromTmdbCandidate`),TMDB 客户端经 `SourcesRuntime.tmdb()`(设置 `metadata.tmdb*`) |
| G2 | organize `minSizeBytes`;identify `minSizeBytes` | 缺省取 `settings.organize.minSizeBytes`(设置契约加字段前恒缺省 → organize 用端口 `minVideoBytes` 50 MiB,identify 不过滤)。organize:低于阈值的视频进组 `excluded[]`,`reason=below_min_size`(sample/no_episode 等照原因名);identify:该项 `skipped="below_min_size"`、不查候选,结果 `excludedBySize` 计数;每项带 `size` |
| G3 | organize `onExisting: skip\|conflict\|replace_with_verify`(缺省 conflict) | 透传 `planImport.onExisting`;选 replace 即视为人工授权(`grantReplace`),执行走现有 replace 分支(新文件核验通过后旧文件进回收站)。预览逐文件 `action`(transfer/replace/skip/hold)+ `existing`(same 同一文件已在库 / skip / replace / conflict / null)+ `skipReason`;conflict 时该组 `error.code=plan_blocked`。策略进请求指纹,换策略须重新预览 |
| G4 | organize `naming: template\|keep`(缺省 template) | keep = 保留源文件名(目录仍按模板/绑定/targetDir;改名后撞名 → hold + duplicate_target)。`planImport` 新参数 `namingMode` / `namingSource`,冻结计划 `organization_record.plan.naming = {mode, templateSource: entity\|root\|global\|override, templateVersion}`(模板来源按 作品 > 库根 > 全局 回落;keep = override)。组响应同名 `naming` + `targetSegments[{text, role: root\|season\|dir, bound}]`(bound = 沿用已有路径绑定) |
| G5 | identify 项 `records{count,lastRecordId,lastStatus}`;`GET /files/record-sources?recordId=`;organize `refreshMetadata`;`POST /files/scrape-batch` | 整理记录按 `organization_file_result.source_path` 回查;record-sources 把记录主视频源(多个且同在源目录下时给目录)按登记目录 hostPath 换算成 FileRef,不在登记目录/已不存在的进 `unresolved`。`refreshMetadata=true`:执行成功的组按作品去重各排一次 `refreshMetadata`(组里 `refreshTaskId`)。scrape-batch:`items`(文件/目录 → 其下已登记资产的作品)∪ `mediaEntityIds` 去重,≤200 个作品;无 active 资产 `not_registered`、不存在 `not_found`、文件不属于任何作品 `no_entity`;带幂等键 |
| G8 | `GET /files/tree?root=&path=` | 目录摘要:`parent`(根为 null)、子目录 `hasChildren`(隐藏点目录与临时文件)、本目录递归视频数(上限 `dirSummaryMax`,超出 `partial`)与已登记位置数 |
| G9 | `POST /files/suggest-name {root, path, mediaEntityId?}` | 按命名模板渲染推荐名;命名引擎接入前返回 **501** `not_implemented:` |
## 授权矩阵(实现)
| 操作 | 管理员会话 / DEV_ADMIN_KEY | API Key |
|---|---|---|
| roots / list / stat / identify / trash 列表 / records | ✓ | `organize.read` |
| mkdir / rename / copy / organize(copy) / restore / scrape / probe | ✓ | `organize.execute` |
| roots / list / stat / identify / trash 列表 / records / tree / record-sources / suggest-name | ✓ | `organize.read` |
| mkdir / rename / copy / organize(copy) / restore / scrape / scrape-batch / probe / entity-from-candidate | ✓ | `organize.execute` |
| move / organize(move) | ✓ | `organize.execute` + **每个源**所属作品(声明作品 ∪ 源下已登记资产的作品)都被持续授权覆盖,或整请求一次性审批;任一不覆盖 → 整体 403 `approval_required` 并列出未授权项;无法归属作品的源只认审批 |
| delete(回收站) | ✓ | `organize.execute` + 每次一次性审批(dryRun 的 planId) |
| download / trash purge | 仅会话(DEV key 403) | ✗ 403(含 admin scope) |