feat(contracts): file manager API schemas and /files endpoint registry

Adds packages/contracts/src/files.ts (roots/list/stat, mkdir/rename,
move/copy/organize two-phase dryRun->planId, delete with reference checks
and trash, identify/scrape/probe) and registers Api.files in api.ts.
Implementation lands in a follow-up commit (W5-A).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 22:37:04 +08:00
1 parent 3665daaf07
commit b86f75c011
2 files changed
+702

No files matched your search

+126
View File
@@ -7,6 +7,7 @@ import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } fro
import * as content from "./content.ts";
import * as events from "./events.ts";
import * as execution from "./execution.ts";
import * as files from "./files.ts";
import * as identity from "./identity.ts";
import * as instances from "./instances.ts";
import * as ops from "./ops.ts";
@@ -1019,6 +1020,129 @@ export const Api = {
}),
},
// ── 文件管理(docs/plan/06-file-manager-spec.md;只浏览已登记根,写操作两段式 dryRun → planId) ──
files: {
roots: ep({
method: "get",
path: "/files/roots",
scope: "organize.read",
summary: "可浏览的根(库根 + 下载根),含实时健康与空间",
response: files.FileRootList,
}),
list: ep({
method: "get",
path: "/files",
scope: "organize.read",
summary: "列目录(分页/排序/过滤/截断,行内业务状态);根离线 503",
query: files.FilesListQuery,
response: files.FilesListResponse,
}),
stat: ep({
method: "get",
path: "/files/stat",
scope: "organize.read",
summary: "单项详情:realpath/inode/nlink/sidecars/路径绑定",
query: files.FileStatQuery,
response: files.FileStat,
}),
mkdir: ep({
method: "post",
path: "/files/mkdir",
scope: "organize.execute",
summary: "新建目录(非递归,已存在 409,仅 writable 根)",
body: files.FilesMkdirRequest,
response: files.FilesMkdirResult,
}),
rename: ep({
method: "post",
path: "/files/rename",
scope: "organize.execute",
summary: "同目录改名(不覆盖;同步 asset_location 与 path_binding 修订;做种中 409)",
body: files.FilesRenameRequest,
response: files.FilesRenameResult,
}),
move: ep({
method: "post",
path: "/files/move",
scope: "organize.execute",
summary: "移动/复制到目录:dryRun 返回 planId,执行回传 planId(源变更 409 plan_stale)",
body: files.FilesMoveRequest,
response: files.FilesTransferResult,
}),
copy: ep({
method: "post",
path: "/files/copy",
scope: "organize.execute",
summary: "复制到目录(同 /files/move mode=copy)",
body: files.FilesCopyRequest,
response: files.FilesTransferResult,
}),
organize: ep({
method: "post",
path: "/files/organize",
scope: "organize.execute",
summary: "按作品分组整理入库(每组走 planImport,与 /organization-records 同实现)",
body: files.FilesOrganizeRequest,
response: files.FilesOrganizeResult,
}),
delete: ep({
method: "post",
path: "/files/delete",
scope: "organize.execute",
summary: "删除(默认进回收站;引用检查:做种/资产/绑定;permanent 仅 admin 会话)",
body: files.FilesDeleteRequest,
response: files.FilesDeleteResult,
}),
trash: ep({
method: "get",
path: "/files/trash",
scope: "organize.read",
summary: "回收站列表",
query: files.FilesTrashQuery,
response: files.FilesTrashList,
}),
trashRestore: ep({
method: "post",
path: "/files/trash/restore",
scope: "organize.execute",
summary: "从回收站还原(原位被占的项失败 conflict)",
body: files.FilesTrashRestoreRequest,
response: files.FilesTrashRestoreResult,
}),
trashPurge: ep({
method: "post",
path: "/files/trash/purge",
scope: "admin",
summary: "清空回收站(缺省只清过期项;服务端另要求 admin 控制台会话,API Key 403)",
body: files.FilesTrashPurgeRequest,
response: files.FilesTrashPurgeResult,
}),
identify: ep({
method: "post",
path: "/files/identify",
scope: "organize.read",
summary: "识别(只读):解析 + 本地/TMDB 召回前 5 候选,不落库",
body: files.FilesIdentifyRequest,
response: files.FilesIdentifyResult,
}),
scrape: ep({
method: "post",
path: "/files/scrape",
scope: "organize.execute",
summary: "刮削已登记作品(字段级刷新,遵守字段锁;NFO 投影 W5)",
body: files.FilesScrapeRequest,
response: files.FilesScrapeResult,
}),
probe: ep({
method: "post",
path: "/files/probe",
scope: "organize.read",
summary: "ffprobe 摘要(≤20 项;已登记资产写回 media_info)",
body: files.FilesProbeRequest,
response: files.FilesProbeResult,
}),
},
// ── 能力 ──
capabilities: {
list: ep({
@@ -1291,3 +1415,5 @@ export function listEndpoints(): EndpointDef[] {
export type EndpointBody<E extends EndpointDef> = E["body"] extends z.ZodType ? z.input<E["body"]> : never;
export type EndpointResponse<E extends EndpointDef> = z.infer<E["response"]>;
export * from "./files.ts";