refactor: 拆分大文件(contracts 端点、整理服务、浏览器纯函数、整理弹窗来源)

- contracts/src/api.ts 2733 行 → api/*.ts 13 个领域文件 + shared.ts;api.ts 按原顺序合并 Api。
  新旧对比:354 个端点内容与顺序一致,分组键与对外导出一致
- acquisition-organize/services/organize.ts 2645 行 → organize-types / -plan / -exec / -finalize / -records,
  organize.ts 保留为汇总入口,外部引用不变
- browser/service.ts 类外的纯函数(视图映射、原因文案、坐标换算、图片尺寸、出口代理)移到 session-view.ts,
  service.ts 照旧转出
- organize-wizard:来源解析 useResolvedSource 移到 source.ts,FormSelect 移到 form-select.tsx

只挪位置不改逻辑;typecheck、lint、test:all、web build、server 编译与编译产物自检通过。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 17:40:43 +08:00
1 parent c9e8120fc6
commit 8bec4bad5d
26 files changed
+5906 -5696

No files matched your search

+322
View File
@@ -0,0 +1,322 @@
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,
}),
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;