using MAF1.PluginContract; using MAF1.Utils; using Microsoft.Extensions.Configuration; namespace MAF1.Plugins; /// 内存中的完整凭据。只给插件进程用,不要序列化给浏览器。 public sealed class CredentialRecord { public string Id { get; init; } = ""; public string Name { get; init; } = ""; public string Type { get; init; } = ""; public string Endpoint { get; init; } = ""; public string ApiKey { get; init; } = ""; public string Model { get; init; } = ""; public Dictionary Extra { get; init; } = new(StringComparer.OrdinalIgnoreCase); } /// GET /api/credentials 的形状:有没有 Key 用布尔值表示,不返回 Key 本身。 public sealed class CredentialPublicView { public string Id { get; init; } = ""; public string Name { get; init; } = ""; public string Type { get; init; } = ""; public string Endpoint { get; init; } = ""; public string Model { get; init; } = ""; public bool HasApiKey { get; init; } } /// /// 从 Llm 段和 Credentials 段组装凭据。节点 config.credentialId 默认 llm-default。 /// public sealed class CredentialStore { private readonly Dictionary _named = new(StringComparer.OrdinalIgnoreCase); public CredentialStore(IConfiguration config) { LlmOptions llm = AgentFactory.Load(config); _named["llm-default"] = new CredentialRecord { Id = "llm-default", Name = "系统默认模型", Type = PluginCredentialTypes.OpenAiCompatible, Endpoint = llm.Endpoint, ApiKey = llm.ApiKey, Model = llm.Model, }; IConfigurationSection section = config.GetSection("Credentials"); foreach (IConfigurationSection child in section.GetChildren()) { string id = child.Key; string type = child["type"] ?? PluginCredentialTypes.OpenAiCompatible; bool useLlm = string.Equals(child["source"], "llm-section", StringComparison.OrdinalIgnoreCase) || child.GetValue("useLlmSection", false); CredentialRecord fallback = _named["llm-default"]; Dictionary extra = new(StringComparer.OrdinalIgnoreCase); foreach (IConfigurationSection extraChild in child.GetSection("extra").GetChildren()) { if (!string.IsNullOrWhiteSpace(extraChild.Value)) { extra[extraChild.Key] = extraChild.Value; } } string apiKey = First( child["apiKey"], useLlm ? fallback.ApiKey : null, PluginCredentialTypes.IsOpenWeather(type) ? Environment.GetEnvironmentVariable("OPENWEATHER_API_KEY") : null); _named[id] = new CredentialRecord { Id = id, Name = child["name"] ?? id, Type = type, Endpoint = First(child["endpoint"], useLlm ? fallback.Endpoint : null), ApiKey = apiKey, Model = First(child["model"], useLlm ? fallback.Model : null), Extra = extra, }; } } /// 给前端的列表。ApiKey 绝不会出现在这里。 public IReadOnlyList ListPublic() => _named.Values .OrderBy(item => item.Id, StringComparer.OrdinalIgnoreCase) .Select(ToPublic) .ToList(); public CredentialRecord Resolve(string? id) { if (string.IsNullOrWhiteSpace(id)) { return _named["llm-default"]; } if (_named.TryGetValue(id, out CredentialRecord? record)) { return record; } throw new InvalidOperationException($"找不到凭据 `{id}`。请在 appsettings.json 的 Credentials 中配置,或改用 llm-default。"); } /// 按插件声明的 type 取凭据。preferredId 类型不匹配时不会凑合用。 public CredentialRecord? TryResolveForType(string declaredType, string? preferredId) { if (!string.IsNullOrWhiteSpace(preferredId) && _named.TryGetValue(preferredId, out CredentialRecord? named)) { if (!PluginCredentialTypes.Matches(declaredType, named.Type)) { throw new InvalidOperationException( $"凭据 `{preferredId}` 的类型是 `{named.Type}`,插件声明需要 `{declaredType}`。"); } return named; } return _named.Values.FirstOrDefault(item => PluginCredentialTypes.Matches(declaredType, item.Type)); } public PluginCredentialPayload ToPayload(CredentialRecord record) => new() { Id = record.Id, Type = record.Type, Endpoint = record.Endpoint, ApiKey = record.ApiKey, Model = record.Model, Extra = record.Extra, }; public static CredentialPublicView ToPublic(CredentialRecord record) => new() { Id = record.Id, Name = record.Name, Type = record.Type, Endpoint = MaskEndpoint(record.Endpoint), Model = record.Model, HasApiKey = !string.IsNullOrWhiteSpace(record.ApiKey), }; private static string MaskEndpoint(string endpoint) => endpoint; private static string First(params string?[] values) => values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? ""; }