账号支持只看本账号看板;模板重新解析改为保留原映射并多轮补缺。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-19 10:38:53 +08:00
co-authored by Cursor
parent 6cd6803147
commit 9bb69ae72e
19 changed files with 807 additions and 114 deletions
+145
View File
@@ -101,6 +101,93 @@ class TestSheetMapParse(unittest.TestCase):
}
self.assertEqual(assess_mapping_gaps(payload, mapping), [])
def test_assess_gaps_air_ignores_sea_labels(self) -> None:
"""空运不按海运词典报「提货地址/货值」缺口。"""
payload = {
"transportMode": "air",
"cellInventory": [
"A!B2 | 起运地/Origin Airport | - | -",
"A!B3 | 毛重/Gross Weight | - | -",
"A!Z99 | 提货地址/Place of Receipt | - | -",
],
}
mapping = {"fields": {"origin": "A!C2", "weight_kg": "A!C3"}, "feeRegions": []}
gaps = assess_mapping_gaps(payload, mapping)
self.assertFalse(any("pickup_address" in g for g in gaps))
self.assertEqual(gaps, [])
def test_assess_gaps_short_en_no_false_positive(self) -> None:
"""短英文(如 POL)单独出现不误报;须中文或 ≥4 字符英文标签。"""
payload = {
"transportMode": "sea",
"cellInventory": ["S!A1 | POL code table | - | -"],
}
mapping = {"fields": {}, "feeRegions": []}
gaps = assess_mapping_gaps(payload, mapping)
self.assertFalse(any("→ 应映射 pol" in g for g in gaps))
def test_assess_gaps_land_needs_route_regions(self) -> None:
payload = {
"transportMode": "land",
"cellInventory": [
"L!A1 | 项目 | - | -",
"L!B1 | 3T | - | -",
"L!C1 | 拼车 | - | -",
],
}
mapping = {"fields": {}, "feeRegions": [], "routeRegions": []}
gaps = assess_mapping_gaps(payload, mapping)
self.assertTrue(any("routeRegions" in g for g in gaps))
def test_assess_gaps_land_route_skips_matrix_fields(self) -> None:
"""陆运已有 routeRegions 时,不把矩阵列头始发地/目的地当 fields 缺口。"""
payload = {
"transportMode": "land",
"cellInventory": [
"L!A1 | 项目 | - | -",
"L!B1 | 始发地 | - | -",
"L!C1 | 目的地 | - | -",
"L!D1 | 运输时效 | - | -",
"L!E1 | 特殊说明 | - | -",
],
}
mapping = {
"fields": {},
"feeRegions": [],
"routeRegions": [{"id": "main", "title": "线路", "anchor": "L!A2"}],
}
gaps = assess_mapping_gaps(payload, mapping)
self.assertFalse(any("origin" in g for g in gaps))
self.assertFalse(any("destination" in g for g in gaps))
self.assertFalse(any("transit_time" in g for g in gaps))
self.assertTrue(any("special_remarks" in g for g in gaps))
def test_merge_prefer_existing_keeps_old_field(self) -> None:
from agent.llm.mode_sheet_map import merge_mapping_prefer_existing
old = {
"fields": {"quote_no": "S!B3", "pol": "S!B5"},
"feeRegions": [{"id": "origin_charges", "title": "旧始发港"}],
"routeRegions": [],
}
new = {
"fields": {"quote_no": "S!Z99", "pod": "S!B6"},
"feeRegions": [
{"id": "origin_charges", "title": "新始发港"},
{"id": "ocean_freight", "title": "海运费用"},
],
"routeRegions": [],
"mappingGaps": [],
}
merged = merge_mapping_prefer_existing(old, new)
self.assertEqual(merged["fields"]["quote_no"], "S!B3") # 旧优先
self.assertEqual(merged["fields"]["pol"], "S!B5") # 旧保留
self.assertEqual(merged["fields"]["pod"], "S!B6") # 新补缺
ids = [r["id"] for r in merged["feeRegions"]]
self.assertEqual(ids[0], "origin_charges")
self.assertEqual(merged["feeRegions"][0]["title"], "旧始发港")
self.assertIn("ocean_freight", ids)
def test_invoke_retries_when_gaps(self) -> None:
calls: list[str] = []
@@ -145,6 +232,64 @@ class TestSheetMapParse(unittest.TestCase):
self.assertEqual(out.get("gaps_after"), out["mapping"]["mappingGaps"])
self.assertEqual(out["mapping"]["mappingGaps"], [])
def test_invoke_multi_round_until_complete(self) -> None:
"""最多 3 轮:第 1 残缺 → 第 2 仍缺 → 第 3 齐。"""
calls: list[int] = []
def fake_chat(*, mode, messages, temperature=0.0): # noqa: ARG001
calls.append(1)
n = len(calls)
if n == 1:
content = (
'{"schemaVersion":"quote-template-mapping-v1",'
'"fields":{"quote_no":"S!B3"},"feeRegions":[]}'
)
elif n == 2:
content = (
'{"schemaVersion":"quote-template-mapping-v1",'
'"fields":{"quote_no":"S!B3","pickup_address":"S!H6"},'
'"feeRegions":[{"id":"origin_charges","title":"始发港费用",'
'"tmsSource":"polCostItems","anchor":"S!A19","stopAtText":["海运费用"],'
'"grow":"insert_before_stop","styleCopyRows":1}]}'
)
else:
content = (
'{"schemaVersion":"quote-template-mapping-v1",'
'"fields":{"quote_no":"S!B3","pickup_address":"S!H6","cargo_value":"S!B11"},'
'"feeRegions":['
'{"id":"origin_charges","title":"始发港费用","tmsSource":"polCostItems",'
'"anchor":"S!A19","stopAtText":["海运费用"],'
'"grow":"insert_before_stop","styleCopyRows":1},'
'{"id":"ocean_freight","title":"海运费用","tmsSource":"oceanCostItems",'
'"anchor":"S!A31","stopAtText":["目的港费用"],'
'"grow":"insert_before_stop","styleCopyRows":1},'
'{"id":"destination_charges","title":"目的港费用","tmsSource":"podCostItems",'
'"anchor":"S!A36","stopAtText":["备注"],'
'"grow":"insert_before_stop","styleCopyRows":1}'
']}'
)
return SimpleNamespace(ok=True, content=content, provider="stub", error="")
payload = {
"versionId": "v2",
"transportMode": "sea",
"fileName": "sea.xlsx",
"sheet": "S",
"cellInventory": [
"S!A18 | 始发港费用/Origin Charges | - | -",
"S!A30 | 海运费用/Ocean Freight | - | -",
"S!A35 | 目的港费用/Destination Charges | - | -",
"S!E6 | 提货地址/Place of Receipt | - | -",
"S!A11 | 货值/Cargo Value | - | -",
],
}
out = invoke_sheet_map(payload, chat_fn=fake_chat, allow_network=False, max_rounds=3)
self.assertTrue(out["ok"])
self.assertEqual(out.get("rounds"), 3)
self.assertEqual(len(calls), 3)
self.assertEqual(out["mapping"]["mappingGaps"], [])
self.assertEqual(len(out["mapping"]["feeRegions"]), 3)
if __name__ == "__main__":
unittest.main()