export type ParserMode = 'ai' | 'shadow' | 'auto' | 'program'; export type ParserPolicy = 'configurable' | 'program_only'; export type BusinessRouteId = | 'team_order_create' | 'team_order_batch_create' | 'shared_plan_create' | 'shared_child_order_create' | 'shared_child_order_batch_create' | 'passenger_list_import_independent' | 'passenger_list_import_shared_child' | 'arrangement_guide_create' | 'arrangement_guide_update' | 'arrangement_vehicle_create' | 'arrangement_vehicle_update' | 'arrangement_hotel_create' | 'arrangement_transport_create' | 'arrangement_transport_update' | 'arrangement_other_create' | 'arrangement_other_update' | 'order_update_shared_plan' | 'order_update_shared_child' | 'order_update_independent' | 'arrangement_hotel_update' | 'order_cancel' | 'order_restore' | 'confirmation_export'; export type ProgramFieldKind = | 'text' | 'named_ref' | 'date' | 'date_or_range' | 'date_range' | 'date_list' | 'passenger_counts' | 'room_counts' | 'positive_integer' | 'boolean' | 'passenger_tsv' | 'update_targets' | 'hotel_changes' | 'export_type'; export interface ProgramFieldDefinition { key: string; label: string; aliases?: readonly string[]; required?: boolean; kind: ProgramFieldKind; } export interface BusinessRouteDefinition { routeId: BusinessRouteId; releaseOrder: number; directive: string; aliases: readonly string[]; action: string; discriminator?: string; programVersion: string; inputContractVersion: string; parserPolicy?: ParserPolicy; fields: readonly ProgramFieldDefinition[]; } const CONFIGURABLE_PARSER_MODES: readonly ParserMode[] = Object.freeze(['ai', 'shadow', 'auto', 'program']); const PROGRAM_ONLY_PARSER_MODES: readonly ParserMode[] = Object.freeze(['program']); export function allowedParserModesForRoute(route: BusinessRouteDefinition): readonly ParserMode[] { return route.parserPolicy === 'program_only' ? PROGRAM_ONLY_PARSER_MODES : CONFIGURABLE_PARSER_MODES; } export function isParserModeAllowedForRoute(route: BusinessRouteDefinition, mode: unknown): mode is ParserMode { return allowedParserModesForRoute(route).includes(String(mode || '') as ParserMode); } export function effectiveParserModeForRoute(route: BusinessRouteDefinition, configuredMode: unknown): ParserMode { if (isParserModeAllowedForRoute(route, configuredMode)) return configuredMode; return route.parserPolicy === 'program_only' ? 'program' : 'ai'; } const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.15'; export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.137'; const lookupFields = ({ customerRequired = true } = {}): ProgramFieldDefinition[] => [ { key: 'order_no', label: '单号', aliases: ['团号', '订单号'], kind: 'text' }, { key: 'customer', label: '预订客户', aliases: ['预定客户', '客户'], required: customerRequired, kind: 'named_ref' }, { key: 'departure_date', label: '出发日期', aliases: ['发团日期'], required: true, kind: 'date' }, { key: 'product', label: '产品名称', aliases: ['产品搜索', '产品'], kind: 'named_ref' }, { key: 'leader', label: '领队', aliases: ['领队姓名'], kind: 'named_ref' } ]; export const BUSINESS_ROUTES: readonly BusinessRouteDefinition[] = Object.freeze([ { routeId: 'team_order_create', releaseOrder: 12, directive: '独立团单个下单', aliases: [], action: 'team_order_create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'departure_date', label: '发团日期', aliases: ['出发日期'], required: true, kind: 'date' }, { key: 'customer', label: '预订客户', aliases: ['预定客户'], required: true, kind: 'named_ref' }, { key: 'product', label: '产品搜索', aliases: ['产品名称'], required: true, kind: 'named_ref' }, { key: 'passenger_counts', label: '预估人数', aliases: ['人数'], required: true, kind: 'passenger_counts' }, { key: 'room_counts', label: '用房数量', aliases: ['房型数量'], required: true, kind: 'room_counts' }, { key: 'special_requests', label: '特殊要求', kind: 'text' } ] }, { routeId: 'team_order_batch_create', releaseOrder: 13, directive: '独立团批量下单', aliases: [], action: 'team_order_batch_create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'departure_range', label: '发团日期', aliases: ['出发日期'], required: true, kind: 'date_range' }, { key: 'customer', label: '预订客户', aliases: ['预定客户'], required: true, kind: 'named_ref' }, { key: 'product', label: '产品搜索', aliases: ['产品名称'], required: true, kind: 'named_ref' }, { key: 'passenger_counts', label: '预估人数', aliases: ['人数'], required: true, kind: 'passenger_counts' }, { key: 'room_counts', label: '用房数量', aliases: ['房型数量'], required: true, kind: 'room_counts' }, { key: 'departure_dates', label: '发团周期', aliases: ['实际发团日期'], required: true, kind: 'date_list' }, { key: 'special_requests', label: '特殊要求', kind: 'text' } ] }, { routeId: 'shared_plan_create', releaseOrder: 14, directive: '散拼团新增计划', aliases: [], action: 'shared_plan_create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'departure_source', label: '发团日期', aliases: ['出发日期'], required: true, kind: 'date_or_range' }, { key: 'product', label: '产品搜索', aliases: ['产品名称'], required: true, kind: 'named_ref' }, { key: 'planned_capacity', label: '计划收客数', aliases: ['目标计划收客数'], required: true, kind: 'positive_integer' }, { key: 'room_counts', label: '用房数量', aliases: ['房型数量'], required: true, kind: 'room_counts' }, { key: 'departure_dates', label: '发团周期', aliases: ['实际发团日期'], kind: 'date_list' }, { key: 'split_customer', label: '拼团客户', kind: 'named_ref' }, { key: 'split_passenger_counts', label: '拼团人数', kind: 'passenger_counts' } ] }, { routeId: 'shared_child_order_create', releaseOrder: 15, directive: '散拼团单个新增子单', aliases: ['散拼团拼单'], action: 'shared_child_order_create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'parent_group_no', label: '母团团号', aliases: ['母团单号'], kind: 'text' }, { key: 'customer', label: '预订客户', aliases: ['预定客户'], required: true, kind: 'named_ref' }, { key: 'departure_date', label: '出发日期', aliases: ['发团日期'], required: true, kind: 'date' }, { key: 'product', label: '产品名称', aliases: ['产品搜索'], kind: 'named_ref' }, { key: 'leader', label: '领队', aliases: ['领队姓名'], kind: 'named_ref' }, { key: 'passenger_counts', label: '人数', aliases: ['预估人数'], required: true, kind: 'passenger_counts' }, { key: 'special_requests', label: '特殊要求', kind: 'text' } ] }, { routeId: 'shared_child_order_batch_create', releaseOrder: 19, directive: '散拼团多个新增子单', aliases: [], action: 'shared_child_order_batch_create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, parserPolicy: 'program_only', fields: [ { key: 'customer', label: '预订客户', aliases: ['预定客户'], required: true, kind: 'named_ref' }, { key: 'departure_range', label: '出发日期', aliases: ['发团日期'], required: true, kind: 'date_range' }, { key: 'product', label: '产品名称', aliases: ['产品搜索'], required: true, kind: 'named_ref' }, { key: 'leader', label: '领队', aliases: ['领队姓名'], kind: 'named_ref' }, { key: 'passenger_counts', label: '人数', aliases: ['预估人数'], required: true, kind: 'passenger_counts' }, { key: 'special_requests', label: '特殊要求', kind: 'text' } ] }, { routeId: 'passenger_list_import_independent', releaseOrder: 10, directive: '导入独立团名单', aliases: [], action: 'passenger_list_import', discriminator: 'independent_order', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, parserPolicy: 'program_only', fields: [ ...lookupFields(), { key: 'passenger_tsv', label: '名单内容', aliases: ['游客名单', '名单'], required: true, kind: 'passenger_tsv' }, { key: 'overwrite_confirmed', label: '覆盖确认', aliases: ['确认覆盖'], kind: 'boolean' } ] }, { routeId: 'passenger_list_import_shared_child', releaseOrder: 11, directive: '导入散拼子单名单', aliases: [], action: 'passenger_list_import', discriminator: 'shared_child_order', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, parserPolicy: 'program_only', fields: [ ...lookupFields(), { key: 'passenger_tsv', label: '名单内容', aliases: ['游客名单', '名单'], required: true, kind: 'passenger_tsv' }, { key: 'overwrite_confirmed', label: '覆盖确认', aliases: ['确认覆盖'], kind: 'boolean' } ] }, { routeId: 'arrangement_guide_create', releaseOrder: 2, directive: '安排导游', aliases: [], action: 'arrangement_guide', discriminator: 'create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'resource', label: '导游搜索', aliases: ['导游'], required: true, kind: 'named_ref' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_vehicle_create', releaseOrder: 3, directive: '安排用车', aliases: [], action: 'arrangement_vehicle', discriminator: 'create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'vehicle_range', label: '用车日期', aliases: ['日期'], required: true, kind: 'date_range' }, { key: 'vehicle_type', label: '用车类型', aliases: ['车型'], kind: 'text' }, { key: 'quantity', label: '数量', required: true, kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_hotel_create', releaseOrder: 4, directive: '安排酒店', aliases: [], action: 'arrangement_hotel', discriminator: 'create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'start_date', label: '入住日期', required: true, kind: 'date' }, { key: 'end_date', label: '离店日期', required: true, kind: 'date' }, { key: 'resource', label: '酒店搜索', aliases: ['酒店'], required: true, kind: 'named_ref' }, { key: 'room_count', label: '间数', aliases: ['房间数', '房间总数'], required: true, kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_transport_create', releaseOrder: 5, directive: '安排大交通', aliases: [], action: 'arrangement_transport', discriminator: 'create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'quantity', label: '数量', required: true, kind: 'positive_integer' }, { key: 'date', label: '交通日期', aliases: ['业务日期'], required: true, kind: 'date' }, { key: 'ticket_description', label: '车票说明', aliases: ['车票说明搜索'], kind: 'text' }, { key: 'item', label: '车票/控位说明', aliases: ['项目', '说明'], kind: 'text' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_other_create', releaseOrder: 6, directive: '安排其他/备案', aliases: ['安排其他', '安排备案'], action: 'arrangement_other', discriminator: 'create', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'date', label: '业务日期', aliases: ['交通日期'], required: true, kind: 'date' }, { key: 'item', label: '项目', aliases: ['备案说明'], kind: 'text' }, { key: 'quantity', label: '数量', kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' }, { key: 'filing_number', label: '备案号', kind: 'text' }, { key: 'filing_date', label: '备案日期', kind: 'date' }, { key: 'entry_port', label: '入境口岸', kind: 'text' }, { key: 'exit_port', label: '出境口岸', kind: 'text' } ] }, { routeId: 'order_update_shared_plan', releaseOrder: 7, directive: '修改散拼母团计划收客数', aliases: ['散拼母团信息修改'], action: 'order_update_shared_plan', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ ...lookupFields({ customerRequired: false }), { key: 'planned_capacity', label: '目标计划收客数', aliases: ['计划收客数'], required: true, kind: 'positive_integer' } ] }, { routeId: 'order_update_shared_child', releaseOrder: 8, directive: '追加散拼子单订房说明', aliases: ['散拼子单信息修改'], action: 'order_update_shared_child', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ ...lookupFields(), { key: 'lodging_note', label: '追加内容', aliases: ['订房说明'], required: true, kind: 'text' } ] }, { routeId: 'order_update_independent', releaseOrder: 18, directive: '独立团信息修改', aliases: [], action: 'order_update_independent', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ ...lookupFields(), { key: 'update_targets', label: '目标修改', aliases: ['修改目标'], required: true, kind: 'update_targets' }, { key: 'business_reason', label: '业务原因', kind: 'text' }, { key: 'expected_time', label: '期望完成时间', aliases: ['完成时间'], kind: 'text' } ] }, { routeId: 'arrangement_hotel_update', releaseOrder: 9, directive: '安排变更', aliases: ['安排酒店变更'], action: 'arrangement_hotel', discriminator: 'update', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'hotel_changes', label: '目标变更', aliases: ['变更目标'], kind: 'hotel_changes' }, { key: 'start_date', label: '入住日期', kind: 'date' }, { key: 'end_date', label: '离店日期', aliases: ['离房日期', '退房日期'], kind: 'date' }, { key: 'resource', label: '酒店搜索', aliases: ['酒店'], kind: 'named_ref' }, { key: 'room_count', label: '间数', aliases: ['房间数'], kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' }, { key: 'arrangement_type', label: '安排类型', kind: 'text' }, { key: 'business_reason', label: '业务原因', kind: 'text' } ] }, { routeId: 'order_cancel', releaseOrder: 16, directive: '取消订单', aliases: [], action: 'order_cancel', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: lookupFields() }, { routeId: 'order_restore', releaseOrder: 17, directive: '恢复订单', aliases: [], action: 'order_restore', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ ...lookupFields(), { key: 'restore_status', label: '恢复状态', aliases: ['目标状态'], kind: 'text' } ] }, { routeId: 'confirmation_export', releaseOrder: 1, directive: '导出团队文件', aliases: [], action: 'confirmation_export', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ ...lookupFields(), { key: 'export_type', label: '文件类型', aliases: ['导出类型'], required: true, kind: 'export_type' }, { key: 'visitor_name', label: '游客姓名', aliases: ['接机牌姓名'], kind: 'text' } ] }, { routeId: 'arrangement_guide_update', releaseOrder: 20, directive: '安排导游变更', aliases: [], action: 'arrangement_guide', discriminator: 'update', parserPolicy: 'program_only', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'resource', label: '导游搜索', aliases: ['导游'], required: true, kind: 'named_ref' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_vehicle_update', releaseOrder: 21, directive: '安排用车变更', aliases: [], action: 'arrangement_vehicle', discriminator: 'update', parserPolicy: 'program_only', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'vehicle_range', label: '用车日期', aliases: ['日期'], required: true, kind: 'date_range' }, { key: 'vehicle_type', label: '用车类型', aliases: ['车型'], kind: 'text' }, { key: 'quantity', label: '数量', required: true, kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_transport_update', releaseOrder: 22, directive: '安排大交通变更', aliases: [], action: 'arrangement_transport', discriminator: 'update', parserPolicy: 'program_only', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'quantity', label: '数量', required: true, kind: 'positive_integer' }, { key: 'date', label: '交通日期', aliases: ['业务日期'], required: true, kind: 'date' }, { key: 'ticket_description', label: '车票说明', aliases: ['车票说明搜索'], kind: 'text' }, { key: 'item', label: '车票/控位说明', aliases: ['项目', '说明'], kind: 'text' }, { key: 'remark', label: '备注', kind: 'text' } ] }, { routeId: 'arrangement_other_update', releaseOrder: 23, directive: '安排其他/备案变更', aliases: ['安排其他变更', '安排备案变更'], action: 'arrangement_other', discriminator: 'update', parserPolicy: 'program_only', programVersion: PROGRAM_VERSION, inputContractVersion: BUSINESS_INPUT_CONTRACT_VERSION, fields: [ { key: 'group_no', label: '团号', aliases: ['单号'], required: true, kind: 'text' }, { key: 'supplier', label: '结算单位搜索', aliases: ['供应商搜索', '结算单位'], required: true, kind: 'named_ref' }, { key: 'date', label: '业务日期', aliases: ['交通日期'], required: true, kind: 'date' }, { key: 'item', label: '项目', aliases: ['备案说明'], kind: 'text' }, { key: 'quantity', label: '数量', kind: 'positive_integer' }, { key: 'remark', label: '备注', kind: 'text' }, { key: 'filing_number', label: '备案号', kind: 'text' }, { key: 'filing_date', label: '备案日期', kind: 'date' }, { key: 'entry_port', label: '入境口岸', kind: 'text' }, { key: 'exit_port', label: '出境口岸', kind: 'text' } ] } ]); const routeMap = new Map(BUSINESS_ROUTES.map((route) => [route.routeId, route])); const directiveLengths = BUSINESS_ROUTES .flatMap((route) => [route.directive, ...route.aliases]) .map((directive) => [...normalizeDirectiveText(directive)].length); const MIN_DIRECTIVE_CANDIDATE_LENGTH = Math.max(1, Math.min(...directiveLengths) - 1); const MAX_DIRECTIVE_CANDIDATE_LENGTH = Math.max(...directiveLengths) + 1; const MAX_SIGNATURE_LABELS = 64; export function businessRouteById(routeId: unknown): BusinessRouteDefinition | null { return routeMap.get(String(routeId || '') as BusinessRouteId) || null; } export function normalizeDirectiveText(value: unknown): string { return String(value || '') .replace(/^\uFEFF/, '') .normalize('NFKC') .trim() .replace(/[\s\u3000]+/g, '') .replace(/[::;;。.!!??]+$/u, ''); } export function normalizeFieldLabel(value: unknown): string { return String(value || '') .normalize('NFKC') .trim() .replace(/[((]\s*(?:必填|选填)\s*[))]/gu, '') .replace(/[\s\u3000]+/g, '') .replace(/[::]+$/u, ''); } export function levenshteinDistance(leftValue: unknown, rightValue: unknown): number { const left = [...String(leftValue || '')]; const right = [...String(rightValue || '')]; if (!left.length) return right.length; if (!right.length) return left.length; const previous = Array.from({ length: right.length + 1 }, (_, index) => index); for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) { const current = [leftIndex]; for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) { current[rightIndex] = Math.min( current[rightIndex - 1] + 1, previous[rightIndex] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1) ); } previous.splice(0, previous.length, ...current); } return previous[right.length]; } export interface BusinessRouteResolution { route: BusinessRouteDefinition | null; routeId: BusinessRouteId | null; matchedDirective: string | null; match: 'exact' | 'alias' | 'fuzzy' | 'signature' | 'unresolved' | 'ambiguous'; distance: number | null; } export function firstNonEmptyLine(message: unknown): string { return String(message || '') .replace(/^\uFEFF/, '') .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean) || ''; } function resolveDirectiveLine(message: unknown): BusinessRouteResolution { const line = normalizeDirectiveText(message); if (!line) return { route: null, routeId: null, matchedDirective: null, match: 'unresolved', distance: null }; for (const route of BUSINESS_ROUTES) { const candidates = [route.directive, ...route.aliases]; for (const candidate of candidates) { if (line === normalizeDirectiveText(candidate)) { return { route, routeId: route.routeId, matchedDirective: candidate, match: candidate === route.directive ? 'exact' : 'alias', distance: 0 }; } } } const byRoute = new Map(); for (const route of BUSINESS_ROUTES) { for (const candidate of [route.directive, ...route.aliases]) { const distance = levenshteinDistance(line, normalizeDirectiveText(candidate)); const current = byRoute.get(route.routeId); if (!current || distance < current.distance) byRoute.set(route.routeId, { route, candidate, distance }); } } const ranked = [...byRoute.values()].sort((left, right) => left.distance - right.distance); if (!ranked.length || ranked[0].distance > 1) { return { route: null, routeId: null, matchedDirective: null, match: 'unresolved', distance: ranked[0]?.distance ?? null }; } if (ranked[1] && ranked[1].distance === ranked[0].distance) { return { route: null, routeId: null, matchedDirective: null, match: 'ambiguous', distance: ranked[0].distance }; } return { route: ranked[0].route, routeId: ranked[0].route.routeId, matchedDirective: ranked[0].candidate, match: 'fuzzy', distance: ranked[0].distance }; } const LOOKUP_ORDER_NO_ALTERNATIVE_ROUTES = new Set([ 'passenger_list_import_independent', 'passenger_list_import_shared_child', 'order_update_shared_child', 'order_update_independent', 'order_cancel', 'order_restore', 'confirmation_export' ]); function signatureRequiredKeys(route: BusinessRouteDefinition, keys: ReadonlySet): string[] { const required = route.fields.filter((field) => field.required).map((field) => field.key); if (!keys.has('order_no')) return required; if (LOOKUP_ORDER_NO_ALTERNATIVE_ROUTES.has(route.routeId)) { return required.filter((key) => !['customer', 'departure_date'].includes(key)); } if (route.routeId === 'order_update_shared_plan') { return required.filter((key) => key !== 'departure_date'); } return required; } function resolveFieldSignature(lines: readonly string[]): BusinessRouteResolution | null { const labels = lines .map((line) => { const delimiter = line.search(/[::]/u); return delimiter >= 0 ? line.slice(0, delimiter).trim() : ''; }) .filter(Boolean); if (labels.length < 2 || labels.length > MAX_SIGNATURE_LABELS) return null; const candidates: BusinessRouteDefinition[] = []; for (const route of BUSINESS_ROUTES) { const keys = new Set(); let accepted = true; for (const label of labels) { const resolution = resolveRouteFieldLabel(route, label); if (!resolution.field) { accepted = false; break; } keys.add(resolution.field.key); } if (!accepted || keys.size < 2) continue; if (signatureRequiredKeys(route, keys).some((key) => !keys.has(key))) continue; candidates.push(route); } if (candidates.length === 1) { const route = candidates[0]; return { route, routeId: route.routeId, matchedDirective: null, match: 'signature', distance: null }; } if (candidates.length > 1) { return { route: null, routeId: null, matchedDirective: null, match: 'ambiguous', distance: null }; } return null; } /** * Resolve a business route from the complete user-visible message, regardless * of whether it arrived from the workbench or AgentBus. A unique directive on * any line wins. When no directive exists, a complete field signature may * select a route only if every label belongs to exactly one viable route. */ export function resolveBusinessRoute(message: unknown): BusinessRouteResolution { const lines = String(message || '') .replace(/^\uFEFF/u, '') .split(/\r?\n/u) .map((line) => line.trim()) .filter(Boolean); if (!lines.length) return { route: null, routeId: null, matchedDirective: null, match: 'unresolved', distance: null }; const directiveMatches = lines .filter((line) => { const length = [...normalizeDirectiveText(line)].length; return length >= MIN_DIRECTIVE_CANDIDATE_LENGTH && length <= MAX_DIRECTIVE_CANDIDATE_LENGTH; }) .map(resolveDirectiveLine); const matched = directiveMatches.filter((resolution) => resolution.routeId); const routeIds = [...new Set(matched.map((resolution) => resolution.routeId))]; if (routeIds.length > 1) { return { route: null, routeId: null, matchedDirective: null, match: 'ambiguous', distance: null }; } if (routeIds.length === 1) { const rank = (match: BusinessRouteResolution['match']) => match === 'exact' ? 0 : match === 'alias' ? 1 : 2; return [...matched].sort((left, right) => rank(left.match) - rank(right.match))[0]; } if (directiveMatches.some((resolution) => resolution.match === 'ambiguous')) { return { route: null, routeId: null, matchedDirective: null, match: 'ambiguous', distance: null }; } const signature = resolveFieldSignature(lines); if (signature) return signature; const distances = directiveMatches .map((resolution) => resolution.distance) .filter((distance): distance is number => distance !== null); return { route: null, routeId: null, matchedDirective: null, match: 'unresolved', distance: distances.length ? Math.min(...distances) : null }; } export interface FieldLabelResolution { field: ProgramFieldDefinition | null; match: 'exact' | 'alias' | 'fuzzy' | 'unknown' | 'ambiguous'; distance: number | null; } export function resolveRouteFieldLabel(route: BusinessRouteDefinition, rawLabel: unknown): FieldLabelResolution { const normalized = normalizeFieldLabel(rawLabel); if (!normalized) return { field: null, match: 'unknown', distance: null }; for (const field of route.fields) { if (normalized === normalizeFieldLabel(field.label)) return { field, match: 'exact', distance: 0 }; if ((field.aliases || []).some((alias) => normalized === normalizeFieldLabel(alias))) { return { field, match: 'alias', distance: 0 }; } } const byField = new Map(); for (const field of route.fields) { for (const candidate of [field.label, ...(field.aliases || [])]) { const distance = levenshteinDistance(normalized, normalizeFieldLabel(candidate)); const current = byField.get(field.key); if (!current || distance < current.distance) byField.set(field.key, { field, distance }); } } const ranked = [...byField.values()].sort((left, right) => left.distance - right.distance); if (!ranked.length || ranked[0].distance > 1) return { field: null, match: 'unknown', distance: ranked[0]?.distance ?? null }; if (ranked[1] && ranked[1].distance === ranked[0].distance) { return { field: null, match: 'ambiguous', distance: ranked[0].distance }; } return { field: ranked[0].field, match: 'fuzzy', distance: ranked[0].distance }; } export const BUSINESS_DIRECTIVE_PREFIXES: readonly string[] = Object.freeze( BUSINESS_ROUTES.flatMap((route) => [route.directive, ...route.aliases]) ); export const BUSINESS_PARSER_RELEASE_SEQUENCE: readonly BusinessRouteId[] = Object.freeze( [...BUSINESS_ROUTES] .sort((left, right) => left.releaseOrder - right.releaseOrder) .map((route) => route.routeId) );