@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.
- package/.claude/settings.local.json +8 -0
- package/README.md +191 -0
- package/docs/plans/2026-02-12-n9e-plugin-design.md +377 -0
- package/docs/plans/implementation-plan.md +2338 -0
- package/index.ts +53 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +24 -0
- package/skills/query-active-alerts/SKILL.md +144 -0
- package/skills/query-alert-mutes/SKILL.md +173 -0
- package/skills/query-alert-rules/SKILL.md +167 -0
- package/skills/query-historical-alerts/SKILL.md +165 -0
- package/src/config-schema.ts +9 -0
- package/src/n9e-client.ts +251 -0
- package/src/openclaw-plugin-sdk.d.ts +47 -0
- package/src/tools/active-alerts.ts +109 -0
- package/src/tools/alert-mutes.ts +123 -0
- package/src/tools/alert-rules.ts +120 -0
- package/src/tools/historical-alerts.ts +134 -0
- package/src/types.ts +180 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,120 @@
|
|
|
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 } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export const alertRulesToolInputSchema = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
rule_name: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "按规则名称搜索(支持模糊匹配)"
|
|
12
|
+
},
|
|
13
|
+
datasource_ids: {
|
|
14
|
+
type: "array",
|
|
15
|
+
items: { type: "number" },
|
|
16
|
+
description: "数据源ID列表"
|
|
17
|
+
},
|
|
18
|
+
disabled: {
|
|
19
|
+
type: "number",
|
|
20
|
+
enum: [0, 1],
|
|
21
|
+
description: "规则状态: 0=启用, 1=禁用"
|
|
22
|
+
},
|
|
23
|
+
limit: {
|
|
24
|
+
type: "number",
|
|
25
|
+
minimum: 1,
|
|
26
|
+
maximum: 1000,
|
|
27
|
+
default: 50,
|
|
28
|
+
description: "返回数量限制"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export const alertRulesToolHandler: ToolHandler = async (input, context) => {
|
|
34
|
+
try {
|
|
35
|
+
// Parse and validate config
|
|
36
|
+
const rawConfig = context.config.plugins?.entries?.["n9e-plugin"]?.config;
|
|
37
|
+
if (!rawConfig) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: "❌ 插件配置未找到,请在配置文件中添加n9e-plugin配置"
|
|
42
|
+
}],
|
|
43
|
+
isError: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const config = n9ePluginConfigSchema.parse(rawConfig);
|
|
48
|
+
const client = new N9eClient(config);
|
|
49
|
+
|
|
50
|
+
// Query alert rules
|
|
51
|
+
const { total, list } = await client.getAlertRules({
|
|
52
|
+
rule_name: input.rule_name,
|
|
53
|
+
datasource_ids: input.datasource_ids,
|
|
54
|
+
disabled: input.disabled as 0 | 1 | undefined,
|
|
55
|
+
limit: input.limit || 50
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Format response
|
|
59
|
+
const rules = list.map(rule => ({
|
|
60
|
+
id: rule.id,
|
|
61
|
+
name: rule.name,
|
|
62
|
+
note: rule.note,
|
|
63
|
+
severity: rule.severity,
|
|
64
|
+
severity_label: SEVERITY_LABELS[rule.severity],
|
|
65
|
+
disabled: rule.disabled === 1,
|
|
66
|
+
status: rule.disabled === 1 ? "禁用" : "启用",
|
|
67
|
+
prom_ql: rule.prom_ql,
|
|
68
|
+
prom_for_duration: rule.prom_for_duration,
|
|
69
|
+
prom_eval_interval: rule.prom_eval_interval,
|
|
70
|
+
notify_channels: rule.notify_channels,
|
|
71
|
+
datasource_ids: rule.datasource_ids,
|
|
72
|
+
group_id: rule.group_id,
|
|
73
|
+
cluster: rule.cluster,
|
|
74
|
+
created_at: new Date(rule.create_at * 1000).toLocaleString("zh-CN", {
|
|
75
|
+
timeZone: "Asia/Shanghai"
|
|
76
|
+
}),
|
|
77
|
+
created_by: rule.create_by,
|
|
78
|
+
updated_at: new Date(rule.update_at * 1000).toLocaleString("zh-CN", {
|
|
79
|
+
timeZone: "Asia/Shanghai"
|
|
80
|
+
}),
|
|
81
|
+
updated_by: rule.update_by
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Statistics
|
|
85
|
+
const enabledCount = rules.filter(r => !r.disabled).length;
|
|
86
|
+
const disabledCount = rules.filter(r => r.disabled).length;
|
|
87
|
+
const severityCount = rules.reduce((acc, rule) => {
|
|
88
|
+
const label = rule.severity_label;
|
|
89
|
+
acc[label] = (acc[label] || 0) + 1;
|
|
90
|
+
return acc;
|
|
91
|
+
}, {} as Record<string, number>);
|
|
92
|
+
|
|
93
|
+
const result = {
|
|
94
|
+
summary: {
|
|
95
|
+
total,
|
|
96
|
+
showing: rules.length,
|
|
97
|
+
enabled_count: enabledCount,
|
|
98
|
+
disabled_count: disabledCount,
|
|
99
|
+
severity_distribution: severityCount
|
|
100
|
+
},
|
|
101
|
+
rules
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: JSON.stringify(result, null, 2)
|
|
108
|
+
}]
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
context.logger.error("Failed to query alert rules:", error);
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `❌ 查询告警规则失败: ${error instanceof Error ? error.message : String(error)}`
|
|
116
|
+
}],
|
|
117
|
+
isError: true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
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 historicalAlertsToolInputSchema = {
|
|
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
|
+
stime: {
|
|
19
|
+
type: "number",
|
|
20
|
+
description: "开始时间戳(秒)"
|
|
21
|
+
},
|
|
22
|
+
etime: {
|
|
23
|
+
type: "number",
|
|
24
|
+
description: "结束时间戳(秒)"
|
|
25
|
+
},
|
|
26
|
+
limit: {
|
|
27
|
+
type: "number",
|
|
28
|
+
minimum: 1,
|
|
29
|
+
maximum: 1000,
|
|
30
|
+
default: 50,
|
|
31
|
+
description: "返回数量限制"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const historicalAlertsToolHandler: ToolHandler = async (input, context) => {
|
|
37
|
+
try {
|
|
38
|
+
// Parse and validate config
|
|
39
|
+
const rawConfig = context.config.plugins?.entries?.["n9e-plugin"]?.config;
|
|
40
|
+
if (!rawConfig) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "❌ 插件配置未找到,请在配置文件中添加n9e-plugin配置"
|
|
45
|
+
}],
|
|
46
|
+
isError: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = n9ePluginConfigSchema.parse(rawConfig);
|
|
51
|
+
const client = new N9eClient(config);
|
|
52
|
+
|
|
53
|
+
// Validate time range
|
|
54
|
+
if (input.stime && input.etime && input.stime >= input.etime) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: "❌ 开始时间必须早于结束时间"
|
|
59
|
+
}],
|
|
60
|
+
isError: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Query historical alerts
|
|
65
|
+
const { total, list } = await client.getHistoricalAlerts({
|
|
66
|
+
severity: input.severity as AlertSeverity | undefined,
|
|
67
|
+
rule_name: input.rule_name,
|
|
68
|
+
stime: input.stime,
|
|
69
|
+
etime: input.etime,
|
|
70
|
+
limit: input.limit || 50
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Format response
|
|
74
|
+
const alerts = list.map(alert => {
|
|
75
|
+
const tags = alert.tags ? JSON.parse(alert.tags) : {};
|
|
76
|
+
return {
|
|
77
|
+
id: alert.id,
|
|
78
|
+
rule_name: alert.rule_name,
|
|
79
|
+
severity: alert.severity,
|
|
80
|
+
severity_label: SEVERITY_LABELS[alert.severity],
|
|
81
|
+
trigger_time: new Date(alert.trigger_time * 1000).toLocaleString("zh-CN", {
|
|
82
|
+
timeZone: "Asia/Shanghai"
|
|
83
|
+
}),
|
|
84
|
+
recover_time: alert.recover_time
|
|
85
|
+
? new Date(alert.recover_time * 1000).toLocaleString("zh-CN", {
|
|
86
|
+
timeZone: "Asia/Shanghai"
|
|
87
|
+
})
|
|
88
|
+
: null,
|
|
89
|
+
is_recovered: alert.is_recovered === 1,
|
|
90
|
+
target_ident: alert.target_ident,
|
|
91
|
+
trigger_value: alert.trigger_value,
|
|
92
|
+
tags,
|
|
93
|
+
rule_note: alert.rule_note,
|
|
94
|
+
group_name: alert.group_name
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Statistics
|
|
99
|
+
const severityCount = alerts.reduce((acc, alert) => {
|
|
100
|
+
const label = alert.severity_label;
|
|
101
|
+
acc[label] = (acc[label] || 0) + 1;
|
|
102
|
+
return acc;
|
|
103
|
+
}, {} as Record<string, number>);
|
|
104
|
+
|
|
105
|
+
const recoveredCount = alerts.filter(a => a.is_recovered).length;
|
|
106
|
+
|
|
107
|
+
const result = {
|
|
108
|
+
summary: {
|
|
109
|
+
total,
|
|
110
|
+
showing: alerts.length,
|
|
111
|
+
severity_distribution: severityCount,
|
|
112
|
+
recovered_count: recoveredCount,
|
|
113
|
+
unrecovered_count: alerts.length - recoveredCount
|
|
114
|
+
},
|
|
115
|
+
alerts
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
content: [{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: JSON.stringify(result, null, 2)
|
|
122
|
+
}]
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
context.logger.error("Failed to query historical alerts:", error);
|
|
126
|
+
return {
|
|
127
|
+
content: [{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `❌ 查询历史告警失败: ${error instanceof Error ? error.message : String(error)}`
|
|
130
|
+
}],
|
|
131
|
+
isError: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Configuration types
|
|
2
|
+
export interface N9ePluginConfig {
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
timeout: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Alert severity mapping
|
|
9
|
+
export type AlertSeverity = 1 | 2 | 3; // 1=严重, 2=警告, 3=提示
|
|
10
|
+
|
|
11
|
+
export const SEVERITY_LABELS: Record<AlertSeverity, string> = {
|
|
12
|
+
1: "严重",
|
|
13
|
+
2: "警告",
|
|
14
|
+
3: "提示"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Active alert (current event)
|
|
18
|
+
export interface AlertCurEvent {
|
|
19
|
+
id: number;
|
|
20
|
+
cluster: string;
|
|
21
|
+
group_id: number;
|
|
22
|
+
group_name?: string;
|
|
23
|
+
hash: string;
|
|
24
|
+
rule_id: number;
|
|
25
|
+
rule_name: string;
|
|
26
|
+
rule_note: string;
|
|
27
|
+
severity: AlertSeverity;
|
|
28
|
+
prom_for_duration: number;
|
|
29
|
+
prom_ql: string;
|
|
30
|
+
prom_eval_interval: number;
|
|
31
|
+
callbacks: string;
|
|
32
|
+
runbook_url?: string;
|
|
33
|
+
notify_recovered: number;
|
|
34
|
+
notify_channels: string;
|
|
35
|
+
notify_groups: string;
|
|
36
|
+
notify_repeat_step: number;
|
|
37
|
+
notify_max_number: number;
|
|
38
|
+
recover_duration: number;
|
|
39
|
+
created_at: number;
|
|
40
|
+
updated_at: number;
|
|
41
|
+
tags: string;
|
|
42
|
+
target_ident: string;
|
|
43
|
+
target_note: string;
|
|
44
|
+
first_trigger_time?: number;
|
|
45
|
+
trigger_time: number;
|
|
46
|
+
trigger_value: string;
|
|
47
|
+
annotations?: string;
|
|
48
|
+
rule_config?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Historical alert event
|
|
52
|
+
export interface AlertHisEvent {
|
|
53
|
+
id: number;
|
|
54
|
+
is_recovered: number;
|
|
55
|
+
cluster: string;
|
|
56
|
+
group_id: number;
|
|
57
|
+
group_name?: string;
|
|
58
|
+
hash: string;
|
|
59
|
+
rule_id: number;
|
|
60
|
+
rule_name: string;
|
|
61
|
+
rule_note: string;
|
|
62
|
+
severity: AlertSeverity;
|
|
63
|
+
prom_for_duration: number;
|
|
64
|
+
prom_ql: string;
|
|
65
|
+
prom_eval_interval: number;
|
|
66
|
+
callbacks: string;
|
|
67
|
+
runbook_url?: string;
|
|
68
|
+
notify_recovered: number;
|
|
69
|
+
notify_channels: string;
|
|
70
|
+
notify_groups: string;
|
|
71
|
+
notify_repeat_step: number;
|
|
72
|
+
notify_max_number: number;
|
|
73
|
+
recover_duration: number;
|
|
74
|
+
created_at: number;
|
|
75
|
+
updated_at: number;
|
|
76
|
+
tags: string;
|
|
77
|
+
target_ident: string;
|
|
78
|
+
target_note: string;
|
|
79
|
+
first_trigger_time?: number;
|
|
80
|
+
trigger_time: number;
|
|
81
|
+
trigger_value: string;
|
|
82
|
+
recover_time?: number;
|
|
83
|
+
last_eval_time?: number;
|
|
84
|
+
annotations?: string;
|
|
85
|
+
rule_config?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Alert rule
|
|
89
|
+
export interface AlertRule {
|
|
90
|
+
id: number;
|
|
91
|
+
group_id: number;
|
|
92
|
+
cluster: string;
|
|
93
|
+
name: string;
|
|
94
|
+
note: string;
|
|
95
|
+
severity: AlertSeverity;
|
|
96
|
+
disabled: number; // 0=enabled, 1=disabled
|
|
97
|
+
prom_for_duration: number;
|
|
98
|
+
prom_ql: string;
|
|
99
|
+
prom_eval_interval: number;
|
|
100
|
+
enable_stime?: string;
|
|
101
|
+
enable_etime?: string;
|
|
102
|
+
enable_days_of_week?: string;
|
|
103
|
+
enable_in_bg?: number;
|
|
104
|
+
notify_recovered: number;
|
|
105
|
+
notify_channels: string;
|
|
106
|
+
notify_repeat_step: number;
|
|
107
|
+
notify_max_number: number;
|
|
108
|
+
recover_duration: number;
|
|
109
|
+
callbacks: string;
|
|
110
|
+
runbook_url?: string;
|
|
111
|
+
append_tags?: string;
|
|
112
|
+
create_at: number;
|
|
113
|
+
create_by: string;
|
|
114
|
+
update_at: number;
|
|
115
|
+
update_by: string;
|
|
116
|
+
datasource_ids?: number[];
|
|
117
|
+
extra_config?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Alert mute
|
|
121
|
+
export interface AlertMute {
|
|
122
|
+
id: number;
|
|
123
|
+
group_id: number;
|
|
124
|
+
cluster: string;
|
|
125
|
+
tags: string;
|
|
126
|
+
cause: string;
|
|
127
|
+
btime: number;
|
|
128
|
+
etime: number;
|
|
129
|
+
disabled: number; // 0=enabled, 1=disabled
|
|
130
|
+
create_at: number;
|
|
131
|
+
create_by: string;
|
|
132
|
+
update_at: number;
|
|
133
|
+
update_by: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Query parameters
|
|
137
|
+
export interface ActiveAlertQuery {
|
|
138
|
+
severity?: AlertSeverity;
|
|
139
|
+
rule_name?: string;
|
|
140
|
+
limit?: number;
|
|
141
|
+
query?: string;
|
|
142
|
+
p?: number; // page number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface HistoricalAlertQuery {
|
|
146
|
+
severity?: AlertSeverity;
|
|
147
|
+
rule_name?: string;
|
|
148
|
+
stime?: number; // start timestamp (seconds)
|
|
149
|
+
etime?: number; // end timestamp (seconds)
|
|
150
|
+
limit?: number;
|
|
151
|
+
p?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface AlertRuleQuery {
|
|
155
|
+
rule_name?: string;
|
|
156
|
+
datasource_ids?: number[];
|
|
157
|
+
disabled?: 0 | 1;
|
|
158
|
+
p?: number;
|
|
159
|
+
limit?: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface AlertMuteQuery {
|
|
163
|
+
query?: string;
|
|
164
|
+
p?: number;
|
|
165
|
+
limit?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// API response wrapper
|
|
169
|
+
export interface N9eApiResponse<T> {
|
|
170
|
+
dat: T;
|
|
171
|
+
err: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface N9eListResponse<T> {
|
|
175
|
+
dat: {
|
|
176
|
+
list: T[];
|
|
177
|
+
total: number;
|
|
178
|
+
};
|
|
179
|
+
err: string;
|
|
180
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": false,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "index.ts"],
|
|
18
|
+
"exclude": ["node_modules"]
|
|
19
|
+
}
|