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]>
315 lines
8.0 KiB
TypeScript
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,
|
|
),
|
|
},
|
|
};
|