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

namespace Stardust.Mcp.Tools.Alarm;

public class AlarmListTool : McpToolBase
{
    public override String Name => "stardust_alarm_list";
    public override String Description => "List alarm records in the token project time window. Optional status and level. Paged.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "monitor";

    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            since = new { type = "string", description = "时间窗,如 -24h 或 ISO 时间,默认最近24小时" },
            page_size = new { type = "integer", description = "每页条数,默认20,最大100" },
        },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        McpGuard.NeedProject(ctx);
        var since = Since(args);
        var page = new PageParameter
        {
            PageIndex = Page(args),
            PageSize = PageSize(args),
            Sort = AlarmRecord._.Id,
            Desc = true,
            RetrieveTotalCount = true,
        };
        // 与 MVC/WebApi AlarmRecordController.Search 同一份。未传 groupId/category/status/关键字时与列表页缺省一致;结束时间留空表示不封顶
        var list = AlarmRecord.Search(-1, null, (AlarmStatuses)(-1), since, default, null, page, ctx.ProjectId);
        var rows = list.Select(e => new { id = e.Id, name = e.Name, category = e.Category, action = e.Action, times = e.Times, content = e.Content, start = e.StartTime }).ToArray();
        var total = (Int32)page.TotalCount;
        return Task.FromResult<Object>(McpResult.Ok($"告警 {rows.Length}/{total} 条", new { records = rows },
            ["stardust_alarm_detail"], null, McpResult.Paged(total, Page(args), PageSize(args))));
    }
}

public class AlarmDetailTool : McpToolBase
{
    public override String Name => "stardust_alarm_detail";
    public override String Description => "Read one alarm and attach the next diagnostic tools for its application.";
    public override McpRiskLevel Risk => McpRiskLevel.L0;
    public override String ScopeDomain => "monitor";

    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            alarm_id = new { type = "integer", description = "告警编号" },
        },
        required = new[] { "alarm_id" },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        McpGuard.NeedProject(ctx);
        var alarm = AlarmRecord.FindById(GetInt64(args, "alarm_id"));
        if (alarm == null) throw new McpException(-32602, "告警不存在。");
        if (!AlarmProject.Contains(alarm, ctx.ProjectId))
            throw new McpException(-32003, $"告警 {alarm.Id} 不属于当前令牌绑定的项目。");

        var app = AlarmProject.FindApp(alarm, ctx.ProjectId);
        return Task.FromResult<Object>(McpResult.Ok($"告警 {alarm.Name}:{alarm.Content}", new
        {
            id = alarm.Id,
            name = alarm.Name,
            action = alarm.Action,
            content = alarm.Content,
            times = alarm.Times,
            app_id = app?.Id,
        }, app == null ? null : ["stardust_app_health", "stardust_trace_search"]));
    }
}

public class AlarmResolveTool : McpToolBase
{
    public override String Name => "stardust_alarm_resolve";
    public override String Description => "Resolve an alarm with a required note. This records the operator decision; it does not delete the alarm.";
    public override McpRiskLevel Risk => McpRiskLevel.L1;
    public override String ScopeDomain => "monitor";

    public override JsonElement InputSchema => JsonSerializer.SerializeToElement(new
    {
        type = "object",
        properties = new
        {
            alarm_id = new { type = "integer", description = "告警编号" },
            note = new { type = "string", description = "处置意见" },
        },
        required = new[] { "alarm_id", "note" },
    });

    public override Task<Object> InvokeAsync(JsonElement args, McpContext ctx)
    {
        McpGuard.NeedProject(ctx);
        var note = GetString(args, "note");
        if (note.IsNullOrEmpty()) throw new McpException(-32602, "无效参数:处置意见 note 必填。");
        var alarm = AlarmRecord.FindById(GetInt64(args, "alarm_id"));
        if (alarm == null) throw new McpException(-32602, "告警不存在。");
        if (!AlarmProject.Contains(alarm, ctx.ProjectId))
            throw new McpException(-32003, $"告警 {alarm.Id} 不属于当前令牌绑定的项目。");

        if (!alarm.Resolve(note)) throw new McpException(-32000, $"告警 {alarm.Id} 处置失败。");
        return Task.FromResult<Object>(McpResult.Ok($"告警 {alarm.Id} 已记下处置意见", new { alarm.Id, alarm.Action },
            null, "处置意见已追加到内容,并写下结束时间。动作名未改,恢复仍按原对象名匹配。"));
    }
}

/// <summary>告警是否属于令牌项目。应用用已有 FindByName,告警组用 FindById,不扫应用表</summary>
static class AlarmProject
{
    public static Boolean Contains(AlarmRecord alarm, Int32 projectId)
    {
        if (alarm == null || projectId <= 0) return false;
        if (FindApp(alarm, projectId) != null) return true;
        if (alarm.GroupId <= 0) return false;

        var group = AlarmGroup.FindById(alarm.GroupId);
        return group != null && group.ProjectId == projectId;
    }

    public static App FindApp(AlarmRecord alarm, Int32 projectId)
    {
        if (alarm == null) return null;

        var app = Match(alarm.Name, projectId) ?? Match(Head(alarm.Action), projectId);
        return app ?? Match(alarm.Category, projectId);
    }

    static App Match(String name, Int32 projectId)
    {
        if (name.IsNullOrEmpty() || projectId <= 0) return null;

        var app = App.FindByName(name);
        return app != null && app.ProjectId == projectId ? app : null;
    }

    static String Head(String action)
    {
        if (action.IsNullOrEmpty()) return null;

        var cut = action.IndexOfAny(['/', '\\']);
        return cut > 0 ? action[..cut] : action;
    }
}