@copylee/dsh-office 0.2.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/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  | Word 原生公式 | AI 在文字里用 `$...$`(行内)和 `$$...$$`(单独一行)写 LaTeX,插件转成 Word 原生公式(分式、根号、上下标、希腊字母、求和、积分、上划线等常用写法);单独一行的公式末尾加 `\tag{1}` 即在右侧对齐编号 |
20
20
  | 填模板 | 报告模板常把正文放在大单元格里。这类表格会按段落展开给模型看,正文、公式、表格都写进单元格里面,而不是堆到文档末尾;新写的正文沿用旁边正文的字体、字号和行距 |
21
21
  | 规范的图表题注 | 插入表格或图片时给出题注,插件按规范排:自动编号(“表 1”“图 1”,是 Word 题注域,增删后自动重排)、居中、表题在上、图题在下;图表前后留出段间距,图片居中 |
22
+ | 不被自带技能带偏 | DSH 桌面版自带的 Office 技能会让模型写脚本生成文件,这样你看不到过程,文件开着时还会写入失败。插件在模型第一次加载这些技能时把它引回来,确有需要(批量转换、应用打不开的文件)再次请求即放行 |
22
23
  | 交付前整体视觉检查 | 保存时如果 AI 改完之后还没看过成品,保存结果会自动附上各页(最多 8 页)的渲染图让它过一遍,有问题先修再保存 |
23
24
  | 批量编辑 | `office_edit` 一次调用按顺序执行一批修改(写一整篇文档、填一张表加公式加图表、做几页幻灯片),中途某一项失败就停下并报告已完成哪些 |
24
25
  | 改错位置的防护 | Word 按“段落编号 + 这段开头的文字”定位。你刚改过导致编号对不上时,这一步会被拒绝并要求重新读取;改写和删除段落必须带开头文字 |
@@ -79,6 +80,7 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
79
80
  | 逐字写入 | 开 | 文字像打字一样逐步出现;关闭后写得最快 |
80
81
  | 迷你卡片 | 开 | AI 编辑时在右下角显示带“跟随”“逐字”开关的小卡片。在卡片上切换过的开关,到 DSH 重启前以卡片为准 |
81
82
  | 交付前整体视觉检查 | 开 | 保存时自动把各页渲染图交给 AI 检查;会多用一些 token |
83
+ | 优先在真实 Office 里操作 | 开 | DSH 桌面版自带 office-docx / office-xlsx / office-pptx 技能,用脚本生成文件。开启后,AI 第一次想加载这些技能时会被引回本插件(对应的 Office 应用已安装时才拦);它再次请求时放行 |
82
84
  | 打开文档时显示到前台 | 开 | 关闭后文档仍会打开,只是不抢当前窗口 |
83
85
  | 核对排版的图片宽度 | 1100 像素 | `office_render` 给模型的图片宽度;越大越清晰,也越费 token |
84
86
 
@@ -560,6 +560,13 @@ static class Program
560
560
  }
561
561
  Dictionary<string, object> info = Info(kind, doc, Active(kind, app));
562
562
  info["how"] = how;
563
+ if (a.Flag("card", false))
564
+ {
565
+ if (!cardStarted) { cardStarted = true; Card.Start(); }
566
+ Following = Card.FollowChosen ? Following : a.Flag("follow", false);
567
+ Typing = Card.TypingChosen ? Typing : a.Flag("typing", false);
568
+ Card.Report("AI 打开了 " + (string)doc.Name);
569
+ }
563
570
  return info;
564
571
  }
565
572
 
