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"]),
});
+2
View File
@@ -93,6 +93,8 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
BrowserSession: { row: C.BrowserSession },
BrowserTab: { row: C.BrowserTab },
BrowserControlLease: { row: C.BrowserControlLease },
// W5-A 文件管理回收站(0024)
TrashItem: { row: C.TrashItemRow },
};
/** 引擎自有表(@umt/tasks 内部使用,不经 API 暴露),不要求 Row schema */
+2
View File
@@ -33,6 +33,7 @@ const STRING_CANDIDATES = [
"catalog.lookup",
"测试 Test",
"123456",
`lib:${UUID}`,
];
const NUMBER_CANDIDATES = [1, 50, 2024, 0.5, 0, 4096, 128000];
@@ -88,6 +89,7 @@ const OVERRIDES: Record<string, (v: Record<string, unknown>) => Record<string, u
ExternalIdentityCreate: (v) => ({ ...v, contentUnitId: null }),
PathBindingCreate: (v) => ({ ...v, bindingKind: "season", seasonNumber: 1 }),
OrganizeRequest: (v) => ({ ...v, sourcePaths: undefined }),
FilesListQuery: (v) => ({ ...v, filter: "video,unidentified" }),
CatalogLookupInput: (v) => v,
IdentityResolveInput: (v) => v,
AssetLocationCreate: (v) => v,