- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
315 lines
8.1 KiB
TypeScript
315 lines
8.1 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,
|
||
),
|
||
},
|
||
};
|