feat(episode-mapping): 映射规则接口与模拟预览、待处理确认 / 忽略、作品页集数映射分区与订阅摘要

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 19:28:09 +08:00
1 parent fe597187dd
commit 45a114263b
26 files changed
+2770 -9

No files matched your search

+168
View File
@@ -42,3 +42,171 @@ export const EpisodeMappingRuleRow = EntityBase.extend({
createdById: z.string().nullable(),
});
export type EpisodeMappingRuleRow = z.infer<typeof EpisodeMappingRuleRow>;
// ── 接口(批次 4):规则增删改、模拟预览、应用后的重算结果 ──
export const EpisodeResolutionMethod = z.enum([
"raw",
"rule",
"absolute_in_season",
"absolute_cross_season",
"air_date",
"unverified",
"unresolved",
"skipped",
]);
export type EpisodeResolutionMethod = z.infer<typeof EpisodeResolutionMethod>;
/** 集数解析结果(source_release.episode_resolution 的结构);airDate 为 YYYYMMDD */
export const EpisodeResolutionView = z.object({
method: EpisodeResolutionMethod,
source: z.object({
season: z.number().int().optional(),
episodes: EpisodeRangeSchema.optional(),
airDate: z.string().optional(),
}),
canonical: z.object({ season: z.number().int(), episodes: EpisodeRangeSchema }).nullable(),
ruleId: z.string().optional(),
ruleFailed: z.string().optional(),
special: EpisodeSpecialKind.optional(),
evidence: z.array(z.string()),
});
export type EpisodeResolutionView = z.infer<typeof EpisodeResolutionView>;
/** 规则内容(新建 / 草稿共用):按 kind 只看对应字段,其余忽略 */
export const EpisodeMappingRuleSpec = z.object({
/** 来源季;null / 不填 = 任意季 */
season: z.number().int().min(0).max(999).nullable().optional(),
/** 发布组;null / 空 = 任意发布组 */
releaseGroup: z.string().trim().max(200).nullable().optional(),
kind: EpisodeMappingRuleKind,
/** expression:偏移表达式,如 `EP-12`、`EP*2-1`(省略 EP 按 MP 约定补全) */
expression: z.string().trim().max(64).nullable().optional(),
/** expression:目标季,缺省为来源季 */
targetSeason: z.number().int().min(0).max(999).nullable().optional(),
tableEntries: z.array(EpisodeMappingTableEntry).max(500).nullable().optional(),
special: EpisodeSpecialKind.nullable().optional(),
});
export type EpisodeMappingRuleSpec = z.infer<typeof EpisodeMappingRuleSpec>;
export const EpisodeMappingRuleCreate = EpisodeMappingRuleSpec.extend({
/** 规则依据(如来自待处理建议的样本),原样存证 */
evidence: JsonObject.optional(),
});
export type EpisodeMappingRuleCreate = z.infer<typeof EpisodeMappingRuleCreate>;
/** 修改:内容字段整体按 kind 重新校验;status 切换启用 / 停用 */
export const EpisodeMappingRuleUpdate = EpisodeMappingRuleSpec.partial().extend({
status: EpisodeMappingRuleStatus.optional(),
});
export type EpisodeMappingRuleUpdate = z.infer<typeof EpisodeMappingRuleUpdate>;
export const EpisodeMappingRuleList = z.object({ items: z.array(EpisodeMappingRuleRow) });
export type EpisodeMappingRuleList = z.infer<typeof EpisodeMappingRuleList>;
/** 按作品重算已绑定资源的结果 */
export const EpisodeReresolveResult = z.object({
/** 重算的资源数 */
releases: z.number().int().min(0),
/** 方式或规范季集变化的资源数 */
changed: z.number().int().min(0),
/** 由待确认转为已解决的资源数 */
resolved: z.number().int().min(0),
});
export type EpisodeReresolveResult = z.infer<typeof EpisodeReresolveResult>;
/** 规则写入(新建 / 修改 / 停用 / 删除)后立即按作品重算 */
export const EpisodeMappingRuleWriteResult = z.object({
rule: EpisodeMappingRuleRow,
reresolve: EpisodeReresolveResult,
});
export type EpisodeMappingRuleWriteResult = z.infer<typeof EpisodeMappingRuleWriteResult>;
export const EpisodeMappingPreviewInput = z.object({
/** 只看这个季(来源季或规范季任一匹配);不填 = 全部 */
season: z.number().int().min(0).max(999).nullable().optional(),
/** 只看这个发布组(忽略大小写);不填 = 全部 */
releaseGroup: z.string().trim().max(200).nullable().optional(),
/** 草稿规则:替换同作用域的已有规则后试算 */
draft: EpisodeMappingRuleSpec.nullable().optional(),
/** 正在修改的规则:试算时用草稿替换它 */
replaceRuleId: Id.optional(),
/** 最多看多少条近期资源(按发布时间倒序) */
limit: z.number().int().min(1).max(300).optional(),
});
export type EpisodeMappingPreviewInput = z.infer<typeof EpisodeMappingPreviewInput>;
export const EpisodeValidationState = z.enum(["pass", "unverified", "fail", "special", "skipped"]);
export type EpisodeValidationState = z.infer<typeof EpisodeValidationState>;
export const EpisodeMappingPreviewItem = z.object({
sourceReleaseId: Id,
title: z.string(),
publishedAt: z.string(),
releaseGroup: z.string().nullable(),
/** 库里的解析结果;没解析过为 null */
current: EpisodeResolutionView.nullable(),
/** 按原标题重解析、套用草稿规则后的结果(没有草稿时 = 当前规则重算) */
preview: EpisodeResolutionView,
validation: EpisodeValidationState,
/** 预览结果与库里的不同(方式、规范季集或特别篇) */
changed: z.boolean(),
});
export type EpisodeMappingPreviewItem = z.infer<typeof EpisodeMappingPreviewItem>;
export const EpisodeMethodCount = z.object({ method: EpisodeResolutionMethod, count: z.number().int().min(0) });
export type EpisodeMethodCount = z.infer<typeof EpisodeMethodCount>;
export const EpisodeSuggestion = z.object({
season: z.number().int().nullable(),
releaseGroup: z.string().nullable(),
expression: z.string(),
targetSeason: z.number().int(),
samples: z.array(
z.object({
source: EpisodeRangeSchema,
target: z.object({ season: z.number().int(), episode: z.number().int() }),
publishedAt: z.string(),
airDate: z.string(),
}),
),
});
export type EpisodeSuggestion = z.infer<typeof EpisodeSuggestion>;
export const EpisodeMappingPreviewGroup = z.object({
releaseGroup: z.string().nullable(),
total: z.number().int().min(0),
/** 预览结果按方式计数(只列非零) */
methods: z.array(EpisodeMethodCount),
/** 待确认数(预览结果) */
pending: z.number().int().min(0),
/** 该组待确认样本推导出的建议规则 */
suggestion: EpisodeSuggestion.nullable(),
});
export type EpisodeMappingPreviewGroup = z.infer<typeof EpisodeMappingPreviewGroup>;
export const EpisodeSeasonSummary = z.object({
seasons: z.array(
z.object({
season: z.number().int(),
/** 总集数;元数据还没给出为 null */
total: z.number().int().nullable(),
/** 已知播出日期的集数 */
aired: z.number().int().min(0),
}),
),
latestSeason: z.number().int().nullable(),
ended: z.boolean(),
});
export type EpisodeSeasonSummary = z.infer<typeof EpisodeSeasonSummary>;
export const EpisodeMappingPreview = z.object({
/** 作品是否参与集数解析(剧集 / 综艺) */
applicable: z.boolean(),
seasons: EpisodeSeasonSummary,
groups: z.array(EpisodeMappingPreviewGroup),
items: z.array(EpisodeMappingPreviewItem),
/** 预览结果里与当前不同的条数 */
changed: z.number().int().min(0),
});
export type EpisodeMappingPreview = z.infer<typeof EpisodeMappingPreview>;