@aight-cool/aight-utils 0.1.15 → 0.1.17
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/index.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bootstrap.ts +3 -1
- package/src/notif-prefs.ts +115 -0
- package/src/push-hook.ts +28 -2
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerPushHook } from "./src/push-hook.js";
|
|
|
14
14
|
import { registerHealth } from "./src/health.js";
|
|
15
15
|
import { registerVersion } from "./src/version.js";
|
|
16
16
|
import { registerGroupRpc } from "./src/groups.js";
|
|
17
|
+
import { registerNotifPrefsRPC } from "./src/notif-prefs.js";
|
|
17
18
|
|
|
18
19
|
const aightPlugin = {
|
|
19
20
|
id: "aight-utils",
|
|
@@ -68,6 +69,7 @@ const aightPlugin = {
|
|
|
68
69
|
registerHealth(api);
|
|
69
70
|
registerVersion(api);
|
|
70
71
|
registerGroupRpc(api);
|
|
72
|
+
registerNotifPrefsRPC(api);
|
|
71
73
|
|
|
72
74
|
api.logger.info("[aight-utils] Plugin loaded");
|
|
73
75
|
},
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/bootstrap.ts
CHANGED
|
@@ -256,7 +256,9 @@ export function registerBootstrap(api: OpenClawPluginApi) {
|
|
|
256
256
|
api.logger.info(`[aight-utils] api.on type: ${typeof api.on}`);
|
|
257
257
|
api.on("before_agent_start", (_event: unknown, ctx: { sessionKey?: string }) => {
|
|
258
258
|
// Inject AIGHT.md context into every agent session
|
|
259
|
-
api.logger.info(
|
|
259
|
+
api.logger.info(
|
|
260
|
+
`[aight-utils] Bootstrap: injecting AIGHT.md into session ${ctx?.sessionKey}`,
|
|
261
|
+
);
|
|
260
262
|
return { systemPrompt: AIGHT_MD };
|
|
261
263
|
});
|
|
262
264
|
api.logger.info("[aight-utils] Bootstrap hook registered (before_agent_start)");
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Preferences — gateway-side storage for push suppression.
|
|
3
|
+
*
|
|
4
|
+
* The app syncs notification preferences here via RPC. The push hook
|
|
5
|
+
* checks these prefs before sending to the relay — if a category is
|
|
6
|
+
* muted, the push never leaves the user's machine.
|
|
7
|
+
*
|
|
8
|
+
* RPC methods:
|
|
9
|
+
* aight.notif.setPrefs — update preferences
|
|
10
|
+
* aight.notif.getPrefs — read current preferences
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import type { OpenClawPluginApi, GatewayRequestHandlerOptions } from "openclaw/plugin-sdk";
|
|
17
|
+
|
|
18
|
+
// ── Types ──
|
|
19
|
+
|
|
20
|
+
export interface NotifPrefs {
|
|
21
|
+
globalMute: boolean;
|
|
22
|
+
muteUntil: string | null;
|
|
23
|
+
agentReplies: boolean;
|
|
24
|
+
groupChat: boolean;
|
|
25
|
+
cron: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULTS: NotifPrefs = {
|
|
29
|
+
globalMute: false,
|
|
30
|
+
muteUntil: null,
|
|
31
|
+
agentReplies: true,
|
|
32
|
+
groupChat: true,
|
|
33
|
+
cron: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Storage ──
|
|
37
|
+
|
|
38
|
+
const PREFS_DIR = path.join(os.homedir(), ".openclaw", "aight");
|
|
39
|
+
const PREFS_FILE = path.join(PREFS_DIR, "notif-prefs.json");
|
|
40
|
+
|
|
41
|
+
export function loadNotifPrefs(): NotifPrefs {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(PREFS_FILE)) return { ...DEFAULTS };
|
|
44
|
+
const raw = fs.readFileSync(PREFS_FILE, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
return { ...DEFAULTS, ...parsed };
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...DEFAULTS };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveNotifPrefs(prefs: NotifPrefs): void {
|
|
53
|
+
fs.mkdirSync(PREFS_DIR, { recursive: true, mode: 0o700 });
|
|
54
|
+
fs.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2), "utf-8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Classification (matches app-side logic) ──
|
|
58
|
+
|
|
59
|
+
export function classifySessionKey(sessionKey: string): "agentReplies" | "groupChat" | "cron" {
|
|
60
|
+
if (sessionKey.includes(":cron:")) return "cron";
|
|
61
|
+
if (sessionKey.includes(":group-chat:")) return "groupChat";
|
|
62
|
+
return "agentReplies";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a push for this sessionKey should be sent.
|
|
67
|
+
*/
|
|
68
|
+
export function shouldSendPush(sessionKey: string): boolean {
|
|
69
|
+
if (!sessionKey) return true; // fail open
|
|
70
|
+
|
|
71
|
+
const prefs = loadNotifPrefs();
|
|
72
|
+
|
|
73
|
+
// Global mute
|
|
74
|
+
if (prefs.globalMute) {
|
|
75
|
+
if (!prefs.muteUntil) return false; // indefinite
|
|
76
|
+
const expiry = new Date(prefs.muteUntil).getTime();
|
|
77
|
+
if (Date.now() < expiry) return false;
|
|
78
|
+
// Mute expired — clear it
|
|
79
|
+
prefs.globalMute = false;
|
|
80
|
+
prefs.muteUntil = null;
|
|
81
|
+
saveNotifPrefs(prefs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Category check
|
|
85
|
+
const category = classifySessionKey(sessionKey);
|
|
86
|
+
return prefs[category];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── RPC Registration ──
|
|
90
|
+
|
|
91
|
+
export function registerNotifPrefsRPC(api: OpenClawPluginApi) {
|
|
92
|
+
api.registerGatewayMethod("aight.notif.getPrefs", ({ respond }: GatewayRequestHandlerOptions) => {
|
|
93
|
+
respond(true, loadNotifPrefs());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
api.registerGatewayMethod(
|
|
97
|
+
"aight.notif.setPrefs",
|
|
98
|
+
({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
99
|
+
const update = (params ?? {}) as Partial<NotifPrefs>;
|
|
100
|
+
const current = loadNotifPrefs();
|
|
101
|
+
|
|
102
|
+
if (typeof update.globalMute === "boolean") current.globalMute = update.globalMute;
|
|
103
|
+
if (typeof update.agentReplies === "boolean") current.agentReplies = update.agentReplies;
|
|
104
|
+
if (typeof update.groupChat === "boolean") current.groupChat = update.groupChat;
|
|
105
|
+
if (typeof update.cron === "boolean") current.cron = update.cron;
|
|
106
|
+
if (update.muteUntil !== undefined) current.muteUntil = update.muteUntil;
|
|
107
|
+
|
|
108
|
+
saveNotifPrefs(current);
|
|
109
|
+
api.logger.info(`[aight-utils] Notification prefs updated: ${JSON.stringify(current)}`);
|
|
110
|
+
respond(true, current);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
api.logger.info("[aight-utils] Notification prefs RPC registered");
|
|
115
|
+
}
|
package/src/push-hook.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
6
6
|
import { getPluginConfig } from "./config.js";
|
|
7
|
-
import { sendPush, loadTokens } from "./push.js";
|
|
7
|
+
import { sendPush, loadTokens, unregisterToken } from "./push.js";
|
|
8
8
|
import { loadGroupName } from "./groups.js";
|
|
9
|
+
import { shouldSendPush } from "./notif-prefs.js";
|
|
9
10
|
|
|
10
11
|
export function registerPushHook(api: OpenClawPluginApi) {
|
|
11
12
|
try {
|
|
@@ -106,10 +107,18 @@ export function registerPushHook(api: OpenClawPluginApi) {
|
|
|
106
107
|
|
|
107
108
|
const cleanBody = preview.trim().replace(/\n+/g, " ").trim();
|
|
108
109
|
|
|
110
|
+
// ── Notification preference gate ──
|
|
111
|
+
// Check if this category is muted — if so, don't send the push at all.
|
|
112
|
+
const sk_ = ctx.sessionKey ?? "";
|
|
113
|
+
if (!shouldSendPush(sk_)) {
|
|
114
|
+
api.logger.info(`[aight-utils] Push suppressed by notification prefs: ${sk_}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
for (const device of tokens) {
|
|
110
119
|
if (!device.sendKey) continue;
|
|
111
120
|
try {
|
|
112
|
-
await sendPush(
|
|
121
|
+
const pushResult = await sendPush(
|
|
113
122
|
device.deviceId,
|
|
114
123
|
{
|
|
115
124
|
title: pushTitle.trim(),
|
|
@@ -119,6 +128,23 @@ export function registerPushHook(api: OpenClawPluginApi) {
|
|
|
119
128
|
},
|
|
120
129
|
freshConfig,
|
|
121
130
|
);
|
|
131
|
+
api.logger.info(
|
|
132
|
+
`[aight-utils] Push sent: session=${ctx.sessionKey} device=${device.deviceId} ok=${pushResult.ok}${pushResult.error ? ` error=${pushResult.error}` : ""}`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Auto-prune stale tokens — if the relay rejects the token, remove it
|
|
136
|
+
if (!pushResult.ok && pushResult.error) {
|
|
137
|
+
const err = pushResult.error.toLowerCase();
|
|
138
|
+
if (
|
|
139
|
+
err.includes("baddevicetoken") ||
|
|
140
|
+
err.includes("unregistered") ||
|
|
141
|
+
err.includes("devicetokennotfortopic") ||
|
|
142
|
+
err.includes("expired")
|
|
143
|
+
) {
|
|
144
|
+
api.logger.info(`[aight-utils] Pruning stale device token: ${device.deviceId}`);
|
|
145
|
+
unregisterToken(device.deviceId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
122
148
|
} catch (err) {
|
|
123
149
|
api.logger.warn(
|
|
124
150
|
`[aight-utils] Push failed: ${err instanceof Error ? err.message : String(err)}`,
|