@copylee/dsh-office 0.2.2 → 0.3.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/README.md +16 -9
- package/helper/OfficeHelper.cs +543 -31
- package/lib/client.js +4 -4
- package/lib/index.js +117 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,13 +14,18 @@
|
|
|
14
14
|
| 实时可见,可以一起改 | 每一步修改即时出现在 Word / Excel / PowerPoint 窗口里。你可以同时在里面打字、调格式,AI 每次动手前读到的都是文档当下的样子 |
|
|
15
15
|
| 不需要窗口在前台 | 修改不靠鼠标键盘,Office 窗口被挡住或在后台也照常进行,你可以同时用电脑做别的事 |
|
|
16
16
|
| 跟随与静默 | 默认“跟随”:AI 改到哪里,窗口自动滚动到哪里(Word 滚到那一段,Excel 滚到那片单元格,PowerPoint 切到那一页),不动你的光标和选区。关闭即静默执行,不动你的视图 |
|
|
17
|
-
| 逐字写入 |
|
|
18
|
-
| 迷你卡片 | AI
|
|
19
|
-
| Word 原生公式 | AI 在文字里用 `$...$`(行内)和 `$$...$$`(单独一行)写 LaTeX,插件转成 Word
|
|
17
|
+
| 逐字写入 | 文字分小段写出,像打字一样出现,表格逐行填入,而不是整块贴上去。每段约多花 0.3 秒,可关闭 |
|
|
18
|
+
| 迷你卡片 | AI 每次动手修改文档时,屏幕右下角出现一张小卡片:正在改哪个文档、改到第几项,带“查看”“逐字”两个开关,点一下立即生效(正在进行的这一批也跟着变)。点“查看”会把文档窗口带到前台并跟随修改位置,按钮变成“查看中”;你切到别的窗口后它自动熄灭,再点一次回来。可拖动,不抢焦点;这一批改完就消失,下一批开始时再出现 |
|
|
19
|
+
| Word 原生公式 | AI 在文字里用 `$...$`(行内)和 `$$...$$`(单独一行)写 LaTeX,插件转成 Word 原生公式:分式、根号、上下标、希腊字母、求和与积分、范数、矩阵(`pmatrix` 等)、分段函数(`cases`)等常用写法;单独一行的公式末尾加 `\tag{1}` 即在右侧对齐编号。正文、表格单元格、题注、查找替换的内容里都可以写 |
|
|
20
|
+
| 引用与参考文献 | 正文里写 `\cite{1}`、`\cite{2,5}`、`\cite{3-6}`,变成上标 [1]、[2,5]、[3-6];`insert_references` 生成参考文献列表。引用是 Word 的交叉引用域,按住 Ctrl 点击跳到对应文献 |
|
|
21
|
+
| 样式与版式 | 直接修改样式本身(标题用什么字体字号、是否自动编号,正文的字体、行距、首行缩进),所有用到它的段落一起变;设置纸张和页边距、页码、页眉、目录(保存时自动刷新)、某段另起一页。这些都不需要再去点 Word 的对话框 |
|
|
20
22
|
| 填模板 | 报告模板常把正文放在大单元格里。这类表格会按段落展开给模型看,正文、公式、表格都写进单元格里面,而不是堆到文档末尾;新写的正文沿用旁边正文的字体、字号和行距 |
|
|
21
23
|
| 规范的图表题注 | 插入表格或图片时给出题注,插件按规范排:自动编号(“表 1”“图 1”,是 Word 题注域,增删后自动重排)、居中、表题在上、图题在下;图表前后留出段间距,图片居中 |
|
|
22
24
|
| 不被自带技能带偏 | DSH 桌面版自带的 Office 技能会让模型写脚本生成文件,这样你看不到过程,文件开着时还会写入失败。插件在模型第一次加载这些技能时把它引回来,确有需要(批量转换、应用打不开的文件)再次请求即放行 |
|
|
23
|
-
| 交付前整体视觉检查 | 保存时如果 AI
|
|
25
|
+
| 交付前整体视觉检查 | 保存时如果 AI 改完之后还没看过成品,保存结果会自动附上整份文档的总览图让它过一遍,有问题先修再保存。总览图把各页(或各张幻灯片)带页码拼在一张图里,一张最多 6 页,最多 18 页;模型也可以随时主动调出总览,再放大某一页细看 |
|
|
26
|
+
| 不拆词、不重复编号 | 插件写入的段落、表格、题注一律按整词换行(有些模板的样式带着“允许西文在单词中间换行”,会把 37000 拆成 37 和 000);标题文字自带编号而样式又自动编号时,去掉自动的那个 |
|
|
27
|
+
| 提醒纯文本公式 | 正文里出现 d_k、10^18 这类没写成公式的下标和幂时,结果里会提醒模型改用公式 |
|
|
28
|
+
| 表格跨页 | 表头行在下一页重复,单行不被拆到两页,表题不会单独留在页尾 |
|
|
24
29
|
| 批量编辑 | `office_edit` 一次调用按顺序执行一批修改(写一整篇文档、填一张表加公式加图表、做几页幻灯片),中途某一项失败就停下并报告已完成哪些 |
|
|
25
30
|
| 改错位置的防护 | Word 按“段落编号 + 这段开头的文字”定位。你刚改过导致编号对不上时,这一步会被拒绝并要求重新读取;改写和删除段落必须带开头文字 |
|
|
26
31
|
| Word 一次撤销 | AI 的一批修改在 Word 里是一个撤销步骤,按一次 Ctrl+Z 整批撤回 |
|
|
@@ -61,12 +66,13 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
61
66
|
| `office_status` | 列出三个应用里现在打开了什么、哪些有未保存的修改 |
|
|
62
67
|
| `office_read` | 把打开的文档读成文本(段落 / 单元格 / 形状) |
|
|
63
68
|
| `office_edit` | 按顺序执行一批修改,不保存 |
|
|
64
|
-
| `office_render` | 把一页 / 一张幻灯片 / 一个单元格区域渲染成图片;Word 和 PowerPoint 一次最多看 4
|
|
69
|
+
| `office_render` | 把一页 / 一张幻灯片 / 一个单元格区域渲染成图片;Word 和 PowerPoint 一次最多看 4 页,或用总览把全部页面拼成一张图 |
|
|
65
70
|
| `office_save` | 保存;给出路径时另存为,扩展名为 `.pdf` 时导出 PDF;改完没看过成品时附带各页图片 |
|
|
71
|
+
| `office_close` | 关闭 AI 自己打开或新建的文档;你原本开着的文档会被拒绝,有未保存修改时必须明确说保存还是丢弃 |
|
|
66
72
|
|
|
67
73
|
`office_edit` 支持的操作:
|
|
68
74
|
|
|
69
|
-
- Word:`insert_paragraphs`、`set_text`、`replace_text`、`format_text`、`delete_range`、`insert_table`、`set_cell`、`insert_image`
|
|
75
|
+
- Word:`insert_paragraphs`、`set_text`、`replace_text`、`format_text`、`delete_range`、`insert_table`、`set_cell`、`insert_image`、`insert_references`、`style_format`、`page_setup`、`page_numbers`、`header`、`page_break`、`insert_toc`、`update_fields`
|
|
70
76
|
- Excel:`write_range`(值或公式)、`format_range`、`autofit`、`insert_rows`、`delete_rows`、`add_sheet`、`rename_sheet`、`delete_sheet`、`add_chart`
|
|
71
77
|
- PowerPoint:`add_slide`、`set_text`、`add_textbox`、`add_image`、`set_shape`、`delete_shape`、`delete_slide`、`move_slide`、`set_notes`
|
|
72
78
|
|
|
@@ -78,8 +84,8 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
78
84
|
|---|---|---|
|
|
79
85
|
| 跟随 AI 的修改位置 | 开 | 窗口自动滚动到 AI 正在修改的位置;关闭为静默执行 |
|
|
80
86
|
| 逐字写入 | 开 | 文字像打字一样逐步出现;关闭后写得最快 |
|
|
81
|
-
| 迷你卡片 | 开 | AI
|
|
82
|
-
| 交付前整体视觉检查 | 开 |
|
|
87
|
+
| 迷你卡片 | 开 | AI 修改文档期间在右下角显示带“查看”“逐字”开关的小卡片。在卡片上切换过的开关(包括你离开文档窗口后自动熄灭的“查看”),到 DSH 重启前以卡片为准 |
|
|
88
|
+
| 交付前整体视觉检查 | 开 | 保存时自动把整份文档的总览图交给 AI 检查;会多用一些 token |
|
|
83
89
|
| 优先在真实 Office 里操作 | 开 | DSH 桌面版自带 office-docx / office-xlsx / office-pptx 技能,用脚本生成文件。开启后,AI 第一次想加载这些技能时会被引回本插件(对应的 Office 应用已安装时才拦);它再次请求时放行 |
|
|
84
90
|
| 打开文档时显示到前台 | 开 | 关闭后文档仍会打开,只是不抢当前窗口 |
|
|
85
91
|
| 核对排版的图片宽度 | 1100 像素 | `office_render` 给模型的图片宽度;越大越清晰,也越费 token |
|
|
@@ -91,7 +97,8 @@ AI 只有在调用保存时才写入文件,不会替你关闭文档。
|
|
|
91
97
|
- Excel 和 PowerPoint 里 AI 的修改无法用 Ctrl+Z 撤回(Excel 的外部修改不进撤销栈,并且会清空已有的撤销记录)。只有 Word 支持整批撤销。
|
|
92
98
|
- 你正在单元格里输入、或 Office 开着模态对话框时,修改会被 Office 拒绝;插件等待约 5 秒后报告“应用忙”。
|
|
93
99
|
- Excel 区域的图片是通过剪贴板取得的,渲染时剪贴板里的文字会被保留,图片、文件等其他内容会丢失。
|
|
94
|
-
- 公式只支持 Word,PowerPoint 和 Excel 里还不能写原生公式;LaTeX
|
|
100
|
+
- 公式只支持 Word,PowerPoint 和 Excel 里还不能写原生公式;LaTeX 只转换常用写法,没转成功的公式保留为文本并在结果里提示。
|
|
101
|
+
- 引用按编号书写,插入或删除文献后不会自动重新编号。
|
|
95
102
|
- 兼容模式(Word 97–2003 格式)的文档里不能建立原生公式。
|
|
96
103
|
- 目前不支持 WPS。
|
|
97
104
|
- 没有打开 Office 时处理文件、批量转换格式不在本插件范围内。
|
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);
|
|
@@ -109,6 +119,8 @@ class Card : Form
|
|
|
109
119
|
static readonly ManualResetEvent Ready = new ManualResetEvent(false);
|
|
110
120
|
/// Set once the user has flipped a switch here: from then on the card, not the settings page, decides.
|
|
111
121
|
public static volatile bool FollowChosen, TypingChosen;
|
|
122
|
+
/// Set for as long as an edit is running: the card stays up through it, however long one operation takes, and goes when it ends.
|
|
123
|
+
public static volatile bool Hold;
|
|
112
124
|
|
|
113
125
|
string line = "";
|
|
114
126
|
DateTime shown = DateTime.MinValue;
|
|
@@ -174,7 +186,7 @@ class Card : Form
|
|
|
174
186
|
}
|
|
175
187
|
bool nowLit = Program.Following && watching;
|
|
176
188
|
if (nowLit != lit) { lit = nowLit; Invalidate(); }
|
|
177
|
-
if (Visible && (DateTime.UtcNow - shown).TotalSeconds >
|
|
189
|
+
if (Visible && !Hold && (DateTime.UtcNow - shown).TotalSeconds > 1) Hide();
|
|
178
190
|
};
|
|
179
191
|
timer.Start();
|
|
180
192
|
}
|
|
@@ -215,7 +227,8 @@ class Card : Form
|
|
|
215
227
|
{
|
|
216
228
|
int right = Width - S(10);
|
|
217
229
|
Chip(g, font, "逐字", Program.Typing, ref right, out typingBox);
|
|
218
|
-
|
|
230
|
+
bool following = Program.Following && Watching();
|
|
231
|
+
Chip(g, font, following ? "查看中" : "查看", following, ref right, out followBox);
|
|
219
232
|
using (SolidBrush dot = new SolidBrush(Color.FromArgb(217, 119, 87))) g.FillEllipse(dot, S(12), (Height - S(8)) / 2, S(8), S(8));
|
|
220
233
|
Rectangle text = new Rectangle(S(26), 0, right - S(30), Height);
|
|
221
234
|
TextRenderer.DrawText(g, line, font, text, Color.FromArgb(235, 235, 240), TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
|
|
@@ -496,6 +509,13 @@ static class Program
|
|
|
496
509
|
static object Run(string cmd, Bag a)
|
|
497
510
|
{
|
|
498
511
|
if (cmd == "ping") return "pong";
|
|
512
|
+
if (cmd == "card")
|
|
513
|
+
{
|
|
514
|
+
// The agent's turn ended: say so and let the card go.
|
|
515
|
+
Card.Hold = a.Flag("hold", false);
|
|
516
|
+
if (cardStarted) Card.Report(a.Str("text", "AI 已完成"));
|
|
517
|
+
return "ok";
|
|
518
|
+
}
|
|
499
519
|
if (cmd == "status") return Status();
|
|
500
520
|
string kind = Kind(a);
|
|
501
521
|
if (cmd == "open") return Open(kind, a);
|
|
@@ -610,7 +630,7 @@ static class Program
|
|
|
610
630
|
if (!cardStarted) { cardStarted = true; Card.Start(); }
|
|
611
631
|
Following = Card.FollowChosen ? Following : a.Flag("follow", false);
|
|
612
632
|
Typing = Card.TypingChosen ? Typing : a.Flag("typing", false);
|
|
613
|
-
|
|
633
|
+
// No card here: it shows while an edit is running.
|
|
614
634
|
}
|
|
615
635
|
return info;
|
|
616
636
|
}
|
|
@@ -639,6 +659,7 @@ static class Program
|
|
|
639
659
|
|
|
640
660
|
static object Save(string kind, dynamic app, dynamic doc, Bag a)
|
|
641
661
|
{
|
|
662
|
+
if (kind == "word") Refresh(doc);
|
|
642
663
|
string path = a.Str("path", null);
|
|
643
664
|
Dictionary<string, object> result = new Dictionary<string, object>();
|
|
644
665
|
if (path == null)
|
|
@@ -744,11 +765,11 @@ static class Program
|
|
|
744
765
|
try { doc.Windows[1].ScrollIntoView(range, true); } catch (Exception) { }
|
|
745
766
|
}
|
|
746
767
|
|
|
747
|
-
/// Pieces to write a text in: up to
|
|
768
|
+
/// Pieces to write a text in: up to 14 of them, so that a long paragraph takes about a third of a second.
|
|
748
769
|
static List<string> Pieces(string text)
|
|
749
770
|
{
|
|
750
771
|
List<string> pieces = new List<string>();
|
|
751
|
-
int size = Math.Max(
|
|
772
|
+
int size = Math.Max(3, (int)Math.Ceiling(text.Length / 14.0));
|
|
752
773
|
for (int i = 0; i < text.Length; i += size) pieces.Add(text.Substring(i, Math.Min(size, text.Length - i)));
|
|
753
774
|
return pieces;
|
|
754
775
|
}
|
|
@@ -766,7 +787,7 @@ static class Program
|
|
|
766
787
|
{
|
|
767
788
|
doc.Range(at, at).InsertAfter(piece);
|
|
768
789
|
at += piece.Length;
|
|
769
|
-
Thread.Sleep(
|
|
790
|
+
Thread.Sleep(14);
|
|
770
791
|
}
|
|
771
792
|
return at;
|
|
772
793
|
}
|
|
@@ -786,6 +807,9 @@ static class Program
|
|
|
786
807
|
{ "to", "→" }, { "rightarrow", "→" }, { "leftarrow", "←" }, { "Rightarrow", "⇒" }, { "Leftrightarrow", "⇔" }, { "in", "∈" }, { "notin", "∉" },
|
|
787
808
|
{ "subset", "⊂" }, { "cup", "∪" }, { "cap", "∩" }, { "forall", "∀" }, { "exists", "∃" }, { "angle", "∠" }, { "perp", "⊥" }, { "parallel", "∥" },
|
|
788
809
|
{ "circ", "∘" }, { "degree", "°" }, { "ldots", "…" }, { "cdots", "⋯" }, { "dots", "…" }, { "prime", "′" }, { "hbar", "ℏ" }, { "ell", "ℓ" },
|
|
810
|
+
{ "Vert", "‖" }, { "lVert", "‖" }, { "rVert", "‖" }, { "vert", "|" }, { "lvert", "|" }, { "rvert", "|" }, { "mid", "|" },
|
|
811
|
+
{ "top", "⊤" }, { "langle", "⟨" }, { "rangle", "⟩" }, { "otimes", "⊗" }, { "oplus", "⊕" }, { "neg", "¬" }, { "ast", "∗" },
|
|
812
|
+
{ "det", "det" }, { "rank", "rank" }, { "dim", "dim" }, { "sup", "sup" }, { "inf", "inf" }, { "arg", "arg" }, { "cond", "cond" },
|
|
789
813
|
{ "quad", " " }, { "qquad", " " }, { "left", "" }, { "right", "" }, { "displaystyle", "" }, { "limits", "" }, { "lim", "lim" },
|
|
790
814
|
{ "sin", "sin" }, { "cos", "cos" }, { "tan", "tan" }, { "cot", "cot" }, { "ln", "ln" }, { "log", "log" }, { "exp", "exp" }, { "max", "max" }, { "min", "min" },
|
|
791
815
|
{ "arcsin", "arcsin" }, { "arccos", "arccos" }, { "arctan", "arctan" }, { "sinh", "sinh" }, { "cosh", "cosh" }, { "tanh", "tanh" },
|
|
@@ -831,9 +855,19 @@ static class Program
|
|
|
831
855
|
{
|
|
832
856
|
StringBuilder o = new StringBuilder();
|
|
833
857
|
int i = 0;
|
|
858
|
+
// A sum, product or integral was written and its limits may still follow; what comes after them is its
|
|
859
|
+
// operand, which Word's linear format introduces with "▒" (left out, Word draws an empty box there).
|
|
860
|
+
bool nary = false;
|
|
834
861
|
while (i < s.Length)
|
|
835
862
|
{
|
|
836
863
|
char c = s[i];
|
|
864
|
+
if (nary && c != '^' && c != '_' && c != ' ')
|
|
865
|
+
{
|
|
866
|
+
if (c == '\\' && string.CompareOrdinal(s, i, "\\limits", 0, 7) == 0) { i += 7; continue; }
|
|
867
|
+
if (c == '\\' && string.CompareOrdinal(s, i, "\\nolimits", 0, 9) == 0) { i += 9; continue; }
|
|
868
|
+
o.Append('▒');
|
|
869
|
+
nary = false;
|
|
870
|
+
}
|
|
837
871
|
if (c == '\\')
|
|
838
872
|
{
|
|
839
873
|
int start = ++i;
|
|
@@ -842,12 +876,51 @@ static class Program
|
|
|
842
876
|
{
|
|
843
877
|
// \, \; \! \ are spacing; \{ \} \% and the like are the character itself.
|
|
844
878
|
char next = i < s.Length ? s[i++] : ' ';
|
|
845
|
-
if (next == '
|
|
879
|
+
if (next == '|') o.Append('‖');
|
|
880
|
+
else if (next == ',' || next == ';' || next == ':' || next == ' ') o.Append(' ');
|
|
846
881
|
else if (next == '\\') o.Append(' ');
|
|
847
882
|
else if (next != '!') o.Append(next);
|
|
848
883
|
continue;
|
|
849
884
|
}
|
|
850
885
|
string name = s.Substring(start, i - start);
|
|
886
|
+
if (name == "begin")
|
|
887
|
+
{
|
|
888
|
+
// \begin{pmatrix} a & b \\ c & d \end{pmatrix}: Word's linear form is (■(a&b@c&d)).
|
|
889
|
+
int close = s.IndexOf('}', i);
|
|
890
|
+
string env = close > i ? s.Substring(i + 1, close - i - 1).Trim() : "";
|
|
891
|
+
string ending = "\\end{" + env + "}";
|
|
892
|
+
int stop = close > i ? s.IndexOf(ending, close, StringComparison.Ordinal) : -1;
|
|
893
|
+
if (stop > 0)
|
|
894
|
+
{
|
|
895
|
+
string body = s.Substring(close + 1, stop - close - 1);
|
|
896
|
+
i = stop + ending.Length;
|
|
897
|
+
if (env == "array" && body.TrimStart().StartsWith("{", StringComparison.Ordinal)) body = body.Substring(body.IndexOf('}') + 1);
|
|
898
|
+
List<string> rows = new List<string>();
|
|
899
|
+
foreach (string row in body.Replace("\\\\", "\u0001").Split('\u0001'))
|
|
900
|
+
{
|
|
901
|
+
if (row.Trim().Length == 0) continue;
|
|
902
|
+
List<string> cells = new List<string>();
|
|
903
|
+
foreach (string cell in row.Split('&')) cells.Add(Tex(cell.Trim()));
|
|
904
|
+
rows.Add(string.Join("&", cells.ToArray()));
|
|
905
|
+
}
|
|
906
|
+
string grid = string.Join("@", rows.ToArray());
|
|
907
|
+
if (env == "pmatrix") o.Append("(■(" + grid + "))");
|
|
908
|
+
else if (env == "bmatrix") o.Append("[■(" + grid + ")]");
|
|
909
|
+
else if (env == "vmatrix") o.Append("|■(" + grid + ")|");
|
|
910
|
+
else if (env == "Vmatrix") o.Append("‖■(" + grid + ")‖");
|
|
911
|
+
else if (env == "cases") o.Append("{█(" + grid + ")┤");
|
|
912
|
+
else if (env == "aligned" || env == "align" || env == "split" || env == "gathered") o.Append("█(" + grid + ")");
|
|
913
|
+
else o.Append("■(" + grid + ")");
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (name == "mathbb")
|
|
918
|
+
{
|
|
919
|
+
string letter = TexArg(s, ref i);
|
|
920
|
+
string doubled = letter == "R" ? "ℝ" : letter == "N" ? "ℕ" : letter == "Z" ? "ℤ" : letter == "Q" ? "ℚ" : letter == "C" ? "ℂ" : letter;
|
|
921
|
+
o.Append(doubled);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
851
924
|
if (name == "frac" || name == "dfrac" || name == "tfrac") {
|
|
852
925
|
string top = TexArg(s, ref i), bottom = TexArg(s, ref i);
|
|
853
926
|
// A factor written right before the fraction (2rac{a}{b}) must not be read into its numerator.
|
|
@@ -881,6 +954,7 @@ static class Program
|
|
|
881
954
|
if (TexSymbols.TryGetValue(name, out symbol))
|
|
882
955
|
{
|
|
883
956
|
o.Append(symbol);
|
|
957
|
+
if (symbol == "∑" || symbol == "∏" || symbol == "∫" || symbol == "∮") nary = true;
|
|
884
958
|
// A function name is applied to what follows: Word needs a space after it.
|
|
885
959
|
if (symbol.Length > 1 && symbol == name && i < s.Length && s[i] != ' ' && s[i] != '^' && s[i] != '_') o.Append(' ');
|
|
886
960
|
}
|
|
@@ -894,8 +968,8 @@ static class Program
|
|
|
894
968
|
bool braced = i < s.Length && s[i] == '{';
|
|
895
969
|
string arg = TexArg(s, ref i);
|
|
896
970
|
o.Append(c).Append(braced && arg.Length > 1 ? "(" + arg + ")" : arg);
|
|
897
|
-
//
|
|
898
|
-
if (i < s.Length &&
|
|
971
|
+
// A space ends the script; otherwise Word reads what follows into it: x^2y, λ_max(A).
|
|
972
|
+
if (!nary && i < s.Length && "^_ )]},.;:,。、".IndexOf(s[i]) < 0) o.Append(' ');
|
|
899
973
|
continue;
|
|
900
974
|
}
|
|
901
975
|
if (c == '{') { string group = TexArg(s, ref i); o.Append("〖" + group + "〗"); continue; }
|
|
@@ -923,6 +997,30 @@ static class Program
|
|
|
923
997
|
System.Text.RegularExpressions.Match m = found[k];
|
|
924
998
|
bool display = m.Groups[1].Success;
|
|
925
999
|
string source = (display ? m.Groups[1].Value : m.Groups[2].Value).Trim();
|
|
1000
|
+
try
|
|
1001
|
+
{
|
|
1002
|
+
if (BuildOne(doc, range, start, m, display, source)) made++;
|
|
1003
|
+
else MathFailed++;
|
|
1004
|
+
}
|
|
1005
|
+
catch (Exception) { MathFailed++; }
|
|
1006
|
+
}
|
|
1007
|
+
return made;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/// Formulas that could not be turned into equations during the current operation (they stay as text).
|
|
1011
|
+
static int MathFailed;
|
|
1012
|
+
|
|
1013
|
+
static string MathNote()
|
|
1014
|
+
{
|
|
1015
|
+
if (MathFailed == 0) return "";
|
|
1016
|
+
string note = " — NOTE " + MathFailed + " formula(s) could not be built and were left as text: check them with office_render and rewrite them more simply";
|
|
1017
|
+
MathFailed = 0;
|
|
1018
|
+
return note;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
static bool BuildOne(dynamic doc, dynamic range, int start, System.Text.RegularExpressions.Match m, bool display, string source)
|
|
1022
|
+
{
|
|
1023
|
+
{
|
|
926
1024
|
// ag{1} numbers a display equation: Word sets "#(1)" flush right on the equation's line.
|
|
927
1025
|
string tag = null;
|
|
928
1026
|
System.Text.RegularExpressions.Match tagged = Tag.Match(source);
|
|
@@ -941,11 +1039,10 @@ static class Program
|
|
|
941
1039
|
dynamic math = doc.OMaths.Add(spot);
|
|
942
1040
|
math.OMaths[1].BuildUp();
|
|
943
1041
|
if (display) { try { math.OMaths[1].Type = 0; math.OMaths[1].Justification = 1; } catch (COMException) { } }
|
|
944
|
-
|
|
1042
|
+
return true;
|
|
945
1043
|
}
|
|
946
|
-
catch (COMException) { }
|
|
1044
|
+
catch (COMException) { return false; }
|
|
947
1045
|
}
|
|
948
|
-
return made;
|
|
949
1046
|
}
|
|
950
1047
|
|
|
951
1048
|
// ───────────────────────── Word ─────────────────────────
|
|
@@ -1121,6 +1218,9 @@ static class Program
|
|
|
1121
1218
|
if (op.Has("spaceBefore")) range.ParagraphFormat.SpaceBefore = (float)op.Num("spaceBefore", 0);
|
|
1122
1219
|
if (op.Has("spaceAfter")) range.ParagraphFormat.SpaceAfter = (float)op.Num("spaceAfter", 0);
|
|
1123
1220
|
if (op.Has("lineSpacing")) { range.ParagraphFormat.LineSpacingRule = 5; range.ParagraphFormat.LineSpacing = (float)(op.Num("lineSpacing", 1) * 12); }
|
|
1221
|
+
if (op.Has("indentChars")) { range.ParagraphFormat.FirstLineIndent = 0; range.ParagraphFormat.CharacterUnitFirstLineIndent = (float)op.Num("indentChars", 2); }
|
|
1222
|
+
if (op.Has("superscript")) range.Font.Superscript = op.Flag("superscript", false) ? 1 : 0;
|
|
1223
|
+
if (op.Has("subscript")) range.Font.Subscript = op.Flag("subscript", false) ? 1 : 0;
|
|
1124
1224
|
}
|
|
1125
1225
|
|
|
1126
1226
|
/// A new empty paragraph where an insert operation points: after / before paragraph "para", or at the start / end.
|
|
@@ -1147,6 +1247,37 @@ static class Program
|
|
|
1147
1247
|
return doc.Range(start, start).Paragraphs[1];
|
|
1148
1248
|
}
|
|
1149
1249
|
|
|
1250
|
+
static readonly System.Text.RegularExpressions.Regex Numbered = new System.Text.RegularExpressions.Regex(@"^\s*(第?[一二三四五六七八九十百]+[、..章节]|[((][一二三四五六七八九十\d]+[))]|\d+(\.\d+)*[\s、..])");
|
|
1251
|
+
|
|
1252
|
+
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");
|
|
1253
|
+
|
|
1254
|
+
/// Subscripts and powers written as plain text (d_k, 10^18) outside $...$: say so, with the first one found.
|
|
1255
|
+
static string Lint(string text)
|
|
1256
|
+
{
|
|
1257
|
+
if (text == null) return "";
|
|
1258
|
+
string outside = Dollars.Replace(text, " ");
|
|
1259
|
+
System.Text.RegularExpressions.Match m = PlainMath.Match(outside);
|
|
1260
|
+
if (!m.Success) return "";
|
|
1261
|
+
int start = Math.Max(0, m.Index - 6);
|
|
1262
|
+
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";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/// Keep Latin words and numbers whole at the end of a line. Some templates carry "allow Latin text to wrap in
|
|
1266
|
+
/// the middle of a word" in a style, and every paragraph written after it inherits that: 37 / 000, mode / l.
|
|
1267
|
+
/// (Word's object model names this backwards: WordWrap = True is the whole-word setting.)
|
|
1268
|
+
static void Whole(dynamic range)
|
|
1269
|
+
{
|
|
1270
|
+
try { range.ParagraphFormat.WordWrap = -1; } catch (Exception) { }
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/// The text already starts with its number ("一、", "2.1 ") and the style would add another: drop the automatic one.
|
|
1274
|
+
static void Unnumber(dynamic paragraph, string text)
|
|
1275
|
+
{
|
|
1276
|
+
if (!Numbered.IsMatch(text)) return;
|
|
1277
|
+
try { if ((int)paragraph.Range.ListFormat.ListType != 0) paragraph.Range.ListFormat.RemoveNumbers(); }
|
|
1278
|
+
catch (Exception) { }
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1150
1281
|
/// A new empty paragraph right after the given one.
|
|
1151
1282
|
static dynamic After(dynamic doc, dynamic paragraph)
|
|
1152
1283
|
{
|
|
@@ -1158,8 +1289,27 @@ static class Program
|
|
|
1158
1289
|
try
|
|
1159
1290
|
{
|
|
1160
1291
|
made.Style = paragraph.Style;
|
|
1161
|
-
|
|
1162
|
-
|
|
1292
|
+
dynamic from = paragraph.Range.ParagraphFormat, to = made.Range.ParagraphFormat;
|
|
1293
|
+
to.Alignment = from.Alignment;
|
|
1294
|
+
to.LeftIndent = from.LeftIndent; to.RightIndent = from.RightIndent;
|
|
1295
|
+
to.CharacterUnitFirstLineIndent = from.CharacterUnitFirstLineIndent; to.FirstLineIndent = from.FirstLineIndent;
|
|
1296
|
+
to.SpaceBefore = from.SpaceBefore; to.SpaceAfter = from.SpaceAfter;
|
|
1297
|
+
to.LineSpacingRule = from.LineSpacingRule;
|
|
1298
|
+
if ((int)from.LineSpacingRule >= 3) to.LineSpacing = from.LineSpacing;
|
|
1299
|
+
dynamic font = paragraph.Range.Font, target = made.Range.Font;
|
|
1300
|
+
// 9999999 / empty mean "mixed" in the source paragraph: leave those to the style.
|
|
1301
|
+
string name = (string)font.Name, farEast = (string)font.NameFarEast;
|
|
1302
|
+
if (!string.IsNullOrEmpty(name)) target.Name = name;
|
|
1303
|
+
if (!string.IsNullOrEmpty(farEast)) target.NameFarEast = farEast;
|
|
1304
|
+
float size = (float)font.Size;
|
|
1305
|
+
if (size > 0 && size < 1000) target.Size = size;
|
|
1306
|
+
foreach (string flag in new string[] { "Bold", "Italic" })
|
|
1307
|
+
{
|
|
1308
|
+
int value = flag == "Bold" ? (int)font.Bold : (int)font.Italic;
|
|
1309
|
+
if (value == 0 || value == -1) { if (flag == "Bold") target.Bold = value; else target.Italic = value; }
|
|
1310
|
+
}
|
|
1311
|
+
int color = (int)font.Color;
|
|
1312
|
+
if (color != 9999999) target.Color = color;
|
|
1163
1313
|
}
|
|
1164
1314
|
catch (COMException) { }
|
|
1165
1315
|
return made;
|
|
@@ -1199,10 +1349,13 @@ static class Program
|
|
|
1199
1349
|
{
|
|
1200
1350
|
dynamic range = caption.Range;
|
|
1201
1351
|
range.ParagraphFormat.Alignment = 1;
|
|
1352
|
+
Whole(range);
|
|
1202
1353
|
range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; range.ParagraphFormat.FirstLineIndent = 0;
|
|
1203
1354
|
range.Font.Bold = 0; range.Font.Italic = 0; range.Font.Color = -16777216; range.Font.Size = 10.5f;
|
|
1204
1355
|
if (!string.IsNullOrEmpty(fontName)) range.Font.Name = fontName;
|
|
1205
1356
|
if (!string.IsNullOrEmpty(fontFarEast)) range.Font.NameFarEast = fontFarEast;
|
|
1357
|
+
// A table's caption must not be left alone at the foot of a page.
|
|
1358
|
+
if (table) range.ParagraphFormat.KeepWithNext = -1;
|
|
1206
1359
|
range.ParagraphFormat.SpaceBefore = table ? Air : 3f;
|
|
1207
1360
|
range.ParagraphFormat.SpaceAfter = table ? 3f : Air;
|
|
1208
1361
|
Mathify(doc, caption.Range);
|
|
@@ -1247,22 +1400,35 @@ static class Program
|
|
|
1247
1400
|
bool all = op.Flag("all", true), matchCase = op.Flag("matchCase", true);
|
|
1248
1401
|
dynamic range = doc.Content;
|
|
1249
1402
|
int count = 0;
|
|
1250
|
-
|
|
1403
|
+
// "^" starts a special code in Word's find box; here the text is always meant literally.
|
|
1404
|
+
string literal = find.Replace("^", "^^");
|
|
1405
|
+
bool rich = replace.IndexOf('$') >= 0 || replace.IndexOf("\\cite", StringComparison.Ordinal) >= 0;
|
|
1406
|
+
string extra = "";
|
|
1407
|
+
while (count < 5000 && (bool)range.Find.Execute(FindText: literal, MatchCase: matchCase, MatchWildcards: false, Forward: true, Wrap: 0))
|
|
1251
1408
|
{
|
|
1252
1409
|
range.Text = replace;
|
|
1253
|
-
|
|
1410
|
+
if (rich)
|
|
1411
|
+
{
|
|
1412
|
+
// Building formulas changes the length: keep the place by its distance from the end.
|
|
1413
|
+
int tail = (int)doc.Content.End - (int)range.End;
|
|
1414
|
+
extra = Enrich(doc, range);
|
|
1415
|
+
int resume = (int)doc.Content.End - tail;
|
|
1416
|
+
range = doc.Range(resume, resume);
|
|
1417
|
+
}
|
|
1418
|
+
else range.Collapse(0);
|
|
1254
1419
|
count++;
|
|
1255
1420
|
if (!all) break;
|
|
1256
1421
|
}
|
|
1257
1422
|
if (count == 0) throw new Fail("NOT_FOUND", "The text \"" + Clip(find, 60) + "\" does not occur in the document.");
|
|
1258
|
-
return "replaced " + count;
|
|
1423
|
+
return "replaced " + count + extra + Lint(replace);
|
|
1259
1424
|
}
|
|
1260
1425
|
if (type == "insert_paragraphs")
|
|
1261
1426
|
{
|
|
1262
1427
|
IList items = op.List("items");
|
|
1263
1428
|
if (items == null) { items = new ArrayList(); items.Add(new Dictionary<string, object> { { "text", op.Need("text") } }); }
|
|
1264
1429
|
dynamic current = null;
|
|
1265
|
-
int made = 0, equations = 0;
|
|
1430
|
+
int made = 0, equations = 0, cites = 0;
|
|
1431
|
+
string lint = "";
|
|
1266
1432
|
foreach (object raw in items)
|
|
1267
1433
|
{
|
|
1268
1434
|
Bag item = raw is string ? new Bag(new Dictionary<string, object> { { "text", raw } }) : new Bag(raw);
|
|
@@ -1278,13 +1444,22 @@ static class Program
|
|
|
1278
1444
|
WordFormat(p.Range, StyleLess(item));
|
|
1279
1445
|
Follow(doc, p.Range);
|
|
1280
1446
|
Write(doc, (int)p.Range.Start, text);
|
|
1447
|
+
if (lint.Length == 0) lint = Lint(text);
|
|
1448
|
+
Unnumber(p, text);
|
|
1449
|
+
try
|
|
1450
|
+
{
|
|
1451
|
+
if (Unnumbered.Count > 0 && Unnumbered.Contains((string)doc.FullName + "|" + (string)p.Style.NameLocal) && (int)p.Range.ListFormat.ListType != 0) p.Range.ListFormat.RemoveNumbers();
|
|
1452
|
+
}
|
|
1453
|
+
catch (Exception) { }
|
|
1454
|
+
Whole(p.Range);
|
|
1455
|
+
cites += Cite(doc, p.Range);
|
|
1281
1456
|
equations += Mathify(doc, p.Range);
|
|
1282
1457
|
current = p;
|
|
1283
1458
|
made++;
|
|
1284
1459
|
}
|
|
1285
1460
|
}
|
|
1286
1461
|
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)" : "");
|
|
1462
|
+
return "inserted " + made + " paragraph(s), now paragraphs " + (lastIndex - made + 1) + "–" + lastIndex + " of " + (int)doc.Paragraphs.Count + (equations > 0 ? ", " + equations + " equation(s)" : "") + (cites > 0 ? ", " + cites + " citation(s)" : "") + MathNote() + lint;
|
|
1288
1463
|
}
|
|
1289
1464
|
if ((type == "set_text" || (type == "delete_range" && op.Has("para"))) && op.Raw("expect") == null)
|
|
1290
1465
|
{
|
|
@@ -1297,8 +1472,9 @@ static class Program
|
|
|
1297
1472
|
range.MoveEnd(1, -1);
|
|
1298
1473
|
range.Text = "";
|
|
1299
1474
|
Write(doc, (int)p.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1300
|
-
|
|
1301
|
-
|
|
1475
|
+
Whole(p.Range);
|
|
1476
|
+
string built = Enrich(doc, p.Range);
|
|
1477
|
+
return "paragraph " + op.Int("para", 0) + " rewritten" + built + Lint(Lines(op.Raw("text") ?? ""));
|
|
1302
1478
|
}
|
|
1303
1479
|
if (type == "format_text" || type == "set_style")
|
|
1304
1480
|
{
|
|
@@ -1343,6 +1519,7 @@ static class Program
|
|
|
1343
1519
|
catch (Exception) { }
|
|
1344
1520
|
Plain(p, "Normal");
|
|
1345
1521
|
Follow(doc, p.Range);
|
|
1522
|
+
string cellLint = "";
|
|
1346
1523
|
dynamic table = doc.Tables.Add(p.Range, rows, cols);
|
|
1347
1524
|
// The cells take the formatting of the paragraph that follows (a heading, say): make them plain.
|
|
1348
1525
|
SetStyle(table.Range, "Normal");
|
|
@@ -1359,6 +1536,7 @@ static class Program
|
|
|
1359
1536
|
if (!string.IsNullOrEmpty(fontFarEast)) table.Range.Font.NameFarEast = fontFarEast;
|
|
1360
1537
|
if (fontSize > 4 && fontSize < 100) table.Range.Font.Size = fontSize;
|
|
1361
1538
|
table.Range.ParagraphFormat.Alignment = 1;
|
|
1539
|
+
Whole(table.Range);
|
|
1362
1540
|
table.Range.Cells.VerticalAlignment = 1;
|
|
1363
1541
|
}
|
|
1364
1542
|
catch (COMException) { }
|
|
@@ -1376,12 +1554,19 @@ static class Program
|
|
|
1376
1554
|
dynamic cell = table.Cell(r + 1, c + 1).Range;
|
|
1377
1555
|
string value = Convert.ToString(cells[c], System.Globalization.CultureInfo.InvariantCulture);
|
|
1378
1556
|
cell.Text = value;
|
|
1379
|
-
if (value.IndexOf('$') >= 0)
|
|
1557
|
+
if (value.IndexOf('$') >= 0 || value.IndexOf("\\cite", StringComparison.Ordinal) >= 0) Enrich(doc, table.Cell(r + 1, c + 1).Range);
|
|
1558
|
+
else if (cellLint.Length == 0) cellLint = Lint(value);
|
|
1380
1559
|
}
|
|
1381
1560
|
if (Typing) Thread.Sleep(25);
|
|
1382
1561
|
}
|
|
1383
1562
|
}
|
|
1384
|
-
if (op.Flag("header", true))
|
|
1563
|
+
if (op.Flag("header", true))
|
|
1564
|
+
{
|
|
1565
|
+
table.Rows[1].Range.Font.Bold = 1;
|
|
1566
|
+
// On a page break the header row is repeated at the top of the next page.
|
|
1567
|
+
try { table.Rows[1].HeadingFormat = -1; } catch (COMException) { }
|
|
1568
|
+
}
|
|
1569
|
+
try { table.Rows.AllowBreakAcrossPages = 0; } catch (COMException) { }
|
|
1385
1570
|
if (op.Has("caption")) Caption(doc, table.Range, true, op.Need("caption"), fontName, fontFarEast);
|
|
1386
1571
|
else
|
|
1387
1572
|
{
|
|
@@ -1393,7 +1578,7 @@ static class Program
|
|
|
1393
1578
|
int tableEnd = (int)doc.Range(0, table.Range.End).Paragraphs.Count;
|
|
1394
1579
|
bool nested = false;
|
|
1395
1580
|
try { nested = (int)table.NestingLevel > 1; } catch (Exception) { }
|
|
1396
|
-
return (nested ? "table inserted inside a cell of table " + (int)doc.Range(0, table.Range.End).Tables.Count : "table " + (int)doc.Range(0, table.Range.End).Tables.Count + " inserted") + " (" + rows + "×" + cols + "), now paragraphs " + (tableEnd - (int)table.Range.Paragraphs.Count + 1) + "–" + tableEnd + " of " + (int)doc.Paragraphs.Count;
|
|
1581
|
+
return (nested ? "table inserted inside a cell of table " + (int)doc.Range(0, table.Range.End).Tables.Count : "table " + (int)doc.Range(0, table.Range.End).Tables.Count + " inserted") + " (" + rows + "×" + cols + "), now paragraphs " + (tableEnd - (int)table.Range.Paragraphs.Count + 1) + "–" + tableEnd + " of " + (int)doc.Paragraphs.Count + cellLint;
|
|
1397
1582
|
}
|
|
1398
1583
|
if (type == "set_cell")
|
|
1399
1584
|
{
|
|
@@ -1403,8 +1588,8 @@ static class Program
|
|
|
1403
1588
|
Follow(doc, cell.Range);
|
|
1404
1589
|
cell.Range.Text = "";
|
|
1405
1590
|
Write(doc, (int)cell.Range.Start, Lines(op.Raw("text") ?? ""));
|
|
1406
|
-
|
|
1407
|
-
return "cell set" +
|
|
1591
|
+
string built = Enrich(doc, doc.Tables[n].Cell(op.Int("row", 1), op.Int("col", 1)).Range);
|
|
1592
|
+
return "cell set" + built + Lint(Lines(op.Raw("text") ?? ""));
|
|
1408
1593
|
}
|
|
1409
1594
|
if (type == "insert_image")
|
|
1410
1595
|
{
|
|
@@ -1424,17 +1609,254 @@ static class Program
|
|
|
1424
1609
|
else p.Range.ParagraphFormat.SpaceAfter = Air;
|
|
1425
1610
|
return "image inserted";
|
|
1426
1611
|
}
|
|
1612
|
+
if (type == "insert_references") return References(doc, op);
|
|
1613
|
+
if (type == "style_format") return StyleFormat(doc, op);
|
|
1614
|
+
if (type == "page_setup") return PageSetup(doc, op);
|
|
1615
|
+
if (type == "page_numbers") return PageNumbers(doc, op);
|
|
1616
|
+
if (type == "insert_toc") return Toc(doc, op);
|
|
1617
|
+
if (type == "page_break")
|
|
1618
|
+
{
|
|
1619
|
+
dynamic p = Para(doc, op.Int("para", 0), op.Str("expect", null));
|
|
1620
|
+
p.Range.ParagraphFormat.PageBreakBefore = op.Flag("on", true) ? -1 : 0;
|
|
1621
|
+
return "paragraph " + op.Int("para", 0) + " now starts a new page";
|
|
1622
|
+
}
|
|
1623
|
+
if (type == "header")
|
|
1624
|
+
{
|
|
1625
|
+
dynamic header = doc.Sections[1].Headers[1].Range;
|
|
1626
|
+
header.Text = Lines(op.Raw("text") ?? "");
|
|
1627
|
+
header.ParagraphFormat.Alignment = 1;
|
|
1628
|
+
if (op.Has("size")) header.Font.Size = (float)op.Num("size", 9);
|
|
1629
|
+
return "page header set";
|
|
1630
|
+
}
|
|
1631
|
+
if (type == "update_fields") { Refresh(doc); return "table of contents and cross-references refreshed"; }
|
|
1427
1632
|
throw new Fail("BAD_ARGS", "Unknown Word operation \"" + type + "\".");
|
|
1428
1633
|
}
|
|
1429
1634
|
|
|
1430
1635
|
static Bag StyleLess(Bag source)
|
|
1431
1636
|
{
|
|
1432
1637
|
Dictionary<string, object> copy = new Dictionary<string, object>();
|
|
1433
|
-
foreach (string key in new string[] { "font", "size", "bold", "italic", "underline", "color", "align", "firstLineIndent", "spaceBefore", "spaceAfter", "lineSpacing" })
|
|
1638
|
+
foreach (string key in new string[] { "font", "size", "bold", "italic", "underline", "color", "align", "firstLineIndent", "spaceBefore", "spaceAfter", "lineSpacing", "indentChars", "superscript", "subscript" })
|
|
1434
1639
|
if (source.Has(key)) copy[key] = source.Raw(key);
|
|
1435
1640
|
return new Bag(copy);
|
|
1436
1641
|
}
|
|
1437
1642
|
|
|
1643
|
+
|
|
1644
|
+
// ───────────────────────── citations ─────────────────────────
|
|
1645
|
+
|
|
1646
|
+
static readonly System.Text.RegularExpressions.Regex Cites = new System.Text.RegularExpressions.Regex(@"\\cite\s*\{([0-9,\-–\s]+)\}");
|
|
1647
|
+
|
|
1648
|
+
/// One citation number at a position, as a superscript that jumps to the reference: a cross-reference field
|
|
1649
|
+
/// when the reference list is already there, otherwise a link to the bookmark the list will bring.
|
|
1650
|
+
static void CiteNumber(dynamic doc, int at, string number)
|
|
1651
|
+
{
|
|
1652
|
+
string mark = "cite_" + number;
|
|
1653
|
+
dynamic spot = doc.Range(at, at);
|
|
1654
|
+
if ((bool)doc.Bookmarks.Exists(mark))
|
|
1655
|
+
{
|
|
1656
|
+
// MERGEFORMAT keeps the superscript when the field is refreshed.
|
|
1657
|
+
dynamic field = doc.Fields.Add(spot, -1, "REF " + mark + " \\h \\* MERGEFORMAT", false);
|
|
1658
|
+
field.Result.Font.Superscript = 1;
|
|
1659
|
+
}
|
|
1660
|
+
else
|
|
1661
|
+
{
|
|
1662
|
+
dynamic link = doc.Hyperlinks.Add(Anchor: spot, Address: "", SubAddress: mark, TextToDisplay: number);
|
|
1663
|
+
link.Range.Font.Superscript = 1;
|
|
1664
|
+
link.Range.Font.Underline = 0;
|
|
1665
|
+
link.Range.Font.Color = -16777216;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/// \cite{1}, \cite{2,5}, \cite{3-6} in a range become superscript [1], [2,5], [3-6] whose numbers jump to the references.
|
|
1670
|
+
static int Cite(dynamic doc, dynamic range)
|
|
1671
|
+
{
|
|
1672
|
+
string text = (string)range.Text;
|
|
1673
|
+
if (text == null || text.IndexOf("\\cite", StringComparison.Ordinal) < 0) return 0;
|
|
1674
|
+
int made = 0;
|
|
1675
|
+
System.Text.RegularExpressions.MatchCollection found = Cites.Matches(text);
|
|
1676
|
+
for (int k = found.Count - 1; k >= 0; k--)
|
|
1677
|
+
{
|
|
1678
|
+
System.Text.RegularExpressions.Match m = found[k];
|
|
1679
|
+
dynamic search = range.Duplicate;
|
|
1680
|
+
if (!(bool)search.Find.Execute(FindText: m.Value, MatchCase: true, MatchWildcards: false, Forward: true, Wrap: 0)) continue;
|
|
1681
|
+
int at = (int)search.Start;
|
|
1682
|
+
search.Text = "";
|
|
1683
|
+
// Written back to front at one position, so each piece lands before the one after it.
|
|
1684
|
+
List<string> pieces = new List<string>();
|
|
1685
|
+
pieces.Add("[");
|
|
1686
|
+
foreach (string part in m.Groups[1].Value.Replace("–", "-").Replace(" ", "").Split(','))
|
|
1687
|
+
{
|
|
1688
|
+
if (part.Length == 0) continue;
|
|
1689
|
+
if (pieces.Count > 1) pieces.Add(",");
|
|
1690
|
+
string[] ends = part.Split('-');
|
|
1691
|
+
pieces.Add("#" + ends[0]);
|
|
1692
|
+
if (ends.Length > 1 && ends[1].Length > 0) { pieces.Add("-"); pieces.Add("#" + ends[1]); }
|
|
1693
|
+
}
|
|
1694
|
+
pieces.Add("]");
|
|
1695
|
+
for (int p = pieces.Count - 1; p >= 0; p--)
|
|
1696
|
+
{
|
|
1697
|
+
string piece = pieces[p];
|
|
1698
|
+
if (piece[0] == '#') { CiteNumber(doc, at, piece.Substring(1)); continue; }
|
|
1699
|
+
doc.Range(at, at).InsertAfter(piece);
|
|
1700
|
+
dynamic plain = doc.Range(at, at + piece.Length);
|
|
1701
|
+
plain.Font.Superscript = 1;
|
|
1702
|
+
}
|
|
1703
|
+
made++;
|
|
1704
|
+
}
|
|
1705
|
+
return made;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/// Formulas and citations written in the text of a range.
|
|
1709
|
+
static string Enrich(dynamic doc, dynamic range)
|
|
1710
|
+
{
|
|
1711
|
+
int cites = Cite(doc, range), equations = Mathify(doc, range);
|
|
1712
|
+
return (equations > 0 ? ", " + equations + " equation(s)" : "") + (cites > 0 ? ", " + cites + " citation(s)" : "") + MathNote();
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/// The reference list: "[n] ..." paragraphs whose numbers carry the bookmarks the citations point at.
|
|
1716
|
+
static string References(dynamic doc, Bag op)
|
|
1717
|
+
{
|
|
1718
|
+
IList items = op.List("items");
|
|
1719
|
+
if (items == null || items.Count == 0) throw new Fail("BAD_ARGS", "\"items\" must list the references, one string each.");
|
|
1720
|
+
System.Text.RegularExpressions.Regex lead = new System.Text.RegularExpressions.Regex(@"^\s*\[(\d+)\]\s*");
|
|
1721
|
+
dynamic current = null;
|
|
1722
|
+
int number = 0;
|
|
1723
|
+
foreach (object raw in items)
|
|
1724
|
+
{
|
|
1725
|
+
string body = Convert.ToString(raw).Trim();
|
|
1726
|
+
System.Text.RegularExpressions.Match m = lead.Match(body);
|
|
1727
|
+
if (m.Success) { number = int.Parse(m.Groups[1].Value); body = body.Substring(m.Length); }
|
|
1728
|
+
else number++;
|
|
1729
|
+
dynamic p = current == null ? NewParagraph(doc, op) : After(doc, current);
|
|
1730
|
+
Plain(p, "Normal");
|
|
1731
|
+
Follow(doc, p.Range);
|
|
1732
|
+
string label = "[" + number + "] ";
|
|
1733
|
+
Write(doc, (int)p.Range.Start, label + body);
|
|
1734
|
+
Whole(p.Range);
|
|
1735
|
+
try
|
|
1736
|
+
{
|
|
1737
|
+
dynamic format = p.Range.ParagraphFormat;
|
|
1738
|
+
format.CharacterUnitFirstLineIndent = 0; format.FirstLineIndent = 0; format.Alignment = 3;
|
|
1739
|
+
format.CharacterUnitLeftIndent = 0; format.LeftIndent = 21f; format.FirstLineIndent = -21f;
|
|
1740
|
+
}
|
|
1741
|
+
catch (COMException) { }
|
|
1742
|
+
int start = (int)p.Range.Start;
|
|
1743
|
+
string mark = "cite_" + number;
|
|
1744
|
+
if ((bool)doc.Bookmarks.Exists(mark)) doc.Bookmarks[mark].Delete();
|
|
1745
|
+
doc.Bookmarks.Add(mark, doc.Range(start + 1, start + 1 + number.ToString().Length));
|
|
1746
|
+
current = p;
|
|
1747
|
+
}
|
|
1748
|
+
// Citations written before the list existed are links; now that their targets exist, make them cross-references.
|
|
1749
|
+
int linked = 0;
|
|
1750
|
+
for (int i = (int)doc.Hyperlinks.Count; i >= 1; i--)
|
|
1751
|
+
{
|
|
1752
|
+
dynamic link = doc.Hyperlinks[i];
|
|
1753
|
+
string target = "";
|
|
1754
|
+
try { target = (string)link.SubAddress; } catch (Exception) { }
|
|
1755
|
+
if (target == null || !target.StartsWith("cite_", StringComparison.Ordinal) || !(bool)doc.Bookmarks.Exists(target)) continue;
|
|
1756
|
+
dynamic spot = link.Range;
|
|
1757
|
+
int at = (int)spot.Start;
|
|
1758
|
+
link.Delete();
|
|
1759
|
+
dynamic text = doc.Range(at, at + target.Length - 5);
|
|
1760
|
+
text.Text = "";
|
|
1761
|
+
dynamic field = doc.Fields.Add(doc.Range(at, at), -1, "REF " + target + " \\h \\* MERGEFORMAT", false);
|
|
1762
|
+
field.Result.Font.Superscript = 1;
|
|
1763
|
+
linked++;
|
|
1764
|
+
}
|
|
1765
|
+
return items.Count + " reference(s) listed" + (linked > 0 ? ", " + linked + " citation(s) in the text now cross-reference them" : "");
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// ───────────────────────── styles and page layout ─────────────────────────
|
|
1769
|
+
|
|
1770
|
+
/// Styles the agent switched automatic numbering off for (document path | style name).
|
|
1771
|
+
static readonly HashSet<string> Unnumbered = new HashSet<string>();
|
|
1772
|
+
|
|
1773
|
+
static dynamic StyleOf(dynamic doc, string name)
|
|
1774
|
+
{
|
|
1775
|
+
int builtin;
|
|
1776
|
+
try { return WordStyles.TryGetValue(name, out builtin) ? doc.Styles[builtin] : doc.Styles[name]; }
|
|
1777
|
+
catch (COMException) { throw new Fail("STYLE_MISSING", "This document has no style \"" + name + "\". Built-in names that always work: " + string.Join(", ", new List<string>(WordStyles.Keys).ToArray()) + "."); }
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/// Change what a style looks like, for every paragraph that uses it.
|
|
1781
|
+
static string StyleFormat(dynamic doc, Bag op)
|
|
1782
|
+
{
|
|
1783
|
+
string name = op.Need("style");
|
|
1784
|
+
dynamic style = StyleOf(doc, name);
|
|
1785
|
+
dynamic font = style.Font, format = style.ParagraphFormat;
|
|
1786
|
+
if (op.Has("font")) { string f = op.Need("font"); font.Name = f; try { font.NameFarEast = f; } catch (COMException) { } }
|
|
1787
|
+
if (op.Has("latinFont")) { string f = op.Need("latinFont"); try { font.NameAscii = f; font.NameOther = f; } catch (COMException) { } }
|
|
1788
|
+
if (op.Has("size")) font.Size = (float)op.Num("size", 12);
|
|
1789
|
+
if (op.Has("bold")) font.Bold = op.Flag("bold", false) ? 1 : 0;
|
|
1790
|
+
if (op.Has("italic")) font.Italic = op.Flag("italic", false) ? 1 : 0;
|
|
1791
|
+
if (op.Has("color")) font.Color = Bgr(op.Need("color"));
|
|
1792
|
+
if (op.Has("align")) { string align = op.Need("align"); format.Alignment = align == "center" ? 1 : align == "right" ? 2 : align == "justify" ? 3 : 0; }
|
|
1793
|
+
if (op.Has("indentChars")) { format.FirstLineIndent = 0; format.CharacterUnitFirstLineIndent = (float)op.Num("indentChars", 2); }
|
|
1794
|
+
if (op.Has("firstLineIndent")) { format.CharacterUnitFirstLineIndent = 0; format.FirstLineIndent = (float)op.Num("firstLineIndent", 0); }
|
|
1795
|
+
if (op.Has("spaceBefore")) format.SpaceBefore = (float)op.Num("spaceBefore", 0);
|
|
1796
|
+
if (op.Has("spaceAfter")) format.SpaceAfter = (float)op.Num("spaceAfter", 0);
|
|
1797
|
+
if (op.Has("lineSpacing")) { format.LineSpacingRule = 5; format.LineSpacing = (float)(op.Num("lineSpacing", 1) * 12); }
|
|
1798
|
+
if (op.Has("pageBreakBefore")) format.PageBreakBefore = op.Flag("pageBreakBefore", false) ? -1 : 0;
|
|
1799
|
+
if (op.Has("numbering") && !op.On("numbering"))
|
|
1800
|
+
{
|
|
1801
|
+
Unnumbered.Add((string)doc.FullName + "|" + (string)style.NameLocal);
|
|
1802
|
+
// Stop the style from numbering its paragraphs by itself (headings that carry their number in the text).
|
|
1803
|
+
try { style.LinkToListTemplate(null); } catch (Exception) { }
|
|
1804
|
+
foreach (dynamic p in doc.Paragraphs)
|
|
1805
|
+
{
|
|
1806
|
+
try { if ((string)p.Style.NameLocal == (string)style.NameLocal && (int)p.Range.ListFormat.ListType != 0) p.Range.ListFormat.RemoveNumbers(); }
|
|
1807
|
+
catch (Exception) { }
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
try { format.WordWrap = -1; } catch (Exception) { }
|
|
1811
|
+
return "style \"" + (string)style.NameLocal + "\" changed";
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
static string PageSetup(dynamic doc, Bag op)
|
|
1815
|
+
{
|
|
1816
|
+
dynamic setup = doc.PageSetup;
|
|
1817
|
+
const float cm = 28.3465f;
|
|
1818
|
+
if (op.Has("paper")) { string paper = op.Need("paper").ToUpperInvariant(); setup.PaperSize = paper == "A3" ? 6 : paper == "LETTER" ? 2 : paper == "B5" ? 13 : 7; }
|
|
1819
|
+
if (op.Has("orientation")) setup.Orientation = op.Need("orientation") == "landscape" ? 1 : 0;
|
|
1820
|
+
if (op.Has("top")) setup.TopMargin = (float)op.Num("top", 2.54) * cm;
|
|
1821
|
+
if (op.Has("bottom")) setup.BottomMargin = (float)op.Num("bottom", 2.54) * cm;
|
|
1822
|
+
if (op.Has("left")) setup.LeftMargin = (float)op.Num("left", 3.17) * cm;
|
|
1823
|
+
if (op.Has("right")) setup.RightMargin = (float)op.Num("right", 3.17) * cm;
|
|
1824
|
+
return "page setup changed";
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
static string PageNumbers(dynamic doc, Bag op)
|
|
1828
|
+
{
|
|
1829
|
+
string align = op.Str("align", "center");
|
|
1830
|
+
dynamic numbers = doc.Sections[1].Footers[1].PageNumbers;
|
|
1831
|
+
numbers.Add(align == "left" ? 0 : align == "right" ? 2 : 1, op.Flag("firstPage", true));
|
|
1832
|
+
if (op.Has("start")) { numbers.RestartNumberingAtSection = true; numbers.StartingNumber = op.Int("start", 1); }
|
|
1833
|
+
return "page numbers added to the footer";
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
static string Toc(dynamic doc, Bag op)
|
|
1837
|
+
{
|
|
1838
|
+
dynamic p = NewParagraph(doc, op);
|
|
1839
|
+
Plain(p, "Normal");
|
|
1840
|
+
if (op.Has("title"))
|
|
1841
|
+
{
|
|
1842
|
+
p.Range.InsertBefore(op.Need("title"));
|
|
1843
|
+
p.Range.ParagraphFormat.Alignment = 1; p.Range.ParagraphFormat.CharacterUnitFirstLineIndent = 0; p.Range.ParagraphFormat.FirstLineIndent = 0;
|
|
1844
|
+
p.Range.Font.Bold = 1; p.Range.Font.Size = 16f;
|
|
1845
|
+
p = After(doc, p);
|
|
1846
|
+
Plain(p, "Normal");
|
|
1847
|
+
}
|
|
1848
|
+
Follow(doc, p.Range);
|
|
1849
|
+
doc.TablesOfContents.Add(Range: p.Range, UseHeadingStyles: true, UpperHeadingLevel: 1, LowerHeadingLevel: Math.Max(1, Math.Min(9, op.Int("levels", 3))));
|
|
1850
|
+
return "table of contents inserted (it is refreshed on every save)";
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/// Bring fields up to date: the table of contents, cross-references, caption numbers.
|
|
1854
|
+
static void Refresh(dynamic doc)
|
|
1855
|
+
{
|
|
1856
|
+
try { foreach (dynamic toc in doc.TablesOfContents) toc.Update(); } catch (Exception) { }
|
|
1857
|
+
try { doc.Fields.Update(); } catch (Exception) { }
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1438
1860
|
// ───────────────────────── Excel ─────────────────────────
|
|
1439
1861
|
|
|
1440
1862
|
static dynamic Sheet(dynamic book, Bag op)
|
|
@@ -1557,8 +1979,8 @@ static class Program
|
|
|
1557
1979
|
range.HorizontalAlignment = align == "center" ? -4108 : align == "right" ? -4152 : -4131;
|
|
1558
1980
|
}
|
|
1559
1981
|
if (op.Has("wrap")) range.WrapText = op.Flag("wrap", false);
|
|
1560
|
-
if (op.
|
|
1561
|
-
if (op.
|
|
1982
|
+
if (op.On("border")) range.Borders.LineStyle = 1;
|
|
1983
|
+
if (op.On("merge")) range.Merge();
|
|
1562
1984
|
if (op.Has("columnWidth")) range.ColumnWidth = op.Num("columnWidth", 10);
|
|
1563
1985
|
if (op.Has("rowHeight")) range.RowHeight = op.Num("rowHeight", 15);
|
|
1564
1986
|
}
|
|
@@ -1745,7 +2167,7 @@ static class Program
|
|
|
1745
2167
|
{
|
|
1746
2168
|
if (!Typing || text.Length < 4) { textRange.Text = text; return; }
|
|
1747
2169
|
textRange.Text = "";
|
|
1748
|
-
foreach (string piece in Pieces(text)) { textRange.InsertAfter(piece); Thread.Sleep(
|
|
2170
|
+
foreach (string piece in Pieces(text)) { textRange.InsertAfter(piece); Thread.Sleep(14); }
|
|
1749
2171
|
}
|
|
1750
2172
|
|
|
1751
2173
|
static void GoTo(dynamic deck, int slide)
|
|
@@ -1861,6 +2283,7 @@ static class Program
|
|
|
1861
2283
|
{
|
|
1862
2284
|
Bag op = new Bag(ops[i]);
|
|
1863
2285
|
type = op.Need("op");
|
|
2286
|
+
if (card) Card.Hold = true;
|
|
1864
2287
|
if (card) Card.Report("AI 正在编辑 " + docName + (ops.Count > 1 ? " · " + (i + 1) + "/" + ops.Count : ""));
|
|
1865
2288
|
string line = kind == "word" ? WordOp(doc, type, op) : kind == "excel" ? ExcelOp(app, doc, type, op) : PptOp(doc, type, op);
|
|
1866
2289
|
string ignored = op.Unread();
|
|
@@ -1883,7 +2306,7 @@ static class Program
|
|
|
1883
2306
|
{
|
|
1884
2307
|
if (undo != null) { try { undo.EndCustomRecord(); } catch (COMException) { } }
|
|
1885
2308
|
}
|
|
1886
|
-
if (card) Card.
|
|
2309
|
+
if (card) Card.Hold = false;
|
|
1887
2310
|
result["done"] = done;
|
|
1888
2311
|
result["total"] = ops.Count;
|
|
1889
2312
|
return result;
|
|
@@ -1891,8 +2314,97 @@ static class Program
|
|
|
1891
2314
|
|
|
1892
2315
|
// ───────────────────────── rendering ─────────────────────────
|
|
1893
2316
|
|
|
2317
|
+
/// One Word page as a picture of the given width.
|
|
2318
|
+
static Bitmap WordPage(dynamic pages, int page, int width)
|
|
2319
|
+
{
|
|
2320
|
+
byte[] bits = (byte[])pages[page].EnhMetaFileBits;
|
|
2321
|
+
using (MemoryStream stream = new MemoryStream(bits))
|
|
2322
|
+
using (Metafile meta = new Metafile(stream))
|
|
2323
|
+
{
|
|
2324
|
+
int height = (int)Math.Round(width * (double)meta.Height / meta.Width);
|
|
2325
|
+
// Drawn straight at the final size, hairlines (table borders) fall between pixels and vanish:
|
|
2326
|
+
// draw the page large first, then scale the picture down.
|
|
2327
|
+
int factor = Math.Max(2, (int)Math.Ceiling(3000.0 / width));
|
|
2328
|
+
Bitmap bitmap = new Bitmap(width, height);
|
|
2329
|
+
using (Bitmap large = new Bitmap(width * factor, height * factor))
|
|
2330
|
+
{
|
|
2331
|
+
using (Graphics g = Graphics.FromImage(large))
|
|
2332
|
+
{
|
|
2333
|
+
g.Clear(Color.White);
|
|
2334
|
+
g.DrawImage(meta, 0, 0, large.Width, large.Height);
|
|
2335
|
+
}
|
|
2336
|
+
using (Graphics g = Graphics.FromImage(bitmap))
|
|
2337
|
+
{
|
|
2338
|
+
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
2339
|
+
g.DrawImage(large, 0, 0, width, height);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return bitmap;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/// Several pages or slides side by side in one picture, each with its number: the whole document at a glance.
|
|
2347
|
+
static object Sheet(string kind, dynamic doc, Bag a)
|
|
2348
|
+
{
|
|
2349
|
+
string path = a.Need("out");
|
|
2350
|
+
int cell = Math.Max(300, Math.Min(1000, a.Int("width", 640)));
|
|
2351
|
+
dynamic pages = kind == "word" ? doc.Windows[1].Panes[1].Pages : null;
|
|
2352
|
+
int total = kind == "word" ? (int)pages.Count : (int)doc.Slides.Count;
|
|
2353
|
+
if (total == 0) throw new Fail("ANCHOR_MISSING", "There is nothing to picture yet.");
|
|
2354
|
+
int first = Math.Max(1, a.Int("from", 1)), last = Math.Min(total, a.Int("to", total));
|
|
2355
|
+
if (first > total) throw new Fail("ANCHOR_MISSING", "There is no " + (kind == "word" ? "page " : "slide ") + first + " (there are " + total + ").");
|
|
2356
|
+
last = Math.Min(last, first + 11);
|
|
2357
|
+
int count = last - first + 1, columns = Math.Min(count, kind == "word" ? 3 : 2), rows = (count + columns - 1) / columns;
|
|
2358
|
+
List<Bitmap> pictures = new List<Bitmap>();
|
|
2359
|
+
string temp = path + ".slide.png";
|
|
2360
|
+
try
|
|
2361
|
+
{
|
|
2362
|
+
for (int n = first; n <= last; n++)
|
|
2363
|
+
{
|
|
2364
|
+
if (kind == "word") pictures.Add(WordPage(pages, n, cell));
|
|
2365
|
+
else
|
|
2366
|
+
{
|
|
2367
|
+
double ratio = (double)doc.PageSetup.SlideHeight / (double)doc.PageSetup.SlideWidth;
|
|
2368
|
+
doc.Slides[n].Export(temp, "PNG", cell, (int)Math.Round(cell * ratio));
|
|
2369
|
+
using (Image loaded = Image.FromFile(temp)) pictures.Add(new Bitmap(loaded));
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
int gap = 14, label = 30, height = 0;
|
|
2373
|
+
foreach (Bitmap picture in pictures) height = Math.Max(height, picture.Height);
|
|
2374
|
+
using (Bitmap sheet = new Bitmap(columns * cell + (columns + 1) * gap, rows * (height + label + gap) + gap))
|
|
2375
|
+
{
|
|
2376
|
+
using (Graphics g = Graphics.FromImage(sheet))
|
|
2377
|
+
using (Font font = new Font("Microsoft YaHei UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel))
|
|
2378
|
+
using (Pen border = new Pen(Color.FromArgb(150, 150, 150)))
|
|
2379
|
+
{
|
|
2380
|
+
g.Clear(Color.FromArgb(232, 232, 236));
|
|
2381
|
+
for (int k = 0; k < pictures.Count; k++)
|
|
2382
|
+
{
|
|
2383
|
+
int x = gap + (k % columns) * (cell + gap), y = gap + (k / columns) * (height + label + gap);
|
|
2384
|
+
g.DrawString((kind == "word" ? "page " : "slide ") + (first + k), font, Brushes.Black, x, y + 4);
|
|
2385
|
+
g.DrawImage(pictures[k], x, y + label);
|
|
2386
|
+
g.DrawRectangle(border, x, y + label, pictures[k].Width - 1, pictures[k].Height - 1);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
sheet.Save(path, ImageFormat.Png);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
finally
|
|
2393
|
+
{
|
|
2394
|
+
foreach (Bitmap picture in pictures) picture.Dispose();
|
|
2395
|
+
try { File.Delete(temp); } catch (Exception) { }
|
|
2396
|
+
}
|
|
2397
|
+
Dictionary<string, object> result = new Dictionary<string, object>();
|
|
2398
|
+
result["what"] = (kind == "word" ? "pages " : "slides ") + first + "–" + last + " of " + total;
|
|
2399
|
+
result["first"] = first; result["last"] = last; result["total"] = total;
|
|
2400
|
+
result["path"] = path;
|
|
2401
|
+
using (Image saved = Image.FromFile(path)) { result["width"] = saved.Width; result["height"] = saved.Height; }
|
|
2402
|
+
return result;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
1894
2405
|
static object Render(string kind, dynamic app, dynamic doc, Bag a)
|
|
1895
2406
|
{
|
|
2407
|
+
if (a.Flag("sheet", false) && kind != "excel") return Sheet(kind, doc, a);
|
|
1896
2408
|
string path = a.Need("out");
|
|
1897
2409
|
int width = Math.Max(320, Math.Min(2400, a.Int("width", 1100)));
|
|
1898
2410
|
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 的修改位置",
|
|
@@ -501,7 +501,7 @@ window.__ModuleLoader__.load({
|
|
|
501
501
|
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "逐字写入", h("span", { style: {
|
|
502
502
|
...S.hint,
|
|
503
503
|
fontWeight: 400
|
|
504
|
-
} }, "
|
|
504
|
+
} }, "文字像打字一样逐步出现,而不是整块贴上去;表格逐行填入。每段约多花 0.3 秒,关闭后写得最快。")), h(Switch, {
|
|
505
505
|
checked: settings.typing,
|
|
506
506
|
disabled,
|
|
507
507
|
label: "逐字写入",
|
|
@@ -511,7 +511,7 @@ window.__ModuleLoader__.load({
|
|
|
511
511
|
})), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "迷你卡片", h("span", { style: {
|
|
512
512
|
...S.hint,
|
|
513
513
|
fontWeight: 400
|
|
514
|
-
} }, "AI
|
|
514
|
+
} }, "AI 每次动手修改文档时,屏幕右下角显示一张小卡片:正在改哪个文档、改到第几项,以及“查看”“逐字”两个开关,点一下立即生效(连正在进行的这一批也会跟着变)。卡片可拖动,不抢焦点;这一批改完就消失,下一批开始时再出现。在卡片上切换过的开关,本次运行期间以卡片为准。")), h(Switch, {
|
|
515
515
|
checked: settings.card,
|
|
516
516
|
disabled,
|
|
517
517
|
label: "迷你卡片",
|
|
@@ -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,8 +251,9 @@ 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
|
|
255
|
-
"- In Word, formulas are LaTeX between dollar signs ($...$ inline, $$...$$ on its own line); they become native equations.",
|
|
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
|
+
"- In Word, formulas are LaTeX between dollar signs ($...$ inline, $$...$$ on its own line); they become native equations. Citations are \\cite{n} plus insert_references.",
|
|
256
|
+
"- Set the look through styles before writing a long document (style_format for Normal and the headings, page_setup); all of that is done with office_edit, not with mouse and keyboard in Word's dialogs.",
|
|
256
257
|
"- 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
258
|
"- If a call reports the app is busy, the user is typing or has a dialog open: wait a moment and retry.",
|
|
258
259
|
"- 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."
|
|
@@ -5958,6 +5959,15 @@ function formatPpt(read) {
|
|
|
5958
5959
|
}
|
|
5959
5960
|
return lines.join("\n");
|
|
5960
5961
|
}
|
|
5962
|
+
/** Failures found before anything was touched. */
|
|
5963
|
+
const PRECHECKED = /* @__PURE__ */ new Set([
|
|
5964
|
+
"BAD_ARGS",
|
|
5965
|
+
"ANCHOR_MISSING",
|
|
5966
|
+
"ANCHOR_MISMATCH",
|
|
5967
|
+
"NOT_FOUND",
|
|
5968
|
+
"STYLE_MISSING",
|
|
5969
|
+
"BUSY"
|
|
5970
|
+
]);
|
|
5961
5971
|
function formatEdit(result) {
|
|
5962
5972
|
const done = result.done.map((line, index) => `${index + 1}. ${line}`);
|
|
5963
5973
|
if (!result.failed) return [
|
|
@@ -5968,7 +5978,8 @@ function formatEdit(result) {
|
|
|
5968
5978
|
const { index, op, error } = result.failed;
|
|
5969
5979
|
return [
|
|
5970
5980
|
`Stopped at operation ${index + 1} (${op}): ${error}`,
|
|
5971
|
-
done.length ? `Applied before it:\n${done.join("\n")}` : "Nothing was applied.",
|
|
5981
|
+
done.length ? `Applied before it:\n${done.join("\n")}` : "Nothing before it was applied.",
|
|
5982
|
+
PRECHECKED.has(result.failed.code) ? "" : "The failed operation itself may have changed part of the document: read it before you retry.",
|
|
5972
5983
|
result.total - index - 1 > 0 ? `The ${result.total - index - 1} operation(s) after it did not run.` : ""
|
|
5973
5984
|
].filter(Boolean).join("\n");
|
|
5974
5985
|
}
|
|
@@ -6107,15 +6118,18 @@ const EDIT_GUIDE = `Edit a document that is open in Word, Excel or PowerPoint, l
|
|
|
6107
6118
|
|
|
6108
6119
|
Word ops. para = paragraph number from office_read; pass expect = its first words so a number made stale by other edits is caught. where = after (default with para) | before | start | end (default without para: the very end of the document).
|
|
6109
6120
|
In a form or template whose sections are table cells, write INSIDE the cell: give para = a paragraph of that cell. Without para the text or table lands after everything, outside the form.
|
|
6110
|
-
Formulas: write LaTeX between dollar signs in any text: $...$ inside a sentence, $$...$$ as a paragraph of its own. Number a display formula when the document numbers its formulas by ending it with \\tag{1}: $$T=2\\pi\\sqrt{l/g} \\tag{1}$$ puts (1) flush right. They become native Word equations (\\frac, \\sqrt, ^, _, Greek letters, \\bar, \\sum, \\int ...). Never write a formula as plain text such as T = 2π√(l/g).
|
|
6121
|
+
Formulas: write LaTeX between dollar signs in any text (paragraphs, table cells, captions, replace_text): $...$ inside a sentence, $$...$$ as a paragraph of its own. Every symbol, subscript, power, norm or matrix in running text goes between dollar signs too ($\\kappa(A)$, $\\|x\\|_2$, $10^{-8}$, \\begin{pmatrix}..\\end{pmatrix}, \\begin{cases}..\\end{cases}); do not type them with Unicode characters such as ‖x‖₂ or κ₂. Number a display formula when the document numbers its formulas by ending it with \\tag{1}: $$T=2\\pi\\sqrt{l/g} \\tag{1}$$ puts (1) flush right. They become native Word equations (\\frac, \\sqrt, ^, _, Greek letters, \\bar, \\sum, \\int ...). Never write a formula as plain text such as T = 2π√(l/g).
|
|
6111
6122
|
- 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.
|
|
6112
6123
|
- set_text {para, expect, text} — rewrite one paragraph, keeping its style. expect is required ("" for an empty paragraph).
|
|
6113
|
-
- replace_text {find, replace, all?}
|
|
6114
|
-
- format_text {para?, to?, find?, style?, font?, size?, bold?, italic?, underline?, color?, align?, firstLineIndent?, spaceBefore?, spaceAfter?, lineSpacing?} — para..to, or the first match of find.
|
|
6124
|
+
- replace_text {find, replace, all?} — find is literal text within one paragraph; replace may hold formulas and citations.
|
|
6125
|
+
- format_text {para?, to?, find?, style?, font?, size?, bold?, italic?, underline?, superscript?, subscript?, color?, align?, indentChars?, firstLineIndent?, spaceBefore?, spaceAfter?, lineSpacing?} — para..to, or the first match of find.
|
|
6115
6126
|
- delete_range {para, expect, to?} — expect is required; para..to in one operation removes a run of paragraphs (leftover hints, surplus blank lines). Paragraph numbers shift after every insert or delete; each result tells you the new numbers.
|
|
6116
6127
|
- insert_table {data:[[cell,..],..], caption?, para?, where?, header?} — with para inside a cell, the table goes inside that cell. set_cell {table, row, col, text}
|
|
6117
6128
|
- Captions: give caption (the title only, no "表 1") on insert_table / insert_image. It is set the standard way: numbered automatically ("表 1", "图 1"), centred, above a table and below a figure. Do not write captions as ordinary paragraphs.
|
|
6118
6129
|
- insert_image {path, caption?, para?, where?, width?} — the picture is centred on a line of its own.
|
|
6130
|
+
- Citations: write \\cite{1}, \\cite{2,5} or \\cite{3-6} in the text where the source is used; they become superscript [1] that jump to the reference. List the sources with insert_references {items:["Author. Title[M]. ...", ..], para?, where?} (numbered [1], [2].. in order; put it under a "参考文献" heading). Do not type [1] by hand.
|
|
6131
|
+
- style_format {style, font?, latinFont?, size?, bold?, italic?, color?, align?, indentChars?, spaceBefore?, spaceAfter?, lineSpacing?, pageBreakBefore?, numbering?: false} — change what a style looks like everywhere it is used (e.g. Heading 1 in 黑体 三号 without automatic numbers; Normal in 宋体 小四, 1.5 lines, 2-character first-line indent). Set the styles first, then write; this replaces fiddling with Word's style dialogs.
|
|
6132
|
+
- page_setup {paper?: A4|A3|B5|Letter, orientation?, top?, bottom?, left?, right?} (margins in cm); page_numbers {align?, start?}; header {text}; page_break {para, expect} (that paragraph starts a new page); insert_toc {title?, levels?, para?, where?}; update_fields {}
|
|
6119
6133
|
|
|
6120
6134
|
Excel ops. sheet = sheet name (default: the active sheet).
|
|
6121
6135
|
- write_range {range: top-left cell, values:[[..],..]} — a string starting with "=" is a formula; also takes the format_range fields.
|
|
@@ -6137,6 +6151,7 @@ function createTools(host) {
|
|
|
6137
6151
|
const { helper } = host;
|
|
6138
6152
|
const tools = [];
|
|
6139
6153
|
let current;
|
|
6154
|
+
const own = /* @__PURE__ */ new Set();
|
|
6140
6155
|
const unseen = /* @__PURE__ */ new Set();
|
|
6141
6156
|
const keyOf = (on) => `${on.app}:${fileName(on.doc).toLowerCase()}`;
|
|
6142
6157
|
/** Pictures of the document as it is now: Word pages or PowerPoint slides first..last, or an Excel range. */
|
|
@@ -6178,6 +6193,38 @@ function createTools(host) {
|
|
|
6178
6193
|
...total === void 0 ? {} : { total }
|
|
6179
6194
|
};
|
|
6180
6195
|
};
|
|
6196
|
+
/** The whole document at a glance: its pages (6 to a picture) or slides tiled with their numbers, up to 18. */
|
|
6197
|
+
const overview = async (on) => {
|
|
6198
|
+
if (on.app === "excel") return picture(on, 0, 0, {}, Math.min(host.settings().renderWidth, 1e3));
|
|
6199
|
+
const folder = join(tmpdir(), "dsh-office");
|
|
6200
|
+
await mkdir(folder, { recursive: true });
|
|
6201
|
+
const lines = [];
|
|
6202
|
+
const images = [];
|
|
6203
|
+
let total = 1;
|
|
6204
|
+
for (let from = 1; from <= Math.min(total, 18); from += 6) {
|
|
6205
|
+
const out = join(folder, `sheet-${process.pid}-${Date.now()}-${from}.png`);
|
|
6206
|
+
try {
|
|
6207
|
+
const result = await helper.call("render", {
|
|
6208
|
+
...on,
|
|
6209
|
+
sheet: true,
|
|
6210
|
+
from,
|
|
6211
|
+
to: from + 5,
|
|
6212
|
+
out,
|
|
6213
|
+
width: 640
|
|
6214
|
+
}, 11e4);
|
|
6215
|
+
total = result.total;
|
|
6216
|
+
images.push(toJson(await host.saveImage(await readFile(out), `${on.app}-overview-${from}.png`)));
|
|
6217
|
+
lines.push(`${APP_NAMES[on.app]} ${result.what}, tiled in one picture.`);
|
|
6218
|
+
} finally {
|
|
6219
|
+
await rm(out, { force: true });
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
if (total > 18) lines.push(`Only the first 18 of ${total} are shown.`);
|
|
6223
|
+
return {
|
|
6224
|
+
lines,
|
|
6225
|
+
images
|
|
6226
|
+
};
|
|
6227
|
+
};
|
|
6181
6228
|
/** The helper arguments naming the document; the app comes from doc / app only, never from a save-as path. */
|
|
6182
6229
|
const target = (args) => {
|
|
6183
6230
|
if (args.doc === void 0 && args.app === void 0) {
|
|
@@ -6233,6 +6280,7 @@ function createTools(host) {
|
|
|
6233
6280
|
app: kind,
|
|
6234
6281
|
doc: doc.path ?? doc.name
|
|
6235
6282
|
};
|
|
6283
|
+
if (doc.how !== "attached") own.add(keyOf(current));
|
|
6236
6284
|
return { text: formatOpened(doc) };
|
|
6237
6285
|
},
|
|
6238
6286
|
presentCall: (args) => card(`打开 ${fileName(args.path) || "新文档"}`)
|
|
@@ -6344,7 +6392,7 @@ function createTools(host) {
|
|
|
6344
6392
|
}));
|
|
6345
6393
|
tools.push(defineTool({
|
|
6346
6394
|
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),
|
|
6395
|
+
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
6396
|
parameters: {
|
|
6349
6397
|
doc: DOC,
|
|
6350
6398
|
app: APP,
|
|
@@ -6360,6 +6408,10 @@ function createTools(host) {
|
|
|
6360
6408
|
type: "integer",
|
|
6361
6409
|
description: "Word / PowerPoint: last page or slide, to get up to 4 in one call."
|
|
6362
6410
|
},
|
|
6411
|
+
overview: {
|
|
6412
|
+
type: "boolean",
|
|
6413
|
+
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."
|
|
6414
|
+
},
|
|
6363
6415
|
sheet: {
|
|
6364
6416
|
type: "string",
|
|
6365
6417
|
description: "Excel: sheet name."
|
|
@@ -6374,9 +6426,19 @@ function createTools(host) {
|
|
|
6374
6426
|
async execute(args, exec) {
|
|
6375
6427
|
const input = args;
|
|
6376
6428
|
const on = target(input), kind = on.app;
|
|
6377
|
-
if (kind === "ppt" && input.slide === void 0) throw new Error("slide is required for PowerPoint.");
|
|
6429
|
+
if (kind === "ppt" && input.slide === void 0 && input.overview !== true) throw new Error("slide is required for PowerPoint (or pass overview: true).");
|
|
6378
6430
|
if (!await host.vision(exec)) return { text: "The current model cannot view images; check the result with office_read instead." };
|
|
6379
|
-
|
|
6431
|
+
if (input.overview === true && kind !== "excel") {
|
|
6432
|
+
const all = await overview(on);
|
|
6433
|
+
unseen.delete(keyOf(on));
|
|
6434
|
+
const [image, ...more] = all.images;
|
|
6435
|
+
return {
|
|
6436
|
+
text: all.lines.join("\n"),
|
|
6437
|
+
...image === void 0 ? {} : { image },
|
|
6438
|
+
...more.length ? { more } : {}
|
|
6439
|
+
};
|
|
6440
|
+
}
|
|
6441
|
+
const { doc: _doc, app: _app, to, page: _page, slide: _slide, overview: _overview, ...rest } = input;
|
|
6380
6442
|
const first = kind === "word" ? input.page ?? 1 : kind === "ppt" ? input.slide : 0;
|
|
6381
6443
|
const last = kind === "excel" || to === void 0 ? first : Math.min(Math.max(to, first), first + 3);
|
|
6382
6444
|
const { lines, images } = await picture(on, first, last, rest, host.settings().renderWidth);
|
|
@@ -6413,10 +6475,16 @@ function createTools(host) {
|
|
|
6413
6475
|
}, 11e4);
|
|
6414
6476
|
const moved = input.path !== void 0 && extname(input.path).toLowerCase() !== ".pdf";
|
|
6415
6477
|
const wasUnseen = unseen.delete(keyOf(on));
|
|
6416
|
-
if (moved)
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6478
|
+
if (moved) {
|
|
6479
|
+
if (own.delete(keyOf(on))) own.add(keyOf({
|
|
6480
|
+
app: on.app,
|
|
6481
|
+
doc: result.path
|
|
6482
|
+
}));
|
|
6483
|
+
current = {
|
|
6484
|
+
app: on.app,
|
|
6485
|
+
doc: result.path
|
|
6486
|
+
};
|
|
6487
|
+
}
|
|
6420
6488
|
const text = `Saved ${result.path} (${result.bytes} bytes).${moved ? " The open document is now this file." : ""}`;
|
|
6421
6489
|
if (!wasUnseen || !host.settings().finalCheck || !await host.vision(exec)) return { text };
|
|
6422
6490
|
try {
|
|
@@ -6424,10 +6492,10 @@ function createTools(host) {
|
|
|
6424
6492
|
app: on.app,
|
|
6425
6493
|
doc: result.path
|
|
6426
6494
|
} : on;
|
|
6427
|
-
const shown = await
|
|
6495
|
+
const shown = await overview(now);
|
|
6428
6496
|
const [image, ...more] = shown.images;
|
|
6429
6497
|
return {
|
|
6430
|
-
text: `${text}\nFinal check: you edited after you last looked at the document, so here is
|
|
6498
|
+
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
6499
|
...image === void 0 ? {} : { image },
|
|
6432
6500
|
...more.length ? { more } : {}
|
|
6433
6501
|
};
|
|
@@ -6437,6 +6505,38 @@ function createTools(host) {
|
|
|
6437
6505
|
},
|
|
6438
6506
|
presentCall: (args) => card(`保存 ${fileName(args.path ?? args.doc) || "当前文档"}`)
|
|
6439
6507
|
}));
|
|
6508
|
+
tools.push(defineTool({
|
|
6509
|
+
name: "office_close",
|
|
6510
|
+
description: "Close a document that YOU opened or created with office_open (a scratch file, a source you are done with). Documents the user already had open are theirs and are refused. Unsaved changes are refused unless you pass save.",
|
|
6511
|
+
parameters: {
|
|
6512
|
+
doc: DOC,
|
|
6513
|
+
app: APP,
|
|
6514
|
+
save: {
|
|
6515
|
+
type: "boolean",
|
|
6516
|
+
description: "true: save and close. false: close and drop unsaved changes."
|
|
6517
|
+
}
|
|
6518
|
+
},
|
|
6519
|
+
output,
|
|
6520
|
+
timeoutMs: 6e4,
|
|
6521
|
+
async execute(args) {
|
|
6522
|
+
const input = args;
|
|
6523
|
+
const on = target(input);
|
|
6524
|
+
if (!own.has(keyOf(on))) throw new Error("This document was open before you came to it, or was not opened by you: leave closing it to the user.");
|
|
6525
|
+
if (input.save === void 0) {
|
|
6526
|
+
const info = (await helper.call("status", {}, 3e4)).flatMap((app) => app.documents).find((doc) => doc.app === on.app && fileName(doc.path ?? doc.name).toLowerCase() === fileName(on.doc).toLowerCase());
|
|
6527
|
+
if (info && !info.saved) throw new Error("It has unsaved changes: pass save: true to keep them or save: false to drop them.");
|
|
6528
|
+
}
|
|
6529
|
+
await helper.call("close", {
|
|
6530
|
+
...on,
|
|
6531
|
+
save: input.save === true
|
|
6532
|
+
}, 5e4);
|
|
6533
|
+
own.delete(keyOf(on));
|
|
6534
|
+
unseen.delete(keyOf(on));
|
|
6535
|
+
if (current && keyOf(current) === keyOf(on)) current = void 0;
|
|
6536
|
+
return { text: `Closed ${fileName(on.doc)}.` };
|
|
6537
|
+
},
|
|
6538
|
+
presentCall: (args) => card(`关闭 ${fileName(args.doc) || "文档"}`)
|
|
6539
|
+
}));
|
|
6440
6540
|
return tools;
|
|
6441
6541
|
}
|
|
6442
6542
|
//#endregion
|
|
@@ -6450,8 +6550,8 @@ const Config = z.object({
|
|
|
6450
6550
|
"en-US": { $description: "Scroll to where the AI is editing; off = silent, your view is left alone" }
|
|
6451
6551
|
}),
|
|
6452
6552
|
typing: z.boolean().default(true).volatile().i18n({
|
|
6453
|
-
"zh-CN": { $description: "
|
|
6454
|
-
"en-US": { $description: "Write text progressively like typing instead of all at once (about
|
|
6553
|
+
"zh-CN": { $description: "文字像打字一样逐步写出,而不是整块出现(每段约多花 0.3 秒)" },
|
|
6554
|
+
"en-US": { $description: "Write text progressively like typing instead of all at once (about 0.3 s more per paragraph)" }
|
|
6455
6555
|
}),
|
|
6456
6556
|
card: z.boolean().default(true).volatile().i18n({
|
|
6457
6557
|
"zh-CN": { $description: "AI 编辑时在屏幕右下角显示迷你卡片,可随时开关“跟随”和“逐字”" },
|
package/package.json
CHANGED