Files
openclaw/extensions/logbook/src/node-host.ts
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

91 lines
3.4 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 { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-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 };
const LOGBOOK_SNAPSHOT_EXEC_TIMEOUT_MS = 25_000;
function readParams(value: unknown): LogbookSnapshotParams {
if (!value || typeof value !== "object") {
return {};
}
const record = value as Record<string, unknown>;
return {
screenIndex: asFiniteNumber(record.screenIndex),
maxWidth: asFiniteNumber(record.maxWidth),
quality: asFiniteNumber(record.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 });
// node.invoke stops waiting after 30 seconds but cannot reap node-host children.
// Share an earlier deadline so both commands terminate before that outer boundary.
const execSignal = AbortSignal.timeout(LOGBOOK_SNAPSHOT_EXEC_TIMEOUT_MS);
// -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, signal: execSignal },
);
await runExec(
"sips",
[
"--resampleHeightWidthMax",
String(maxWidth),
"-s",
"format",
"jpeg",
"-s",
"formatOptions",
String(qualityPct),
filePath,
],
{ logOutput: false, signal: execSignal },
);
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 });
}
}