using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; namespace MAF1.PluginContract; /// /// 进程外插件的 stdio 协议:宿主把 PluginRequest JSON 写入 stdin,插件把输出 JSON 写到 stdout。 /// 学习时对照 PluginProcessRunner:那边启动进程、写 stdin、读 stdout。 /// public static class PluginStdio { /// 插件入口第一步:读完 stdin 再干活。宿主写完会关闭 stdin。 public static async Task ReadRequestAsync(CancellationToken cancellationToken = default) { using Stream stdin = Console.OpenStandardInput(); using StreamReader reader = new(stdin, Encoding.UTF8); string json = await reader.ReadToEndAsync(cancellationToken); if (string.IsNullOrWhiteSpace(json)) { throw new InvalidOperationException("插件没有从标准输入读到 JSON 请求。"); } PluginRequest? request = JsonSerializer.Deserialize(json, PluginJson.Options); return request ?? new PluginRequest(); } /// 把 Outputs 写成一行 JSON。不要往 stdout 打日志,日志请走 stderr。 public static async Task WriteOutputsAsync(Dictionary outputs, CancellationToken cancellationToken = default) { string json = JsonSerializer.Serialize(outputs, PluginJson.Options); await Console.Out.WriteAsync(json.AsMemory(), cancellationToken); await Console.Out.FlushAsync(cancellationToken); } /// 把宿主注入的 LLM 凭据写进环境变量,这样 AgentFactory.Load 能读到 Key。 public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary credentials) { foreach (KeyValuePair pair in credentials) { PluginCredentialPayload cred = pair.Value; if (cred.Type is "openai-compatible" or "llm" || pair.Key.Equals("llm", StringComparison.OrdinalIgnoreCase)) { SetIfNotEmpty("OPENAI_ENDPOINT", cred.Endpoint); SetIfNotEmpty("OPENAI_API_KEY", cred.ApiKey); SetIfNotEmpty("OPENAI_CHAT_MODEL", cred.Model); } foreach (KeyValuePair extra in cred.Extra) { if (!string.IsNullOrWhiteSpace(extra.Value)) { Environment.SetEnvironmentVariable(extra.Key, extra.Value); } } } } /// /// 插件自己读天气等配置时用。优先 MAF1_CONTENT_ROOT(宿主 exe 目录),否则向上找 appsettings.json。 /// public static IConfiguration LoadHostConfiguration() { string contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT"); if (string.IsNullOrWhiteSpace(contentRoot) || !Directory.Exists(contentRoot)) { contentRoot = AppContext.BaseDirectory; DirectoryInfo? parent = Directory.GetParent(contentRoot.TrimEnd(Path.DirectorySeparatorChar)); if (parent?.Name.Equals("plugins", StringComparison.OrdinalIgnoreCase) == false && parent?.Parent is not null && File.Exists(Path.Combine(parent.Parent.FullName, "appsettings.json"))) { contentRoot = parent.Parent.FullName; } else if (parent is not null && File.Exists(Path.Combine(parent.FullName, "appsettings.json"))) { contentRoot = parent.FullName; } } return new ConfigurationBuilder() .SetBasePath(contentRoot) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) .AddEnvironmentVariables() .Build(); } public static string? ReadString(Dictionary inputs, string key) { if (!inputs.TryGetValue(key, out object? value) || value is null) { return null; } if (value is JsonElement el) { return el.ValueKind == JsonValueKind.String ? el.GetString() : el.ToString(); } return value.ToString(); } public static List ReadStringList(Dictionary inputs, string key) { if (!inputs.TryGetValue(key, out object? value) || value is null) { return []; } if (value is JsonElement el) { if (el.ValueKind == JsonValueKind.Array) { return el.EnumerateArray() .Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() : item.ToString()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s!.Trim()) .ToList(); } if (el.ValueKind == JsonValueKind.String) { return Split(el.GetString()); } } if (value is IEnumerable typed) { return typed.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToList(); } return Split(value.ToString()); } private static List Split(string? text) { if (string.IsNullOrWhiteSpace(text)) { return []; } return text.Split(['、', ',', ';', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); } private static void SetIfNotEmpty(string name, string? value) { if (!string.IsNullOrWhiteSpace(value)) { Environment.SetEnvironmentVariable(name, value); } } }