10 Commits
Author SHA1 Message Date
jkooe f85acad5db chore(release): v2.0.2 2026-10-11 10:18:28 +08:00
jkooe 22bc732334 feat(console): 控制台随包分发 Node 运行时(目标机器无需预装 Node)
控制台的 sidecar 是 .mjs,此前只能在装了 Node 的机器上跑(resolve_node 会回退系统 node)。
新增 scripts/prepare-console.mjs 把 Node 运行时准备为 Tauri resource,随包分发给最终用户。

- prepare-console.mjs:复制本机 node → apps/console/src-tauri/runtime/,macOS 上 strip -x
  (108MB→86MB)**并 adhoc 重签**,再生成注入片段 tauri.bundle.gen.json;带幂等(同源跳过)
  与**产物自检**(真跑一次 node,失败即删除产物并报错,避免坏产物被静默复用)。
- apps/console/scripts/verify-bundle.mjs:验证「打包产物能否自举」——真 spawn 打包内的 node
  去跑打包内的 sidecar 并走一次真实 JSON-RPC 握手。
- package.json:新增 prepare:node / verify:bundle,bundle = sidecar:build → prepare:node →
  tauri build --config → verify:bundle(一条命令产出可分发件并自证可用)。
- src-tauri/.gitignore:忽略 /runtime/(~86MB)与生成的注入片段(生成物不入库)。

三处实测踩出来的约束(均已写进脚本注释与 console-plan.md):

1. **macOS 上 strip 会使 Mach-O 代码签名失效 → 内核直接 SIGKILL(exit 137)**,且无任何有用报错。
   实测对照:原样复制 ✓ / strip 不重签 ✗(137) / strip + `codesign --force --sign -` ✓。
   故 strip 之后的 adhoc 重签是**必要配套**,不是可选优化。
2. **Node 运行时不得声明进默认 tauri.conf.json** —— bundle.resources 是编译期契约
   (tauri-build 校验声明的文件必须存在),声明「运行期才就绪」的资源会让 cargo check/test
   在 build.rs 阶段直接失败(2026-10-10 已踩)。故一律走 `tauri build --config` 注入。
3. **resource 映射必须与 resolve_node 对齐** —— `runtime/node → node` 使产物落在
   Contents/Resources/node,正是 src-tauri/src/sidecar.rs 里 resolve_node 查找的路径。

实测(macOS arm64):.app **90.8 MiB**(原 6.75 MiB,增量即运行时 86MB)/
.dmg **33.78 MiB**(分发件,仍低于 40MB 目标);应用进程实测可启动且无崩溃输出。
对照:本机实测 Electron 运行时本体(Electron Framework.framework)207MB —— 不到一半。
2026-10-11 08:55:31 +08:00
jkooe 4f4d8913b5 chore(release): v2.0.1 2026-10-11 08:02:19 +08:00
jkooe 2fe717cb62 fix(ci): 修 clippy 的两个错误(含一个真实并发问题)+ 本地补齐 lint 验证
## CI 失败的真实原因(上一轮我误判为 resources,实为 clippy)
`-D warnings` 下两个错误:
1. **`clippy::await_holding_lock`(sidecar.rs,真问题)**:
   `if let Some(child) = self.child.lock().unwrap().take() { child.kill().await }`
   —— MutexGuard 存活到 if 块结束,**跨 await 持有 std Mutex**:不仅触发 lint,
   真会阻塞 executor。已改为「先取出 child(锁随即释放),再 await kill」。
2. `clippy::doc_lazy_continuation`(lib.rs 文档注释):列表与后续段落之间缺空行。

## 我的验证方式错在哪(已修正)
上一轮我本地只跑 `cargo test`,**没跑 clippy** —— 而 CI 是 `clippy -D warnings` + `test`
两道闸。本地与 CI **标准不一致**,于是"本地绿、CI 红"。
本轮起:**本地按 CI 的完整命令集验证**(clippy --all-targets -- -D warnings → cargo test)。

