Files
nodeagent/apps/cli/src/cmd/gui.ts
T
jkooe caa5b8a2c1 refactor(cli): 拆分 1977 行的 index.ts 为 11 个模块(纯搬移,零逻辑变更)
## 动机(审查报告 §5.5 / 阿特拉斯报告)
单文件 1977 行难维护、难 review、难以定位改动影响面。本次按职责拆分:

    src/index.ts   315 行  入口:参数解析后按子命令调度
    src/options.ts 155 行  命令行参数解析(parseArgs)
    src/help.ts     75 行  帮助文本
    src/types.ts    74 行  共享类型与常量
    src/core.ts    236 行  运行时:客户端/守护进程代理/输出工具/--node 覆盖
    src/cmd/conn.ts   217 行  连接与设备(connect/keygen/nodes/use/remove/discover)
    src/cmd/system.ts 278 行  系统面(info/status/ps/services/exec/restart/audit/metrics/invoke/apps/install)
    src/cmd/ops.ts    499 行  自动化(bg/tasks/task/fanout/record/deploy/events/macro/clip)
    src/cmd/gui.ts    173 行  图形(screen/screenshot/mouse/key)
    src/cmd/fs.ts     172 行  文件(ls/stat/cat/pull/push)
    src/cmd/net.ts    116 行  网络两阶段提交

## 拆分中处理的真实工程问题(都不是「机械搬移」能躲过的)
1. **跨模块可变状态**:`--node` 的全局覆盖 `currentNodeOverride` 原来是模块级 let。
   ESM 的 import 绑定是只读的,直接 import 后赋值编译不过 → 改为访问器
   setNodeOverride()/getNodeOverride()(并把「为什么不能用变量」写进注释)。
2. **散落在函数之间的顶层常量**:CHUNK_BYTES 恰好位于两个函数之间,按函数切分会
   误入别的模块 → 归入 types.ts 统一导出。DAEMON_SOCK 同理并去重。
3. 导入按模块职责补齐(parseArgs/getClientConfig 等)。

## 验证
- 构建 6 包全过;单测 124/124;e2e 26/26
- **真机 CLI 冒烟 25 条命令全通过**(CLI 不在 e2e 覆盖内,必须实测):
  info/status/ps/services/ls/metrics/audit/nodes/net status/screen/window.list/tasks/clip/apps/exec
  + push/stat/cat/pull(校验回读内容一致)/screenshot/key type/bg/record/mouse/discover
2026-10-01 21:04:24 +08:00

