feat: 集成豆包docx技能

This commit is contained in:
hp0912 2026-09-09 07:28:58 +08:00
parent ae5ba75b80
commit 041144960b
29 changed files with 3560 additions and 361 deletions

View File

@ -1,375 +1,76 @@
--- ---
name: docx name: docx
description: "创建、读取、编辑、转换、批注、接受修订、校验和渲染本地或远程 HTTPS Microsoft Word 文档,并下载 Word 任务所需且不超过 25 MiB 的图片、音视频、压缩包和其他 HTTPS 附件,按需识别文档图片、截图和扫描页中的文字。用户提到 Word、文档、报告、备忘录、合同、信函、模板、目录、页眉页脚、页码、表格、图片、图片文字 OCR、批注或修订,或提供 HTTPS Word 地址、.docx、.dotx、.doc 文件时使用;支持源文档安全下载、结构化创建、跨 Run 查找替换、本地 RapidOCR、安全 OOXML 解包/打包、旧格式转换、关系与 XML 校验及逐页视觉检查。若主要交付物是 PDF、电子表格、Google Docs 或普通代码,则不要使用。" description: "创建、读取、编辑、转换、批注、修订和校验 Word 文档,并处理简历优化、专利撰写/审查/答复/布局、签证材料填写和公文写作。提供 Word/PDF/文本资料提取、图片 OCR、模板回填及 Typst 简历 PDF 排版。用户提供 Word 文档或提出上述专业文书需求时使用;独立 PDF 操作、电子表格、Google Docs 或普通代码任务使用对应技能。"
--- ---
# Word 文档处理 # Word 与专业文书
## 强制执行规则 按需读取专业指南,文件操作统一使用下表中的固定脚本。普通 Word 任务直接进入文件流程;仅咨询内容时回答用户的问题,不额外生成文件。
当前智能体不能直接执行 shell、任意 Python 代码或系统命令。只能通过 `execute_skill_script` 调用本 Skill 中真实存在的固定 Python 脚本。 ## 调用与运行环境
- 只调用下表列出的可执行脚本,不执行 `scripts/` 目录、`scripts/_docx_common.py` 或 `scripts/_document_builder.py`。 - 用 `read_skill_resource` 读取资料,传 `skill_name="docx"` 和完整的根目录相对路径,例如 `references/resume-writing.md`。所有指南中的 `references/`、`scripts/`、`assets/` 都以本 skill 根目录为基准,不用 `../`,也不把业务指南当成独立 skill 激活。
- 不把 `python3`、`soffice`、`libreoffice`、`pandoc`、`pdftoppm`、`zip`、`unzip`、`find`、`rm` 或其他系统命令作为脚本参数。 - 当前机器人通过 `execute_skill_script` 执行固定脚本,传 `skill_name="docx"`、`script_path="scripts/create_document.py"` 和脚本参数。不要执行 shell、临时 Python/JavaScript 代码,或把 `python3`、`uv run`、系统命令作为脚本参数。
- LibreOffice、Pandoc、Poppler 和 ZIP 操作只允许由固定 Python 脚本在内部调用。 - 只执行下表的入口,不执行目录或以 `_` 开头的内部模块。LibreOffice、Pandoc、Poppler、Typst、ZIP 操作由入口脚本在内部调用。
- 每次检查脚本返回 JSON;只有 `ok` 为 `true` 时才继续。`validate_document.py` 还必须返回 `status: valid`。 - 每次检查返回 JSON 的 `ok`;失败时按 `error` 调整参数或报告失败。校验还需 `status: valid`、`issue_count: 0`。
- 只在需要读取图片、截图或扫描页中的文字时调用 `ocr_document.py`。只使用 `pages[]` 中 `usable_for_summary: true` 的 `text`;低置信度结果不得作为可靠正文。 - 保留用户源文件。最终文档写入 `/usr/local/src/word/`,缓存、资料与中间文件写入 `/usr/local/src/word/tmp/<任务名>/`,文件参数使用绝对路径。
- 远程 Word 源文档只交给 `download_document.py`;任务所需的远程图片、视频、音频、压缩包或其他附件只交给 `download_attachment.py`。不要在回复、日志摘要或文件名中复述可能含敏感查询参数的完整 URL。 - 容器依赖在镜像构建时安装,任务中不安装包。需要核对依赖、特定字体或排查字体替换时运行 `scripts/inspect_environment.py`,参考 `references/fonts.md`。
- 不覆盖用户提供的源文件。Word 最终文件一律写入 `/usr/local/src/word/`,下载缓存、中间文件和渲染结果一律写入 `/usr/local/src/word/tmp/<任务名>/`。始终传绝对路径;固定脚本会自动创建目录并拒绝该根目录之外的输出。 - 不在回复或文件名中复述带敏感查询参数的完整下载 URL。
- 环境已预置依赖,不安装软件包,也不提示用户安装依赖。
## 脚本清单 ## 专业指南
| 脚本 | 用途 | 底层能力 | 只读取本次需要的指南,再按其中索引加载细节。
| 用户需求 | 指南 | 内容 |
| --- | --- | --- | | --- | --- | --- |
| `scripts/download_document.py` | 下载并校验远程 HTTPS Word 文档 | Python `urllib`、安全 OOXML 解析、`python-docx` | | 简历、CV、职位定制、模板填写、成就梳理 | `references/resume-writing.md` | 保留模板、内容优化、ATS 检查、STAR 辅导 |
| `scripts/download_attachment.py` | 下载图片、音视频、压缩包等通用 HTTPS 附件 | Python `urllib`、HEAD 大小探测、流式硬限制 | | 权利要求、说明书、技术交底、审查意见、专利布局、FTO | `references/patent-writing.md` | 撰写、审查、答复、策略;专利生成预设 |
| `scripts/inspect_document.py` | 分段读取正文、表格、样式、批注和修订 | `python-docx`、安全 OOXML 解析 | | 签证表格、申请说明信、行程单、邀请函、在职证明 | `references/visa-documents.md` | 字段核对、资料一致性、支持材料模板 |
| `scripts/ocr_document.py` | 按页识别图片、截图和扫描页中的文字 | RapidOCR、ONNX Runtime、LibreOffice、Poppler、`pdfplumber` | | 起诉状、答辩状、法律公告、会议通知、会议纪要 | `references/official-documents.md` | 文书要素与模板;按适用场景核对格式 |
| `scripts/create_document.py` | 按受控 JSON 创建专业 DOCX | `python-docx`、Pillow | | Word 详细参数、JSON 结构、高级 XML 操作 | `references/word-operations.md` | 所有 Word 操作的统一接口 |
| `scripts/edit_document.py` | 查找替换、追加/插入内容、调整样式和页面 | `python-docx` | | 用户要求 Typst 排版,或从零创建 PDF 简历 | `references/typst.md` | 固定编译入口、简历模板和 PDF 检查 |
| `scripts/add_comment.py` | 给精确文本范围添加批注 | `python-docx` |
| `scripts/accept_changes.py` | 接受文档中的全部修订 | 固定 LibreOffice 宏 |
| `scripts/convert_document.py` | `.doc/.dotx/.docx` 转 DOCX/PDF/Markdown/文本 | LibreOffice、Pandoc、`python-docx` |
| `scripts/unpack_document.py` | 安全解包 OOXML 供高级编辑 | Python `zipfile` |
| `scripts/pack_document.py` | 把 OOXML 目录安全打包为 DOCX | Python `zipfile`、`python-docx` |
| `scripts/validate_document.py` | 校验 ZIP、XML、关系、批注、修订和可渲染性 | `defusedxml`、`python-docx`、LibreOffice |
| `scripts/render_document.py` | 把 Word 文档渲染为逐页 PNG/PDF | LibreOffice、Poppler |
## 标准流程 ## 文件流程
1. 输入是 HTTPS 地址时,先调用 `download_document.py` 下载到本次任务临时目录;本地文件直接进入下一步。 1. **确定交付物**:用户明确要求的格式优先,其次沿用原模板,未指定时按需求选择。Word 交付真实 DOCX;Word 的 PDF 副本用转换脚本。PDF 源资料不保证能恢复成与原版完全一致的可编辑 Word,重建时说明版式差异。
2. 旧版 `.doc` 或模板 `.dotx` 先调用 `convert_document.py` 转为 `.docx`;保留原文件。 2. **读取源资料**:远程 Word 先用 `download_document.py`,其他任务附件用 `download_attachment.py`(上限 25 MiB)。DOC/DOTX 先转换 DOCX。编辑、总结前用 `inspect_document.py` 读取正文、表格、章节、页眉脚、批注和修订,并按游标续读。PDF/TXT/MD/HTML 资料用 `extract_source.py`,其参数见下节。
3. 编辑、总结或重组现有文档前调用 `inspect_document.py`,确认段落、表格、章节、页眉页脚、批注和修订状态。 3. **按需 OCR**:Word 图片或扫描页使用 `ocr_document.py`,只采用 `usable_for_summary: true` 的文字,不覆盖可靠原生文本。PDF 返回 `needs_ocr: true` 时,通过可用的 `pdf` skill 对指定页 OCR;不要直接读取二进制文件假定已有正文。
4. 需要读取截图、扫描页或图片中的文字时调用 `ocr_document.py`。省略 `--pages` 可自动选择含有效图片的页面;不要默认 OCR 没有图片的普通正文页,也不要用 OCR 覆盖可靠的原生文本。 4. **组织内容**:专业文书按对应指南处理;保留来源、真实事实、用户明确的格式和已有授权。缺失关键事实时询问,不编造姓名、指标、日期、依据或正式签发状态。
5. 新建文档使用 `create_document.py`;常规编辑使用 `edit_document.py`;添加批注使用 `add_comment.py`。 5. **生成或回填**:无模板用 `create_document.py`;有模板用 `edit_document.py`;批注用 `add_comment.py`。需要保留模板时避免重建整份文件。普通跨 Run 替换保留未修改片段样式;替换后检查分页与内容溢出。
6. 输入有修订时,先确认用户希望保留还是接受。普通编辑脚本默认拒绝含修订的文档,避免把修订静默损坏。 6. **处理修订**:依据用户已经明确的意图保留或接受原修订,未明确才询问。仅文本替换需要显示修订时,可使用 `edit_document.py --track-changes --author <作者>`;其范围与限制见操作指南,不把结构或样式修改伪装为修订。
7. 只有固定编辑脚本不能完成的 OOXML 高级需求,才使用 `unpack_document.py` → 编辑 XML 文件 → `pack_document.py`;不得直接运行 ZIP 或 shell 命令。 7. **校验与渲染**:创建/修改 DOCX 后运行 `validate_document.py --check-convert`,再用 `render_document.py` 渲染全部页面并逐页检查;按 `next_page` 继续到 `has_more: false`。检查修订、批注、页眉脚、表格、图片、目录、乱码和空白页。Typst 产物按对应指南检查 PDF。
8. 所有创建或修改结果必须调用 `validate_document.py --check-convert`,确保 `status: valid`、`issue_count: 0`。 8. **交付**:核对用户要求和承诺的内容已覆盖,适用的来源放入文档并在关键数据旁标注;模板不允许新增来源章节时另附来源说明。用户要求向当前微信会话交付文件时,激活可用的 `send-file` skill 发送最终文件;未授权发送时只报告已生成的文件。没有对应交付工具时如实说明,不调用不存在的通知或画布工具。
9. 再调用 `render_document.py` 渲染全部页面,逐页检查版式;有游标时继续到 `has_more: false`。
10. 结构、内容、修订/批注和视觉检查都通过后才交付。
## 下载远程文档 ## 可执行脚本
只接受 HTTPS 地址。完整保留 URL 及查询参数传给脚本,但不要在回复、日志摘要或输出文件名中复述敏感参数。 | 脚本 | 用途 |
调用 `scripts/download_document.py`:
```text
--url 'https://example.com/report.docx?signature=...' --output '/usr/local/src/word/tmp/<任务名>/source.docx'
```
可选参数:
- `--timeout <1-600>`:连接和读取超时秒数,默认 `60`。
- `--max-bytes <字节数>`:默认且最高 `26214400`(25 MiB),只允许设置更小的限制。
- `--overwrite`:只在目标是本次任务生成的旧缓存时使用。
`output` 扩展名必须是 `.docx`、`.dotx` 或 `.doc`。脚本阻止 HTTPS 重定向降级到 HTTP,流式限制大小,先写同目录临时文件,再原子发布;DOCX/DOTX 会检查 ZIP 路径、成员大小、必要部件和内容类型,并用 `python-docx` 打开。实际 OOXML 格式与 `output` 扩展名不一致时,根据错误中的实际格式更正缓存扩展名,再调用同一脚本。
成功结果包含 `path`、`size_bytes`、`format` 和 `validation`;OOXML 还包含段落、表格和章节数量。后续脚本只使用返回的本地 `path`,不再访问原 URL。
## 下载通用附件
需要下载作为 Word 任务素材的图片、视频、音频、压缩包或其他文件时,调用 `scripts/download_attachment.py`:
```text
--url 'https://example.com/asset.bin?signature=...' --output '/usr/local/src/word/tmp/<任务名>/asset.bin'
```
只接受 HTTPS 地址,`output` 可使用任意附件扩展名。可选参数只有 `--timeout <1-600>`(默认 `60`)和 `--overwrite`。附件上限固定为 25 MiB(26214400 字节),不可调高:脚本先用 HEAD 探测远端声明大小,再检查 GET 响应声明,并在流式接收时持续兜底计数;任一阶段发现超限都会返回 `ok: false` 和明确的“已拒绝下载”错误,且不会发布部分文件。
成功结果包含 `path`、实际 `size_bytes`、`declared_size_bytes`、`size_limit_bytes`、`size_probe` 和 `content_type`。本脚本不校验文件业务格式;远程 Word 源文档仍使用 `download_document.py`。
## 检查文档
调用 `scripts/inspect_document.py`:
```text
--input 'source.docx'
```
可选参数:
- `--start-paragraph <索引>`、`--max-paragraphs <1-300>`:分段读取正文,索引从 `0` 开始。
- `--start-table <索引>`、`--max-tables <0-50>`、`--max-table-cells <数量>`:限制表格输出。
- `--max-chars <1000-200000>`:限制单次正文字符数。
- `--include-runs`:需要检查局部字体、粗体、斜体或跨 Run 替换问题时使用。
重点检查:
- `tracked_changes.total` 和 `authors`:是否存在修订及修订作者。
- `comments`:批注正文和作者。
- `sections`:纸张、方向、页边距、页眉、页脚。
- `has_images`、`inline_image_count`、`media_part_count`:是否需要进一步读取图片文字;浮动图片可能只计入媒体部件。
- `archive.missing_required_parts`、`duplicate_members`:结构异常。
- `has_more`、`next_paragraph`、`next_table`:继续读取长文档。
## 识别图片中的文字
需要读取图片、截图或扫描页中的文字时调用:
```text
--input 'source.docx'
```
省略 `--pages` 时,脚本会把 Word 临时转换为 PDF,自动选择包含足够大图片的页面,每次最多处理 4 页。需要识别较小图片或指定页面时传:
```text
--input 'source.docx' --pages '2,5-6'
```
脚本通过 LibreOffice 和 Poppler 临时渲染页面,使用本地 RapidOCR 识别图片区域;临时 PDF 和 PNG 会自动删除,不联网,也不调用大模型识图。PDF 原生文本层用于过滤正文、页眉、页脚和页码产生的重复 OCR,因此 `pages[].text` 只返回可靠的额外图片文字。
检查:
- `candidate_pages`:自动检测到的图片页;`selection_mode` 表示自动或显式选页。
- `status: good` 且 `usable_for_summary: true`:可以把 `text` 补充到原生文档内容中。
- `status: no_image_text`:图片区域没有识别到额外文字,不是错误。
- `status: sparse` 或 `low_confidence`:不要使用返回文字;根据 `needs_review` 人工核验。
- `filtered_native_line_count` 和 `filtered_outside_image_line_count`:被当作原生文字或图片区域外文字过滤的 OCR 行数。
默认 260 DPI,可用 `--dpi 150-400` 调整。若 `has_more: true`:`next_offset > 0` 时传 `--pages <next_page> --start-offset <next_offset>`;`next_offset = 0` 时把 `remaining_pages` 作为下一次 `--pages`。普通小徽标和面积不足页面约 1.5% 的图片不会进入自动候选,但仍可用 `--pages` 显式识别。
## 创建文档
调用 `scripts/create_document.py`:
```text
--output '/usr/local/src/word/result.docx' --spec '<JSON对象>'
```
内容较长时先把 JSON 写到任务临时目录,再传 `--spec-file`。目标是本次任务旧产物且确认可覆盖时才传 `--overwrite`。
文档说明顶层结构:
```json
{
"properties": {
"title": "2026 年度经营报告",
"author": "示例公司",
"subject": "经营分析"
},
"page": {
"size": "A4",
"orientation": "portrait",
"margins": {
"top": 0.85,
"bottom": 0.85,
"left": 0.9,
"right": 0.9
}
},
"default_font": {
"name": "Arial",
"east_asia": "Noto Sans CJK SC",
"size": 10.5,
"line_spacing": 1.15,
"space_after": 6
},
"styles": {
"Title": {"size": 24, "bold": true, "color": "1F4E78"},
"Heading 1": {"size": 16, "bold": true, "color": "1F4E78"}
},
"header": {
"text": "示例公司 · 年度报告",
"alignment": "right"
},
"footer": {
"text": "",
"alignment": "center",
"page_number": true,
"page_number_prefix": "第 ",
"page_number_suffix": " 页"
},
"blocks": []
}
```
支持的 `blocks[].type`:
| 类型 | 关键字段 |
| --- | --- | | --- | --- |
| `paragraph` | `text` 或 `runs`;可选 `style/alignment/space_before/space_after/indent` | | `scripts/inspect_environment.py` | 检查依赖与精确字体家族;`--font` 可重复传入 |
| `heading` | `level`(1–9)、`text` 或 `runs` | | `scripts/download_document.py` | 安全下载并校验 HTTPS Word 文件 |
| `bullet_list` | `items[]`,可选 `level` | | `scripts/download_attachment.py` | 下载任务附件,限制 25 MiB |
| `numbered_list` | `items[]`,可选 `level` | | `scripts/inspect_document.py` | 分段读取 Word 正文、表格、样式、批注、修订 |
| `table` | `rows[][]`;可选 `column_widths/header_rows/style/header_fill/merges` | | `scripts/extract_source.py` | 分页读取 PDF 及 TXT/MD/HTML 支持资料 |
| `image` | `path`;可选 `width_inches/height_inches/alignment/caption` | | `scripts/ocr_document.py` | 提取 Word 图片/扫描页中的可靠文字 |
| `toc` | 可选 `title`、`levels`,如 `1-3` | | `scripts/create_document.py` | JSON 创建 DOCX;`--preset patent` 生成专利文档 |
| `horizontal_rule` | 可选 `color/size/style` | | `scripts/edit_document.py` | 模板回填、替换、插入删除、调整页面;可记录文本替换修订 |
| `page_break` | 无其他必填字段 | | `scripts/add_comment.py` | 为精确文本范围添加批注 |
| `section_break` | 可选 `break_type/page size/orientation/margins` | | `scripts/accept_changes.py` | 接受全部已有修订并保留原文件 |
| `spacer` | 可选 `points` | | `scripts/convert_document.py` | Word 格式转换及 PDF/Markdown/文本导出 |
| `scripts/unpack_document.py` | 安全解包,仅用于常规接口不能完成的 XML 编辑 |
| `scripts/pack_document.py` | 安全打包 OOXML 目录 |
| `scripts/validate_document.py` | 检查 ZIP、XML、关系、批注、修订元数据与可渲染性 |
| `scripts/render_document.py` | 分批生成 Word 逐页 PNG/PDF |
| `scripts/compile_typst.py` | 编译任务内 Typst 源文件,或使用内置简历模板生成 PDF |
带局部格式和链接的段落: ## 支持资料提取
```json 调用 `scripts/extract_source.py`,例如:
{
"type": "paragraph",
"alignment": "justify",
"runs": [
{"text": "重要:", "bold": true, "color": "C00000"},
{"text": "本报告数据截至 2026-06-30。"},
{
"text": "查看来源",
"hyperlink": "https://example.com/source"
}
]
}
```
表格示例:
```json
{
"type": "table",
"rows": [
["指标", "本期", "同比"],
["收入", "1,250 万元", "12.5%"],
["毛利率", "38.2%", "2.1 个百分点"]
],
"column_widths": [2.2, 1.7, 1.7],
"header_rows": 1,
"header_fill": "1F4E78",
"style": "Table Grid"
}
```
`runs[]` 支持 `bold/italic/underline/strike/color/font/east_asia_font/size/superscript/subscript/style/hyperlink`。不要用换行符模拟段落或分页;使用独立 `paragraph` 或 `page_break`。项目符号和编号必须使用列表块,不要手写 `•` 或数字前缀。
## 编辑文档
调用 `scripts/edit_document.py`:
```text ```text
--input 'source.docx' --output '/usr/local/src/word/edited.docx' --spec '<JSON对象>' --input '/usr/local/src/word/tmp/<任务名>/resume.pdf' --page 1
``` ```
JSON 顶层只有 `operations`。支持: - 输入支持 PDF/TXT/MD/HTML/HTM,最大 25 MiB。Word 使用 `inspect_document.py`,旧格式先转换。
- PDF 每次读取一页;`--columns auto|1|2` 控制单/双栏,默认保守检测中央栏间距。表格从正文中排除后另列,避免重复。复杂跨栏布局须按原页核对阅读顺序;精细表格单元格提取可使用 `pdf` skill。
| `operations[].type` | 关键字段 | - `--max-chars` 为 256–12000,默认 12000;`has_more: true` 时把 `next_page` 作为 `--page`,`next_offset` 作为 `--start-offset` 继续。TXT/MD/HTML 使用逻辑页 1 和字符偏移续读。
| --- | --- | - `usable_for_summary: false` 的文本不能作为可靠事实;扫描或乱码 PDF 的 `needs_ocr: true` 表示需对该页补 OCR。`has_images` 本身不是执行 OCR 的理由。
| `replace_text` | `find`、`replace`;可选 `scope/match_case/whole_word/count/required` |
| `append_blocks` | `blocks[]`,格式与创建脚本相同 |
| `insert_blocks_after` | `find`、`blocks[]`;可选 `match: exact|contains` |
| `remove_paragraphs` | `text`;可选 `match/count/required` |
| `set_paragraph_style` | `style`,以及 `indexes[]` 或 `contains` |
| `set_properties` | `properties` |
| `set_page` | `page`;`section` 为索引或 `all` |
| `set_header_footer` | 可选 `header`、`footer` |
| `remove_tables` | `indexes[]` |
查找替换会处理 Word 把可见短语拆成多个 `<w:r>` 的情况,并尽量保留首个匹配 Run 的格式。默认范围 `all` 包含正文、表格、页眉和页脚;可指定 `body/tables/headers/footers`。
输入含现有修订时默认停止:
- 用户希望干净副本:先用 `accept_changes.py`。
- 用户明确要求保留修订:才传 `--allow-existing-revisions`;修改后的内容本身不会自动变成新的修订。
- 用户要求“所有修改都显示为修订”且固定脚本无法表达时,不要伪装完成;说明当前安全脚本只支持接受现有修订,不支持通用修订式编辑。
## 添加批注
调用 `scripts/add_comment.py`:
```text
--input 'source.docx' --output '/usr/local/src/word/commented.docx' --find '费用上限' --comment '请确认该上限是否含税' --author '审阅人' --initials 'SR'
```
可选:
- `--scope <body|tables|headers|footers|all>`。
- `--occurrence <序号>`:为全文第几个匹配添加批注,默认 `1`。
- `--ignore-case`。
脚本会在必要时拆分 Run,让批注尽量精确锚定到目标文本,而不是整个段落。
## 接受修订
调用 `scripts/accept_changes.py`:
```text
--input 'redlined.docx' --output '/usr/local/src/word/clean.docx'
```
脚本只执行固定的“接受全部修订”宏,不能运行用户提供的宏。必须检查:
- `revision_markers_before` 大于 `0` 时,`revision_markers_after` 必须为 `0`。
- `status` 必须为 `success`。
- 之后仍要执行结构校验和逐页渲染,特别检查删除段落、编号列表和空白段落。
## 转换
调用 `scripts/convert_document.py`:
```text
--input 'legacy.doc' --output '/usr/local/src/word/tmp/task/source.docx'
```
支持:
- `.doc` / `.dotx` → `.docx`。
- `.docx` → `.pdf`,用于预览或用户明确要求的 PDF 副本。
- `.docx` → `.md` / `.txt`,默认按接受修订后的视图导出;可传 `--track-changes reject|all`。
不要把转换为 Markdown 的结果当作版式等价副本;表格宽度、浮动图片、页眉页脚、脚注和分页可能简化。
## 高级 OOXML 编辑
只有 `edit_document.py` 无法完成且确实需要编辑底层部件时:
1. 调用 `scripts/unpack_document.py --input <docx> --output-dir <空目录>`。
2. 用可用的文件编辑工具最小化修改 `word/document.xml` 或相关部件;不要重排、格式化或重写无关 XML。
3. 调用 `scripts/pack_document.py --input-dir <目录> --output <新docx>`。
4. 调用 `validate_document.py --check-convert` 和 `render_document.py`。
解包脚本拒绝路径穿越、符号链接和压缩炸弹;打包脚本拒绝缺少 `[Content_Types].xml`、`_rels/.rels` 或 `word/document.xml` 的目录。不要手动调用 `unzip`、`zip`、`find` 或删除命令。
## 校验
调用 `scripts/validate_document.py`:
```text
--input '/usr/local/src/word/result.docx' --check-convert
```
必须满足:
- `status: valid`。
- `issue_count: 0`。
- `archive.missing_required_parts` 和 `duplicate_members` 为空。
- 批注引用完整,内部关系目标存在,所有 XML 可安全解析。
- 修订元素有作者和时间;干净副本的 `tracked_changes.total` 应为 `0`。
- `render_check.success: true` 且页数大于 `0`。
该检查验证结构和可打开性,不替代人工视觉检查。
## 渲染与视觉检查
调用 `scripts/render_document.py`:
```text
--input '/usr/local/src/word/result.docx' --output-dir '/usr/local/src/word/tmp/task/rendered'
```
默认 150 DPI、单次最多 20 页。可传:
- `--start-page`、`--end-page`、`--max-pages`:分批渲染。
- `--dpi <72-300>`:小字、复杂表格或页眉脚注可提高到 180–220。
- `--include-pdf`:同时保留 `document.pdf`。
- `--overwrite`:只覆盖本次任务旧渲染。
若 `has_more: true`,用 `next_page` 继续。通过可用的图片查看工具逐页检查。
## 质量要求
- 默认采用 A4、合理页边距、清晰标题层级;现有文档的规范优先。
- 中文使用 `Noto Sans CJK SC` 或原文档字体,拉丁文字使用 Arial;不要依赖运行环境中不存在的专有字体。
- 标题必须使用内置 `Heading 1`–`Heading 9` 或具有大纲级别的样式,目录才能收录。
- 表格明确设置列宽、重复表头并禁止跨页拆分关键行;不要使用百分比列宽假设不同客户端一致。
- 表格底色使用明确填充色;不要用表格模拟水平线。
- 页码、目录和交叉引用使用字段,不手写空格或点号对齐。
- 图片保持纵横比,注明来源或说明文字,确认没有超出版心。
- 不用 `\n` 代替独立段落,不用空格填充对齐,不把分页符直接放在正文字符串中。
- 检查孤行孤字、标题落在页尾、表格断裂、图片拉伸、文字裁切、异常空白页、乱码、重叠和页码连续性。
- 最终文档必须内容准确、结构有效、可由 LibreOffice 打开,并通过全部页面视觉检查。

