import { ContentUnit, ContentUnitUpdate, CursorQuery, cursorPage, Decision, DecisionKind, DecisionResolveInput, DecisionStatus, ExternalIdentity, ExternalIdentityCreate, ExternalIdentityUpdate, FeedHealth, FeedSource, FeedSourceCreate, FeedSourceUpdate, Id, IdentityRevision, IdentityStatus, IdParam, MediaEntity, MediaEntityCreate, MediaEntityMerge, MediaEntityStatus, MediaEntityUpdate, MediaKind, Observation, ObservationStatus, OkResult, ResolutionStatus, Site, SiteAccount, SiteAccountCreate, SiteAccountUpdate, SiteCreate, SiteListItem, SiteListQuery, SiteUpdate, SourceRelease, TitleAlias, TitleAliasCreate, } from "@umt/contracts"; import { z } from "zod"; // 与 contracts Api(crud())同路径、同 scope 的端点定义,但保留具体 schema 类型供路由推断入参。 // scope:读 = read;写 = admin(站点/Feed 配置)或 organize.execute(身份纠错、待处理)。 const list = ( path: string, summary: string, row: R, filters: F, tags: string[], ) => ({ method: "get" as const, path, scope: "read" as const, summary, tags, query: CursorQuery.extend(filters), response: cursorPage(row), }); const get = (path: string, summary: string, row: R, tags: string[]) => ({ method: "get" as const, path, scope: "read" as const, summary, tags, params: IdParam, response: row, }); const write = ( method: M, path: string, scope: "admin" | "organize.execute", summary: string, body: B, response: R, tags: string[], withId = true, ) => ({ method, path, scope, summary, tags, ...(withId ? { params: IdParam } : {}), body: body as B, response, }); const TS = ["sites"]; const TF = ["feed-sources"]; const TR = ["source-releases"]; const TE = ["media-entities"]; const TI = ["identity"]; const TD = ["decisions"]; export const DecisionMinSeverity = z.enum(["info", "warning", "critical"]); export const D = { sites: { list: { method: "get" as const, path: "/sites", scope: "read" as const, summary: "站点列表(include=accounts 附带账号与整体账号状态)", tags: TS, query: SiteListQuery, response: cursorPage(SiteListItem), }, get: get("/sites/{id}", "站点详情", Site, TS), create: { method: "post" as const, path: "/sites", scope: "admin" as const, summary: "登记站点", tags: TS, body: SiteCreate, response: Site, }, update: write("patch", "/sites/{id}", "admin", "修改站点(启停、快照周期、限流、代理)", SiteUpdate, Site, TS), }, siteAccounts: { list: list("/site-accounts", "站点账号列表", SiteAccount, { siteId: Id.optional() }, TS), get: get("/site-accounts/{id}", "站点账号详情", SiteAccount, TS), create: { method: "post" as const, path: "/site-accounts", scope: "admin" as const, summary: "新建站点账号(凭证只写,存为 secret)", tags: TS, body: SiteAccountCreate, response: SiteAccount, }, update: write( "patch", "/site-accounts/{id}", "admin", "修改站点账号 / 轮换凭证", SiteAccountUpdate, SiteAccount, TS, ), }, feedSources: { list: list("/feed-sources", "Feed 列表", FeedSource, { siteId: Id.optional(), health: FeedHealth.optional() }, TF), get: get("/feed-sources/{id}", "Feed 详情", FeedSource, TF), create: { method: "post" as const, path: "/feed-sources", scope: "admin" as const, summary: "新建 Feed(url 只写,存为 secret;对外只给脱敏 URL)", tags: TF, body: FeedSourceCreate, response: FeedSource, }, update: write("patch", "/feed-sources/{id}", "admin", "修改 Feed", FeedSourceUpdate, FeedSource, TF), fetchNow: write( "post", "/feed-sources/{id}/fetch", "admin", "立即抓取(仍受站点限流)", undefined, z.object({ taskId: Id }), TF, ), }, sourceReleases: { list: list( "/source-releases", "资源列表", SourceRelease, { siteId: Id.optional(), mediaEntityId: Id.optional(), resolutionStatus: ResolutionStatus.optional(), q: z.string().max(200).optional(), }, TR, ), get: get("/source-releases/{id}", "资源详情", SourceRelease, TR), }, observations: { list: list( "/observations", "观察列表", Observation, { feedId: Id.optional(), status: ObservationStatus.optional() }, TR, ), get: get("/observations/{id}", "观察详情", Observation, TR), }, mediaEntities: { list: list( "/media-entities", "作品列表", MediaEntity, { kind: MediaKind.optional(), status: MediaEntityStatus.optional(), q: z.string().max(200).optional() }, TE, ), get: get("/media-entities/{id}", "作品(旧 ID 自动跳转)", MediaEntity, TE), create: { method: "post" as const, path: "/media-entities", scope: "organize.execute" as const, summary: "手动建本地作品(TMDB 无记录时可先建档)", tags: TE, body: MediaEntityCreate, response: MediaEntity, }, update: write( "patch", "/media-entities/{id}", "organize.execute", "修改作品(标题/原名/年份按字段覆盖并锁定)", MediaEntityUpdate, MediaEntity, TE, ), merge: write( "post", "/media-entities/{id}/merge", "organize.execute", "合并重复作品(保留旧 ID 跳转)", MediaEntityMerge, MediaEntity, TE, ), }, contentUnits: { list: list( "/content-units", "内容单元列表", ContentUnit, { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() }, TE, ), get: get("/content-units/{id}", "内容单元详情", ContentUnit, TE), update: write( "patch", "/content-units/{id}", "organize.execute", "修改内容单元(改编号即确认并锁定)", ContentUnitUpdate, ContentUnit, TE, ), }, titleAliases: { list: list( "/title-aliases", "别名列表", TitleAlias, { mediaEntityId: Id.optional(), q: z.string().max(200).optional() }, TI, ), get: get("/title-aliases/{id}", "别名详情", TitleAlias, TI), create: write("post", "/title-aliases", "organize.execute", "新增别名", TitleAliasCreate, TitleAlias, TI, false), remove: write("delete", "/title-aliases/{id}", "organize.execute", "删除别名", undefined, OkResult, TI), }, externalIdentities: { list: list( "/external-identities", "外部身份列表", ExternalIdentity, { mediaEntityId: Id.optional(), provider: z.string().optional(), status: IdentityStatus.optional() }, TI, ), get: get("/external-identities/{id}", "外部身份详情", ExternalIdentity, TI), create: write( "post", "/external-identities", "organize.execute", "新增外部身份(accepted 等同人工纠错)", ExternalIdentityCreate, ExternalIdentity, TI, false, ), update: write( "patch", "/external-identities/{id}", "organize.execute", "修订外部身份(乐观并发)", ExternalIdentityUpdate, ExternalIdentity, TI, ), revisions: get( "/external-identities/{id}/revisions", "身份修订历史", z.object({ items: z.array(IdentityRevision) }), TI, ), }, decisions: { list: list( "/decisions", "待处理事项(缺省只列未关闭)", Decision, { status: DecisionStatus.optional(), kind: DecisionKind.optional(), /** 只看不低于此级别的事项;收件箱缺省传 warning 隐藏"仅信息"类 */ minSeverity: DecisionMinSeverity.optional(), }, TD, ), get: get("/decisions/{id}", "待处理事项详情", Decision, TD), resolve: write( "post", "/decisions/{id}/resolve", "organize.execute", "处理待处理事项", DecisionResolveInput, Decision, TD, ), }, };