diff --git a/packages/contracts/src/files.ts b/packages/contracts/src/files.ts index 14a3b558..1551a081 100644 --- a/packages/contracts/src/files.ts +++ b/packages/contracts/src/files.ts @@ -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; @@ -396,6 +400,25 @@ export const FileDeleteCheck = z.object({ }); export type FileDeleteCheck = z.infer; +/** 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; + export const TrashItem = z.object({ id: Id, rootId: FileRootId, @@ -414,6 +437,9 @@ export type TrashItem = z.infer; 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; 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"]), }); diff --git a/packages/contracts/test/db-sync.test.ts b/packages/contracts/test/db-sync.test.ts index 253b6860..a7635ae3 100644 --- a/packages/contracts/test/db-sync.test.ts +++ b/packages/contracts/test/db-sync.test.ts @@ -93,6 +93,8 @@ const ROWS: Record BrowserSession: { row: C.BrowserSession }, BrowserTab: { row: C.BrowserTab }, BrowserControlLease: { row: C.BrowserControlLease }, + // W5-A 文件管理回收站(0024) + TrashItem: { row: C.TrashItemRow }, }; /** 引擎自有表(@umt/tasks 内部使用,不经 API 暴露),不要求 Row schema */ diff --git a/packages/contracts/test/schemas.test.ts b/packages/contracts/test/schemas.test.ts index 370edba1..633870fb 100644 --- a/packages/contracts/test/schemas.test.ts +++ b/packages/contracts/test/schemas.test.ts @@ -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) => Record ({ ...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, diff --git a/packages/db/migrations/0024_trash.ts b/packages/db/migrations/0024_trash.ts new file mode 100644 index 00000000..cee79afe --- /dev/null +++ b/packages/db/migrations/0024_trash.ts @@ -0,0 +1,41 @@ +import type { Kysely } from "kysely"; +import { dropTables, entityTable, execDdl } from "../src/migration-helpers.ts"; + +// W5-A 文件管理(docs/plan/06-file-manager-spec.md §2.3.3): +// - trash_item:删除进回收站的记录(原根/原相对路径/回收站内路径/到期时间/关联资产快照); +// 还原或清除后删行(历史留在 audit_event)。root_id 为文件管理的根 id(lib: / dl::)。 +// - artifact_files_idem_key:文件管理写操作的幂等键(artifact title='files_idem',content.key)唯一。 + +// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 +export async function up(db: Kysely): Promise { + await execDdl( + db, + ` +${entityTable( + "trash_item", + ` + root_id text not null, + library_root_id uuid references library_root (id) on delete set null, + root_host_path text not null, + original_path text not null, + trash_path text not null, + is_dir boolean not null default false, + size_bytes bigint not null default 0, + actor text, + deleted_at timestamptz not null default now(), + expires_at timestamptz not null, + asset_snapshot jsonb, + constraint trash_item_trash_path_key unique (trash_path)`, +)} +create index trash_item_expires_idx on trash_item (expires_at); +create index trash_item_root_idx on trash_item (root_id, deleted_at desc); + +create unique index artifact_files_idem_key on artifact ((content->>'key')) where title = 'files_idem'; +`, + ); +} + +// biome-ignore lint/suspicious/noExplicitAny: 同上 +export async function down(db: Kysely): Promise { + await execDdl(db, `drop index if exists artifact_files_idem_key;\n${dropTables(["trash_item"])}`); +} diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index f5f52816..78325240 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -1276,6 +1276,23 @@ export interface TitleAliasTable { updated_at: GeneratedTimestamptz; } +export interface TrashItemTable { + id: Generated; + root_id: string; + library_root_id: string | null; + root_host_path: string; + original_path: string; + trash_path: string; + is_dir: Generated; + size_bytes: Generated; + actor: string | null; + deleted_at: GeneratedTimestamptz; + expires_at: Timestamptz; + asset_snapshot: Json | null; + created_at: GeneratedTimestamptz; + updated_at: GeneratedTimestamptz; +} + export interface DB { acquisition_plan: AcquisitionPlanTable; acquisition_slot: AcquisitionSlotTable; @@ -1338,6 +1355,7 @@ export interface DB { tasks_op_ledger: TasksOpLedgerTable; tasks_run_meta: TasksRunMetaTable; title_alias: TitleAliasTable; + trash_item: TrashItemTable; } export type AcquisitionPlanRow = Selectable; @@ -1523,3 +1541,6 @@ export type TasksRunMetaUpdate = Updateable; export type TitleAliasRow = Selectable; export type NewTitleAlias = Insertable; export type TitleAliasUpdate = Updateable; +export type TrashItemRow = Selectable; +export type NewTrashItem = Insertable; +export type TrashItemUpdate = Updateable; diff --git a/packages/db/test/schema.test.ts b/packages/db/test/schema.test.ts index bf6c9cb6..c5577a08 100644 --- a/packages/db/test/schema.test.ts +++ b/packages/db/test/schema.test.ts @@ -88,6 +88,8 @@ const EXPECTED_TABLES = [ "browser_session", "browser_tab", "browser_control_lease", + // W5-A 文件管理回收站(0024) + "trash_item", ].sort(); /** 不走 entityTable() 的表:追加型/引擎自有表,没有 uuid id + created_at/updated_at 三件套 */