## 验证
- `cargo clippy --all-targets -- -D warnings` → **零警告**
- `cargo test` → **3/3 通过**
2026-10-10 15:33:11 +08:00
jkooe 9bba53d5c0 fix(ci): 修 Rust Shell 工作流 —— resources 只声明构建期必产出的文件
CI 失败根因(本地已复现确认):`tauri-build`(build.rs) 会在**编译期校验**
`tauri.conf.json` 的 `bundle.resources` 声明文件是否存在。
我之前声明了两项:`sidecar.mjs`(CI 未生成)+ `resources/node`(Node 运行时未下载)
→ build.rs 直接失败("failed to run custom build command")。

修复:
- `tauri.conf.json` 的 resources **只留 sidecar.mjs**,并写明为何不能声明未就绪的文件
- `rust.yml` 在 clippy/test **之前**加「构建 sidecar」步骤(`pnpm --filter nodeagent-console sidecar:build`)
- console-plan.md:⑤ 不再把 node 记为已完成(未验证的不写成做完),
  补「已知待办」(Node 运行时随包分发 + GUI 端到端人工验证)与「构建约束」说明

本地复现验证:删掉 `sidecar/dist/` → `cargo check` 复现同一错误;恢复 → 通过 ✓
2026-10-10 15:25:12 +08:00
jkooe 53bf5a0634 feat(console)!: Rust 壳改用 Node sidecar(选项 D 完成)—— 退役 crates/nodeagent-client
## 动机(消除「两套协议实现」)
原设计让 Rust 壳自己实现 client,与 TS 版(@nodeagent/client)并存 → 改一处漏一处时
构建过、单测过,只在真机暴露。现 Rust 壳只做**进程管理 + 消息转发**,协议实现只剩 TS 一份。

## ④ Rust 壳改造
- 新增 `src-tauri/src/sidecar.rs`(~290 行):Sidecar 管理器 —— spawn node、
  行分隔 JSON-RPC 的请求/响应匹配(oneshot + 超时兜底)、通知回调(state/log/agent_event)、
  `resolve_node`/`resolve_sidecar` 路径解析(env → Tauri resource → 开发期系统 node)
- 重写 `lib.rs` 为薄转发层:`ensure_sidecar` 懒启动;**5 个 command 签名与 3 个 event 名
  (na-state/na-log/na-event)一律不变 → 前端零改动**;`get_capabilities`/`get_state`
  读本地缓存以**保持同步签名**;`RunEvent::Exit` 时优雅 shutdown(kill_on_drop 兜底不留孤儿)
- `Cargo.toml`:移除 nodeagent-client 依赖、加 tokio;**版本 0.1.0 → 2.0.0**
  (原先与 tauri.conf.json 的 2.0.0 不一致,属版本号统一范围)

## ⑤ 打包配置
- `tauri.conf.json`:`bundle.resources` 声明 sidecar.mjs 与 node;
  `beforeBuildCommand` 串上 `sidecar:build`(否则 resource 缺失)

## ⑥ 退役
- 删除整个 `crates/`(12 文件,含 nodeagent-client 与其 probe 示例)
- `rust.yml` 由「Rust client 单测」改为「**Rust 壳单测(真跑 sidecar)**」:
  装 Tauri 系统依赖 + Node + pnpm,跑 clippy `-D warnings` 与 `cargo test`
- `package.json`:`test:rust` 改指 console 壳;移除 `probe`
  (其「对真机逐项核对字段」的用途已由 CLI `nodeagent invoke` 覆盖)
- 引用清理:README(数字与协议客户端说明)、console 前端两处注释、
  console-plan(开头加「原方案已被取代」提示 + 补改造详情)、ROADMAP 历史描述加退役批注

## 验证
- **`cargo test` 3/3 通过**(真跑 Node sidecar 子进程:spawn/请求-响应/错误不崩)——
  这是关键:sidecar 管理器**不依赖 Tauri GUI**,故核心逻辑可在 CI 真实验证,不必等人工开窗口
