@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/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 }, "打开文档时显示到前台", h("span", { style: {
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 .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.",
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
- "- After editing, check the layout with office_render when it matters (slides, formatted pages), then office_save. Nothing is saved until you call office_save; do not close the user's documents.",
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._0d6b19188928ab894325adc32e18b0b5/node_modules/@deepseek-ai/dsh-scope/lib/index.js
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-pro_0ed6bf01f07b8aa74e2681abf2790db7/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
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-value_a1b445b943dd6f2898d88d6b9ffb1320/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
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-crypt_566393aaceb27aff7afe31ca52cb276c/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
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._b8424359025b5f3ee36fc59cc7db98be/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
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._0055658947bc774f21f98df07f5a8468/node_modules/@deepseek-ai/dsh-tools/lib/index.js
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.table !== void 0) return `${item.i}–${item.to} [table ${item.table}, ${item.size}]`;
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 [`All ${result.total} operation(s) applied (not saved yet):`, ...done].join("\n");
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
- type: "object",
5955
- additionalProperties: false,
5956
- properties: {
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) => value.image === void 0 ? [{
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: value.image
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
- - 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. A newline in text starts another paragraph. Items also take the format_text fields.
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?}; set_cell {table, row, col, text}
6074
- - insert_image {path, para?, where?, width?}
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: host.settings().showOnOpen
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
- return { text: formatEdit(await helper.call("edit", {
6240
- ...target(input),
6241
- ops: input.ops
6242
- }, 27e4)) };
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, to check layout after editing: a Word page, a PowerPoint slide, or an Excel range (default: the used range). Cheaper and sharper than a screenshot, and works with the window in the background.",
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 folder = join(tmpdir(), "dsh-office");
6280
- await mkdir(folder, { recursive: true });
6281
- const out = join(folder, `render-${process.pid}-${Date.now()}.png`);
6282
- try {
6283
- const { doc: _doc, app: _app, ...rest } = input;
6284
- const result = await helper.call("render", {
6285
- ...on,
6286
- ...rest,
6287
- out,
6288
- width: host.settings().renderWidth
6289
- }, 5e4);
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
- ...target(input),
6411
+ ...on,
6319
6412
  ...input.path === void 0 ? {} : { path: input.path }
6320
6413
  }, 11e4);
6321
- return { text: `Saved ${result.path} (${result.bytes} bytes).` };
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;
@@ -29,6 +29,8 @@ export interface WordItem {
29
29
  to?: number;
30
30
  table?: number;
31
31
  size?: string;
32
+ open?: boolean;
33
+ inTable?: boolean;
32
34
  }
33
35
  export interface WordRead {
34
36
  paragraphs: number;
@@ -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
  }