初始提交
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MAF1.PluginContract;
|
||||
|
||||
public sealed class PluginManifest
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Version { get; set; } = "1.0.0";
|
||||
public PluginLaunch Launch { get; set; } = new();
|
||||
public int TimeoutSeconds { get; set; }
|
||||
public Dictionary<string, string> Env { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public List<PluginCredentialNeed> Credentials { get; set; } = [];
|
||||
public List<PluginPort> Inputs { get; set; } = [];
|
||||
public List<PluginPort> Outputs { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class PluginLaunch
|
||||
{
|
||||
public string Command { get; set; } = "";
|
||||
public List<string> Args { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class PluginPort
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Type { get; set; } = "string";
|
||||
public string Description { get; set; } = "";
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PluginCredentialNeed
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public bool Required { get; set; } = true;
|
||||
public string Description { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class PluginRequest
|
||||
{
|
||||
public Dictionary<string, object?> Inputs { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[JsonPropertyName("credentials")]
|
||||
public Dictionary<string, PluginCredentialPayload> Credentials { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class PluginCredentialPayload
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public string? Endpoint { get; set; }
|
||||
public string? ApiKey { get; set; }
|
||||
public string? Model { get; set; }
|
||||
public Dictionary<string, string> Extra { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static class PluginJson
|
||||
{
|
||||
public static readonly System.Text.Json.JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace MAF1.PluginContract;
|
||||
|
||||
public static class PluginStdio
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary<string, PluginCredentialPayload> credentials)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> extra in cred.Extra)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(extra.Value))
|
||||
{
|
||||
Environment.SetEnvironmentVariable(extra.Key, extra.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<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(string name, string? value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user