mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-05 14:46:53 +08:00
v0.1.3-rc.5
This commit is contained in:
15
.env.example
15
.env.example
@@ -22,8 +22,8 @@ SHARED_TOKEN=
|
||||
# 令牌文件路径(已弃用)
|
||||
# TOKEN_FILE=.token
|
||||
|
||||
# 令牌列表文件路径
|
||||
TOKEN_LIST_FILE=.tokens
|
||||
# 令牌列表文件路径(已弃用)
|
||||
# TOKEN_LIST_FILE=.tokens
|
||||
|
||||
# (实验性)是否启用慢速池(true/false)
|
||||
ENABLE_SLOW_POOL=false
|
||||
@@ -93,8 +93,11 @@ SERVICE_TIMEOUT=30
|
||||
# 包含网络引用
|
||||
INCLUDE_WEB_REFERENCES=false
|
||||
|
||||
# 持久化日志文件路径
|
||||
LOGS_FILE_PATH=logs.bin
|
||||
# 持久化日志文件路径(已弃用)
|
||||
# LOGS_FILE_PATH=logs.bin
|
||||
|
||||
# 持久化页面配置文件路径
|
||||
PAGES_FILE_PATH=pages.bin
|
||||
# 持久化页面配置文件路径(已弃用)
|
||||
# PAGES_FILE_PATH=pages.bin
|
||||
|
||||
# 程序数据目录
|
||||
DATA_DIR=data
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,7 +16,7 @@ node_modules
|
||||
/cursor-api
|
||||
/cursor-api.exe
|
||||
/release
|
||||
|
||||
/data
|
||||
/*.py
|
||||
/logs
|
||||
/dev*
|
||||
|
309
Cargo.lock
generated
309
Cargo.lock
generated
@@ -23,7 +23,7 @@ version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
@@ -69,9 +69,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
@@ -220,9 +220,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "4.0.1"
|
||||
version = "4.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
|
||||
checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
@@ -230,9 +230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
@@ -276,15 +276,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.10"
|
||||
version = "1.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
|
||||
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -333,9 +333,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -361,7 +361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cursor-api"
|
||||
version = "0.1.3-rc.4.3"
|
||||
version = "0.1.3-rc.5"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
@@ -378,6 +378,7 @@ dependencies = [
|
||||
"paste",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest",
|
||||
@@ -412,7 +413,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -438,9 +439,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
@@ -460,9 +461,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "faststr"
|
||||
version = "0.2.27"
|
||||
version = "0.2.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9154486833a83cb5d99de8c4d831314b8ae810dd4ef18d89ceb7a9c7c728dd74"
|
||||
checksum = "403ebc0cd0c6dbff1cae7098168eff6bac83fad5928b6e91f29388b8dbb61653"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rkyv 0.8.10",
|
||||
@@ -481,9 +482,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
@@ -575,7 +576,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -626,7 +627,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.13.3+wasi-0.2.2",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -647,9 +660,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
|
||||
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -727,9 +740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.9.5"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@@ -739,9 +752,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.5.2"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
|
||||
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -948,7 +961,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1017,9 +1030,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1070,9 +1083,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.25"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
@@ -1103,9 +1116,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
@@ -1118,7 +1131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -1130,29 +1143,29 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||
|
||||
[[package]]
|
||||
name = "munge"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64142d38c84badf60abf06ff9bd80ad2174306a5b11bd4706535090a30a419df"
|
||||
checksum = "8743b8dfaf66acac79aca9ff2440e8680fef745b6260e6a31d1772b14cfa2862"
|
||||
dependencies = [
|
||||
"munge_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "munge_macro"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e"
|
||||
checksum = "66191390a55bb9830fa8468c12634442ea4199c6e390ddf08ddcace35b3cd5da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -1194,15 +1207,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
version = "1.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.68"
|
||||
version = "0.10.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"cfg-if",
|
||||
@@ -1221,20 +1234,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.104"
|
||||
version = "0.9.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1279,9 +1292,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
@@ -1324,7 +1337,7 @@ version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1334,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1348,9 +1361,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
@@ -1358,9 +1371,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools",
|
||||
@@ -1372,28 +1385,28 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
@@ -1435,7 +1448,7 @@ checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1470,20 +1483,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"zerocopy 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
@@ -1491,18 +1504,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.3.1",
|
||||
"zerocopy 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
@@ -1524,7 +1538,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1621,15 +1635,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -1689,7 +1702,7 @@ checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1700,9 +1713,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.43"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"errno",
|
||||
@@ -1713,9 +1726,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.21"
|
||||
version = "0.23.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
@@ -1735,9 +1748,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.10.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
|
||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
@@ -1758,9 +1771,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
@@ -1808,29 +1821,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.137"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1909,9 +1922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@@ -1961,12 +1974,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -1992,9 +1999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.96"
|
||||
version = "2.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
|
||||
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2018,7 +2025,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2063,13 +2070,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.15.0"
|
||||
version = "3.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
|
||||
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -2101,7 +2108,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2112,7 +2119,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2165,7 +2172,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2296,15 +2303,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
@@ -2337,11 +2344,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.12.1"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2371,6 +2378,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.13.3+wasi-0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
@@ -2393,7 +2409,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2428,7 +2444,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -2532,7 +2548,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2543,7 +2559,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2667,6 +2683,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
@@ -2708,7 +2733,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2719,7 +2744,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2730,7 +2764,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2750,7 +2795,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2779,7 +2824,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
71
Cargo.toml
71
Cargo.toml
@@ -1,47 +1,48 @@
|
||||
[package]
|
||||
name = "cursor-api"
|
||||
version = "0.1.3-rc.4.3"
|
||||
edition = "2021"
|
||||
version = "0.1.3-rc.5"
|
||||
edition = "2024"
|
||||
authors = ["wisdgod <nav@wisdgod.com>"]
|
||||
description = "OpenAI format compatibility layer for the Cursor API"
|
||||
repository = "https://github.com/wisdgod/cursor-api"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.4"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
serde_json = "1.0.134"
|
||||
prost-build = "^0.13"
|
||||
sha2 = { version = "^0.10.8", default-features = false }
|
||||
serde_json = "^1.0"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", features = ["json"] }
|
||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||
# brotli = { version = "7.0.0", default-features = false, features = ["std"] }
|
||||
bytes = "1.9.0"
|
||||
chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] }
|
||||
dotenvy = "0.15.7"
|
||||
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
|
||||
futures = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
gif = { version = "0.13.1", default-features = false, features = ["std"] }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
|
||||
memmap2 = "0.9.5"
|
||||
# openssl = { version = "0.10.68", features = ["vendored"] }
|
||||
parking_lot = "0.12.3"
|
||||
paste = "1.0.15"
|
||||
prost = "0.13.4"
|
||||
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
||||
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
|
||||
reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
|
||||
rkyv = { version = "0.7.45", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] }
|
||||
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] }
|
||||
serde_json = { package = "sonic-rs", version = "0.3.17" }
|
||||
# serde_json = "1.0.137"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["time"] }
|
||||
tower-http = { version = "0.6.2", features = ["cors", "limit"] }
|
||||
url = { version = "2.5.4", default-features = false }
|
||||
uuid = { version = "1.12.1", features = ["v4"] }
|
||||
axum = { version = "^0.8", features = ["json"] }
|
||||
base64 = { version = "^0.22", default-features = false, features = ["std"] }
|
||||
# brotli = { version = "^7.0", default-features = false, features = ["std"] }
|
||||
bytes = "^1.10"
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] }
|
||||
dotenvy = "^0.15"
|
||||
flate2 = { version = "^1.0", default-features = false, features = ["rust_backend"] }
|
||||
futures = { version = "^0.3", default-features = false, features = ["std"] }
|
||||
gif = { version = "^0.13", default-features = false, features = ["std"] }
|
||||
hex = { version = "^0.4", default-features = false, features = ["std"] }
|
||||
image = { version = "^0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
|
||||
memmap2 = "^0.9"
|
||||
# openssl = { version = "^0.10", features = ["vendored"] }
|
||||
parking_lot = "^0.12"
|
||||
paste = "^1.0"
|
||||
prost = "^0.13"
|
||||
prost-types = "^0.13"
|
||||
rand = { version = "^0.9", default-features = false, features = ["thread_rng"] }
|
||||
regex = { version = "^1.11", default-features = false, features = ["std", "perf"] }
|
||||
reqwest = { version = "^0.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
|
||||
rkyv = { version = "^0.7", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] }
|
||||
serde = { version = "^1.0", default-features = false, features = ["std", "derive"] }
|
||||
serde_json = { package = "sonic-rs", version = "^0.3" }
|
||||
# serde_json = "^1.0"
|
||||
sha2 = { version = "^0.10", default-features = false }
|
||||
sysinfo = { version = "^0.33", default-features = false, features = ["system"] }
|
||||
tokio = { version = "^1.43", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] }
|
||||
tokio-stream = { version = "^0.1", features = ["time"] }
|
||||
tower-http = { version = "^0.6", features = ["cors", "limit"] }
|
||||
url = { version = "^2.5", default-features = false }
|
||||
uuid = { version = "^1.14", features = ["v4"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
10
Dockerfile
10
Dockerfile
@@ -1,5 +1,5 @@
|
||||
ARG TARGETARCH
|
||||
FROM --platform=linux/${TARGETARCH} rust:1.84.0-slim-bookworm as builder
|
||||
FROM --platform=linux/${TARGETARCH} rust:1-slim-bookworm as builder
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -10,13 +10,7 @@ RUN apt-get update && \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . .
|
||||
RUN case "$TARGETARCH" in \
|
||||
amd64) TARGET_CPU="x86-64-v3" ;; \
|
||||
arm64) TARGET_CPU="neoverse-n1" ;; \
|
||||
*) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
RUSTFLAGS="-C link-arg=-s -C target-cpu=$TARGET_CPU" cargo build --release && \
|
||||
cp target/release/cursor-api /app/cursor-api
|
||||
RUN case "$TARGETARCH" in amd64) TARGET_CPU="x86-64-v3" ;; arm64) TARGET_CPU="neoverse-n1" ;; *) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; esac && RUSTFLAGS="-C link-arg=-s -C target-cpu=$TARGET_CPU" cargo build --release && cp target/release/cursor-api /app/cursor-api
|
||||
|
||||
# 运行阶段
|
||||
FROM --platform=linux/${TARGETARCH} debian:bookworm-slim
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Dockerfile.cross
|
||||
|
||||
FROM --platform=linux/amd64 rust:1.84.0-slim-bookworm
|
||||
FROM --platform=linux/amd64 rust:1-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Dockerfile.cross
|
||||
|
||||
FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm
|
||||
FROM --platform=linux/arm64 rust:1-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
102
README.md
102
README.md
@@ -59,17 +59,18 @@ gpt-4o-128k
|
||||
gemini-1.5-flash-500k
|
||||
claude-3-haiku-200k
|
||||
claude-3-5-sonnet-200k
|
||||
claude-3-5-sonnet-20241022
|
||||
gpt-4o-mini
|
||||
o1-mini
|
||||
o1-preview
|
||||
o1
|
||||
claude-3.5-haiku
|
||||
gemini-exp-1206
|
||||
gemini-2.0-pro-exp
|
||||
gemini-2.0-flash-thinking-exp
|
||||
gemini-2.0-flash-exp
|
||||
gemini-2.0-flash
|
||||
deepseek-v3
|
||||
deepseek-r1
|
||||
o3-mini
|
||||
grok-2
|
||||
```
|
||||
|
||||
## 接口说明
|
||||
@@ -148,6 +149,32 @@ data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
### 获取模型列表
|
||||
|
||||
* 接口地址: `/v1/models`
|
||||
* 请求方法: GET
|
||||
* 认证方式: Bearer Token
|
||||
|
||||
#### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "string",
|
||||
"object": "model",
|
||||
"created": number,
|
||||
"owned_by": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新模型列表说明
|
||||
|
||||
每次携带Token时都会拉取最新的模型列表,与上次更新需距离至少30分钟。
|
||||
|
||||
### Token管理接口
|
||||
|
||||
#### 简易Token信息管理页面
|
||||
@@ -194,7 +221,8 @@ data: [DONE]
|
||||
"tokens": number,
|
||||
"max_requests": number,
|
||||
"max_tokens": number
|
||||
}
|
||||
},
|
||||
"start_of_month": "string"
|
||||
},
|
||||
"user": {
|
||||
"email": "string",
|
||||
@@ -260,12 +288,15 @@ data: [DONE]
|
||||
* 请求格式:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"token": "string",
|
||||
"checksum": "string" // 可选,如果不提供将自动生成
|
||||
}
|
||||
]
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"token": "string",
|
||||
"checksum": "string" // 可选,如果不提供将自动生成
|
||||
}
|
||||
],
|
||||
"tags": ["string"]
|
||||
}
|
||||
```
|
||||
|
||||
* 响应格式:
|
||||
@@ -308,6 +339,29 @@ data: [DONE]
|
||||
- failed_tokens: 返回未找到的token列表
|
||||
- detailed: 返回完整信息(包括updated_tokens和failed_tokens)
|
||||
|
||||
#### 更新Tokens标签
|
||||
|
||||
* 接口地址: `/tokens/tags/update`
|
||||
* 请求方法: POST
|
||||
* 认证方式: Bearer Token
|
||||
* 请求格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"tokens": ["string"],
|
||||
"tags": ["string"]
|
||||
}
|
||||
```
|
||||
|
||||
* 响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "string" // "标签更新成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 构建API Key
|
||||
|
||||
* 接口地址: `/build-key`
|
||||
@@ -354,7 +408,7 @@ data: [DONE]
|
||||
|------|------|
|
||||
| 提取关键信息,生成更短的密钥 | 可能存在版本兼容性问题 |
|
||||
| 支持携带自定义配置 | 增加了程序复杂度 |
|
||||
| 采用非常规编码方式,提升安全性 | |
|
||||
| 采用非常规编码方式,提升安全性 | 项目是开源的,安全性的提升相当于没有 |
|
||||
| 更容易验证Key的合法性 | |
|
||||
| 取消预校验带来轻微性能提升 | |
|
||||
|
||||
@@ -470,26 +524,6 @@ data: [DONE]
|
||||
|
||||
### 其他接口
|
||||
|
||||
#### 获取模型列表
|
||||
|
||||
* 接口地址: `/v1/models`
|
||||
* 请求方法: GET
|
||||
* 响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "string",
|
||||
"object": "model",
|
||||
"created": number,
|
||||
"owned_by": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取一个随机hash
|
||||
|
||||
* 接口地址: `/get-hash`
|
||||
@@ -605,7 +639,8 @@ string
|
||||
"tokens": number,
|
||||
"max_requests": number,
|
||||
"max_tokens": number
|
||||
}
|
||||
},
|
||||
"start_of_month": "string"
|
||||
},
|
||||
"user": {
|
||||
"email": "string",
|
||||
@@ -673,7 +708,8 @@ string
|
||||
"tokens": number,
|
||||
"max_requests": number,
|
||||
"max_tokens": number
|
||||
}
|
||||
},
|
||||
"start_of_month": "string"
|
||||
},
|
||||
"user": {
|
||||
"email": "string",
|
||||
|
21
build.rs
21
build.rs
@@ -116,7 +116,7 @@ fn minify_assets() -> Result<()> {
|
||||
let files_to_update: Vec<_> = current_hashes
|
||||
.iter()
|
||||
.filter(|(path, current_hash)| {
|
||||
let is_readme = path.file_name().map_or(false, |f| f == "README.md");
|
||||
let is_readme = path.file_name().is_some_and(|f| f == "README.md");
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
|
||||
// 为 README.md 和其他文件使用不同的输出路径检查
|
||||
@@ -136,9 +136,7 @@ fn minify_assets() -> Result<()> {
|
||||
}
|
||||
|
||||
// 检查原始文件是否发生变化
|
||||
saved_hashes
|
||||
.get(*path)
|
||||
.map_or(true, |saved_hash| saved_hash != *current_hash)
|
||||
saved_hashes.get(*path) != Some(*current_hash)
|
||||
})
|
||||
.map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
@@ -168,7 +166,7 @@ fn minify_assets() -> Result<()> {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Proto 文件处理
|
||||
println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto");
|
||||
// println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto");
|
||||
println!("cargo:rerun-if-changed=src/chat/config/key.proto");
|
||||
// 获取环境变量 PROTOC
|
||||
let protoc_path = match std::env::var_os("PROTOC") {
|
||||
@@ -185,12 +183,13 @@ fn main() -> Result<()> {
|
||||
config.protoc_executable(protoc_path);
|
||||
}
|
||||
// config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
|
||||
config
|
||||
.compile_protos(
|
||||
&["src/chat/aiserver/v1/lite.proto"],
|
||||
&["src/chat/aiserver/v1/"],
|
||||
)
|
||||
.unwrap();
|
||||
// config.enum_attribute(".aiserver.v1", "#[allow(clippy::enum_variant_names)]");
|
||||
// config
|
||||
// .compile_protos(
|
||||
// &["src/chat/aiserver/v1/lite.proto"],
|
||||
// &["src/chat/aiserver/v1/"],
|
||||
// )
|
||||
// .unwrap();
|
||||
config
|
||||
.compile_protos(&["src/chat/config/key.proto"], &["src/chat/config/"])
|
||||
.unwrap();
|
||||
|
@@ -2,3 +2,4 @@ pub mod config;
|
||||
pub mod constant;
|
||||
pub mod model;
|
||||
pub mod lazy;
|
||||
// pub mod rule;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
use super::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, model::AppConfig};
|
||||
use crate::common::model::{
|
||||
config::{ConfigData, ConfigUpdateRequest},
|
||||
ApiStatus, ErrorResponse, NormalResponse,
|
||||
config::{ConfigData, ConfigUpdateRequest},
|
||||
};
|
||||
use axum::{
|
||||
http::{header::AUTHORIZATION, HeaderMap, StatusCode},
|
||||
Json,
|
||||
http::{HeaderMap, StatusCode, header::AUTHORIZATION},
|
||||
};
|
||||
|
||||
// 定义处理更新操作的宏
|
||||
@@ -41,7 +41,7 @@ pub async fn handle_config_update(
|
||||
.ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failed,
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(401),
|
||||
error: Some("未提供认证令牌".to_string()),
|
||||
message: None,
|
||||
@@ -52,7 +52,7 @@ pub async fn handle_config_update(
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failed,
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(401),
|
||||
error: Some("无效的认证令牌".to_string()),
|
||||
message: None,
|
||||
@@ -85,7 +85,7 @@ pub async fn handle_config_update(
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failed,
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(500),
|
||||
error: Some(format!("更新页面内容失败: {}", e)),
|
||||
message: None,
|
||||
@@ -119,7 +119,7 @@ pub async fn handle_config_update(
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failed,
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(500),
|
||||
error: Some(format!("重置页面内容失败: {}", e)),
|
||||
message: None,
|
||||
@@ -149,7 +149,7 @@ pub async fn handle_config_update(
|
||||
_ => Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failed,
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(400),
|
||||
error: Some("无效的操作类型".to_string()),
|
||||
message: None,
|
||||
|
@@ -1,83 +1,122 @@
|
||||
macro_rules! def_pub_const {
|
||||
($name:ident, $value:expr) => {
|
||||
pub const $name: &'static str = $value;
|
||||
// 单个常量定义
|
||||
// ($name:ident, $value:expr) => {
|
||||
// pub const $name: &'static str = $value;
|
||||
// };
|
||||
|
||||
// 批量常量定义
|
||||
($($name:ident => $value:expr),+ $(,)?) => {
|
||||
$(
|
||||
pub const $name: &'static str = $value;
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
pub const COMMA: char = ',';
|
||||
|
||||
def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION"));
|
||||
// def_pub_const!(PKG_NAME, env!("CARGO_PKG_NAME"));
|
||||
// def_pub_const!(PKG_DESCRIPTION, env!("CARGO_PKG_DESCRIPTION"));
|
||||
// def_pub_const!(PKG_AUTHORS, env!("CARGO_PKG_AUTHORS"));
|
||||
// def_pub_const!(PKG_REPOSITORY, env!("CARGO_PKG_REPOSITORY"));
|
||||
|
||||
def_pub_const!(EMPTY_STRING, "");
|
||||
|
||||
def_pub_const!(COMMA_STRING, ",");
|
||||
|
||||
def_pub_const!(ROUTE_ROOT_PATH, "/");
|
||||
def_pub_const!(ROUTE_HEALTH_PATH, "/health");
|
||||
def_pub_const!(ROUTE_GET_HASH, "/get-hash");
|
||||
def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum");
|
||||
def_pub_const!(ROUTE_GET_TIMESTAMP_HEADER, "/get-tsheader");
|
||||
def_pub_const!(ROUTE_USER_INFO_PATH, "/userinfo");
|
||||
def_pub_const!(ROUTE_API_PATH, "/api");
|
||||
def_pub_const!(ROUTE_LOGS_PATH, "/logs");
|
||||
def_pub_const!(ROUTE_CONFIG_PATH, "/config");
|
||||
def_pub_const!(ROUTE_TOKENS_PATH, "/tokens");
|
||||
def_pub_const!(ROUTE_TOKENS_GET_PATH, "/tokens/get");
|
||||
def_pub_const!(ROUTE_TOKENS_RELOAD_PATH, "/tokens/reload");
|
||||
def_pub_const!(ROUTE_TOKENS_UPDATE_PATH, "/tokens/update");
|
||||
def_pub_const!(ROUTE_TOKENS_ADD_PATH, "/tokens/add");
|
||||
def_pub_const!(ROUTE_TOKENS_DELETE_PATH, "/tokens/delete");
|
||||
def_pub_const!(ROUTE_ENV_EXAMPLE_PATH, "/env-example");
|
||||
def_pub_const!(ROUTE_STATIC_PATH, "/static/{path}");
|
||||
def_pub_const!(ROUTE_SHARED_STYLES_PATH, "/static/shared-styles.css");
|
||||
def_pub_const!(ROUTE_SHARED_JS_PATH, "/static/shared.js");
|
||||
def_pub_const!(ROUTE_ABOUT_PATH, "/about");
|
||||
def_pub_const!(ROUTE_README_PATH, "/readme");
|
||||
def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration");
|
||||
def_pub_const!(ROUTE_BUILD_KEY_PATH, "/build-key");
|
||||
|
||||
def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".tokens");
|
||||
|
||||
def_pub_const!(STATUS_PENDING, "pending");
|
||||
def_pub_const!(STATUS_SUCCESS, "success");
|
||||
def_pub_const!(STATUS_FAILED, "failed");
|
||||
|
||||
def_pub_const!(HEADER_NAME_GHOST_MODE, "x-ghost-mode");
|
||||
|
||||
def_pub_const!(TRUE, "true");
|
||||
def_pub_const!(FALSE, "false");
|
||||
|
||||
// def_pub_const!(CONTENT_TYPE_PROTO, "application/proto");
|
||||
def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto");
|
||||
def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8");
|
||||
// Package related constants
|
||||
def_pub_const!(
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8,
|
||||
"text/plain;charset=utf-8"
|
||||
);
|
||||
def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8");
|
||||
def_pub_const!(
|
||||
CONTENT_TYPE_TEXT_JS_WITH_UTF8,
|
||||
"text/javascript;charset=utf-8"
|
||||
PKG_VERSION => env!("CARGO_PKG_VERSION")
|
||||
// PKG_NAME => env!("CARGO_PKG_NAME"),
|
||||
// PKG_DESCRIPTION => env!("CARGO_PKG_DESCRIPTION"),
|
||||
// PKG_AUTHORS => env!("CARGO_PKG_AUTHORS"),
|
||||
// PKG_REPOSITORY => env!("CARGO_PKG_REPOSITORY")
|
||||
);
|
||||
|
||||
def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer ");
|
||||
// Basic string constants
|
||||
def_pub_const!(
|
||||
EMPTY_STRING => "",
|
||||
COMMA_STRING => ","
|
||||
);
|
||||
|
||||
def_pub_const!(CURSOR_API2_HOST, "api2.cursor.sh");
|
||||
def_pub_const!(CURSOR_HOST, "www.cursor.com");
|
||||
def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings");
|
||||
// Route related constants
|
||||
def_pub_const!(
|
||||
ROUTE_ROOT_PATH => "/",
|
||||
ROUTE_HEALTH_PATH => "/health",
|
||||
ROUTE_GET_HASH => "/get-hash",
|
||||
ROUTE_GET_CHECKSUM => "/get-checksum",
|
||||
ROUTE_GET_TIMESTAMP_HEADER => "/get-tsheader",
|
||||
ROUTE_USER_INFO_PATH => "/userinfo",
|
||||
ROUTE_API_PATH => "/api",
|
||||
ROUTE_LOGS_PATH => "/logs",
|
||||
ROUTE_CONFIG_PATH => "/config",
|
||||
ROUTE_TOKENS_PATH => "/tokens",
|
||||
ROUTE_TOKENS_GET_PATH => "/tokens/get",
|
||||
ROUTE_TOKENS_UPDATE_PATH => "/tokens/update",
|
||||
ROUTE_TOKENS_ADD_PATH => "/tokens/add",
|
||||
ROUTE_TOKENS_DELETE_PATH => "/tokens/delete",
|
||||
ROUTE_TOKEN_TAGS_UPDATE_PATH => "/tokens/tags/update",
|
||||
ROUTE_ENV_EXAMPLE_PATH => "/env-example",
|
||||
ROUTE_STATIC_PATH => "/static/{path}",
|
||||
ROUTE_SHARED_STYLES_PATH => "/static/shared-styles.css",
|
||||
ROUTE_SHARED_JS_PATH => "/static/shared.js",
|
||||
ROUTE_ABOUT_PATH => "/about",
|
||||
ROUTE_README_PATH => "/readme",
|
||||
ROUTE_BASIC_CALIBRATION_PATH => "/basic-calibration",
|
||||
ROUTE_BUILD_KEY_PATH => "/build-key"
|
||||
);
|
||||
|
||||
def_pub_const!(OBJECT_CHAT_COMPLETION, "chat.completion");
|
||||
def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk");
|
||||
// def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME => ".tokens");
|
||||
|
||||
// def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat");
|
||||
// def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo");
|
||||
// Status constants
|
||||
def_pub_const!(
|
||||
STATUS_PENDING => "pending",
|
||||
STATUS_SUCCESS => "success",
|
||||
STATUS_FAILED => "failed"
|
||||
);
|
||||
|
||||
def_pub_const!(FINISH_REASON_STOP, "stop");
|
||||
// Header constants
|
||||
def_pub_const!(
|
||||
HEADER_NAME_GHOST_MODE => "x-ghost-mode"
|
||||
);
|
||||
|
||||
def_pub_const!(ERR_INVALID_PATH, "无效的路径");
|
||||
// Boolean constants
|
||||
def_pub_const!(
|
||||
TRUE => "true",
|
||||
FALSE => "false"
|
||||
);
|
||||
|
||||
// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good");
|
||||
// Content type constants
|
||||
def_pub_const!(
|
||||
CONTENT_TYPE_PROTO => "application/proto",
|
||||
CONTENT_TYPE_CONNECT_PROTO => "application/connect+proto",
|
||||
CONTENT_TYPE_TEXT_HTML_WITH_UTF8 => "text/html;charset=utf-8",
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8 => "text/plain;charset=utf-8",
|
||||
CONTENT_TYPE_TEXT_CSS_WITH_UTF8 => "text/css;charset=utf-8",
|
||||
CONTENT_TYPE_TEXT_JS_WITH_UTF8 => "text/javascript;charset=utf-8"
|
||||
);
|
||||
|
||||
// Authorization constants
|
||||
def_pub_const!(
|
||||
AUTHORIZATION_BEARER_PREFIX => "Bearer "
|
||||
);
|
||||
|
||||
// Cursor related constants
|
||||
def_pub_const!(
|
||||
CURSOR_API2_HOST => "api2.cursor.sh",
|
||||
CURSOR_HOST => "www.cursor.com",
|
||||
CURSOR_SETTINGS_URL => "https://www.cursor.com/settings"
|
||||
);
|
||||
|
||||
// Object type constants
|
||||
def_pub_const!(
|
||||
OBJECT_CHAT_COMPLETION => "chat.completion",
|
||||
OBJECT_CHAT_COMPLETION_CHUNK => "chat.completion.chunk"
|
||||
);
|
||||
|
||||
// def_pub_const!(
|
||||
// CURSOR_API2_STREAM_CHAT => "StreamChat",
|
||||
// CURSOR_API2_GET_USER_INFO => "GetUserInfo"
|
||||
// );
|
||||
|
||||
// Finish reason constants
|
||||
def_pub_const!(
|
||||
FINISH_REASON_STOP => "stop"
|
||||
);
|
||||
|
||||
// Error message constants
|
||||
def_pub_const!(
|
||||
ERR_INVALID_PATH => "无效的路径"
|
||||
);
|
||||
|
||||
// def_pub_const!(ERR_CHECKSUM_NO_GOOD => "checksum no good");
|
||||
|
@@ -1,10 +1,9 @@
|
||||
use super::constant::{
|
||||
COMMA, CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING,
|
||||
};
|
||||
use super::constant::{COMMA, CURSOR_API2_HOST, CURSOR_HOST, EMPTY_STRING};
|
||||
use crate::common::utils::{
|
||||
parse_ascii_char_from_env, parse_bool_from_env, parse_string_from_env, parse_usize_from_env,
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
use chrono::Local;
|
||||
use std::{path::PathBuf, sync::LazyLock};
|
||||
use tokio::sync::{Mutex, OnceCell};
|
||||
|
||||
macro_rules! def_pub_static {
|
||||
@@ -32,17 +31,15 @@ macro_rules! def_pub_static {
|
||||
|
||||
def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING);
|
||||
def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING);
|
||||
def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME);
|
||||
def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX));
|
||||
def_pub_static!(
|
||||
ROUTE_CHAT_PATH,
|
||||
format!("{}/v1/chat/completions", *ROUTE_PREFIX)
|
||||
);
|
||||
|
||||
pub static START_TIME: LazyLock<chrono::DateTime<chrono::Local>> =
|
||||
LazyLock::new(chrono::Local::now);
|
||||
pub static START_TIME: LazyLock<chrono::DateTime<Local>> = LazyLock::new(Local::now);
|
||||
|
||||
pub fn get_start_time() -> chrono::DateTime<chrono::Local> {
|
||||
pub fn get_start_time() -> chrono::DateTime<Local> {
|
||||
*START_TIME
|
||||
}
|
||||
|
||||
@@ -114,6 +111,12 @@ def_cursor_api_url!(
|
||||
"/aiserver.v1.AiService/StreamChatWeb"
|
||||
);
|
||||
|
||||
def_cursor_api_url!(
|
||||
CURSOR_API2_CHAT_MODELS_URL,
|
||||
CURSOR_API2_HOST,
|
||||
"/aiserver.v1.AiService/AvailableModels"
|
||||
);
|
||||
|
||||
def_cursor_api_url!(
|
||||
CURSOR_API2_STRIPE_URL,
|
||||
CURSOR_API2_HOST,
|
||||
@@ -124,11 +127,26 @@ def_cursor_api_url!(CURSOR_USAGE_API_URL, CURSOR_HOST, "/api/usage");
|
||||
|
||||
def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me");
|
||||
|
||||
pub(super) static LOGS_FILE_PATH: LazyLock<String> =
|
||||
LazyLock::new(|| parse_string_from_env("LOGS_FILE_PATH", "logs.bin"));
|
||||
static DATA_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let data_dir = parse_string_from_env("DATA_DIR", "data");
|
||||
let path = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe_path| exe_path.parent().map(|p| p.to_path_buf()))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(data_dir);
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(&path).expect("无法创建数据目录");
|
||||
}
|
||||
path
|
||||
});
|
||||
|
||||
pub(super) static PAGES_FILE_PATH: LazyLock<String> =
|
||||
LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin"));
|
||||
pub(super) static CONFIG_FILE_PATH: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| DATA_DIR.join("config.bin"));
|
||||
|
||||
pub(super) static LOGS_FILE_PATH: LazyLock<PathBuf> = LazyLock::new(|| DATA_DIR.join("logs.bin"));
|
||||
|
||||
pub(super) static TOKENS_FILE_PATH: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| DATA_DIR.join("tokens.bin"));
|
||||
|
||||
pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
|
||||
|
||||
@@ -157,14 +175,14 @@ pub(crate) async fn get_log_file() -> &'static Mutex<tokio::fs::File> {
|
||||
#[macro_export]
|
||||
macro_rules! debug_println {
|
||||
($($arg:tt)*) => {
|
||||
if *crate::app::lazy::DEBUG {
|
||||
if *$crate::app::lazy::DEBUG {
|
||||
let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let log_message = format!("{} - {}", time, format!($($arg)*));
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
|
||||
// 使用 tokio 的 spawn 在后台异步写入日志
|
||||
tokio::spawn(async move {
|
||||
let log_file = crate::app::lazy::get_log_file().await;
|
||||
let log_file = $crate::app::lazy::get_log_file().await;
|
||||
// 使用 MutexGuard 获取可变引用
|
||||
let mut file = log_file.lock().await;
|
||||
if let Err(err) = file.write_all(log_message.as_bytes()).await {
|
||||
@@ -183,6 +201,8 @@ macro_rules! debug_println {
|
||||
pub static REQUEST_LOGS_LIMIT: LazyLock<usize> =
|
||||
LazyLock::new(|| std::cmp::min(parse_usize_from_env("REQUEST_LOGS_LIMIT", 100), 2000));
|
||||
|
||||
pub static IS_UNLIMITED_REQUEST_LOGS: LazyLock<bool> = LazyLock::new(|| *REQUEST_LOGS_LIMIT == 0);
|
||||
|
||||
pub static SERVICE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||
let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30);
|
||||
u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30)
|
||||
|
511
src/app/model.rs
511
src/app/model.rs
@@ -1,30 +1,30 @@
|
||||
use crate::{
|
||||
app::constant::{
|
||||
EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH,
|
||||
ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
|
||||
ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH,
|
||||
},
|
||||
chat::model::Message,
|
||||
common::{
|
||||
client::rebuild_http_client,
|
||||
model::{userinfo::TokenProfile, ApiStatus},
|
||||
utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env},
|
||||
model::{ApiStatus, userinfo::TokenProfile},
|
||||
utils::{generate_checksum_with_repair, get_token_profile},
|
||||
},
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
use std::{collections::HashSet, fs::OpenOptions};
|
||||
|
||||
mod usage_check;
|
||||
pub use usage_check::UsageCheck;
|
||||
mod vision_ability;
|
||||
pub use vision_ability::VisionAbility;
|
||||
mod config;
|
||||
pub use config::AppConfig;
|
||||
mod proxies;
|
||||
pub use proxies::Proxies;
|
||||
mod build_key;
|
||||
pub use build_key::*;
|
||||
|
||||
use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS};
|
||||
use super::{
|
||||
constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS},
|
||||
lazy::{LOGS_FILE_PATH, TOKENS_FILE_PATH},
|
||||
};
|
||||
|
||||
// 页面内容类型枚举
|
||||
#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
@@ -44,52 +44,6 @@ impl Default for PageContent {
|
||||
}
|
||||
}
|
||||
|
||||
// 静态配置
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
vision_ability: VisionAbility,
|
||||
slow_pool: bool,
|
||||
allow_claude: bool,
|
||||
pages: Pages,
|
||||
usage_check: UsageCheck,
|
||||
dynamic_key: bool,
|
||||
share_token: String,
|
||||
is_share: bool,
|
||||
proxies: Proxies,
|
||||
web_refs: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
pub enum VisionAbility {
|
||||
#[serde(rename = "none", alias = "disabled")]
|
||||
None,
|
||||
#[serde(rename = "base64", alias = "base64-only")]
|
||||
Base64,
|
||||
#[serde(rename = "all", alias = "base64-http")]
|
||||
All,
|
||||
}
|
||||
|
||||
impl VisionAbility {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"none" | "disabled" => Self::None,
|
||||
"base64" | "base64-only" => Self::Base64,
|
||||
"all" | "base64-http" => Self::All,
|
||||
_ => Self::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, VisionAbility::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VisionAbility {
|
||||
fn default() -> Self {
|
||||
Self::Base64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
pub struct Pages {
|
||||
pub root_content: PageContent,
|
||||
@@ -104,232 +58,140 @@ pub struct Pages {
|
||||
pub build_key_content: PageContent,
|
||||
}
|
||||
|
||||
// 运行时状态
|
||||
pub struct AppState {
|
||||
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
pub struct TokenGroup {
|
||||
pub index: u16,
|
||||
pub name: String,
|
||||
pub tokens: Vec<TokenInfo>,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
// Token管理器
|
||||
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
pub struct TokenManager {
|
||||
pub tokens: Vec<TokenInfo>,
|
||||
pub tags: HashSet<String>, // 存储所有已使用的标签
|
||||
}
|
||||
|
||||
// 请求统计管理器
|
||||
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
pub struct RequestStatsManager {
|
||||
pub total_requests: u64,
|
||||
pub active_requests: u64,
|
||||
pub error_requests: u64,
|
||||
pub request_logs: Vec<RequestLog>,
|
||||
pub token_infos: Vec<TokenInfo>,
|
||||
}
|
||||
|
||||
// 全局配置实例
|
||||
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
|
||||
LazyLock::new(|| RwLock::new(AppConfig::default()));
|
||||
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
pub struct AppState {
|
||||
pub token_manager: TokenManager,
|
||||
pub request_manager: RequestStatsManager,
|
||||
}
|
||||
|
||||
macro_rules! config_methods {
|
||||
($($field:ident: $type:ty, $default:expr;)*) => {
|
||||
$(
|
||||
paste::paste! {
|
||||
pub fn [<get_ $field>]() -> $type
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
APP_CONFIG.read().$field
|
||||
}
|
||||
|
||||
pub fn [<update_ $field>](value: $type)
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != value {
|
||||
APP_CONFIG.write().$field = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn [<reset_ $field>]()
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
let default_value = $default;
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != default_value {
|
||||
APP_CONFIG.write().$field = default_value;
|
||||
}
|
||||
}
|
||||
impl TokenManager {
|
||||
pub fn new(tokens: Vec<TokenInfo>) -> Self {
|
||||
let mut tags = HashSet::new();
|
||||
for token in &tokens {
|
||||
if let Some(token_tags) = &token.tags {
|
||||
tags.extend(token_tags.iter().cloned());
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! config_methods_clone {
|
||||
($($field:ident: $type:ty, $default:expr;)*) => {
|
||||
$(
|
||||
paste::paste! {
|
||||
pub fn [<get_ $field>]() -> $type
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
APP_CONFIG.read().$field.clone()
|
||||
}
|
||||
Self { tokens, tags }
|
||||
}
|
||||
|
||||
pub fn [<update_ $field>](value: $type)
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != value {
|
||||
APP_CONFIG.write().$field = value;
|
||||
}
|
||||
}
|
||||
pub fn update_global_tags(&mut self, new_tags: &[String]) {
|
||||
// 将新标签添加到全局标签集合中
|
||||
self.tags.extend(new_tags.iter().cloned());
|
||||
}
|
||||
|
||||
pub fn [<reset_ $field>]()
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
let default_value = $default;
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != default_value {
|
||||
APP_CONFIG.write().$field = default_value;
|
||||
}
|
||||
}
|
||||
pub fn update_tokens_tags(
|
||||
&mut self,
|
||||
tokens: Vec<String>,
|
||||
new_tags: Vec<String>,
|
||||
) -> Result<(), &'static str> {
|
||||
// 创建tokens的HashSet用于快速查找
|
||||
let tokens_set: HashSet<_> = tokens.iter().collect();
|
||||
|
||||
// 更新指定tokens的标签
|
||||
for token_info in &mut self.tokens {
|
||||
if tokens_set.contains(&token_info.token) {
|
||||
token_info.tags = Some(new_tags.clone());
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn init() {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.vision_ability =
|
||||
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
|
||||
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
|
||||
config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false);
|
||||
config.usage_check =
|
||||
UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING));
|
||||
config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false);
|
||||
config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING);
|
||||
config.is_share = !config.share_token.is_empty();
|
||||
config.proxies = match std::env::var("PROXIES") {
|
||||
Ok(proxies) => Proxies::from_str(proxies.as_str()),
|
||||
Err(_) => Proxies::default(),
|
||||
// 更新全局标签集合
|
||||
self.tags = self
|
||||
.tokens
|
||||
.iter()
|
||||
.filter_map(|t| t.tags.clone())
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_tokens_by_tag(&self, tag: &str) -> Vec<&TokenInfo> {
|
||||
self.tokens
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
t.tags
|
||||
.as_ref()
|
||||
.is_some_and(|tags| tags.contains(&tag.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_checksum(&mut self) {
|
||||
for token_info in self.tokens.iter_mut() {
|
||||
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_tokens(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bytes = rkyv::to_bytes::<_, 256>(self)?;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&*TOKENS_FILE_PATH)?;
|
||||
|
||||
if bytes.len() > usize::MAX / 2 {
|
||||
return Err("Token数据过大".into());
|
||||
}
|
||||
|
||||
file.set_len(bytes.len() as u64)?;
|
||||
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
|
||||
mmap.copy_from_slice(&bytes);
|
||||
mmap.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_tokens() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let file = match OpenOptions::new().read(true).open(&*TOKENS_FILE_PATH) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(Self::new(Vec::new()));
|
||||
}
|
||||
Err(e) => return Err(Box::new(e)),
|
||||
};
|
||||
config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false)
|
||||
}
|
||||
|
||||
config_methods! {
|
||||
slow_pool: bool, false;
|
||||
allow_claude: bool, false;
|
||||
dynamic_key: bool, false;
|
||||
web_refs: bool, false;
|
||||
}
|
||||
|
||||
config_methods_clone! {
|
||||
vision_ability: VisionAbility, VisionAbility::default();
|
||||
usage_check: UsageCheck, UsageCheck::default();
|
||||
}
|
||||
|
||||
pub fn get_share_token() -> String {
|
||||
APP_CONFIG.read().share_token.clone()
|
||||
}
|
||||
|
||||
pub fn update_share_token(value: String) {
|
||||
let current = Self::get_share_token();
|
||||
if current != value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.share_token = value;
|
||||
config.is_share = !config.share_token.is_empty();
|
||||
if file.metadata()?.len() > usize::MAX as u64 {
|
||||
return Err("Token文件过大".into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_share_token() {
|
||||
let current = Self::get_share_token();
|
||||
if !current.is_empty() {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.share_token = String::new();
|
||||
config.is_share = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_proxies() -> Proxies {
|
||||
APP_CONFIG.read().proxies.clone()
|
||||
}
|
||||
|
||||
pub fn update_proxies(value: Proxies) {
|
||||
let current = Self::get_proxies();
|
||||
if current != value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.proxies = value;
|
||||
rebuild_http_client();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_proxies() {
|
||||
let default_value = Proxies::default();
|
||||
let current = Self::get_proxies();
|
||||
if current != default_value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.proxies = default_value;
|
||||
rebuild_http_client();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_page_content(path: &str) -> Option<PageContent> {
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()),
|
||||
ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()),
|
||||
ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()),
|
||||
ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()),
|
||||
ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()),
|
||||
ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()),
|
||||
ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()),
|
||||
ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()),
|
||||
ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()),
|
||||
ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
|
||||
let mut config = APP_CONFIG.write();
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => config.pages.root_content = content,
|
||||
ROUTE_LOGS_PATH => config.pages.logs_content = content,
|
||||
ROUTE_CONFIG_PATH => config.pages.config_content = content,
|
||||
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content,
|
||||
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
|
||||
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
|
||||
ROUTE_ABOUT_PATH => config.pages.about_content = content,
|
||||
ROUTE_README_PATH => config.pages.readme_content = content,
|
||||
ROUTE_API_PATH => config.pages.api_content = content,
|
||||
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content,
|
||||
_ => return Err(ERR_INVALID_PATH),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
|
||||
let mut config = APP_CONFIG.write();
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(),
|
||||
ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(),
|
||||
ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(),
|
||||
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(),
|
||||
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(),
|
||||
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
|
||||
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
|
||||
ROUTE_README_PATH => config.pages.readme_content = PageContent::default(),
|
||||
ROUTE_API_PATH => config.pages.api_content = PageContent::default(),
|
||||
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(),
|
||||
_ => return Err(ERR_INVALID_PATH),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_share() -> bool {
|
||||
APP_CONFIG.read().is_share
|
||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||
let archived = unsafe { rkyv::archived_root::<Self>(&mmap) };
|
||||
Ok(archived.deserialize(&mut rkyv::Infallible)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(token_infos: Vec<TokenInfo>) -> Self {
|
||||
// 尝试加载保存的日志
|
||||
let request_logs = tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current()
|
||||
.block_on(async { Self::load_saved_logs().await.unwrap_or_default() })
|
||||
});
|
||||
|
||||
impl RequestStatsManager {
|
||||
pub fn new(request_logs: Vec<RequestLog>) -> Self {
|
||||
Self {
|
||||
total_requests: request_logs.len() as u64,
|
||||
active_requests: 0,
|
||||
@@ -338,14 +200,96 @@ impl AppState {
|
||||
.filter(|log| matches!(log.status, LogStatus::Failed))
|
||||
.count() as u64,
|
||||
request_logs,
|
||||
token_infos,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_checksum(&mut self) {
|
||||
for token_info in self.token_infos.iter_mut() {
|
||||
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
|
||||
pub async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&*LOGS_FILE_PATH)?;
|
||||
|
||||
if bytes.len() > usize::MAX / 2 {
|
||||
return Err("日志数据过大".into());
|
||||
}
|
||||
|
||||
file.set_len(bytes.len() as u64)?;
|
||||
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
|
||||
mmap.copy_from_slice(&bytes);
|
||||
mmap.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_logs() -> Result<Vec<RequestLog>, Box<dyn std::error::Error>> {
|
||||
let file = match OpenOptions::new().read(true).open(&*LOGS_FILE_PATH) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Err(e) => return Err(Box::new(e)),
|
||||
};
|
||||
|
||||
if file.metadata()?.len() > usize::MAX as u64 {
|
||||
return Err("日志文件过大".into());
|
||||
}
|
||||
|
||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||
let archived = unsafe { rkyv::archived_root::<Vec<RequestLog>>(&mmap) };
|
||||
Ok(archived.deserialize(&mut rkyv::Infallible)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
// 尝试加载保存的数据
|
||||
let (request_logs, mut token_manager) = tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
let logs = RequestStatsManager::load_logs().await.unwrap_or_default();
|
||||
let token_manager = TokenManager::load_tokens()
|
||||
.await
|
||||
.unwrap_or_else(|_| TokenManager::new(Vec::new()));
|
||||
(logs, token_manager)
|
||||
})
|
||||
});
|
||||
|
||||
// 查询缺失的 token profiles
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
for token_info in token_manager.tokens.iter_mut() {
|
||||
if token_info.profile.is_none() {
|
||||
token_info.profile = get_token_profile(&token_info.token).await;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
token_manager,
|
||||
request_manager: RequestStatsManager::new(request_logs),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_state(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 并行保存 logs 和 tokens
|
||||
let (logs_result, tokens_result) = tokio::join!(
|
||||
self.request_manager.save_logs(),
|
||||
self.token_manager.save_tokens()
|
||||
);
|
||||
|
||||
logs_result?;
|
||||
tokens_result?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,12 +361,13 @@ pub struct ChatRequest {
|
||||
}
|
||||
|
||||
// 用于存储 token 信息
|
||||
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
#[derive(Clone, Serialize, Archive, RkyvSerialize, RkyvDeserialize)]
|
||||
pub struct TokenInfo {
|
||||
pub token: String,
|
||||
pub checksum: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<TokenProfile>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// TokenUpdateRequest 结构体
|
||||
@@ -431,6 +376,13 @@ pub struct TokenUpdateRequest {
|
||||
pub tokens: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenAddRequest {
|
||||
pub tokens: Vec<TokenAddRequestTokenInfo>,
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenAddRequestTokenInfo {
|
||||
pub token: String,
|
||||
@@ -484,3 +436,26 @@ pub struct TokensDeleteResponse {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub failed_tokens: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TokenInfoResponse {
|
||||
pub status: ApiStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tokens: Option<Vec<TokenInfo>>,
|
||||
pub tokens_count: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
// 标签相关的请求/响应结构体
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenTagsUpdateRequest {
|
||||
pub tokens: Vec<String>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TokenTagsResponse {
|
||||
pub status: ApiStatus,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS};
|
||||
use crate::{app::constant::COMMA, chat::constant::Models};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BuildKeyRequest {
|
||||
@@ -16,7 +16,7 @@ pub struct BuildKeyRequest {
|
||||
}
|
||||
pub struct UsageCheckModelConfig {
|
||||
pub model_type: UsageCheckModelType,
|
||||
pub model_ids: Vec<&'static str>,
|
||||
pub model_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for UsageCheckModelConfig {
|
||||
@@ -42,10 +42,7 @@ impl<'de> Deserialize<'de> for UsageCheckModelConfig {
|
||||
.split(COMMA)
|
||||
.filter_map(|model| {
|
||||
let model = model.trim();
|
||||
AVAILABLE_MODELS
|
||||
.iter()
|
||||
.find(|m| m.id == model)
|
||||
.map(|m| m.id)
|
||||
Models::find_id(model)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
@@ -1,69 +1,248 @@
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use rkyv::{archived_root, Deserialize as _};
|
||||
use std::fs::OpenOptions;
|
||||
use parking_lot::RwLock;
|
||||
use rkyv::{Deserialize as _, archived_root};
|
||||
use std::{fs::OpenOptions, sync::LazyLock};
|
||||
|
||||
use crate::app::lazy::{LOGS_FILE_PATH, PAGES_FILE_PATH};
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{
|
||||
EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH,
|
||||
ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
|
||||
ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH,
|
||||
},
|
||||
lazy::CONFIG_FILE_PATH,
|
||||
},
|
||||
common::{
|
||||
client::rebuild_http_client,
|
||||
utils::{parse_bool_from_env, parse_string_from_env},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{AppConfig, AppState, Pages, RequestLog, APP_CONFIG};
|
||||
use super::{PageContent, Pages, Proxies, UsageCheck, VisionAbility};
|
||||
|
||||
impl AppState {
|
||||
// 保存日志的方法
|
||||
pub(crate) async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 序列化日志
|
||||
let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?;
|
||||
// 静态配置
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
vision_ability: VisionAbility,
|
||||
slow_pool: bool,
|
||||
allow_claude: bool,
|
||||
pages: Pages,
|
||||
usage_check: UsageCheck,
|
||||
dynamic_key: bool,
|
||||
share_token: String,
|
||||
is_share: bool,
|
||||
proxies: Proxies,
|
||||
web_refs: bool,
|
||||
}
|
||||
|
||||
// 创建或打开文件
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(LOGS_FILE_PATH.as_str())?;
|
||||
// 全局配置实例
|
||||
static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
|
||||
LazyLock::new(|| RwLock::new(AppConfig::default()));
|
||||
|
||||
// 添加大小检查
|
||||
if bytes.len() > usize::MAX / 2 {
|
||||
return Err("日志数据过大".into());
|
||||
}
|
||||
macro_rules! config_methods {
|
||||
($($field:ident: $type:ty, $default:expr;)*) => {
|
||||
$(
|
||||
paste::paste! {
|
||||
pub fn [<get_ $field>]() -> $type
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
APP_CONFIG.read().$field
|
||||
}
|
||||
|
||||
// 设置文件大小
|
||||
file.set_len(bytes.len() as u64)?;
|
||||
pub fn [<update_ $field>](value: $type)
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != value {
|
||||
APP_CONFIG.write().$field = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建可写入的内存映射
|
||||
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
|
||||
|
||||
// 写入数据
|
||||
mmap.copy_from_slice(&bytes);
|
||||
|
||||
// 同步到磁盘
|
||||
mmap.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 加载日志的方法
|
||||
pub(super) async fn load_saved_logs() -> Result<Vec<RequestLog>, Box<dyn std::error::Error>> {
|
||||
let file = match OpenOptions::new().read(true).open(LOGS_FILE_PATH.as_str()) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(Vec::new());
|
||||
pub fn [<reset_ $field>]()
|
||||
where
|
||||
$type: Copy + PartialEq,
|
||||
{
|
||||
let default_value = $default;
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != default_value {
|
||||
APP_CONFIG.write().$field = default_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(Box::new(e)),
|
||||
};
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// 添加文件大小检查
|
||||
if file.metadata()?.len() > usize::MAX as u64 {
|
||||
return Err("日志文件过大".into());
|
||||
}
|
||||
macro_rules! config_methods_clone {
|
||||
($($field:ident: $type:ty, $default:expr;)*) => {
|
||||
$(
|
||||
paste::paste! {
|
||||
pub fn [<get_ $field>]() -> $type
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
APP_CONFIG.read().$field.clone()
|
||||
}
|
||||
|
||||
// 创建只读内存映射
|
||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||
pub fn [<update_ $field>](value: $type)
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != value {
|
||||
APP_CONFIG.write().$field = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证并反序列化数据
|
||||
let archived = unsafe { archived_root::<Vec<RequestLog>>(&mmap) };
|
||||
Ok(archived.deserialize(&mut rkyv::Infallible)?)
|
||||
}
|
||||
pub fn [<reset_ $field>]()
|
||||
where
|
||||
$type: Clone + PartialEq,
|
||||
{
|
||||
let default_value = $default;
|
||||
let current = Self::[<get_ $field>]();
|
||||
if current != default_value {
|
||||
APP_CONFIG.write().$field = default_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn init() {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.vision_ability =
|
||||
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
|
||||
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
|
||||
config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false);
|
||||
config.usage_check =
|
||||
UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING));
|
||||
config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false);
|
||||
config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING);
|
||||
config.is_share = !config.share_token.is_empty();
|
||||
config.proxies = match std::env::var("PROXIES") {
|
||||
Ok(proxies) => Proxies::from_str(proxies.as_str()),
|
||||
Err(_) => Proxies::default(),
|
||||
};
|
||||
config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false)
|
||||
}
|
||||
|
||||
config_methods! {
|
||||
slow_pool: bool, false;
|
||||
allow_claude: bool, false;
|
||||
dynamic_key: bool, false;
|
||||
web_refs: bool, false;
|
||||
}
|
||||
|
||||
config_methods_clone! {
|
||||
vision_ability: VisionAbility, VisionAbility::default();
|
||||
usage_check: UsageCheck, UsageCheck::default();
|
||||
}
|
||||
|
||||
pub fn get_share_token() -> String {
|
||||
APP_CONFIG.read().share_token.clone()
|
||||
}
|
||||
|
||||
pub fn update_share_token(value: String) {
|
||||
let current = Self::get_share_token();
|
||||
if current != value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.share_token = value;
|
||||
config.is_share = !config.share_token.is_empty();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_share_token() {
|
||||
let current = Self::get_share_token();
|
||||
if !current.is_empty() {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.share_token = String::new();
|
||||
config.is_share = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_proxies() -> Proxies {
|
||||
APP_CONFIG.read().proxies.clone()
|
||||
}
|
||||
|
||||
pub fn update_proxies(value: Proxies) {
|
||||
let current = Self::get_proxies();
|
||||
if current != value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.proxies = value;
|
||||
rebuild_http_client();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_proxies() {
|
||||
let default_value = Proxies::default();
|
||||
let current = Self::get_proxies();
|
||||
if current != default_value {
|
||||
let mut config = APP_CONFIG.write();
|
||||
config.proxies = default_value;
|
||||
rebuild_http_client();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_page_content(path: &str) -> Option<PageContent> {
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()),
|
||||
ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()),
|
||||
ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()),
|
||||
ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()),
|
||||
ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()),
|
||||
ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()),
|
||||
ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()),
|
||||
ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()),
|
||||
ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()),
|
||||
ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
|
||||
let mut config = APP_CONFIG.write();
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => config.pages.root_content = content,
|
||||
ROUTE_LOGS_PATH => config.pages.logs_content = content,
|
||||
ROUTE_CONFIG_PATH => config.pages.config_content = content,
|
||||
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content,
|
||||
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
|
||||
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
|
||||
ROUTE_ABOUT_PATH => config.pages.about_content = content,
|
||||
ROUTE_README_PATH => config.pages.readme_content = content,
|
||||
ROUTE_API_PATH => config.pages.api_content = content,
|
||||
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content,
|
||||
_ => return Err(ERR_INVALID_PATH),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
|
||||
let mut config = APP_CONFIG.write();
|
||||
match path {
|
||||
ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(),
|
||||
ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(),
|
||||
ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(),
|
||||
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(),
|
||||
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(),
|
||||
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
|
||||
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
|
||||
ROUTE_README_PATH => config.pages.readme_content = PageContent::default(),
|
||||
ROUTE_API_PATH => config.pages.api_content = PageContent::default(),
|
||||
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(),
|
||||
_ => return Err(ERR_INVALID_PATH),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_share() -> bool {
|
||||
APP_CONFIG.read().is_share
|
||||
}
|
||||
|
||||
pub fn save_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let pages = APP_CONFIG.read().pages.clone();
|
||||
let bytes = rkyv::to_bytes::<_, 256>(&pages)?;
|
||||
@@ -72,7 +251,8 @@ impl AppConfig {
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(PAGES_FILE_PATH.as_str())?;
|
||||
.truncate(true)
|
||||
.open(&*CONFIG_FILE_PATH)?;
|
||||
|
||||
// 添加大小检查
|
||||
if bytes.len() > usize::MAX / 2 {
|
||||
@@ -89,7 +269,7 @@ impl AppConfig {
|
||||
}
|
||||
|
||||
pub fn load_saved_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = match OpenOptions::new().read(true).open(PAGES_FILE_PATH.as_str()) {
|
||||
let file = match OpenOptions::new().read(true).open(&*CONFIG_FILE_PATH) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(());
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use reqwest::{Client, Proxy};
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde::{Serialize, Serializer};
|
||||
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||
|
||||
use crate::app::constant::COMMA_STRING;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
app::constant::{COMMA, COMMA_STRING},
|
||||
chat::{config::key_config, constant::AVAILABLE_MODELS},
|
||||
chat::{config::key_config, constant::Models},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||
@@ -10,7 +10,7 @@ pub enum UsageCheck {
|
||||
None,
|
||||
Default,
|
||||
All,
|
||||
Custom(Vec<&'static str>),
|
||||
Custom(Vec<String>),
|
||||
}
|
||||
|
||||
impl UsageCheck {
|
||||
@@ -21,10 +21,10 @@ impl UsageCheck {
|
||||
Type::Default | Type::Disabled => Self::None,
|
||||
Type::All => Self::All,
|
||||
Type::Custom => {
|
||||
let models: Vec<&'static str> = model
|
||||
let models: Vec<_> = model
|
||||
.model_ids
|
||||
.iter()
|
||||
.filter_map(|id| AVAILABLE_MODELS.iter().find(|m| m.id == id).map(|m| m.id))
|
||||
.filter_map(|id| Models::find_id(id))
|
||||
.collect();
|
||||
if models.is_empty() {
|
||||
Self::None
|
||||
@@ -119,14 +119,11 @@ impl<'de> Deserialize<'de> for UsageCheck {
|
||||
return Ok(UsageCheck::None);
|
||||
}
|
||||
|
||||
let models: Vec<&'static str> = list
|
||||
let models: Vec<_> = list
|
||||
.split(COMMA)
|
||||
.filter_map(|model| {
|
||||
let model = model.trim();
|
||||
AVAILABLE_MODELS
|
||||
.iter()
|
||||
.find(|m| m.id == model)
|
||||
.map(|m| m.id)
|
||||
Models::find_id(model)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -150,14 +147,11 @@ impl UsageCheck {
|
||||
if list.is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
let models: Vec<&'static str> = list
|
||||
let models: Vec<_> = list
|
||||
.split(COMMA)
|
||||
.filter_map(|model| {
|
||||
let model = model.trim();
|
||||
AVAILABLE_MODELS
|
||||
.iter()
|
||||
.find(|m| m.id == model)
|
||||
.map(|m| m.id)
|
||||
Models::find_id(model)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
32
src/app/model/vision_ability.rs
Normal file
32
src/app/model/vision_ability.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
pub enum VisionAbility {
|
||||
#[serde(rename = "none", alias = "disabled")]
|
||||
None,
|
||||
#[serde(rename = "base64", alias = "base64-only")]
|
||||
Base64,
|
||||
#[serde(rename = "all", alias = "base64-http")]
|
||||
All,
|
||||
}
|
||||
|
||||
impl VisionAbility {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"none" | "disabled" => Self::None,
|
||||
"base64" | "base64-only" => Self::Base64,
|
||||
"all" | "base64-http" => Self::All,
|
||||
_ => Self::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, VisionAbility::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VisionAbility {
|
||||
fn default() -> Self {
|
||||
Self::Base64
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use image::guess_format;
|
||||
use prost::Message as _;
|
||||
use rand::Rng as _;
|
||||
use reqwest::Client;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -10,17 +10,79 @@ use crate::{
|
||||
lazy::DEFAULT_INSTRUCTIONS,
|
||||
model::{AppConfig, VisionAbility},
|
||||
},
|
||||
common::client::HTTP_CLIENT,
|
||||
common::{client::HTTP_CLIENT, utils::encode_message},
|
||||
};
|
||||
|
||||
use super::{
|
||||
aiserver::v1::{
|
||||
conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
|
||||
AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest,
|
||||
ImageProto, ModelDetails, WebReference, conversation_message, image_proto,
|
||||
},
|
||||
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
|
||||
model::{Message, MessageContent, Role},
|
||||
};
|
||||
|
||||
fn parse_web_references(text: &str) -> Vec<WebReference> {
|
||||
let mut web_refs = Vec::new();
|
||||
let lines = text.lines().skip(1); // 跳过 "WebReferences:" 行
|
||||
|
||||
for line in lines {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// 跳过序号和空格
|
||||
let mut chars = line.chars();
|
||||
for c in chars.by_ref() {
|
||||
if c == '.' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let remaining = chars.as_str().trim_start();
|
||||
|
||||
// 解析 [title](url) 部分
|
||||
if !remaining.starts_with('[') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut title = String::new();
|
||||
let mut url = String::new();
|
||||
let mut chunk = String::new();
|
||||
let mut current = &mut title;
|
||||
let mut state = 0; // 0: title, 1: url, 2: chunk
|
||||
|
||||
let mut chars = remaining.chars();
|
||||
chars.next(); // 跳过 '['
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
match (state, c) {
|
||||
(0, ']') => {
|
||||
state = 1;
|
||||
if chars.next() != Some('(') {
|
||||
break;
|
||||
}
|
||||
current = &mut url;
|
||||
}
|
||||
(1, ')') => {
|
||||
state = 2;
|
||||
if chars.next() == Some('<') {
|
||||
current = &mut chunk;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(2, '>') => break,
|
||||
(_, c) => current.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
web_refs.push(WebReference { title, url, chunk });
|
||||
}
|
||||
|
||||
web_refs
|
||||
}
|
||||
|
||||
async fn process_chat_inputs(
|
||||
inputs: Vec<Message>,
|
||||
disable_vision: bool,
|
||||
@@ -96,6 +158,17 @@ async fn process_chat_inputs(
|
||||
is_agentic: false,
|
||||
file_diff_trajectories: vec![],
|
||||
conversation_summary: None,
|
||||
existed_subsequent_terminal_command: false,
|
||||
existed_previous_terminal_command: false,
|
||||
docs_references: vec![],
|
||||
web_references: vec![],
|
||||
git_context: None,
|
||||
attached_folders_list_dir_results: vec![],
|
||||
cached_conversation_summary: None,
|
||||
human_changes: vec![],
|
||||
attached_human_changes: false,
|
||||
summarized_composers: vec![],
|
||||
cursor_rules: vec![],
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
@@ -119,7 +192,7 @@ async fn process_chat_inputs(
|
||||
// 如果第一条是 assistant,插入空的 user 消息
|
||||
if chat_inputs
|
||||
.first()
|
||||
.map_or(false, |input| input.role == Role::Assistant)
|
||||
.is_some_and(|input| input.role == Role::Assistant)
|
||||
{
|
||||
chat_inputs.insert(
|
||||
0,
|
||||
@@ -153,7 +226,7 @@ async fn process_chat_inputs(
|
||||
// 确保最后一条是 user
|
||||
if chat_inputs
|
||||
.last()
|
||||
.map_or(false, |input| input.role == Role::Assistant)
|
||||
.is_some_and(|input| input.role == Role::Assistant)
|
||||
{
|
||||
chat_inputs.push(Message {
|
||||
role: Role::User,
|
||||
@@ -201,6 +274,21 @@ async fn process_chat_inputs(
|
||||
}
|
||||
};
|
||||
|
||||
let (text, web_references) =
|
||||
if input.role == Role::Assistant && text.starts_with("WebReferences:") {
|
||||
if let Some(pos) = text.find("\n\n") {
|
||||
let (web_refs_text, content_text) = text.split_at(pos);
|
||||
(
|
||||
content_text[2..].to_string(), // 跳过 "\n\n"
|
||||
parse_web_references(web_refs_text),
|
||||
)
|
||||
} else {
|
||||
(text, vec![])
|
||||
}
|
||||
} else {
|
||||
(text, vec![])
|
||||
};
|
||||
|
||||
messages.push(ConversationMessage {
|
||||
text,
|
||||
r#type: if input.role == Role::User {
|
||||
@@ -238,6 +326,17 @@ async fn process_chat_inputs(
|
||||
is_agentic: false,
|
||||
file_diff_trajectories: vec![],
|
||||
conversation_summary: None,
|
||||
existed_subsequent_terminal_command: false,
|
||||
existed_previous_terminal_command: false,
|
||||
docs_references: vec![],
|
||||
web_references,
|
||||
git_context: None,
|
||||
attached_folders_list_dir_results: vec![],
|
||||
cached_conversation_summary: None,
|
||||
human_changes: vec![],
|
||||
attached_human_changes: false,
|
||||
summarized_composers: vec![],
|
||||
cursor_rules: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,7 +345,7 @@ async fn process_chat_inputs(
|
||||
if last_msg.r#type == conversation_message::MessageType::Human as i32 {
|
||||
let text = &last_msg.text;
|
||||
let mut chars = text.chars().peekable();
|
||||
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '@' {
|
||||
let mut url = String::new();
|
||||
@@ -387,13 +486,7 @@ pub async fn encode_chat_message(
|
||||
is_search: bool,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 在进入异步操作前获取并释放锁
|
||||
let enable_slow_pool = {
|
||||
if enable_slow_pool {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let enable_slow_pool = { if enable_slow_pool { Some(true) } else { None } };
|
||||
|
||||
let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await;
|
||||
|
||||
@@ -401,19 +494,24 @@ pub async fn encode_chat_message(
|
||||
Some(ExplicitContext {
|
||||
context: instructions,
|
||||
repo_context: None,
|
||||
rules: vec![],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let base_uuid = rand::random::<u16>();
|
||||
let external_links = urls.into_iter().enumerate().map(|(i, url)| {
|
||||
let uuid = base_uuid.wrapping_add(i as u16);
|
||||
ChatExternalLink {
|
||||
url,
|
||||
uuid: uuid.to_string(),
|
||||
}
|
||||
}).collect();
|
||||
let base_uuid = rand::rng().random::<u16>();
|
||||
let external_links = urls
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, url)| {
|
||||
let uuid = base_uuid.wrapping_add(i as u16);
|
||||
ChatExternalLink {
|
||||
url,
|
||||
uuid: uuid.to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let chat = GetChatRequest {
|
||||
current_file: None,
|
||||
@@ -461,13 +559,9 @@ pub async fn encode_chat_message(
|
||||
is_composer: None,
|
||||
runnable_code_blocks: Some(false),
|
||||
should_cache: Some(false),
|
||||
allow_model_fallbacks: None,
|
||||
number_of_times_shown_fallback_model_warning: None,
|
||||
};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
chat.encode(&mut encoded)?;
|
||||
|
||||
let len_prefix = format!("{:010x}", encoded.len()).to_uppercase();
|
||||
let content = hex::encode_upper(&encoded);
|
||||
|
||||
Ok(hex::decode(len_prefix + &content)?)
|
||||
encode_message(&chat, true)
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));
|
||||
// include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));
|
||||
include!("v1/aiserver.v1.rs");
|
||||
use error_details::Error;
|
||||
|
||||
impl ErrorDetails {
|
||||
@@ -7,6 +8,7 @@ impl ErrorDetails {
|
||||
Ok(error) => match error {
|
||||
Error::Unspecified => 500,
|
||||
Error::BadApiKey
|
||||
| Error::BadUserApiKey
|
||||
| Error::InvalidAuthId
|
||||
| Error::AuthTokenNotFound
|
||||
| Error::AuthTokenExpired
|
||||
@@ -33,7 +35,9 @@ impl ErrorDetails {
|
||||
| Error::BadModelName
|
||||
| Error::SlashEditFileTooLong
|
||||
| Error::FileUnsupported
|
||||
| Error::ClaudeImageTooLarge => 400,
|
||||
| Error::ClaudeImageTooLarge
|
||||
| Error::ConversationTooLong => 400,
|
||||
Error::Timeout => 504,
|
||||
Error::Deprecated
|
||||
| Error::FreeUserUsageLimit
|
||||
| Error::ProUserUsageLimit
|
||||
|
4149
src/chat/aiserver/v1/aiserver.v1.rs
Normal file
4149
src/chat/aiserver/v1/aiserver.v1.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
144
src/chat/aiserver/v1/timestamp.proto
Normal file
144
src/chat/aiserver/v1/timestamp.proto
Normal file
@@ -0,0 +1,144 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "google.golang.org/protobuf/types/known/timestamppb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "TimestampProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// A Timestamp represents a point in time independent of any time zone or local
|
||||
// calendar, encoded as a count of seconds and fractions of seconds at
|
||||
// nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
// January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
// Gregorian calendar backwards to year one.
|
||||
//
|
||||
// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||
// second table is needed for interpretation, using a [24-hour linear
|
||||
// smear](https://developers.google.com/time/smear).
|
||||
//
|
||||
// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||
// restricting to that range, we ensure that we can convert to and from [RFC
|
||||
// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// Example 1: Compute Timestamp from POSIX `time()`.
|
||||
//
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds(time(NULL));
|
||||
// timestamp.set_nanos(0);
|
||||
//
|
||||
// Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||
//
|
||||
// struct timeval tv;
|
||||
// gettimeofday(&tv, NULL);
|
||||
//
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds(tv.tv_sec);
|
||||
// timestamp.set_nanos(tv.tv_usec * 1000);
|
||||
//
|
||||
// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
//
|
||||
// FILETIME ft;
|
||||
// GetSystemTimeAsFileTime(&ft);
|
||||
// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||
//
|
||||
// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||
// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||
// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||
//
|
||||
// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
//
|
||||
// long millis = System.currentTimeMillis();
|
||||
//
|
||||
// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||
// .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||
//
|
||||
// Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||
//
|
||||
// Instant now = Instant.now();
|
||||
//
|
||||
// Timestamp timestamp =
|
||||
// Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||
// .setNanos(now.getNano()).build();
|
||||
//
|
||||
// Example 6: Compute Timestamp from current time in Python.
|
||||
//
|
||||
// timestamp = Timestamp()
|
||||
// timestamp.GetCurrentTime()
|
||||
//
|
||||
// # JSON Mapping
|
||||
//
|
||||
// In JSON format, the Timestamp type is encoded as a string in the
|
||||
// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||
// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||
// where {year} is always expressed using four digits while {month}, {day},
|
||||
// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||
// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||
// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||
// is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||
// "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||
// able to accept both UTC and other timezones (as indicated by an offset).
|
||||
//
|
||||
// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||
// 01:30 UTC on January 15, 2017.
|
||||
//
|
||||
// In JavaScript, one can convert a Date object to this format using the
|
||||
// standard
|
||||
// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
// method. In Python, a standard `datetime.datetime` object can be converted
|
||||
// to this format using
|
||||
// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||
// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||
// the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
|
||||
// ) to obtain a formatter capable of generating timestamps in this format.
|
||||
//
|
||||
message Timestamp {
|
||||
// Represents seconds of UTC time since Unix epoch
|
||||
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||
// 9999-12-31T23:59:59Z inclusive.
|
||||
int64 seconds = 1;
|
||||
|
||||
// Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
// second values with fractions must still have non-negative nanos values
|
||||
// that count forward in time. Must be from 0 to 999,999,999
|
||||
// inclusive.
|
||||
int32 nanos = 2;
|
||||
}
|
@@ -1,183 +1,205 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::{sync::Arc, time::{Duration, Instant}};
|
||||
|
||||
use super::model::Model;
|
||||
|
||||
macro_rules! def_pub_const {
|
||||
// 单个常量定义分支
|
||||
($name:ident, $value:expr) => {
|
||||
pub const $name: &'static str = $value;
|
||||
};
|
||||
}
|
||||
def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF");
|
||||
def_pub_const!(
|
||||
ERR_UNSUPPORTED_IMAGE_FORMAT,
|
||||
"不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF"
|
||||
);
|
||||
def_pub_const!(ERR_NODATA, "No data");
|
||||
|
||||
const MODEL_OBJECT: &str = "model";
|
||||
const CREATED: &i64 = &1706659200;
|
||||
|
||||
def_pub_const!(ANTHROPIC, "anthropic");
|
||||
def_pub_const!(CURSOR, "cursor");
|
||||
def_pub_const!(GOOGLE, "google");
|
||||
def_pub_const!(OPENAI, "openai");
|
||||
def_pub_const!(DEEPSEEK, "deepseek");
|
||||
|
||||
def_pub_const!(CLAUDE_3_5_SONNET, "claude-3.5-sonnet");
|
||||
def_pub_const!(GPT_4, "gpt-4");
|
||||
def_pub_const!(GPT_4O, "gpt-4o");
|
||||
def_pub_const!(CLAUDE_3_OPUS, "claude-3-opus");
|
||||
def_pub_const!(CURSOR_FAST, "cursor-fast");
|
||||
def_pub_const!(CURSOR_SMALL, "cursor-small");
|
||||
def_pub_const!(GPT_3_5_TURBO, "gpt-3.5-turbo");
|
||||
def_pub_const!(GPT_4_TURBO_2024_04_09, "gpt-4-turbo-2024-04-09");
|
||||
def_pub_const!(GPT_4O_128K, "gpt-4o-128k");
|
||||
def_pub_const!(GEMINI_1_5_FLASH_500K, "gemini-1.5-flash-500k");
|
||||
def_pub_const!(CLAUDE_3_HAIKU_200K, "claude-3-haiku-200k");
|
||||
def_pub_const!(CLAUDE_3_5_SONNET_200K, "claude-3-5-sonnet-200k");
|
||||
def_pub_const!(CLAUDE_3_5_SONNET_20241022, "claude-3-5-sonnet-20241022");
|
||||
def_pub_const!(GPT_4O_MINI, "gpt-4o-mini");
|
||||
def_pub_const!(O1_MINI, "o1-mini");
|
||||
def_pub_const!(O1_PREVIEW, "o1-preview");
|
||||
def_pub_const!(O1, "o1");
|
||||
def_pub_const!(CLAUDE_3_5_HAIKU, "claude-3.5-haiku");
|
||||
def_pub_const!(GEMINI_EXP_1206, "gemini-exp-1206");
|
||||
def_pub_const!(
|
||||
GEMINI_2_0_FLASH_THINKING_EXP,
|
||||
"gemini-2.0-flash-thinking-exp"
|
||||
);
|
||||
def_pub_const!(GEMINI_2_0_FLASH_EXP, "gemini-2.0-flash-exp");
|
||||
def_pub_const!(DEEPSEEK_V3, "deepseek-v3");
|
||||
def_pub_const!(DEEPSEEK_R1, "deepseek-r1");
|
||||
|
||||
// #[derive(Clone, PartialEq, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
|
||||
// pub enum ModelType {
|
||||
// Claude35Sonnet,
|
||||
// Gpt4,
|
||||
// Gpt4o,
|
||||
// Claude3Opus,
|
||||
// CursorFast,
|
||||
// CursorSmall,
|
||||
// Gpt35Turbo,
|
||||
// Gpt4Turbo202404,
|
||||
// Gpt4o128k,
|
||||
// Gemini15Flash500k,
|
||||
// Claude3Haiku200k,
|
||||
// Claude35Sonnet200k,
|
||||
// Claude35Sonnet20241022,
|
||||
// Gpt4oMini,
|
||||
// O1Mini,
|
||||
// O1Preview,
|
||||
// O1,
|
||||
// Claude35Haiku,
|
||||
// GeminiExp1206,
|
||||
// Gemini20FlashThinkingExp,
|
||||
// Gemini20FlashExp,
|
||||
// DeepseekV3,
|
||||
// DeepseekR1,
|
||||
// }
|
||||
|
||||
macro_rules! create_model {
|
||||
($($id:expr, $owner:expr),* $(,)?) => {
|
||||
pub const AVAILABLE_MODELS: [Model; count!($( ($id, $owner) )*)] = [
|
||||
$(
|
||||
Model {
|
||||
id: $id,
|
||||
created: CREATED,
|
||||
object: MODEL_OBJECT,
|
||||
owned_by: $owner,
|
||||
},
|
||||
)*
|
||||
];
|
||||
// 批量定义分支
|
||||
($($name:ident => $value:expr),+ $(,)?) => {
|
||||
$(
|
||||
pub const $name: &'static str = $value;
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! count {
|
||||
() => (0);
|
||||
(($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
|
||||
// 错误信息
|
||||
def_pub_const!(
|
||||
ERR_UNSUPPORTED_GIF => "不支持动态 GIF",
|
||||
ERR_UNSUPPORTED_IMAGE_FORMAT => "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF",
|
||||
ERR_NODATA => "No data",
|
||||
);
|
||||
|
||||
// 系统常量
|
||||
pub const MODEL_OBJECT: &str = "model";
|
||||
pub const CREATED: &i64 = &1706659200;
|
||||
|
||||
// AI 服务商
|
||||
def_pub_const!(
|
||||
ANTHROPIC => "anthropic",
|
||||
CURSOR => "cursor",
|
||||
GOOGLE => "google",
|
||||
OPENAI => "openai",
|
||||
DEEPSEEK => "deepseek",
|
||||
XAI => "xai",
|
||||
UNKNOWN => "unknown",
|
||||
);
|
||||
|
||||
// AI 模型
|
||||
def_pub_const!(
|
||||
// Anthropic 模型
|
||||
CLAUDE_3_OPUS => "claude-3-opus",
|
||||
CLAUDE_3_5_SONNET => "claude-3.5-sonnet",
|
||||
CLAUDE_3_HAIKU_200K => "claude-3-haiku-200k",
|
||||
CLAUDE_3_5_SONNET_200K => "claude-3-5-sonnet-200k",
|
||||
CLAUDE_3_5_SONNET_20241022 => "claude-3-5-sonnet-20241022",
|
||||
CLAUDE_3_5_HAIKU => "claude-3.5-haiku",
|
||||
|
||||
// OpenAI 模型
|
||||
GPT_4 => "gpt-4",
|
||||
GPT_4O => "gpt-4o",
|
||||
GPT_3_5_TURBO => "gpt-3.5-turbo",
|
||||
GPT_4_TURBO_2024_04_09 => "gpt-4-turbo-2024-04-09",
|
||||
GPT_4O_128K => "gpt-4o-128k",
|
||||
GPT_4O_MINI => "gpt-4o-mini",
|
||||
O1_MINI => "o1-mini",
|
||||
O1_PREVIEW => "o1-preview",
|
||||
O1 => "o1",
|
||||
O3_MINI => "o3-mini",
|
||||
|
||||
// Cursor 模型
|
||||
CURSOR_FAST => "cursor-fast",
|
||||
CURSOR_SMALL => "cursor-small",
|
||||
|
||||
// Google 模型
|
||||
GEMINI_1_5_FLASH_500K => "gemini-1.5-flash-500k",
|
||||
GEMINI_EXP_1206 => "gemini-exp-1206",
|
||||
GEMINI_2_0_PRO_EXP => "gemini-2.0-pro-exp",
|
||||
GEMINI_2_0_FLASH_THINKING_EXP => "gemini-2.0-flash-thinking-exp",
|
||||
GEMINI_2_0_FLASH => "gemini-2.0-flash",
|
||||
|
||||
// Deepseek 模型
|
||||
DEEPSEEK_V3 => "deepseek-v3",
|
||||
DEEPSEEK_R1 => "deepseek-r1",
|
||||
|
||||
// XAI 模型
|
||||
GROK_2 => "grok-2",
|
||||
);
|
||||
|
||||
macro_rules! create_models {
|
||||
($($model:expr => $owner:expr),* $(,)?) => {
|
||||
static INSTANCE: std::sync::LazyLock<RwLock<Models>> = std::sync::LazyLock::new(|| {
|
||||
RwLock::new(Models {
|
||||
models: Arc::new(vec![
|
||||
$(
|
||||
Model {
|
||||
id: $model.into(),
|
||||
created: CREATED,
|
||||
object: MODEL_OBJECT,
|
||||
owned_by: $owner,
|
||||
},
|
||||
)*
|
||||
]),
|
||||
last_update: Instant::now() - Duration::from_secs(30 * 60),
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// impl ModelType {
|
||||
// pub fn as_str_name(&self) -> &'static str {
|
||||
// match self {
|
||||
// ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET,
|
||||
// ModelType::Gpt4 => GPT_4,
|
||||
// ModelType::Gpt4o => GPT_4O,
|
||||
// ModelType::Claude3Opus => CLAUDE_3_OPUS,
|
||||
// ModelType::CursorFast => CURSOR_FAST,
|
||||
// ModelType::CursorSmall => CURSOR_SMALL,
|
||||
// ModelType::Gpt35Turbo => GPT_3_5_TURBO,
|
||||
// ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09,
|
||||
// ModelType::Gpt4o128k => GPT_4O_128K,
|
||||
// ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K,
|
||||
// ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K,
|
||||
// ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K,
|
||||
// ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022,
|
||||
// ModelType::Gpt4oMini => GPT_4O_MINI,
|
||||
// ModelType::O1Mini => O1_MINI,
|
||||
// ModelType::O1Preview => O1_PREVIEW,
|
||||
// ModelType::O1 => O1,
|
||||
// ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU,
|
||||
// ModelType::GeminiExp1206 => GEMINI_EXP_1206,
|
||||
// ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP,
|
||||
// ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP,
|
||||
// ModelType::DeepseekV3 => DEEPSEEK_V3,
|
||||
// ModelType::DeepseekR1 => DEEPSEEK_R1,
|
||||
// }
|
||||
// }
|
||||
pub struct Models {
|
||||
pub models: Arc<Vec<Model>>,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
// pub fn from_str_name(id :&str) -> Option<ModelType> {
|
||||
// match id {
|
||||
// CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet),
|
||||
// GPT_4 => Some(ModelType::Gpt4),
|
||||
// GPT_4O => Some(ModelType::Gpt4o),
|
||||
// CLAUDE_3_OPUS => Some(ModelType::Claude3Opus),
|
||||
// CURSOR_FAST => Some(ModelType::CursorFast),
|
||||
// CURSOR_SMALL => Some(ModelType::CursorSmall),
|
||||
// GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo),
|
||||
// GPT_4_TURBO_2024_04_09 => Some(ModelType::Gpt4Turbo202404),
|
||||
// GPT_4O_128K => Some(ModelType::Gpt4o128k),
|
||||
// GEMINI_1_5_FLASH_500K => Some(ModelType::Gemini15Flash500k),
|
||||
// CLAUDE_3_HAIKU_200K => Some(ModelType::Claude3Haiku200k),
|
||||
// CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k),
|
||||
// CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022),
|
||||
// GPT_4O_MINI => Some(ModelType::Gpt4oMini),
|
||||
// O1_MINI => Some(ModelType::O1Mini),
|
||||
// O1_PREVIEW => Some(ModelType::O1Preview),
|
||||
// O1 => Some(ModelType::O1),
|
||||
// CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku),
|
||||
// GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206),
|
||||
// GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp),
|
||||
// GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp),
|
||||
// DEEPSEEK_V3 => Some(ModelType::DeepseekV3),
|
||||
// DEEPSEEK_R1 => Some(ModelType::DeepseekR1),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
impl Models {
|
||||
// 返回读锁
|
||||
pub fn read() -> parking_lot::RwLockReadGuard<'static, Models> {
|
||||
INSTANCE.read()
|
||||
}
|
||||
|
||||
// 返回 Arc 的克隆
|
||||
pub fn to_arc() -> Arc<Vec<Model>> {
|
||||
INSTANCE.read().models.clone()
|
||||
}
|
||||
|
||||
// 克隆所有模型
|
||||
// pub fn cloned() -> Vec<Model> {
|
||||
// INSTANCE.read().models.as_ref().clone()
|
||||
// }
|
||||
|
||||
// 检查模型是否存在
|
||||
pub fn exists(model_id: &str) -> bool {
|
||||
Self::read().models.iter().any(|m| m.id == model_id)
|
||||
}
|
||||
|
||||
// 查找模型并返回其 ID
|
||||
pub fn find_id(model: &str) -> Option<String> {
|
||||
Self::read()
|
||||
.models
|
||||
.iter()
|
||||
.find(|m| m.id == model)
|
||||
.map(|m| m.id.clone())
|
||||
}
|
||||
|
||||
// 返回所有模型 ID 的列表
|
||||
pub fn ids() -> Vec<String> {
|
||||
Self::read()
|
||||
.models
|
||||
.iter()
|
||||
.map(|m| m.id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 写入方法
|
||||
pub fn update(new_models: Vec<Model>) -> Result<(), &'static str> {
|
||||
if new_models.is_empty() {
|
||||
return Err("Models list cannot be empty");
|
||||
}
|
||||
|
||||
let mut data = INSTANCE.write();
|
||||
|
||||
// 检查时间间隔(30分钟)
|
||||
if data.last_update.elapsed() < Duration::from_secs(30 * 60) {
|
||||
return Err("Cannot update models more frequently than every 30 minutes");
|
||||
}
|
||||
|
||||
// 检查内容是否有变化
|
||||
if *data.models == new_models {
|
||||
return Err("No changes in models");
|
||||
}
|
||||
|
||||
// 更新数据和时间戳
|
||||
data.models = Arc::new(new_models);
|
||||
data.last_update = Instant::now();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// macro_rules! count {
|
||||
// () => (0);
|
||||
// (($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
|
||||
// }
|
||||
|
||||
create_model!(
|
||||
CLAUDE_3_5_SONNET, ANTHROPIC,
|
||||
GPT_4, OPENAI,
|
||||
GPT_4O, OPENAI,
|
||||
CLAUDE_3_OPUS, ANTHROPIC,
|
||||
CURSOR_FAST, CURSOR,
|
||||
CURSOR_SMALL, CURSOR,
|
||||
GPT_3_5_TURBO, OPENAI,
|
||||
GPT_4_TURBO_2024_04_09, OPENAI,
|
||||
GPT_4O_128K, OPENAI,
|
||||
GEMINI_1_5_FLASH_500K, GOOGLE,
|
||||
CLAUDE_3_HAIKU_200K, ANTHROPIC,
|
||||
CLAUDE_3_5_SONNET_200K, ANTHROPIC,
|
||||
CLAUDE_3_5_SONNET_20241022, ANTHROPIC,
|
||||
GPT_4O_MINI, OPENAI,
|
||||
O1_MINI, OPENAI,
|
||||
O1_PREVIEW, OPENAI,
|
||||
O1, OPENAI,
|
||||
CLAUDE_3_5_HAIKU, ANTHROPIC,
|
||||
GEMINI_EXP_1206, GOOGLE,
|
||||
GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE,
|
||||
GEMINI_2_0_FLASH_EXP, GOOGLE,
|
||||
DEEPSEEK_V3, DEEPSEEK,
|
||||
DEEPSEEK_R1, DEEPSEEK,
|
||||
create_models!(
|
||||
CLAUDE_3_5_SONNET => ANTHROPIC,
|
||||
GPT_4 => OPENAI,
|
||||
GPT_4O => OPENAI,
|
||||
CLAUDE_3_OPUS => ANTHROPIC,
|
||||
CURSOR_FAST => CURSOR,
|
||||
CURSOR_SMALL => CURSOR,
|
||||
GPT_3_5_TURBO => OPENAI,
|
||||
GPT_4_TURBO_2024_04_09 => OPENAI,
|
||||
GPT_4O_128K => OPENAI,
|
||||
GEMINI_1_5_FLASH_500K => GOOGLE,
|
||||
CLAUDE_3_HAIKU_200K => ANTHROPIC,
|
||||
CLAUDE_3_5_SONNET_200K => ANTHROPIC,
|
||||
GPT_4O_MINI => OPENAI,
|
||||
O1_MINI => OPENAI,
|
||||
O1_PREVIEW => OPENAI,
|
||||
O1 => OPENAI,
|
||||
CLAUDE_3_5_HAIKU => ANTHROPIC,
|
||||
GEMINI_2_0_PRO_EXP => GOOGLE,
|
||||
GEMINI_2_0_FLASH_THINKING_EXP => GOOGLE,
|
||||
GEMINI_2_0_FLASH => GOOGLE,
|
||||
DEEPSEEK_V3 => DEEPSEEK,
|
||||
DEEPSEEK_R1 => DEEPSEEK,
|
||||
O3_MINI => OPENAI,
|
||||
GROK_2 => XAI,
|
||||
);
|
||||
|
||||
pub const USAGE_CHECK_MODELS: [&str; 11] = [
|
||||
@@ -200,5 +222,3 @@ pub const LONG_CONTEXT_MODELS: [&str; 4] = [
|
||||
CLAUDE_3_HAIKU_200K,
|
||||
CLAUDE_3_5_SONNET_200K,
|
||||
];
|
||||
|
||||
// include!("constant/models.rs");
|
||||
|
@@ -1,118 +0,0 @@
|
||||
pub struct DefaultModel {
|
||||
pub default_on: bool,
|
||||
pub is_long_context_only: Option<bool>,
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
pub const AVAILABLE_MODELS2: [DefaultModel; 22] = [
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(false),
|
||||
name: CLAUDE_3_5_SONNET,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GPT_4,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: None,
|
||||
name: GPT_4O,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: CLAUDE_3_OPUS,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: CURSOR_FAST,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: CURSOR_SMALL,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GPT_3_5_TURBO,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GPT_4_TURBO_2024_04_09,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(true),
|
||||
name: GPT_4O_128K,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(true),
|
||||
name: GEMINI_1_5_FLASH_500K,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(true),
|
||||
name: CLAUDE_3_HAIKU_200K,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(true),
|
||||
name: CLAUDE_3_5_SONNET_200K,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: Some(false),
|
||||
name: CLAUDE_3_5_SONNET_20241022,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(false),
|
||||
name: GPT_4O_MINI,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(false),
|
||||
name: O1_MINI,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(false),
|
||||
name: O1_PREVIEW,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: true,
|
||||
is_long_context_only: Some(false),
|
||||
name: O1,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: Some(false),
|
||||
name: CLAUDE_3_5_HAIKU,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GEMINI_EXP_1206,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GEMINI_2_0_FLASH_THINKING_EXP,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: GEMINI_2_0_FLASH_EXP,
|
||||
},
|
||||
DefaultModel {
|
||||
default_on: false,
|
||||
is_long_context_only: None,
|
||||
name: DEEPSEEK_V3,
|
||||
},
|
||||
];
|
@@ -1,6 +1,6 @@
|
||||
use super::aiserver::v1::ErrorDetails;
|
||||
use crate::common::model::{ApiStatus, ErrorResponse as CommonErrorResponse};
|
||||
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD};
|
||||
use prost::Message as _;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -42,7 +42,7 @@ pub struct ErrorDetail {
|
||||
// }
|
||||
|
||||
impl ChatError {
|
||||
pub fn to_error_response(self) -> ErrorResponse {
|
||||
pub fn into_error_response(self) -> ErrorResponse {
|
||||
if self.error.details.is_empty() {
|
||||
return ErrorResponse {
|
||||
status: 500,
|
||||
@@ -108,7 +108,7 @@ impl ErrorResponse {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_common(self) -> CommonErrorResponse {
|
||||
pub fn into_common(self) -> CommonErrorResponse {
|
||||
CommonErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: Some(self.status),
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::app::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{header::AUTHORIZATION, Request, StatusCode},
|
||||
http::{Request, StatusCode, header::AUTHORIZATION},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
|
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -80,22 +82,28 @@ pub struct Usage {
|
||||
// 模型定义
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Model {
|
||||
pub id: &'static str,
|
||||
pub id: String,
|
||||
pub created: &'static i64,
|
||||
pub object: &'static str,
|
||||
pub owned_by: &'static str,
|
||||
}
|
||||
|
||||
use super::constant::USAGE_CHECK_MODELS;
|
||||
impl PartialEq for Model {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
use super::constant::{Models, USAGE_CHECK_MODELS};
|
||||
use crate::app::model::{AppConfig, UsageCheck};
|
||||
|
||||
impl Model {
|
||||
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {
|
||||
pub fn is_usage_check(model_id: &String, usage_check: Option<UsageCheck>) -> bool {
|
||||
match usage_check.unwrap_or(AppConfig::get_usage_check()) {
|
||||
UsageCheck::None => false,
|
||||
UsageCheck::Default => USAGE_CHECK_MODELS.contains(&self.id),
|
||||
UsageCheck::Default => USAGE_CHECK_MODELS.contains(&model_id.as_str()),
|
||||
UsageCheck::All => true,
|
||||
UsageCheck::Custom(models) => models.contains(&self.id),
|
||||
UsageCheck::Custom(models) => models.contains(model_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,5 +111,18 @@ impl Model {
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelsResponse {
|
||||
pub object: &'static str,
|
||||
pub data: &'static [Model],
|
||||
pub data: Arc<Vec<Model>>,
|
||||
}
|
||||
|
||||
impl ModelsResponse {
|
||||
pub(super) fn new(data: Arc<Vec<Model>>) -> Self {
|
||||
Self {
|
||||
object: "list",
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_default_models() -> Self {
|
||||
Self::new(Models::to_arc())
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,15 @@ mod logs;
|
||||
pub use logs::{handle_logs, handle_logs_post};
|
||||
mod health;
|
||||
pub use health::{handle_health, handle_root};
|
||||
mod token;
|
||||
pub use token::{handle_basic_calibration, handle_tokens_page};
|
||||
mod tokens;
|
||||
pub use tokens::{
|
||||
handle_add_tokens, handle_basic_calibration, handle_delete_tokens, handle_get_checksum,
|
||||
handle_get_hash, handle_get_timestamp_header, handle_get_tokens, handle_reload_tokens,
|
||||
handle_tokens_page, handle_update_tokens,
|
||||
handle_add_tokens, handle_delete_tokens, handle_get_tokens, handle_update_token_tags,
|
||||
handle_update_tokens,
|
||||
};
|
||||
mod checksum;
|
||||
pub use checksum::{handle_get_checksum, handle_get_hash, handle_get_timestamp_header};
|
||||
mod profile;
|
||||
pub use profile::handle_user_info;
|
||||
mod config;
|
||||
|
@@ -1,26 +1,29 @@
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
|
||||
use crate::{
|
||||
AppConfig, PageContent,
|
||||
app::constant::{
|
||||
CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH,
|
||||
},
|
||||
AppConfig, PageContent,
|
||||
};
|
||||
|
||||
pub async fn handle_api_page() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/api.min.html").to_string())
|
||||
.body(Body::from(include_str!("../../../static/api.min.html")))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
59
src/chat/route/checksum.rs
Normal file
59
src/chat/route/checksum.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use axum::{
|
||||
extract::Query,
|
||||
http::{HeaderMap, header::CONTENT_TYPE},
|
||||
response::{IntoResponse as _, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
app::constant::CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8,
|
||||
common::utils::{
|
||||
generate_checksum_with_default, generate_checksum_with_repair, generate_hash,
|
||||
generate_timestamp_header,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn handle_get_hash() -> Response {
|
||||
let hash = generate_hash();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, hash).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChecksumQuery {
|
||||
#[serde(default)]
|
||||
pub checksum: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_get_checksum(Query(query): Query<ChecksumQuery>) -> Response {
|
||||
let checksum = match query.checksum {
|
||||
None => generate_checksum_with_default(),
|
||||
Some(checksum) => generate_checksum_with_repair(&checksum),
|
||||
};
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, checksum).into_response()
|
||||
}
|
||||
|
||||
pub async fn handle_get_timestamp_header() -> Response {
|
||||
let timestamp_header = generate_timestamp_header();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, timestamp_header).into_response()
|
||||
}
|
@@ -1,30 +1,33 @@
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{
|
||||
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_CSS_WITH_UTF8, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_JS_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH
|
||||
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_CSS_WITH_UTF8,
|
||||
CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_JS_WITH_UTF8,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH, ROUTE_BUILD_KEY_PATH,
|
||||
ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH,
|
||||
},
|
||||
lazy::{AUTH_TOKEN, KEY_PREFIX},
|
||||
model::{AppConfig, BuildKeyRequest, BuildKeyResponse, PageContent, UsageCheckModelType},
|
||||
},
|
||||
chat::config::{key_config, KeyConfig},
|
||||
chat::config::{KeyConfig, key_config},
|
||||
common::utils::{to_base64, token_to_tokeninfo},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::Path,
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE, LOCATION},
|
||||
HeaderMap, StatusCode,
|
||||
header::{AUTHORIZATION, CONTENT_TYPE, LOCATION},
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use prost::Message as _;
|
||||
|
||||
pub async fn handle_env_example() -> impl IntoResponse {
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(include_str!("../../../.env.example").to_string())
|
||||
.body(Body::from(include_str!("../../../.env.example")))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -33,15 +36,15 @@ pub async fn handle_config_page() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_CONFIG_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/config.min.html").to_string())
|
||||
.body(Body::from(include_str!("../../../static/config.min.html")))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -52,11 +55,13 @@ pub async fn handle_static(Path(path): Path<String>) -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_SHARED_STYLES_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8)
|
||||
.body(include_str!("../../../static/shared-styles.min.css").to_string())
|
||||
.body(Body::from(include_str!(
|
||||
"../../../static/shared-styles.min.css"
|
||||
)))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) | PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -64,17 +69,19 @@ pub async fn handle_static(Path(path): Path<String>) -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_SHARED_JS_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8)
|
||||
.body(include_str!("../../../static/shared.min.js").to_string())
|
||||
.body(Body::from(
|
||||
include_str!("../../../static/shared.min.js").to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) | PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body("Not found".to_string())
|
||||
.body(Body::from("Not found"))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -83,15 +90,15 @@ pub async fn handle_readme() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/readme.min.html").to_string())
|
||||
.body(Body::from(include_str!("../../../static/readme.min.html")))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -105,11 +112,11 @@ pub async fn handle_about() -> impl IntoResponse {
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -118,15 +125,17 @@ pub async fn handle_build_key_page() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_BUILD_KEY_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/build_key.min.html").to_string())
|
||||
.body(Body::from(include_str!(
|
||||
"../../../static/build_key.min.html"
|
||||
)))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -142,7 +151,9 @@ pub async fn handle_build_key(
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX));
|
||||
|
||||
if auth_header.map_or(true, |h| h != AppConfig::get_share_token().as_str() && h != AUTH_TOKEN.as_str()) {
|
||||
if auth_header
|
||||
.is_none_or(|h| h != AppConfig::get_share_token().as_str() && h != AUTH_TOKEN.as_str())
|
||||
{
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(BuildKeyResponse::Error("Unauthorized".to_owned())),
|
||||
@@ -157,7 +168,7 @@ pub async fn handle_build_key(
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(BuildKeyResponse::Error("Invalid auth token".to_owned())),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,9 +184,7 @@ pub async fn handle_build_key(
|
||||
if let Some(usage_check_models) = request.usage_check_models {
|
||||
let usage_check = key_config::UsageCheckModel {
|
||||
r#type: match usage_check_models.model_type {
|
||||
UsageCheckModelType::Default => {
|
||||
key_config::usage_check_model::Type::Default as i32
|
||||
}
|
||||
UsageCheckModelType::Default => key_config::usage_check_model::Type::Default as i32,
|
||||
UsageCheckModelType::Disabled => {
|
||||
key_config::usage_check_model::Type::Disabled as i32
|
||||
}
|
||||
|
@@ -6,28 +6,28 @@ use crate::{
|
||||
ROUTE_BASIC_CALIBRATION_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH,
|
||||
ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER,
|
||||
ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
|
||||
ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH,
|
||||
ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH, ROUTE_TOKENS_UPDATE_PATH,
|
||||
ROUTE_USER_INFO_PATH,
|
||||
ROUTE_STATIC_PATH, ROUTE_TOKEN_TAGS_UPDATE_PATH, ROUTE_TOKENS_ADD_PATH,
|
||||
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
|
||||
ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
|
||||
},
|
||||
lazy::{get_start_time, AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
|
||||
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH, get_start_time},
|
||||
model::{AppConfig, AppState, PageContent},
|
||||
},
|
||||
chat::constant::AVAILABLE_MODELS,
|
||||
chat::constant::Models,
|
||||
common::model::{
|
||||
health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats},
|
||||
ApiStatus,
|
||||
health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats},
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{
|
||||
header::{CONTENT_TYPE, LOCATION},
|
||||
HeaderMap, StatusCode,
|
||||
header::{CONTENT_TYPE, LOCATION},
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Local;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
@@ -44,11 +44,11 @@ pub async fn handle_root() -> impl IntoResponse {
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub async fn handle_health(
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.map_or(false, |token| token == AUTH_TOKEN.as_str())
|
||||
.is_some_and(|token| token == AUTH_TOKEN.as_str())
|
||||
{
|
||||
// 只有在需要系统信息时才创建实例
|
||||
let mut sys = System::new_with_specifics(
|
||||
@@ -93,8 +93,8 @@ pub async fn handle_health(
|
||||
|
||||
Some(SystemStats {
|
||||
started: start_time.to_string(),
|
||||
total_requests: state.total_requests,
|
||||
active_requests: state.active_requests,
|
||||
total_requests: state.request_manager.total_requests,
|
||||
active_requests: state.request_manager.active_requests,
|
||||
system: SystemInfo {
|
||||
memory: MemoryInfo {
|
||||
rss: memory, // 物理内存使用量(字节)
|
||||
@@ -113,7 +113,7 @@ pub async fn handle_health(
|
||||
version: PKG_VERSION,
|
||||
uptime,
|
||||
stats,
|
||||
models: AVAILABLE_MODELS.iter().map(|m| m.id).collect::<Vec<_>>(),
|
||||
models: Models::ids(),
|
||||
endpoints: vec![
|
||||
ROUTE_CHAT_PATH.as_str(),
|
||||
ROUTE_MODELS_PATH.as_str(),
|
||||
@@ -122,6 +122,7 @@ pub async fn handle_health(
|
||||
ROUTE_TOKENS_UPDATE_PATH,
|
||||
ROUTE_TOKENS_ADD_PATH,
|
||||
ROUTE_TOKENS_DELETE_PATH,
|
||||
ROUTE_TOKEN_TAGS_UPDATE_PATH,
|
||||
ROUTE_LOGS_PATH,
|
||||
ROUTE_ENV_EXAMPLE_PATH,
|
||||
ROUTE_CONFIG_PATH,
|
||||
|
@@ -10,14 +10,14 @@ use crate::{
|
||||
common::{model::ApiStatus, utils::extract_token},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
HeaderMap, StatusCode,
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Local;
|
||||
use std::sync::Arc;
|
||||
@@ -28,17 +28,15 @@ pub async fn handle_logs() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_LOGS_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(
|
||||
include_str!("../../../static/logs.min.html").to_string(),
|
||||
))
|
||||
.body(Body::from(include_str!("../../../static/logs.min.html")))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(content.clone()))
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
@@ -62,10 +60,10 @@ pub async fn handle_logs_post(
|
||||
if auth_header == auth_token {
|
||||
return Ok(Json(LogsResponse {
|
||||
status: ApiStatus::Success,
|
||||
total: state.total_requests,
|
||||
active: Some(state.active_requests),
|
||||
error: Some(state.error_requests),
|
||||
logs: state.request_logs.clone(),
|
||||
total: state.request_manager.total_requests,
|
||||
active: Some(state.request_manager.active_requests),
|
||||
error: Some(state.request_manager.error_requests),
|
||||
logs: state.request_manager.request_logs.clone(),
|
||||
timestamp: Local::now().to_string(),
|
||||
}));
|
||||
}
|
||||
@@ -75,6 +73,7 @@ pub async fn handle_logs_post(
|
||||
|
||||
// 否则筛选出token匹配的日志
|
||||
let filtered_logs: Vec<RequestLog> = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter()
|
||||
.filter(|log| log.token_info.token == token_part)
|
||||
|
@@ -1,10 +1,13 @@
|
||||
use crate::{
|
||||
chat::constant::ERR_NODATA,
|
||||
common::{model::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}},
|
||||
common::{
|
||||
model::userinfo::GetUserInfo,
|
||||
utils::{extract_token, get_token_profile},
|
||||
},
|
||||
};
|
||||
use axum::Json;
|
||||
|
||||
use super::tokens::TokenRequest;
|
||||
use super::token::TokenRequest;
|
||||
|
||||
pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> {
|
||||
let auth_token = match request.token {
|
||||
@@ -12,7 +15,7 @@ pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUser
|
||||
None => {
|
||||
return Json(GetUserInfo::Error {
|
||||
error: ERR_NODATA.to_string(),
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,12 +24,12 @@ pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUser
|
||||
None => {
|
||||
return Json(GetUserInfo::Error {
|
||||
error: ERR_NODATA.to_string(),
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
match get_token_profile(&token).await {
|
||||
Some(usage) => Json(GetUserInfo::Usage(usage)),
|
||||
Some(usage) => Json(GetUserInfo::Usage(Box::new(usage))),
|
||||
None => Json(GetUserInfo::Error {
|
||||
error: ERR_NODATA.to_string(),
|
||||
}),
|
||||
|
99
src/chat/route/token.rs
Normal file
99
src/chat/route/token.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{
|
||||
CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENS_PATH,
|
||||
},
|
||||
model::{AppConfig, PageContent},
|
||||
},
|
||||
common::{
|
||||
model::ApiStatus,
|
||||
utils::{extract_time, extract_time_ks, extract_user_id, validate_token_and_checksum},
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
http::header::CONTENT_TYPE,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub async fn handle_tokens_page() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_TOKENS_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(include_str!("../../../static/tokens.min.html")))
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(Body::from(content))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenRequest {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BasicCalibrationResponse {
|
||||
pub status: ApiStatus,
|
||||
pub message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub create_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub checksum_time: Option<u64>,
|
||||
}
|
||||
|
||||
pub async fn handle_basic_calibration(
|
||||
Json(request): Json<TokenRequest>,
|
||||
) -> Json<BasicCalibrationResponse> {
|
||||
// 从请求头中获取并验证 auth token
|
||||
let auth_token = match request.token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Json(BasicCalibrationResponse {
|
||||
status: ApiStatus::Error,
|
||||
message: Some("未提供授权令牌".to_string()),
|
||||
user_id: None,
|
||||
create_at: None,
|
||||
checksum_time: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 校验 token 和 checksum
|
||||
let (token, checksum) = match validate_token_and_checksum(&auth_token) {
|
||||
Some(parts) => parts,
|
||||
None => {
|
||||
return Json(BasicCalibrationResponse {
|
||||
status: ApiStatus::Error,
|
||||
message: Some("无效令牌或无效校验和".to_string()),
|
||||
user_id: None,
|
||||
create_at: None,
|
||||
checksum_time: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提取用户ID和创建时间
|
||||
let user_id = extract_user_id(&token);
|
||||
let create_at = extract_time(&token).map(|dt| dt.to_string());
|
||||
let checksum_time = extract_time_ks(&checksum[..8]);
|
||||
|
||||
// 返回校验结果
|
||||
Json(BasicCalibrationResponse {
|
||||
status: ApiStatus::Success,
|
||||
message: Some("校验成功".to_string()),
|
||||
user_id,
|
||||
create_at,
|
||||
checksum_time,
|
||||
})
|
||||
}
|
@@ -1,83 +1,29 @@
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{
|
||||
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENS_PATH,
|
||||
},
|
||||
lazy::{AUTH_TOKEN, TOKEN_LIST_FILE},
|
||||
constant::AUTHORIZATION_BEARER_PREFIX,
|
||||
lazy::AUTH_TOKEN,
|
||||
model::{
|
||||
AppConfig, AppState, PageContent, TokenAddRequestTokenInfo, TokenInfo,
|
||||
TokenUpdateRequest, TokensDeleteRequest, TokensDeleteResponse,
|
||||
AppState, TokenAddRequest, TokenInfo, TokenInfoResponse, TokenManager,
|
||||
TokenTagsResponse, TokenTagsUpdateRequest, TokenUpdateRequest, TokensDeleteRequest,
|
||||
TokensDeleteResponse,
|
||||
},
|
||||
},
|
||||
common::{
|
||||
model::{error::ChatError, ApiStatus, ErrorResponse},
|
||||
model::{ApiStatus, ErrorResponse, error::ChatError, userinfo::TokenProfile},
|
||||
utils::{
|
||||
extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default,
|
||||
generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens,
|
||||
parse_token, validate_token, validate_token_and_checksum, write_tokens,
|
||||
generate_checksum_with_default, generate_checksum_with_repair,
|
||||
load_tokens_from_content, parse_token, validate_token,
|
||||
},
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
HeaderMap,
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode, header::AUTHORIZATION},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub async fn handle_get_hash() -> Response {
|
||||
let hash = generate_hash();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, hash).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChecksumQuery {
|
||||
#[serde(default)]
|
||||
pub checksum: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_get_checksum(Query(query): Query<ChecksumQuery>) -> Response {
|
||||
let checksum = match query.checksum {
|
||||
None => generate_checksum_with_default(),
|
||||
Some(checksum) => generate_checksum_with_repair(&checksum),
|
||||
};
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, checksum).into_response()
|
||||
}
|
||||
|
||||
pub async fn handle_get_timestamp_header() -> Response {
|
||||
let timestamp_header = generate_timestamp_header();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
|
||||
);
|
||||
|
||||
(headers, timestamp_header).into_response()
|
||||
}
|
||||
|
||||
pub async fn handle_get_tokens(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
@@ -93,7 +39,8 @@ pub async fn handle_get_tokens(
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let tokens = state.lock().await.token_infos.clone();
|
||||
let state = state.lock().await;
|
||||
let tokens = state.token_manager.tokens.clone();
|
||||
let tokens_count = tokens.len();
|
||||
|
||||
Ok(Json(TokenInfoResponse {
|
||||
@@ -104,49 +51,6 @@ pub async fn handle_get_tokens(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TokenInfoResponse {
|
||||
pub status: ApiStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tokens: Option<Vec<TokenInfo>>,
|
||||
pub tokens_count: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn handle_reload_tokens(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<TokenInfoResponse>, StatusCode> {
|
||||
// 验证 AUTH_TOKEN
|
||||
let auth_header = headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if auth_header != AUTH_TOKEN.as_str() {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 重新加载 tokens
|
||||
let tokens = load_tokens();
|
||||
let tokens_count = tokens.len();
|
||||
|
||||
// 更新应用状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = tokens;
|
||||
}
|
||||
|
||||
Ok(Json(TokenInfoResponse {
|
||||
status: ApiStatus::Success,
|
||||
tokens: None,
|
||||
tokens_count,
|
||||
message: Some("Token list has been reloaded".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_update_tokens(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
@@ -163,19 +67,50 @@ pub async fn handle_update_tokens(
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let token_list_file = TOKEN_LIST_FILE.as_str();
|
||||
// 获取当前的 token_manager 以保留现有 token 的 profile 和 tags
|
||||
let current_token_manager = {
|
||||
let state = state.lock().await;
|
||||
state.token_manager.clone()
|
||||
};
|
||||
|
||||
std::fs::write(&token_list_file, &request.tokens)
|
||||
// 创建 token -> (profile, tags) 映射
|
||||
let token_info_map: HashMap<String, (Option<TokenProfile>, Option<Vec<String>>)> =
|
||||
current_token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.map(|token| {
|
||||
(
|
||||
token.token.clone(),
|
||||
(token.profile.clone(), token.tags.clone()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 从请求内容加载新的 tokens
|
||||
let mut new_tokens = load_tokens_from_content(&request.tokens);
|
||||
|
||||
// 为相同的 token 保留原有的 profile 和 tags
|
||||
for token_info in &mut new_tokens {
|
||||
if let Some((profile, tags)) = token_info_map.get(&token_info.token) {
|
||||
token_info.profile = profile.clone();
|
||||
token_info.tags = tags.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的 TokenManager
|
||||
let token_manager = TokenManager::new(new_tokens);
|
||||
let tokens_count = token_manager.tokens.len();
|
||||
|
||||
// 保存到文件
|
||||
token_manager
|
||||
.save_tokens()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 重新加载 tokens
|
||||
let token_infos = load_tokens();
|
||||
let tokens_count = token_infos.len();
|
||||
|
||||
// 更新应用状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = token_infos;
|
||||
state.token_manager = token_manager;
|
||||
}
|
||||
|
||||
Ok(Json(TokenInfoResponse {
|
||||
@@ -189,7 +124,7 @@ pub async fn handle_update_tokens(
|
||||
pub async fn handle_add_tokens(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<Vec<TokenAddRequestTokenInfo>>,
|
||||
Json(request): Json<TokenAddRequest>,
|
||||
) -> Result<Json<TokenInfoResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// 验证 AUTH_TOKEN
|
||||
let auth_header = headers
|
||||
@@ -208,64 +143,65 @@ pub async fn handle_add_tokens(
|
||||
));
|
||||
}
|
||||
|
||||
let token_list_file = TOKEN_LIST_FILE.as_str();
|
||||
|
||||
// 获取当前的 tokens 并创建新的 token_infos
|
||||
let mut token_infos = {
|
||||
// 获取当前的 token_manager
|
||||
let mut token_manager = {
|
||||
let state = state.lock().await;
|
||||
state.token_infos.clone()
|
||||
state.token_manager.clone()
|
||||
};
|
||||
|
||||
// 创建现有token的集合
|
||||
let existing_tokens: std::collections::HashSet<_> =
|
||||
token_infos.iter().map(|info| info.token.as_str()).collect();
|
||||
|
||||
// 预分配容量
|
||||
let mut new_tokens = Vec::with_capacity(request.len());
|
||||
let existing_tokens: std::collections::HashSet<_> = token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.map(|info| info.token.as_str())
|
||||
.collect();
|
||||
|
||||
// 处理新的tokens
|
||||
for token_info in request {
|
||||
let mut new_tokens = Vec::with_capacity(request.tokens.len());
|
||||
for token_info in request.tokens {
|
||||
let parsed_token = parse_token(&token_info.token);
|
||||
if !existing_tokens.contains(parsed_token.as_str()) && validate_token(&parsed_token) {
|
||||
new_tokens.push(TokenInfo {
|
||||
token: parsed_token,
|
||||
// 如果提供了checksum就使用提供的,否则生成新的
|
||||
checksum: token_info
|
||||
.checksum
|
||||
.as_deref()
|
||||
.map(generate_checksum_with_repair)
|
||||
.unwrap_or_else(generate_checksum_with_default),
|
||||
profile: None,
|
||||
tags: request.tags.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有新tokens才进行后续操作
|
||||
if !new_tokens.is_empty() {
|
||||
// 预分配足够的容量
|
||||
token_infos.reserve(new_tokens.len());
|
||||
token_infos.extend(new_tokens);
|
||||
// 添加新tokens
|
||||
token_manager.tokens.extend(new_tokens);
|
||||
let tokens_count = token_manager.tokens.len();
|
||||
|
||||
// 写入文件
|
||||
write_tokens(&token_infos, token_list_file).map_err(|_| {
|
||||
// 更新全局标签
|
||||
if let Some(ref tags) = request.tags {
|
||||
token_manager.update_global_tags(tags);
|
||||
}
|
||||
|
||||
// 保存到文件
|
||||
token_manager.save_tokens().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some("Failed to update token list file".to_string()),
|
||||
message: Some("无法更新token list文件".to_string()),
|
||||
error: Some("Failed to save token list".to_string()),
|
||||
message: Some("无法保存token list".to_string()),
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 获取最终的tokens数量(在更新状态之前)
|
||||
let tokens_count = token_infos.len();
|
||||
|
||||
// 更新应用状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = token_infos;
|
||||
state.token_manager = token_manager;
|
||||
}
|
||||
|
||||
Ok(Json(TokenInfoResponse {
|
||||
@@ -275,12 +211,13 @@ pub async fn handle_add_tokens(
|
||||
message: Some("New tokens have been added and reloaded".to_string()),
|
||||
}))
|
||||
} else {
|
||||
// 如果没有新tokens,使用原始数量
|
||||
let tokens_count = token_infos.len();
|
||||
// 如果没有新tokens,返回当前状态
|
||||
let tokens = token_manager.tokens.clone();
|
||||
let tokens_count = tokens.len();
|
||||
|
||||
Ok(Json(TokenInfoResponse {
|
||||
status: ApiStatus::Success,
|
||||
tokens: None,
|
||||
tokens: Some(tokens),
|
||||
tokens_count,
|
||||
message: Some("No new tokens were added".to_string()),
|
||||
}))
|
||||
@@ -309,11 +246,11 @@ pub async fn handle_delete_tokens(
|
||||
));
|
||||
}
|
||||
|
||||
let token_infos = state.lock().await.token_infos.clone();
|
||||
let original_count = token_infos.len(); // 提前存储原始长度
|
||||
|
||||
// 获取token_list文件路径
|
||||
let token_list_file = TOKEN_LIST_FILE.as_str();
|
||||
// 获取当前的 token_manager
|
||||
let mut token_manager = {
|
||||
let state = state.lock().await;
|
||||
state.token_manager.clone()
|
||||
};
|
||||
|
||||
// 创建要删除的tokens的HashSet,提高查找效率
|
||||
let tokens_to_delete: std::collections::HashSet<_> = request.tokens.iter().collect();
|
||||
@@ -324,7 +261,12 @@ pub async fn handle_delete_tokens(
|
||||
request
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|token| !token_infos.iter().any(|info| &info.token == *token))
|
||||
.filter(|token| {
|
||||
!token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.any(|token_info| token_info.token == **token)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
@@ -332,28 +274,26 @@ pub async fn handle_delete_tokens(
|
||||
None
|
||||
};
|
||||
|
||||
// 预分配容量并过滤掉要删除的tokens
|
||||
let estimated_capacity = original_count.saturating_sub(tokens_to_delete.len());
|
||||
let mut filtered_token_infos = Vec::with_capacity(estimated_capacity);
|
||||
let original_count: usize = token_manager.tokens.len();
|
||||
|
||||
// 一次性过滤tokens
|
||||
for info in token_infos {
|
||||
if !tokens_to_delete.contains(&info.token) {
|
||||
filtered_token_infos.push(info);
|
||||
}
|
||||
}
|
||||
// 从每个分组中删除指定的tokens
|
||||
token_manager
|
||||
.tokens
|
||||
.retain(|token_info| !tokens_to_delete.contains(&token_info.token));
|
||||
|
||||
let new_count: usize = token_manager.tokens.len();
|
||||
|
||||
// 如果有tokens被删除才进行更新操作
|
||||
if filtered_token_infos.len() < original_count {
|
||||
// 写入文件
|
||||
write_tokens(&filtered_token_infos, token_list_file).map_err(|_| {
|
||||
if new_count < original_count {
|
||||
// 保存到文件
|
||||
token_manager.save_tokens().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some("Failed to update token list file".to_string()),
|
||||
message: Some("无法更新token list文件".to_string()),
|
||||
error: Some("Failed to save token list".to_string()),
|
||||
message: Some("无法保存token list".to_string()),
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
@@ -361,9 +301,10 @@ pub async fn handle_delete_tokens(
|
||||
// 如果需要的话计算 updated_tokens
|
||||
let updated_tokens = if request.expectation.needs_updated_tokens() {
|
||||
Some(
|
||||
filtered_token_infos
|
||||
token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.map(|info| info.token.clone())
|
||||
.map(|t| t.token.clone())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
@@ -373,7 +314,7 @@ pub async fn handle_delete_tokens(
|
||||
// 更新状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = filtered_token_infos;
|
||||
state.token_manager = token_manager;
|
||||
}
|
||||
|
||||
Ok(Json(TokensDeleteResponse {
|
||||
@@ -387,9 +328,10 @@ pub async fn handle_delete_tokens(
|
||||
status: ApiStatus::Success,
|
||||
updated_tokens: if request.expectation.needs_updated_tokens() {
|
||||
Some(
|
||||
filtered_token_infos
|
||||
token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.map(|info| info.token.clone())
|
||||
.map(|t| t.token.clone())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
@@ -400,82 +342,62 @@ pub async fn handle_delete_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_tokens_page() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_TOKENS_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/tokens.min.html").to_string())
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.unwrap(),
|
||||
PageContent::Html(content) => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(content.clone())
|
||||
.unwrap(),
|
||||
pub async fn handle_update_token_tags(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<TokenTagsUpdateRequest>,
|
||||
) -> Result<Json<TokenTagsResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// 验证 AUTH_TOKEN
|
||||
let auth_header = headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
))?;
|
||||
|
||||
if auth_header != AUTH_TOKEN.as_str() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenRequest {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BasicCalibrationResponse {
|
||||
pub status: ApiStatus,
|
||||
pub message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub create_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub checksum_time: Option<u64>,
|
||||
}
|
||||
|
||||
pub async fn handle_basic_calibration(
|
||||
Json(request): Json<TokenRequest>,
|
||||
) -> Json<BasicCalibrationResponse> {
|
||||
// 从请求头中获取并验证 auth token
|
||||
let auth_token = match request.token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Json(BasicCalibrationResponse {
|
||||
status: ApiStatus::Error,
|
||||
message: Some("未提供授权令牌".to_string()),
|
||||
user_id: None,
|
||||
create_at: None,
|
||||
checksum_time: None,
|
||||
})
|
||||
// 获取并更新 token_manager
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Err(e) = state
|
||||
.token_manager
|
||||
.update_tokens_tags(request.tokens, request.tags)
|
||||
{
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some(e.to_string()),
|
||||
message: Some("更新标签失败".to_string()),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// 校验 token 和 checksum
|
||||
let (token, checksum) = match validate_token_and_checksum(&auth_token) {
|
||||
Some(parts) => parts,
|
||||
None => {
|
||||
return Json(BasicCalibrationResponse {
|
||||
status: ApiStatus::Error,
|
||||
message: Some("无效令牌或无效校验和".to_string()),
|
||||
user_id: None,
|
||||
create_at: None,
|
||||
checksum_time: None,
|
||||
})
|
||||
// 保存更改
|
||||
if (state.token_manager.save_tokens().await).is_err() {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some("Failed to save token tags".to_string()),
|
||||
message: Some("无法保存标签信息".to_string()),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 提取用户ID和创建时间
|
||||
let user_id = extract_user_id(&token);
|
||||
let create_at = extract_time(&token).map(|dt| dt.to_string());
|
||||
let checksum_time = extract_time_ks(&checksum[..8]);
|
||||
|
||||
// 返回校验结果
|
||||
Json(BasicCalibrationResponse {
|
||||
Ok(Json(TokenTagsResponse {
|
||||
status: ApiStatus::Success,
|
||||
message: Some("校验成功".to_string()),
|
||||
user_id,
|
||||
create_at,
|
||||
checksum_time,
|
||||
})
|
||||
message: Some("标签更新成功".to_string()),
|
||||
}))
|
||||
}
|
||||
|
@@ -4,7 +4,10 @@ use crate::{
|
||||
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
|
||||
OBJECT_CHAT_COMPLETION_CHUNK,
|
||||
},
|
||||
lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT},
|
||||
lazy::{
|
||||
AUTH_TOKEN, CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, IS_UNLIMITED_REQUEST_LOGS,
|
||||
KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT,
|
||||
},
|
||||
model::{
|
||||
AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo,
|
||||
UsageCheck,
|
||||
@@ -12,7 +15,7 @@ use crate::{
|
||||
},
|
||||
chat::{
|
||||
config::KeyConfig,
|
||||
constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS},
|
||||
constant::{Models, USAGE_CHECK_MODELS},
|
||||
error::StreamError,
|
||||
model::{
|
||||
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
|
||||
@@ -21,22 +24,22 @@ use crate::{
|
||||
},
|
||||
common::{
|
||||
client::build_client,
|
||||
model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse},
|
||||
model::{ApiStatus, ErrorResponse, error::ChatError, userinfo::MembershipType},
|
||||
utils::{
|
||||
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
|
||||
validate_token_and_checksum, TrimNewlines as _,
|
||||
TrimNewlines as _, format_time_ms, from_base64, get_available_models,
|
||||
get_token_profile, tokeninfo_to_token, validate_token_and_checksum,
|
||||
},
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
HeaderMap, StatusCode,
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
},
|
||||
response::Response,
|
||||
Json,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
@@ -44,17 +47,129 @@ use prost::Message as _;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{constant::LONG_CONTEXT_MODELS, model::Model};
|
||||
|
||||
// 辅助函数:提取认证token
|
||||
fn extract_auth_token(headers: &HeaderMap) -> Result<&str, (StatusCode, Json<ErrorResponse>)> {
|
||||
headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
))
|
||||
}
|
||||
|
||||
// 辅助函数:解析token信息
|
||||
async fn resolve_token_info(
|
||||
auth_header: &str,
|
||||
state: &Arc<Mutex<AppState>>,
|
||||
) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
|
||||
match auth_header {
|
||||
// 管理员Token处理
|
||||
token if is_admin_token(token) => resolve_admin_token(state).await,
|
||||
|
||||
// 动态密钥处理
|
||||
token if is_dynamic_key(token) => resolve_dynamic_key(token),
|
||||
|
||||
// 普通用户Token处理
|
||||
token => validate_token_and_checksum(token).ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:检查是否为管理员token
|
||||
fn is_admin_token(token: &str) -> bool {
|
||||
token == AUTH_TOKEN.as_str()
|
||||
|| (AppConfig::is_share() && token == AppConfig::get_share_token().as_str())
|
||||
}
|
||||
|
||||
// 辅助函数:检查是否为动态密钥
|
||||
fn is_dynamic_key(token: &str) -> bool {
|
||||
AppConfig::get_dynamic_key() && token.starts_with(&*KEY_PREFIX)
|
||||
}
|
||||
|
||||
// 辅助函数:处理管理员token
|
||||
async fn resolve_admin_token(
|
||||
state: &Arc<Mutex<AppState>>,
|
||||
) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
|
||||
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let state_guard = state.lock().await;
|
||||
let token_infos = &state_guard.token_manager.tokens;
|
||||
|
||||
if token_infos.is_empty() {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(ChatError::NoTokens.to_json()),
|
||||
));
|
||||
}
|
||||
|
||||
let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
|
||||
let token_info = &token_infos[index];
|
||||
|
||||
Ok((token_info.token.clone(), token_info.checksum.clone()))
|
||||
}
|
||||
|
||||
// 辅助函数:处理动态密钥
|
||||
fn resolve_dynamic_key(token: &str) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
|
||||
from_base64(&token[*KEY_PREFIX_LEN..])
|
||||
.and_then(|decoded_bytes| KeyConfig::decode(&decoded_bytes[..]).ok())
|
||||
.and_then(|key_config| key_config.auth_token)
|
||||
.and_then(|token_info| tokeninfo_to_token(&token_info))
|
||||
.ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
))
|
||||
}
|
||||
|
||||
// 模型列表处理
|
||||
pub async fn handle_models() -> Json<ModelsResponse> {
|
||||
Json(ModelsResponse {
|
||||
object: "list",
|
||||
data: &AVAILABLE_MODELS,
|
||||
})
|
||||
pub async fn handle_models(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<ModelsResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// 如果没有认证头,返回默认可用模型
|
||||
if headers.get(AUTHORIZATION).is_none() {
|
||||
return Ok(Json(ModelsResponse::with_default_models()));
|
||||
}
|
||||
|
||||
// 提取和验证认证token
|
||||
let auth_token = extract_auth_token(&headers)?;
|
||||
let (token, checksum) = resolve_token_info(auth_token, &state).await?;
|
||||
|
||||
// 获取可用模型列表
|
||||
let models = get_available_models(&token, &checksum).await.ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16()),
|
||||
error: Some("Failed to fetch available models".to_string()),
|
||||
message: Some("Unable to get available models".to_string()),
|
||||
}),
|
||||
))?;
|
||||
|
||||
// 更新模型列表
|
||||
if let Err(e) = Models::update(models) {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Failure,
|
||||
code: Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16()),
|
||||
error: Some("Failed to update models".to_string()),
|
||||
message: Some(e.to_string()),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(ModelsResponse::new(Models::to_arc())))
|
||||
}
|
||||
|
||||
// 聊天处理函数的签名
|
||||
@@ -73,15 +188,15 @@ pub async fn handle_chat(
|
||||
};
|
||||
|
||||
// 验证模型是否支持并获取模型信息
|
||||
let model = AVAILABLE_MODELS.iter().find(|m| m.id == model_name);
|
||||
let model_supported = model.is_some();
|
||||
|
||||
if !(model_supported || allow_claude && request.model.starts_with("claude")) {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ChatError::ModelNotSupported(request.model).to_json()),
|
||||
));
|
||||
}
|
||||
let model =
|
||||
if Models::exists(&model_name) || (allow_claude && request.model.starts_with("claude")) {
|
||||
Some(&model_name)
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ChatError::ModelNotSupported(request.model).to_json()),
|
||||
));
|
||||
};
|
||||
|
||||
let request_time = chrono::Local::now();
|
||||
|
||||
@@ -114,7 +229,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
let state_guard = state.lock().await;
|
||||
let token_infos = &state_guard.token_infos;
|
||||
let token_infos = &state_guard.token_manager.tokens;
|
||||
|
||||
// 检查是否存在可用的token
|
||||
if token_infos.is_empty() {
|
||||
@@ -159,56 +274,85 @@ pub async fn handle_chat(
|
||||
{
|
||||
let state_clone = state.clone();
|
||||
let mut state = state.lock().await;
|
||||
state.total_requests += 1;
|
||||
state.active_requests += 1;
|
||||
state.request_manager.total_requests += 1;
|
||||
state.request_manager.active_requests += 1;
|
||||
|
||||
// 查找最新的相同token的日志,检查使用情况
|
||||
let need_profile_check = state
|
||||
.request_logs
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|log| log.token_info.token == auth_token && log.token_info.profile.is_some())
|
||||
.and_then(|log| log.token_info.profile.as_ref())
|
||||
.map(|profile| {
|
||||
if profile.stripe.membership_type != MembershipType::Free {
|
||||
return false;
|
||||
let mut found_count: u32 = 0;
|
||||
let mut no_prompt_count: u32 = 0;
|
||||
let mut need_profile_check = false;
|
||||
|
||||
for log in state.request_manager.request_logs.iter().rev() {
|
||||
if log.token_info.token == auth_token {
|
||||
if !LONG_CONTEXT_MODELS.contains(&log.model.as_str()) {
|
||||
found_count += 1;
|
||||
}
|
||||
|
||||
let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str());
|
||||
let standard = &profile.usage.standard;
|
||||
let premium = &profile.usage.premium;
|
||||
|
||||
if is_premium {
|
||||
premium
|
||||
.max_requests
|
||||
.map_or(false, |max| premium.num_requests >= max)
|
||||
} else {
|
||||
standard
|
||||
.max_requests
|
||||
.map_or(false, |max| standard.num_requests >= max)
|
||||
if log.prompt.is_none() {
|
||||
no_prompt_count += 1;
|
||||
}
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
// 如果达到限制,直接返回未授权错误
|
||||
if found_count == 1 && log.token_info.profile.is_some() {
|
||||
if let Some(profile) = &log.token_info.profile {
|
||||
if profile.stripe.membership_type == MembershipType::Free {
|
||||
let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str());
|
||||
need_profile_check =
|
||||
if is_premium {
|
||||
profile.usage.premium.max_requests.is_some_and(|max| {
|
||||
profile.usage.premium.num_requests >= max
|
||||
})
|
||||
} else {
|
||||
profile.usage.standard.max_requests.is_some_and(|max| {
|
||||
profile.usage.standard.num_requests >= max
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_count == 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_count == 2 && no_prompt_count == 2 {
|
||||
state.request_manager.active_requests -= 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
return Err((
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
Json(ErrorResponse {
|
||||
status: ApiStatus::Error,
|
||||
code: Some(429),
|
||||
error: Some("rate_limit_exceeded".to_string()),
|
||||
message: Some("Too many requests without prompt".to_string()),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
// 处理检查结果
|
||||
if need_profile_check {
|
||||
state.active_requests -= 1;
|
||||
state.error_requests += 1;
|
||||
state.request_manager.active_requests -= 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
));
|
||||
}
|
||||
|
||||
let next_id = state.request_logs.last().map_or(1, |log| log.id + 1);
|
||||
let next_id = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.last()
|
||||
.map_or(1, |log| log.id + 1);
|
||||
current_id = next_id;
|
||||
|
||||
// 如果需要获取用户使用情况,创建后台任务获取profile
|
||||
if model
|
||||
.map(|m| {
|
||||
m.is_usage_check(UsageCheck::from_proto(
|
||||
current_config.usage_check_models.as_ref(),
|
||||
))
|
||||
Model::is_usage_check(
|
||||
m,
|
||||
UsageCheck::from_proto(current_config.usage_check_models.as_ref()),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@@ -222,30 +366,35 @@ pub async fn handle_chat(
|
||||
|
||||
// 先找到所有需要更新的位置的索引
|
||||
let token_info_idx = state
|
||||
.token_infos
|
||||
.token_manager
|
||||
.tokens
|
||||
.iter()
|
||||
.position(|info| info.token == auth_token_clone);
|
||||
|
||||
let log_idx = state.request_logs.iter().rposition(|log| log.id == log_id);
|
||||
let log_idx = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter()
|
||||
.rposition(|log| log.id == log_id);
|
||||
|
||||
// 根据索引更新
|
||||
match (token_info_idx, log_idx) {
|
||||
(Some(t_idx), Some(l_idx)) => {
|
||||
state.token_infos[t_idx].profile = profile.clone();
|
||||
state.request_logs[l_idx].token_info.profile = profile;
|
||||
state.token_manager.tokens[t_idx].profile = profile.clone();
|
||||
state.request_manager.request_logs[l_idx].token_info.profile = profile;
|
||||
}
|
||||
(Some(t_idx), None) => {
|
||||
state.token_infos[t_idx].profile = profile;
|
||||
state.token_manager.tokens[t_idx].profile = profile;
|
||||
}
|
||||
(None, Some(l_idx)) => {
|
||||
state.request_logs[l_idx].token_info.profile = profile;
|
||||
state.request_manager.request_logs[l_idx].token_info.profile = profile;
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.request_logs.push(RequestLog {
|
||||
state.request_manager.request_logs.push(RequestLog {
|
||||
id: next_id,
|
||||
timestamp: request_time,
|
||||
model: request.model.clone(),
|
||||
@@ -253,6 +402,7 @@ pub async fn handle_chat(
|
||||
token: auth_token.clone(),
|
||||
checksum: checksum.clone(),
|
||||
profile: None,
|
||||
tags: None,
|
||||
},
|
||||
prompt: None,
|
||||
timing: TimingInfo {
|
||||
@@ -264,8 +414,10 @@ pub async fn handle_chat(
|
||||
error: None,
|
||||
});
|
||||
|
||||
if state.request_logs.len() > *REQUEST_LOGS_LIMIT {
|
||||
state.request_logs.remove(0);
|
||||
if !*IS_UNLIMITED_REQUEST_LOGS
|
||||
&& state.request_manager.request_logs.len() > *REQUEST_LOGS_LIMIT
|
||||
{
|
||||
state.request_manager.request_logs.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +435,7 @@ pub async fn handle_chat(
|
||||
Err(e) => {
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -291,8 +444,8 @@ pub async fn handle_chat(
|
||||
log.status = LogStatus::Failed;
|
||||
log.error = Some(e.to_string());
|
||||
}
|
||||
state.active_requests -= 1;
|
||||
state.error_requests += 1;
|
||||
state.request_manager.active_requests -= 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(
|
||||
@@ -303,7 +456,16 @@ pub async fn handle_chat(
|
||||
};
|
||||
|
||||
// 构建请求客户端
|
||||
let client = build_client(&auth_token, &checksum, is_search);
|
||||
let client = build_client(
|
||||
&auth_token,
|
||||
&checksum,
|
||||
if is_search {
|
||||
&CURSOR_API2_CHAT_WEB_URL
|
||||
} else {
|
||||
&CURSOR_API2_CHAT_URL
|
||||
},
|
||||
true,
|
||||
);
|
||||
// 添加超时设置
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
|
||||
@@ -319,6 +481,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -334,6 +497,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -342,8 +506,8 @@ pub async fn handle_chat(
|
||||
log.status = LogStatus::Failed;
|
||||
log.error = Some(e.to_string());
|
||||
}
|
||||
state.active_requests -= 1;
|
||||
state.error_requests += 1;
|
||||
state.request_manager.active_requests -= 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
}
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -356,6 +520,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -364,8 +529,8 @@ pub async fn handle_chat(
|
||||
log.status = LogStatus::Failed;
|
||||
log.error = Some("Request timeout".to_string());
|
||||
}
|
||||
state.active_requests -= 1;
|
||||
state.error_requests += 1;
|
||||
state.request_manager.active_requests -= 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
}
|
||||
return Err((
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
@@ -377,7 +542,7 @@ pub async fn handle_chat(
|
||||
// 释放活动请求计数
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.active_requests -= 1;
|
||||
state.request_manager.active_requests -= 1;
|
||||
}
|
||||
|
||||
let convert_web_ref = current_config.include_web_references();
|
||||
@@ -460,6 +625,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = ctx.state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -494,6 +660,7 @@ pub async fn handle_chat(
|
||||
StreamMessage::Debug(debug_prompt) => {
|
||||
if let Ok(mut state) = ctx.state.try_lock() {
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -518,11 +685,12 @@ pub async fn handle_chat(
|
||||
if let Err(StreamError::ChatError(error)) =
|
||||
decoder.lock().await.decode(&chunk, convert_web_ref)
|
||||
{
|
||||
let error_response = error.to_error_response();
|
||||
let error_response = error.into_error_response();
|
||||
// 更新请求日志为失败
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -532,12 +700,12 @@ pub async fn handle_chat(
|
||||
log.error = Some(error_response.native_code());
|
||||
log.timing.total =
|
||||
format_time_ms(start_time.elapsed().as_secs_f64());
|
||||
state.error_requests += 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
}
|
||||
}
|
||||
return Err((
|
||||
error_response.status_code(),
|
||||
Json(error_response.to_common()),
|
||||
Json(error_response.into_common()),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -553,6 +721,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -560,7 +729,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
log.status = LogStatus::Failed;
|
||||
log.error = Some("Empty stream response".to_string());
|
||||
state.error_requests += 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
}
|
||||
}
|
||||
return Err((
|
||||
@@ -667,6 +836,7 @@ pub async fn handle_chat(
|
||||
StreamMessage::Debug(debug_prompt) => {
|
||||
if let Ok(mut state) = state.try_lock() {
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -681,10 +851,10 @@ pub async fn handle_chat(
|
||||
}
|
||||
}
|
||||
Err(StreamError::ChatError(error)) => {
|
||||
let error_response = error.to_error_response();
|
||||
let error_response = error.into_error_response();
|
||||
return Err((
|
||||
error_response.status_code(),
|
||||
Json(error_response.to_common()),
|
||||
Json(error_response.into_common()),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -705,6 +875,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
@@ -712,7 +883,7 @@ pub async fn handle_chat(
|
||||
{
|
||||
log.status = LogStatus::Failed;
|
||||
log.error = Some("Empty response received".to_string());
|
||||
state.error_requests += 1;
|
||||
state.request_manager.error_requests += 1;
|
||||
}
|
||||
}
|
||||
return Err((
|
||||
@@ -747,6 +918,7 @@ pub async fn handle_chat(
|
||||
let total_time = format_time_ms(start_time.elapsed().as_secs_f64());
|
||||
let mut state = state.lock().await;
|
||||
if let Some(log) = state
|
||||
.request_manager
|
||||
.request_logs
|
||||
.iter_mut()
|
||||
.rev()
|
||||
|
@@ -1,10 +1,10 @@
|
||||
use crate::chat::{
|
||||
aiserver::v1::StreamChatResponse,
|
||||
aiserver::v1::{StreamChatResponse, WebReference},
|
||||
error::{ChatError, StreamError},
|
||||
};
|
||||
use flate2::read::GzDecoder;
|
||||
use prost::Message;
|
||||
use std::{collections::BTreeMap, io::Read};
|
||||
use std::io::Read;
|
||||
|
||||
// 解压gzip数据
|
||||
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
|
||||
@@ -24,17 +24,23 @@ pub trait ToMarkdown {
|
||||
fn to_markdown(&self) -> String;
|
||||
}
|
||||
|
||||
impl ToMarkdown for BTreeMap<String, String> {
|
||||
impl ToMarkdown for Vec<WebReference> {
|
||||
fn to_markdown(&self) -> String {
|
||||
if self.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut result = String::from("WebReferences:\n");
|
||||
for (i, (url, title)) in self.iter().enumerate() {
|
||||
result.push_str(&format!("{}. [{}]({})\n", i + 1, title, url));
|
||||
for (i, web_ref) in self.iter().enumerate() {
|
||||
result.push_str(&format!(
|
||||
"{}. [{}]({})<{}>\n",
|
||||
i + 1,
|
||||
web_ref.title,
|
||||
web_ref.url,
|
||||
web_ref.chunk
|
||||
));
|
||||
}
|
||||
result.push_str("\n");
|
||||
result.push('\n');
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -44,7 +50,7 @@ pub enum StreamMessage {
|
||||
// 调试
|
||||
Debug(String),
|
||||
// 网络引用
|
||||
WebReference(BTreeMap<String, String>),
|
||||
WebReference(Vec<WebReference>),
|
||||
// 内容开始标志
|
||||
ContentStart,
|
||||
// 消息内容
|
||||
@@ -98,11 +104,15 @@ impl StreamDecoder {
|
||||
self.first_result_ready
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, data: &[u8], convert_web_ref: bool) -> Result<Vec<StreamMessage>, StreamError> {
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
data: &[u8],
|
||||
convert_web_ref: bool,
|
||||
) -> Result<Vec<StreamMessage>, StreamError> {
|
||||
self.buffer.extend_from_slice(data);
|
||||
|
||||
if self.buffer.len() < 5 {
|
||||
if self.buffer.len() == 0 {
|
||||
if self.buffer.is_empty() {
|
||||
return Err(StreamError::EmptyStream);
|
||||
}
|
||||
crate::debug_println!("数据长度小于5字节,当前数据: {}", hex::encode(&self.buffer));
|
||||
@@ -133,15 +143,12 @@ impl StreamDecoder {
|
||||
|
||||
let msg_data = &self.buffer[offset + 5..offset + 5 + msg_len];
|
||||
|
||||
match self.process_message(msg_type, msg_data)? {
|
||||
Some(msg) => {
|
||||
if convert_web_ref {
|
||||
messages.push(msg.convert_web_ref_to_content());
|
||||
} else {
|
||||
messages.push(msg);
|
||||
}
|
||||
if let Some(msg) = self.process_message(msg_type, msg_data)? {
|
||||
if convert_web_ref {
|
||||
messages.push(msg.convert_web_ref_to_content());
|
||||
} else {
|
||||
messages.push(msg);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
offset += 5 + msg_len;
|
||||
@@ -157,7 +164,8 @@ impl StreamDecoder {
|
||||
}
|
||||
}
|
||||
if !self.first_result_ready {
|
||||
self.first_result_ready = self.first_result.is_some() && self.buffer.is_empty() && !self.first_result_taken;
|
||||
self.first_result_ready =
|
||||
self.first_result.is_some() && self.buffer.is_empty() && !self.first_result_taken;
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
@@ -182,17 +190,13 @@ impl StreamDecoder {
|
||||
|
||||
fn handle_text_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> {
|
||||
if let Ok(response) = StreamChatResponse::decode(msg_data) {
|
||||
// crate::debug_println!("[text] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response);
|
||||
// println!("[text] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response);
|
||||
if !response.text.is_empty() {
|
||||
Ok(Some(StreamMessage::Content(response.text)))
|
||||
} else if let Some(filled_prompt) = response.filled_prompt {
|
||||
Ok(Some(StreamMessage::Debug(filled_prompt)))
|
||||
} else if let Some(web_citation) = response.web_citation {
|
||||
let mut refs = BTreeMap::new();
|
||||
for reference in web_citation.references {
|
||||
refs.insert(reference.url, reference.title);
|
||||
}
|
||||
Ok(Some(StreamMessage::WebReference(refs)))
|
||||
Ok(Some(StreamMessage::WebReference(web_citation.references)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -204,17 +208,13 @@ impl StreamDecoder {
|
||||
fn handle_gzip_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> {
|
||||
if let Some(text) = decompress_gzip(msg_data) {
|
||||
if let Ok(response) = StreamChatResponse::decode(&text[..]) {
|
||||
// crate::debug_println!("[gzip] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response);
|
||||
// println!("[gzip] StreamChatResponse [hex: {}]: {:?}", hex::encode(msg_data), response);
|
||||
if !response.text.is_empty() {
|
||||
Ok(Some(StreamMessage::Content(response.text)))
|
||||
} else if let Some(filled_prompt) = response.filled_prompt {
|
||||
Ok(Some(StreamMessage::Debug(filled_prompt)))
|
||||
} else if let Some(web_citation) = response.web_citation {
|
||||
let mut refs = BTreeMap::new();
|
||||
for reference in web_citation.references {
|
||||
refs.insert(reference.url, reference.title);
|
||||
}
|
||||
Ok(Some(StreamMessage::WebReference(refs)))
|
||||
Ok(Some(StreamMessage::WebReference(web_citation.references)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ impl StreamDecoder {
|
||||
return Ok(Some(StreamMessage::StreamEnd));
|
||||
}
|
||||
if let Ok(text) = String::from_utf8(msg_data.to_vec()) {
|
||||
// println!("JSON消息: {}", text);
|
||||
// println!("[text] JSON消息 [hex: {}]: {}", hex::encode(msg_data), text);
|
||||
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
|
||||
return Err(StreamError::ChatError(error));
|
||||
}
|
||||
@@ -248,7 +248,7 @@ impl StreamDecoder {
|
||||
return Ok(Some(StreamMessage::StreamEnd));
|
||||
}
|
||||
if let Ok(text) = String::from_utf8(text) {
|
||||
// println!("JSON消息: {}", text);
|
||||
// println!("[gzip] JSON消息 [hex: {}]: {}", hex::encode(msg_data), text);
|
||||
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
|
||||
return Err(StreamError::ChatError(error));
|
||||
}
|
||||
@@ -293,8 +293,11 @@ mod tests {
|
||||
}
|
||||
StreamMessage::WebReference(refs) => {
|
||||
println!("网页引用:");
|
||||
for (i, (url, title)) in refs.iter().enumerate() {
|
||||
println!("{}. {} - {}", i, url, title);
|
||||
for (i, web_ref) in refs.iter().enumerate() {
|
||||
println!(
|
||||
"{}. {} - {} - {}",
|
||||
i, web_ref.url, web_ref.title, web_ref.chunk
|
||||
);
|
||||
}
|
||||
}
|
||||
StreamMessage::Debug(prompt) => {
|
||||
@@ -375,8 +378,11 @@ mod tests {
|
||||
}
|
||||
StreamMessage::WebReference(refs) => {
|
||||
println!("网页引用 [hex: {}]:", hex_str);
|
||||
for (i, (url, title)) in refs.iter().enumerate() {
|
||||
println!("{}. {} - {}", i, url, title);
|
||||
for (i, web_ref) in refs.iter().enumerate() {
|
||||
println!(
|
||||
"{}. {} - {} - {}",
|
||||
i, web_ref.url, web_ref.title, web_ref.chunk
|
||||
);
|
||||
}
|
||||
}
|
||||
StreamMessage::Debug(prompt) => {
|
||||
|
@@ -1,3 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod model;
|
||||
pub mod utils;
|
||||
pub mod client;
|
||||
|
@@ -1,17 +1,21 @@
|
||||
use super::utils::generate_hash;
|
||||
use crate::{app::{
|
||||
constant::{
|
||||
CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL,
|
||||
HEADER_NAME_GHOST_MODE, TRUE,
|
||||
use crate::{
|
||||
AppConfig,
|
||||
app::{
|
||||
constant::{
|
||||
CONTENT_TYPE_CONNECT_PROTO, CONTENT_TYPE_PROTO, CURSOR_API2_HOST, CURSOR_HOST,
|
||||
CURSOR_SETTINGS_URL, HEADER_NAME_GHOST_MODE, TRUE,
|
||||
},
|
||||
lazy::{
|
||||
CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST,
|
||||
USE_REVERSE_PROXY,
|
||||
},
|
||||
},
|
||||
lazy::{
|
||||
CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST, USE_REVERSE_PROXY
|
||||
},
|
||||
}, AppConfig};
|
||||
};
|
||||
use reqwest::header::{
|
||||
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE,
|
||||
DNT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
|
||||
};
|
||||
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT,
|
||||
HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
|
||||
};
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use std::sync::LazyLock;
|
||||
use uuid::Uuid;
|
||||
@@ -35,7 +39,10 @@ def_const!(VALUE_LANGUAGE, "zh-CN");
|
||||
def_const!(EMPTY, "empty");
|
||||
def_const!(CORS, "cors");
|
||||
def_const!(NO_CACHE, "no-cache");
|
||||
def_const!(UA_WIN, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
def_const!(
|
||||
UA_WIN,
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
);
|
||||
def_const!(SAME_ORIGIN, "same-origin");
|
||||
def_const!(KEEP_ALIVE, "keep-alive");
|
||||
def_const!(TRAILERS, "trailers");
|
||||
@@ -66,13 +73,13 @@ pub fn rebuild_http_client() {
|
||||
/// # 返回
|
||||
///
|
||||
/// * `reqwest::RequestBuilder` - 配置好的请求构建器
|
||||
pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> RequestBuilder {
|
||||
pub fn build_client(
|
||||
auth_token: &str,
|
||||
checksum: &str,
|
||||
url: &str,
|
||||
is_stream: bool,
|
||||
) -> RequestBuilder {
|
||||
let trace_id = Uuid::new_v4().to_string();
|
||||
let url = if is_search {
|
||||
&*CURSOR_API2_CHAT_WEB_URL
|
||||
} else {
|
||||
&*CURSOR_API2_CHAT_URL
|
||||
};
|
||||
|
||||
let client = if *USE_REVERSE_PROXY {
|
||||
HTTP_CLIENT
|
||||
@@ -81,14 +88,18 @@ pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> Reques
|
||||
.header(HOST, &*REVERSE_PROXY_HOST)
|
||||
.header(PROXY_HOST, CURSOR_API2_HOST)
|
||||
} else {
|
||||
HTTP_CLIENT
|
||||
.read()
|
||||
.post(url)
|
||||
.header(HOST, CURSOR_API2_HOST)
|
||||
HTTP_CLIENT.read().post(url).header(HOST, CURSOR_API2_HOST)
|
||||
};
|
||||
|
||||
client
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO)
|
||||
.header(
|
||||
CONTENT_TYPE,
|
||||
if is_stream {
|
||||
CONTENT_TYPE_CONNECT_PROTO
|
||||
} else {
|
||||
CONTENT_TYPE_PROTO
|
||||
},
|
||||
)
|
||||
.bearer_auth(auth_token)
|
||||
.header("connect-accept-encoding", ENCODINGS)
|
||||
.header("connect-protocol-version", ONE)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod health;
|
||||
pub mod config;
|
||||
pub mod token;
|
||||
pub mod userinfo;
|
||||
|
||||
@@ -16,8 +16,8 @@ pub enum ApiStatus {
|
||||
Success,
|
||||
#[serde(rename = "error")]
|
||||
Error,
|
||||
#[serde(rename = "failed")]
|
||||
Failed,
|
||||
#[serde(rename = "failure")]
|
||||
Failure,
|
||||
}
|
||||
|
||||
// #[derive(Serialize)]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies};
|
||||
use crate::app::model::{PageContent, Proxies, UsageCheck, VisionAbility};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ConfigData {
|
||||
|
@@ -25,10 +25,10 @@ impl ChatError {
|
||||
};
|
||||
|
||||
ErrorResponse {
|
||||
status: super::ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some(error.to_string()),
|
||||
message: Some(message),
|
||||
status: super::ApiStatus::Error,
|
||||
code: None,
|
||||
error: Some(error.to_string()),
|
||||
message: Some(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ pub struct HealthCheckResponse {
|
||||
pub uptime: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stats: Option<SystemStats>,
|
||||
pub models: Vec<&'static str>,
|
||||
pub models: Vec<String>,
|
||||
pub endpoints: Vec<&'static str>,
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum GetUserInfo {
|
||||
Usage(TokenProfile),
|
||||
Usage(Box<TokenProfile>),
|
||||
Error { error: String },
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct ModelUsage {
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub requests_total: Option<u32>,
|
||||
pub total_requests: Option<u32>,
|
||||
#[serde(rename(deserialize = "numTokens", serialize = "tokens"))]
|
||||
pub num_tokens: u32,
|
||||
#[serde(
|
||||
@@ -74,6 +74,8 @@ pub struct UsageProfile {
|
||||
pub standard: ModelUsage,
|
||||
#[serde(rename(deserialize = "gpt-4-32k"))]
|
||||
pub unknown: ModelUsage,
|
||||
#[serde(rename(deserialize = "startOfMonth"))]
|
||||
pub start_of_month: DateTime<Local>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||
|
@@ -1,15 +1,28 @@
|
||||
mod checksum;
|
||||
use ::base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use ::base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
pub use checksum::*;
|
||||
mod token;
|
||||
use prost::Message as _;
|
||||
pub use token::*;
|
||||
mod base64;
|
||||
pub use base64::*;
|
||||
|
||||
use super::model::{token::TokenPayload, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}};
|
||||
use crate::app::{
|
||||
constant::{COMMA, FALSE, TRUE},
|
||||
lazy::{TOKEN_DELIMITER, USE_COMMA_DELIMITER},
|
||||
use super::model::{
|
||||
token::TokenPayload,
|
||||
userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile},
|
||||
};
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{COMMA, FALSE, TRUE},
|
||||
lazy::{CURSOR_API2_CHAT_MODELS_URL, TOKEN_DELIMITER, USE_COMMA_DELIMITER},
|
||||
},
|
||||
chat::{
|
||||
aiserver::v1::{AvailableModelsRequest, AvailableModelsResponse},
|
||||
constant::{
|
||||
ANTHROPIC, CREATED, CURSOR, DEEPSEEK, GOOGLE, MODEL_OBJECT, OPENAI, UNKNOWN, XAI,
|
||||
},
|
||||
model::Model,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
|
||||
@@ -124,6 +137,59 @@ pub async fn get_user_profile(auth_token: &str) -> Option<UserProfile> {
|
||||
Some(user_profile)
|
||||
}
|
||||
|
||||
pub async fn get_available_models(auth_token: &str, checksum: &str) -> Option<Vec<Model>> {
|
||||
let client =
|
||||
super::client::build_client(auth_token, checksum, &CURSOR_API2_CHAT_MODELS_URL, false);
|
||||
let request = AvailableModelsRequest {
|
||||
is_nightly: true,
|
||||
include_long_context_models: true,
|
||||
};
|
||||
let response = client
|
||||
.body(encode_message(&request, false).unwrap())
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.bytes()
|
||||
.await
|
||||
.ok()?;
|
||||
let available_models = AvailableModelsResponse::decode(response.as_ref()).ok()?;
|
||||
Some(
|
||||
available_models
|
||||
.models
|
||||
.into_iter()
|
||||
.map(|model| Model {
|
||||
id: model.name.clone(),
|
||||
created: CREATED,
|
||||
object: MODEL_OBJECT,
|
||||
owned_by: {
|
||||
let mut chars = model.name.chars();
|
||||
match chars.next() {
|
||||
Some('g') => match chars.next() {
|
||||
Some('p') => OPENAI, // g + p → "gp" (gpt)
|
||||
Some('e') => GOOGLE, // g + e → "ge" (gemini)
|
||||
Some('r') => XAI, // g + r → "ge" (grok)
|
||||
_ => UNKNOWN,
|
||||
},
|
||||
Some('o') => match chars.next() {
|
||||
// o 开头需要二次判断
|
||||
Some('1') | Some('3') => OPENAI, // o1/o3 系列
|
||||
_ => UNKNOWN,
|
||||
},
|
||||
Some('c') => match chars.next() {
|
||||
Some('l') => ANTHROPIC, // c + l → "cl" (claude)
|
||||
Some('u') => CURSOR, // c + u → "cu" (cursor)
|
||||
_ => UNKNOWN,
|
||||
},
|
||||
Some('d') if chars.next() == Some('e') => DEEPSEEK, // d + e → "de" (deepseek)
|
||||
// 其他情况
|
||||
_ => UNKNOWN,
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> {
|
||||
// 尝试使用自定义分隔符查找
|
||||
let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER);
|
||||
@@ -277,5 +343,33 @@ pub fn tokeninfo_to_token(info: &key_config::TokenInfo) -> Option<(String, Strin
|
||||
};
|
||||
|
||||
// 组合 token
|
||||
Some((format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature), generate_checksum(&device_id, mac_addr.as_deref())))
|
||||
Some((
|
||||
format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature),
|
||||
generate_checksum(&device_id, mac_addr.as_deref()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn encode_message(
|
||||
message: &impl prost::Message,
|
||||
with_gzip: bool,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut encoded = Vec::new();
|
||||
message.encode(&mut encoded)?;
|
||||
|
||||
if !with_gzip {
|
||||
return Ok(encoded);
|
||||
}
|
||||
// 构造 5 字节头部 [0x00, len_be_bytes...]
|
||||
let mut header = Vec::with_capacity(5);
|
||||
header.push(0x00); // 压缩标记位
|
||||
|
||||
// 将长度转换为 u32 大端字节(显式长度检查)
|
||||
let len = u32::try_from(encoded.len()).map_err(|_| "Message length exceeds u32::MAX")?; // 明确错误类型
|
||||
header.extend_from_slice(&len.to_be_bytes());
|
||||
|
||||
// 组合最终数据
|
||||
let mut result = header;
|
||||
result.extend(encoded);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use rand::Rng;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use rand::Rng as _;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub fn generate_hash() -> String {
|
||||
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let random_bytes = rand::rng().random::<[u8; 32]>();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(random_bytes);
|
||||
hex::encode(hasher.finalize())
|
||||
|
@@ -1,24 +1,9 @@
|
||||
use super::generate_checksum_with_repair;
|
||||
use crate::app::{
|
||||
constant::{COMMA, EMPTY_STRING},
|
||||
lazy::TOKEN_LIST_FILE,
|
||||
model::TokenInfo,
|
||||
};
|
||||
use crate::app::{constant::COMMA, model::TokenInfo};
|
||||
use crate::common::model::token::TokenPayload;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use chrono::{DateTime, Local, TimeZone};
|
||||
|
||||
// 规范化文件内容并写入
|
||||
fn normalize_and_write(content: &str, file_path: &str) -> String {
|
||||
let normalized = content.replace("\r\n", "\n");
|
||||
if normalized != content {
|
||||
if let Err(e) = std::fs::write(file_path, &normalized) {
|
||||
eprintln!("警告: 无法更新规范化的文件: {}", e);
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
// 解析token
|
||||
pub fn parse_token(token_part: &str) -> String {
|
||||
// 查找最后一个:或%3A的位置
|
||||
@@ -38,82 +23,41 @@ pub fn parse_token(token_part: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Token 加载函数
|
||||
pub fn load_tokens() -> Vec<TokenInfo> {
|
||||
let token_list_file = TOKEN_LIST_FILE.as_str();
|
||||
|
||||
// 确保文件存在
|
||||
if !std::path::Path::new(&token_list_file).exists() {
|
||||
if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) {
|
||||
eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 读取和规范化 token-list 文件
|
||||
let token_map: std::collections::HashMap<String, String> =
|
||||
match std::fs::read_to_string(&token_list_file) {
|
||||
Ok(content) => {
|
||||
let normalized = normalize_and_write(&content, &token_list_file);
|
||||
normalized
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(COMMA).collect();
|
||||
match parts[..] {
|
||||
[token_part, checksum] => {
|
||||
let token = parse_token(token_part);
|
||||
Some((token, generate_checksum_with_repair(checksum)))
|
||||
}
|
||||
_ => {
|
||||
eprintln!("警告: 忽略无效的token-list行: {}", line);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
// Token 加载函数,支持从字符串内容加载
|
||||
pub fn load_tokens_from_content(content: &str) -> Vec<TokenInfo> {
|
||||
let token_map: std::collections::HashMap<String, String> = content
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("警告: 无法读取token-list文件: {}", e);
|
||||
std::collections::HashMap::new()
|
||||
|
||||
let parts: Vec<&str> = line.split(COMMA).collect();
|
||||
match parts[..] {
|
||||
[token_part, checksum] => {
|
||||
let token = parse_token(token_part);
|
||||
Some((token, generate_checksum_with_repair(checksum)))
|
||||
}
|
||||
_ => {
|
||||
eprintln!("警告: 忽略无效的token-list行: {}", line);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 更新 token-list 文件
|
||||
let token_list_content = token_map
|
||||
.iter()
|
||||
.map(|(token, checksum)| format!("{},{}", token, checksum))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
if let Err(e) = std::fs::write(&token_list_file, token_list_content) {
|
||||
eprintln!("警告: 无法更新token-list文件: {}", e);
|
||||
}
|
||||
|
||||
// 转换为 TokenInfo vector
|
||||
token_map
|
||||
.into_iter()
|
||||
.map(|(token, checksum)| TokenInfo {
|
||||
token: token.clone(),
|
||||
token,
|
||||
checksum,
|
||||
profile: None,
|
||||
tags: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn write_tokens(token_infos: &[TokenInfo], file_path: &str) -> std::io::Result<()> {
|
||||
let content = token_infos
|
||||
.iter()
|
||||
.map(|info| format!("{},{}", info.token, info.checksum))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
std::fs::write(file_path, content)
|
||||
}
|
||||
|
||||
pub(super) const HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
pub(super) const ISSUER: &str = "https://authentication.cursor.sh";
|
||||
pub(super) const SCOPE: &str = "openid profile email offline_access";
|
||||
|
31
src/main.rs
31
src/main.rs
@@ -8,16 +8,16 @@ use app::{
|
||||
PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH,
|
||||
ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM,
|
||||
ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH,
|
||||
ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH,
|
||||
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
|
||||
ROUTE_TOKENS_RELOAD_PATH, ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
|
||||
ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKEN_TAGS_UPDATE_PATH,
|
||||
ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
|
||||
ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
|
||||
},
|
||||
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
|
||||
model::*,
|
||||
};
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use chat::{
|
||||
route::{
|
||||
@@ -25,12 +25,12 @@ use chat::{
|
||||
handle_build_key, handle_build_key_page, handle_config_page, handle_delete_tokens,
|
||||
handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header,
|
||||
handle_get_tokens, handle_health, handle_logs, handle_logs_post, handle_readme,
|
||||
handle_reload_tokens, handle_root, handle_static, handle_tokens_page, handle_update_tokens,
|
||||
handle_user_info,
|
||||
handle_root, handle_static, handle_tokens_page, handle_update_token_tags,
|
||||
handle_update_tokens, handle_user_info,
|
||||
},
|
||||
service::{handle_chat, handle_models},
|
||||
};
|
||||
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
|
||||
use common::utils::{parse_string_from_env, parse_usize_from_env};
|
||||
use std::sync::Arc;
|
||||
use tokio::signal;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -58,11 +58,8 @@ async fn main() {
|
||||
// 初始化全局配置
|
||||
AppConfig::init();
|
||||
|
||||
// 加载 tokens
|
||||
let token_infos = load_tokens();
|
||||
|
||||
// 初始化应用状态
|
||||
let state = Arc::new(Mutex::new(AppState::new(token_infos)));
|
||||
let state = Arc::new(Mutex::new(AppState::new()));
|
||||
|
||||
// 尝试加载保存的配置
|
||||
if let Err(e) = AppConfig::load_saved_config() {
|
||||
@@ -89,7 +86,7 @@ async fn main() {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(wait_duration)).await;
|
||||
|
||||
let mut app_state = state_for_reload.lock().await;
|
||||
app_state.update_checksum();
|
||||
app_state.token_manager.update_checksum();
|
||||
// debug_println!("checksum 自动刷新: {}", next_reload);
|
||||
}
|
||||
});
|
||||
@@ -130,12 +127,12 @@ async fn main() {
|
||||
println!("配置已保存");
|
||||
}
|
||||
|
||||
// 保存日志
|
||||
// 保存状态
|
||||
let state = state_for_shutdown.lock().await;
|
||||
if let Err(e) = state.save_logs().await {
|
||||
eprintln!("保存日志失败: {}", e);
|
||||
if let Err(e) = state.save_state().await {
|
||||
eprintln!("保存状态失败: {}", e);
|
||||
} else {
|
||||
println!("日志已保存");
|
||||
println!("状态已保存");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,7 +143,6 @@ async fn main() {
|
||||
.route(ROUTE_TOKENS_PATH, get(handle_tokens_page))
|
||||
.route(ROUTE_MODELS_PATH.as_str(), get(handle_models))
|
||||
.route(ROUTE_TOKENS_GET_PATH, post(handle_get_tokens))
|
||||
.route(ROUTE_TOKENS_RELOAD_PATH, post(handle_reload_tokens))
|
||||
.route(ROUTE_TOKENS_UPDATE_PATH, post(handle_update_tokens))
|
||||
.route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens))
|
||||
.route(ROUTE_TOKENS_DELETE_PATH, post(handle_delete_tokens))
|
||||
@@ -167,6 +163,7 @@ async fn main() {
|
||||
.route(ROUTE_USER_INFO_PATH, post(handle_user_info))
|
||||
.route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page))
|
||||
.route(ROUTE_BUILD_KEY_PATH, post(handle_build_key))
|
||||
.route(ROUTE_TOKEN_TAGS_UPDATE_PATH, post(handle_update_token_tags))
|
||||
.layer(RequestBodyLimitLayer::new(
|
||||
1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2),
|
||||
))
|
||||
|
108
static/logs.html
108
static/logs.html
@@ -376,6 +376,23 @@
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 图表样式 */
|
||||
.chart-container {
|
||||
background: var(--card-background);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing);
|
||||
border: 1px solid var(--border-color);
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -418,6 +435,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加图表容器 -->
|
||||
<div class="chart-container">
|
||||
<canvas id="requestsChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table id="logsTable">
|
||||
<thead>
|
||||
@@ -529,8 +551,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加 Chart.js 库 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.js" integrity="sha512-0im+NZpDrlsC+p6iSc13cqlMNPqdT6e0hUF8NAaxdaGOmPuV9DdVpWYOCHHrMQNVDb2TByQoDbHx34MT6g16ZA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script>
|
||||
let refreshInterval;
|
||||
let requestsChart;
|
||||
|
||||
function updateStats(data) {
|
||||
document.getElementById('totalRequests').textContent = data.total || 0;
|
||||
@@ -676,9 +702,91 @@
|
||||
return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('requestsChart').getContext('2d');
|
||||
requestsChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: '每小时请求数',
|
||||
data: [],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '24小时请求统计'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
function updateChart(data) {
|
||||
if (!requestsChart) {
|
||||
initChart();
|
||||
}
|
||||
|
||||
// 按小时统计请求数量
|
||||
const hourlyStats = new Map();
|
||||
const now = new Date();
|
||||
const past24Hours = new Date(now - 24 * 60 * 60 * 1000);
|
||||
|
||||
// 初始化24小时的时间段
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const hour = new Date(now - i * 60 * 60 * 1000);
|
||||
const hourKey = hour.toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric'
|
||||
});
|
||||
hourlyStats.set(hourKey, 0);
|
||||
}
|
||||
|
||||
// 统计每小时的请求数
|
||||
data.logs.forEach(log => {
|
||||
const logTime = new Date(log.timestamp);
|
||||
if (logTime >= past24Hours) {
|
||||
const hourKey = logTime.toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric'
|
||||
});
|
||||
if (hourlyStats.has(hourKey)) {
|
||||
hourlyStats.set(hourKey, hourlyStats.get(hourKey) + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 转换为图表数据
|
||||
const sortedHours = Array.from(hourlyStats.keys()).reverse();
|
||||
const counts = sortedHours.map(hour => hourlyStats.get(hour));
|
||||
|
||||
requestsChart.data.labels = sortedHours;
|
||||
requestsChart.data.datasets[0].data = counts;
|
||||
requestsChart.update();
|
||||
}
|
||||
|
||||
function updateTable(data) {
|
||||
const tbody = document.getElementById('logsBody');
|
||||
updateStats(data);
|
||||
updateChart(data);
|
||||
|
||||
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ? `<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` : '-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
|
||||
}
|
||||
|
@@ -469,7 +469,10 @@
|
||||
}
|
||||
|
||||
const data = await makeAuthenticatedRequest('/tokens/add', {
|
||||
body: JSON.stringify(tokenList)
|
||||
body: JSON.stringify({
|
||||
tokens: tokenList,
|
||||
tags: []
|
||||
})
|
||||
});
|
||||
|
||||
if (data) {
|
||||
|
84
tools/get-token/Cargo.lock
generated
84
tools/get-token/Cargo.lock
generated
@@ -15,19 +15,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.5"
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -51,7 +48,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
name = "get-token"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"rusqlite",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -72,17 +71,28 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
@@ -97,18 +107,18 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -128,10 +138,42 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
name = "ryu"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -141,9 +183,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.91"
|
||||
version = "2.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
|
||||
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -152,9 +194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
|
@@ -4,7 +4,10 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] }
|
||||
base64 = "0.22.1"
|
||||
# rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] }
|
||||
rusqlite = "0.32.1"
|
||||
serde_json = "1.0.138"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
@@ -1,28 +1,150 @@
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use rusqlite::Connection;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn get_machine_id() -> String {
|
||||
if cfg!(windows) {
|
||||
let output = Command::new("REG")
|
||||
.args(&[
|
||||
"QUERY",
|
||||
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
|
||||
"/v",
|
||||
"MachineGuid",
|
||||
])
|
||||
.output()
|
||||
.expect("无法执行 REG 命令");
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.find(|line| line.contains("MachineGuid"))
|
||||
.and_then(|line| line.split_whitespace().last())
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let output = Command::new("ioreg")
|
||||
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
|
||||
.output()
|
||||
.expect("无法执行 ioreg 命令");
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.find(|line| line.contains("IOPlatformUUID"))
|
||||
.and_then(|line| line.split("\"").nth(3))
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
} else if cfg!(target_os = "linux") {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :")
|
||||
.output()
|
||||
.expect("无法获取 machine-id");
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("kenv -q smbios.system.uuid || sysctl -n kern.hostuuid")
|
||||
.output()
|
||||
.expect("无法获取 UUID");
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn obfuscate_bytes(bytes: &mut [u8]) {
|
||||
let mut prev: u8 = 165;
|
||||
for (idx, byte) in bytes.iter_mut().enumerate() {
|
||||
let old_value = *byte;
|
||||
*byte = (old_value ^ prev).wrapping_add((idx % 256) as u8);
|
||||
prev = *byte;
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_timestamp_header() -> String {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
/ 1_000;
|
||||
|
||||
let mut timestamp_bytes = vec![
|
||||
((timestamp >> 8) & 0xFF) as u8,
|
||||
(timestamp & 0xFF) as u8,
|
||||
((timestamp >> 24) & 0xFF) as u8,
|
||||
((timestamp >> 16) & 0xFF) as u8,
|
||||
((timestamp >> 8) & 0xFF) as u8,
|
||||
(timestamp & 0xFF) as u8,
|
||||
];
|
||||
|
||||
obfuscate_bytes(&mut timestamp_bytes);
|
||||
BASE64.encode(×tamp_bytes)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let home_dir = env::var("HOME")
|
||||
.or_else(|_| env::var("USERPROFILE"))
|
||||
.unwrap();
|
||||
let db_path = if cfg!(target_os = "windows") {
|
||||
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb")
|
||||
} else if cfg!(target_os = "linux") {
|
||||
PathBuf::from(home_dir).join(".config/Cursor/User/globalStorage/state.vscdb")
|
||||
} else {
|
||||
PathBuf::from(home_dir)
|
||||
let db_path = if cfg!(windows) {
|
||||
let app_data = env::var("APPDATA").unwrap_or_else(|_| {
|
||||
let profile = env::var("USERPROFILE").expect("未找到 USERPROFILE 环境变量");
|
||||
PathBuf::from(profile)
|
||||
.join("AppData")
|
||||
.join("Roaming")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
PathBuf::from(app_data).join(r"Cursor\User\globalStorage\state.vscdb")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let home = env::var("HOME").expect("未找到 HOME 环境变量");
|
||||
PathBuf::from(home)
|
||||
.join("Library/Application Support/Cursor/User/globalStorage/state.vscdb")
|
||||
} else if cfg!(target_os = "linux") {
|
||||
let config_home = env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| {
|
||||
let home = env::var("HOME").expect("未找到 HOME 环境变量");
|
||||
format!("{}/.config", home)
|
||||
});
|
||||
PathBuf::from(config_home).join("Cursor/User/globalStorage/state.vscdb")
|
||||
} else {
|
||||
panic!("不支持的操作系统平台")
|
||||
};
|
||||
|
||||
match Connection::open(&db_path) {
|
||||
Ok(conn) => {
|
||||
match conn.query_row(
|
||||
let token = conn.query_row(
|
||||
"SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'",
|
||||
[],
|
||||
|row| row.get::<_, String>(0),
|
||||
) {
|
||||
Ok(token) => println!("访问令牌: {}", token.trim()),
|
||||
);
|
||||
|
||||
let storage_path = db_path.parent().unwrap().join("storage.json");
|
||||
let storage_content = std::fs::read_to_string(storage_path).unwrap_or_default();
|
||||
let storage_json: serde_json::Value =
|
||||
serde_json::from_str(&storage_content).unwrap_or_default();
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
println!("访问令牌: {}", token.trim());
|
||||
|
||||
// if let Some(machine_id) = storage_json["telemetry.machineId"].as_str() {
|
||||
// println!("machineId: {}", machine_id);
|
||||
// }
|
||||
|
||||
// if let Some(mac_machine_id) = storage_json["telemetry.macMachineId"].as_str() {
|
||||
// println!("macMachineId: {}", mac_machine_id);
|
||||
// }
|
||||
|
||||
let sys_machine_id = get_machine_id();
|
||||
println!("系统 machine-id: {}", sys_machine_id);
|
||||
|
||||
if let (Some(machine_id), Some(mac_machine_id)) = (
|
||||
storage_json["telemetry.machineId"].as_str(),
|
||||
storage_json["telemetry.macMachineId"].as_str(),
|
||||
) {
|
||||
println!(
|
||||
"校验和: {}{}/{}",
|
||||
generate_timestamp_header(),
|
||||
machine_id,
|
||||
mac_machine_id
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => eprintln!("获取令牌时出错: {}", err),
|
||||
}
|
||||
}
|
||||
|
219
tools/set-token/Cargo.lock
generated
Normal file
219
tools/set-token/Cargo.lock
generated
Normal file
@@ -0,0 +1,219 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "set-token"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
16
tools/set-token/Cargo.toml
Normal file
16
tools/set-token/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "set-token"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
regex = "1.11.1"
|
||||
rusqlite = "0.33.0"
|
||||
serde_json = "1.0.138"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
opt-level = 3
|
538
tools/set-token/src/main.rs
Normal file
538
tools/set-token/src/main.rs
Normal file
@@ -0,0 +1,538 @@
|
||||
use rusqlite::{Connection, Result};
|
||||
use serde_json::{Value, from_str, to_string_pretty};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn get_cursor_path() -> PathBuf {
|
||||
let home = if cfg!(windows) {
|
||||
env::var("USERPROFILE").unwrap_or_else(|_| env::var("HOME").unwrap())
|
||||
} else {
|
||||
env::var("HOME").unwrap()
|
||||
};
|
||||
|
||||
let base_path = PathBuf::from(home);
|
||||
|
||||
if cfg!(windows) {
|
||||
base_path.join("AppData\\Roaming\\Cursor")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
base_path.join("Library/Application Support/Cursor")
|
||||
} else {
|
||||
base_path.join(".config/Cursor")
|
||||
}
|
||||
}
|
||||
|
||||
fn update_sqlite_tokens(
|
||||
refresh_token: &str,
|
||||
access_token: &str,
|
||||
email: &str,
|
||||
signup_type: &str,
|
||||
membership_type: &str,
|
||||
) -> Result<()> {
|
||||
let db_path = get_cursor_path().join("User/globalStorage/state.vscdb");
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// 获取原始值
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT key, value FROM ItemTable WHERE key IN (
|
||||
'cursorAuth/refreshToken',
|
||||
'cursorAuth/accessToken',
|
||||
'cursorAuth/cachedEmail',
|
||||
'cursorAuth/cachedSignUpType',
|
||||
'cursorAuth/stripeMembershipType'
|
||||
)",
|
||||
)?;
|
||||
|
||||
println!("\n原始值:");
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (key, value) = row?;
|
||||
println!("{}: {}", key, value);
|
||||
}
|
||||
|
||||
// 更新值
|
||||
conn.execute(
|
||||
"UPDATE ItemTable SET value = ? WHERE key = 'cursorAuth/refreshToken'",
|
||||
[refresh_token],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE ItemTable SET value = ? WHERE key = 'cursorAuth/accessToken'",
|
||||
[access_token],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE ItemTable SET value = ? WHERE key = 'cursorAuth/cachedEmail'",
|
||||
[email],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE ItemTable SET value = ? WHERE key = 'cursorAuth/cachedSignUpType'",
|
||||
[signup_type],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE ItemTable SET value = ? WHERE key = 'cursorAuth/stripeMembershipType'",
|
||||
[membership_type],
|
||||
)?;
|
||||
|
||||
println!("\n更新后的值:");
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT key, value FROM ItemTable WHERE key IN (
|
||||
'cursorAuth/refreshToken',
|
||||
'cursorAuth/accessToken',
|
||||
'cursorAuth/cachedEmail',
|
||||
'cursorAuth/cachedSignUpType',
|
||||
'cursorAuth/stripeMembershipType'
|
||||
)",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (key, value) = row?;
|
||||
println!("{}: {}", key, value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_storage_json(machine_ids: &[String; 4]) -> io::Result<()> {
|
||||
let storage_path = get_cursor_path().join("User/globalStorage/storage.json");
|
||||
let content = fs::read_to_string(&storage_path)?;
|
||||
let mut json: Value = from_str(&content)?;
|
||||
|
||||
if let Value::Object(ref mut map) = json {
|
||||
map.insert(
|
||||
"telemetry.macMachineId".to_string(),
|
||||
Value::String(machine_ids[0].clone()),
|
||||
);
|
||||
map.insert(
|
||||
"telemetry.sqmId".to_string(),
|
||||
Value::String(machine_ids[1].clone()),
|
||||
);
|
||||
map.insert(
|
||||
"telemetry.machineId".to_string(),
|
||||
Value::String(machine_ids[2].clone()),
|
||||
);
|
||||
map.insert(
|
||||
"telemetry.devDeviceId".to_string(),
|
||||
Value::String(machine_ids[3].clone()),
|
||||
);
|
||||
}
|
||||
|
||||
fs::write(storage_path, to_string_pretty(&json)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_valid_jwt(token: &str) -> bool {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
println!("警告: Token 格式不正确,应该包含3个由'.'分隔的部分");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否以 "ey" 开头
|
||||
if !token.starts_with("ey") {
|
||||
println!("警告: Token 应该以'ey'开头");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn is_valid_sha256(id: &str) -> bool {
|
||||
// SHA256 哈希是64个十六进制字符
|
||||
if id.len() != 64 {
|
||||
println!("警告: ID 长度应为64个字符");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否都是有效的十六进制字符
|
||||
if !id.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
println!("警告: ID 应只包含十六进制字符(0-9, a-f)");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn is_valid_sqm_id(id: &str) -> bool {
|
||||
// 格式应为 {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} (大写)
|
||||
if id.len() != 38 {
|
||||
println!("警告: SQM ID 格式不正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !id.starts_with('{') || !id.ends_with('}') {
|
||||
println!("警告: SQM ID 应该被花括号包围");
|
||||
return false;
|
||||
}
|
||||
|
||||
let uuid = &id[1..37];
|
||||
if !uuid
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
println!("警告: UUID 部分应为大写字母、数字和连字符");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn is_valid_device_id(id: &str) -> bool {
|
||||
// 格式应为 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
if id.len() != 36 {
|
||||
println!("警告: Device ID 格式不正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
println!("警告: Device ID 应为小写字母、数字和连字符");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn is_valid_email(email: &str) -> bool {
|
||||
if !email.contains('@') || !email.contains('.') {
|
||||
println!("警告: 邮箱格式不正确");
|
||||
return false;
|
||||
}
|
||||
let parts: Vec<&str> = email.split('@').collect();
|
||||
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
||||
println!("警告: 邮箱格式不正确");
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn is_valid_uuid(uuid: &str) -> bool {
|
||||
// UUID格式应为: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
if uuid.len() != 36 {
|
||||
println!("警告: UUID 格式不正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = uuid.split('-').collect();
|
||||
if parts.len() != 5
|
||||
|| parts[0].len() != 8
|
||||
|| parts[1].len() != 4
|
||||
|| parts[2].len() != 4
|
||||
|| parts[3].len() != 4
|
||||
|| parts[4].len() != 12
|
||||
{
|
||||
println!("警告: UUID 格式不正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !uuid.chars().all(|c| c.is_ascii_hexdigit() || c == '-') {
|
||||
println!("警告: UUID 应只包含十六进制字符(0-9, a-f)和连字符");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn create_uuid_launcher(uuid: &str) -> io::Result<()> {
|
||||
let tools_dir = get_cursor_path().join("tools/set-token");
|
||||
fs::create_dir_all(&tools_dir)?;
|
||||
|
||||
// 创建 inject.js
|
||||
let inject_js = format!(
|
||||
r#"// 保存原始 require
|
||||
const originalRequire = module.constructor.prototype.require;
|
||||
|
||||
// 重写 require 函数
|
||||
module.constructor.prototype.require = function(path) {{
|
||||
const result = originalRequire.apply(this, arguments);
|
||||
|
||||
// 检测目标模块
|
||||
if (path.includes('main.js')) {{
|
||||
// 保存原始函数
|
||||
const originalModule = result;
|
||||
|
||||
// 创建代理对象
|
||||
return new Proxy(originalModule, {{
|
||||
get(target, prop) {{
|
||||
// 拦截 execSync 调用
|
||||
if (prop === 'execSync') {{
|
||||
return function() {{
|
||||
// 返回自定义的 UUID
|
||||
const platform = process.platform;
|
||||
switch (platform) {{
|
||||
case 'darwin':
|
||||
return 'IOPlatformUUID="{}"';
|
||||
case 'win32':
|
||||
return ' HARDWARE\\DESCRIPTION\\System\\BIOS SystemProductID REG_SZ {}';
|
||||
case 'linux':
|
||||
case 'freebsd':
|
||||
return '{}';
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${{platform}}`);
|
||||
}}
|
||||
}};
|
||||
}}
|
||||
return target[prop];
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
return result;
|
||||
}};"#,
|
||||
uuid, uuid, uuid
|
||||
);
|
||||
|
||||
// 写入 inject.js
|
||||
fs::write(tools_dir.join("inject.js"), inject_js)?;
|
||||
|
||||
if cfg!(windows) {
|
||||
// 创建 Windows CMD 脚本
|
||||
let cmd_script = format!(
|
||||
"@echo off\r\n\
|
||||
set NODE_OPTIONS=--require \"%~dp0inject.js\"\r\n\
|
||||
start \"\" \"%LOCALAPPDATA%\\Programs\\Cursor\\Cursor.exe\""
|
||||
);
|
||||
fs::write(tools_dir.join("start-cursor.cmd"), cmd_script)?;
|
||||
|
||||
// 创建 Windows PowerShell 脚本
|
||||
let ps_script = format!(
|
||||
"$env:NODE_OPTIONS = \"--require `\"$PSScriptRoot\\inject.js`\"\"\r\n\
|
||||
Start-Process -FilePath \"$env:LOCALAPPDATA\\Programs\\Cursor\\Cursor.exe\""
|
||||
);
|
||||
fs::write(tools_dir.join("start-cursor.ps1"), ps_script)?;
|
||||
} else {
|
||||
// 创建 Shell 脚本
|
||||
let shell_script = format!(
|
||||
"#!/bin/bash\n\
|
||||
SCRIPT_DIR=\"$(cd \"$(dirname \"${{BASH_SOURCE[0]}}\")\" && pwd)\"\n\
|
||||
export NODE_OPTIONS=\"--require $SCRIPT_DIR/inject.js\"\n\
|
||||
if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n\
|
||||
open -a Cursor\n\
|
||||
else\n\
|
||||
cursor # Linux,根据实际安装路径调整\n\
|
||||
fi"
|
||||
);
|
||||
let script_path = tools_dir.join("start-cursor.sh");
|
||||
fs::write(&script_path, shell_script)?;
|
||||
|
||||
// 在类Unix系统上设置可执行权限
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&script_path)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&script_path, perms)?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n注入脚本已创建在: {}", tools_dir.display());
|
||||
println!("\n使用方法:");
|
||||
if cfg!(windows) {
|
||||
println!(
|
||||
"方法1: 双击运行 {}",
|
||||
tools_dir.join("start-cursor.cmd").display()
|
||||
);
|
||||
println!(
|
||||
"方法2: 在 PowerShell 中运行 {}",
|
||||
tools_dir.join("start-cursor.ps1").display()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"在终端中运行: {}",
|
||||
tools_dir.join("start-cursor.sh").display()
|
||||
);
|
||||
}
|
||||
println!("\n注意:每次启动 Cursor 时都需要使用这个脚本。");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
println!("\n请选择操作:");
|
||||
println!("0. 退出");
|
||||
println!("1. 更新 Token");
|
||||
println!("2. 更新设备 ID");
|
||||
println!("3. 创建自定义UUID启动脚本");
|
||||
|
||||
print!("请输入选项 (0-3): ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice).unwrap();
|
||||
|
||||
match choice.trim() {
|
||||
"0" => break,
|
||||
"1" => {
|
||||
let mut refresh_token = String::new();
|
||||
loop {
|
||||
print!("请输入 Refresh Token: ");
|
||||
io::stdout().flush().unwrap();
|
||||
refresh_token.clear();
|
||||
io::stdin().read_line(&mut refresh_token).unwrap();
|
||||
refresh_token = refresh_token.trim().to_string();
|
||||
|
||||
if is_valid_jwt(&refresh_token) {
|
||||
break;
|
||||
}
|
||||
println!("请重新输入正确格式的 Token");
|
||||
}
|
||||
|
||||
print!("Access Token 是否与 Refresh Token 相同? (y/n): ");
|
||||
io::stdout().flush().unwrap();
|
||||
let mut same = String::new();
|
||||
io::stdin().read_line(&mut same).unwrap();
|
||||
|
||||
let access_token = if same.trim().eq_ignore_ascii_case("y") {
|
||||
refresh_token.clone()
|
||||
} else {
|
||||
let mut access_token = String::new();
|
||||
loop {
|
||||
print!("请输入 Access Token: ");
|
||||
io::stdout().flush().unwrap();
|
||||
access_token.clear();
|
||||
io::stdin().read_line(&mut access_token).unwrap();
|
||||
access_token = access_token.trim().to_string();
|
||||
|
||||
if is_valid_jwt(&access_token) {
|
||||
break;
|
||||
}
|
||||
println!("请重新输入正确格式的 Token");
|
||||
}
|
||||
access_token
|
||||
};
|
||||
|
||||
let mut email = String::new();
|
||||
loop {
|
||||
print!("请输入邮箱: ");
|
||||
io::stdout().flush().unwrap();
|
||||
email.clear();
|
||||
io::stdin().read_line(&mut email).unwrap();
|
||||
email = email.trim().to_string();
|
||||
|
||||
if is_valid_email(&email) {
|
||||
break;
|
||||
}
|
||||
println!("请重新输入正确格式的邮箱");
|
||||
}
|
||||
|
||||
let mut signup_type = String::new();
|
||||
loop {
|
||||
println!("\n可选的注册类型:");
|
||||
println!("1. Auth_0");
|
||||
println!("2. Github");
|
||||
println!("3. Google");
|
||||
println!("4. unknown");
|
||||
println!("(WorkOS - 仅供展示,不可选择)");
|
||||
print!("请选择注册类型 (1-4): ");
|
||||
io::stdout().flush().unwrap();
|
||||
signup_type.clear();
|
||||
io::stdin().read_line(&mut signup_type).unwrap();
|
||||
|
||||
let signup_type_str = match signup_type.trim() {
|
||||
"1" => "Auth_0",
|
||||
"2" => "Github",
|
||||
"3" => "Google",
|
||||
"4" => "unknown",
|
||||
_ => continue,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
signup_type = signup_type_str;
|
||||
break;
|
||||
}
|
||||
|
||||
let mut membership_type = String::new();
|
||||
loop {
|
||||
println!("\n可选的会员类型:");
|
||||
println!("1. free");
|
||||
println!("2. pro");
|
||||
println!("3. enterprise");
|
||||
println!("4. free_trial");
|
||||
print!("请选择会员类型 (1-4): ");
|
||||
io::stdout().flush().unwrap();
|
||||
membership_type.clear();
|
||||
io::stdin().read_line(&mut membership_type).unwrap();
|
||||
|
||||
let membership_type_str = match membership_type.trim() {
|
||||
"1" => "free",
|
||||
"2" => "pro",
|
||||
"3" => "enterprise",
|
||||
"4" => "free_trial",
|
||||
_ => continue,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
membership_type = membership_type_str;
|
||||
break;
|
||||
}
|
||||
|
||||
match update_sqlite_tokens(
|
||||
&refresh_token,
|
||||
&access_token,
|
||||
&email,
|
||||
&signup_type,
|
||||
&membership_type,
|
||||
) {
|
||||
Ok(_) => println!("所有信息更新成功!"),
|
||||
Err(e) => println!("更新失败: {}", e),
|
||||
}
|
||||
}
|
||||
"2" => {
|
||||
let mut ids = Vec::new();
|
||||
let validators: [(Box<dyn Fn(&str) -> bool>, &str); 4] = [
|
||||
(Box::new(is_valid_sha256), "macMachineId"),
|
||||
(Box::new(is_valid_sqm_id), "sqmId"),
|
||||
(Box::new(is_valid_sha256), "machineId"),
|
||||
(Box::new(is_valid_device_id), "devDeviceId"),
|
||||
];
|
||||
|
||||
for (validator, name) in validators.iter() {
|
||||
loop {
|
||||
print!("请输入 {}: ", name);
|
||||
io::stdout().flush().unwrap();
|
||||
let mut id = String::new();
|
||||
io::stdin().read_line(&mut id).unwrap();
|
||||
let id = id.trim().to_string();
|
||||
|
||||
if validator(&id) {
|
||||
ids.push(id);
|
||||
break;
|
||||
}
|
||||
println!("请重新输入正确格式的 ID");
|
||||
}
|
||||
}
|
||||
|
||||
match update_storage_json(&ids.try_into().unwrap()) {
|
||||
Ok(_) => println!("设备 ID 更新成功!"),
|
||||
Err(e) => println!("更新失败: {}", e),
|
||||
}
|
||||
}
|
||||
"3" => {
|
||||
let mut uuid = String::new();
|
||||
loop {
|
||||
print!("请输入自定义 UUID (格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx): ");
|
||||
io::stdout().flush().unwrap();
|
||||
uuid.clear();
|
||||
io::stdin().read_line(&mut uuid).unwrap();
|
||||
uuid = uuid.trim().to_string();
|
||||
|
||||
if is_valid_uuid(&uuid) {
|
||||
break;
|
||||
}
|
||||
println!("请重新输入正确格式的 UUID");
|
||||
}
|
||||
|
||||
match create_uuid_launcher(&uuid) {
|
||||
Ok(_) => println!("启动脚本创建成功!"),
|
||||
Err(e) => println!("创建失败: {}", e),
|
||||
}
|
||||
}
|
||||
_ => println!("无效选项,请重试"),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user