/** * 归属解析 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 6.2)。 * * 通道入站到统一事件流之间由归属解析完成路由。匹配次序确定、**逐级短路**: * ① 线程精确匹配 —— 会话/线程 ID 精确匹配某在途任务的出站记录 * ② 关联键匹配监听 —— 提取的关联键命中某任务监听的过滤条件 * ③ 地址 + 时间窗 —— 往来地址在时间窗内匹配唯一在途任务;命中多个即无主 * 提取失败或歧义**一律降级为无主** —— 宁交人,不错投。 */ import type { OwnershipResolution, PositionId, TaskId } from '@contracts'; /** 通道实例配置的归属键提取规则(声明式) */ export type OwnershipKeyRules = { /** 从正文/主题提取关联键的正则(带一个捕获组),如单号 */ readonly patterns: ReadonlyArray<{ key: string; regex: string; source: 'subject' | 'body' | 'both' }>; /** 地址 + 时间窗的窗口长度(毫秒) */ readonly addressWindowMs: number; /** 无主入站的默认认领岗位 */ readonly fallbackPosition?: PositionId; }; export type InboundFacts = { readonly threadId?: string; readonly from: string; readonly to: readonly string[]; readonly subject: string; readonly body: string; readonly receivedAt: Date; }; /** 出站关联记录:任务发过什么,线程 ID 与往来地址由此登记 */ export type OutboundRef = { readonly taskId: TaskId; readonly threadId?: string; readonly address: string; readonly sentAt: Date; }; /** 在途任务的监听过滤面 */ export type ListenerKeyIndex = { readonly taskId: TaskId; readonly keyFilter: Readonly>; }; /** 从入站事实里按规则提取关联键 */ export function extractKeys(facts: InboundFacts, rules: OwnershipKeyRules): Record { const keys: Record = {}; if (facts.threadId !== undefined) keys['threadId'] = facts.threadId; for (const rule of rules.patterns) { const haystack = rule.source === 'subject' ? facts.subject : rule.source === 'body' ? facts.body : `${facts.subject}\n${facts.body}`; const match = new RegExp(rule.regex).exec(haystack); const captured = match?.[1] ?? match?.[0]; if (captured !== undefined) keys[rule.key] = captured; } return keys; } export type ResolveInput = { readonly facts: InboundFacts; readonly rules: OwnershipKeyRules; readonly keys: Readonly>; readonly outbound: readonly OutboundRef[]; readonly listeners: readonly ListenerKeyIndex[]; }; export function resolveOwnership(input: ResolveInput): OwnershipResolution { const { facts, rules, keys, outbound, listeners } = input; // ① 线程精确匹配 if (facts.threadId !== undefined) { const hits = outbound.filter((o) => o.threadId === facts.threadId); const first = hits[0]; if (first !== undefined) return { outcome: 'owned', rule: 'thread', taskId: first.taskId }; } // ② 关联键命中监听的过滤条件 const keyHits = listeners.filter((l) => { const entries = Object.entries(l.keyFilter); if (entries.length === 0) return false; return entries.every(([k, v]) => keys[k] === v); }); const uniqueKeyTasks = [...new Set(keyHits.map((h) => h.taskId))]; if (uniqueKeyTasks.length === 1) { return { outcome: 'owned', rule: 'listener-key', taskId: uniqueKeyTasks[0]! }; } if (uniqueKeyTasks.length > 1) return unowned('ambiguous', rules); // ③ 地址 + 时间窗 const windowStart = facts.receivedAt.getTime() - rules.addressWindowMs; const addresses = new Set([facts.from, ...facts.to].map((a) => a.toLowerCase())); const windowHits = outbound.filter( (o) => o.sentAt.getTime() >= windowStart && addresses.has(o.address.toLowerCase()), ); const uniqueWindowTasks = [...new Set(windowHits.map((h) => h.taskId))]; if (uniqueWindowTasks.length === 1) { return { outcome: 'owned', rule: 'address-window', taskId: uniqueWindowTasks[0]! }; } if (uniqueWindowTasks.length > 1) return unowned('ambiguous', rules); return unowned('no-match', rules); } function unowned( reason: 'no-match' | 'ambiguous' | 'extract-failed', rules: OwnershipKeyRules, ): OwnershipResolution { return { outcome: 'unowned', reason, ...(rules.fallbackPosition === undefined ? {} : { fallbackPosition: rules.fallbackPosition }), }; }