import fs from "node:fs/promises"; import path from "node:path"; import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/text-runtime"; import {
definePluginEntry,
type OpenClawPluginApi,
type OpenClawPluginService,
} from "./runtime-api.js";
type ArmGroup = "camera" | "screen" | "writes" | "all";
type ArmStateFileV1 = {
version: 1;
armedAtMs: number;
expiresAtMs: number | null;
removedFromDeny: string[];
};
function parseDurationMs(input: string | undefined): number | null { const raw = normalizeOptionalLowercaseString(input); if (!raw) { returnnull;
} const m = raw.match(/^(\d+)(s|m|h|d)$/); if (!m) { returnnull;
} const n = Number.parseInt(m[1] ?? "", 10); if (!Number.isFinite(n) || n <= 0) { returnnull;
} const unit = m[2]; const mult = unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000; return n * mult;
}
function formatDuration(ms: number): string { const s = Math.max(0, Math.floor(ms / 1000)); if (s < 60) { return `${s}s`;
} const m = Math.floor(s / 60); if (m < 60) { return `${m}m`;
} const h = Math.floor(m / 60); if (h < 48) { return `${h}h`;
} const d = Math.floor(h / 24); return `${d}d`;
}
function resolveStatePath(stateDir: string): string { return path.join(stateDir, ...STATE_REL_PATH);
}
async function readArmState(statePath: string): Promise<ArmStateFile | null> { try { const raw = await fs.readFile(statePath, "utf8"); // Type as unknown record first to allow property access during validation const parsed = JSON.parse(raw) as Record<string, unknown>; if (parsed.version !== 1 && parsed.version !== 2) { returnnull;
} if (typeof parsed.armedAtMs !== "number") { returnnull;
} if (!(parsed.expiresAtMs === null || typeof parsed.expiresAtMs === "number")) { returnnull;
}
if (parsed.version === 1) { if (
!Array.isArray(parsed.removedFromDeny) ||
!parsed.removedFromDeny.every((v: unknown) => typeof v === "string")
) { returnnull;
} return parsed as unknown as ArmStateFile;
}
const group = typeof parsed.group === "string" ? parsed.group : ""; if (group !== "camera" && group !== "screen" && group !== "writes" && group !== "all") { returnnull;
} if (
!Array.isArray(parsed.armedCommands) ||
!parsed.armedCommands.every((v: unknown) => typeof v === "string")
) { returnnull;
} if (
!Array.isArray(parsed.addedToAllow) ||
!parsed.addedToAllow.every((v: unknown) => typeof v === "string")
) { returnnull;
} if (
!Array.isArray(parsed.removedFromDeny) ||
!parsed.removedFromDeny.every((v: unknown) => typeof v === "string")
) { returnnull;
} return parsed as unknown as ArmStateFile;
} catch { returnnull;
}
}
if (state.version === 1) { for (const cmd of state.removedFromDeny) { if (!deny.has(cmd)) {
deny.add(cmd);
restored.push(cmd);
}
}
} else { for (const cmd of state.addedToAllow) { if (allow.delete(cmd)) {
removed.push(cmd);
}
} for (const cmd of state.removedFromDeny) { if (!deny.has(cmd)) {
deny.add(cmd);
restored.push(cmd);
}
}
}
function formatHelp(): string { return [ "Phone control commands:", "", "/phone status", "/phone arm <group> [duration]", "/phone disarm", "", "Groups:",
`- ${formatGroupList()}`, "", "Duration format: 30s | 10m | 2h | 1d (default: 10m).", "", "Notes:", "- This only toggles what the gateway is allowed to invoke on phone nodes.", "- iOS will still ask for permissions (camera, photos, contacts, etc.) on first use.",
].join("\n");
}
function parseGroup(raw: string | undefined): ArmGroup | null { const value = normalizeOptionalLowercaseString(raw) ?? ""; if (!value) { returnnull;
} if (value === "camera" || value === "screen" || value === "writes" || value === "all") { return value;
} returnnull;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.