Problem
任何一個 mutation 操作(update_cell / replace_text …)都會讓 <w:tbl> 的 <w:tblPr> 被清空到只剩 <w:tblW>(與 <w:jc>,若原本有)。<w:tblStyle>、<w:tblBorders>、<w:tblLook> 全部靜默消失 —— 工具回報成功、檔案照常開得起來,只是表格框線與樣式不見了。
關鍵區分:純 save round-trip 無損。 只 open_document → save_document(零編輯)產出的 word/document.xml 與原檔 byte-identical。一旦文件被標記 dirty、改走「從解析後的模型重新序列化」,tblPr 就被截斷。這指向 parse 階段沒有把 tblPr 的子元素收進模型,而非 writer 的問題。
Reproduction
從零建立最小 fixture(python-docx),不依賴任何既有檔案:
from docx import Document
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
d = Document()
t = d.add_table(rows=2, cols=2)
t.style = 'Table Grid' # -> <w:tblStyle w:val="TableGrid"/>
t.cell(0,0).text = 'A'; t.cell(0,1).text = 'B'
t.cell(1,0).text = 'C'
tblPr = t._tbl.tblPr
b = OxmlElement('w:tblBorders') # explicit borders, as Word writes them
for edge, sz in (('top','12'),('left','12'),('bottom','12'),('right','12'),
('insideH','6'),('insideV','6')):
e = OxmlElement(f'w:{edge}')
e.set(qn('w:val'),'single'); e.set(qn('w:sz'),sz)
e.set(qn('w:space'),'0'); e.set(qn('w:color'),'auto')
b.append(e)
tblPr.append(b)
d.save('minimal_bordered.docx')
然後:
open_document(path='minimal_bordered.docx', doc_id='x', track_changes=false)
update_cell(doc_id='x', table_index=0, row=1, col=1, text='D')
save_document(doc_id='x', path='minimal_bordered_after.docx')
Expected
<w:tblPr> 內容不變 —— 一次 cell 文字更新不應觸碰表格層級屬性。
Actual
BEFORE(逐字取自 fixture 的 word/document.xml):
<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:type="auto" w:w="0"/><w:tblLook w:firstColumn="1" w:firstRow="1" w:lastColumn="0" w:lastRow="0" w:noHBand="0" w:noVBand="1" w:val="04A0"/><w:tblBorders><w:top w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="6" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="6" w:space="0" w:color="auto"/></w:tblBorders><w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr>
AFTER(同一份,只跑過一次 update_cell):
<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
tblStyle 1→0、tblBorders 1→0、tblLook 2→0。
Evidence matrix
同一份真實 Word 表單(2 欄 8 列表格,tblStyle + tblBorders + tblLook 齊全)跑四種變體,word/document.xml 字元數與元素計數:
| 變體 |
document.xml (chars) |
tblStyle |
tblBorders |
tblLook |
| 原始 |
49,865 |
1 |
1 |
1 |
| open → save,零編輯 |
49,865 |
1 |
1 |
1 |
只跑一次 update_cell |
43,297 |
0 |
0 |
0 |
只跑一次 replace_text |
42,367 |
0 |
0 |
0 |
| 兩者都跑 |
38,816 |
0 |
0 |
0 |
tcPr / trPr / tcW / vAlign / gridCol 計數在所有變體都不變 —— 只有 table 層級屬性遺失,cell/row 層級完好。這進一步把嫌疑收斂到 tblPr 的 parse/model 覆蓋範圍。
Round-trip 同時也會把 <w:szCs> 大量補寫進每個帶 <w:sz> 的 run(33 → 176)。那是 normalization、不是遺失,但顯示 rPr 也在被重寫,值得一併確認覆蓋範圍。
Impact
Suggested direction
- 修:把
tblPr 的完整子元素集合納入 model(或對未建模的子元素採 pass-through 保留原 XML node)。tblPr 的合法子元素還包含 tblpPr / tblOverlap / bidiVisual / tblStyleRowBandSize / tblStyleColBandSize / tblCellMar / tblCaption / tblDescription / shd 等,建議一次檢查完整清單而非只補這三個。
- 測:加一條 regression test —— 帶
tblStyle + tblBorders + tblLook 的 fixture,跑任一 mutation 後 assert tblPr 子元素集合不變。理想上對 tcPr / pPr / rPr / sectPr 做同樣的「mutation 不得改變未觸碰的屬性」不變式測試。
- 防:pass-through 保留未建模節點,比逐一補齊 model 更能防住同類問題(
tblPr 只是第一個被發現的,其他容器很可能有相同缺口)。
Workaround
在修好之前,對含框線表格的既有 .docx 填表,改走 OPC zip 手術(直接改 word/document.xml 後原樣重打包,其餘 part 逐一 byte-for-byte 複製)。實測:14 個 zip part 中 13 個 byte-identical,只有 document.xml 變動,tblPr 完整保留。
Environment
- plugin
che-word-mcp 3.22.0(macdoc marketplace;mcp/che-word-mcp submodule 位於 v3.20.0-26-gb59d5f2)
- macOS 26(Darwin 27.0.0)
- fixture 由 python-docx 1.2.0 產生
- 觸發工具:
update_cell、replace_text(推測所有標記 dirty 的 mutation 皆會觸發,未逐一驗證)
Type
bug
Problem
任何一個 mutation 操作(
update_cell/replace_text…)都會讓<w:tbl>的<w:tblPr>被清空到只剩<w:tblW>(與<w:jc>,若原本有)。<w:tblStyle>、<w:tblBorders>、<w:tblLook>全部靜默消失 —— 工具回報成功、檔案照常開得起來,只是表格框線與樣式不見了。關鍵區分:純 save round-trip 無損。 只
open_document→save_document(零編輯)產出的word/document.xml與原檔 byte-identical。一旦文件被標記 dirty、改走「從解析後的模型重新序列化」,tblPr就被截斷。這指向 parse 階段沒有把tblPr的子元素收進模型,而非 writer 的問題。Reproduction
從零建立最小 fixture(python-docx),不依賴任何既有檔案:
然後:
Expected
<w:tblPr>內容不變 —— 一次 cell 文字更新不應觸碰表格層級屬性。Actual
BEFORE(逐字取自 fixture 的
word/document.xml):AFTER(同一份,只跑過一次
update_cell):tblStyle 1→0、tblBorders 1→0、tblLook 2→0。Evidence matrix
同一份真實 Word 表單(2 欄 8 列表格,
tblStyle+tblBorders+tblLook齊全)跑四種變體,word/document.xml字元數與元素計數:tblStyletblBorderstblLookupdate_cellreplace_texttcPr/trPr/tcW/vAlign/gridCol計數在所有變體都不變 —— 只有 table 層級屬性遺失,cell/row 層級完好。這進一步把嫌疑收斂到tblPr的 parse/model 覆蓋範圍。Round-trip 同時也會把
<w:szCs>大量補寫進每個帶<w:sz>的 run(33 → 176)。那是 normalization、不是遺失,但顯示 rPr 也在被重寫,值得一併確認覆蓋範圍。Impact
Updated cell at table[0][1][1]。使用者只能靠肉眼在 Word 裡發現框線消失。.docx)是 Word MCP 最常見的用途,而這類表單幾乎必然帶tblBorders。任何一次 cell 填寫就毀掉整份表格外觀。Suggested direction
tblPr的完整子元素集合納入 model(或對未建模的子元素採 pass-through 保留原 XML node)。tblPr的合法子元素還包含tblpPr/tblOverlap/bidiVisual/tblStyleRowBandSize/tblStyleColBandSize/tblCellMar/tblCaption/tblDescription/shd等,建議一次檢查完整清單而非只補這三個。tblStyle+tblBorders+tblLook的 fixture,跑任一 mutation 後 asserttblPr子元素集合不變。理想上對tcPr/pPr/rPr/sectPr做同樣的「mutation 不得改變未觸碰的屬性」不變式測試。tblPr只是第一個被發現的,其他容器很可能有相同缺口)。Workaround
在修好之前,對含框線表格的既有
.docx填表,改走 OPC zip 手術(直接改word/document.xml後原樣重打包,其餘 part 逐一 byte-for-byte 複製)。實測:14 個 zip part 中 13 個 byte-identical,只有document.xml變動,tblPr完整保留。Environment
che-word-mcp3.22.0(macdoc marketplace;mcp/che-word-mcpsubmodule 位於v3.20.0-26-gb59d5f2)update_cell、replace_text(推測所有標記 dirty 的 mutation 皆會觸發,未逐一驗證)Type
bug