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);
}
}
}