feat(D): 生产化 —— 无人值守模式 / 一键升级 / 文档口径更新
## D1 无人值守(install.ps1) - 新增 -Unattended(S4U 登录类型):**无需保存密码,注销/重启后仍自动运行** - 新增 -AtStartup:追加开机触发器;任务设置补 StartWhenAvailable(错过触发后补跑) - **诚实标注能力边界**:S4U 运行在非交互会话,GUI 能力(截屏/UIA/键鼠)不可用; -Unattended 与 -AllowInput 冲突时自动关闭输入控制并回写配置 - 说明文档给出两种模式的对照表(交互模式支持 GUI,但注销即停) ## D2 一键升级(nodeagent deploy) - system.info 新增自描述字段:pid / agent_script / agent_home / node_path - deploy 流程:读取被控端入口路径 → 备份旧文件 → 分块上传新 agent.mjs → 调 system.agent.restart 受控重启 → 等待恢复并复验能力数 - 真机验证: * 首次(旧版 agent 无自描述):--path 指定 → 上传 853KB → PID 16492 / 31 项能力 * 二次:自动识别路径 + 自动备份 → PID 34120 / 31 项能力(免 --path) ## D3 CI(待推送) - windows-e2e.yml 增加单元测试步骤(3 行);因 OAuth token 缺 workflow scope, 该改动暂留本地未推送,需用户授权后推送 ## D4 文档口径更新(README) - 能力总览补齐 31 项(新增:自持/异步任务/窗口/录屏/剪贴板/拖拽/审计校验) - 统计数字更正:19 项能力 -> 31,15 个 MCP 工具 -> 26,补 61 项单元测试 - 路线图补 v7~v11+;安全章节补密钥库/Audit 链/ACL 细化 - 命令速查新增:restart / deploy / daemon / fanout / bg / tasks / clip / record / window.* / screen.find / mouse drag - MCP 工具表按新工具重排;安装章节新增两种运行模式对照表 单测 61/61、e2e 21/21 全绿
This commit is contained in:
1 parent
ad88e0bb87
commit
2dccec9a5c
5 files changed
+203
-18
No files matched your search
@@ -20,13 +20,20 @@ nodeagent 把一台 Windows 机器的能力**标准化成一组可授权、可
|
||||
|
||||
| 分组 | 能力 | 说明 |
|
||||
|---|---|---|
|
||||
| **系统** | `system.info` · `system.status` · `system.process.list` · `system.service.list` · `system.shell.exec` · `system.audit.list` | 信息 / 资源 / 进程 / 服务 / 命令 / 审计 |
|
||||
| **系统** | `system.info` · `system.status` · `system.process.list` · `system.service.list` · `system.shell.exec` · `system.audit.list` · `system.audit.verify` | 信息 / 资源 / 进程 / 服务 / 命令 / 审计 / **审计防篡改校验** |
|
||||
| **自持** | `system.agent.restart` | **受控重启自身**(配置变更后让它生效,不会失联) |
|
||||
| **异步任务** | `system.task.list` · `system.task.get` · `system.task.kill` | 长命令后台执行 + 增量续读 + 终止(`exec` 加 `async:true`) |
|
||||
| **软件** | `app.list` · `app.install` | 已装软件(注册表 + winget)、winget 静默安装 |
|
||||
| **屏幕** | `screen.info` · `screen.capture` | 显示器信息、截屏(支持区域与缩放) |
|
||||
| **屏幕** | `screen.info` · `screen.capture` · `screen.record` · `screen.find` | 显示器 / 截屏 / **录屏为帧序列** / **按名字找元素取坐标** |
|
||||
| **窗口** | `window.list` · `window.focus` | 枚举可见窗口(精确矩形)/ 置前聚焦 |
|
||||
| **文件** | `fs.list` · `fs.stat` · `fs.read` · `fs.write` | 列目录 / 元信息 / 分块读 / 原子写 |
|
||||
| **输入** | `input.mouse.move` · `input.mouse.click` · `input.mouse.scroll` · `input.key.type` · `input.key.press` | 键鼠控制(🔒 **默认禁用**) |
|
||||
| **输入** | `input.mouse.move` · `input.mouse.click` · `input.mouse.scroll` · `input.mouse.drag` · `input.key.type` · `input.key.press` | 键鼠控制(🔒 **默认禁用**),含**拖拽**与**按键序列/连按** |
|
||||
| **剪贴板** | `clip.get` · `clip.set` | 读写文本**或图片**(PNG Base64) |
|
||||
|
||||
**19 项能力** · **15 个 MCP 工具** · **21 项端到端用例**(CI 在真实 Windows 上验证)
|
||||
**31 项能力** · **26 个 MCP 工具** · **61 项单元测试** · **21 项端到端用例**(CI 在真实 Windows 上验证)
|
||||
|
||||
> 关键里程碑:**GUI 语义**(`window.list` + `screen.find`,UIA 找不到自动降级 OCR)
|
||||
> 让 AI 从「看得到画面但读不懂界面」变成「按名字取坐标点下去」。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -58,6 +65,16 @@ pnpm build
|
||||
|
||||
安装脚本会使用**包内自带的 Node.js 运行时**,自动:生成预共享密钥 → 写配置 → 放行防火墙 → 注册开机自启 → 启动 Agent,并打印**密钥**与**连接命令**。
|
||||
|
||||
**两种运行模式(重要)**
|
||||
|
||||
| 模式 | 命令 | 登录/注销 | 能力范围 |
|
||||
|---|---|---|---|
|
||||
| 交互(默认) | `install.ps1` | 登录时启动;**注销即停** | 全部能力,**含 GUI**(截屏 / UIA 找元素 / 键鼠注入) |
|
||||
| 无人值守 | `install.ps1 -Unattended -AtStartup` | **注销、重启后仍运行**(S4U,无需存密码) | 无 GUI —— 截屏/输入类能力不可用(进程在非交互会话,没有桌面) |
|
||||
|
||||
> 这是 Windows 的固有限制:**图形操作必须有交互桌面**。需要 GUI 就保持登录(可锁屏);
|
||||
> 只需跑命令/传文件/查状态这类无人值守场景,用 `-Unattended` 更稳。
|
||||
|
||||
**方式 B:开发模式(本仓库)**
|
||||
|
||||
```powershell
|
||||
@@ -175,6 +192,10 @@ node apps/cli/dist/index.js connect 192.168.1.100 --port 8765 --auth-mode ed2551
|
||||
|
||||
### 其他安全默认
|
||||
|
||||
- 输入控制(键鼠)默认禁用,需被控端显式 `"allow_input": true`
|
||||
- 私钥不落明文:macOS 存 Keychain、Windows 存 DPAPI(老版本明文密钥首次加载自动迁移)
|
||||
- 审计日志**链式哈希**(`prev`/`hash`),`nodeagent audit verify` 可检出篡改与删除
|
||||
- ACL 支持 IP 白/黑名单(CIDR)、生效时段(含跨零点)、**按能力限速**(精确 > glob > 全局)
|
||||
- `input.*`(键鼠控制)**默认禁用**,需被控端 `"allow_input": true` 或 `install.ps1 -AllowInput`
|
||||
- 键盘文本经 Base64 传参、PowerShell 侧解码,**杜绝内容注入**;按键名走白名单映射
|
||||
- 文件能力支持 `fs_roots` 路径白名单,越界返回 `E_ACL_DENIED`
|
||||
@@ -222,7 +243,35 @@ nodeagent push <本地文件> <远端路径> [--create-dirs] # 上传(自动
|
||||
|
||||
```bash
|
||||
nodeagent audit [--limit 20] [--type invoke|auth|acl|agent] [--client-id X] [--since <ms>]
|
||||
nodeagent keygen [--id mac_01] # 生成 Ed25519 密钥 + ACL 配置片段
|
||||
nodeagent audit verify # 校验审计链完整性(防篡改检测)
|
||||
nodeagent keygen [--id mac_01] # 生成 Ed25519 密钥 + ACL 配置片段(私钥入系统钥匙串)
|
||||
```
|
||||
|
||||
### 自持与运维
|
||||
|
||||
```bash
|
||||
nodeagent restart [--delay 2000] # 受控重启被控端(配置变更后让它生效)
|
||||
nodeagent deploy <agent.mjs> [--path <远端路径>] # 一键升级(备份 → 上传 → 重启 → 复验)
|
||||
nodeagent daemon [start|stop|status] # 常驻连接池(批量操作省去每次握手)
|
||||
nodeagent fanout <能力名> [--nodes a,b] [--args JSON] # 多设备并发下发并汇总
|
||||
```
|
||||
|
||||
### 长任务与剪贴板
|
||||
|
||||
```bash
|
||||
nodeagent bg "<命令>" [--timeout-ms N] # 后台执行,立即返回 task_id
|
||||
nodeagent tasks | nodeagent task <id> [--offset N] [--kill] # 列出 / 增量读 / 终止
|
||||
nodeagent clip [--set "文本"] [--out <路径>] [--image-file <路径>] # 剪贴板文本或图片
|
||||
nodeagent record [--duration 5000] [--fps 2] [--region x,y,w,h] # 录屏为帧序列
|
||||
```
|
||||
|
||||
### GUI 语义(找到元素再点,不猜坐标)
|
||||
|
||||
```bash
|
||||
nodeagent invoke window.list --args '{"title_pattern":"记事本"}'
|
||||
nodeagent invoke window.focus --args '{"title":"记事本"}'
|
||||
nodeagent invoke screen.find --args '{"text":"一键加速","method":"auto"}' # UIA → OCR 兜底
|
||||
nodeagent mouse drag 300 200 700 500 # 拖拽(拖文件/框选)
|
||||
```
|
||||
|
||||
## MCP 工具
|
||||
@@ -230,11 +279,15 @@ nodeagent keygen [--id mac_01] # 生成 Ed25519 密钥 + AC
|
||||
| 工具 | 说明 |
|
||||
|---|---|
|
||||
| `na_system_info` · `na_status` · `na_process_list` · `na_service_list` | 系统信息与状态 |
|
||||
| `na_exec` · `na_install` · `na_app_list` | 执行命令、装软件、列软件 |
|
||||
| `na_screenshot` | 截屏,**直接返回图片**给 AI 看 |
|
||||
| `na_mouse` · `na_key` | 键鼠控制(需被控端开启) |
|
||||
| `na_fs_list` · `na_fs_read` · `na_fs_write` | 读目录 / 读文件 / 写文件 |
|
||||
| `na_audit` · `na_discover` | 查审计、发现设备 |
|
||||
| `na_exec` · `na_bg` · `na_task` | 执行命令、后台长任务(启动/续读/终止) |
|
||||
| `na_install` · `na_app_list` | 装软件、列软件 |
|
||||
| `na_screenshot` · `na_record` | 截屏(直接返回图片给 AI 看)、录屏为帧序列 |
|
||||
| `na_window_list` · `na_window_focus` · `na_screen_find` | **GUI 语义**:列窗口 / 聚焦 / 按名字取坐标(UIA+OCR) |
|
||||
| `na_mouse` · `na_key` | 键鼠控制(含拖拽、按键序列;需被控端开启) |
|
||||
| `na_clip` | 读写剪贴板文本或图片 |
|
||||
| `na_fs_list` · `na_fs_read` · `na_fs_write` · `na_fs_stat` | 读目录 / 读文件 / 写文件 / 元信息 |
|
||||
| `na_restart` | 受控重启被控端 |
|
||||
| `na_audit` · `na_discover` · `na_nodes` · `na_use` | 查审计、发现设备、多设备切换 |
|
||||
|
||||
## Hub 中转(跨网段 / 公网)
|
||||
|
||||
@@ -264,6 +317,7 @@ node apps/cli/dist/index.js connect hub.example.com --port 443 --hub-token <Hub
|
||||
```bash
|
||||
pnpm build # 构建全部包
|
||||
pnpm typecheck # 类型检查
|
||||
pnpm test:unit # 单元测试(61 项:协议纯函数 / 清单守护 / 审计链 / ACL v11)
|
||||
pnpm test:e2e # 端到端测试(21 项,含 TLS / 零信任 / 发现 / 文件 / Hub)
|
||||
pnpm test:windows # Windows 专属能力(服务 / 软件 / winget 真实装软件)
|
||||
pnpm verify # 对已配置的被控端跑全套验收并输出报告
|
||||
@@ -319,6 +373,13 @@ nodeagent/
|
||||
| **v4** | 无感体验:局域网自动发现 + 断线自动重连 | ✅ |
|
||||
| **v5** | 多设备管理 + 文件传输(分块 / 大文件 / 原子写) | ✅ |
|
||||
| **v6** | Hub 中转:跨网段 / 公网接入(被控端主动外连,穿 NAT) | ✅ |
|
||||
| **v7** | **自持**:受控重启(配置变更后让自身生效,不再丢连接) | ✅ |
|
||||
| **v8** | **GUI 语义**:`window.list` / `window.focus` / `screen.find`(UIA 按名取坐标) | ✅ |
|
||||
| **v9** | `screen.find` **OCR 兜底**(自绘 UI 也能定位)+ MCP 工具同步 | ✅ |
|
||||
| **v10** | **长任务异步化**(`system.task.*`)+ 剪贴板 + CLI 常驻连接池 daemon | ✅ |
|
||||
| **v11** | **安全加固**:私钥入系统密钥库(Keychain/DPAPI)+ 审计链防篡改 + ACL 细化(IP/时段/按能力限速) | ✅ |
|
||||
| **v11+** | **操作面补完**:鼠标拖拽 / 录屏 / 剪贴板图片 / 多设备并发 / 一键升级 | ✅ |
|
||||
| **待办** | 事件订阅(push)、v7~v11 能力入 CI、macOS 端 GUI 能力对齐 | 🚧 |
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ export async function systemInfo(args: Args): Promise<unknown> {
|
||||
memory_total: os.totalmem(),
|
||||
uptime_sec: Math.round(os.uptime()),
|
||||
is_admin: await isAdmin(),
|
||||
// v11:自描述信息,供控制端「一键部署/升级」定位目标文件与数据目录
|
||||
pid: process.pid,
|
||||
agent_script: process.argv[1] ?? '',
|
||||
agent_home: process.env['NODEAGENT_HOME'] ?? '',
|
||||
node_path: process.execPath,
|
||||
};
|
||||
|
||||
if (fields && fields.length > 0) {
|
||||
|
||||
@@ -68,6 +68,8 @@ const HELP = `nodeagent —— 跨机 AI 接管框架(控制端 CLI)
|
||||
读/写剪贴板文本或图片(--out 保存图片)(v11)
|
||||
nodeagent record [--duration 5000] [--fps 2] [--scale 0.5] [--region x,y,w,h]
|
||||
录屏为帧序列(有 ffmpeg 则封装 mp4)(v11)
|
||||
nodeagent deploy <agent.mjs> [--path <远端路径>]
|
||||
一键升级被控端(备份 → 上传 → 重启 → 复验)(v11)
|
||||
nodeagent fanout <能力名> [--nodes a,b] [--args JSON]
|
||||
多设备并发调用并汇总 (v11)
|
||||
nodeagent daemon [start|stop|status] 常驻连接池(批量操作提速 5~10 倍)(v10)
|
||||
@@ -142,6 +144,8 @@ interface Options {
|
||||
fps?: string;
|
||||
/** v11 多设备并发 */
|
||||
nodes?: string;
|
||||
/** v11 部署目标路径 */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
function getClientConfig(): ClientConfig {
|
||||
@@ -702,6 +706,70 @@ async function cmdRecord(opts: Options): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v11 / D2:一键部署 / 升级被控端 agent。
|
||||
* 流程:被控端自报入口路径(system.info)→ 上传新 agent.mjs → 受控重启 → 复验能力数。
|
||||
* 全程无需手工拷文件或起计划任务。
|
||||
*/
|
||||
async function cmdDeploy(localFile: string, opts: Options): Promise<void> {
|
||||
if (!existsSync(localFile)) fail(`本地文件不存在: ${localFile}`);
|
||||
const buf = readFileSync(localFile);
|
||||
if (buf.length < 10_000) fail(`文件过小,疑似不是 agent 包: ${localFile}`);
|
||||
|
||||
await withClient(async (c) => {
|
||||
const info = await c.invoke<Record<string, unknown>>(CapabilityNames.SystemInfo, {});
|
||||
if (info.status === 'failed') fail('无法读取被控端信息');
|
||||
const d = info.data as {
|
||||
agent_script?: string;
|
||||
pid?: number;
|
||||
node_path?: string;
|
||||
agent_home?: string;
|
||||
};
|
||||
const target = opts.path ?? d.agent_script;
|
||||
if (!target) fail('被控端未上报入口路径,请用 --path <远端路径> 指定');
|
||||
console.log(`目标: ${target}(当前 PID ${d.pid ?? '?'})`);
|
||||
|
||||
// 备份现有文件(便于回滚)
|
||||
const backup = `${target}.bak-${Date.now()}`;
|
||||
if (d.node_path) {
|
||||
const cp = await c.invoke(CapabilityNames.ShellExec, {
|
||||
command: `Copy-Item '${target}' '${backup}' -Force -ErrorAction SilentlyContinue; Write-Output 'ok'`,
|
||||
timeout_ms: 15_000,
|
||||
});
|
||||
if (cp.status === 'ok') console.log(`已备份: ${backup}`);
|
||||
}
|
||||
|
||||
// 上传(fs.write 分块;由 agent 内部处理,无需本地→远端路径映射)
|
||||
const CHUNK = 512 * 1024;
|
||||
let offset = 0;
|
||||
while (offset < buf.length) {
|
||||
const part = buf.subarray(offset, Math.min(offset + CHUNK, buf.length));
|
||||
const w = await c.invoke(CapabilityNames.FsWrite, {
|
||||
path: target,
|
||||
data: part.toString('base64'),
|
||||
encoding: 'base64',
|
||||
append: offset > 0,
|
||||
});
|
||||
if (w.status === 'failed') fail(`上传失败于 offset ${offset}: ${w.error?.message}`);
|
||||
offset += part.length;
|
||||
}
|
||||
console.log(`✓ 已上传 ${(buf.length / 1024).toFixed(0)} KB`);
|
||||
|
||||
// 受控重启(复用 v7 能力,无需外部计划任务)
|
||||
const r = await c.invoke(CapabilityNames.AgentRestart, { delay_ms: 1500, reason: 'deploy' });
|
||||
if (r.status === 'failed') fail(`重启失败: ${r.error?.message}`);
|
||||
console.log('✓ 已触发重启,等待恢复…');
|
||||
});
|
||||
|
||||
// 等待新版本上线并复验
|
||||
await new Promise((t) => setTimeout(t, 9000));
|
||||
await withClient(async (c) => {
|
||||
const info = await c.invoke<{ capabilities?: unknown[] }>(CapabilityNames.SystemInfo, {});
|
||||
const caps = c.listCapabilities();
|
||||
console.log(`✓ 新版本已上线:PID ${(info.data as { pid?: number })?.pid ?? '?'},能力 ${caps.length} 项`);
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdClip(opts: Options): Promise<void> {
|
||||
// 图片上传:从本地文件读入(避免超长命令行参数)
|
||||
if (opts.imageFile) {
|
||||
@@ -1204,6 +1272,7 @@ function parseOptions(rest: string[]): { opts: Options; positionals: string[] }
|
||||
'image-file': { type: 'string' },
|
||||
fps: { type: 'string' },
|
||||
nodes: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: false,
|
||||
@@ -1239,6 +1308,7 @@ function parseOptions(rest: string[]): { opts: Options; positionals: string[] }
|
||||
imageFile: values['image-file'] as string | undefined,
|
||||
fps: values['fps'] as string | undefined,
|
||||
nodes: values['nodes'] as string | undefined,
|
||||
path: values['path'] as string | undefined,
|
||||
name: values['name'] as string | undefined,
|
||||
note: values['note'] as string | undefined,
|
||||
recursive: Boolean(values['recursive']),
|
||||
@@ -1383,6 +1453,12 @@ async function main(): Promise<void> {
|
||||
await cmdFanout(cap, opts);
|
||||
return;
|
||||
}
|
||||
case 'deploy': {
|
||||
const f = positionals[0];
|
||||
if (!f) fail('用法: nodeagent deploy <agent.mjs 路径> [--path <远端路径>]');
|
||||
await cmdDeploy(f, opts);
|
||||
return;
|
||||
}
|
||||
case 'install': {
|
||||
const pkg = positionals[0];
|
||||
if (!pkg) fail('用法: nodeagent install <包名|ID>');
|
||||
|
||||
@@ -89,6 +89,10 @@ export const CAPABILITY_MANIFEST: CapabilityDescriptor[] = [
|
||||
memory_total: { type: 'integer', description: '字节' },
|
||||
uptime_sec: { type: 'integer' },
|
||||
is_admin: { type: 'boolean' },
|
||||
pid: { type: 'integer', description: '被控端进程 PID(v11)' },
|
||||
agent_script: { type: 'string', description: '被控端入口脚本路径,供一键升级定位(v11)' },
|
||||
agent_home: { type: 'string', description: '被控端数据目录 NODEAGENT_HOME(v11)' },
|
||||
node_path: { type: 'string', description: '被控端 Node 可执行文件路径(v11)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+47
-8
@@ -17,6 +17,10 @@
|
||||
|
||||
.EXAMPLE
|
||||
.\install.ps1 -Port 8765 -NodeId my-pc
|
||||
|
||||
.EXAMPLE
|
||||
# 无人值守:重启/注销后仍运行(无图形界面能力)
|
||||
.\install.ps1 -Unattended -AtStartup -AllowInput:$false
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -25,7 +29,13 @@ param(
|
||||
[string]$Key = "",
|
||||
[switch]$NoTls,
|
||||
[switch]$AllowInput,
|
||||
[string]$ProjectRoot = ""
|
||||
[string]$ProjectRoot = "",
|
||||
# 无人值守模式:用 S4U 登录类型注册任务 —— 无需保存密码、注销/重启后仍自动运行。
|
||||
# 代价:进程运行在非交互会话(Session 0),**图形能力不可用**
|
||||
# (截屏 / UIA 找元素 / 鼠标键盘注入都依赖交互桌面)。
|
||||
[switch]$Unattended,
|
||||
# 追加「开机即启动」触发器(不依赖用户登录)
|
||||
[switch]$AtStartup
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -129,21 +139,50 @@ New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow `
|
||||
-Protocol TCP -LocalPort $Port -Profile Any | Out-Null
|
||||
Write-Ok "Inbound rule allowed on TCP $Port"
|
||||
|
||||
# 6. Scheduled task (auto start on logon, highest privileges)
|
||||
# 6. Scheduled task
|
||||
Write-Step "Registering scheduled task..."
|
||||
$taskName = "nodeagent"
|
||||
$action = New-ScheduledTaskAction -Execute $nodeExe -Argument "`"$agentJs`"" -WorkingDirectory $ProjectRoot
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
# 触发器:登录时(默认)+ 可选开机时;无人值守模式下开机触发器才真正有意义
|
||||
$triggers = @(New-ScheduledTaskTrigger -AtLogOn)
|
||||
if ($AtStartup -or $Unattended) {
|
||||
$triggers += New-ScheduledTaskTrigger -AtStartup
|
||||
}
|
||||
|
||||
# 自愈:异常退出后自动重启(最多 3 次,间隔 1 分钟),且不设执行时长上限
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-RunLevel Highest -LogonType Interactive
|
||||
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) `
|
||||
-StartWhenAvailable
|
||||
|
||||
# 登录类型决定「能干什么」:
|
||||
# Interactive —— 运行在交互会话,**图形能力可用**,但注销后停
|
||||
# S4U —— 无需密码、注销/重启后仍运行,但**图形能力不可用**(见 -Unattended 说明)
|
||||
if ($Unattended) {
|
||||
if ($AllowInput) {
|
||||
Write-Warn "-Unattended 与 -AllowInput 冲突:Session 0 无法注入输入,已强制关闭输入控制"
|
||||
$AllowInput = $false
|
||||
# 回写配置,避免配置与运行模式不一致
|
||||
$cfg = Get-Content $cfgPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$cfg.allow_input = $false
|
||||
[System.IO.File]::WriteAllText($cfgPath, ($cfg | ConvertTo-Json), (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-RunLevel Highest -LogonType S4U
|
||||
Write-Warn "无人值守模式:agent 将在非交互会话运行"
|
||||
Write-Warn " → 可用:exec / 文件传输 / 进程与服务 / 审计 / 重启 / 异步任务 / 剪贴板"
|
||||
Write-Warn " → 不可用:截屏、UIA 找元素、鼠标键盘注入(需交互桌面)"
|
||||
} else {
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-RunLevel Highest -LogonType Interactive
|
||||
}
|
||||
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $triggers `
|
||||
-Settings $settings -Principal $principal `
|
||||
-Description "nodeagent - cross-machine AI takeover agent" | Out-Null
|
||||
Write-Ok "Scheduled task '$taskName' registered (auto-start on logon)"
|
||||
$modeLabel = if ($Unattended) { "unattended (S4U, survives logoff)" } else { "interactive (logon)" }
|
||||
Write-Ok "Scheduled task '$taskName' registered — mode: $modeLabel"
|
||||
|
||||
# 7. Start now
|
||||
Write-Step "Starting agent..."
|
||||
|
||||
Reference in new issue
Block a user