dsh-office-tools 1.0.0 → 1.0.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/README.md +2 -0
- package/README.zh.md +2 -0
- package/lib/index.js +32 -17
- package/lib/types/fschannel.d.ts +30 -4
- package/package.json +44 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Eight model-facing Office file tools for DeepSeek Harness, running entirely in t
|
|
|
6
6
|
|
|
7
7
|
> Install: `dsh plugin --profile web add github:kw78/dsh-office-tools`
|
|
8
8
|
|
|
9
|
+
> DSH Store lists a fixed Commit; verify the installed source against it: `ae1ddd40f98debf4b1430748d287951f35faaf36` (tag `v1.0.0`). Later commits on `main` carry test/doc additions only unless the version bumps.
|
|
10
|
+
|
|
9
11
|
Eight model-facing Office file tools for DeepSeek Harness, running entirely in the plugin host half — dependency-free since 1.0.0: every package is generated and parsed by this plugin's own OOXML engine, and every byte crosses the official DSH filesystem service (`ctx.fs`), never the raw file system.
|
|
10
12
|
|
|
11
13
|
| Tool | Purpose |
|
package/README.zh.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
> 安装:`dsh plugin --profile web add github:kw78/dsh-office-tools`
|
|
6
6
|
|
|
7
|
+
> DSH Store 固定 Commit 校验源:`ae1ddd40f98debf4b1430748d287951f35faaf36`(tag `v1.0.0`)。其后的 `main` 提交仅为测试/文档补充,除非版本号提升。
|
|
8
|
+
|
|
7
9
|
为 DeepSeek Harness 提供 8 个模型可调用的 Office 文件工具,全部运行在 host 半 —— 1.0.0 起零运行时依赖:所有文件由插件自带的 OOXML 引擎生成与解析,所有字节都经由官方文件服务 `ctx.fs` 传输,插件自身不直接触碰文件系统。
|
|
8
10
|
|
|
9
11
|
| 工具 | 作用 |
|
package/lib/index.js
CHANGED
|
@@ -357,6 +357,15 @@ import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
357
357
|
var MAX_TEXT_CHARS = 2e5;
|
|
358
358
|
var MAX_READ_CELLS = 2e5;
|
|
359
359
|
var MAX_WRITE_CELLS = 2e5;
|
|
360
|
+
async function resolveWritePolicy(ctx, exec) {
|
|
361
|
+
if (ctx.fs.sandboxMode === void 0) return void 0;
|
|
362
|
+
const service = typeof ctx.get === "function" ? ctx.get("sandboxPolicy") : void 0;
|
|
363
|
+
if (service === void 0 || service === null) {
|
|
364
|
+
throw new Error("office tools: the mounted filesystem confines but ctx.sandboxPolicy is missing");
|
|
365
|
+
}
|
|
366
|
+
if (typeof service.resolve !== "function") return void 0;
|
|
367
|
+
return service.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
|
|
368
|
+
}
|
|
360
369
|
function workspaceRootOf(exec) {
|
|
361
370
|
const cwd = exec.agent?.session.header.cwd;
|
|
362
371
|
if (cwd === void 0 || cwd === "") {
|
|
@@ -374,19 +383,20 @@ function assertLexicallyWithin(root, candidate) {
|
|
|
374
383
|
throw new Error(`path "${candidate}" escapes the session workspace "${root}"`);
|
|
375
384
|
}
|
|
376
385
|
}
|
|
377
|
-
async function resolveOfficePath(exec, ctx, rawPath, allowedExts, mustExist) {
|
|
386
|
+
async function resolveOfficePath(exec, ctx, rawPath, allowedExts, mustExist, writePolicy) {
|
|
378
387
|
exec.signal.throwIfAborted();
|
|
379
388
|
if (rawPath.trim() === "") throw new Error("path must be a non-empty string");
|
|
380
389
|
const root = workspaceRootOf(exec);
|
|
381
|
-
const
|
|
382
|
-
|
|
390
|
+
const effectiveRoot = writePolicy?.workspaceRoot ? resolve(writePolicy.workspaceRoot) : root;
|
|
391
|
+
const candidate = resolve(isAbsolute(rawPath) ? rawPath : join(effectiveRoot, rawPath));
|
|
392
|
+
assertLexicallyWithin(effectiveRoot, candidate);
|
|
383
393
|
const ext = extname(candidate).toLowerCase();
|
|
384
394
|
if (!allowedExts.includes(ext)) {
|
|
385
395
|
throw new Error(`expected ${allowedExts.join(" or ")} file, got extension "${ext || "(none)"}"`);
|
|
386
396
|
}
|
|
387
397
|
const fs = ctx.fs;
|
|
388
|
-
const rootTarget = await fs.resolve(".", { cwd:
|
|
389
|
-
const target = await fs.resolve(rawPath, { cwd:
|
|
398
|
+
const rootTarget = await fs.resolve(".", { cwd: effectiveRoot, signal: exec.signal });
|
|
399
|
+
const target = await fs.resolve(rawPath, { cwd: effectiveRoot, signal: exec.signal });
|
|
390
400
|
if (!fs.contains(rootTarget, target)) {
|
|
391
401
|
throw new Error(`path "${rawPath}" escapes the session workspace "${root}"`);
|
|
392
402
|
}
|
|
@@ -412,9 +422,9 @@ async function readOfficeBytes(exec, ctx, target) {
|
|
|
412
422
|
exec.signal.throwIfAborted();
|
|
413
423
|
return { bytes, sizeBytes: bytes.byteLength };
|
|
414
424
|
}
|
|
415
|
-
async function saveOfficeText(exec, ctx, target, text) {
|
|
425
|
+
async function saveOfficeText(exec, ctx, target, text, writePolicy) {
|
|
416
426
|
exec.signal.throwIfAborted();
|
|
417
|
-
await ctx.fs.writeText(target, text, void 0, exec.signal);
|
|
427
|
+
await ctx.fs.writeText(target, text, void 0, exec.signal, writePolicy);
|
|
418
428
|
exec.signal.throwIfAborted();
|
|
419
429
|
return Buffer.byteLength(text, "utf-8");
|
|
420
430
|
}
|
|
@@ -854,7 +864,8 @@ function registerExcelCreate(ctx) {
|
|
|
854
864
|
locations: [{ path: args.path }]
|
|
855
865
|
}),
|
|
856
866
|
async execute(args, exec) {
|
|
857
|
-
const
|
|
867
|
+
const writePolicy = await resolveWritePolicy(ctx, exec);
|
|
868
|
+
const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], false, writePolicy);
|
|
858
869
|
await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
|
|
859
870
|
validateSheetSpecs(args.sheets);
|
|
860
871
|
exec.signal.throwIfAborted();
|
|
@@ -879,7 +890,7 @@ function registerExcelCreate(ctx) {
|
|
|
879
890
|
});
|
|
880
891
|
const text = buildXlsxText(models);
|
|
881
892
|
exec.signal.throwIfAborted();
|
|
882
|
-
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
|
|
893
|
+
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
|
|
883
894
|
return { path: target.display, sizeBytes, sheets: summaries };
|
|
884
895
|
}
|
|
885
896
|
}));
|
|
@@ -1018,7 +1029,8 @@ function registerExcelUpdate(ctx) {
|
|
|
1018
1029
|
locations: [{ path: args.path }]
|
|
1019
1030
|
}),
|
|
1020
1031
|
async execute(args, exec) {
|
|
1021
|
-
const
|
|
1032
|
+
const writePolicy = await resolveWritePolicy(ctx, exec);
|
|
1033
|
+
const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], true, writePolicy);
|
|
1022
1034
|
if ((args.sheets?.length ?? 0) === 0 && (args.cell_updates?.length ?? 0) === 0) {
|
|
1023
1035
|
throw new Error("excel_update needs at least one entry in sheets or cell_updates");
|
|
1024
1036
|
}
|
|
@@ -1062,7 +1074,7 @@ function registerExcelUpdate(ctx) {
|
|
|
1062
1074
|
exec.signal.throwIfAborted();
|
|
1063
1075
|
const orderedModels = names.map((name2) => models.get(name2)).filter((model) => model !== void 0).map((model, index) => ({ ...model, part: `worksheets/sheet${index + 1}.xml` }));
|
|
1064
1076
|
const text = buildXlsxText(orderedModels);
|
|
1065
|
-
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
|
|
1077
|
+
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
|
|
1066
1078
|
return {
|
|
1067
1079
|
path: target.display,
|
|
1068
1080
|
sizeBytes,
|
|
@@ -1743,7 +1755,8 @@ ${sketchSlide(value.slideWidthInches, value.slideHeightInches, slide.elements)}`
|
|
|
1743
1755
|
locations: [{ path: args.path }]
|
|
1744
1756
|
}),
|
|
1745
1757
|
async execute(args, exec) {
|
|
1746
|
-
const
|
|
1758
|
+
const writePolicy = await resolveWritePolicy(ctx, exec);
|
|
1759
|
+
const target = await resolveOfficePath(exec, ctx, args.path, [".pptx"], false, writePolicy);
|
|
1747
1760
|
await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
|
|
1748
1761
|
if ((args.slides?.length ?? 0) > 0) validateSlideSpecs(args.slides);
|
|
1749
1762
|
if (args.title === void 0 && (args.slides?.length ?? 0) === 0) {
|
|
@@ -1752,7 +1765,7 @@ ${sketchSlide(value.slideWidthInches, value.slideHeightInches, slide.elements)}`
|
|
|
1752
1765
|
exec.signal.throwIfAborted();
|
|
1753
1766
|
const { text, layout } = await buildPptxText(args, exec, ctx, target);
|
|
1754
1767
|
exec.signal.throwIfAborted();
|
|
1755
|
-
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
|
|
1768
|
+
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
|
|
1756
1769
|
const result = {
|
|
1757
1770
|
path: target.display,
|
|
1758
1771
|
sizeBytes,
|
|
@@ -1937,7 +1950,8 @@ function registerWordUpdate(ctx) {
|
|
|
1937
1950
|
locations: [{ path: args.path }]
|
|
1938
1951
|
}),
|
|
1939
1952
|
async execute(args, exec) {
|
|
1940
|
-
const
|
|
1953
|
+
const writePolicy = await resolveWritePolicy(ctx, exec);
|
|
1954
|
+
const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], true, writePolicy);
|
|
1941
1955
|
const { paragraphs: paragraphCount, cells } = wordCreateCounts(args);
|
|
1942
1956
|
if ((args.paragraphs?.length ?? 0) === 0 && (args.bullets?.length ?? 0) === 0 && args.table === void 0) {
|
|
1943
1957
|
throw new Error("word_update needs at least one of paragraphs, bullets, or table");
|
|
@@ -1966,7 +1980,7 @@ function registerWordUpdate(ctx) {
|
|
|
1966
1980
|
}
|
|
1967
1981
|
const text = buildAsciiZip(parts);
|
|
1968
1982
|
exec.signal.throwIfAborted();
|
|
1969
|
-
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
|
|
1983
|
+
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
|
|
1970
1984
|
return {
|
|
1971
1985
|
path: target.display,
|
|
1972
1986
|
sizeBytes,
|
|
@@ -2220,7 +2234,8 @@ function registerWordCreate(ctx) {
|
|
|
2220
2234
|
locations: [{ path: args.path }]
|
|
2221
2235
|
}),
|
|
2222
2236
|
async execute(args, exec) {
|
|
2223
|
-
const
|
|
2237
|
+
const writePolicy = await resolveWritePolicy(ctx, exec);
|
|
2238
|
+
const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], false, writePolicy);
|
|
2224
2239
|
await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
|
|
2225
2240
|
const { paragraphs: paragraphCount, cells } = wordCreateCounts(args);
|
|
2226
2241
|
if (paragraphCount > 1e4) throw new Error("too many paragraphs/bullets/table rows (maximum 10000)");
|
|
@@ -2230,7 +2245,7 @@ function registerWordCreate(ctx) {
|
|
|
2230
2245
|
}
|
|
2231
2246
|
const text = buildDocxText(args);
|
|
2232
2247
|
exec.signal.throwIfAborted();
|
|
2233
|
-
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
|
|
2248
|
+
const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
|
|
2234
2249
|
const result = {
|
|
2235
2250
|
path: target.display,
|
|
2236
2251
|
sizeBytes,
|
package/lib/types/fschannel.d.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* never touches the file system directly: workspace containment, symlink
|
|
9
9
|
* resolution, and atomic publication all belong to the backend, and this
|
|
10
10
|
* module only adds the Office-tool path policy on top (extension allow-lists,
|
|
11
|
-
* size caps, overwrite refusal, display paths).
|
|
11
|
+
* size caps, overwrite refusal, display paths). Writes carry the per-call
|
|
12
|
+
* sandbox policy (`ctx.sandboxPolicy.resolve({ session })` when a confining
|
|
13
|
+
* backend is mounted) so sandboxing backends fence them by the session's
|
|
14
|
+
* effective policy rather than the deployment default.
|
|
12
15
|
*/
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
17
|
import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs';
|
|
14
18
|
import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
15
19
|
/** Cap for text materialized into a single tool result. */
|
|
@@ -22,6 +26,22 @@ export declare const MAX_WRITE_CELLS = 200000;
|
|
|
22
26
|
export interface FsContext {
|
|
23
27
|
fs: FileSystem;
|
|
24
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The per-call sandbox execution policy `ctx.fs.writeText` accepts as its
|
|
31
|
+
* fifth parameter (`SandboxExecutionPolicy` from `@deepseek-ai/dsh-sandbox`):
|
|
32
|
+
* a sandboxing backend fences the write by it, the bare backend ignores it,
|
|
33
|
+
* and omitting it leaves the backend its deployment default — whose writable
|
|
34
|
+
* roots need not include this session's workspace. Derived from the official
|
|
35
|
+
* `writeText` signature so the plugin never depends on `@deepseek-ai/dsh-sandbox`
|
|
36
|
+
* directly.
|
|
37
|
+
*/
|
|
38
|
+
export type OfficeWritePolicy = NonNullable<Parameters<FileSystem['writeText']>[4]>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the per-call write policy the way the official fs tool layer does:
|
|
41
|
+
* skip when the mounted backend does not confine (`fs.sandboxMode` undefined),
|
|
42
|
+
* otherwise ask the shared `sandboxPolicy` service for the calling session.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveWritePolicy(ctx: Context & FsContext, exec: ToolRunContext): Promise<OfficeWritePolicy | undefined>;
|
|
25
45
|
export interface ResolvedOfficePath {
|
|
26
46
|
/** The path exactly as the model passed it. */
|
|
27
47
|
input: string;
|
|
@@ -41,8 +61,12 @@ export interface ResolvedOfficePath {
|
|
|
41
61
|
* @param rawPath - the model-supplied path string.
|
|
42
62
|
* @param allowedExts - acceptable lowercased extensions WITH dots (e.g. `.docx`).
|
|
43
63
|
* @param mustExist - when true, stat the target and refuse anything but a regular file.
|
|
64
|
+
* @param writePolicy - the per-call write policy, when a sandbox controller is
|
|
65
|
+
* mounted; its `workspaceRoot` is the authoritative workspace the write is
|
|
66
|
+
* fenced to, so relative paths resolve against it exactly like the official
|
|
67
|
+
* fs tools do. Omitted (reads, bare backends) keeps the session cwd.
|
|
44
68
|
*/
|
|
45
|
-
export declare function resolveOfficePath(exec: ToolRunContext, ctx: FsContext, rawPath: string, allowedExts: readonly string[], mustExist: boolean): Promise<ResolvedOfficePath>;
|
|
69
|
+
export declare function resolveOfficePath(exec: ToolRunContext, ctx: FsContext, rawPath: string, allowedExts: readonly string[], mustExist: boolean, writePolicy?: OfficeWritePolicy): Promise<ResolvedOfficePath>;
|
|
46
70
|
/**
|
|
47
71
|
* Read a bounded Office file through the backend, observing tool-call
|
|
48
72
|
* cancellation and refusing files above the hard cap before any transfer.
|
|
@@ -54,9 +78,11 @@ export declare function readOfficeBytes(exec: ToolRunContext, ctx: FsContext, ta
|
|
|
54
78
|
/**
|
|
55
79
|
* Publish one generated package: the text is pure ASCII (asserted by the zip
|
|
56
80
|
* planner), so the backend's atomic UTF-8 write lands it on disk
|
|
57
|
-
* byte-identical.
|
|
81
|
+
* byte-identical. The per-call write policy rides along as `writeText`'s
|
|
82
|
+
* fifth parameter so a sandboxing backend fences the write by the session's
|
|
83
|
+
* effective policy instead of its deployment default. Returns the on-disk size.
|
|
58
84
|
*/
|
|
59
|
-
export declare function saveOfficeText(exec: ToolRunContext, ctx: FsContext, target: FsTarget, text: string): Promise<number>;
|
|
85
|
+
export declare function saveOfficeText(exec: ToolRunContext, ctx: FsContext, target: FsTarget, text: string, writePolicy?: OfficeWritePolicy): Promise<number>;
|
|
60
86
|
/**
|
|
61
87
|
* Reject an overwrite when `overwrite` is false and the target already
|
|
62
88
|
* exists. Callers use this BEFORE doing expensive generation so the model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-office-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "DSH model-facing Office tools: create, read, and update Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files inside the session workspace, entirely through the official DSH filesystem service.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -114,5 +114,47 @@
|
|
|
114
114
|
"publishConfig": {
|
|
115
115
|
"access": "public"
|
|
116
116
|
},
|
|
117
|
-
"packageManager": "pnpm@
|
|
117
|
+
"packageManager": "pnpm@10.34.5",
|
|
118
|
+
"dshWorkshop": {
|
|
119
|
+
"schema": "omdsh-workshop-package/v1",
|
|
120
|
+
"type": "plugin",
|
|
121
|
+
"integration": {
|
|
122
|
+
"protocol": "harness-profile",
|
|
123
|
+
"artifact": "cordis.patch.yml"
|
|
124
|
+
},
|
|
125
|
+
"install": {
|
|
126
|
+
"mode": "transactional",
|
|
127
|
+
"adapter": "profile-bundle",
|
|
128
|
+
"failurePolicy": "generation-rollback",
|
|
129
|
+
"touchesCurrentBeforeActivation": false
|
|
130
|
+
},
|
|
131
|
+
"lifecycle": {
|
|
132
|
+
"activation": "restart-profile",
|
|
133
|
+
"dispose": "supported"
|
|
134
|
+
},
|
|
135
|
+
"permissions": [
|
|
136
|
+
"files:workspace-read",
|
|
137
|
+
"files:workspace-write"
|
|
138
|
+
],
|
|
139
|
+
"compatibility": {
|
|
140
|
+
"dshVersions": [
|
|
141
|
+
"0.1.1-rc.2",
|
|
142
|
+
"0.1.2-alpha.4",
|
|
143
|
+
"0.1.2-alpha.5",
|
|
144
|
+
"0.1.2-rc.1"
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
"capability": {
|
|
148
|
+
"id": "word_create",
|
|
149
|
+
"kind": "tool",
|
|
150
|
+
"invocation": "word_create({\"path\": \"hub-check.docx\", \"paragraphs\": [\"dsh-hub intake check\"]}) in a session with workspace cwd",
|
|
151
|
+
"expected": "a real .docx (first two bytes PK) exists at the workspace path; the result reports sizeBytes > 0; word_read returns 'dsh-hub intake check'"
|
|
152
|
+
},
|
|
153
|
+
"evidence": {
|
|
154
|
+
"install": null,
|
|
155
|
+
"failureIsolation": null,
|
|
156
|
+
"hotReload": null,
|
|
157
|
+
"remove": null
|
|
158
|
+
}
|
|
159
|
+
}
|
|
118
160
|
}
|