@copylee/dsh-office 0.2.2 → 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 +6 -3
- package/helper/OfficeHelper.cs +172 -7
- package/lib/client.js +1 -1
- package/lib/index.js +52 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,10 @@
|
|
|
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` 支持的操作:
|
|
@@ -79,7 +82,7 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
79
82
|
| 跟随 AI 的修改位置 | 开 | 窗口自动滚动到 AI 正在修改的位置;关闭为静默执行 |
|
|
80
83
|
| 逐字写入 | 开 | 文字像打字一样逐步出现;关闭后写得最快 |
|
|
81
84
|
| 迷你卡片 | 开 | AI 编辑时在右下角显示带“跟随”“逐字”开关的小卡片。在卡片上切换过的开关(包括你离开文档窗口后自动熄灭的“跟随”),到 DSH 重启前以卡片为准 |
|
|
82
|
-
| 交付前整体视觉检查 | 开 |
|
|
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);
|
|
@@ -215,7 +225,8 @@ class Card : Form
|
|
|
215
225
|
{
|
|
216
226
|
int right = Width - S(10);
|
|
217
227
|
Chip(g, font, "逐字", Program.Typing, ref right, out typingBox);
|
|
218
|
-
|
|
228
|
+
bool following = Program.Following && Watching();
|
|
229
|
+
Chip(g, font, following ? "跟随中" : "跟随", following, ref right, out followBox);
|
|
219
230
|
using (SolidBrush dot = new SolidBrush(Color.FromArgb(217, 119, 87))) g.FillEllipse(dot, S(12), (Height - S(8)) / 2, S(8), S(8));
|
|
220
231
|
Rectangle text = new Rectangle(S(26), 0, right - S(30), Height);
|
|
221
232
|
TextRenderer.DrawText(g, line, font, text, Color.FromArgb(235, 235, 240), TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
|
|
@@ -1147,6 +1158,37 @@ static class Program
|
|
|
1147
1158
|
return doc.Range(start, start).Paragraphs[1];
|
|
1148
1159
|
}
|
|
1149
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
|
+
|
|
1150
1192
|
/// A new empty paragraph right after the given one.
|
|
1151
1193
|
static dynamic After(dynamic doc, dynamic paragraph)
|
|
1152
1194
|
{
|
|
@@ -1158,8 +1200,27 @@ static class Program
|
|
|
1158
1200
|
try
|
|
1159
1201
|
{
|
|
1160
1202
|
made.Style = paragraph.Style;
|
|
1161
|
-
|
|
1162
|
-
|
|
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;
|
|
1163
1224
|
}
|
|
1164
1225
|
catch (COMException) { }
|
|
1165
1226
|
return made;
|
|
@@ -1199,10 +1260,13 @@ static class Program
|
|
|
1199
1260
|
{
|
|
1200
1261
|
dynamic range = caption.Range;
|
|
1201
1262
|
range.ParagraphFormat.Alignment = 1;
|
|
1263
|
+
Whole(range);
|
|
1202
1264
|
range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; range.ParagraphFormat.FirstLineIndent = 0;
|
|
1203
1265
|
range.Font.Bold = 0; range.Font.Italic = 0; range.Font.Color = -16777216; range.Font.Size = 10.5f;
|
|
1204
1266
|
if (!string.IsNullOrEmpty(fontName)) range.Font.Name = fontName;
|
|
1205
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;
|
|
1206
1270
|
range.ParagraphFormat.SpaceBefore = table ? Air : 3f;
|
|
1207
1271
|
range.ParagraphFormat.SpaceAfter = table ? 3f : Air;
|
|
1208
1272
|
Mathify(doc, caption.Range);
|
|
@@ -1263,6 +1327,7 @@ static class Program
|
|
|
1263
1327
|
if (items == null) { items = new ArrayList(); items.Add(new Dictionary<string, object> { { "text", op.Need("text") } }); }
|
|
1264
1328
|
dynamic current = null;
|
|
1265
1329
|
int made = 0, equations = 0;
|
|
1330
|
+
string lint = "";
|
|
1266
1331
|
foreach (object raw in items)
|
|
1267
1332
|
{
|
|
1268
1333
|
Bag item = raw is string ? new Bag(new Dictionary<string, object> { { "text", raw } }) : new Bag(raw);
|
|
@@ -1278,13 +1343,16 @@ static class Program
|
|
|
1278
1343
|
WordFormat(p.Range, StyleLess(item));
|
|
1279
1344
|
Follow(doc, p.Range);
|
|
1280
1345
|
Write(doc, (int)p.Range.Start, text);
|
|
1346
|
+
if (lint.Length == 0) lint = Lint(text);
|
|
1347
|
+
Unnumber(p, text);
|
|
1348
|
+
Whole(p.Range);
|
|
1281
1349
|
equations += Mathify(doc, p.Range);
|
|
1282
1350
|
current = p;
|
|
1283
1351
|
made++;
|
|
1284
1352
|
}
|
|
1285
1353
|
}
|
|
1286
1354
|
int lastIndex = Index(doc, current);
|
|
1287
|
-
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;
|
|
1288
1356
|
}
|
|
1289
1357
|
if ((type == "set_text" || (type == "delete_range" && op.Has("para"))) && op.Raw("expect") == null)
|
|
1290
1358
|
{
|
|
@@ -1297,6 +1365,7 @@ static class Program
|
|
|
1297
1365
|
range.MoveEnd(1, -1);
|
|
1298
1366
|
range.Text = "";
|
|
1299
1367
|
Write(doc, (int)p.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1368
|
+
Whole(p.Range);
|
|
1300
1369
|
int built = Mathify(doc, p.Range);
|
|
1301
1370
|
return "paragraph " + op.Int("para", 0) + " rewritten" + (built > 0 ? ", " + built + " equation(s)" : "");
|
|
1302
1371
|
}
|
|
@@ -1359,6 +1428,7 @@ static class Program
|
|
|
1359
1428
|
if (!string.IsNullOrEmpty(fontFarEast)) table.Range.Font.NameFarEast = fontFarEast;
|
|
1360
1429
|
if (fontSize > 4 && fontSize < 100) table.Range.Font.Size = fontSize;
|
|
1361
1430
|
table.Range.ParagraphFormat.Alignment = 1;
|
|
1431
|
+
Whole(table.Range);
|
|
1362
1432
|
table.Range.Cells.VerticalAlignment = 1;
|
|
1363
1433
|
}
|
|
1364
1434
|
catch (COMException) { }
|
|
@@ -1381,7 +1451,13 @@ static class Program
|
|
|
1381
1451
|
if (Typing) Thread.Sleep(25);
|
|
1382
1452
|
}
|
|
1383
1453
|
}
|
|
1384
|
-
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) { }
|
|
1385
1461
|
if (op.Has("caption")) Caption(doc, table.Range, true, op.Need("caption"), fontName, fontFarEast);
|
|
1386
1462
|
else
|
|
1387
1463
|
{
|
|
@@ -1557,8 +1633,8 @@ static class Program
|
|
|
1557
1633
|
range.HorizontalAlignment = align == "center" ? -4108 : align == "right" ? -4152 : -4131;
|
|
1558
1634
|
}
|
|
1559
1635
|
if (op.Has("wrap")) range.WrapText = op.Flag("wrap", false);
|
|
1560
|
-
if (op.
|
|
1561
|
-
if (op.
|
|
1636
|
+
if (op.On("border")) range.Borders.LineStyle = 1;
|
|
1637
|
+
if (op.On("merge")) range.Merge();
|
|
1562
1638
|
if (op.Has("columnWidth")) range.ColumnWidth = op.Num("columnWidth", 10);
|
|
1563
1639
|
if (op.Has("rowHeight")) range.RowHeight = op.Num("rowHeight", 15);
|
|
1564
1640
|
}
|
|
@@ -1891,8 +1967,97 @@ static class Program
|
|
|
1891
1967
|
|
|
1892
1968
|
// ───────────────────────── rendering ─────────────────────────
|
|
1893
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
|
+
|
|
1894
2058
|
static object Render(string kind, dynamic app, dynamic doc, Bag a)
|
|
1895
2059
|
{
|
|
2060
|
+
if (a.Flag("sheet", false) && kind != "excel") return Sheet(kind, doc, a);
|
|
1896
2061
|
string path = a.Need("out");
|
|
1897
2062
|
int width = Math.Max(320, Math.Min(2400, a.Int("width", 1100)));
|
|
1898
2063
|
Dictionary<string, object> result = new Dictionary<string, object>();
|
package/lib/client.js
CHANGED
|
@@ -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