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

namespace Stardust.Mcp.Tools.Deploy;

static class DeployLookup
{
    public static AppDeploy ResolveDeploy(JsonElement args, McpContext ctx)
    {
        if (args.TryGetProperty("deploy_id", out var deployId) && deployId.ValueKind == JsonValueKind.Number && deployId.GetInt32() > 0)
            return McpGuard.Deploy(ctx, deployId.GetInt32());
        return McpGuard.DeployByApp(ctx, McpGuard.FindApp(ctx, McpToolBase.ReadInt(args, "app_id"), McpToolBase.ReadString(args, "app_name")).Id);
    }
}

public class DeployStatusTool : McpToolBase
{
    public override String Name => "stardust_deploy_status";
    public override String Description => "Answer whether a release landed: current version, nodes behind the target, process liveness and recent dispatch records.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            deploy_id = new { type = "integer", description = "部署集编号,与 app_id 二选一" },
            app_id = new { type = "integer", description = "应用编号,与 deploy_id 二选一" },
        },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var deploy = args.TryGetProperty("deploy_id", out _) ? McpGuard.Deploy(ctx, GetInt32(args, "deploy_id")) : McpGuard.DeployByApp(ctx, GetInt32(args, "app_id"));
        var nodes = AppDeployNode.FindAllByDeployId(deploy.Id);
        var behind = nodes.Where(e => e.Enable && e.Version != deploy.Version).Select(e => new { e.Id, e.NodeId, e.IP, current = e.Version, target = deploy.Version }).ToArray();
        var alive = nodes.Count(e => e.ProcessId > 0);
        return Task.FromResult<Object>(McpResult.Ok($"{deploy.Name} 当前版本 {deploy.Version},{behind.Length} 台落后,{alive} 台进程存活", new
        {
            deploy_id = deploy.Id,
            version = deploy.Version,
            auto_publish = deploy.AutoPublish,
            behind,
            alive,
        }, ["stardust_deploy_node_operate", "stardust_deploy_history"]));
    }
}

public class DeployVersionsTool : McpToolBase
{
    public override String Name => "stardust_deploy_versions";
    public override String Description => "List deployment versions with enable state, package size, commit and usability.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            deploy_id = new { type = "integer", description = "部署集编号" },
            app_id = new { type = "integer", description = "应用编号" },
            app_name = new { type = "string", description = "应用名" },
        },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var deploy = DeployLookup.ResolveDeploy(args, ctx);
        // 与 MVC/API AppDeployVersionController.Search 同一重载。不传时间与关键字,取出该部署集全部版本后再按编号倒序裁剪。
        var rows = AppDeployVersion.Search(deploy.Id, null, null, DateTime.MinValue, DateTime.MinValue, null, null).OrderByDescending(e => e.Id)
            .Select(e => new { e.Id, e.Version, e.Enable, e.Size, e.CommitId, commit = e.CommitLog, usable = e.Enable && !e.Url.IsNullOrEmpty() }).ToArray();
        return Task.FromResult<Object>(McpResult.Ok($"{deploy.Name} 版本 {rows.Length} 个", new { records = rows }));
    }
}

public class DeployHistoryTool : McpToolBase
{
    public override String Name => "stardust_deploy_history";
    public override String Description => "List deployment dispatch logs for an application or deployment set. Pass app_id, app_name or deploy_id. Success means the command was submitted, not that the node finished it.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            deploy_id = new { type = "integer", description = "部署集编号" },
            app_id = new { type = "integer", description = "应用编号" },
            app_name = new { type = "string", description = "应用名" },
            since = new { type = "string", description = "时间窗,如 -24h" },
            page_size = new { type = "integer", description = "每页条数" },
        },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var deploy = DeployLookup.ResolveDeploy(args, ctx);
        // 与 MVC/API AppDeployHistoryController.Search 同一重载。since 作为开始时间,结束留空;分页在查询内完成。
        var page = new PageParameter
        {
            PageIndex = Page(args),
            PageSize = PageSize(args),
            Sort = AppDeployHistory._.Id,
            Desc = true,
            RetrieveTotalCount = true,
        };
        var list = AppDeployHistory.Search(deploy.Id, -1, null, Since(args), DateTime.MinValue, null, page);
        var total = (Int32)page.TotalCount;
        var rows = list.Select(e => new { e.Id, time = e.CreateTime, e.NodeId, e.Action, e.Success, e.Remark }).ToArray();
        return Task.FromResult<Object>(McpResult.Ok($"部署下发记录 {rows.Length}/{total} 条。成功只表示命令已下发,不是节点执行结果", new { records = rows },
            null, null, McpResult.Paged(total, page.PageIndex, page.PageSize)));
    }
}

