feat(kernel): add core installation and management
This commit is contained in:
1 parent
8c8cb02f14
commit
3bb5ff7dd4
10 files changed
+3275
-67
No files matched your search
@@ -3,6 +3,7 @@ dist/
|
||||
src-tauri/gen/
|
||||
src-tauri/target/
|
||||
bin-work/
|
||||
cpa-core/
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
@@ -0,0 +1,8 @@
|
||||
CLIProxyAPI is the core file of this project. After downloading it from the GitHub repository, you can simply extract it locally and use it directly.
|
||||
|
||||
Latest release page: https://github.com/router-for-me/CLIProxyAPI/releases/latest
|
||||
|
||||
After extraction, you will find the cli-proxy-api binary file and the config.example.yaml configuration template.
|
||||
|
||||
You need to copy config.example.yaml to config.yaml and modify the DIY configuration before running it. Of course, the default configuration can also run directly.
|
||||
|
||||
Generated
+456
-13
@@ -47,6 +47,15 @@ version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -309,6 +318,23 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -385,8 +411,18 @@ dependencies = [
|
||||
name = "cpa-gui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -398,6 +434,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -525,6 +570,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -698,7 +754,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"toml 1.1.3+spec-1.1.0",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
]
|
||||
@@ -726,6 +782,16 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
@@ -751,6 +817,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1002,8 +1078,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1025,8 +1103,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1276,6 +1357,22 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -1655,6 +1752,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
@@ -1676,6 +1779,12 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@@ -2229,7 +2338,7 @@ version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.12+spec-1.1.0",
|
||||
"toml_edit 0.25.13+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2274,6 +2383,62 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
@@ -2295,6 +2460,32 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
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 = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -2370,6 +2561,47 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.4.2",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -2400,10 +2632,24 @@ dependencies = [
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"wasm-streams 0.5.0",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[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-hash"
|
||||
version = "2.1.3"
|
||||
@@ -2419,12 +2665,66 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -2615,6 +2915,18 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.21.0"
|
||||
@@ -2685,7 +2997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -2813,6 +3125,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -2929,6 +3247,17 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -2965,7 +3294,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.13.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -3130,7 +3459,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"toml 1.1.3+spec-1.1.0",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -3145,7 +3474,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"embed-resource",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"toml 1.1.3+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3263,9 +3592,31 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -3308,9 +3659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"serde_core",
|
||||
@@ -3374,9 +3725,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.12+spec-1.1.0"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
@@ -3395,9 +3746,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -3556,6 +3907,12 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[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"
|
||||
@@ -3720,6 +4077,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.5.0"
|
||||
@@ -3743,6 +4113,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.5"
|
||||
@@ -3799,6 +4179,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
@@ -4029,6 +4418,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -4297,6 +4695,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
@@ -4341,6 +4749,12 @@ dependencies = [
|
||||
"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.4"
|
||||
@@ -4374,8 +4788,37 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
@@ -9,4 +9,14 @@ edition = "2021"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
flate2 = "1"
|
||||
futures-util = "0.3"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
tar = "0.4"
|
||||
tauri = { version = "2", features = [] }
|
||||
tokio = { version = "1", features = ["macros", "sync", "time"] }
|
||||
tokio-util = "0.7"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for the main CPA GUI window.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
|
||||
}
|
||||
+1230
-1
File diff suppressed because it is too large.
Load diff
+26
-7
@@ -3,8 +3,16 @@ import { ConfigPanelPage } from './pages/ConfigPanel';
|
||||
import { KernelPage } from './pages/Kernel';
|
||||
|
||||
const pages = [
|
||||
{ id: 'kernel', label: '内核管理', component: KernelPage },
|
||||
{ id: 'config', label: '配置面板', component: ConfigPanelPage },
|
||||
{
|
||||
id: 'kernel',
|
||||
label: '内核',
|
||||
component: KernelPage,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
label: '配置',
|
||||
component: ConfigPanelPage,
|
||||
},
|
||||
] as const;
|
||||
|
||||
type PageId = (typeof pages)[number]['id'];
|
||||
@@ -22,10 +30,14 @@ function App() {
|
||||
<div className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-brand" title="CLI Proxy API GUI">
|
||||
<strong>Easy CLIProxyAPI</strong>
|
||||
<span className="brand-mark">CP</span>
|
||||
<div>
|
||||
<strong>Easy CLIProxyAPI</strong>
|
||||
<span>Desktop Console</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="nav-section">
|
||||
<nav className="nav-section" aria-label="主导航">
|
||||
{pages.map((page) => (
|
||||
<button
|
||||
key={page.id}
|
||||
@@ -37,11 +49,18 @@ function App() {
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<span>CPA GUI</span>
|
||||
<strong>0.1</strong>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="content">
|
||||
<ActivePage />
|
||||
</main>
|
||||
<div className="workspace">
|
||||
<main className="content">
|
||||
<ActivePage />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,46 @@
|
||||
export function ConfigPanelPage() {
|
||||
return (
|
||||
<section className="page">
|
||||
<h1>配置面板</h1>
|
||||
<section className="page config-page">
|
||||
<div className="config-layout">
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>基础配置</h2>
|
||||
<p>配置文件和工作目录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<h2>基础配置</h2>
|
||||
<label className="field">
|
||||
配置文件
|
||||
<input placeholder="待接入配置文件" />
|
||||
</label>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
配置文件
|
||||
<input placeholder="待接入配置文件" />
|
||||
</label>
|
||||
<label className="field">
|
||||
工作目录
|
||||
<input placeholder="待接入工作目录" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel quiet-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>连接信息</h2>
|
||||
<p>当前前端占位状态</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="info-list">
|
||||
<div>
|
||||
<dt>配置源</dt>
|
||||
<dd>未接入</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>保存状态</dt>
|
||||
<dd>待实现</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+696
-11
@@ -1,23 +1,708 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
type CorePlatform = {
|
||||
os: string;
|
||||
arch: string;
|
||||
assetOs: string;
|
||||
assetArch: string;
|
||||
archiveKind: 'tar.gz' | 'zip';
|
||||
};
|
||||
|
||||
type CoreStatus = {
|
||||
installed: boolean;
|
||||
running: boolean;
|
||||
managed: boolean;
|
||||
processId: number | null;
|
||||
currentVersion: string | null;
|
||||
installDir: string;
|
||||
binaryPath: string | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type CoreLatest = {
|
||||
version: string;
|
||||
assetName: string;
|
||||
};
|
||||
|
||||
type CoreInstallResult = {
|
||||
version: string;
|
||||
assetName: string;
|
||||
installDir: string;
|
||||
binaryPath: string | null;
|
||||
};
|
||||
|
||||
type CoreInstallTask = {
|
||||
running: boolean;
|
||||
cancellable: boolean;
|
||||
phase: string;
|
||||
downloaded: number;
|
||||
total: number | null;
|
||||
percent: number | null;
|
||||
message: string | null;
|
||||
result: CoreInstallResult | null;
|
||||
};
|
||||
|
||||
type MessageType = 'info' | 'success' | 'error';
|
||||
|
||||
let latestAutoCheckStarted = false;
|
||||
let cachedLatest: CoreLatest | null = null;
|
||||
let cachedLatestError = '';
|
||||
let latestCheckPromise: Promise<CoreLatest> | null = null;
|
||||
|
||||
function requestLatestCore() {
|
||||
if (latestCheckPromise) {
|
||||
return latestCheckPromise;
|
||||
}
|
||||
|
||||
latestCheckPromise = invoke<CoreLatest>('check_latest_core')
|
||||
.then((result) => {
|
||||
cachedLatest = result;
|
||||
cachedLatestError = '';
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
cachedLatest = null;
|
||||
cachedLatestError = String(error);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
latestCheckPromise = null;
|
||||
});
|
||||
|
||||
return latestCheckPromise;
|
||||
}
|
||||
|
||||
export function KernelPage() {
|
||||
const [status, setStatus] = useState('检测中');
|
||||
const [coreStatus, setCoreStatus] = useState<CoreStatus | null>(null);
|
||||
const [statusError, setStatusError] = useState('');
|
||||
const [platform, setPlatform] = useState<CorePlatform | null>(null);
|
||||
const [platformError, setPlatformError] = useState('');
|
||||
const [latest, setLatest] = useState<CoreLatest | null>(cachedLatest);
|
||||
const [latestError, setLatestError] = useState(cachedLatestError);
|
||||
const [checkingLatest, setCheckingLatest] = useState(Boolean(latestCheckPromise));
|
||||
const [allowLanAccess, setAllowLanAccess] = useState(false);
|
||||
const [customPort, setCustomPort] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [processBusy, setProcessBusy] = useState(false);
|
||||
const [processMessage, setProcessMessage] = useState('');
|
||||
const [processMessageType, setProcessMessageType] = useState<MessageType>('info');
|
||||
const [message, setMessage] = useState('');
|
||||
const [messageType, setMessageType] = useState<MessageType>('info');
|
||||
const [progress, setProgress] = useState<CoreInstallTask | null>(null);
|
||||
const [installDialogOpen, setInstallDialogOpen] = useState(false);
|
||||
const [cancellingInstall, setCancellingInstall] = useState(false);
|
||||
const installDialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const applyInstallTask = (task: CoreInstallTask, showFinishedDialog = true) => {
|
||||
if (!task.running && !task.message && !task.result) {
|
||||
setProgress(null);
|
||||
setInstalling(false);
|
||||
setCancellingInstall(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setInstalling(task.running);
|
||||
if (!task.running) {
|
||||
setCancellingInstall(false);
|
||||
}
|
||||
|
||||
if (task.running || showFinishedDialog) {
|
||||
setProgress(task);
|
||||
setInstallDialogOpen(true);
|
||||
} else {
|
||||
setProgress(null);
|
||||
}
|
||||
|
||||
if (task.result) {
|
||||
setMessage(task.message || `${task.result.version} 安装完成`);
|
||||
setMessageType('success');
|
||||
void loadCoreStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.message) {
|
||||
setMessage(task.message);
|
||||
setMessageType(task.phase === '安装失败' ? 'error' : 'info');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
invoke<string>('health_check')
|
||||
.then(setStatus)
|
||||
.catch(() => setStatus('需要在 Tauri 窗口中运行'));
|
||||
let disposed = false;
|
||||
let unlisten: (() => void) | null = null;
|
||||
|
||||
listen<CoreInstallTask>('core-install-progress', (event) => {
|
||||
applyInstallTask(event.payload);
|
||||
})
|
||||
.then((unlistenProgress) => {
|
||||
if (disposed) {
|
||||
unlistenProgress();
|
||||
} else {
|
||||
unlisten = unlistenProgress;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!disposed) {
|
||||
setMessage(`监听下载进度失败: ${String(error)}`);
|
||||
setMessageType('error');
|
||||
}
|
||||
});
|
||||
|
||||
loadCoreStatus();
|
||||
loadPlatform();
|
||||
loadInstallTask();
|
||||
|
||||
if (!latestAutoCheckStarted) {
|
||||
latestAutoCheckStarted = true;
|
||||
void checkLatest();
|
||||
} else if (latestCheckPromise) {
|
||||
void checkLatest();
|
||||
}
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="page">
|
||||
<h1>内核管理</h1>
|
||||
useEffect(() => {
|
||||
if (!installDialogOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
<div className="panel">
|
||||
<h2>运行状态</h2>
|
||||
<p>{status}</p>
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
installDialogRef.current?.focus();
|
||||
|
||||
const preventEscapeClose = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', preventEscapeClose);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
document.removeEventListener('keydown', preventEscapeClose);
|
||||
};
|
||||
}, [installDialogOpen]);
|
||||
|
||||
const loadCoreStatus = async () => {
|
||||
try {
|
||||
const result = await invoke<CoreStatus>('get_core_status');
|
||||
setCoreStatus(result);
|
||||
setStatusError('');
|
||||
} catch (error) {
|
||||
setCoreStatus(null);
|
||||
setStatusError(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const loadPlatform = async () => {
|
||||
try {
|
||||
const result = await invoke<CorePlatform>('detect_core_platform');
|
||||
setPlatform(result);
|
||||
setPlatformError('');
|
||||
} catch (error) {
|
||||
setPlatform(null);
|
||||
setPlatformError(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const checkLatest = async () => {
|
||||
setCheckingLatest(true);
|
||||
setLatestError('');
|
||||
setMessage('');
|
||||
setMessageType('info');
|
||||
|
||||
try {
|
||||
const result = await requestLatestCore();
|
||||
setLatest(result);
|
||||
} catch (error) {
|
||||
setLatest(null);
|
||||
setLatestError(String(error));
|
||||
} finally {
|
||||
setCheckingLatest(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadInstallTask = async () => {
|
||||
try {
|
||||
const task = await invoke<CoreInstallTask>('get_core_install_task');
|
||||
applyInstallTask(task, false);
|
||||
} catch (error) {
|
||||
setMessage(`读取安装任务失败: ${String(error)}`);
|
||||
setMessageType('error');
|
||||
}
|
||||
};
|
||||
|
||||
const installVersion = async (version: string) => {
|
||||
setInstalling(true);
|
||||
setMessage(`正在安装 ${version}`);
|
||||
setMessageType('info');
|
||||
setCancellingInstall(false);
|
||||
setInstallDialogOpen(true);
|
||||
setProgress({
|
||||
running: true,
|
||||
cancellable: true,
|
||||
phase: '准备下载',
|
||||
downloaded: 0,
|
||||
total: null,
|
||||
percent: null,
|
||||
message: null,
|
||||
result: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await invoke<CoreInstallResult>('install_core_version', { version });
|
||||
setMessage(`${result.version} 安装完成`);
|
||||
setMessageType('success');
|
||||
setProgress({
|
||||
running: false,
|
||||
cancellable: false,
|
||||
phase: '安装完成',
|
||||
downloaded: 1,
|
||||
total: 1,
|
||||
percent: 100,
|
||||
message: `${result.version} 安装完成`,
|
||||
result,
|
||||
});
|
||||
await loadCoreStatus();
|
||||
} catch (error) {
|
||||
const errorMessage = String(error);
|
||||
setMessage(errorMessage);
|
||||
setMessageType(errorMessage.includes('取消') ? 'info' : 'error');
|
||||
setProgress((current) => ({
|
||||
running: false,
|
||||
cancellable: false,
|
||||
phase: errorMessage.includes('取消') ? '已取消' : '安装失败',
|
||||
downloaded: current?.downloaded ?? 0,
|
||||
total: current?.total ?? null,
|
||||
percent: current?.percent ?? null,
|
||||
message: errorMessage,
|
||||
result: null,
|
||||
}));
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelInstall = async () => {
|
||||
if (cancellingInstall || !progress?.running || !progress.cancellable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCancellingInstall(true);
|
||||
setMessage('正在取消下载');
|
||||
setMessageType('info');
|
||||
|
||||
try {
|
||||
await invoke('cancel_core_install');
|
||||
} catch (error) {
|
||||
setCancellingInstall(false);
|
||||
setMessage(String(error));
|
||||
setMessageType('error');
|
||||
}
|
||||
};
|
||||
|
||||
const closeInstallDialog = () => {
|
||||
if (installing || progress?.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInstallDialogOpen(false);
|
||||
setProgress(null);
|
||||
setCancellingInstall(false);
|
||||
};
|
||||
|
||||
const runCoreProcessCommand = async (command: string, doing: string, done: string) => {
|
||||
setProcessBusy(true);
|
||||
setProcessMessage(doing);
|
||||
setProcessMessageType('info');
|
||||
|
||||
try {
|
||||
const result = await invoke<CoreStatus>(command);
|
||||
setCoreStatus(result);
|
||||
setStatusError('');
|
||||
setProcessMessage(done);
|
||||
setProcessMessageType('success');
|
||||
} catch (error) {
|
||||
setProcessMessage(String(error));
|
||||
setProcessMessageType('error');
|
||||
await loadCoreStatus();
|
||||
} finally {
|
||||
setProcessBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const latestVersion = latest?.version ?? '';
|
||||
const currentVersion = coreStatus?.currentVersion ?? '';
|
||||
const coreInstalled = Boolean(coreStatus?.installed);
|
||||
const coreRunning = Boolean(coreStatus?.running);
|
||||
const busy = checkingLatest || installing || processBusy;
|
||||
const installDisabled = busy || Boolean(coreStatus?.running);
|
||||
const computedPercent =
|
||||
progress?.percent ??
|
||||
(progress?.total && progress.total > 0 ? (progress.downloaded / progress.total) * 100 : null);
|
||||
const progressKnown = computedPercent !== null;
|
||||
const progressPercent = clampPercent(computedPercent ?? 0);
|
||||
const progressText = progress
|
||||
? progress.phase === '安装完成'
|
||||
? '已完成'
|
||||
: progress.phase === '解压中'
|
||||
? '正在解压文件'
|
||||
: progress.total
|
||||
? `${formatBytes(progress.downloaded)} / ${formatBytes(progress.total)}`
|
||||
: progress.downloaded > 0
|
||||
? formatBytes(progress.downloaded)
|
||||
: '等待进度信息'
|
||||
: '';
|
||||
const statusTone = statusError ? 'error' : coreRunning ? 'success' : 'neutral';
|
||||
const statusLabel = coreStatus
|
||||
? coreRunning
|
||||
? '运行中'
|
||||
: coreInstalled
|
||||
? '已停止'
|
||||
: '未安装'
|
||||
: statusError
|
||||
? '检测失败'
|
||||
: '检测中';
|
||||
const statusMessage = coreStatus?.message || statusError || '正在检测 CPA 内核';
|
||||
const latestLabel = checkingLatest
|
||||
? '检查中'
|
||||
: latestVersion || (latestError ? '检查失败' : '未检查');
|
||||
const updateStateLabel = checkingLatest
|
||||
? '正在检查'
|
||||
: latestError
|
||||
? '检查失败'
|
||||
: !latestVersion
|
||||
? '尚未检查'
|
||||
: currentVersion === latestVersion
|
||||
? '已是最新版本'
|
||||
: '可安装新版本';
|
||||
const platformLabel = platform ? `${platform.os} / ${platform.arch}` : platformError || '检测中';
|
||||
const platformOsLabel = platform?.os || (platformError ? '检测失败' : '检测中');
|
||||
const platformArchLabel = platform?.arch || (platformError ? '检测失败' : '检测中');
|
||||
const installTaskRunning = Boolean(installing || progress?.running);
|
||||
const controlSubtitle = processMessage || statusMessage;
|
||||
const controlSubtitleTone: MessageType = processMessage
|
||||
? processMessageType
|
||||
: statusError
|
||||
? 'error'
|
||||
: 'info';
|
||||
const versionStatusLabel = installTaskRunning
|
||||
? cancellingInstall
|
||||
? '正在取消下载'
|
||||
: progress?.phase || '正在安装'
|
||||
: message || updateStateLabel;
|
||||
const versionStatusTone: MessageType = installTaskRunning
|
||||
? 'info'
|
||||
: message
|
||||
? messageType
|
||||
: latestError
|
||||
? 'error'
|
||||
: currentVersion && currentVersion === latestVersion
|
||||
? 'success'
|
||||
: 'info';
|
||||
const installDialogTone: MessageType = progress?.result
|
||||
? 'success'
|
||||
: progress?.phase === '安装失败'
|
||||
? 'error'
|
||||
: 'info';
|
||||
const installDialogTitle = installTaskRunning
|
||||
? cancellingInstall
|
||||
? '正在取消下载'
|
||||
: '正在安装内核'
|
||||
: progress?.result
|
||||
? '安装完成'
|
||||
: progress?.phase === '已取消'
|
||||
? '下载已取消'
|
||||
: '安装失败';
|
||||
const installDialogMessage = cancellingInstall
|
||||
? '正在等待下载任务停止'
|
||||
: progress?.message || (installTaskRunning ? message || '安装任务正在进行' : '');
|
||||
const installDialogAction = installTaskRunning
|
||||
? cancellingInstall
|
||||
? '正在取消'
|
||||
: progress?.cancellable
|
||||
? '取消下载'
|
||||
: '处理中'
|
||||
: '关闭';
|
||||
const installDialogActionDisabled =
|
||||
installTaskRunning && (cancellingInstall || !progress?.cancellable);
|
||||
|
||||
return (
|
||||
<section className="page kernel-page">
|
||||
<div className="status-strip">
|
||||
<div className="status-card status-card-primary">
|
||||
<span className={`status-dot ${statusTone}`} />
|
||||
<div>
|
||||
<span>内核状态</span>
|
||||
<strong>{statusLabel}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="status-card">
|
||||
<span>当前版本</span>
|
||||
<strong>{currentVersion || '未安装'}</strong>
|
||||
</div>
|
||||
<div className="status-card">
|
||||
<span>最新版本</span>
|
||||
<strong>{latestLabel}</strong>
|
||||
</div>
|
||||
<div className="status-card">
|
||||
<span>运行平台</span>
|
||||
<strong>{platformLabel}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="kernel-layout">
|
||||
<div className="panel control-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>运行控制</h2>
|
||||
<p className={controlSubtitleTone} title={controlSubtitle}>
|
||||
{controlSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`state-pill ${statusTone}`}>{statusLabel}</span>
|
||||
</div>
|
||||
|
||||
<dl className="panel-detail-grid">
|
||||
<div className="panel-detail-row">
|
||||
<dt>安装状态</dt>
|
||||
<dd>{coreStatus ? (coreInstalled ? '已安装' : '未安装') : '检测中'}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>运行状态</dt>
|
||||
<dd>{coreStatus ? (coreRunning ? '运行中' : '未运行') : '检测中'}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>进程 PID</dt>
|
||||
<dd>{coreStatus?.processId || '无'}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>局域网</dt>
|
||||
<dd className="detail-control-cell">
|
||||
<span className="switch-control" title="允许局域网访问">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="允许局域网访问"
|
||||
checked={allowLanAccess}
|
||||
onChange={(event) => setAllowLanAccess(event.currentTarget.checked)}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>
|
||||
<label htmlFor="custom-port">端口</label>
|
||||
</dt>
|
||||
<dd className="detail-control-cell">
|
||||
<input
|
||||
id="custom-port"
|
||||
className="compact-text-input"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={5}
|
||||
placeholder="端口号"
|
||||
value={customPort}
|
||||
onChange={(event) =>
|
||||
setCustomPort(event.currentTarget.value.replace(/\D/g, '').slice(0, 5))
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="button-row panel-action-row control-action-row">
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
disabled={!coreInstalled || coreRunning || installing || processBusy}
|
||||
onClick={() =>
|
||||
runCoreProcessCommand('start_core_process', '正在启动 CPA 内核', 'CPA 内核已启动')
|
||||
}
|
||||
>
|
||||
{processBusy ? '处理中' : '启动'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="danger-button"
|
||||
disabled={!coreInstalled || !coreRunning || installing || processBusy}
|
||||
onClick={() =>
|
||||
runCoreProcessCommand('stop_core_process', '正在关闭 CPA 内核', 'CPA 内核已关闭')
|
||||
}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={!coreInstalled || !coreRunning || installing || processBusy}
|
||||
onClick={() =>
|
||||
runCoreProcessCommand(
|
||||
'restart_core_process',
|
||||
'正在重启 CPA 内核',
|
||||
'CPA 内核已重启',
|
||||
)
|
||||
}
|
||||
>
|
||||
重启
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={processBusy}
|
||||
onClick={loadCoreStatus}
|
||||
>
|
||||
刷新状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="panel version-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<h2>版本管理</h2>
|
||||
<p>检查和安装 CPA 内核版本</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="panel-detail-grid">
|
||||
<div className="panel-detail-row">
|
||||
<dt>当前版本</dt>
|
||||
<dd>{currentVersion || '未安装'}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>最新版本</dt>
|
||||
<dd>{latestLabel}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>操作系统</dt>
|
||||
<dd title={platformError || undefined}>{platformOsLabel}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>系统架构</dt>
|
||||
<dd title={platformError || undefined}>{platformArchLabel}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>更新状态</dt>
|
||||
<dd className={versionStatusTone} title={versionStatusLabel}>
|
||||
{versionStatusLabel}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="button-row panel-action-row version-action-row">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={busy}
|
||||
onClick={checkLatest}
|
||||
>
|
||||
{checkingLatest ? '检查中' : '检查更新'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
title={latestVersion ? `安装 ${latestVersion}` : '安装最新版'}
|
||||
disabled={!latestVersion || installDisabled}
|
||||
onClick={() => installVersion(latestVersion)}
|
||||
>
|
||||
安装最新
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
title="重新安装当前版本"
|
||||
disabled={!currentVersion || installDisabled}
|
||||
onClick={() => installVersion(currentVersion)}
|
||||
>
|
||||
重新安装
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{installDialogOpen && progress ? (
|
||||
<div className="install-dialog-backdrop">
|
||||
<div
|
||||
ref={installDialogRef}
|
||||
className={`install-dialog ${installDialogTone}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="install-dialog-title"
|
||||
aria-describedby="install-dialog-message"
|
||||
aria-busy={installTaskRunning}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="install-dialog-heading">
|
||||
<span>内核安装</span>
|
||||
<h2 id="install-dialog-title">{installDialogTitle}</h2>
|
||||
</div>
|
||||
|
||||
<div className="install-dialog-phase">
|
||||
<span>当前阶段</span>
|
||||
<strong>{cancellingInstall ? '正在取消' : progress.phase}</strong>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`install-progress-track ${
|
||||
progressKnown ? '' : installTaskRunning ? 'unknown is-running' : 'unknown'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="install-progress-fill"
|
||||
style={progressKnown ? { width: `${progressPercent}%` } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="install-progress-meta">
|
||||
<strong>{progressKnown ? `${progressPercent.toFixed(1)}%` : '进度未知'}</strong>
|
||||
<span>{progressText}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="install-dialog-message"
|
||||
className={`install-dialog-message ${installDialogTone}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{installDialogMessage || '\u00a0'}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={installTaskRunning ? 'danger-button' : 'primary-button'}
|
||||
disabled={installDialogActionDisabled}
|
||||
onClick={installTaskRunning ? cancelInstall : closeInstallDialog}
|
||||
>
|
||||
{installDialogAction}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function clampPercent(percent: number) {
|
||||
return Math.min(100, Math.max(0, percent));
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
+801
-27
@@ -3,6 +3,8 @@
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #2d2a26;
|
||||
background: #faf9f5;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -22,111 +24,883 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 3px solid rgba(45, 42, 38, 0.18);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
grid-template-columns: 236px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
background: #faf9f5;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding: 16px;
|
||||
padding: 18px;
|
||||
background: #f0eee8;
|
||||
border-right: 1px solid #e3e1db;
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 10px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 54px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
place-items: center;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 8px;
|
||||
background: #2d2a26;
|
||||
color: #fffdf8;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sidebar-brand strong,
|
||||
.sidebar-brand span {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand strong {
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
font-size: 15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-brand div > span {
|
||||
margin-top: 4px;
|
||||
color: #7a746c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.nav-section button {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
min-height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #2d2a26;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nav-section button:hover {
|
||||
background: #e8e5dc;
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.nav-section button.active {
|
||||
background: #dfdcd4;
|
||||
border-color: #d5d2cb;
|
||||
background: #dfdcd4;
|
||||
color: #2d2a26;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #e3e1db;
|
||||
color: #7a746c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sidebar-footer strong {
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.state-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 999px;
|
||||
background: #fffdf8;
|
||||
color: #5f5a53;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state-pill.success {
|
||||
border-color: #b6d3bd;
|
||||
background: #eef7f0;
|
||||
color: #2f6b3f;
|
||||
}
|
||||
|
||||
.state-pill.error {
|
||||
border-color: #dfb7b0;
|
||||
background: #fff1ee;
|
||||
color: #9f2d20;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 48px;
|
||||
width: 100%;
|
||||
padding: 24px 40px 42px;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 860px;
|
||||
width: min(100%, 1180px);
|
||||
}
|
||||
|
||||
.page h1 {
|
||||
margin: 0 0 24px;
|
||||
font-size: 28px;
|
||||
.kernel-page,
|
||||
.config-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e1db;
|
||||
border-radius: 8px;
|
||||
background: #fffdf8;
|
||||
box-shadow: 0 1px 2px rgba(45, 42, 38, 0.04);
|
||||
}
|
||||
|
||||
.quiet-panel {
|
||||
background: #faf9f5;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 49px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-heading > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0;
|
||||
color: #2d2a26;
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.panel-heading p {
|
||||
min-height: 19px;
|
||||
margin: 5px 0 0;
|
||||
overflow: hidden;
|
||||
color: #5f5a53;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel-heading p.success {
|
||||
color: #2f6b3f;
|
||||
}
|
||||
|
||||
.panel-heading p.error {
|
||||
color: #9f2d20;
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
min-height: 84px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e3e1db;
|
||||
border-radius: 8px;
|
||||
background: #fffdf8;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
.status-card-primary {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel p {
|
||||
.status-card-primary > div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-card span {
|
||||
overflow: hidden;
|
||||
color: #7a746c;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-card strong {
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
font-size: 17px;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
background: #9b958c;
|
||||
box-shadow: 0 0 0 5px rgba(155, 149, 140, 0.14);
|
||||
}
|
||||
|
||||
.status-dot.success {
|
||||
background: #2f6b3f;
|
||||
box-shadow: 0 0 0 5px rgba(47, 107, 63, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: #9f2d20;
|
||||
box-shadow: 0 0 0 5px rgba(159, 45, 32, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.neutral {
|
||||
background: #9b958c;
|
||||
}
|
||||
|
||||
.kernel-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.config-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(420px, 1.15fr) minmax(340px, 0.85fr);
|
||||
align-items: start;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.version-panel {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.kernel-layout > .panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: 65px 232px 56px;
|
||||
height: 395px;
|
||||
}
|
||||
|
||||
.kernel-layout > .panel > .panel-heading {
|
||||
min-height: 0;
|
||||
height: 65px;
|
||||
margin: 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.kernel-layout .panel-action-row {
|
||||
height: 56px;
|
||||
margin: 0;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.panel-detail-grid {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(5, 40px);
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 92px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.panel-detail-row dt {
|
||||
color: #7a746c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.panel-detail-row dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel-detail-row dd.success {
|
||||
color: #2f6b3f;
|
||||
}
|
||||
|
||||
.panel-detail-row dd.error {
|
||||
color: #9f2d20;
|
||||
}
|
||||
|
||||
.detail-control-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 40px;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.compact-text-input {
|
||||
width: min(180px, 100%);
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #2d2a26;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.compact-text-input::placeholder {
|
||||
color: #9b958c;
|
||||
}
|
||||
|
||||
.control-action-row {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.version-action-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-rows: repeat(4, 20px);
|
||||
min-height: 110px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-list div {
|
||||
display: grid;
|
||||
grid-template-columns: 92px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.info-list dt {
|
||||
color: #7a746c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-list dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 18px;
|
||||
border: 1px dashed #d5d2cb;
|
||||
border-radius: 8px;
|
||||
background: #fffdf8;
|
||||
color: #5f5a53;
|
||||
}
|
||||
|
||||
.empty-state strong {
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.secondary-button,
|
||||
.danger-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
border: 1px solid #2d2a26;
|
||||
background: #2d2a26;
|
||||
color: #fffdf8;
|
||||
}
|
||||
|
||||
.primary-button:hover:not(:disabled) {
|
||||
background: #433f39;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
border: 1px solid #d5d2cb;
|
||||
background: #fffdf8;
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.secondary-button:hover:not(:disabled) {
|
||||
background: #f4f1ea;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
border: 1px solid #a83f32;
|
||||
background: #fffdf8;
|
||||
color: #9f2d20;
|
||||
}
|
||||
|
||||
.danger-button:hover:not(:disabled) {
|
||||
background: #fff1ee;
|
||||
}
|
||||
|
||||
.primary-button:disabled,
|
||||
.secondary-button:disabled,
|
||||
.danger-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.switch-control input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 999px;
|
||||
background: #e8e5dc;
|
||||
transition:
|
||||
background 140ms ease,
|
||||
border-color 140ms ease;
|
||||
}
|
||||
|
||||
.switch-track::after {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 2px;
|
||||
border-radius: 999px;
|
||||
background: #fffdf8;
|
||||
box-shadow: 0 1px 2px rgba(45, 42, 38, 0.18);
|
||||
transition: transform 140ms ease;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.switch-control input:checked + .switch-track {
|
||||
border-color: #2d2a26;
|
||||
background: #2d2a26;
|
||||
}
|
||||
|
||||
.switch-control input:checked + .switch-track::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-width: 420px;
|
||||
min-width: 0;
|
||||
color: #5f5a53;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #2d2a26;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.field input::placeholder {
|
||||
color: #9b958c;
|
||||
}
|
||||
|
||||
.field input:disabled {
|
||||
cursor: not-allowed;
|
||||
background: #f4f1ea;
|
||||
color: #9b958c;
|
||||
}
|
||||
|
||||
.install-dialog-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: rgba(45, 42, 38, 0.42);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.install-dialog {
|
||||
display: grid;
|
||||
grid-template-rows: 48px 42px 10px 22px 72px 40px;
|
||||
gap: 15px;
|
||||
width: min(420px, 100%);
|
||||
height: 360px;
|
||||
padding: 24px;
|
||||
border: 1px solid #d5d2cb;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
background: #fffdf8;
|
||||
box-shadow: 0 18px 48px rgba(45, 42, 38, 0.24);
|
||||
}
|
||||
|
||||
.install-dialog-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-dialog-heading > span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #7a746c;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.install-dialog-heading h2 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
font-size: 20px;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.install-dialog-phase {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
background: #f4f1ea;
|
||||
}
|
||||
|
||||
.install-dialog-phase span {
|
||||
color: #7a746c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.install-dialog-phase strong {
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.install-progress-track {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #e8e5dc;
|
||||
}
|
||||
|
||||
.install-progress-fill {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #2d2a26;
|
||||
transition: width 160ms ease;
|
||||
}
|
||||
|
||||
.install-dialog.success .install-progress-fill {
|
||||
background: #2f6b3f;
|
||||
}
|
||||
|
||||
.install-dialog.error .install-progress-fill {
|
||||
background: #9f2d20;
|
||||
}
|
||||
|
||||
.install-progress-track.unknown .install-progress-fill {
|
||||
visibility: hidden;
|
||||
width: 36%;
|
||||
}
|
||||
|
||||
.install-progress-track.unknown.is-running .install-progress-fill {
|
||||
visibility: visible;
|
||||
animation: install-progress-slide 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.install-progress-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
color: #6d6760;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.install-progress-meta strong,
|
||||
.install-progress-meta span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.install-progress-meta strong {
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.install-dialog-message {
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #faf9f5;
|
||||
color: #5f5a53;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.install-dialog-message.success {
|
||||
background: #eef7f0;
|
||||
color: #2f6b3f;
|
||||
}
|
||||
|
||||
.install-dialog-message.error {
|
||||
background: #fff1ee;
|
||||
color: #9f2d20;
|
||||
}
|
||||
|
||||
.install-dialog > button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@keyframes install-progress-slide {
|
||||
from {
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(300%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 160px 1fr;
|
||||
grid-template-columns: 204px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.kernel-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.control-panel,
|
||||
.version-panel {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 12px;
|
||||
position: static;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
height: auto;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-section button {
|
||||
width: auto;
|
||||
min-width: 96px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.status-strip,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.kernel-layout > .panel {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.panel-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
grid-auto-columns: auto;
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.secondary-button,
|
||||
.danger-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kernel-layout .panel-heading {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.kernel-layout .panel-action-row {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.kernel-layout .control-action-row {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.kernel-layout .version-action-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.info-list div {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.install-dialog-backdrop {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.install-progress-track.unknown.is-running .install-progress-fill {
|
||||
animation-duration: 2.2s;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user