复刻 @nodeagent/client 的协议行为,供桌面控制台(Tauri 壳)经 Cargo path 依赖: - 模块:error / types / ulid / crypto / tls / discovery / client - 握手:hello → challenge → auth → auth_ok;psk(HMAC-SHA256) 与 ed25519 两种模式 - 证书指纹 sha256(DER) 钉住(E_CERT_MISMATCH);ULID(Crockford Base32) - 断线指数退避重连 1s→30s(±15% 抖动) Rust async Send 三层陷阱(本轮主要成本,解法已写入 client.rs 注释): 1. futures_util::lock::Mutex 无手写 Sync 实现 → 含它的类型整体 !Sync 2. tokio::sync::MutexGuard 非 Send 3. establish → spawn(read_loop) → on_disconnected → establish 构成**类型级递归**, 编译器无法证明 Send。最终采用**监督者模式**:单个 supervise 任务串行 「建立+握手 → 等读循环 Notify 断开 → 退避重连」,读循环只 notify 不回调 establish,从而打破递归;发送半用专用写任务 + mpsc 解耦(同步 send,不跨 await 持锁)。 测试:9 单测(crypto/ulid/Send+Sync 断言)+ 2 E2E(psk 握手→invoke 全链路、连不上端口 返回 NODE_OFFLINE),`cargo clippy --all-targets -- -D warnings` 零告警。 另附 examples/connect.rs(连真实被控端)与 examples/probe.rs(字段契约探针, 输出顶层键+数组首元素键,并按 system.info.os 区分「平台不支持」与「真失败」)。 .gitignore:crates/ 转正后必须显式忽略 /crates/target/ —— 此前是靠整个 /crates/ 被忽略才没让 4GB 级构建产物进库,取消那条保护后会裸奔。
54 lines
1.6 KiB
Rust
54 lines
1.6 KiB
Rust
//! 连接一个真实 nodeagent 被控端的最小示例。
|
|
//!
|
|
//! 用法:`cargo run --example connect -- <url> <key> [client_id]`
|
|
//! 例如:`cargo run --example connect -- ws://127.0.0.1:8765 test-psk-key`
|
|
|
|
use nodeagent_client::{Client, ClientEvent, ClientOptions};
|
|
use serde_json::json;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
let url = args
|
|
.get(1)
|
|
.cloned()
|
|
.unwrap_or_else(|| "ws://127.0.0.1:8765".to_string());
|
|
let key = args.get(2).cloned().unwrap_or_default();
|
|
let client_id = args
|
|
.get(3)
|
|
.cloned()
|
|
.unwrap_or_else(|| "rust-example".to_string());
|
|
|
|
let client = Client::new(ClientOptions {
|
|
url: url.clone(),
|
|
key,
|
|
client_id,
|
|
insecure: true,
|
|
auto_reconnect: false,
|
|
..Default::default()
|
|
});
|
|
|
|
client.set_handler(std::sync::Arc::new(|evt| {
|
|
if let ClientEvent::Log(msg) = evt {
|
|
println!("[log] {msg}");
|
|
}
|
|
}));
|
|
|
|
let caps = client.connect().await?;
|
|
println!("握手成功,{} 项能力", caps.len());
|
|
println!("agent 版本: {:?}", client.agent_version());
|
|
println!("证书指纹: {:?}", client.peer_cert_fingerprint());
|
|
for c in caps.iter().take(12) {
|
|
println!(" - {} (risk={}, v{})", c.name, c.risk, c.version);
|
|
}
|
|
|
|
let res = client.invoke("system.info", json!({})).await?;
|
|
println!("\nsystem.info → status={}", res.status);
|
|
if let Some(data) = &res.data {
|
|
println!("{}", serde_json::to_string_pretty(data)?);
|
|
}
|
|
|
|
client.close();
|
|
Ok(())
|
|
}
|