- `cargo check` 零警告;clippy 标准与 CI 一致
- 273 单测 / 7 包构建 全绿
- ⚠️ **仍需人工验证**:Tauri GUI 端到端(开窗口 → 连被控端 → 点界面),沙箱跑不了图形应用
2026-10-10 15:16:24 +08:00
jkooe 096193a8ec feat(console): 采纳 sidecar 方案(选项 D 第一步)—— Node sidecar 实现 + 8/8 真机 e2e
## 为什么
原设计让 Rust 壳自己实现一份 client(crates/nodeagent-client),代价是**两套协议实现静默漂移**
(改一处漏一处时构建/单测都不报错,只在真机暴露)。sidecar 让 Rust 壳退化为
「进程管理 + 消息转发」,协议实现只剩 TS 一份,从根上消除漂移。

## 本次交付
1. `apps/console/sidecar/main.mjs`(190 行):把 `@nodeagent/client` 暴露成
   **JSON-RPC 2.0 over stdio**(行分隔)。方法 connect/disconnect/invoke/state;
   onStateChange/onLog/onEvent 三个回调 → `event` 通知(kind=state/log/agent_event)
2. `apps/console/sidecar/build.mjs`:esbuild **JS API** 打成单文件(避开 pnpm `.bin` 的
   ENOENT 坑),并注入 `createRequire` shim —— 否则 `ws` 的动态 require 会报
   "Dynamic require of events is not supported"
3. `apps/console/package.json`:加 `@nodeagent/client`/`protocol` 依赖 + sidecar:dev/build 脚本
4. `docs/console-plan.md`:补「实施决策」节(决策理由、协议定义、进度表、打包方向)

## 两条协议纪律(写进注释与文档)
- **stdout 只写协议消息**,日志一律 stderr(混入非 JSON 行会让 Rust 侧解析失败)
- 任何异常都转成响应,**绝不因单个请求崩进程**

## 错误形态归一(实测中修正)
被控端对未知能力/ACL 拒绝/超时是回 JSON-RPC error → `client.invoke()` 抛异常。
若原样上抛前端要处理两种形态,故归一为 `{status:'failed',error:{name,message,data?}}`;
而「未连接/缺参数」仍走协议错误(语义不同)。

## 验证(真实链路,非 mock)
本地 agent → sidecar **8/8 全绿**:connect 回传 51 项能力与 agent_version ✓ ·
invoke system.info ✓ · state 反映已连接 ✓ · 事件通知已推送 ✓ ·
非法能力返回 failed 且不崩 ✓ · disconnect ✓ · **stdout 无日志污染** ✓
打包产物拷到仓库外**独立运行通过**(227KB 单文件自足)。

## 待办(下一批)
Rust 壳改造(command 签名不变 → 前端零改动)· tauri.conf.json 的 resource 配置 +
Node 运行时打包 · 退役 crates/nodeagent-client
2026-10-10 15:11:39 +08:00
jkooe a515eb2e63 chore(release): v2.0.0 2026-10-10 12:14:02 +08:00
jkooe 921ff89edf feat(v23): 连接层三项防护 + 安装期密钥改 stdin(堵住最后一条密钥暴露面)
## ① 安装期密钥不再走命令行(路线图方向三,最后一条暴露面)
此前 install.cmd 用 -Key 传参,**同机其他用户可从 Win32_Process.CommandLine
明文读到**。现改为管道喂 stdin:
- install.ps1 新增 -KeyFromStdin:先判 [Console]::IsInputRedirected 再读
  (没管道时不阻塞)、Trim 去 CRLF、hex 16-128 校验;优先级 -Key > stdin > 复用既有 > 新生成
- install.cmd:echo %PSK%| powershell ... -KeyFromStdin(管道符前不留空格,
  否则空格会被 echo 进密钥 —— 老坑)

## ② 握手失败封禁(此前只计数,攻击者可无限重试)
- 同来源连续失败达 max_attempts(默认 5)→ 拉黑 ban_ms × 2^(N-max),指数退避封顶 24h;
  封禁期过计数保留,继续翻倍
- 5 个失败路径统一记账(挑战过期/nonce 不匹配/未注册/Ed25519 失败/PSK 失败);
  **认证成功即清零**(不误伤手滑的控制端)
- 白名单来源永不封禁;归一化剥离 ::ffff: 前缀(否则同来源被当两个键)
- 在连接入口挡(TLS 之后、业务握手之前),错误响应带 retry_after_ms
- 封禁状态放 core(handleAuth 在内),AgentCore 暴露 banRemainingMs/states