View File

@ -1,4 +1,4 @@
interface: interface:
display_name: "Word 文档" display_name: "Word 与专业文书"
short_description: "安全读取、编辑、图片 OCR、校验并渲染专业 Word 文档" short_description: "读取编辑 Word,优化简历、撰写专利与公文、填写签证材料,并校验排版"
default_prompt: "使用 $docx 读取或创建 Word 文档,按需识别图片文字,并完成内容与版式校验。" default_prompt: "使用 $docx 处理 Word 或专业文书,按需读取业务指南并完成内容、格式和交付检查。"

View File

@ -0,0 +1,22 @@
#let resume = json("resume.json")
#set page(paper: "a4", margin: (x: 2cm, y: 1.7cm))
#set text(font: ("Inter", "Noto Sans CJK SC"), size: 10pt, lang: "zh")
#set par(leading: 0.65em)
#set heading(outlined: true)
#align(center, text(size: 20pt, weight: "bold", resume.name))
#align(center, text(resume.contact.join(" | ")))
#if resume.summary != "" [
#v(6pt)
#text(resume.summary)
]
#for section in resume.sections [
#block(above: 10pt, below: 4pt)[
#heading(level: 1, text(size: 12pt, weight: "bold", section.title))
#line(length: 100%, stroke: 0.5pt)
]
#for item in section.items [
#block(above: 3pt, below: 3pt, text(item))
]
]

View File

@ -0,0 +1,43 @@
# 运行依赖与字体
基础镜像预置 Python 文档/PDF/OCR 库、LibreOffice、Pandoc、Poppler、Node docx、Typst、Fontconfig。任务中不安装软件。依赖问题用 `scripts/inspect_environment.py` 检查实际容器;Dockerfile 中的安装声明不等于某个旧运行容器已更新。
```text
--font 'Arial' --font 'Times New Roman' --font 'SimSun' --font 'FangSong' --font 'Microsoft YaHei' --font '方正小标宋简体' --font 'PingFang SC' --font 'SF Pro'
```
返回 `tools/packages/missing_tools/missing_packages` 及 `requested_fonts/missing_fonts`。字体按 `fc-list` 实际家族名匹配;`fc-match` 找到替代字体不能证明原字体存在。`ok: true` 仅表示检查执行成功,仍需阅读缺项。
## 字体选择
| 需求 | 可用字体来源 | 使用方式 |
| --- | --- | --- |
| 通用中文、简历、界面文字 | 镜像的 Noto Sans CJK SC / Noto Serif CJK SC | 默认可用,覆盖中文正文 |
| 宋、黑、楷、仿宋风格 | CTAN Fandol:FandolSong / FandolHei / FandolKai / FandolFang | 使用实际家族名;缺字可由 Noto 补充,不冒称原版宋体/仿宋 |
| Arial、Times New Roman | Debian 的 Microsoft core fonts 安装包 | 实际库存确认后用于西文;失败时明确提示或采用已有 Liberation 替代 |
| 原版宋体、黑体、仿宋、楷体 | 镜像的 SimSun / SimHei / FangSong / KaiTi | 使用实际家族名或对应中文名;仿宋和楷体不等于另行指定的 GB2312 版本 |
| 微软雅黑 | 镜像的 Microsoft YaHei / Microsoft YaHei UI | 已配置常规、粗体、细体,按模板使用对应家族 |
| 方正小标宋简体 | 镜像的 FZXiaoBiaoSong-B05S / 方正小标宋简体 | 使用完整家族名;不将简称“方正小标宋体”或 GBK 版本当作已安装的家族 |
| 苹果风格简体中文 | 镜像的 PingFang SC / 苹方-简 | 已配置六种字重;设置为东亚字体 |
| 苹果风格英文与数字 | 镜像的 SF Pro | 已配置可变字重正体和斜体;设置为西文字体 |
| Wingdings 等其他指定字体 | 额外字体文件 | 当前默认字体库不包含;可在镜像 custom-fonts 目录接入 |
用户已有模板优先保留字体。要求精确字号、行数、分页或官方字体时,生成前检查指定家族;缺少精确字体时说明实际使用的字体,不能把替代显示称为完全一致。正式公文的适用规范与字体要求按用户模板及主管机关要求核对。
## 苹方与 SF Pro 混排
用户要求苹果风格时,Word 创建说明可加入以下设置,并补全实际 `blocks`。沿用用户已有模板时保留其指定字体。
```json
{"default_font": {"name": "SF Pro", "east_asia": "PingFang SC"}}
```
单个 Run 使用 `font: "SF Pro"` 和 `east_asia_font: "PingFang SC"`。Typst 使用 `#set text(font: ("SF Pro", "PingFang SC"))`,让英文、数字优先采用 SF Pro,中文采用苹方 SC。字体存在不代表不同系统的字号、字距和渲染完全相同,仍需按文件流程检查页面。
## 来源
- Fandol 字库及 GPL 字体例外说明:https://ctan.org/pkg/fandol
- Debian Microsoft core fonts 包:https://packages.debian.org/trixie/ttf-mscorefonts-installer
- Typst 官方发行版:https://github.com/typst/typst/releases
原版中文字体的固定下载地址、版本与 SHA-256 见基础镜像 `fonts/cjk-fonts.tsv`、`fonts/apple-fonts.tsv` 和 README;以实际运行容器库存为准。

View File

@ -0,0 +1,260 @@
# 公文写作技能
根据用户需求,在三类公文场景中路由并生成格式合规的文书。
## 路由判定
| 用户意图信号 | 类别 | 跳转 |
|---|---|---|
| 起诉状、民事诉状、传票、应诉通知、立案通知、答辩状、上诉状 | 诉讼通知类 | → 第 1 节 |
| 减资公告、清算公告、注销公告、债权人公告、公示、登报公告 | 法律公告类 | → 第 2 节 |
| 会议通知、会议纪要、会议告示、开会通知、工作会议 | 会议告示类 | → 第 3 节 |
## 文件与事实依据
用户明确要求的输出格式优先,其次沿用原模板。文件操作统一使用主入口脚本,参数见 `references/word-operations.md`。用户只要文字时不额外生成文件。
诉讼、公告等文书的法域、程序、期限和适用标准必须按当前官方资料核对。模板中的案号、机关名称、日期、印章及身份信息只能用真实资料或清晰的待填字段,不虚构已正式签发的状态。
## 通用排版规范(GB/T 9704-2012)
以下用于用户明确要求相应党政公文规范的场景;普通会议通知不一律套用。生成前核对适用标准,并使用 `scripts/inspect_environment.py` 检查所需字体。缺少原版字体时先说明;Fandol 可用于普通草稿,但不能据此声称精确符合指定字体。
- **纸张**:A4
- **页边距**:上 3.7cm、下 3.5cm、左 2.8cm、右 2.6cm
- **标题**:方正小标宋简体,二号,居中
- **正文**:仿宋,三号,每页 22 行,每行 28 字
- **结构层次**:一级"一、"(黑体)→ 二级"(一)"(楷体)→ 三级"1."(仿宋)→ 四级"(1)"(仿宋)
- **发文字号**:年份用六角括号〔〕
- **成文日期**:用阿拉伯数字,右空四字编排
- **印章**:与正文间隔两行
---
## 1. 诉讼通知类
依据最高人民法院《民事诉讼文书样式》和《人民法院民事裁判文书制作规范》。
### 1.1 文书类型与必备要素
| 文书 | 核心要素 |
|---|---|
| **民事起诉状** | 原告信息、被告信息、诉讼请求(逐项列明)、事实与理由、证据清单、致送法院、签名日期、副本份数 |
| **答辩状** | 答辩人信息、原诉案号、针对诉请逐项答辩、事实与理由、证据、签名日期 |
| **上诉状** | 上诉人/被上诉人信息、原审法院/案号/判决日期、上诉请求、上诉理由、致送法院 |
| **立案通知书** | 案号、起诉人信息、对方当事人及案由、立案决定、权利义务告知、预交费用 |
| **应诉通知书** | 案号、被告信息、原告及案由、答辩期限、举证期限、权利义务告知 |
### 1.2 民事起诉状模板
```
民 事 起 诉 状
原告:[姓名/名称],[性别],[民族],[出生日期],
住所地:[地址],联系电话:[电话]。
(法人写:名称、住所地、法定代表人及职务、统一社会信用代码)
被告:[姓名/名称],[性别],[民族],[出生日期],
住所地:[地址],联系电话:[电话]。
诉讼请求:
一、[具体请求,如:判令被告支付货款人民币XX元及利息];
二、[第二项请求];
三、本案诉讼费用由被告承担。
事实与理由:
[客观陈述纠纷的时间、地点、经过、结果和争议焦点,引用相关法律依据]
证据清单:
1. [证据名称]——证明[待证事实]
2. [证据名称]——证明[待证事实]
此致
[法院全称]
起诉人:[签名/盖章]
[年] 年 [月] 月 [日] 日
附:本起诉状副本 [X] 份
```
### 1.3 撰写规则
- **诉讼请求**必须量化为具体金额或明确行为,不得含糊
- **事实与理由**按时间线组织,做到"六何":何人、何时、何地、何事、何因、何果
- **法律依据**引用条文时写明法律全称+条款号,如"依据《中华人民共和国民法典》第五百七十七条"
- **当事人信息**必须准确完整,自然人需含身份证号(如已知),法人需含统一社会信用代码
- 副本份数 = 被告人数
### 1.4 参考模板(需要时读取)
详细的诉讼文书模板集:`references/official/litigation-templates.md`
---
## 2. 法律公告类
依据《公司法》《公司登记管理条例》等法律法规。
### 2.1 公告类型与必备要素
| 公告 | 核心要素 | 法定要求 |
|---|---|---|
| **减资公告** | 公司名称、信用代码、原注册资本、减后资本、减资原因、债权人权利告知 | 依据现行公司法、减资程序和通知方式核对 |
| **清算公告** | 公司名称、清算组信息、清算原因、债权申报截止日、联系方式 | 依据现行法律、清算类型和通知方式核对 |
| **注销公告** | 公司名称、信用代码、注销原因、债务清偿声明 | 依据现行登记规定和主管机关要求核对 |
| **债权人公告** | 案号、债务人信息、申报期限、申报方式、逾期后果 | 依据案件适用程序及法院正式通知核对 |
### 2.2 减资公告模板
```
减少注册资本公告
[公司全称](统一社会信用代码:[代码]),经股东会决议,
拟将注册资本由人民币 [原金额] 万元减至人民币 [新金额] 万元。
减资原因:[简要说明]
根据《中华人民共和国公司法》第二百二十四条规定,请各债权人
自本公告发布之日起四十五日内,向本公司提出债权申报或要求
提供相应担保。
联系地址:[地址]
联系电话:[电话]
联系人:[姓名]
特此公告。
[公司全称](盖章)
[年] 年 [月] 月 [日] 日
```
### 2.3 清算公告模板
```
清算公告
[公司全称](统一社会信用代码:[代码])经股东会决议决定解散,
已依法成立清算组,清算组由 [成员姓名] 组成。
请本公司债权人自本公告发布之日起四十五日内,向清算组申报债权。
债权人申报债权时,应提供:
1. 债权人名称或姓名、联系方式
2. 债权数额、产生时间
3. 有无担保及担保情况
4. 相关证明材料
逾期未申报的,视为放弃相关权利,清算组将按已申报债权进行清偿分配。
清算组联系方式:
地址:[地址]
电话:[电话]
邮箱:[邮箱]
特此公告。
[公司全称] 清算组
[年] 年 [月] 月 [日] 日
```
### 2.4 撰写规则
- 公告需刊登在**省级以上公开发行的报纸**或**国家企业信用信息公示系统**
- 法定期限必须标注核对依据;不要直接沿用示例天数
- 必须包含债权人的权利告知和申报方式
- 公司信息必须与工商登记一致(全称、信用代码)
- 多次公告的,以**第一次公告日**起算期限
---
## 3. 会议告示类
依据 GB/T 9704-2012《党政机关公文格式》及通用公文写作规范。
### 3.1 文书类型与必备要素
| 文书 | 核心要素 |
|---|---|
| **会议通知** | 发文机关、文件字号、会议名称/议题、时间、地点、参会人员范围、准备事项、联系方式、落款日期 |
| **会议纪要** | 会议名称、时间地点、主持人、出席人员、列席人员、议定事项(逐条列明)、落款 |
| **会议告示(公告)** | 告示标题、会议目的、时间地点、面向对象、注意事项、发布单位、日期 |
### 3.2 会议通知模板
```
[发文机关名称] 文件
[机关代字]〔[年份]〕[序号] 号
关于召开 [会议名称] 的通知
[主送机关/参会单位]:
经研究,定于 [时间] 召开 [会议名称]。现将有关事项通知如下:
一、会议议题
[议题内容]
二、参加人员
[列明参会人员范围和具体要求]
三、会议时间
[具体日期和时段],[报到时间](如适用)
四、会议地点
[详细地址和会议室名称]
五、有关事项
(一)请参会人员提前准备 [材料/发言提纲等]。
(二)请于 [截止日期] 前将参会回执报送至 [部门]。
(三)[其他事项,如食宿、交通等安排]
联系人:[姓名] 电话:[电话]
[发文机关名称]
[年] 年 [月] 月 [日] 日
```
### 3.3 会议纪要模板
```
[会议名称] 纪要
[时间],[主持人职务及姓名] 在 [地点] 主持召开了 [会议名称]。
[出席人员]、[列席人员] 参加了会议。
会议听取了 [汇报内容],经讨论研究,形成以下纪要:
一、关于 [议题一]
会议认为,[决议内容]。
会议决定,[具体行动/责任分工/时间节点]。
二、关于 [议题二]
会议指出,[决议内容]。
会议要求,[具体行动/责任分工/时间节点]。
三、[其他议定事项]
[单位名称]
[年] 年 [月] 月 [日] 日
(出席人员:[逐一列明])
(列席人员:[逐一列明])
```
### 3.4 撰写规则
- 通知必须做到"五明确":**时间、地点、人员、议题、要求**缺一不可
- 纪要的议定事项必须落实到**责任人、行动项、时间节点**
- 发文字号格式:`[机关代字]〔[年份]〕[序号] 号`,年份用六角括号
- 主送机关顶格写,抄送机关在版记部分标注
- 紧急会议通知应标注紧急程度(特急/加急)
---
## 交付前自检
- **要素完整**:对照对应模板,核心要素无遗漏
- **法律准确**:引用的法律条文、法定期限与现行法律一致
- **格式合规**:排版符合 GB/T 9704 或法院文书样式要求
- **信息一致**:当事人/公司信息前后一致,无矛盾
- **措辞严谨**:法律文书用语规范,不使用口语化表达

View File

