@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/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 }, "打开文档时显示到前台", h("span", { style: {
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
- "- 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.",
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._0d6b19188928ab894325adc32e18b0b5/node_modules/@deepseek-ai/dsh-scope/lib/index.js
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-pro_0ed6bf01f07b8aa74e2681abf2790db7/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
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-value_a1b445b943dd6f2898d88d6b9ffb1320/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
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-crypt_566393aaceb27aff7afe31ca52cb276c/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
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._b8424359025b5f3ee36fc59cc7db98be/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
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._0055658947bc774f21f98df07f5a8468/node_modules/@deepseek-ai/dsh-tools/lib/index.js
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.table !== void 0) return `${item.i}–${item.to} [table ${item.table}, ${item.size}]`;
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 [`All ${result.total} operation(s) applied (not saved yet):`, ...done].join("\n");
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
- 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
- }
6012
+ image: IMAGE,
6013
+ more: {
6014
+ type: "array",
6015
+ items: IMAGE
5993
6016
  }
5994
6017
  }
5995
6018
  },
5996
- render: (_args, value) => value.image === void 0 ? [{
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: value.image
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
- - 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.
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?}; set_cell {table, row, col, text}
6074
- - insert_image {path, para?, where?, width?}
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
- return { text: formatEdit(await helper.call("edit", {
6240
- ...target(input),
6241
- ops: input.ops
6242
- }, 27e4)) };
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, 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.",
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 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
- }
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
- ...target(input),
6386
+ ...on,
6319
6387
  ...input.path === void 0 ? {} : { path: input.path }
6320
6388
  }, 11e4);
6321
- return { text: `Saved ${result.path} (${result.bytes} bytes).` };
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" }
@@ -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,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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copylee/dsh-office",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "DeepSeek Harness Office 插件(Windows):AI 直接在你打开的 Word / Excel / PowerPoint 里改文档,改动实时出现在窗口中,你可以同时编辑;读结构、批量编辑、渲染成图核对、保存。",
5
5
  "type": "module",
6
6
  "author": "copylee",