public class PipelineRunDetailTool : McpToolBase
{
    public override String Name => "stardust_pipeline_run_detail";
    public override String Description => "Read one pipeline run with steps, commit and a warning when every step was skipped but the run is marked success.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            run_id = new { type = "integer", description = "流水线运行编号" },
        },
        required = new[] { "run_id" },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var run = AppPipelineRun.FindById(GetInt32(args, "run_id")) ?? throw new McpException(-32602, "流水线运行不存在。");
        var pipeline = AppPipeline.FindById(run.PipelineId) ?? throw new McpException(-32602, "流水线不存在。");
        McpGuard.Deploy(ctx, pipeline.DeployId);
        var steps = AppPipelineStep.FindAllByRunId(run.Id).OrderBy(e => e.StepIndex).ToList();
        var allSkipped = steps.Count > 0 && steps.All(e => e.Status.EqualIgnoreCase("Skipped"));
        return Task.FromResult<Object>(McpResult.Ok(allSkipped ? "该运行标记成功,但所有步骤都被跳过,没有节点实际部署" : $"运行 {run.Id} 状态 {run.Status}", new
        {
            run.Id,
            status = run.Status.ToString(),
            run.CommitId,
            run.CommitMessage,
            all_skipped = allSkipped,
            steps = steps.Select(e => new { e.StepType, e.NodeId, e.Status, e.Message }).ToArray(),
        }));
    }
}

public class UseVersionTool : McpToolBase
{
    public override String Name => "stardust_deploy_use_version";
    public override String Description => "Switch the effective deployment version. If auto publish is off, this only changes the version number and does not install nodes.";
    public override McpRiskLevel Risk => McpRiskLevel.L1;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            version_id = new { type = "integer", description = "版本编号" },
        },
        required = new[] { "version_id" },
    });

    public override async Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var ver = AppDeployVersion.FindById(GetInt32(args, "version_id")) ?? throw new McpException(-32602, "版本不存在。");
        var deploy = McpGuard.Deploy(ctx, ver.DeployId);
        var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
        var message = await ops.UseVersionAsync(ver.Id, ctx.CallerIp);
        var hint = deploy.AutoPublish ? "已开启自动发布,启用节点会收到安装命令" : "未开启自动发布,仅切换版本号,需要 stardust_deploy_node_operate 或 stardust_deploy_release 才会下发";
        return McpResult.Ok($"版本已切换为 {ver.Version}。{hint}", new { deploy_id = deploy.Id, version = ver.Version, auto_publish = deploy.AutoPublish, message },
            null, "如需回退到旧版本,重新调用 stardust_deploy_use_version 选择旧版本编号");
    }
}

public class NodeOperateTool : McpToolBase
{
    public override String Name => "stardust_deploy_node_operate";
    public override String Description => "Operate one deployment node: install, start, restart, stop or uninstall. stop and uninstall require confirm=true because they disable the node.";
    public override McpRiskLevel Risk => McpRiskLevel.L1;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            deploy_node_id = new { type = "integer", description = "部署节点编号" },
            action = new { type = "string", @enum = new[] { "install", "start", "restart", "stop", "uninstall" }, description = "节点操作" },
            confirm = new { type = "boolean", description = "stop/uninstall 必须传 true" },
        },
        required = new[] { "deploy_node_id", "action" },
    });
    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx) => Operate(args, ctx);

    internal static async Task<Object> Operate(JsonElement args, McpContext ctx)
    {
        var node = AppDeployNode.FindById(GetInt32(args, "deploy_node_id")) ?? throw new McpException(-32602, "部署节点不存在。");
        var deploy = McpGuard.Deploy(ctx, node.DeployId);
        var act = GetString(args, "action");
        if (!new[] { "install", "start", "restart", "stop", "uninstall" }.Contains(act))
            throw new McpException(-32602, "action 只能是 install/start/restart/stop/uninstall。");
        if ((act == "stop" || act == "uninstall") && !GetBoolean(args, "confirm"))
            throw new McpException(-32003, "stop/uninstall 会把部署节点 Enable 置为 false,必须传 confirm=true。");
        var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
        var id = await ops.ControlDeployAsync(deploy.Id, node.Id, act, ctx.CallerIp);
        var rollback = act is "stop" or "uninstall"
            ? "节点已被停用;如需恢复,调用 stardust_deploy_node_operate 执行 install 或 start"
            : "如需回滚此操作,调用 stardust_deploy_history 查看结果后用 stardust_deploy_node_operate 下发相反动作";
        return McpResult.Ok($"已向节点 {node.NodeId} 下发 {act}。命令已发出,结果看部署历史", new { command_id = id, node.Id, action = act },
            null, rollback);
    }
}

