jkooe
|
a75c5c9bc5
|
chore(release): 版本统一至 v2.1.0 + CHANGELOG 回填未发布提交
- 经 scripts/lib/version-write.mjs 写入全部 11 处(8 package.json +
tauri.conf.json + Cargo.toml/lock),无散点遗漏、无 skipped
- CHANGELOG [未发布] 段补入 v2.0.3..HEAD 全部提交主题:
L2 系统级审计(7d781d1)、交付流水线(2bf45fd)、Linux 适配(00992e5)、
质量门禁与 CI(141f479 等 4 个)、console-release(9f95560);
并修正「安装包尚未接入 Release」的过期记述
- version-write 单测 12/12 绿(含仓库版本号一致性守卫)
|
2026-10-25 23:37:52 +08:00 |
|
jkooe
|
240c52c44f
|
fix(console): 「连接本机」必失败 —— 地址硬编码 ws:// 无视 tls
## 皇上实测
点「连接本机」→ 界面显示**连接失败**。
## 根因(协议层,不是鉴权)
`agent.rs` 里拼监听地址时**硬编码** `ws://127.0.0.1:{port}`,
完全没看同一个函数里刚解析出来的 `tls` 字段:
```rust
"url": format!("ws://127.0.0.1:{port}"), // ← tls = true 时这是错的
```
而被控端 `agent.json` 默认 `tls: true`(只接受 TLS)。于是控制台用
**明文 WebSocket** 去连一个**只监听 TLS** 的端口:服务端在等 TLS ClientHello,
收到的却是 HTTP GET,握手当场失败。
**鉴别特征**:被控端审计里连一条记录都没有(根本没进到鉴权层),
所以「密钥对不对」根本轮不到 —— 这类故障看审计有无 `auth.success` 即可一刀切开:
- 有记录 → 问题在鉴权/ACL
- 无记录 → 问题在协议/网络层(本次就是)
## 修法
- `url` 改为按 tls 选择:`if tls { "wss" } else { "ws" }`
- 顺带修两处同类默认值(同一个坑):
- `stores/conn.ts` 兜底地址 → `wss://`
- `NodesView` 远程节点输入默认值与占位 → `wss://`
## 守卫
新增单测 `连接地址应跟随tls开关而非硬编码ws`:分别写 tls=true/false 的
agent.json,断言得到 `wss://` 与 `ws://`。防止将来又被"简化"回硬编码。
验证:`cargo test --lib` **22 项全绿**;`vue-tsc --noEmit` 通过。
|
2026-10-17 22:15:17 +08:00 |
|
jkooe
|
0782d901a3
|
fix(console): 消除两处同步阻塞调用 —— 修「按钮一直转圈、PID 不再更新」
## 真机现象
被控端明明在跑,界面上「启动/停止/重启」三个按钮**同时**转圈不结束,
PID 停在旧值(显示 30160,实际已是 22948),点导航也不切页。
只能重开控制台。
## 根因一(臣上一版自己引入的)
`looks_like_agent()` 用 `std::process::Command::output()` 调 PowerShell:
**同步阻塞 + 无超时**。而这台机器上 PowerShell/CIM 本身就慢甚至偶发卡死
(实测 `Get-ScheduledTask` 2.0s、`Get-NetTCPConnection` 1.7s,
另有一次 CIM 查询直接 30s 超时)。
它在 `start()` 里被调用(复用判定),一旦卡住就**永久占住运行时线程** →
`run()` 的 finally 里的 `refresh()` 永远走不到 → `busy` 卡死、状态不再更新。
连带 sidecar 的请求也拿不到执行机会,表现为「重连中」但审计里没有任何握手记录。
改:`looks_like_agent` 改 async + `tokio::process` + **3 秒超时**。
超时/失败一律**判为「不是被控端」**(拒绝接管)—— 宁可让用户看到「端口被占用」
去手工排查,也不能误杀别人的进程。
## 根因二
`autostart::is_enabled()` 每次 `agent_status` 都会跑一次 `schtasks /Query`
(实测约 2 秒)。每刷新一次界面白等 2 秒,叠加后同样表现为转圈不结束。
改:加 5 秒 TTL 缓存(自启是低频状态),并在 `set()` 后 `invalidate_cache()`,
保证用户点完开关立刻看到新值而不是 5 秒前的旧值。
验证:`cargo build` / `cargo clippy` 干净;`cargo test --lib` **21 项全绿**。
|
2026-10-17 21:55:44 +08:00 |
|
jkooe
|
00659165b1
|
fix(console): 状态永远停在「启动中」—— refresh 被 catch 跳过
## 真机症状
被控端明明在跑(点「启动」后端答「**被控端已在运行**」、端口在听、验收 15/15 全通),
界面却一直显示「启动中」,「停止 / 重启」永久灰着 —— 用户完全无法操作。
## 根因(两条更新通道同时失效)
1. `run()` 把 `await refresh()` 写在 **try 里**:
```ts
try { const r = await fn(); await refresh(); return r; }
catch (e) { ... } // ← 动作一抛错,refresh 就被跳过
finally { busy = false }
```
那一次 `start()` 抛了「已在运行」,于是 refresh 被跳过,界面停在旧值。
2. `na-agent-state` 推送在 **WebView 就绪前**发出,前端 `bind()` 的监听还没挂上 → 事件丢失。
3. `LocalServiceView` 的 `onMounted` 只调 `agent.bind()`,而 `bind()` 有 `bound`
守卫 —— 第二次进页面直接 return,**不会刷新**。所以切页也救不回来。
后端状态其实是对的(`inner.state` 已是 running),纯前端未同步。
## 修法
- `run()`:`refresh()` 移入 **finally** —— 成败都同步状态。
`refresh()` 自身已有 try/catch,不会掩盖原始错误。
- `LocalServiceView.onMounted`:显式再 `await agent.refresh()`,
绕过 `bind()` 的 bound 守卫,每次进页面都拉一次(最便宜的状态自愈)。
- `snapshot()`:加状态自愈兜底 —— `STARTING` 且已有 PID 且超 2 秒,就按客观事实
校正为 `RUNNING`(拿得到 PID 是进程已起的硬证据);`STOPPING` 且无 PID 则校正为 `STOPPED`。
## 自愈的三条单测(含反向守卫)
- 已起 + 超 2 秒 → 必须 `running`
- 刚起(<2 秒)→ **不得**提前自愈(否则真实启动失败会被伪装成运行中)
- 无 PID → 不得自愈(进程根本没起来,不能装成 running)
验证:`cargo build` / `cargo test --lib`(17 项全绿)/ `vue-tsc --noEmit` 均通过。
|
2026-10-17 21:09:13 +08:00 |
|
jkooe
|
704fb8cfc9
|
feat(console): 退出前确认是否保留被控端 + 下次启动复用已运行实例
## 问题(皇上实测)
1. 退出控制台后仍有一个被控端在跑 —— 因为 `kill_on_drop(false)`,
agent 成了孤儿继续监听端口。用户以为关了窗口就干净了。
2. 下次启动时该孤儿占着 8765,`start()` 直接报「端口已被占用」,
明明有个健康实例却用不上。
## 需求 1:退出前确认
托盘「退出」与「关 X(未开启最小化到托盘时)」两条路都会留下孤儿,
故两条都必须问,否则从 X 走就绕过了确认。
- Rust:`quit` 不再直接 `app.exit(0)`,改为 emit `na-quit-requested` + 唤出窗口
- Rust:新增 `quit_app(keep_agent)` 命令 —— true 直接退出(保留 agent),
false 先 `stop()` 再退出
- 前端:新增 `QuitGuard.vue`(挂在 `n-dialog-provider` 内),三选一:
保留被控端退出 / 停止被控端退出 / 关闭弹窗即取消
为什么不加 `tauri-plugin-dialog`:前端已有 `n-dialog-provider`,
不必新增依赖,视觉也与全应用一致。
## 需求 2:复用已运行实例(adopt)
`start()` 遇到端口占用时不再一刀切报错,而是先分清占用者:
- 占用者**确实是** nodeagent 被控端(`looks_like_agent`:命令行含 `agent.mjs`)
→ **复用**:记 PID、置 RUNNING、`external=true`,返回 `adopted:true`
- 否则(用户自己的服务 / 别的应用)→ 仍报「端口已被占用,且占用者不是被控端」
—— 绝不误接管别人的进程
配套:
- `Inner` 增 `external` 标记;`snapshot()` 暴露给前端
- `stop()` 支持外部实例(无 Child 句柄,走 `stop_pid()` → taskkill /T /F),
并**等待端口真正释放**再返回,否则 restart 会误判「还活着」又复用同一个 PID
- `restart()` 无需改动:stop 收掉外部实例 + 600ms + start 自己 spawn
→ 天然把控制权收回本控制台
- 前端:状态栏加「已复用外部实例」标签
## 顺带修复(同一类问题)
上一提交把「检查更新」从全局 `busy` 拆出后,发现 `applyUpdate` 也占全局 `busy`
(下载 RPC 超时 330s),一并迁到 `updateBusy`。
验证:`cargo build` / `cargo clippy` 干净;`vue-tsc --noEmit` 通过。
|
2026-10-17 20:18:32 +08:00 |
|
jkooe
|
b34e8c73e0
|
fix(console): 让「检查更新」在境内真的能用 —— 自持出网 + 系统代理发现
真机症状:控制台点「检查更新」必报 `检查更新失败:fetch failed`。
上一轮曾把此项归因为「环境网络问题」而收工;本轮按「即使环境本身不通,
代码也不该让用户无从自救」重新定位,确认为**产品缺陷**。
三条根因(均经隔离实测)
- Node 内建 fetch(undici)默认不读任何代理环境变量。`NODE_USE_ENV_PROXY=1` 可开,
未开时 env 被**完全忽略** —— `env -i` 对照实验:带 flag 得 `fetch failed`(证明 flag 生效),
不带得 `timeout`(证明 env 被忽略)。
- 系统代理(WinINET / macOS 网络设置)Node 亦不读;而 `fetch(url,{dispatcher})` 走不通 ——
dispatcher 必须是 undici 的 Dispatcher 实例,Node 未把 undici 暴露为可导入模块,
整包 vendored 会撑胖 sidecar 且其依赖 .wasm 解析器,esbuild 单文件打包有风险。
- 内部超时 20s 对真实重定向链必然失败:`latest.json` → 版本化 URL → CDN,
实测耗时 2.0 / 10.0 / 39.7 / 68.6 / >109 秒。
修法
- 新增 `apps/console/sidecar/proxy.mjs`(自持出网,零新依赖):https 目标走 `CONNECT host:443`
隧道后叠 TLS(隧道返回的 socket 已是 TLS,须用 `http.request` 承载,用 `https.request` 会二次握手;
且**不可把 URL 对象当第一参**,否则会覆盖手搭的代理字段退化成直连);http 目标走绝对形态请求行;
含 NO_PROXY 匹配、系统代理发现(`reg query` / `scutil --proxy`,PAC 不可解析时记 note 而非静默)、
socks5 等看不懂的形态一律记 note。
- `check_update` / `download` 全部改走 `httpGet`,整条重定向链共享一个超时预算与 AbortController。
- 内外超时成对下发:`sidecar.rs` 新增 `UPDATE_CHECK_TIMEOUT_MS`/`_RPC_TIMEOUT_SECS` 与
`UPDATE_DOWNLOAD_TIMEOUT_MS`/`_RPC_TIMEOUT_SECS` 两组常量 + **编译期不变式**
(`const _: () => assert!(...)`),保证「出网超时 < RPC 超时」——否则用户只会看到
丢掉真实原因的「check_update 超时」假失败。订正 6 处因超时改动而变陈旧的注释。
- 失败信息带 `url` + `via`/`proxy`/`proxy_source`/`timeout_ms`/`hint`;界面新增「出网」行,
并修掉重复前缀(`检查更新失败:检查更新失败:…`)。
顺带修掉两个真机暴露的缺陷
- **隧道/TLS 阶段的 abort 无人受理**:`createConnection` 回调返回前外层请求尚未建立,
外层 `signal`/`timeout` 无人受理 → 真机实测配 75s 超时却挂了 109s。
- **`ECONNRESET` 被归入 abort 类** → 报成「请求超时」(真机:耗时 82.9s 却报「120000ms 内未完成」)。
改为只认真 abort,并新增网络错误翻译(ENOTFOUND/EAI_AGAIN/ECONNREFUSED/ECONNRESET/EPIPE)。
守卫与验证
- 新增 `tests/unit/sidecar-proxy.test.mjs`(16 用例,四段:纯函数 / 真发请求 / 端到端
spawn sidecar 跑 JSON-RPC / 形态守卫)。夹具用本机真 `scutil --proxy` 与真 `reg query` 输出,
地址改为 RFC 5737 文档段,不写内网 IP、不写工具实名。
- 守卫落在「最终下发的请求」与「产物字节」上,逐条**注入故障实测会红、还原后逐字节一致**:
① `check_update` 退回裸 `fetch` → 代理未收到该次请求 → 红
② 去掉 `isBypassed` 后缀匹配 → 红
③ 出网/RPC 超时改回反序 → 红 / `error[E0080]`
④ 去掉隧道 abort 挂载 → 红(`实际 4002ms`)
⑤ `ECONNRESET` 塞回 abort 类 → 红
- 全量:`unit` **341/341**、`rust` **18/18**、`clippy -D warnings` 干净、`vue-tsc` 退出码 0。
未覆盖项(如实记档)
- https 经 CONNECT 隧道叠 TLS 无离线自动化用例:`selfsigned` 只在 `apps/agent` 包内,
测试进程解析不到 → 无法离线造受信任 TLS 站点;该路径仅真机验证。
- 真机「经代理取 GitHub 成功」的那一组未取到(宿主两条代理线路当时都取不到 GitHub),
属环境而非产品缺陷;错误信息已可操作(含 `via`/`proxy_source`/`hint`)。
- 同类缺陷**第三处未改**:`apps/agent/src/capabilities/selfupdate.ts` 仍是裸 `fetch`
(属被控端变更,改则需升 PATCH)。
注:本项为上壳变更,不触发 release patch;`apps/console` 至今未接入 `release.mjs`。
|
2026-10-13 22:41:02 +08:00 |
|
jkooe
|
9384b64c42
|
fix(console): sidecar 补 CREATE_NO_WINDOW(不再弹常驻黑窗)
真机复验 verbatim 修复时暴露的既有缺陷:点「检查更新」后屏幕上多出一个不消失的
黑窗(WindowsTerminal 承载,标题即 node.exe 路径)。
根因:控制台自身是 GUI 子系统,spawn 控制台子系统程序(node.exe)时 Windows 会
新建控制台,必须 CREATE_NO_WINDOW 抑制。AgentManager::start 早有此标志,
sidecar_command 漏了。修复 verbatim 前 sidecar 秒退、黑窗一闪而过;
sidecar 常驻后它才留在屏幕上。
修法:sidecar_command 补 creation_flags;常量提为 sidecar::CREATE_NO_WINDOW
由两处共用,避免值漂移。
本项无自动化守卫(如实记档):creation_flags 只有 setter 没有 getter,
断言不到最终值;扫源码属源码级检查,违反铁律 1。靠真机截图对照把关。
clippy 零告警,test:rust 18/18(行为零变化的注释/常量整理 + 一处新标志)。
|
2026-10-13 20:41:02 +08:00 |
|
jkooe
|
2437b0e863
|
fix(console): unverbatim 用 to_str 而非 to_string_lossy(不损坏非 UTF-8 路径)
to_string_lossy 会把非法字节替换成 U+FFFD 再重建路径 —— 那是损坏路径,
比不净化更糟:原本只是 node 报 EISDIR,改完变成指向一个不存在的目录。
改为 to_str():非 UTF-8 → None → 原样返回(Windows 上是含未配对代理项的路径,
Unix 上是含非法字节的文件名),绝不悄悄改写。
新增守卫 unverbatim_对非_utf8_的_verbatim_路径必须原样返回不得损坏(#[cfg(unix)]):
构造必须同时含 \\?\ 前缀与非法字节才有区分力 —— 不带前缀时新旧实现都走
「原样返回」那一步,测不出差异。
注入故障(退回 to_string_lossy)实测该守卫如期变红;还原后 sha256 逐字节一致。
pnpm test:rust 18/18(+1),clippy 零告警。
|
2026-10-13 20:21:51 +08:00 |
|
jkooe
|
56f64e1cc1
|
docs(console): 把 verbatim 路径的证据精确到「脚本参数」这一维
真机二维四格实验(ProcessStartInfo,精确对应 Command::new(prog).arg(script)):
G1 plain-prog + plain-script -> exit 0,正常
G2 plain-prog + VERBATIM-script -> exit 1,EISDIR lstat 'C:'
G3 VERBATIM-prog + plain-script -> exit 0,正常
G4 VERBATIM-prog + VERBATIM -> exit 1,EISDIR lstat 'C:'
结论:炸的是「脚本参数」带 verbatim,不是「可执行文件」带 verbatim。
旁证:真机上被控端与 sidecar 的 program 都带 \\?\ 却长期存活。
因此改准注释口径:净化 script 是关键修复,净化 node 属防御性
(守住「凡交给外部进程的路径都干净」这条单一纪律,不靠记位置)。
行为零变化(纯注释 + CHANGELOG)。
|
2026-10-13 20:16:07 +08:00 |
|
jkooe
|
eec76ed91e
|
fix(console): 交给 node 的路径去掉 Windows verbatim 前缀(真机 sidecar 秒退根因)
真机现象(Windows 11 ARM64,控制台 v2.0.3):点「检查更新」立刻报
「sidecar 已退出(stdout 关闭),请重试」—— sidecar 在请求进行中真的退出了。
真机 A/B(同一个 node.exe、同一份 sidecar.mjs,只换脚本参数写法):
C:\…\sidecar.mjs → 2.5 秒返回结果,进程常驻
\\?\C:\…\sidecar.mjs → 0.47 秒退出,exit 1,零 JS 执行
根因:Tauri 的 resource_dir() 在 Windows 上返回 canonicalize 后的 verbatim 路径,
把它当参数交给 node,node 启动期 realpathSync 把 `\\?\C:` 拆成 `C:` →
EISDIR ... lstat 'C:' → exit 1,一行 JS 都来不及执行。
本地/CI/开发模式全测不出:开发期走 CARGO_MANIFEST_DIR 的普通路径,只有打包后才暴露。
修法:
- 新增 unverbatim():剥 \\?\UNC\ → \\、剥 \\?\ → 无前缀,纯字符串处理,全平台可调用
- 新增 sidecar_command():唯一把路径交给 node 的地方,单测断言其最终 program/args
- resolve_node / resolve_sidecar / resolve_bundled_agent 全部出口加闸
- Sidecar::spawn 与 AgentManager::start 进程边界对 node/script/data_dir 三条路径净化
- autostart::app_exe() 净化(该路径进计划任务 -Execute)
回归:新增 3 条守卫;注入故障(去掉 sidecar_command 的 unverbatim)实测第三条如期变红,
还原后 sha256 逐字节一致。pnpm test:rust 17/17(14 → +3),clippy 零告警。
|
2026-10-13 20:11:03 +08:00 |
|
jkooe
|
73ed06f5f1
|
fix(console): 修复 sidecar 退出后被复用(Windows 更新超时根因)
现象(Win11 ARM64 真机):点「检查更新」等满 45 秒报「check_update 超时」。
对照:同一台机器手工拉起 sidecar 跑同一请求 6.989s 成功;现场 node.exe
只有被控端一个 —— 控制台的 sidecar 进程根本不存在。
根因:ensure_sidecar 只判「缓存里有没有实例」,不判「那个实例还活着没」。
往已退出进程的管道写,在 Windows 上未必立刻报错(缓冲区吞下即返回成功),
于是请求既不报错也永远等不到响应,只能干等到 request 的超时。
修法:
- Sidecar 增 dead: Arc<AtomicBool>,三处置死(stdout EOF / 写入或 flush 失败
/ shutdown 主动关闭);stdout 关闭时唤醒所有在等的请求,让它们立刻失败。
- 新增 is_alive():dead / child.try_wait() / 已被 take 走,拿不准一律按不可用。
- request() 入口先判存活,不可用即秒失败。
- ensure_sidecar 取到缓存实例先判存活,已退出则丢弃重建。
- 新增 3 条单测(含「对已退出的 sidecar 发请求必须秒失败」——给 30s 超时但
断言耗时 < 5s,因为修复前的表现正是干等到超时)。注入故障实测守卫变红。
- pnpm test:rust 14/14;test:unit 321/321。
属控制台上壳变更,不触发 release patch;但需重建控制台安装包才能让用户拿到修复。
|
2026-10-13 17:59:49 +08:00 |
|
jkooe
|
79969e9d5d
|
chore(release): v2.0.3
|
2026-10-13 17:36:54 +08:00 |
|
jkooe
|
530bb4fca7
|
fix(console): 加单实例保护,修「Windows 端起了两个被控端」
真机现象两桩:① Windows 上同时跑着两个被控端;② 旧的服务端还在跑时控制台起不来。
根因同一个:控制台没有单实例锁,再启动一次就是第二个进程 → 第二个 AgentManager
→ 抢同一个 8765 端口 → 后起的那个起不来。另有一条陈旧的 na-gui 计划任务指向测试
部署目录,也在抢同一个端口。
- 引入 tauri-plugin-single-instance(**必须注册为第一个插件**)。
- 第二次启动**不是**静默退出,而是「聚焦既有窗口 + 发 na-second-instance 事件」让前端
刷新状态。这是刻意的:自启动实例是**无窗口**的,若第二次启动静默退出,用户双击图标
只会看到「什么都不发生」—— 那比缺陷本身更难排查。带自启参数(AGENT_ONLY_FLAG)时跳过显示。
- 端口占用的报错补上「怎么找出占用者」:netstat -ano | findstr :<port> /
lsof -nP -iTCP:<port> -sTCP:LISTEN,并新增一条单测。
|
2026-10-13 17:16:41 +08:00 |
|
jkooe
|
2eb6a6f8bb
|
feat(console): 本机被控端托管 —— 启停/重启/日志/自更新/托盘/开机自启
目标:双击应用即用,全程不碰命令行。
要求 落点
不再打开命令行窗口 agent.rs 的 CREATE_NO_WINDOW + 自启拉控制台而非裸 node
启动/停止/重启 AgentManager 五态状态机 + 界面按钮 + 托盘菜单
检查更新 check_update / download / apply_update
日志功能 stdout/stderr 双泵 + 环形缓冲 + 实时推送 + 过滤/导出
后台运行 托盘常驻,关窗最小化而非退出
开机自启 macOS LaunchAgent / Windows 计划任务
新增「本机服务」页(/local,默认首页)与「服务日志」页(/logs)。
默认路由由 /nodes 改为 /local:打开应用先看到本机被控端的启停面板,
而不是先要求用户填一个连接地址。
四条实测踩出来的关键约束(均已在代码里注明原因):
1. 退出监控用轮询 try_wait,绝不把 Child 交给 wait()
—— wait() 会取得 Child 所有权,此后 stop() 再也拿不到它去 kill。
2. 日志游标用独立单调 seq,不用位置下标
—— 环形缓冲淘汰旧行后位置下标会漂移,前端增量拉取静默错位(不报错)。
3. start() / restart() 的接收者必须是 self: &Arc<Self>
—— 监控任务要持自持引用。
4. macOS gui/<uid> 的 uid 运行时 id -u 现取,不硬编码 501。
播种(seeding)防降级:只有「随包版本 > 已安装版本」才覆盖数据目录里的脚本,
且版本比较按数值逐段(2.0.10 > 2.0.9)。若写成「同版本才跳过」,
用户自更新过被控端后下次开控制台会被静默降回旧版。
「实例是否在跑」不靠单一信号:管理器状态与 TCP 端口探测交叉判定,
以区分「本控制台托管的实例」与「手工安装的非托管实例」并给出不同处置。
消灭黑窗有三条措施,第 2 条最易漏:Rust 的 std::process::Command 默认不设
CREATE_NO_WINDOW,于是「界面上点一下启动」= 屏幕上闪一个控制台窗口。
控制台现在随包分发被控端脚本(资源 2 份 → 3 份:sidecar.mjs / node / agent.mjs),
用户装完控制台本机被控端即已就绪。
新增 tests/unit/console-commands.test.mjs(6 用例)钉住三类「不报错的失效」:
invoke ⇄ generate_handler! 双向、emit ⇄ listen 双向(这类失效会让界面永不刷新且零报错)、
bundle 资源名 ⇄ Rust resolve_* 字面量、sidecar 方法名、路由表 ⇄ 实际 view 文件。
已注入故障实测会红(改命令名与事件名 → 当场 2 条失败),非空转守卫。
验证:cargo check 0 错 0 警 / cargo test --lib 10·10 通过(含真起 node 子进程)/
clippy -D warnings 通过 / vue-tsc 通过 / 单测 280·280(274 → 280)/
verify-bundle 真 spawn 打包内 node 跑通打包内 sidecar。
⚠️ 托盘、关窗最小化、开机自启、界面点按钮仍需图形环境人工实点。
|
2026-10-11 11:41:56 +08:00 |
|
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
|
a515eb2e63
|
chore(release): v2.0.0
|
2026-10-10 12:14:02 +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 |
|