[feat] extend keyboard and pointer control with bounded held input

This commit is contained in:
dijunkun committed 2026-11-06 20:54:28 +08:00
1 parent dd4baf2fc4
commit 12afcce690
11 files changed
+141 -11

No files matched your search

+15 -2
View File
@@ -73,10 +73,21 @@ const key = z.enum([
"BACKSPACE", "TAB", "ENTER", "SHIFT", "CTRL", "ALT", "PAUSE", "CAPS_LOCK",
"ESC", "SPACE", "PAGE_UP", "PAGE_DOWN", "END", "HOME", "LEFT", "UP", "RIGHT", "DOWN",
"INSERT", "DELETE", "META", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
"F13", "F14", "F15", "F16", "F17", "F18",
"F19", "F20", "F21", "F22", "F23", "F24",
"NUMPAD0", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD4", "NUMPAD5",
"NUMPAD6", "NUMPAD7", "NUMPAD8", "NUMPAD9", "NUM_LOCK", "SCROLL_LOCK",
"PRINT_SCREEN", "MENU", "MULTIPLY", "ADD", "SUBTRACT", "DECIMAL",
"DIVIDE", "VOLUME_MUTE", "VOLUME_DOWN", "VOLUME_UP", "MEDIA_NEXT", "MEDIA_PREVIOUS",
"MEDIA_STOP", "MEDIA_PLAY_PAUSE",
"SEMICOLON", "EQUAL", "COMMA", "MINUS", "PERIOD", "SLASH", "BACKQUOTE", "BRACKET_LEFT", "BACKSLASH", "BRACKET_RIGHT", "QUOTE",
]);
export const action = z.discriminatedUnion("type", [
point.extend({ type: z.literal("move"), modifiers }),
point.extend({ type: z.literal("mouse_down"), button }),
point.extend({ type: z.literal("mouse_up"), button }),
z.strictObject({ type: z.literal("key_down"), key }),
z.strictObject({ type: z.literal("key_up"), key }),
point.extend({ type: z.literal("click"), button, modifiers }),
point.extend({ type: z.literal("double_click"), button, modifiers }),
point.extend({
@@ -88,14 +99,16 @@ export const action = z.discriminatedUnion("type", [
z.strictObject({
type: z.literal("drag"),
path: z.array(point).min(2).max(128),
duration_ms: z.int().min(100).max(2000).optional(),
duration_ms: z.int().min(100).max(30000).optional(),
button,
modifiers,
}),
z.strictObject({ type: z.literal("key_press"), key, modifiers })
z.strictObject({ type: z.literal("key_press"), key, modifiers,
duration_ms: z.int().min(10).max(30000).optional() })
.refine((p) => !p.modifiers?.some((modifier) => modifier === p.key)),
z.strictObject({
type: z.literal("hotkey"),
duration_ms: z.int().min(10).max(30000).optional(),
keys: z.array(key).min(2).max(6).refine(unique),
}),
z.strictObject({
+1 -1
View File
@@ -173,7 +173,7 @@ export class NativeClient {
if (this.pending.size >= 32 || this.child.stdin!.writableLength > 262144)
return Promise.reject(new Failure("BUSY", false, true));
const id = ++this.sequence;
const sideEffect = method === "action" || method === "unlock" ||
const sideEffect = method === "action" || method === "unlock" || method === "release_inputs" ||
(method === "clipboard" && args.operation === "set") ||
(method === "system" && args.command !== "status") ||
(method === "files" && ["upload", "download", "cancel"].includes(args.operation));
+3
View File
@@ -343,6 +343,9 @@ export function createServer(
"Query the remote operating system's lock state and available secure-desktop service. unknown means the platform could not determine the state.",
async (p, ctx) => output(await sessions.system({ ...p, command: "status" }, ctx.mcpReq.signal)),
{ readOnly: true });
add("crossdesk_release_inputs", z.strictObject({ session_id: id }),
"Release all keys and mouse buttons held by this session and cancel its current input action. Use when finishing a sequence of key_down/mouse_down actions or recovering from interruption. Held input expires after 15 seconds without another action.",
async (p) => output(await sessions.releaseInputs(p.session_id)));
add("crossdesk_system_command", z.strictObject({ session_id: id, operation_id: id,
command: z.enum(["lock", "send_sas"]) }),
"Lock the remote session or request Windows secure attention through its service. Requires control authorization. Reuse operation_id to query the same command. Check system status and a fresh screenshot after acceptance.",
+6 -1
View File
@@ -441,7 +441,7 @@ export class Sessions {
const s = this.session(session_id);
this.checkUnlock(s);
if (s.unlock && this.unlockPending(s.unlock)) throw new Failure("UNLOCK_PENDING");
const result = await s.native.request("action", args, 5000, signal);
const result = await s.native.request("action", args, (args.action.duration_ms ?? 0) + 5000, signal);
if (observe_after && result.status === "submitted") {
try {
result.observation = await this.screenshot(
@@ -509,6 +509,11 @@ export class Sessions {
throw new Failure("UNLOCK_PENDING");
return s.native.request("system", args, 7000, signal);
}
async releaseInputs(sessionId: string) {
const s = this.session(sessionId);
this.cancelUnlock(s);
return s.native.request("release_inputs");
}
async control(id: string, enabled: boolean) {
if (!enabled) this.cancelUnlock(this.session(id));
return this.session(id).native.request("control", { enabled });