dsh-office-tools 1.0.4 → 1.0.6

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 CHANGED
@@ -61,6 +61,8 @@ pnpm install && pnpm run check # typecheck + tests + build
61
61
  pnpm run test:e2e # real-composition E2E (sandbox-policy + fs-sandbox + tools)
62
62
  ```
63
63
 
64
- Compatibility: DSH `>=0.1.0-rc.6`; exact per-release records live in `package.json` (`dsh.compatibility.dshReleases`).
64
+ Compatibility: the declared DSH 0.1.x ranges and `>=0.2.0-rc.1 <0.3.0-0`; exact tested releases live in `package.json` (`dsh.compatibility.dshReleases`). CI exercises both 0.1.2-rc.1 and 0.2.0-rc.1. DSH 0.3.x is not declared supported.
65
+
66
+ Generated packages are limited to 50 MiB. Up to 200 PPT slides (including an optional title slide) are accepted; linked images remain external. See [architecture and migration plan](docs/ARCHITECTURE.zh.md) for the binary-write seam needed for true embedding.
65
67
 
66
68
  Further reading: [DEVELOPMENT.md](docs/DEVELOPMENT.md) · [ROADMAP.md](docs/ROADMAP.md) · [hub-registration.md](docs/hub-registration.md) · MIT [License](LICENSE)
package/README.zh.md CHANGED
@@ -61,6 +61,8 @@ pnpm install && pnpm run check # typecheck + tests + build
61
61
  pnpm run test:e2e # 真实组合 E2E(sandbox-policy + fs-sandbox + tools)
62
62
  ```
63
63
 
64
- 兼容性:DSH `>=0.1.0-rc.6`;逐版本的精确兼容记录见 `package.json`(`dsh.compatibility.dshReleases`)。
64
+ 兼容性:声明的 DSH 0.1.x 区间,以及 `>=0.2.0-rc.1 <0.3.0-0`;逐版本的实测记录见 `package.json`(`dsh.compatibility.dshReleases`)。CI 验证 0.1.2-rc.1 和 0.2.0-rc.1,尚未声明支持 DSH 0.3.x。
65
+
66
+ 生成文件最多 50 MiB;PPT 最多 200 页(包含可选封面)。图片仍为外部链接。分层架构与真实图片嵌入的迁移方案见 [架构分析](docs/ARCHITECTURE.zh.md)。
65
67
 
66
68
  更多文档:[DEVELOPMENT.md](docs/DEVELOPMENT.md) · [ROADMAP.md](docs/ROADMAP.md) · [hub-registration.md](docs/hub-registration.md) · MIT [License](LICENSE)
package/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-office-tools",
3
- "version": "1.0.0",
3
+ "version": "1.0.6",
4
4
  "main": "lib/index.js",
5
5
  "description": "Office tools for the model: create, read, and update Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files inside the session workspace through the official DSH filesystem service.",
6
6
  "engines": {
7
- "dsh": ">=0.1.0-rc.6"
7
+ "dsh": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0"
8
8
  },
