feat: add Chinese office productivity plugins
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: create-edit-documents
|
||||
description: 创建、读取、编辑、审阅和验证 DOCX/Word 文档。用户要求制作报告、方案、备忘录、合同草稿、修改已有 Word 文件、提取文档结构、检查排版或保留模板格式时使用。
|
||||
---
|
||||
|
||||
# CJYSA 文档
|
||||
|
||||
## 工作原则
|
||||
|
||||
- 先判断任务是只读、创建还是编辑;只读任务不要改写文件。
|
||||
- 创建新文档时优先使用 Codex 工作区依赖加载器返回的 Python 和 `python-docx`。
|
||||
- 编辑已有文档时保留原文件,输出新副本;除非用户明确要求重排,只做局部修改。
|
||||
- 任何交付都先做结构检查;具备 DOCX 渲染能力时,再逐页检查预览。
|
||||
- 不复制或依赖 OpenAI 官方 Documents 插件中的脚本、模板或素材。
|
||||
|
||||
## 标准流程
|
||||
|
||||
1. 调用工作区依赖加载器,使用其返回的 Python 路径,不猜测运行时位置。
|
||||
2. 对输入文档运行:
|
||||
|
||||
```powershell
|
||||
& "<bundled-python>" "<skill-dir>\scripts\docx_tool.py" inspect "<input.docx>"
|
||||
```
|
||||
|
||||
3. 创建文档时,先确定页面尺寸、边距、标题层级、字体、段落间距和表格样式,再编写任务专用 builder。简单文本可直接使用:
|
||||
|
||||
```powershell
|
||||
& "<bundled-python>" "<skill-dir>\scripts\docx_tool.py" create `
|
||||
--input "<content.txt>" --output "<output.docx>" --title "标题"
|
||||
```
|
||||
|
||||
4. 简单文字替换可使用 `replace`;涉及跨段、批注、修订、目录、页眉页脚或复杂表格时,编写任务专用 OOXML/python-docx 脚本并保留备份。
|
||||
5. 再次运行 `inspect`,确认文档可打开、段落和表格数量合理、无意外空白内容。
|
||||
6. 若环境提供 Word、WPS、LibreOffice 或其他可靠渲染方式,导出 PDF/PNG 并逐页查看;不可渲染时明确说明只完成了结构验证。
|
||||
|
||||
## 编辑约束
|
||||
|
||||
- 不覆盖用户原文件,除非用户明确授权。
|
||||
- 尽量保留段落样式、分节、页眉页脚、图片关系和表格宽度。
|
||||
- 不把所有内容重新写成一个段落来完成局部修改。
|
||||
- 涉及敏感信息时,不在日志中输出全文;只输出统计或脱敏摘要。
|
||||
- 交付前按 [quality-checks.md](references/quality-checks.md) 检查。
|
||||
|
||||
## 输出
|
||||
|
||||
最终只链接用户要求的 DOCX,不主动附带内部检查日志或预览文件。
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "CJYSA 文档"
|
||||
short_description: "创建、编辑、审阅并验证专业 Word 文档文件"
|
||||
default_prompt: "Use $create-edit-documents to create or edit a polished DOCX file."
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,9 @@
|
||||
# DOCX 交付检查
|
||||
|
||||
- 文件可正常打开,页数和内容量符合预期。
|
||||
- 标题层级连续,正文样式统一,无意外空段落。
|
||||
- 表格没有明显缺列、空表或极端列宽。
|
||||
- 页眉、页脚、页码和分节未被局部编辑破坏。
|
||||
- 图片关系有效,替代文本在需要无障碍交付时存在。
|
||||
- 若完成渲染:逐页确认无裁切、重叠、乱码和异常空白页。
|
||||
- 若未完成渲染:在交付说明中明确只通过结构检查。
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small, original DOCX helper for structural inspection and simple edits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.shared import Cm, Pt
|
||||
|
||||
|
||||
def ensure_distinct(source: Path, output: Path) -> None:
|
||||
if source.resolve() == output.resolve():
|
||||
raise SystemExit("Refusing to overwrite the input file; choose a different output path.")
|
||||
|
||||
|
||||
def inspect_docx(path: Path) -> None:
|
||||
doc = Document(path)
|
||||
headings = []
|
||||
nonempty = 0
|
||||
for index, paragraph in enumerate(doc.paragraphs, start=1):
|
||||
text = paragraph.text.strip()
|
||||
if text:
|
||||
nonempty += 1
|
||||
if text and paragraph.style and paragraph.style.name.lower().startswith("heading"):
|
||||
headings.append({"paragraph": index, "style": paragraph.style.name, "text": text[:160]})
|
||||
|
||||
tables = []
|
||||
for index, table in enumerate(doc.tables, start=1):
|
||||
tables.append(
|
||||
{
|
||||
"table": index,
|
||||
"rows": len(table.rows),
|
||||
"columns": max((len(row.cells) for row in table.rows), default=0),
|
||||
}
|
||||
)
|
||||
|
||||
relationships = doc.part.rels.values()
|
||||
images = sum(1 for rel in relationships if "image" in rel.reltype)
|
||||
report = {
|
||||
"file": str(path.resolve()),
|
||||
"paragraphs": len(doc.paragraphs),
|
||||
"nonempty_paragraphs": nonempty,
|
||||
"tables": tables,
|
||||
"sections": len(doc.sections),
|
||||
"images": images,
|
||||
"headings": headings,
|
||||
}
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def add_content(doc: Document, text: str) -> None:
|
||||
for raw in text.splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line:
|
||||
doc.add_paragraph()
|
||||
elif line.startswith("### "):
|
||||
doc.add_heading(line[4:].strip(), level=3)
|
||||
elif line.startswith("## "):
|
||||
doc.add_heading(line[3:].strip(), level=2)
|
||||
elif line.startswith("# "):
|
||||
doc.add_heading(line[2:].strip(), level=1)
|
||||
elif line.startswith(("- ", "* ")):
|
||||
doc.add_paragraph(line[2:].strip(), style="List Bullet")
|
||||
elif len(line) > 3 and line[:3].rstrip(".").isdigit() and line[1:3] in {". ", "、"}:
|
||||
doc.add_paragraph(line[3:].strip(), style="List Number")
|
||||
else:
|
||||
doc.add_paragraph(line)
|
||||
|
||||
|
||||
def create_docx(input_path: Path, output: Path, title: str | None) -> None:
|
||||
doc = Document()
|
||||
section = doc.sections[0]
|
||||
section.top_margin = Cm(2.3)
|
||||
section.bottom_margin = Cm(2.3)
|
||||
section.left_margin = Cm(2.5)
|
||||
section.right_margin = Cm(2.5)
|
||||
|
||||
normal = doc.styles["Normal"]
|
||||
normal.font.name = "Microsoft YaHei"
|
||||
normal.font.size = Pt(10.5)
|
||||
normal.paragraph_format.space_after = Pt(6)
|
||||
normal.paragraph_format.line_spacing = 1.35
|
||||
|
||||
if title:
|
||||
paragraph = doc.add_paragraph()
|
||||
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = paragraph.add_run(title)
|
||||
run.bold = True
|
||||
run.font.name = "Microsoft YaHei"
|
||||
run.font.size = Pt(22)
|
||||
paragraph.paragraph_format.space_after = Pt(18)
|
||||
|
||||
add_content(doc, input_path.read_text(encoding="utf-8"))
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(output)
|
||||
inspect_docx(output)
|
||||
|
||||
|
||||
def replace_text(source: Path, output: Path, old: str, new: str) -> None:
|
||||
ensure_distinct(source, output)
|
||||
doc = Document(source)
|
||||
replacements = 0
|
||||
for paragraph in doc.paragraphs:
|
||||
if old in paragraph.text:
|
||||
original = paragraph.text
|
||||
for run in paragraph.runs:
|
||||
run.text = ""
|
||||
paragraph.add_run(original.replace(old, new))
|
||||
replacements += original.count(old)
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for paragraph in cell.paragraphs:
|
||||
if old in paragraph.text:
|
||||
original = paragraph.text
|
||||
for run in paragraph.runs:
|
||||
run.text = ""
|
||||
paragraph.add_run(original.replace(old, new))
|
||||
replacements += original.count(old)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(output)
|
||||
print(json.dumps({"output": str(output.resolve()), "replacements": replacements}, ensure_ascii=False))
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
inspect_cmd = sub.add_parser("inspect")
|
||||
inspect_cmd.add_argument("input", type=Path)
|
||||
|
||||
create_cmd = sub.add_parser("create")
|
||||
create_cmd.add_argument("--input", required=True, type=Path)
|
||||
create_cmd.add_argument("--output", required=True, type=Path)
|
||||
create_cmd.add_argument("--title")
|
||||
|
||||
replace_cmd = sub.add_parser("replace")
|
||||
replace_cmd.add_argument("input", type=Path)
|
||||
replace_cmd.add_argument("--output", required=True, type=Path)
|
||||
replace_cmd.add_argument("--old", required=True)
|
||||
replace_cmd.add_argument("--new", required=True)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
if args.command == "inspect":
|
||||
inspect_docx(args.input)
|
||||
elif args.command == "create":
|
||||
create_docx(args.input, args.output, args.title)
|
||||
elif args.command == "replace":
|
||||
replace_text(args.input, args.output, args.old, args.new)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user