feat: 将插件 stdin 协议定为 v1,并按声明注入凭据

进程外插件改为只读环境变量和自身配置,避免读宿主 appsettings;宿主按 plugin.json 声明注入 LLM / OpenWeather 等凭据。
This commit is contained in:
2026-09-11 10:46:52 +08:00
parent d1b979f2db
commit b631acd42e
22 changed files with 377 additions and 95 deletions
+44 -34
View File
@@ -33,53 +33,55 @@ public static class PluginStdio
await Console.Out.FlushAsync(cancellationToken);
}
/// <summary>把宿主注入的 LLM 凭据写进环境变量,这样 AgentFactory.Load 能读到 Key。</summary>
public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary<string, PluginCredentialPayload> credentials)
/// <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)
{
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);
}
ApplyOne(pair.Key, pair.Value, target);
}
}
foreach (KeyValuePair<string, string> extra in cred.Extra)
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))
{
if (!string.IsNullOrWhiteSpace(extra.Value))
{
Environment.SetEnvironmentVariable(extra.Key, extra.Value);
}
SetIfNotEmpty(target, extra.Key, extra.Value);
}
}
}
/// <summary>
/// 插件自己读天气等配置时用。优先 MAF1_CONTENT_ROOT(宿主 exe 目录),否则向上找 appsettings.json
/// 只读插件自己目录的 appsettings.json(工作目录或 dll 旁),再加上环境变量。不要去读宿主 exe 目录
/// </summary>
public static IConfiguration LoadHostConfiguration()
public static IConfiguration LoadPluginConfiguration()
{
string? contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
if (string.IsNullOrWhiteSpace(contentRoot) || !Directory.Exists(contentRoot))
string basePath = Directory.GetCurrentDirectory();
if (!File.Exists(Path.Combine(basePath, "appsettings.json")))
{
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;
}
basePath = AppContext.BaseDirectory;
}
return new ConfigurationBuilder()
.SetBasePath(contentRoot)
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
@@ -143,11 +145,19 @@ public static class PluginStdio
.ToList();
}
private static void SetIfNotEmpty(string name, string? value)
private static void SetIfNotEmpty(IDictionary<string, string?>? target, string name, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
if (string.IsNullOrWhiteSpace(value))
{
Environment.SetEnvironmentVariable(name, value);
return;
}
if (target is not null)
{
target[name] = value;
return;
}
Environment.SetEnvironmentVariable(name, value);
}
}