feat(db,contracts): trash_item migration and file manager contract refinements

- 0024_trash: trash_item table + unique idempotency index on artifact(files_idem)
- files.ts: TrashItemRow, delete planId/planExpiresAt, organize group warnings,
  scrape returns refreshMetadata taskId
- registry tests list the new table/row schema

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 23:02:14 +08:00
1 parent 673cb3920a
commit 09f6e3a3e5
6 files changed
+98 -2

No files matched your search

+30 -2
View File
@@ -350,6 +350,8 @@ export const FilesOrganizeGroup = z.object({
truncated: z.boolean(),
recordId: Id.nullable(),
taskId: Id.nullable(),
/** 暂未生效的逐项覆写(season/episodes/slotId 等待 planImport 扩 fileOverrides) */
warnings: z.array(z.string()),
/** 该组失败原因(plan_stale / 授权 / H&R 等),其他组不受影响 */
error: z.object({ code: z.string(), message: z.string() }).nullable(),
});
@@ -376,6 +378,8 @@ export const FilesDeleteRequest = z.object({
unbind: z.boolean().default(false),
/** 永久删除二次确认:必须等于每项文件名(单项时) */
confirmName: z.string().max(255).optional(),
/** dryRun 返回的 planId;API Key 执行时必填(一次性审批绑定该计划),会话可省略 */
planId: Id.optional(),
});
export type FilesDeleteRequest = z.infer<typeof FilesDeleteRequest>;
@@ -396,6 +400,25 @@ export const FileDeleteCheck = z.object({
});
export type FileDeleteCheck = z.infer<typeof FileDeleteCheck>;
/** trash_item 表行(db-sync 对照用;API 用下方 TrashItem 投影) */
export const TrashItemRow = z.object({
id: Id,
rootId: z.string(),
libraryRootId: Id.nullable(),
rootHostPath: z.string(),
originalPath: z.string(),
trashPath: z.string(),
isDir: z.boolean(),
sizeBytes: NonNegInt,
actor: z.string().nullable(),
deletedAt: Timestamp,
expiresAt: Timestamp,
assetSnapshot: JsonObject.nullable(),
createdAt: Timestamp,
updatedAt: Timestamp,
});
export type TrashItemRow = z.infer<typeof TrashItemRow>;
export const TrashItem = z.object({
id: Id,
rootId: FileRootId,
@@ -414,6 +437,9 @@ export type TrashItem = z.infer<typeof TrashItem>;
export const FilesDeleteResult = z.object({
dryRun: z.boolean(),
/** dryRun 生成的删除计划(执行时回传;审批绑定) */
planId: Id.nullable(),
planExpiresAt: Timestamp.nullable(),
mode: z.enum(["trash", "permanent"]),
checks: z.array(FileDeleteCheck),
/** 执行后:进入回收站的项(permanent 为空) */
@@ -527,9 +553,11 @@ export const FilesScrapeRequest = z.object({
export type FilesScrapeRequest = z.infer<typeof FilesScrapeRequest>;
export const FilesScrapeResult = z.object({
mediaEntityId: Id,
changedFields: z.array(z.string()),
/** refreshMetadata 引擎 run id(异步;字段级刷新,用户锁优先,不动路径) */
taskId: z.string(),
/** 当前被用户锁定、刷新不会改动的字段 */
lockedFields: z.array(z.string()),
/** NFO/图片投影:W5 实现前恒为 not_implemented */
/** NFO/图片投影:W5 实现前请求时为 not_implemented */
nfo: z.enum(["written", "skipped", "not_requested", "not_implemented"]),
images: z.enum(["written", "skipped", "not_requested", "not_implemented"]),
});