节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
7.41 KiB
Stardust
using System.Text.Json;
using NewLife;
using Stardust.Data;
using Stardust.Data.Configs;
using Stardust.Server.Services;

using Stardust.Data.Platform;

namespace Stardust.Web.Mcp.Actions.Config;

/// <summary>获取应用配置。通过ConfigService.GetConfigs合并三层配置(本应用+共享+全局),返回键值字典</summary>
public class ConfigGetAction : McpActionBase
{
    private readonly ConfigService _configService;

    /// <summary>构造函数注入ConfigService</summary>
    public ConfigGetAction(ConfigService configService) => _configService = configService;

    /// <summary>动作名</summary>
    public override String Name => "config_get";

    /// <summary>动作描述</summary>
    public override String Description => "获取指定应用的配置字典(合并本应用+共享应用+全局应用三层配置,解析内嵌引用)。需要Token已授权该应用。";

    /// <summary>所属模块</summary>
    public override McpModuleType Module => McpModuleType.Config;

    /// <summary>所需资源授权。框架层校验app_id在授权范围</summary>
    public override ResourceRequirement? RequiredResource => new()
    {
        Type = McpResourceType.App.ToWireName(),
        Field = "app_id",
    };

    /// <summary>输入参数JSON Schema</summary>
    public override JsonElement InputSchema
    {
        get
        {
            var json = """
            {
              "type": "object",
              "properties": {
                "app_id": {"type": "integer", "description": "应用ID"},
                "scope": {"type": "string", "description": "可选,作用域(如 production/staging),不传返回所有作用域"}
              },
              "required": ["app_id"]
            }
            """;
            using var doc = JsonDocument.Parse(json);
            return doc.RootElement.Clone();
        }
    }

    /// <summary>调用动作</summary>
    public override Task<Object> InvokeAsync(JsonElement @params, McpContext context)
    {
        var appId = GetInt32(@params, "app_id");
        if (appId <= 0) throw new McpException(-32602, "Invalid params: app_id must be a positive integer");

        var scope = GetString(@params, "scope");

        // 查找应用配置(AppConfig.FindByAppId 按 App.Id 查找)
        var appConfig = AppConfig.FindByAppId(appId);
        if (appConfig == null) throw new McpException(-32601, $"AppConfig not found for app_id={appId}");
        if (!appConfig.Enable) throw new McpException(-32603, $"AppConfig is disabled: app_id={appId}");

        // 获取合并后的配置字典
        var configs = _configService.GetConfigs(appConfig, scope);

        // 转换为记录列表(便于序列化和分页)
        var records = configs
            .Where(kv => !kv.Key.IsNullOrEmpty() && !kv.Key.StartsWith("_"))
            .Select(kv => new { key = kv.Key, value = kv.Value })
            .OrderBy(r => r.key)
            .ToList();

        return Task.FromResult<Object>(new
        {
            app_id = appId,
            app_name = appConfig.Name,
            scope,
            version = appConfig.Version,
            total = records.Count,
            records,
        });
    }
}

/// <summary>设置应用配置。批量写入配置键值对(不会自动发布,需另行调用config_publish或等待定时发布)</summary>
public class ConfigSetAction : McpActionBase
{
    private readonly ConfigService _configService;

    /// <summary>构造函数注入ConfigService</summary>
    public ConfigSetAction(ConfigService configService) => _configService = configService;

    /// <summary>动作名</summary>
    public override String Name => "config_set";

    /// <summary>动作描述</summary>
    public override String Description => "批量设置应用配置键值对(支持value+可选comment)。注意:不会自动发布,需另行触发发布。需要Token已授权该应用。";

    /// <summary>所属模块</summary>
    public override McpModuleType Module => McpModuleType.Config;

    /// <summary>所需资源授权。框架层校验app_id在授权范围</summary>
    public override ResourceRequirement? RequiredResource => new()
    {
        Type = McpResourceType.App.ToWireName(),
        Field = "app_id",
    };

    /// <summary>输入参数JSON Schema</summary>
    public override JsonElement InputSchema
    {
        get
        {
            var json = """
            {
              "type": "object",
              "properties": {
                "app_id": {"type": "integer", "description": "应用ID"},
                "configs": {
                  "type": "object",
                  "description": "配置字典。key为配置键,value可以是字符串(直接作为Value)或对象(含value和comment字段)",
                  "additionalProperties": true
                },
                "publish": {"type": "boolean", "description": "可选,是否立即发布,默认false"}
              },
              "required": ["app_id", "configs"]
            }
            """;
            using var doc = JsonDocument.Parse(json);
            return doc.RootElement.Clone();
        }
    }

    /// <summary>调用动作</summary>
    public override async Task<Object> InvokeAsync(JsonElement @params, McpContext context)
    {
        var appId = GetInt32(@params, "app_id");
        if (appId <= 0) throw new McpException(-32602, "Invalid params: app_id must be a positive integer");

        if (!@params.TryGetProperty("configs", out var configsEl) || configsEl.ValueKind != JsonValueKind.Object)
            throw new McpException(-32602, "Invalid params: configs must be an object");

        var publish = @params.TryGetProperty("publish", out var pubEl) && pubEl.ValueKind == JsonValueKind.True;

        // 查找应用配置
        var appConfig = AppConfig.FindByAppId(appId);
        if (appConfig == null) throw new McpException(-32601, $"AppConfig not found for app_id={appId}");
        if (!appConfig.Enable) throw new McpException(-32603, $"AppConfig is disabled: app_id={appId}");
        if (appConfig.Readonly) throw new McpException(-32603, $"AppConfig is readonly: app_id={appId}");

        // 构造配置字典(ConfigService.SetConfigs 接受 IDictionary<String, Object>)
        // 直接传 JsonElement(SetConfigs 内部已处理 JsonElement 对象形式)
        var configs = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
        foreach (var prop in configsEl.EnumerateObject())
        {
            if (prop.Name.IsNullOrEmpty()) continue;
            // 字符串值直接用,对象值原样传 JsonElement(SetConfigs 内部会解析 Value/Comment)
            if (prop.Value.ValueKind == JsonValueKind.String)
                configs[prop.Name] = prop.Value.GetString() ?? String.Empty;
            else
                configs[prop.Name] = prop.Value.Clone();
        }

        if (configs.Count == 0)
            throw new McpException(-32602, "Invalid params: configs is empty");

        // 写入配置
        var count = _configService.SetConfigs(appConfig, configs);

        // 可选:立即发布
        Int32? publishResult = null;
        if (publish)
        {
            publishResult = await _configService.Publish(appConfig.AppId);
        }

        return new
        {
            app_id = appId,
            app_name = appConfig.Name,
            written_count = count,
            published = publish,
            publish_version = publishResult,
        };
    }
}