完整差異
@@ -8,8 +8,8 @@\n "image": null,\n "sourceImageCandidates": [],\n "zh": {\n- "title": "美航空公司反对中国国航增飞纽约华盛顿",\n- "summary": "美航空公司反对中国国航增飞纽约与华盛顿航班",\n+ "title": "美國航空業者反對中國國際航空增飛紐約、華盛頓",\n+ "summary": "美國航空業者反對中國國際航空增飛紐約與華盛頓航班",\n "body": [\n "美國主要航空公司正反對中國國際航空(Air China)申請增加北京往返紐約與華盛頓的航班,理由是中國航空公司可使用俄羅斯領空,形成美國競爭對手無法比擬的優勢。",\n "路透社報導指出,這些航班與中國國家主席習近平計劃訪問美國有關,屬暫時性安排,而非永久增加航班;習近平預定於2026年9月23日至25日訪問美國,與總統川普會談。",\n@@ -135,8 +135,8 @@\n "item_type": "new",\n "previous_event_id": null,\n "category": "aviation_industry",\n- "headline": "美航空公司反对中国国航增飞纽约华盛顿",\n- "summary": "美航空公司反对中国国航增飞纽约与华盛顿航班",\n+ "headline": "美國航空業者反對中國國際航空增飛紐約、華盛頓",\n+ "summary": "美國航空業者反對中國國際航空增飛紐約與華盛頓航班",\n "headline_en": "US airlines oppose Air China request for extra New York and Washington flights",\n "summary_en": "US airlines oppose Air China's bid for extra New York Washington flights",\n "event_time": null,\n@@ -37,6 +37,8 @@\n from difflib import SequenceMatcher\n from pathlib import Path\n \n+from opencc import OpenCC\n+\n from common import (\n ARTICLES_DIR,\n BASE_PATH,\n@@ -87,6 +89,37 @@\n EN_BRIEF_MIN_WORDS = 90\n EN_BRIEF_MAX_WORDS = 150\n \n+_S2T = OpenCC("s2t")\n+\n+\n+def simplified_chinese_error(draft: dict) -> str | None:\n+ """Reject reader-facing Chinese copy that still contains simplified glyphs."""\n+ fields: list[tuple[str, object]] = []\n+ zh = draft.get("zh")\n+ if isinstance(zh, dict):\n+ fields.extend([\n+ ("zh.title", zh.get("title")),\n+ ("zh.summary", zh.get("summary")),\n+ ])\n+ body = zh.get("body")\n+ if isinstance(body, list):\n+ fields.extend((f"zh.body[{index}]", paragraph)\n+ for index, paragraph in enumerate(body))\n+ flash = draft.get("flash")\n+ if isinstance(flash, dict):\n+ fields.append(("flash.zh", flash.get("zh")))\n+ incident = draft.get("incident")\n+ if isinstance(incident, dict):\n+ for key in ("phase", "location", "desc"):\n+ bilingual = incident.get(key)\n+ if isinstance(bilingual, dict):\n+ fields.append((f"incident.{key}.zh", bilingual.get("zh")))\n+ for field, value in fields:\n+ if isinstance(value, str) and _S2T.convert(value) != value:\n+ return f"{field} contains Simplified Chinese characters"\n+ return None\n+\n+\n _EDITORIAL_PROCESS_RE = re.compile(\n r"(?:本文|本報導|這篇(?:文章|報導)).{0,60}"\n r"(?:採用|使用|引用|補足|補充|核對|交叉確認|資料來源|來源資料)"\n@@ -1023,6 +1056,9 @@ def validate_draft(draft):\n if len(body) > MAX_BODY_PARAGRAPHS:\n return (f"{lang}.body has {len(body)} paragraphs; maximum is "\n f"{MAX_BODY_PARAGRAPHS}")\n+ language_error = simplified_chinese_error(draft)\n+ if language_error:\n+ return language_error\n process_error = editorial_process_error(draft)\n if process_error:\n return process_error\n@@ -6,3 +6,4 @@ lxml>=5.0,<6\n jinja2>=3.1,<4\n cryptography>=45,<47\n airportsdata==20260315\n+opencc-python-reimplemented>=0.1.7,<1\n@@ -533,6 +533,14 @@ def test_extract_json_and_validate_draft():\n \n check(write.validate_draft(DRAFT_SAFETY) is None, "safety draft validates")\n check(write.validate_draft(DRAFT_BIZ) is None, "biz draft validates")\n+ broken = json.loads(json.dumps(DRAFT_BIZ))\n+ broken["zh"]["title"] = "美航空公司反对中国国航增飞纽约华盛顿"\n+ check("Simplified Chinese" in (write.validate_draft(broken) or ""),\n+ "simplified Chinese title is rejected before publication")\n+ broken = json.loads(json.dumps(DRAFT_BIZ))\n+ broken["flash"]["zh"] = "中国国航增飞纽约航班"\n+ check("Simplified Chinese" in (write.validate_draft(broken) or ""),\n+ "simplified Chinese flash is rejected before publication")\n check(write.zh_body_character_count(DRAFT_BIZ["zh"]["body"])\n >= write.ZH_BODY_MIN_CHARS, "zh test fixture clears body floor")\n check(write.en_body_word_count(DRAFT_BIZ["en"]["body"])