import type { ExternalIdentity, ExternalIdentityCreate, IdentityResolveOutput, IdentityStatus, MediaEntity, ResolutionStatus, } from "@umt/contracts"; import type { IdentityCandidate, IdentityResolution, RankedCandidate } from "@umt/domain"; import { toContractEvidenceLevel, toDomainMediaType } from "./evidence.ts"; // 身份解析:domain.resolveIdentity(auto / ambiguous / no_match)→ contracts 的能力输出状态 // 与 source_release.resolution_status。 export type DomainResolutionStatus = IdentityResolution["status"]; export type ContractResolveStatus = IdentityResolveOutput["status"]; export interface ResolveStatusOptions { /** * auto 时是否可直接接受(automation 总开关/授权/人工锁都允许)。 * false 时产出 proposed:证据足够但需要确认,进待识别池。 */ autoAccept: boolean; } const RESOLVE_STATUS: Record ContractResolveStatus> = { auto: (o) => (o.autoAccept ? "resolved" : "proposed"), ambiguous: () => "ambiguous", no_match: () => "unidentifiable", }; export const DOMAIN_RESOLUTION_STATUSES = Object.keys(RESOLVE_STATUS) as DomainResolutionStatus[]; export function toContractResolveStatus( status: DomainResolutionStatus, opts: ResolveStatusOptions, ): ContractResolveStatus { return RESOLVE_STATUS[status](opts); } const RELEASE_RESOLUTION: Record = { resolved: "resolved", // 待确认的提议与歧义一样进待识别池 proposed: "ambiguous", ambiguous: "ambiguous", unidentifiable: "unidentifiable", }; /** 能力输出状态 → source_release.resolution_status */ export function toReleaseResolutionStatus(status: ContractResolveStatus): ResolutionStatus { return RELEASE_RESOLUTION[status]; } /** * 排名候选 → external_identity 新建输入。本地候选(provider=local)不是外部身份,返回 null; * 被否决的候选同样返回 null。 */ export function rankedToIdentityCreate( r: RankedCandidate, target: { mediaEntityId: string } | { contentUnitId: string }, status: IdentityStatus = "proposed", ): ExternalIdentityCreate | null { const level = toContractEvidenceLevel(r.level); if (!level || r.candidate.provider === "local") return null; return { ...target, provider: r.candidate.provider, entityType: r.candidate.entityType, externalId: r.candidate.externalId, status, evidenceLevel: level, evidence: { titleMatch: r.titleMatch, yearMatch: r.yearMatch, typeMatch: r.typeMatch, rules: r.evidence, }, }; } /** * 本地作品 → domain 身份候选(本地优先召回)。externalId 用本地 UUID; * 已接受的外部身份作为 linkedExternalIds,用于与远端候选判同。 * 媒体类型无法映射到 movie/tv(合集/其他)时返回 null。 */ export function localIdentityCandidate( entity: Pick, aliases: readonly string[], identities: readonly Pick[] = [], ): IdentityCandidate | null { const entityType = toDomainMediaType(entity.kind); if (!entityType) return null; const titles = [entity.title, ...(entity.originalTitle ? [entity.originalTitle] : [])]; return { provider: "local", entityType, externalId: entity.id, titles, ...(entity.year !== null ? { year: entity.year } : {}), aliases: [...aliases], localEntityId: entity.id, linkedExternalIds: identities .filter((i) => i.status === "accepted") .map((i) => ({ provider: i.provider, id: i.externalId })), }; }