@deepseek-ai/dsh-skill-office 0.1.6-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/skill/skill-office/README.md
5
+ README.md: 18cb08a027a447bbb66e35061d25fe853f51977a
6
+ README.zh.md: 432043c5a06116f3f2928c995301ff032cda75bb
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: "Bundled Word, PowerPoint, and Excel instructions for deployments providing Office file authoring and structural checks."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-skill-office
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Agents can load Word, PowerPoint, and Excel workflows that use the bundled Python environment by default and respect explicit user or AGENTS.md environment choices. The skills cover creation, focused edits, structural checks, and file delivery. Visual inspection is conditional on image-capable models and an available rendering tool; ordinary document delivery does not require installing a renderer.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this provider beside the skill registry and `dsh-tool-skill` to expose `office-docx`, `office-pptx`, and `office-xlsx` in the session catalog. The provider supplies instructions and scripts; the deployment supplies its interpreters, authoring libraries, execution tools, and file delivery tool.
29
+
30
+ ### Minimal configuration
31
+
32
+ ```yaml
33
+ - name: '@deepseek-ai/dsh-skill-office'
34
+ ```
35
+
36
+ | Field | Default | Meaning |
37
+ |---|---|---|
38
+ | `assetRoot` | Packaged `assets/` | Absolute resource directory containing the three skill folders and shared `scripts/`; deployments can place it outside an application archive. |
39
+
40
+ Relative paths, missing resources, and skill files without a YAML frontmatter description reject activation. Disposing the plugin removes its candidates. Project and user skill precedence remains owned by the skill registry.
41
+
42
+ ### Structural checks
43
+
44
+ The shared Python checker reads DOCX, PPTX, or XLSX without modifying the source. It recognizes Transitional and Strict OOXML namespaces, validates ZIP/XML and internal package relationships, reports document structure, and optionally checks required text or slide/sheet count. DOCX text assertions cover the main body, section-referenced headers and footers, and body-referenced footnotes and endnotes; comments, glossary text, and unreferenced parts or notes do not satisfy them. It uses only the Python standard library. Invalid packages, corrupt or encrypted ZIP members, and report-file write failures produce a JSON failure report on stdout. A passing report does not establish appearance, feature preservation, or calculated formula results.
45
+
46
+ -----
47
+
48
+ <a id="understand-the-implementation"></a>
49
+ ## Understand the implementation
50
+
51
+ <details>
52
+ <summary>Implementation internals — click to expand</summary>
53
+
54
+ The provider registers three bundled candidates and reads their descriptions from the shipped YAML frontmatter at activation. Loaded instruction bodies exclude that metadata. Each loaded skill exposes its own filesystem directory, so the execution tool can resolve the shared checker without relying on the task working directory. Configurable external resources support carriers whose application archive is not readable by Python.
55
+
56
+ | File | Responsibility |
57
+ |---|---|
58
+ | [`src/index.ts`](src/index.ts) | Provider registration and configured resource paths. |
59
+ | [`assets/`](assets/) | Three workflows and the read-only OOXML checker. |
60
+ | — | No runtime invariant companion is published: the provider owns immutable candidates, and the skill registry owns registration lifecycle and precedence. |
61
+
62
+ </details>
63
+
64
+ -----
65
+
66
+ <a id="further-exploration"></a>
67
+ ## Further Exploration
68
+
69
+ - [Skill registry](../skill/README.md) — discovery and precedence.
70
+ - [Skill tool](../tool-skill/README.md) — model-visible catalogs and bodies.
71
+ - [File delivery](../../deliverables/tool-present/README.md) — current source-path delivery.
72
+
73
+ -----
74
+
75
+ <a id="model-experience"></a>
76
+ ## Model Experience
77
+
78
+ Indirectly, through `dsh-tool-skill`, which renders the catalog entries and selected instruction body.
79
+
80
+ #### KV Cache effect
81
+
82
+ Mounting the provider adds three catalog entries; loading a skill adds its body at the existing skill-tool insertion point. The provider does not add separate prompt sections.
83
+
84
+ ## Known Limitations and Deferred Work
85
+
86
+ <a id="known-limitations-and-deferred-work"></a>
87
+
88
+ - The provider does not install Python, authoring libraries, or a rendering engine. The checker requires Python 3.9 or later.
89
+ - Structural checks do not judge pagination, clipping, fonts, chart appearance, or Excel recalculation.
90
+ - The checker accepts DOCX, PPTX, and XLSX only; legacy, encrypted, and macro-enabled formats require an appropriate separate workflow.
91
+
92
+ <a id="dev-note"></a>
93
+ ### Dev Note
94
+
95
+ <details>
96
+ <summary>Working context for maintainers — click to expand</summary>
97
+
98
+ None.
99
+
100
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: "随包附带的 Word、PowerPoint 和 Excel 指令,供需要 Office 文件编写与结构检查能力的部署使用。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-skill-office
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ Agent(智能体)可以加载 Word、PowerPoint 和 Excel 工作流,默认使用内置 Python 环境,并遵循用户或 AGENTS.md 明确指定的环境。这些 skill(技能)涵盖创建、局部编辑、结构检查和文件交付。视觉检查以模型支持图片且有可用渲染工具为前提;普通文档交付不要求安装渲染器。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [了解实现](#understand-the-implementation)
18
+ - [延伸阅读](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与暂缓事项](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 将本提供方与 skill 注册表及 `dsh-tool-skill` 一同挂载,即可在会话目录中提供 `office-docx`、`office-pptx` 和 `office-xlsx`。提供方携带指令和脚本;部署提供解释器、编写库、执行工具与文件交付工具。
29
+
30
+ ### 最小配置
31
+
32
+ ```yaml
33
+ - name: '@deepseek-ai/dsh-skill-office'
34
+ ```
35
+
36
+ | 字段 | 默认值 | 含义 |
37
+ |---|---|---|
38
+ | `assetRoot` | 包内的 `assets/` | 包含三个 skill 文件夹和共享 `scripts/` 的绝对资源目录;部署可将其放在应用归档之外。 |
39
+
40
+ 相对路径、资源缺失或 skill 文件的 YAML frontmatter 缺少描述都会导致激活失败。卸载插件会移除其候选项。项目和用户 skill 的优先级仍由 skill 注册表负责。
41
+
42
+ ### 结构检查
43
+
44
+ 共享 Python 检查器读取 DOCX、PPTX 或 XLSX,不修改源文件。它识别 Transitional 与 Strict OOXML 命名空间,验证 ZIP/XML 和包内引用关系,报告文档结构,并可检查必需文本或幻灯片/工作表数量。DOCX 文本断言覆盖正文、分节引用的页眉和页脚,以及正文引用的脚注和尾注;批注、词库文本与未引用的部件或脚注/尾注不能满足断言。它只使用 Python 标准库。无效包、损坏或加密的 ZIP 成员和报告文件写入失败都会在标准输出中产生 JSON 失败报告。检查通过不代表外观、特性保留或公式计算结果已得到验证。
45
+
46
+ -----
47
+
48
+ <a id="understand-the-implementation"></a>
49
+ ## 了解实现
50
+
51
+ <details>
52
+ <summary>实现细节——点击展开</summary>
53
+
54
+ 提供方注册三个内置候选项,并在激活时从随包 YAML frontmatter 读取描述。加载后的指令正文不包含这些元数据。加载后的每个 skill 暴露自己的文件系统目录,因此执行工具可以定位共享检查器,而不依赖任务工作目录。可配置的外部资源支持 Python 无法读取应用归档的分发方式。
55
+
56
+ | 文件 | 职责 |
57
+ |---|---|
58
+ | [`src/index.ts`](src/index.ts) | 提供方注册与配置资源路径。 |
59
+ | [`assets/`](assets/) | 三种工作流和只读 OOXML 检查器。 |
60
+ | — | 不发布运行时不变量伴随入口:提供方拥有不可变候选项,skill 注册表负责注册生命周期和优先级。 |
61
+
62
+ </details>
63
+
64
+ -----
65
+
66
+ <a id="further-exploration"></a>
67
+ ## 延伸阅读
68
+
69
+ - [skill 注册表](../skill/README.zh.md)——发现与优先级。
70
+ - [skill 工具](../tool-skill/README.zh.md)——模型可见目录与正文。
71
+ - [文件交付](../../deliverables/tool-present/README.zh.md)——当前源路径交付。
72
+
73
+ -----
74
+
75
+ <a id="model-experience"></a>
76
+ ## 模型体验
77
+
78
+ 通过 `dsh-tool-skill` 间接呈现,由其渲染目录项与选中的指令正文。
79
+
80
+ #### KV 缓存影响
81
+
82
+ 挂载提供方会增加三个目录项;加载 skill 时,其正文进入既有 skill 工具的插入位置。提供方不另增提示词分区。
83
+
84
+ ## 已知限制与暂缓事项
85
+
86
+ <a id="known-limitations-and-deferred-work"></a>
87
+
88
+ - 提供方不安装 Python、编写库或渲染引擎。检查器要求 Python 3.9 或更高版本。
89
+ - 结构检查不判断分页、裁切、字体、图表外观或 Excel 重算。
90
+ - 检查器只接受 DOCX、PPTX 和 XLSX;传统格式、加密文件和启用宏的格式需要合适的其他工作流。
91
+
92
+ <a id="dev-note"></a>
93
+ ### 开发备注
94
+
95
+ <details>
96
+ <summary>维护者的工作上下文——点击展开</summary>
97
+
98
+ 无。
99
+
100
+ </details>
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: office-docx
3
+ description: Create, read, edit, and check Word documents (.docx), including reports, letters, and formatted tables. Use when a DOCX file is an input or requested deliverable.
4
+ ---
5
+
6
+ # Word documents
7
+
8
+ Use `python-docx` for DOCX creation and ordinary edits. Follow an explicit user or applicable AGENTS.md requirement for a project environment or another library. Otherwise call `load_workspace_dependencies` and execute the returned Python path with its bundled libraries. Do not install packages or discover a system Python for the default workflow. If the tool is unavailable, use an already configured environment and report a missing dependency only when it prevents the requested operation.
9
+
10
+ Keep source scripts, intermediate files, and final documents in the task workspace; the runtime and this skill directory are read-only resources. Use the user's requested language and preserve an existing document's design unless a redesign is requested.
11
+
12
+ ## Create and edit
13
+
14
+ For existing files, inspect paragraphs, runs, tables, sections, headers, and footers before changing the affected content. Save to a new file unless the user requests an in-place edit. Replacing a paragraph's `.text` destroys its run formatting; change the relevant runs when formatting must survive. Reconstructing the whole document can lose features outside python-docx's supported editing API.
15
+
16
+ Use paragraph styles for headings and body text. Size tables for the section that contains them, and account for merged cells and nested tables. Chinese, Japanese, and Korean text may need an explicit `w:eastAsia` font assignment in addition to `run.font.name`; font names alone do not establish glyph availability or rendered appearance.
17
+
18
+ For a new document, use the selected Python executable:
19
+
20
+ ```python
21
+ from docx import Document
22
+
23
+ document = Document()
24
+ document.add_heading("Project report", level=0)
25
+ document.add_paragraph("Summary", style="Heading 1")
26
+ document.add_paragraph("The requested findings go here.")
27
+ document.save("report.docx")
28
+ ```
29
+
30
+ python-docx does not paginate or render documents. Do not represent ordinary replacement, colored text, or comments as tracked changes. When real revisions or unsupported OOXML features matter, preserve their package parts and verify the requested operation rather than silently discarding them.
31
+
32
+ ## Check and deliver
33
+
34
+ Run the shared checker with the selected Python executable; `<skill-directory>` is this loaded skill's resource base:
35
+
36
+ ```text
37
+ <python> <skill-directory>/../scripts/check_office.py <document.docx> --out <checks.json>
38
+ ```
39
+
40
+ It checks ZIP/XML integrity and internal relationships, and reports paragraphs, logical table dimensions, and sections. Optional `--contains TEXT` arguments assert required text. A successful structural check does not verify pagination, clipping, fonts, or visual appearance. Compare the summary and reopened document with the user's request, including unchanged content that matters to an edit.
41
+
42
+ If `render_document` is available and visual inspection is useful, call it on the final DOCX without `pages` to prepare page 1 and learn `pageCount`. It checks the current main model's actual image capability; do not choose a second model. On `status: "skipped"`, complete structural and content checks and deliver the document, briefly stating that visual layout was not inspected. On a ready result, call `read_image` on `pages[].imagePath` and request remaining pages in small batches. Check page breaks, clipped text, headings, table widths, and consistency with the requested format or source design. Fix the source and render affected pages again.
43
+
44
+ Use the available rendering tool for this check. Review `warnings` such as missing fonts. LibreOffice pagination can differ from Microsoft Word. If rendering is unavailable or fails, preserve the usable document and report the inspection limit; do not require the user to install a renderer.
45
+
46
+ Call `present({"files":[{"path":"report.docx"}]})` with the actual final DOCX path. It exposes the current source file without copying or preserving its bytes, so keep that file in place and do not present temporary QA reports unless requested. If `present` is unavailable, provide the final workspace path using the session's supported file delivery method.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: office-pptx
3
+ description: Create, read, edit, and check PowerPoint presentations (.pptx), including slide text, tables, images, and charts. Use when a PPTX file is an input or requested deliverable.
4
+ ---
5
+
6
+ # PowerPoint presentations
7
+
8
+ Follow an explicit user or applicable AGENTS.md requirement for an environment or library. Otherwise call `load_workspace_dependencies` and use its Python executable and bundled presentation libraries. Do not install npm or pip packages or locate a system interpreter for the default workflow. If the tool is unavailable, use an already configured environment and report a missing dependency only when it prevents the requested operation.
9
+
10
+ Keep scripts and output files in the task workspace. The runtime and skill directory contain shared read-only resources. Match the requested slide language and the supplied presentation's design when editing it.
11
+
12
+ ## Create and edit
13
+
14
+ Use `python-pptx` to inspect or modify an existing presentation. Inspect slide layouts, text runs, images, tables, and charts before editing. Change only the requested content, preserve mixed text formatting, and save to a new file unless the user requests an in-place edit. Rebuilding slides can discard unsupported animation, SmartArt, or other extension content.
15
+
16
+ For a new deck, use `python-pptx` with editable text, tables, and charts. Use local image assets rather than network-dependent image URLs. Set slide dimensions, text sizes, and chart data explicitly.
17
+
18
+ A minimal editable deck, run with the selected Python executable:
19
+
20
+ ```python
21
+ from pptx import Presentation
22
+ from pptx.chart.data import CategoryChartData
23
+ from pptx.enum.chart import XL_CHART_TYPE
24
+ from pptx.util import Inches, Pt
25
+
26
+ presentation = Presentation()
27
+ presentation.slide_width = Inches(13.333)
28
+ presentation.slide_height = Inches(7.5)
29
+ slide = presentation.slides.add_slide(presentation.slide_layouts[6])
30
+ title = slide.shapes.add_textbox(Inches(0.6), Inches(0.4), Inches(12), Inches(0.8))
31
+ run = title.text_frame.paragraphs[0].add_run()
32
+ run.text = "Quarterly report"
33
+ run.font.size = Pt(30)
34
+ data = CategoryChartData()
35
+ data.categories = ["Q1", "Q2"]
36
+ data.add_series("Revenue", [12, 18])
37
+ slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED,
38
+ Inches(0.8), Inches(1.6), Inches(11.5), Inches(4.8), data)
39
+ presentation.save("report.pptx")
40
+ ```
41
+
42
+ Use `Inches` or `Cm` for positions and sizes and `Pt` for font sizes. To edit a generated title while retaining its other slides and charts:
43
+
44
+ ```python
45
+ from pptx import Presentation
46
+
47
+ presentation = Presentation("report.pptx")
48
+ for shape in presentation.slides[0].shapes:
49
+ if shape.has_text_frame and shape.text == "Quarterly report":
50
+ shape.text_frame.paragraphs[0].runs[0].text = "Quarterly results"
51
+ presentation.save("report-edited.pptx")
52
+ ```
53
+
54
+ Check that text and images fit the slide dimensions, titles form a useful sequence, and chart labels agree with source values. A native chart's embedded workbook is part of the deliverable and must contain the intended data. Library support for writing PPTX is not a rendering engine or a guarantee that every PowerPoint feature survives editing.
55
+
56
+ ## Check and deliver
57
+
58
+ Run the shared checker with the selected Python executable; `<skill-directory>` is this loaded skill's resource base:
59
+
60
+ ```text
61
+ <python> <skill-directory>/../scripts/check_office.py <presentation.pptx> --out <checks.json>
62
+ ```
63
+
64
+ It checks ZIP/XML integrity and internal relationships and reports slide count and extracted text. Use repeated `--contains TEXT` arguments for required slide text and `--count N` for a requested slide count; `--contains` excludes chart text and speaker notes. Reopen the file to check the requested edits, chart data, and notes. Structural success does not establish text fit, alignment, readable contrast, or rendering fidelity.
65
+
66
+ If `render_document` is available and visual inspection is useful, call it on the final PPTX, initially omitting `pages` to prepare page 1 and obtain `pageCount`. The tool checks the current main model's actual image capability; do not choose a second model. If `status` is `skipped`, complete structural and content checks and deliver the PPTX, briefly stating that visual layout was not inspected. If ready, call `read_image` on its returned `pages[].imagePath`, then inspect remaining slides in small batches such as `pages: [2, 3, 4]`. Check clipping, alignment, contrast, chart labels, and consistency with the requested design or supplied reference; fix the source and render the affected pages again.
67
+
68
+ Use the available rendering tool for this check. Inspect `warnings`, especially missing fonts. LibreOffice previews do not certify pixel-identical PowerPoint or Keynote output, animation, or media playback. If the rendering tool is unavailable or fails, retain the usable source and report the inspection limit; do not require the user to install another tool.
69
+
70
+ Call `present({"files":[{"path":"report-edited.pptx"}]})` with the actual final PPTX path. It exposes the current source file without copying or preserving its bytes, so keep that file in place and leave intermediate images and reports out of delivery unless requested. If `present` is unavailable, use the session's supported file delivery method.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: office-xlsx
3
+ description: Read, create, and modify Excel workbooks (.xlsx), including cell values, formulas, formatting, and pandas analysis. Use when an Excel workbook is an input or deliverable.
4
+ ---
5
+
6
+ # Excel workbooks
7
+
8
+ Follow an explicit user or applicable AGENTS.md requirement for an environment or library. Otherwise call `load_workspace_dependencies` and run its Python executable with bundled `openpyxl` and `pandas`. Do not install packages or search for a system Python for the default workflow. If the tool is unavailable, use an already configured environment and report a missing dependency only when it prevents the requested operation.
9
+
10
+ Keep scripts, working files, and outputs in the task workspace. The runtime and skill directory are shared read-only resources. Save to a new workbook unless the user requests an in-place edit.
11
+
12
+ ## Read and modify
13
+
14
+ Use `openpyxl` for existing XLSX workbooks and targeted cell or style changes. Load with `data_only=False` when formulas must survive. Inspect sheet names, the affected cell types, formulas, styles, merged ranges, and referenced ranges before editing. Check the saved file by reopening it, including unchanged content that the request requires preserving.
15
+
16
+ Use pandas for data analysis and transformations. A DataFrame is not the workbook: exporting it over an existing file can lose sheets, formulas, charts, and formatting. Write analysis results back to the intended ranges with openpyxl. `XlsxWriter` can create new workbooks, but cannot read or modify existing ones.
17
+
18
+ For a new workbook, the selected Python environment can write editable values, styles, and formulas directly:
19
+
20
+ ```python
21
+ from openpyxl import Workbook
22
+ from openpyxl.styles import Font
23
+
24
+ workbook = Workbook()
25
+ sheet = workbook.active
26
+ sheet.title = "Revenue"
27
+ for row in [("Quarter", "Revenue"), ("Q1", 12), ("Q2", 18), ("Total", "=SUM(B2:B3)")]:
28
+ sheet.append(row)
29
+ for cell in sheet[1]:
30
+ cell.font = Font(bold=True)
31
+ sheet.column_dimensions["A"].width = 18
32
+ sheet.column_dimensions["B"].width = 18
33
+ workbook.save("report.xlsx")
34
+ ```
35
+
36
+ Preserve numbers, dates, booleans, and identifiers as the intended cell types; formatting is not a type conversion. For modifications, load the existing file with `openpyxl.load_workbook("input.xlsx", data_only=False)`, change the requested ranges, and save a separate result. Avoid a DataFrame round trip when workbook features must survive.
37
+
38
+ Writing a formula does not calculate its result. openpyxl and XlsxWriter do not evaluate Excel formulas; `data_only=True` returns stored cached values that may be absent or stale. Verify formulas and inputs separately, and state when current results require recalculation in a spreadsheet application. Do not replace requested formulas with constants or report cached values as newly calculated results.
39
+
40
+ Do not rename `.xls`, `.xlsb`, encrypted files, or macro-enabled files to `.xlsx`. They need an appropriate supported operation. `keep_vba=True` can preserve VBA package content in an XLSM workflow, but does not execute or edit macros and does not guarantee every advanced workbook feature survives. Preserve the original and verify such requirements explicitly.
41
+
42
+ ## Check and deliver
43
+
44
+ Run the shared checker with the selected Python executable; `<skill-directory>` is this loaded skill's resource base:
45
+
46
+ ```text
47
+ <python> <skill-directory>/../scripts/check_office.py <workbook.xlsx> --out <checks.json>
48
+ ```
49
+
50
+ It checks ZIP/XML integrity and internal relationships, and reports sheet names, populated-cell counts, and formula counts. Repeated `--contains TEXT` arguments check string cells and sheet names, and `--count N` checks sheet count. `--contains` excludes numeric cells and does not validate formula results; verify those separately with openpyxl. The checker does not calculate formulas or judge workbook appearance. Compare relevant values, types, formulas, styles, and totals with the task's source data.
51
+
52
+ If `render_document` is available and visual inspection is useful, call it on the final workbook without `pages` to prepare page 1 and learn `pageCount`. It checks the current main model's actual image capability; do not choose a second model. On `status: "skipped"`, complete structural and data checks and deliver the workbook, briefly stating that visual layout was not inspected. Otherwise call `read_image` on `pages[].imagePath` and request remaining pages in small batches. Check column widths, number formats, clipping, charts, and print areas; fix the workbook and review the affected pages again.
53
+
54
+ Rendered pages follow spreadsheet print settings, so a page is not necessarily a worksheet. Set appropriate print areas and scaling when a readable printed layout is requested. Review `warnings` such as missing fonts. LibreOffice preview conversion neither updates the original workbook's cached formulas nor certifies native Excel calculation or appearance. If rendering is unavailable or fails, preserve the usable workbook and report the inspection limit; ordinary cell edits do not require installing an external renderer.
55
+
56
+ Call `present({"files":[{"path":"report.xlsx"}]})` with the actual final workbook path. It exposes the current source file without copying or preserving its bytes, so keep that file in place and omit intermediate scripts and reports unless requested. If `present` is unavailable, use the session's supported file delivery method.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only OOXML checks and structural summaries; no rendering or formula evaluation.
3
+
4
+ Run with INPUT.docx, INPUT.pptx, or INPUT.xlsx. Optional --contains assertions
5
+ check extracted text; DOCX excludes comments, glossary text, and unreferenced parts or notes.
6
+ --count checks slides or sheets. JSON escapes non-ASCII
7
+ characters and goes to stdout and optionally --out. Exit 0 means the requested
8
+ structural checks passed, 1 means a document, assertion, or report write failed, and 2 means
9
+ invalid command-line arguments.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import posixpath
17
+ import sys
18
+ import zipfile
19
+ import zlib
20
+ from pathlib import Path
21
+ from urllib.parse import unquote, urlsplit
22
+ from xml.etree import ElementTree as ET
23
+
24
+ W_NAMESPACES = (
25
+ "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
26
+ "http://purl.oclc.org/ooxml/wordprocessingml/main",
27
+ )
28
+ A_NAMESPACES = (
29
+ "http://schemas.openxmlformats.org/drawingml/2006/main",
30
+ "http://purl.oclc.org/ooxml/drawingml/main",
31
+ )
32
+ P_NAMESPACES = (
33
+ "http://schemas.openxmlformats.org/presentationml/2006/main",
34
+ "http://purl.oclc.org/ooxml/presentationml/main",
35
+ )
36
+ S_NAMESPACES = (
37
+ "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
38
+ "http://purl.oclc.org/ooxml/spreadsheetml/main",
39
+ )
40
+ R_NAMESPACES = (
41
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
42
+ "http://purl.oclc.org/ooxml/officeDocument/relationships",
43
+ )
44
+ MAIN_PARTS = {
45
+ ".docx": ("word/document.xml", "wordprocessingml.document.main+xml"),
46
+ ".pptx": ("ppt/presentation.xml", "presentationml.presentation.main+xml"),
47
+ ".xlsx": ("xl/workbook.xml", "spreadsheetml.sheet.main+xml"),
48
+ }
49
+
50
+
51
+ def namespace(root: ET.Element, supported: tuple[str, ...], part: str) -> str:
52
+ """Return the main XML namespace after checking its OOXML variant."""
53
+ uri = root.tag[1:].split("}", 1)[0] if root.tag.startswith("{") else ""
54
+ if uri not in supported:
55
+ raise ValueError(f"{part} uses unsupported XML namespace: {uri or '(none)'}")
56
+ return "{" + uri + "}"
57
+
58
+
59
+ def relationship_id(node: ET.Element, part: str) -> str:
60
+ """Read an office-document relationship id from Transitional or Strict OOXML."""
61
+ for uri in R_NAMESPACES:
62
+ value = node.get("{" + uri + "}id")
63
+ if value is not None:
64
+ return value
65
+ raise ValueError(f"{part} has a reference without a relationship id")
66
+
67
+
68
+ def relationship_types(kind: str) -> set[str]:
69
+ """Return the Transitional and Strict relationship type names for one role."""
70
+ return {f"{uri}/{kind}" for uri in R_NAMESPACES}
71
+
72
+
73
+ def iter_namespaces(root: ET.Element, namespaces: tuple[str, ...], local_name: str):
74
+ """Iterate matching elements across Transitional and Strict namespaces."""
75
+ for uri in namespaces:
76
+ yield from root.iter("{" + uri + "}" + local_name)
77
+
78
+
79
+ def relationship_target(part: str, target: str) -> str:
80
+ """Resolve a package relationship without fetching external resources."""
81
+ path = unquote(urlsplit(target).path)
82
+ return posixpath.normpath(path.lstrip("/") if path.startswith("/") else posixpath.join(posixpath.dirname(part), path))
83
+
84
+
85
+ def relationships(part: str, xml: dict[str, ET.Element], types: set[str] | None = None) -> dict[str, str]:
86
+ path = posixpath.join(posixpath.dirname(part), "_rels", posixpath.basename(part) + ".rels")
87
+ root = xml.get(path)
88
+ if root is None:
89
+ return {}
90
+ return {
91
+ rel.attrib["Id"]: relationship_target(part, rel.attrib["Target"])
92
+ for rel in root if rel.get("TargetMode") != "External" and (types is None or rel.get("Type") in types)
93
+ }
94
+
95
+
96
+ def related_xml(part: str, reference: str, links: dict[str, str], xml: dict[str, ET.Element]) -> ET.Element:
97
+ """Read a related XML part with diagnostics naming its source and reference."""
98
+ if reference not in links:
99
+ raise ValueError(f"{part} references missing relationship: {reference}")
100
+ target = links[reference]
101
+ if target not in xml:
102
+ raise ValueError(f"{part} relationship {reference} targets a non-XML member: {target}")
103
+ return xml[target]
104
+
105
+
106
+ def inspect_docx(xml: dict[str, ET.Element]) -> tuple[dict, str]:
107
+ part = "word/document.xml"
108
+ root = xml[part]
109
+ w = namespace(root, W_NAMESPACES, part)
110
+ body = root.find(f"{w}body")
111
+ if body is None:
112
+ raise ValueError("word/document.xml has no document body")
113
+ tables = []
114
+ for table in body.iter(f"{w}tbl"):
115
+ grid = table.findall(f"{w}tblGrid/{w}gridCol")
116
+ rows = table.findall(f"{w}tr")
117
+ # Merged cells span logical grid columns; counting physical cells loses them.
118
+ columns = len(grid) if grid else max((sum(
119
+ int(cell.find(f"{w}tcPr/{w}gridSpan").get(f"{w}val", "1"))
120
+ if cell.find(f"{w}tcPr/{w}gridSpan") is not None else 1
121
+ for cell in row.findall(f"{w}tc")
122
+ ) for row in rows), default=0)
123
+ tables.append({"rows": len(rows), "columns": columns})
124
+ sections = []
125
+ for section in body.iter(f"{w}sectPr"):
126
+ size = section.find(f"{w}pgSz")
127
+ margins = section.find(f"{w}pgMar")
128
+ sections.append({
129
+ "page_twips": {} if size is None else {key.removeprefix(w): value for key, value in size.attrib.items()},
130
+ "margins_twips": {} if margins is None else {key.removeprefix(w): value for key, value in margins.attrib.items()},
131
+ })
132
+ text_parts = [body]
133
+ for kind in ("header", "footer"):
134
+ links = relationships(part, xml, relationship_types(kind))
135
+ for section in body.iter(f"{w}sectPr"):
136
+ for reference in section.findall(f"{w}{kind}Reference"):
137
+ text_parts.append(related_xml(part, relationship_id(reference, part), links, xml))
138
+ for kind in ("footnote", "endnote"):
139
+ references = {node.attrib[f"{w}id"] for node in body.iter(f"{w}{kind}Reference")}
140
+ if not references:
141
+ continue
142
+ links = relationships(part, xml, relationship_types(f"{kind}s"))
143
+ for reference in links:
144
+ tree = related_xml(part, reference, links, xml)
145
+ text_parts.extend(note for note in tree.findall(f"{w}{kind}") if note.get(f"{w}id") in references)
146
+ text = "\n".join("".join(node.text or "" for node in paragraph.iter(f"{w}t")) for tree in text_parts
147
+ for paragraph in tree.iter(f"{w}p"))
148
+ return {"paragraphs": len(list(body.iter(f"{w}p"))), "tables": tables, "sections": sections}, text
149
+
150
+
151
+ def inspect_pptx(xml: dict[str, ET.Element]) -> tuple[dict, str]:
152
+ part = "ppt/presentation.xml"
153
+ links = relationships(part, xml)
154
+ root = xml[part]
155
+ p = namespace(root, P_NAMESPACES, part)
156
+ slides = root.findall(f"{p}sldIdLst/{p}sldId")
157
+ texts = []
158
+ for slide in slides:
159
+ reference = relationship_id(slide, part)
160
+ tree = related_xml(part, reference, links, xml)
161
+ texts.append("\n".join("".join(node.text or "" for node in iter_namespaces(paragraph, A_NAMESPACES, "t"))
162
+ for paragraph in iter_namespaces(tree, A_NAMESPACES, "p")))
163
+ return {"slides": len(slides)}, "\n".join(texts)
164
+
165
+
166
+ def inspect_xlsx(xml: dict[str, ET.Element]) -> tuple[dict, str]:
167
+ part = "xl/workbook.xml"
168
+ links = relationships(part, xml)
169
+ root = xml[part]
170
+ s = namespace(root, S_NAMESPACES, part)
171
+ sheets = []
172
+ texts = []
173
+ shared = xml.get("xl/sharedStrings.xml")
174
+ shared_s = s if shared is None else namespace(shared, S_NAMESPACES, "xl/sharedStrings.xml")
175
+ shared_strings = [] if shared is None else [
176
+ "".join(node.text or "" for node in item.iter(f"{shared_s}t")) for item in shared.iter(f"{shared_s}si")
177
+ ]
178
+ for sheet in root.findall(f"{s}sheets/{s}sheet"):
179
+ reference = relationship_id(sheet, part)
180
+ tree = related_xml(part, reference, links, xml)
181
+ sheet_s = namespace(tree, S_NAMESPACES, links[reference])
182
+ cells = list(tree.iter(f"{sheet_s}c"))
183
+ formulas = sum(cell.find(f"{sheet_s}f") is not None for cell in cells)
184
+ sheets.append({"name": sheet.attrib["name"], "cells": len(cells), "formulas": formulas})
185
+ for cell in cells:
186
+ if cell.get("t") == "s":
187
+ value = cell.findtext(f"{sheet_s}v", "")
188
+ location = f"{links[reference]} cell {cell.get('r', '(no reference)')}"
189
+ try:
190
+ index = int(value)
191
+ except ValueError as error:
192
+ raise ValueError(f"{location}: invalid shared string index {value!r}") from error
193
+ if not 0 <= index < len(shared_strings):
194
+ raise ValueError(f"{location}: shared string index out of range: {index}")
195
+ texts.append(shared_strings[index])
196
+ texts.extend("".join(node.text or "" for node in cell.iter(f"{sheet_s}t")) for cell in cells)
197
+ texts.extend(cell.findtext(f"{sheet_s}v", "") for cell in cells if cell.get("t") == "str")
198
+ texts.append(sheet.attrib["name"])
199
+ return {"sheets": sheets, "formulas_evaluated": False}, "\n".join(texts)
200
+
201
+
202
+ def inspect(path: Path) -> tuple[dict, str]:
203
+ """Validate package members and relationships before inspecting the main part."""
204
+ main, content_type = MAIN_PARTS[path.suffix.lower()]
205
+ with zipfile.ZipFile(path) as archive:
206
+ members = archive.namelist()
207
+ if len(members) != len(set(members)):
208
+ raise ValueError("ZIP contains duplicate member names")
209
+ corrupt = archive.testzip()
210
+ if corrupt is not None:
211
+ raise ValueError(f"ZIP member failed its CRC check: {corrupt}")
212
+ xml = {name: ET.fromstring(archive.read(name)) for name in members
213
+ if name.endswith((".xml", ".rels"))}
214
+ if main not in xml:
215
+ raise ValueError(f"missing main part: {main}")
216
+ types = xml.get("[Content_Types].xml")
217
+ if types is None or not any(node.get("PartName") == "/" + main
218
+ and node.get("ContentType", "").endswith(content_type) for node in types):
219
+ raise ValueError(f"[Content_Types].xml does not declare {main} as {path.suffix.lower()}")
220
+ for name, tree in xml.items():
221
+ if not name.endswith(".rels"):
222
+ continue
223
+ source = "" if name == "_rels/.rels" else posixpath.join(posixpath.dirname(posixpath.dirname(name)), posixpath.basename(name)[:-5])
224
+ for rel in tree:
225
+ if rel.get("TargetMode") == "External":
226
+ continue
227
+ target = relationship_target(source, rel.attrib["Target"])
228
+ if target not in members:
229
+ raise ValueError(f"{name} references missing package member: {target}")
230
+ if path.suffix.lower() == ".docx":
231
+ return inspect_docx(xml)
232
+ if path.suffix.lower() == ".pptx":
233
+ return inspect_pptx(xml)
234
+ return inspect_xlsx(xml)
235
+
236
+
237
+ def main() -> int:
238
+ parser = argparse.ArgumentParser(description=__doc__)
239
+ parser.add_argument("input", type=Path)
240
+ parser.add_argument("--out", type=Path)
241
+ parser.add_argument("--contains", action="append", default=[], metavar="TEXT")
242
+ parser.add_argument("--count", type=int, help="expected slide or sheet count")
243
+ args = parser.parse_args()
244
+ if args.input.suffix.lower() not in MAIN_PARTS:
245
+ parser.error("input must be .docx, .pptx, or .xlsx; converting the filename does not convert its contents")
246
+ if args.count is not None and (args.count < 0 or args.input.suffix.lower() == ".docx"):
247
+ parser.error("--count must be non-negative and applies only to slides or sheets")
248
+ if args.out is not None and args.out.resolve() == args.input.resolve():
249
+ parser.error("--out must differ from the input document")
250
+ checks = []
251
+ summary = {}
252
+ try:
253
+ summary, text = inspect(args.input)
254
+ checks.append({"id": "package", "status": "pass"})
255
+ for required in args.contains:
256
+ checks.append({"id": "contains", "status": "pass" if required in text else "fail", "text": required})
257
+ if args.count is not None:
258
+ actual = summary.get("slides", len(summary.get("sheets", [])))
259
+ checks.append({"id": "count", "status": "pass" if actual == args.count else "fail", "expected": args.count, "actual": actual})
260
+ except (OSError, ValueError, KeyError, RuntimeError, ET.ParseError, zipfile.BadZipFile, zlib.error) as error:
261
+ checks.append({"id": "package", "status": "fail", "detail": str(error)})
262
+ failed = any(check["status"] == "fail" for check in checks)
263
+ report = {"format": args.input.suffix.lower()[1:], "verdict": "fail" if failed else "pass", "checks": checks, "summary": summary}
264
+ output = json.dumps(report, ensure_ascii=True, indent=2) + "\n"
265
+ if args.out is not None:
266
+ try:
267
+ args.out.parent.mkdir(parents=True, exist_ok=True)
268
+ args.out.write_text(output, encoding="utf-8")
269
+ except OSError as error:
270
+ failed = True
271
+ report["verdict"] = "fail"
272
+ checks.append({"id": "output", "status": "fail", "detail": str(error)})
273
+ output = json.dumps(report, ensure_ascii=True, indent=2) + "\n"
274
+ sys.stdout.write(output)
275
+ return 1 if failed else 0
276
+
277
+
278
+ if __name__ == "__main__":
279
+ sys.exit(main())
package/lib/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
7
+ import { parse } from "yaml";
8
+ //#region lib/types/index.js
9
+ /** Bundled Office workflows and filesystem resources for document authoring and checks. */
10
+ const SKILL_NAMES = [
11
+ "office-docx",
12
+ "office-pptx",
13
+ "office-xlsx"
14
+ ];
15
+ /** Validated resource configuration. */
16
+ const Config = z.object({ assetRoot: z.string().min(1) });
17
+ /** Cordis plugin identity. */
18
+ const name = "skill-office";
19
+ /** Registry used by the bundled provider. */
20
+ const inject = ["skills"];
21
+ function parseSkill(raw, path) {
22
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw);
23
+ if (frontmatter?.[1] === void 0) throw new Error(`skill-office: ${path} has no YAML frontmatter`);
24
+ const metadata = parse(frontmatter[1]);
25
+ const description = typeof metadata === "object" && metadata !== null && "description" in metadata ? metadata.description : void 0;
26
+ if (typeof description !== "string" || description.length === 0) throw new Error(`skill-office: ${path} has no description`);
27
+ return {
28
+ description,
29
+ content: raw.slice(frontmatter[0].length).trim()
30
+ };
31
+ }
32
+ /**
33
+ * Register Office skills with resources readable by the script interpreter.
34
+ * @param ctx - Context carrying the skill registry.
35
+ * @param config - Optional external assets directory for packaged applications.
36
+ */
37
+ function apply(ctx, config = {}) {
38
+ const assetRoot = config.assetRoot ?? fileURLToPath(new URL("../assets/", import.meta.url));
39
+ if (!isAbsolute(assetRoot)) throw new Error("skill-office: assetRoot must be an absolute directory");
40
+ if (!statSync(join(assetRoot, "scripts", "check_office.py")).isFile()) throw new Error("skill-office: assets must contain scripts/check_office.py");
41
+ const candidates = SKILL_NAMES.map((skillName) => {
42
+ const directory = join(assetRoot, skillName);
43
+ const path = join(directory, "SKILL.md");
44
+ const { description } = parseSkill(readFileSync(path, "utf8"), path);
45
+ return {
46
+ name: skillName,
47
+ description,
48
+ invocation: {
49
+ modelInvocable: true,
50
+ userInvocable: true
51
+ },
52
+ provider: "dsh-office",
53
+ source: "bundled",
54
+ rank: BUNDLED_SKILL_RANK,
55
+ resourceBase: {
56
+ kind: "directory",
57
+ path: directory
58
+ },
59
+ locator: path
60
+ };
61
+ });
62
+ const provider = {
63
+ name: "dsh-office",
64
+ list: () => Promise.resolve(candidates),
65
+ async get(candidate, options) {
66
+ const { rank: _rank, locator, ...summary } = candidate;
67
+ const raw = await readFile(locator, {
68
+ encoding: "utf8",
69
+ signal: options.signal
70
+ });
71
+ return {
72
+ ...summary,
73
+ content: parseSkill(raw, locator).content
74
+ };
75
+ }
76
+ };
77
+ ctx.skills.registerProvider(() => provider);
78
+ }
79
+ //#endregion
80
+ export { Config, apply, inject, name };
@@ -0,0 +1,21 @@
1
+ /** Bundled Office workflows and filesystem resources for document authoring and checks. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import z from '@deepseek-ai/schemastery';
4
+ /** Office skill resource location. */
5
+ export interface Config {
6
+ /** Absolute assets directory containing the three skill folders and shared scripts; defaults to packaged assets. */
7
+ assetRoot?: string;
8
+ }
9
+ /** Validated resource configuration. */
10
+ export declare const Config: z<Config>;
11
+ /** Cordis plugin identity. */
12
+ export declare const name = "skill-office";
13
+ /** Registry used by the bundled provider. */
14
+ export declare const inject: string[];
15
+ /**
16
+ * Register Office skills with resources readable by the script interpreter.
17
+ * @param ctx - Context carrying the skill registry.
18
+ * @param config - Optional external assets directory for packaged applications.
19
+ */
20
+ export declare function apply(ctx: Context, config?: Config): void;
21
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-skill-office",
3
+ "description": "Bundled Word, PowerPoint, and Excel workflows and structural checks",
4
+ "version": "0.1.6-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/skill/skill-office"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib/index.js",
25
+ "assets",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@deepseek-ai/dsh-skill": "^0.1.6-alpha.2",
31
+ "@deepseek-ai/cordis": "^4.0.2"
32
+ },
33
+ "devDependencies": {
34
+ "@deepseek-ai/dsh-skill": "^0.1.6-alpha.2",
35
+ "@deepseek-ai/cordis": "^4.0.2",
36
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
37
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3"
38
+ },
39
+ "dependencies": {
40
+ "yaml": "^2.4.2",
41
+ "@deepseek-ai/schemastery": "^3.18.2"
42
+ }
43
+ }