- classify enrich sources: encyclopedia, platform pages (hongguo, kuaikaw, video platforms), douban (search snippet only) and other sites' search snippets (snippet mode, pages never opened); one extractor per source, fields carry source kind and confidence, merged by noisy-OR across hosts - new cast field (applied as MediaCredit with placeholder ids) - robots.txt check (cached per host 1 day) and login-wall/captcha give-up - per-enrichment source attempts + GET /identity/enrich-stats, 30-day hit-rate table under settings -> metadata -> identity - setting identity.enrichSnippetAllSites (default on); default domains add baike.com and kuaikaw.cn - replay fixtures from the real run on the 4 user-library short dramas (sanitized Brave responses, hongguo page, robots.txt) Co-Authored-By: Claude Opus 5.5 <[email protected]>
473 lines
18 KiB
TypeScript
473 lines
18 KiB
TypeScript
import { z } from "zod";
|
||
import { ActorType, EntityBase, Id, JsonObject, Timestamp } from "./common.ts";
|
||
|
||
// 身份域:外部身份映射与修订(REQ §7.2)
|
||
// provider + entityType + externalId 对 accepted 唯一;TMDB movie:123 与 tv:123 是两个身份。
|
||
|
||
export const IdentityStatus = z.enum(["accepted", "proposed", "conflicted", "retired"]);
|
||
export type IdentityStatus = z.infer<typeof IdentityStatus>;
|
||
export const ExternalEntityType = z.enum(["movie", "tv", "season", "episode", "collection", "person"]);
|
||
export type ExternalEntityType = z.infer<typeof ExternalEntityType>;
|
||
/** 证据等级:未经标注集校准前不展示概率(REQ §5.3) */
|
||
export const EvidenceLevel = z.enum(["manual", "rule_confirmed", "multi_evidence", "needs_review"]);
|
||
export type EvidenceLevel = z.infer<typeof EvidenceLevel>;
|
||
export const IdentityVerifyStatus = z.enum(["unverified", "ok", "not_found", "type_mismatch", "error"]);
|
||
export type IdentityVerifyStatus = z.infer<typeof IdentityVerifyStatus>;
|
||
export const IdentityProvider = z.string().regex(/^[a-z][a-z0-9_]*$/);
|
||
|
||
export const ExternalIdentity = EntityBase.extend({
|
||
mediaEntityId: Id.nullable(),
|
||
contentUnitId: Id.nullable(),
|
||
provider: IdentityProvider,
|
||
entityType: ExternalEntityType,
|
||
externalId: z.string().min(1),
|
||
status: IdentityStatus,
|
||
evidenceLevel: EvidenceLevel,
|
||
evidence: JsonObject,
|
||
/** 人工锁:自动匹配/刷新不得改动 */
|
||
locked: z.boolean(),
|
||
revision: z.number().int().min(1),
|
||
validFrom: Timestamp.nullable(),
|
||
validUntil: Timestamp.nullable(),
|
||
verifiedAt: Timestamp.nullable(),
|
||
/** 404/超时/鉴权失败只记录到这里,不是删除映射的依据 */
|
||
verifyStatus: IdentityVerifyStatus,
|
||
/** 附属字段,如 TMDB episode_group */
|
||
extra: JsonObject,
|
||
/** 派生字段(列表返回):作品标题(挂在内容单元上时取所属作品) */
|
||
mediaEntityTitle: z.string().nullable().optional(),
|
||
});
|
||
export type ExternalIdentity = z.infer<typeof ExternalIdentity>;
|
||
|
||
const oneTarget = (v: { mediaEntityId?: string | null; contentUnitId?: string | null }) =>
|
||
Number(!!v.mediaEntityId) + Number(!!v.contentUnitId) === 1;
|
||
|
||
export const ExternalIdentityCreate = ExternalIdentity.pick({
|
||
mediaEntityId: true,
|
||
contentUnitId: true,
|
||
provider: true,
|
||
entityType: true,
|
||
externalId: true,
|
||
status: true,
|
||
evidenceLevel: true,
|
||
evidence: true,
|
||
locked: true,
|
||
validFrom: true,
|
||
validUntil: true,
|
||
extra: true,
|
||
})
|
||
.partial({
|
||
mediaEntityId: true,
|
||
contentUnitId: true,
|
||
status: true,
|
||
evidenceLevel: true,
|
||
evidence: true,
|
||
locked: true,
|
||
validFrom: true,
|
||
validUntil: true,
|
||
extra: true,
|
||
})
|
||
.refine(oneTarget, { message: "mediaEntityId 与 contentUnitId 必须且只能给一个" });
|
||
export type ExternalIdentityCreate = z.infer<typeof ExternalIdentityCreate>;
|
||
|
||
/** 变更一律生成 identity_revision;expectedRevision 用于乐观并发 */
|
||
export const ExternalIdentityUpdate = z.object({
|
||
expectedRevision: z.number().int().min(1),
|
||
status: IdentityStatus.optional(),
|
||
evidenceLevel: EvidenceLevel.optional(),
|
||
evidence: JsonObject.optional(),
|
||
locked: z.boolean().optional(),
|
||
validUntil: Timestamp.nullable().optional(),
|
||
extra: JsonObject.optional(),
|
||
reason: z.string().max(2000).optional(),
|
||
});
|
||
export type ExternalIdentityUpdate = z.infer<typeof ExternalIdentityUpdate>;
|
||
|
||
export const IdentityRevisionAction = z.enum([
|
||
"created",
|
||
"status_changed",
|
||
"relinked",
|
||
"evidence_updated",
|
||
"locked",
|
||
"unlocked",
|
||
]);
|
||
export type IdentityRevisionAction = z.infer<typeof IdentityRevisionAction>;
|
||
|
||
/** 身份修订历史:只追加,不修改 */
|
||
export const IdentityRevision = EntityBase.extend({
|
||
externalIdentityId: Id,
|
||
revision: z.number().int().min(1),
|
||
action: IdentityRevisionAction,
|
||
fromStatus: IdentityStatus.nullable(),
|
||
toStatus: IdentityStatus.nullable(),
|
||
before: JsonObject.nullable(),
|
||
after: JsonObject,
|
||
actorType: ActorType,
|
||
actorId: z.string().nullable(),
|
||
reason: z.string().nullable(),
|
||
});
|
||
export type IdentityRevision = z.infer<typeof IdentityRevision>;
|
||
/** 由服务端在身份变更事务内写入;此输入仅供内部仓储使用 */
|
||
export const IdentityRevisionCreate = IdentityRevision.pick({
|
||
externalIdentityId: true,
|
||
revision: true,
|
||
action: true,
|
||
fromStatus: true,
|
||
toStatus: true,
|
||
before: true,
|
||
after: true,
|
||
actorType: true,
|
||
actorId: true,
|
||
reason: true,
|
||
}).partial({ fromStatus: true, toStatus: true, before: true, actorId: true, reason: true });
|
||
export type IdentityRevisionCreate = z.infer<typeof IdentityRevisionCreate>;
|
||
|
||
// ── W2-B:待识别池、识别尝试、合并跳转与人工纠错(REQ §7.2、§7.5) ──
|
||
|
||
export const IdentityCandidateStatus = z.enum(["pending", "resolved", "dismissed", "archived"]);
|
||
export type IdentityCandidateStatus = z.infer<typeof IdentityCandidateStatus>;
|
||
/**
|
||
* proposed = 证据足够但自动化关闭/只读,待确认;
|
||
* unmatched = 惰性识别:本地召回无结果且没有触发 TMDB 查询(不相关的 RSS / 列表资源),只进池、不开待处理
|
||
*/
|
||
export const IdentityCandidateReason = z.enum(["ambiguous", "no_match", "proposed", "lookup_failed", "unmatched"]);
|
||
export type IdentityCandidateReason = z.infer<typeof IdentityCandidateReason>;
|
||
|
||
/** 待识别池里的一个候选(只存 JSON,不建正式作品) */
|
||
export const PoolCandidate = z.object({
|
||
provider: z.string(),
|
||
entityType: z.enum(["movie", "tv"]),
|
||
externalId: z.string(),
|
||
title: z.string(),
|
||
year: z.number().int().nullable(),
|
||
/** 本地候选的作品 ID */
|
||
localEntityId: Id.nullable(),
|
||
level: z.enum(["rule_confirmed", "multi_evidence", "needs_confirm", "rejected"]),
|
||
evidence: z.array(z.string()),
|
||
});
|
||
export type PoolCandidate = z.infer<typeof PoolCandidate>;
|
||
|
||
/** 待识别池条目:每条 source_release 至多一条;失败原因、证据版本与下一次复查时间 */
|
||
export const IdentityCandidate = EntityBase.extend({
|
||
sourceReleaseId: Id,
|
||
status: IdentityCandidateStatus,
|
||
reason: IdentityCandidateReason,
|
||
/** 解析出的检索条件(标题/年份/季集) */
|
||
query: JsonObject,
|
||
/** 归一化标题(与 title_alias.normalized 同口径):新增别名时按它唤醒复查,待处理按"来源 + 它"合并 */
|
||
normalizedTitle: z.string().nullable(),
|
||
candidates: z.array(PoolCandidate),
|
||
evidenceVersion: z.string(),
|
||
attempts: z.number().int().nonnegative(),
|
||
lastAttemptAt: Timestamp.nullable(),
|
||
nextReviewAt: Timestamp.nullable(),
|
||
decisionId: Id.nullable(),
|
||
resolvedMediaEntityId: Id.nullable(),
|
||
resolvedBy: z.string().nullable(),
|
||
resolvedAt: Timestamp.nullable(),
|
||
});
|
||
export type IdentityCandidate = z.infer<typeof IdentityCandidate>;
|
||
|
||
export const ResolutionOutcome = z.enum([
|
||
"auto",
|
||
"proposed",
|
||
"ambiguous",
|
||
"no_match",
|
||
"lookup_failed",
|
||
"manual",
|
||
"skipped_locked",
|
||
"unmatched",
|
||
]);
|
||
export type ResolutionOutcome = z.infer<typeof ResolutionOutcome>;
|
||
|
||
/** 一次识别尝试(只追加) */
|
||
export const ResolutionAttempt = EntityBase.extend({
|
||
sourceReleaseId: Id,
|
||
observationId: Id.nullable(),
|
||
outcome: ResolutionOutcome,
|
||
reason: z.string().nullable(),
|
||
evidenceVersion: z.string(),
|
||
candidateCount: z.number().int().nonnegative(),
|
||
mediaEntityId: Id.nullable(),
|
||
best: JsonObject.nullable(),
|
||
error: JsonObject.nullable(),
|
||
actorType: ActorType,
|
||
actorId: z.string().nullable(),
|
||
});
|
||
export type ResolutionAttempt = z.infer<typeof ResolutionAttempt>;
|
||
|
||
/** 作品合并后的旧 ID 跳转 */
|
||
export const IdentityRedirect = EntityBase.extend({
|
||
fromEntityId: Id,
|
||
toEntityId: Id,
|
||
reason: z.string().nullable(),
|
||
actorType: ActorType,
|
||
actorId: z.string().nullable(),
|
||
});
|
||
export type IdentityRedirect = z.infer<typeof IdentityRedirect>;
|
||
|
||
/** 外部身份引用(provider + 类型 + ID) */
|
||
export const ExternalRef = z.object({
|
||
provider: IdentityProvider,
|
||
entityType: z.enum(["movie", "tv"]),
|
||
externalId: z.string().min(1),
|
||
});
|
||
export type ExternalRef = z.infer<typeof ExternalRef>;
|
||
|
||
/**
|
||
* 人工指定来源资源的身份:本地作品 / 外部身份(未登记时按外部详情建档)/ 池内候选序号,三选一。
|
||
* 指定后加人工锁,自动识别不再改写。
|
||
*/
|
||
export const ReleaseIdentityAssign = z
|
||
.object({
|
||
mediaEntityId: Id.optional(),
|
||
external: ExternalRef.optional(),
|
||
candidateIndex: z.number().int().min(0).optional(),
|
||
/** 同时关联到某个内容单元(季/集);缺省按解析出的季集建临时单元 */
|
||
contentUnitId: Id.nullable().optional(),
|
||
lock: z.boolean().default(true),
|
||
reason: z.string().max(2000).optional(),
|
||
})
|
||
.refine((v) => [v.mediaEntityId, v.external, v.candidateIndex].filter((x) => x !== undefined).length === 1, {
|
||
message: "mediaEntityId / external / candidateIndex 必须且只能给一个",
|
||
});
|
||
export type ReleaseIdentityAssign = z.infer<typeof ReleaseIdentityAssign>;
|
||
|
||
/** 改作品的外部映射:旧的同 provider+类型 accepted 映射退役(留修订),新映射 accepted + 人工锁 */
|
||
export const EntityIdentitySet = ExternalRef.extend({
|
||
reason: z.string().max(2000).optional(),
|
||
/** 默认加人工锁 */
|
||
lock: z.boolean().default(true),
|
||
});
|
||
export type EntityIdentitySet = z.infer<typeof EntityIdentitySet>;
|
||
|
||
export const IdentityPoolQuery = z.object({
|
||
status: IdentityCandidateStatus.optional(),
|
||
reason: IdentityCandidateReason.optional(),
|
||
/** 按原始标题/解析标题检索(已观察到的资源必须能从待识别入口找回) */
|
||
q: z.string().trim().min(1).max(200).optional(),
|
||
siteId: Id.optional(),
|
||
cursor: z.string().min(1).optional(),
|
||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||
});
|
||
export type IdentityPoolQuery = z.infer<typeof IdentityPoolQuery>;
|
||
|
||
/** 待识别池列表条目:附来源资源摘要 */
|
||
export const IdentityPoolItem = IdentityCandidate.extend({
|
||
release: z.object({
|
||
id: Id,
|
||
siteId: Id,
|
||
siteReleaseId: z.string(),
|
||
title: z.string(),
|
||
subtitle: z.string().nullable(),
|
||
categoryRaw: z.string().nullable(),
|
||
firstSeenAt: Timestamp,
|
||
resolutionStatus: z.string(),
|
||
}),
|
||
});
|
||
export type IdentityPoolItem = z.infer<typeof IdentityPoolItem>;
|
||
|
||
// ── META-1:TMDB 缺失作品的元数据补全(REQ R03 / R10、§7.3、§7.5)──
|
||
// 网页搜索 + 读取白名单域名的网页 → 模型(或规则)抽字段,每个字段带来源网页与证据等级;
|
||
// 建议先存补全记录(metadata_enrichment),用户确认后才建本地临时作品并写字段级元数据(来源 web / agent,不冒充 TMDB)。
|
||
|
||
/** 补全字段的证据等级(服务端按网页原文核对得出,不采信模型自报) */
|
||
export const EnrichEvidenceLevel = z.enum(["rule_confirmed", "multi_evidence", "needs_review"]);
|
||
export type EnrichEvidenceLevel = z.infer<typeof EnrichEvidenceLevel>;
|
||
|
||
/** 字段证据:写在 metadata_field.evidence,作品详情"元数据来源"里显示链接 */
|
||
export const FieldEvidence = z.object({
|
||
sourceUrl: z.string().nullable(),
|
||
evidenceLevel: EnrichEvidenceLevel,
|
||
enrichmentId: Id.nullable(),
|
||
/** model = 模型从网页正文抽取;rule = 规则从网页正文抽取 */
|
||
extractor: z.enum(["model", "rule"]).optional(),
|
||
});
|
||
export type FieldEvidence = z.infer<typeof FieldEvidence>;
|
||
|
||
/**
|
||
* META-2 补全来源类别:encyclopedia = 百科(百度 / 搜狗 / 快懂百科、维基);platform = 平台作品页(短剧平台、视频平台);
|
||
* douban = 豆瓣(只用搜索摘要,不读页面);snippet = 其它网站的搜索摘要(只读搜索结果摘要,不读页面);other = 白名单里的其它网站
|
||
*/
|
||
export const EnrichSourceKind = z.enum(["encyclopedia", "platform", "douban", "snippet", "other"]);
|
||
export type EnrichSourceKind = z.infer<typeof EnrichSourceKind>;
|
||
|
||
const enrichValue = <T extends z.ZodType>(value: T) =>
|
||
z.object({
|
||
value,
|
||
/** 出处网页(白名单域名,或摘要模式下的搜索结果链接);模型给不出出处时为 null */
|
||
sourceUrl: z.string().nullable(),
|
||
evidenceLevel: EnrichEvidenceLevel,
|
||
/** 值在出处网页正文(或搜索摘要)里原样出现(否则是 Agent 归纳 / 改写的生成内容) */
|
||
verbatim: z.boolean(),
|
||
/** META-2:出处的来源类别(旧记录没有) */
|
||
sourceKind: EnrichSourceKind.optional(),
|
||
/** META-2:合并后的置信度 0–1(多个网站给出同一值时按 noisy-OR 叠加) */
|
||
confidence: z.number().min(0).max(1).optional(),
|
||
/** META-2:给出同一值的不同网站数 */
|
||
hosts: z.number().int().nonnegative().optional(),
|
||
});
|
||
|
||
export const EnrichFields = z.object({
|
||
title: enrichValue(z.string().min(1).max(200)).nullable(),
|
||
aliases: z.array(enrichValue(z.string().min(1).max(200))).max(20),
|
||
year: enrichValue(z.number().int().min(1900).max(2100)).nullable(),
|
||
platform: enrichValue(z.string().min(1).max(100)).nullable(),
|
||
episodeCount: enrichValue(z.number().int().min(1).max(2000)).nullable(),
|
||
synopsis: enrichValue(z.string().min(1).max(2000)).nullable(),
|
||
posterUrl: enrichValue(z.string().min(1).max(2048)).nullable(),
|
||
genre: z.array(enrichValue(z.string().min(1).max(40))).max(10),
|
||
/** META-2:主演(旧记录没有,按空数组) */
|
||
cast: z
|
||
.array(enrichValue(z.string().min(1).max(60)))
|
||
.max(20)
|
||
.default([]),
|
||
});
|
||
export type EnrichFields = z.infer<typeof EnrichFields>;
|
||
|
||
export const EnrichFieldName = z.enum([
|
||
"title",
|
||
"aliases",
|
||
"year",
|
||
"platform",
|
||
"episodeCount",
|
||
"synopsis",
|
||
"posterUrl",
|
||
"genre",
|
||
"cast",
|
||
]);
|
||
export type EnrichFieldName = z.infer<typeof EnrichFieldName>;
|
||
|
||
/** 搜索结果里的一条链接:是否在白名单、是否读取、为什么跳过 */
|
||
export const EnrichSource = z.object({
|
||
url: z.string(),
|
||
title: z.string().nullable(),
|
||
domain: z.string(),
|
||
allowed: z.boolean(),
|
||
fetched: z.boolean(),
|
||
/**
|
||
* not_allowed = 不在白名单(摘要模式仍可能用它的搜索摘要);site = 是已登记站点的域名;irrelevant = 标题对不上;
|
||
* limit = 超出读取条数;error = 读取失败;snippet_only = 只用搜索摘要的网站(豆瓣);robots = robots.txt 不允许;
|
||
* blocked = 页面是登录墙 / 验证码,放弃该来源
|
||
*/
|
||
skipped: z
|
||
.enum(["not_allowed", "site", "irrelevant", "limit", "error", "snippet_only", "robots", "blocked"])
|
||
.nullable(),
|
||
error: z.string().nullable(),
|
||
/** META-2 来源类别 */
|
||
kind: EnrichSourceKind.optional(),
|
||
/** META-2 这条链接的搜索摘要被用来抽字段 */
|
||
snippetUsed: z.boolean().optional(),
|
||
});
|
||
export type EnrichSource = z.infer<typeof EnrichSource>;
|
||
|
||
export const EnrichSuggestion = z.object({
|
||
fields: EnrichFields,
|
||
/** 建档类型推断:有集数按剧集 */
|
||
kind: z.enum(["series", "movie"]),
|
||
queries: z.array(z.string()),
|
||
sources: z.array(EnrichSource),
|
||
extractor: z.enum(["model", "rule"]),
|
||
model: z.string().nullable(),
|
||
});
|
||
export type EnrichSuggestion = z.infer<typeof EnrichSuggestion>;
|
||
|
||
/** META-2 一次补全里某类来源的尝试与命中:results = 标题对得上的搜索结果数;hit = 最终建议里至少一个字段取自它 */
|
||
export const EnrichSourceAttempt = z.object({
|
||
kind: EnrichSourceKind,
|
||
results: z.number().int().nonnegative(),
|
||
fetched: z.number().int().nonnegative(),
|
||
hit: z.boolean(),
|
||
fields: z.array(z.string()),
|
||
});
|
||
export type EnrichSourceAttempt = z.infer<typeof EnrichSourceAttempt>;
|
||
|
||
export const EnrichUsage = z.object({
|
||
searchCalls: z.number().int().nonnegative(),
|
||
fetchCalls: z.number().int().nonnegative(),
|
||
inputTokens: z.number().int().nonnegative(),
|
||
outputTokens: z.number().int().nonnegative(),
|
||
/** META-2 各类来源的尝试与命中(命中率统计用;旧记录没有) */
|
||
attempts: z.array(EnrichSourceAttempt).optional(),
|
||
});
|
||
export type EnrichUsage = z.infer<typeof EnrichUsage>;
|
||
|
||
export const EnrichmentStatus = z.enum(["suggested", "applied", "failed"]);
|
||
export type EnrichmentStatus = z.infer<typeof EnrichmentStatus>;
|
||
export const EnrichmentTrigger = z.enum(["manual", "auto", "agent"]);
|
||
export type EnrichmentTrigger = z.infer<typeof EnrichmentTrigger>;
|
||
|
||
/** 补全记录(metadata_enrichment 行) */
|
||
export const MetadataEnrichment = EntityBase.extend({
|
||
identityCandidateId: Id.nullable(),
|
||
normalizedTitle: z.string(),
|
||
query: JsonObject,
|
||
status: EnrichmentStatus,
|
||
trigger: EnrichmentTrigger,
|
||
suggestion: JsonObject.nullable(),
|
||
error: z.string().nullable(),
|
||
usage: JsonObject,
|
||
mediaEntityId: Id.nullable(),
|
||
appliedAt: Timestamp.nullable(),
|
||
appliedBy: z.string().nullable(),
|
||
createdBy: z.string().nullable(),
|
||
});
|
||
export type MetadataEnrichment = z.infer<typeof MetadataEnrichment>;
|
||
|
||
/** GET /identity/enrich-stats:最近 N 天补全命中率(按来源类别) */
|
||
export const EnrichStatsQuery = z.object({ days: z.coerce.number().int().min(1).max(365).default(30) });
|
||
export const EnrichStats = z.object({
|
||
days: z.number().int(),
|
||
/** 发起过搜索的补全次数(命中 7 天缓存的不计) */
|
||
total: z.number().int().nonnegative(),
|
||
/** 其中出了建议的次数 */
|
||
suggested: z.number().int().nonnegative(),
|
||
/** 有来源明细(META-2 之后)的次数:下面各来源的命中率以它为分母 */
|
||
tracked: z.number().int().nonnegative(),
|
||
sources: z.array(
|
||
z.object({
|
||
kind: EnrichSourceKind,
|
||
/** 有标题对得上的搜索结果的次数 */
|
||
withResults: z.number().int().nonnegative(),
|
||
/** 建议里至少一个字段取自这类来源的次数 */
|
||
hits: z.number().int().nonnegative(),
|
||
/** hits / tracked(0–1);tracked = 0 时为 null */
|
||
rate: z.number().min(0).max(1).nullable(),
|
||
}),
|
||
),
|
||
});
|
||
export type EnrichStats = z.infer<typeof EnrichStats>;
|
||
|
||
/** POST /identity-pool/{id}/enrich 的请求体;fresh = 忽略归一化标题缓存重新查(计入每日预算) */
|
||
export const EnrichRequest = z.object({
|
||
hints: z.string().trim().max(200).optional(),
|
||
fresh: z.boolean().optional(),
|
||
});
|
||
export type EnrichRequest = z.infer<typeof EnrichRequest>;
|
||
|
||
/** POST /identity-pool/{id}/enrich/apply */
|
||
export const EnrichApplyInput = z.object({
|
||
enrichmentId: Id,
|
||
/** 只应用这些字段;缺省应用建议里有值的全部字段 */
|
||
fields: z.array(EnrichFieldName).min(1).optional(),
|
||
/** 应用的字段同时加锁(之后的自动来源不改写) */
|
||
lock: z.boolean().default(false),
|
||
/** 并入已有作品而不是新建(同名去重时由用户选) */
|
||
mediaEntityId: Id.optional(),
|
||
kind: z.enum(["series", "movie"]).optional(),
|
||
});
|
||
export type EnrichApplyInput = z.infer<typeof EnrichApplyInput>;
|
||
|
||
export const EnrichApplyResult = z.object({
|
||
enrichmentId: Id,
|
||
mediaEntityId: Id,
|
||
/** false = 同名去重命中已有作品,字段写到它上面 */
|
||
created: z.boolean(),
|
||
contentUnits: z.number().int().nonnegative(),
|
||
fields: z.array(z.string()),
|
||
aliases: z.number().int().nonnegative(),
|
||
/** 待识别资源已关联到该作品 */
|
||
assigned: z.boolean(),
|
||
});
|
||
export type EnrichApplyResult = z.infer<typeof EnrichApplyResult>;
|