mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
feat(nodes): expose device diagnostics and notification actions
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<typeof readGatewayCallOptions>;
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user