@copylee/dsh-office 0.2.1 → 0.2.3
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 +8 -5
- package/helper/OfficeHelper.cs +228 -9
- package/lib/client.js +2 -2
- package/lib/index.js +52 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
| 不需要窗口在前台 | 修改不靠鼠标键盘,Office 窗口被挡住或在后台也照常进行,你可以同时用电脑做别的事 |
|
|
16
16
|
| 跟随与静默 | 默认“跟随”:AI 改到哪里,窗口自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不动你的光标和选区。关闭即静默执行,不动你的视图 |
|
|
17
17
|
| 逐字写入 | 文字分小段写出,像打字一样出现,表格逐行填入,而不是整块贴上去。每段最多多花约 1 秒,可关闭 |
|
|
18
|
-
| 迷你卡片 | AI
|
|
18
|
+
| 迷你卡片 | AI 打开或编辑文档时屏幕右下角出现一张小卡片:正在改哪个文档、改到第几项,带“跟随”“逐字”两个开关,点一下立即生效(正在进行的这一批也跟着变)。点“跟随”会把文档窗口带到前台并开始跟随;你切到别的窗口后它自动熄灭,再点一次回来。可拖动,不抢焦点,AI 停手几秒后自动消失 |
|
|
19
19
|
| Word 原生公式 | AI 在文字里用 `$...$`(行内)和 `$$...$$`(单独一行)写 LaTeX,插件转成 Word 原生公式(分式、根号、上下标、希腊字母、求和、积分、上划线等常用写法);单独一行的公式末尾加 `\tag{1}` 即在右侧对齐编号 |
|
|
20
20
|
| 填模板 | 报告模板常把正文放在大单元格里。这类表格会按段落展开给模型看,正文、公式、表格都写进单元格里面,而不是堆到文档末尾;新写的正文沿用旁边正文的字体、字号和行距 |
|
|
21
21
|
| 规范的图表题注 | 插入表格或图片时给出题注,插件按规范排:自动编号(“表 1”“图 1”,是 Word 题注域,增删后自动重排)、居中、表题在上、图题在下;图表前后留出段间距,图片居中 |
|
|
22
22
|
| 不被自带技能带偏 | DSH 桌面版自带的 Office 技能会让模型写脚本生成文件,这样你看不到过程,文件开着时还会写入失败。插件在模型第一次加载这些技能时把它引回来,确有需要(批量转换、应用打不开的文件)再次请求即放行 |
|
|
23
|
-
| 交付前整体视觉检查 | 保存时如果 AI
|
|
23
|
+
| 交付前整体视觉检查 | 保存时如果 AI 改完之后还没看过成品,保存结果会自动附上整份文档的总览图让它过一遍,有问题先修再保存。总览图把各页(或各张幻灯片)带页码拼在一张图里,一张最多 6 页,最多 18 页;模型也可以随时主动调出总览,再放大某一页细看 |
|
|
24
|
+
| 不拆词、不重复编号 | 插件写入的段落、表格、题注一律按整词换行(有些模板的样式带着“允许西文在单词中间换行”,会把 37000 拆成 37 和 000);标题文字自带编号而样式又自动编号时,去掉自动的那个 |
|
|
25
|
+
| 提醒纯文本公式 | 正文里出现 d_k、10^18 这类没写成公式的下标和幂时,结果里会提醒模型改用公式 |
|
|
26
|
+
| 表格跨页 | 表头行在下一页重复,单行不被拆到两页,表题不会单独留在页尾 |
|
|
24
27
|
| 批量编辑 | `office_edit` 一次调用按顺序执行一批修改(写一整篇文档、填一张表加公式加图表、做几页幻灯片),中途某一项失败就停下并报告已完成哪些 |
|
|
25
28
|
| 改错位置的防护 | Word 按“段落编号 + 这段开头的文字”定位。你刚改过导致编号对不上时,这一步会被拒绝并要求重新读取;改写和删除段落必须带开头文字 |
|
|
26
29
|
| Word 一次撤销 | AI 的一批修改在 Word 里是一个撤销步骤,按一次 Ctrl+Z 整批撤回 |
|
|
@@ -61,7 +64,7 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
61
64
|
| `office_status` | 列出三个应用里现在打开了什么、哪些有未保存的修改 |
|
|
62
65
|
| `office_read` | 把打开的文档读成文本(段落 / 单元格 / 形状) |
|
|
63
66
|
| `office_edit` | 按顺序执行一批修改,不保存 |
|
|
64
|
-
| `office_render` | 把一页 / 一张幻灯片 / 一个单元格区域渲染成图片;Word 和 PowerPoint 一次最多看 4
|
|
67
|
+
| `office_render` | 把一页 / 一张幻灯片 / 一个单元格区域渲染成图片;Word 和 PowerPoint 一次最多看 4 页,或用总览把全部页面拼成一张图 |
|
|
65
68
|
| `office_save` | 保存;给出路径时另存为,扩展名为 `.pdf` 时导出 PDF;改完没看过成品时附带各页图片 |
|
|
66
69
|
|
|
67
70
|
`office_edit` 支持的操作:
|
|
@@ -78,8 +81,8 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
78
81
|
|---|---|---|
|
|
79
82
|
| 跟随 AI 的修改位置 | 开 | 窗口自动滚动到 AI 正在修改的位置;关闭为静默执行 |
|
|
80
83
|
| 逐字写入 | 开 | 文字像打字一样逐步出现;关闭后写得最快 |
|
|
81
|
-
| 迷你卡片 | 开 | AI
|
|
82
|
-
| 交付前整体视觉检查 | 开 |
|
|
84
|
+
| 迷你卡片 | 开 | AI 编辑时在右下角显示带“跟随”“逐字”开关的小卡片。在卡片上切换过的开关(包括你离开文档窗口后自动熄灭的“跟随”),到 DSH 重启前以卡片为准 |
|
|
85
|
+
| 交付前整体视觉检查 | 开 | 保存时自动把整份文档的总览图交给 AI 检查;会多用一些 token |
|
|
83
86
|
| 优先在真实 Office 里操作 | 开 | DSH 桌面版自带 office-docx / office-xlsx / office-pptx 技能,用脚本生成文件。开启后,AI 第一次想加载这些技能时会被引回本插件(对应的 Office 应用已安装时才拦);它再次请求时放行 |
|
|
84
87
|
| 打开文档时显示到前台 | 开 | 关闭后文档仍会打开,只是不抢当前窗口 |
|
|
85
88
|
| 核对排版的图片宽度 | 1100 像素 | `office_render` 给模型的图片宽度;越大越清晰,也越费 token |
|
package/helper/OfficeHelper.cs
CHANGED
|
@@ -68,6 +68,16 @@ class Bag
|
|
|
68
68
|
object v = Raw(key);
|
|
69
69
|
return v is bool ? (bool)v : fallback;
|
|
70
70
|
}
|
|
71
|
+
/// A switch given loosely: true, "thin", "all", 1 are on; false, "none", "off", 0 and absent are off.
|
|
72
|
+
public bool On(string key)
|
|
73
|
+
{
|
|
74
|
+
object v = Raw(key);
|
|
75
|
+
if (v == null) return false;
|
|
76
|
+
if (v is bool) return (bool)v;
|
|
77
|
+
string text = Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture).Trim().ToLowerInvariant();
|
|
78
|
+
return text.Length > 0 && text != "false" && text != "none" && text != "off" && text != "no" && text != "0";
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
public IList List(string key)
|
|
72
82
|
{
|
|
73
83
|
object v = Raw(key);
|
|
@@ -119,6 +129,20 @@ class Card : Form
|
|
|
119
129
|
readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
|
|
120
130
|
|
|
121
131
|
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr handle, int command);
|
|
132
|
+
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
133
|
+
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr handle);
|
|
134
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr handle);
|
|
135
|
+
|
|
136
|
+
/// The user is looking at the document: its window is the foreground one.
|
|
137
|
+
static bool Watching()
|
|
138
|
+
{
|
|
139
|
+
IntPtr doc = Program.DocWindow;
|
|
140
|
+
return doc != IntPtr.Zero && GetForegroundWindow() == doc;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Following means "keep the document in front of me and scroll to the edits". It holds while the user
|
|
144
|
+
// stays in that window; once they go elsewhere it switches itself off, and the button brings them back.
|
|
145
|
+
bool wasWatching, lit;
|
|
122
146
|
|
|
123
147
|
protected override bool ShowWithoutActivation { get { return true; } }
|
|
124
148
|
|
|
@@ -147,7 +171,21 @@ class Card : Form
|
|
|
147
171
|
Location = new Point(area.Right - Width - S(18), area.Bottom - Height - S(18));
|
|
148
172
|
using (System.Drawing.Drawing2D.GraphicsPath path = Rounded(new Rectangle(0, 0, Width, Height), S(10))) Region = new Region(path);
|
|
149
173
|
timer.Interval = 500;
|
|
150
|
-
timer.Tick += delegate
|
|
174
|
+
timer.Tick += delegate
|
|
175
|
+
{
|
|
176
|
+
bool watching = Watching();
|
|
177
|
+
if (watching) wasWatching = true;
|
|
178
|
+
else if (wasWatching && Program.Following)
|
|
179
|
+
{
|
|
180
|
+
// The user left the document for another window.
|
|
181
|
+
wasWatching = false;
|
|
182
|
+
Program.Following = false;
|
|
183
|
+
FollowChosen = true;
|
|
184
|
+
}
|
|
185
|
+
bool nowLit = Program.Following && watching;
|
|
186
|
+
if (nowLit != lit) { lit = nowLit; Invalidate(); }
|
|
187
|
+
if (Visible && (DateTime.UtcNow - shown).TotalSeconds > 6) Hide();
|
|
188
|
+
};
|
|
151
189
|
timer.Start();
|
|
152
190
|
}
|
|
153
191
|
|
|
@@ -187,7 +225,8 @@ class Card : Form
|
|
|
187
225
|
{
|
|
188
226
|
int right = Width - S(10);
|
|
189
227
|
Chip(g, font, "逐字", Program.Typing, ref right, out typingBox);
|
|
190
|
-
|
|
228
|
+
bool following = Program.Following && Watching();
|
|
229
|
+
Chip(g, font, following ? "跟随中" : "跟随", following, ref right, out followBox);
|
|
191
230
|
using (SolidBrush dot = new SolidBrush(Color.FromArgb(217, 119, 87))) g.FillEllipse(dot, S(12), (Height - S(8)) / 2, S(8), S(8));
|
|
192
231
|
Rectangle text = new Rectangle(S(26), 0, right - S(30), Height);
|
|
193
232
|
TextRenderer.DrawText(g, line, font, text, Color.FromArgb(235, 235, 240), TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
|
|
@@ -208,7 +247,23 @@ class Card : Form
|
|
|
208
247
|
{
|
|
209
248
|
pressed = false;
|
|
210
249
|
if (dragged) return;
|
|
211
|
-
if (followBox.Contains(e.Location))
|
|
250
|
+
if (followBox.Contains(e.Location))
|
|
251
|
+
{
|
|
252
|
+
FollowChosen = true;
|
|
253
|
+
if (Program.Following && Watching()) Program.Following = false;
|
|
254
|
+
else
|
|
255
|
+
{
|
|
256
|
+
// Bring the document to the front and follow from here on.
|
|
257
|
+
Program.Following = true;
|
|
258
|
+
wasWatching = true;
|
|
259
|
+
IntPtr doc = Program.DocWindow;
|
|
260
|
+
if (doc != IntPtr.Zero)
|
|
261
|
+
{
|
|
262
|
+
if (IsIconic(doc)) ShowWindow(doc, 9);
|
|
263
|
+
SetForegroundWindow(doc);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
212
267
|
else if (typingBox.Contains(e.Location)) { Program.Typing = !Program.Typing; TypingChosen = true; }
|
|
213
268
|
shown = DateTime.UtcNow;
|
|
214
269
|
Invalidate();
|
|
@@ -560,6 +615,7 @@ static class Program
|
|
|
560
615
|
}
|
|
561
616
|
Dictionary<string, object> info = Info(kind, doc, Active(kind, app));
|
|
562
617
|
info["how"] = how;
|
|
618
|
+
Remember(doc);
|
|
563
619
|
if (a.Flag("card", false))
|
|
564
620
|
{
|
|
565
621
|
if (!cardStarted) { cardStarted = true; Card.Start(); }
|
|
@@ -680,6 +736,14 @@ static class Program
|
|
|
680
736
|
|
|
681
737
|
/// Scroll the window to where the agent is working (off = silent mode: the user's view is left alone).
|
|
682
738
|
public static volatile bool Following;
|
|
739
|
+
/// The window of the document being worked on (0 when unknown), for the card to bring forward.
|
|
740
|
+
public static IntPtr DocWindow = IntPtr.Zero;
|
|
741
|
+
|
|
742
|
+
static void Remember(dynamic doc)
|
|
743
|
+
{
|
|
744
|
+
try { DocWindow = new IntPtr(Convert.ToInt64(doc.Windows[1].Hwnd)); }
|
|
745
|
+
catch (Exception) { DocWindow = IntPtr.Zero; }
|
|
746
|
+
}
|
|
683
747
|
/// Write text a few characters at a time, the way a person types, instead of all at once.
|
|
684
748
|
public static volatile bool Typing;
|
|
685
749
|
|
|
@@ -1094,6 +1158,37 @@ static class Program
|
|
|
1094
1158
|
return doc.Range(start, start).Paragraphs[1];
|
|
1095
1159
|
}
|
|
1096
1160
|
|
|
1161
|
+
static readonly System.Text.RegularExpressions.Regex Numbered = new System.Text.RegularExpressions.Regex(@"^\s*(第?[一二三四五六七八九十百]+[、..章节]|[((][一二三四五六七八九十\d]+[))]|\d+(\.\d+)*[\s、..])");
|
|
1162
|
+
|
|
1163
|
+
static readonly System.Text.RegularExpressions.Regex PlainMath = new System.Text.RegularExpressions.Regex(@"[A-Za-z0-9\)\}]_(\{[^}]+\}|[A-Za-z0-9])|[A-Za-z0-9\)]\^(\{[^}]+\}|\(?-?\d)|\\(frac|sqrt|sum|int|alpha|beta|theta|pi)\b");
|
|
1164
|
+
|
|
1165
|
+
/// Subscripts and powers written as plain text (d_k, 10^18) outside $...$: say so, with the first one found.
|
|
1166
|
+
static string Lint(string text)
|
|
1167
|
+
{
|
|
1168
|
+
if (text == null) return "";
|
|
1169
|
+
string outside = Dollars.Replace(text, " ");
|
|
1170
|
+
System.Text.RegularExpressions.Match m = PlainMath.Match(outside);
|
|
1171
|
+
if (!m.Success) return "";
|
|
1172
|
+
int start = Math.Max(0, m.Index - 6);
|
|
1173
|
+
return " — NOTE plain-text formula \"" + outside.Substring(start, Math.Min(outside.Length - start, m.Length + 12)).Trim() + "\": write formulas, subscripts and powers between dollar signs ($d_k$, $10^{18}$) so they are typeset";
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/// Keep Latin words and numbers whole at the end of a line. Some templates carry "allow Latin text to wrap in
|
|
1177
|
+
/// the middle of a word" in a style, and every paragraph written after it inherits that: 37 / 000, mode / l.
|
|
1178
|
+
/// (Word's object model names this backwards: WordWrap = True is the whole-word setting.)
|
|
1179
|
+
static void Whole(dynamic range)
|
|
1180
|
+
{
|
|
1181
|
+
try { range.ParagraphFormat.WordWrap = -1; } catch (Exception) { }
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/// The text already starts with its number ("一、", "2.1 ") and the style would add another: drop the automatic one.
|
|
1185
|
+
static void Unnumber(dynamic paragraph, string text)
|
|
1186
|
+
{
|
|
1187
|
+
if (!Numbered.IsMatch(text)) return;
|
|
1188
|
+
try { if ((int)paragraph.Range.ListFormat.ListType != 0) paragraph.Range.ListFormat.RemoveNumbers(); }
|
|
1189
|
+
catch (Exception) { }
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1097
1192
|
/// A new empty paragraph right after the given one.
|
|
1098
1193
|
static dynamic After(dynamic doc, dynamic paragraph)
|
|
1099
1194
|
{
|
|
@@ -1105,8 +1200,27 @@ static class Program
|
|
|
1105
1200
|
try
|
|
1106
1201
|
{
|
|
1107
1202
|
made.Style = paragraph.Style;
|
|
1108
|
-
|
|
1109
|
-
|
|
1203
|
+
dynamic from = paragraph.Range.ParagraphFormat, to = made.Range.ParagraphFormat;
|
|
1204
|
+
to.Alignment = from.Alignment;
|
|
1205
|
+
to.LeftIndent = from.LeftIndent; to.RightIndent = from.RightIndent;
|
|
1206
|
+
to.CharacterUnitFirstLineIndent = from.CharacterUnitFirstLineIndent; to.FirstLineIndent = from.FirstLineIndent;
|
|
1207
|
+
to.SpaceBefore = from.SpaceBefore; to.SpaceAfter = from.SpaceAfter;
|
|
1208
|
+
to.LineSpacingRule = from.LineSpacingRule;
|
|
1209
|
+
if ((int)from.LineSpacingRule >= 3) to.LineSpacing = from.LineSpacing;
|
|
1210
|
+
dynamic font = paragraph.Range.Font, target = made.Range.Font;
|
|
1211
|
+
// 9999999 / empty mean "mixed" in the source paragraph: leave those to the style.
|
|
1212
|
+
string name = (string)font.Name, farEast = (string)font.NameFarEast;
|
|
1213
|
+
if (!string.IsNullOrEmpty(name)) target.Name = name;
|
|
1214
|
+
if (!string.IsNullOrEmpty(farEast)) target.NameFarEast = farEast;
|
|
1215
|
+
float size = (float)font.Size;
|
|
1216
|
+
if (size > 0 && size < 1000) target.Size = size;
|
|
1217
|
+
foreach (string flag in new string[] { "Bold", "Italic" })
|
|
1218
|
+
{
|
|
1219
|
+
int value = flag == "Bold" ? (int)font.Bold : (int)font.Italic;
|
|
1220
|
+
if (value == 0 || value == -1) { if (flag == "Bold") target.Bold = value; else target.Italic = value; }
|
|
1221
|
+
}
|
|
1222
|
+
int color = (int)font.Color;
|
|
1223
|
+
if (color != 9999999) target.Color = color;
|
|
1110
1224
|
}
|
|
1111
1225
|
catch (COMException) { }
|
|
1112
1226
|
return made;
|
|
@@ -1146,10 +1260,13 @@ static class Program
|
|
|
1146
1260
|
{
|
|
1147
1261
|
dynamic range = caption.Range;
|
|
1148
1262
|
range.ParagraphFormat.Alignment = 1;
|
|
1263
|
+
Whole(range);
|
|
1149
1264
|
range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; range.ParagraphFormat.FirstLineIndent = 0;
|
|
1150
1265
|
range.Font.Bold = 0; range.Font.Italic = 0; range.Font.Color = -16777216; range.Font.Size = 10.5f;
|
|
1151
1266
|
if (!string.IsNullOrEmpty(fontName)) range.Font.Name = fontName;
|
|
1152
1267
|
if (!string.IsNullOrEmpty(fontFarEast)) range.Font.NameFarEast = fontFarEast;
|
|
1268
|
+
// A table's caption must not be left alone at the foot of a page.
|
|
1269
|
+
if (table) range.ParagraphFormat.KeepWithNext = -1;
|
|
1153
1270
|
range.ParagraphFormat.SpaceBefore = table ? Air : 3f;
|
|
1154
1271
|
range.ParagraphFormat.SpaceAfter = table ? 3f : Air;
|
|
1155
1272
|
Mathify(doc, caption.Range);
|
|
@@ -1210,6 +1327,7 @@ static class Program
|
|
|
1210
1327
|
if (items == null) { items = new ArrayList(); items.Add(new Dictionary<string, object> { { "text", op.Need("text") } }); }
|
|
1211
1328
|
dynamic current = null;
|
|
1212
1329
|
int made = 0, equations = 0;
|
|
1330
|
+
string lint = "";
|
|
1213
1331
|
foreach (object raw in items)
|
|
1214
1332
|
{
|
|
1215
1333
|
Bag item = raw is string ? new Bag(new Dictionary<string, object> { { "text", raw } }) : new Bag(raw);
|
|
@@ -1225,13 +1343,16 @@ static class Program
|
|
|
1225
1343
|
WordFormat(p.Range, StyleLess(item));
|
|
1226
1344
|
Follow(doc, p.Range);
|
|
1227
1345
|
Write(doc, (int)p.Range.Start, text);
|
|
1346
|
+
if (lint.Length == 0) lint = Lint(text);
|
|
1347
|
+
Unnumber(p, text);
|
|
1348
|
+
Whole(p.Range);
|
|
1228
1349
|
equations += Mathify(doc, p.Range);
|
|
1229
1350
|
current = p;
|
|
1230
1351
|
made++;
|
|
1231
1352
|
}
|
|
1232
1353
|
}
|
|
1233
1354
|
int lastIndex = Index(doc, current);
|
|
1234
|
-
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count + (equations > 0 ? ", " + equations + " equation(s)" : "");
|
|
1355
|
+
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count + (equations > 0 ? ", " + equations + " equation(s)" : "") + lint;
|
|
1235
1356
|
}
|
|
1236
1357
|
if ((type == "set_text" || (type == "delete_range" && op.Has("para"))) && op.Raw("expect") == null)
|
|
1237
1358
|
{
|
|
@@ -1244,6 +1365,7 @@ static class Program
|
|
|
1244
1365
|
range.MoveEnd(1, -1);
|
|
1245
1366
|
range.Text = "";
|
|
1246
1367
|
Write(doc, (int)p.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1368
|
+
Whole(p.Range);
|
|
1247
1369
|
int built = Mathify(doc, p.Range);
|
|
1248
1370
|
return "paragraph " + op.Int("para", 0) + " rewritten" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
1249
1371
|
}
|
|
@@ -1306,6 +1428,7 @@ static class Program
|
|
|
1306
1428
|
if (!string.IsNullOrEmpty(fontFarEast)) table.Range.Font.NameFarEast = fontFarEast;
|
|
1307
1429
|
if (fontSize > 4 && fontSize < 100) table.Range.Font.Size = fontSize;
|
|
1308
1430
|
table.Range.ParagraphFormat.Alignment = 1;
|
|
1431
|
+
Whole(table.Range);
|
|
1309
1432
|
table.Range.Cells.VerticalAlignment = 1;
|
|
1310
1433
|
}
|
|
1311
1434
|
catch (COMException) { }
|
|
@@ -1328,7 +1451,13 @@ static class Program
|
|
|
1328
1451
|
if (Typing) Thread.Sleep(25);
|
|
1329
1452
|
}
|
|
1330
1453
|
}
|
|
1331
|
-
if (op.Flag("header", true))
|
|
1454
|
+
if (op.Flag("header", true))
|
|
1455
|
+
{
|
|
1456
|
+
table.Rows[1].Range.Font.Bold = 1;
|
|
1457
|
+
// On a page break the header row is repeated at the top of the next page.
|
|
1458
|
+
try { table.Rows[1].HeadingFormat = -1; } catch (COMException) { }
|
|
1459
|
+
}
|
|
1460
|
+
try { table.Rows.AllowBreakAcrossPages = 0; } catch (COMException) { }
|
|
1332
1461
|
if (op.Has("caption")) Caption(doc, table.Range, true, op.Need("caption"), fontName, fontFarEast);
|
|
1333
1462
|
else
|
|
1334
1463
|
{
|
|
@@ -1504,8 +1633,8 @@ static class Program
|
|
|
1504
1633
|
range.HorizontalAlignment = align == "center" ? -4108 : align == "right" ? -4152 : -4131;
|
|
1505
1634
|
}
|
|
1506
1635
|
if (op.Has("wrap")) range.WrapText = op.Flag("wrap", false);
|
|
1507
|
-
if (op.
|
|
1508
|
-
if (op.
|
|
1636
|
+
if (op.On("border")) range.Borders.LineStyle = 1;
|
|
1637
|
+
if (op.On("merge")) range.Merge();
|
|
1509
1638
|
if (op.Has("columnWidth")) range.ColumnWidth = op.Num("columnWidth", 10);
|
|
1510
1639
|
if (op.Has("rowHeight")) range.RowHeight = op.Num("rowHeight", 15);
|
|
1511
1640
|
}
|
|
@@ -1793,6 +1922,7 @@ static class Program
|
|
|
1793
1922
|
if (!Card.FollowChosen) Following = a.Flag("follow", false);
|
|
1794
1923
|
if (!Card.TypingChosen) Typing = a.Flag("typing", false);
|
|
1795
1924
|
bool card = a.Flag("card", false);
|
|
1925
|
+
Remember(doc);
|
|
1796
1926
|
if (card && !cardStarted) { cardStarted = true; Card.Start(); }
|
|
1797
1927
|
string docName = (string)doc.Name;
|
|
1798
1928
|
dynamic undo = null;
|
|
@@ -1837,8 +1967,97 @@ static class Program
|
|
|
1837
1967
|
|
|
1838
1968
|
// ───────────────────────── rendering ─────────────────────────
|
|
1839
1969
|
|
|
1970
|
+
/// One Word page as a picture of the given width.
|
|
1971
|
+
static Bitmap WordPage(dynamic pages, int page, int width)
|
|
1972
|
+
{
|
|
1973
|
+
byte[] bits = (byte[])pages[page].EnhMetaFileBits;
|
|
1974
|
+
using (MemoryStream stream = new MemoryStream(bits))
|
|
1975
|
+
using (Metafile meta = new Metafile(stream))
|
|
1976
|
+
{
|
|
1977
|
+
int height = (int)Math.Round(width * (double)meta.Height / meta.Width);
|
|
1978
|
+
// Drawn straight at the final size, hairlines (table borders) fall between pixels and vanish:
|
|
1979
|
+
// draw the page large first, then scale the picture down.
|
|
1980
|
+
int factor = Math.Max(2, (int)Math.Ceiling(3000.0 / width));
|
|
1981
|
+
Bitmap bitmap = new Bitmap(width, height);
|
|
1982
|
+
using (Bitmap large = new Bitmap(width * factor, height * factor))
|
|
1983
|
+
{
|
|
1984
|
+
using (Graphics g = Graphics.FromImage(large))
|
|
1985
|
+
{
|
|
1986
|
+
g.Clear(Color.White);
|
|
1987
|
+
g.DrawImage(meta, 0, 0, large.Width, large.Height);
|
|
1988
|
+
}
|
|
1989
|
+
using (Graphics g = Graphics.FromImage(bitmap))
|
|
1990
|
+
{
|
|
1991
|
+
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
1992
|
+
g.DrawImage(large, 0, 0, width, height);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return bitmap;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/// Several pages or slides side by side in one picture, each with its number: the whole document at a glance.
|
|
2000
|
+
static object Sheet(string kind, dynamic doc, Bag a)
|
|
2001
|
+
{
|
|
2002
|
+
string path = a.Need("out");
|
|
2003
|
+
int cell = Math.Max(300, Math.Min(1000, a.Int("width", 640)));
|
|
2004
|
+
dynamic pages = kind == "word" ? doc.Windows[1].Panes[1].Pages : null;
|
|
2005
|
+
int total = kind == "word" ? (int)pages.Count : (int)doc.Slides.Count;
|
|
2006
|
+
if (total == 0) throw new Fail("ANCHOR_MISSING", "There is nothing to picture yet.");
|
|
2007
|
+
int first = Math.Max(1, a.Int("from", 1)), last = Math.Min(total, a.Int("to", total));
|
|
2008
|
+
if (first > total) throw new Fail("ANCHOR_MISSING", "There is no " + (kind == "word" ? "page " : "slide ") + first + " (there are " + total + ").");
|
|
2009
|
+
last = Math.Min(last, first + 11);
|
|
2010
|
+
int count = last - first + 1, columns = Math.Min(count, kind == "word" ? 3 : 2), rows = (count + columns - 1) / columns;
|
|
2011
|
+
List<Bitmap> pictures = new List<Bitmap>();
|
|
2012
|
+
string temp = path + ".slide.png";
|
|
2013
|
+
try
|
|
2014
|
+
{
|
|
2015
|
+
for (int n = first; n <= last; n++)
|
|
2016
|
+
{
|
|
2017
|
+
if (kind == "word") pictures.Add(WordPage(pages, n, cell));
|
|
2018
|
+
else
|
|
2019
|
+
{
|
|
2020
|
+
double ratio = (double)doc.PageSetup.SlideHeight / (double)doc.PageSetup.SlideWidth;
|
|
2021
|
+
doc.Slides[n].Export(temp, "PNG", cell, (int)Math.Round(cell * ratio));
|
|
2022
|
+
using (Image loaded = Image.FromFile(temp)) pictures.Add(new Bitmap(loaded));
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
int gap = 14, label = 30, height = 0;
|
|
2026
|
+
foreach (Bitmap picture in pictures) height = Math.Max(height, picture.Height);
|
|
2027
|
+
using (Bitmap sheet = new Bitmap(columns * cell + (columns + 1) * gap, rows * (height + label + gap) + gap))
|
|
2028
|
+
{
|
|
2029
|
+
using (Graphics g = Graphics.FromImage(sheet))
|
|
2030
|
+
using (Font font = new Font("Microsoft YaHei UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel))
|
|
2031
|
+
using (Pen border = new Pen(Color.FromArgb(150, 150, 150)))
|
|
2032
|
+
{
|
|
2033
|
+
g.Clear(Color.FromArgb(232, 232, 236));
|
|
2034
|
+
for (int k = 0; k < pictures.Count; k++)
|
|
2035
|
+
{
|
|
2036
|
+
int x = gap + (k % columns) * (cell + gap), y = gap + (k / columns) * (height + label + gap);
|
|
2037
|
+
g.DrawString((kind == "word" ? "page " : "slide ") + (first + k), font, Brushes.Black, x, y + 4);
|
|
2038
|
+
g.DrawImage(pictures[k], x, y + label);
|
|
2039
|
+
g.DrawRectangle(border, x, y + label, pictures[k].Width - 1, pictures[k].Height - 1);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
sheet.Save(path, ImageFormat.Png);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
finally
|
|
2046
|
+
{
|
|
2047
|
+
foreach (Bitmap picture in pictures) picture.Dispose();
|
|
2048
|
+
try { File.Delete(temp); } catch (Exception) { }
|
|
2049
|
+
}
|
|
2050
|
+
Dictionary<string, object> result = new Dictionary<string, object>();
|
|
2051
|
+
result["what"] = (kind == "word" ? "pages " : "slides ") + first + "–" + last + " of " + total;
|
|
2052
|
+
result["first"] = first; result["last"] = last; result["total"] = total;
|
|
2053
|
+
result["path"] = path;
|
|
2054
|
+
using (Image saved = Image.FromFile(path)) { result["width"] = saved.Width; result["height"] = saved.Height; }
|
|
2055
|
+
return result;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1840
2058
|
static object Render(string kind, dynamic app, dynamic doc, Bag a)
|
|
1841
2059
|
{
|
|
2060
|
+
if (a.Flag("sheet", false) && kind != "excel") return Sheet(kind, doc, a);
|
|
1842
2061
|
string path = a.Need("out");
|
|
1843
2062
|
int width = Math.Max(320, Math.Min(2400, a.Int("width", 1100)));
|
|
1844
2063
|
Dictionary<string, object> result = new Dictionary<string, object>();
|
package/lib/client.js
CHANGED
|
@@ -491,7 +491,7 @@ window.__ModuleLoader__.load({
|
|
|
491
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
492
|
...S.hint,
|
|
493
493
|
fontWeight: 400
|
|
494
|
-
} }, "AI 改到哪里,窗口就自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不用自己翻找。关闭即静默执行:AI
|
|
494
|
+
} }, "AI 改到哪里,窗口就自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不用自己翻找。关闭即静默执行:AI 照常修改,但不动你的视图。想看时点迷你卡片上的“跟随”:文档窗口被带到前台并开始跟随;你切到别的窗口后它自动熄灭,再点一次回来。")), h(Switch, {
|
|
495
495
|
checked: settings.follow,
|
|
496
496
|
disabled,
|
|
497
497
|
label: "跟随 AI 的修改位置",
|
|
@@ -521,7 +521,7 @@ window.__ModuleLoader__.load({
|
|
|
521
521
|
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "交付前整体视觉检查", h("span", { style: {
|
|
522
522
|
...S.hint,
|
|
523
523
|
fontWeight: 400
|
|
524
|
-
} }, "AI
|
|
524
|
+
} }, "AI 保存时,如果它改完之后还没看过成品,就自动把整份文档拼成带页码的总览图(一张图最多 6 页)交给它过一遍,发现问题先修再交付。会多用一些 token。")), h(Switch, {
|
|
525
525
|
checked: settings.finalCheck,
|
|
526
526
|
disabled,
|
|
527
527
|
label: "交付前整体视觉检查",
|
package/lib/index.js
CHANGED
|
@@ -251,7 +251,7 @@ function promptText() {
|
|
|
251
251
|
"- Put all the changes you can into one office_edit call; it runs them in order.",
|
|
252
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.",
|
|
253
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
|
|
254
|
+
"- Finish with a polish pass, it is part of the job: office_render with overview: true for the whole document at a glance, then the pages that need a closer look; 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
255
|
"- In Word, formulas are LaTeX between dollar signs ($...$ inline, $$...$$ on its own line); they become native equations.",
|
|
256
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.",
|
|
257
257
|
"- If a call reports the app is busy, the user is typing or has a dialog open: wait a moment and retry.",
|
|
@@ -6178,6 +6178,38 @@ function createTools(host) {
|
|
|
6178
6178
|
...total === void 0 ? {} : { total }
|
|
6179
6179
|
};
|
|
6180
6180
|
};
|
|
6181
|
+
/** The whole document at a glance: its pages (6 to a picture) or slides tiled with their numbers, up to 18. */
|
|
6182
|
+
const overview = async (on) => {
|
|
6183
|
+
if (on.app === "excel") return picture(on, 0, 0, {}, Math.min(host.settings().renderWidth, 1e3));
|
|
6184
|
+
const folder = join(tmpdir(), "dsh-office");
|
|
6185
|
+
await mkdir(folder, { recursive: true });
|
|
6186
|
+
const lines = [];
|
|
6187
|
+
const images = [];
|
|
6188
|
+
let total = 1;
|
|
6189
|
+
for (let from = 1; from <= Math.min(total, 18); from += 6) {
|
|
6190
|
+
const out = join(folder, `sheet-${process.pid}-${Date.now()}-${from}.png`);
|
|
6191
|
+
try {
|
|
6192
|
+
const result = await helper.call("render", {
|
|
6193
|
+
...on,
|
|
6194
|
+
sheet: true,
|
|
6195
|
+
from,
|
|
6196
|
+
to: from + 5,
|
|
6197
|
+
out,
|
|
6198
|
+
width: 640
|
|
6199
|
+
}, 11e4);
|
|
6200
|
+
total = result.total;
|
|
6201
|
+
images.push(toJson(await host.saveImage(await readFile(out), `${on.app}-overview-${from}.png`)));
|
|
6202
|
+
lines.push(`${APP_NAMES[on.app]} ${result.what}, tiled in one picture.`);
|
|
6203
|
+
} finally {
|
|
6204
|
+
await rm(out, { force: true });
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
if (total > 18) lines.push(`Only the first 18 of ${total} are shown.`);
|
|
6208
|
+
return {
|
|
6209
|
+
lines,
|
|
6210
|
+
images
|
|
6211
|
+
};
|
|
6212
|
+
};
|
|
6181
6213
|
/** The helper arguments naming the document; the app comes from doc / app only, never from a save-as path. */
|
|
6182
6214
|
const target = (args) => {
|
|
6183
6215
|
if (args.doc === void 0 && args.app === void 0) {
|
|
@@ -6344,7 +6376,7 @@ function createTools(host) {
|
|
|
6344
6376
|
}));
|
|
6345
6377
|
tools.push(defineTool({
|
|
6346
6378
|
name: "office_render",
|
|
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),
|
|
6379
|
+
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), an Excel range (default: the used range), or with overview: true the whole document tiled into one picture. 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.",
|
|
6348
6380
|
parameters: {
|
|
6349
6381
|
doc: DOC,
|
|
6350
6382
|
app: APP,
|
|
@@ -6360,6 +6392,10 @@ function createTools(host) {
|
|
|
6360
6392
|
type: "integer",
|
|
6361
6393
|
description: "Word / PowerPoint: last page or slide, to get up to 4 in one call."
|
|
6362
6394
|
},
|
|
6395
|
+
overview: {
|
|
6396
|
+
type: "boolean",
|
|
6397
|
+
description: "Word / PowerPoint: every page or slide tiled into one picture (6 pages per picture, numbered): the whole document at a glance, for the final check. Then look closer at a page with page / slide."
|
|
6398
|
+
},
|
|
6363
6399
|
sheet: {
|
|
6364
6400
|
type: "string",
|
|
6365
6401
|
description: "Excel: sheet name."
|
|
@@ -6374,9 +6410,19 @@ function createTools(host) {
|
|
|
6374
6410
|
async execute(args, exec) {
|
|
6375
6411
|
const input = args;
|
|
6376
6412
|
const on = target(input), kind = on.app;
|
|
6377
|
-
if (kind === "ppt" && input.slide === void 0) throw new Error("slide is required for PowerPoint.");
|
|
6413
|
+
if (kind === "ppt" && input.slide === void 0 && input.overview !== true) throw new Error("slide is required for PowerPoint (or pass overview: true).");
|
|
6378
6414
|
if (!await host.vision(exec)) return { text: "The current model cannot view images; check the result with office_read instead." };
|
|
6379
|
-
|
|
6415
|
+
if (input.overview === true && kind !== "excel") {
|
|
6416
|
+
const all = await overview(on);
|
|
6417
|
+
unseen.delete(keyOf(on));
|
|
6418
|
+
const [image, ...more] = all.images;
|
|
6419
|
+
return {
|
|
6420
|
+
text: all.lines.join("\n"),
|
|
6421
|
+
...image === void 0 ? {} : { image },
|
|
6422
|
+
...more.length ? { more } : {}
|
|
6423
|
+
};
|
|
6424
|
+
}
|
|
6425
|
+
const { doc: _doc, app: _app, to, page: _page, slide: _slide, overview: _overview, ...rest } = input;
|
|
6380
6426
|
const first = kind === "word" ? input.page ?? 1 : kind === "ppt" ? input.slide : 0;
|
|
6381
6427
|
const last = kind === "excel" || to === void 0 ? first : Math.min(Math.max(to, first), first + 3);
|
|
6382
6428
|
const { lines, images } = await picture(on, first, last, rest, host.settings().renderWidth);
|
|
@@ -6424,10 +6470,10 @@ function createTools(host) {
|
|
|
6424
6470
|
app: on.app,
|
|
6425
6471
|
doc: result.path
|
|
6426
6472
|
} : on;
|
|
6427
|
-
const shown = await
|
|
6473
|
+
const shown = await overview(now);
|
|
6428
6474
|
const [image, ...more] = shown.images;
|
|
6429
6475
|
return {
|
|
6430
|
-
text: `${text}\nFinal check: you edited after you last looked at the document, so here is
|
|
6476
|
+
text: `${text}\nFinal check: you edited after you last looked at the document, so here is the whole of it as it is now. ${shown.lines.join(" ")}\nGo through every page 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, a nearly empty last page), look closer with office_render, fix it with office_edit and save again; if all is well, say so.`,
|
|
6431
6477
|
...image === void 0 ? {} : { image },
|
|
6432
6478
|
...more.length ? { more } : {}
|
|
6433
6479
|
};
|
package/package.json
CHANGED