@zhihand/mcp 0.26.3 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zhihand +261 -131
- package/dist/core/command.d.ts +5 -2
- package/dist/core/command.js +62 -1
- package/dist/core/device.d.ts +1 -0
- package/dist/core/device.js +22 -0
- package/dist/daemon/dispatcher.js +36 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -3
- package/dist/tools/schemas.d.ts +4 -1
- package/dist/tools/schemas.js +19 -1
- package/dist/tools/system.d.ts +17 -0
- package/dist/tools/system.js +11 -0
- package/package.json +1 -1
package/bin/zhihand
CHANGED
|
@@ -53,7 +53,10 @@ Usage:
|
|
|
53
53
|
zhihand pair Pair with a phone device
|
|
54
54
|
zhihand detect Detect available CLI tools
|
|
55
55
|
|
|
56
|
-
zhihand
|
|
56
|
+
zhihand list List all available tests with IDs
|
|
57
|
+
zhihand test Run all safe device tests
|
|
58
|
+
zhihand test <ids> Run specific test(s), e.g. 'zhihand test 4' or '4,9,20'
|
|
59
|
+
zhihand test all Run ALL tests (including unsafe, e.g. power button)
|
|
57
60
|
zhihand serve Start MCP Server (stdio mode, backward compat)
|
|
58
61
|
|
|
59
62
|
Options:
|
|
@@ -286,13 +289,97 @@ switch (command) {
|
|
|
286
289
|
break;
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
case "list":
|
|
289
293
|
case "test": {
|
|
290
294
|
const { resolveConfig: resolveTestConfig } = await import("../dist/core/config.js");
|
|
291
|
-
const { createControlCommand, enqueueCommand } = await import("../dist/core/command.js");
|
|
295
|
+
const { createControlCommand, createSystemCommand, enqueueCommand } = await import("../dist/core/command.js");
|
|
292
296
|
const { waitForCommandAck } = await import("../dist/core/sse.js");
|
|
293
297
|
const { fetchScreenshotBinary } = await import("../dist/core/screenshot.js");
|
|
294
298
|
const { fetchDeviceProfile, getStaticContext, isDeviceProfileLoaded, formatDeviceStatus } = await import("../dist/core/device.js");
|
|
295
299
|
|
|
300
|
+
// ── Test Registry ────────────────────────────────────────
|
|
301
|
+
// Kind: "profile" | "status" | "screenshot" | "hid" | "system"
|
|
302
|
+
// Platform: undefined | "android" | "ios" (skipped on non-matching)
|
|
303
|
+
// Unsafe: won't run in full-suite unless explicitly requested
|
|
304
|
+
const REGISTRY = [
|
|
305
|
+
// Phase A — Device Info API
|
|
306
|
+
{ id: 1, phase: "Device Info", label: "Fetch device profile", kind: "profile" },
|
|
307
|
+
{ id: 2, phase: "Device Info", label: "Device status fields", kind: "status" },
|
|
308
|
+
// Phase B — Screenshot
|
|
309
|
+
{ id: 3, phase: "Screenshot", label: "Screenshot", kind: "screenshot" },
|
|
310
|
+
// Phase C — Tap / Touch
|
|
311
|
+
{ id: 4, phase: "Tap/Touch", label: "Click center", kind: "hid", params: { action: "click", xRatio: 0.5, yRatio: 0.5 } },
|
|
312
|
+
{ id: 5, phase: "Tap/Touch", label: "Double click", kind: "hid", params: { action: "doubleclick", xRatio: 0.5, yRatio: 0.5 } },
|
|
313
|
+
{ id: 6, phase: "Tap/Touch", label: "Long click (800ms)", kind: "hid", params: { action: "longclick", xRatio: 0.5, yRatio: 0.5, durationMs: 800 } },
|
|
314
|
+
{ id: 7, phase: "Tap/Touch", label: "Right click", kind: "hid", params: { action: "rightclick", xRatio: 0.5, yRatio: 0.5 } },
|
|
315
|
+
{ id: 8, phase: "Tap/Touch", label: "Middle click", kind: "hid", params: { action: "middleclick", xRatio: 0.5, yRatio: 0.5 } },
|
|
316
|
+
// Phase D — Swipe / Scroll
|
|
317
|
+
{ id: 9, phase: "Swipe/Scroll", label: "Swipe up", kind: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.7, endXRatio: 0.5, endYRatio: 0.3, durationMs: 300 } },
|
|
318
|
+
{ id: 10, phase: "Swipe/Scroll", label: "Swipe down", kind: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.3, endXRatio: 0.5, endYRatio: 0.7, durationMs: 300 } },
|
|
319
|
+
{ id: 11, phase: "Swipe/Scroll", label: "Swipe left", kind: "hid", params: { action: "swipe", startXRatio: 0.7, startYRatio: 0.5, endXRatio: 0.3, endYRatio: 0.5, durationMs: 300 } },
|
|
320
|
+
{ id: 12, phase: "Swipe/Scroll", label: "Swipe right", kind: "hid", params: { action: "swipe", startXRatio: 0.3, startYRatio: 0.5, endXRatio: 0.7, endYRatio: 0.5, durationMs: 300 } },
|
|
321
|
+
{ id: 13, phase: "Swipe/Scroll", label: "Scroll down", kind: "hid", params: { action: "scroll", xRatio: 0.5, yRatio: 0.5, direction: "down", amount: 3 } },
|
|
322
|
+
{ id: 14, phase: "Swipe/Scroll", label: "Scroll up", kind: "hid", params: { action: "scroll", xRatio: 0.5, yRatio: 0.5, direction: "up", amount: 3 } },
|
|
323
|
+
// Phase E — Text + Keys
|
|
324
|
+
{ id: 15, phase: "Text+Keys", label: "Type text", kind: "hid", params: { action: "type", text: "zhihand" } },
|
|
325
|
+
{ id: 16, phase: "Text+Keys", label: "Enter key", kind: "hid", params: { action: "enter" } },
|
|
326
|
+
{ id: 17, phase: "Text+Keys", label: "Key combo (select all)", kind: "hid", platformAware: "select_all" },
|
|
327
|
+
// Phase F — App Navigation
|
|
328
|
+
{ id: 18, phase: "Navigation", label: "Press Home", kind: "hid", params: { action: "home" } },
|
|
329
|
+
{ id: 19, phase: "Navigation", label: "Press Back", kind: "hid", params: { action: "back" } },
|
|
330
|
+
{ id: 20, phase: "Navigation", label: "Open WeChat", kind: "hid", platformAware: "open_wechat" },
|
|
331
|
+
// Phase G — Clipboard
|
|
332
|
+
{ id: 21, phase: "Clipboard", label: "Clipboard set", kind: "hid", platformAware: "clipboard_set" },
|
|
333
|
+
// Phase H — System Navigation
|
|
334
|
+
{ id: 22, phase: "System Nav", label: "Notification shade", kind: "system", params: { action: "notification" } },
|
|
335
|
+
{ id: 23, phase: "System Nav", label: "Recent apps", kind: "system", params: { action: "recent" } },
|
|
336
|
+
{ id: 24, phase: "System Nav", label: "Search (query='zhihand')", kind: "system", params: { action: "search", text: "zhihand" } },
|
|
337
|
+
{ id: 25, phase: "System Nav", label: "Switch input", kind: "system", params: { action: "switch_input" } },
|
|
338
|
+
{ id: 26, phase: "System Nav", label: "Siri", kind: "system", params: { action: "siri" }, platform: "ios" },
|
|
339
|
+
{ id: 27, phase: "System Nav", label: "Control Center", kind: "system", params: { action: "control_center" }, platform: "ios" },
|
|
340
|
+
{ id: 28, phase: "System Nav", label: "Open browser", kind: "system", params: { action: "open_browser" }, platform: "android" },
|
|
341
|
+
{ id: 29, phase: "System Nav", label: "Shortcut help", kind: "system", params: { action: "shortcut_help" }, platform: "android" },
|
|
342
|
+
// Phase I — Media
|
|
343
|
+
{ id: 30, phase: "Media", label: "Volume up", kind: "system", params: { action: "volume_up" } },
|
|
344
|
+
{ id: 31, phase: "Media", label: "Volume down", kind: "system", params: { action: "volume_down" } },
|
|
345
|
+
{ id: 32, phase: "Media", label: "Mute toggle", kind: "system", params: { action: "mute" } },
|
|
346
|
+
{ id: 33, phase: "Media", label: "Play/Pause", kind: "system", params: { action: "play_pause" } },
|
|
347
|
+
{ id: 34, phase: "Media", label: "Next track", kind: "system", params: { action: "next_track" } },
|
|
348
|
+
{ id: 35, phase: "Media", label: "Prev track", kind: "system", params: { action: "prev_track" } },
|
|
349
|
+
{ id: 36, phase: "Media", label: "Fast forward", kind: "system", params: { action: "fast_forward" } },
|
|
350
|
+
{ id: 37, phase: "Media", label: "Rewind", kind: "system", params: { action: "rewind" } },
|
|
351
|
+
{ id: 38, phase: "Media", label: "Stop", kind: "system", params: { action: "stop" } },
|
|
352
|
+
// Phase J — Hardware
|
|
353
|
+
{ id: 39, phase: "Hardware", label: "Brightness up", kind: "system", params: { action: "brightness_up" } },
|
|
354
|
+
{ id: 40, phase: "Hardware", label: "Brightness down", kind: "system", params: { action: "brightness_down" } },
|
|
355
|
+
{ id: 41, phase: "Hardware", label: "Power button (⚠️ may lock screen)", kind: "system", params: { action: "power" }, unsafe: true },
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
// ── "list" sub-command ───────────────────────────────────
|
|
359
|
+
if (command === "list") {
|
|
360
|
+
console.log("📋 ZhiHand Test Registry\n");
|
|
361
|
+
let currentPhase = "";
|
|
362
|
+
for (const t of REGISTRY) {
|
|
363
|
+
if (t.phase !== currentPhase) {
|
|
364
|
+
console.log(`\n ── ${t.phase} ──`);
|
|
365
|
+
currentPhase = t.phase;
|
|
366
|
+
}
|
|
367
|
+
const tags = [];
|
|
368
|
+
if (t.platform) tags.push(`${t.platform}-only`);
|
|
369
|
+
if (t.unsafe) tags.push("unsafe");
|
|
370
|
+
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
371
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}${tagStr}`);
|
|
372
|
+
}
|
|
373
|
+
console.log(`\n Total: ${REGISTRY.length} tests`);
|
|
374
|
+
console.log("\nUsage:");
|
|
375
|
+
console.log(" zhihand test # run all safe tests");
|
|
376
|
+
console.log(" zhihand test 4 # run test #4 only");
|
|
377
|
+
console.log(" zhihand test 4,9,20 # run tests #4, #9, #20");
|
|
378
|
+
console.log(" zhihand test all # run ALL tests (including unsafe)");
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── "test" sub-command ───────────────────────────────────
|
|
296
383
|
let testConfig;
|
|
297
384
|
try {
|
|
298
385
|
testConfig = resolveTestConfig(values.device ?? process.env.ZHIHAND_DEVICE);
|
|
@@ -302,176 +389,219 @@ switch (command) {
|
|
|
302
389
|
process.exit(1);
|
|
303
390
|
}
|
|
304
391
|
|
|
392
|
+
// Parse which tests to run from positional args
|
|
393
|
+
const filterArg = positionals[1]; // e.g. "4" or "4,9,20" or "all"
|
|
394
|
+
let selectedIds = null; // null = default (all safe)
|
|
395
|
+
let includeUnsafe = false;
|
|
396
|
+
if (filterArg) {
|
|
397
|
+
if (filterArg === "all") {
|
|
398
|
+
includeUnsafe = true;
|
|
399
|
+
} else {
|
|
400
|
+
selectedIds = new Set(
|
|
401
|
+
filterArg.split(",").map((s) => {
|
|
402
|
+
const trimmed = s.trim();
|
|
403
|
+
const n = Number(trimmed);
|
|
404
|
+
// Strict: reject ranges like "4-10" (Number returns NaN), floats, empty
|
|
405
|
+
return Number.isInteger(n) && n > 0 ? n : NaN;
|
|
406
|
+
}).filter((n) => !isNaN(n))
|
|
407
|
+
);
|
|
408
|
+
if (selectedIds.size === 0) {
|
|
409
|
+
console.error(`Invalid test IDs: ${filterArg}`);
|
|
410
|
+
console.error("Run 'zhihand list' to see available tests.");
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
// Explicit selection implies user knows what they're doing
|
|
414
|
+
includeUnsafe = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
305
418
|
console.log("🧪 ZhiHand Device Test");
|
|
306
419
|
console.log(` Device: ${testConfig.credentialId}`);
|
|
307
420
|
console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
|
|
308
421
|
|
|
422
|
+
// Pre-fetch device profile so platform-aware tests work.
|
|
423
|
+
// Note: platform is read dynamically via getDevicePlatform() so Test 1
|
|
424
|
+
// (Fetch device profile) can populate it before later tests consume it.
|
|
425
|
+
try {
|
|
426
|
+
await fetchDeviceProfile(testConfig);
|
|
427
|
+
} catch { /* non-fatal — Test 1 will retry and platform will update */ }
|
|
428
|
+
const getDevicePlatform = () => isDeviceProfileLoaded() ? getStaticContext().platform : "unknown";
|
|
429
|
+
|
|
309
430
|
let passed = 0;
|
|
310
431
|
let failed = 0;
|
|
432
|
+
let skipped = 0;
|
|
311
433
|
let totalSteps = 0;
|
|
312
434
|
|
|
313
|
-
// ──
|
|
314
|
-
|
|
435
|
+
// ── Resolve platform-aware params (evaluated at test time) ──
|
|
436
|
+
function resolvePlatformAwareParams(variant) {
|
|
437
|
+
const platform = getDevicePlatform();
|
|
438
|
+
if (variant === "open_wechat") {
|
|
439
|
+
return platform === "ios"
|
|
440
|
+
? { action: "open_app", bundleId: "com.tencent.xin" }
|
|
441
|
+
: { action: "open_app", appPackage: "com.tencent.mm" };
|
|
442
|
+
}
|
|
443
|
+
if (variant === "clipboard_set") {
|
|
444
|
+
return { action: "clipboard", text: `zhihand_test_${Date.now()}` };
|
|
445
|
+
}
|
|
446
|
+
if (variant === "select_all") {
|
|
447
|
+
const keys = platform === "ios" ? "cmd+a" : "ctrl+a";
|
|
448
|
+
return { action: "keycombo", keys };
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Test runners (shared ACK logic) ──
|
|
454
|
+
async function runCommandTest(t, command) {
|
|
315
455
|
totalSteps++;
|
|
316
|
-
process.stdout.write(` ${label}... `);
|
|
456
|
+
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
317
457
|
const t0 = Date.now();
|
|
318
458
|
try {
|
|
319
|
-
const
|
|
320
|
-
const queued = await enqueueCommand(testConfig, cmd);
|
|
459
|
+
const queued = await enqueueCommand(testConfig, command);
|
|
321
460
|
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
322
461
|
const ms = Date.now() - t0;
|
|
323
462
|
if (ack.acked) {
|
|
324
463
|
const ackStatus = ack.command?.ack_status ?? "ok";
|
|
325
|
-
const detail = ackStatus !== "ok" ? ` [${ackStatus}]` : "";
|
|
326
464
|
const resultInfo = ack.command?.ack_result ? ` ${JSON.stringify(ack.command.ack_result)}` : "";
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
465
|
+
if (ackStatus === "ok") {
|
|
466
|
+
console.log(`✅ (${ms}ms)${resultInfo}`);
|
|
467
|
+
passed++;
|
|
468
|
+
} else {
|
|
469
|
+
console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
|
|
470
|
+
failed++;
|
|
471
|
+
}
|
|
330
472
|
} else {
|
|
331
473
|
console.log(`⏱️ Timeout (${ms}ms)`);
|
|
332
474
|
failed++;
|
|
333
|
-
return null;
|
|
334
475
|
}
|
|
335
476
|
} catch (err) {
|
|
336
477
|
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
337
478
|
failed++;
|
|
338
|
-
return null;
|
|
339
479
|
}
|
|
340
480
|
}
|
|
341
481
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
482
|
+
async function runSingleTest(t) {
|
|
483
|
+
// Platform skip (evaluated at test time — Test 1 may have just populated the profile)
|
|
484
|
+
const currentPlatform = getDevicePlatform();
|
|
485
|
+
if (t.platform && t.platform !== currentPlatform) {
|
|
486
|
+
totalSteps++;
|
|
487
|
+
skipped++;
|
|
488
|
+
console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ Skipped (${t.platform}-only, device is ${currentPlatform})`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
switch (t.kind) {
|
|
493
|
+
case "profile": {
|
|
494
|
+
totalSteps++;
|
|
495
|
+
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
496
|
+
const t0 = Date.now();
|
|
497
|
+
try {
|
|
498
|
+
await fetchDeviceProfile(testConfig);
|
|
499
|
+
const ms = Date.now() - t0;
|
|
500
|
+
if (isDeviceProfileLoaded()) {
|
|
501
|
+
const s = getStaticContext();
|
|
502
|
+
console.log(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
|
|
503
|
+
passed++;
|
|
504
|
+
} else {
|
|
505
|
+
console.log(`⚠️ Loaded but empty (${ms}ms)`);
|
|
506
|
+
failed++;
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
510
|
+
failed++;
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "status": {
|
|
515
|
+
totalSteps++;
|
|
516
|
+
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
517
|
+
try {
|
|
518
|
+
const status = formatDeviceStatus();
|
|
519
|
+
const ignoredDefaults = new Set(["unknown", "0x0", "-1% (unknown)", "0"]);
|
|
520
|
+
const fields = Object.keys(status).filter((k) => {
|
|
521
|
+
const v = status[k];
|
|
522
|
+
if (v === null || v === undefined) return false;
|
|
523
|
+
if (ignoredDefaults.has(String(v))) return false;
|
|
524
|
+
return true;
|
|
525
|
+
});
|
|
526
|
+
console.log(`✅ ${fields.length} fields (${fields.join(", ")})`);
|
|
527
|
+
passed++;
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.log(`❌ ${err.message}`);
|
|
530
|
+
failed++;
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case "screenshot": {
|
|
535
|
+
totalSteps++;
|
|
536
|
+
process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
|
|
537
|
+
const t0 = Date.now();
|
|
538
|
+
try {
|
|
539
|
+
const cmd = createControlCommand({ action: "screenshot" });
|
|
540
|
+
const queued = await enqueueCommand(testConfig, cmd);
|
|
541
|
+
const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
|
|
542
|
+
if (ack.acked) {
|
|
543
|
+
const buf = await fetchScreenshotBinary(testConfig);
|
|
544
|
+
console.log(`✅ ${(buf.length / 1024).toFixed(0)}KB (${Date.now() - t0}ms)`);
|
|
545
|
+
passed++;
|
|
546
|
+
} else {
|
|
547
|
+
console.log(`⏱️ Timeout (${Date.now() - t0}ms)`);
|
|
548
|
+
failed++;
|
|
549
|
+
}
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
552
|
+
failed++;
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case "hid": {
|
|
557
|
+
const params = t.platformAware ? resolvePlatformAwareParams(t.platformAware) : t.params;
|
|
558
|
+
await runCommandTest(t, createControlCommand(params));
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "system": {
|
|
562
|
+
await runCommandTest(t, createSystemCommand(t.params));
|
|
563
|
+
break;
|
|
359
564
|
}
|
|
360
|
-
} catch (err) {
|
|
361
|
-
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
362
|
-
failed++;
|
|
363
565
|
}
|
|
364
566
|
}
|
|
365
567
|
|
|
366
568
|
const pause = () => new Promise((r) => setTimeout(r, 1500));
|
|
367
569
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
console.log(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
|
|
380
|
-
passed++;
|
|
381
|
-
} else {
|
|
382
|
-
console.log(`⚠️ Loaded but empty (${ms}ms)`);
|
|
383
|
-
failed++;
|
|
384
|
-
}
|
|
385
|
-
} catch (err) {
|
|
386
|
-
console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
|
|
387
|
-
failed++;
|
|
388
|
-
}
|
|
570
|
+
// Select tests to run
|
|
571
|
+
const toRun = REGISTRY.filter((t) => {
|
|
572
|
+
if (selectedIds) return selectedIds.has(t.id);
|
|
573
|
+
if (t.unsafe && !includeUnsafe) return false;
|
|
574
|
+
return true;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (toRun.length === 0) {
|
|
578
|
+
console.error("No matching tests.");
|
|
579
|
+
console.error("Run 'zhihand list' to see available tests.");
|
|
580
|
+
process.exit(1);
|
|
389
581
|
}
|
|
390
582
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const fields = Object.keys(status).filter((k) => {
|
|
398
|
-
const v = status[k];
|
|
399
|
-
if (v === null || v === undefined) return false;
|
|
400
|
-
if (ignoredDefaults.has(String(v))) return false;
|
|
401
|
-
return true;
|
|
402
|
-
});
|
|
403
|
-
console.log(`✅ ${fields.length} fields (${fields.join(", ")})`);
|
|
404
|
-
passed++;
|
|
405
|
-
} catch (err) {
|
|
406
|
-
console.log(`❌ ${err.message}`);
|
|
407
|
-
failed++;
|
|
583
|
+
// Warn about missing IDs
|
|
584
|
+
if (selectedIds) {
|
|
585
|
+
const foundIds = new Set(toRun.map((t) => t.id));
|
|
586
|
+
const missing = [...selectedIds].filter((id) => !foundIds.has(id));
|
|
587
|
+
if (missing.length) {
|
|
588
|
+
console.warn(`⚠️ Unknown test IDs: ${missing.join(", ")}`);
|
|
408
589
|
}
|
|
409
590
|
}
|
|
410
591
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
await runHidStep("4. Click center", { action: "click", xRatio: 0.5, yRatio: 0.5 });
|
|
418
|
-
await pause();
|
|
419
|
-
await runHidStep("5. Swipe up", { action: "swipe", startXRatio: 0.5, startYRatio: 0.7, endXRatio: 0.5, endYRatio: 0.3, durationMs: 300 });
|
|
420
|
-
await pause();
|
|
421
|
-
await runHidStep("6. Swipe down", { action: "swipe", startXRatio: 0.5, startYRatio: 0.3, endXRatio: 0.5, endYRatio: 0.7, durationMs: 300 });
|
|
422
|
-
await pause();
|
|
423
|
-
await runHidStep("7. Scroll down", { action: "scroll", xRatio: 0.5, yRatio: 0.5, direction: "down", amount: 3 });
|
|
424
|
-
await pause();
|
|
425
|
-
await runHidStep("8. Scroll up", { action: "scroll", xRatio: 0.5, yRatio: 0.5, direction: "up", amount: 3 });
|
|
426
|
-
await pause();
|
|
427
|
-
|
|
428
|
-
// ── Phase 3: App + Navigation ────────────────────────────
|
|
429
|
-
console.log(" ── Phase 3: App + Navigation ──");
|
|
430
|
-
await runHidStep("9. Press Home", { action: "home" });
|
|
431
|
-
await pause();
|
|
432
|
-
{
|
|
433
|
-
const platform = isDeviceProfileLoaded() ? getStaticContext().platform : "android";
|
|
434
|
-
const openParams = platform === "ios"
|
|
435
|
-
? { action: "open_app", bundleId: "com.tencent.xin" }
|
|
436
|
-
: { action: "open_app", appPackage: "com.tencent.mm" };
|
|
437
|
-
await runHidStep(`10. Open WeChat (${platform})`, openParams);
|
|
438
|
-
}
|
|
439
|
-
await pause();
|
|
440
|
-
await runHidStep("11. Press Back", { action: "back" });
|
|
441
|
-
await pause();
|
|
442
|
-
|
|
443
|
-
// ── Phase 4: Clipboard Roundtrip ─────────────────────────
|
|
444
|
-
console.log(" ── Phase 4: Clipboard Roundtrip ──");
|
|
445
|
-
const clipboardTestText = `zhihand_test_${Date.now()}`;
|
|
446
|
-
|
|
447
|
-
const setAck = await runHidStep("12. Clipboard set", { action: "clipboard", clipboardAction: "set", text: clipboardTestText });
|
|
448
|
-
await pause();
|
|
449
|
-
|
|
450
|
-
const getAck = await runHidStep("13. Clipboard get", { action: "clipboard", clipboardAction: "get" });
|
|
451
|
-
await pause();
|
|
452
|
-
|
|
453
|
-
// Verify roundtrip
|
|
454
|
-
totalSteps++;
|
|
455
|
-
process.stdout.write(" 14. Clipboard roundtrip verify... ");
|
|
456
|
-
if (setAck && getAck) {
|
|
457
|
-
const returned = getAck.command?.ack_result?.text ?? getAck.command?.ack_result?.clipboard ?? null;
|
|
458
|
-
if (returned === clipboardTestText) {
|
|
459
|
-
console.log(`✅ Match: "${clipboardTestText}"`);
|
|
460
|
-
passed++;
|
|
461
|
-
} else if (returned) {
|
|
462
|
-
console.log(`⚠️ Mismatch: sent "${clipboardTestText}", got "${returned}"`);
|
|
463
|
-
failed++;
|
|
464
|
-
} else {
|
|
465
|
-
console.log(`⚠️ No text in ack_result (keys: ${JSON.stringify(Object.keys(getAck.command?.ack_result ?? {}))})`);
|
|
466
|
-
failed++;
|
|
592
|
+
let currentPhase = "";
|
|
593
|
+
for (let i = 0; i < toRun.length; i++) {
|
|
594
|
+
const t = toRun[i];
|
|
595
|
+
if (t.phase !== currentPhase) {
|
|
596
|
+
console.log(` ── ${t.phase} ──`);
|
|
597
|
+
currentPhase = t.phase;
|
|
467
598
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
failed++;
|
|
599
|
+
await runSingleTest(t);
|
|
600
|
+
if (i < toRun.length - 1) await pause();
|
|
471
601
|
}
|
|
472
602
|
|
|
473
603
|
// ── Summary ──────────────────────────────────────────────
|
|
474
|
-
console.log(`\n Result: ${passed}/${totalSteps} passed`);
|
|
604
|
+
console.log(`\n Result: ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
|
|
475
605
|
if (failed === 0) {
|
|
476
606
|
console.log(" ✅ All tests passed! Device is fully responsive.");
|
|
477
607
|
} else {
|
package/dist/core/command.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ZhiHandConfig } from "./config.ts";
|
|
2
2
|
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
3
|
-
export type ClipboardAction = "get" | "set";
|
|
4
3
|
export interface ControlParams {
|
|
5
4
|
action: string;
|
|
6
5
|
xRatio?: number;
|
|
@@ -9,7 +8,6 @@ export interface ControlParams {
|
|
|
9
8
|
direction?: ScrollDirection;
|
|
10
9
|
amount?: number;
|
|
11
10
|
keys?: string;
|
|
12
|
-
clipboardAction?: ClipboardAction;
|
|
13
11
|
durationMs?: number;
|
|
14
12
|
startXRatio?: number;
|
|
15
13
|
startYRatio?: number;
|
|
@@ -39,6 +37,11 @@ export interface WaitForCommandAckResult {
|
|
|
39
37
|
command?: QueuedCommandRecord;
|
|
40
38
|
}
|
|
41
39
|
export declare function createControlCommand(params: ControlParams): QueuedControlCommand;
|
|
40
|
+
export interface SystemParams {
|
|
41
|
+
action: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function createSystemCommand(params: SystemParams): QueuedControlCommand;
|
|
42
45
|
export declare function enqueueCommand(config: ZhiHandConfig, command: QueuedControlCommand): Promise<QueuedCommandRecord>;
|
|
43
46
|
export declare function getCommand(config: ZhiHandConfig, commandId: string): Promise<QueuedCommandRecord>;
|
|
44
47
|
export declare function formatAckSummary(action: string, result: WaitForCommandAckResult): string;
|
package/dist/core/command.js
CHANGED
|
@@ -49,9 +49,11 @@ export function createControlCommand(params) {
|
|
|
49
49
|
case "enter":
|
|
50
50
|
return { type: "receive_enter", payload: {} };
|
|
51
51
|
case "clipboard":
|
|
52
|
+
// App only supports set — payload: { clipboard: "text" }
|
|
53
|
+
// No get support on device side; clipboardAction is ignored
|
|
52
54
|
return {
|
|
53
55
|
type: "receive_clipboard",
|
|
54
|
-
payload: {
|
|
56
|
+
payload: { clipboard: params.text ?? "" },
|
|
55
57
|
};
|
|
56
58
|
case "open_app": {
|
|
57
59
|
const appPayload = {};
|
|
@@ -91,6 +93,65 @@ export function createControlCommand(params) {
|
|
|
91
93
|
throw new Error(`Unsupported action: ${params.action}`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
96
|
+
const IOS_ONLY_ACTIONS = new Set(["siri", "control_center"]);
|
|
97
|
+
const ANDROID_ONLY_ACTIONS = new Set(["open_browser", "shortcut_help"]);
|
|
98
|
+
export function createSystemCommand(params) {
|
|
99
|
+
const platform = isDeviceProfileLoaded() ? getStaticContext().platform : "unknown";
|
|
100
|
+
// Platform validation — block mismatched platform-specific actions
|
|
101
|
+
if (platform === "android" && IOS_ONLY_ACTIONS.has(params.action)) {
|
|
102
|
+
throw new Error(`Action '${params.action}' is not supported on Android.`);
|
|
103
|
+
}
|
|
104
|
+
if (platform === "ios" && ANDROID_ONLY_ACTIONS.has(params.action)) {
|
|
105
|
+
throw new Error(`Action '${params.action}' is not supported on iOS.`);
|
|
106
|
+
}
|
|
107
|
+
switch (params.action) {
|
|
108
|
+
// System navigation
|
|
109
|
+
case "notification":
|
|
110
|
+
return { type: "receive_notification", payload: {} };
|
|
111
|
+
case "recent":
|
|
112
|
+
return { type: "receive_recent", payload: {} };
|
|
113
|
+
case "search":
|
|
114
|
+
return { type: "receive_search", payload: { query: params.text ?? "" } };
|
|
115
|
+
case "switch_input":
|
|
116
|
+
return { type: "receive_switch_input", payload: {} };
|
|
117
|
+
case "siri":
|
|
118
|
+
return { type: "receive_siri", payload: {} };
|
|
119
|
+
case "control_center":
|
|
120
|
+
return { type: "receive_control_center", payload: {} };
|
|
121
|
+
case "open_browser":
|
|
122
|
+
return { type: "receive_open_browser", payload: {} };
|
|
123
|
+
case "shortcut_help":
|
|
124
|
+
return { type: "receive_shortcut_help", payload: {} };
|
|
125
|
+
// Media controls
|
|
126
|
+
case "volume_up":
|
|
127
|
+
return { type: "receive_volume_up", payload: {} };
|
|
128
|
+
case "volume_down":
|
|
129
|
+
return { type: "receive_volume_down", payload: {} };
|
|
130
|
+
case "mute":
|
|
131
|
+
return { type: "receive_mute", payload: {} };
|
|
132
|
+
case "play_pause":
|
|
133
|
+
return { type: "receive_play_pause", payload: {} };
|
|
134
|
+
case "stop":
|
|
135
|
+
return { type: "receive_stop", payload: {} };
|
|
136
|
+
case "next_track":
|
|
137
|
+
return { type: "receive_next_track", payload: {} };
|
|
138
|
+
case "prev_track":
|
|
139
|
+
return { type: "receive_prev_track", payload: {} };
|
|
140
|
+
case "fast_forward":
|
|
141
|
+
return { type: "receive_fast_forward", payload: {} };
|
|
142
|
+
case "rewind":
|
|
143
|
+
return { type: "receive_rewind", payload: {} };
|
|
144
|
+
// Hardware
|
|
145
|
+
case "brightness_up":
|
|
146
|
+
return { type: "receive_brightness_up", payload: {} };
|
|
147
|
+
case "brightness_down":
|
|
148
|
+
return { type: "receive_brightness_down", payload: {} };
|
|
149
|
+
case "power":
|
|
150
|
+
return { type: "receive_power", payload: {} };
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unsupported system action: ${params.action}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
94
155
|
export async function enqueueCommand(config, command) {
|
|
95
156
|
const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands`;
|
|
96
157
|
const body = { command: { ...command, message_id: command.messageId ?? nextMessageId() } };
|
package/dist/core/device.d.ts
CHANGED
|
@@ -43,5 +43,6 @@ export declare function extractDynamic(profile: Record<string, unknown>): Dynami
|
|
|
43
43
|
export declare function updateDeviceProfile(raw: Record<string, unknown>): void;
|
|
44
44
|
export declare function fetchDeviceProfile(config: ZhiHandConfig): Promise<void>;
|
|
45
45
|
export declare function buildControlToolDescription(): string;
|
|
46
|
+
export declare function buildSystemToolDescription(): string;
|
|
46
47
|
export declare function buildScreenshotToolDescription(): string;
|
|
47
48
|
export declare function formatDeviceStatus(): Record<string, unknown>;
|
package/dist/core/device.js
CHANGED
|
@@ -176,6 +176,28 @@ export function buildControlToolDescription() {
|
|
|
176
176
|
}
|
|
177
177
|
return desc;
|
|
178
178
|
}
|
|
179
|
+
export function buildSystemToolDescription() {
|
|
180
|
+
if (!loaded || staticCtx.platform === "unknown") {
|
|
181
|
+
return "System navigation and media controls. Actions: notification, recent, search, switch_input, siri (iOS), control_center (iOS), open_browser (Android), shortcut_help (Android), volume_up/down, mute, play_pause, stop, next/prev_track, fast_forward, rewind, brightness_up/down, power.";
|
|
182
|
+
}
|
|
183
|
+
const platform = staticCtx.platform;
|
|
184
|
+
const parts = [
|
|
185
|
+
`System navigation and media controls for ${platform} device (${staticCtx.model}).`,
|
|
186
|
+
];
|
|
187
|
+
// Navigation
|
|
188
|
+
parts.push("Navigation: notification, recent, search (optional text query), switch_input.");
|
|
189
|
+
if (platform === "ios") {
|
|
190
|
+
parts.push("iOS: siri, control_center.");
|
|
191
|
+
}
|
|
192
|
+
else if (platform === "android") {
|
|
193
|
+
parts.push("Android: open_browser, shortcut_help.");
|
|
194
|
+
}
|
|
195
|
+
// Media
|
|
196
|
+
parts.push("Media: volume_up, volume_down, mute, play_pause, stop, next_track, prev_track, fast_forward, rewind.");
|
|
197
|
+
// Hardware
|
|
198
|
+
parts.push("Hardware: brightness_up, brightness_down, power.");
|
|
199
|
+
return parts.join(" ");
|
|
200
|
+
}
|
|
179
201
|
export function buildScreenshotToolDescription() {
|
|
180
202
|
if (!loaded || staticCtx.platform === "unknown") {
|
|
181
203
|
return "Take a screenshot of the phone screen.";
|
|
@@ -372,6 +372,17 @@ function buildSystemContext() {
|
|
|
372
372
|
else {
|
|
373
373
|
openAppDoc = "- open_app: Open an app. Params: appPackage (Android, e.g. 'com.tencent.mm'), bundleId (iOS), urlScheme (e.g. 'weixin://')";
|
|
374
374
|
}
|
|
375
|
+
// Platform-specific system actions
|
|
376
|
+
let platformSystemDoc;
|
|
377
|
+
if (static_?.platform === "ios") {
|
|
378
|
+
platformSystemDoc = "- siri: Activate Siri\n- control_center: Open Control Center";
|
|
379
|
+
}
|
|
380
|
+
else if (static_?.platform === "android") {
|
|
381
|
+
platformSystemDoc = "- open_browser: Launch default browser\n- shortcut_help: Show keyboard shortcuts overlay";
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
platformSystemDoc = "- siri: Activate Siri (iOS only)\n- control_center: Open Control Center (iOS only)\n- open_browser: Launch default browser (Android only)\n- shortcut_help: Show keyboard shortcuts overlay (Android only)";
|
|
385
|
+
}
|
|
375
386
|
return `You are ZhiHand, an AI assistant connected to the user's mobile phone via MCP tools.
|
|
376
387
|
|
|
377
388
|
## Device
|
|
@@ -397,17 +408,39 @@ Control the phone. Requires "action" parameter. All coordinates use normalized r
|
|
|
397
408
|
- home: Press Home button (no params)
|
|
398
409
|
- enter: Press Enter key (no params)
|
|
399
410
|
${openAppDoc}
|
|
400
|
-
- clipboard:
|
|
411
|
+
- clipboard: Set clipboard text. Params: text (the content to copy)
|
|
401
412
|
- screenshot: Capture screen via control (same as zhihand_screenshot)
|
|
402
413
|
- wait: Wait before next action. Params: durationMs (default 1000)
|
|
403
414
|
|
|
415
|
+
### zhihand_system
|
|
416
|
+
System navigation and media controls. Requires "action" parameter.
|
|
417
|
+
|
|
418
|
+
**System navigation:**
|
|
419
|
+
- notification: Open notification shade/center
|
|
420
|
+
- recent: Show app switcher / recent apps
|
|
421
|
+
- search: Open system search. Optional "text" param to type query after opening
|
|
422
|
+
- switch_input: Switch input method (only works in text input fields)
|
|
423
|
+
${platformSystemDoc}
|
|
424
|
+
|
|
425
|
+
**Media controls:**
|
|
426
|
+
- volume_up / volume_down: Adjust volume
|
|
427
|
+
- mute: Toggle mute
|
|
428
|
+
- play_pause / stop: Playback control
|
|
429
|
+
- next_track / prev_track: Skip track
|
|
430
|
+
- fast_forward / rewind: Seek
|
|
431
|
+
|
|
432
|
+
**Hardware:**
|
|
433
|
+
- brightness_up / brightness_down: Adjust brightness
|
|
434
|
+
- power: Press power button
|
|
435
|
+
|
|
404
436
|
### zhihand_status
|
|
405
437
|
Get device status: platform, battery, network, BLE connection, dark mode, storage, etc.
|
|
406
438
|
|
|
407
439
|
## Rules
|
|
408
440
|
- When the user asks to see their screen, ALWAYS call zhihand_screenshot first.
|
|
409
|
-
- When the user asks to open an app (e.g. WeChat, Settings), use open_app action.
|
|
410
|
-
- When the user asks to go back/home, use back/home actions.
|
|
441
|
+
- When the user asks to open an app (e.g. WeChat, Settings), use open_app action with zhihand_control.
|
|
442
|
+
- When the user asks to go back/home, use back/home actions with zhihand_control.
|
|
443
|
+
- For system functions (notifications, volume, brightness, media), use zhihand_system.
|
|
411
444
|
- For all tap/click operations, use xRatio and yRatio (0-1 normalized coordinates based on the screenshot).`;
|
|
412
445
|
}
|
|
413
446
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.28.0";
|
|
3
3
|
export declare function createServer(deviceName?: string): McpServer;
|
|
4
4
|
export declare function startStdioServer(deviceName?: string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { resolveConfig } from "./core/config.js";
|
|
4
|
-
import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js";
|
|
4
|
+
import { controlSchema, systemSchema, screenshotSchema, pairSchema } from "./tools/schemas.js";
|
|
5
5
|
import { executeControl } from "./tools/control.js";
|
|
6
|
+
import { executeSystem } from "./tools/system.js";
|
|
6
7
|
import { handleScreenshot } from "./tools/screenshot.js";
|
|
7
8
|
import { handlePair } from "./tools/pair.js";
|
|
8
|
-
import { getStaticContext, getDynamicContext, fetchDeviceProfile, buildControlToolDescription, buildScreenshotToolDescription, formatDeviceStatus, } from "./core/device.js";
|
|
9
|
-
export const PACKAGE_VERSION = "0.
|
|
9
|
+
import { getStaticContext, getDynamicContext, fetchDeviceProfile, buildControlToolDescription, buildSystemToolDescription, buildScreenshotToolDescription, formatDeviceStatus, } from "./core/device.js";
|
|
10
|
+
export const PACKAGE_VERSION = "0.28.0";
|
|
10
11
|
export function createServer(deviceName) {
|
|
11
12
|
const server = new McpServer({
|
|
12
13
|
name: "zhihand",
|
|
@@ -18,6 +19,11 @@ export function createServer(deviceName) {
|
|
|
18
19
|
const config = resolveConfig(deviceName);
|
|
19
20
|
return await executeControl(config, params);
|
|
20
21
|
});
|
|
22
|
+
// zhihand_system — system navigation + media controls (separate tool per Gemini design review)
|
|
23
|
+
server.tool("zhihand_system", buildSystemToolDescription(), systemSchema, async (params) => {
|
|
24
|
+
const config = resolveConfig(deviceName);
|
|
25
|
+
return await executeSystem(config, params);
|
|
26
|
+
});
|
|
21
27
|
// zhihand_screenshot — capture current screen without any action
|
|
22
28
|
server.tool("zhihand_screenshot", buildScreenshotToolDescription(), screenshotSchema, async () => {
|
|
23
29
|
const config = resolveConfig(deviceName);
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export declare const controlSchema: {
|
|
|
7
7
|
direction: z.ZodOptional<z.ZodEnum<["up", "down", "left", "right"]>>;
|
|
8
8
|
amount: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
9
9
|
keys: z.ZodOptional<z.ZodString>;
|
|
10
|
-
clipboardAction: z.ZodOptional<z.ZodEnum<["get", "set"]>>;
|
|
11
10
|
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
12
11
|
startXRatio: z.ZodOptional<z.ZodNumber>;
|
|
13
12
|
startYRatio: z.ZodOptional<z.ZodNumber>;
|
|
@@ -17,6 +16,10 @@ export declare const controlSchema: {
|
|
|
17
16
|
bundleId: z.ZodOptional<z.ZodString>;
|
|
18
17
|
urlScheme: z.ZodOptional<z.ZodString>;
|
|
19
18
|
};
|
|
19
|
+
export declare const systemSchema: {
|
|
20
|
+
action: z.ZodEnum<["notification", "recent", "search", "switch_input", "siri", "control_center", "open_browser", "shortcut_help", "volume_up", "volume_down", "mute", "play_pause", "stop", "next_track", "prev_track", "fast_forward", "rewind", "brightness_up", "brightness_down", "power"]>;
|
|
21
|
+
text: z.ZodOptional<z.ZodString>;
|
|
22
|
+
};
|
|
20
23
|
export declare const screenshotSchema: {};
|
|
21
24
|
export declare const pairSchema: {
|
|
22
25
|
forceNew: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
package/dist/tools/schemas.js
CHANGED
|
@@ -13,7 +13,7 @@ export const controlSchema = {
|
|
|
13
13
|
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction"),
|
|
14
14
|
amount: z.number().int().positive().default(3).optional().describe("Scroll steps (default 3)"),
|
|
15
15
|
keys: z.string().optional().describe("Key combo string, e.g. 'ctrl+c', 'alt+tab'"),
|
|
16
|
-
clipboardAction
|
|
16
|
+
// clipboardAction removed — app only supports set (text via "text" param)
|
|
17
17
|
durationMs: z.number().int().positive().max(10000).optional().describe("Duration in ms: wait (default 1000), longclick (default 800), swipe (default 300). Max 10000"),
|
|
18
18
|
startXRatio: z.number().min(0).max(1).optional().describe("Swipe start X [0,1]"),
|
|
19
19
|
startYRatio: z.number().min(0).max(1).optional().describe("Swipe start Y [0,1]"),
|
|
@@ -23,6 +23,24 @@ export const controlSchema = {
|
|
|
23
23
|
bundleId: z.string().optional().describe("iOS bundle ID, e.g. 'com.tencent.xin'"),
|
|
24
24
|
urlScheme: z.string().optional().describe("URL scheme, e.g. 'weixin://'"),
|
|
25
25
|
};
|
|
26
|
+
// zhihand_system — system navigation + media controls (separate from UI control)
|
|
27
|
+
export const systemSchema = {
|
|
28
|
+
action: z.enum([
|
|
29
|
+
// System navigation — cross-platform
|
|
30
|
+
"notification", "recent", "search", "switch_input",
|
|
31
|
+
// System navigation — iOS only
|
|
32
|
+
"siri", "control_center",
|
|
33
|
+
// System navigation — Android only
|
|
34
|
+
"open_browser", "shortcut_help",
|
|
35
|
+
// Media controls — cross-platform
|
|
36
|
+
"volume_up", "volume_down", "mute",
|
|
37
|
+
"play_pause", "stop", "next_track", "prev_track",
|
|
38
|
+
"fast_forward", "rewind",
|
|
39
|
+
// Hardware — cross-platform
|
|
40
|
+
"brightness_up", "brightness_down", "power",
|
|
41
|
+
]).describe("System or media action to perform"),
|
|
42
|
+
text: z.string().optional().describe("Optional text, e.g. search query for 'search' action"),
|
|
43
|
+
};
|
|
26
44
|
export const screenshotSchema = {};
|
|
27
45
|
export const pairSchema = {
|
|
28
46
|
forceNew: z.boolean().default(false).optional().describe("Force new pairing even if already paired"),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zhihand_system tool handler — system navigation + media controls.
|
|
3
|
+
*
|
|
4
|
+
* Separated from zhihand_control to keep UI-control schema focused and
|
|
5
|
+
* reduce LLM parameter hallucination (Gemini design review recommendation).
|
|
6
|
+
*/
|
|
7
|
+
import type { ZhiHandConfig } from "../core/config.ts";
|
|
8
|
+
import type { SystemParams } from "../core/command.ts";
|
|
9
|
+
type TextContent = {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
type ToolResult = {
|
|
14
|
+
content: TextContent[];
|
|
15
|
+
};
|
|
16
|
+
export declare function executeSystem(config: ZhiHandConfig, params: SystemParams): Promise<ToolResult>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createSystemCommand, enqueueCommand, formatAckSummary } from "../core/command.js";
|
|
2
|
+
import { waitForCommandAck } from "../core/sse.js";
|
|
3
|
+
export async function executeSystem(config, params) {
|
|
4
|
+
const command = createSystemCommand(params);
|
|
5
|
+
const queued = await enqueueCommand(config, command);
|
|
6
|
+
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
7
|
+
const summary = formatAckSummary(params.action, ack);
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: "text", text: summary }],
|
|
10
|
+
};
|
|
11
|
+
}
|