@ -0,0 +1,118 @@
# 诉讼文书模板集
依据最高人民法院《民事诉讼文书样式》(2024年修订)及《人民法院民事裁判文书制作规范》。
## 答辩状
```
民 事 答 辩 状
答辩人:[姓名/名称],[基本信息同起诉状格式]
因 [原告姓名/名称] 诉答辩人 [案由] 纠纷一案(案号:[案号]),
答辩人现提出答辩如下:
一、针对原告第一项诉讼请求的答辩
[逐项针对原告诉请进行答辩,陈述事实和法律依据]
二、针对原告第二项诉讼请求的答辩
[同上]
三、[补充答辩意见]
综上所述,原告的诉讼请求缺乏事实和法律依据,
请求法院依法驳回原告的全部/部分诉讼请求。
证据清单:
1. [证据名称]——证明[待证事实]
2. [证据名称]——证明[待证事实]
此致
[法院全称]
答辩人:[签名/盖章]
[年] 年 [月] 月 [日] 日
```
## 上诉状
```
民 事 上 诉 状
上诉人(原审[原告/被告]):[姓名/名称],[基本信息]
被上诉人(原审[被告/原告]):[姓名/名称],[基本信息]
上诉人因与被上诉人 [案由] 纠纷一案,
不服 [原审法院全称] [年] 年 [月] 月 [日] 日作出的
([案号])民事判决书,现提出上诉。
上诉请求:
一、撤销原审判决第 [X] 项;
二、依法改判 [具体请求](或发回重审);
三、[一审/二审]诉讼费用由被上诉人承担。
上诉理由:
一、原审认定事实错误
[具体阐述]
二、原审适用法律错误
[具体阐述]
三、[其他理由]
此致
[上一级法院全称]
上诉人:[签名/盖章]
[年] 年 [月] 月 [日] 日
附:本上诉状副本 [X] 份
```
## 财产保全申请书
```
财 产 保 全 申 请 书
申请人:[姓名/名称],[基本信息]
被申请人:[姓名/名称],[基本信息]
请求事项:
请求对被申请人名下价值人民币 [金额] 元的财产采取保全措施,
包括但不限于:[银行存款/房产/车辆/股权等]。
事实与理由:
申请人与被申请人因 [案由] 纠纷,[简述情况]。
因被申请人存在 [转移财产/恶意处分财产等] 的情形,
如不及时采取保全措施,将导致申请人的合法权益受到难以弥补的损害。
依据《中华人民共和国民事诉讼法》第一百零三条、第一百零四条之规定,
特向贵院申请财产保全。
申请人愿意依法提供担保。
此致
[法院全称]
申请人:[签名/盖章]
[年] 年 [月] 月 [日] 日
```
## 常见案由对应的诉讼请求模式
| 案由 | 典型诉讼请求写法 |
|---|---|
| 买卖合同纠纷 | 判令被告支付货款人民币XX元及逾期利息(自X年X月X日起至实际付清之日止,按LPR计算) |
| 民间借贷纠纷 | 判令被告偿还借款本金XX元及利息(按年利率X%计算) |
| 劳动争议 | 判令被告支付经济补偿金/赔偿金人民币XX元;补缴X年X月至X年X月的社会保险 |
| 房屋租赁纠纷 | 判令被告腾退位于[地址]的房屋并支付拖欠租金XX元及占用费 |
| 机动车交通事故 | 判令被告赔偿医疗费XX元、误工费XX元、残疾赔偿金XX元,共计人民币XX元 |
| 离婚纠纷 | 判准原告与被告离婚;婚生子女由原告抚养,被告每月支付抚养费XX元;依法分割夫妻共同财产 |
## 文书语言规范
- 用"原告""被告"而非"我""对方"
- 金额同时用大写和小写:`人民币壹万元整(¥10,000.00)`
- 日期格式:`二〇二六年三月十日` 或 `2026年3月10日`
- 引用法条格式:`《中华人民共和国民法典》第五百七十七条`
- 避免情绪化表述,保持客观中立

View File

@ -0,0 +1,46 @@
# 专利撰写、审查、答复与布局
## 模式
| 用户意图 | 处理重点 | 指南 |
| --- | --- | --- |
| 根据技术交底撰写 | 发明点 → 权利要求 → 说明书 → 摘要与附图 → 自检 | `references/patent/drafting-workflow.md` |
| 审查现有申请 | 检查支持关系、充分公开、术语及引用关系,按严重程度列出问题 | `references/patent/review-checklist.md` |
| 答复审查意见 OA | 对应每项理由和对比文件,区分争辩与修改,避免超出原始公开 | `references/patent/oa-response.md` |
| 布局/规避/FTO | 保护目标、技术拆解、竞争专利、法律状态与覆盖范围 | `references/patent/patent-strategy.md` |
先利用已有资料确定法域、申请阶段和用户期望的产物。没有完成检索、法律状态核验或对比分析时,明确尚未完成的部分,不能把写作框架当成确定的授权或不侵权结论。用户的专业程度按对话判断,不预设其职业。
以权利要求为主线;说明书支持各项技术特征;全文术语一致。从属权利要求明确引用关系,说明书按技术领域、背景技术、发明内容、附图说明、具体实施方式组织。摘要长度按申请法域核对,中文初稿通常控制在 300 字以内。
## 文件流程
Word 先读取完整结构,其他资料用 `scripts/extract_source.py`。有原稿时使用 `scripts/edit_document.py` 回填;用户需要修订对照时添加 `--track-changes --author <作者>`,只支持文本替换。复杂结构改动可另给对照说明或批注,不伪造修订状态。
无模板时调用 `scripts/create_document.py --preset patent`,保留 v3 的专利内容结构,通过统一创建和校验流程生成:
```text
--preset patent --output '/usr/local/src/word/patent.docx' --spec-file '/usr/local/src/word/tmp/<任务名>/patent.json'
```
```json
{
"properties": {"title": "专利申请初稿"},
"claims": [
{"number": 1, "text": "一种由技术交底支持的方法……", "dependent": false},
{"number": 2, "text": "根据权利要求1所述的方法……", "dependent": true}
],
"specification": {
"field": "技术领域内容",
"background": ["背景技术内容"],
"summary": ["技术方案与效果"],
"drawings": ["图1的说明;实际附图需另行提供或生成"],
"detailed": ["具体实施方式"]
},
"abstract": "摘要内容"
}
```
可只提供非空 claims、specification、abstract 中的一部分。权利要求编号必须从 1 连续;预设使用真实编号列表、标题样式、A4 和 2.5 cm 页边距。权利要求书、说明书、摘要分别分节,设置独立页眉并从第 1 页编号。预设使用镜像现有的 Noto/Liberation 字体,属于通用初稿排版;有明确字体或官方模板要求时按操作指南使用通用 JSON/原模板,并检查字体,不能声称预设自动满足所有官方格式。
预设中的 drawings 只生成附图说明,不生成技术附图。核对权利要求与说明书覆盖、附图实际交付、来源及用户要求后,再执行主入口校验和渲染。

View File

