mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 08:51:10 +00:00
feat(gateway): enable Android notify + notification events
This commit is contained in:
@@ -347,7 +347,7 @@ describe("resolveNodeCommandAllowlist", () => {
|
||||
expect(allow.has("notifications.actions")).toBe(true);
|
||||
expect(allow.has("device.permissions")).toBe(true);
|
||||
expect(allow.has("device.health")).toBe(true);
|
||||
expect(allow.has("system.notify")).toBe(false);
|
||||
expect(allow.has("system.notify")).toBe(true);
|
||||
});
|
||||
|
||||
it("can explicitly allow dangerous commands via allowCommands", () => {
|
||||
|
||||
@@ -82,6 +82,7 @@ const PLATFORM_DEFAULTS: Record<string, string[]> = {
|
||||
...CAMERA_COMMANDS,
|
||||
...LOCATION_COMMANDS,
|
||||
...ANDROID_NOTIFICATION_COMMANDS,
|
||||
NODE_SYSTEM_NOTIFY_COMMAND,
|
||||
...ANDROID_DEVICE_COMMANDS,
|
||||
...CONTACTS_COMMANDS,
|
||||
...CALENDAR_COMMANDS,
|
||||
|
||||
@@ -351,6 +351,64 @@ describe("voice transcript events", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifications changed events", () => {
|
||||
beforeEach(() => {
|
||||
enqueueSystemEventMock.mockClear();
|
||||
requestHeartbeatNowMock.mockClear();
|
||||
});
|
||||
|
||||
it("enqueues notifications.changed posted events", async () => {
|
||||
const ctx = buildCtx();
|
||||
await handleNodeEvent(ctx, "node-n1", {
|
||||
event: "notifications.changed",
|
||||
payloadJSON: JSON.stringify({
|
||||
change: "posted",
|
||||
key: "notif-1",
|
||||
packageName: "com.example.chat",
|
||||
title: "Message",
|
||||
text: "Ping from Alex",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
|
||||
"Notification posted (node=node-n1 key=notif-1 package=com.example.chat): Message - Ping from Alex",
|
||||
{ sessionKey: "node-node-n1", contextKey: "notification:notif-1" },
|
||||
);
|
||||
expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "notifications-event" });
|
||||
});
|
||||
|
||||
it("enqueues notifications.changed removed events", async () => {
|
||||
const ctx = buildCtx();
|
||||
await handleNodeEvent(ctx, "node-n2", {
|
||||
event: "notifications.changed",
|
||||
payloadJSON: JSON.stringify({
|
||||
change: "removed",
|
||||
key: "notif-2",
|
||||
packageName: "com.example.mail",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
|
||||
"Notification removed (node=node-n2 key=notif-2 package=com.example.mail)",
|
||||
{ sessionKey: "node-node-n2", contextKey: "notification:notif-2" },
|
||||
);
|
||||
expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "notifications-event" });
|
||||
});
|
||||
|
||||
it("ignores notifications.changed payloads missing required fields", async () => {
|
||||
const ctx = buildCtx();
|
||||
await handleNodeEvent(ctx, "node-n3", {
|
||||
event: "notifications.changed",
|
||||
payloadJSON: JSON.stringify({
|
||||
change: "posted",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
|
||||
expect(requestHeartbeatNowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent request events", () => {
|
||||
beforeEach(() => {
|
||||
agentCommandMock.mockClear();
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { formatForLog } from "./ws-log.js";
|
||||
|
||||
const MAX_EXEC_EVENT_OUTPUT_CHARS = 180;
|
||||
const MAX_NOTIFICATION_EVENT_TEXT_CHARS = 120;
|
||||
const VOICE_TRANSCRIPT_DEDUPE_WINDOW_MS = 1500;
|
||||
const MAX_RECENT_VOICE_TRANSCRIPTS = 200;
|
||||
|
||||
@@ -122,6 +123,18 @@ function compactExecEventOutput(raw: string) {
|
||||
return `${normalized.slice(0, safe)}…`;
|
||||
}
|
||||
|
||||
function compactNotificationEventText(raw: string) {
|
||||
const normalized = raw.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
if (normalized.length <= MAX_NOTIFICATION_EVENT_TEXT_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
const safe = Math.max(1, MAX_NOTIFICATION_EVENT_TEXT_CHARS - 1);
|
||||
return `${normalized.slice(0, safe)}…`;
|
||||
}
|
||||
|
||||
type LoadedSessionEntry = ReturnType<typeof loadSessionEntry>;
|
||||
|
||||
async function touchSessionStore(params: {
|
||||
@@ -441,6 +454,40 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "notifications.changed": {
|
||||
const obj = parsePayloadObject(evt.payloadJSON);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
const change = normalizeNonEmptyString(obj.change)?.toLowerCase();
|
||||
if (change !== "posted" && change !== "removed") {
|
||||
return;
|
||||
}
|
||||
const key = normalizeNonEmptyString(obj.key);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const sessionKey = normalizeNonEmptyString(obj.sessionKey) ?? `node-${nodeId}`;
|
||||
const packageName = normalizeNonEmptyString(obj.packageName);
|
||||
const title = compactNotificationEventText(normalizeNonEmptyString(obj.title) ?? "");
|
||||
const text = compactNotificationEventText(normalizeNonEmptyString(obj.text) ?? "");
|
||||
|
||||
let summary = `Notification ${change} (node=${nodeId} key=${key}`;
|
||||
if (packageName) {
|
||||
summary += ` package=${packageName}`;
|
||||
}
|
||||
summary += ")";
|
||||
if (change === "posted") {
|
||||
const messageParts = [title, text].filter(Boolean);
|
||||
if (messageParts.length > 0) {
|
||||
summary += `: ${messageParts.join(" - ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
enqueueSystemEvent(summary, { sessionKey, contextKey: `notification:${key}` });
|
||||
requestHeartbeatNow({ reason: "notifications-event" });
|
||||
return;
|
||||
}
|
||||
case "chat.subscribe": {
|
||||
if (!evt.payloadJSON) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user