Files
LWLT-AIBOT/control-plane/src/business-routes.ts
2026-09-17 11:03:19 +08:00

741 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.13';
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.136';
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<BusinessRouteId, { route: BusinessRouteDefinition; candidate: string; distance: number }>();
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<BusinessRouteId>([
'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>): 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<string>();
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<string, { field: ProgramFieldDefinition; distance: number }>();
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)
);