From 4abb10fe518db682a8b3bc09c7d8bcdfcdacdbc9 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Sat, 23 May 2026 13:34:12 +0800 Subject: [PATCH 01/10] feat(preview): render format changes and table row revisions in HTML preview Add visual indicators for revision types that were previously invisible in the HTML preview (view html / watch): - rPrChange: yellow highlight with bottom border on affected runs - pPrChange: yellow highlight with left border on affected paragraphs - tblPrChange: yellow highlight with left border on affected tables - Table row ins/del/moveFrom/moveTo: flatten revision-wrapped rows and apply green (ins) / red strikethrough (del) row styling - Add CSS classes: .track-format, .track-ins-row, .track-del-row Before this change, only w:ins and w:del run-level revisions were visible. Format changes and table row revisions were silently absent from the preview. --- .../Word/WordHandler.HtmlPreview.Css.cs | 3 + .../Word/WordHandler.HtmlPreview.Tables.cs | 85 ++++++++++++++++++- .../Word/WordHandler.HtmlPreview.Text.cs | 28 ++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Css.cs b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Css.cs index 6184f0400..97051e297 100644 --- a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Css.cs +++ b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Css.cs @@ -2573,6 +2573,9 @@ the float past that clip. */ th, td {{ border: none; padding: 0 5.4pt; text-align: inherit; vertical-align: top; break-inside: auto; }} tr {{ break-inside: auto; }} th {{ font-weight: 600; }} + .track-format {{ background: #FFF9C4; border-left: 4px solid #FFC107; }} + .track-ins-row {{ background: #E8F5E9; }} + .track-del-row {{ background: #FFEBEE; text-decoration: line-through; color: #C62828; }} @media print {{ body {{ background: white; padding: 0; }} .page {{ box-shadow: none; margin: 0; max-width: none; transform: none !important; }} hr.page-break {{ page-break-after: always; border: none; margin: 0; }} }}"; diff --git a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Tables.cs b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Tables.cs index f46282876..dfebcffd9 100644 --- a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Tables.cs +++ b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Tables.cs @@ -147,12 +147,25 @@ private void RenderTableHtml(StringBuilder sb, Table table, string? dataPath = n } var tableClass = tableBordersNone ? "borderless" : ""; + + // Tracked table format change (tblPrChange) — yellow highlight with author attribution + var tblPrChange = tblPr?.GetFirstChild(); + var tblFmtAuthorAttr = ""; + if (tblPrChange != null) + { + tableClass = string.IsNullOrEmpty(tableClass) ? "track-format" : tableClass + " track-format"; + tableStyles.Add("background:#FFF9C4;border-left:4px solid #FFC107"); + var tblAuthor = tblPrChange.Author?.Value ?? ""; + if (!string.IsNullOrEmpty(tblAuthor)) + tblFmtAuthorAttr = $" title=\"Format changed by {HtmlEncodeAttr(tblAuthor)}\""; + } + var tableStyleAttr = tableStyles.Count > 0 ? $" style=\"{string.Join(";", tableStyles)}\"" : ""; var dataPathAttr = !string.IsNullOrEmpty(dataPath) ? $" data-path=\"{dataPath}\"" : ""; if (!string.IsNullOrEmpty(tableClass)) - sb.AppendLine($""); + sb.AppendLine($"
"); else - sb.AppendLine($""); + sb.AppendLine($""); // Get column widths from grid // tblLayout=fixed → use fixed col widths; auto/missing → let browser auto-fit by content @@ -199,6 +212,24 @@ private void RenderTableHtml(StringBuilder sb, Table table, string? dataPath = n } var rows = table.Elements().ToList(); + // Also collect rows from revision wrappers (ins/del/moveTo/moveFrom) + var revisionRowFlags = new Dictionary(); + foreach (var child in table.ChildElements) + { + if (child.LocalName is "ins" or "del" or "moveTo" or "moveFrom") + { + var revType = child.LocalName; + var revAuthor = child.GetAttributes().FirstOrDefault(a => a.LocalName == "author").Value; + foreach (var nestedRow in child.Descendants()) + { + if (!rows.Contains(nestedRow)) + { + rows.Add(nestedRow); + revisionRowFlags[nestedRow] = (revType, revAuthor); + } + } + } + } var totalRows = rows.Count; var totalCols = tblGrid?.Elements().Count() ?? rows.FirstOrDefault()?.Elements().Count() ?? 0; @@ -229,7 +260,32 @@ private void RenderTableHtml(StringBuilder sb, Table table, string? dataPath = n // have a stable /body/table[N] index. var rowDataPath = !string.IsNullOrEmpty(dataPath) ? $"{dataPath}/tr[{rowIdx + 1}]" : null; var rowDataPathAttr = rowDataPath != null ? $" data-path=\"{rowDataPath}\"" : ""; - sb.AppendLine(isHeader ? $"" : $""); + // Tracked row revision (wrapped in ins/del/moveTo/moveFrom) — visual marker + var rowRevAttr = ""; + var rowRevSuffix = ""; + if (revisionRowFlags.TryGetValue(row, out var revInfo)) + { + var (revType, revAuthor) = revInfo; + if (revType == "ins" || revType == "moveTo") + { + rowRevAttr = string.IsNullOrEmpty(revAuthor) + ? " title=\"Inserted row\"" + : $" title=\"Inserted row by {HtmlEncodeAttr(revAuthor)}\""; + rowRevSuffix = " track-ins-row"; + } + else // del or moveFrom + { + rowRevAttr = string.IsNullOrEmpty(revAuthor) + ? " title=\"Deleted row\"" + : $" title=\"Deleted row by {HtmlEncodeAttr(revAuthor)}\""; + rowRevSuffix = " track-del-row"; + } + } + sb.AppendLine(isHeader + ? $"" + : string.IsNullOrEmpty(rowRevSuffix) + ? $"" + : $""); int colIdx = 0; foreach (var cell in row.Elements()) @@ -546,9 +602,30 @@ private static int GetGridColumn(TableRow row, TableCell cell) return null; } - private static int CountRowSpan(Table table, TableRow startRow, TableCell startCell) + /// + /// Get all TableRow elements from a table, including those nested inside + /// revision wrappers (w:ins, w:del, w:moveTo, w:moveFrom). + /// + private static List GetFlattenedTableRows(Table table) { var rows = table.Elements().ToList(); + foreach (var child in table.ChildElements) + { + if (child.LocalName is "ins" or "del" or "moveTo" or "moveFrom") + { + foreach (var nestedRow in child.Descendants()) + { + if (!rows.Contains(nestedRow)) + rows.Add(nestedRow); + } + } + } + return rows; + } + + private static int CountRowSpan(Table table, TableRow startRow, TableCell startCell) + { + var rows = GetFlattenedTableRows(table); var startRowIdx = rows.IndexOf(startRow); if (startRowIdx < 0) return 1; diff --git a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Text.cs b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Text.cs index 792f32296..e5e55b0d5 100644 --- a/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Text.cs +++ b/src/officecli/Handlers/Word/WordHandler.HtmlPreview.Text.cs @@ -46,7 +46,22 @@ private void RenderParagraphHtml(StringBuilder sb, Paragraph para) classes.Add("has-ptab"); if (classes.Count > 0) sb.Append($" class=\"{string.Join(" ", classes)}\""); + + // Tracked paragraph format change (pPrChange) — yellow left border with author attribution + var pPrChange = para.ParagraphProperties?.GetFirstChild(); + if (pPrChange != null) + { + var pAuthor = pPrChange.Author?.Value ?? ""; + if (!string.IsNullOrEmpty(pAuthor)) + sb.Append($" title=\"Format changed by {HtmlEncodeAttr(pAuthor)}\""); + } + var pStyle = GetParagraphInlineCss(para); + if (pPrChange != null) + { + var fmtStyle = "background:#FFF9C4;border-left:4px solid #FFC107"; + pStyle = string.IsNullOrEmpty(pStyle) ? fmtStyle : pStyle + ";" + fmtStyle; + } if (!string.IsNullOrEmpty(pStyle)) sb.Append($" style=\"{pStyle}\""); sb.Append(">"); @@ -303,6 +318,17 @@ c is Break || c is TabChar || c is SymbolChar || c is CarriageReturn return; if (rProps.SpecVanish != null && (rProps.SpecVanish.Val == null || rProps.SpecVanish.Val.Value)) return; + + // Tracked format change (rPrChange) — yellow highlight with author attribution + var rPrChange = run.RunProperties?.GetFirstChild(); + bool hasRPrChange = rPrChange != null; + if (hasRPrChange) + { + var author = rPrChange!.Author?.Value ?? ""; + var authorAttr = string.IsNullOrEmpty(author) ? "" : $" title=\"Format changed by {HtmlEncodeAttr(author)}\""; + sb.Append($""); + } + var style = GetRunInlineCss(rProps, para); var needsSpan = !string.IsNullOrEmpty(style); @@ -470,6 +496,8 @@ c is Break || c is TabChar || c is SymbolChar || c is CarriageReturn if (needsSpan && !_ctx.LineBreakEnabled) sb.Append(""); + if (hasRPrChange) + sb.Append(""); } // ==================== OLE Object Preview Rendering ==================== From 490313bb9973c05796a38ad0da0b1d2c95e072d1 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Sat, 23 May 2026 13:34:22 +0800 Subject: [PATCH 02/10] feat(watch): add revision API endpoints and toolbar for accept/reject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new HTTP endpoints to the watch server: - POST /api/revision/accept — accept all tracked changes - POST /api/revision/reject — reject all tracked changes - GET /api/revision/count — return revision count as JSON All endpoints spawn officecli commands (following the existing /api/edit pattern) and notify SSE clients with a "full" refresh after accept/reject operations. Additionally, inject a floating revision toolbar into watch HTML output for Word documents: - Shows revision count badge (fetched from /api/revision/count) - Accept All (green) / Reject All (red) buttons - Auto-hides when no revisions exist - Only appears for Word documents (detected by data-block markers) - Refreshes automatically via SSE update events This enables AionUI and other watch consumers to provide revision management without any frontend code changes. --- src/officecli/Core/Watch/WatchServer.cs | 201 +++++++++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/src/officecli/Core/Watch/WatchServer.cs b/src/officecli/Core/Watch/WatchServer.cs index 29108b227..3d8c23fa9 100644 --- a/src/officecli/Core/Watch/WatchServer.cs +++ b/src/officecli/Core/Watch/WatchServer.cs @@ -1726,6 +1726,27 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken token) return; } + if (requestLine.StartsWith("POST /api/revision/accept", StringComparison.Ordinal)) + { + await HandleRevisionAcceptAsync(stream, headers, bodyPrefix, token); + client.Close(); + return; + } + + if (requestLine.StartsWith("POST /api/revision/reject", StringComparison.Ordinal)) + { + await HandleRevisionRejectAsync(stream, headers, bodyPrefix, token); + client.Close(); + return; + } + + if (requestLine.StartsWith("GET /api/revision/count", StringComparison.Ordinal)) + { + await HandleRevisionCountAsync(stream, headers, bodyPrefix, token); + client.Close(); + return; + } + // BUG-TESTER-R503: GET/PUT/etc on /api/selection must return 405, // not fall through to the HTML preview. Without this, an API // client that uses the wrong verb gets back a 200 HTML page and @@ -2012,6 +2033,127 @@ private async Task HandlePostEditAsync(NetworkStream stream, Dictionary + /// Handle POST /api/revision/accept — accept all tracked changes. + /// Spawns officecli set with acceptallchanges=all, waits for completion, + /// then signals SSE clients with a "full" refresh. + /// + private async Task HandleRevisionAcceptAsync(NetworkStream stream, Dictionary headers, string bodyPrefix, CancellationToken token) + { + await RunRevisionActionAsync(stream, "acceptallchanges=all", token); + } + + /// + /// Handle POST /api/revision/reject — reject all tracked changes. + /// Spawns officecli set with rejectallchanges=all, waits for completion, + /// then signals SSE clients with a "full" refresh. + /// + private async Task HandleRevisionRejectAsync(NetworkStream stream, Dictionary headers, string bodyPrefix, CancellationToken token) + { + await RunRevisionActionAsync(stream, "rejectallchanges=all", token); + } + + /// + /// Common helper for revision accept/reject actions. + /// Spawns officecli set with the given prop, returns 204 on success. + /// After the command completes, sends a "full" SSE event to trigger + /// a page refresh on all connected browsers. + /// + private async Task RunRevisionActionAsync(NetworkStream stream, string propArg, CancellationToken token) + { + int statusCode = 204; + string statusText = "No Content"; + try + { + var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName + ?? (OperatingSystem.IsWindows() ? "officecli.exe" : "officecli"); + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = exe, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + psi.ArgumentList.Add("set"); + psi.ArgumentList.Add(_filePath); + psi.ArgumentList.Add("/"); + psi.ArgumentList.Add("--prop"); + psi.ArgumentList.Add(propArg); + using var proc = System.Diagnostics.Process.Start(psi); + if (proc != null) + { + await proc.WaitForExitAsync(token); + // After the set command completes, notify SSE clients to refresh + _version++; + SendSseEvent("full", 0, null, null, _version); + } + } + catch + { + statusCode = 400; statusText = "Bad Request"; + } + var resp = Encoding.UTF8.GetBytes( + $"HTTP/1.1 {statusCode} {statusText}\r\nContent-Length: 0\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\n\r\n"); + await stream.WriteAsync(resp, token); + } + + /// + /// Handle GET /api/revision/count — query revision count. + /// Spawns officecli query with the revision argument, captures output, + /// and returns the count as JSON: {"count": N} + /// + private async Task HandleRevisionCountAsync(NetworkStream stream, Dictionary headers, string bodyPrefix, CancellationToken token) + { + int statusCode = 200; + string statusText = "OK"; + string jsonBody = "{\"count\":0}"; + try + { + var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName + ?? (OperatingSystem.IsWindows() ? "officecli.exe" : "officecli"); + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = exe, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + psi.ArgumentList.Add("query"); + psi.ArgumentList.Add(_filePath); + psi.ArgumentList.Add("revision"); + using var proc = System.Diagnostics.Process.Start(psi); + if (proc != null) + { + var output = await proc.StandardOutput.ReadToEndAsync(token); + await proc.WaitForExitAsync(token); + proc.Close(); + // "officecli query revision" outputs one line per revision. + // Count non-empty lines to get the revision count. + if (string.IsNullOrWhiteSpace(output)) + { + jsonBody = "{\"count\":0}"; + } + else + { + var lineCount = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length; + jsonBody = $"{{\"count\":{lineCount}}}"; + } + } + } + catch + { + statusCode = 500; statusText = "Internal Server Error"; + jsonBody = "{\"count\":0,\"error\":\"query failed\"}"; + } + var bodyBytes = Encoding.UTF8.GetBytes(jsonBody); + var resp = Encoding.UTF8.GetBytes( + $"HTTP/1.1 {statusCode} {statusText}\r\nContent-Type: application/json\r\nContent-Length: {bodyBytes.Length}\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\n\r\n"); + await stream.WriteAsync(resp, token); + await stream.WriteAsync(bodyBytes, token); + } + private void BroadcastSelectionUpdate(List paths) { var sb = new StringBuilder(); @@ -2116,10 +2258,65 @@ private async Task HandleSseAsync(NetworkStream stream, CancellationToken token) private static string InjectSseScript(string html) { var script = _sseScriptBlock.Value; + // Inject revision toolbar (HTML+CSS+JS) alongside the existing SSE scripts. + // Only activates for Word documents (detected by "data-block" markers). + // The toolbar fetches /api/revision/count, shows Accept All/Reject All buttons. + var revisionToolbar = """ + +"""; var idx = html.LastIndexOf("", StringComparison.OrdinalIgnoreCase); if (idx >= 0) - return html[..idx] + script + html[idx..]; - return html + script; + return html[..idx] + script + revisionToolbar + html[idx..]; + return html + script + revisionToolbar; } public void Dispose() From e875b2f03a1d0c3f873a1e4a6591273223bf4e1b Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Tue, 26 May 2026 22:18:30 +0800 Subject: [PATCH 03/10] fix(watch): migrate revision API from legacy aliases to revision.action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - acceptallchanges=all → revision.action=accept - rejectallchanges=all → revision.action=reject - path / → /revision - update doc comments --- src/officecli/Core/Watch/WatchServer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/officecli/Core/Watch/WatchServer.cs b/src/officecli/Core/Watch/WatchServer.cs index 3d8c23fa9..5bce7833a 100644 --- a/src/officecli/Core/Watch/WatchServer.cs +++ b/src/officecli/Core/Watch/WatchServer.cs @@ -2035,27 +2035,27 @@ private async Task HandlePostEditAsync(NetworkStream stream, Dictionary /// Handle POST /api/revision/accept — accept all tracked changes. - /// Spawns officecli set with acceptallchanges=all, waits for completion, + /// Spawns officecli set with revision.action=accept, waits for completion, /// then signals SSE clients with a "full" refresh. /// private async Task HandleRevisionAcceptAsync(NetworkStream stream, Dictionary headers, string bodyPrefix, CancellationToken token) { - await RunRevisionActionAsync(stream, "acceptallchanges=all", token); + await RunRevisionActionAsync(stream, "revision.action=accept", token); } /// /// Handle POST /api/revision/reject — reject all tracked changes. - /// Spawns officecli set with rejectallchanges=all, waits for completion, + /// Spawns officecli set with revision.action=reject, waits for completion, /// then signals SSE clients with a "full" refresh. /// private async Task HandleRevisionRejectAsync(NetworkStream stream, Dictionary headers, string bodyPrefix, CancellationToken token) { - await RunRevisionActionAsync(stream, "rejectallchanges=all", token); + await RunRevisionActionAsync(stream, "revision.action=reject", token); } /// /// Common helper for revision accept/reject actions. - /// Spawns officecli set with the given prop, returns 204 on success. + /// Spawns "officecli set /revision --prop revision.action=..." returns 204 on success. /// After the command completes, sends a "full" SSE event to trigger /// a page refresh on all connected browsers. /// @@ -2077,7 +2077,7 @@ private async Task RunRevisionActionAsync(NetworkStream stream, string propArg, }; psi.ArgumentList.Add("set"); psi.ArgumentList.Add(_filePath); - psi.ArgumentList.Add("/"); + psi.ArgumentList.Add("/revision"); psi.ArgumentList.Add("--prop"); psi.ArgumentList.Add(propArg); using var proc = System.Diagnostics.Process.Start(psi); From 9e892c55670f3be83430a439f741bcef802a32e7 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Tue, 26 May 2026 22:36:05 +0800 Subject: [PATCH 04/10] docs: add fork differences notice at top of README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dfd690cd4..f82cb4932 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ # OfficeCLI +> **This is a fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) maintained by [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF). +> +> **Differences from upstream:** +> - **HTML preview: Track Changes rendering** — revision marks (ins/del/format/move) are visually rendered in `view html` and Watch mode with distinct CSS styling +> - **Watch server: Revision API endpoints** — `POST /api/revision/accept`, `POST /api/revision/reject`, `GET /api/revision/count` for in-browser accept/reject controls +> - **Revision toolbar** — floating toolbar injected into Watch HTML with revision count badge and accept/reject buttons +> +> All changes are cleanly rebased on upstream `main` (3 commits ahead). Build as usual: `dotnet publish -c Release -r osx-arm64 --self-contained -o out`. > **OfficeCLI is the world's first and the best Office suite designed for AI agents.** - **Give any AI agent full control over Word, Excel, and PowerPoint — in one line of code.** Open-source. Single binary. No Office installation. No dependencies. Works everywhere. From 7723216f0faffbd75c4b08eae1eeb6f1ddd84626 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Tue, 26 May 2026 22:38:44 +0800 Subject: [PATCH 05/10] docs: add Chinese fork notice, mention AionUI optimization --- README.md | 2 +- README_zh.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f82cb4932..8e1890e7c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OfficeCLI -> **This is a fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) maintained by [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF). +> **This is a fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) maintained by [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF), optimized for [AionUI](https://github.com/iOfficeAI/AionUi) preview. > > **Differences from upstream:** > - **HTML preview: Track Changes rendering** — revision marks (ins/del/format/move) are visually rendered in `view html` and Watch mode with distinct CSS styling diff --git a/README_zh.md b/README_zh.md index 1ddbd4404..42a67085a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,5 +1,13 @@ # OfficeCLI +> **这是 [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) 的一个 fork**,由 [@NextDoorLaoHuang-HF](https://github.com/NextDoorLaoHuang-HF) 维护,为 [AionUI](https://github.com/iOfficeAI/AionUi) 预览体验优化。 +> +> **相对于上游的差异:** +> - **HTML 预览:修订模式(Track Changes)渲染** — 插入/删除/格式/移动标记在 `view html` 和 Watch 模式中以独立 CSS 样式可视化渲染 +> - **Watch 服务:修订 API 端点** — `POST /api/revision/accept`、`POST /api/revision/reject`、`GET /api/revision/count`,支持浏览器内接受/拒绝修订 +> - **修订工具栏** — Watch HTML 中注入浮动工具栏,显示修订计数徽章和接受/拒绝按钮 +> +> 所有改动干净地 rebase 在上游 `main` 之上(领先 3 个提交)。构建方式不变:`dotnet publish -c Release -r osx-arm64 --self-contained -o out`。 > **OfficeCLI 是全球首个、也是最好的专为 AI 智能体设计的 Office 套件。** **让任何 AI 智能体完全掌控 Word、Excel 和 PowerPoint——只需一行代码。** From d67b5aebbddc85db1567a1305fa232b3ce1ffc72 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Wed, 27 May 2026 13:48:59 +0800 Subject: [PATCH 06/10] feat(setup-aionui): add AionUI installer + skill + early dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AionuiInstaller.cs: one-shot integration that installs officecli-track-changes skill and registers Word 修订助手 assistant - Add skills/officecli-track-changes/SKILL.md: skill file defining OOXML Track Changes capabilities for AionUI agents - Add SkillInstaller entry for track-changes skill - Add early dispatch for setup-aionui command in Program.cs (placed between mcp and install blocks for early exit) - Supports --dry-run, --verbose, and --force flags - Auto-detects AionUI config directory across macOS/Linux/Windows - Handles base64url-encoded aionui-config.txt - Idempotent: detects existing skill and assistant, skips re-registration - Creates .bak backup before modifying config - Named id prefix custom-officecli-revision-* for easy identification --- skills/officecli-track-changes/SKILL.md | 117 ++++++++++ src/officecli/Core/AionuiInstaller.cs | 287 ++++++++++++++++++++++++ src/officecli/Core/SkillInstaller.cs | 1 + src/officecli/Program.cs | 6 + 4 files changed, 411 insertions(+) create mode 100644 skills/officecli-track-changes/SKILL.md create mode 100644 src/officecli/Core/AionuiInstaller.cs diff --git a/skills/officecli-track-changes/SKILL.md b/skills/officecli-track-changes/SKILL.md new file mode 100644 index 000000000..ea1b10563 --- /dev/null +++ b/skills/officecli-track-changes/SKILL.md @@ -0,0 +1,117 @@ +--- +name: officecli-track-changes +description: "Track Changes / 修订模式 for .docx files. Use when the user wants to review, audit, redline, or revise a document with tracked changes (修订模式, 审查, redline, revision). MUST load alongside officecli skill." +--- + +# Track Changes / Revision Mode (Word .docx) + +When the user asks to review, audit, redline, or revise a document with tracked changes, use `--prop revision.type=...` / `--prop revision.author=...`. Do NOT use python-docx, unpack/pack, raw-set, or any other approach — officecli v1.0.98+ handles OOXML revision markup natively via the `revision.*` API. + +**API migration**: Previous `trackChange=ins/del/format` has been replaced by `revision.type=ins/del/format`. See the property table below. + +## Preferred: find + revision (Word-style Find & Replace with Track Changes) + +One command does it all — the handler auto-creates paired ins+del markers: + +```bash +# Replace text with tracked change (one command) +officecli set "$FILE" /body --prop find="old text" --prop replace="new text" --prop revision.author=AI + +# Delete text (tracked deletion — no insertion) +officecli set "$FILE" /body --prop find="text to remove" --prop replace= --prop revision.author=AI + +# Format change on matched text (bold all occurrences of "keyword") +officecli set "$FILE" /body --prop find="keyword" --prop bold=true --prop revision.author=AI + +# Regex-based find+replace +officecli set "$FILE" /body --prop 'find=\$\d+' --prop regex=true --prop replace="[PRICE]" --prop revision.author=AI +``` + +## Alternate: Direct run wrapping with `set` + `revision.type` + +```bash +# Mark existing text as inserted +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=ins --prop revision.author=AI + +# Mark existing text as deleted +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=del --prop revision.author=AI + +# Move revision (paired — same revision.id) +officecli set "$FILE" '/body/p[1]/r[1]' --prop revision.type=moveFrom --prop revision.author=AI --prop revision.id=100 +officecli set "$FILE" '/body/p[2]/r[1]' --prop revision.type=moveTo --prop revision.author=AI --prop revision.id=100 +``` + +## Legacy: Split+remove+add workflow (complex multi-change paragraphs) + +```bash +# Step 0: Read run structure FIRST +officecli get "$FILE" '/body/p[N]' --depth 2 + +# Step 1: Isolate target into own run +officecli set "$FILE" '/body/p[N]' --prop find="8" --prop color=auto + +# Step 2: Remove original run +officecli remove "$FILE" '/body/p[N]/r[2]' + +# Step 3: Add ins run +officecli add "$FILE" '/body/p[N]' --type run --after '/body/p[N]/r[1]' --prop revision.type=ins --prop revision.author=AI --prop text="4" + +# Step 4: Add del run +officecli add "$FILE" '/body/p[N]' --type run --after '/body/p[N]/r[1]' --prop revision.type=del --prop revision.author=AI --prop text="8" +``` + +## Revision properties (creation) + +| Property | Values | Where | +|----------|--------|-------| +| `revision.type` | `ins`, `del`, `format`, `moveFrom`, `moveTo` | set on run/paragraph/table/row/cell/section | +| `revision.author` | Any string | paired with `revision.type` | +| `revision.date` | ISO 8601 (optional) | paired with `revision.type` | +| `revision.id` | Integer (auto if omitted) | required for moveFrom/moveTo pairs | + +## Revision actions (accept/reject) + +```bash +# Accept all / reject all +officecli set "$FILE" /revision --prop revision.action=accept +officecli set "$FILE" /revision --prop revision.action=reject + +# Filtered accept/reject +officecli set "$FILE" '/revision[@author=AI]' --prop revision.action=accept +officecli set "$FILE" '/revision[@type=ins]' --prop revision.action=accept +officecli set "$FILE" '/revision[@id=42]' --prop revision.action=accept +``` + +## Verify + +```bash +officecli query "$FILE" revision # list all revisions +officecli query "$FILE" revision --json # JSON output +``` + +**Do NOT use `view html` in AionUI** — AionUI already has a live preview via `officecli watch`. Use `query revision` to verify. + +## Contract review pattern + +```bash +FILE="contract_reviewed.docx" +cp original.docx "$FILE" +officecli open "$FILE" + +# One command per change with find + revision +officecli set "$FILE" /body --prop find="3个工作日" --prop replace="5个工作日" --prop revision.author=甲方 +officecli set "$FILE" /body --prop find="8小时" --prop replace="4小时" --prop revision.author=甲方 +officecli set "$FILE" /body --prop find="24小时" --prop replace="12小时" --prop revision.author=甲方 + +officecli query "$FILE" revision +officecli close "$FILE" +``` + +## Critical rules + +1. **Prefer `find + revision`** — one command replaces old→new with ins+del pair. +2. **Use `set` + `revision.type=...`** to wrap existing content — no need to remove/add. +3. **Never fall back to python-docx, unpack/pack, or raw XML** — v1.0.98+ handles all revision types natively. +4. **`revision.type=format` needs a real property change** — pair with `bold=true`, `font.color=...`, etc. +5. **moveFrom/moveTo must share `revision.id`** — same ID on both halves for paired move. +6. **`find` matches within a single run** — if 0 matches, use `get --depth 2` to see boundaries. diff --git a/src/officecli/Core/AionuiInstaller.cs b/src/officecli/Core/AionuiInstaller.cs new file mode 100644 index 000000000..9a88b52de --- /dev/null +++ b/src/officecli/Core/AionuiInstaller.cs @@ -0,0 +1,287 @@ +// Copyright 2025 OfficeCLI (officecli.ai) +// SPDX-License-Identifier: Apache-2.0 + +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace OfficeCli.Core; + +/// +/// One-shot AionUI integration: installs the officecli-track-changes skill +/// and registers the "Word 修订助手" assistant so users get revision +/// capabilities immediately after running `officecli setup-aionui`. +/// +internal static class AionuiInstaller +{ + private const string SkillFolder = "officecli-track-changes"; + private const string SkillResource = "skills/officecli-track-changes/SKILL.md"; + private const string ConfigFileName = "aionui-config.txt"; + private const string AssistantIdPrefix = "custom-officecli-revision-"; + + /// + /// Main entry point. Called from Program.cs early-dispatch. + /// + public static int Run(string[] args) + { + var verbose = args.Contains("--verbose") || args.Contains("-v"); + var dryRun = args.Contains("--dry-run"); + var force = args.Contains("--force"); + + var configDir = DetectConfigDir(); + if (configDir == null) + { + Console.Error.WriteLine("AionUI config directory not found. Is AionUI installed?"); + Console.Error.WriteLine("Supported paths:"); + Console.Error.WriteLine(" macOS: ~/Library/Application Support/AionUI/config/"); + Console.Error.WriteLine(" Linux: ~/.config/AionUI/config/"); + Console.Error.WriteLine(" Windows: %APPDATA%/AionUI/config/"); + return 1; + } + + Console.WriteLine($"AionUI detected at: {configDir}"); + if (dryRun) Console.WriteLine("[dry-run] No changes will be made."); + Console.WriteLine(); + + var ok = true; + + // 1. Install skill file + ok &= InstallSkill(configDir, verbose, dryRun); + + // 2. Register assistant in config + ok &= RegisterAssistant(configDir, verbose, dryRun, force); + + if (dryRun) + { + Console.WriteLine(); + Console.WriteLine("[dry-run] Run without --dry-run to apply changes."); + } + else if (ok) + { + Console.WriteLine(); + Console.WriteLine("Done! Restart AionUI to see \"Word 修订助手\" in the assistant list."); + } + + return ok ? 0 : 1; + } + + // ─── Detection ─────────────────────────────────────────── + + private static string? DetectConfigDir() + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + // Ordered by platform likelihood — first match wins + var candidates = new List(); + + if (OperatingSystem.IsMacOS()) + { + candidates.Add(Path.Combine(home, "Library", "Application Support", "AionUI", "config")); + } + else if (OperatingSystem.IsLinux()) + { + candidates.Add(Path.Combine(home, ".config", "AionUI", "config")); + } + else if (OperatingSystem.IsWindows()) + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + candidates.Add(Path.Combine(appData, "AionUI", "config")); + } + + // Cross-platform fallback: try all known paths + candidates.Add(Path.Combine(home, "Library", "Application Support", "AionUI", "config")); + candidates.Add(Path.Combine(home, ".config", "AionUI", "config")); + + foreach (var dir in candidates) + { + if (Directory.Exists(dir)) + return dir; + } + + return null; + } + + // ─── Skill installation ────────────────────────────────── + + private static bool InstallSkill(string configDir, bool verbose, bool dryRun) + { + var skillsDir = Path.Combine(configDir, "skills"); + var targetDir = Path.Combine(skillsDir, SkillFolder); + var targetFile = Path.Combine(targetDir, "SKILL.md"); + + var content = LoadEmbeddedSkill(); + if (content == null) + { + Console.Error.WriteLine($" ✗ Embedded skill resource not found: {SkillResource}"); + return false; + } + + if (File.Exists(targetFile) && File.ReadAllText(targetFile) == content) + { + Console.WriteLine($" ✓ Skill already up to date: {SkillFolder}"); + return true; + } + + if (dryRun) + { + Console.WriteLine($" [dry-run] Would install skill: {targetFile}"); + return true; + } + + try + { + Directory.CreateDirectory(targetDir); + File.WriteAllText(targetFile, content); + Console.WriteLine($" ✓ Skill installed: {targetFile}"); + return true; + } + catch (Exception ex) + { + Console.Error.WriteLine($" ✗ Failed to install skill: {ex.Message}"); + if (verbose) Console.Error.WriteLine(ex); + return false; + } + } + + private static string? LoadEmbeddedSkill() + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(SkillResource); + if (stream == null) return null; + using var reader = new StreamReader(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + + // ─── Assistant registration ────────────────────────────── + + private static bool RegisterAssistant(string configDir, bool verbose, bool dryRun, bool force) + { + var configPath = Path.Combine(configDir, ConfigFileName); + if (!File.Exists(configPath)) + { + Console.Error.WriteLine($" ✗ Config file not found: {configPath}"); + return false; + } + + try + { + // Read and decode config + var raw = File.ReadAllText(configPath).Trim(); + var json = DecodeConfig(raw); + var root = JsonNode.Parse(json); + if (root == null) + { + Console.Error.WriteLine(" ✗ Failed to parse AionUI config JSON"); + return false; + } + + var assistants = root["assistants"]?.AsArray(); + if (assistants == null) + { + // Create assistants array if missing + assistants = new JsonArray(); + root["assistants"] = assistants; + } + + // Check if already registered + var existing = assistants.FirstOrDefault(a => + a?["name"]?.GetValue() == "Word 修订助手"); + if (existing != null && !force) + { + Console.WriteLine($" ✓ Assistant already registered: Word 修订助手 (id: {existing["id"]})"); + return true; + } + + if (existing != null && force) + { + assistants.Remove(existing); + if (verbose) Console.WriteLine(" --force: removed existing assistant entry"); + } + + // Build assistant entry + var entry = new JsonObject + { + ["id"] = AssistantIdPrefix + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + ["name"] = "Word 修订助手", + ["description"] = "专注 Word 文档修订(Track Changes)的助手,支持查找替换修订、格式修订、接受/拒绝修订。", + ["avatar"] = "📝", + ["isPreset"] = true, + ["isBuiltin"] = false, + ["presetAgentType"] = "opencode", + ["enabled"] = true, + ["enabledSkills"] = new JsonArray("officecli-docx", "officecli-track-changes"), + ["customSkillNames"] = new JsonArray(), + }; + + if (dryRun) + { + Console.WriteLine($" [dry-run] Would register assistant: Word 修订助手 (id: {entry["id"]})"); + return true; + } + + // Backup original config + var backupPath = configPath + ".bak"; + File.Copy(configPath, backupPath, overwrite: true); + if (verbose) Console.WriteLine($" Backed up config to: {backupPath}"); + + // Add assistant and re-encode + assistants.Add((JsonNode)entry); + var newJson = root.ToJsonString(new JsonSerializerOptions { WriteIndented = false }); + var encoded = EncodeConfig(newJson); + File.WriteAllText(configPath, encoded); + + Console.WriteLine($" ✓ Assistant registered: Word 修订助手 (id: {entry["id"]})"); + return true; + } + catch (Exception ex) + { + Console.Error.WriteLine($" ✗ Failed to register assistant: {ex.Message}"); + if (verbose) Console.Error.WriteLine(ex); + return false; + } + } + + // ─── Config encoding ───────────────────────────────────── + + /// + /// Decode AionUI config: base64url → URL-decode → JSON string. + /// + private static string DecodeConfig(string raw) + { + var bytes = Base64UrlDecode(raw); + var urlEncoded = Encoding.UTF8.GetString(bytes); + return Uri.UnescapeDataString(urlEncoded); + } + + /// + /// Encode AionUI config: JSON string → URL-encode → base64url. + /// + private static string EncodeConfig(string json) + { + var urlEncoded = Uri.EscapeDataString(json); + var bytes = Encoding.UTF8.GetBytes(urlEncoded); + return Base64UrlEncode(bytes); + } + + private static byte[] Base64UrlDecode(string input) + { + var base64 = input + .Replace('-', '+') + .Replace('_', '/'); + switch (base64.Length % 4) + { + case 2: base64 += "=="; break; + case 3: base64 += "="; break; + } + return Convert.FromBase64String(base64); + } + + private static string Base64UrlEncode(byte[] bytes) + { + return Convert.ToBase64String(bytes) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd('='); + } +} diff --git a/src/officecli/Core/SkillInstaller.cs b/src/officecli/Core/SkillInstaller.cs index 635f1b8ba..f9d831786 100644 --- a/src/officecli/Core/SkillInstaller.cs +++ b/src/officecli/Core/SkillInstaller.cs @@ -48,6 +48,7 @@ private static readonly (string[] Aliases, string DisplayName, string DetectDir, ["academic-paper"] = "officecli-academic-paper", ["data-dashboard"] = "officecli-data-dashboard", ["financial-model"] = "officecli-financial-model", + ["track-changes"] = "officecli-track-changes", }; /// diff --git a/src/officecli/Program.cs b/src/officecli/Program.cs index 03bab369c..f4dc7dc4d 100644 --- a/src/officecli/Program.cs +++ b/src/officecli/Program.cs @@ -86,6 +86,12 @@ return 1; } +// Setup AionUI command: officecli setup-aionui [--verbose] [--dry-run] [--force] +if (args.Length >= 1 && args[0] == "setup-aionui") +{ + return OfficeCli.Core.AionuiInstaller.Run(args.Skip(1).ToArray()); +} + // Install command: officecli install [target] if (args.Length >= 1 && args[0] == "install") { From 2f3453912e1b122eb1ebedfb641c07e2f5607071 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Wed, 27 May 2026 15:29:45 +0800 Subject: [PATCH 07/10] fix(setup-aionui): robust idempotency with _setupBy marker + ToString fallback - Primary check: _setupBy == 'officecli' marker field (deterministic) - Fallback: name match via ToString() (avoids GetValue() edge cases) - Added _setupBy marker to all new assistant entries --- src/officecli/Core/AionuiInstaller.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/officecli/Core/AionuiInstaller.cs b/src/officecli/Core/AionuiInstaller.cs index 9a88b52de..0e1800030 100644 --- a/src/officecli/Core/AionuiInstaller.cs +++ b/src/officecli/Core/AionuiInstaller.cs @@ -184,9 +184,16 @@ private static bool RegisterAssistant(string configDir, bool verbose, bool dryRu root["assistants"] = assistants; } - // Check if already registered + // Check if already registered — use _setupBy marker for deterministic match + // (more robust than name-only matching which can fail on encoding edge cases) var existing = assistants.FirstOrDefault(a => - a?["name"]?.GetValue() == "Word 修订助手"); + a?["_setupBy"]?.GetValue() == "officecli"); + if (existing == null) + { + // Fallback: name-based check for manually created assistants + existing = assistants.FirstOrDefault(a => + a?["name"]?.ToString() == "Word 修订助手"); + } if (existing != null && !force) { Console.WriteLine($" ✓ Assistant already registered: Word 修订助手 (id: {existing["id"]})"); @@ -212,6 +219,7 @@ private static bool RegisterAssistant(string configDir, bool verbose, bool dryRu ["enabled"] = true, ["enabledSkills"] = new JsonArray("officecli-docx", "officecli-track-changes"), ["customSkillNames"] = new JsonArray(), + ["_setupBy"] = "officecli", }; if (dryRun) From d9720650489f1b4c7501bf608bd47802c9b8f0bd Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Wed, 27 May 2026 16:19:19 +0800 Subject: [PATCH 08/10] docs: add setup-aionui to README (EN + ZH) --- README.md | 51 ++++++++++++++++++++++----------------------------- README_zh.md | 51 ++++++++++++++++++++++----------------------------- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 8e1890e7c..77edefb96 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ That's it. The skill file teaches the agent how to install the binary and use al **Option A — GUI:** Install [**AionUi**](https://github.com/iOfficeAI/AionUi) — a desktop app that lets you create and edit Office documents through natural language, powered by OfficeCLI under the hood. Just describe what you want, and AionUi handles the rest. -**Option B — CLI:** Download the binary for your platform from [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases), then run: +**Option B — CLI:** Build from source (see [Installation](#installation) below), then run: ```bash officecli install @@ -91,12 +91,21 @@ officecli install This copies the binary to your PATH and installs the **officecli skill** into every AI coding agent it detects — Claude Code, Cursor, Windsurf, GitHub Copilot, and more. Your agent can immediately create, read, and edit Office documents on your behalf, no extra configuration needed. +**AionUI users** — get Track Changes / revision mode with one command: + +```bash +officecli setup-aionui +``` + +This installs the `officecli-track-changes` skill and registers the **Word 修订助手** assistant in AionUI. Restart AionUI and you'll see it in the assistant list — ready for contract review, redlining, and revision workflows. + ## For Developers — See It Live in 30 Seconds ```bash -# 1. Install (macOS / Linux) -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash -# Windows (PowerShell): irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +# 1. Build and install (macOS / Linux) +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ # 2. Create a blank PowerPoint officecli create deck.pptx @@ -205,39 +214,23 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" ## Installation -Ships as a single self-contained binary. The .NET runtime is embedded -- nothing to install, no runtime to manage. +This fork does not publish its own releases. Build from source: -**One-line install:** +**Prerequisites:** +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) ```bash -# macOS / Linux -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash - -# Windows (PowerShell) -irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git +cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ ``` -**Or download manually** from [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): - -| Platform | Binary | -|----------|--------| -| macOS Apple Silicon | `officecli-mac-arm64` | -| macOS Intel | `officecli-mac-x64` | -| Linux x64 | `officecli-linux-x64` | -| Linux ARM64 | `officecli-linux-arm64` | -| Windows x64 | `officecli-win-x64.exe` | -| Windows ARM64 | `officecli-win-arm64.exe` | +**Other platforms:** replace `osx-arm64` with `osx-x64`, `linux-x64`, `linux-arm64`, `win-x64`, or `win-arm64`. Verify installation: `officecli --version` -**Or self-install from a downloaded binary (or run bare `officecli` to auto-install):** - -```bash -officecli install # explicit -officecli # bare invocation also triggers install -``` - -Updates are checked automatically in the background. Disable with `officecli config autoUpdate false` or skip per-invocation with `OFFICECLI_SKIP_UPDATE=1`. Configuration lives under `~/.officecli/config.json`. +> **Tip:** For the original upstream release with prebuilt binaries, see [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI#installation). ## Key Features diff --git a/README_zh.md b/README_zh.md index 42a67085a..a6e33ee80 100644 --- a/README_zh.md +++ b/README_zh.md @@ -84,7 +84,7 @@ curl -fsSL https://officecli.ai/SKILL.md **方式 A — 图形界面:** 安装 [**AionUi**](https://github.com/iOfficeAI/AionUi) — 一款桌面应用,用自然语言就能创建和编辑 Office 文档,底层由 OfficeCLI 驱动。只需描述你想要什么,AionUi 帮你搞定。 -**方式 B — 命令行:** 从 [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases) 下载对应平台的二进制文件,然后运行: +**方式 B — 命令行:** 从源码构建(见下方[安装](#安装)),然后运行: ```bash officecli install @@ -92,12 +92,21 @@ officecli install 该命令会将二进制文件复制到 PATH,并自动将 **officecli 技能文件**安装到检测到的所有 AI 编程助手 — Claude Code、Cursor、Windsurf、GitHub Copilot 等。您的智能体可以立即创建、读取和编辑 Office 文档,无需额外配置。 +**AionUI 用户** — 一条命令启用修订模式(Track Changes): + +```bash +officecli setup-aionui +``` + +这会安装 `officecli-track-changes` 技能并在 AionUI 中注册 **Word 修订助手**。重启 AionUI 后即可在助手列表中看到 — 适用于合同审查、修订标记等工作流。 + ## 开发者 — 30 秒亲眼看到效果 ```bash -# 1. 安装(macOS / Linux) -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash -# Windows (PowerShell): irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +# 1. 构建并安装(macOS / Linux) +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ # 2. 创建一个空白 PowerPoint officecli create deck.pptx @@ -206,39 +215,23 @@ officecli add deck.pptx / --type slide --prop title="Q4 Report" ## 安装 -单一自包含可执行文件,.NET 运行时已内嵌 -- 无需安装任何依赖,无需管理运行时。 +本 fork 不发布独立 release。请从源码构建: -**一键安装:** +**前置条件:** +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) ```bash -# macOS / Linux -curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash - -# Windows (PowerShell) -irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git +cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +sudo cp out/officecli /usr/local/bin/ ``` -**或手动下载** [GitHub Releases](https://github.com/iOfficeAI/OfficeCLI/releases): - -| 平台 | 文件名 | -|------|--------| -| macOS Apple Silicon | `officecli-mac-arm64` | -| macOS Intel | `officecli-mac-x64` | -| Linux x64 | `officecli-linux-x64` | -| Linux ARM64 | `officecli-linux-arm64` | -| Windows x64 | `officecli-win-x64.exe` | -| Windows ARM64 | `officecli-win-arm64.exe` | +**其他平台:** 将 `osx-arm64` 替换为 `osx-x64`、`linux-x64`、`linux-arm64`、`win-x64` 或 `win-arm64`。 验证安装:`officecli --version` -**或从已下载的二进制文件自安装(直接运行 `officecli` 也会触发安装):** - -```bash -officecli install # 显式安装 -officecli # 直接运行也会触发安装 -``` - -OfficeCLI 会在后台自动检查更新。通过 `officecli config autoUpdate false` 关闭,或通过 `OFFICECLI_SKIP_UPDATE=1` 跳过单次检查。配置文件位于 `~/.officecli/config.json`。 +> **提示:** 如需上游原始 release(含预编译二进制),请见 [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI#安装)。 ## 核心功能 From 9dc782588b769085ca8cbd4072e4ecb992b2b63f Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Wed, 27 May 2026 16:21:54 +0800 Subject: [PATCH 09/10] docs: add AGENTS.md for AI agent onboarding --- AGENTS.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..2a717d3e5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# AGENTS.md — OfficeCLI Fork (AionUI-optimized) + +> **For AI agents** working with this repository. This is a **fork** of [iOfficeAI/OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) optimized for [AionUI](https://github.com/iOfficeAI/AionUi) preview with enhanced Track Changes / revision mode support. + +## Quick start (if user asked you to install this project) + +```bash +# 1. Build from source +git clone https://github.com/NextDoorLaoHuang-HF/OfficeCLI.git && cd OfficeCLI +dotnet publish src/officecli/officecli.csproj -c Release -r osx-arm64 --self-contained -o out +# Other platforms: replace osx-arm64 with osx-x64, linux-x64, linux-arm64, win-x64, or win-arm64 + +# 2. Install to PATH +sudo cp out/officecli /usr/local/bin/ # macOS/Linux +# or: copy out/officecli.exe %LOCALAPPDATA%\OfficeCli\ (Windows) + +# 3. Verify +officecli --version +``` + +## Install skills to AI agents + +```bash +officecli install # install base skill to all detected agents +officecli skills install word # install docx-specific skill +``` + +## AionUI integration (Track Changes / 修订模式) + +If the user uses **AionUI** and wants revision/track-changes capabilities: + +```bash +officecli setup-aionui +``` + +This does two things: +1. Installs the `officecli-track-changes` skill into AionUI's skills directory +2. Registers the **Word 修订助手** assistant preset in AionUI's configuration + +After running, tell the user to **restart AionUI** — the "Word 修订助手" will appear in the assistant list, pre-configured with both `officecli-docx` and `officecli-track-changes` skills. + +The `setup-aionui` command is **idempotent** — safe to run multiple times. + +### Track Changes workflow after setup + +```bash +# Preferred: find + revision (one command) +officecli set "$FILE" /body --prop find="old" --prop replace="new" --prop revision.author=AI + +# Verify +officecli query "$FILE" revision + +# Accept/reject +officecli set "$FILE" /revision --prop revision.action=accept +``` + +## Key differences from upstream + +| Feature | Upstream | This fork | +|---------|----------|-----------| +| Binary distribution | Prebuilt releases | Build from source | +| AionUI assistant | Word Creator (generic) | + Word 修订助手 (revision-focused) | +| HTML preview | Standard | Enhanced: 7 CSS classes for revision marks | +| Watch API | Standard | + /api/revision/accept\|reject\|count endpoints | +| `setup-aionui` command | None | ✅ One-shot AionUI integration | +| `officecli-track-changes` skill | None | ✅ Embedded in binary | + +## Important files + +- `SKILL.md` — base skill file (embedded in binary, installed by `officecli install`) +- `skills/officecli-docx/SKILL.md` — docx-specific skill +- `skills/officecli-track-changes/SKILL.md` — track changes / revision mode skill +- `src/officecli/Core/AionuiInstaller.cs` — `setup-aionui` implementation +- `src/officecli/Core/SkillInstaller.cs` — skill installation to AI agents From be59f3fde936fb76dcfe09adeaf3d2d9668deb81 Mon Sep 17 00:00:00 2001 From: LaoHuang <845658924@qq.com> Date: Wed, 27 May 2026 20:13:41 +0800 Subject: [PATCH 10/10] docs(skill): add preview-after-modifications guidance for agents - Show visual preview after revisions when client supports web pages - AionUI: no action (watch server already shows live preview) - CLI-only: fall back to text summary via query revision --- skills/officecli-track-changes/SKILL.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/skills/officecli-track-changes/SKILL.md b/skills/officecli-track-changes/SKILL.md index ea1b10563..3f4eb368b 100644 --- a/skills/officecli-track-changes/SKILL.md +++ b/skills/officecli-track-changes/SKILL.md @@ -89,7 +89,22 @@ officecli query "$FILE" revision # list all revisions officecli query "$FILE" revision --json # JSON output ``` -**Do NOT use `view html` in AionUI** — AionUI already has a live preview via `officecli watch`. Use `query revision` to verify. +## Preview after modifications + +After completing revisions, **show the user a visual preview** so they can see revision marks rendered (red strikethrough, green underline, yellow highlight). Choose the right method for your environment: + +**If your client supports displaying web pages** (browser tool, webview, iframe): +```bash +officecli view "$FILE" html -o /tmp/preview.html +# Then open /tmp/preview.html in your client's browser tool +``` + +**If the user is in AionUI**, the preview panel already shows live updates via `officecli watch` — no action needed. **Do NOT call `view html` in AionUI** (redundant — opens a second tab). + +**If the user is in a CLI-only environment**, fall back to text summary: +```bash +officecli query "$FILE" revision # show revision list as text +``` ## Contract review pattern