进程外插件改为只读环境变量和自身配置,避免读宿主 appsettings;宿主按 plugin.json 声明注入 LLM / OpenWeather 等凭据。
164 lines
5.6 KiB
C#
164 lines
5.6 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace MAF1.PluginContract;
|
|
|
|
/// <summary>
|
|
/// 进程外插件的 stdio 协议:宿主把 PluginRequest JSON 写入 stdin,插件把输出 JSON 写到 stdout。
|
|
/// 学习时对照 PluginProcessRunner:那边启动进程、写 stdin、读 stdout。
|
|
/// </summary>
|
|
public static class PluginStdio
|
|
{
|
|
/// <summary>插件入口第一步:读完 stdin 再干活。宿主写完会关闭 stdin。</summary>
|
|
public static async Task<PluginRequest> 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<PluginRequest>(json, PluginJson.Options);
|
|
return request ?? new PluginRequest();
|
|
}
|
|
|
|
/// <summary>把 Outputs 写成一行 JSON。不要往 stdout 打日志,日志请走 stderr。</summary>
|
|
public static async Task WriteOutputsAsync(Dictionary<string, object?> outputs, CancellationToken cancellationToken = default)
|
|
{
|
|
string json = JsonSerializer.Serialize(outputs, PluginJson.Options);
|
|
await Console.Out.WriteAsync(json.AsMemory(), cancellationToken);
|
|
await Console.Out.FlushAsync(cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把凭据写进环境变量。不传 target 时改当前进程(插件自己);宿主传入 ProcessStartInfo.Environment。
|
|
/// </summary>
|
|
public static void ApplyCredentialsToEnvironment(
|
|
IReadOnlyDictionary<string, PluginCredentialPayload> credentials,
|
|
IDictionary<string, string?>? target = null)
|
|
{
|
|
foreach (KeyValuePair<string, PluginCredentialPayload> pair in credentials)
|
|
{
|
|
ApplyOne(pair.Key, pair.Value, target);
|
|
}
|
|
}
|
|
|
|
private static void ApplyOne(string name, PluginCredentialPayload cred, IDictionary<string, string?>? 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<string, string> extra in cred.Extra)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(extra.Value))
|
|
{
|
|
SetIfNotEmpty(target, extra.Key, extra.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 只读插件自己目录的 appsettings.json(工作目录或 dll 旁),再加上环境变量。不要去读宿主 exe 目录。
|
|
/// </summary>
|
|
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<string, object?> 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<string> ReadStringList(Dictionary<string, object?> 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<string> typed)
|
|
{
|
|
return typed.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToList();
|
|
}
|
|
|
|
return Split(value.ToString());
|
|
}
|
|
|
|
private static List<string> Split(string? text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return text.Split(['、', ',', ';', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.ToList();
|
|
}
|
|
|
|
private static void SetIfNotEmpty(IDictionary<string, string?>? target, string name, string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (target is not null)
|
|
{
|
|
target[name] = value;
|
|
return;
|
|
}
|
|
|
|
Environment.SetEnvironmentVariable(name, value);
|
|
}
|
|
}
|