public class DeployReleaseTool : McpToolBase
{
    public override String Name => "stardust_deploy_release";
    public override String Description => "Release an application. Pass app_id or app_name, and version or version_id. Omit deploy_node_ids to release all enabled nodes. dry_run=true only returns the plan.";
    public override McpRiskLevel Risk => McpRiskLevel.L1;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            deploy_id = new { type = "integer", description = "部署集编号" },
            app_id = new { type = "integer", description = "应用编号" },
            app_name = new { type = "string", description = "应用名" },
            version_id = new { type = "integer", description = "版本编号,与 version 二选一" },
            version = new { type = "string", description = "版本号,与 version_id 二选一" },
            deploy_node_ids = new { type = "array", items = new { type = "integer" }, description = "目标节点编号,空表示全部启用节点" },
            dry_run = new { type = "boolean", description = "只出计划不执行" },
        },
    });

    public override async Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var deploy = args.TryGetProperty("deploy_id", out var deployId) && deployId.GetInt32() > 0
            ? McpGuard.Deploy(ctx, deployId.GetInt32())
            : McpGuard.DeployByApp(ctx, McpGuard.FindApp(ctx, GetInt32(args, "app_id"), GetString(args, "app_name")).Id);
        AppDeployVersion ver;
        if (GetInt32(args, "version_id") > 0)
            ver = AppDeployVersion.FindById(GetInt32(args, "version_id")) ?? throw new McpException(-32602, "版本不存在。");
        else
        {
            var version = GetString(args, "version");
            if (version.IsNullOrEmpty()) throw new McpException(-32602, "请传 version 或 version_id。可先调 stardust_deploy_versions。");
            ver = AppDeployVersion.FindByDeployIdAndVersion(deploy.Id, version)
                ?? throw new McpException(-32602, $"部署集里没有版本 {version}。");
        }
        if (ver.DeployId != deploy.Id) throw new McpException(-32602, "版本不属于该部署集。");
        var nodes = args.TryGetProperty("deploy_node_ids", out var idEl) && idEl.ValueKind == JsonValueKind.Array
            ? idEl.EnumerateArray().Select(e => AppDeployNode.FindById(e.GetInt32())).Where(e => e != null && e.DeployId == deploy.Id).ToArray()
            : AppDeployNode.FindAllByDeployId(deploy.Id).Where(e => e.Enable).ToArray();
        if (nodes.Length == 0) throw new McpException(-32602, "没有可发布的启用节点。");
        var plan = nodes.Select(e => new { e.Id, e.NodeId, from = e.Version, to = ver.Version }).ToArray();
        if (GetBoolean(args, "dry_run")) return McpResult.Ok($"发布计划 {plan.Length} 台,未执行", new { plan });
        var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
        // 只切版本。安装由下面按选中节点下发,避免再走一遍自动发布把启用节点装两次。
        await ops.UseVersionAsync(ver.Id, ctx.CallerIp, false);
        var results = new List<Object>();
        foreach (var node in nodes)
        {
            var id = await ops.ControlDeployAsync(deploy.Id, node.Id, "install", ctx.CallerIp);
            results.Add(new { node.Id, command_id = id, status = "submitted" });
        }
        return McpResult.Ok($"已切换到 {ver.Version} 并向 {results.Count} 台下发安装。命令已发出,结果看部署历史", new { results },
            null, "如需回滚,调用 stardust_deploy_use_version 切回旧版本号,再对目标节点下发 install");
    }
}

public class PipelineTriggerTool : McpToolBase
{
    public override String Name => "stardust_pipeline_trigger";
    public override String Description => "Manually trigger a pipeline. The result distinguishes a created run from a rejected trigger and includes the reason.";
    public override McpRiskLevel Risk => McpRiskLevel.L1;
    public override String ScopeDomain => "deploy";
    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            pipeline_id = new { type = "integer", description = "流水线编号" },
        },
        required = new[] { "pipeline_id" },
    });

    public override async Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        var pipeline = AppPipeline.FindById(GetInt32(args, "pipeline_id")) ?? throw new McpException(-32602, "流水线不存在。");
        McpGuard.Deploy(ctx, pipeline.DeployId);
        var ops = ctx.Services.GetService(typeof(IMcpOperations)) as IMcpOperations ?? throw new McpException(-32000, "宿主未注册 MCP 写操作适配器。");
        try
        {
            var run = await ops.TriggerPipelineAsync(pipeline.Id, ctx.CallerIp);
            return McpResult.Ok("流水线已创建运行", new { created = true, run },
                null, "如构建失败,调用 stardust_pipeline_run_detail 查看失败步骤,修正后重新触发");
        }
        catch (Exception ex)
        {
            return McpResult.Ok("流水线未创建运行:" + ex.Message, new { created = false, reason = ex.Message });
        }
    }
}