mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
b080dd1e76
* 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.
91 lines
3.4 KiB
TypeScript
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 });
|
|
}
|
|
}
|