## ③ 连接上限 + 空闲断开
- max_connections 默认 8(超限 close 1013);idle_timeout_ms 默认 30 分钟
- 空闲清扫**保守**:只要还有任务在跑或还有事件订阅就不断 —— 不把挂着长任务
  等结果的控制端踢下线(新增 runningTaskCount / watchCount 两个判据)
- states 由 WeakMap 改 Map(需要 size 与遍历),close 时手动 delete 防泄漏
- 新增审计类型 session.idle_close(console 的 AuditType 同步,契约测试守住)

## 验证
- 单测 **227 全绿**(新增 security-hardening 6 例:退避公式翻倍/封顶、IP 归一化、
  白名单、hex 校验、配置形状);e2e **30** 全绿
- **本地真机三场景**:正确密钥可连 → 连续错误被拒 → 封禁期连正确密钥也被拒 ✓
- ⚠️ 待 Windows 真机:install.cmd 双击首装(改了安装入口,必须验管道喂钥)
2026-10-07 21:23:37 +08:00
jkooe dac8a0a2a7 feat(console): 入库 Tauri 2 桌面控制台(主控/被控一体)
apps/console —— 跨端(macOS/Windows)桌面控制台,复用 ops 侧的 Node agent 作本地被控端,
不做第二套 agent。技术栈:Tauri 2(Rust 壳 + 系统 WebView,包体小)+
Vue 3 + TS + Vite + Pinia + Naive UI(暗色)+ Vue Router(hash)。

- command 层(src-tauri/src/lib.rs):connect / disconnect / invoke_capability /
  get_capabilities / get_state;client 事件经 Emitter 桥接为 na-log / na-state / na-event 推前端。
  握手后**先 await 成功再落 state**(不持锁 await)。
- 7 页面:设备管理 / 状态总览 / 终端 / 进程与服务 / 软件 / 文件 / 审计。
- 实测:macOS .app 6.75 MiB(方案指标 <30MB;Electron 同级约 100MB+),
  直接执行二进制进程存活、无崩溃 —— `codesign --verify --deep --strict` 那条
  "code has no resources but signature indicates they must be present" 是 adhoc
  链接签名的固有噪声,实测无害(勿被误导去「修签名」)。

⚠️ 本轮用 examples/probe.rs 对真实被控端核对字段契约,抓出 9 处**静默空列**
(不报错、类型系统也抓不到,界面只显示 "-"),全部按契约修掉:
- 进程表读 r.cpu / r.memory → 真源 cpu_pct / memory_bytes(CPU、内存两列恒为 "-")
- 服务表读 r.status → 真源 state
- 磁盘读 d.mount||d.name / d.used → 真源只有 drive + total/free,
  **没有 used**(须自算 total-free)
- 网卡读 {name,rx_bytes,tx_bytes} → 真源 {adapter,ip,up}
- 软件表读 a.id → app.list 结果**没有 id**,应为 publisher
- 审计 broken_at 当数字渲染 → 真源是 {file,line,reason} 对象
另清掉幻影字段 SystemInfo.node(契约里只有 node_path)。

配套两点设计:
- **平台门控**:连接后探测 system.info.os → isWindows/platformReady;app.* 与
  system.service.list 仅 Windows 被控端可用,故软件页与服务 Tab **事前禁用并说明原因**,
  而不是发出注定失败的请求再解释错误。
- **结构化错误**:协议真源 InvokeResult.error 只有 {name,message,data?}(**没有 code**),
  故分支一律按 name;前端 CapabilityFailure 暴露 errorName,并提供
  isUnsupportedPlatform / isAclDenied / isRateLimited。未改协议(改动会扩散 4 端)。

tests/unit/console-contract.test.mjs(20 例):把「前端声明 ⊆ 契约」与「契约 ⊆ 前端声明」
双向钉死,另校验平台门控清单与 agent 侧 requireWindows 集合一致、AuditType 覆盖
apps/agent/src/audit.ts 全部取值。**已注入漂移实测会红**(把 cpu_pct 改回 cpu →
正/反向各挂 1 项),非空转守卫。
2026-10-07 10:14:12 +08:00