变更问题修复
This commit is contained in:
1 parent
6de4422733
commit
9625110f44
62 files changed
+18116
-95
No files matched your search
@@ -79,8 +79,8 @@ export function effectiveParserModeForRoute(route: BusinessRouteDefinition, conf
|
||||
return route.parserPolicy === 'program_only' ? 'program' : 'ai';
|
||||
}
|
||||
|
||||
const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.11';
|
||||
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.132';
|
||||
const PROGRAM_VERSION = 'ltjt-program-parser-v1.0.12';
|
||||
export const BUSINESS_INPUT_CONTRACT_VERSION = 'business-input-templates-0.5.133';
|
||||
|
||||
const lookupFields = ({ customerRequired = true } = {}): ProgramFieldDefinition[] => [
|
||||
{ key: 'order_no', label: '单号', aliases: ['团号', '订单号'], kind: 'text' },
|
||||
|
||||
@@ -187,6 +187,23 @@ function parseDateParts(rawValue: string): { year: number | null; month: number;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseOriginalDate(rawValue: string): FieldParseResult {
|
||||
const parts = parseDateParts(rawValue);
|
||||
if (!parts || !validDate(parts.year ?? 2000, parts.month, parts.day)) return { ok: false, message: '请填写有效的原安排日期 YYYY-MM-DD 或 MM-DD。' };
|
||||
return { ok: true, value: parts.year === null ? `${pad2(parts.month)}-${pad2(parts.day)}` : formatDate(parts.year, parts.month, parts.day) };
|
||||
}
|
||||
|
||||
function parseOriginalDateRange(rawValue: string): FieldParseResult {
|
||||
const pieces = compactText(rawValue).split(/\s*(?:到|至|~|~|—|–)\s*/u).filter(Boolean);
|
||||
if (pieces.length !== 2) return { ok: false, message: '请填写原用车开始日期和结束日期。' };
|
||||
const start = parseOriginalDate(pieces[0]);
|
||||
const end = parseOriginalDate(pieces[1]);
|
||||
if (!start.ok) return start;
|
||||
if (!end.ok) return end;
|
||||
if (String(start.value).length === 10 && String(end.value).length === 10 && String(end.value) < String(start.value)) return { ok: false, message: '原用车结束日期不能早于开始日期。' };
|
||||
return { ok: true, value: { start_date: start.value, end_date: end.value } };
|
||||
}
|
||||
|
||||
function parseDate(rawValue: string, context: DateContext, {
|
||||
nextOccurrence = false,
|
||||
referenceYear
|
||||
@@ -910,7 +927,7 @@ function buildOperation(route: BusinessRouteDefinition, values: Map<string, unkn
|
||||
if (values.get('hotel_changes')) return { ok: false, code: 'program_field_conflict', message: '按日期变更酒店时,请使用入住日期、离店日期、酒店搜索和间数,不要混填目标变更。', field: 'hotel_changes' };
|
||||
const missing = datedFields.find((key) => !values.has(key));
|
||||
if (missing) return { ok: false, code: 'program_missing_required', message: '请补齐原安排的入住日期、离店日期,以及新酒店搜索和间数。', field: missing };
|
||||
if (String(values.get('end_date')) <= String(values.get('start_date'))) return { ok: false, code: 'program_invalid_value', message: '原安排的离店日期必须晚于入住日期。', field: 'end_date' };
|
||||
if (String(values.get('start_date')).length === 10 && String(values.get('end_date')).length === 10 && String(values.get('end_date')) <= String(values.get('start_date'))) return { ok: false, code: 'program_invalid_value', message: '原安排的离店日期必须晚于入住日期。', field: 'end_date' };
|
||||
return { ok: true, operation: operation(route.action, {
|
||||
existing_refs: { identifier: values.get('group_no') },
|
||||
arrangement: {
|
||||
@@ -959,6 +976,10 @@ function buildOperation(route: BusinessRouteDefinition, values: Map<string, unkn
|
||||
|
||||
function parseField(field: ProgramFieldDefinition, rawValue: string, route: BusinessRouteDefinition, context: DateContext, parsed: Map<string, unknown>): FieldParseResult {
|
||||
if (isPlaceholder(rawValue)) return { ok: false, message: '该字段尚未填写。' };
|
||||
if (['arrangement_hotel_update', 'arrangement_vehicle_update', 'arrangement_transport_update', 'arrangement_other_update'].includes(route.routeId)) {
|
||||
if (field.kind === 'date') return parseOriginalDate(rawValue);
|
||||
if (field.kind === 'date_range') return parseOriginalDateRange(rawValue);
|
||||
}
|
||||
const nextOccurrence = NEXT_OCCURRENCE_DATE_ROUTES.has(route.routeId);
|
||||
switch (field.kind) {
|
||||
case 'text': return { ok: true, value: compactText(rawValue) };
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseProgramInput } from '../src/program-parser.js';
|
||||
|
||||
const hotel = (start: string, end: string, directive = '安排酒店变更') => `${directive}\n团号:LW-260901VIP-A\n入住日期:${start}\n离店日期:${end}\n酒店搜索:测试酒店TWN\n间数:12`;
|
||||
const parse = (rawText: string, receivedAt = '2026-09-14T13:16:07+08:00') => parseProgramInput({ rawText, receivedAt });
|
||||
|
||||
test('original hotel dates never roll forward with the submission date or group number', async () => {
|
||||
for (const now of ['2026-09-14T13:16:07+08:00', '2027-01-01T00:00:00+08:00']) {
|
||||
const result = await parse(hotel('9-4', '9-5'), now);
|
||||
assert.equal(result.status, 'agent_parse_passed', JSON.stringify(result));
|
||||
assert.deepEqual((result.operation as any).data.arrangement.selection, { start_date: '09-04', end_date: '09-05' });
|
||||
assert.equal((result.operation as any).data.arrangement.changes.end_date, undefined);
|
||||
}
|
||||
const local = await parse(hotel('9-12', '9-13').replace('LW-260901VIP-A', 'LLW-270911A-A'));
|
||||
assert.deepEqual((local.operation as any).data.arrangement.selection, { start_date: '09-12', end_date: '09-13' });
|
||||
});
|
||||
|
||||
test('original dates retain explicit years and allow cross-year, mixed-year and leap-day selectors', async () => {
|
||||
for (const [start, end, expected] of [
|
||||
['2026-9-4', '2026-9-5', ['2026-09-04', '2026-09-05']],
|
||||
['12月31日', '1月2日', ['12-31', '01-02']],
|
||||
['2026-12-31', '1-2', ['2026-12-31', '01-02']],
|
||||
['2/29', '3/1', ['02-29', '03-01']]
|
||||
] as const) {
|
||||
const result = await parse(hotel(start, end));
|
||||
assert.equal(result.status, 'agent_parse_passed', JSON.stringify(result));
|
||||
assert.deepEqual((result.operation as any).data.arrangement.selection, { start_date: expected[0], end_date: expected[1] });
|
||||
}
|
||||
for (const [start, end] of [['2-30', '3-1'], ['2026-2-29', '2026-3-1'], ['2026-9-5', '2026-9-4'], ['2026-9-4', '2026-9-4']]) {
|
||||
const result = await parse(hotel(start, end));
|
||||
assert.ok(['agent_parse_blocked', 'agent_parse_needs_input'].includes(result.status));
|
||||
assert.equal(result.operation, null);
|
||||
}
|
||||
});
|
||||
|
||||
test('vehicle, transport and other original dates also preserve month/day including filing date', async () => {
|
||||
const group = '\n团号:LW-260901VIP-A\n结算单位搜索:测试新供应商\n数量:2';
|
||||
for (const [input, selection] of [
|
||||
[`安排用车变更${group}\n用车日期:12-31到1-2`, { start_date: '12-31', end_date: '01-02' }],
|
||||
[`安排大交通变更${group}\n交通日期:9-4`, { date: '09-04' }],
|
||||
[`安排其他/备案变更${group}\n业务日期:9-4\n备案日期:9-3`, { date: '09-04', filing_date: '09-03' }]
|
||||
] as const) {
|
||||
const result = await parse(input);
|
||||
assert.equal(result.status, 'agent_parse_passed', JSON.stringify(result));
|
||||
assert.deepEqual((result.operation as any).data.arrangement.selection, selection);
|
||||
}
|
||||
});
|
||||
|
||||
test('hotel creation and legacy target-date changes retain their existing year semantics', async () => {
|
||||
const create = await parse(hotel('9-4', '9-5', '安排酒店'));
|
||||
assert.equal((create.operation as any).data.arrangement.start_date, '2026-09-04');
|
||||
assert.equal((create.operation as any).data.arrangement.end_date, '2026-09-05');
|
||||
const legacy = await parse('安排变更\n团号:LW-260901VIP-A\n目标变更:离店日期改为9-5\n安排类型:酒店');
|
||||
assert.equal(legacy.status, 'agent_parse_passed', JSON.stringify(legacy));
|
||||
assert.equal((legacy.operation as any).data.arrangement.changes.end_date, '2027-09-05');
|
||||
});
|
||||
@@ -189,7 +189,7 @@ test('versioned sanitized gold corpus replays deterministically', async () => {
|
||||
const corpus = JSON.parse(await readFile(
|
||||
new URL('../../samples/business-parser-gold-corpus.json', import.meta.url), 'utf8'
|
||||
));
|
||||
assert.equal(corpus.version, 'ltjt-program-parser-v1.0.11');
|
||||
assert.equal(corpus.version, 'ltjt-program-parser-v1.0.12');
|
||||
assert.ok(corpus.cases.length >= 28);
|
||||
for (const candidate of corpus.cases) {
|
||||
const parse = () => parseProgramInput({
|
||||
|
||||
Reference in new issue
Block a user