@copylee/dsh-office 0.1.0 → 0.2.0
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 +15 -2
- package/helper/OfficeHelper.cs +637 -32
- package/lib/client.js +49 -1
- package/lib/index.js +197 -91
- package/lib/types/format.d.ts +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/settings.d.ts +8 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -37,6 +37,10 @@ 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,
|
|
40
44
|
showOnOpen: true,
|
|
41
45
|
renderWidth: 1100
|
|
42
46
|
};
|
|
@@ -46,6 +50,10 @@ window.__ModuleLoader__.load({
|
|
|
46
50
|
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
51
|
const width = out.renderWidth;
|
|
48
52
|
return {
|
|
53
|
+
follow: typeof out.follow === "boolean" ? out.follow : DEFAULTS.follow,
|
|
54
|
+
typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
|
|
55
|
+
card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
|
|
56
|
+
finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
|
|
49
57
|
showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
|
|
50
58
|
renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
|
|
51
59
|
};
|
|
@@ -478,7 +486,47 @@ window.__ModuleLoader__.load({
|
|
|
478
486
|
} }, APP_NAMES[app.app]), h("span", { style: {
|
|
479
487
|
...S.hint,
|
|
480
488
|
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 }, "
|
|
489
|
+
} }, !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: {
|
|
490
|
+
...S.hint,
|
|
491
|
+
fontWeight: 400
|
|
492
|
+
} }, "AI 改到哪里,窗口就自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不用自己翻找。关闭即静默执行:AI 照常修改,但不动你的视图。")), h(Switch, {
|
|
493
|
+
checked: settings.follow,
|
|
494
|
+
disabled,
|
|
495
|
+
label: "跟随 AI 的修改位置",
|
|
496
|
+
onChange: (value) => {
|
|
497
|
+
change("follow", value);
|
|
498
|
+
}
|
|
499
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "逐字写入", h("span", { style: {
|
|
500
|
+
...S.hint,
|
|
501
|
+
fontWeight: 400
|
|
502
|
+
} }, "文字像打字一样逐步出现,而不是整块贴上去;表格逐行填入。每段最多多花约 1 秒,关闭后写得最快。")), h(Switch, {
|
|
503
|
+
checked: settings.typing,
|
|
504
|
+
disabled,
|
|
505
|
+
label: "逐字写入",
|
|
506
|
+
onChange: (value) => {
|
|
507
|
+
change("typing", value);
|
|
508
|
+
}
|
|
509
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "迷你卡片", h("span", { style: {
|
|
510
|
+
...S.hint,
|
|
511
|
+
fontWeight: 400
|
|
512
|
+
} }, "AI 编辑时在屏幕右下角显示一张小卡片:正在改哪个文档、改到第几项,以及“跟随”“逐字”两个开关,点一下立即生效(连正在进行的这一批也会跟着变)。卡片可拖动,不抢焦点,AI 停手几秒后自动消失。在卡片上切换过的开关,本次运行期间以卡片为准。")), h(Switch, {
|
|
513
|
+
checked: settings.card,
|
|
514
|
+
disabled,
|
|
515
|
+
label: "迷你卡片",
|
|
516
|
+
onChange: (value) => {
|
|
517
|
+
change("card", value);
|
|
518
|
+
}
|
|
519
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "交付前整体视觉检查", h("span", { style: {
|
|
520
|
+
...S.hint,
|
|
521
|
+
fontWeight: 400
|
|
522
|
+
} }, "AI 保存时,如果它改完之后还没看过成品,就自动把文档各页(最多 8 页)渲染成图片交给它过一遍,发现问题先修再交付。会多用一些 token。")), h(Switch, {
|
|
523
|
+
checked: settings.finalCheck,
|
|
524
|
+
disabled,
|
|
525
|
+
label: "交付前整体视觉检查",
|
|
526
|
+
onChange: (value) => {
|
|
527
|
+
change("finalCheck", value);
|
|
528
|
+
}
|
|
529
|
+
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "打开文档时显示到前台", h("span", { style: {
|
|
482
530
|
...S.hint,
|
|
483
531
|
fontWeight: 400
|
|
484
532
|
} }, "关闭后文档仍会打开,只是不抢你当前的窗口。")), h(Switch, {
|
package/lib/index.js
CHANGED
|
@@ -231,7 +231,10 @@ function promptText() {
|
|
|
231
231
|
"- For any .docx / .xlsx / .pptx work, call office_open first, then office_read, then office_edit. Prefer this over writing the file with scripts or libraries: an open file cannot be overwritten, and the user would not see the work.",
|
|
232
232
|
"- Put all the changes you can into one office_edit call; it runs them in order.",
|
|
233
233
|
"- 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
|
-
"-
|
|
234
|
+
"- 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.",
|
|
235
|
+
"- 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.",
|
|
236
|
+
"- In Word, formulas are LaTeX between dollar signs ($...$ inline, $$...$$ on its own line); they become native equations.",
|
|
237
|
+
"- 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
238
|
"- If a call reports the app is busy, the user is typing or has a dialog open: wait a moment and retry.",
|
|
236
239
|
"- 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
240
|
].join("\n");
|
|
@@ -246,6 +249,10 @@ function promptText() {
|
|
|
246
249
|
/** The Loader entry id (settings namespace); see cordis.patch.yml. */
|
|
247
250
|
const ENTRY_ID = "copylee-office";
|
|
248
251
|
const DEFAULTS = {
|
|
252
|
+
follow: true,
|
|
253
|
+
typing: true,
|
|
254
|
+
card: true,
|
|
255
|
+
finalCheck: true,
|
|
249
256
|
showOnOpen: true,
|
|
250
257
|
renderWidth: 1100
|
|
251
258
|
};
|
|
@@ -255,12 +262,16 @@ function resolveConfig(raw) {
|
|
|
255
262
|
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
263
|
const width = out.renderWidth;
|
|
257
264
|
return {
|
|
265
|
+
follow: typeof out.follow === "boolean" ? out.follow : DEFAULTS.follow,
|
|
266
|
+
typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
|
|
267
|
+
card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
|
|
268
|
+
finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
|
|
258
269
|
showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
|
|
259
270
|
renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
|
|
260
271
|
};
|
|
261
272
|
}
|
|
262
273
|
//#endregion
|
|
263
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.2.
|
|
274
|
+
//#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
275
|
/**
|
|
265
276
|
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
266
277
|
*
|
|
@@ -539,7 +550,7 @@ function scopeTarget(base, key) {
|
|
|
539
550
|
return carrier;
|
|
540
551
|
}
|
|
541
552
|
//#endregion
|
|
542
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-typert-
|
|
553
|
+
//#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
554
|
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
544
555
|
/**
|
|
545
556
|
* One Remote call failure: a real Error carrying its stable code and typed
|
|
@@ -699,7 +710,7 @@ function validateName(subject, value) {
|
|
|
699
710
|
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
700
711
|
}
|
|
701
712
|
//#endregion
|
|
702
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-
|
|
713
|
+
//#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
714
|
/** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
|
|
704
715
|
/**
|
|
705
716
|
* Mark an unreachable closed-union branch.
|
|
@@ -911,7 +922,7 @@ function deepFreeze(value) {
|
|
|
911
922
|
return value;
|
|
912
923
|
}
|
|
913
924
|
//#endregion
|
|
914
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-
|
|
925
|
+
//#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
926
|
/**
|
|
916
927
|
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
917
928
|
* @returns the UUID string.
|
|
@@ -947,7 +958,7 @@ function brandString(value) {
|
|
|
947
958
|
return value;
|
|
948
959
|
}
|
|
949
960
|
//#endregion
|
|
950
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.
|
|
961
|
+
//#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
962
|
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
952
963
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
953
964
|
//#endregion
|
|
@@ -2128,7 +2139,7 @@ function adapterFailureChunk(error, signal) {
|
|
|
2128
2139
|
};
|
|
2129
2140
|
}
|
|
2130
2141
|
//#endregion
|
|
2131
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0._2cc6ce11d95db65b014013a78bf96eaa/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
|
|
2142
|
+
//#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
2143
|
/**
|
|
2133
2144
|
* The escalation vocabulary and choreography shared by every sandbox-enforcing
|
|
2134
2145
|
* tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the
|
|
@@ -2214,7 +2225,7 @@ async function approveEscalation(request, approval) {
|
|
|
2214
2225
|
}
|
|
2215
2226
|
}
|
|
2216
2227
|
//#endregion
|
|
2217
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.2.
|
|
2228
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.2.0-rc.2_c6e351a6a6cfd1c2a664558bffcd410d/node_modules/@deepseek-ai/dsh-tools/lib/index.js
|
|
2218
2229
|
/**
|
|
2219
2230
|
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
2220
2231
|
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
@@ -5893,9 +5904,12 @@ function formatWord(read) {
|
|
|
5893
5904
|
return [
|
|
5894
5905
|
`${read.paragraphs} paragraphs, ${read.tables} table(s)${read.pages === void 0 ? "" : `, ${read.pages} page(s)`}.`,
|
|
5895
5906
|
...(read.items ?? []).map((item) => {
|
|
5896
|
-
if (item.
|
|
5907
|
+
if (item.size !== void 0) {
|
|
5908
|
+
const name = item.table === void 0 ? "table inside a cell" : `table ${item.table}`;
|
|
5909
|
+
return `${item.i}–${item.to} [${name}, ${item.size}]${item.open ? " holds the text below; write inside it by paragraph number:" : ""}`;
|
|
5910
|
+
}
|
|
5897
5911
|
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)"}`;
|
|
5912
|
+
return `${item.inTable ? " " : ""}${item.i}${tag ? ` [${tag}]` : ""} ${item.text ? item.text : "(empty)"}`;
|
|
5899
5913
|
}),
|
|
5900
5914
|
...read.more ? [read.more] : []
|
|
5901
5915
|
].join("\n");
|
|
@@ -5925,7 +5939,11 @@ function formatPpt(read) {
|
|
|
5925
5939
|
}
|
|
5926
5940
|
function formatEdit(result) {
|
|
5927
5941
|
const done = result.done.map((line, index) => `${index + 1}. ${line}`);
|
|
5928
|
-
if (!result.failed) return [
|
|
5942
|
+
if (!result.failed) return [
|
|
5943
|
+
`All ${result.total} operation(s) applied (not saved yet):`,
|
|
5944
|
+
...done,
|
|
5945
|
+
"Before you finish, look at the pages you changed with office_render and fix what is off."
|
|
5946
|
+
].join("\n");
|
|
5929
5947
|
const { index, op, error } = result.failed;
|
|
5930
5948
|
return [
|
|
5931
5949
|
`Stopped at operation ${index + 1} (${op}): ${error}`,
|
|
@@ -5941,6 +5959,47 @@ function formatEdit(result) {
|
|
|
5941
5959
|
* instead of one per change.
|
|
5942
5960
|
*/
|
|
5943
5961
|
const { extname, isAbsolute } = win32;
|
|
5962
|
+
const IMAGE = {
|
|
5963
|
+
type: "object",
|
|
5964
|
+
additionalProperties: false,
|
|
5965
|
+
properties: {
|
|
5966
|
+
attachmentId: {
|
|
5967
|
+
type: "string",
|
|
5968
|
+
required: true
|
|
5969
|
+
},
|
|
5970
|
+
mediaType: {
|
|
5971
|
+
type: "string",
|
|
5972
|
+
required: true
|
|
5973
|
+
},
|
|
5974
|
+
bytes: {
|
|
5975
|
+
type: "integer",
|
|
5976
|
+
required: true
|
|
5977
|
+
},
|
|
5978
|
+
width: {
|
|
5979
|
+
type: "integer",
|
|
5980
|
+
required: true
|
|
5981
|
+
},
|
|
5982
|
+
height: {
|
|
5983
|
+
type: "integer",
|
|
5984
|
+
required: true
|
|
5985
|
+
},
|
|
5986
|
+
name: { type: "string" },
|
|
5987
|
+
originalDimensions: {
|
|
5988
|
+
type: "object",
|
|
5989
|
+
additionalProperties: false,
|
|
5990
|
+
properties: {
|
|
5991
|
+
width: {
|
|
5992
|
+
type: "integer",
|
|
5993
|
+
required: true
|
|
5994
|
+
},
|
|
5995
|
+
height: {
|
|
5996
|
+
type: "integer",
|
|
5997
|
+
required: true
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
};
|
|
5944
6003
|
const output = {
|
|
5945
6004
|
schema: {
|
|
5946
6005
|
type: "object",
|
|
@@ -5950,59 +6009,20 @@ const output = {
|
|
|
5950
6009
|
type: "string",
|
|
5951
6010
|
required: true
|
|
5952
6011
|
},
|
|
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
|
-
}
|
|
6012
|
+
image: IMAGE,
|
|
6013
|
+
more: {
|
|
6014
|
+
type: "array",
|
|
6015
|
+
items: IMAGE
|
|
5993
6016
|
}
|
|
5994
6017
|
}
|
|
5995
6018
|
},
|
|
5996
|
-
render: (_args, value) =>
|
|
5997
|
-
type: "text",
|
|
5998
|
-
text: value.text
|
|
5999
|
-
}] : [{
|
|
6019
|
+
render: (_args, value) => [{
|
|
6000
6020
|
type: "text",
|
|
6001
6021
|
text: value.text
|
|
6002
|
-
}, {
|
|
6022
|
+
}, ...[value.image, ...value.more ?? []].filter((image) => image !== void 0).map((image) => ({
|
|
6003
6023
|
type: "image",
|
|
6004
|
-
attachment:
|
|
6005
|
-
}]
|
|
6024
|
+
attachment: image
|
|
6025
|
+
}))]
|
|
6006
6026
|
};
|
|
6007
6027
|
function toJson(ref) {
|
|
6008
6028
|
return {
|
|
@@ -6064,14 +6084,17 @@ const APP = {
|
|
|
6064
6084
|
};
|
|
6065
6085
|
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
6086
|
|
|
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
|
-
|
|
6087
|
+
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).
|
|
6088
|
+
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.
|
|
6089
|
+
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).
|
|
6090
|
+
- 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
6091
|
- set_text {para, expect, text} — rewrite one paragraph, keeping its style. expect is required ("" for an empty paragraph).
|
|
6070
6092
|
- replace_text {find, replace, all?}
|
|
6071
6093
|
- 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
|
|
6094
|
+
- 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.
|
|
6095
|
+
- 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}
|
|
6096
|
+
- 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.
|
|
6097
|
+
- insert_image {path, caption?, para?, where?, width?} — the picture is centred on a line of its own.
|
|
6075
6098
|
|
|
6076
6099
|
Excel ops. sheet = sheet name (default: the active sheet).
|
|
6077
6100
|
- write_range {range: top-left cell, values:[[..],..]} — a string starting with "=" is a formula; also takes the format_range fields.
|
|
@@ -6093,6 +6116,47 @@ function createTools(host) {
|
|
|
6093
6116
|
const { helper } = host;
|
|
6094
6117
|
const tools = [];
|
|
6095
6118
|
let current;
|
|
6119
|
+
const unseen = /* @__PURE__ */ new Set();
|
|
6120
|
+
const keyOf = (on) => `${on.app}:${fileName(on.doc).toLowerCase()}`;
|
|
6121
|
+
/** Pictures of the document as it is now: Word pages or PowerPoint slides first..last, or an Excel range. */
|
|
6122
|
+
const picture = async (on, first, last, extra, width) => {
|
|
6123
|
+
const folder = join(tmpdir(), "dsh-office");
|
|
6124
|
+
await mkdir(folder, { recursive: true });
|
|
6125
|
+
const lines = [];
|
|
6126
|
+
const images = [];
|
|
6127
|
+
let total;
|
|
6128
|
+
for (let number = first; number <= last; number++) {
|
|
6129
|
+
const out = join(folder, `render-${process.pid}-${Date.now()}-${number}.png`);
|
|
6130
|
+
try {
|
|
6131
|
+
const which = on.app === "word" ? { page: number } : on.app === "ppt" ? { slide: number } : {};
|
|
6132
|
+
const result = await helper.call("render", {
|
|
6133
|
+
...on,
|
|
6134
|
+
...extra,
|
|
6135
|
+
...which,
|
|
6136
|
+
out,
|
|
6137
|
+
width
|
|
6138
|
+
}, 5e4);
|
|
6139
|
+
images.push(toJson(await host.saveImage(await readFile(out), `${on.app}-${result.what.replace(/[^\w一-龥]+/g, "-")}.png`)));
|
|
6140
|
+
lines.push(`${APP_NAMES[on.app]} ${result.what}, ${result.width}×${result.height}.`);
|
|
6141
|
+
const of = / of (\d+)$/.exec(result.what);
|
|
6142
|
+
if (of) {
|
|
6143
|
+
total = Number(of[1]);
|
|
6144
|
+
last = Math.min(last, total);
|
|
6145
|
+
}
|
|
6146
|
+
} catch (error) {
|
|
6147
|
+
if (images.length === 0) throw error;
|
|
6148
|
+
if (on.app === "word") lines.push(error instanceof Error ? error.message : String(error));
|
|
6149
|
+
break;
|
|
6150
|
+
} finally {
|
|
6151
|
+
await rm(out, { force: true });
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
return {
|
|
6155
|
+
lines,
|
|
6156
|
+
images,
|
|
6157
|
+
...total === void 0 ? {} : { total }
|
|
6158
|
+
};
|
|
6159
|
+
};
|
|
6096
6160
|
/** The helper arguments naming the document; the app comes from doc / app only, never from a save-as path. */
|
|
6097
6161
|
const target = (args) => {
|
|
6098
6162
|
if (args.doc === void 0 && args.app === void 0) {
|
|
@@ -6236,10 +6300,17 @@ function createTools(host) {
|
|
|
6236
6300
|
const input = args;
|
|
6237
6301
|
if (!Array.isArray(input.ops) || input.ops.length === 0) throw new Error("ops must be a non-empty array.");
|
|
6238
6302
|
if (input.ops.length > 200) throw new Error("At most 200 operations per call.");
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6303
|
+
const { follow, typing, card } = host.settings();
|
|
6304
|
+
const on = target(input);
|
|
6305
|
+
const result = await helper.call("edit", {
|
|
6306
|
+
...on,
|
|
6307
|
+
ops: input.ops,
|
|
6308
|
+
follow,
|
|
6309
|
+
typing,
|
|
6310
|
+
card
|
|
6311
|
+
}, 27e4);
|
|
6312
|
+
if (result.done.length > 0) unseen.add(keyOf(on));
|
|
6313
|
+
return { text: formatEdit(result) };
|
|
6243
6314
|
},
|
|
6244
6315
|
presentCall: (args) => {
|
|
6245
6316
|
const { doc, ops } = args;
|
|
@@ -6248,7 +6319,7 @@ function createTools(host) {
|
|
|
6248
6319
|
}));
|
|
6249
6320
|
tools.push(defineTool({
|
|
6250
6321
|
name: "office_render",
|
|
6251
|
-
description: "Get a picture of how the document really looks
|
|
6322
|
+
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
6323
|
parameters: {
|
|
6253
6324
|
doc: DOC,
|
|
6254
6325
|
app: APP,
|
|
@@ -6260,6 +6331,10 @@ function createTools(host) {
|
|
|
6260
6331
|
type: "integer",
|
|
6261
6332
|
description: "PowerPoint: slide number."
|
|
6262
6333
|
},
|
|
6334
|
+
to: {
|
|
6335
|
+
type: "integer",
|
|
6336
|
+
description: "Word / PowerPoint: last page or slide, to get up to 4 in one call."
|
|
6337
|
+
},
|
|
6263
6338
|
sheet: {
|
|
6264
6339
|
type: "string",
|
|
6265
6340
|
description: "Excel: sheet name."
|
|
@@ -6276,31 +6351,23 @@ function createTools(host) {
|
|
|
6276
6351
|
const on = target(input), kind = on.app;
|
|
6277
6352
|
if (kind === "ppt" && input.slide === void 0) throw new Error("slide is required for PowerPoint.");
|
|
6278
6353
|
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
|
-
}
|
|
6354
|
+
const { doc: _doc, app: _app, to, page: _page, slide: _slide, ...rest } = input;
|
|
6355
|
+
const first = kind === "word" ? input.page ?? 1 : kind === "ppt" ? input.slide : 0;
|
|
6356
|
+
const last = kind === "excel" || to === void 0 ? first : Math.min(Math.max(to, first), first + 3);
|
|
6357
|
+
const { lines, images } = await picture(on, first, last, rest, host.settings().renderWidth);
|
|
6358
|
+
unseen.delete(keyOf(on));
|
|
6359
|
+
const [image, ...more] = images;
|
|
6360
|
+
return {
|
|
6361
|
+
text: lines.join("\n"),
|
|
6362
|
+
...image === void 0 ? {} : { image },
|
|
6363
|
+
...more.length ? { more } : {}
|
|
6364
|
+
};
|
|
6298
6365
|
},
|
|
6299
6366
|
presentCall: (args) => card(`查看 ${fileName(args.doc) || "当前文档"} 的排版`)
|
|
6300
6367
|
}));
|
|
6301
6368
|
tools.push(defineTool({
|
|
6302
6369
|
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).",
|
|
6370
|
+
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
6371
|
parameters: {
|
|
6305
6372
|
doc: DOC,
|
|
6306
6373
|
app: APP,
|
|
@@ -6311,14 +6378,37 @@ function createTools(host) {
|
|
|
6311
6378
|
},
|
|
6312
6379
|
output,
|
|
6313
6380
|
timeoutMs: 12e4,
|
|
6314
|
-
async execute(args) {
|
|
6381
|
+
async execute(args, exec) {
|
|
6315
6382
|
const input = args;
|
|
6316
6383
|
if (input.path !== void 0 && !isAbsolute(input.path)) throw new Error("path must be an absolute path.");
|
|
6384
|
+
const on = target(input);
|
|
6317
6385
|
const result = await helper.call("save", {
|
|
6318
|
-
...
|
|
6386
|
+
...on,
|
|
6319
6387
|
...input.path === void 0 ? {} : { path: input.path }
|
|
6320
6388
|
}, 11e4);
|
|
6321
|
-
|
|
6389
|
+
const moved = input.path !== void 0 && extname(input.path).toLowerCase() !== ".pdf";
|
|
6390
|
+
const wasUnseen = unseen.delete(keyOf(on));
|
|
6391
|
+
if (moved) current = {
|
|
6392
|
+
app: on.app,
|
|
6393
|
+
doc: result.path
|
|
6394
|
+
};
|
|
6395
|
+
const text = `Saved ${result.path} (${result.bytes} bytes).${moved ? " The open document is now this file." : ""}`;
|
|
6396
|
+
if (!wasUnseen || !host.settings().finalCheck || !await host.vision(exec)) return { text };
|
|
6397
|
+
try {
|
|
6398
|
+
const now = moved ? {
|
|
6399
|
+
app: on.app,
|
|
6400
|
+
doc: result.path
|
|
6401
|
+
} : on;
|
|
6402
|
+
const shown = await picture(now, 1, on.app === "excel" ? 1 : 8, {}, Math.min(host.settings().renderWidth, 800));
|
|
6403
|
+
const [image, ...more] = shown.images;
|
|
6404
|
+
return {
|
|
6405
|
+
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.`,
|
|
6406
|
+
...image === void 0 ? {} : { image },
|
|
6407
|
+
...more.length ? { more } : {}
|
|
6408
|
+
};
|
|
6409
|
+
} catch {
|
|
6410
|
+
return { text };
|
|
6411
|
+
}
|
|
6322
6412
|
},
|
|
6323
6413
|
presentCall: (args) => card(`保存 ${fileName(args.path ?? args.doc) || "当前文档"}`)
|
|
6324
6414
|
}));
|
|
@@ -6330,6 +6420,22 @@ const name = "@copylee/dsh-office";
|
|
|
6330
6420
|
const inject = ["tools", "attachments"];
|
|
6331
6421
|
const STATUS_ROUTE = "/api/dsh-office/status";
|
|
6332
6422
|
const Config = z.object({
|
|
6423
|
+
follow: z.boolean().default(true).volatile().i18n({
|
|
6424
|
+
"zh-CN": { $description: "跟随 AI 正在修改的位置(自动滚动到那里);关闭为静默执行,不动你的视图" },
|
|
6425
|
+
"en-US": { $description: "Scroll to where the AI is editing; off = silent, your view is left alone" }
|
|
6426
|
+
}),
|
|
6427
|
+
typing: z.boolean().default(true).volatile().i18n({
|
|
6428
|
+
"zh-CN": { $description: "文字像打字一样逐步写出,而不是整块出现(每段最多约 1 秒)" },
|
|
6429
|
+
"en-US": { $description: "Write text progressively like typing instead of all at once (about a second per paragraph at most)" }
|
|
6430
|
+
}),
|
|
6431
|
+
card: z.boolean().default(true).volatile().i18n({
|
|
6432
|
+
"zh-CN": { $description: "AI 编辑时在屏幕右下角显示迷你卡片,可随时开关“跟随”和“逐字”" },
|
|
6433
|
+
"en-US": { $description: "Show a small card at the bottom right while the AI edits, with switches for following and typing" }
|
|
6434
|
+
}),
|
|
6435
|
+
finalCheck: z.boolean().default(true).volatile().i18n({
|
|
6436
|
+
"zh-CN": { $description: "交付前整体视觉检查:保存时如果 AI 改完后还没看过成品,自动把各页渲染成图片给它检查" },
|
|
6437
|
+
"en-US": { $description: "Whole-document visual check: on save, show the AI every page if it has not looked since its last edits" }
|
|
6438
|
+
}),
|
|
6333
6439
|
showOnOpen: z.boolean().default(true).volatile().i18n({
|
|
6334
6440
|
"zh-CN": { $description: "打开文档时把它的窗口带到前台" },
|
|
6335
6441
|
"en-US": { $description: "Bring the document window to the front when it is opened" }
|
package/lib/types/format.d.ts
CHANGED
package/lib/types/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ 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;
|
|
8
12
|
showOnOpen?: boolean;
|
|
9
13
|
renderWidth?: number;
|
|
10
14
|
}
|
package/lib/types/settings.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
/** The Loader entry id (settings namespace); see cordis.patch.yml. */
|
|
7
7
|
export declare const ENTRY_ID = "copylee-office";
|
|
8
8
|
export interface Settings {
|
|
9
|
+
/** Scroll the window to where the agent is working; off = silent, the user's view is left alone. */
|
|
10
|
+
follow: boolean;
|
|
11
|
+
/** Write text a few characters at a time, the way a person types. */
|
|
12
|
+
typing: boolean;
|
|
13
|
+
/** Show the small card with the two switches while the agent edits. */
|
|
14
|
+
card: boolean;
|
|
15
|
+
/** On save, show the model every page as it is now if it has not looked since its last edits. */
|
|
16
|
+
finalCheck: boolean;
|
|
9
17
|
/** Bring the document's window to the front when office_open opens it. */
|
|
10
18
|
showOnOpen: boolean;
|
|
11
19
|
/** Width in pixels of the pictures office_render gives the model. */
|
package/package.json
CHANGED