@steadwing/openalerts 0.2.1 → 0.2.3
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/README.md +78 -47
- package/dist/core/engine.d.ts +7 -0
- package/dist/core/engine.js +50 -6
- package/dist/core/evaluator.js +19 -2
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -0
- package/dist/core/llm-enrichment.d.ts +21 -0
- package/dist/core/llm-enrichment.js +180 -0
- package/dist/core/rules.js +63 -25
- package/dist/core/types.d.ts +6 -1
- package/dist/core/types.js +1 -1
- package/dist/index.js +9 -4
- package/dist/plugin/adapter.d.ts +14 -2
- package/dist/plugin/adapter.js +81 -9
- package/dist/plugin/commands.js +2 -21
- package/dist/plugin/dashboard-html.js +111 -97
- package/dist/plugin/dashboard-routes.js +43 -5
- package/dist/plugin/log-bridge.js +27 -0
- package/package.json +4 -1
package/dist/core/rules.js
CHANGED
|
@@ -32,7 +32,7 @@ function isRuleEnabled(ctx, ruleId) {
|
|
|
32
32
|
const infraErrors = {
|
|
33
33
|
id: "infra-errors",
|
|
34
34
|
defaultCooldownMs: 15 * 60 * 1000,
|
|
35
|
-
defaultThreshold:
|
|
35
|
+
defaultThreshold: 1,
|
|
36
36
|
evaluate(event, ctx) {
|
|
37
37
|
if (event.type !== "infra.error")
|
|
38
38
|
return null;
|
|
@@ -40,8 +40,8 @@ const infraErrors = {
|
|
|
40
40
|
return null;
|
|
41
41
|
const channel = event.channel ?? "unknown";
|
|
42
42
|
pushWindow(ctx, "infra-errors", { ts: ctx.now });
|
|
43
|
-
const threshold = getRuleThreshold(ctx, "infra-errors",
|
|
44
|
-
const windowMs =
|
|
43
|
+
const threshold = getRuleThreshold(ctx, "infra-errors", 1);
|
|
44
|
+
const windowMs = 60 * 1000; // 1 minute
|
|
45
45
|
const count = countInWindow(ctx, "infra-errors", windowMs);
|
|
46
46
|
if (count < threshold)
|
|
47
47
|
return null;
|
|
@@ -52,7 +52,7 @@ const infraErrors = {
|
|
|
52
52
|
ruleId: "infra-errors",
|
|
53
53
|
severity: "error",
|
|
54
54
|
title: "Infrastructure errors spike",
|
|
55
|
-
detail: `${count} infra
|
|
55
|
+
detail: `${count} infra error(s) on ${channel} in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
|
|
56
56
|
ts: ctx.now,
|
|
57
57
|
fingerprint,
|
|
58
58
|
};
|
|
@@ -62,32 +62,37 @@ const infraErrors = {
|
|
|
62
62
|
const llmErrors = {
|
|
63
63
|
id: "llm-errors",
|
|
64
64
|
defaultCooldownMs: 15 * 60 * 1000,
|
|
65
|
-
defaultThreshold:
|
|
65
|
+
defaultThreshold: 1,
|
|
66
66
|
evaluate(event, ctx) {
|
|
67
|
-
|
|
67
|
+
// Trigger on LLM call/error events AND agent errors (agent failing before/during LLM call)
|
|
68
|
+
if (event.type !== "llm.call" && event.type !== "llm.error" && event.type !== "agent.error")
|
|
68
69
|
return null;
|
|
69
70
|
if (!isRuleEnabled(ctx, "llm-errors"))
|
|
70
71
|
return null;
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
if (event.
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// Stats are tracked in the evaluator (independent of rule state).
|
|
73
|
+
// Only proceed for actual errors:
|
|
74
|
+
if (event.type === "llm.call") {
|
|
75
|
+
// Only explicit error/timeout outcomes trigger alerting; undefined = OK
|
|
76
|
+
if (event.outcome !== "error" && event.outcome !== "timeout")
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// llm.error and agent.error are always errors — no outcome check needed
|
|
76
80
|
const channel = event.channel ?? "unknown";
|
|
77
81
|
pushWindow(ctx, "llm-errors", { ts: ctx.now });
|
|
78
|
-
const threshold = getRuleThreshold(ctx, "llm-errors",
|
|
79
|
-
const windowMs =
|
|
82
|
+
const threshold = getRuleThreshold(ctx, "llm-errors", 1);
|
|
83
|
+
const windowMs = 60 * 1000; // 1 minute
|
|
80
84
|
const count = countInWindow(ctx, "llm-errors", windowMs);
|
|
81
85
|
if (count < threshold)
|
|
82
86
|
return null;
|
|
83
87
|
const fingerprint = `llm-errors:${channel}`;
|
|
88
|
+
const label = event.type === "agent.error" ? "agent error(s)" : "LLM error(s)";
|
|
84
89
|
return {
|
|
85
90
|
type: "alert",
|
|
86
91
|
id: makeAlertId("llm-errors", fingerprint, ctx.now),
|
|
87
92
|
ruleId: "llm-errors",
|
|
88
93
|
severity: "error",
|
|
89
94
|
title: "LLM call errors",
|
|
90
|
-
detail: `${count}
|
|
95
|
+
detail: `${count} ${label} on ${channel} in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
|
|
91
96
|
ts: ctx.now,
|
|
92
97
|
fingerprint,
|
|
93
98
|
};
|
|
@@ -103,7 +108,7 @@ const sessionStuck = {
|
|
|
103
108
|
return null;
|
|
104
109
|
if (!isRuleEnabled(ctx, "session-stuck"))
|
|
105
110
|
return null;
|
|
106
|
-
|
|
111
|
+
// Stats tracked in evaluator (independent of rule state)
|
|
107
112
|
const ageMs = event.ageMs ?? 0;
|
|
108
113
|
const threshold = getRuleThreshold(ctx, "session-stuck", 120_000);
|
|
109
114
|
if (ageMs < threshold)
|
|
@@ -153,10 +158,8 @@ const heartbeatFail = {
|
|
|
153
158
|
fingerprint,
|
|
154
159
|
};
|
|
155
160
|
}
|
|
156
|
-
// Reset on success
|
|
157
|
-
|
|
158
|
-
ctx.state.consecutives.set(counterKey, 0);
|
|
159
|
-
}
|
|
161
|
+
// Reset on any non-error (success, undefined, etc.)
|
|
162
|
+
ctx.state.consecutives.set(counterKey, 0);
|
|
160
163
|
return null;
|
|
161
164
|
},
|
|
162
165
|
};
|
|
@@ -169,12 +172,12 @@ const queueDepth = {
|
|
|
169
172
|
// Fire on heartbeat (which carries queue depth) and dedicated queue_depth events
|
|
170
173
|
if (event.type !== "infra.heartbeat" && event.type !== "infra.queue_depth")
|
|
171
174
|
return null;
|
|
172
|
-
|
|
173
|
-
return null;
|
|
174
|
-
// Update last heartbeat timestamp (used by gateway-down rule)
|
|
175
|
+
// Always update heartbeat timestamp regardless of rule state (gateway-down depends on it)
|
|
175
176
|
if (event.type === "infra.heartbeat") {
|
|
176
177
|
ctx.state.lastHeartbeatTs = ctx.now;
|
|
177
178
|
}
|
|
179
|
+
if (!isRuleEnabled(ctx, "queue-depth"))
|
|
180
|
+
return null;
|
|
178
181
|
const queued = event.queueDepth ?? 0;
|
|
179
182
|
const threshold = getRuleThreshold(ctx, "queue-depth", 10);
|
|
180
183
|
if (queued < threshold)
|
|
@@ -198,11 +201,15 @@ const highErrorRate = {
|
|
|
198
201
|
defaultCooldownMs: 30 * 60 * 1000,
|
|
199
202
|
defaultThreshold: 50, // percent
|
|
200
203
|
evaluate(event, ctx) {
|
|
201
|
-
if (event.type !== "llm.call")
|
|
204
|
+
if (event.type !== "llm.call" && event.type !== "llm.error" && event.type !== "agent.error")
|
|
202
205
|
return null;
|
|
203
206
|
if (!isRuleEnabled(ctx, "high-error-rate"))
|
|
204
207
|
return null;
|
|
205
|
-
|
|
208
|
+
// agent.error and llm.error are always errors; llm.call checks outcome (timeout counts as error)
|
|
209
|
+
const isError = event.type === "agent.error" ||
|
|
210
|
+
event.type === "llm.error" ||
|
|
211
|
+
event.outcome === "error" ||
|
|
212
|
+
event.outcome === "timeout";
|
|
206
213
|
pushWindow(ctx, "msg-outcomes", { ts: ctx.now, value: isError ? 1 : 0 });
|
|
207
214
|
const window = ctx.state.windows.get("msg-outcomes");
|
|
208
215
|
if (!window || window.length < 20)
|
|
@@ -227,11 +234,41 @@ const highErrorRate = {
|
|
|
227
234
|
};
|
|
228
235
|
},
|
|
229
236
|
};
|
|
237
|
+
// ─── Rule: tool-errors ───────────────────────────────────────────────────
|
|
238
|
+
const toolErrors = {
|
|
239
|
+
id: "tool-errors",
|
|
240
|
+
defaultCooldownMs: 15 * 60 * 1000,
|
|
241
|
+
defaultThreshold: 1, // 1 tool error in 1 minute
|
|
242
|
+
evaluate(event, ctx) {
|
|
243
|
+
if (event.type !== "tool.error")
|
|
244
|
+
return null;
|
|
245
|
+
if (!isRuleEnabled(ctx, "tool-errors"))
|
|
246
|
+
return null;
|
|
247
|
+
pushWindow(ctx, "tool-errors", { ts: ctx.now });
|
|
248
|
+
const threshold = getRuleThreshold(ctx, "tool-errors", 1);
|
|
249
|
+
const windowMs = 60 * 1000; // 1 minute
|
|
250
|
+
const count = countInWindow(ctx, "tool-errors", windowMs);
|
|
251
|
+
if (count < threshold)
|
|
252
|
+
return null;
|
|
253
|
+
const toolName = event.meta?.toolName ?? "unknown";
|
|
254
|
+
const fingerprint = `tool-errors:${toolName}`;
|
|
255
|
+
return {
|
|
256
|
+
type: "alert",
|
|
257
|
+
id: makeAlertId("tool-errors", fingerprint, ctx.now),
|
|
258
|
+
ruleId: "tool-errors",
|
|
259
|
+
severity: "warn",
|
|
260
|
+
title: "Tool errors spike",
|
|
261
|
+
detail: `${count} tool error(s) in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
|
|
262
|
+
ts: ctx.now,
|
|
263
|
+
fingerprint,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
230
267
|
// ─── Rule: gateway-down ──────────────────────────────────────────────────────
|
|
231
268
|
const gatewayDown = {
|
|
232
269
|
id: "gateway-down",
|
|
233
270
|
defaultCooldownMs: 60 * 60 * 1000,
|
|
234
|
-
defaultThreshold:
|
|
271
|
+
defaultThreshold: 30_000, // 30 seconds
|
|
235
272
|
evaluate(event, ctx) {
|
|
236
273
|
// This rule is called by the watchdog timer, not by events directly.
|
|
237
274
|
if (event.type !== "watchdog.tick")
|
|
@@ -270,5 +307,6 @@ export const ALL_RULES = [
|
|
|
270
307
|
heartbeatFail,
|
|
271
308
|
queueDepth,
|
|
272
309
|
highErrorRate,
|
|
310
|
+
toolErrors,
|
|
273
311
|
gatewayDown,
|
|
274
312
|
];
|
package/dist/core/types.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type AlertTarget = {
|
|
|
51
51
|
to: string;
|
|
52
52
|
accountId?: string;
|
|
53
53
|
};
|
|
54
|
+
/** Enriches an alert with LLM-generated summary/action. Returns enriched alert or null to skip. */
|
|
55
|
+
export type AlertEnricher = (alert: AlertEvent) => Promise<AlertEvent | null>;
|
|
54
56
|
export type RuleOverride = {
|
|
55
57
|
enabled?: boolean;
|
|
56
58
|
threshold?: number;
|
|
@@ -65,6 +67,7 @@ export type MonitorConfig = {
|
|
|
65
67
|
maxLogSizeKb?: number;
|
|
66
68
|
maxLogAgeDays?: number;
|
|
67
69
|
quiet?: boolean;
|
|
70
|
+
llmEnriched?: boolean;
|
|
68
71
|
rules?: Record<string, RuleOverride>;
|
|
69
72
|
};
|
|
70
73
|
export type OpenAlertsInitOptions = {
|
|
@@ -80,6 +83,8 @@ export type OpenAlertsInitOptions = {
|
|
|
80
83
|
logPrefix?: string;
|
|
81
84
|
/** Diagnosis hint shown in critical alerts (e.g., 'Run "openclaw doctor"') */
|
|
82
85
|
diagnosisHint?: string;
|
|
86
|
+
/** Optional LLM enricher — adds smart summaries to alerts before dispatch */
|
|
87
|
+
enricher?: AlertEnricher;
|
|
83
88
|
};
|
|
84
89
|
export type OpenAlertsLogger = {
|
|
85
90
|
info: (msg: string) => void;
|
|
@@ -148,5 +153,5 @@ export declare const DEFAULTS: {
|
|
|
148
153
|
readonly pruneIntervalMs: number;
|
|
149
154
|
readonly platformFlushIntervalMs: number;
|
|
150
155
|
readonly platformBatchSize: 100;
|
|
151
|
-
readonly gatewayDownThresholdMs:
|
|
156
|
+
readonly gatewayDownThresholdMs: 30000;
|
|
152
157
|
};
|
package/dist/core/types.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OpenAlertsEngine } from "./core/index.js";
|
|
2
2
|
import { onDiagnosticEvent, registerLogTransport } from "openclaw/plugin-sdk";
|
|
3
3
|
import { createLogBridge } from "./plugin/log-bridge.js";
|
|
4
|
-
import { OpenClawAlertChannel, parseConfig, resolveAlertTarget, translateOpenClawEvent, translateToolCallHook, translateAgentStartHook, translateAgentEndHook, translateSessionStartHook, translateSessionEndHook, translateMessageSentHook, translateMessageReceivedHook, translateBeforeToolCallHook, translateBeforeCompactionHook, translateAfterCompactionHook, translateMessageSendingHook, translateToolResultPersistHook, translateGatewayStartHook, translateGatewayStopHook, } from "./plugin/adapter.js";
|
|
4
|
+
import { OpenClawAlertChannel, createOpenClawEnricher, parseConfig, resolveAlertTarget, translateOpenClawEvent, translateToolCallHook, translateAgentStartHook, translateAgentEndHook, translateSessionStartHook, translateSessionEndHook, translateMessageSentHook, translateMessageReceivedHook, translateBeforeToolCallHook, translateBeforeCompactionHook, translateAfterCompactionHook, translateMessageSendingHook, translateToolResultPersistHook, translateGatewayStartHook, translateGatewayStopHook, } from "./plugin/adapter.js";
|
|
5
5
|
import { bindEngine, createMonitorCommands } from "./plugin/commands.js";
|
|
6
6
|
import { createDashboardHandler, closeDashboardConnections, } from "./plugin/dashboard-routes.js";
|
|
7
7
|
const PLUGIN_ID = "openalerts";
|
|
@@ -13,12 +13,16 @@ let logBridgeCleanup = null;
|
|
|
13
13
|
function createMonitorService(api) {
|
|
14
14
|
return {
|
|
15
15
|
id: PLUGIN_ID,
|
|
16
|
-
start(ctx) {
|
|
16
|
+
async start(ctx) {
|
|
17
17
|
const logger = ctx.logger;
|
|
18
18
|
const config = parseConfig(api.pluginConfig);
|
|
19
19
|
// Resolve alert target + create OpenClaw alert channel
|
|
20
|
-
const target = resolveAlertTarget(api, config);
|
|
20
|
+
const target = await resolveAlertTarget(api, config);
|
|
21
21
|
const channels = target ? [new OpenClawAlertChannel(api, target)] : [];
|
|
22
|
+
// Create LLM enricher if enabled (default: false)
|
|
23
|
+
const enricher = config.llmEnriched === true
|
|
24
|
+
? createOpenClawEnricher(api, logger)
|
|
25
|
+
: null;
|
|
22
26
|
// Create and start the universal engine
|
|
23
27
|
engine = new OpenAlertsEngine({
|
|
24
28
|
stateDir: ctx.stateDir,
|
|
@@ -27,6 +31,7 @@ function createMonitorService(api) {
|
|
|
27
31
|
logger,
|
|
28
32
|
logPrefix: LOG_PREFIX,
|
|
29
33
|
diagnosisHint: 'Run "openclaw doctor" to diagnose.',
|
|
34
|
+
enricher: enricher ?? undefined,
|
|
30
35
|
});
|
|
31
36
|
engine.start();
|
|
32
37
|
// Wire commands to engine
|
|
@@ -169,7 +174,7 @@ function createMonitorService(api) {
|
|
|
169
174
|
const targetDesc = target
|
|
170
175
|
? `alerting to ${target.channel}:${target.to}`
|
|
171
176
|
: "log-only (no alert channel detected)";
|
|
172
|
-
logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active,
|
|
177
|
+
logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active, 8 rules active`);
|
|
173
178
|
},
|
|
174
179
|
stop() {
|
|
175
180
|
closeDashboardConnections();
|
package/dist/plugin/adapter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AlertChannel, AlertEvent, AlertTarget, MonitorConfig, OpenAlertsEvent } from "../core/index.js";
|
|
1
|
+
import type { AlertChannel, AlertEnricher, AlertEvent, AlertTarget, MonitorConfig, OpenAlertsEvent } from "../core/index.js";
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
3
|
/**
|
|
4
4
|
* Translate an OpenClaw diagnostic event into a universal OpenAlertsEvent.
|
|
@@ -128,11 +128,23 @@ export declare class OpenClawAlertChannel implements AlertChannel {
|
|
|
128
128
|
readonly name: string;
|
|
129
129
|
private api;
|
|
130
130
|
private target;
|
|
131
|
+
private warnedMissing;
|
|
131
132
|
constructor(api: OpenClawPluginApi, target: AlertTarget);
|
|
132
133
|
send(alert: AlertEvent, formatted: string): Promise<void>;
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
135
136
|
* Resolve the alert target from plugin config or by auto-detecting from OpenClaw config.
|
|
136
137
|
*/
|
|
137
|
-
export declare function resolveAlertTarget(api: OpenClawPluginApi, pluginConfig: MonitorConfig): AlertTarget | null
|
|
138
|
+
export declare function resolveAlertTarget(api: OpenClawPluginApi, pluginConfig: MonitorConfig): Promise<AlertTarget | null>;
|
|
138
139
|
export declare function parseConfig(raw: Record<string, unknown> | undefined): MonitorConfig;
|
|
140
|
+
/**
|
|
141
|
+
* Create an AlertEnricher from the OpenClaw plugin API.
|
|
142
|
+
* Reads the model from api.config.agents.defaults.model.primary (e.g. "openai/gpt-5-nano")
|
|
143
|
+
* and resolves the API key from process.env.
|
|
144
|
+
* Returns null if no model is configured or enricher can't be created.
|
|
145
|
+
*/
|
|
146
|
+
export declare function createOpenClawEnricher(api: OpenClawPluginApi, logger?: {
|
|
147
|
+
info: (msg: string) => void;
|
|
148
|
+
warn: (msg: string) => void;
|
|
149
|
+
error: (msg: string) => void;
|
|
150
|
+
}): AlertEnricher | null;
|
package/dist/plugin/adapter.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLlmEnricher, resolveApiKeyEnvVar } from "../core/llm-enrichment.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
1
5
|
// ─── Diagnostic Event Translation ───────────────────────────────────────────
|
|
2
6
|
//
|
|
3
7
|
// OpenClaw emits 12 diagnostic event types through onDiagnosticEvent():
|
|
@@ -230,7 +234,7 @@ export function translateMessageSentHook(data, context) {
|
|
|
230
234
|
sessionKey: context.sessionId,
|
|
231
235
|
outcome: data.success ? "success" : "error",
|
|
232
236
|
error: data.error,
|
|
233
|
-
meta: { to: data.to, source: "hook:message_sent" },
|
|
237
|
+
meta: { to: data.to, content: data.content, source: "hook:message_sent" },
|
|
234
238
|
};
|
|
235
239
|
}
|
|
236
240
|
/** Translate gateway_start hook data into OpenAlertsEvent. */
|
|
@@ -261,6 +265,7 @@ export function translateMessageReceivedHook(data, context) {
|
|
|
261
265
|
outcome: "success",
|
|
262
266
|
meta: {
|
|
263
267
|
from: data.from,
|
|
268
|
+
content: data.content,
|
|
264
269
|
accountId: context.accountId,
|
|
265
270
|
openclawHook: "message_received",
|
|
266
271
|
source: "hook:message_received",
|
|
@@ -310,6 +315,7 @@ export function translateAfterCompactionHook(data, context) {
|
|
|
310
315
|
messageCount: data.messageCount,
|
|
311
316
|
tokenCount: data.tokenCount,
|
|
312
317
|
compactedCount: data.compactedCount,
|
|
318
|
+
compaction: true,
|
|
313
319
|
openclawHook: "after_compaction",
|
|
314
320
|
source: "hook:after_compaction",
|
|
315
321
|
},
|
|
@@ -324,6 +330,7 @@ export function translateMessageSendingHook(data, context) {
|
|
|
324
330
|
outcome: "success",
|
|
325
331
|
meta: {
|
|
326
332
|
to: data.to,
|
|
333
|
+
content: data.content,
|
|
327
334
|
accountId: context.accountId,
|
|
328
335
|
openclawHook: "message_sending",
|
|
329
336
|
source: "hook:message_sending",
|
|
@@ -356,6 +363,7 @@ export class OpenClawAlertChannel {
|
|
|
356
363
|
name;
|
|
357
364
|
api;
|
|
358
365
|
target;
|
|
366
|
+
warnedMissing = false;
|
|
359
367
|
constructor(api, target) {
|
|
360
368
|
this.api = api;
|
|
361
369
|
this.target = target;
|
|
@@ -364,8 +372,13 @@ export class OpenClawAlertChannel {
|
|
|
364
372
|
async send(alert, formatted) {
|
|
365
373
|
const runtime = this.api.runtime;
|
|
366
374
|
const channel = runtime.channel;
|
|
367
|
-
if (!channel)
|
|
375
|
+
if (!channel) {
|
|
376
|
+
if (!this.warnedMissing) {
|
|
377
|
+
this.warnedMissing = true;
|
|
378
|
+
throw new Error(`runtime.channel not available — alert dropped`);
|
|
379
|
+
}
|
|
368
380
|
return;
|
|
381
|
+
}
|
|
369
382
|
const opts = this.target.accountId
|
|
370
383
|
? { accountId: this.target.accountId }
|
|
371
384
|
: {};
|
|
@@ -377,20 +390,22 @@ export class OpenClawAlertChannel {
|
|
|
377
390
|
signal: "sendMessageSignal",
|
|
378
391
|
};
|
|
379
392
|
const methodName = channelMethods[this.target.channel];
|
|
380
|
-
if (!methodName)
|
|
381
|
-
|
|
393
|
+
if (!methodName) {
|
|
394
|
+
throw new Error(`unsupported channel "${this.target.channel}" — no send method mapped`);
|
|
395
|
+
}
|
|
382
396
|
const channelMod = channel[this.target.channel];
|
|
383
397
|
const sendFn = channelMod?.[methodName];
|
|
384
|
-
if (sendFn) {
|
|
385
|
-
|
|
398
|
+
if (!sendFn) {
|
|
399
|
+
throw new Error(`${this.target.channel}.${methodName} not found on runtime — alert dropped`);
|
|
386
400
|
}
|
|
401
|
+
await sendFn(this.target.to, formatted, opts);
|
|
387
402
|
}
|
|
388
403
|
}
|
|
389
404
|
// ─── Alert Target Resolution ────────────────────────────────────────────────
|
|
390
405
|
/**
|
|
391
406
|
* Resolve the alert target from plugin config or by auto-detecting from OpenClaw config.
|
|
392
407
|
*/
|
|
393
|
-
export function resolveAlertTarget(api, pluginConfig) {
|
|
408
|
+
export async function resolveAlertTarget(api, pluginConfig) {
|
|
394
409
|
// 1. Explicit config
|
|
395
410
|
if (pluginConfig.alertChannel && pluginConfig.alertTo) {
|
|
396
411
|
return {
|
|
@@ -400,7 +415,8 @@ export function resolveAlertTarget(api, pluginConfig) {
|
|
|
400
415
|
};
|
|
401
416
|
}
|
|
402
417
|
const cfg = api.config;
|
|
403
|
-
|
|
418
|
+
const channelsCfg = cfg.channels ??
|
|
419
|
+
{};
|
|
404
420
|
const channelKeys = [
|
|
405
421
|
"telegram",
|
|
406
422
|
"discord",
|
|
@@ -408,14 +424,33 @@ export function resolveAlertTarget(api, pluginConfig) {
|
|
|
408
424
|
"whatsapp",
|
|
409
425
|
"signal",
|
|
410
426
|
];
|
|
427
|
+
// 2. Auto-detect from static allowFrom in channel config
|
|
411
428
|
for (const channelKey of channelKeys) {
|
|
412
|
-
const channelConfig =
|
|
429
|
+
const channelConfig = channelsCfg[channelKey];
|
|
413
430
|
if (!channelConfig || typeof channelConfig !== "object")
|
|
414
431
|
continue;
|
|
415
432
|
const target = extractFirstAllowFrom(channelKey, channelConfig);
|
|
416
433
|
if (target)
|
|
417
434
|
return target;
|
|
418
435
|
}
|
|
436
|
+
// 3. Auto-detect from pairing store (runtime-paired users)
|
|
437
|
+
// The store lives at ~/.openclaw/credentials/<channel>-allowFrom.json
|
|
438
|
+
const credDir = join(process.env.OPENCLAW_HOME ?? join(homedir(), ".openclaw"), "credentials");
|
|
439
|
+
for (const channelKey of channelKeys) {
|
|
440
|
+
const channelConfig = channelsCfg[channelKey];
|
|
441
|
+
if (!channelConfig || typeof channelConfig !== "object")
|
|
442
|
+
continue;
|
|
443
|
+
try {
|
|
444
|
+
const raw = await readFile(join(credDir, `${channelKey}-allowFrom.json`), "utf-8");
|
|
445
|
+
const data = JSON.parse(raw);
|
|
446
|
+
if (Array.isArray(data.allowFrom) && data.allowFrom.length > 0) {
|
|
447
|
+
return { channel: channelKey, to: String(data.allowFrom[0]) };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// File doesn't exist or isn't valid — skip this channel
|
|
452
|
+
}
|
|
453
|
+
}
|
|
419
454
|
return null;
|
|
420
455
|
}
|
|
421
456
|
function extractFirstAllowFrom(channel, channelConfig) {
|
|
@@ -451,8 +486,45 @@ export function parseConfig(raw) {
|
|
|
451
486
|
maxLogSizeKb: typeof raw.maxLogSizeKb === "number" ? raw.maxLogSizeKb : undefined,
|
|
452
487
|
maxLogAgeDays: typeof raw.maxLogAgeDays === "number" ? raw.maxLogAgeDays : undefined,
|
|
453
488
|
quiet: typeof raw.quiet === "boolean" ? raw.quiet : undefined,
|
|
489
|
+
llmEnriched: typeof raw.llmEnriched === "boolean" ? raw.llmEnriched : undefined,
|
|
454
490
|
rules: raw.rules && typeof raw.rules === "object"
|
|
455
491
|
? raw.rules
|
|
456
492
|
: undefined,
|
|
457
493
|
};
|
|
458
494
|
}
|
|
495
|
+
// ─── LLM Enricher Factory ───────────────────────────────────────────────────
|
|
496
|
+
/**
|
|
497
|
+
* Create an AlertEnricher from the OpenClaw plugin API.
|
|
498
|
+
* Reads the model from api.config.agents.defaults.model.primary (e.g. "openai/gpt-5-nano")
|
|
499
|
+
* and resolves the API key from process.env.
|
|
500
|
+
* Returns null if no model is configured or enricher can't be created.
|
|
501
|
+
*/
|
|
502
|
+
export function createOpenClawEnricher(api, logger) {
|
|
503
|
+
try {
|
|
504
|
+
const cfg = api.config;
|
|
505
|
+
const agents = cfg.agents;
|
|
506
|
+
const defaults = agents?.defaults;
|
|
507
|
+
const model = defaults?.model;
|
|
508
|
+
const primary = model?.primary;
|
|
509
|
+
if (typeof primary !== "string" || !primary.includes("/")) {
|
|
510
|
+
logger?.warn("openalerts: llm-enrichment skipped — no model configured at agents.defaults.model.primary");
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
// Resolve API key here (in adapter) to keep env access separate from network calls
|
|
514
|
+
const envVar = resolveApiKeyEnvVar(primary);
|
|
515
|
+
if (!envVar) {
|
|
516
|
+
logger?.warn("openalerts: llm-enrichment skipped — unknown provider");
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const apiKey = process.env[envVar];
|
|
520
|
+
if (!apiKey) {
|
|
521
|
+
logger?.warn(`openalerts: llm-enrichment skipped — ${envVar} not set in environment`);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
return createLlmEnricher({ modelString: primary, apiKey, logger });
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
logger?.warn(`openalerts: llm-enrichment setup failed: ${String(err)}`);
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
package/dist/plugin/commands.js
CHANGED
|
@@ -29,7 +29,7 @@ export function createMonitorCommands(api) {
|
|
|
29
29
|
handler: () => handleDashboard(),
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
name: "
|
|
32
|
+
name: "test_alert",
|
|
33
33
|
description: "Send a test alert to verify alert delivery",
|
|
34
34
|
acceptsArgs: false,
|
|
35
35
|
handler: () => handleTestAlert(),
|
|
@@ -72,26 +72,7 @@ function handleTestAlert() {
|
|
|
72
72
|
if (!_engine) {
|
|
73
73
|
return { text: "OpenAlerts not initialized yet. Wait for gateway startup." };
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
// This won't fire an actual alert unless the threshold (3 errors) is reached,
|
|
77
|
-
// so we fire a one-off test alert directly through the engine.
|
|
78
|
-
const testEvent = {
|
|
79
|
-
type: "alert",
|
|
80
|
-
id: `test:manual:${Date.now()}`,
|
|
81
|
-
ruleId: "test",
|
|
82
|
-
severity: "info",
|
|
83
|
-
title: "Test alert — delivery verified",
|
|
84
|
-
detail: "This is a test alert from /test-alert. If you see this, alert delivery is working.",
|
|
85
|
-
ts: Date.now(),
|
|
86
|
-
fingerprint: `test:manual`,
|
|
87
|
-
};
|
|
88
|
-
// Ingest as a custom event so it appears in the dashboard
|
|
89
|
-
_engine.ingest({
|
|
90
|
-
type: "custom",
|
|
91
|
-
ts: Date.now(),
|
|
92
|
-
outcome: "success",
|
|
93
|
-
meta: { openclawLog: "test_alert", source: "command:test-alert" },
|
|
94
|
-
});
|
|
75
|
+
_engine.sendTestAlert();
|
|
95
76
|
return {
|
|
96
77
|
text: "Test alert sent. Check your alert channel (Telegram/Discord/etc) for delivery confirmation.\n\nIf you don't receive it, check /health for channel status.",
|
|
97
78
|
};
|