mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-02-01 03:47:45 +01:00
fix: add node tool failure context
This commit is contained in:
@@ -9,6 +9,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
|
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
|
||||||
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
|
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
|
||||||
|
|
||||||
## 2026.1.20
|
## 2026.1.20
|
||||||
|
|||||||
@@ -112,334 +112,349 @@ export function createNodesTool(options?: {
|
|||||||
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (action) {
|
try {
|
||||||
case "status":
|
switch (action) {
|
||||||
return jsonResult(await callGatewayTool("node.list", gatewayOpts, {}));
|
case "status":
|
||||||
case "describe": {
|
return jsonResult(await callGatewayTool("node.list", gatewayOpts, {}));
|
||||||
const node = readStringParam(params, "node", { required: true });
|
case "describe": {
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
const node = readStringParam(params, "node", { required: true });
|
||||||
return jsonResult(await callGatewayTool("node.describe", gatewayOpts, { nodeId }));
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
}
|
return jsonResult(await callGatewayTool("node.describe", gatewayOpts, { nodeId }));
|
||||||
case "pending":
|
|
||||||
return jsonResult(await callGatewayTool("node.pair.list", gatewayOpts, {}));
|
|
||||||
case "approve": {
|
|
||||||
const requestId = readStringParam(params, "requestId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await callGatewayTool("node.pair.approve", gatewayOpts, {
|
|
||||||
requestId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "reject": {
|
|
||||||
const requestId = readStringParam(params, "requestId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await callGatewayTool("node.pair.reject", gatewayOpts, {
|
|
||||||
requestId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "notify": {
|
|
||||||
const node = readStringParam(params, "node", { required: true });
|
|
||||||
const title = typeof params.title === "string" ? params.title : "";
|
|
||||||
const body = typeof params.body === "string" ? params.body : "";
|
|
||||||
if (!title.trim() && !body.trim()) {
|
|
||||||
throw new Error("title or body required");
|
|
||||||
}
|
}
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
case "pending":
|
||||||
await callGatewayTool("node.invoke", gatewayOpts, {
|
return jsonResult(await callGatewayTool("node.pair.list", gatewayOpts, {}));
|
||||||
nodeId,
|
case "approve": {
|
||||||
command: "system.notify",
|
const requestId = readStringParam(params, "requestId", {
|
||||||
params: {
|
required: true,
|
||||||
title: title.trim() || undefined,
|
});
|
||||||
body: body.trim() || undefined,
|
return jsonResult(
|
||||||
sound: typeof params.sound === "string" ? params.sound : undefined,
|
await callGatewayTool("node.pair.approve", gatewayOpts, {
|
||||||
priority: typeof params.priority === "string" ? params.priority : undefined,
|
requestId,
|
||||||
delivery: typeof params.delivery === "string" ? params.delivery : undefined,
|
}),
|
||||||
},
|
);
|
||||||
idempotencyKey: crypto.randomUUID(),
|
}
|
||||||
});
|
case "reject": {
|
||||||
return jsonResult({ ok: true });
|
const requestId = readStringParam(params, "requestId", {
|
||||||
}
|
required: true,
|
||||||
case "camera_snap": {
|
});
|
||||||
const node = readStringParam(params, "node", { required: true });
|
return jsonResult(
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
await callGatewayTool("node.pair.reject", gatewayOpts, {
|
||||||
const facingRaw =
|
requestId,
|
||||||
typeof params.facing === "string" ? params.facing.toLowerCase() : "both";
|
}),
|
||||||
const facings: CameraFacing[] =
|
);
|
||||||
facingRaw === "both"
|
}
|
||||||
? ["front", "back"]
|
case "notify": {
|
||||||
: facingRaw === "front" || facingRaw === "back"
|
const node = readStringParam(params, "node", { required: true });
|
||||||
? [facingRaw]
|
const title = typeof params.title === "string" ? params.title : "";
|
||||||
: (() => {
|
const body = typeof params.body === "string" ? params.body : "";
|
||||||
throw new Error("invalid facing (front|back|both)");
|
if (!title.trim() && !body.trim()) {
|
||||||
})();
|
throw new Error("title or body required");
|
||||||
const maxWidth =
|
}
|
||||||
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
? params.maxWidth
|
await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
: undefined;
|
nodeId,
|
||||||
const quality =
|
command: "system.notify",
|
||||||
typeof params.quality === "number" && Number.isFinite(params.quality)
|
params: {
|
||||||
? params.quality
|
title: title.trim() || undefined,
|
||||||
: undefined;
|
body: body.trim() || undefined,
|
||||||
const delayMs =
|
sound: typeof params.sound === "string" ? params.sound : undefined,
|
||||||
typeof params.delayMs === "number" && Number.isFinite(params.delayMs)
|
priority: typeof params.priority === "string" ? params.priority : undefined,
|
||||||
? params.delayMs
|
delivery: typeof params.delivery === "string" ? params.delivery : undefined,
|
||||||
: undefined;
|
},
|
||||||
const deviceId =
|
idempotencyKey: crypto.randomUUID(),
|
||||||
typeof params.deviceId === "string" && params.deviceId.trim()
|
});
|
||||||
? params.deviceId.trim()
|
return jsonResult({ ok: true });
|
||||||
: undefined;
|
}
|
||||||
|
case "camera_snap": {
|
||||||
|
const node = readStringParam(params, "node", { required: true });
|
||||||
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
|
const facingRaw =
|
||||||
|
typeof params.facing === "string" ? params.facing.toLowerCase() : "both";
|
||||||
|
const facings: CameraFacing[] =
|
||||||
|
facingRaw === "both"
|
||||||
|
? ["front", "back"]
|
||||||
|
: facingRaw === "front" || facingRaw === "back"
|
||||||
|
? [facingRaw]
|
||||||
|
: (() => {
|
||||||
|
throw new Error("invalid facing (front|back|both)");
|
||||||
|
})();
|
||||||
|
const maxWidth =
|
||||||
|
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
||||||
|
? params.maxWidth
|
||||||
|
: undefined;
|
||||||
|
const quality =
|
||||||
|
typeof params.quality === "number" && Number.isFinite(params.quality)
|
||||||
|
? params.quality
|
||||||
|
: undefined;
|
||||||
|
const delayMs =
|
||||||
|
typeof params.delayMs === "number" && Number.isFinite(params.delayMs)
|
||||||
|
? params.delayMs
|
||||||
|
: undefined;
|
||||||
|
const deviceId =
|
||||||
|
typeof params.deviceId === "string" && params.deviceId.trim()
|
||||||
|
? params.deviceId.trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const content: AgentToolResult<unknown>["content"] = [];
|
const content: AgentToolResult<unknown>["content"] = [];
|
||||||
const details: Array<Record<string, unknown>> = [];
|
const details: Array<Record<string, unknown>> = [];
|
||||||
|
|
||||||
for (const facing of facings) {
|
for (const facing of facings) {
|
||||||
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
|
nodeId,
|
||||||
|
command: "camera.snap",
|
||||||
|
params: {
|
||||||
|
facing,
|
||||||
|
maxWidth,
|
||||||
|
quality,
|
||||||
|
format: "jpg",
|
||||||
|
delayMs,
|
||||||
|
deviceId,
|
||||||
|
},
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
})) as { payload?: unknown };
|
||||||
|
const payload = parseCameraSnapPayload(raw?.payload);
|
||||||
|
const normalizedFormat = payload.format.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalizedFormat !== "jpg" &&
|
||||||
|
normalizedFormat !== "jpeg" &&
|
||||||
|
normalizedFormat !== "png"
|
||||||
|
) {
|
||||||
|
throw new Error(`unsupported camera.snap format: ${payload.format}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isJpeg = normalizedFormat === "jpg" || normalizedFormat === "jpeg";
|
||||||
|
const filePath = cameraTempPath({
|
||||||
|
kind: "snap",
|
||||||
|
facing,
|
||||||
|
ext: isJpeg ? "jpg" : "png",
|
||||||
|
});
|
||||||
|
await writeBase64ToFile(filePath, payload.base64);
|
||||||
|
content.push({ type: "text", text: `MEDIA:${filePath}` });
|
||||||
|
content.push({
|
||||||
|
type: "image",
|
||||||
|
data: payload.base64,
|
||||||
|
mimeType:
|
||||||
|
imageMimeFromFormat(payload.format) ?? (isJpeg ? "image/jpeg" : "image/png"),
|
||||||
|
});
|
||||||
|
details.push({
|
||||||
|
facing,
|
||||||
|
path: filePath,
|
||||||
|
width: payload.width,
|
||||||
|
height: payload.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: AgentToolResult<unknown> = { content, details };
|
||||||
|
return await sanitizeToolResultImages(result, "nodes:camera_snap");
|
||||||
|
}
|
||||||
|
case "camera_list": {
|
||||||
|
const node = readStringParam(params, "node", { required: true });
|
||||||
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
nodeId,
|
nodeId,
|
||||||
command: "camera.snap",
|
command: "camera.list",
|
||||||
|
params: {},
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
})) as { payload?: unknown };
|
||||||
|
const payload =
|
||||||
|
raw && typeof raw.payload === "object" && raw.payload !== null ? raw.payload : {};
|
||||||
|
return jsonResult(payload);
|
||||||
|
}
|
||||||
|
case "camera_clip": {
|
||||||
|
const node = readStringParam(params, "node", { required: true });
|
||||||
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
|
const facing =
|
||||||
|
typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
|
||||||
|
if (facing !== "front" && facing !== "back") {
|
||||||
|
throw new Error("invalid facing (front|back)");
|
||||||
|
}
|
||||||
|
const durationMs =
|
||||||
|
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
||||||
|
? params.durationMs
|
||||||
|
: typeof params.duration === "string"
|
||||||
|
? parseDurationMs(params.duration)
|
||||||
|
: 3000;
|
||||||
|
const includeAudio =
|
||||||
|
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
||||||
|
const deviceId =
|
||||||
|
typeof params.deviceId === "string" && params.deviceId.trim()
|
||||||
|
? params.deviceId.trim()
|
||||||
|
: undefined;
|
||||||
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
|
nodeId,
|
||||||
|
command: "camera.clip",
|
||||||
params: {
|
params: {
|
||||||
facing,
|
facing,
|
||||||
maxWidth,
|
durationMs,
|
||||||
quality,
|
includeAudio,
|
||||||
format: "jpg",
|
format: "mp4",
|
||||||
delayMs,
|
|
||||||
deviceId,
|
deviceId,
|
||||||
},
|
},
|
||||||
idempotencyKey: crypto.randomUUID(),
|
idempotencyKey: crypto.randomUUID(),
|
||||||
})) as { payload?: unknown };
|
})) as { payload?: unknown };
|
||||||
const payload = parseCameraSnapPayload(raw?.payload);
|
const payload = parseCameraClipPayload(raw?.payload);
|
||||||
const normalizedFormat = payload.format.toLowerCase();
|
|
||||||
if (
|
|
||||||
normalizedFormat !== "jpg" &&
|
|
||||||
normalizedFormat !== "jpeg" &&
|
|
||||||
normalizedFormat !== "png"
|
|
||||||
) {
|
|
||||||
throw new Error(`unsupported camera.snap format: ${payload.format}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isJpeg = normalizedFormat === "jpg" || normalizedFormat === "jpeg";
|
|
||||||
const filePath = cameraTempPath({
|
const filePath = cameraTempPath({
|
||||||
kind: "snap",
|
kind: "clip",
|
||||||
facing,
|
facing,
|
||||||
ext: isJpeg ? "jpg" : "png",
|
ext: payload.format,
|
||||||
});
|
});
|
||||||
await writeBase64ToFile(filePath, payload.base64);
|
await writeBase64ToFile(filePath, payload.base64);
|
||||||
content.push({ type: "text", text: `MEDIA:${filePath}` });
|
return {
|
||||||
content.push({
|
content: [{ type: "text", text: `FILE:${filePath}` }],
|
||||||
type: "image",
|
details: {
|
||||||
data: payload.base64,
|
facing,
|
||||||
mimeType:
|
path: filePath,
|
||||||
imageMimeFromFormat(payload.format) ?? (isJpeg ? "image/jpeg" : "image/png"),
|
durationMs: payload.durationMs,
|
||||||
});
|
hasAudio: payload.hasAudio,
|
||||||
details.push({
|
},
|
||||||
facing,
|
};
|
||||||
path: filePath,
|
|
||||||
width: payload.width,
|
|
||||||
height: payload.height,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
case "screen_record": {
|
||||||
const result: AgentToolResult<unknown> = { content, details };
|
const node = readStringParam(params, "node", { required: true });
|
||||||
return await sanitizeToolResultImages(result, "nodes:camera_snap");
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
|
const durationMs =
|
||||||
|
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
||||||
|
? params.durationMs
|
||||||
|
: typeof params.duration === "string"
|
||||||
|
? parseDurationMs(params.duration)
|
||||||
|
: 10_000;
|
||||||
|
const fps =
|
||||||
|
typeof params.fps === "number" && Number.isFinite(params.fps) ? params.fps : 10;
|
||||||
|
const screenIndex =
|
||||||
|
typeof params.screenIndex === "number" && Number.isFinite(params.screenIndex)
|
||||||
|
? params.screenIndex
|
||||||
|
: 0;
|
||||||
|
const includeAudio =
|
||||||
|
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
||||||
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
|
nodeId,
|
||||||
|
command: "screen.record",
|
||||||
|
params: {
|
||||||
|
durationMs,
|
||||||
|
screenIndex,
|
||||||
|
fps,
|
||||||
|
format: "mp4",
|
||||||
|
includeAudio,
|
||||||
|
},
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
})) as { payload?: unknown };
|
||||||
|
const payload = parseScreenRecordPayload(raw?.payload);
|
||||||
|
const filePath =
|
||||||
|
typeof params.outPath === "string" && params.outPath.trim()
|
||||||
|
? params.outPath.trim()
|
||||||
|
: screenRecordTempPath({ ext: payload.format || "mp4" });
|
||||||
|
const written = await writeScreenRecordToFile(filePath, payload.base64);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `FILE:${written.path}` }],
|
||||||
|
details: {
|
||||||
|
path: written.path,
|
||||||
|
durationMs: payload.durationMs,
|
||||||
|
fps: payload.fps,
|
||||||
|
screenIndex: payload.screenIndex,
|
||||||
|
hasAudio: payload.hasAudio,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "location_get": {
|
||||||
|
const node = readStringParam(params, "node", { required: true });
|
||||||
|
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||||
|
const maxAgeMs =
|
||||||
|
typeof params.maxAgeMs === "number" && Number.isFinite(params.maxAgeMs)
|
||||||
|
? params.maxAgeMs
|
||||||
|
: undefined;
|
||||||
|
const desiredAccuracy =
|
||||||
|
params.desiredAccuracy === "coarse" ||
|
||||||
|
params.desiredAccuracy === "balanced" ||
|
||||||
|
params.desiredAccuracy === "precise"
|
||||||
|
? params.desiredAccuracy
|
||||||
|
: undefined;
|
||||||
|
const locationTimeoutMs =
|
||||||
|
typeof params.locationTimeoutMs === "number" &&
|
||||||
|
Number.isFinite(params.locationTimeoutMs)
|
||||||
|
? params.locationTimeoutMs
|
||||||
|
: undefined;
|
||||||
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
|
nodeId,
|
||||||
|
command: "location.get",
|
||||||
|
params: {
|
||||||
|
maxAgeMs,
|
||||||
|
desiredAccuracy,
|
||||||
|
timeoutMs: locationTimeoutMs,
|
||||||
|
},
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
})) as { payload?: unknown };
|
||||||
|
return jsonResult(raw?.payload ?? {});
|
||||||
|
}
|
||||||
|
case "run": {
|
||||||
|
const node = readStringParam(params, "node", { required: true });
|
||||||
|
const nodes = await listNodes(gatewayOpts);
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"system.run requires a paired companion app or node host (no nodes available).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const nodeId = resolveNodeIdFromList(nodes, node);
|
||||||
|
const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId);
|
||||||
|
const supportsSystemRun = Array.isArray(nodeInfo?.commands)
|
||||||
|
? nodeInfo?.commands?.includes("system.run")
|
||||||
|
: false;
|
||||||
|
if (!supportsSystemRun) {
|
||||||
|
throw new Error(
|
||||||
|
"system.run requires a companion app or node host; the selected node does not support system.run.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const commandRaw = params.command;
|
||||||
|
if (!commandRaw) {
|
||||||
|
throw new Error("command required (argv array, e.g. ['echo', 'Hello'])");
|
||||||
|
}
|
||||||
|
if (!Array.isArray(commandRaw)) {
|
||||||
|
throw new Error("command must be an array of strings (argv), e.g. ['echo', 'Hello']");
|
||||||
|
}
|
||||||
|
const command = commandRaw.map((c) => String(c));
|
||||||
|
if (command.length === 0) {
|
||||||
|
throw new Error("command must not be empty");
|
||||||
|
}
|
||||||
|
const cwd =
|
||||||
|
typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined;
|
||||||
|
const env = parseEnvPairs(params.env);
|
||||||
|
const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs);
|
||||||
|
const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs);
|
||||||
|
const needsScreenRecording =
|
||||||
|
typeof params.needsScreenRecording === "boolean"
|
||||||
|
? params.needsScreenRecording
|
||||||
|
: undefined;
|
||||||
|
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||||
|
nodeId,
|
||||||
|
command: "system.run",
|
||||||
|
params: {
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
timeoutMs: commandTimeoutMs,
|
||||||
|
needsScreenRecording,
|
||||||
|
agentId,
|
||||||
|
sessionKey,
|
||||||
|
},
|
||||||
|
timeoutMs: invokeTimeoutMs,
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
})) as { payload?: unknown };
|
||||||
|
return jsonResult(raw?.payload ?? {});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action: ${action}`);
|
||||||
}
|
}
|
||||||
case "camera_list": {
|
} catch (err) {
|
||||||
const node = readStringParam(params, "node", { required: true });
|
const nodeLabel =
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
typeof params.node === "string" && params.node.trim() ? params.node.trim() : "auto";
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
const gatewayLabel =
|
||||||
nodeId,
|
gatewayOpts.gatewayUrl && gatewayOpts.gatewayUrl.trim()
|
||||||
command: "camera.list",
|
? gatewayOpts.gatewayUrl.trim()
|
||||||
params: {},
|
: "default";
|
||||||
idempotencyKey: crypto.randomUUID(),
|
const agentLabel = agentId ?? "unknown";
|
||||||
})) as { payload?: unknown };
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
const payload =
|
throw new Error(
|
||||||
raw && typeof raw.payload === "object" && raw.payload !== null ? raw.payload : {};
|
`agent=${agentLabel} node=${nodeLabel} gateway=${gatewayLabel} action=${action}: ${message}`,
|
||||||
return jsonResult(payload);
|
);
|
||||||
}
|
|
||||||
case "camera_clip": {
|
|
||||||
const node = readStringParam(params, "node", { required: true });
|
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
|
||||||
const facing = typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
|
|
||||||
if (facing !== "front" && facing !== "back") {
|
|
||||||
throw new Error("invalid facing (front|back)");
|
|
||||||
}
|
|
||||||
const durationMs =
|
|
||||||
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
|
||||||
? params.durationMs
|
|
||||||
: typeof params.duration === "string"
|
|
||||||
? parseDurationMs(params.duration)
|
|
||||||
: 3000;
|
|
||||||
const includeAudio =
|
|
||||||
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
|
||||||
const deviceId =
|
|
||||||
typeof params.deviceId === "string" && params.deviceId.trim()
|
|
||||||
? params.deviceId.trim()
|
|
||||||
: undefined;
|
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
|
||||||
nodeId,
|
|
||||||
command: "camera.clip",
|
|
||||||
params: {
|
|
||||||
facing,
|
|
||||||
durationMs,
|
|
||||||
includeAudio,
|
|
||||||
format: "mp4",
|
|
||||||
deviceId,
|
|
||||||
},
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
})) as { payload?: unknown };
|
|
||||||
const payload = parseCameraClipPayload(raw?.payload);
|
|
||||||
const filePath = cameraTempPath({
|
|
||||||
kind: "clip",
|
|
||||||
facing,
|
|
||||||
ext: payload.format,
|
|
||||||
});
|
|
||||||
await writeBase64ToFile(filePath, payload.base64);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `FILE:${filePath}` }],
|
|
||||||
details: {
|
|
||||||
facing,
|
|
||||||
path: filePath,
|
|
||||||
durationMs: payload.durationMs,
|
|
||||||
hasAudio: payload.hasAudio,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "screen_record": {
|
|
||||||
const node = readStringParam(params, "node", { required: true });
|
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
|
||||||
const durationMs =
|
|
||||||
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
|
||||||
? params.durationMs
|
|
||||||
: typeof params.duration === "string"
|
|
||||||
? parseDurationMs(params.duration)
|
|
||||||
: 10_000;
|
|
||||||
const fps =
|
|
||||||
typeof params.fps === "number" && Number.isFinite(params.fps) ? params.fps : 10;
|
|
||||||
const screenIndex =
|
|
||||||
typeof params.screenIndex === "number" && Number.isFinite(params.screenIndex)
|
|
||||||
? params.screenIndex
|
|
||||||
: 0;
|
|
||||||
const includeAudio =
|
|
||||||
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
|
||||||
nodeId,
|
|
||||||
command: "screen.record",
|
|
||||||
params: {
|
|
||||||
durationMs,
|
|
||||||
screenIndex,
|
|
||||||
fps,
|
|
||||||
format: "mp4",
|
|
||||||
includeAudio,
|
|
||||||
},
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
})) as { payload?: unknown };
|
|
||||||
const payload = parseScreenRecordPayload(raw?.payload);
|
|
||||||
const filePath =
|
|
||||||
typeof params.outPath === "string" && params.outPath.trim()
|
|
||||||
? params.outPath.trim()
|
|
||||||
: screenRecordTempPath({ ext: payload.format || "mp4" });
|
|
||||||
const written = await writeScreenRecordToFile(filePath, payload.base64);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `FILE:${written.path}` }],
|
|
||||||
details: {
|
|
||||||
path: written.path,
|
|
||||||
durationMs: payload.durationMs,
|
|
||||||
fps: payload.fps,
|
|
||||||
screenIndex: payload.screenIndex,
|
|
||||||
hasAudio: payload.hasAudio,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "location_get": {
|
|
||||||
const node = readStringParam(params, "node", { required: true });
|
|
||||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
|
||||||
const maxAgeMs =
|
|
||||||
typeof params.maxAgeMs === "number" && Number.isFinite(params.maxAgeMs)
|
|
||||||
? params.maxAgeMs
|
|
||||||
: undefined;
|
|
||||||
const desiredAccuracy =
|
|
||||||
params.desiredAccuracy === "coarse" ||
|
|
||||||
params.desiredAccuracy === "balanced" ||
|
|
||||||
params.desiredAccuracy === "precise"
|
|
||||||
? params.desiredAccuracy
|
|
||||||
: undefined;
|
|
||||||
const locationTimeoutMs =
|
|
||||||
typeof params.locationTimeoutMs === "number" &&
|
|
||||||
Number.isFinite(params.locationTimeoutMs)
|
|
||||||
? params.locationTimeoutMs
|
|
||||||
: undefined;
|
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
|
||||||
nodeId,
|
|
||||||
command: "location.get",
|
|
||||||
params: {
|
|
||||||
maxAgeMs,
|
|
||||||
desiredAccuracy,
|
|
||||||
timeoutMs: locationTimeoutMs,
|
|
||||||
},
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
})) as { payload?: unknown };
|
|
||||||
return jsonResult(raw?.payload ?? {});
|
|
||||||
}
|
|
||||||
case "run": {
|
|
||||||
const node = readStringParam(params, "node", { required: true });
|
|
||||||
const nodes = await listNodes(gatewayOpts);
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
"system.run requires a paired companion app or node host (no nodes available).",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const nodeId = resolveNodeIdFromList(nodes, node);
|
|
||||||
const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId);
|
|
||||||
const supportsSystemRun = Array.isArray(nodeInfo?.commands)
|
|
||||||
? nodeInfo?.commands?.includes("system.run")
|
|
||||||
: false;
|
|
||||||
if (!supportsSystemRun) {
|
|
||||||
throw new Error(
|
|
||||||
"system.run requires a companion app or node host; the selected node does not support system.run.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const commandRaw = params.command;
|
|
||||||
if (!commandRaw) {
|
|
||||||
throw new Error("command required (argv array, e.g. ['echo', 'Hello'])");
|
|
||||||
}
|
|
||||||
if (!Array.isArray(commandRaw)) {
|
|
||||||
throw new Error("command must be an array of strings (argv), e.g. ['echo', 'Hello']");
|
|
||||||
}
|
|
||||||
const command = commandRaw.map((c) => String(c));
|
|
||||||
if (command.length === 0) {
|
|
||||||
throw new Error("command must not be empty");
|
|
||||||
}
|
|
||||||
const cwd =
|
|
||||||
typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined;
|
|
||||||
const env = parseEnvPairs(params.env);
|
|
||||||
const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs);
|
|
||||||
const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs);
|
|
||||||
const needsScreenRecording =
|
|
||||||
typeof params.needsScreenRecording === "boolean"
|
|
||||||
? params.needsScreenRecording
|
|
||||||
: undefined;
|
|
||||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
|
||||||
nodeId,
|
|
||||||
command: "system.run",
|
|
||||||
params: {
|
|
||||||
command,
|
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
timeoutMs: commandTimeoutMs,
|
|
||||||
needsScreenRecording,
|
|
||||||
agentId,
|
|
||||||
sessionKey,
|
|
||||||
},
|
|
||||||
timeoutMs: invokeTimeoutMs,
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
})) as { payload?: unknown };
|
|
||||||
return jsonResult(raw?.payload ?? {});
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown action: ${action}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user