mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 08:11:15 +00:00
* refactor(process): centralize bounded command execution * refactor(process): migrate core one-shot commands * refactor(plugins): migrate one-shot commands * fix(process): await Windows tree termination * chore(plugin-sdk): refresh process runtime surface * refactor(process): migrate remaining bounded commands * refactor(process): normalize command result handling * refactor(process): split execution responsibilities * chore(plugin-sdk): refresh API baseline * chore(process): remove release-owned changelog entry * fix(process): narrow binary command input checks * fix(process): cap sandbox command output * fix(qa-lab): preserve exact node probe env * chore(ci): refresh dead export baseline * fix(process): preserve force-kill command deadlines * fix(process): avoid post-exit timeout reclassification * test(process): update scp staging wrapper mock * test(process): update remaining wrapper mocks * refactor(qa-lab): preserve Execa tar execution
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
// Logbook node-host command: screen capture for headless node hosts (macOS).
|
|
// Nodes without the OpenClaw app (plain `openclaw node host run`) advertise
|
|
// logbook.snapshot so capture works anywhere the plugin is enabled.
|
|
import { randomUUID } from "node:crypto";
|
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { runExec } from "openclaw/plugin-sdk/process-runtime";
|
|
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
|
|
type LogbookSnapshotParams = {
|
|
screenIndex?: number;
|
|
maxWidth?: number;
|
|
quality?: number;
|
|
};
|
|
|
|
type LogbookSnapshotPayload = { format: "jpeg"; base64: string } | { error: string };
|
|
|
|
function readParams(value: unknown): LogbookSnapshotParams {
|
|
if (!value || typeof value !== "object") {
|
|
return {};
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const num = (key: string) => {
|
|
const candidate = record[key];
|
|
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
|
|
};
|
|
return { screenIndex: num("screenIndex"), maxWidth: num("maxWidth"), quality: num("quality") };
|
|
}
|
|
|
|
export async function handleLogbookSnapshot(rawParams: unknown): Promise<LogbookSnapshotPayload> {
|
|
if (process.platform !== "darwin") {
|
|
return { error: `logbook.snapshot is not supported on ${process.platform}` };
|
|
}
|
|
const params = readParams(rawParams);
|
|
const screenIndex = Math.max(0, Math.round(params.screenIndex ?? 0));
|
|
const maxWidth = params.maxWidth && params.maxWidth >= 480 ? Math.round(params.maxWidth) : 1440;
|
|
const qualityPct = Math.min(
|
|
100,
|
|
Math.max(
|
|
10,
|
|
Math.round(
|
|
(params.quality && params.quality > 0 && params.quality <= 1 ? params.quality : 0.6) * 100,
|
|
),
|
|
),
|
|
);
|
|
// The shared helper rejects unsafe temp roots; the private subdirectory
|
|
// keeps captures out of the broader OpenClaw temp namespace.
|
|
const captureDir = path.join(resolvePreferredOpenClawTmpDir(), "logbook");
|
|
await mkdir(captureDir, { recursive: true, mode: 0o700 });
|
|
await chmod(captureDir, 0o700);
|
|
const filePath = path.join(captureDir, `logbook-snapshot-${randomUUID()}.jpg`);
|
|
try {
|
|
// Pre-create owner-only: screencapture truncates the existing inode, so
|
|
// the capture never becomes world-readable even if the dir mode drifts.
|
|
await writeFile(filePath, "", { mode: 0o600 });
|
|
// -x: no capture sound; -C: include cursor; -D is 1-based display index.
|
|
await runExec(
|
|
"screencapture",
|
|
["-x", "-C", "-D", String(screenIndex + 1), "-t", "jpg", filePath],
|
|
{ logOutput: false },
|
|
);
|
|
await runExec(
|
|
"sips",
|
|
[
|
|
"--resampleHeightWidthMax",
|
|
String(maxWidth),
|
|
"-s",
|
|
"format",
|
|
"jpeg",
|
|
"-s",
|
|
"formatOptions",
|
|
String(qualityPct),
|
|
filePath,
|
|
],
|
|
{ logOutput: false },
|
|
);
|
|
const buffer = await readFile(filePath);
|
|
return { format: "jpeg", base64: buffer.toString("base64") };
|
|
} catch (err) {
|
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
} finally {
|
|
await rm(filePath, { force: true });
|
|
}
|
|
}
|