feat: add Chinese office productivity plugins

This commit is contained in:
CJYSEX
2026-07-23 12:15:04 +08:00
parent 9a24a2125f
commit 1b674e31da
24 changed files with 1001 additions and 2 deletions

View File

@@ -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不主动附带内部检查日志或预览文件。

View File

@@ -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

View File

@@ -0,0 +1,9 @@
# DOCX 交付检查
- 文件可正常打开,页数和内容量符合预期。
- 标题层级连续,正文样式统一,无意外空段落。
- 表格没有明显缺列、空表或极端列宽。
- 页眉、页脚、页码和分节未被局部编辑破坏。
- 图片关系有效,替代文本在需要无障碍交付时存在。
- 若完成渲染:逐页确认无裁切、重叠、乱码和异常空白页。
- 若未完成渲染:在交付说明中明确只通过结构检查。

View File

@@ -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()