@ -0,0 +1,247 @@
本指南提供写作与审查方法。涉及具体申请法域、法定期限或法律效力时,核对当前主管机关资料,并标注来源;未完成检索时不作确定性结论。
# 专利撰写完整指引
从技术交底书到完整专利申请文件的全流程。包含工作流、权利要求书撰写方法论、说明书撰写方法论。
## 目录
- [阶段1:技术交底分析](#阶段1技术交底分析)
- [阶段2:可专利性预评估](#阶段2可专利性预评估)
- [阶段3:权利要求书起草](#阶段3权利要求书起草)
- [阶段4:说明书撰写](#阶段4说明书撰写)
- [阶段5:摘要与附图说明](#阶段5摘要与附图说明)
- [阶段6:自检与交付](#阶段6自检与交付)
---
## 阶段1:技术交底分析
**目标**:从用户提供的材料中提炼发明全貌。
**核心任务**:
1. **提取技术问题**:现有技术存在什么缺陷?本发明要解决什么问题?技术问题要具体,不要笼统说"效率低",要说清楚低在哪里、为什么低。
2. **提取技术方案**:关键步骤是什么?各部件/模块之间的关系?重点关注"怎么做"。
3. **提取技术效果**:与技术问题一一对应——解决了什么问题,就应该有什么效果。
4. **识别发明点**:哪些是现有技术已有的,哪些是创新所在?发明点是后续权利要求"特征部分"的核心。
5. **识别变体方案**:步骤顺序可否调整、某个模块可否用其他方式实现?变体方案影响从属权利要求的设计。
**输出**:结构化的发明分析摘要(技术领域、技术问题、技术方案、发明点、技术效果、变体方案)。
**质量检查**:问题→方案→效果三者是否自洽?发明点能否一句话说清楚?有没有遗漏关键细节?
---
## 阶段2:可专利性预评估
**目标**:正式撰写之前,快速判断可专利性,确定保护策略。
**核心任务**:
1. **新颖性初判**:核心发明点是否可能已被公开?如果用户提供了已知现有技术,重点分析区别。
2. **创造性初判**:区别特征是否显而易见?是否有技术偏见、启示不足等有利论点?
3. **保护策略建议**:建议的权利要求类型、独权中应含的必要技术特征、授权风险点。
**注意**:明确告知用户这是初步判断,不替代正式检索。
---
## 阶段3:权利要求书起草
权利要求书直接定义保护范围,是整个撰写过程中最核心的阶段。
### 3.1 独立权利要求
独立权利要求限定保护的最大范围,只包含实现发明目的不可缺少的技术特征(必要技术特征)。
**两段式结构**:
```
一种[主题名称],包括[前序部分的已知特征],
其特征在于,[特征部分的创新特征]。
```
- **前序部分**:主题名称 + 与现有技术共有的必要特征
- **特征部分**:"其特征在于"引导,写本发明区别于现有技术的特征
**撰写步骤**:
1. **确定主题类型**:方法(步骤描述)、装置/系统(模块描述)、存储介质。
2. **列出所有技术特征**,逐个做"去掉测试"——去掉后还能解决技术问题吗?能去掉的不是必要特征,放入从权。
3. **划分前序和特征部分**。
4. **上位化概括**:具体实现方式适度抽象。如"用卷积神经网络提取特征"上位为"通过特征提取模型提取特征"。太具体保护范围窄,太抽象可能得不到说明书支持。
**示例**——上位化前后对比:
修正前(过于具体,保护范围窄):
```
1. 一种图像识别方法,其特征在于,包括:
使用800万像素摄像头采集RGB图像;
将所述RGB图像缩放至224×224像素;
使用ResNet-50模型提取512维特征向量;
使用SVM分类器对所述特征向量进行分类。
```
修正后(适度上位化):
```
1. 一种图像识别方法,其特征在于,包括:
采集待识别图像;
对所述待识别图像进行特征提取,获得图像特征;
基于所述图像特征进行分类,获得识别结果。
```
ResNet-50、SVM 等具体实现放到从属权利要求中。
### 3.2 从属权利要求
从属权利要求在独权基础上构建保护层次——独权被缩限时,从权可以作为退路。
```
根据权利要求[编号]所述的[主题名称],其特征在于,[附加技术特征]。
```
**设计原则**:
1. **每条只限定一个维度**,避免两个不相关特征捆绑。
2. **由宽到窄层层递进**:第一层大类限定 → 第二层细化 → 第三层最具体实现。
3. **有战略意义**:保护有商业价值的具体实现,或封堵规避路径。
4. **覆盖变体方案**:多种实现方式用并列从权分别覆盖。
**引用方式**:单引(`根据权利要求1所述的...`)或多引(`根据权利要求1或2所述的...`)。多引权利要求不能再被多引。
### 3.3 多类型权利要求布局
| 类型 | 保护什么 | 适用场景 |
|------|---------|---------|
| 方法权利要求 | 实现步骤 | 核心流程有创新 |
| 装置/系统权利要求 | 功能模块 | 产品形态有创新 |
| 存储介质权利要求 | 承载程序的介质 | 软件类发明 |
| 电子设备权利要求 | 处理器+存储器+程序 | 软件类发明的硬件维度 |
**基础布局**:1条方法独权 + 5-8条方法从权 + 1条装置独权 + 3-5条装置从权(共10-15条)。
**完整布局**:加上存储介质和电子设备独权(共20-25条)。
装置权利要求与方法权利要求一一对应:方法中的每个步骤对应装置中的一个模块。
### 3.4 用语规范
| 场景 | 正确 | 错误 |
|------|------|------|
| 首次引入技术特征 | **一种**数据处理方法 / **一个**处理模块 | ~~所述数据处理方法~~(首次就用"所述") |
| 后续引用同一特征 | **所述**数据处理方法 | ~~该数据处理方法~~("该"不如"所述"规范) |
**避免的用语**:大约/基本上(不明确)、优选/最好(择优表述)、等/例如(开放列举)、高效的/优秀的(主观评价)、必要时/可选地(权利要求特征要么有要么没有)。
**连接词**:`包括/包含`(开放式,推荐)、`由...组成`(封闭式,保护范围窄)。
### 3.5 常见问题
1. **独权特征过多**→ 识别非必要特征,移到从权。
2. **缺少先行词**→ 检查每个"所述"能否在前面找到引入。
3. **从权引用链断裂**→ 画引用树,检查编号对应。
4. **方法/装置权利要求不对应**→ 逐步骤检查对应关系。
### 3.6 权利要求自检清单
- [ ] 独权只含必要技术特征
- [ ] 至少有方法和装置两种类型的独权
- [ ] 每条从权只限定一个维度
- [ ] 每个"所述XX"都有先行词
- [ ] 首次引入用"一种/一个",后续用"所述"
- [ ] 同一特征全文术语一致
- [ ] 无模糊用语和主观评价词
- [ ] 从权引用编号正确,多引权利要求未被再次多引
- [ ] 方法步骤与装置模块一一对应
- [ ] 总条数 10-25 条
---
## 阶段4:说明书撰写
说明书的使命是**充分公开**发明内容(让本领域技术人员能实现),同时**支持**权利要求中的每个技术特征。
### 4.1 五大部分
**一、技术领域**(1-2句话)
`本发明涉及[领域]技术领域,尤其涉及[细分方向]。` 不要太宽泛("计算机技术")也不要太狭窄。
**二、背景技术**(3-5段)
1. 描述技术背景和应用场景
2. 描述现有技术做法(客观、具体)
3. 指出现有技术不足(用"然而""但是"引出),要**具体**而非笼统
反面:`目前图像识别技术仍存在一些问题。`
正面:`现有方法依赖大规模标注数据训练,在标注稀缺的专业领域,识别准确率显著下降。`
**三、发明内容**(3-5段)
1. 要解决的技术问题(承接背景技术的不足)
2. 技术方案(散文形式的独权内容)
3. 技术效果(与问题一一对应,尽量量化)
**四、附图说明**
每幅图一句话:`图1为本发明实施例提供的XX的流程示意图。` 图号连续。
**五、具体实施方式**(篇幅最长)
- 实施例一:对应独权,逐步骤/逐模块详细描述,使用附图标记(获取模块110、处理单元120)
- 实施例二~N:对应从权的变体方案,差异部分详写,相同部分引用
- 关键步骤必须给出足够细节,避免"黑箱"
### 4.2 充分公开的判断
思维实验:一个本领域工作 3-5 年的工程师,读完后不需要创造性劳动就能实现。
检查:关键步骤有无"黑箱"?关键参数有无给出?效果有无验证?
### 4.3 权利要求与说明书的对应
| 权利要求内容 | 说明书中的位置 |
|---|---|
| 独权的技术方案 | "发明内容"+ 主要实施例 |
| 从权的附加特征 | 详细描述或变体实施例 |
| 参数范围 | 实施例中的参数说明或实验数据 |
关键规则:权利要求中不应出现说明书完全没涉及的内容。
### 4.4 常见问题
1. **术语不一致**→ 以权利要求书为准,说明书全文统一修改。
2. **实施方式过于简略**→ 逐条检查每个技术特征是否有足够展开。
3. **背景与发明内容脱节**→ 背景中提的问题,发明内容必须回应。
4. **缺少效果论证**→ 补充实验对比、性能指标或理论分析。
---
## 阶段5:摘要与附图说明
1. **摘要**:300字以内,包含技术问题 + 方案要点 + 效果。不用"本发明"开头,直接切入。
2. **附图说明**:确认编号连续,每幅图都有描述。纯文字交付时给出附图的文字描述方案。
---
## 阶段6:自检与交付
**精简自检清单**(详细版见 `references/patent/review-checklist.md`):
**权利要求书**:
- [ ] 独权只含必要技术特征
- [ ] 从权引用关系正确,每条只限定一个维度
- [ ] 用语规范,无模糊词和缺失先行词
**说明书**:
- [ ] 五大部分齐全
- [ ] 每个权利要求特征都在实施方式中有描述
- [ ] 术语与权利要求书完全一致
**摘要**:300字以内,涵盖问题、方案、效果。
**一致性**:附图编号连续,权利要求与说明书术语一致。
**交付顺序**:权利要求书 → 说明书 → 摘要 → 附图。
**文档格式交付**:有模板时用 `scripts/edit_document.py`;无模板时用 `scripts/create_document.py --preset patent`。参数结构见 `references/patent-writing.md`,校验和交付遵循主入口。

View File

@ -0,0 +1,252 @@
本指南提供写作与审查方法。涉及具体申请法域、法定期限或法律效力时,核对当前主管机关资料,并标注来源;未完成检索时不作确定性结论。
# 审查意见答复策略
收到审查意见通知书(OA)后,需要系统化地分析每条驳回理由,制定修改策略,并撰写有说服力的意见陈述书。本文档提供完整的答复方法论。
---
## 目录
- [答复工作流](#答复工作流)
- [新颖性驳回的应对](#新颖性驳回的应对)
- [创造性驳回的应对](#创造性驳回的应对)
- [清楚性与支持性驳回的应对](#清楚性与支持性驳回的应对)
- [权利要求修改策略](#权利要求修改策略)
- [意见陈述书撰写框架](#意见陈述书撰写框架)
---
## 答复工作流
### 第一步:全面分析审查意见
逐条阅读审查意见,提取以下信息:
1. **驳回类型**:新颖性、创造性、清楚性、支持性、充分公开,还是其他?
2. **涉及的权利要求**:哪些权利要求被驳回?独权还是从权?
3. **引用的对比文件**:审查员用了哪些现有技术文件?
4. **审查员的逻辑**:审查员认为对比文件公开了什么?认为区别在哪里?认为区别为什么显而易见?
把每条审查意见整理成结构化的表格:
```
| 序号 | 涉及权利要求 | 驳回类型 | 对比文件 | 审查员观点摘要 |
|------|-------------|---------|---------|--------------|
| 1 | 权1-5 | 创造性 | D1+D2 | D1公开了...,D2公开了...,结合后显而易见 |
| 2 | 权6-8 | 清楚性 | - | "处理"含义不清楚 |
```
### 第二步:评估每条意见的合理性
对每条审查意见做独立判断:
- **审查员的理解是否正确?** 审查员是否正确理解了权利要求的技术方案?有时审查员的误解本身就是答复的突破口。
- **对比文件的引用是否恰当?** 对比文件是否真的公开了审查员所说的特征?有没有曲解?
- **逻辑是否站得住脚?** 特别是创造性驳回中,"结合的动机"是否充分?
### 第三步:制定答复方案
对每条审查意见,确定采用以下哪种策略(或组合使用):
| 策略 | 适用场景 |
|---|---|
| **争辩**(不修改权利要求,论证审查员判断有误) | 审查员明显误解了技术方案或对比文件 |
| **缩限**(在权利要求中增加限定特征) | 当前独权确实与对比文件区别不大,但从权中有区别特征 |
| **删除**(删除被驳回的权利要求) | 该权利要求对保护策略不重要 |
| **合并**(将从权特征并入独权) | 从权中的特征恰好能构成与对比文件的实质区别 |
| **改写**(重写权利要求的表述方式) | 清楚性问题、表述不当 |
### 第四步:执行修改并撰写意见陈述书
先修改权利要求书和说明书(如需要),再撰写意见陈述书论证修改后的方案满足授权条件。
---
## 新颖性驳回的应对
新颖性驳回意味着审查员认为一篇对比文件已经公开了权利要求的全部技术特征。
### 分析方法
逐一比对权利要求中的每个技术特征与对比文件的公开内容:
```
| 技术特征 | 对比文件是否公开 | 分析 |
|---------|----------------|------|
| 特征A | 是 | D1第X段公开了... |
| 特征B | 是 | D1图X中显示了... |
| 特征C | 否/部分 | D1的XX与本申请的XX存在以下区别:... |
```
### 答复思路
1. **找区别特征**:仔细比较,找到至少一个对比文件没有公开(或公开内容不同)的技术特征。新颖性是"全面覆盖"原则——只要有一个特征不同,新颖性就成立。
2. **对比文件是否真的公开了该特征**:审查员有时会对对比文件做扩大解读。检查对比文件原文,看是否确实明确公开了审查员所称的内容。如果对比文件只是"隐含"或"示意性提到",可以争辩并非"明确公开"。
3. **技术特征的内涵差异**:即使表面用语相似,如果技术内涵不同(比如"处理"在不同上下文中含义不同),可以从技术本质层面论证区别。
4. **如果确实没有区别**:将从属权利要求中的区别特征并入独立权利要求,缩限保护范围以克服新颖性问题。
---
## 创造性驳回的应对
创造性驳回是最常见也是最复杂的驳回类型。审查员通常用"三步法"来判断创造性:
1. 确定最接近的现有技术
2. 确定区别特征和实际解决的技术问题
3. 判断该区别特征对于本领域技术人员是否显而易见
答复也围绕这三步展开。
### 策略一:质疑"最接近的现有技术"
- 审查员选择的对比文件是否真的是"最接近的"?
- 如果对比文件所属的技术领域与本申请差异较大,可以质疑其作为最接近现有技术的合理性
### 策略二:重新界定区别特征
- 审查员是否遗漏了某些区别特征?
- 审查员是否把权利要求中的多个特征拆分开来,分别在不同对比文件中找到了对应?如果是,检查这些特征之间是否存在协同关系——整体组合产生的效果不是各部分简单相加
### 策略三:论证"结合动机"不成立
这是创造性答复中最有力的论据方向。审查员要证明两篇对比文件"可以结合",而你要论证"没有理由结合":
- **技术领域差异**:两篇对比文件是否属于不同技术领域?不同领域的技术方案组合需要更强的结合动机
- **技术问题差异**:两篇对比文件各自要解决的技术问题是否相同?如果不同,把它们结合在一起的动机何在?
- **功能/作用差异**:区别特征在对比文件中的作用与在本申请中的作用是否相同?如果不同,"启示"就不成立
- **技术矛盾**:两篇对比文件的方案是否存在矛盾?结合后是否会破坏某篇文件的技术效果?
- **反向教导**:对比文件是否明确建议了与本申请相反的方向?
### 策略四:强调意想不到的技术效果
如果本申请的技术方案产生了意想不到的技术效果(不是现有技术的简单叠加效果),这是创造性的强有力证据:
- 效果是否超出了本领域技术人员的预期?
- 效果是否不是各特征效果的简单加和,而是产生了协同效应?
- 是否有实验数据或对比数据支撑?
### 策略五:缩限权利要求
如果上述策略都难以说服审查员,考虑将从属权利要求中的特征并入独立权利要求:
- 选择哪个从权特征并入?选择那个最能构成与对比文件实质区别的特征
- 并入后是否仍有足够的保护范围?评估商业价值
- 并入的特征是否在说明书中有充分支持?
---
## 清楚性与支持性驳回的应对
### 清楚性问题
审查员认为权利要求的表述不清楚,保护范围不确定。
**常见原因与对策**:
| 审查员指出的问题 | 对策 |
|---|---|
| 术语含义不清 | 替换为更明确的技术术语,或在权利要求中加入定义性限定 |
| 保护范围不清 | 进一步限定技术特征,消除歧义 |
| 缺少先行词 | 补充先行词,确保每个"所述"都有对应引入 |
| 语法歧义 | 改写句式,消除多种理解的可能性 |
### 支持性问题
审查员认为权利要求的保护范围超出了说明书公开的内容。
**常见原因与对策**:
| 审查员指出的问题 | 对策 |
|---|---|
| 上位概括过度 | 在说明书中补充更多下位实施例,或者在权利要求中适度缩限 |
| 功能性限定无支撑 | 在说明书中补充实现该功能的具体结构/方法 |
| 效果无验证 | 在说明书中补充实验数据或理论分析(需注意不超范围) |
---
## 权利要求修改策略
修改权利要求时要遵守"不超范围"原则:修改后的内容必须在原始申请文件记载的范围内。
### 修改方式一览
| 修改方式 | 操作 | 效果 | 风险 |
|---|---|---|---|
| 缩限 | 将从权特征并入独权 | 缩小保护范围,但克服驳回 | 保护范围可能过窄 |
| 删除 | 删除某条权利要求 | 放弃该条保护 | 少了一层防线 |
| 合并 | 合并多条权利要求的特征 | 形成新的保护组合 | 需确保组合在原文中有基础 |
| 改写 | 改变表述方式但不改变技术方案 | 消除形式问题 | 可能被认为修改了保护范围 |
### 修改优先级决策
当需要缩限独权时,如何选择并入哪个从权特征:
1. **区别度最大**:选择与对比文件区别最明显的特征,最有可能克服驳回
2. **商业价值最高**:选择竞争对手最可能实施的特征,保护范围最有商业意义
3. **证据最充分**:选择在说明书中有最详细描述和效果论证的特征
4. **综合判断**:在以上三者之间找平衡
---
## 意见陈述书撰写框架
意见陈述书是说服审查员的核心文件。结构清晰、论证有力是关键。
### 基本框架
```
## 意见陈述书
### 一、修改说明
[简要说明对权利要求书/说明书做了哪些修改,以及修改的依据]
修改内容:
- 权利要求1:将原从属权利要求X的特征并入,增加了"..."的限定
- 权利要求Y-Z:因独权修改,从权引用关系做相应调整
- [其他修改]
修改依据:上述修改内容均记载于原说明书第X段/原权利要求X中,未超出原始申请文件记载的范围。
### 二、关于新颖性/创造性的意见
#### 2.1 修改后的权利要求1与对比文件D1的比较
[逐特征比对,明确区别特征]
#### 2.2 区别特征的分析
[论证区别特征为什么不是显而易见的——选用前面"创造性驳回的应对"中的具体策略]
#### 2.3 技术效果
[论证修改后的技术方案所产生的技术效果]
### 三、关于其他审查意见的回应
[逐条回应其他问题]
### 四、结论
综上所述,申请人认为修改后的权利要求[概述理由],恳请审查员核准授权。
```
### 撰写要点
1. **对审查意见逐条回应**,不遗漏。即使某条意见因为修改已经自然克服,也要明确说明。
2. **论证要有具体的技术分析**,不要只说"区别明显"或"不是显而易见的",要说清楚为什么。
3. **引用原文要准确**,引用对比文件的具体段落、图号,引用本申请说明书的具体段落。
4. **语气专业但坚定**。对于有理有据的争辩不要畏缩,但也不要对审查员的观点使用贬义措辞。
5. **修改和争辩可以并行**。比如:先缩限权利要求以确保至少能授权,同时对原独权的创造性问题提出争辩意见——如果争辩成功,可以恢复更宽的保护范围。
---
## 文件格式化输出
原稿使用 `scripts/edit_document.py` 回填,需显示修改时添加 `--track-changes --author <作者>`,所有操作为 `replace_text`。新意见陈述书用 `scripts/create_document.py` 的通用 JSON 创建。参数见 `references/word-operations.md`;批注、原有修订和交付验收遵循主入口。

View File

@ -0,0 +1,246 @@
本指南提供写作与审查方法。涉及具体申请法域、法定期限或法律效力时,核对当前主管机关资料,并标注来源;未完成检索时不作确定性结论。
# 专利布局与策略
从单个技术方案的保护到整体专利组合的规划,本文档覆盖专利策略的核心方法论:布局规划、规避设计分析、FTO(自由实施)评估。
---
## 目录
- [专利布局规划](#专利布局规划)
- [规避设计分析](#规避设计分析)
- [FTO自由实施分析](#fto自由实施分析)
- [专利组合管理](#专利组合管理)
---
## 专利布局规划
专利布局的目标是用有限数量的专利,最大化对技术方案的保护覆盖。好的布局不仅保护自己的产品,还能限制竞争对手的行动空间。
### 维度拆解法
将技术方案沿不同维度拆解,每个维度考虑是否值得单独申请专利:
| 拆解维度 | 说明 | 举例 |
|---|---|---|
| **核心算法/方法** | 实现目标的核心步骤 | 一种基于XX的数据处理方法 |
| **系统架构** | 各模块如何组合 | 一种包含XX和YY的数据处理系统 |
| **数据结构/格式** | 独特的数据组织方式 | 一种用于XX的数据索引结构 |
| **预处理/后处理** | 核心流程前后的辅助步骤 | 一种用于XX的数据预处理方法 |
| **接口/交互** | 与外部系统或用户的交互方式 | 一种XX的交互控制方法 |
| **优化/改进** | 对基础方案的性能优化 | 一种提高XX效率的方法 |
| **应用场景** | 核心方案在特定场景下的应用 | 一种基于XX的医疗诊断方法 |
| **硬件/装置** | 执行方案的硬件形态 | 一种XX处理装置 |
不是每个维度都需要单独申请。判断标准是:**这个维度的方案,是否有独立的商业价值或阻截竞争对手的作用?**
### 布局策略选择
根据企业目标和资源,选择适合的布局策略:
**进攻型布局**
- 目标:阻止竞争对手进入该技术领域
- 做法:围绕核心技术密集申请,覆盖所有可行的实现路径
- 适用:在技术领先阶段,或技术壁垒较高的领域
**防御型布局**
- 目标:保护自身产品不受侵权指控
- 做法:重点保护产品实际使用的技术方案,积累用于交叉许可的筹码
- 适用:技术跟随者,或知识产权风险较高的领域
**储备型布局**
- 目标:提前占位,为未来的技术路线铺路
- 做法:对未来可能使用的技术方向先行申请
- 适用:技术路线尚不明确,但方向性判断清晰
### 布局规划输出
为用户制定布局方案时,输出以下内容:
```
## 专利布局方案
### 技术方案概览
[简述核心技术方案和技术优势]
### 布局目标
[进攻型/防御型/储备型,以及具体目标]
### 专利申请清单
| 序号 | 申请主题 | 保护维度 | 优先级 | 与核心专利的关系 |
|------|---------|---------|-------|----------------|
| 1 | ... | 核心算法 | 高 | 核心专利 |
| 2 | ... | 系统架构 | 高 | 核心专利 |
| 3 | ... | 预处理 | 中 | 外围专利 |
| ... | ... | ... | ... | ... |
### 申请顺序建议
[哪些先申请,哪些可以后申请,为什么]
### 风险提示
[布局中的薄弱环节和需要关注的点]
```
---
## 规避设计分析
规避设计是在不侵犯他人专利的前提下,实现类似或等效的技术效果。规避分析的反面也成立:分析自己的专利可能被他人如何规避,从而在布局时堵住漏洞。
### 分析框架
#### 第一步:解析目标专利的权利要求
将独立权利要求拆分为各个技术特征:
```
权利要求1:一种XX方法,包括:
特征A:...
特征B:...
特征C:...
其特征在于:
特征D:...
特征E:...
```
#### 第二步:逐特征分析规避可能性
对每个技术特征评估是否可以规避:
| 特征 | 规避可能性 | 规避思路 | 是否影响技术效果 |
|---|---|---|---|
| 特征A | 低 | 属于通用手段,难以规避 | - |
| 特征B | 中 | 可以用XX替代 | 效果基本不变 |
| 特征C | 高 | 可以省略该步骤 | 效果略有下降但可接受 |
| 特征D | 高 | 可以用完全不同的YY方案 | 效果可能更好 |
| 特征E | 低 | 无有效替代方案 | - |
#### 第三步:设计规避方案
侵权判定采用"全面覆盖原则"——只有当被控侵权方案包含了权利要求中的全部技术特征时,才构成侵权。因此,只要缺少(或实质性替换)其中任何一个技术特征,就可能不构成侵权。
规避方案要做到:
1. 至少一个技术特征与权利要求不同
2. 这个差异不构成"等同特征"——即不是以基本相同的手段实现基本相同的功能达到基本相同的效果
3. 规避后的方案仍能实现商业目标
#### 第四步:评估等同侵权风险
即使字面上规避了权利要求,仍需评估是否存在"等同侵权"的风险。等同判断标准:
- **手段是否基本相同**?用不同的技术实现同一功能
- **功能是否基本相同**?替代方案在系统中起到的作用是否一样
- **效果是否基本相同**?最终达到的技术效果是否相当
如果三者都是"基本相同",即使字面上不同,仍可能被认定为等同侵权。
### 自身专利的防规避分析
在撰写自己的专利时,从规避者的角度审视权利要求:
1. 独权中每个特征,竞争对手能不能找到替代方案?
2. 如果能,有没有在从权中覆盖这个替代方案?
3. 独权的上位概括是否足够宽泛,能涵盖主要的等同实现?
---
## FTO自由实施分析
FTO(Freedom to Operate)分析评估一个产品或技术方案是否侵犯他人的有效专利权。
### 分析流程
#### 第一步:界定分析范围
- **产品/技术方案**:要评估的具体技术方案是什么?要明确到技术特征层面
- **目标市场**:产品要在哪些国家/地区销售或使用?不同地区的专利权独立
- **时间范围**:产品什么时候上市?需要关注当前有效的专利,也要关注已公开但未授权的申请
#### 第二步:识别相关专利
基于技术方案的关键特征,确定需要排查的专利范围:
- 按技术关键词检索(在网页聊天场景下,可以帮助用户制定检索策略,由用户执行检索)
- 按技术分类号检索
- 按竞争对手名称检索
- 关注同领域被高频引用的专利
#### 第三步:侵权风险评估
对每件相关专利进行侵权比对:
```
| 专利号 | 权利人 | 独权要点 | 侵权风险 | 风险分析 |
|--------|-------|---------|---------|---------|
| CN... | XX公司 | 一种... | 高/中/低 | 产品方案包含了该专利全部特征/可规避特征X |
```
风险等级判断:
- **高风险**:产品方案与权利要求全面覆盖,且无明显规避空间
- **中风险**:存在部分重叠,或可能构成等同侵权
- **低风险**:有明确的区别特征,且不构成等同
#### 第四步:风险应对建议
针对识别出的风险专利,给出应对建议:
| 应对策略 | 适用场景 |
|---|---|
| 规避设计 | 有技术上可行的替代方案 |
| 申请许可 | 无法规避,但许可成本可接受 |
| 交叉许可 | 己方有对方需要的专利,可以互换 |
| 提出无效 | 风险专利本身存在新颖性/创造性缺陷 |
| 等待失效 | 风险专利临近到期 |
### FTO报告输出格式
```
## FTO分析报告
### 分析对象
[产品/技术方案描述]
### 分析范围
[目标市场、时间范围、检索策略]
### 风险专利清单
[高风险/中风险/低风险分类列表]
### 高风险专利详细分析
[逐件分析侵权风险和规避可能]
### 应对建议
[按优先级排列的具体行动方案]
### 结论
[总体风险评估和建议]
```
---
## 专利组合管理
### 组合评估维度
当用户需要评估已有的专利组合时,从以下维度分析:
| 维度 | 关注点 |
|---|---|
| **覆盖度** | 核心技术方案是否都有专利覆盖?有没有保护空白? |
| **层次性** | 是否有核心专利+外围专利的层次结构?从权是否提供了足够的退路? |
| **多样性** | 是否覆盖了方法、装置、系统等多种类型? |
| **有效性** | 专利是否仍在有效期内?维护费是否按时缴纳? |
| **质量** | 权利要求是否清楚、保护范围是否合理? |
### 组合优化建议
基于评估结果,给出具体的优化方向:
- **填补空白**:识别出未被保护的关键技术点,建议新申请
- **加强薄弱环节**:对保护范围过窄的专利,考虑提交新的申请来补充
- **淘汰低价值专利**:对于保护范围已被规避或技术已过时的专利,考虑放弃以节省维护成本
- **关注到期时间**:临近到期的核心专利,是否有后续专利接力?

View File

@ -0,0 +1,160 @@
本指南提供写作与审查方法。涉及具体申请法域、法定期限或法律效力时,核对当前主管机关资料,并标注来源;未完成检索时不作确定性结论。
# 专利申请文件审查清单
对已完成的专利申请文件进行系统化质量检查。审查结果按三个严重等级分类,帮助快速定位和优先处理关键问题。
---
## 问题严重等级
| 等级 | 含义 | 处理优先级 |
|---|---|---|
| **致命** | 直接导致驳回或权利范围严重缺陷,提交前必须修正 | 立即修正 |
| **重要** | 可能导致审查意见或减弱保护效果,强烈建议修正 | 优先修正 |
| **改进** | 不影响授权但可以提升文件质量,视时间精力决定 | 有余力时修正 |
审查时对发现的每个问题标注等级,修改时按"致命 → 重要 → 改进"的顺序处理。
---
## 一、权利要求书审查
### 1.1 清楚性检查
权利要求是否清楚地限定了保护范围?
| 检查项 | 等级 | 说明 |
|---|---|---|
| 有无模糊限定词 | 致命 | 检查是否含有"大约""基本上""适当地""近似"等词,除非给出了明确的判断标准 |
| 有无主观评价词 | 致命 | 检查是否含有"高效""优质""良好""最佳""足够"等无客观标准的词 |
| 有无相对性用语 | 重要 | 检查是否含有"较大""较小""较高""较低"等没有比较基准的词 |
| 技术特征是否具体 | 重要 | 每个技术特征是否有明确的技术含义,而非功能性的空泛描述 |
| 主题名称是否恰当 | 改进 | 主题名称是否准确反映了权利要求保护的技术方案 |
**模糊用语速查表**(以下词语在权利要求中出现时需要警惕):
> 大约、约、基本上、实质上、大致、近似、适当、合适、适宜、足够、必要时、可选地、优选、优选地、较好、较佳、等等、诸如、例如、高效、优秀、先进、良好、显著、明显
### 1.2 先行词一致性检查
"所述"引用是否都能找到先行词?
| 检查项 | 等级 | 说明 |
|---|---|---|
| 每个"所述XX"是否有先行词 | 致命 | 逐条检查:每个"所述"后面的术语,是否在前面的权利要求中用"一种/一个"引入过 |
| 术语是否全文一致 | 致命 | 同一技术特征在不同权利要求中名称是否完全一致,不能一处写"处理模块"另一处写"处理单元" |
| 首次引入是否用"一种/一个" | 重要 | 每个技术特征的首次出现是否使用了正确的引入格式 |
**检查方法**:对每条权利要求,提取所有"所述XX"的引用,在该条权利要求及其引用链上逐一查找对应的引入。
### 1.3 权利要求结构检查
权利要求体系的结构是否合理?
| 检查项 | 等级 | 说明 |
|---|---|---|
| 独权是否只含必要技术特征 | 重要 | 独权中有没有可以移到从权中的非必要特征?独权特征过多导致保护范围过窄 |
| 从权引用关系是否正确 | 致命 | 从权引用的编号是否指向正确的权利要求?引用链是否逻辑自洽? |
| 从权是否每条只限定一个维度 | 重要 | 一条从权中是否同时限定了两个不相关的特征? |
| 多引权利要求是否被再次多引 | 致命 | 多引的从权不能再被多引 |
| 方法权利要求与装置权利要求是否对应 | 重要 | 方法步骤与装置模块是否一一对应? |
| 权利要求总数是否合理 | 改进 | 通常10-25条为宜 |
### 1.4 保护范围检查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 独权保护范围是否足够宽 | 重要 | 有没有不必要的限定缩小了保护范围? |
| 从权是否覆盖了主要变体 | 重要 | 核心技术特征的替代实现方式是否有从权覆盖? |
| 是否有多种类型的权利要求 | 改进 | 是否同时包含方法和装置权利要求?软件类是否包含存储介质权利要求? |
---
## 二、说明书审查
### 2.1 充分公开检查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 权利要求中的每个特征是否都有描述 | 致命 | 逐条比对:权利要求中的每个技术特征,在说明书中是否都有对应的详细描述? |
| 关键步骤是否有具体实现方式 | 致命 | 有没有"黑箱"——只说了做什么,没说怎么做? |
| 关键参数是否给出 | 重要 | 涉及阈值、比例、范围等参数时,有没有给出具体值或选择依据? |
| 本领域技术人员能否实现 | 重要 | 整体判断:读完说明书后,一个有经验的工程师能否不做创造性劳动就实现发明? |
### 2.2 支持性检查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 独权的方案在"发明内容"中是否有概述 | 重要 | "发明内容"部分的技术方案描述是否覆盖了独权的核心内容 |
| 从权的附加特征在实施例中是否有体现 | 重要 | 每条从权的附加特征是否在至少一个实施例中有详细描述 |
| 上位概念是否有下位支撑 | 重要 | 独权中使用了上位概念(如"分类模型")时,说明书中是否给出了具体的下位实例(如"SVM、随机森林、神经网络") |
### 2.3 结构完整性检查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 五大部分是否齐全 | 致命 | 技术领域、背景技术、发明内容、附图说明、具体实施方式 |
| 技术问题→方案→效果逻辑链是否完整 | 重要 | 背景技术中的不足 → 发明内容中的方案 → 技术效果,三者是否环环相扣 |
| 附图说明与实施方式中的引用是否一致 | 重要 | 附图说明中列出的每幅图,在具体实施方式中是否都有引用?反过来也要检查 |
### 2.4 术语一致性检查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 说明书与权利要求书术语是否一致 | 致命 | 同一技术特征在两份文件中是否使用完全相同的术语 |
| 说明书内部术语是否一致 | 重要 | 同一概念在说明书不同部分是否使用了同一名称 |
| 附图标记是否一致 | 改进 | 同一组件的附图标记在全文中是否统一 |
---
## 三、摘要审查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 字数是否合规 | 重要 | 通常控制在300字以内 |
| 是否涵盖三要素 | 重要 | 技术问题(或技术领域)+ 技术方案要点 + 主要用途或效果 |
| 是否独立可读 | 改进 | 不依赖说明书,单独阅读摘要能否理解发明概要 |
| 是否含有不当内容 | 改进 | 摘要中不应有商业宣传性语言 |
---
## 四、形式审查
| 检查项 | 等级 | 说明 |
|---|---|---|
| 权利要求编号是否连续 | 致命 | 从1开始连续编号,不跳号 |
| 从权引用编号是否存在 | 致命 | 引用的权利要求编号指向的条目是否真实存在 |
| 附图编号是否连续 | 重要 | 图1、图2、图3...不跳号 |
| 说明书各部分标题是否规范 | 改进 | 使用标准的部分标题名称 |
---
## 审查输出格式
审查完成后,按以下格式输出审查报告:
```
## 审查报告
### 致命问题(X项)
1. [位置] 问题描述 → 建议修改方案
2. ...
### 重要问题(X项)
1. [位置] 问题描述 → 建议修改方案
2. ...
### 改进建议(X项)
1. [位置] 问题描述 → 建议修改方案
2. ...
### 总体评价
[对文件整体质量的简要评价,以及建议的修改优先级]
```
其中"位置"标注为:权利要求X / 说明书-技术领域 / 说明书-背景技术 / 说明书-发明内容 / 说明书-具体实施方式 / 摘要 等。
如果用户要求,在给出审查报告后可以直接输出修改后的完整文件。
如果用户提供了 DOCX,先读取结构,再用 `scripts/edit_document.py` 回填。需要修订标记的文本替换添加 `--track-changes --author <作者>`;操作结构、已有修订限制和校验流程见 `references/word-operations.md`。

View File

@ -0,0 +1,23 @@
# 简历写作与优化
文件操作统一遵循主入口,参数见 `references/word-operations.md`。用户给了 Word 模板时保留原文件和样式;PDF 可作为内容与版式参考,但不能声称提取文字就保留了原 PDF 版式。
## 选择工作流
| 需求 | 流程 | 详细指南 |
| --- | --- | --- |
| 优化/职位定制/检查问题 | 读取完整简历 → 对照 JD → 改进内容 → 按用户要求回填或给建议 | `references/resume/optimization.md` |
| 填空白模板/从零撰写 | 收集必需资料 → 组织板块 → 回填或创建 | `references/resume/building.md` |
| 不知道写什么/缺少成果 | 对话梳理职责、行动与影响 → STAR 表述 | `references/resume/coaching.md` |
先确认用户已有的目标职位、语言、模板和篇幅要求,不重复询问已经给出的信息。数字和成就必须来自用户资料或可说明口径的估算;不能用范例代替事实,不把“参与”无依据改成“主导”。ATS 仅检查关键词和结构,不承诺固定评分。
## 文件处理
- DOCX:`scripts/inspect_document.py`,需要局部格式时加 `--include-runs`;长文档按游标读完。PDF:`scripts/extract_source.py`,核对多栏顺序与 OCR 状态。
- 回填:把改动整理为 `replace_text` 操作交给 `scripts/edit_document.py`,不要将整段内容塞进第一个文本片段。
- 同一占位符出现多次时,先检查目标段落或表格上下文。无法唯一定位时使用操作指南的最小 XML 编辑流程,不能全局替换所有下划线。
- 无模板新建 DOCX:`scripts/create_document.py`。仅需 PDF、且需要独立排版时,可用内置 Typst 简历模板,见 `references/typst.md`。
- 已生成 DOCX 的 PDF 副本用 `scripts/convert_document.py`,避免维护两套不一致内容。
交付前核对联系方式、日期、职位匹配、页数与原模板样式,执行主入口的结构和逐页视觉检查。

View File

@ -0,0 +1,92 @@
# 构建简历
从零创建简历内容,或在用户提供的模板中填入内容。
## 目录
- [场景判断](#场景判断)
- [读取文件与填写模板](#读取文件与填写模板)
- [从零构建](#从零构建)
- [内容组织原则](#内容组织原则)
---
## 场景判断
| 场景 | 做法 |
|------|------|
| 用户给了简历模板(空白 Word) | 读取结构 → 精确回填 → 校验与渲染 |
| 用户给了旧简历(有内容的 Word/PDF) | 提取内容 → 编辑优化 → Word 回填;PDF 重建时说明版式差异 |
| 用户什么都没给 | 通过对话收集信息 → 生成新文件 |
---
## 读取文件与填写模板
按 `references/resume-writing.md` 的文件流程执行。Word 用 `scripts/inspect_document.py`,PDF 用 `scripts/extract_source.py`;回填使用 `scripts/edit_document.py`,保留原模板。
## 从零构建
用户没有模板也没有旧简历时,通过对话收集信息。
### 收集顺序
不要一次问完。分步来:
**第一步:基础信息**
- 姓名、联系方式
- 当前/最近的工作和职位
- 目标岗位(如果有)
**第二步:工作经历**
- 从最近的工作开始,逐个展开
- 每个职位问 3-5 个核心成就
- 如果用户说不出来,切换到辅导模式(参见 `references/resume/coaching.md`)
**第三步:补充板块**
- 教育背景、技能、项目、证书等
### 对话开场
> "先告诉我你最近的工作——在哪家公司、做什么岗位、干了多久?"
不要从"你想做什么格式的简历"开始。先收集内容,格式最后再说。
### 收集完成后
内容收集完毕后,需要生成文件。此时有两条路:
1. **生成 Word** — 使用 `scripts/create_document.py`,参数见 `references/word-operations.md`。
2. **用户要 PDF 时** — 可从已完成的 DOCX 转换;用户需要独立 Typst 排版时见 `references/typst.md`
---
## 内容组织原则
### 成就描述公式
```
[强动作动词] + [做了什么] + [技术/方法] + [量化成果]
```
**好的例子:**
- 主导搭建实时风控系统,基于 Flink 处理引擎,日均处理 5000 万笔交易,误报率降低 40%
- 设计并落地微服务拆分方案,将单体应用拆为 12 个服务,部署频率从月度提升至日级
**差的例子:**
- 负责后端开发
- 参与了数据库优化
### 篇幅控制
| 经验水平 | 目标页数 | 每个职位描述条数 |
|----------|---------|-----------------|
| 0-3 年 | 1 页 | 3-4 条 |
| 3-8 年 | 1-2 页 | 4-5 条(近期)/ 2-3 条(早期) |
| 8 年以上 | 2 页 | 4-6 条(近期)/ 1-2 条(早期) |
### 摘要公式
`[职级/角色] + [工作年限] + [核心能力] + [行业经验] + [关键价值]`
> 拥有 8 年后端开发经验的高级工程师,专注于分布式系统和微服务架构。在金融科技和电商领域主导过多个核心系统的设计与落地,擅长高并发场景下的性能优化。

View File

@ -0,0 +1,217 @@
# 简历辅导
通过对话帮用户发掘隐藏的成就,扩展单薄的内容。
## 目录
- [何时启动辅导](#何时启动辅导)
- [对话原则](#对话原则)
- [STAR 提问法](#star-提问法)
- [引导问题库](#引导问题库)
- [指标估算技巧](#指标估算技巧)
- [发现到描述的转化](#发现到描述的转化)
---
## 何时启动辅导
以下信号说明用户需要辅导而非直接优化:
- 用户说"我不知道该写什么"、"没什么好写的"、"我的工作没什么特别的"
- 每个职位少于 3 条成就描述
- 描述极度简短和模糊(如"负责后端开发")
- 用户说"帮我想想还有什么可以写的"
辅导也可以在其他流程中局部启动——比如在优化流程中发现某个职位内容特别薄弱,就针对那个职位做一轮短辅导。
---
## 对话原则
### 一次只问一个问题
不要一口气列出 5 个问题。每次只问一个,等回答后再决定下一个。
### 从影响力入手
不要从"你的职责是什么"开始——那只会得到"负责 XXX"。从影响力入手:
> "在这份工作中,你做过的最有价值的事情是什么?"
### 跟着线索走
用户的回答中往往藏着值得深挖的线索。如果用户提到"哦对了我们当时还做了一个内部工具",不要忽略它——追问下去。
### 不要像审讯
语气自然,像同事在茶水间聊天,不要像面试官在拷问。
> "听起来那个项目挺有挑战的,最后效果怎么样?"
### 适时肯定
当用户讲出好素材时,告诉他们:
> "这个很好,这就是简历上应该写的那种成就。让我帮你整理一下措辞。"
---
## STAR 提问法
当用户给出模糊描述时,用 STAR 框架展开。
### 框架
| 维度 | 问什么 | 为了得到什么 |
|------|--------|-------------|
| **S**ituation(情境) | 当时是什么背景?遇到什么问题? | 上下文和挑战 |
| **T**ask(任务) | 你具体负责什么? | 角色和范围 |
| **A**ction(行动) | 你具体做了什么?用了什么方法? | 技术细节和做法 |
| **R**esult(结果) | 最终效果如何?有数据吗? | 量化成果 |
### 示例对话
用户说:
> "我优化过数据库性能"
**S — 追问情境:**
> "当时数据库有什么问题?是查询慢还是扛不住并发?"
用户:"查询很慢,有几个接口响应要 2-3 秒"
**T — 追问任务:**
> "是你一个人负责排查还是团队一起?"
用户:"我主要负责的,DBA 偶尔帮忙看看"
**A — 追问行动:**
> "具体做了哪些优化?加索引、改写 SQL、上缓存?"
用户:"主要是加了复合索引,改了几个 N+1 查询,热点数据加了 Redis 缓存"
**R — 追问结果:**
> "优化后响应时间降到多少了?大概影响多少接口?"
用户:"最慢的接口从 3 秒降到 200 毫秒,总共优化了十几个接口"
**转化为描述:**
> 主导核心接口性能优化,通过复合索引、N+1 查询消除和 Redis 缓存策略,将最慢接口响应时间从 3 秒降至 200 毫秒,覆盖 10+ 个高频接口
---
## 引导问题库
根据用户情况选用合适的问题。不要全问,挑最可能产出好素材的。
### 影响力类(首选)
- "你在这个岗位上做过的最有影响力的事情是什么?"
- "如果你不在这个团队,什么事情会做不成或做得差很多?"
- "有没有什么项目让你特别自豪?"
- "你的上级或同事最认可你的是什么?"
### 规模类
- "你负责的系统大概有多少用户/多大的数据量/多少 QPS?"
- "团队多大?你带过人吗?"
- "这个项目的预算或资源投入大概是多少?"
### 问题解决类
- "你遇到过最棘手的技术难题是什么?怎么解决的?"
- "有没有什么项目一开始别人觉得做不成,但你做成了?"
- "你有没有发现过别人没注意到的问题并解决的?"
### 改进类
- "你有没有改进过什么流程、工具或系统?效果如何?"
- "你有没有自动化过什么手动工作?"
- "你有没有提出过什么建议被团队采纳了?"
### 领导力类
- "你有没有带过新人或做过技术分享?"
- "有没有跨团队协调的经验?"
- "你有没有推动过什么技术选型或架构决策?"
### 按行业定制
**技术岗位额外问:**
- 系统规模、生产环境影响、架构决策、性能指标
**产品/运营岗位额外问:**
- 转化率、用户增长、留存数据、A/B 测试结果
**管理岗位额外问:**
- 团队规模变化、人才培养成果、业务指标达成
---
## 指标估算技巧
用户经常说"我不记得具体数字了"。用以下技巧帮他们估算:
### 量级估算
> "大概是几百、几千还是几万?"
仅使用用户能确认的估算,标明“约”及口径;没有事实依据时采用定性描述。
### 对比估算
> "跟之前相比,大概快了多少倍/省了多少?"
写法:比之前快 3 倍、效率提升约 50%
### 规模描述
> "你负责的系统大概对接了多少个下游服务?覆盖多少业务线?"
写法:覆盖 15+ 微服务、支撑 3 条核心业务线
### 时间描述
> "这个项目用了多长时间完成?比预期快还是慢?"
写法:提前 2 周交付、3 个月内完成从 0 到 1 的搭建
### 实在没有数字时
用定性描述:
- "成为团队标准做法"
- "被公司内多个团队复用"
- "获得了 CTO 在全员会上的表扬"
---
## 发现到描述的转化
辅导过程中发现好素材后,立刻转化为简历描述,让用户确认。
### 转化流程
1. **即时起草**:用户说完一个成就,马上写一条描述
2. **读回确认**:"我把它整理成这样——'主导搭建实时风控系统...'——准确吗?有什么需要调整的?"
3. **调整措辞**:根据用户反馈修改
4. **标记状态**:确认的标记为完成,待调整的标记为草稿
### 转化模板
```
[强动作动词] + [做了什么] + [技术/方法细节] + [量化成果]
```
**输入(用户原话):**
> "我搞了个工具自动生成测试数据,之前大家都是手写 mock 数据,很慢"
**输出(简历描述):**
> 开发自动化测试数据生成工具,替代手动 mock 流程,将测试准备时间从 2 小时缩短至 5 分钟,被团队全员采用
### 常见转化
| 用户原话 | 简历描述 |
|---------|---------|
| "我修了很多 bug" | 主导核心模块的稳定性治理,在 Q3 将线上故障率降低 60% |
| "我带了 3 个实习生" | 指导 3 名实习生完成从入职到独立负责模块的培养,其中 2 人转正 |
| "我搭了一套监控" | 搭建基于 Prometheus + Grafana 的全链路监控体系,覆盖 20+ 核心服务,MTTR 从 30 分钟缩短至 5 分钟 |
| "我重构了一个老系统" | 主导遗留系统重构,将 10 万行 PHP 代码迁移至 Go 微服务架构,性能提升 4 倍,维护成本降低 50% |

View File

@ -0,0 +1,196 @@
# 优化简历
改进已有简历的内容质量,或针对特定职位做定制优化。
## 目录
- [通用内容优化](#通用内容优化)
- [职位定制优化](#职位定制优化)
- [ATS 优化](#ats-优化)
- [常见问题修复](#常见问题修复)
---
## 通用内容优化
不针对特定职位,全面提升简历的表达质量。
### 优化前先通读
先完整阅读用户的简历内容,识别以下常见问题:
- 弱动词("负责"、"参与"、"协助"等)
- 缺少量化指标
- 描述过于笼统或简短
- 专业摘要空洞
然后按优先级逐项改进。
### 动作动词强化
弱动词是简历最常见的问题。替换规则:
| 弱表达 | 替换为 |
|--------|--------|
| 负责... | 主导/管理/搭建... |
| 参与了... | 推动/落地/设计... |
| 协助... | 支撑/赋能/促成... |
| 完成了... | 交付/上线/发布... |
| 做了... | 开发/构建/实现... |
| Worked on... | Built/Developed/Designed... |
| Helped with... | Enabled/Facilitated/Drove... |
| Responsible for... | Owned/Led/Managed... |
| Participated in... | Contributed to/Collaborated on... |
### 指标量化
优先使用用户提供或能从资料推导的量化指标;缺少依据时保留定性描述,不编造数字。
**量化维度:**
| 维度 | 模板 | 示例 |
|------|------|------|
| 时间/速度 | 将 X 从 A 降低到 B | 将部署时间从 2 小时缩短至 15 分钟 |
| 成本/收入 | 节省/创造 $X | 通过架构优化年度节省云成本 200 万 |
| 规模/体量 | 处理/服务 X 个/条/人 | 日均处理 5000 万笔交易 |
| 质量/准确率 | 将 X 从 A% 提升至 B% | 将模型准确率从 78% 提升至 94% |
| 团队/影响 | 带领 X 人 / 覆盖 X 个团队 | 带领 6 人团队完成核心系统重构 |
| 效率 | 自动化 X% / 减少 X 小时 | 自动化 85% 的人工数据校验流程 |
**没有精确数字时的策略:**
- 有用户依据的粗略估算:标注“约”并说明估算口径,不能把示例数字当成用户经历
- 对比基线:比之前快 3 倍
- 描述规模:覆盖 15+ 微服务
- 描述频率:日级、实时、按需
### 描述改写示例
**改写前:**
> 负责后端服务开发
**改写后:**
> 主导搭建订单处理微服务,基于 Go 和 gRPC 实现,支撑日均 800 万笔订单,P99 延迟 < 50ms
**改写前:**
> 参与了机器学习模型的训练和部署
**改写后:**
> 开发基于 XGBoost 的信用评分模型,AUC 达到 0.85,上线后坏账率下降 18%,年度减少损失约 3000 万
### 专业摘要优化
检查清单:
- [ ] 2-4 句,不是一个长段落
- [ ] 包含工作年限和职级
- [ ] 提到 3-5 个核心技能领域
- [ ] 包含行业/领域背景
- [ ] 展示价值而非描述活动
- [ ] 不用"善于沟通"、"学习能力强"等空话
---
## 职位定制优化
用户有目标岗位的职位描述(JD)时,针对性地优化简历。
### 第一步:分析职位描述
从 JD 中提取以下信息:
1. **硬性要求**:工作年限、必备技能、学历要求
2. **优选条件**:加分技能、偏好经验
3. **核心职责**:日常工作内容、主要交付物
4. **关键词**:反复出现的术语(高优先级)、出现在标题/首段的术语
### 第二步:关键词差距分析
对比简历和 JD,分为四类:
| 类别 | 含义 | 处理方式 |
|------|------|---------|
| **关键缺失** | 你有这个技能但简历里没提 | 立即添加到技能板块 + 融入描述 |
| **简单补充** | 匹配优选条件的技能 | 添加到技能板块 |
| **用词不一致** | 同一个技能不同叫法(如 "ML" vs "机器学习") | 统一为 JD 中的用词 |
| **真实缺口** | 要求的技能你确实没有 | 记录但不伪造 |
**绝对不要捏造你没有的技能或经历。** 可以调整措辞、突出相关经验,但不能无中生有。
### 第三步:定制内容
**专业摘要定制:**
- 使用 JD 中的语言风格
- 突出与目标岗位最匹配的经验
- 包含目标岗位名称或类似表述
- 展示 2-3 个 JD 中的核心要求技能
**成就描述定制:**
- 重新排序:最相关的成就放最前面
- 改写措辞:融入 JD 中的关键词
- 强调匹配:突出与目标职责吻合的经验
**技能板块定制:**
- JD 中要求的技能放到最前面
- 补充遗漏的已有技能
- 按 JD 的分类逻辑重新组织
### 定制示例
**JD 要求:** "有分布式系统和微服务架构经验,熟悉 Kubernetes 和 CI/CD"
**原始描述:**
> 开发了后端服务
**定制后:**
> 主导设计基于微服务架构的订单系统,拆分为 8 个独立服务部署在 Kubernetes 集群上,通过 GitLab CI/CD 实现自动化部署,发布频率从月度提升至日级
---
## ATS 优化
ATS(Applicant Tracking System)是很多公司用来筛选简历的系统。确保简历能被正确解析。
### ATS 检查清单
**板块标题:**
- [ ] 使用标准标题:工作经历、教育背景、技能、专业摘要
- [ ] 不要用创意标题如"我的旅程"、"技能树"
**关键词:**
- [ ] 包含 JD 中的原文关键词
- [ ] 缩写和全称都写:如 "自然语言处理(NLP)"
- [ ] 在技能板块和描述中都出现关键词
**格式:**
- [ ] 简洁清晰的结构
- [ ] 联系信息在顶部
- [ ] 统一的日期格式
- [ ] 倒序排列工作经历
### ATS 匹配检查
检查职位关键词、经验、学历和职位名称与 JD 的对应关系。不同招聘系统的算法不同,不使用固定权重或虚构评分。
---
## 常见问题修复
### 简历太长
1. 压缩 10 年以上的旧经历为 1-2 条
2. 每个职位保留最强的 3-5 条描述
3. 删除重复的成就
4. 精简技能板块(去掉过时或基础的技能)
5. 摘要压缩到 2-3 句
### 简历太短
1. 用辅导方法发掘更多成就(参见 `references/resume/coaching.md`)
2. 添加项目板块
3. 扩展技能板块的分类
4. 加入证书、语言能力等可选板块
### 内容与目标不匹配
1. 重新排序描述,匹配的放前面
2. 改写措辞融入目标领域的术语
3. 对于转行者,重点强调可迁移的能力
4. 必要时用辅导方法重新发掘相关经验

View File

@ -0,0 +1,39 @@
# Typst 简历与独立 PDF 排版
Typst 将排版源文件编译为 PDF。Word 交付继续使用 DOCX 创建/编辑流程;已完成 DOCX 的 PDF 副本优先转换,保持内容一致。需要独立 PDF 简历或用户明确要求 Typst 时使用本流程。
## 内置简历模板
调用 `scripts/compile_typst.py`:
```text
--template resume --output '/usr/local/src/word/resume.pdf' --spec-file '/usr/local/src/word/tmp/<任务名>/resume.json'
```
```json
{
"name": "张三",
"contact": ["zhangsan@example.com", "北京"],
"summary": "基于用户真实经历撰写的简介",
"sections": [
{"title": "工作经历", "items": ["公司、职位、时间", "有事实依据的职责与成果"]},
{"title": "教育背景", "items": ["学校、专业、时间"]}
]
}
```
同样可用 `--spec` 传 JSON。数据按普通文本放入模板,不把用户内容拼成 Typst 代码。模板位于 `assets/resume.typ`;默认使用 Inter 与 Noto Sans CJK SC。
## 自定义 Typst 排版
把源文件和本次任务的图片/JSON 等素材放在 `/usr/local/src/word/tmp/<任务名>/`,使用可用的文件写入工具创建 `.typ`,再调用固定入口:
```text
--input '/usr/local/src/word/tmp/<任务名>/resume.typ' --output '/usr/local/src/word/resume.pdf'
```
脚本限定 Typst 项目根目录为源文件所在目录,素材引用必须在该目录内。优先使用内置语法及本地素材,不依赖临时下载的外部模板包。需要特定字体时先按 `references/fonts.md` 检查;字号、页边距和文字长度调整都应根据实际 PDF 页面验证。
默认超时 120 秒,可传 `--timeout`(1–900);`--overwrite` 仅用于本次任务旧产物。脚本检查 PDF 可读且有页面,返回 `page_count`、`warnings` 和 `requires_visual_review`;字体警告不能忽略。最后通过可用的 `pdf` skill 检查文本和逐页渲染,检查全部页面后才交付。
官方说明:https://typst.app/docs/ ,字体设置:https://typst.app/docs/reference/text/text/ 。

View File

@ -0,0 +1,14 @@
# 签证材料与官方模板填写
适用于签证表格、申请说明信(cover letter)、行程单、邀请函、在职证明及其他官方 Word 模板。表格字段和支持材料以目的地、签证类别、申请地点和当前官方清单为准;cover letter 不自动解释为求职信。
## 流程
1. 收集已提供的模板及个人资料。Word 用 `scripts/inspect_document.py`;PDF/TXT/MD/HTML 用 `scripts/extract_source.py`。图片可按需放入临时 Word 后用 `scripts/ocr_document.py` 识别;姓名、护照号、日期等关键字段必须与清晰原件或用户确认核对,低置信度 OCR 不作为事实。
2. 识别模板字段,区分已填、待填、不适用项;字段与日期格式参考 `references/visa/field-mappings.md`,具体规则以当前表格要求为准。
3. 只询问缺失且必要的信息;可从出入境日期计算天数,但要区分入境停留天数与住宿晚数。
4. 用 `scripts/edit_document.py` 精确回填。重复下划线或复选框必须结合字段上下文定位;不能全文替换全部占位符。固定接口不能唯一定位时按 `references/word-operations.md` 的 XML 流程最小修改,保留格式与关系。
5. 无模板时参考 `references/visa/common-visa-docs.md` 的结构,再用 `scripts/create_document.py` 创建;不编写临时 Node/Python 生成脚本。
6. 核对姓名、证件号、日期、行程、金额、雇主信息在所有文件中的一致性,再执行主入口校验和逐页渲染。
姓名、地址、语言、日期顺序、货币、电话国家代码、护照有效期均按对应官方要求;不把“至少六个月”视作所有国家与签证类别的固定规则。没有真实预订或任职依据时保留待填项,不能虚构预订确认、雇主签名或公章。用户资料只用于所请求的文件。

View File

@ -0,0 +1,281 @@
具体要求以目的地、签证类别和当前官方表格为准;示例日期、姓名及金额不能直接用于真实申请。
# Common Visa Document Templates
Standard structures for frequently needed visa support documents. Use these when creating documents from scratch with the shared DOCX creator or as guidance when filling templates.
## Table of Contents
1. [Cover Letter / 签证申请信](#cover-letter)
2. [Travel Itinerary / 行程计划表](#travel-itinerary)
3. [Invitation Letter / 邀请函](#invitation-letter)
4. [Employment Certificate / 在职证明](#employment-certificate)
5. [Financial Support Statement / 资金证明说明](#financial-support-statement)
6. [Hotel Booking Summary / 酒店预订确认](#hotel-booking-summary)
---
## Cover Letter
A cover letter accompanies the visa application, summarizing the trip purpose, itinerary, and the applicant's ties to home country.
### Structure
```
[Applicant's Name]
[Address]
[Date]
To: Visa Section, [Embassy/Consulate of Country]
Subject: Application for [Visa Type] Visa
Dear Sir/Madam,
[Paragraph 1: Self-introduction and visa purpose]
I, [Full Name as in passport], Chinese citizen, passport number [Passport No.],
am applying for a [tourism/business/student] visa to [Country].
[Paragraph 2: Trip details]
I plan to visit [Country] from [Start Date] to [End Date] (approximately [N] days).
During my stay, I will visit [cities/places]. I will be staying at [hotel/host address].
[Paragraph 3: Financial capacity]
I am employed at [Company Name] as [Position] with a monthly income of RMB [Amount].
I have sufficient funds to cover all expenses during my trip, as evidenced by the
attached bank statements.
[Paragraph 4: Ties to home country and return guarantee]
I have strong ties to China including [family/property/employment], and I will
return to China on [Return Date] as scheduled.
[Paragraph 5: Closing]
I have attached all required documents for your review. Thank you for considering
my application.
Sincerely,
[Signature]
[Full Name]
[Phone Number]
[Email]
```
### Key Points
- Language: Use the destination country's official language or English
- Keep it concise — one page maximum
- Include specific dates and concrete details
- For Schengen: mention which countries you will visit and entry/exit points
- For US: mention ties to home country prominently
---
## Travel Itinerary
A day-by-day plan showing the travel route, accommodations, and activities.
### Table Format
| Date | City | Transportation | Hotel/Accommodation | Activities |
|------|------|---------------|---------------------|------------|
| Day 1 (MM/DD) | Arrival City | Flight XX123 | Hotel Name, Address | Arrive, check in |
| Day 2 (MM/DD) | City A | — | Same hotel | Visit [attraction] |
| Day 3 (MM/DD) | City B | Train/Bus | Hotel Name, Address | Transfer, sightseeing |
| ... | ... | ... | ... | ... |
| Day N (MM/DD) | Departure City | Flight XX456 | — | Depart for China |
### Key Points
- Include flight numbers for arrival and departure
- Match hotel names with booking confirmations
- For Schengen multi-country trips, clearly show border crossings
- Dates must match the visa application period
- Activities should be plausible and specific
---
## Invitation Letter
### Business Invitation Letter
```
[Company Letterhead]
[Date]
To: Visa Section, [Embassy/Consulate]
INVITATION LETTER
We hereby invite the following person to visit [Company Name] for business purposes:
Invitee Information:
Full Name: [Name as in passport]
Date of Birth: [DOB]
Passport Number: [Passport No.]
Position: [Job Title]
Company: [Invitee's Company]
Purpose of Visit: [Business meetings / Conference / Training / etc.]
Duration: [Start Date] to [End Date]
Location: [Office Address]
All expenses including [accommodation/transportation/meals] will be covered by
[inviting company / invitee's company].
We guarantee that the invitee will comply with local laws and return to their
home country upon completion of the visit.
Sincerely,
[Authorized Signatory Name]
[Title]
[Company Name]
[Contact Information]
[Company Stamp/Seal]
```
### Personal Invitation Letter
```
[Date]
To: Visa Section, [Embassy/Consulate]
INVITATION LETTER
I, [Host Name], [nationality/residency status], residing at [Address],
invite [Invitee Full Name] to visit me in [Country].
Host Information:
Full Name: [Host Name]
ID/Passport Number: [Number]
Address: [Full Address]
Phone: [Phone Number]
Relationship to Invitee: [Friend / Relative / etc.]
Invitee Information:
Full Name: [Name as in passport]
Date of Birth: [DOB]
Passport Number: [Passport No.]
Visit Details:
Purpose: [Personal visit / Tourism / Family reunion]
Duration: [Start Date] to [End Date]
Accommodation: [Guest will stay at my residence / hotel]
I will be responsible for [accommodation / partial expenses / guidance during stay].
Sincerely,
[Host Signature]
[Host Name]
Attached: Copy of host's ID/residence permit
```
---
## Employment Certificate
Issued by the employer to confirm the applicant's employment status, salary, and approved leave.
### Structure
```
[Company Letterhead with Logo]
[Date]
TO WHOM IT MAY CONCERN
EMPLOYMENT CERTIFICATE
This is to certify that [Full Name], passport number [Passport No.],
has been employed by [Company Full Legal Name] since [Start Date].
Employee Details:
Position: [Job Title]
Department: [Department Name]
Monthly Salary: RMB [Amount] (before tax)
Annual Income: RMB [Amount] (before tax)
[Employee Name] has been approved for leave from [Start Date] to [End Date]
to travel to [Country] for [tourism/business] purposes. His/Her position
will be retained during the absence, and he/she is expected to resume work
on [Return Date].
Company Information:
Company Name: [Full Legal Name]
Address: [Company Address]
Business Registration No.: [Registration Number]
Phone: [Company Phone]
This certificate is issued for the purpose of visa application only.
[Authorized Signatory]
[Name and Title]
[Company Stamp/Seal]
```
### Key Points
- Must be on company letterhead
- Include company stamp/seal (very important for Chinese employers)
- Salary should match tax records
- Clearly state the leave is approved and position is retained
- Language: English (or destination country's language)
---
## Financial Support Statement
A document explaining the applicant's financial situation, often accompanying bank statements.
### Structure
```
FINANCIAL SUPPORT STATEMENT
I, [Full Name], hereby declare the following financial information in support
of my visa application to [Country]:
1. Employment Income
Monthly salary: RMB [Amount] (after tax)
Annual bonus: approximately RMB [Amount]
2. Bank Deposits
[Bank Name] Account ending in [XXXX]: Balance RMB [Amount]
[Bank Name] Account ending in [XXXX]: Balance RMB [Amount]
Total liquid assets: RMB [Amount]
3. Other Assets (if applicable)
Real estate: [Property description, estimated value]
Vehicle: [Vehicle description]
Investments: [Stocks/funds, approximate value]
4. Travel Budget
Estimated total trip cost: RMB [Amount] / EUR [Amount] / USD [Amount]
Including: airfare, accommodation, meals, transportation, activities
I confirm that I have sufficient financial means to cover all expenses
during my stay in [Country] from [Start Date] to [End Date] without
requiring any public assistance.
Date: [Date]
Signature: [Signature]
Full Name: [Name]
```
---
## Hotel Booking Summary
A consolidated list of hotel reservations, useful when the visa application requires accommodation proof.
### Table Format
| Dates | City | Hotel Name | Address | Confirmation No. | Guests |
|-------|------|-----------|---------|-------------------|--------|
| MM/DD - MM/DD | City A | Hotel Name | Full Address | CONF123 | [Names] |
| MM/DD - MM/DD | City B | Hotel Name | Full Address | CONF456 | [Names] |
### Key Points
- Hotel bookings should cover the entire stay
- Names on bookings must match passport names
- Include confirmation numbers
- For Schengen: bookings for all countries visited
- Many hotels offer free cancellation — book refundable options for visa application

View File

@ -0,0 +1,163 @@
具体要求以目的地、签证类别和当前官方表格为准;示例日期、姓名及金额不能直接用于真实申请。
# Visa Field Mappings & Format Conventions
Reference for field names, translations, and format requirements across different visa types.
## Table of Contents
1. [Personal Information Fields](#personal-information-fields)
2. [Travel Information Fields](#travel-information-fields)
3. [Employment Fields](#employment-fields)
4. [Date Formats by Country](#date-formats-by-country)
5. [Name Format Rules](#name-format-rules)
6. [Currency & Amount Formats](#currency-and-amount-formats)
7. [Common Visa Form Checkbox Values](#common-checkbox-values)
---
## Personal Information Fields
| English | 中文 | 日本語 | Example Value | Notes |
|---------|------|--------|---------------|-------|
| Surname / Family Name | 姓 | 姓 | ZHANG | Uppercase, as in passport |
| Given Name / First Name | 名 | 名 | SAN | Uppercase, as in passport |
| Full Name | 姓名 | 氏名 | ZHANG SAN | Surname first for Asian forms |
| Sex / Gender | 性别 | 性別 | Male / Female | Some forms: M/F |
| Date of Birth | 出生日期 | 生年月日 | 1990-01-15 | Format varies by country |
| Place of Birth | 出生地 | 出生地 | BEIJING, CHINA | City + Country |
| Nationality | 国籍 | 国籍 | CHINESE | Or: CHINA / P.R. CHINA |
| Passport Number | 护照号码 | 旅券番号 | E12345678 | Exactly as on passport |
| Passport Issue Date | 护照签发日期 | 旅券発行日 | 2020-03-15 | |
| Passport Expiry Date | 护照有效期 | 旅券有効期限 | 2030-03-14 | Check the destination and visa category’s current passport-validity rule |
| Passport Issuing Authority | 签发机关 | 発行官庁 | MPS Exit-Entry Administration | Or: 公安部出入境管理局 |
| Marital Status | 婚姻状况 | 配偶の有無 | Single/Married/Divorced/Widowed | |
| National ID Number | 身份证号 | — | 110101199001150000 | 18 digits |
| Phone Number | 电话号码 | 電話番号 | +86-138-0000-0000 | Include country code |
| Email | 电子邮箱 | メールアドレス | zhangsan@email.com | |
| Home Address | 家庭住址 | 現住所 | No.1 Changan Street, Beijing | Full address in English |
---
## Travel Information Fields
| English | 中文 | 日本語 | Example Value |
|---------|------|--------|---------------|
| Purpose of Visit | 访问目的 | 渡航目的 | Tourism / Business / Study |
| Intended Date of Arrival | 预计到达日期 | 入国予定日 | 2025-06-15 |
| Intended Date of Departure | 预计离开日期 | 出国予定日 | 2025-06-30 |
| Duration of Stay | 停留天数 | 滞在予定期間 | 15 days |
| Port of Entry | 入境口岸 | 入国港 | Tokyo Narita / Frankfurt |
| Accommodation | 住宿地址 | 滞在先 | Hotel Name + Address |
| Host / Inviting Person | 邀请人 | 招聘者 | Name + Contact |
| Previous Visits | 以往出访记录 | 過去の渡航歴 | Japan (2023), USA (2022) |
| Countries to Visit | 拟访问国家 | 訪問予定国 | France, Germany, Italy |
---
## Employment Fields
| English | 中文 | 日本語 | Example Value |
|---------|------|--------|---------------|
| Employer Name | 工作单位 | 勤務先名称 | ABC Technology Co., Ltd. |
| Employer Address | 单位地址 | 勤務先住所 | No.100 Zhongguancun, Beijing |
| Employer Phone | 单位电话 | 勤務先電話 | +86-10-12345678 |
| Job Title / Position | 职位 | 職業/役職 | Software Engineer / Manager |
| Monthly Salary | 月收入 | 月収 | RMB 30,000 |
| Employment Start Date | 入职日期 | 就職日 | 2018-07-01 |
---
## Date Formats by Country
Different countries require different date formats on visa applications:
| Country/Region | Format | Example (Jan 15, 2025) |
|----------------|--------|------------------------|
| **Schengen / EU** | DD/MM/YYYY | 15/01/2025 |
| **United States** | MM/DD/YYYY | 01/15/2025 |
| **United Kingdom** | DD/MM/YYYY | 15/01/2025 |
| **Japan** | YYYY年MM月DD日 or YYYY/MM/DD | 2025年01月15日 |
| **Canada** | YYYY-MM-DD or DD/MM/YYYY | 2025-01-15 |
| **Australia** | DD/MM/YYYY | 15/01/2025 |
| **China (origin)** | YYYY年MM月DD日 or YYYY-MM-DD | 2025年01月15日 |
| **ISO Standard** | YYYY-MM-DD | 2025-01-15 |
When unsure, check the form itself for format hints (e.g., "DD/MM/YYYY" printed near the field).
---
## Name Format Rules
### Chinese Names on Visa Forms
| Scenario | Format | Example |
|----------|--------|---------|
| Passport MRZ | SURNAME<<GIVEN | ZHANG<<SAN |
| Schengen form | Surname: ZHANG, Given: SAN | Separate fields |
| US DS-160 | Surname: ZHANG, Given names: SAN | |
| Japan form | 姓: ZHANG (張), 名: SAN (三) | Include Chinese chars if requested |
| General rule | Always match passport exactly | Never translate or romanize differently |
### Multi-word Given Names
- ZHANG XIAOMING → Surname: ZHANG, Given: XIAOMING (or XIAO MING if passport shows space)
- Follow the passport machine-readable zone (MRZ) exactly
### Former Names
If the form asks for former/maiden names and applicant has none, write "N/A" or leave blank per form instructions.
---
## Currency and Amount Formats
| Country | Currency | Symbol | Format Example |
|---------|----------|--------|----------------|
| China | Chinese Yuan | ¥ / RMB / CNY | RMB 50,000.00 |
| Eurozone | Euro | € / EUR | EUR 5,000.00 |
| United States | US Dollar | $ / USD | USD 5,000.00 |
| United Kingdom | Pound Sterling | £ / GBP | GBP 4,000.00 |
| Japan | Japanese Yen | ¥ / JPY | JPY 500,000 |
| Canada | Canadian Dollar | C$ / CAD | CAD 5,000.00 |
| Australia | Australian Dollar | A$ / AUD | AUD 5,000.00 |
### Conversion Notes
- Always state the original currency on bank statements
- If the form asks for amounts in local currency, provide both: "RMB 50,000 (approx. EUR 6,400)"
- Use the exchange rate from the application date
---
## Common Checkbox Values
Visa forms often have checkboxes or radio buttons. Common field values:
### Purpose of Visit
- Tourism / Holiday / Sightseeing (旅游/观光)
- Business (商务)
- Visiting Friends/Relatives (探亲访友)
- Study / Education (留学/学习)
- Medical Treatment (医疗)
- Transit (过境)
- Work (工作) — usually requires separate work visa
- Conference / Seminar (会议/研讨会)
### Marital Status
- Single (未婚)
- Married (已婚)
- Divorced (离婚)
- Widowed (丧偶)
- Separated (分居)
- Registered Partnership (注册伴侣)
### Entry Type
- Single Entry (单次入境)
- Double Entry (两次入境)
- Multiple Entry (多次入境)
### Means of Support
- Cash (现金)
- Credit Card (信用卡)
- Traveler's Cheques (旅行支票)
- Pre-paid Accommodation (预付住宿)
- Pre-paid Transport (预付交通)
- Sponsored / Hosted (被资助/被接待)

View File

@ -0,0 +1,341 @@
# Word 文件操作参数
所有脚本通过 `execute_skill_script` 调用,`skill_name="docx"`,`script_path` 从 skill 根目录开始。下文只列参数,不是 shell 命令。文件路径使用容器内绝对路径。
## 下载远程文档
只接受 HTTPS 地址。完整保留 URL 及查询参数传给脚本,但不要在回复、日志摘要或输出文件名中复述敏感参数。
调用 `scripts/download_document.py`:
```text
--url 'https://example.com/report.docx?signature=...' --output '/usr/local/src/word/tmp/<任务名>/source.docx'
```
可选参数:
- `--timeout <1-600>`:连接和读取超时秒数,默认 `60`。
- `--max-bytes <字节数>`:默认且最高 `26214400`(25 MiB),只允许设置更小的限制。
- `--overwrite`:只在目标是本次任务生成的旧缓存时使用。
`output` 扩展名必须是 `.docx`、`.dotx` 或 `.doc`。脚本阻止 HTTPS 重定向降级到 HTTP,流式限制大小,先写同目录临时文件,再原子发布;DOCX/DOTX 会检查 ZIP 路径、成员大小、必要部件和内容类型,并用 `python-docx` 打开。实际 OOXML 格式与 `output` 扩展名不一致时,根据错误中的实际格式更正缓存扩展名,再调用同一脚本。
成功结果包含 `path`、`size_bytes`、`format` 和 `validation`;OOXML 还包含段落、表格和章节数量。后续脚本只使用返回的本地 `path`,不再访问原 URL。
## 下载通用附件
需要下载作为 Word 任务素材的图片、视频、音频、压缩包或其他文件时,调用 `scripts/download_attachment.py`:
```text
--url 'https://example.com/asset.bin?signature=...' --output '/usr/local/src/word/tmp/<任务名>/asset.bin'
```
只接受 HTTPS 地址,`output` 可使用任意附件扩展名。可选参数只有 `--timeout <1-600>`(默认 `60`)和 `--overwrite`。附件上限固定为 25 MiB(26214400 字节),不可调高:脚本先用 HEAD 探测远端声明大小,再检查 GET 响应声明,并在流式接收时持续兜底计数;任一阶段发现超限都会返回 `ok: false` 和明确的“已拒绝下载”错误,且不会发布部分文件。
成功结果包含 `path`、实际 `size_bytes`、`declared_size_bytes`、`size_limit_bytes`、`size_probe` 和 `content_type`。本脚本不校验文件业务格式;远程 Word 源文档仍使用 `download_document.py`。
## 检查文档
调用 `scripts/inspect_document.py`:
```text
--input '/usr/local/src/word/tmp/<任务名>/source.docx'
```
可选参数:
- `--start-paragraph <索引>`、`--max-paragraphs <1-300>`:分段读取正文,索引从 `0` 开始。
- `--start-table <索引>`、`--max-tables <0-50>`、`--max-table-cells <数量>`:限制表格输出。
- `--max-chars <1000-200000>`:限制单次正文字符数。
- `--include-runs`:需要检查局部字体、粗体、斜体或跨 Run 替换问题时使用。
重点检查:
- `tracked_changes.total` 和 `authors`:是否存在修订及修订作者。
- `comments`:批注正文和作者。
- `sections`:纸张、方向、页边距、页眉、页脚。
- `has_images`、`inline_image_count`、`media_part_count`:是否需要进一步读取图片文字;浮动图片可能只计入媒体部件。
- `archive.missing_required_parts`、`duplicate_members`:结构异常。
- `has_more`、`next_paragraph`、`next_table`:继续读取长文档。
## 识别图片中的文字
需要读取图片、截图或扫描页中的文字时调用:
```text
--input '/usr/local/src/word/tmp/<任务名>/source.docx'
```
省略 `--pages` 时,脚本会把 Word 临时转换为 PDF,自动选择包含足够大图片的页面,每次最多处理 4 页。需要识别较小图片或指定页面时传:
```text
--input '/usr/local/src/word/tmp/<任务名>/source.docx' --pages '2,5-6'
```
脚本通过 LibreOffice 和 Poppler 临时渲染页面,使用本地 RapidOCR 识别图片区域;临时 PDF 和 PNG 会自动删除,不联网,也不调用大模型识图。PDF 原生文本层用于过滤正文、页眉、页脚和页码产生的重复 OCR,因此 `pages[].text` 只返回可靠的额外图片文字。
检查:
- `candidate_pages`:自动检测到的图片页;`selection_mode` 表示自动或显式选页。
- `status: good` 且 `usable_for_summary: true`:可以把 `text` 补充到原生文档内容中。
- `status: no_image_text`:图片区域没有识别到额外文字,不是错误。
- `status: sparse` 或 `low_confidence`:不要使用返回文字;根据 `needs_review` 人工核验。
- `filtered_native_line_count` 和 `filtered_outside_image_line_count`:被当作原生文字或图片区域外文字过滤的 OCR 行数。
默认 260 DPI,可用 `--dpi 150-400` 调整。若 `has_more: true`:`next_offset > 0` 时传 `--pages <next_page> --start-offset <next_offset>`;`next_offset = 0` 时把 `remaining_pages` 作为下一次 `--pages`。普通小徽标和面积不足页面约 1.5% 的图片不会进入自动候选,但仍可用 `--pages` 显式识别。
## 创建文档
调用 `scripts/create_document.py`(专利结构可加 `--preset patent`,见 `references/patent-writing.md`):
```text
--output '/usr/local/src/word/result.docx' --spec '<JSON对象>'
```
内容较长时先把 JSON 写到任务临时目录,再传 `--spec-file`。目标是本次任务旧产物且确认可覆盖时才传 `--overwrite`。
文档说明顶层结构:
```json
{
"properties": {
"title": "2026 年度经营报告",
"author": "示例公司",
"subject": "经营分析"
},
"page": {
"size": "A4",
"orientation": "portrait",
"margins": {
"top": 0.85,
"bottom": 0.85,
"left": 0.9,
"right": 0.9
}
},
"default_font": {
"name": "Arial",
"east_asia": "Noto Sans CJK SC",
"size": 10.5,
"line_spacing": 1.15,
"space_after": 6
},
"styles": {
"Title": {"size": 24, "bold": true, "color": "1F4E78"},
"Heading 1": {"size": 16, "bold": true, "color": "1F4E78"}
},
"header": {
"text": "示例公司 · 年度报告",
"alignment": "right"
},
"footer": {
"text": "",
"alignment": "center",
"page_number": true,
"page_number_prefix": "第 ",
"page_number_suffix": " 页"
},
"blocks": [],
"sections": []
}
```
支持的 `blocks[].type`:
| 类型 | 关键字段 |
| --- | --- |
| `paragraph` | `text` 或 `runs`;可选 `style/alignment/space_before/space_after/left_indent/right_indent/first_line_indent` |
| `heading` | `level`(1–9)、`text` 或 `runs` |
| `bullet_list` | `items[]`,可选 `level` |
| `numbered_list` | `items[]`,可选 `level` |
| `table` | `rows[][]`;可选 `column_widths/header_rows/style/header_fill/merges` |
| `image` | `path`;可选 `width_inches/height_inches/alignment/caption` |
| `toc` | 可选 `title`、`levels`,如 `1-3` |
| `horizontal_rule` | 可选 `color/size/style` |
| `page_break` | 无其他必填字段 |
| `section_break` | 可选 `break_type/page size/orientation/margins` |
| `spacer` | 可选 `points` |
带局部格式和链接的段落:
```json
{
"type": "paragraph",
"alignment": "justify",
"runs": [
{"text": "重要:", "bold": true, "color": "C00000"},
{"text": "本报告数据截至 2026-06-30。"},
{
"text": "查看来源",
"hyperlink": "https://example.com/source"
}
]
}
```
表格示例:
```json
{
"type": "table",
"rows": [
["指标", "本期", "同比"],
["收入", "1,250 万元", "12.5%"],
["毛利率", "38.2%", "2.1 个百分点"]
],
"column_widths": [2.2, 1.7, 1.7],
"header_rows": 1,
"header_fill": "1F4E78",
"style": "Table Grid"
}
```
`runs[]` 支持 `bold/italic/underline/strike/color/font/east_asia_font/size/superscript/subscript/style/hyperlink`。不要用换行符模拟段落或分页;使用独立 `paragraph` 或 `page_break`。项目符号和编号必须使用列表块,不要手写 `•` 或数字前缀。
## 编辑文档
调用 `scripts/edit_document.py`:
```text
--input '/usr/local/src/word/tmp/<任务名>/source.docx' --output '/usr/local/src/word/edited.docx' --spec '<JSON对象>'
```
JSON 顶层只有 `operations`。支持:
| `operations[].type` | 关键字段 |
| --- | --- |
| `replace_text` | `find`、`replace`;可选 `scope/match_case/whole_word/count/required` |
| `append_blocks` | `blocks[]`,格式与创建脚本相同 |
| `insert_blocks_after` | `find`、`blocks[]`;可选 `match: exact|contains` |
| `remove_paragraphs` | `text`;可选 `match/count/required` |
| `set_paragraph_style` | `style`,以及 `indexes[]` 或 `contains` |
| `set_properties` | `properties` |
| `set_page` | `page`;`section` 为索引或 `all` |
| `set_header_footer` | 可选 `header`、`footer` |
| `remove_tables` | `indexes[]` |
查找替换会处理 Word 把可见短语拆成多个 `<w:r>` 的情况,并尽量保留首个匹配 Run 的格式。默认范围 `all` 包含正文、表格、页眉和页脚;可指定 `body/tables/headers/footers`。
仅当用户要求记录文本替换时传 `--track-changes --author <作者>`,此时所有操作必须是 `replace_text`。脚本生成真实的删除/插入修订,保留未改动 Run 的格式。含已有修订的输入、跨书签/批注范围、含域/图片等复杂节点或带换行的修订替换会明确拒绝;需要先缩小目标或按用户已明确的意图处理原修订。
输入含现有修订时默认停止:
- 用户希望干净副本:先用 `accept_changes.py`。
- 用户明确要求保留修订:才传 `--allow-existing-revisions`;修改后的内容本身不会自动变成新的修订。
- 新增、删除段落、调整格式等操作暂不支持自动记录修订。不要把这些操作作为“已显示修订”交付。
## 添加批注
调用 `scripts/add_comment.py`:
```text
--input '/usr/local/src/word/tmp/<任务名>/source.docx' --output '/usr/local/src/word/commented.docx' --find '费用上限' --comment '请确认该上限是否含税' --author '审阅人' --initials 'SR'
```
可选:
- `--scope <body|tables|headers|footers|all>`。
- `--occurrence <序号>`:为全文第几个匹配添加批注,默认 `1`。
- `--ignore-case`。
脚本会在必要时拆分 Run,让批注尽量精确锚定到目标文本,而不是整个段落。
## 接受修订
调用 `scripts/accept_changes.py`:
```text
--input '/usr/local/src/word/tmp/<任务名>/redlined.docx' --output '/usr/local/src/word/clean.docx'
```
脚本只执行固定的“接受全部修订”宏,不能运行用户提供的宏。必须检查:
- `revision_markers_before` 大于 `0` 时,`revision_markers_after` 必须为 `0`。
- `status` 必须为 `success`。
- 之后仍要执行结构校验和逐页渲染,特别检查删除段落、编号列表和空白段落。
## 转换
调用 `scripts/convert_document.py`:
```text
--input '/usr/local/src/word/tmp/<任务名>/legacy.doc' --output '/usr/local/src/word/tmp/task/source.docx'
```
支持:
- `.doc` / `.dotx` → `.docx`。
- `.docx` → `.pdf`,用于预览或用户明确要求的 PDF 副本。
- `.docx` → `.md` / `.txt`,默认按接受修订后的视图导出;可传 `--track-changes reject|all`。
不要把转换为 Markdown 的结果当作版式等价副本;表格宽度、浮动图片、页眉页脚、脚注和分页可能简化。
## 高级 OOXML 编辑
只有 `edit_document.py` 无法完成且确实需要编辑底层部件时:
1. 调用 `scripts/unpack_document.py --input <docx> --output-dir <空目录>`。
2. 用可用的文件编辑工具最小化修改 `word/document.xml` 或相关部件;不要重排、格式化或重写无关 XML。
3. 调用 `scripts/pack_document.py --input-dir <目录> --output <新docx>`。
4. 调用 `validate_document.py --check-convert` 和 `render_document.py`。
解包脚本拒绝路径穿越、符号链接和压缩炸弹;打包脚本拒绝缺少 `[Content_Types].xml`、`_rels/.rels` 或 `word/document.xml` 的目录。不要手动调用 `unzip`、`zip`、`find` 或删除命令。
## 校验
调用 `scripts/validate_document.py`:
```text
--input '/usr/local/src/word/result.docx' --check-convert
```
必须满足:
- `status: valid`。
- `issue_count: 0`。
- `archive.missing_required_parts` 和 `duplicate_members` 为空。
- 批注引用完整,内部关系目标存在,所有 XML 可安全解析。
- 修订元素有作者和时间;干净副本的 `tracked_changes.total` 应为 `0`。
- `render_check.success: true` 且页数大于 `0`。
该检查验证结构和可打开性,不替代人工视觉检查。
## 渲染与视觉检查
调用 `scripts/render_document.py`:
```text
--input '/usr/local/src/word/result.docx' --output-dir '/usr/local/src/word/tmp/task/rendered'
```
默认 150 DPI、单次最多 20 页。可传:
- `--start-page`、`--end-page`、`--max-pages`:分批渲染。
- `--dpi <72-300>`:小字、复杂表格或页眉脚注可提高到 180–220。
- `--include-pdf`:同时保留 `document.pdf`。
- `--overwrite`:只覆盖本次任务旧渲染。
若 `has_more: true`,用 `next_page` 继续。通过可用的图片查看工具逐页检查。
## 质量要求
- 默认采用 A4、合理页边距、清晰标题层级;现有文档的规范优先。
- 字体必须实际安装;检查方法和可用字体见 `references/fonts.md`。已有模板优先保留字体,缺失时明确报告;普通新文档可用 Noto/Fandol 与 Arial/Times New Roman。
- 标题必须使用内置 `Heading 1`–`Heading 9` 或具有大纲级别的样式,目录才能收录。
- 表格明确设置列宽、重复表头并禁止跨页拆分关键行;不要使用百分比列宽假设不同客户端一致。
- 表格底色使用明确填充色;不要用表格模拟水平线。
- 页码、目录和交叉引用使用字段,不手写空格或点号对齐。
- 图片保持纵横比,注明来源或说明文字,确认没有超出版心。
- 不用 `\n` 代替独立段落,不用空格填充对齐,不把分页符直接放在正文字符串中。
- 检查孤行孤字、标题落在页尾、表格断裂、图片拉伸、文字裁切、异常空白页、乱码、重叠和页码连续性。
- 最终文档必须内容准确、结构有效、可由 LibreOffice 打开,并通过全部页面视觉检查。
## 分节页眉、页脚和页码
创建说明可带 `sections`,每项使用已存在且不重复的 `index`(从 0 开始),可设置 `header`、`footer` 和 `page_number_start`。先用 `section_break` 块创建分节,再覆盖对应节。全局 `header/footer` 用作默认值,分节设置优先且解除与上一节的链接。
```json
{"sections": [{"index": 1, "header": {"text": "说明书", "alignment": "center"}, "page_number_start": 1}]}
```
页边距、列宽和缩进的单位是英寸,`2.5 cm = 2.5 / 2.54` 英寸。字号与段前段后间距使用 pt。`default_font.line_spacing` 是倍数;段落块的 `line_spacing` 是固定 pt,不能把 `1.5` 当成 1.5 倍行距传入段落块。

View File

@ -720,10 +720,13 @@ def _clear_container(container: Any) -> None:
container._element.remove(table._element) container._element.remove(table._element)
def apply_header_footer(document: Any, raw_header: Any, raw_footer: Any) -> None: def apply_header_footer(
for section in document.sections: document: Any, raw_header: Any, raw_footer: Any, *, sections: Optional[list[Any]] = None
) -> None:
for section in document.sections if sections is None else sections:
if raw_header is not None: if raw_header is not None:
spec = expect_object(raw_header, "header") spec = expect_object(raw_header, "header")
section.header.is_linked_to_previous = False
_clear_container(section.header) _clear_container(section.header)
if "blocks" in spec: if "blocks" in spec:
add_blocks(document, section.header, spec["blocks"]) add_blocks(document, section.header, spec["blocks"])
@ -737,6 +740,7 @@ def apply_header_footer(document: Any, raw_header: Any, raw_footer: Any) -> None
apply_paragraph_format(paragraph, spec) apply_paragraph_format(paragraph, spec)
if raw_footer is not None: if raw_footer is not None:
spec = expect_object(raw_footer, "footer") spec = expect_object(raw_footer, "footer")
section.footer.is_linked_to_previous = False
_clear_container(section.footer) _clear_container(section.footer)
if "blocks" in spec: if "blocks" in spec:
add_blocks(document, section.footer, spec["blocks"]) add_blocks(document, section.footer, spec["blocks"])
@ -756,6 +760,36 @@ def apply_header_footer(document: Any, raw_header: Any, raw_footer: Any) -> None
) )
def apply_section_overrides(document: Any, raw_sections: Any) -> None:
from docx.oxml import OxmlElement
seen: set[int] = set()
for raw in expect_list(raw_sections, "sections"):
spec = expect_object(raw, "sections[]")
unknown = set(spec) - {"index", "header", "footer", "page_number_start"}
if unknown:
raise ValueError(f"sections[] 包含未知字段:{sorted(unknown)}")
index = int(spec["index"])
if index < 0 or index >= len(document.sections) or index in seen:
raise ValueError("sections[].index 必须是存在且不重复的分节索引")
seen.add(index)
section = document.sections[index]
apply_header_footer(document, spec.get("header"), spec.get("footer"), sections=[section])
if "page_number_start" in spec:
start = int(spec["page_number_start"])
if start < 1 or start > 32767:
raise ValueError("page_number_start 必须在 1 到 32767 之间")
page_number = section._sectPr.find(qn("pgNumType"))
if page_number is None:
page_number = OxmlElement("w:pgNumType")
section._sectPr.insert_element_before(
page_number, "w:cols", "w:formProt", "w:vAlign", "w:noEndnote",
"w:titlePg", "w:textDirection", "w:bidi", "w:rtlGutter", "w:docGrid",
"w:printerSettings", "w:sectPrChange",
)
page_number.set(qn("start"), str(start))
def set_update_fields(document: Any) -> None: def set_update_fields(document: Any) -> None:
from docx.oxml import OxmlElement from docx.oxml import OxmlElement

View File

@ -0,0 +1,90 @@
"""Map patent content to the shared, validated DOCX creation pipeline."""
from __future__ import annotations
from typing import Any
from _document_builder import expect_list, expect_object
def _text(value: Any, label: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{label} 必须是非空文本")
return value
def build_patent_spec(data: dict[str, Any]) -> dict[str, Any]:
unknown = set(data) - {"claims", "specification", "abstract", "properties"}
if unknown:
raise ValueError(f"专利说明包含未知字段:{sorted(unknown)}")
blocks: list[dict[str, Any]] = []
sections: list[dict[str, Any]] = []
margin = 2.5 / 2.54
page = {"size": "A4", "margins": dict.fromkeys(("top", "bottom", "left", "right"), margin)}
def start_section(title: str) -> None:
if sections:
blocks.append({"type": "section_break", **page})
sections.append({
"index": len(sections),
"header": {"text": title, "alignment": "center"},
"footer": {"text": "", "alignment": "center", "page_number": True,
"page_number_prefix": "第 ", "page_number_suffix": " 页"},
"page_number_start": 1,
})
blocks.append({"type": "heading", "level": 1, "text": title, "alignment": "center"})
claims = expect_list(data.get("claims", []), "claims")
if claims:
start_section("权利要求书")
items = []
for index, raw in enumerate(claims, 1):
claim = expect_object(raw, "claims[]")
if set(claim) - {"number", "text", "dependent"}:
raise ValueError("claims[] 仅支持 number、text、dependent")
if claim.get("number") != index:
raise ValueError("claims[].number 必须从 1 开始连续编号")
items.append({"text": _text(claim.get("text"), "claims[].text"),
"alignment": "justify", "keep_together": True})
blocks.append({"type": "numbered_list", "items": items})
specification = expect_object(data.get("specification", {}), "specification")
titles = [("field", "技术领域"), ("background", "背景技术"), ("summary", "发明内容"),
("drawings", "附图说明"), ("detailed", "具体实施方式")]
unknown = set(specification) - {key for key, _ in titles}
if unknown:
raise ValueError(f"specification 包含未知字段:{sorted(unknown)}")
if specification:
start_section("说明书")
for key, title in titles:
if key not in specification:
continue
value = specification[key]
paragraphs = value if isinstance(value, list) else [value]
if not paragraphs:
raise ValueError(f"specification.{key} 不能为空")
blocks.append({"type": "heading", "level": 2, "text": title})
blocks.extend({"type": "paragraph", "text": _text(text, f"specification.{key}"),
"alignment": "justify", "first_line_indent": 24 / 72}
for text in paragraphs)
if "abstract" in data:
abstract = _text(data["abstract"], "abstract")
start_section("摘要")
blocks.append({"type": "paragraph", "text": abstract, "alignment": "justify",
"first_line_indent": 24 / 72})
if not blocks:
raise ValueError("专利说明至少需要非空 claims、specification 或 abstract")
return {
"properties": expect_object(data.get("properties", {}), "properties"),
"page": page,
"default_font": {"name": "Liberation Serif", "east_asia": "Noto Serif CJK SC",
"size": 12, "line_spacing": 1.5},
"styles": {
"Heading 1": {"font": "Liberation Sans", "east_asia_font": "Noto Sans CJK SC",
"size": 16, "bold": True, "color": "000000"},
"Heading 2": {"font": "Liberation Sans", "east_asia_font": "Noto Sans CJK SC",
"size": 14, "bold": True, "color": "000000"},
},
"blocks": blocks,
"sections": sections,
}

View File

@ -0,0 +1,97 @@
"""Tracked replacements for contiguous plain-text runs, retaining each run's style."""
from __future__ import annotations
import re
import zipfile
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from _docx_common import W_NS, parse_xml_bytes, qn
class TrackedReplacement:
def __init__(self, source: Path, author: str):
self.author = author.strip()
if not self.author:
raise ValueError("修订作者不能为空")
self.date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
self.next_id = 0
self.count = 0
with zipfile.ZipFile(source) as archive:
for name in archive.namelist():
if name.startswith("word/") and name.endswith(".xml"):
root = parse_xml_bytes(archive.read(name), label=name)
for element in root.iter():
value = element.get(qn("id"), "")
if value.isdecimal():
self.next_id = max(self.next_id, int(value) + 1)
def _revision(self, name: str) -> Any:
from docx.oxml import OxmlElement
element = OxmlElement(f"w:{name}")
element.set(qn("id"), str(self.next_id))
element.set(qn("author"), self.author)
element.set(qn("date"), self.date)
self.next_id += 1
return element
@staticmethod
def _fragment(run: Any, paragraph: Any, text: str, *, deleted: bool = False) -> Any:
from docx.text.run import Run
element = deepcopy(run._r)
Run(element, paragraph).text = text
if deleted:
for node in element.findall(qn("t")):
node.tag = qn("delText")
return element
def replace(self, paragraph: Any, matches: list[re.Match[str]], replacement: str) -> None:
if any(char in replacement for char in "\n\r\t"):
raise ValueError("修订替换仅支持段落内文本;换行或制表符需要单独编辑")
# Edits run backwards. Earlier offsets remain valid in the remaining plain runs.
for match in reversed(matches):
start, end = match.span()
runs = paragraph.runs
spans = []
offset = 0
for run in runs:
spans.append((offset, offset + len(run.text)))
offset += len(run.text)
first = next(i for i, (_, b) in enumerate(spans) if start < b)
last = next(i for i, (a, b) in enumerate(spans) if a < end <= b)
selected = runs[first:last + 1]
children = list(paragraph._p)
left = children.index(selected[0]._r)
right = children.index(selected[-1]._r)
if children[left:right + 1] != [run._r for run in selected]:
raise ValueError("修订目标跨越书签、批注或其他非文本节点,请缩小替换范围")
if any(child.tag not in {qn("rPr"), qn("t")} for run in selected for child in run._r):
raise ValueError("修订目标包含域、图片、换行或其他复杂节点,请使用精确批注或缩小范围")
prefix = selected[0].text[:start - spans[first][0]]
suffix = selected[-1].text[end - spans[last][0]:]
nodes = []
if prefix:
nodes.append(self._fragment(selected[0], paragraph, prefix))
deletion = self._revision("del")
for i in range(first, last + 1):
a, b = spans[i]
text = runs[i].text[max(start - a, 0):min(end, b) - a]
if text:
deletion.append(self._fragment(runs[i], paragraph, text, deleted=True))
nodes.append(deletion)
if replacement:
insertion = self._revision("ins")
insertion.append(self._fragment(selected[0], paragraph, replacement))
nodes.append(insertion)
if suffix:
nodes.append(self._fragment(selected[-1], paragraph, suffix))
for run in selected:
paragraph._p.remove(run._r)
for index, node in enumerate(nodes, left):
paragraph._p.insert(index, node)
self.count += 1

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Compile task-local Typst source to a checked PDF via a fixed command."""
from __future__ import annotations
import os
import json
import shutil
import tempfile
from contextlib import ExitStack
from pathlib import Path
from _docx_common import (SkillArgumentParser, WORD_OUTPUT_ROOT, find_program, input_file, load_json_argument,
output_file, publish_file, run_cli, run_program)
def resume_source(spec, directory):
if set(spec) - {"name", "contact", "summary", "sections"}:
raise ValueError("简历仅支持 name、contact、summary、sections")
if not isinstance(spec.get("name"), str) or not spec["name"].strip():
raise ValueError("name 必须是非空文本")
spec.setdefault("contact", [])
spec.setdefault("summary", "")
if not isinstance(spec["contact"], list) or not all(isinstance(x, str) for x in spec["contact"]):
raise ValueError("contact 必须是文本数组")
if not isinstance(spec["summary"], str) or not isinstance(spec.get("sections"), list):
raise ValueError("summary 必须是文本,sections 必须是数组")
for section in spec["sections"]:
if (not isinstance(section, dict) or set(section) != {"title", "items"}
or not isinstance(section["title"], str) or not isinstance(section["items"], list)
or not all(isinstance(item, str) for item in section["items"])):
raise ValueError("sections[] 必须包含文本 title 和文本数组 items")
(directory / "resume.json").write_text(json.dumps(spec, ensure_ascii=False), encoding="utf-8")
source = directory / "resume.typ"
shutil.copy2(Path(__file__).resolve().parent.parent / "assets/resume.typ", source)
return source
def main():
from pypdf import PdfReader
parser = SkillArgumentParser(description="把任务目录中的 Typst 文档编译为 PDF")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--input")
group.add_argument("--template", choices=["resume"])
parser.add_argument("--spec")
parser.add_argument("--spec-file")
parser.add_argument("--output", required=True)
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args()
with ExitStack() as stack:
if args.template:
task_root = WORD_OUTPUT_ROOT / "tmp"
task_root.mkdir(parents=True, exist_ok=True)
task_dir = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix="typst-resume-", dir=task_root)))
source = resume_source(load_json_argument(args.spec, args.spec_file, label="简历说明"), task_dir)
else:
if args.spec is not None or args.spec_file is not None:
raise ValueError("spec/spec-file 仅用于 --template resume")
source = input_file(args.input, {".typ"})
try:
source.relative_to((WORD_OUTPUT_ROOT / "tmp").resolve())
except ValueError as exc:
raise ValueError("Typst 源文件及其素材必须放在 /usr/local/src/word/tmp/<任务名>/") from exc
if source.stat().st_size > 2 * 1024 * 1024:
raise ValueError("Typst 源文件不能超过 2 MiB")
destination = output_file(args.output, {".pdf"}, overwrite=args.overwrite)
descriptor, name = tempfile.mkstemp(prefix=".typst-", suffix=".pdf", dir=destination.parent)
os.close(descriptor)
temporary = Path(name)
try:
result = run_program([find_program("typst"), "compile", "--root", str(source.parent),
"--diagnostic-format", "short", str(source), str(temporary)],
timeout=args.timeout, cwd=source.parent)
reader = PdfReader(str(temporary))
page_count = len(reader.pages)
if reader.is_encrypted or page_count < 1:
raise ValueError("Typst 未生成可读取的 PDF 页面")
publish_file(temporary, destination, overwrite=args.overwrite)
finally:
temporary.unlink(missing_ok=True)
return {"path": str(destination), "source": args.input, "template": args.template, "page_count": page_count,
"warnings": result.stderr[-8000:], "requires_visual_review": True}
if __name__ == "__main__":
raise SystemExit(run_cli(main))

View File

@ -15,6 +15,7 @@ from _document_builder import (
apply_header_footer, apply_header_footer,
apply_named_styles, apply_named_styles,
apply_page_settings, apply_page_settings,
apply_section_overrides,
expect_list, expect_list,
set_update_fields, set_update_fields,
) )
@ -35,6 +36,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--output", required=True) parser.add_argument("--output", required=True)
parser.add_argument("--spec", help="内联 JSON 文档说明") parser.add_argument("--spec", help="内联 JSON 文档说明")
parser.add_argument("--spec-file", help="JSON 文档说明文件") parser.add_argument("--spec-file", help="JSON 文档说明文件")
parser.add_argument("--preset", choices=["patent"], help="将专利内容转换为标准文档说明")
parser.add_argument("--overwrite", action="store_true") parser.add_argument("--overwrite", action="store_true")
return parser return parser
@ -49,6 +51,10 @@ def main() -> dict[str, Any]:
overwrite=args.overwrite, overwrite=args.overwrite,
) )
spec = load_json_argument(args.spec, args.spec_file, label="文档说明") spec = load_json_argument(args.spec, args.spec_file, label="文档说明")
if args.preset == "patent":
from _patent_spec import build_patent_spec
spec = build_patent_spec(spec)
allowed = { allowed = {
"properties", "properties",
"page", "page",
@ -57,6 +63,7 @@ def main() -> dict[str, Any]:
"header", "header",
"footer", "footer",
"blocks", "blocks",
"sections",
} }
unknown = set(spec) - allowed unknown = set(spec) - allowed
if unknown: if unknown:
@ -78,6 +85,7 @@ def main() -> dict[str, Any]:
spec.get("header"), spec.get("header"),
spec.get("footer"), spec.get("footer"),
) )
apply_section_overrides(document, spec.get("sections", []))
set_update_fields(document) set_update_fields(document)
descriptor, temp_name = tempfile.mkstemp( descriptor, temp_name = tempfile.mkstemp(

View File

@ -122,6 +122,7 @@ def _replace_in_paragraph(
match_case: bool, match_case: bool,
whole_word: bool, whole_word: bool,
remaining: int, remaining: int,
tracker: Any = None,
) -> int: ) -> int:
runs = paragraph.runs runs = paragraph.runs
if not runs: if not runs:
@ -138,6 +139,9 @@ def _replace_in_paragraph(
found = found[:remaining] found = found[:remaining]
if not found: if not found:
return 0 return 0
if tracker is not None:
tracker.replace(paragraph, found, replacement)
return len(found)
spans: list[tuple[int, int]] = [] spans: list[tuple[int, int]] = []
offset = 0 offset = 0
@ -176,7 +180,7 @@ def _replace_in_paragraph(
return len(found) return len(found)
def _op_replace_text(document: Any, op: dict[str, Any]) -> dict[str, Any]: def _op_replace_text(document: Any, op: dict[str, Any], tracker: Any = None) -> dict[str, Any]:
find = str(op.get("find", "")) find = str(op.get("find", ""))
if not find: if not find:
raise ValueError("replace_text.find 不能为空") raise ValueError("replace_text.find 不能为空")
@ -199,6 +203,7 @@ def _op_replace_text(document: Any, op: dict[str, Any]) -> dict[str, Any]:
match_case=bool(op.get("match_case", True)), match_case=bool(op.get("match_case", True)),
whole_word=bool(op.get("whole_word", False)), whole_word=bool(op.get("whole_word", False)),
remaining=remaining, remaining=remaining,
tracker=tracker,
) )
if count: if count:
total += count total += count
@ -373,6 +378,8 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--spec", help="内联 JSON 编辑说明") parser.add_argument("--spec", help="内联 JSON 编辑说明")
parser.add_argument("--spec-file", help="JSON 编辑说明文件") parser.add_argument("--spec-file", help="JSON 编辑说明文件")
parser.add_argument("--overwrite", action="store_true") parser.add_argument("--overwrite", action="store_true")
parser.add_argument("--track-changes", action="store_true", help="将 replace_text 操作记录为修订")
parser.add_argument("--author", default="assistant", help="新修订的作者")
parser.add_argument( parser.add_argument(
"--allow-existing-revisions", "--allow-existing-revisions",
action="store_true", action="store_true",
@ -404,6 +411,10 @@ def main() -> dict[str, Any]:
raise ValueError(f"operations 不能超过 {MAX_OPERATIONS} 项") raise ValueError(f"operations 不能超过 {MAX_OPERATIONS} 项")
has_revisions = _has_revisions(source) has_revisions = _has_revisions(source)
if args.track_changes and has_revisions:
raise ValueError("修订替换需要无现有修订的副本;请先根据用户要求处理原有修订")
if args.track_changes and any(expect_object(op, "operations[]").get("type") != "replace_text" for op in operations):
raise ValueError("--track-changes 仅支持 replace_text;其他操作不能伪装为修订")
if has_revisions and not args.allow_existing_revisions: if has_revisions and not args.allow_existing_revisions:
raise ValueError( raise ValueError(
"输入文档含修订记录。为避免把未接受修订静默改坏," "输入文档含修订记录。为避免把未接受修订静默改坏,"
@ -412,10 +423,16 @@ def main() -> dict[str, Any]:
) )
document = Document(str(source)) document = Document(str(source))
tracker = None
if args.track_changes:
from _tracked_replace import TrackedReplacement
tracker = TrackedReplacement(source, args.author)
results: list[dict[str, Any]] = [] results: list[dict[str, Any]] = []
for index, operation in enumerate(operations): for index, operation in enumerate(operations):
try: try:
results.append(_apply_operation(document, operation)) results.append(_op_replace_text(document, operation, tracker) if tracker is not None
else _apply_operation(document, operation))
except Exception as exc: except Exception as exc:
raise ValueError(f"第 {index + 1} 个操作失败:{exc}") from exc raise ValueError(f"第 {index + 1} 个操作失败:{exc}") from exc
set_update_fields(document) set_update_fields(document)
@ -450,6 +467,7 @@ def main() -> dict[str, Any]:
"table_count": len(document.tables), "table_count": len(document.tables),
"section_count": len(document.sections), "section_count": len(document.sections),
"source_had_revisions": has_revisions, "source_had_revisions": has_revisions,
"tracked_replacement_count": tracker.count if tracker is not None else 0,
"archive": archive, "archive": archive,
"requires_visual_review": True, "requires_visual_review": True,
} }

View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Read supporting PDF/text/HTML material through a bounded JSON interface."""
from __future__ import annotations
from html.parser import HTMLParser
from pathlib import Path
from typing import Any
from _docx_common import SkillArgumentParser, input_file, run_cli
class HTMLText(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.parts: list[str] = []
self.hidden = 0
def handle_starttag(self, tag, attrs):
if tag in {"script", "style"}:
self.hidden += 1
elif not self.hidden and tag in {"p", "div", "br", "li", "tr", "h1", "h2", "h3"}:
self.parts.append("\n")
def handle_endtag(self, tag):
if tag in {"script", "style"} and self.hidden:
self.hidden -= 1
elif not self.hidden and tag in {"p", "div", "li", "tr", "h1", "h2", "h3"}:
self.parts.append("\n")
def handle_data(self, data):
if not self.hidden:
self.parts.append(data)
def read_text(source: Path) -> str:
raw = source.read_bytes()
encodings = ["utf-16"] if raw.startswith((b"\xff\xfe", b"\xfe\xff")) else ["utf-8-sig", "gb18030", "big5"]
for encoding in encodings:
try:
text = raw.decode(encoding)
break
except UnicodeDecodeError:
continue
else:
raise ValueError("无法可靠识别文本编码,请提供 UTF-8 文本")
if source.suffix.lower() in {".html", ".htm"}:
parser = HTMLText()
parser.feed(text)
text = "\n".join(line.strip() for line in "".join(parser.parts).splitlines() if line.strip())
return text
def read_pdf_page(page: Any, columns: str) -> tuple[str, int, int]:
tables = page.find_tables()
boxes = [table.bbox for table in tables]
def outside_tables(obj):
x = (obj.get("x0", 0) + obj.get("x1", 0)) / 2
y = (obj.get("top", 0) + obj.get("bottom", 0)) / 2
return not any(a <= x <= c and b <= y <= d for a, b, c, d in boxes)
body = page.filter(outside_tables)
midpoint = (page.bbox[0] + page.bbox[2]) / 2
centers = [(char["x0"] + char["x1"]) / 2 for char in body.chars if char.get("text", "").strip()]
left_count = sum(x < midpoint for x in centers)
right_count = len(centers) - left_count
# Require a genuine central gutter. A balanced full-width paragraph is not two columns.
gutter = page.width * 0.02
detected = (min(left_count, right_count) >= 8 and not any(abs(x - midpoint) < gutter for x in centers))
count = (2 if detected else 1) if columns == "auto" else int(columns)
if count == 2:
# Partition by character centre: never discard text in a cropped central gap.
parts = [body.filter(lambda obj: (obj.get("x0", 0) + obj.get("x1", 0)) / 2 < midpoint).extract_text() or "",
body.filter(lambda obj: (obj.get("x0", 0) + obj.get("x1", 0)) / 2 >= midpoint).extract_text() or ""]
else:
parts = [body.extract_text() or ""]
for index, table in enumerate(tables, 1):
rows = [" | ".join(str(cell or "").replace("\n", " ") for cell in row) for row in table.extract()]
parts.append(f"[表格 {index}]\n" + "\n".join(rows))
return "\n\n".join(part.strip() for part in parts if part.strip()), count, len(tables)
def main() -> dict[str, Any]:
parser = SkillArgumentParser(description="提取 Word 任务的 PDF、TXT、Markdown 或 HTML 资料")
parser.add_argument("--input", required=True)
parser.add_argument("--page", type=int, default=1)
parser.add_argument("--start-offset", type=int, default=0)
parser.add_argument("--max-chars", type=int, default=12000)
parser.add_argument("--columns", choices=["auto", "1", "2"], default="auto")
args = parser.parse_args()
source = input_file(args.input, {".pdf", ".txt", ".md", ".html", ".htm"})
if source.stat().st_size > 25 * 1024 * 1024:
raise ValueError("输入资料不能超过 25 MiB")
if not 256 <= args.max_chars <= 12000 or args.start_offset < 0 or args.page < 1:
raise ValueError("max-chars 必须为 256–12000,start-offset 不小于 0,page 从 1 开始")
page_count, columns, table_count, has_images = 1, 1, 0, False
if source.suffix.lower() == ".pdf":
import pdfplumber
from pypdf import PdfReader
if PdfReader(str(source)).is_encrypted:
raise ValueError("请提供已解密的 PDF 副本")
with pdfplumber.open(source) as pdf:
page_count = len(pdf.pages)
if args.page > page_count:
raise ValueError("page 超出 PDF 页数")
page = pdf.pages[args.page - 1]
text, columns, table_count = read_pdf_page(page, args.columns)
has_images = bool(page.images)
else:
if args.page != 1:
raise ValueError("文本资料只有一个逻辑页,请使用 start-offset 续读")
text = read_text(source)
if args.start_offset > len(text):
raise ValueError("start-offset 超出当前页文本长度")
end = min(args.start_offset + args.max_chars, len(text))
more_text = end < len(text)
usable = bool(text.strip()) and text.count("\ufffd") / max(1, len(text)) < 0.02
return {
"path": str(source), "page": args.page, "page_count": page_count,
"text": text[args.start_offset:end], "start_offset": args.start_offset,
"columns": columns, "table_count": table_count, "has_images": has_images,
"usable_for_summary": usable, "needs_ocr": source.suffix.lower() == ".pdf" and not usable,
"has_more": more_text or args.page < page_count,
"next_page": args.page if more_text else (args.page + 1 if args.page < page_count else None),
"next_offset": end if more_text else 0,
}
if __name__ == "__main__":
raise SystemExit(run_cli(main))

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Report installed document tools and exact font-family availability."""
from __future__ import annotations
import importlib.metadata
import shutil
from _docx_common import SkillArgumentParser, run_cli, run_program
def main():
parser = SkillArgumentParser(description="检查文档依赖和指定字体是否实际安装")
parser.add_argument("--font", action="append", default=[], help="需精确检查的字体家族,可重复传入")
args = parser.parse_args()
tools = {name: shutil.which(name) for name in ("soffice", "pandoc", "pdftoppm", "pdftotext", "typst", "fc-list")}
packages = {}
for name in ("python-docx", "lxml", "defusedxml", "Pillow", "pdfplumber", "pypdf", "rapidocr", "onnxruntime"):
try:
packages[name] = importlib.metadata.version(name)
except importlib.metadata.PackageNotFoundError:
packages[name] = None
fonts = set()
if tools["fc-list"]:
result = run_program([tools["fc-list"], "--format", "%{family}\n"], timeout=30)
fonts = {name.strip() for line in result.stdout.splitlines() for name in line.split(",") if name.strip()}
lookup = {name.casefold() for name in fonts}
requested = {name: name.strip().casefold() in lookup for name in args.font}
return {
"tools": tools, "packages": packages,
"missing_tools": [name for name, path in tools.items() if not path],
"missing_packages": [name for name, version in packages.items() if not version],
"requested_fonts": requested,
"missing_fonts": [name for name, installed in requested.items() if not installed],
"font_inventory_available": bool(tools["fc-list"]),
"font_families": sorted(fonts),
}
if __name__ == "__main__":
raise SystemExit(run_cli(main))

View File

@ -0,0 +1,230 @@
"""Regression checks for the merged document workflows; no Docker or network required."""
from __future__ import annotations
import importlib
import json
import shutil
import sys
import tempfile
import unittest
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch
sys.dont_write_bytecode = True
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from reportlab.pdfgen.canvas import Canvas
import _docx_common as common
import compile_typst
class DocumentWorkflows(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="docx-merge-test-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name).resolve()
self.addCleanup(patch.stopall)
patch.object(common, "WORD_OUTPUT_ROOT", self.root).start()
patch.object(compile_typst, "WORD_OUTPUT_ROOT", self.root).start()
self.source = self.root / "source.docx"
def call(self, script, *args):
with patch.object(sys, "argv", [script, *map(str, args)]):
return importlib.import_module(script).main()
def fixture(self):
doc = Document()
p = doc.add_paragraph()
p.add_run("BEFORE ")
p.add_run("HEL").bold = True
p.add_run("LO")
p.add_run(" AFTER").italic = True
doc.save(self.source)
return doc
def edit(self, operations, *args):
output = self.root / "edited.docx"
result = self.call("edit_document", "--input", self.source, "--output", output,
"--spec", json.dumps({"operations": operations}), *args)
return result, Document(output)
def test_cross_run_preserves_unmodified_styles_and_escapes_text(self):
self.fixture()
result, doc = self.edit([{"type": "replace_text", "find": "HELLO", "replace": "A & <B>"}])
p = doc.paragraphs[0]
self.assertEqual(p.text, "BEFORE A & <B> AFTER")
self.assertTrue(next(r for r in p.runs if "A &" in r.text).bold)
self.assertTrue(next(r for r in p.runs if " AFTER" in r.text).italic)
validation = self.call("validate_document", "--input", result["path"])
self.assertEqual(validation["status"], "valid")
def test_single_and_split_matches_are_both_replaced(self):
doc = self.fixture()
doc.add_paragraph("HELLO HELLO")
doc.save(self.source)
result, doc = self.edit([{"type": "replace_text", "find": "HELLO", "replace": "NEW"}])
self.assertEqual(result["operation_results"][0]["replacement_count"], 3)
self.assertNotIn("HELLO", "".join(p.text for p in doc.paragraphs))
def test_tracked_replacement_has_correct_nesting_and_accepted_text(self):
self.fixture()
result, doc = self.edit([{"type": "replace_text", "find": "HELLO", "replace": "A & <B>"}],
"--track-changes", "--author", "审阅 & A")
self.assertEqual(result["tracked_replacement_count"], 1)
self.assertFalse(doc.element.xpath(".//w:r/w:ins | .//w:r/w:del"))
self.assertEqual(len(doc.element.xpath(".//w:p/w:ins")), 1)
self.assertEqual(len(doc.element.xpath(".//w:p/w:del")), 1)
for node in doc.element.xpath(".//w:ins | .//w:del"):
self.assertEqual(node.get(qn("w:author")), "审阅 & A")
accepted = deepcopy(doc.element)
for node in accepted.xpath(".//w:del"):
node.getparent().remove(node)
for node in accepted.xpath(".//w:ins"):
parent = node.getparent()
index = parent.index(node)
for child in list(node):
parent.insert(index, child)
index += 1
parent.remove(node)
self.assertEqual("".join(n.text or "" for n in accepted.xpath(".//w:t")), "BEFORE A & <B> AFTER")
self.assertTrue(next(r for r in doc.paragraphs[0].runs if " AFTER" in r.text).italic)
self.assertEqual(self.call("validate_document", "--input", result["path"])["status"], "valid")
def test_multiple_tracked_matches_in_one_run(self):
doc = Document()
doc.add_paragraph("old old old")
doc.save(self.source)
result, doc = self.edit([{"type": "replace_text", "find": "old", "replace": "new"}], "--track-changes")
self.assertEqual(result["tracked_replacement_count"], 3)
self.assertEqual(len(doc.element.xpath(".//w:p/w:ins")), 3)
ids = [node.get(qn("w:id")) for node in doc.element.xpath(".//w:ins | .//w:del")]
self.assertEqual(len(ids), len(set(ids)))
def test_tracked_edits_reject_structural_operations(self):
self.fixture()
with self.assertRaisesRegex(ValueError, "仅支持 replace_text"):
self.edit([{"type": "append_blocks", "blocks": [{"type": "paragraph", "text": "x"}]}], "--track-changes")
self.assertFalse((self.root / "edited.docx").exists())
def test_tracked_edits_reject_bookmark_crossing(self):
doc = self.fixture()
mark = OxmlElement("w:bookmarkStart")
mark.set(qn("w:id"), "7")
mark.set(qn("w:name"), "target")
doc.paragraphs[0]._p.insert(2, mark)
doc.save(self.source)
with self.assertRaisesRegex(ValueError, "书签"):
self.edit([{"type": "replace_text", "find": "HELLO", "replace": "new"}], "--track-changes")
self.assertFalse((self.root / "edited.docx").exists())
def test_patent_preset_has_independent_headers_and_page_number_restarts(self):
output = self.root / "patent.docx"
spec = {"claims": [{"number": 1, "text": "一种方法 & 装置", "dependent": False}],
"specification": {"field": "领域", "detailed": ["实现 <描述>"]}, "abstract": "摘要"}
result = self.call("create_document", "--preset", "patent", "--output", output, "--spec", json.dumps(spec))
doc = Document(output)
self.assertEqual(len(doc.sections), 3)
self.assertEqual([s.header.paragraphs[0].text for s in doc.sections], ["权利要求书", "说明书", "摘要"])
for section in doc.sections:
self.assertEqual(section._sectPr.find(qn("w:pgNumType")).get(qn("w:start")), "1")
self.assertFalse(section.header.is_linked_to_previous)
self.assertFalse(section.footer.is_linked_to_previous)
self.assertTrue(any(p.style.name == "Heading 2" for p in doc.paragraphs))
self.assertEqual(self.call("validate_document", "--input", result["path"])["status"], "valid")
def test_invalid_patent_numbering_does_not_publish_a_file(self):
output = self.root / "patent.docx"
with self.assertRaisesRegex(ValueError, "连续编号"):
self.call("create_document", "--preset", "patent", "--output", output,
"--spec", json.dumps({"claims": [{"number": 2, "text": "wrong"}]}))
self.assertFalse(output.exists())
def test_generic_document_api_still_supports_tables_and_sections(self):
output = self.root / "generic.docx"
spec = {"header": {"text": "Default"}, "blocks": [
{"type": "heading", "text": "Report"},
{"type": "table", "rows": [["Key", "Value"], ["A", "B"]]},
{"type": "section_break"}, {"type": "paragraph", "text": "Second"}],
"sections": [{"index": 1, "header": {"text": "Second header"}}]}
self.call("create_document", "--output", output, "--spec", json.dumps(spec))
doc = Document(output)
self.assertEqual(doc.tables[0].cell(1, 1).text, "B")
self.assertEqual([s.header.paragraphs[0].text for s in doc.sections], ["Default", "Second header"])
def test_output_root_is_enforced(self):
with self.assertRaisesRegex(ValueError, "必须输出到"):
common.output_file(str(self.root.parent / "outside.docx"))
def test_html_extraction_decodes_entities_and_omits_scripts(self):
path = self.root / "input.html"
path.write_text('<p>A &amp; B</p><script>secret()</script><style>body{}</style><p>正文</p>')
result = self.call("extract_source", "--input", path)
self.assertEqual(result["text"], "A & B\n正文")
self.assertTrue(result["usable_for_summary"])
def test_text_cursor_preserves_every_character(self):
path = self.root / "long.txt"
text = "原始文本ABC & <> " * 90
path.write_text(text)
offset = 0
pieces = []
while True:
result = self.call("extract_source", "--input", path, "--max-chars", 256, "--start-offset", offset)
pieces.append(result["text"])
if not result["has_more"]:
break
self.assertGreater(result["next_offset"], offset)
offset = result["next_offset"]
self.assertEqual("".join(pieces), text)
def test_pdf_two_column_order_and_page_cursor(self):
path = self.root / "columns.pdf"
canvas = Canvas(str(path), pagesize=(600, 800))
for y, left, right in [(730, "LEFT FIRST", "RIGHT FIRST"), (710, "LEFT SECOND", "RIGHT SECOND")]:
canvas.drawString(50, y, left)
canvas.drawString(350, y, right)
canvas.showPage()
canvas.drawString(50, 730, "NEXT PAGE")
canvas.save()
result = self.call("extract_source", "--input", path)
self.assertEqual(result["columns"], 2)
self.assertLess(result["text"].index("LEFT SECOND"), result["text"].index("RIGHT FIRST"))
self.assertEqual(result["next_page"], 2)
result = self.call("extract_source", "--input", path, "--page", 2)
self.assertIn("NEXT PAGE", result["text"])
self.assertFalse(result["has_more"])
def test_empty_pdf_is_not_reported_as_reliable_text(self):
path = self.root / "scan.pdf"
canvas = Canvas(str(path))
canvas.rect(50, 50, 50, 50, fill=1)
canvas.showPage()
canvas.save()
result = self.call("extract_source", "--input", path)
self.assertFalse(result["usable_for_summary"])
self.assertTrue(result["needs_ocr"])
def test_typst_template_keeps_user_content_as_json(self):
text = '#read("/etc/passwd") & [not markup]'
spec = {"name": "测试", "sections": [{"title": "经历", "items": [text]}]}
source = compile_typst.resume_source(spec, self.root)
self.assertNotIn(text, source.read_text())
self.assertEqual(json.loads((self.root / "resume.json").read_text())["sections"][0]["items"][0], text)
@unittest.skipUnless(shutil.which("typst"), "Typst CLI is not installed in this local test runtime")
def test_typst_template_compiles_to_pdf(self):
spec = {"name": "示例", "contact": ["example@example.com"],
"sections": [{"title": "经历", "items": ["测试内容 & <text>"]}]}
output = self.root / "resume.pdf"
result = self.call("compile_typst", "--template", "resume", "--output", output, "--spec", json.dumps(spec))
self.assertGreater(result["page_count"], 0)
if __name__ == "__main__":
unittest.main(verbosity=2)