Files
UHD_Media_Tool/apps/server/src/modules/sources-identity/defs.ts
T
rootandClaude Opus 5.5 05a7706dde feat(sources): site center backend (credentials, refresh login, user data, health check, delete)
PUT /sites/{id}/credentials by kind (moved from auth-settings, legacy body
accepted), POST refresh-login (async task or wait), POST health-check,
GET user-data history, DELETE /sites/{id} (soft delete, releases kept),
DELETE /site-accounts/{id}, GET /sites?include=accounts; refreshSiteAccount
task with login-failure decisions and auto re-login on auth_expired;
scheduled jobs sites.user_data (6h) and sites.health_check (1h);
settings.sites defaults for rate limit, snapshot interval, UA and proxy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:18:40 +08:00

315 lines
8.0 KiB
TypeScript

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 = <R extends z.ZodType, F extends z.ZodRawShape>(
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 = <R extends z.ZodType>(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 = <M extends "post" | "patch" | "delete", B extends z.ZodType | undefined, R extends z.ZodType>(
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 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() },
TD,
),
get: get("/decisions/{id}", "待处理事项详情", Decision, TD),
resolve: write(
"post",
"/decisions/{id}/resolve",
"organize.execute",
"处理待处理事项",
DecisionResolveInput,
Decision,
TD,
),
},
};