@copylee/dsh-office 0.1.0 → 0.2.1
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 +21 -21
- package/README.md +17 -2
- package/helper/OfficeHelper.cs +649 -33
- package/lib/client.js +61 -1
- package/lib/index.js +251 -94
- package/lib/types/format.d.ts +2 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/prompt.d.ts +4 -0
- package/lib/types/settings.d.ts +10 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -37,6 +37,11 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
/** The Loader entry id (settings namespace); see cordis.patch.yml. */
|
|
38
38
|
const ENTRY_ID = "copylee-office";
|
|
39
39
|
const DEFAULTS = {
|
|
40
|
+
follow: true,
|
|
41
|
+
typing: true,
|
|
42
|
+
card: true,
|
|
43
|
+
finalCheck: true,
|
|
44
|
+
preferLive: true,
|
|
40
45
|
showOnOpen: true,
|
|
41
46
|
renderWidth: 1100
|
|
42
47
|
};
|
|
@@ -46,6 +51,11 @@ window.__ModuleLoader__.load({
|
|
|
46
51
|
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) out[key] = value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
47
52
|
const width = out.renderWidth;
|
|
48
53
|
return {
|
|
54
|
+
follow: typeof out.follow === "boolean" ? out.follow : DEFAULTS.follow,
|
|
55
|
+
typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
|
|
56
|
+
card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
|
|
57
|
+
finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
|
|
58
|
+
preferLive: typeof out.preferLive === "boolean" ? out.preferLive : DEFAULTS.preferLive,
|
|
49
59
|
showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
|
|
50
60
|
renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
|
|
51
61
|
};
|
|
@@ -478,7 +488,57 @@ window.__ModuleLoader__.load({
|
|
|
478
488
|
} }, APP_NAMES[app.app]), h("span", { style: {
|
|
479
489
|
...S.hint,
|
|
480
490
|
fontSize: 13
|
|
481
|
-
} }, !app.installed ? "未安装" : !app.running ? "已安装,未运行(需要时自动启动)" : app.documents.length === 0 ? "运行中,没有打开的文档" : `运行中:${app.documents.map((doc) => doc.name + (doc.saved ? "" : "(未保存)")).join("、")}`))), h("p", { style: S.hint }, "需要本机装有微软 Office。Word 里 AI 的一批修改可以用一次 Ctrl+Z 撤回;Excel 和 PowerPoint 的修改无法用 Ctrl+Z 撤回。你正在单元格里输入或开着对话框时,AI 会等你。")), h("section", { style: S.card }, h("h3", { style: S.cardTitle }, "行为"), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "
|
|
491
|
+
} }, !app.installed ? "未安装" : !app.running ? "已安装,未运行(需要时自动启动)" : app.documents.length === 0 ? "运行中,没有打开的文档" : `运行中:${app.documents.map((doc) => doc.name + (doc.saved ? "" : "(未保存)")).join("、")}`))), h("p", { style: S.hint }, "需要本机装有微软 Office。Word 里 AI 的一批修改可以用一次 Ctrl+Z 撤回;Excel 和 PowerPoint 的修改无法用 Ctrl+Z 撤回。你正在单元格里输入或开着对话框时,AI 会等你。")), h("section", { style: S.card }, h("h3", { style: S.cardTitle }, "行为"), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "跟随 AI 的修改位置", h("span", { style: {
|
|
492
|
+
...S.hint,
|
|
493
|
+
fontWeight: 400
|
|
494
|
+
} }, "AI 改到哪里,窗口就自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不用自己翻找。关闭即静默执行:AI 照常修改,但不动你的视图。")), h(Switch, {
|
|
495
|
+
checked: settings.follow,
|
|
496
|
+
disabled,
|
|
497
|
+
label: "跟随 AI 的修改位置",
|
|
498
|
+
onChange: (value) => {
|
|
499
|
+
change("follow", value);
|
|
500
|
+
}
|
|
501
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "逐字写入", h("span", { style: {
|
|
502
|
+
...S.hint,
|
|
503
|
+
fontWeight: 400
|
|
504
|
+
} }, "文字像打字一样逐步出现,而不是整块贴上去;表格逐行填入。每段最多多花约 1 秒,关闭后写得最快。")), h(Switch, {
|
|
505
|
+
checked: settings.typing,
|
|
506
|
+
disabled,
|
|
507
|
+
label: "逐字写入",
|
|
508
|
+
onChange: (value) => {
|
|
509
|
+
change("typing", value);
|
|
510
|
+
}
|
|
511
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "迷你卡片", h("span", { style: {
|
|
512
|
+
...S.hint,
|
|
513
|
+
fontWeight: 400
|
|
514
|
+
} }, "AI 编辑时在屏幕右下角显示一张小卡片:正在改哪个文档、改到第几项,以及“跟随”“逐字”两个开关,点一下立即生效(连正在进行的这一批也会跟着变)。卡片可拖动,不抢焦点,AI 停手几秒后自动消失。在卡片上切换过的开关,本次运行期间以卡片为准。")), h(Switch, {
|
|
515
|
+
checked: settings.card,
|
|
516
|
+
disabled,
|
|
517
|
+
label: "迷你卡片",
|
|
518
|
+
onChange: (value) => {
|
|
519
|
+
change("card", value);
|
|
520
|
+
}
|
|
521
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "交付前整体视觉检查", h("span", { style: {
|
|
522
|
+
...S.hint,
|
|
523
|
+
fontWeight: 400
|
|
524
|
+
} }, "AI 保存时,如果它改完之后还没看过成品,就自动把文档各页(最多 8 页)渲染成图片交给它过一遍,发现问题先修再交付。会多用一些 token。")), h(Switch, {
|
|
525
|
+
checked: settings.finalCheck,
|
|
526
|
+
disabled,
|
|
527
|
+
label: "交付前整体视觉检查",
|
|
528
|
+
onChange: (value) => {
|
|
529
|
+
change("finalCheck", value);
|
|
530
|
+
}
|
|
531
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "优先在真实 Office 里操作", h("span", { style: {
|
|
532
|
+
...S.hint,
|
|
533
|
+
fontWeight: 400
|
|
534
|
+
} }, "DSH 自带的 office-docx / office-xlsx / office-pptx 技能是用脚本生成文件,你看不到过程,公式和排版也由脚本决定。开启后,AI 第一次想加载这些技能时会被引回本插件(对应的 Office 应用已安装时才拦);它确有需要而再次请求时放行。")), h(Switch, {
|
|
535
|
+
checked: settings.preferLive,
|
|
536
|
+
disabled,
|
|
537
|
+
label: "优先在真实 Office 里操作",
|
|
538
|
+
onChange: (value) => {
|
|
539
|
+
change("preferLive", value);
|
|
540
|
+
}
|
|
541
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "打开文档时显示到前台", h("span", { style: {
|
|
482
542
|
...S.hint,
|
|
483
543
|
fontWeight: 400
|
|
484
544
|
} }, "关闭后文档仍会打开,只是不抢你当前的窗口。")), h(Switch, {
|
package/lib/index.js
CHANGED
|
@@ -223,15 +223,37 @@ var HelperClient = class {
|
|
|
223
223
|
};
|
|
224
224
|
//#endregion
|
|
225
225
|
//#region lib/types/prompt.js
|
|
226
|
+
/** The host's file-based Office skills, and the app each one stands in for. */
|
|
227
|
+
const FILE_SKILLS = {
|
|
228
|
+
"office-docx": "word",
|
|
229
|
+
"office-xlsx": "excel",
|
|
230
|
+
"office-pptx": "ppt"
|
|
231
|
+
};
|
|
232
|
+
const APPS = {
|
|
233
|
+
word: "Microsoft Word",
|
|
234
|
+
excel: "Microsoft Excel",
|
|
235
|
+
ppt: "Microsoft PowerPoint"
|
|
236
|
+
};
|
|
237
|
+
/** What the model is told the first time it reaches for one of those skills. */
|
|
238
|
+
function redirectText(skill, app) {
|
|
239
|
+
return [
|
|
240
|
+
`Not loaded: ${APPS[app]} is installed on this computer, so do this work inside it with the office_ tools instead of building the file with a script.`,
|
|
241
|
+
"Call office_open with the absolute path (a path that does not exist yet creates the document; the app is started for you if it is not running), then office_read, office_edit, office_render, office_save. The user watches the document take shape in the window and can edit alongside; formulas, captions and layout are done by the app itself.",
|
|
242
|
+
`Only if the task truly cannot be done that way (converting many files in bulk, a file the app cannot open, no document to show), call skill "${skill}" again and it will load.`
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
226
245
|
/** System-prompt section: when to reach for the office_ tools, in as few tokens as will do. */
|
|
227
246
|
function promptText() {
|
|
228
247
|
return [
|
|
229
248
|
"# Office documents (Word, Excel, PowerPoint)",
|
|
230
249
|
"The office_ tools work inside the real Office apps on this computer: the document is open in its window, every change you make appears there at once, and the user can read and edit alongside you.",
|
|
231
|
-
"- For any
|
|
250
|
+
"- For any Word, Excel or PowerPoint work, creating a new document included, start with office_open (it starts the app if it is not running, and a path that does not exist yet creates the file), then office_read and office_edit. Do not load the office-docx / office-xlsx / office-pptx skills and do not write the file with a script: an open file cannot be overwritten, and the user would not see the work.",
|
|
232
251
|
"- Put all the changes you can into one office_edit call; it runs them in order.",
|
|
233
252
|
"- The user may edit while you work. Read again before editing a part you have not just read, and pass \"expect\" on Word paragraphs.",
|
|
234
|
-
"-
|
|
253
|
+
"- Use these tools, not python-docx / openpyxl / python-pptx or other file libraries, whenever the document is to be worked on in the user's Office app or is already open there; and check the result with office_render, not by converting the file with another renderer, which lays it out differently from Word.",
|
|
254
|
+
"- Finish with a polish pass, it is part of the job: office_render every page or slide you changed, look at the pictures the way a careful reader would, fix what is off with office_edit, and render again until it is right. Look for: content outside the frame or section it belongs to; fonts, sizes and line spacing that differ from the surrounding text or from what the template asks for; formulas left as plain text; tables that are too wide, uneven or uncentred; captions not next to their table or figure; leftover template hints and sample text; runs of blank paragraphs and half-empty pages. Then office_save. Nothing is saved until you call office_save; do not close the user's documents.",
|
|
255
|
+
"- In Word, formulas are LaTeX between dollar signs ($...$ inline, $$...$$ on its own line); they become native equations.",
|
|
256
|
+
"- When filling in a template, keep its structure: put each part where its heading is, replace or delete the sample text and hints, and match the formatting the template asks for.",
|
|
235
257
|
"- If a call reports the app is busy, the user is typing or has a dialog open: wait a moment and retry.",
|
|
236
258
|
"- If you also have mouse / keyboard computer tools, use them on an Office window only for what office_edit cannot do, such as a setting that exists only in a dialog."
|
|
237
259
|
].join("\n");
|
|
@@ -246,6 +268,11 @@ function promptText() {
|
|
|
246
268
|
/** The Loader entry id (settings namespace); see cordis.patch.yml. */
|
|
247
269
|
const ENTRY_ID = "copylee-office";
|
|
248
270
|
const DEFAULTS = {
|
|
271
|
+
follow: true,
|
|
272
|
+
typing: true,
|
|
273
|
+
card: true,
|
|
274
|
+
finalCheck: true,
|
|
275
|
+
preferLive: true,
|
|
249
276
|
showOnOpen: true,
|
|
250
277
|
renderWidth: 1100
|
|
251
278
|
};
|
|
@@ -255,12 +282,17 @@ function resolveConfig(raw) {
|
|
|
255
282
|
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) out[key] = value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
256
283
|
const width = out.renderWidth;
|
|
257
284
|
return {
|
|
285
|
+
follow: typeof out.follow === "boolean" ? out.follow : DEFAULTS.follow,
|
|
286
|
+
typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
|
|
287
|
+
card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
|
|
288
|
+
finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
|
|
289
|
+
preferLive: typeof out.preferLive === "boolean" ? out.preferLive : DEFAULTS.preferLive,
|
|
258
290
|
showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
|
|
259
291
|
renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
|
|
260
292
|
};
|
|
261
293
|
}
|
|
262
294
|
//#endregion
|
|
263
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.2.
|
|
295
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-invariants_0d6b19188928ab894325adc32e18b0b5/node_modules/@deepseek-ai/dsh-scope/lib/index.js
|
|
264
296
|
/**
|
|
265
297
|
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
266
298
|
*
|
|
@@ -539,7 +571,7 @@ function scopeTarget(base, key) {
|
|
|
539
571
|
return carrier;
|
|
540
572
|
}
|
|
541
573
|
//#endregion
|
|
542
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-
|
|
574
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
|
|
543
575
|
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
544
576
|
/**
|
|
545
577
|
* One Remote call failure: a real Error carrying its stable code and typed
|
|
@@ -699,7 +731,7 @@ function validateName(subject, value) {
|
|
|
699
731
|
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
700
732
|
}
|
|
701
733
|
//#endregion
|
|
702
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-
|
|
734
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
|
|
703
735
|
/** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
|
|
704
736
|
/**
|
|
705
737
|
* Mark an unreachable closed-union branch.
|
|
@@ -911,7 +943,7 @@ function deepFreeze(value) {
|
|
|
911
943
|
return value;
|
|
912
944
|
}
|
|
913
945
|
//#endregion
|
|
914
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-
|
|
946
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
|
|
915
947
|
/**
|
|
916
948
|
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
917
949
|
* @returns the UUID string.
|
|
@@ -947,7 +979,7 @@ function brandString(value) {
|
|
|
947
979
|
return value;
|
|
948
980
|
}
|
|
949
981
|
//#endregion
|
|
950
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.
|
|
982
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
|
|
951
983
|
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
952
984
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
953
985
|
//#endregion
|
|
@@ -2128,7 +2160,7 @@ function adapterFailureChunk(error, signal) {
|
|
|
2128
2160
|
};
|
|
2129
2161
|
}
|
|
2130
2162
|
//#endregion
|
|
2131
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0._2cc6ce11d95db65b014013a78bf96eaa/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
|
|
2163
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-llm@0.2._2cc6ce11d95db65b014013a78bf96eaa/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
|
|
2132
2164
|
/**
|
|
2133
2165
|
* The escalation vocabulary and choreography shared by every sandbox-enforcing
|
|
2134
2166
|
* tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the
|
|
@@ -2214,7 +2246,7 @@ async function approveEscalation(request, approval) {
|
|
|
2214
2246
|
}
|
|
2215
2247
|
}
|
|
2216
2248
|
//#endregion
|
|
2217
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.2.
|
|
2249
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.2.0-rc.2_c6e351a6a6cfd1c2a664558bffcd410d/node_modules/@deepseek-ai/dsh-tools/lib/index.js
|
|
2218
2250
|
/**
|
|
2219
2251
|
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
2220
2252
|
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
@@ -5864,7 +5896,7 @@ function formatStatus(apps) {
|
|
|
5864
5896
|
continue;
|
|
5865
5897
|
}
|
|
5866
5898
|
if (!app.running) {
|
|
5867
|
-
lines.push(`${name}: not running`);
|
|
5899
|
+
lines.push(`${name}: installed, not running (office_open starts it)`);
|
|
5868
5900
|
continue;
|
|
5869
5901
|
}
|
|
5870
5902
|
if (app.error) {
|
|
@@ -5893,9 +5925,12 @@ function formatWord(read) {
|
|
|
5893
5925
|
return [
|
|
5894
5926
|
`${read.paragraphs} paragraphs, ${read.tables} table(s)${read.pages === void 0 ? "" : `, ${read.pages} page(s)`}.`,
|
|
5895
5927
|
...(read.items ?? []).map((item) => {
|
|
5896
|
-
if (item.
|
|
5928
|
+
if (item.size !== void 0) {
|
|
5929
|
+
const name = item.table === void 0 ? "table inside a cell" : `table ${item.table}`;
|
|
5930
|
+
return `${item.i}–${item.to} [${name}, ${item.size}]${item.open ? " holds the text below; write inside it by paragraph number:" : ""}`;
|
|
5931
|
+
}
|
|
5897
5932
|
const tag = item.level !== void 0 ? `H${item.level}` : item.style !== void 0 && !BODY_STYLES.has(item.style.toLowerCase()) ? item.style : "";
|
|
5898
|
-
return `${item.i}${tag ? ` [${tag}]` : ""} ${item.text ? item.text : "(empty)"}`;
|
|
5933
|
+
return `${item.inTable ? " " : ""}${item.i}${tag ? ` [${tag}]` : ""} ${item.text ? item.text : "(empty)"}`;
|
|
5899
5934
|
}),
|
|
5900
5935
|
...read.more ? [read.more] : []
|
|
5901
5936
|
].join("\n");
|
|
@@ -5925,7 +5960,11 @@ function formatPpt(read) {
|
|
|
5925
5960
|
}
|
|
5926
5961
|
function formatEdit(result) {
|
|
5927
5962
|
const done = result.done.map((line, index) => `${index + 1}. ${line}`);
|
|
5928
|
-
if (!result.failed) return [
|
|
5963
|
+
if (!result.failed) return [
|
|
5964
|
+
`All ${result.total} operation(s) applied (not saved yet):`,
|
|
5965
|
+
...done,
|
|
5966
|
+
"Before you finish, look at the pages you changed with office_render and fix what is off."
|
|
5967
|
+
].join("\n");
|
|
5929
5968
|
const { index, op, error } = result.failed;
|
|
5930
5969
|
return [
|
|
5931
5970
|
`Stopped at operation ${index + 1} (${op}): ${error}`,
|
|
@@ -5941,6 +5980,47 @@ function formatEdit(result) {
|
|
|
5941
5980
|
* instead of one per change.
|
|
5942
5981
|
*/
|
|
5943
5982
|
const { extname, isAbsolute } = win32;
|
|
5983
|
+
const IMAGE = {
|
|
5984
|
+
type: "object",
|
|
5985
|
+
additionalProperties: false,
|
|
5986
|
+
properties: {
|
|
5987
|
+
attachmentId: {
|
|
5988
|
+
type: "string",
|
|
5989
|
+
required: true
|
|
5990
|
+
},
|
|
5991
|
+
mediaType: {
|
|
5992
|
+
type: "string",
|
|
5993
|
+
required: true
|
|
5994
|
+
},
|
|
5995
|
+
bytes: {
|
|
5996
|
+
type: "integer",
|
|
5997
|
+
required: true
|
|
5998
|
+
},
|
|
5999
|
+
width: {
|
|
6000
|
+
type: "integer",
|
|
6001
|
+
required: true
|
|
6002
|
+
},
|
|
6003
|
+
height: {
|
|
6004
|
+
type: "integer",
|
|
6005
|
+
required: true
|
|
6006
|
+
},
|
|
6007
|
+
name: { type: "string" },
|
|
6008
|
+
originalDimensions: {
|
|
6009
|
+
type: "object",
|
|
6010
|
+
additionalProperties: false,
|
|
6011
|
+
properties: {
|
|
6012
|
+
width: {
|
|
6013
|
+
type: "integer",
|
|
6014
|
+
required: true
|
|
6015
|
+
},
|
|
6016
|
+
height: {
|
|
6017
|
+
type: "integer",
|
|
6018
|
+
required: true
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
};
|
|
5944
6024
|
const output = {
|
|
5945
6025
|
schema: {
|
|
5946
6026
|
type: "object",
|
|
@@ -5950,59 +6030,20 @@ const output = {
|
|
|
5950
6030
|
type: "string",
|
|
5951
6031
|
required: true
|
|
5952
6032
|
},
|
|
5953
|
-
image:
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
attachmentId: {
|
|
5958
|
-
type: "string",
|
|
5959
|
-
required: true
|
|
5960
|
-
},
|
|
5961
|
-
mediaType: {
|
|
5962
|
-
type: "string",
|
|
5963
|
-
required: true
|
|
5964
|
-
},
|
|
5965
|
-
bytes: {
|
|
5966
|
-
type: "integer",
|
|
5967
|
-
required: true
|
|
5968
|
-
},
|
|
5969
|
-
width: {
|
|
5970
|
-
type: "integer",
|
|
5971
|
-
required: true
|
|
5972
|
-
},
|
|
5973
|
-
height: {
|
|
5974
|
-
type: "integer",
|
|
5975
|
-
required: true
|
|
5976
|
-
},
|
|
5977
|
-
name: { type: "string" },
|
|
5978
|
-
originalDimensions: {
|
|
5979
|
-
type: "object",
|
|
5980
|
-
additionalProperties: false,
|
|
5981
|
-
properties: {
|
|
5982
|
-
width: {
|
|
5983
|
-
type: "integer",
|
|
5984
|
-
required: true
|
|
5985
|
-
},
|
|
5986
|
-
height: {
|
|
5987
|
-
type: "integer",
|
|
5988
|
-
required: true
|
|
5989
|
-
}
|
|
5990
|
-
}
|
|
5991
|
-
}
|
|
5992
|
-
}
|
|
6033
|
+
image: IMAGE,
|
|
6034
|
+
more: {
|
|
6035
|
+
type: "array",
|
|
6036
|
+
items: IMAGE
|
|
5993
6037
|
}
|
|
5994
6038
|
}
|
|
5995
6039
|
},
|
|
5996
|
-
render: (_args, value) =>
|
|
6040
|
+
render: (_args, value) => [{
|
|
5997
6041
|
type: "text",
|
|
5998
6042
|
text: value.text
|
|
5999
|
-
}]
|
|
6000
|
-
type: "text",
|
|
6001
|
-
text: value.text
|
|
6002
|
-
}, {
|
|
6043
|
+
}, ...[value.image, ...value.more ?? []].filter((image) => image !== void 0).map((image) => ({
|
|
6003
6044
|
type: "image",
|
|
6004
|
-
attachment:
|
|
6005
|
-
}]
|
|
6045
|
+
attachment: image
|
|
6046
|
+
}))]
|
|
6006
6047
|
};
|
|
6007
6048
|
function toJson(ref) {
|
|
6008
6049
|
return {
|
|
@@ -6064,14 +6105,17 @@ const APP = {
|
|
|
6064
6105
|
};
|
|
6065
6106
|
const EDIT_GUIDE = `Edit a document that is open in Word, Excel or PowerPoint, live: each change appears in the window, and the user can keep working in it. Runs ops in order and stops at the first that fails. Does not save (use office_save). Word edits are one undo step for the user; Excel and PowerPoint edits cannot be undone with Ctrl+Z.
|
|
6066
6107
|
|
|
6067
|
-
Word ops. para = paragraph number from office_read; pass expect = its first words so a number made stale by other edits is caught. where = after (default with para) | before | start | end (default without para).
|
|
6068
|
-
|
|
6108
|
+
Word ops. para = paragraph number from office_read; pass expect = its first words so a number made stale by other edits is caught. where = after (default with para) | before | start | end (default without para: the very end of the document).
|
|
6109
|
+
In a form or template whose sections are table cells, write INSIDE the cell: give para = a paragraph of that cell. Without para the text or table lands after everything, outside the form.
|
|
6110
|
+
Formulas: write LaTeX between dollar signs in any text: $...$ inside a sentence, $$...$$ as a paragraph of its own. Number a display formula when the document numbers its formulas by ending it with \\tag{1}: $$T=2\\pi\\sqrt{l/g} \\tag{1}$$ puts (1) flush right. They become native Word equations (\\frac, \\sqrt, ^, _, Greek letters, \\bar, \\sum, \\int ...). Never write a formula as plain text such as T = 2π√(l/g).
|
|
6111
|
+
- insert_paragraphs {items:[{text, style?}], para?, expect?, where?} — style: Normal, Title, Heading 1..6, List Bullet, List Number, Quote, or a style name the document has. Without style a paragraph is body text in the font, size and spacing of the text it is placed next to. A newline in text starts another paragraph. Items also take the format_text fields.
|
|
6069
6112
|
- set_text {para, expect, text} — rewrite one paragraph, keeping its style. expect is required ("" for an empty paragraph).
|
|
6070
6113
|
- replace_text {find, replace, all?}
|
|
6071
6114
|
- format_text {para?, to?, find?, style?, font?, size?, bold?, italic?, underline?, color?, align?, firstLineIndent?, spaceBefore?, spaceAfter?, lineSpacing?} — para..to, or the first match of find.
|
|
6072
|
-
- delete_range {para, expect, to?} — expect is required. Paragraph numbers shift after every insert or delete; each result tells you the new numbers.
|
|
6073
|
-
- insert_table {data:[[cell,..],..], para?, where?, header?}
|
|
6074
|
-
- insert_image
|
|
6115
|
+
- delete_range {para, expect, to?} — expect is required; para..to in one operation removes a run of paragraphs (leftover hints, surplus blank lines). Paragraph numbers shift after every insert or delete; each result tells you the new numbers.
|
|
6116
|
+
- insert_table {data:[[cell,..],..], caption?, para?, where?, header?} — with para inside a cell, the table goes inside that cell. set_cell {table, row, col, text}
|
|
6117
|
+
- Captions: give caption (the title only, no "表 1") on insert_table / insert_image. It is set the standard way: numbered automatically ("表 1", "图 1"), centred, above a table and below a figure. Do not write captions as ordinary paragraphs.
|
|
6118
|
+
- insert_image {path, caption?, para?, where?, width?} — the picture is centred on a line of its own.
|
|
6075
6119
|
|
|
6076
6120
|
Excel ops. sheet = sheet name (default: the active sheet).
|
|
6077
6121
|
- write_range {range: top-left cell, values:[[..],..]} — a string starting with "=" is a formula; also takes the format_range fields.
|
|
@@ -6093,6 +6137,47 @@ function createTools(host) {
|
|
|
6093
6137
|
const { helper } = host;
|
|
6094
6138
|
const tools = [];
|
|
6095
6139
|
let current;
|
|
6140
|
+
const unseen = /* @__PURE__ */ new Set();
|
|
6141
|
+
const keyOf = (on) => `${on.app}:${fileName(on.doc).toLowerCase()}`;
|
|
6142
|
+
/** Pictures of the document as it is now: Word pages or PowerPoint slides first..last, or an Excel range. */
|
|
6143
|
+
const picture = async (on, first, last, extra, width) => {
|
|
6144
|
+
const folder = join(tmpdir(), "dsh-office");
|
|
6145
|
+
await mkdir(folder, { recursive: true });
|
|
6146
|
+
const lines = [];
|
|
6147
|
+
const images = [];
|
|
6148
|
+
let total;
|
|
6149
|
+
for (let number = first; number <= last; number++) {
|
|
6150
|
+
const out = join(folder, `render-${process.pid}-${Date.now()}-${number}.png`);
|
|
6151
|
+
try {
|
|
6152
|
+
const which = on.app === "word" ? { page: number } : on.app === "ppt" ? { slide: number } : {};
|
|
6153
|
+
const result = await helper.call("render", {
|
|
6154
|
+
...on,
|
|
6155
|
+
...extra,
|
|
6156
|
+
...which,
|
|
6157
|
+
out,
|
|
6158
|
+
width
|
|
6159
|
+
}, 5e4);
|
|
6160
|
+
images.push(toJson(await host.saveImage(await readFile(out), `${on.app}-${result.what.replace(/[^\w一-龥]+/g, "-")}.png`)));
|
|
6161
|
+
lines.push(`${APP_NAMES[on.app]} ${result.what}, ${result.width}×${result.height}.`);
|
|
6162
|
+
const of = / of (\d+)$/.exec(result.what);
|
|
6163
|
+
if (of) {
|
|
6164
|
+
total = Number(of[1]);
|
|
6165
|
+
last = Math.min(last, total);
|
|
6166
|
+
}
|
|
6167
|
+
} catch (error) {
|
|
6168
|
+
if (images.length === 0) throw error;
|
|
6169
|
+
if (on.app === "word") lines.push(error instanceof Error ? error.message : String(error));
|
|
6170
|
+
break;
|
|
6171
|
+
} finally {
|
|
6172
|
+
await rm(out, { force: true });
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
return {
|
|
6176
|
+
lines,
|
|
6177
|
+
images,
|
|
6178
|
+
...total === void 0 ? {} : { total }
|
|
6179
|
+
};
|
|
6180
|
+
};
|
|
6096
6181
|
/** The helper arguments naming the document; the app comes from doc / app only, never from a save-as path. */
|
|
6097
6182
|
const target = (args) => {
|
|
6098
6183
|
if (args.doc === void 0 && args.app === void 0) {
|
|
@@ -6135,10 +6220,14 @@ function createTools(host) {
|
|
|
6135
6220
|
...app === void 0 ? {} : { app },
|
|
6136
6221
|
...path === void 0 ? {} : { path }
|
|
6137
6222
|
});
|
|
6223
|
+
const { showOnOpen, follow, typing, card } = host.settings();
|
|
6138
6224
|
const doc = await helper.call("open", {
|
|
6139
6225
|
app: kind,
|
|
6140
6226
|
...path === void 0 ? {} : { path },
|
|
6141
|
-
show:
|
|
6227
|
+
show: showOnOpen,
|
|
6228
|
+
follow,
|
|
6229
|
+
typing,
|
|
6230
|
+
card
|
|
6142
6231
|
}, 8e4);
|
|
6143
6232
|
current = {
|
|
6144
6233
|
app: kind,
|
|
@@ -6236,10 +6325,17 @@ function createTools(host) {
|
|
|
6236
6325
|
const input = args;
|
|
6237
6326
|
if (!Array.isArray(input.ops) || input.ops.length === 0) throw new Error("ops must be a non-empty array.");
|
|
6238
6327
|
if (input.ops.length > 200) throw new Error("At most 200 operations per call.");
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6328
|
+
const { follow, typing, card } = host.settings();
|
|
6329
|
+
const on = target(input);
|
|
6330
|
+
const result = await helper.call("edit", {
|
|
6331
|
+
...on,
|
|
6332
|
+
ops: input.ops,
|
|
6333
|
+
follow,
|
|
6334
|
+
typing,
|
|
6335
|
+
card
|
|
6336
|
+
}, 27e4);
|
|
6337
|
+
if (result.done.length > 0) unseen.add(keyOf(on));
|
|
6338
|
+
return { text: formatEdit(result) };
|
|
6243
6339
|
},
|
|
6244
6340
|
presentCall: (args) => {
|
|
6245
6341
|
const { doc, ops } = args;
|
|
@@ -6248,7 +6344,7 @@ function createTools(host) {
|
|
|
6248
6344
|
}));
|
|
6249
6345
|
tools.push(defineTool({
|
|
6250
6346
|
name: "office_render",
|
|
6251
|
-
description: "Get a picture of how the document really looks
|
|
6347
|
+
description: "Get a picture of how the open document really looks: a Word page, a PowerPoint slide (with \"to\": up to 4 in a row), or an Excel range (default: the used range). Use it on every page you changed before you finish: it shows what office_read cannot, such as content that landed outside its frame, a table that is too wide, text that overflows. It pictures the document as Word itself lays it out, unsaved changes included, with the window in the background.",
|
|
6252
6348
|
parameters: {
|
|
6253
6349
|
doc: DOC,
|
|
6254
6350
|
app: APP,
|
|
@@ -6260,6 +6356,10 @@ function createTools(host) {
|
|
|
6260
6356
|
type: "integer",
|
|
6261
6357
|
description: "PowerPoint: slide number."
|
|
6262
6358
|
},
|
|
6359
|
+
to: {
|
|
6360
|
+
type: "integer",
|
|
6361
|
+
description: "Word / PowerPoint: last page or slide, to get up to 4 in one call."
|
|
6362
|
+
},
|
|
6263
6363
|
sheet: {
|
|
6264
6364
|
type: "string",
|
|
6265
6365
|
description: "Excel: sheet name."
|
|
@@ -6276,31 +6376,23 @@ function createTools(host) {
|
|
|
6276
6376
|
const on = target(input), kind = on.app;
|
|
6277
6377
|
if (kind === "ppt" && input.slide === void 0) throw new Error("slide is required for PowerPoint.");
|
|
6278
6378
|
if (!await host.vision(exec)) return { text: "The current model cannot view images; check the result with office_read instead." };
|
|
6279
|
-
const
|
|
6280
|
-
|
|
6281
|
-
const
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
const ref = await host.saveImage(await readFile(out), `${kind}-${result.what.replace(/[^\w一-龥]+/g, "-")}.png`);
|
|
6291
|
-
return {
|
|
6292
|
-
text: `${APP_NAMES[kind]} ${result.what}, ${result.width}×${result.height}.`,
|
|
6293
|
-
image: toJson(ref)
|
|
6294
|
-
};
|
|
6295
|
-
} finally {
|
|
6296
|
-
await rm(out, { force: true });
|
|
6297
|
-
}
|
|
6379
|
+
const { doc: _doc, app: _app, to, page: _page, slide: _slide, ...rest } = input;
|
|
6380
|
+
const first = kind === "word" ? input.page ?? 1 : kind === "ppt" ? input.slide : 0;
|
|
6381
|
+
const last = kind === "excel" || to === void 0 ? first : Math.min(Math.max(to, first), first + 3);
|
|
6382
|
+
const { lines, images } = await picture(on, first, last, rest, host.settings().renderWidth);
|
|
6383
|
+
unseen.delete(keyOf(on));
|
|
6384
|
+
const [image, ...more] = images;
|
|
6385
|
+
return {
|
|
6386
|
+
text: lines.join("\n"),
|
|
6387
|
+
...image === void 0 ? {} : { image },
|
|
6388
|
+
...more.length ? { more } : {}
|
|
6389
|
+
};
|
|
6298
6390
|
},
|
|
6299
6391
|
presentCall: (args) => card(`查看 ${fileName(args.doc) || "当前文档"} 的排版`)
|
|
6300
6392
|
}));
|
|
6301
6393
|
tools.push(defineTool({
|
|
6302
6394
|
name: "office_save",
|
|
6303
|
-
description: "Save the open document. With path: save a copy under another name or format (.pdf exports a PDF and keeps the document as it is; other extensions switch the document to the new file).",
|
|
6395
|
+
description: "Save the open document; if you have edited since you last looked at it, the reply also shows you its pages for a final check. With path: save a copy under another name or format (.pdf exports a PDF and keeps the document as it is; other extensions switch the document to the new file).",
|
|
6304
6396
|
parameters: {
|
|
6305
6397
|
doc: DOC,
|
|
6306
6398
|
app: APP,
|
|
@@ -6311,14 +6403,37 @@ function createTools(host) {
|
|
|
6311
6403
|
},
|
|
6312
6404
|
output,
|
|
6313
6405
|
timeoutMs: 12e4,
|
|
6314
|
-
async execute(args) {
|
|
6406
|
+
async execute(args, exec) {
|
|
6315
6407
|
const input = args;
|
|
6316
6408
|
if (input.path !== void 0 && !isAbsolute(input.path)) throw new Error("path must be an absolute path.");
|
|
6409
|
+
const on = target(input);
|
|
6317
6410
|
const result = await helper.call("save", {
|
|
6318
|
-
...
|
|
6411
|
+
...on,
|
|
6319
6412
|
...input.path === void 0 ? {} : { path: input.path }
|
|
6320
6413
|
}, 11e4);
|
|
6321
|
-
|
|
6414
|
+
const moved = input.path !== void 0 && extname(input.path).toLowerCase() !== ".pdf";
|
|
6415
|
+
const wasUnseen = unseen.delete(keyOf(on));
|
|
6416
|
+
if (moved) current = {
|
|
6417
|
+
app: on.app,
|
|
6418
|
+
doc: result.path
|
|
6419
|
+
};
|
|
6420
|
+
const text = `Saved ${result.path} (${result.bytes} bytes).${moved ? " The open document is now this file." : ""}`;
|
|
6421
|
+
if (!wasUnseen || !host.settings().finalCheck || !await host.vision(exec)) return { text };
|
|
6422
|
+
try {
|
|
6423
|
+
const now = moved ? {
|
|
6424
|
+
app: on.app,
|
|
6425
|
+
doc: result.path
|
|
6426
|
+
} : on;
|
|
6427
|
+
const shown = await picture(now, 1, on.app === "excel" ? 1 : 8, {}, Math.min(host.settings().renderWidth, 800));
|
|
6428
|
+
const [image, ...more] = shown.images;
|
|
6429
|
+
return {
|
|
6430
|
+
text: `${text}\nFinal check: you edited after you last looked at the document, so here is ${on.app === "excel" ? "the sheet" : `${shown.images.length}${shown.total !== void 0 && shown.total > shown.images.length ? ` of ${shown.total}` : ""} ${on.app === "word" ? "page(s)" : "slide(s)"}`} as it is now. Go through every picture before you tell the user it is done. If anything is off (content outside its frame, uneven fonts or spacing, formulas as plain text, bad tables, leftover hints, blank gaps), fix it with office_edit and save again; if all is well, say so.`,
|
|
6431
|
+
...image === void 0 ? {} : { image },
|
|
6432
|
+
...more.length ? { more } : {}
|
|
6433
|
+
};
|
|
6434
|
+
} catch {
|
|
6435
|
+
return { text };
|
|
6436
|
+
}
|
|
6322
6437
|
},
|
|
6323
6438
|
presentCall: (args) => card(`保存 ${fileName(args.path ?? args.doc) || "当前文档"}`)
|
|
6324
6439
|
}));
|
|
@@ -6330,6 +6445,26 @@ const name = "@copylee/dsh-office";
|
|
|
6330
6445
|
const inject = ["tools", "attachments"];
|
|
6331
6446
|
const STATUS_ROUTE = "/api/dsh-office/status";
|
|
6332
6447
|
const Config = z.object({
|
|
6448
|
+
follow: z.boolean().default(true).volatile().i18n({
|
|
6449
|
+
"zh-CN": { $description: "跟随 AI 正在修改的位置(自动滚动到那里);关闭为静默执行,不动你的视图" },
|
|
6450
|
+
"en-US": { $description: "Scroll to where the AI is editing; off = silent, your view is left alone" }
|
|
6451
|
+
}),
|
|
6452
|
+
typing: z.boolean().default(true).volatile().i18n({
|
|
6453
|
+
"zh-CN": { $description: "文字像打字一样逐步写出,而不是整块出现(每段最多约 1 秒)" },
|
|
6454
|
+
"en-US": { $description: "Write text progressively like typing instead of all at once (about a second per paragraph at most)" }
|
|
6455
|
+
}),
|
|
6456
|
+
card: z.boolean().default(true).volatile().i18n({
|
|
6457
|
+
"zh-CN": { $description: "AI 编辑时在屏幕右下角显示迷你卡片,可随时开关“跟随”和“逐字”" },
|
|
6458
|
+
"en-US": { $description: "Show a small card at the bottom right while the AI edits, with switches for following and typing" }
|
|
6459
|
+
}),
|
|
6460
|
+
finalCheck: z.boolean().default(true).volatile().i18n({
|
|
6461
|
+
"zh-CN": { $description: "交付前整体视觉检查:保存时如果 AI 改完后还没看过成品,自动把各页渲染成图片给它检查" },
|
|
6462
|
+
"en-US": { $description: "Whole-document visual check: on save, show the AI every page if it has not looked since its last edits" }
|
|
6463
|
+
}),
|
|
6464
|
+
preferLive: z.boolean().default(true).volatile().i18n({
|
|
6465
|
+
"zh-CN": { $description: "优先在真实 Office 里操作:AI 想加载 DSH 自带的 office-docx / xlsx / pptx 技能(用脚本改文件)时,先把它引回本插件" },
|
|
6466
|
+
"en-US": { $description: "Prefer the real Office app: when the AI reaches for the host's file-based office skills, point it back to this plugin first" }
|
|
6467
|
+
}),
|
|
6333
6468
|
showOnOpen: z.boolean().default(true).volatile().i18n({
|
|
6334
6469
|
"zh-CN": { $description: "打开文档时把它的窗口带到前台" },
|
|
6335
6470
|
"en-US": { $description: "Bring the document window to the front when it is opened" }
|
|
@@ -6379,6 +6514,28 @@ function apply(ctx, config = {}) {
|
|
|
6379
6514
|
}
|
|
6380
6515
|
});
|
|
6381
6516
|
for (const tool of tools) ctx.effect(() => ctx.tools.register(tool), `office: tool ${tool.name}`);
|
|
6517
|
+
let installed;
|
|
6518
|
+
const installedApps = () => {
|
|
6519
|
+
installed ??= helper.call("status", {}, 3e4).then((apps) => new Set(apps.filter((app) => app.installed).map((app) => app.app))).catch(() => {
|
|
6520
|
+
installed = void 0;
|
|
6521
|
+
return /* @__PURE__ */ new Set();
|
|
6522
|
+
});
|
|
6523
|
+
return installed;
|
|
6524
|
+
};
|
|
6525
|
+
const redirected = /* @__PURE__ */ new Set();
|
|
6526
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
6527
|
+
if (exec.name !== "skill" || !settings().preferLive) return next();
|
|
6528
|
+
const skill = String(exec.arguments?.name ?? "");
|
|
6529
|
+
const app = FILE_SKILLS[skill];
|
|
6530
|
+
if (app === void 0) return next();
|
|
6531
|
+
const key = `${exec.agent?.session?.id ?? "default"}:${skill}`;
|
|
6532
|
+
if (redirected.has(key) || !(await installedApps()).has(app)) return next();
|
|
6533
|
+
redirected.add(key);
|
|
6534
|
+
return {
|
|
6535
|
+
kind: "deny",
|
|
6536
|
+
reason: redirectText(skill, app)
|
|
6537
|
+
};
|
|
6538
|
+
});
|
|
6382
6539
|
ctx.inject(["webServer"], (webCtx) => {
|
|
6383
6540
|
const handler = async (_req, res) => {
|
|
6384
6541
|
let body;
|
package/lib/types/format.d.ts
CHANGED
package/lib/types/index.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export declare const name = "@copylee/dsh-office";
|
|
|
5
5
|
export declare const inject: string[];
|
|
6
6
|
export declare const STATUS_ROUTE = "/api/dsh-office/status";
|
|
7
7
|
export interface Config {
|
|
8
|
+
follow?: boolean;
|
|
9
|
+
typing?: boolean;
|
|
10
|
+
card?: boolean;
|
|
11
|
+
finalCheck?: boolean;
|
|
12
|
+
preferLive?: boolean;
|
|
8
13
|
showOnOpen?: boolean;
|
|
9
14
|
renderWidth?: number;
|
|
10
15
|
}
|