Files
iofficeai--officecli/src/officecli/Core/BatchExecutor.cs
T
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Has been cancelled
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (windows-latest) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/sdk) (push) Has been cancelled
Publish SDK (npm) / publish (officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (macos-latest) (push) Has been cancelled
SDK smoke / smoke (ubuntu-latest) (push) Has been cancelled
Skill parity / diff (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

124 lines
6.3 KiB
C#

// Copyright 2026 OfficeCLI (https://OfficeCLI.AI)
// SPDX-License-Identifier: Apache-2.0
using System.Text.Json;
namespace OfficeCli.Core;
/// <summary>
/// Public entry point for running a batch against an already-open
/// <see cref="IDocumentHandler"/> without going through a CLI process or
/// resident pipe — for embedders that link officecli directly (in-process
/// hosts). Reuses the same batch-item dispatch and JSON
/// envelope shape as the CLI `batch` command and the Node/Python SDKs, so a
/// caller sees one consistent protocol regardless of transport.
/// </summary>
public static class BatchExecutor
{
/// <summary>
/// Runs a batch and returns EXACTLY what the CLI `batch` command writes to
/// stdout, so an in-process host is byte-identical to the CLI (and to
/// what the watch server relays): with <paramref name="json"/> the
/// `{success, data:{results, summary}}` envelope, without it the
/// `[i] ...\n\nBatch complete: ...` text. This is the same output path the
/// CLI command uses (RunNonResidentBatch → PrintBatchResults → WrapEnvelope,
/// see CommandBuilder.Batch.cs); the earlier shortcut here emitted a bare
/// results array with `json` hardcoded, diverging from every other transport.
/// </summary>
/// <param name="itemsJson">A JSON array of batch items — the same shape documented
/// for the CLI `batch` command and the `BatchItem` SDK type.</param>
/// <param name="json">Mirrors the CLI `--json` toggle: structured envelope vs plain text.</param>
public static string ExecuteBatch(IDocumentHandler handler, string itemsJson, bool json, bool stopOnError = false)
{
try
{
var items = JsonSerializer.Deserialize(itemsJson, BatchJsonContext.Default.ListBatchItem) ?? new List<BatchItem>();
var unrecognizedLatex = new List<string>();
var results = CommandBuilder.RunNonResidentBatch(handler, items, stopOnError, json, unrecognizedLatex);
// Reuse the CLI's own body formatter so both modes are byte-identical.
using var sw = new System.IO.StringWriter();
CommandBuilder.PrintBatchResults(results, json, items.Count, sw);
var inner = sw.ToString().TrimEnd('\n', '\r');
if (!json) return inner;
// Same outer envelope the CLI batch command applies: unrecognized-LaTeX
// warnings + outer success true only when every step succeeded
// (CommandBuilder.Batch.cs). Judgment command — a single failed step
// flips outer success to false.
var warnings = new List<CliWarning>();
foreach (var tok in unrecognizedLatex)
warnings.Add(new CliWarning
{
Message = $"unrecognized_latex_command: {tok}",
Code = "unrecognized_latex_command",
Suggestion = "Check the command spelling; see https://katex.org/docs/supported.html for supported syntax.",
});
var success = results.Count == 0 || !results.Any(r => !r.Success);
return OutputFormatter.WrapEnvelope(inner, warnings, success);
}
catch (Exception ex)
{
return RenderTopLevelError(ex, json);
}
}
/// <summary>
/// Mirrors the SDKs' <c>send(item)</c> (as distinct from <c>batch(items)</c>):
/// runs ONE batch-shaped item and returns EXACTLY what the CLI single command
/// writes to stdout — with <paramref name="json"/> the standalone command's
/// envelope (WrapEnvelopeText's `{success,data,message}` for text commands
/// like set/add/remove, WrapEnvelope's `{success,data}` for data commands like
/// get/query), without it the plain text. A failure throws instead of being
/// captured per-item, matching the standalone command's exit contract.
/// </summary>
/// <param name="itemJson">A single batch item object — the same shape as one
/// entry in <see cref="ExecuteBatch"/>'s array, or the SDKs' `send(item)` argument.</param>
/// <param name="json">Mirrors the CLI `--json` toggle: structured envelope vs plain text.</param>
public static string ExecuteSend(IDocumentHandler handler, string itemJson, bool json)
{
try
{
var item = JsonSerializer.Deserialize(itemJson, BatchJsonContext.Default.BatchItem)
?? throw new ArgumentException("send: empty item");
var inner = CommandBuilder.ExecuteBatchItem(handler, item, json);
if (!json) return inner;
// Match the standalone command's envelope by inner shape — a JSON payload
// (get/query/view/validate/…) wraps with WrapEnvelope; a plain-text
// message (set/add/remove/…) wraps with WrapEnvelopeText (which adds the
// `message` field the CLI emits). Content-based so a new command inherits
// the right envelope without a hand-kept command→envelope map.
var trimmed = inner.TrimStart();
return trimmed.StartsWith('{') || trimmed.StartsWith('[')
? OutputFormatter.WrapEnvelope(inner)
: OutputFormatter.WrapEnvelopeText(inner);
}
catch (Exception ex)
{
return RenderTopLevelError(ex, json);
}
}
/// <summary>
/// Mirror of the CLI's shared <c>WriteError(ex, json)</c> (CommandBuilder):
/// in JSON mode a failed command puts the <c>{success:false, error:{…}}</c>
/// envelope on stdout — return it byte-identically. In text mode the CLI
/// writes to stderr and leaves stdout empty; with no stderr channel in-process
/// we surface the error by propagating it (the embedder host's failure
/// signal), matching the prior throw-on-failure contract.
/// </summary>
private static string RenderTopLevelError(Exception ex, bool json)
{
// CONSISTENCY(error-wrap): same friendlier rendering the CLI applies to a
// bare XmlException from an externally-corrupted OOXML part.
var rendered = ex is System.Xml.XmlException xe
? new System.IO.InvalidDataException(
$"Malformed XML in document part: {xe.Message} " +
$"(the file appears to have a corrupted OOXML part).", xe)
: ex;
if (json) return OutputFormatter.WrapErrorEnvelope(rendered);
throw rendered;
}
}