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 單軌設定