@johnny-joster/n9e-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: query-historical-alerts
3
+ description: 查询夜莺平台历史告警事件
4
+ ---
5
+
6
+ # 查询历史告警
7
+
8
+ ## 何时使用
9
+
10
+ 当用户询问过去某段时间的告警记录时使用。典型场景:
11
+
12
+ - "昨天有哪些告警"
13
+ - "过去一周的告警趋势"
14
+ - "上个月的告警统计"
15
+ - "查看历史告警记录"
16
+ - "某个时间段发生了什么告警"
17
+
18
+ ## 使用方法
19
+
20
+ 调用 `n9e_query_historical_alerts` 工具,根据用户提及的时间范围设置参数。
21
+
22
+ ### 参数说明
23
+
24
+ - **severity** (可选): 严重级别 (1=严重, 2=警告, 3=提示)
25
+ - **rule_name** (可选): 规则名称关键词
26
+ - **stime** (可选): 开始时间戳(秒) - 需要转换相对时间
27
+ - **etime** (可选): 结束时间戳(秒)
28
+ - **limit** (可选): 返回数量,默认50条
29
+
30
+ ### 时间转换
31
+
32
+ 用户常用相对时间表达,需要转换为Unix时间戳(秒):
33
+
34
+ - "昨天": stime = 昨天0点, etime = 昨天23:59
35
+ - "过去24小时": stime = 当前时间 - 86400秒
36
+ - "本周": stime = 本周一0点, etime = 当前时间
37
+ - "上周": stime = 上周一0点, etime = 上周日23:59
38
+
39
+ ### 调用示例
40
+
41
+ ```json
42
+ // 查询昨天的告警
43
+ {
44
+ "stime": 1707667200,
45
+ "etime": 1707753599,
46
+ "limit": 50
47
+ }
48
+
49
+ // 查询过去24小时严重告警
50
+ {
51
+ "severity": 1,
52
+ "stime": 1707753600,
53
+ "limit": 20
54
+ }
55
+
56
+ // 查询CPU相关历史告警
57
+ {
58
+ "rule_name": "CPU",
59
+ "stime": 1707580800,
60
+ "etime": 1707753600
61
+ }
62
+ ```
63
+
64
+ ## 返回数据结构
65
+
66
+ ### summary 摘要
67
+ - total: 总告警数
68
+ - showing: 当前显示数量
69
+ - severity_distribution: 严重级别分布
70
+ - recovered_count: 已恢复数量
71
+ - unrecovered_count: 未恢复数量
72
+
73
+ ### alerts 告警列表
74
+ 每条告警包含:
75
+ - id: 告警事件ID
76
+ - rule_name: 规则名称
77
+ - severity / severity_label: 严重级别
78
+ - trigger_time: 触发时间
79
+ - recover_time: 恢复时间(可能为null)
80
+ - is_recovered: 是否已恢复(布尔值)
81
+ - target_ident: 目标标识
82
+ - trigger_value: 触发值
83
+ - tags: 标签对象
84
+ - rule_note: 规则说明
85
+ - group_name: 业务组名
86
+
87
+ ## 如何回答用户
88
+
89
+ 1. **时间范围确认**
90
+ - 明确告知用户查询的时间范围
91
+
92
+ 2. **总结统计**
93
+ - 总告警数、恢复率、严重级别分布
94
+
95
+ 3. **趋势分析** (如适用)
96
+ - 哪个时段告警最多
97
+ - 哪些规则触发最频繁
98
+ - 恢复时间的长短
99
+
100
+ 4. **重点展示**
101
+ - 优先展示严重级别高的
102
+ - 优先展示未恢复的
103
+
104
+ ## 使用示例
105
+
106
+ ### 示例 1: 昨天的告警
107
+
108
+ **用户**: "昨天有哪些告警"
109
+
110
+ **操作**: 计算昨天的时间范围并调用工具
111
+
112
+ **回答模板**:
113
+ > 昨天(2月11日)共有15条告警事件,其中12条已恢复、3条未恢复。
114
+ >
115
+ > 严重级别分布:
116
+ > - 严重: 5条
117
+ > - 警告: 8条
118
+ > - 提示: 2条
119
+ >
120
+ > 未恢复告警:
121
+ > 1. **数据库连接池耗尽** (db-server-01) - 触发于 11日 18:30
122
+ > 2. **磁盘IO异常** (storage-02) - 触发于 11日 20:15
123
+ > 3. **API响应超时** (api-gateway) - 触发于 11日 22:45
124
+
125
+ ### 示例 2: 过去一周趋势
126
+
127
+ **用户**: "过去一周的告警趋势"
128
+
129
+ **操作**: 计算过去7天时间范围
130
+
131
+ **回答模板**:
132
+ > 过去一周(2月5日-2月11日)共有87条告警:
133
+ >
134
+ > 趋势分析:
135
+ > - 2月9日告警最多(23条),主要是网络波动导致
136
+ > - CPU使用率告警占比最高(32条,37%)
137
+ > - 恢复率: 91%(79/87)
138
+ >
139
+ > 最频繁的告警规则:
140
+ > 1. CPU使用率过高 - 32次
141
+ > 2. 内存不足 - 18次
142
+ > 3. 磁盘空间不足 - 15次
143
+
144
+ ### 示例 3: 特定时间段特定规则
145
+
146
+ **用户**: "上周CPU相关的严重告警有哪些"
147
+
148
+ **操作**: 设置上周时间范围、severity=1、rule_name="CPU"
149
+
150
+ **回答模板**:
151
+ > 上周(2月5日-2月11日)共有8条CPU相关的严重告警:
152
+ >
153
+ > 1. **CPU使用率持续过高** (web-01)
154
+ > - 触发: 2月6日 14:20 → 恢复: 2月6日 15:30 (持续1小时10分)
155
+ >
156
+ > 2. **CPU负载异常** (app-03)
157
+ > - 触发: 2月7日 09:15 → 恢复: 2月7日 09:45 (持续30分钟)
158
+ > ...
159
+
160
+ ## 注意事项
161
+
162
+ 1. **时间转换准确性**: 确保正确转换用户的时间表达
163
+ 2. **恢复状态**: 明确标识哪些告警已恢复、哪些仍在持续
164
+ 3. **持续时长**: 如果有恢复时间,可以计算告警持续时长
165
+ 4. **数据量提示**: 如果历史数据很多,建议分页或缩小范围
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const n9ePluginConfigSchema = z.object({
4
+ apiBaseUrl: z.string().url().describe("夜莺API地址"),
5
+ apiKey: z.string().min(1).describe("API访问密钥"),
6
+ timeout: z.number().min(1000).max(120000).default(30000).describe("请求超时(毫秒)")
7
+ });
8
+
9
+ export type N9ePluginConfig = z.infer<typeof n9ePluginConfigSchema>;
@@ -0,0 +1,251 @@
1
+ import type {
2
+ N9ePluginConfig,
3
+ AlertCurEvent,
4
+ AlertHisEvent,
5
+ AlertRule,
6
+ AlertMute,
7
+ ActiveAlertQuery,
8
+ HistoricalAlertQuery,
9
+ AlertRuleQuery,
10
+ AlertMuteQuery,
11
+ N9eApiResponse,
12
+ N9eListResponse
13
+ } from "./types.ts";
14
+
15
+ export class N9eClient {
16
+ constructor(private config: N9ePluginConfig) {}
17
+
18
+ /**
19
+ * Generic HTTP request handler
20
+ */
21
+ private async request<T>(
22
+ method: "GET" | "POST" | "PUT" | "DELETE",
23
+ path: string,
24
+ params?: Record<string, any>,
25
+ body?: any
26
+ ): Promise<T> {
27
+ const url = new URL(path, this.config.apiBaseUrl);
28
+
29
+ // Add query parameters for GET requests
30
+ if (method === "GET" && params) {
31
+ Object.entries(params).forEach(([key, value]) => {
32
+ if (value !== undefined && value !== null) {
33
+ url.searchParams.append(key, String(value));
34
+ }
35
+ });
36
+ }
37
+
38
+ const headers: Record<string, string> = {
39
+ "Content-Type": "application/json",
40
+ "X-API-Key": this.config.apiKey
41
+ };
42
+
43
+ const options: RequestInit = {
44
+ method,
45
+ headers,
46
+ signal: AbortSignal.timeout(this.config.timeout)
47
+ };
48
+
49
+ if (body && method !== "GET") {
50
+ options.body = JSON.stringify(body);
51
+ }
52
+
53
+ try {
54
+ const response = await fetch(url.toString(), options);
55
+
56
+ if (!response.ok) {
57
+ const errorText = await response.text().catch(() => "Unknown error");
58
+
59
+ // Map HTTP status to user-friendly messages
60
+ switch (response.status) {
61
+ case 401:
62
+ case 403:
63
+ throw new Error("认证失败,请检查API Key配置");
64
+ case 404:
65
+ throw new Error("未找到指定的资源");
66
+ case 500:
67
+ throw new Error("夜莺服务器错误,请稍后重试");
68
+ default:
69
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
70
+ }
71
+ }
72
+
73
+ const data = await response.json();
74
+ return data as T;
75
+ } catch (error) {
76
+ if (error instanceof Error) {
77
+ if (error.name === "AbortError" || error.name === "TimeoutError") {
78
+ throw new Error("请求超时,请检查网络连接");
79
+ }
80
+ if (error.message.includes("fetch failed")) {
81
+ throw new Error("无法连接到夜莺平台,请检查网络或配置");
82
+ }
83
+ throw error;
84
+ }
85
+ throw new Error("未知错误");
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Query active alerts
91
+ */
92
+ async getActiveAlerts(params: ActiveAlertQuery = {}): Promise<{ total: number; list: AlertCurEvent[] }> {
93
+ const queryParams = {
94
+ p: params.p || 1,
95
+ limit: params.limit || 50,
96
+ ...(params.severity && { severity: params.severity }),
97
+ ...(params.rule_name && { rule_name: params.rule_name }),
98
+ ...(params.query && { query: params.query })
99
+ };
100
+
101
+ const response = await this.request<N9eListResponse<AlertCurEvent>>(
102
+ "GET",
103
+ "/api/n9e/alert-cur-events/list",
104
+ queryParams
105
+ );
106
+
107
+ if (response.err) {
108
+ throw new Error(response.err);
109
+ }
110
+
111
+ return response.dat;
112
+ }
113
+
114
+ /**
115
+ * Get active alert by ID
116
+ */
117
+ async getActiveAlertById(id: number): Promise<AlertCurEvent> {
118
+ const response = await this.request<N9eApiResponse<AlertCurEvent>>(
119
+ "GET",
120
+ `/api/n9e/alert-cur-event/${id}`
121
+ );
122
+
123
+ if (response.err) {
124
+ throw new Error(response.err);
125
+ }
126
+
127
+ return response.dat;
128
+ }
129
+
130
+ /**
131
+ * Query historical alerts
132
+ */
133
+ async getHistoricalAlerts(params: HistoricalAlertQuery = {}): Promise<{ total: number; list: AlertHisEvent[] }> {
134
+ const queryParams = {
135
+ p: params.p || 1,
136
+ limit: params.limit || 50,
137
+ ...(params.severity && { severity: params.severity }),
138
+ ...(params.rule_name && { rule_name: params.rule_name }),
139
+ ...(params.stime && { stime: params.stime }),
140
+ ...(params.etime && { etime: params.etime })
141
+ };
142
+
143
+ const response = await this.request<N9eListResponse<AlertHisEvent>>(
144
+ "GET",
145
+ "/api/n9e/alert-his-events/list",
146
+ queryParams
147
+ );
148
+
149
+ if (response.err) {
150
+ throw new Error(response.err);
151
+ }
152
+
153
+ return response.dat;
154
+ }
155
+
156
+ /**
157
+ * Get historical alert by ID
158
+ */
159
+ async getHistoricalAlertById(id: number): Promise<AlertHisEvent> {
160
+ const response = await this.request<N9eApiResponse<AlertHisEvent>>(
161
+ "GET",
162
+ `/api/n9e/alert-his-event/${id}`
163
+ );
164
+
165
+ if (response.err) {
166
+ throw new Error(response.err);
167
+ }
168
+
169
+ return response.dat;
170
+ }
171
+
172
+ /**
173
+ * Query alert rules
174
+ */
175
+ async getAlertRules(params: AlertRuleQuery = {}): Promise<{ total: number; list: AlertRule[] }> {
176
+ const queryParams = {
177
+ p: params.p || 1,
178
+ limit: params.limit || 50,
179
+ ...(params.rule_name && { rule_name: params.rule_name }),
180
+ ...(params.disabled !== undefined && { disabled: params.disabled }),
181
+ ...(params.datasource_ids && { datasource_ids: params.datasource_ids.join(",") })
182
+ };
183
+
184
+ const response = await this.request<N9eListResponse<AlertRule>>(
185
+ "GET",
186
+ "/api/n9e/alert-rules",
187
+ queryParams
188
+ );
189
+
190
+ if (response.err) {
191
+ throw new Error(response.err);
192
+ }
193
+
194
+ return response.dat;
195
+ }
196
+
197
+ /**
198
+ * Get alert rule by ID
199
+ */
200
+ async getAlertRuleById(id: number): Promise<AlertRule> {
201
+ const response = await this.request<N9eApiResponse<AlertRule>>(
202
+ "GET",
203
+ `/api/n9e/alert-rule/${id}`
204
+ );
205
+
206
+ if (response.err) {
207
+ throw new Error(response.err);
208
+ }
209
+
210
+ return response.dat;
211
+ }
212
+
213
+ /**
214
+ * Query alert mutes
215
+ */
216
+ async getAlertMutes(params: AlertMuteQuery = {}): Promise<{ total: number; list: AlertMute[] }> {
217
+ const queryParams = {
218
+ p: params.p || 1,
219
+ limit: params.limit || 50,
220
+ ...(params.query && { query: params.query })
221
+ };
222
+
223
+ const response = await this.request<N9eListResponse<AlertMute>>(
224
+ "GET",
225
+ "/api/n9e/v1/n9e/alert-mutes",
226
+ queryParams
227
+ );
228
+
229
+ if (response.err) {
230
+ throw new Error(response.err);
231
+ }
232
+
233
+ return response.dat;
234
+ }
235
+
236
+ /**
237
+ * Get alert mute by ID
238
+ */
239
+ async getAlertMuteById(id: number): Promise<AlertMute> {
240
+ const response = await this.request<N9eApiResponse<AlertMute>>(
241
+ "GET",
242
+ `/api/n9e/alert-mutes-detail/${id}`
243
+ );
244
+
245
+ if (response.err) {
246
+ throw new Error(response.err);
247
+ }
248
+
249
+ return response.dat;
250
+ }
251
+ }
@@ -0,0 +1,47 @@
1
+ // Type declarations for openclaw/plugin-sdk
2
+ declare module "openclaw/plugin-sdk" {
3
+ export interface ToolHandler {
4
+ (input: any, context: ToolContext): Promise<ToolResult>;
5
+ }
6
+
7
+ export interface ToolContext {
8
+ config: {
9
+ plugins?: {
10
+ entries?: {
11
+ [key: string]: {
12
+ config?: any;
13
+ };
14
+ };
15
+ };
16
+ };
17
+ logger: {
18
+ info: (...args: any[]) => void;
19
+ error: (...args: any[]) => void;
20
+ warn: (...args: any[]) => void;
21
+ debug: (...args: any[]) => void;
22
+ };
23
+ }
24
+
25
+ export interface ToolResult {
26
+ content: Array<{
27
+ type: string;
28
+ text: string;
29
+ }>;
30
+ isError?: boolean;
31
+ }
32
+
33
+ export interface OpenClawPluginApi {
34
+ registerTool: (tool: {
35
+ name: string;
36
+ description: string;
37
+ inputSchema: any;
38
+ handler: ToolHandler;
39
+ }) => void;
40
+ logger: {
41
+ info: (...args: any[]) => void;
42
+ error: (...args: any[]) => void;
43
+ warn: (...args: any[]) => void;
44
+ debug: (...args: any[]) => void;
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,109 @@
1
+ import type { ToolHandler } from "openclaw/plugin-sdk";
2
+ import { N9eClient } from "../n9e-client.ts";
3
+ import { n9ePluginConfigSchema } from "../config-schema.ts";
4
+ import { SEVERITY_LABELS, type AlertSeverity } from "../types.ts";
5
+
6
+ export const activeAlertsToolInputSchema = {
7
+ type: "object",
8
+ properties: {
9
+ severity: {
10
+ type: "number",
11
+ enum: [1, 2, 3],
12
+ description: "严重级别: 1=严重, 2=警告, 3=提示"
13
+ },
14
+ rule_name: {
15
+ type: "string",
16
+ description: "按规则名称筛选(支持模糊匹配)"
17
+ },
18
+ limit: {
19
+ type: "number",
20
+ minimum: 1,
21
+ maximum: 1000,
22
+ default: 50,
23
+ description: "返回数量限制"
24
+ },
25
+ query: {
26
+ type: "string",
27
+ description: "搜索查询字符串"
28
+ }
29
+ }
30
+ } as const;
31
+
32
+ export const activeAlertsToolHandler: ToolHandler = async (input, context) => {
33
+ try {
34
+ // Parse and validate config
35
+ const rawConfig = context.config.plugins?.entries?.["n9e-plugin"]?.config;
36
+ if (!rawConfig) {
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: "❌ 插件配置未找到,请在配置文件中添加n9e-plugin配置"
41
+ }],
42
+ isError: true
43
+ };
44
+ }
45
+
46
+ const config = n9ePluginConfigSchema.parse(rawConfig);
47
+ const client = new N9eClient(config);
48
+
49
+ // Query active alerts
50
+ const { total, list } = await client.getActiveAlerts({
51
+ severity: input.severity as AlertSeverity | undefined,
52
+ rule_name: input.rule_name,
53
+ limit: input.limit || 50,
54
+ query: input.query
55
+ });
56
+
57
+ // Format response
58
+ const alerts = list.map(alert => {
59
+ const tags = alert.tags ? JSON.parse(alert.tags) : {};
60
+ return {
61
+ id: alert.id,
62
+ rule_name: alert.rule_name,
63
+ severity: alert.severity,
64
+ severity_label: SEVERITY_LABELS[alert.severity],
65
+ trigger_time: new Date(alert.trigger_time * 1000).toLocaleString("zh-CN", {
66
+ timeZone: "Asia/Shanghai"
67
+ }),
68
+ status: "triggered",
69
+ target_ident: alert.target_ident,
70
+ trigger_value: alert.trigger_value,
71
+ tags,
72
+ rule_note: alert.rule_note,
73
+ group_name: alert.group_name
74
+ };
75
+ });
76
+
77
+ // Count by severity
78
+ const severityCount = alerts.reduce((acc, alert) => {
79
+ const label = alert.severity_label;
80
+ acc[label] = (acc[label] || 0) + 1;
81
+ return acc;
82
+ }, {} as Record<string, number>);
83
+
84
+ const result = {
85
+ summary: {
86
+ total,
87
+ showing: alerts.length,
88
+ severity_distribution: severityCount
89
+ },
90
+ alerts
91
+ };
92
+
93
+ return {
94
+ content: [{
95
+ type: "text",
96
+ text: JSON.stringify(result, null, 2)
97
+ }]
98
+ };
99
+ } catch (error) {
100
+ context.logger.error("Failed to query active alerts:", error);
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: `❌ 查询活跃告警失败: ${error instanceof Error ? error.message : String(error)}`
105
+ }],
106
+ isError: true
107
+ };
108
+ }
109
+ };
@@ -0,0 +1,123 @@
1
+ import type { ToolHandler } from "openclaw/plugin-sdk";
2
+ import { N9eClient } from "../n9e-client.ts";
3
+ import { n9ePluginConfigSchema } from "../config-schema.ts";
4
+
5
+ export const alertMutesToolInputSchema = {
6
+ type: "object",
7
+ properties: {
8
+ query: {
9
+ type: "string",
10
+ description: "搜索查询字符串"
11
+ },
12
+ limit: {
13
+ type: "number",
14
+ minimum: 1,
15
+ maximum: 1000,
16
+ default: 50,
17
+ description: "返回数量限制"
18
+ }
19
+ }
20
+ } as const;
21
+
22
+ export const alertMutesToolHandler: ToolHandler = async (input, context) => {
23
+ try {
24
+ // Parse and validate config
25
+ const rawConfig = context.config.plugins?.entries?.["n9e-plugin"]?.config;
26
+ if (!rawConfig) {
27
+ return {
28
+ content: [{
29
+ type: "text",
30
+ text: "❌ 插件配置未找到,请在配置文件中添加n9e-plugin配置"
31
+ }],
32
+ isError: true
33
+ };
34
+ }
35
+
36
+ const config = n9ePluginConfigSchema.parse(rawConfig);
37
+ const client = new N9eClient(config);
38
+
39
+ // Query alert mutes
40
+ const { total, list } = await client.getAlertMutes({
41
+ query: input.query,
42
+ limit: input.limit || 50
43
+ });
44
+
45
+ // Format response
46
+ const now = Math.floor(Date.now() / 1000);
47
+ const mutes = list.map(mute => {
48
+ const tags = mute.tags ? JSON.parse(mute.tags) : {};
49
+ const isActive = mute.disabled === 0 && mute.btime <= now && mute.etime >= now;
50
+ const isPast = mute.etime < now;
51
+ const isFuture = mute.btime > now;
52
+
53
+ let status = "未知";
54
+ if (mute.disabled === 1) {
55
+ status = "已禁用";
56
+ } else if (isPast) {
57
+ status = "已过期";
58
+ } else if (isFuture) {
59
+ status = "未生效";
60
+ } else if (isActive) {
61
+ status = "生效中";
62
+ }
63
+
64
+ return {
65
+ id: mute.id,
66
+ cause: mute.cause,
67
+ tags,
68
+ status,
69
+ disabled: mute.disabled === 1,
70
+ begin_time: new Date(mute.btime * 1000).toLocaleString("zh-CN", {
71
+ timeZone: "Asia/Shanghai"
72
+ }),
73
+ end_time: new Date(mute.etime * 1000).toLocaleString("zh-CN", {
74
+ timeZone: "Asia/Shanghai"
75
+ }),
76
+ group_id: mute.group_id,
77
+ cluster: mute.cluster,
78
+ created_at: new Date(mute.create_at * 1000).toLocaleString("zh-CN", {
79
+ timeZone: "Asia/Shanghai"
80
+ }),
81
+ created_by: mute.create_by,
82
+ updated_at: new Date(mute.update_at * 1000).toLocaleString("zh-CN", {
83
+ timeZone: "Asia/Shanghai"
84
+ }),
85
+ updated_by: mute.update_by
86
+ };
87
+ });
88
+
89
+ // Statistics
90
+ const activeCount = mutes.filter(m => m.status === "生效中").length;
91
+ const disabledCount = mutes.filter(m => m.disabled).length;
92
+ const pastCount = mutes.filter(m => m.status === "已过期").length;
93
+ const futureCount = mutes.filter(m => m.status === "未生效").length;
94
+
95
+ const result = {
96
+ summary: {
97
+ total,
98
+ showing: mutes.length,
99
+ active_count: activeCount,
100
+ disabled_count: disabledCount,
101
+ past_count: pastCount,
102
+ future_count: futureCount
103
+ },
104
+ mutes
105
+ };
106
+
107
+ return {
108
+ content: [{
109
+ type: "text",
110
+ text: JSON.stringify(result, null, 2)
111
+ }]
112
+ };
113
+ } catch (error) {
114
+ context.logger.error("Failed to query alert mutes:", error);
115
+ return {
116
+ content: [{
117
+ type: "text",
118
+ text: `❌ 查询告警屏蔽失败: ${error instanceof Error ? error.message : String(error)}`
119
+ }],
120
+ isError: true
121
+ };
122
+ }
123
+ };