183 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { closeSync, existsSync, openSync, readFileSync, readSync, renameSync, statSync, writeFileSync, writeSync } from 'node:fs';
import fsSync from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path, { join } from 'node:path';
import {
NodeAgentClient,
loadMacroFile,
runMacro,
ClientError,
loadConfig,
saveConfig,
configPath,
toWsUrl,
loadKeys,
createKeys,
keysFilePath,
discoverOnce,
resolveTarget,
resolveNodeSelector,
emptyConfig,
type ClientConfig,
type ResolvedTarget,
type NodeProfile,
} from '@nodeagent/client';
import {
CapabilityNames,
matchPattern,
DEFAULT_DISCOVERY_PORT,
type CapabilityDescriptor,
type InvokeResult,
} from '@nodeagent/protocol';
import {
callAndPrint,
fail,
getClientConfig,
humanSize,
printJson,
riskIcon,
withClient,
withClientDirect,
} from '../core.js';
import { CHUNK_BYTES, type Options } from '../types.js';
// CLI 命令组:cmd/gui.ts
export async function cmdScreen(opts: Options): Promise<void> {
await withClient((c) =>
callAndPrint(c, CapabilityNames.ScreenInfo, {}, opts.json, (data) => {
const rows = (
data as { displays: Array<{ id: number; name: string; width: number; height: number; is_primary: boolean }> }
).displays;
for (const d of rows) {
console.log(
` #${d.id} ${String(d.width).padStart(5)}x${String(d.height).padEnd(5)} ${d.is_primary ? '[主屏]' : ' '} ${d.name}`,
);
}
}),
);
}
export async function cmdScreenshot(opts: Options): Promise<void> {
const args: Record<string, unknown> = {};
if (opts.format) args['format'] = opts.format;
if (opts.scale) args['scale'] = Number(opts.scale);
if (opts.region) {
const nums = opts.region.split(',').map(Number);
if (nums.length !== 4 || nums.some((n) => !Number.isFinite(n))) fail('--region 格式应为 x,y,width,height');
args['region'] = { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
}
await withClient((c) =>
callAndPrint(c, CapabilityNames.ScreenCapture, args, opts.json, (data) => {
const d = data as { image: string; format: string; width: number; height: number; bytes: number };
const out = opts.out ?? `screenshot.${d.format === 'png' ? 'png' : 'jpg'}`;
writeFileSync(out, Buffer.from(d.image, 'base64'));
console.log(`✓ 已保存 ${out} ${d.width}x${d.height} ${humanSize(d.bytes)}`);
}),
);
}
export async function cmdMouse(action: string | undefined, positionals: string[], opts: Options): Promise<void> {
const [a, b] = positionals;
switch (action) {
case 'move': {
if (!a || !b) fail('用法: nodeagent mouse move <x> <y> [--duration 300]');
const args: Record<string, unknown> = { x: Number(a), y: Number(b) };
if (opts.duration) args['duration_ms'] = Number(opts.duration);
await withClient((c) =>
callAndPrint(c, CapabilityNames.MouseMove, args, opts.json, (d) => {
const r = d as { x: number; y: number };
console.log(`✓ 鼠标已移动到 (${r.x}, ${r.y})`);
}),
);
return;
}
case 'click': {
const args: Record<string, unknown> = {};
if (a && b) {
args['x'] = Number(a);
args['y'] = Number(b);
}
if (opts.button) args['button'] = opts.button;
await withClient((c) =>
callAndPrint(c, CapabilityNames.MouseClick, args, opts.json, (d) => {
const r = d as { x: number; y: number; button: string };
console.log(`✓ 已${r.button}键点击 (${r.x}, ${r.y})`);
}),
);
return;
}
case 'scroll': {
if (!a) fail('用法: nodeagent mouse scroll <delta> [y]');
const args: Record<string, unknown> = { delta: Number(a) };
if (b) args['y'] = Number(b);
await withClient((c) =>
callAndPrint(c, CapabilityNames.MouseScroll, args, opts.json, (d) => {
const r = d as { delta: number };
console.log(`✓ 已滚动 ${r.delta} 格`);
}),
);
return;
}
case 'drag': {
const [x1, y1, x2, y2] = positionals;
if (!x1 || !y1 || !x2 || !y2) {
fail('用法: nodeagent mouse drag <x1> <y1> <x2> <y2> [--button left|right|middle]');
}
const args: Record<string, unknown> = {
from_x: Number(x1),
from_y: Number(y1),
to_x: Number(x2),
to_y: Number(y2),
};
if (opts.button) args['button'] = opts.button;
if (opts.duration) args['step_delay_ms'] = Number(opts.duration);
await withClient((c) =>
callAndPrint(c, CapabilityNames.MouseDrag, args, opts.json, (d) => {
const r = d as { from: { x: number; y: number }; to: { x: number; y: number }; steps: number };
console.log(
`✓ 已拖拽 (${r.from.x}, ${r.from.y}) → (${r.to.x}, ${r.to.y}),${r.steps} 步`,
);
}),
);
return;
}
default:
fail('用法: nodeagent mouse <move|click|scroll> ...');
}
}
export async function cmdKey(action: string | undefined, positionals: string[], opts: Options): Promise<void> {
switch (action) {
case 'type': {
const text = positionals.join(' ');
if (!text) fail('用法: nodeagent key type "<文本>"');
const args: Record<string, unknown> = { text };
if (opts.interval) args['interval_ms'] = Number(opts.interval);
await withClient((c) =>
callAndPrint(c, CapabilityNames.KeyType, args, opts.json, (d) => {
const r = d as { length: number };
console.log(`✓ 已输入 ${r.length} 个字符`);
}),
);
return;
}
case 'press': {
if (positionals.length === 0) fail('用法: nodeagent key press <键1> [键2] ...(如 ctrl c)');
await withClient((c) =>
callAndPrint(c, CapabilityNames.KeyPress, { keys: positionals }, opts.json, (d) => {
const r = d as { keys: string[] };
console.log(`✓ 已按下 ${r.keys.join('+')}`);
}),
);
return;
}
default:
fail('用法: nodeagent key <type|press> ...');
}
}
// ---------- v3+ 审计 ----------
/** v11:审计链完整性校验。 */