From 1c81866541f379b4b929ee5c1d36a40f68bf96c2 Mon Sep 17 00:00:00 2001 From: OoO Date: Sat, 2 May 2026 17:39:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(ppt):=20final=20critic=20cleanup=20?= =?UTF-8?q?=E2=80=94=20Medium-1=20OOXML=20order=20+=20Info-1/2=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 清掉剩餘 critic finding(Medium-1 + Info-1 + Info-2): Medium-1: _set_run_fonts 違反 OOXML CT_TextCharacterProperties 子元素順序 - 抽出 _insert_rpr_child(rPr, tag) helper:依 ECMA-376 §21.1.2.3 規定 的順序表(_RPR_CHILD_ORDER)找正確位置插入 - 找第一個排在 target 之後的子元素 → insert 前面;否則 append - 當前 from-scratch 場景安全;未來改讀模板 .pptx 時也不會踩雷 - 驗證:產生 monthly v3.1,399 個 rPr 全部符合 schema 順序,0 違反 Info-1: cleanup_expired_ppt_cache docstring 補語意說明 - 明確說明 dry_run=True/False 時 deleted_files / deleted_rows / freed_bytes 欄位語意分別為「將刪除」預估值 vs「已實刪」實際值 Info-2: TEMPLATE_VERSIONS 標記退役 type - growth / vendor / bcg 三個 type 從未實際落地(依 ADR-014 校正 2026-04-28) - 加 DEPRECATED 註解,避免後人誤以為已支援 Info-3 SKIP(評估後不做): - get_template_version import 改放模組頂部會 trigger ppt_generator 模組級 REPORTS_DIR.mkdir() 副作用,read-only filesystem 環境會掛 - 保留延遲 import 是 graceful degradation 的 intentional 設計 至此 critic 38967ce 審查清單全綠: - 0 critical, 2 HIGH (3b0b4b3), 4 medium (52c06f6 + 本 commit), 3 info Co-Authored-By: Claude Opus 4.7 (1M context) --- routes/openclaw_bot_routes.py | 6 ++-- services/ppt_generator.py | 58 ++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index 3e97e9f..9a90b90 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -2246,8 +2246,10 @@ def cleanup_expired_ppt_cache(days_old: int = 7, dry_run: bool = True) -> dict: launchd / cron 排程務必顯式傳: cleanup_expired_ppt_cache(days_old=7, dry_run=False) - 回傳:{'deleted_files': N, 'deleted_rows': N, 'freed_bytes': N, 'errors': [...]} - dry_run=True 時 deleted_* 為「將刪除」預估值。 + 回傳統計欄位語意(critic Info-1): + dry_run=True 時:deleted_files / deleted_rows / freed_bytes 為「將刪除」預估值 + dry_run=False 時:上述為「已實刪」實際值 + errors 為過程中發生例外的列表(id + 錯誤訊息) """ from database.manager import DatabaseManager from database.ppt_reports import PPTReport diff --git a/services/ppt_generator.py b/services/ppt_generator.py index 9c2ca1a..9b11d4a 100644 --- a/services/ppt_generator.py +++ b/services/ppt_generator.py @@ -47,9 +47,12 @@ TEMPLATE_VERSIONS = { 'strategy': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁 'competitor': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁 'promo': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁 - 'growth': 'v2.0', - 'vendor': 'v2.0', - 'bcg': 'v2.0', + # ── DEPRECATED:以下 type 從未實際落地(依 ADR-014 校正 2026-04-28)。 + # 函式 generate_growth_ppt / generate_vendor_ppt / generate_bcg_ppt 仍存在於本檔, + # 但路由層未綁定指令;保留版本字串避免如未來重啟時快取 schema 對不上。 + 'growth': 'v2.0', # DEPRECATED — 從未落地 + 'vendor': 'v2.0', # DEPRECATED — 從未落地 + 'bcg': 'v2.0', # DEPRECATED — 從未落地 } @@ -156,26 +159,59 @@ def _add_rect(slide, l, t, w, h, fill_hex, line_hex=None): return s +# OOXML CT_TextCharacterProperties 子元素順序(依 ECMA-376 第一部分 §21.1.2.3) +# critic Medium-1:SubElement 永遠 append 會違反 schema;必須按此順序 insert +# 否則 LibreOffice / Keynote / 嚴格驗證器會拒絕讀取或丟棄 latin/ea 元素。 +_RPR_CHILD_ORDER = [ + 'ln', 'noFill', 'solidFill', 'gradFill', 'blipFill', 'pattFill', + 'effectLst', 'effectDag', 'highlight', 'uLnTx', 'uLn', + 'uFillTx', 'uFill', 'latin', 'ea', 'cs', 'sym', + 'hlinkClick', 'hlinkMouseOver', 'rtl', 'extLst', +] +_OOXML_DRAWING_NS = 'http://schemas.openxmlformats.org/drawingml/2006/main' + + +def _insert_rpr_child(rPr, tag: str): + """在 rPr 下依 ECMA-376 schema 順序插入 元素。 + 若 rPr 內已有此 tag,先全部移除避免重複。 + 回傳新建的元素。 + """ + from lxml import etree + nsmap = {'a': _OOXML_DRAWING_NS} + # 先清掉同名舊元素 + for el in rPr.findall(f'a:{tag}', nsmap): + rPr.remove(el) + + target_idx = _RPR_CHILD_ORDER.index(tag) if tag in _RPR_CHILD_ORDER else len(_RPR_CHILD_ORDER) + # 找第一個排在 target_idx 之後的子元素 → insert 在它前面 + for i, child in enumerate(rPr): + local = etree.QName(child).localname + if local in _RPR_CHILD_ORDER and _RPR_CHILD_ORDER.index(local) > target_idx: + new_el = etree.Element(f'{{{_OOXML_DRAWING_NS}}}{tag}') + rPr.insert(i, new_el) + return new_el + # 沒有後序元素 → append + new_el = etree.SubElement(rPr, f'{{{_OOXML_DRAWING_NS}}}{tag}') + return new_el + + def _set_run_fonts(run, latin_font: str = None, ea_font: str = None): """為單一 run 同時設定 Latin 字型與 EastAsian (CJK) 字型。 解法:直接寫入 a:rPr 下的 a:latin / a:ea 元素,避開 python-pptx 只能設一個 font.name 的限制 — 這是「中英分軌」呈現的關鍵。 + + critic Medium-1:使用 _insert_rpr_child 維持 OOXML schema 規定的子元素順序, + 讓未來若改成讀模板 .pptx(rPr 內已有 hlinkClick/cs 等後序元素)時也能正確插入。 """ if not latin_font and not ea_font: return try: - from lxml import etree rPr = run._r.get_or_add_rPr() - nsmap = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'} if latin_font: - for el in rPr.findall('a:latin', nsmap): - rPr.remove(el) - latin_el = etree.SubElement(rPr, '{%s}latin' % nsmap['a']) + latin_el = _insert_rpr_child(rPr, 'latin') latin_el.set('typeface', latin_font) if ea_font: - for el in rPr.findall('a:ea', nsmap): - rPr.remove(el) - ea_el = etree.SubElement(rPr, '{%s}ea' % nsmap['a']) + ea_el = _insert_rpr_child(rPr, 'ea') ea_el.set('typeface', ea_font) except Exception: # 失敗時退回 font.name 單軌設定