修復含空格/括號檔名的圖片連結失效,自檢改驗連結可 render
來源檔名含空格或不平衡括號時,convert 產出的 Markdown 圖片連結會失效—— 圖檔明明存在於磁碟,整行卻退化成純文字(CommonMark 對未包住的空格與不平衡 括號視為分隔符)。用真 CommonMark parser 驗過:中文與成對括號正常,空格與 落單括號會壞。 - convert.py / from_docx.py / from_pdf.py / from_pptx.py:四處圖片連結目標 一律以 <> 包住(),同時涵蓋空格與不平衡括號。 - selfcheck.py:新增 assert_links_ok()——以 markdown_it 真 render,斷言圖片 連結數 == <img> 數且目標檔存在,取代原本只檢查 "![" 字串在不在的表面斷言。 測資補內嵌圖片(docx/pdf/pptx 各一路徑)、檔名改含空格與不平衡括號。 - requirements.txt:明確宣告 markdown-it-py(原為 fastmcp 傳遞依賴,自檢直接 使用,宣告以免上游調整依賴樹後自檢失效)。 - AGENTS.md §13.4:新增「斷言要驗結果可用,不是驗字串存在」規則。 驗證:四個自檢全通過;移除任一處 <> 修法,assert_links_ok 即失敗。 ingest / lint / search 在含括號檔名下全鏈路已另行驗證正常(source_ref 走 YAML 純量,不受影響)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,8 @@ def _vision_pdf(root, pdf_path, assets, conv_name):
|
||||
blocks = []
|
||||
for i, (img, text) in enumerate(zip(images, texts), 1):
|
||||
rel = img.relative_to(md_dir).as_posix()
|
||||
blocks.append(f"<!-- page {i} (vision: {model}) -->\n\n\n\n{text}")
|
||||
# <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效
|
||||
blocks.append(f"<!-- page {i} (vision: {model}) -->\n\n\n\n{text}")
|
||||
return "\n\n".join(blocks), "ok", "from_vision"
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ def _par_md(par, doc, assets_dir, md_dir, counter):
|
||||
rel = (assets_dir / name).relative_to(md_dir).as_posix()
|
||||
else:
|
||||
rel = name # dry-run:僅佔位
|
||||
parts.append(f"")
|
||||
# 連結目標以 <> 包住:來源檔名可能含空格或不平衡括號,兩者都會讓
|
||||
# Markdown 連結失效(圖片存在卻整行退化成純文字)
|
||||
parts.append(f"")
|
||||
parts.append(run.text)
|
||||
text = "".join(parts).strip()
|
||||
lvl = _heading_level(par)
|
||||
|
||||
@@ -42,7 +42,8 @@ def convert(src, assets_dir=None, md_dir=None):
|
||||
assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
pix.save(str(assets_dir / name))
|
||||
rel = (assets_dir / name).relative_to(md_dir).as_posix()
|
||||
blocks.append(f"")
|
||||
# <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效
|
||||
blocks.append(f"")
|
||||
doc.close()
|
||||
return "\n\n".join(blocks), "ok"
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ def convert(src, assets_dir=None, md_dir=None):
|
||||
rel = (assets_dir / name).relative_to(md_dir).as_posix()
|
||||
else:
|
||||
rel = name
|
||||
blocks.append(f"")
|
||||
# <> 包住:檔名含空格或不平衡括號時,未包住的連結會失效
|
||||
blocks.append(f"")
|
||||
if slide.has_notes_slide:
|
||||
notes = slide.notes_slide.notes_text_frame.text.strip()
|
||||
if notes:
|
||||
|
||||
@@ -26,13 +26,39 @@ import to_image
|
||||
|
||||
# 測資檔名一律用「中文 + 空格」——真實來源檔就長這樣,而 git status --porcelain
|
||||
# 對這兩者都會加引號轉義。ASCII 檔名的測資曾讓 ingest 的 preflight bug 溜過(§12)。
|
||||
DOCX, XLSX, PDF, SCAN = ("壓測 報告.docx", "測試案例 清單.xlsx",
|
||||
"規格 說明.pdf", "掃描件 無文字層.pdf")
|
||||
DOCX, XLSX, PDF, SCAN = ("壓測 報告(1).docx", "測試案例 清單.xlsx",
|
||||
"規格 說明.pdf", "掃描件(2 期.pdf")
|
||||
PPTX, HTML, TXT, DOC = ("結案 簡報.pptx", "指引 快照.html",
|
||||
"不支援 筆記.txt", "舊版 報告.doc")
|
||||
|
||||
|
||||
def assert_links_ok(md, base):
|
||||
"""每個圖片連結都要真的 render 成 <img>、且目標檔存在。
|
||||
|
||||
只斷言 "![" 在不在會讓損毀連結矇混過關——含空格/不平衡括號的檔名會產出
|
||||
語法有效但解析失敗的連結,圖檔明明在磁碟上卻整行退化成純文字(§13.4)。
|
||||
"""
|
||||
import re
|
||||
import urllib.parse
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
want = md.count("![")
|
||||
html = MarkdownIt("commonmark").render(md)
|
||||
got = html.count("<img")
|
||||
assert got == want, f"{want} 個圖片連結只有 {got} 個 render 成功:\n{md[:300]}"
|
||||
for m in re.finditer(r'<img src="([^"]+)"', html):
|
||||
p = base / urllib.parse.unquote(m.group(1))
|
||||
assert p.is_file(), f"連結目標不存在:{p}"
|
||||
return want
|
||||
|
||||
|
||||
def make_fixtures(d):
|
||||
import fitz
|
||||
png = d / "img.png" # 內嵌圖片用:docx/pdf/pptx 三條抽圖路徑都要走到
|
||||
pix = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, 8, 8))
|
||||
pix.clear_with(128)
|
||||
pix.save(str(png))
|
||||
|
||||
import docx as docxlib
|
||||
doc = docxlib.Document()
|
||||
doc.add_heading("支付閘道測試報告", level=1)
|
||||
@@ -42,6 +68,7 @@ def make_fixtures(d):
|
||||
t.rows[0].cells[1].text = "結果"
|
||||
t.rows[1].cells[0].text = "TC-001"
|
||||
t.rows[1].cells[1].text = "FAIL"
|
||||
doc.add_picture(str(png))
|
||||
doc.save(d / DOCX)
|
||||
|
||||
import openpyxl
|
||||
@@ -52,10 +79,10 @@ def make_fixtures(d):
|
||||
ws.append(["TC-001", "逾時 | 重試"])
|
||||
wb.save(d / XLSX)
|
||||
|
||||
import fitz
|
||||
pdf = fitz.open()
|
||||
page = pdf.new_page()
|
||||
page.insert_text((72, 72), "Payment gateway timeout defect reproduced under load test.")
|
||||
page.insert_image(fitz.Rect(72, 100, 122, 150), filename=str(png))
|
||||
pdf.save(d / PDF)
|
||||
pdf.close()
|
||||
scanned = fitz.open()
|
||||
@@ -64,9 +91,11 @@ def make_fixtures(d):
|
||||
scanned.close()
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
||||
slide.shapes.title.text = "結案報告"
|
||||
slide.shapes.add_picture(str(png), Inches(1), Inches(3))
|
||||
slide.notes_slide.notes_text_frame.text = "備註:法遵項目全數通過"
|
||||
prs.save(d / PPTX)
|
||||
|
||||
@@ -103,12 +132,12 @@ def main():
|
||||
(root / sub).mkdir(parents=True)
|
||||
(root / "raw/manifest.json").write_text('{"version": 1, "files": {}}', encoding="utf-8")
|
||||
|
||||
convert.main([str(fx / DOCX), str(fx / SCAN),
|
||||
str(fx / HTML), "--root", str(root)])
|
||||
convert.main([str(fx / DOCX), str(fx / XLSX), str(fx / PDF), str(fx / SCAN),
|
||||
str(fx / PPTX), str(fx / HTML), "--root", str(root)])
|
||||
m = json.loads((root / "raw/manifest.json").read_text(encoding="utf-8"))
|
||||
origs = {k: v for k, v in m["files"].items() if v["kind"] == "original"}
|
||||
convs = {k: v for k, v in m["files"].items() if v["kind"] == "converted"}
|
||||
assert len(origs) == 3 and len(convs) == 2, m
|
||||
assert len(origs) == 6 and len(convs) == 5, m # SCAN 為 needs_ocr,無 converted
|
||||
assert origs[f"raw/originals/{SCAN}"]["status"] == "needs_ocr"
|
||||
for k, v in convs.items():
|
||||
assert (root / k).is_file(), k
|
||||
@@ -116,6 +145,13 @@ def main():
|
||||
assert (root / f"raw/originals/{DOCX}").is_file()
|
||||
print("PASS: convert.py 端到端 + manifest 對應")
|
||||
|
||||
# 圖片連結必須真的能 render——DOCX/PDF/PPTX 三條抽圖路徑都要驗到,
|
||||
# 且測資檔名刻意含空格與不平衡括號(最容易讓連結失效的形狀)
|
||||
linked = sum(assert_links_ok((root / k).read_text(encoding="utf-8"),
|
||||
root / "raw/converted") for k in convs)
|
||||
assert linked >= 3, f"應至少驗到 docx/pdf/pptx 各一個圖片連結,實際 {linked}"
|
||||
print(f"PASS: 圖片連結可 render 且目標存在({linked} 個,檔名含空格/括號)")
|
||||
|
||||
# 冪等:同檔再跑一次 → skip,manifest 不變
|
||||
before = (root / "raw/manifest.json").read_text(encoding="utf-8")
|
||||
convert.main([str(fx / DOCX), "--root", str(root)])
|
||||
@@ -228,6 +264,7 @@ def main():
|
||||
assert len(conv_rel) == 1 and mv["files"][conv_rel[0]]["converter"] == "from_vision", mv
|
||||
md_v = (root_v / conv_rel[0]).read_text(encoding="utf-8")
|
||||
assert "可疑交易監控" in md_v and "(vision" in md_v and "![page 1]" in md_v, md_v
|
||||
assert_links_ok(md_v, root_v / "raw/converted") # vision 頁的連結同樣要能 render
|
||||
print("PASS: convert.py --vision 端到端(needs_ocr → 本地 vision 轉錄)")
|
||||
|
||||
print("ALL PASS")
|
||||
|
||||
Reference in New Issue
Block a user