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 +3 -1
- package/README.zh.md +3 -1
- package/dsh.plugin.json +2 -2
- package/lib/index.js +93 -43
- package/lib/types/tools/ppt.d.ts +1 -1
- package/package.json +13 -9
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
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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:
|
|
95
|
-
crc
|
|
128
|
+
content: part.content + "\n".repeat(pad) + suffix,
|
|
129
|
+
crc,
|
|
96
130
|
offset,
|
|
97
|
-
localExtraLength:
|
|
131
|
+
localExtraLength: extraLength,
|
|
98
132
|
commentLength: 0,
|
|
99
133
|
centralExtraLength: 0
|
|
100
134
|
});
|
|
101
|
-
offset
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 >
|
|
129
|
-
throw new Error(`an ASCII-safe package holds 1
|
|
130
|
-
}
|
|
131
|
-
const
|
|
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
|
|
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." },
|
package/lib/types/tools/ppt.d.ts
CHANGED
|
@@ -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.
|
|
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": "
|
|
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": {
|