diff --git a/docs/tools/index.md b/docs/tools/index.md index 0a9024880c4..9b9a4586cdc 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -355,8 +355,8 @@ Core actions: - `notify` (macOS `system.notify`) - `run` (macOS `system.run`) - `camera_list`, `camera_snap`, `camera_clip`, `screen_record` -- `location_get`, `notifications_list` -- `device_status`, `device_info` +- `location_get`, `notifications_list`, `notifications_action` +- `device_status`, `device_info`, `device_permissions`, `device_health` Notes: diff --git a/src/agents/openclaw-tools.camera.test.ts b/src/agents/openclaw-tools.camera.test.ts index b6d9e315656..0101d5f2d3f 100644 --- a/src/agents/openclaw-tools.camera.test.ts +++ b/src/agents/openclaw-tools.camera.test.ts @@ -174,6 +174,40 @@ describe("nodes notifications_list", () => { }); }); +describe("nodes notifications_action", () => { + it("invokes notifications.actions dismiss", async () => { + callGateway.mockImplementation(async ({ method, params }) => { + if (method === "node.list") { + return mockNodeList(["notifications.actions"]); + } + if (method === "node.invoke") { + expect(params).toMatchObject({ + nodeId: NODE_ID, + command: "notifications.actions", + params: { + key: "n1", + action: "dismiss", + }, + }); + return { payload: { ok: true, key: "n1", action: "dismiss" } }; + } + return unexpectedGatewayMethod(method); + }); + + const result = await executeNodes({ + action: "notifications_action", + node: NODE_ID, + notificationKey: "n1", + notificationAction: "dismiss", + }); + + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringContaining('"dismiss"'), + }); + }); +}); + describe("nodes device_status and device_info", () => { it("invokes device.status and returns payload", async () => { callGateway.mockImplementation(async ({ method, params }) => { @@ -237,6 +271,71 @@ describe("nodes device_status and device_info", () => { text: expect.stringContaining('"systemName"'), }); }); + + it("invokes device.permissions and returns payload", async () => { + callGateway.mockImplementation(async ({ method, params }) => { + if (method === "node.list") { + return mockNodeList(["device.permissions"]); + } + if (method === "node.invoke") { + expect(params).toMatchObject({ + nodeId: NODE_ID, + command: "device.permissions", + params: {}, + }); + return { + payload: { + permissions: { + camera: { status: "granted", promptable: false }, + }, + }, + }; + } + return unexpectedGatewayMethod(method); + }); + + const result = await executeNodes({ + action: "device_permissions", + node: NODE_ID, + }); + + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringContaining('"permissions"'), + }); + }); + + it("invokes device.health and returns payload", async () => { + callGateway.mockImplementation(async ({ method, params }) => { + if (method === "node.list") { + return mockNodeList(["device.health"]); + } + if (method === "node.invoke") { + expect(params).toMatchObject({ + nodeId: NODE_ID, + command: "device.health", + params: {}, + }); + return { + payload: { + memory: { pressure: "normal" }, + battery: { chargingType: "usb" }, + }, + }; + } + return unexpectedGatewayMethod(method); + }); + + const result = await executeNodes({ + action: "device_health", + node: NODE_ID, + }); + + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringContaining('"memory"'), + }); + }); }); describe("nodes run", () => { diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index a2f347e0e52..b16744057d1 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -42,14 +42,18 @@ const NODES_TOOL_ACTIONS = [ "screen_record", "location_get", "notifications_list", + "notifications_action", "device_status", "device_info", + "device_permissions", + "device_health", "run", "invoke", ] as const; const NOTIFY_PRIORITIES = ["passive", "active", "timeSensitive"] as const; const NOTIFY_DELIVERIES = ["system", "overlay", "auto"] as const; +const NOTIFICATIONS_ACTIONS = ["open", "dismiss", "reply"] as const; const CAMERA_FACING = ["front", "back", "both"] as const; const LOCATION_ACCURACY = ["coarse", "balanced", "precise"] as const; type GatewayCallOptions = ReturnType; @@ -117,6 +121,10 @@ const NodesToolSchema = Type.Object({ maxAgeMs: Type.Optional(Type.Number()), locationTimeoutMs: Type.Optional(Type.Number()), desiredAccuracy: optionalStringEnum(LOCATION_ACCURACY), + // notifications_action + notificationAction: optionalStringEnum(NOTIFICATIONS_ACTIONS), + notificationKey: Type.Optional(Type.String()), + notificationReplyText: Type.Optional(Type.String()), // run command: Type.Optional(Type.Array(Type.String())), cwd: Type.Optional(Type.String()), @@ -302,7 +310,9 @@ export function createNodesTool(options?: { case "camera_list": case "notifications_list": case "device_status": - case "device_info": { + case "device_info": + case "device_permissions": + case "device_health": { const node = readStringParam(params, "node", { required: true }); const command = action === "camera_list" @@ -311,7 +321,11 @@ export function createNodesTool(options?: { ? "notifications.list" : action === "device_status" ? "device.status" - : "device.info"; + : action === "device_info" + ? "device.info" + : action === "device_permissions" + ? "device.permissions" + : "device.health"; const payloadRaw = await invokeNodeCommandPayload({ gatewayOpts, node, @@ -321,6 +335,41 @@ export function createNodesTool(options?: { payloadRaw && typeof payloadRaw === "object" && payloadRaw !== null ? payloadRaw : {}; return jsonResult(payload); } + case "notifications_action": { + const node = readStringParam(params, "node", { required: true }); + const notificationKey = readStringParam(params, "notificationKey", { required: true }); + const notificationAction = + typeof params.notificationAction === "string" + ? params.notificationAction.trim().toLowerCase() + : ""; + if ( + notificationAction !== "open" && + notificationAction !== "dismiss" && + notificationAction !== "reply" + ) { + throw new Error("notificationAction must be open|dismiss|reply"); + } + const notificationReplyText = + typeof params.notificationReplyText === "string" + ? params.notificationReplyText.trim() + : undefined; + if (notificationAction === "reply" && !notificationReplyText) { + throw new Error("notificationReplyText required when notificationAction=reply"); + } + const payloadRaw = await invokeNodeCommandPayload({ + gatewayOpts, + node, + command: "notifications.actions", + commandParams: { + key: notificationKey, + action: notificationAction, + replyText: notificationReplyText, + }, + }); + const payload = + payloadRaw && typeof payloadRaw === "object" && payloadRaw !== null ? payloadRaw : {}; + return jsonResult(payload); + } case "camera_clip": { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node);