Files
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Waiting to run
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/sdk) (push) Waiting to run
Publish SDK (npm) / publish (officecli-sdk) (push) Waiting to run
SDK smoke / smoke (windows-latest) (push) Waiting to run
SDK smoke / smoke (macos-latest) (push) Waiting to run
SDK smoke / smoke (ubuntu-latest) (push) Waiting to run
Skill parity / diff (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

283 lines
13 KiB
C#

// Copyright 2026 OfficeCLI (https://OfficeCLI.AI)
// SPDX-License-Identifier: Apache-2.0
using System.Text.Json;
using System.Text.Json.Serialization;
namespace OfficeCli;
internal class LenientStringDictionaryConverter : JsonConverter<Dictionary<string, string>>
{
public override Dictionary<string, string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// Array form: ["key=value", ...]. This mirrors the single-command MCP
// `props` argument and the CLI `--prop key=value` flag, so an agent that
// learned props from `set`/`add` produces the same shape inside a batch
// item. Before this, batch props was object-only and every array-form
// batch failed with "Expected object for props" — observed as a 100%
// batch-failure for models that (correctly) reused the single-command
// props shape. Lenient split on the first '=' matches McpServer.ParseProps.
if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray) return dict;
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("Expected \"key=value\" string in props array");
var kv = reader.GetString()!;
var eq = kv.IndexOf('=');
if (eq > 0) dict[kv[..eq]] = kv[(eq + 1)..]; // skip malformed, as ParseProps does
}
throw new JsonException("Unexpected end of JSON");
}
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Expected object or [\"key=value\"] array for props");
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject) return dict;
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException("Expected property name");
var key = reader.GetString()!;
reader.Read();
var value = reader.TokenType switch
{
JsonTokenType.String => reader.GetString()!,
JsonTokenType.Number => reader.TryGetInt64(out var l) ? l.ToString() : reader.GetDouble().ToString(),
JsonTokenType.True => "true",
JsonTokenType.False => "false",
JsonTokenType.Null => "",
_ => throw new JsonException($"Unexpected token {reader.TokenType} for prop value '{key}'")
};
dict[key] = value;
}
throw new JsonException("Unexpected end of JSON");
}
public override void Write(Utf8JsonWriter writer, Dictionary<string, string> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var kv in value)
writer.WriteString(kv.Key, kv.Value);
writer.WriteEndObject();
}
}
internal class BatchItemConverter : JsonConverter<BatchItem>
{
private static readonly LenientStringDictionaryConverter PropsConverter = new();
public override BatchItem? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException("Expected StartObject for BatchItem");
var item = new BatchItem();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject) return item;
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException("Expected PropertyName");
var prop = reader.GetString()!;
reader.Read();
switch (prop.ToLowerInvariant())
{
case "command":
case "op":
item.Command = reader.GetString() ?? "";
break;
case "path": item.Path = reader.GetString(); break;
case "parent": item.Parent = reader.GetString(); break;
case "type": item.Type = reader.GetString(); break;
case "from": item.From = reader.GetString(); break;
case "index": item.Index = reader.TokenType == JsonTokenType.Null ? null : reader.GetInt32(); break;
case "after": item.After = reader.GetString(); break;
case "before": item.Before = reader.GetString(); break;
case "to": item.To = reader.GetString(); break;
case "path2": item.Path2 = reader.GetString(); break;
case "props": item.Props = PropsConverter.Read(ref reader, typeof(Dictionary<string, string>), options); break;
case "selector": item.Selector = reader.GetString(); break;
case "text": item.Text = reader.GetString(); break;
case "mode": item.Mode = reader.GetString(); break;
case "depth": item.Depth = reader.TokenType == JsonTokenType.Null ? null : reader.GetInt32(); break;
case "part": item.Part = reader.GetString(); break;
case "xpath": item.Xpath = reader.GetString(); break;
case "action": item.Action = reader.GetString(); break;
case "xml": item.Xml = reader.GetString(); break;
default: reader.Skip(); break;
}
}
throw new JsonException("Unexpected end of JSON for BatchItem");
}
public override void Write(Utf8JsonWriter writer, BatchItem value, JsonSerializerOptions options)
{
writer.WriteStartObject();
if (!string.IsNullOrEmpty(value.Command)) writer.WriteString("command", value.Command);
if (value.Path != null) writer.WriteString("path", value.Path);
if (value.Parent != null) writer.WriteString("parent", value.Parent);
if (value.Type != null) writer.WriteString("type", value.Type);
if (value.From != null) writer.WriteString("from", value.From);
if (value.Index.HasValue) writer.WriteNumber("index", value.Index.Value);
if (value.After != null) writer.WriteString("after", value.After);
if (value.Before != null) writer.WriteString("before", value.Before);
if (value.To != null) writer.WriteString("to", value.To);
if (value.Path2 != null) writer.WriteString("path2", value.Path2);
if (value.Props != null) { writer.WritePropertyName("props"); PropsConverter.Write(writer, value.Props, options); }
if (value.Selector != null) writer.WriteString("selector", value.Selector);
if (value.Text != null) writer.WriteString("text", value.Text);
if (value.Mode != null) writer.WriteString("mode", value.Mode);
if (value.Depth.HasValue) writer.WriteNumber("depth", value.Depth.Value);
if (value.Part != null) writer.WriteString("part", value.Part);
if (value.Xpath != null) writer.WriteString("xpath", value.Xpath);
if (value.Action != null) writer.WriteString("action", value.Action);
if (value.Xml != null) writer.WriteString("xml", value.Xml);
writer.WriteEndObject();
}
}
[JsonConverter(typeof(BatchItemConverter))]
public class BatchItem
{
public string Command { get; set; } = "";
public string? Path { get; set; }
public string? Parent { get; set; }
public string? Type { get; set; }
public string? From { get; set; }
public int? Index { get; set; }
public string? After { get; set; }
public string? Before { get; set; }
public string? To { get; set; }
// swap's second element. Canonical name across the single-command MCP tool
// and the CLI (`swap path1 path2`); a batch swap that reused that name was
// previously dropped (BatchItem had no path2), so swap only worked via the
// off-name `to`. Accept both — see the swap case in ExecuteBatchItem.
public string? Path2 { get; set; }
public Dictionary<string, string>? Props { get; set; }
public string? Selector { get; set; }
public string? Text { get; set; }
public string? Mode { get; set; }
public int? Depth { get; set; }
public string? Part { get; set; }
public string? Xpath { get; set; }
public string? Action { get; set; }
public string? Xml { get; set; }
internal static readonly HashSet<string> KnownFields = new(StringComparer.OrdinalIgnoreCase)
{
"command", "op", "path", "parent", "type", "from", "index", "after", "before", "to", "path2",
"props", "selector", "text", "mode", "depth", "part", "xpath", "action", "xml"
};
public ResidentRequest ToResidentRequest()
{
var req = new ResidentRequest { Command = Command };
if (Path != null) req.Args["path"] = Path;
if (Parent != null) req.Args["parent"] = Parent;
if (Type != null) req.Args["type"] = Type;
if (From != null) req.Args["from"] = From;
if (Index.HasValue) req.Args["index"] = Index.Value.ToString();
if (After != null) req.Args["after"] = After;
if (Before != null) req.Args["before"] = Before;
if (To != null) req.Args["to"] = To;
if (Path2 != null) req.Args["path2"] = Path2;
if (Selector != null) req.Args["selector"] = Selector;
if (Text != null) req.Args["text"] = Text;
if (Mode != null) req.Args["mode"] = Mode;
if (Depth.HasValue) req.Args["depth"] = Depth.Value.ToString();
if (Part != null) req.Args["part"] = Part;
if (Xpath != null) req.Args["xpath"] = Xpath;
if (Action != null) req.Args["action"] = Action;
if (Xml != null) req.Args["xml"] = Xml;
if (Props != null)
req.Props = Props;
return req;
}
}
[JsonConverter(typeof(BatchResultConverter))]
public class BatchResult
{
public int Index { get; set; }
public bool Success { get; set; }
public string? Output { get; set; }
public string? Error { get; set; }
/// <summary>The original batch item, included when the command fails so the agent can inspect/retry.</summary>
public BatchItem? Item { get; set; }
}
/// <summary>
/// Custom converter for BatchResult that writes Output as raw JSON (not double-encoded)
/// when the Output string is valid JSON.
/// </summary>
internal class BatchResultConverter : JsonConverter<BatchResult>
{
public override BatchResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
var result = new BatchResult();
if (root.TryGetProperty("index", out var idx)) result.Index = idx.GetInt32();
if (root.TryGetProperty("success", out var suc)) result.Success = suc.GetBoolean();
if (root.TryGetProperty("output", out var outp)) result.Output = outp.ValueKind == JsonValueKind.String ? outp.GetString() : outp.GetRawText();
if (root.TryGetProperty("error", out var err)) result.Error = err.GetString();
if (root.TryGetProperty("item", out var itm)) result.Item = JsonSerializer.Deserialize(itm.GetRawText(), BatchJsonContext.Default.BatchItem);
return result;
}
public override void Write(Utf8JsonWriter writer, BatchResult value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber("index", value.Index);
writer.WriteBoolean("success", value.Success);
if (value.Output != null)
{
// If Output is valid JSON (object or array), write it as raw JSON to avoid double-encoding
if (IsJsonObjectOrArray(value.Output))
{
writer.WritePropertyName("output");
using var doc = JsonDocument.Parse(value.Output);
doc.RootElement.WriteTo(writer);
}
else
{
writer.WriteString("output", value.Output);
}
}
if (value.Error != null)
{
writer.WriteString("error", value.Error);
if (value.Item != null)
{
writer.WritePropertyName("item");
JsonSerializer.Serialize(writer, value.Item, BatchJsonContext.Default.BatchItem);
}
}
writer.WriteEndObject();
}
private static bool IsJsonObjectOrArray(string s)
{
if (string.IsNullOrWhiteSpace(s)) return false;
var trimmed = s.TrimStart();
if (trimmed.Length == 0) return false;
if (trimmed[0] != '{' && trimmed[0] != '[') return false;
try
{
using var doc = JsonDocument.Parse(s);
return doc.RootElement.ValueKind is JsonValueKind.Object or JsonValueKind.Array;
}
catch { return false; }
}
}
[JsonSourceGenerationOptions]
[JsonSerializable(typeof(BatchItem))]
[JsonSerializable(typeof(List<BatchItem>))]
[JsonSerializable(typeof(List<BatchResult>))]
internal partial class BatchJsonContext : JsonSerializerContext;