From 5571cf1aea604c36c3c5cf59c3fccd1e697a9a59 Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:22:58 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=8F=96=E6=B6=88=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E4=BA=A4=E7=9A=84=E5=9C=A8=E5=BB=BA=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=EF=BC=88=E4=BF=9D=E6=8C=81=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8D=E5=8A=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一个提交(v1.6)用了 git add -A,把工作区里**不属于本任务的用户在建文件** 一起带入并推送:crates/(Rust 客户端脚手架)、docs/console-plan.md、 docs/ROADMAP.md,以及 docs/RETROSPECTIVE.md 的本地改动。 本提交将它们移出 git 跟踪(磁盘文件全部保留、回到未跟踪状态), 使公开仓库只包含 v1.6 的预期变更。如需从历史中彻底抹去, 可另行 filter-repo 重写(公开仓库,需皇上决策)。 --- crates/Cargo.lock | 1174 ------------------------- crates/Cargo.toml | 5 - crates/nodeagent-client/Cargo.toml | 22 - crates/nodeagent-client/src/crypto.rs | 131 --- crates/nodeagent-client/src/error.rs | 95 -- crates/nodeagent-client/src/lib.rs | 19 - crates/nodeagent-client/src/types.rs | 212 ----- crates/nodeagent-client/src/ulid.rs | 72 -- docs/ROADMAP.md | 359 -------- docs/console-plan.md | 258 ------ 10 files changed, 2347 deletions(-) delete mode 100644 crates/Cargo.lock delete mode 100644 crates/Cargo.toml delete mode 100644 crates/nodeagent-client/Cargo.toml delete mode 100644 crates/nodeagent-client/src/crypto.rs delete mode 100644 crates/nodeagent-client/src/error.rs delete mode 100644 crates/nodeagent-client/src/lib.rs delete mode 100644 crates/nodeagent-client/src/types.rs delete mode 100644 crates/nodeagent-client/src/ulid.rs delete mode 100644 docs/ROADMAP.md delete mode 100644 docs/console-plan.md diff --git a/crates/Cargo.lock b/crates/Cargo.lock deleted file mode 100644 index 83b606a..0000000 --- a/crates/Cargo.lock +++ /dev/null @@ -1,1174 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aws-lc-rs" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cc" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "ctutils" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" -dependencies = [ - "cmov", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - -[[package]] -name = "der" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" -dependencies = [ - "const-oid", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "ctutils", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "ed25519" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" -dependencies = [ - "curve25519-dalek", - "ed25519", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - -[[package]] -name = "find-msvc-tools" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-macro", - "futures-sink", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hybrid-array" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" -dependencies = [ - "typenum", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "libc" -version = "0.2.190" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" - -[[package]] -name = "litemap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "mio" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nodeagent-client" -version = "0.1.0" -dependencies = [ - "base64", - "ed25519-dalek", - "futures-util", - "hmac", - "rand", - "rustls", - "rustls-pemfile", - "serde", - "serde_json", - "sha2", - "thiserror", - "tokio", - "tokio-tungstenite", - "url", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "potential_utf" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" -dependencies = [ - "zerovec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spki" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" -dependencies = [ - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tungstenite" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.9", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/Cargo.toml b/crates/Cargo.toml deleted file mode 100644 index 7502396..0000000 --- a/crates/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[workspace] -resolver = "2" -members = ["nodeagent-client"] - -[workspace.dependencies] diff --git a/crates/nodeagent-client/Cargo.toml b/crates/nodeagent-client/Cargo.toml deleted file mode 100644 index 3863923..0000000 --- a/crates/nodeagent-client/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "nodeagent-client" -version = "0.1.0" -edition = "2021" -description = "nodeagent 控制端 Rust 客户端(行为对齐 @nodeagent/client)" -license = "MIT" - -[dependencies] -base64 = "0.23.1" -ed25519-dalek = { version = "3.0.0", features = ["pkcs8"] } -futures-util = "0.3.34" -hmac = "0.13.0" -rand = "0.10.3" -rustls = "0.23.45" -rustls-pemfile = "2.2.0" -serde = { version = "1.0.229", features = ["derive"] } -serde_json = "1.0.151" -sha2 = "0.11.0" -thiserror = "2.0.21" -tokio = { version = "1.53.2", features = ["rt-multi-thread", "net", "time", "sync", "macros", "io-util"] } -tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-webpki-roots"] } -url = "2.5.8" diff --git a/crates/nodeagent-client/src/crypto.rs b/crates/nodeagent-client/src/crypto.rs deleted file mode 100644 index ab69f3b..0000000 --- a/crates/nodeagent-client/src/crypto.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! 密码学工具:HMAC / ed25519 签名 / 证书指纹 / nonce。 -//! 逐一对齐 `packages/protocol/src/{hmac,ed25519}.ts` 与 `net.ts`。 - -use base64::engine::general_purpose::STANDARD; -use base64::Engine as _; -use hmac::{Hmac, Mac}; -use sha2::{Digest, Sha256}; - -type HmacSha256 = Hmac; - -/// 计算 HMAC-SHA256,返回 Base64(对齐 hmac.ts 的 `computeHmac`)。 -/// 密钥为预共享密钥字符串,nonce 以 UTF-8 编码。 -pub fn compute_hmac(key: &str, nonce: &str) -> String { - // HMAC 接受任意长度 key,此处不可能失败,unwrap 仅为类型收敛。 - let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC 接受任意长度 key"); - mac.update(nonce.as_bytes()); - STANDARD.encode(mac.finalize().into_bytes()) -} - -/// 生成随机 nonce(32 字节,Base64),对齐 `generateNonce`。 -pub fn generate_nonce() -> String { - let mut bytes = [0u8; 32]; - for b in bytes.iter_mut() { - *b = rand::random::(); - } - STANDARD.encode(bytes) -} - -/// 证书指纹:sha256(DER) 的十六进制小写 64 位(对齐 net.ts 的 `certFingerprint`)。 -pub fn cert_fingerprint(der: &[u8]) -> String { - let digest = Sha256::digest(der); - digest.iter().map(|b| format!("{b:02x}")).collect() -} - -/// 对 nonce 原文做 ed25519 签名,返回 Base64(对齐 ed25519.ts 的 `signNonce`)。 -/// 私钥为 Base64(PKCS8 DER)。 -pub fn sign_nonce(private_key_b64: &str, nonce: &str) -> Result { - use ed25519_dalek::pkcs8::DecodePrivateKey; - use ed25519_dalek::Signer; - - let der = STANDARD - .decode(private_key_b64) - .map_err(|e| format!("私钥 Base64 解码失败: {e}"))?; - let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(&der) - .map_err(|e| format!("PKCS8 私钥解析失败: {e}"))?; - let sig = signing_key.sign(nonce.as_bytes()); - Ok(STANDARD.encode(sig.to_bytes())) -} - -/// 验签(对齐 `verifyNonce`);任何异常均视为失败(不抛出)。 -pub fn verify_nonce(public_key_b64: &str, nonce: &str, signature_b64: &str) -> bool { - use ed25519_dalek::pkcs8::DecodePublicKey; - use ed25519_dalek::Verifier; - - let Ok(pk_der) = STANDARD.decode(public_key_b64) else { - return false; - }; - let Ok(vk) = ed25519_dalek::VerifyingKey::from_public_key_der(&pk_der) else { - return false; - }; - let Ok(sig_bytes) = STANDARD.decode(signature_b64) else { - return false; - }; - let Ok(sig) = ed25519_dalek::Signature::from_slice(&sig_bytes) else { - return false; - }; - vk.verify(nonce.as_bytes(), &sig).is_ok() -} - -/// 公钥指纹(SHA-256 前 16 位 hex),对齐 `keyId`。 -pub fn key_id(public_key_b64: &str) -> String { - let der = STANDARD.decode(public_key_b64).unwrap_or_default(); - let digest = Sha256::digest(&der); - // 前 16 个 hex 字符 = 前 8 字节。 - digest.iter().take(8).map(|b| format!("{b:02x}")).collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use ed25519_dalek::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey}; - - fn gen_keypair() -> (String, String) { - use ed25519_dalek::SigningKey; - let sk = SigningKey::generate(&mut rand::rng()); - let pk = sk.verifying_key(); - ( - STANDARD.encode(pk.to_public_key_der().expect("编码公钥")), - STANDARD.encode(sk.to_pkcs8_der().expect("编码私钥")), - ) - } - - #[test] - fn hmac_matches_known_vector() { - // 与 Node 版对拍的固定向量:HMAC-SHA256("key", "nonce")。 - let out = compute_hmac("secret", "hello"); - // 用独立的 hmac 重算并比对,避免自证。 - let mut mac = HmacSha256::new_from_slice(b"secret").unwrap(); - mac.update(b"hello"); - let expect = STANDARD.encode(mac.finalize().into_bytes()); - assert_eq!(out, expect); - // Base64(HMAC-SHA256("secret","hello")) 的已知值(由 RFC 测试向量推得,供跨端核对)。 - assert_eq!(out, "iKqz7ejTrflNJquQ07r9SiCDBww7zOnAFO4EpEOEfAs="); - } - - #[test] - fn sign_and_verify_roundtrip() { - let (pk, sk) = gen_keypair(); - let nonce = generate_nonce(); - let sig = sign_nonce(&sk, &nonce).unwrap(); - assert!(verify_nonce(&pk, &nonce, &sig)); - assert!(!verify_nonce(&pk, "tampered", &sig)); - } - - #[test] - fn key_id_is_16_hex() { - let (pk, _sk) = gen_keypair(); - let id = key_id(&pk); - assert_eq!(id.len(), 16); - assert!(id.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn cert_fingerprint_sha256_der() { - // 空 DER 的 sha256 是已知常量。 - assert_eq!( - cert_fingerprint(b""), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - ); - } -} diff --git a/crates/nodeagent-client/src/error.rs b/crates/nodeagent-client/src/error.rs deleted file mode 100644 index 23a9f57..0000000 --- a/crates/nodeagent-client/src/error.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! 协议错误码与客户端错误类型(对齐 `packages/protocol/src/errors.ts`)。 - -use std::fmt; - -/// 协议错误码常量(对齐 errors.ts 的 `ErrorCodes`)。 -pub mod ErrorCodes { - pub const PARSE_ERROR: i64 = -32700; - pub const INVALID_REQUEST: i64 = -32600; - pub const METHOD_NOT_FOUND: i64 = -32601; - pub const PARAM_INVALID: i64 = -32602; - pub const AUTH_FAILED: i64 = -32401; - pub const AUTH_REQUIRED: i64 = -32402; - pub const CAPABILITY_NOT_FOUND: i64 = -32403; - pub const CAPABILITY_DISABLED: i64 = -32404; - pub const NODE_OFFLINE: i64 = -32405; - pub const ACL_DENIED: i64 = -32406; - pub const RATE_LIMITED: i64 = -32407; - pub const TIMEOUT: i64 = -32412; - pub const EXECUTION_FAILED: i64 = -32419; - pub const PROTOCOL_MISMATCH: i64 = -32420; - pub const UNSUPPORTED_PLATFORM: i64 = -32421; - /// v21:被控端证书指纹与控制端钉住的不一致(可能被冒充)。 - pub const CERT_MISMATCH: i64 = -32422; -} - -/// 错误码 → 名称。 -pub fn error_name(code: i64) -> &'static str { - match code { - ErrorCodes::PARSE_ERROR => "E_PARSE_ERROR", - ErrorCodes::INVALID_REQUEST => "E_INVALID_REQUEST", - ErrorCodes::METHOD_NOT_FOUND => "E_METHOD_NOT_FOUND", - ErrorCodes::PARAM_INVALID => "E_PARAM_INVALID", - ErrorCodes::AUTH_FAILED => "E_AUTH_FAILED", - ErrorCodes::AUTH_REQUIRED => "E_AUTH_REQUIRED", - ErrorCodes::CAPABILITY_NOT_FOUND => "E_CAPABILITY_NOT_FOUND", - ErrorCodes::CAPABILITY_DISABLED => "E_CAPABILITY_DISABLED", - ErrorCodes::NODE_OFFLINE => "E_NODE_OFFLINE", - ErrorCodes::ACL_DENIED => "E_ACL_DENIED", - ErrorCodes::RATE_LIMITED => "E_RATE_LIMITED", - ErrorCodes::TIMEOUT => "E_TIMEOUT", - ErrorCodes::EXECUTION_FAILED => "E_EXECUTION_FAILED", - ErrorCodes::PROTOCOL_MISMATCH => "E_PROTOCOL_MISMATCH", - ErrorCodes::UNSUPPORTED_PLATFORM => "E_UNSUPPORTED_PLATFORM", - ErrorCodes::CERT_MISMATCH => "E_CERT_MISMATCH", - _ => "E_UNKNOWN", - } -} - -/// 客户端错误(携带协议错误码,对齐 client.ts 的 `ClientError`)。 -#[derive(Debug, Clone)] -pub struct ClientError { - pub code: i64, - pub name: String, - pub message: String, - pub data: Option, -} - -impl ClientError { - pub fn new(code: i64, message: impl Into) -> Self { - Self { - code, - name: error_name(code).to_string(), - message: message.into(), - data: None, - } - } - - pub fn with_data(mut self, data: serde_json::Value) -> Self { - self.data = Some(data); - self - } - - /// 被控端离线/不可达。 - pub fn offline(message: impl Into) -> Self { - Self::new(ErrorCodes::NODE_OFFLINE, message) - } - - /// 调用超时。 - pub fn timeout(message: impl Into) -> Self { - Self::new(ErrorCodes::TIMEOUT, message) - } - - /// 证书指纹不一致。 - pub fn cert_mismatch(message: impl Into) -> Self { - Self::new(ErrorCodes::CERT_MISMATCH, message) - } -} - -impl fmt::Display for ClientError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.name, self.message) - } -} - -impl std::error::Error for ClientError {} diff --git a/crates/nodeagent-client/src/lib.rs b/crates/nodeagent-client/src/lib.rs deleted file mode 100644 index 72a53a5..0000000 --- a/crates/nodeagent-client/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! nodeagent 控制端 Rust 客户端。 -//! -//! 行为对齐 `packages/client`(Node 版),复刻: -//! - 握手 hello → challenge → auth → auth_ok(psk HMAC / ed25519 签名) -//! - 泛型能力调用 invoke(带超时与 id 匹配) -//! - 事件通知(无 id 的 JSON-RPC notification,`event` 方法) -//! - 断线指数退避重连(1s → 30s,±15% 抖动) -//! - TLS 自签证书 + 指纹钉住(v21,E_CERT_MISMATCH) - -pub mod client; -pub mod crypto; -pub mod discovery; -pub mod error; -pub mod tls; -pub mod types; -pub mod ulid; - -pub use client::{Client, ClientEvent, ClientOptions, ConnState}; -pub use error::{ClientError, ErrorCodes}; diff --git a/crates/nodeagent-client/src/types.rs b/crates/nodeagent-client/src/types.rs deleted file mode 100644 index 7ad634f..0000000 --- a/crates/nodeagent-client/src/types.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! 协议消息类型(对齐 `packages/protocol/src/messages.ts`)。 -//! -//! 所有对外字段名保持 snake_case 与 TS 侧一致,经 serde 序列化/反序列化。 - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -/// 协议版本(对齐 methods.ts 的 `PROTOCOL_VERSION`)。 -pub const PROTOCOL_VERSION: &str = "1.0"; - -/// JSON-RPC 2.0 请求信封(nodeagent 简化版)。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RpcRequest { - pub jsonrpc: String, - /// ULID,响应回填相同 id。 - pub id: String, - pub method: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub params: Option, -} - -/// JSON-RPC 2.0 成功响应。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RpcResponse { - pub jsonrpc: String, - pub id: String, - pub result: Value, -} - -/// 协议错误对象。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RpcError { - pub code: i64, - pub name: String, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -/// JSON-RPC 2.0 错误响应。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RpcErrorResponse { - pub jsonrpc: String, - #[serde(default)] - pub id: Option, - pub error: RpcError, -} - -/// 任意入站消息:成功响应或错误响应(无 id 的通知由上层单独处理)。 -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -pub enum InboundMessage { - Response(RpcResponse), - Error(RpcErrorResponse), - Notification { - #[serde(default)] - id: Option, - method: String, - #[serde(default)] - params: Option, - }, -} - -/// 判断消息是否为错误响应。 -pub fn is_error_response(msg: &Value) -> bool { - msg.get("error").is_some() -} - -// ---------- 握手 ---------- - -/// hello 入参。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HelloParams { - pub protocol: String, - pub client_id: String, -} - -/// challenge 出参。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChallengeParams { - pub nonce: String, - pub expires_at: i64, -} - -/// auth 入参。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthParams { - pub client_id: String, - pub nonce: String, - /// psk 模式:Base64(HMAC-SHA256(pre_shared_key, nonce))。 - #[serde(skip_serializing_if = "Option::is_none")] - pub hmac: Option, - /// ed25519 模式:Base64(Ed25519_Sign(privkey, nonce))。 - #[serde(skip_serializing_if = "Option::is_none")] - pub signature: Option, -} - -/// auth_ok 出参。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthOkParams { - pub capabilities: Vec, - /// ed25519 模式:本次实际被授权的能力(全量子集)。 - #[serde(skip_serializing_if = "Option::is_none")] - pub authorized: Option>, - /// 本次连接采用的认证模式。 - #[serde(skip_serializing_if = "Option::is_none")] - pub auth_mode: Option, - /// v20:被控端语义化版本。 - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_version: Option, - /// v20:被控端构建信息。 - #[serde(skip_serializing_if = "Option::is_none")] - pub build: Option, - /// v20:被控端支持的协议版本。 - #[serde(skip_serializing_if = "Option::is_none")] - pub protocol: Option, -} - -/// 被控端构建信息(v20)。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BuildInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub commit: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub built_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cert_sha256: Option, -} - -// ---------- 调用 ---------- - -/// invoke 入参。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InvokeParams { - pub capability: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub args: Option, -} - -/// 能力执行结果(业务层)。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InvokeResult { - pub status: String, // "ok" | "failed" - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl InvokeResult { - pub fn is_ok(&self) -> bool { - self.status == "ok" - } -} - -/// 能力执行失败的错误信息。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InvokeError { - pub name: String, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -/// 能力描述(能力清单条目)。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CapabilityDescriptor { - pub name: String, - pub version: String, - pub description: String, - pub risk: String, // "low" | "medium" | "high" - pub params_schema: JsonSchema, - #[serde(skip_serializing_if = "Option::is_none")] - pub returns_schema: Option, -} - -/// JSON Schema 子集(见 DEVELOPMENT.md 4.3)。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JsonSchema { - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub schema_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option>, - #[serde(rename = "additionalProperties", skip_serializing_if = "Option::is_none")] - pub additional_properties: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub r#enum: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub maximum: Option, - #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")] - pub min_length: Option, - #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")] - pub max_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pattern: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>, - #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")] - pub min_items: Option, - #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")] - pub max_items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, -} diff --git a/crates/nodeagent-client/src/ulid.rs b/crates/nodeagent-client/src/ulid.rs deleted file mode 100644 index da318c2..0000000 --- a/crates/nodeagent-client/src/ulid.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! ULID 生成器(Crockford Base32,26 字符:10 位时间戳 + 16 位随机)。 -//! 对齐 `packages/protocol/src/ulid.ts`(自实现,避免额外依赖)。 - -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Crockford Base32 字母表(与 ulid.ts 完全一致)。 -const ENCODING: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; - -/// 生成一个 ULID(单调递增:同一毫秒内保证字典序递增)。 -pub fn ulid() -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - ulid_at(now) -} - -/// 按指定时间戳生成 ULID(供测试注入确定性时间)。 -pub fn ulid_at(now: u64) -> String { - // 时间戳部分:10 字符,低位在前地填充(不足 10 位高位补 '0')。 - let mut time_part = [b'0'; 10]; - let mut t = now; - for i in (0..10).rev() { - time_part[i] = ENCODING[(t % 32) as usize]; - t /= 32; - } - - // 随机部分:16 字符。256 % 32 == 0,取模无偏差。 - let mut rand_part = [b'0'; 16]; - for slot in rand_part.iter_mut() { - *slot = ENCODING[(rand::random::() % 32) as usize]; - } - - let mut out = String::with_capacity(26); - out.push_str(std::str::from_utf8(&time_part).unwrap()); - out.push_str(std::str::from_utf8(&rand_part).unwrap()); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn length_is_26() { - assert_eq!(ulid().len(), 26); - } - - #[test] - fn only_valid_charset() { - let s = ulid(); - let set: Vec = ENCODING.iter().map(|&b| b as char).collect(); - assert!(s.chars().all(|c| set.contains(&c))); - } - - #[test] - fn monotonic_same_millis_lexicographic() { - // 同一毫秒内,两个 ULID 的时间前缀必须相同(保证字典序可比较)。 - let a = ulid_at(1_700_000_000_000); - let b = ulid_at(1_700_000_000_000); - assert_eq!(&a[..10], &b[..10]); - } - - #[test] - fn distinct() { - let mut seen = std::collections::HashSet::new(); - for _ in 0..1000 { - seen.insert(ulid()); - } - assert_eq!(seen.len(), 1000); - } -} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index 2de1b02..0000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,359 +0,0 @@ -# nodeagent 进度盘点与下一步路线 - -> 盘点日期:2026-10-07(**当日第二次更新**,据工作区最新推进刷新) -> 盘点方式:只读核查仓库(`git fetch` + 源码 grep + 本地跑单测) -> 基线:远端 main `0d62cb1`(tag **v1.5.0**)|本地 HEAD `7ad820c`(1 个未推送 CI 提交) -> 工作区:有**未提交的业务改动** —— `gui.await`(v1.6,语义方向一的首个落地) -> 适用范围:本文档随仓库公开,**不含任何真机地址与账户信息**(见 §6 脱敏约定) - ---- - -## 一、结论先行 - -功能层面已基本收口。**43 项能力**覆盖了「命令接管 → 图形接管 → 零信任 → 文件传输 → Hub 中转 → 自更新」六条主线,安全加固已发版两批。 - -**方向一(语义可断言)已开工**:`gui.await` 在工作区完成实现(170 行 + 57 行 manifest + 注册),把 GUI 操作从「盲试」推进到「可等待、可断言」。**但尚未收尾** —— 详见 §4。 - -当前瓶颈: - -| 性质 | 问题 | 影响 | -|---|---|---| -| **在建未收尾** | `gui.await` 已实现但**未提交、未发版、单测未覆盖、CI 未绿** | 能力数断言失败(见下),远端拿不到 | -| **收尾缺口** | 本地 1 个 CI 提交未推送;`ci/workflow-unit-tests` 分支贡献已并入但分支未清理 | CI 护栏未生效于远端 | -| **语义深度** | `gui.await` 解决了「等得到」,但 `screen.find` 仍不返回 UIA **属性**(value/enabled/selected) | 能等「元素出现」,还不能等「元素变成某状态」 | -| **状态留存** | 事件订阅已通,但无采样留存与回看 | 间歇性问题(代理不通、杀软拦截)仍只能靠巧遇 | - -> ⚠️ **当前工作区单测是红的**:195 例中 **194 通过、1 失败** —— `tests/unit/capabilities.test.mjs:66` 硬编码 `assert.equal(CAPABILITY_MANIFEST.length, 42)`,而 `gui.await` 使能力数变 43。**修断言前不要提交**,否则 CI 必红。 - -优先级:**`gui.await` 收尾 > 语义属性 > 状态采样 > 安全残余**。 - ---- - -## 二、现状快照 - -### 2.1 版本与仓库 - -| 项 | 值 | -|---|---| -| `package.json` version | `1.5.0`(root 及 6 个子包一致)—— **未随 `gui.await` 上调** | -| 本地 HEAD | `7ad820c` ci: 补全 Windows 真机验证 | -| 远端 main | `0d62cb1`(tag **`v1.5.0`**) | -| 本地领先远端 | **1 个提交**(仅 `.github/workflows/windows-e2e.yml`,未推送) | -| **未提交业务改动** | `gui.await` 全套:`apps/agent/src/capabilities/await.ts`(新增 170 行)/`capabilities/index.ts`(+3 注册)/`packages/protocol/.../names.ts`(+1 能力名)/`manifest/graphics.ts`(+57 行契约) | -| 未合并分支 | `ci/workflow-unit-tests`(其贡献**已手工并入** `7ad820c`,分支本身基于 09-29 老 main,直接 merge 会回退上万行,故**只取内容不 merge**,分支可清理) | -| 未跟踪文件 | `docs/ROADMAP.md`(本文档)、`docs/console-plan.md`(大前端方案) | -| Release 资产 | `nodeagent-win-x64.zip`(≈32.9MB,含 Node 运行时)/ `agent.mjs` / `latest.json` / `SHA256SUMS` | - -**近期提交脉络**: - -``` -0d62cb1 (tag v1.5.0) chore(release): v1.5.0 -1c836e0 feat(v1.5): 快捷键控制范围扩展(字符串热键/预设别名/长按/后台定向投递) -0ee8231 feat(v1.2): 媒体键 WM_APPCOMMAND 通道 + 定向投递 -42ec556 (tag v1.4.3) chore(release): v1.4.3 -7ad820c (本地 HEAD) ci: 补全 Windows 真机验证 ← 未推送 -[工作区未提交] feat(v1.6): gui.await 等待条件 ← 在建 -``` - -### 2.2 代码与测试 - -| 项 | 数值 | -|---|---| -| `apps/` 代码量 | 12,201 行 TS(agent / cli / hub / mcp)+ `await.ts` 170 行(在建) | -| `packages/` 代码量 | 3,620 行 TS(client / protocol)+ manifest 57 行(在建) | -| 合计 | **约 1.58 万行** | -| 单测 | 18 个文件 / **195 例:194 通过、1 失败**(本地实跑) | -| e2e | 2 个套件(`run.mjs` 通用 29 例 / `windows.mjs` Windows 专属 13 例) | -| **`gui.await` 覆盖** | **0** —— 单测、e2e、CLI、MCP 均无 `gui.await`/`GuiAwait` 命中 | - -> ⚠️ 单测数从 162 增至 195(新增 33 例,来自 v1.5 快捷键的按键表覆盖断言),但**红了一例**:`tests/unit/capabilities.test.mjs:66` 的 `assert.equal(CAPABILITY_MANIFEST.length, 42)`,实际 43。**修法**:改为 43,或更稳妥地改成「断言每个 `CapabilityNames` 键都在 manifest 中」以免每加一能力都要改数。 - -**最大的三个文件**(后续拆分候选): - -| 文件 | 行数 | 说明 | -|---|---|---| -| `apps/mcp/src/index.ts` | 1000 | MCP 工具定义全集中,协议前缀 `na_` | -| `apps/agent/src/capabilities/window.ts` | 963 | UIA 枚举 + 模板匹配 + OCR 三条路径 | -| `apps/agent/src/capabilities/network.ts` | 747 | 两阶段提交网络变更 | - -### 2.3 能力清单(43 项,以 `packages/protocol/src/capabilities/names.ts` 为准) - -| 域 | 能力 | -|---|---| -| **系统与命令**(5) | `system.info` `system.status` `system.process.list` `system.service.list` `system.shell.exec` | -| **软件**(2) | `app.list` `app.install`(winget 静默优先) | -| **屏幕与录制**(4) | `screen.info` `screen.capture` `screen.record` `screen.find` | -| **输入**(6) | `input.mouse.move` `input.mouse.click` `input.mouse.scroll` `input.mouse.drag` `input.key.type` `input.key.press` | -| **窗口**(2) | `window.list` `window.focus` | -| **GUI 等待**(1) | **`gui.await`** 🆕 v1.6 在建未提交 —— 等条件成立再返回 | -| **文件**(4) | `fs.list` `fs.stat` `fs.read` `fs.write`(分块 RPC + 原子写) | -| **剪贴板**(2) | `clip.get` `clip.set` | -| **异步任务**(3) | `system.task.list` `system.task.get` `system.task.kill` | -| **审计与指标**(3) | `system.audit.list` `system.audit.verify`(哈希链) `system.metrics` | -| **事件订阅**(4) | `event.watch` `event.unwatch` `event.list` `event.poll` | -| **自持与更新**(2) | `system.agent.restart`(走计划任务) `system.agent.update`(拉取式,校验 sha256) | -| **音频**(2) | `system.audio.get` `system.audio.set`(Core Audio COM) | -| **网络变更**(3) | `system.net.apply` `system.net.confirm` `system.net.status` | - -**`gui.await` 设计要点**(工作区在建,v1.6): - -| 项 | 内容 | -|---|---| -| 四类条件 | `window` / `control` / `process` / `file`,各自复用既有 `window.list` / `screen.find` / `process.list` / `fs.stat` —— **零新增平台探测代码**,Windows/macOS 行为与底层能力完全一致 | -| 两个状态 | `state=present`(默认,等出现)/`absent`(等消失) | -| 参数 | `timeout_ms`(0~60000,默认 5000)/`interval_ms`(50~5000,默认 400) | -| 返回 | `{satisfied, condition, state, elapsed_ms, attempts, last_seen, last_error?, note?}` | -| **超时是正常返回** | 返 `satisfied:false` + `last_seen` + `note`,**不抛异常** —— 避免上层反复重试。`last_error` 带回末次异常,用于区分「真的没有」与「每次都在报错」 | -| 轮询期异常 | 视为「尚未命中」并记录,不让单次抖动整句失败(启动中的程序常短暂报错) | -| 风控 | `risk: 'low'`(纯只读),`additionalProperties: false` | - -典型编排: - -``` -await gui.await(condition:"control", text:"完成", timeout_ms:30000) - → input.mouse.click(命中坐标) - → gui.await(condition:"control", text:"安装中", state:"absent") -``` - -> v1.5.0 未新增能力名,而是**扩展了 `input.key.press` 的参数面**:字符串热键 `hotkey: "ctrl+shift+esc"`、约 50 个预设别名(`copy` / `show_desktop` / `task_manager` / `volume_mute_toggle`…)、`hold_ms` 长按、`route=post` + `target_pid` 后台定向投递;按键表补齐左右侧修饰键、小键盘、OEM 符号键(解锁 `win+d` / `ctrl++` / `win+.`)、IME 键。**这些是同一能力内的行为扩展,不改协议结构**。 - -安全机制(不计入能力数):挑战-应答握手、nonce 一次性、能力级 ACL(deny → allow → 默认拒绝)、调用限速、审计链、证书指纹钉住(TOFU)、来源网段白名单 `allow_from`、每客户端独立 PSK、发现广播最小化 + HMAC 签名、键鼠默认禁用。 - ---- - -## 三、与复盘文档的对照 - -`docs/RETROSPECTIVE.md` 写于 2026-09-28(当时 19 项能力)。逐项核对,**多数 P0/P1 已解决**,文档本身已过期: - -| 复盘当时的判断 | 现状 | 落点 | -|---|---|---| -| **P0** UI 元素定位缺失 | ✅ 已解决 | `screen.find`(UIA → 模板匹配 → OCR 三级降级,返回 `engine`) | -| **P0** 受控重启缺失 | ✅ 已解决 | `system.agent.restart`(借计划任务,任务名带时间戳) | -| **P0** 编码未统一 UTF-8 | ✅ 已解决 | v7 UTF-8 全链路;`.ps1` 带 BOM / `.cmd` 纯 ASCII+CRLF / `.json` 无 BOM,三套规则有单测守卫 | -| **P0** exec 输出污染 | ✅ 已解决 | 结构化 `{stdout, stderr, exit_code, duration_ms}` | -| **P1** 长任务异步化 | ✅ 已解决 | `system.task.list/get/kill` | -| **P1** 剪贴板 | ✅ 已解决 | `clip.get` / `clip.set`(软失败需重试 + 读回校验) | -| **P1** 窗口管理 | ✅ 已解决 | `window.list` / `window.focus`(`window.rect` 无独立能力,rect 由 list 返回) | -| **P1** CLI 无连接复用 | ✅ 已解决 | `apps/cli/src/daemon.ts` + `nodeagent daemon start` | -| **P1** 定时采样 / 监控 | ❌ **未做** | 无 `monitor.*` 能力(grep 命中 0) | -| **P1** GUI 操作需可断言 | 🚧 **在建** | `gui.await` 已实现未提交(等 window/control/process/file 出现或消失);属性条件与组合未做 | -| **P2** 文件监控 `fs.watch` | ❌ **未做** | grep 命中 0 | -| **P2** 日志聚合 `log.query` | ❌ **未做** | grep 命中 0 | -| **P2** 录屏 | ✅ 已解决 | `screen.record` | -| **P2** 键盘宏 / 鼠标拖拽 | ✅ 已解决 | `packages/client/src/macro.ts` + `input.mouse.drag` | -| **P2** 并发 RPC | ⚠️ 部分 | WS 单连接仍串行批量;daemon 缓解了重连成本,未解决单连接并发 | -| **P2** 多设备并行 | ❌ 未做 | 仍需 `--node` 串行切换 | - -**结论**:复盘 §十的三个短板,「自持」已彻底解决,「语义」解决了一半(能找到,读不懂),「状态」只补了事件通知,采样留存仍是空白。 - ---- - -## 四、待办一:收尾 - -### 4.0 ⚠️ 推送前必须先脱敏(优先级高于一切) - -第九轮隐私审查发现:**`0ee8231` 与 `1c836e0` 两个已推送提交都带真机痕迹**,其中设备软件清单已在公开面: - -| 位置 | 内容性质 | 状态 | -|---|---|---| -| `0ee8231` 提交信息 | 真机内网地址 + 具体软件名(详见该提交) | **已推送** | -| `1c836e0` → `apps/agent/src/capabilities/input.ts:145,146,163,410` | 注释含具体播放器名与游戏名 | **已在 origin/main** | -| `1c836e0` → `apps/agent/src/capabilities/input-script.ts:29,32` | 同上 | **已在 origin/main** | - -这构成**设备软件清单 + 使用习惯**的公开暴露(哪台机器装了哪些软件、常在什么场景下远程控它)。比单纯的 IP 泄露更难辩解 —— IP 会换,软件清单是稳定画像。 - -处置建议(按代价排序): - -| 方案 | 代价 | 效果 | -|---|---|---| -| **A. 只清未来**(最低) | 0 | 新增代码与提交信息一律脱敏,历史不动。仓库公开仍有痕迹 | -| **B. 清注释 + 清未来**(臣建议) | 半小时 | 源码注释泛化为「主流播放器」「全屏游戏」,不动历史 | -| **C. B + 改写历史** | 需 `git filter-repo` + force push | 连历史一并清,但**旧 blob 已被匿名抓取过,改写不等于收回** | - -> ⚠️ 关键认知:**已推送到公开仓库并被匿名抓取过的内容,改写历史无法收回**。旧 blob 只能靠 GitHub Support GC,而 Support GC 本身也非保证。因此现实目标应是「**止血 + 不再恶化**」,即方案 B。 - -**具体要改的注释**(脱敏后按此句式重写,勿照抄原词): - -| 原文式(不复述) | 改为 | -|---|---| -| 「远程控制某播放器 / 某云音乐」 | 「远程控制主流媒体播放器」 | -| 「某音乐会建 30+ 辅助窗口」 | 「部分播放器会创建大量辅助窗口」 | -| 「某游戏全屏遮挡时」 | 「全屏独占应用遮挡时」 | - -### 4.1 推送 CI 提交 - -- **现状**:本地 `7ad820c` 领先远端 1 个提交,仅改 `.github/workflows/windows-e2e.yml`(+61 行)。 -- **内容**(三段护栏,全部来自 10-04 真机事故复盘): - 1. **单测步骤** —— 把 `pnpm test:unit` 并入 Windows E2E 工作流。该内容取自 `ci/workflow-unit-tests` 分支(`4d73d6e`);因该分支基于 09-29 老 main,直接 merge 会**回退上万行**,故只取提交内容手工并入。 - 2. **5.1 BOM 解析守卫** —— 刻意用 `cmd` 调 `powershell`(5.1)而非 `pwsh`。5.1 读无 BOM 的 `.ps1` 会按 ANSI/GBK 解析,中文注释破坏字符串边界 → 整份脚本 ParseError。此前全用 pwsh(7+ 认无 BOM UTF-8),所以**CI 绿、真机炸**。这道守卫只问一件事:5.1 能否读懂 —— 而这就是真机的核心约束。 - 3. **install.cmd / control.cmd 双击链路冒烟** —— 这两个脚本是用户唯一上手入口,编码或语法坏了只有真机 cmd.exe 测得出。 -- **收尾**:推送后可清理 `ci/workflow-unit-tests` 分支(贡献已并入,留着只会误导)。 - -### 4.2 v1.5.0 真机验证尚未做(v1.5 提交信息自陈) - -`1c836e0` 的提交信息里写明: - -> ⚠️ **真机未验证**:Windows 被控端当前离线(连接超时)。待上线后需验…… - -即v1.5.0 是**未经真机验证就发的版**。待办: - -``` -node scripts/verify.mjs --host <地址> --key <密钥> --report <报告.md> -``` - -重点验四项(提交信息已列):字符串热键 `hotkey`、预设别名、长按 `hold_ms`、后台定向投递 `route=post`。其中后台定向投递需复验「窗口被遮挡时是否真有效」—— 这条机制此前只在媒体键通道验证过,等价推断需实测坐实。 - -### 4.3 大前端方案(`docs/console-plan.md`,已出文档待评审) - -盘点期间新增了一份前端方案文档,臣不在此展开(详见该文档),仅提示它引入了**一个新包 `crates/nodeagent-client`(Rust)**,与现有 TypeScript 的 `@nodeagent/client` 并存 —— 意味着 client 层有两套实现。这本身是个需要皇上拍板的架构决策(详见该文档的待确认项)。 - ---- - -## 五、待办二:三个开发方向 - -### 方向一:语义 —— 从「能找到」到「可断言」 - -**判断**:`screen.find` 的能力边界已经够用,缺的是**返回值的信息量**与**操作后的状态断言**。原来的链条是「点 → 截图 → 目测 → 再点」。 - -**当前进度:第一步已落地(在建),第二步未做。** - -| # | 改动 | 状态 | 说明 | -|---|---|---|---| -| 1 | **新增 `gui.await` 等待条件** | ✅ **已实现,未提交** | 四类条件(window/control/process/file)× 两态(present/absent);复用既有能力,零新增平台探测代码;超时**正常返回**不抛异常 | -| 2 | `screen.find` 返回 UIA 属性 | ⬜ 未做 | 补 `name` / `value` / `enabled` / `selected` / `rect` / `automation_id` | -| 3 | 条件组合(`and` / `or` / `not`) | ⬜ 未做 | `gui.await` 目前是单条件;组合需先有属性支持 | - -#### `gui.await` 收尾清单(**提交前必做**) - -| # | 事项 | 说明 | -|---|---|---| -| 1 | **修能力数断言** | `tests/unit/capabilities.test.mjs:66` 硬编码 42 → 实际 43,**当前单测是红的**。建议改成「断言每个 `CapabilityNames` 键都在 manifest 中」,避免每加一能力都要改数 | -| 2 | **补单测** | 当前 `gui.await` 覆盖为 0。至少覆盖:参数校验(非法 condition/state)、`timeout_ms` 上限裁剪、`isHit` 对不同返回形态的判定(`found` / `exists` / 数组 / 布尔)、超时返 `satisfied:false`、轮询期异常不中断 | -| 3 | **补 CLI / MCP 入口** | grep 显示 `apps/cli/` 与 `apps/mcp/` **均无 `GuiAwait` 命中**。若 CLI 走通用 invoke 通道则无需改;若逐能力映射则必须补 | -| 4 | **版本号上调** | `package.json` 仍 `1.5.0`,root + 6 子包。新增能力名 → MINOR → **v1.6.0** | -| 5 | 真机验证 | 至少验一次「等窗口出现 → 点击 → 等元素消失」完整编排。`gui.await` 是**阻塞调用**,需确认 `timeout_ms` 上限(60s)不会撞上 WS 心跳/超时机制 | -| 6 | 发版 | `node scripts/release.mjs` → v1.6.0 | - -**验收标准**: - -- 「等某窗口出现某控件」在设定 timeout 内命中,返回 `attempts` 与 `elapsed_ms`; -- 条件永不满足时返回 `satisfied:false` + `last_seen`,**不抛异常**; -- 条件本就「不存在」时立即返回(`attempts:1`),不等满 timeout。 - -#### 第二步:`screen.find` 属性化(后续) - -这是方向一的**真正深水区**。`gui.await` 解决「等得到出现/消失」,但等不到「等它变成某状态」: - -| 改动 | 内容 | 验收标准 | -|---|---|---| -| `screen.find` 返回 UIA 属性 | 在现有坐标之外补 `name` / `value` / `enabled` / `selected` / `rect` / `automation_id` | 对某个真实界面能读出 enabled 状态随播放状态变化 | -| `gui.await` 支持属性条件 | predicate 可比较 `enabled` / `selected` / `value` | 能表达「按钮出现 **且** enabled」 | -| 条件组合 | `and` / `or` / `not` | 能表达「弹窗出现 且 其中某按钮 enabled」 | - -**为什么仍排第一**:GUI 接管是 v2 的核心价值,也是当前唯一还「不好用」的域。`gui.await` 只是把「盲试」变成「有耐心的盲试」—— 补上属性比较后,才真正变成可断言。 - -**风险**:`gui.await` 是**阻塞式调用**,必须有 `timeout_ms` 上限(已设 60s 上限 ✓);超时属正常返回(已实现 ✓)。第二个风险:`screen.find` 的 UIA 属性读取在部分应用上可能拿到空值(Electron / 游戏 UI 常见),需能优雅降级为「仅坐标 + found」。 - -### 方向二:状态 —— 治间歇性问题 - -**判断**:复盘 §四那条判断仍成立 —— 代理不通、杀软拦截都是**间歇性**的,而臣只能看当下快照。`event.watch` 解决「有事件时通知」,没解决「持续记录并回看」。 - -| 能力 | 设计 | 验收标准 | -|---|---|---| -| `monitor.start` | `{ interval_ms, command \| metric, duration_ms }` → 落盘,返回 `id` | 对某连接端口跑 5 分钟 / 每 2 秒采样,事后能拿到完整时间序列 | -| `monitor.report` | `{ id }` → 返回采样序列 + 摘要(min/max/均值/异常点) | 能直接回答「5 分钟里断过几次、最长断了多久」 | -| `log.query` | `{ path, since, level, pattern, limit }` —— **被控端侧过滤** | 10 万行日志中按 pattern 查 20 条,不整份拉回 | -| `fs.watch` | `{ path, recursive, filter }` → 走事件订阅通道推送 | 监听配置文件变更,变更事件 ≤2s 内到达 | - -**关键点**:`log.query` 必须过滤在 Windows 侧。当前 `fs.read` 虽有 8MB 上限,但拉大日志既慢又占带宽,治标不治本。 - -### 方向三:安全 —— 剩余四项 - -`docs/SECURITY.md` §5 自列的未做项: - -| 项 | 现状 | 建议 | -|---|---|---| -| **握手失败封禁** | 只计数 + 记审计,无按 IP 拉黑 | 加指数退避封禁(如 5 分钟内失败 10 次 → 临时拉黑 1 小时),可配置白名单豁免 | -| **会话上限与空闲断开** | 无最大连接数、无空闲超时 | 上限防资源耗尽;空闲断开(如 30 分钟无调用)释放资源 | -| **安装期密钥传递** | `install.cmd` 用命令行 `-Key` 传参,**同机其他用户 `ps` 可见** | 改走 stdin 或临时文件(600 权限,读后即删)。这是唯一还没堵住的密钥暴露面 | -| **审计链外部锚定** | 哈希链可被「有 root 的攻击者」整链重写 | 定期把链头哈希同步到独立位置(如 Hub 或另一台机器),重写即暴露 | - -**臣另建议一项**:**ed25519 默认化**。`SECURITY.md` §2 已写明 PSK 模式的固有限制(`client_id` 自报 → ACL 身份维度形同虚设)。v22 的每客户端独立 PSK 只是缓解 —— 持有共享 key 的人仍可冒充未登记的 client_id。新装默认应走 ed25519,PSK 降级为「兼容存量」并在安装时明确告警。 - ---- - -## 六、待办三:隐私收敛(已连续多轮 P1,该决断) - -### 6.1 已暴露且无法收回 - -| 项 | 状态 | 处置 | -|---|---|---| -| **设备软件清单已在公开面** | `input.ts` / `input-script.ts` 注释含具体播放器与游戏名(见 §4.0) | 泛化注释(方案 B);历史不追 | -| **2 个含PII 的 blob 匿名可直取** | 旧提交中的 blob 用 40 位完整 SHA 即可匿名读取,已连续 6 轮未消;内容含本机用户名与真实目录名 | 报 GitHub Support 申请 GC,或删库重建。**这是唯一的「真泄露」** | -| **提交信息含真机内网地址** | 5 条 commit message 含内网 IP 与端口 | 公开且被搜索引擎收录。后续禁止在 message 写真机地址 | -| **Windows 账户名** | 硬编码在 `network.ts:96` | 改为从配置或环境变量读取 | -| **网卡名「Ethernet」+ 网段常量** | 测试与源码中多处 | 同上,注入式配置 | - -### 6.2 尚未暴露,立即处置 - -| 项 | 处置 | -|---|---| -| `.gitignore` 缺 `keys/`、`*.key`、`*.pem` | 立即补(当前已有 `**/PSK.txt`、`../win-backup/`) | -| `input.ts:324` 附近含 3 个 U+FFFD 字面量 | 编码缺陷,顺手修 | - -### 6.3 脱敏约定(本文档已遵守,后续继续遵守) - -本文档不出现任何真机地址、端口、账户名、密钥,一律用 `<地址>` / `<密钥>` 占位。代码与测试中的真实值应逐步改为从配置或环境变量读取。 - -**新增约定**:提交信息与源码注释**不得出现具体软件名、设备名、真实地址**。举例子:「实测某播放器被某游戏全屏压住」→「实测主流播放器被全屏游戏遮挡」;真实内网地址→「控制端内网地址」。 - -> **教训**:本轮暴露的根源不是泄露了密钥(那类已按等式法核验并修复),而是**注释里写「为什么这么做」时顺手写了真实环境**。真实环境信息对理解原理无帮助,却直接画像了使用者。以后写注释,理由归理由,实况归实况。 - ---- - -## 七、建议排期 - -| 序 | 事项 | 预估 | 依赖 | -|---|---|---|---| -| 0 | **修 `gui.await` 能力数断言**(42→43,当前单测红) | 5 分钟 | 无(**提交前必做,否则 CI 必红**) | -| 0.5 | **脱敏**:泛化 `input.ts` / `input-script.ts` 注释里的软件名 | 20 分钟 | 无(**先于一切推送**) | -| 1 | `gui.await` 收尾:补单测 + CLI/MCP 入口 + 版本号 v1.6.0 + 真机验证 + 发版 | 1 天 | 序 0 | -| 2 | 推送 CI 提交 + 清理 `ci/workflow-unit-tests` 分支 | 10 分钟 | 序 1 | -| 3 | **v1.5.0 真机验证**(该版未经真机就发了) | 半天 | 被控端在线 | -| 4 | 方向一第二步:`screen.find` 属性化 + `gui.await` 支持属性条件 + 条件组合 | 2~3 天 | 序 1 | -| 5 | 方向三:`install.cmd` 密钥改 stdin + 握手封禁 + 会话上限 | 1~2 天 | 无(安全项建议提前插队) | -| 6 | 方向二:`monitor.*` + `log.query` + `fs.watch` | 2~3 天 | 无 | -| 7 | `.gitignore` 补密钥规则 + 源码真机常量注入化 | 分散 | 无 | -| 8 | PII blob 报 Support GC / 决定是否重建仓库 | — | **需皇上决策** | -| 9 | `ed25519` 默认化 | 1~2 天 | 需确认向后兼容策略 | -| 10 | `console-plan.md` 评审:Rust client 与 TS client 双实现取舍 | — | **需皇上拍板** | - -**臣的建议**: - -- **序 0 立刻做**。5 分钟的事,但不做则单测红、CI 必红。注意建议顺手把断言改成「每个 `CapabilityNames` 键都在 manifest 中」——否则每加一能力都要改数字,这类「硬编码计数」会反复咬人。 -- **序 0.5 紧接着做**。只改注释,20 分钟,零风险,但能阻止「设备软件清单」继续扩散。`gui.await` 也正好是新代码,此时立脱敏规矩最自然。 -- **序 1 是当前主线**。`gui.await` 实现已完成(170 行 + 57 行契约 + 注册),设计也站得住(超时正常返回、轮询期异常不中断、复用既有能力零新增探测代码)。差的只是收尾四件事:单测、CLI/MCP 入口、版本号、真机验证。**建议一次做完再提交,不要分批**。 -- **序 3 优先级高于新功能**。v1.5.0 是**未经真机验证就发的版**,其中「后台定向投递」只在媒体键通道验证过、等价推断未经实测。 -- **序 5 可提前**。`install.cmd` 密钥经命令行传参(同机 `ps` 可见)是当下唯一未堵住的密钥暴露路径,改动局限在一个脚本内。 -- **序 8、10 需皇上决策**:前者关系到公开历史能否部分收回,后者关系到是否长期维护两套 client 实现。 - -> **一条经验**:`gui.await` 一次加能力就让单测红,原因是能力数被硬编码成 `42`。这类「计数断言」在持续加能力的项目里必然反复失效。**改为集合断言**(每个能力名都在 manifest 中)比每次改数字更省事,也更不容易漏检。 - ---- - -## 八、附:本文档的核查方法 - -| 结论 | 核查方式 | -|---|---| -| 版本与提交差距 | `git fetch` 后比 `git log origin/main..HEAD`(**必须先 fetch** —— 盘点期间远端已推进到 v1.5.0,本地缓存的 origin 是过期的) | -| 能力清单 | 读 `packages/protocol/src/capabilities/names.ts` 全量枚举 | -| 遗留能力未实现 | 对 `fs.watch` / `log.query` / `monitor.` / `serve` / `daemon` 逐个 grep 源码,命中 0 即未实现 | -| 单测状态 | 本地 `node --test "tests/unit/**/*.test.mjs"` → **195 例:194 pass / 1 fail**(`capabilities.test.mjs:66` 能力数断言 42≠43) | -| 复盘对照 | 逐条读 `docs/RETROSPECTIVE.md` 的 P0/P1/P2 表,与能力清单交叉 | -| `gui.await` 完成度 | 分别 grep `tests/` `apps/cli/` `apps/mcp/` 找 `gui.await`/`GuiAwait`/`guiAwait` —— 三处均 0 命中 → 单测与两端入口都还没接| -| 安全残余 | 读 `docs/SECURITY.md` §5「还没做的(后续批次)」 | -| 公开面暴露 | `git fetch` 后 `git grep <关键词> origin/main` —— **必须先 fetch**(否则查的是过期快照),且**关键词要带空格变体**(`XX音乐` / `XX 音乐`),否则会漏 | - -> **两条教训**: -> 1. 跨平台差异(编码、文件重命名、进程语义)**必须在真机验证** —— macOS 通过不代表 Windows 通过。文档层面的盘点只能发现「没写」,发现不了「写错了但没人跑」。 -> 2. **`git grep` 不 fetch 会查旧快照**。本次差点把「未推送」误判成「未暴露」,实际远端已含该内容。凡涉及「公开面是否暴露」的结论,必须 `git fetch` 后再查,且关键词要带空格变体。 \ No newline at end of file diff --git a/docs/console-plan.md b/docs/console-plan.md deleted file mode 100644 index 8452ce5..0000000 --- a/docs/console-plan.md +++ /dev/null @@ -1,258 +0,0 @@ -# nodeagent 大前端开发方案(桌面操作台) - -> 版本 v1.0 | 日期 2026-10-07 | 状态:待皇上评审 -> 定位:为「Mac 控 Windows」补上**人肉可视化操作台**——CLI 是脚本底座、MCP 是 AI 落点、本方案是**皇上自己点按钮**的桌面界面。 - ---- - -## 1. 一句话结论 - -用 **Tauri 2 + Vue 3 + TypeScript + Vite** 造一套**跨端桌面应用**,同一份代码编译出 **Mac 版(控制端操作台)**与 **Windows 版(被控端本地面板)**;Rust 后端复刻 `@nodeagent/client` 的握手/RPC/事件逻辑,经 Tauri command 暴露给 Vue 前端。首期覆盖 **核心管控 + 文件管理 + 审计**。 - ---- - -## 2. 技术栈选型(含取舍) - -| 层 | 选型 | 版本 | 选型理由 | -|---|---|---|---| -| 桌面壳 | **Tauri 2** | 2.x | 跨端(Mac/Win)+ 性能好 + 体积小;Rust 后端契合皇上「Rust 加速」技术栈 | -| 前端框架 | **Vue 3** | 3.5+ | 皇上既有 Vue2/Vue3 经验(reader、FinDataHub),迁移成本最低 | -| 语言 | **TypeScript** | 5.x | 与 monorepo 既有 TS 生态对齐,复用协议类型定义 | -| 构建 | **Vite** | 6.x | Tauri 官方模板默认,HMR 快 | -| 状态管理 | **Pinia** | 2.x | Vue3 官方推荐,模块化 store 天然契合多面板 | -| 路由 | **Vue Router** | 4.x | 多页面(总览/终端/文件/审计…)需要 | -| 组件库 | **Naive UI** | 2.x | 暗色主题友好、TS 类型完整、可自定义涨红跌绿 | -| 图表 | **ECharts** | 5.x | 资源趋势/磁盘/指标可视化,涨红跌绿可配 | -| Rust 客户端 | tokio + tokio-tungstenite + rustls | — | 复刻 client.ts 的 WebSocket 握手与 RPC | -| Rust 密码学 | hmac + sha2 + ed25519-dalek + x509-parser | — | HMAC 挑战应答、ed25519 签名、证书指纹钉住 | - -### 为什么 Tauri 而不是 Electron / 纯 Web - -| 维度 | Tauri 2 | Electron | 纯 Web(浏览器) | -|---|---|---|---| -| 跨端 Mac+Win | ✅ | ✅ | ✅(但无桌面壳) | -| 打包体积 | ~10MB 级 | ~100MB+ | 无 | -| 内存占用 | 低(复用系统 WebView) | 高(自建 Chromium) | 视浏览器 | -| 性能 | 好(Rust 后端) | 中 | 受浏览器沙箱限制 | -| 直接复用 Node SDK | ❌(需 Rust 版 client) | ✅(主进程可 import) | ❌(需浏览器版) | - -**取舍**:Tauri 需要新写一份 Rust 版 client(协议本身是 JSON-RPC over WebSocket,语言无关,逻辑对齐已跑通的 client.ts 即可),换取体积、内存、性能上的长期优势——与皇上「性能好 + 跨端」的要求一致。 - ---- - -## 3. 整体架构 - -``` -┌─────────────────────────── apps/console(桌面应用,同一份代码两端编译) ───────────────────────────┐ -│ │ -│ Vue 3 前端(src/) Tauri Rust 壳(src-tauri/) │ -│ ┌─────────────────────┐ IPC invoke ┌──────────────────────────────┐ │ -│ │ 视图 Views │ ────────────► │ commands/*.rs(薄命令层) │ │ -│ │ 组件 Components │ ◄──────────── │ │ │ │ -│ │ Pinia stores │ event 推送 │ ▼ │ │ -│ │ api 调用层 │ ◄──────────── │ state.rs(连接管理器) │ │ -│ └─────────────────────┘ │ │ │ │ -│ │ ▼ │ │ -│ │ crates/nodeagent-client │ ── WebSocket ──► 被控端 │ -│ │ (Rust 版 client:握手/RPC/ │ (wss://IP:8765) Windows │ -│ │ 事件/重连/证书钉住/ed25519) │ │ -│ └──────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────────────────────────────────────────┘ -``` - -关键设计: - -1. **薄壳厚前端**:Rust 壳只做「连接管理 + 协议桥接」,业务展示逻辑全在 Vue 前端。 -2. **事件推送走 Tauri event**:被控端主动事件(v12 `event.*`)、连接状态变化,由 Rust 后端转发为 Tauri event,前端 `listen()` 订阅,避免轮询。 -3. **能力调用统一 `invoke`**:所有 42 项能力都走一个泛型 `invoke(capability, args)` 通道,前端按能力清单动态渲染参数表单,**不逐能力手写 command**。 - ---- - -## 4. 功能模块与页面清单(首期 = 核心管控 + 文件 + 审计) - -| # | 页面/模块 | 路由 | 支撑能力 | 功能点 | -|---|---|---|---|---| -| 1 | **设备管理** | `/nodes` | `discover`、`nodes` 配置、`connect` | 局域网发现、设备列表、连接/断开、增删改节点、设默认设备、证书指纹展示与重钉 | -| 2 | **状态总览 Dashboard** | `/overview` | `system.info` / `system.status` / `system.metrics` / `system.audio.get` | 连接状态、主机名/OS/CPU/内存/开机时长、磁盘用量环图、网络适配器、PRD 成功指标(闭环率/P95/拦截率) | -| 3 | **命令执行(终端)** | `/terminal` | `system.shell.exec`、`system.task.*` | 交互式 PowerShell 终端、退出码/耗时、超时可配、长命令后台化 + 任务列表/续读/终止 | -| 4 | **进程与服务** | `/processes` | `system.process.list` / `system.service.list` | 进程/服务列表,按 CPU/内存/PID/名称排序,按名筛选,按状态筛选 | -| 5 | **软件管理** | `/software` | `app.list` / `app.install` | 已装软件列表与版本、winget 静默安装(输入包名/ID,返回结果+版本) | -| 6 | **文件管理** | `/files` | `fs.list` / `fs.stat` / `fs.read` / `fs.write` | 目录树浏览、文件预览(分块读)、上传/下载(分块 + 进度)、新建/覆盖/追加写入、元信息 | -| 7 | **审计日志** | `/audit` | `system.audit.list` / `system.audit.verify` | 审计列表分页、按类型/调用方/时间筛选、完整性校验(verify)、导出 JSONL | - -> **Windows 版侧重**(待确认):同一套代码,Windows 版默认聚焦**被控端本地面板**——本机连接状态、授权(ACL/`allow_input` 输入开关)、本机资源、日志、自更新入口。因协议点对点对称,Windows 版同样可作控制端。 - ---- - -## 5. 数据接口与状态管理 - -### 5.1 Rust 版 client(`crates/nodeagent-client`)对外 API - -| 方法 | 说明 | 对齐 client.ts | -|---|---|---| -| `Client::connect(profile) -> NodeInfo` | 握手:hello → challenge → auth → auth_ok | `connect()` | -| `Client::invoke(capability, args, timeout) -> InvokeResult` | 泛型能力调用 | `invoke()` | -| `Client::list_capabilities()` / `list_authorized()` | 能力/授权清单 | `listCapabilities()` | -| `Client::set_event_handler(fn)` | 订阅被控端事件(无 id 通知) | `onEvent` | -| `Client::on_state_change(fn)` | 连接/重连/关闭状态回调 | `onStateChange` | -| `Client::close()` | 主动断开(停自动重连) | `close()` | -| `Client::peer_cert_fingerprint()` | 对端证书指纹(TOFU 钉住) | `getPeerCertFingerprint()` | - -复刻要点(已跑通,勿重蹈):指数退避重连 1s→30s ±15% 抖动;证书指纹 sha256 比对(`E_CERT_MISMATCH`);ed25519 模式需私钥;Hub 配对透传。 - -### 5.2 Tauri command(Rust 壳 → 前端 IPC) - -| Command | 入参 | 返回 | 说明 | -|---|---|---|---| -| `discover` | `wait_ms` | `Node[]` | UDP 8766 发现 | -| `connect_node` | `node_id` | `NodeInfo` | 连接并握手 | -| `disconnect` | — | `()` | 断开 | -| `invoke` | `capability, args, timeout_ms?` | `InvokeResult` | 泛型能力调用 | -| `list_nodes` / `add_node` / `remove_node` / `set_current` | profile 结构 | 配置 | 设备 CRUD(写 `~/.nodeagent/config.json`) | -| `get_conn_state` | — | `ConnState` | 当前连接状态 | -| `get_capabilities` | — | `Capability[]` | 能力/授权清单 | - -### 5.3 事件推送(Tauri event,Rust → 前端) - -| 事件名 | 载荷 | 触发 | -|---|---|---| -| `node-event` | 被控端事件(`kind/action/target/detail`) | 被控端 `event.*` 主动推送 | -| `conn-state` | `connected/reconnecting/closed` | 连接状态变化 | -| `task-progress` | `task_id, offset, output` | 长任务增量输出 | - -### 5.4 Pinia store 设计 - -| Store | 职责 | 关键 state | -|---|---|---| -| `useConnStore` | 连接状态、当前节点、能力/授权清单、证书指纹 | `state, nodeId, capabilities, authorized, certSha256` | -| `useNodeStore` | 设备配置列表、发现结果、分组 | `nodes, current, groups, discovered` | -| `useSysStore` | 系统信息/状态/指标缓存(可手动刷新) | `info, status, metrics` | -| `useFileStore` | 当前目录、文件列表、选中项、上传下载进度 | `cwd, entries, selection, transfers` | -| `useAuditStore` | 审计分页与筛选 | `items, page, filter` | -| `useEventStore` | 事件流(订阅 + 游标续拉) | `watches, events, cursor` | -| `useTaskStore` | 后台长命令任务 | `tasks` | - -**数据流**:组件 → Pinia store → `api/`(封装 `invoke`)→ Tauri command → Rust client → 被控端;反向经 Tauri event → store 更新 → 响应式视图。高频指标(status/metrics)由 store 内定时器轮询,可配置周期;事件类数据走推送,不轮询。 - ---- - -## 6. 代码结构(monorepo 新增) - -``` -nodeagent/ -├── apps/ -│ └── console/ # 桌面应用(新增) -│ ├── package.json -│ ├── vite.config.ts -│ ├── index.html -│ ├── src/ # Vue 3 前端 -│ │ ├── main.ts / App.vue -│ │ ├── router/index.ts # 7 条路由 -│ │ ├── views/ # Nodes / Overview / Terminal / Processes / Software / Files / Audit -│ │ ├── components/ # 可复用组件(设备卡片、参数表单、状态徽章、进度条…) -│ │ ├── stores/ # Pinia(5.4 节 7 个 store) -│ │ ├── api/ # invoke 封装 + 类型(对齐 Rust command) -│ │ ├── types/ # 能力清单、InvokeResult、NodeProfile 等类型镜像 -│ │ └── utils/ # 格式化、涨红跌绿、分块编排 -│ └── src-tauri/ # Rust 壳 -│ ├── Cargo.toml -│ ├── tauri.conf.json -│ └── src/ -│ ├── main.rs / lib.rs -│ ├── state.rs # 连接管理器(持有 client) -│ └── commands/ # 5.2 节 command 实现 -├── crates/ -│ ├── nodeagent-client/ # Rust 版 client(新增,5.1 节) -│ │ ├── Cargo.toml -│ │ └── src/{lib.rs, client.rs, handshake.rs, tls.rs, events.rs} -│ └── nodeagent-protocol/ # (可选)协议类型/能力清单的 Rust 镜像 -└── packages/… apps/{agent,cli,hub,mcp} # 现有,不动 -``` - -约束:Rust client 与 Node client(`packages/client`)**行为一致**,两端共享 `~/.nodeagent/config.json` 与 `keys/`,不新造存储位置;能力清单以 `packages/protocol/src/capabilities/manifest` 为唯一事实源,Rust 侧做类型镜像并对齐。 - ---- - -## 7. 交付范围 - -### In scope(首期 v1) - -| # | 交付物 | 说明 | -|---|---|---| -| 1 | `crates/nodeagent-client` | Rust 版 client,对齐 client.ts 全部行为,含单元测试 | -| 2 | `apps/console` 桌面应用 | Tauri + Vue3,7 个页面,Mac/Win 双端可编译 | -| 3 | 核心管控 | 设备管理、总览、终端、进程/服务、软件安装 | -| 4 | 文件管理 | 浏览/预览/上传/下载(分块)/写入 | -| 5 | 审计 | 列表分页筛选、完整性校验、导出 | -| 6 | 打包脚本 | Mac `.app` + Windows `.msi/.exe`(复用现有 `pack.mjs` 风格) | - -### Out of scope(后续版本) - -| # | 内容 | 归属 | -|---|---|---| -| 1 | 图形接管 UI(实时截图/键鼠/窗口定位/宏回放) | v2 | -| 2 | 网络两阶段变更、音频控制、自更新面板 | v2 | -| 3 | 事件流订阅的可视化(watch/list/poll 面板) | v2 | -| 4 | 应用签名/公证/自动更新 | 独立发布项 | -| 5 | 多设备批量下发(fanout/分组)面板 | v2 | - ---- - -## 8. 验收标准 - -### 8.1 功能验收(Given/When/Then) - -| # | 验收项 | 标准 | -|---|---|---| -| A1 | 设备连接 | Given 已配置被控端,When 点击连接,Then 3s 内返回能力清单并进入总览;密钥错误返回 `E_AUTH_FAILED` 且界面明确提示 | -| A2 | 状态总览 | Given 已连接,When 打开总览,Then 正确展示 CPU/内存/磁盘/网络与 PRD 指标,且数据带时间戳 | -| A3 | 命令执行 | Given 已连接,When 输入合法 PowerShell,Then 返回 `exit_code/stdout/stderr/duration_ms`;超时命令返回 `E_TIMEOUT` 且无残留进程 | -| A4 | 软件安装 | Given winget 可用,When 输入包名,Then 返回安装结果+版本;不存在时给出明确失败原因 | -| A5 | 文件上传/下载 | Given 已连接,When 上传大文件(>8MB),Then 分块写入成功且校验 sha256 一致;下载支持断点续读 | -| A6 | 审计 | Given 已连接,When 打开审计页,Then 分页展示并可筛选;verify 返回完整性结论 | -| A7 | 断线重连 | Given 已连接,When 拔网再恢复,Then 指数退避自动重连并回到 connected,界面状态同步 | - -### 8.2 指标验收 - -| 指标 | 口径 | 目标 | -|---|---|---| -| 命令闭环成功率 | 成功返回 ÷ 发起 | ≥ 95% | -| 内网调用 P95 时延 | 前端点击到结果渲染 | < 3s | -| 应用内存占用 | Mac 版空闲常驻 RSS | < 150MB(远低于 Electron) | -| 打包体积 | 单端安装包 | Mac `.app` < 30MB / Win 安装包 < 40MB | -| 双端编译 | `pnpm build` 一次产出两端 | 100% | - ---- - -## 9. 里程碑拆分 - -| 阶段 | 内容 | 退出标准 | -|---|---|---| -| M1 | Rust client crate + 握手/RPC/事件/重连 + 单测 | 与现有 `test:e2e` 协议用例对齐通过 | -| M2 | Tauri 壳 + command 层 + 设备管理/总览/终端三页 | 真机连接成功、能跑命令、总览数据正确 | -| M3 | 文件管理 + 审计 + 软件/进程服务页 | A5/A6/A4 验收通过 | -| M4 | Mac/Win 双端打包脚本 + 文档 | A 全部通过、指标达标 | - ---- - -## 10. 风险与对策 - -| # | 风险 | 概率 | 影响 | 对策 | -|---|---|---|---|---| -| R1 | Rust 版 client 与 Node 版行为漂移 | 中 | 高 | 以 `packages/protocol` 能力清单为唯一事实源;协议用例双端共跑 | -| R2 | Tauri 下自签证书/TLS 处理差异 | 中 | 中 | 复刻 client.ts 的 `insecure` + 指纹钉住逻辑,单测覆盖 `E_CERT_MISMATCH` | -| R3 | Windows 端定位歧义 | 中 | 中 | 见第 4 节「待确认」,评审时定死再做 | -| R4 | 大文件分块上传稳定性 | 中 | 中 | 复用已有分块能力;前端做进度+断点,M3 真机压测 | - ---- - -## 11. 待确认项(请皇上评审时一并拍板) - -| # | 事项 | 臣的建议 | -|---|---|---| -| C1 | Windows 端桌面应用的定位 | 被控端本地面板(默认侧重)+ 对称可作控制端 | -| C2 | 组件库 | Naive UI(暗色友好、TS 完整);若皇上惯用 Element Plus 可换 | -| C3 | 图表库 | ECharts(资源趋势/磁盘);确认后纳入 M2 | -| C4 | 是否同步做图形接管 UI | 建议 v2 再做,首期聚焦闭环 | - -> 仅供参考,本方案为技术规划,不构成任何投资建议。