## 动机(审查报告 §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
183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
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:审计链完整性校验。 */
|