Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Compile docx documents into a DocumentModel, let the Agent deeply understand the content, then precisely edit via 89 EditOps — zero third-party dependencies.
Architecture
.docx file → ingest() → DocumentModel → view() to understand the full picture
↓
Agent generates EditOps
↓
apply_edits() → new .docx
Key insight: Zero loss of original XML attributes. EditOps only make localized, precise modifications.
Environment
Python 3.8+ (Pyodide compatible)
Zero third-party dependencies (zipfile + ElementTree only)
edits = [
EditOp.replace_text("p-003", "old text", "new text"),
EditOp.change_style("p-001", "Heading1"),
EditOp.edit_table_cell("tbl-000", 1, 1, "new content"),
EditOp.set_run_format("p-002", {"bold": True, "color": "FF0000"}, run_index=0),
]
ok = apply_edits(
'/mnt/{rootName}/template.docx', edits, '/mnt/{rootName}/output.docx',
model=model, wiki_dir='/mnt/{rootName}/template_wiki'
)
print(f"All successful: {ok}")
# ⚠️ IMPORTANT: Clean up ingest wiki artifacts
shutil.rmtree('/mnt/{rootName}/template_wiki', ignore_errors=True)
Creating New docx from Scratch
Use the bundled blank.docx template (with full styles) and build with EditOps:
import sys, shutil
sys.path.insert(0, '/mnt_skills/builtin/cw-word-editor/scripts')
from model import EditOp, EditAction, DocumentModel, ParagraphNode
from writeback import apply_edits
import base64
# 1. Copy blank template
output = '/mnt/{rootName}/output.docx'
shutil.copy2('/mnt_skills/builtin/cw-word-editor/blank.docx', output)
# 2. Build model manually (blank template has only 1 empty paragraph p-000)
model = DocumentModel()
model.paragraphs = [ParagraphNode(id="p-000", index=0, text="")]
# 3. Generate EditOps (omit position to append before sectPr in order)
edits = [
EditOp.insert_paragraph("Document Title", style="Title"),
EditOp.insert_paragraph("Body paragraph one"),
EditOp.add_table(rows=3, cols=2,
header_row=["Header1", "Header2"],
data=[["r1c1", "r1c2"], ["r2c1", "r2c2"]]
), # ✅ header_row + data filled directly, no need for edit_table_cell# Image: use from_dict to pass image_data (base64 encoded)
EditOp.from_dict({"action": "add_image",
"params": {"width": 500, "height": 300,
"image_path": "chart.png",
"image_data": base64_chart_string}}),
EditOp.add_footnote("", "Footnote text", target_text="Body paragraph one"),
EditOp.add_comment("", "Comment text", target_text="Body paragraph one", author="Reviewer"),
EditOp.add_bookmark("", "chapter1", target_text="Document Title"),
EditOp.add_hyperlink("", url="https://example.com", text="Link text", target_text="Body paragraph one"),
EditOp.set_header(text="Header text"),
EditOp.set_footer(text="Footer text"),
EditOp.add_page_number(alignment="center"),
EditOp.set_core_properties(title="Document Title", creator="Author"),
]
ok = apply_edits(output, edits, output, model=model)
# ⚠️ IMPORTANT: Clean up ingest artifactsimport shutil
shutil.rmtree('/mnt/{rootName}/_wiki', ignore_errors=True)
Key Notes
position is optional: insert_paragraph, add_table, add_image, add_toc all accept optional position; defaults to appending before sectPr (equivalent to end of document).
Blank template paragraph auto-removed: apply_edits automatically detects and removes the blank.docx template paragraph (footnote/comment references are migrated to the last content paragraph).
image_data requires from_dict: EditOp.add_image() factory method does not accept image_data; to embed image data use EditOp.from_dict({"action": "add_image", "params": {"image_data": base64_string, ...}}).
Style name auto-mapping: The engine builds a style name → styleId map automatically. WPS templates use numeric styleIds (e.g. "718" for Title, "700" for Heading 1), but you code with standard names like "Title"/"Heading1" and the engine resolves them.
EditOps execute in order: When position is omitted, all content is inserted before sectPr in EditOps list order, and the blank template paragraph is cleaned up at the end.
Single-pass creation: All EditOps complete in a single apply_edits call, including image embedding, footnote creation, style patching, and other post-processing steps.
CJK (Chinese/Japanese/Korean) Font for Matplotlib Charts
Pyodide has no CJK fonts. When chart labels/titles contain CJK characters, fetch and load the bundled font on demand:
import matplotlib
matplotlib.use('Agg')
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
import tempfile, os
asyncdefload_cjk_font():
"""Fetch Noto Sans SC (SIL OFL) from app assets and load into matplotlib."""
font_name = 'NotoSansSC-Regular.otf'
tmp_path = os.path.join(tempfile.gettempdir(), font_name)
ifnot os.path.exists(tmp_path):
from pyodide.http import pyfetch
resp = await pyfetch(f'/assets/fonts/{font_name}')
data = await resp.bytes()
withopen(tmp_path, 'wb') as f:
f.write(data)
fm.fontManager.addfont(tmp_path)
plt.rcParams['font.sans-serif'] = ['Noto Sans SC'] + plt.rcParams['font.sans-serif']
plt.rcParams['axes.unicode_minus'] = False# Call once before any plt.plot / plt.bar / etc.await load_cjk_font()
# Now CJK text works in charts
fig, ax = plt.subplots()
ax.set_title('中文标题') # renders correctly
ax.bar(['苹果', '香蕉', '橙子'], [3, 5, 2])
Key points:
Font file is at /assets/fonts/NotoSansSC-Regular.otf (7.9MB, SIL OFL, bundled with the app)
Only fetch when needed — no upfront download or OPFS storage
addfont() requires a file path (not BytesIO), so write to a temp file first
Subsequent calls in the same session skip the fetch (temp file already exists)
Emoji NOT supported: matplotlib uses FreeType which cannot render color emoji (COLRv1/CBDT). All emoji glyphs appear as empty boxes. Use plain text labels instead of emoji in charts.
Full Example: Generate a Report with Chart from Scratch
In addition to target_id (e.g. "p-003"), all EditOp factory methods that accept target_id also support target_text to locate the target paragraph by its text content.
# Locate by target_id (traditional, requires knowing IDs after ingest)
EditOp.replace_text("p-034", "old text", "new text")
# Locate by target_text (no ID needed, use a text snippet from the paragraph)
EditOp.replace_text("", "old text", "new text", target_text="paragraph containing this text")
EditOp.change_style("", "Heading1", target_text="some heading text")
EditOp.add_footnote("", "Footnote content", target_text="paragraph to add footnote to")
EditOp.add_endnote("", "Endnote content", target_text="paragraph to add endnote to")
EditOp.add_comment("", "Comment content", target_text="paragraph to comment on", author="Reviewer")
EditOp.add_bookmark("", "bm1", target_text="paragraph to bookmark")
EditOp.add_hyperlink("", "https://example.com", text="Link", target_text="paragraph to add link to")
EditOp.add_break("", break_type="page", target_text="page break after this heading")
EditOp.add_field("", "DATE", target_text="some text")
EditOp.set_run_format("", {"bold": True}, target_text="paragraph to bold")
EditOp.set_paragraph_format("", {"spacing_before": "400"}, target_text="paragraph to adjust spacing")
EditOp.set_paragraph_shading("", fill="FFFF00", target_text="paragraph to shade")
EditOp.set_paragraph_border("", borders={"top": {"val": "single", "sz": "4", "space": "1", "color": "000000"}}, target_text="paragraph to border")
EditOp.set_tab_stops("", tabs=[{"val": "right", "pos": "9360", "leader": "dot"}], target_text="paragraph to add tab stops")
EditOp.set_run_text_effects("", outline=True, target_text="paragraph to outline")
EditOp.fill_blanks("", ["value1"], target_text="paragraph to fill blanks")
EditOp.set_list_style("", list_type="bullet", target_text="paragraph to make list")
EditOp.set_list_level("", num_id="1", ilvl=0, target_text="paragraph to adjust level")
EditOp.set_run_language("", val="en-US", east_asia="zh-CN", target_text="paragraph to set language")
EditOp.set_run_border("", val="single", color="FF0000", target_text="paragraph to add char border")
EditOp.set_paragraph_outline_level("", level=0, target_text="paragraph to set outline level")
Resolution priority (_resolve_target_para):
target_id + id_to_elem (Python object reference, unaffected by insertions/deletions)
target_id + id_to_index (positional index)
target_text (full-text substring search, must match exactly 1 paragraph)
⚠️ Note: w must be the shorter edge (portrait) or longer edge (landscape), consistent with orientation. WPS templates may write inconsistent values; writeback auto-corrects them.
After ingest(), each table in model.tables is a TableNode with:
tbl.rows / tbl.cols — dimensions (int, NOT lists)
tbl.cells — flat list of TableCell objects, each with .row, .col, .text
tbl.get_row(row_index) — get cells for one row, sorted by column
tbl.iter_rows() — iterate all rows as lists of cells
tbl.get_cell(row, col) — get a specific cell
# ❌ WRONG — tbl.rows is an int (row count), NOT a listfor row in tbl.rows: # TypeError: 'int' object is not iterable
...
# ✅ CORRECT — use iter_rows()for row in tbl.iter_rows():
texts = [c.text for c in row]
print(" | ".join(texts))
# ✅ CORRECT — use get_row(n)
header = tbl.get_row(0)
for cell in header:
print(f"col {cell.col}: {cell.text}")
# ✅ CORRECT — use get_cell(r, c)
cell = tbl.get_cell(1, 2)
if cell:
print(cell.text)
# ✅ CORRECT — manual iteration via range + cellsfor r inrange(tbl.rows):
row_cells = sorted([c for c in tbl.cells if c.row == r], key=lambda c: c.col)
texts = [c.text for c in row_cells]
print(f"Row {r}: {' | '.join(texts)}")
Split document into chunks, each containing complete paragraph text (no truncation), suitable for subagents to read one chunk at a time.
Strategies:
Strategy
Description
Best For
"fixed"
Fixed paragraph count per chunk (default 50)
General, uniform splitting
"heading"
Split by Heading1 titles
Reading/editing by chapter
"range"
By paragraph ID range
Precisely reading a specific area
"section"
By Word section
Multi-section documents
Return value:list[dict], each dict:
{
"index": 0, # Chunk number"start_id": "p-000", # Start paragraph ID"end_id": "p-049", # End paragraph ID"paragraphs": 50, # Paragraph count"chars": 2048, # Total characters"content": "..."# Full text (with hierarchy markers)
}
Usage examples:
from view import chunk
# Fixed 50 paragraphs per chunk
chunks = chunk(model, strategy="fixed", size=50)
# Split by chapters
chunks = chunk(model, strategy="heading")
# Read a specific range
chunks = chunk(model, strategy="range", start_id="p-010", end_id="p-050")
# Print chunk summary (without full content)for c in chunks:
print(f"Chunk {c['index']}: {c['start_id']}→{c['end_id']} | {c['paragraphs']} paragraphs | {c['chars']} chars")
Large Document Reading Best Practices
When a document exceeds ~200 paragraphs, outputting the full text may exceed the LLM context window. Use the main agent dispatching multiple subagents for parallel reading pattern.
⚠️ Important limitation: subagents cannot spawn subagents (platform limitation, subagentRuntime is undefined in child level).
Therefore subagent dispatching must be done by the main agent itself, cannot be delegated to a subagent.
Workflow
1. Main agent: scan(model) → understand document structure and outline
2. Main agent: chunk(model, strategy="heading") → get chunk list
3. Main agent: write each chunk['content'] to temp file (/mnt_assets/_chunks/chunk_N.txt)
4. Main agent: parallel spawn_subagent × N (each subagent reads one file)
→ subagent uses read tool to read vfs://assets/_chunks/chunk_N.txt
→ subagent returns summary of that segment
5. Main agent: collect all subagent summaries, synthesize full understanding
6. Main agent: delete temp chunk files
Why Write Files?
Subagents are dispatched via spawn_subagent tool, with prompt content passed directly. For large chunks (>10K chars):
Putting directly in the prompt wastes tokens
Writing to a file and letting subagents read on-demand is more efficient
Failed chunks can be retried individually without re-transmitting content
Full Example (Python chunking + spawn_subagent parallel dispatch)
import sys, os
sys.path.insert(0, '/mnt_skills/builtin/cw-word-editor/scripts')
from ingest import ingest
from view import scan, chunk
# ── Step 1: ingest + view outline ──
model = ingest('/mnt/{rootName}/large_doc.docx', '/mnt/{rootName}/_wiki')
print(scan(model, max_lines=30))
# ── Step 2: Chunk ──
chunks = chunk(model, strategy="heading")
print(f"Document split into {len(chunks)} chapters")
# ── Step 3: Write chunk files ──
os.makedirs('/mnt_assets/_chunks', exist_ok=True)
for c in chunks:
path = f'/mnt_assets/_chunks/chunk_{c["index"]}.txt'withopen(path, 'w') as f:
f.write(c['content'])
print(f" Wrote chunk_{c['index']}.txt ({c['chars']:,} chars)")
Then in the same conversation turn (main agent uses tools directly), dispatch subagents in parallel:
// Prompt template for each subagent:
spawn_subagent(
name: "reader-{index}",
description: "Read document chapter {index}",
mode: "plan",
prompt: """
Read the following file and summarize the key points. Output only the summary.
File path: vfs://assets/_chunks/chunk_{index}.txt
Summary requirements:
1. List each policy/item name
2. For each, summarize the core content in one sentence
"""
)
💡 Tip: You can launch multiple subagents in a single spawn_subagent call block; the platform executes them in parallel.
If a subagent fails, use resume_subagent or re-launch with spawn_subagent to retry individually.