741 lines
31 KiB
TypeScript
741 lines
31 KiB
TypeScript
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)
|
||
);
|