""" 口述空运抽字段:解析 DeepSeek B JSON,不打外网。 """ from __future__ import annotations import os import sys import unittest from types import SimpleNamespace ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if ROOT not in sys.path: sys.path.insert(0, ROOT) os.environ.setdefault("LEDGER_BACKEND", "memory") from agent.llm.extract_text import ( detect_transport_mode, extract_inquiry_snapshot, parse_labeled_facts, ) from agent.schema.field_validate import normalize_facts from agent.llm.mode_extract_fields import invoke_shell, parse_extract_payload from agent.policy.air_text_flow import AirTextInquiryFlow from agent.ledger.memory_ledger import MemoryLedger ORAL = "上海到洛杉矶空运,3件 180kg,体积2.5,普货纸箱" B_JSON = { "transport_mode": "AIR", "fields": { "起运港": "上海", "目的港": "洛杉矶", "品名": "普货", "件数": "3", "毛重": "180", "体积": "2.5", "包装方式": "纸箱", "报价日期": "", }, } class OralExtractTests(unittest.TestCase): def test_parse_b_json(self) -> None: parsed = parse_extract_payload(B_JSON) self.assertTrue(parsed["ok"]) self.assertEqual(parsed["business_line"], "AIR") self.assertEqual(parsed["facts"]["起运港"], "上海") self.assertNotIn("报价日期", parsed["facts"]) def test_invoke_shell_with_json(self) -> None: out = invoke_shell(payload={"assistant_json": B_JSON}) self.assertTrue(out["ok"]) self.assertEqual(out["facts"]["目的港"], "洛杉矶") def test_invoke_shell_without_json_keeps_legacy(self) -> None: out = invoke_shell(payload={"a": 1}) self.assertEqual(out.get("error"), "extract_fields_shell_not_implemented") def test_city_pair_without_mode_does_not_default_air(self) -> None: """没写运输方式时,模型猜的空运不能当成已选。港口和日期仍留下。""" text = "查一下10月9日郑州-吉隆坡的报价" guessed = parse_extract_payload( { "transport_mode": "AIR", "fields": { "起运港": "郑州", "目的港": "吉隆坡", "报价日期": "2026-10-09", }, } ) snap = extract_inquiry_snapshot(text, b_result=guessed) self.assertEqual(snap["business_line"], "") self.assertEqual(snap["facts"].get("起运港"), "郑州") self.assertEqual(snap["facts"].get("目的港"), "吉隆坡") self.assertEqual(snap["facts"].get("报价日期"), "2026-10-09") flow = AirTextInquiryFlow(MemoryLedger()) replies: list[str] = [] phase = flow.on_text( sender_id="u-mode", text=text, reply=lambda body, extra=None: replies.append(body), b_result=guessed, allow_b=False, ) self.assertEqual(phase, "need_mode") self.assertTrue(any("告知我询价的运输方式" in x for x in replies)) self.assertFalse(any(x.startswith("运输方式:空运") or "运输方式:空运" in x for x in replies)) def test_oral_snapshot_uses_b_not_regex(self) -> None: snap = extract_inquiry_snapshot(ORAL, b_result=parse_extract_payload(B_JSON)) self.assertEqual(snap["business_line"], "AIR") self.assertEqual(snap["facts"]["件数"], "3件") self.assertEqual(snap["source"], "deepseek_b") def test_oral_without_b_does_not_guess_ports(self) -> None: snap = extract_inquiry_snapshot(ORAL) self.assertEqual(snap["business_line"], "AIR") self.assertFalse(snap["facts"].get("起运港")) def test_bare_dap_is_trade_term(self) -> None: """句尾单独的 DAP 记成贸易条款,不依赖模型有没有这个字段。""" snap = extract_inquiry_snapshot("查一下空运,出货日期9月30日广州到香港的普货价格,DAP") self.assertEqual(snap["business_line"], "AIR") self.assertEqual(snap["facts"].get("贸易条款"), "DAP") def test_trade_term_example_line_is_not_filled(self) -> None: snap = extract_inquiry_snapshot("贸易条款(非必填):\n(如:DDP、DAP、CIF、CIP)") self.assertFalse(snap["facts"].get("贸易条款")) labeled = extract_inquiry_snapshot("贸易条款:CIF\n(如:DDP、DAP、CIF、CIP)") self.assertEqual(labeled["facts"].get("贸易条款"), "CIF") def test_labeled_customs_port(self) -> None: self.assertEqual(parse_labeled_facts("通关口岸:皇岗").get("通关口岸"), "皇岗") snap = extract_inquiry_snapshot("通关口岸(非必填):皇岗") self.assertEqual(snap["facts"].get("通关口岸"), "皇岗") snap2 = extract_inquiry_snapshot("通关口岸皇岗") self.assertEqual(snap2["facts"].get("通关口岸"), "皇岗") def test_b_keeps_land_customs_port(self) -> None: parsed = parse_extract_payload( { "transport_mode": "LAND", "fields": {"起运港": "广州", "通关口岸": "皇岗"}, } ) self.assertEqual(parsed["facts"].get("通关口岸"), "皇岗") def test_assemble_air_query_zhuhai_clark(self) -> None: from agent.schema.tms_air_query import assemble_air_query out = assemble_air_query( { "起运港": "珠海", "目的港": "克拉克", "品名": "茶叶", "件数": "10件", "毛重": "100KGS", "体积": "2CBM", "包装方式": "托盘", "报价日期": "2026-09-12", } ) self.assertTrue(out["ok"]) self.assertEqual(out["facts"]["originCode"], "ZUH") self.assertEqual(out["facts"]["destinationCode"], "CRK") self.assertEqual(out["facts"]["packageType"], "托盘") self.assertEqual(out["facts"]["件数"], "10件") bulk = assemble_air_query( { "起运港": "珠海", "目的港": "克拉克", "品名": "茶叶", "件数": "10", "毛重": "100", "体积": "2", "包装方式": "散货", "报价日期": "2026-09-12", } ) self.assertTrue(bulk["ok"]) self.assertEqual(bulk["facts"]["包装方式"], "散货") self.assertNotIn("包装方式", bulk["missing"]) def test_place_stops_before_cargo_and_price(self) -> None: """目的地只留地名。后面的品名、包装、的价格不能粘在地名上。""" from agent.schema.field_validate import validate_required_fields check = validate_required_fields( facts={ "目的港": "吉隆坡,普货,散货的价格", "品名": "普货", "包装方式": "散货", "报价日期": "2026-09-29", }, business_line="AIR", ) self.assertEqual(check["facts"]["目的港"], "吉隆坡") self.assertEqual(check["facts"]["品名"], "普货") self.assertEqual(check["facts"]["包装方式"], "散货") bare = validate_required_fields( facts={"目的港": "洛杉矶,茶叶,纸箱的价格"}, business_line="AIR", ) self.assertEqual(bare["facts"]["目的港"], "洛杉矶") self.assertEqual(bare["facts"]["品名"], "茶叶") self.assertEqual(bare["facts"]["包装方式"], "纸箱") def test_fly_route_overwrites_stale_nkg_kul(self) -> None: from agent.schema.field_validate import harvest_oral_measures from agent.schema.tms_air_query import assemble_air_query from agent.policy import inquiry_copy as copy first = harvest_oral_measures( "从南京飞吉隆坡,9月14日茶叶,托盘,4.2cbm,1.5KG", {}, ) self.assertEqual(first["起运港"], "南京") self.assertEqual(first["目的港"], "吉隆坡") assembled = assemble_air_query( {**first, "originCode": "NKG", "destinationCode": "KUL", "件数": "10"} ) card = copy.inquiry_card(work_order_no="WO1", facts=assembled["facts"]) self.assertIn("起运地:南京", card) self.assertIn("目的地:吉隆坡", card) self.assertNotIn("起运地:NKG", card) self.assertNotIn("目的地:KUL", card) second = harvest_oral_measures( "北京飞吉隆坡,9月14日茶叶,4.2cbm,1.5KG", {**assembled["facts"]}, ) again = assemble_air_query(second) self.assertEqual(again["facts"]["起运港"], "北京") self.assertEqual(again["facts"]["originCode"], "PEK") self.assertEqual(again["facts"]["destinationCode"], "KUL") shown = copy.inquiry_card(work_order_no="WO2", facts=again["facts"]) self.assertIn("起运地:北京", shown) self.assertNotIn("起运地:NKG", shown) self.assertNotIn("起运地:南京", shown) def test_iata_pair_kept_not_rewritten_to_city(self) -> None: from agent.schema.field_validate import harvest_oral_measures from agent.schema.tms_air_query import assemble_air_query from agent.policy import inquiry_copy as copy oral = "空运 SZX-CGK 发光二级管组件 11P 2400KG 20cbm" facts = harvest_oral_measures( oral, {"起运港": "深圳", "目的港": "雅加达", "品名": "发光二级管组件"}, ) self.assertEqual(facts["起运港"], "SZX") self.assertEqual(facts["目的港"], "CGK") assembled = assemble_air_query( { **facts, "件数": "11P", "毛重": "2400KG", "体积": "20cbm", "包装方式": "托盘", "报价日期": "2026-09-18", } ) self.assertEqual(assembled["facts"]["originCode"], "SZX") self.assertEqual(assembled["facts"]["destinationCode"], "CGK") card = copy.inquiry_card(work_order_no="WO1", facts=assembled["facts"]) self.assertIn("起运地:SZX", card) self.assertIn("目的地:CGK", card) self.assertNotIn("深圳", card) self.assertNotIn("雅加达", card) def test_currency_pair_is_not_air_route(self) -> None: from agent.schema.field_validate import harvest_oral_measures facts = harvest_oral_measures( "空运 货值 USD-CNY 茶叶", {"起运港": "上海", "目的港": "洛杉矶"}, ) self.assertEqual(facts["起运港"], "上海") self.assertEqual(facts["目的港"], "洛杉矶") def test_oral_pack_and_quote_date_from_current_sentence(self) -> None: from agent.schema.field_validate import harvest_oral_measures first = harvest_oral_measures( "南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG", {}, ) self.assertEqual(first["包装方式"], "托盘") self.assertEqual(first["报价日期"], "2026-09-15") self.assertEqual(first["毛重"], "1.5KG") self.assertEqual(first["体积"], "4.2cbm") second = harvest_oral_measures( "京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG", first, ) self.assertEqual(second["包装方式"], "散货") self.assertEqual(second["报价日期"], "2026-09-16") self.assertEqual(second["毛重"], "2.5KG") self.assertEqual(second["体积"], "5.2cbm") def test_clarify_replaces_pallet_with_bulk_and_keeps_quote_date(self) -> None: replies: list[str] = [] def reply(text: str, extra=None) -> None: replies.append(text) _ = extra flow = AirTextInquiryFlow(MemoryLedger()) flow.on_text( sender_id="u_date", text="南京飞吉隆坡,9月15日茶叶,托盘,4.2cbm,1.5KG", reply=reply, injected_facts={ "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", "包装方式": "托盘", }, injected_mode="AIR", ) self.assertEqual(flow.session_of("u_date").facts["包装方式"], "托盘") self.assertEqual(flow.session_of("u_date").facts["报价日期"], "2026-09-15") flow.on_text( sender_id="u_date", text="京飞吉隆坡,9月16日茶叶,散货,5.2cbm,2.5KG", reply=reply, injected_facts={ "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", }, injected_mode="AIR", ) sess = flow.session_of("u_date") self.assertEqual(sess.facts["包装方式"], "散货") self.assertEqual(sess.facts["报价日期"], "2026-09-16") last = replies[-1] self.assertIn("包装类型:散货", last) self.assertIn("报价日期:2026-09-16", last) def test_weight_keeps_user_kg_not_kgs(self) -> None: from agent.policy import inquiry_copy as copy from agent.schema.field_validate import harvest_oral_measures facts = harvest_oral_measures("从南京飞吉隆坡,茶叶,1.5KG,4.2cbm,托盘", {}) self.assertEqual(facts["毛重"], "1.5KG") shown = copy.inquiry_card( work_order_no="WO1", facts={**facts, "起运港": "南京", "目的港": "吉隆坡", "品名": "茶叶", "件数": "10"}, ) self.assertIn("重量(KG):1.5KG", shown) self.assertNotIn("1.5KGS", shown) def test_dimension_text_is_volume_without_multiplying(self) -> None: from agent.schema.field_validate import harvest_oral_measures facts = harvest_oral_measures("茶叶,1.1*2.1*2.2,11kg,21件", {}) self.assertEqual(facts["体积"], "1.1*2.1*2.2") self.assertEqual(facts["毛重"], "11kg") self.assertEqual(facts["件数"], "21件") def test_oral_units_fill_weight_volume_without_b(self) -> None: snap = extract_inquiry_snapshot( "珠海到克拉克空运,茶叶 10件 100KGS 2CBM 托盘" ) self.assertEqual(snap["facts"]["件数"], "10件") self.assertEqual(snap["facts"]["毛重"], "100KGS") self.assertEqual(snap["facts"]["体积"], "2CBM") self.assertFalse(snap["facts"].get("起运港")) def test_labeled_weight_alias(self) -> None: from agent.llm.extract_text import parse_labeled_facts from agent.schema.field_validate import normalize_facts facts = parse_labeled_facts("重量(KG):180\n体积(CBM):2.5") norm = normalize_facts(facts) self.assertEqual(norm["毛重"], "180") self.assertEqual(norm["体积"], "2.5") def test_extract_prompt_keeps_iata(self) -> None: from agent.llm.mode_extract_fields import build_extract_messages blob = str(build_extract_messages("空运 SZX-CGK")) self.assertIn("SZX-CGK", blob) self.assertIn("禁止翻译成城市名", blob) def test_chat_fn_fixture_drives_flow(self) -> None: def chat_fn(**kwargs): return SimpleNamespace( ok=True, content="", error="", tool_calls=[ { "function": { "name": "extract_inquiry_fields", "arguments": __import__("json").dumps(B_JSON, ensure_ascii=False), } } ], ) replies: list[str] = [] def reply(text: str, extra=None) -> None: replies.append(text) _ = extra flow = AirTextInquiryFlow(MemoryLedger()) phase = flow.on_text( sender_id="oral1", text=ORAL, reply=reply, chat_fn=chat_fn, ) if phase == "wait_confirm": phase = flow.on_text(sender_id="oral1", text="确定", reply=reply) self.assertEqual(phase, "wait_collab") joined = "\n".join(replies) self.assertIn("工单号:", joined) self.assertIn("起运地:上海", joined) self.assertIn("目的地:洛杉矶", joined) self.assertNotIn("【询价卡片】", joined) def test_mode_index_maps_land_sea_air(self) -> None: self.assertEqual(detect_transport_mode("1"), "LAND") self.assertEqual(detect_transport_mode("2"), "SEA") self.assertEqual(detect_transport_mode("3"), "AIR") self.assertEqual(detect_transport_mode("1.陆运"), "LAND") self.assertEqual(detect_transport_mode("空运"), "AIR") self.assertEqual(detect_transport_mode("10件"), "") self.assertEqual(detect_transport_mode("1.件数(必填):10"), "") class ClarifyPasteTests(unittest.TestCase): def test_paste_numbered_required_lines_and_skip_examples(self) -> None: """复制补问清单:只收冒号后的值,示例行和空项都不算填了。""" text = """ 1.件数(必填):10 (如:10) 2.重量(KG)(必填):10 (如:100) 3.体积(CBM)(必填):10 (如:2) 4.包装类型(必填): (如:托盘、散货) """ facts = normalize_facts(parse_labeled_facts(text)) self.assertEqual(facts.get("件数"), "10") self.assertEqual(facts.get("毛重"), "10") self.assertEqual(facts.get("体积"), "10") self.assertNotIn("包装方式", facts) self.assertNotEqual(facts.get("包装方式"), "托盘、散货") def test_blank_cargo_ready_does_not_take_next_item_number(self) -> None: """货好时间空着时,下一行「4.提货地址」的编号不能写进货好时间。示例行也不算填了。""" text = """ 1.包装类型(必填):托盘 (如:托盘、散货) 2.货源地(非必填): (如:东莞) 3.货好时间(非必填): (如:随时可提) 4.提货地址(非必填): (如:东莞仓库) 5.收货地址(非必填): (如:马尼拉) 6.贸易条款(非必填): (如:DDP、DAP、CIF、CIP) """ facts = extract_inquiry_snapshot(text, allow_b=False)["facts"] self.assertEqual(facts.get("包装方式") or facts.get("包装类型"), "托盘") self.assertNotIn("货好时间", facts) self.assertNotIn("提货地址", facts) self.assertNotIn("货源地", facts) self.assertNotIn("贸易条款", facts) class ImageOptionalClipTests(unittest.TestCase): def test_numbered_extra_lines_do_not_enter_fields(self) -> None: oral = """ 运输方式:空运 目的地:印尼巴淡 件数:1PLT 重量:55.8KGS 体积:1.23CBM 包装类型:托盘 报价日期:2026-09-23 起运地:深圳坪山保税仓 货好时间:今天货好 4. 希望送到目的地工厂时间: 尽快 5. 提货地址:深圳市坪山区龙田街道大工业区绿荫北路7号601、 602、603 (保税仓库) 6. 送货地址: PT SAT NUSAPERSADA TBK----JL. PELITA VI NO.99 """ snap = extract_inquiry_snapshot(oral, allow_b=False) facts = snap["facts"] self.assertEqual(facts.get("货好时间"), "今天货好") self.assertIn("坪山区", facts.get("提货地址") or "") self.assertNotIn("希望送到", facts.get("货好时间") or "") self.assertNotIn("希望送到", facts.get("提货地址") or "") self.assertNotIn("送货地址", facts.get("提货地址") or "") self.assertIn("PT SAT", facts.get("收货地址") or "") from agent.policy import inquiry_copy as copy card = copy.ask_clarify( facts=facts, missing_keys=["品名"], transport_mode="空运", ) self.assertNotIn("希望送到", card) self.assertNotIn("4. 希望送到", card) self.assertIn("收货地址", card) # 测服手工海运报价单:表头和格子在同一行,还带 POL/POD/Terms。 SEA_SHEET_ORAL = """ 起运港/POL(有指定起运港口的必填) 纽约港 *品名/Commodity: 机器设备 目的港/POD(有指定目的港口的必填) 巴生西港 *货物数量/Quantity (或提供装箱单/Packing List) 18 整柜或拼柜:FCL 箱型箱量:40HQ*1 贸易条款/Terms: CIF 要求运输时效/Requested Transit Time 运输分类:其他 报价日期:2026-09-07 """ class SeaSheetHeaderClipTests(unittest.TestCase): def test_quote_sheet_headers_do_not_stick_to_values(self) -> None: """报价单表头不能进补问:起运港只要纽约港,贸易条款只要 CIF。""" snap = extract_inquiry_snapshot(SEA_SHEET_ORAL, allow_b=False) facts = snap["facts"] self.assertEqual(facts.get("起运港"), "纽约港") self.assertEqual(facts.get("目的港"), "巴生西港") self.assertEqual(facts.get("品名"), "机器设备") self.assertEqual(str(facts.get("货量") or facts.get("货物数量") or ""), "18") self.assertEqual(facts.get("整柜或拼柜"), "FCL") self.assertEqual(facts.get("箱型箱量"), "40HQ*1") self.assertEqual(facts.get("贸易条款"), "CIF") self.assertNotIn("必填", facts.get("起运港") or "") self.assertNotIn("Terms", facts.get("贸易条款") or "") self.assertNotIn("Commodity", facts.get("品名") or "") self.assertNotIn("时效", facts.get("贸易条款") or "") from agent.policy import inquiry_copy as copy card = "\n".join( copy.inquiry_field_lines( work_order_no="", facts=facts, transport_mode="海运", ) ) self.assertIn("纽约港", card) self.assertNotIn("口的必填", card) self.assertNotIn("Commodity", card) self.assertNotIn("Requested Transit", card) def test_next_column_header_is_not_the_value(self) -> None: """ 手工海运单同一行是「字段、格子、下一列表头」。 目的港只要巴生西港,不能收成费用汇总/POD Local Charges。 """ oral = """ 起运港/POL(有指定起运港口的必填) 纽约港 目的港/POD(有指定目的港口的必填) 巴生西港 费用汇总/POD Local Charges *品名/Commodity: 机器设备 *货物数量/Quantity (或提供装箱单/Packing List) 18 提货地址/Place of Receipt 整柜或拼柜:FCL 箱型箱量:40HQ*1 送货地址/Place of Delivery 贸易条款/Terms: CIF 要求货物抵达时间/Requested Arrival Time 运输分类:其他 报价日期:2026-09-07 """ facts = extract_inquiry_snapshot(oral, allow_b=False)["facts"] self.assertEqual(facts.get("起运港"), "纽约港") self.assertEqual(facts.get("目的港"), "巴生西港") self.assertEqual(facts.get("品名"), "机器设备") self.assertEqual(str(facts.get("货量") or facts.get("货物数量") or ""), "18") self.assertEqual(facts.get("整柜或拼柜"), "FCL") self.assertEqual(facts.get("箱型箱量"), "40HQ*1") self.assertEqual(facts.get("贸易条款"), "CIF") self.assertNotIn("费用汇总", facts.get("目的港") or "") self.assertNotIn("提货地址", facts.get("货量") or facts.get("货物数量") or "") self.assertNotIn("送货地址", facts.get("整柜或拼柜") or "") self.assertNotIn("抵达", facts.get("箱型箱量") or "") def test_quote_date_synonyms_for_sea_land_air(self) -> None: """询价/走货/发货/出货/提货的日期或时间,海运陆运空运都记成报价日期。""" from agent.schema.field_validate import QUOTE_DATE_SYNONYMS, normalize_facts modes = (("海运", "SEA"), ("陆运", "LAND"), ("空运", "AIR")) for label in QUOTE_DATE_SYNONYMS: self.assertEqual(normalize_facts({label: "2026-09-23"})["报价日期"], "2026-09-23") for word, mode in modes: snap = extract_inquiry_snapshot(f"{word}\n{label}:2026-09-23", allow_b=False) self.assertEqual(snap["business_line"], mode, label) self.assertEqual(snap["facts"].get("报价日期"), "2026-09-23", label) self.assertNotIn(label, snap["facts"]) def test_quote_date_synonym_does_not_steal_ready_or_pickup(self) -> None: """货好时间、提货地址仍是原字段;同义词日期不被前面的日期抢走。""" snap = extract_inquiry_snapshot( "海运\n货好时间:9月20日\n提货地址:上海仓库\n询价日期:2026-09-15", allow_b=False, ) facts = snap["facts"] self.assertEqual(facts.get("报价日期"), "2026-09-15") self.assertEqual(facts.get("货好时间"), "9月20日") self.assertEqual(facts.get("提货地址"), "上海仓库") oral = extract_inquiry_snapshot("空运 发货日期9月15日", allow_b=False) self.assertEqual(oral["facts"].get("报价日期"), "2026-09-15") # 测服 2026-10-05:空运报价单整表被抽进补问,费用和条款进了起运地、货好时间。 AIR_QUOTE_SHEET = """ 运输方式:空运 目的地:CRK 品名:化妆品 件数:11 重量:1KG 体 积: 100 CBM 报价日期:46307 货好时间: 体 积: 100 CBM 时效要求: 计 费 重: 10 KG 派送地址: 二、报价明细 项目 费用名称 单位 币别 单价 数量 金额 备注 空运段 空运费 起运港 操作费 文件费 如需多个分单另计 *代理报关费 发生时收取 *提货费 发生时收取 其他 *其他杂费 发生时收取 预估费用 0 三、报价条件 1、报价适用范围:一般货物(非危险品) 贸易条款:DAP 托运方:某某有限公司 承运方:深圳市友通达国际货运代理有限公司 """ class AirQuoteSheetInquiryOnlyTests(unittest.TestCase): def test_quote_sheet_keeps_inquiry_fields_only(self) -> None: """费用明细、报价条件、落款不能进补问。起运地不能收成操作费。""" snap = extract_inquiry_snapshot(AIR_QUOTE_SHEET, allow_b=False) facts = snap["facts"] self.assertEqual(snap["business_line"], "AIR") self.assertEqual(facts.get("目的港"), "CRK") self.assertEqual(facts.get("品名"), "化妆品") self.assertIn("11", str(facts.get("件数") or "")) self.assertIn("1", str(facts.get("毛重") or "")) self.assertNotIn("10", str(facts.get("毛重") or "")) self.assertIn("100", str(facts.get("体积") or "")) self.assertEqual(facts.get("报价日期"), "2026-10-12") self.assertNotIn("起运港", facts) self.assertNotIn("货好时间", facts) self.assertNotIn("贸易条款", facts) blob = "\n".join(facts.values()) self.assertNotIn("操作费", blob) self.assertNotIn("报价明细", blob) self.assertNotIn("报价条件", blob) self.assertNotIn("空舱", blob) self.assertNotIn("友通达", blob) if __name__ == "__main__": unittest.main()