import { z } from "zod"; import * as classification from "../classification.ts"; import { cursorPage, IdParam } from "../common.ts"; import * as files from "../files.ts"; import * as organization from "../organization.ts"; import * as organizeSources from "../organize-sources.ts"; import { ep } from "./shared.ts"; export const organizeApi = { // ── 整理(公开合同) ── organization: { list: ep({ method: "get", path: "/organization-records", scope: "organize.read", summary: "整理记录列表", query: organization.OrganizationListQuery, response: cursorPage(organization.PublicOrganizationRecord), }), get: ep({ method: "get", path: "/organization-records/{id}", scope: "organize.read", summary: "整理记录详情(含逐文件结果)", params: IdParam, response: organization.PublicOrganizationRecord, }), changes: ep({ method: "get", path: "/organization-records/changes", scope: "organize.read", summary: "按 changeSeq 增量读取(含既有记录状态更新)", query: organization.OrganizationChangesQuery, response: organization.OrganizationChangesResponse, }), stats: ep({ method: "get", path: "/organization-records/stats", scope: "organize.read", summary: "整理记录按状态计数(筛选同列表)", query: organization.OrganizationStatsQuery, response: organization.OrganizationStats, }), remove: ep({ method: "post", path: "/organization-records/delete", scope: "organize.execute", summary: "删除整理记录,可选把源文件 / 媒体库文件移入回收站(dryRun 只核对)", body: organization.OrganizationDeleteRequest, response: organization.OrganizationDeleteResult, }), dismissAttention: ep({ method: "post", path: "/organization-records/{id}/dismiss-attention", scope: "organize.execute", summary: "忽略这条整理记录的问题:不再算需要处理,关联的未关闭事项一并关闭", params: IdParam, response: organization.PublicOrganizationRecord, }), agentIdentify: ep({ method: "post", path: "/organization-records/{id}/agent-identify", scope: "organize.execute", summary: "让智能体识别这条整理记录的作品(没识别出作品的记录),认出来自动重新整理", params: IdParam, response: organization.OrganizationAgentIdentifyResult, }), create: ep({ method: "post", path: "/organization-records", scope: "organize.execute", summary: "手动发起整理", body: organization.OrganizeRequest, response: organization.OrganizeAccepted, }), }, // ── 文件管理(docs/runbook/files.md;只浏览已登记根,写操作两段式 dryRun → planId) ── classification: { get: ep({ method: "get", path: "/classification", scope: "organize.read", summary: "分类配置", response: classification.ClassificationConfigView, }), put: ep({ method: "put", path: "/classification", scope: "admin", summary: "保存分类配置", body: classification.ClassificationPut, response: classification.ClassificationConfigView, }), preview: ep({ method: "post", path: "/classification/preview", scope: "organize.read", summary: "测试分类规则", body: classification.ClassificationPreviewInput, response: classification.ClassificationPreviewResult, }), }, organizeSources: { list: ep({ method: "get", path: "/organize-sources", scope: "organize.read", summary: "整理来源", response: organizeSources.OrganizeSourceList, }), save: ep({ method: "put", path: "/organize-sources/{id}", scope: "admin", summary: "保存整理来源", params: IdParam, body: organizeSources.OrganizeSourceInput, response: organizeSources.OrganizeSourceView, }), remove: ep({ method: "delete", path: "/organize-sources/{id}", scope: "admin", summary: "删除整理来源", params: IdParam, response: z.object({ deleted: z.boolean() }), }), stock: ep({ method: "post", path: "/organize-sources/{id}/stock", scope: "organize.execute", summary: "扫描存量文件", params: IdParam, body: organizeSources.OrganizeStockInput, response: organizeSources.OrganizeStockResult, }), }, 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", auth: "session", summary: "清空回收站(缺省只清过期项;仅管理员控制台会话,API Key 与 DEV 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 与图片(按路径识别作品,不要求已登记;认不准时返回候选,选定后再提交)", body: files.FilesScrapeRequest, response: files.FilesScrapeResult, }), scrapeRecords: ep({ method: "get", path: "/files/scrape-records", scope: "organize.read", summary: "按 id 查刮削记录(目录、识别结果、逐文件结果)", query: files.FilesScrapeRecordsQuery, response: files.FilesScrapeRecordsResult, }), probe: ep({ method: "post", path: "/files/probe", scope: "organize.execute", summary: "ffprobe 摘要(≤20 项;已登记资产写回 media_info,所以要 organize.execute)", body: files.FilesProbeRequest, response: files.FilesProbeResult, }), download: ep({ method: "get", path: "/files/download", scope: "admin", auth: "session", summary: "下载小文件(nfo/字幕/图片/文本/.torrent,≤20 MiB;流式;仅管理员控制台会话;审计)", query: files.FilesDownloadQuery, response: files.FilesDownloadResponse, }), preview: ep({ method: "get", path: "/files/preview", scope: "admin", auth: "session", summary: "结构化预览(文本/NFO 前 1 MiB 带编码探测、字幕前 200 条、.torrent 文件列表、图片尺寸;仅管理员控制台会话)", query: files.FilesPreviewQuery, response: files.FilePreview, }), records: ep({ method: "get", path: "/files/records", scope: "organize.read", summary: "按整理记录 id 或 planId 回查整理记录(整理页预填)", query: files.FilesRecordsQuery, response: files.FilesRecordsResult, }), // W9-H 文件管理 v2 后端缺口 entityFromCandidate: ep({ method: "post", path: "/files/entity-from-candidate", scope: "organize.execute", summary: "按 TMDB 候选就地建本地作品 + 外部映射(accepted/manual)+ 季集单元;幂等", body: files.FilesEntityFromCandidateRequest, response: files.FilesEntityFromCandidateResult, }), scrapeBatch: ep({ method: "post", path: "/files/scrape-batch", scope: "organize.execute", summary: "批量刷新元数据:按作品去重入队 refreshMetadata(锁定字段不变)", body: files.FilesScrapeBatchRequest, response: files.FilesScrapeBatchResult, }), tree: ep({ method: "get", path: "/files/tree", scope: "organize.read", summary: "目录摘要:上一级、子目录(是否有下级)、视频/已登记计数(树与上一级用)", query: files.FilesTreeQuery, response: files.FilesTreeResult, }), recordSources: ep({ method: "get", path: "/files/record-sources", scope: "organize.read", summary: "整理记录的源路径换算成登记目录 FileRef(重新整理用)", query: files.FilesRecordSourcesQuery, response: files.FilesRecordSourcesResult, }), suggestName: ep({ method: "post", path: "/files/suggest-name", scope: "organize.read", summary: "按命名模板(作品 > 库根 > 全局)生成推荐名;识别不可信时 name=null + 原因", body: files.FilesSuggestNameRequest, response: files.FilesSuggestNameResult, }), }, } as const;