9
9
  "contributes": {
10
10
  "tools": [
package/lib/index.js CHANGED
@@ -52,15 +52,49 @@ var DOS_DATE = 2026 - 1980 << 9 | 1 << 5 | 1;
52
52
  var LOCAL_HEADER_BYTES = 30;
53
53
  var CENTRAL_HEADER_BYTES = 46;
54
54
  var EOCD_BYTES = 22;
55
- var MAX_PLAN_PAD = 65536;
55
+ var MAX_SAFE_U16 = 32639;
56
+ var CRC_SUFFIX_BYTES = 8;
57
+ function nextSafeField(value) {
58
+ let candidate = value;
59
+ for (let byte = 0; byte < 4; byte += 1) {
60
+ const unit = 2 ** (8 * byte);
61
+ if (Math.floor(candidate / unit) % 256 > 127) {
62
+ const band = unit * 256;
63
+ candidate = (Math.floor(candidate / band) + 1) * band;
64
+ }
65
+ }
66
+ if (!safeField(candidate)) throw new Error("ASCII-safe ZIP field exceeds its representable range");
67
+ return candidate;
68
+ }
69
+ function safeCrcSuffix(state) {
70
+ for (let variant = 0; variant < 256; variant += 1) {
71
+ let folded = state;
72
+ let suffix = "";
73
+ for (let bit = 0; bit < CRC_SUFFIX_BYTES; bit += 1) {
74
+ const byte = variant & 1 << bit ? 9 : 32;
75
+ suffix += String.fromCharCode(byte);
76
+ folded = crcFold(folded, byte);
77
+ }
78
+ const crc = (folded ^ 4294967295) >>> 0;
79
+ if (safeField(crc)) return { suffix, crc };
80
+ }
81
+ throw new Error("internal error: trailing XML whitespace cannot produce a byte-safe CRC");
82
+ }
56
83
  function extraField(total) {
57
84
  if (total === 0) return "";
58
85
  return u16(0) + u16(total - 4) + "\0".repeat(total - 4);
59
86
  }
60
87
  function feasibleExtraLength(end) {
61
88
  if (safeField(end)) return 0;
62
- for (let total = 4; total <= 127; total += 1) {
63
- if (safeField(end + total)) return total;
89
+ let total = nextSafeField(Math.max(4, nextSafeField(end) - end));
90
+ while (total <= MAX_SAFE_U16) {
91
+ if (!safeField(total - 4)) {
92
+ total = nextSafeField(total + 4);
93
+ continue;
94
+ }
95
+ const next = nextSafeField(end + total);
96
+ if (next === end + total) return total;
97
+ total = nextSafeField(total + next - (end + total));
64
98
  }
65
99
  return void 0;
66
100
  }
@@ -69,49 +103,46 @@ function planEntries(parts) {
69
103
  let offset = 0;
70
104
  for (const part of parts) {
71
105
  assertAscii(part.name, "zip entry name");
106
+ assertAscii(part.content, "zip entry content");
72
107
  const nameLength = part.name.length;
73
108
  const headerSize = LOCAL_HEADER_BYTES + nameLength;
74
109
  const baseLength = part.content.length;
75
- const minEnd = offset + headerSize + baseLength;
76
- let chosen;
77
- let state = contentCrcState(part.content);
78
- for (let pad = 0; pad <= MAX_PLAN_PAD && chosen === void 0; pad += 1) {
79
- if (pad > 0) state = crcFold(state, 10);
80
- const length = baseLength + pad;
81
- if (!safeField(length)) continue;
82
- const crc = (state ^ 4294967295) >>> 0;
83
- if (!safeField(crc)) continue;
84
- const extraLength = feasibleExtraLength(minEnd + pad);
85
- if (extraLength === void 0) continue;
86
- const content = pad === 0 ? part.content : part.content + "\n".repeat(pad);
87
- chosen = { content, crc, extraLength, end: minEnd + pad + extraLength };
110
+ if (!safeField(nameLength) || nameLength > MAX_SAFE_U16) {
111
+ throw new Error(`zip entry name is too long or has a non-byte-safe length: "${part.name}"`);
88
112
  }
89
- if (chosen === void 0) {
90
- throw new Error(`cannot lay out an ASCII-safe zip entry for "${part.name}" (offset=${offset})`);
113
+ let length = nextSafeField(baseLength + CRC_SUFFIX_BYTES);
114
+ let extraLength;
115
+ for (; ; ) {
116
+ const end = offset + headerSize + length;
117
+ if (end > MAX_OFFICE_FILE_BYTES) throw new Error("generated Office package exceeds the 50 MiB write budget");
118
+ extraLength = feasibleExtraLength(end);
119
+ if (extraLength !== void 0) break;
120
+ length = nextSafeField(length + Math.max(1, nextSafeField(end) - end - MAX_SAFE_U16));
91
121
  }
122
+ const pad = length - baseLength - CRC_SUFFIX_BYTES;
123
+ let state = contentCrcState(part.content);
124
+ for (let index = 0; index < pad; index += 1) state = crcFold(state, 10);
125
+ const { suffix, crc } = safeCrcSuffix(state);
92
126
  planned.push({
93
127
  name: part.name,
94
- content: chosen.content,
95
- crc: chosen.crc,
128
+ content: part.content + "\n".repeat(pad) + suffix,
129
+ crc,
96
130
  offset,
97
- localExtraLength: chosen.extraLength,
131
+ localExtraLength: extraLength,
98
132
  commentLength: 0,
99
133
  centralExtraLength: 0
100
134
  });
101
- offset = chosen.end;
135
+ offset += headerSize + extraLength + length;
102
136
  }
103
137
  const baseDirectorySize = planned.reduce((sum, entry) => sum + CENTRAL_HEADER_BYTES + entry.name.length, 0);
104
- const extraCandidates = [0];
105
- for (let total = 4; total <= 127; total += 1) extraCandidates.push(total);
106
138
  let commentLength = -1;
107
139
  let centralExtraLength = -1;
108
- outer: for (const extra of extraCandidates) {
109
- for (let comment = 0; comment <= 127; comment += 1) {
110
- if (safeField(baseDirectorySize + extra + comment)) {
111
- centralExtraLength = extra;
112
- commentLength = comment;
113
- break outer;
114
- }
140
+ for (let comment = 0; comment <= MAX_SAFE_U16; comment = nextSafeField(comment + 1)) {
141
+ const extra = feasibleExtraLength(baseDirectorySize + comment);
142
+ if (extra !== void 0) {
143
+ centralExtraLength = extra;
144
+ commentLength = comment;
145
+ break;
115
146
  }
116
147
  }
117
148
  if (commentLength < 0 || centralExtraLength < 0) {
@@ -125,10 +156,29 @@ function planEntries(parts) {
125
156
  return { entries: planned, directoryOffset: offset };
126
157
  }
127
158
  function buildAsciiZip(parts) {
128
- if (parts.length === 0 || parts.length > 127) {
129
- throw new Error(`an ASCII-safe package holds 1..127 entries, got ${parts.length}`);
130
- }
131
- const plan = planEntries(parts);
159
+ if (parts.length === 0 || parts.length > MAX_SAFE_U16) {
160
+ throw new Error(`an ASCII-safe package holds 1..${MAX_SAFE_U16} entries, got ${parts.length}`);
161
+ }
162
+ const count = nextSafeField(parts.length);
163
+ const names = new Set(parts.map((part) => part.name));
164
+ if (names.size !== parts.length) throw new Error("cannot write a zip archive with duplicate entry names");
165
+ const expanded = [...parts];
166
+ const paddingNames = [];
167
+ for (let index = 1; expanded.length < count; index += 1) {
168
+ const name2 = `dshPadding/pad${index}.xml`;
169
+ if (names.has(name2)) continue;
170
+ names.add(name2);
171
+ paddingNames.push(name2);
172
+ expanded.push({ name: name2, content: '<padding xmlns="urn:dsh-office-tools:padding"/>' });
173
+ }
174
+ const typesIndex = expanded.findIndex((part) => part.name === "[Content_Types].xml");
175
+ if (paddingNames.length > 0 && typesIndex >= 0 && !/<Default\b[^>]*\bExtension=["']xml["']/i.test(expanded[typesIndex].content)) {
176
+ const original = expanded[typesIndex];
177
+ const overrides = paddingNames.map((name2) => `<Override PartName="/${name2}" ContentType="application/xml"/>`).join("");
178
+ if (!/<\/(?:\w+:)?Types\s*>/.test(original.content)) throw new Error("cannot add padding parts to malformed content types");
179
+ expanded[typesIndex] = { ...original, content: original.content.replace(/<\/(?:\w+:)?Types\s*>/, `${overrides}$&`) };
180
+ }
181
+ const plan = planEntries(expanded);
132
182
  const planned = plan.entries;
133
183
  const local = [];
134
184
  const central = [];
@@ -187,6 +237,9 @@ function buildAsciiZip(parts) {
187
237
  if (!safeField(directorySize) || !safeField(directoryOffset)) {
188
238
  throw new Error("internal error: the planned central directory fields are not ASCII-safe");
189
239
  }
240
+ if (directoryOffset + directorySize + EOCD_BYTES > MAX_OFFICE_FILE_BYTES) {
241
+ throw new Error("generated Office package exceeds the 50 MiB write budget");
242
+ }
190
243
  const eocd = [
191
244
  "PK",
192
245
  u16(0),
@@ -1391,6 +1444,7 @@ async function buildPptxText(args, exec, ctx, deck) {
1391
1444
  }
1392
1445
  const slideCount = builds.length;
1393
1446
  if (slideCount === 0) throw new Error("ppt_create needs a title or at least one slide");
1447
+ if (slideCount > 200) throw new Error("too many slides including the title slide (maximum 200)");
1394
1448
  for (const [slideIndex, build] of builds.entries()) {
1395
1449
  const images = build.spec.images ?? [];
1396
1450
  build.images = [];
@@ -1465,7 +1519,7 @@ function sketchSlide(widthIn, heightIn, elements) {
1465
1519
  }
1466
1520
  };
1467
1521
  for (const element of elements) {
1468
- const label = element.type === "image" ? "IMG" : (element.text ?? element.type).split(/\s+/)[0]?.slice(0, 10) || element.type;
1522
+ const label = element.type === "image" ? "IMG" : element.type === "graphicFrame" ? "FRAME" : (element.text ?? element.type).split(/\s+/)[0]?.slice(0, 10) || element.type;
1469
1523
  drawRect(element.xIn, element.yIn, element.wIn, element.hIn, label);
1470
1524
  }
1471
1525
  const border = [];
@@ -1532,16 +1586,12 @@ function extractElements(slideXmlText) {
1532
1586
  };
1533
1587
  if (block.startsWith("<p:pic")) {
1534
1588
  const descr = block.match(PICTURE_DESCR)?.[1];
1535
- elements.push({ type: "image", ...base, alt: descr === void 0 ? void 0 : decodeXmlEntities(descr) });
1589
+ elements.push(descr === void 0 ? { type: "image", ...base } : { type: "image", ...base, alt: decodeXmlEntities(descr) });
1536
1590
  continue;
1537
1591
  }
1538
1592
  if (block.startsWith("<p:graphicFrame")) {
1539
1593
  const table = extractTables(block)[0];
1540
- elements.push({
1541
- type: "table",
1542
- ...base,
1543
- text: table === void 0 ? void 0 : `${table.length}x${table[0]?.length ?? 0}`
1544
- });
1594
+ elements.push(table === void 0 ? { type: "graphicFrame", ...base } : { type: "table", ...base, text: `${table.length}x${table[0]?.length ?? 0}` });
1545
1595
  continue;
1546
1596
  }
1547
1597
  const text = extractParagraphs(stripTables(block), false).join(" | ");
@@ -1593,7 +1643,7 @@ var ELEMENT_SCHEMA = {
1593
1643
  type: "object",
1594
1644
  additionalProperties: false,
1595
1645
  properties: {
1596
- type: { type: "string", required: true, description: "Element kind: text, bullets, image, or table." },
1646
+ type: { type: "string", required: true, description: "Element kind: text, bullets, image, table, or graphicFrame (an OLE object, chart, or other non-table frame)." },
1597
1647
  xIn: { type: "number", required: true, description: "Left edge in inches from the slide origin." },
1598
1648
  yIn: { type: "number", required: true, description: "Top edge in inches from the slide origin." },
1599
1649
  wIn: { type: "number", required: true, description: "Width in inches." },
@@ -44,7 +44,7 @@ interface PptCreateArgs {
44
44
  }
45
45
  /** One placed element, echoed by create and parsed back by read. */
46
46
  export interface SlideElementBox {
47
- type: 'text' | 'bullets' | 'image' | 'table';
47
+ type: 'text' | 'bullets' | 'image' | 'table' | 'graphicFrame';
48
48
  xIn: number;
49
49
  yIn: number;
50
50
  wIn: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-office-tools",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",
@@ -18,14 +18,15 @@
18
18
  "patch": "./cordis.patch.yml"
19
19
  },
20
20
  "compatibility": {
21
- "dsh": ">=0.1.0-rc.6",
21
+ "dsh": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0",
22
22
  "dshReleases": {
23
23
  "0.1.1-rc.2": "compatible",
24
24
  "0.1.2-alpha.4": "compatible",
25
25
  "0.1.2-alpha.5": "compatible",
26
26
  "0.1.2-rc.1": "compatible",
27
27
  "0.1.5-rc.1": "compatible",
28
- "0.1.5-rc.2": "compatible"
28
+ "0.1.5-rc.2": "compatible",
29
+ "0.2.0-rc.1": "compatible"
29
30
  }
30
31
  }
31
32
  },
@@ -48,11 +49,11 @@
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@deepseek-ai/cordis": "^4.0.1",
51
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0",
52
- "@deepseek-ai/dsh-fs": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0",
53
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0",
54
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0",
55
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0"
52
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0",
53
+ "@deepseek-ai/dsh-fs": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0",
54
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0",
55
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0",
56
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-rc.0 || ^0.1.2-alpha.0 || ^0.1.5-alpha.0 || >=0.2.0-rc.1 <0.3.0-0"
56
57
  },
57
58
  "peerDependenciesMeta": {
58
59
  "@deepseek-ai/cordis": {
@@ -87,8 +88,10 @@
87
88
  "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
88
89
  "@deepseek-ai/schemastery": "^3.18.2",
89
90
  "@types/node": "^24.0.0",
91
+ "@types/semver": "7.7.1",
90
92
  "esbuild": "^0.25.0",
91
93
  "jszip": "^3.10.1",
94
+ "semver": "7.8.5",
92
95
  "typescript": "^5.6.0",
93
96
  "vitest": "^4.1.8",
94
97
  "xlsx": "^0.18.5"
@@ -150,7 +153,8 @@
150
153
  "0.1.2-alpha.5",
151
154
  "0.1.2-rc.1",
152
155
  "0.1.5-rc.1",
153
- "0.1.5-rc.2"
156
+ "0.1.5-rc.2",
157
+ "0.2.0-rc.1"
154
158
  ]
155
159
  },
156
160
  "capability": {