@@ -1068,7 +1075,11 @@ static class Program
1068
1075
  {
1069
1076
  string where = op.Str("where", op.Has("para") ? "after" : "end");
1070
1077
  // A new, empty document: its one empty paragraph is where the text goes, wherever the operation points.
1071
- if ((int)doc.Paragraphs.Count == 1 && (string)doc.Paragraphs[1].Range.Text == "\r") return doc.Paragraphs[1];
1078
+ if ((int)doc.Paragraphs.Count == 1 && (string)doc.Paragraphs[1].Range.Text == "\r")
1079
+ {
1080
+ op.Raw("para"); op.Raw("expect");
1081
+ return doc.Paragraphs[1];
1082
+ }
1072
1083
  if (where == "end")
1073
1084
  {
1074
1085
  dynamic last = doc.Paragraphs.Last;
package/lib/client.js CHANGED
@@ -41,6 +41,7 @@ window.__ModuleLoader__.load({
41
41
  typing: true,
42
42
  card: true,
43
43
  finalCheck: true,
44
+ preferLive: true,
44
45
  showOnOpen: true,
45
46
  renderWidth: 1100
46
47
  };
@@ -54,6 +55,7 @@ window.__ModuleLoader__.load({
54
55
  typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
55
56
  card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
56
57
  finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
58
+ preferLive: typeof out.preferLive === "boolean" ? out.preferLive : DEFAULTS.preferLive,
57
59
  showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
58
60
  renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
59
61
  };
@@ -526,6 +528,16 @@ window.__ModuleLoader__.load({
526
528
  onChange: (value) => {
527
529
  change("finalCheck", value);
528
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
+ }
529
541
  })), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "打开文档时显示到前台", h("span", { style: {
530
542
  ...S.hint,
531
543
  fontWeight: 400
package/lib/index.js CHANGED
@@ -223,12 +223,31 @@ 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
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.",
@@ -253,6 +272,7 @@ const DEFAULTS = {
253
272
  typing: true,
254
273
  card: true,
255
274
  finalCheck: true,
275
+ preferLive: true,
256
276
  showOnOpen: true,
257
277
  renderWidth: 1100
258
278
  };
@@ -266,6 +286,7 @@ function resolveConfig(raw) {
266
286
  typing: typeof out.typing === "boolean" ? out.typing : DEFAULTS.typing,
267
287
  card: typeof out.card === "boolean" ? out.card : DEFAULTS.card,
268
288
  finalCheck: typeof out.finalCheck === "boolean" ? out.finalCheck : DEFAULTS.finalCheck,
289
+ preferLive: typeof out.preferLive === "boolean" ? out.preferLive : DEFAULTS.preferLive,
269
290
  showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
270
291
  renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
271
292
  };
@@ -5875,7 +5896,7 @@ function formatStatus(apps) {
5875
5896
  continue;
5876
5897
  }
5877
5898
  if (!app.running) {
5878
- lines.push(`${name}: not running`);
5899
+ lines.push(`${name}: installed, not running (office_open starts it)`);
5879
5900
  continue;
5880
5901
  }
5881
5902
  if (app.error) {
@@ -6199,10 +6220,14 @@ function createTools(host) {
6199
6220
  ...app === void 0 ? {} : { app },
6200
6221
  ...path === void 0 ? {} : { path }
6201
6222
  });
6223
+ const { showOnOpen, follow, typing, card } = host.settings();
6202
6224
  const doc = await helper.call("open", {
6203
6225
  app: kind,
6204
6226
  ...path === void 0 ? {} : { path },
6205
- show: host.settings().showOnOpen
6227
+ show: showOnOpen,
6228
+ follow,
6229
+ typing,
6230
+ card
6206
6231
  }, 8e4);
6207
6232
  current = {
6208
6233
  app: kind,
@@ -6436,6 +6461,10 @@ const Config = z.object({
6436
6461
  "zh-CN": { $description: "交付前整体视觉检查:保存时如果 AI 改完后还没看过成品,自动把各页渲染成图片给它检查" },
6437
6462
  "en-US": { $description: "Whole-document visual check: on save, show the AI every page if it has not looked since its last edits" }
6438
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
+ }),
6439
6468
  showOnOpen: z.boolean().default(true).volatile().i18n({
6440
6469
  "zh-CN": { $description: "打开文档时把它的窗口带到前台" },
6441
6470
  "en-US": { $description: "Bring the document window to the front when it is opened" }
@@ -6485,6 +6514,28 @@ function apply(ctx, config = {}) {
6485
6514
  }
6486
6515
  });
6487
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
+ });
6488
6539
  ctx.inject(["webServer"], (webCtx) => {
6489
6540
  const handler = async (_req, res) => {
6490
6541
  let body;
@@ -9,6 +9,7 @@ export interface Config {
9
9
  typing?: boolean;
10
10
  card?: boolean;
11
11
  finalCheck?: boolean;
12
+ preferLive?: boolean;
12
13
  showOnOpen?: boolean;
13
14
  renderWidth?: number;
14
15
  }
@@ -1,2 +1,6 @@
1
+ /** The host's file-based Office skills, and the app each one stands in for. */
2
+ export declare const FILE_SKILLS: Record<string, 'word' | 'excel' | 'ppt'>;
3
+ /** What the model is told the first time it reaches for one of those skills. */
4
+ export declare function redirectText(skill: string, app: 'word' | 'excel' | 'ppt'): string;
1
5
  /** System-prompt section: when to reach for the office_ tools, in as few tokens as will do. */
2
6
  export declare function promptText(): string;
@@ -14,6 +14,8 @@ export interface Settings {
14
14
  card: boolean;
15
15
  /** On save, show the model every page as it is now if it has not looked since its last edits. */
16
16
  finalCheck: boolean;
17
+ /** Turn the model away from the host's file-based Office skills when the real app is installed. */
18
+ preferLive: boolean;
17
19
  /** Bring the document's window to the front when office_open opens it. */
18
20
  showOnOpen: boolean;
19
21
  /** 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.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "DeepSeek Harness Office 插件(Windows):AI 直接在你打开的 Word / Excel / PowerPoint 里改文档,改动实时出现在窗口中,你可以同时编辑;读结构、批量编辑、渲染成图核对、保存。",
5
5
  "type": "module",
6
6
  "author": "copylee",