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); } /// /// 把凭据写进环境变量。不传 target 时改当前进程(插件自己);宿主传入 ProcessStartInfo.Environment。 /// public static void ApplyCredentialsToEnvironment( IReadOnlyDictionary credentials, IDictionary? target = null) { foreach (KeyValuePair pair in credentials) { ApplyOne(pair.Key, pair.Value, target); } } private static void ApplyOne(string name, PluginCredentialPayload cred, IDictionary? target) { if (PluginCredentialTypes.IsLlm(cred.Type) || name.Equals("llm", StringComparison.OrdinalIgnoreCase)) { SetIfNotEmpty(target, "OPENAI_ENDPOINT", cred.Endpoint); SetIfNotEmpty(target, "OPENAI_API_KEY", cred.ApiKey); SetIfNotEmpty(target, "OPENAI_CHAT_MODEL", cred.Model); } if (PluginCredentialTypes.IsOpenWeather(cred.Type) || name.Equals("weather", StringComparison.OrdinalIgnoreCase)) { SetIfNotEmpty(target, "OPENWEATHER_API_KEY", cred.ApiKey); } foreach (KeyValuePair extra in cred.Extra) { if (!string.IsNullOrWhiteSpace(extra.Value)) { SetIfNotEmpty(target, extra.Key, extra.Value); } } } /// /// 只读插件自己目录的 appsettings.json(工作目录或 dll 旁),再加上环境变量。不要去读宿主 exe 目录。 /// public static IConfiguration LoadPluginConfiguration() { string basePath = Directory.GetCurrentDirectory(); if (!File.Exists(Path.Combine(basePath, "appsettings.json"))) { basePath = AppContext.BaseDirectory; } return new ConfigurationBuilder() .SetBasePath(basePath) .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(IDictionary? target, string name, string? value) { if (string.IsNullOrWhiteSpace(value)) { return; } if (target is not null) { target[name] = value; return; } Environment.SetEnvironmentVariable(name, value); } }