v0.1.3-rc.5

This commit is contained in:
wisdgod
2025-02-24 08:50:37 +08:00
parent fb0de13712
commit 0e65370ca2
59 changed files with 8861 additions and 2505 deletions

View File

@@ -22,8 +22,8 @@ SHARED_TOKEN=
# 令牌文件路径(已弃用) # 令牌文件路径(已弃用)
# TOKEN_FILE=.token # TOKEN_FILE=.token
# 令牌列表文件路径 # 令牌列表文件路径(已弃用)
TOKEN_LIST_FILE=.tokens # TOKEN_LIST_FILE=.tokens
# 实验性是否启用慢速池true/false # 实验性是否启用慢速池true/false
ENABLE_SLOW_POOL=false ENABLE_SLOW_POOL=false
@@ -93,8 +93,11 @@ SERVICE_TIMEOUT=30
# 包含网络引用 # 包含网络引用
INCLUDE_WEB_REFERENCES=false 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
View File

@@ -16,7 +16,7 @@ node_modules
/cursor-api /cursor-api
/cursor-api.exe /cursor-api.exe
/release /release
/data
/*.py /*.py
/logs /logs
/dev* /dev*

309
Cargo.lock generated
View File

@@ -23,7 +23,7 @@ version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
"once_cell", "once_cell",
"version_check", "version_check",
] ]
@@ -69,9 +69,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.95" version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]] [[package]]
name = "async-compression" name = "async-compression"
@@ -220,9 +220,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli-decompressor" name = "brotli-decompressor"
version = "4.0.1" version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@@ -230,9 +230,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "bytecheck" name = "bytecheck"
@@ -276,15 +276,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.10" version = "1.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -333,9 +333,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -361,7 +361,7 @@ dependencies = [
[[package]] [[package]]
name = "cursor-api" name = "cursor-api"
version = "0.1.3-rc.4.3" version = "0.1.3-rc.5"
dependencies = [ dependencies = [
"axum", "axum",
"base64", "base64",
@@ -378,6 +378,7 @@ dependencies = [
"paste", "paste",
"prost", "prost",
"prost-build", "prost-build",
"prost-types",
"rand", "rand",
"regex", "regex",
"reqwest", "reqwest",
@@ -412,7 +413,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -438,9 +439,9 @@ dependencies = [
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
@@ -460,9 +461,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "faststr" name = "faststr"
version = "0.2.27" version = "0.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9154486833a83cb5d99de8c4d831314b8ae810dd4ef18d89ceb7a9c7c728dd74" checksum = "403ebc0cd0c6dbff1cae7098168eff6bac83fad5928b6e91f29388b8dbb61653"
dependencies = [ dependencies = [
"bytes", "bytes",
"rkyv 0.8.10", "rkyv 0.8.10",
@@ -481,9 +482,9 @@ dependencies = [
[[package]] [[package]]
name = "fixedbitset" name = "fixedbitset"
version = "0.4.2" version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flate2" name = "flate2"
@@ -575,7 +576,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -626,7 +627,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "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]] [[package]]
@@ -647,9 +660,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -727,9 +740,9 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.5" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
@@ -739,9 +752,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.5.2" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -948,7 +961,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -1017,9 +1030,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -1070,9 +1083,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]] [[package]]
name = "matchit" name = "matchit"
@@ -1103,9 +1116,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@@ -1118,7 +1131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -1130,29 +1143,29 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]] [[package]]
name = "munge" name = "munge"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64142d38c84badf60abf06ff9bd80ad2174306a5b11bd4706535090a30a419df" checksum = "8743b8dfaf66acac79aca9ff2440e8680fef745b6260e6a31d1772b14cfa2862"
dependencies = [ dependencies = [
"munge_macro", "munge_macro",
] ]
[[package]] [[package]]
name = "munge_macro" name = "munge_macro"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e" checksum = "66191390a55bb9830fa8468c12634442ea4199c6e390ddf08ddcace35b3cd5da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.12" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -1194,15 +1207,15 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.2" version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.68" version = "0.10.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"cfg-if", "cfg-if",
@@ -1221,20 +1234,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.104" version = "0.9.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -1279,9 +1292,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "petgraph" name = "petgraph"
version = "0.6.5" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [ dependencies = [
"fixedbitset", "fixedbitset",
"indexmap", "indexmap",
@@ -1324,7 +1337,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [ dependencies = [
"zerocopy", "zerocopy 0.7.35",
] ]
[[package]] [[package]]
@@ -1334,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -1348,9 +1361,9 @@ dependencies = [
[[package]] [[package]]
name = "prost" name = "prost"
version = "0.13.4" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive",
@@ -1358,9 +1371,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-build" name = "prost-build"
version = "0.13.4" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [ dependencies = [
"heck", "heck",
"itertools", "itertools",
@@ -1372,28 +1385,28 @@ dependencies = [
"prost", "prost",
"prost-types", "prost-types",
"regex", "regex",
"syn 2.0.96", "syn 2.0.98",
"tempfile", "tempfile",
] ]
[[package]] [[package]]
name = "prost-derive" name = "prost-derive"
version = "0.13.4" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.13.4" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [ dependencies = [
"prost", "prost",
] ]
@@ -1435,7 +1448,7 @@ checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -1470,20 +1483,20 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [ dependencies = [
"libc",
"rand_chacha", "rand_chacha",
"rand_core", "rand_core",
"zerocopy 0.8.20",
] ]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.3.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core",
@@ -1491,18 +1504,19 @@ dependencies = [
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.1",
"zerocopy 0.8.20",
] ]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.8" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
] ]
@@ -1524,7 +1538,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -1621,15 +1635,14 @@ dependencies = [
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.15",
"libc", "libc",
"spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -1689,7 +1702,7 @@ checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -1700,9 +1713,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.43" version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"errno", "errno",
@@ -1713,9 +1726,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.21" version = "0.23.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@@ -1735,9 +1748,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.10.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@@ -1758,9 +1771,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]] [[package]]
name = "schannel" name = "schannel"
@@ -1808,29 +1821,29 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.137" version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1909,9 +1922,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]] [[package]]
name = "socket2" name = "socket2"
@@ -1961,12 +1974,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@@ -1992,9 +1999,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.96" version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2018,7 +2025,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2063,13 +2070,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.15.0" version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"getrandom", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -2101,7 +2108,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2112,7 +2119,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2165,7 +2172,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2296,15 +2303,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
@@ -2337,11 +2344,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.12.1" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.1",
] ]
[[package]] [[package]]
@@ -2371,6 +2378,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.100"
@@ -2393,7 +2409,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -2428,7 +2444,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -2532,7 +2548,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2543,7 +2559,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@@ -2667,6 +2683,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 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]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"
@@ -2708,7 +2733,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"synstructure", "synstructure",
] ]
@@ -2719,7 +2744,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [ dependencies = [
"byteorder", "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]] [[package]]
@@ -2730,7 +2764,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "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]] [[package]]
@@ -2750,7 +2795,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"synstructure", "synstructure",
] ]
@@ -2779,7 +2824,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]

View File

@@ -1,47 +1,48 @@
[package] [package]
name = "cursor-api" name = "cursor-api"
version = "0.1.3-rc.4.3" version = "0.1.3-rc.5"
edition = "2021" edition = "2024"
authors = ["wisdgod <nav@wisdgod.com>"] authors = ["wisdgod <nav@wisdgod.com>"]
description = "OpenAI format compatibility layer for the Cursor API" description = "OpenAI format compatibility layer for the Cursor API"
repository = "https://github.com/wisdgod/cursor-api" repository = "https://github.com/wisdgod/cursor-api"
[build-dependencies] [build-dependencies]
prost-build = "0.13.4" prost-build = "^0.13"
sha2 = { version = "0.10.8", default-features = false } sha2 = { version = "^0.10.8", default-features = false }
serde_json = "1.0.134" serde_json = "^1.0"
[dependencies] [dependencies]
axum = { version = "0.8.1", features = ["json"] } axum = { version = "^0.8", features = ["json"] }
base64 = { version = "0.22.1", default-features = false, features = ["std"] } base64 = { version = "^0.22", default-features = false, features = ["std"] }
# brotli = { version = "7.0.0", default-features = false, features = ["std"] } # brotli = { version = "^7.0", default-features = false, features = ["std"] }
bytes = "1.9.0" bytes = "^1.10"
chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] } chrono = { version = "^0.4", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] }
dotenvy = "0.15.7" dotenvy = "^0.15"
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] } flate2 = { version = "^1.0", default-features = false, features = ["rust_backend"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] } futures = { version = "^0.3", default-features = false, features = ["std"] }
gif = { version = "0.13.1", default-features = false, features = ["std"] } gif = { version = "^0.13", default-features = false, features = ["std"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] } hex = { version = "^0.4", default-features = false, features = ["std"] }
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] } image = { version = "^0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
memmap2 = "0.9.5" memmap2 = "^0.9"
# openssl = { version = "0.10.68", features = ["vendored"] } # openssl = { version = "^0.10", features = ["vendored"] }
parking_lot = "0.12.3" parking_lot = "^0.12"
paste = "1.0.15" paste = "^1.0"
prost = "0.13.4" prost = "^0.13"
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } prost-types = "^0.13"
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } rand = { version = "^0.9", default-features = false, features = ["thread_rng"] }
reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } regex = { version = "^1.11", default-features = false, features = ["std", "perf"] }
rkyv = { version = "0.7.45", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] } reqwest = { version = "^0.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } rkyv = { version = "^0.7", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] }
serde_json = { package = "sonic-rs", version = "0.3.17" } serde = { version = "^1.0", default-features = false, features = ["std", "derive"] }
# serde_json = "1.0.137" serde_json = { package = "sonic-rs", version = "^0.3" }
sha2 = { version = "0.10.8", default-features = false } # serde_json = "^1.0"
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } sha2 = { version = "^0.10", default-features = false }
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] } sysinfo = { version = "^0.33", default-features = false, features = ["system"] }
tokio-stream = { version = "0.1.17", features = ["time"] } tokio = { version = "^1.43", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] }
tower-http = { version = "0.6.2", features = ["cors", "limit"] } tokio-stream = { version = "^0.1", features = ["time"] }
url = { version = "2.5.4", default-features = false } tower-http = { version = "^0.6", features = ["cors", "limit"] }
uuid = { version = "1.12.1", features = ["v4"] } url = { version = "^2.5", default-features = false }
uuid = { version = "^1.14", features = ["v4"] }
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -1,5 +1,5 @@
ARG TARGETARCH 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 ARG TARGETARCH
@@ -10,13 +10,7 @@ RUN apt-get update && \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY . . COPY . .
RUN case "$TARGETARCH" in \ 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
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 FROM --platform=linux/${TARGETARCH} debian:bookworm-slim

View File

@@ -1,6 +1,6 @@
# Dockerfile.cross # Dockerfile.cross
FROM --platform=linux/amd64 rust:1.84.0-slim-bookworm FROM --platform=linux/amd64 rust:1-slim-bookworm
WORKDIR /app WORKDIR /app

View File

@@ -1,6 +1,6 @@
# Dockerfile.cross # Dockerfile.cross
FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm FROM --platform=linux/arm64 rust:1-slim-bookworm
WORKDIR /app WORKDIR /app

102
README.md
View File

@@ -59,17 +59,18 @@ gpt-4o-128k
gemini-1.5-flash-500k gemini-1.5-flash-500k
claude-3-haiku-200k claude-3-haiku-200k
claude-3-5-sonnet-200k claude-3-5-sonnet-200k
claude-3-5-sonnet-20241022
gpt-4o-mini gpt-4o-mini
o1-mini o1-mini
o1-preview o1-preview
o1 o1
claude-3.5-haiku claude-3.5-haiku
gemini-exp-1206 gemini-2.0-pro-exp
gemini-2.0-flash-thinking-exp gemini-2.0-flash-thinking-exp
gemini-2.0-flash-exp gemini-2.0-flash
deepseek-v3 deepseek-v3
deepseek-r1 deepseek-r1
o3-mini
grok-2
``` ```
## 接口说明 ## 接口说明
@@ -148,6 +149,32 @@ data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"
data: [DONE] data: [DONE]
``` ```
### 获取模型列表
* 接口地址: `/v1/models`
* 请求方法: GET
* 认证方式: Bearer Token
#### 响应格式
```json
{
"object": "list",
"data": [
{
"id": "string",
"object": "model",
"created": number,
"owned_by": "string"
}
]
}
```
#### 更新模型列表说明
每次携带Token时都会拉取最新的模型列表与上次更新需距离至少30分钟。
### Token管理接口 ### Token管理接口
#### 简易Token信息管理页面 #### 简易Token信息管理页面
@@ -194,7 +221,8 @@ data: [DONE]
"tokens": number, "tokens": number,
"max_requests": number, "max_requests": number,
"max_tokens": number "max_tokens": number
} },
"start_of_month": "string"
}, },
"user": { "user": {
"email": "string", "email": "string",
@@ -260,12 +288,15 @@ data: [DONE]
* 请求格式: * 请求格式:
```json ```json
[ {
{ "tokens": [
"token": "string", {
"checksum": "string" // 可选,如果不提供将自动生成 "token": "string",
} "checksum": "string" // 可选,如果不提供将自动生成
] }
],
"tags": ["string"]
}
``` ```
* 响应格式: * 响应格式:
@@ -308,6 +339,29 @@ data: [DONE]
- failed_tokens: 返回未找到的token列表 - failed_tokens: 返回未找到的token列表
- detailed: 返回完整信息包括updated_tokens和failed_tokens - detailed: 返回完整信息包括updated_tokens和failed_tokens
#### 更新Tokens标签
* 接口地址: `/tokens/tags/update`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
```json
{
"tokens": ["string"],
"tags": ["string"]
}
```
* 响应格式:
```json
{
"status": "success",
"message": "string" // "标签更新成功"
}
```
#### 构建API Key #### 构建API Key
* 接口地址: `/build-key` * 接口地址: `/build-key`
@@ -354,7 +408,7 @@ data: [DONE]
|------|------| |------|------|
| 提取关键信息,生成更短的密钥 | 可能存在版本兼容性问题 | | 提取关键信息,生成更短的密钥 | 可能存在版本兼容性问题 |
| 支持携带自定义配置 | 增加了程序复杂度 | | 支持携带自定义配置 | 增加了程序复杂度 |
| 采用非常规编码方式,提升安全性 | | | 采用非常规编码方式,提升安全性 | 项目是开源的,安全性的提升相当于没有 |
| 更容易验证Key的合法性 | | | 更容易验证Key的合法性 | |
| 取消预校验带来轻微性能提升 | | | 取消预校验带来轻微性能提升 | |
@@ -470,26 +524,6 @@ data: [DONE]
### 其他接口 ### 其他接口
#### 获取模型列表
* 接口地址: `/v1/models`
* 请求方法: GET
* 响应格式:
```json
{
"object": "list",
"data": [
{
"id": "string",
"object": "model",
"created": number,
"owned_by": "string"
}
]
}
```
#### 获取一个随机hash #### 获取一个随机hash
* 接口地址: `/get-hash` * 接口地址: `/get-hash`
@@ -605,7 +639,8 @@ string
"tokens": number, "tokens": number,
"max_requests": number, "max_requests": number,
"max_tokens": number "max_tokens": number
} },
"start_of_month": "string"
}, },
"user": { "user": {
"email": "string", "email": "string",
@@ -673,7 +708,8 @@ string
"tokens": number, "tokens": number,
"max_requests": number, "max_requests": number,
"max_tokens": number "max_tokens": number
} },
"start_of_month": "string"
}, },
"user": { "user": {
"email": "string", "email": "string",

View File

@@ -116,7 +116,7 @@ fn minify_assets() -> Result<()> {
let files_to_update: Vec<_> = current_hashes let files_to_update: Vec<_> = current_hashes
.iter() .iter()
.filter(|(path, current_hash)| { .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(""); let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
// 为 README.md 和其他文件使用不同的输出路径检查 // 为 README.md 和其他文件使用不同的输出路径检查
@@ -136,9 +136,7 @@ fn minify_assets() -> Result<()> {
} }
// 检查原始文件是否发生变化 // 检查原始文件是否发生变化
saved_hashes saved_hashes.get(*path) != Some(*current_hash)
.get(*path)
.map_or(true, |saved_hash| saved_hash != *current_hash)
}) })
.map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned()) .map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned())
.collect(); .collect();
@@ -168,7 +166,7 @@ fn minify_assets() -> Result<()> {
fn main() -> Result<()> { fn main() -> Result<()> {
// Proto 文件处理 // 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"); println!("cargo:rerun-if-changed=src/chat/config/key.proto");
// 获取环境变量 PROTOC // 获取环境变量 PROTOC
let protoc_path = match std::env::var_os("PROTOC") { let protoc_path = match std::env::var_os("PROTOC") {
@@ -185,12 +183,13 @@ fn main() -> Result<()> {
config.protoc_executable(protoc_path); config.protoc_executable(protoc_path);
} }
// config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); // config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
config // config.enum_attribute(".aiserver.v1", "#[allow(clippy::enum_variant_names)]");
.compile_protos( // config
&["src/chat/aiserver/v1/lite.proto"], // .compile_protos(
&["src/chat/aiserver/v1/"], // &["src/chat/aiserver/v1/lite.proto"],
) // &["src/chat/aiserver/v1/"],
.unwrap(); // )
// .unwrap();
config config
.compile_protos(&["src/chat/config/key.proto"], &["src/chat/config/"]) .compile_protos(&["src/chat/config/key.proto"], &["src/chat/config/"])
.unwrap(); .unwrap();

View File

@@ -2,3 +2,4 @@ pub mod config;
pub mod constant; pub mod constant;
pub mod model; pub mod model;
pub mod lazy; pub mod lazy;
// pub mod rule;

View File

@@ -1,11 +1,11 @@
use super::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, model::AppConfig}; use super::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, model::AppConfig};
use crate::common::model::{ use crate::common::model::{
config::{ConfigData, ConfigUpdateRequest},
ApiStatus, ErrorResponse, NormalResponse, ApiStatus, ErrorResponse, NormalResponse,
config::{ConfigData, ConfigUpdateRequest},
}; };
use axum::{ use axum::{
http::{header::AUTHORIZATION, HeaderMap, StatusCode},
Json, Json,
http::{HeaderMap, StatusCode, header::AUTHORIZATION},
}; };
// 定义处理更新操作的宏 // 定义处理更新操作的宏
@@ -41,7 +41,7 @@ pub async fn handle_config_update(
.ok_or(( .ok_or((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Failed, status: ApiStatus::Failure,
code: Some(401), code: Some(401),
error: Some("未提供认证令牌".to_string()), error: Some("未提供认证令牌".to_string()),
message: None, message: None,
@@ -52,7 +52,7 @@ pub async fn handle_config_update(
return Err(( return Err((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Failed, status: ApiStatus::Failure,
code: Some(401), code: Some(401),
error: Some("无效的认证令牌".to_string()), error: Some("无效的认证令牌".to_string()),
message: None, message: None,
@@ -85,7 +85,7 @@ pub async fn handle_config_update(
return Err(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Failed, status: ApiStatus::Failure,
code: Some(500), code: Some(500),
error: Some(format!("更新页面内容失败: {}", e)), error: Some(format!("更新页面内容失败: {}", e)),
message: None, message: None,
@@ -119,7 +119,7 @@ pub async fn handle_config_update(
return Err(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Failed, status: ApiStatus::Failure,
code: Some(500), code: Some(500),
error: Some(format!("重置页面内容失败: {}", e)), error: Some(format!("重置页面内容失败: {}", e)),
message: None, message: None,
@@ -149,7 +149,7 @@ pub async fn handle_config_update(
_ => Err(( _ => Err((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Failed, status: ApiStatus::Failure,
code: Some(400), code: Some(400),
error: Some("无效的操作类型".to_string()), error: Some("无效的操作类型".to_string()),
message: None, message: None,

View File

@@ -1,83 +1,122 @@
macro_rules! def_pub_const { 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 = ','; pub const COMMA: char = ',';
def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION")); // Package related constants
// 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");
def_pub_const!( def_pub_const!(
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION => env!("CARGO_PKG_VERSION")
"text/plain;charset=utf-8" // PKG_NAME => env!("CARGO_PKG_NAME"),
); // PKG_DESCRIPTION => env!("CARGO_PKG_DESCRIPTION"),
def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8"); // PKG_AUTHORS => env!("CARGO_PKG_AUTHORS"),
def_pub_const!( // PKG_REPOSITORY => env!("CARGO_PKG_REPOSITORY")
CONTENT_TYPE_TEXT_JS_WITH_UTF8,
"text/javascript;charset=utf-8"
); );
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"); // Route related constants
def_pub_const!(CURSOR_HOST, "www.cursor.com"); def_pub_const!(
def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings"); 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!(DEFAULT_TOKEN_LIST_FILE_NAME => ".tokens");
def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk");
// def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); // Status constants
// def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); 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");

View File

@@ -1,10 +1,9 @@
use super::constant::{ use super::constant::{COMMA, CURSOR_API2_HOST, CURSOR_HOST, EMPTY_STRING};
COMMA, CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING,
};
use crate::common::utils::{ use crate::common::utils::{
parse_ascii_char_from_env, parse_bool_from_env, parse_string_from_env, parse_usize_from_env, 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}; use tokio::sync::{Mutex, OnceCell};
macro_rules! def_pub_static { 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!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING);
def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", 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_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX));
def_pub_static!( def_pub_static!(
ROUTE_CHAT_PATH, ROUTE_CHAT_PATH,
format!("{}/v1/chat/completions", *ROUTE_PREFIX) format!("{}/v1/chat/completions", *ROUTE_PREFIX)
); );
pub static START_TIME: LazyLock<chrono::DateTime<chrono::Local>> = pub static START_TIME: LazyLock<chrono::DateTime<Local>> = LazyLock::new(Local::now);
LazyLock::new(chrono::Local::now);
pub fn get_start_time() -> chrono::DateTime<chrono::Local> { pub fn get_start_time() -> chrono::DateTime<Local> {
*START_TIME *START_TIME
} }
@@ -114,6 +111,12 @@ def_cursor_api_url!(
"/aiserver.v1.AiService/StreamChatWeb" "/aiserver.v1.AiService/StreamChatWeb"
); );
def_cursor_api_url!(
CURSOR_API2_CHAT_MODELS_URL,
CURSOR_API2_HOST,
"/aiserver.v1.AiService/AvailableModels"
);
def_cursor_api_url!( def_cursor_api_url!(
CURSOR_API2_STRIPE_URL, CURSOR_API2_STRIPE_URL,
CURSOR_API2_HOST, 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"); def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me");
pub(super) static LOGS_FILE_PATH: LazyLock<String> = static DATA_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
LazyLock::new(|| parse_string_from_env("LOGS_FILE_PATH", "logs.bin")); 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> = pub(super) static CONFIG_FILE_PATH: LazyLock<PathBuf> =
LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin")); 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)); 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_export]
macro_rules! debug_println { macro_rules! debug_println {
($($arg:tt)*) => { ($($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 time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("{} - {}", time, format!($($arg)*)); let log_message = format!("{} - {}", time, format!($($arg)*));
use tokio::io::AsyncWriteExt as _; use tokio::io::AsyncWriteExt as _;
// 使用 tokio 的 spawn 在后台异步写入日志 // 使用 tokio 的 spawn 在后台异步写入日志
tokio::spawn(async move { 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 获取可变引用 // 使用 MutexGuard 获取可变引用
let mut file = log_file.lock().await; let mut file = log_file.lock().await;
if let Err(err) = file.write_all(log_message.as_bytes()).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> = pub static REQUEST_LOGS_LIMIT: LazyLock<usize> =
LazyLock::new(|| std::cmp::min(parse_usize_from_env("REQUEST_LOGS_LIMIT", 100), 2000)); 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(|| { pub static SERVICE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30); let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30);
u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30) u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30)

View File

@@ -1,30 +1,30 @@
use crate::{ 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, chat::model::Message,
common::{ common::{
client::rebuild_http_client, model::{ApiStatus, userinfo::TokenProfile},
model::{userinfo::TokenProfile, ApiStatus}, utils::{generate_checksum_with_repair, get_token_profile},
utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env},
}, },
}; };
use parking_lot::RwLock; use memmap2::{MmapMut, MmapOptions};
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::LazyLock; use std::{collections::HashSet, fs::OpenOptions};
mod usage_check; mod usage_check;
pub use usage_check::UsageCheck; pub use usage_check::UsageCheck;
mod vision_ability;
pub use vision_ability::VisionAbility;
mod config; mod config;
pub use config::AppConfig;
mod proxies; mod proxies;
pub use proxies::Proxies; pub use proxies::Proxies;
mod build_key; mod build_key;
pub use 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)] #[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)] #[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct Pages { pub struct Pages {
pub root_content: PageContent, pub root_content: PageContent,
@@ -104,232 +58,140 @@ pub struct Pages {
pub build_key_content: PageContent, pub build_key_content: PageContent,
} }
// 运行时状态 #[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct AppState { 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 total_requests: u64,
pub active_requests: u64, pub active_requests: u64,
pub error_requests: u64, pub error_requests: u64,
pub request_logs: Vec<RequestLog>, pub request_logs: Vec<RequestLog>,
pub token_infos: Vec<TokenInfo>,
} }
// 全局配置实例 #[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> = pub struct AppState {
LazyLock::new(|| RwLock::new(AppConfig::default())); pub token_manager: TokenManager,
pub request_manager: RequestStatsManager,
}
macro_rules! config_methods { impl TokenManager {
($($field:ident: $type:ty, $default:expr;)*) => { pub fn new(tokens: Vec<TokenInfo>) -> Self {
$( let mut tags = HashSet::new();
paste::paste! { for token in &tokens {
pub fn [<get_ $field>]() -> $type if let Some(token_tags) = &token.tags {
where tags.extend(token_tags.iter().cloned());
$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;
}
}
} }
)* }
};
}
macro_rules! config_methods_clone { Self { tokens, tags }
($($field:ident: $type:ty, $default:expr;)*) => { }
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Clone + PartialEq,
{
APP_CONFIG.read().$field.clone()
}
pub fn [<update_ $field>](value: $type) pub fn update_global_tags(&mut self, new_tags: &[String]) {
where // 将新标签添加到全局标签集合中
$type: Clone + PartialEq, self.tags.extend(new_tags.iter().cloned());
{ }
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
pub fn [<reset_ $field>]() pub fn update_tokens_tags(
where &mut self,
$type: Clone + PartialEq, tokens: Vec<String>,
{ new_tags: Vec<String>,
let default_value = $default; ) -> Result<(), &'static str> {
let current = Self::[<get_ $field>](); // 创建tokens的HashSet用于快速查找
if current != default_value { let tokens_set: HashSet<_> = tokens.iter().collect();
APP_CONFIG.write().$field = default_value;
} // 更新指定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() { self.tags = self
let mut config = APP_CONFIG.write(); .tokens
config.vision_ability = .iter()
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)); .filter_map(|t| t.tags.clone())
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false); .flatten()
config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false); .collect();
config.usage_check =
UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING)); Ok(())
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(); pub fn get_tokens_by_tag(&self, tag: &str) -> Vec<&TokenInfo> {
config.proxies = match std::env::var("PROXIES") { self.tokens
Ok(proxies) => Proxies::from_str(proxies.as_str()), .iter()
Err(_) => Proxies::default(), .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! { if file.metadata()?.len() > usize::MAX as u64 {
slow_pool: bool, false; return Err("Token文件过大".into());
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 mmap = unsafe { MmapOptions::new().map(&file)? };
let current = Self::get_share_token(); let archived = unsafe { rkyv::archived_root::<Self>(&mmap) };
if !current.is_empty() { Ok(archived.deserialize(&mut rkyv::Infallible)?)
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
} }
} }
impl AppState { impl RequestStatsManager {
pub fn new(token_infos: Vec<TokenInfo>) -> Self { pub fn new(request_logs: Vec<RequestLog>) -> Self {
// 尝试加载保存的日志
let request_logs = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(async { Self::load_saved_logs().await.unwrap_or_default() })
});
Self { Self {
total_requests: request_logs.len() as u64, total_requests: request_logs.len() as u64,
active_requests: 0, active_requests: 0,
@@ -338,14 +200,96 @@ impl AppState {
.filter(|log| matches!(log.status, LogStatus::Failed)) .filter(|log| matches!(log.status, LogStatus::Failed))
.count() as u64, .count() as u64,
request_logs, request_logs,
token_infos,
} }
} }
pub fn update_checksum(&mut self) { pub async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
for token_info in self.token_infos.iter_mut() { let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?;
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
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 信息 // 用于存储 token 信息
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] #[derive(Clone, Serialize, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct TokenInfo { pub struct TokenInfo {
pub token: String, pub token: String,
pub checksum: String, pub checksum: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<TokenProfile>, pub profile: Option<TokenProfile>,
pub tags: Option<Vec<String>>,
} }
// TokenUpdateRequest 结构体 // TokenUpdateRequest 结构体
@@ -431,6 +376,13 @@ pub struct TokenUpdateRequest {
pub tokens: String, pub tokens: String,
} }
#[derive(Deserialize)]
pub struct TokenAddRequest {
pub tokens: Vec<TokenAddRequestTokenInfo>,
#[serde(default)]
pub tags: Option<Vec<String>>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct TokenAddRequestTokenInfo { pub struct TokenAddRequestTokenInfo {
pub token: String, pub token: String,
@@ -484,3 +436,26 @@ pub struct TokensDeleteResponse {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub failed_tokens: Option<Vec<String>>, 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>,
}

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS}; use crate::{app::constant::COMMA, chat::constant::Models};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct BuildKeyRequest { pub struct BuildKeyRequest {
@@ -16,7 +16,7 @@ pub struct BuildKeyRequest {
} }
pub struct UsageCheckModelConfig { pub struct UsageCheckModelConfig {
pub model_type: UsageCheckModelType, pub model_type: UsageCheckModelType,
pub model_ids: Vec<&'static str>, pub model_ids: Vec<String>,
} }
impl<'de> Deserialize<'de> for UsageCheckModelConfig { impl<'de> Deserialize<'de> for UsageCheckModelConfig {
@@ -42,10 +42,7 @@ impl<'de> Deserialize<'de> for UsageCheckModelConfig {
.split(COMMA) .split(COMMA)
.filter_map(|model| { .filter_map(|model| {
let model = model.trim(); let model = model.trim();
AVAILABLE_MODELS Models::find_id(model)
.iter()
.find(|m| m.id == model)
.map(|m| m.id)
}) })
.collect() .collect()
}; };

View File

@@ -1,69 +1,248 @@
use memmap2::{MmapMut, MmapOptions}; use memmap2::{MmapMut, MmapOptions};
use rkyv::{archived_root, Deserialize as _}; use parking_lot::RwLock;
use std::fs::OpenOptions; 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 { // 静态配置
// 保存日志的方法 #[derive(Default, Clone)]
pub(crate) async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> { pub struct AppConfig {
// 序列化日志 vision_ability: VisionAbility,
let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?; 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() static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
.read(true) LazyLock::new(|| RwLock::new(AppConfig::default()));
.write(true)
.create(true)
.open(LOGS_FILE_PATH.as_str())?;
// 添加大小检查 macro_rules! config_methods {
if bytes.len() > usize::MAX / 2 { ($($field:ident: $type:ty, $default:expr;)*) => {
return Err("日志数据过大".into()); $(
} paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Copy + PartialEq,
{
APP_CONFIG.read().$field
}
// 设置文件大小 pub fn [<update_ $field>](value: $type)
file.set_len(bytes.len() as u64)?; where
$type: Copy + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
// 创建可写入的内存映射 pub fn [<reset_ $field>]()
let mut mmap = unsafe { MmapMut::map_mut(&file)? }; where
$type: Copy + PartialEq,
// 写入数据 {
mmap.copy_from_slice(&bytes); let default_value = $default;
let current = Self::[<get_ $field>]();
// 同步到磁盘 if current != default_value {
mmap.flush()?; APP_CONFIG.write().$field = default_value;
}
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());
} }
Err(e) => return Err(Box::new(e)), )*
}; };
}
// 添加文件大小检查 macro_rules! config_methods_clone {
if file.metadata()?.len() > usize::MAX as u64 { ($($field:ident: $type:ty, $default:expr;)*) => {
return Err("日志文件过大".into()); $(
} paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Clone + PartialEq,
{
APP_CONFIG.read().$field.clone()
}
// 创建只读内存映射 pub fn [<update_ $field>](value: $type)
let mmap = unsafe { MmapOptions::new().map(&file)? }; where
$type: Clone + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
// 验证并反序列化数据 pub fn [<reset_ $field>]()
let archived = unsafe { archived_root::<Vec<RequestLog>>(&mmap) }; where
Ok(archived.deserialize(&mut rkyv::Infallible)?) $type: Clone + PartialEq,
} {
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
)*
};
} }
impl AppConfig { 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>> { pub fn save_config() -> Result<(), Box<dyn std::error::Error>> {
let pages = APP_CONFIG.read().pages.clone(); let pages = APP_CONFIG.read().pages.clone();
let bytes = rkyv::to_bytes::<_, 256>(&pages)?; let bytes = rkyv::to_bytes::<_, 256>(&pages)?;
@@ -72,7 +251,8 @@ impl AppConfig {
.read(true) .read(true)
.write(true) .write(true)
.create(true) .create(true)
.open(PAGES_FILE_PATH.as_str())?; .truncate(true)
.open(&*CONFIG_FILE_PATH)?;
// 添加大小检查 // 添加大小检查
if bytes.len() > usize::MAX / 2 { if bytes.len() > usize::MAX / 2 {
@@ -89,7 +269,7 @@ impl AppConfig {
} }
pub fn load_saved_config() -> Result<(), Box<dyn std::error::Error>> { 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, Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(()); return Ok(());

View File

@@ -1,6 +1,6 @@
use reqwest::{Client, Proxy}; use reqwest::{Client, Proxy};
use serde::{Serialize, Serializer};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use serde::{Serialize, Serializer};
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; // use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use crate::app::constant::COMMA_STRING; use crate::app::constant::COMMA_STRING;

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
app::constant::{COMMA, COMMA_STRING}, app::constant::{COMMA, COMMA_STRING},
chat::{config::key_config, constant::AVAILABLE_MODELS}, chat::{config::key_config, constant::Models},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; // use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
@@ -10,7 +10,7 @@ pub enum UsageCheck {
None, None,
Default, Default,
All, All,
Custom(Vec<&'static str>), Custom(Vec<String>),
} }
impl UsageCheck { impl UsageCheck {
@@ -21,10 +21,10 @@ impl UsageCheck {
Type::Default | Type::Disabled => Self::None, Type::Default | Type::Disabled => Self::None,
Type::All => Self::All, Type::All => Self::All,
Type::Custom => { Type::Custom => {
let models: Vec<&'static str> = model let models: Vec<_> = model
.model_ids .model_ids
.iter() .iter()
.filter_map(|id| AVAILABLE_MODELS.iter().find(|m| m.id == id).map(|m| m.id)) .filter_map(|id| Models::find_id(id))
.collect(); .collect();
if models.is_empty() { if models.is_empty() {
Self::None Self::None
@@ -119,14 +119,11 @@ impl<'de> Deserialize<'de> for UsageCheck {
return Ok(UsageCheck::None); return Ok(UsageCheck::None);
} }
let models: Vec<&'static str> = list let models: Vec<_> = list
.split(COMMA) .split(COMMA)
.filter_map(|model| { .filter_map(|model| {
let model = model.trim(); let model = model.trim();
AVAILABLE_MODELS Models::find_id(model)
.iter()
.find(|m| m.id == model)
.map(|m| m.id)
}) })
.collect(); .collect();
@@ -150,14 +147,11 @@ impl UsageCheck {
if list.is_empty() { if list.is_empty() {
return Self::default(); return Self::default();
} }
let models: Vec<&'static str> = list let models: Vec<_> = list
.split(COMMA) .split(COMMA)
.filter_map(|model| { .filter_map(|model| {
let model = model.trim(); let model = model.trim();
AVAILABLE_MODELS Models::find_id(model)
.iter()
.find(|m| m.id == model)
.map(|m| m.id)
}) })
.collect(); .collect();

View 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
}
}

View File

@@ -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 image::guess_format;
use prost::Message as _; use rand::Rng as _;
use reqwest::Client; use reqwest::Client;
use uuid::Uuid; use uuid::Uuid;
@@ -10,17 +10,79 @@ use crate::{
lazy::DEFAULT_INSTRUCTIONS, lazy::DEFAULT_INSTRUCTIONS,
model::{AppConfig, VisionAbility}, model::{AppConfig, VisionAbility},
}, },
common::client::HTTP_CLIENT, common::{client::HTTP_CLIENT, utils::encode_message},
}; };
use super::{ use super::{
aiserver::v1::{ 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}, constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
model::{Message, MessageContent, Role}, 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( async fn process_chat_inputs(
inputs: Vec<Message>, inputs: Vec<Message>,
disable_vision: bool, disable_vision: bool,
@@ -96,6 +158,17 @@ async fn process_chat_inputs(
is_agentic: false, is_agentic: false,
file_diff_trajectories: vec![], file_diff_trajectories: vec![],
conversation_summary: None, 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![], vec![],
); );
@@ -119,7 +192,7 @@ async fn process_chat_inputs(
// 如果第一条是 assistant插入空的 user 消息 // 如果第一条是 assistant插入空的 user 消息
if chat_inputs if chat_inputs
.first() .first()
.map_or(false, |input| input.role == Role::Assistant) .is_some_and(|input| input.role == Role::Assistant)
{ {
chat_inputs.insert( chat_inputs.insert(
0, 0,
@@ -153,7 +226,7 @@ async fn process_chat_inputs(
// 确保最后一条是 user // 确保最后一条是 user
if chat_inputs if chat_inputs
.last() .last()
.map_or(false, |input| input.role == Role::Assistant) .is_some_and(|input| input.role == Role::Assistant)
{ {
chat_inputs.push(Message { chat_inputs.push(Message {
role: Role::User, 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 { messages.push(ConversationMessage {
text, text,
r#type: if input.role == Role::User { r#type: if input.role == Role::User {
@@ -238,6 +326,17 @@ async fn process_chat_inputs(
is_agentic: false, is_agentic: false,
file_diff_trajectories: vec![], file_diff_trajectories: vec![],
conversation_summary: None, 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 { if last_msg.r#type == conversation_message::MessageType::Human as i32 {
let text = &last_msg.text; let text = &last_msg.text;
let mut chars = text.chars().peekable(); let mut chars = text.chars().peekable();
while let Some(c) = chars.next() { while let Some(c) = chars.next() {
if c == '@' { if c == '@' {
let mut url = String::new(); let mut url = String::new();
@@ -387,13 +486,7 @@ pub async fn encode_chat_message(
is_search: bool, is_search: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁 // 在进入异步操作前获取并释放锁
let enable_slow_pool = { let enable_slow_pool = { if enable_slow_pool { Some(true) } else { None } };
if enable_slow_pool {
Some(true)
} else {
None
}
};
let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await; let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await;
@@ -401,19 +494,24 @@ pub async fn encode_chat_message(
Some(ExplicitContext { Some(ExplicitContext {
context: instructions, context: instructions,
repo_context: None, repo_context: None,
rules: vec![],
}) })
} else { } else {
None None
}; };
let base_uuid = rand::random::<u16>(); let base_uuid = rand::rng().random::<u16>();
let external_links = urls.into_iter().enumerate().map(|(i, url)| { let external_links = urls
let uuid = base_uuid.wrapping_add(i as u16); .into_iter()
ChatExternalLink { .enumerate()
url, .map(|(i, url)| {
uuid: uuid.to_string(), let uuid = base_uuid.wrapping_add(i as u16);
} ChatExternalLink {
}).collect(); url,
uuid: uuid.to_string(),
}
})
.collect();
let chat = GetChatRequest { let chat = GetChatRequest {
current_file: None, current_file: None,
@@ -461,13 +559,9 @@ pub async fn encode_chat_message(
is_composer: None, is_composer: None,
runnable_code_blocks: Some(false), runnable_code_blocks: Some(false),
should_cache: Some(false), should_cache: Some(false),
allow_model_fallbacks: None,
number_of_times_shown_fallback_model_warning: None,
}; };
let mut encoded = Vec::new(); encode_message(&chat, true)
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)?)
} }

View File

@@ -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; use error_details::Error;
impl ErrorDetails { impl ErrorDetails {
@@ -7,6 +8,7 @@ impl ErrorDetails {
Ok(error) => match error { Ok(error) => match error {
Error::Unspecified => 500, Error::Unspecified => 500,
Error::BadApiKey Error::BadApiKey
| Error::BadUserApiKey
| Error::InvalidAuthId | Error::InvalidAuthId
| Error::AuthTokenNotFound | Error::AuthTokenNotFound
| Error::AuthTokenExpired | Error::AuthTokenExpired
@@ -33,7 +35,9 @@ impl ErrorDetails {
| Error::BadModelName | Error::BadModelName
| Error::SlashEditFileTooLong | Error::SlashEditFileTooLong
| Error::FileUnsupported | Error::FileUnsupported
| Error::ClaudeImageTooLarge => 400, | Error::ClaudeImageTooLarge
| Error::ConversationTooLong => 400,
Error::Timeout => 504,
Error::Deprecated Error::Deprecated
| Error::FreeUserUsageLimit | Error::FreeUserUsageLimit
| Error::ProUserUsageLimit | Error::ProUserUsageLimit

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;
}

View File

@@ -1,183 +1,205 @@
use parking_lot::RwLock;
use std::{sync::Arc, time::{Duration, Instant}};
use super::model::Model; use super::model::Model;
macro_rules! def_pub_const { macro_rules! def_pub_const {
// 单个常量定义分支
($name:ident, $value:expr) => { ($name:ident, $value:expr) => {
pub const $name: &'static str = $value; 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; ($($name:ident => $value:expr),+ $(,)?) => {
$(
def_pub_const!(ANTHROPIC, "anthropic"); pub const $name: &'static str = $value;
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,
},
)*
];
}; };
} }
macro_rules! count { // 错误信息
() => (0); def_pub_const!(
(($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*)); 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 struct Models {
// pub fn as_str_name(&self) -> &'static str { pub models: Arc<Vec<Model>>,
// match self { last_update: Instant,
// 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 fn from_str_name(id :&str) -> Option<ModelType> { impl Models {
// match id { // 返回读锁
// CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet), pub fn read() -> parking_lot::RwLockReadGuard<'static, Models> {
// GPT_4 => Some(ModelType::Gpt4), INSTANCE.read()
// GPT_4O => Some(ModelType::Gpt4o), }
// CLAUDE_3_OPUS => Some(ModelType::Claude3Opus),
// CURSOR_FAST => Some(ModelType::CursorFast), // 返回 Arc 的克隆
// CURSOR_SMALL => Some(ModelType::CursorSmall), pub fn to_arc() -> Arc<Vec<Model>> {
// GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo), INSTANCE.read().models.clone()
// 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), // pub fn cloned() -> Vec<Model> {
// CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k), // INSTANCE.read().models.as_ref().clone()
// CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022), // }
// GPT_4O_MINI => Some(ModelType::Gpt4oMini),
// O1_MINI => Some(ModelType::O1Mini), // 检查模型是否存在
// O1_PREVIEW => Some(ModelType::O1Preview), pub fn exists(model_id: &str) -> bool {
// O1 => Some(ModelType::O1), Self::read().models.iter().any(|m| m.id == model_id)
// CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku), }
// GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206),
// GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp), // 查找模型并返回其 ID
// GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp), pub fn find_id(model: &str) -> Option<String> {
// DEEPSEEK_V3 => Some(ModelType::DeepseekV3), Self::read()
// DEEPSEEK_R1 => Some(ModelType::DeepseekR1), .models
// _ => None, .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!( create_models!(
CLAUDE_3_5_SONNET, ANTHROPIC, CLAUDE_3_5_SONNET => ANTHROPIC,
GPT_4, OPENAI, GPT_4 => OPENAI,
GPT_4O, OPENAI, GPT_4O => OPENAI,
CLAUDE_3_OPUS, ANTHROPIC, CLAUDE_3_OPUS => ANTHROPIC,
CURSOR_FAST, CURSOR, CURSOR_FAST => CURSOR,
CURSOR_SMALL, CURSOR, CURSOR_SMALL => CURSOR,
GPT_3_5_TURBO, OPENAI, GPT_3_5_TURBO => OPENAI,
GPT_4_TURBO_2024_04_09, OPENAI, GPT_4_TURBO_2024_04_09 => OPENAI,
GPT_4O_128K, OPENAI, GPT_4O_128K => OPENAI,
GEMINI_1_5_FLASH_500K, GOOGLE, GEMINI_1_5_FLASH_500K => GOOGLE,
CLAUDE_3_HAIKU_200K, ANTHROPIC, CLAUDE_3_HAIKU_200K => ANTHROPIC,
CLAUDE_3_5_SONNET_200K, ANTHROPIC, CLAUDE_3_5_SONNET_200K => ANTHROPIC,
CLAUDE_3_5_SONNET_20241022, ANTHROPIC, GPT_4O_MINI => OPENAI,
GPT_4O_MINI, OPENAI, O1_MINI => OPENAI,
O1_MINI, OPENAI, O1_PREVIEW => OPENAI,
O1_PREVIEW, OPENAI, O1 => OPENAI,
O1, OPENAI, CLAUDE_3_5_HAIKU => ANTHROPIC,
CLAUDE_3_5_HAIKU, ANTHROPIC, GEMINI_2_0_PRO_EXP => GOOGLE,
GEMINI_EXP_1206, GOOGLE, GEMINI_2_0_FLASH_THINKING_EXP => GOOGLE,
GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE, GEMINI_2_0_FLASH => GOOGLE,
GEMINI_2_0_FLASH_EXP, GOOGLE, DEEPSEEK_V3 => DEEPSEEK,
DEEPSEEK_V3, DEEPSEEK, DEEPSEEK_R1 => DEEPSEEK,
DEEPSEEK_R1, DEEPSEEK, O3_MINI => OPENAI,
GROK_2 => XAI,
); );
pub const USAGE_CHECK_MODELS: [&str; 11] = [ pub const USAGE_CHECK_MODELS: [&str; 11] = [
@@ -200,5 +222,3 @@ pub const LONG_CONTEXT_MODELS: [&str; 4] = [
CLAUDE_3_HAIKU_200K, CLAUDE_3_HAIKU_200K,
CLAUDE_3_5_SONNET_200K, CLAUDE_3_5_SONNET_200K,
]; ];
// include!("constant/models.rs");

View File

@@ -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,
},
];

View File

@@ -1,6 +1,6 @@
use super::aiserver::v1::ErrorDetails; use super::aiserver::v1::ErrorDetails;
use crate::common::model::{ApiStatus, ErrorResponse as CommonErrorResponse}; 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 prost::Message as _;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -42,7 +42,7 @@ pub struct ErrorDetail {
// } // }
impl ChatError { impl ChatError {
pub fn to_error_response(self) -> ErrorResponse { pub fn into_error_response(self) -> ErrorResponse {
if self.error.details.is_empty() { if self.error.details.is_empty() {
return ErrorResponse { return ErrorResponse {
status: 500, status: 500,
@@ -108,7 +108,7 @@ impl ErrorResponse {
) )
} }
pub fn to_common(self) -> CommonErrorResponse { pub fn into_common(self) -> CommonErrorResponse {
CommonErrorResponse { CommonErrorResponse {
status: ApiStatus::Error, status: ApiStatus::Error,
code: Some(self.status), code: Some(self.status),

View File

@@ -1,7 +1,7 @@
use crate::app::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN}; use crate::app::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN};
use axum::{ use axum::{
body::Body, body::Body,
http::{header::AUTHORIZATION, Request, StatusCode}, http::{Request, StatusCode, header::AUTHORIZATION},
middleware::Next, middleware::Next,
response::Response, response::Response,
}; };

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -80,22 +82,28 @@ pub struct Usage {
// 模型定义 // 模型定义
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct Model { pub struct Model {
pub id: &'static str, pub id: String,
pub created: &'static i64, pub created: &'static i64,
pub object: &'static str, pub object: &'static str,
pub owned_by: &'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}; use crate::app::model::{AppConfig, UsageCheck};
impl Model { 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()) { match usage_check.unwrap_or(AppConfig::get_usage_check()) {
UsageCheck::None => false, 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::All => true,
UsageCheck::Custom(models) => models.contains(&self.id), UsageCheck::Custom(models) => models.contains(model_id),
} }
} }
} }
@@ -103,5 +111,18 @@ impl Model {
#[derive(Serialize)] #[derive(Serialize)]
pub struct ModelsResponse { pub struct ModelsResponse {
pub object: &'static str, 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())
}
} }

View File

@@ -2,12 +2,15 @@ mod logs;
pub use logs::{handle_logs, handle_logs_post}; pub use logs::{handle_logs, handle_logs_post};
mod health; mod health;
pub use health::{handle_health, handle_root}; pub use health::{handle_health, handle_root};
mod token;
pub use token::{handle_basic_calibration, handle_tokens_page};
mod tokens; mod tokens;
pub use tokens::{ pub use tokens::{
handle_add_tokens, handle_basic_calibration, handle_delete_tokens, handle_get_checksum, handle_add_tokens, handle_delete_tokens, handle_get_tokens, handle_update_token_tags,
handle_get_hash, handle_get_timestamp_header, handle_get_tokens, handle_reload_tokens, handle_update_tokens,
handle_tokens_page, handle_update_tokens,
}; };
mod checksum;
pub use checksum::{handle_get_checksum, handle_get_hash, handle_get_timestamp_header};
mod profile; mod profile;
pub use profile::handle_user_info; pub use profile::handle_user_info;
mod config; mod config;

View File

@@ -1,26 +1,29 @@
use axum::response::{IntoResponse, Response}; use axum::{
body::Body,
response::{IntoResponse, Response},
};
use reqwest::header::CONTENT_TYPE; use reqwest::header::CONTENT_TYPE;
use crate::{ use crate::{
AppConfig, PageContent,
app::constant::{ app::constant::{
CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH, 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 { pub async fn handle_api_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() { match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }

View 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()
}

View File

@@ -1,30 +1,33 @@
use crate::{ use crate::{
app::{ app::{
constant::{ 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}, lazy::{AUTH_TOKEN, KEY_PREFIX},
model::{AppConfig, BuildKeyRequest, BuildKeyResponse, PageContent, UsageCheckModelType}, model::{AppConfig, BuildKeyRequest, BuildKeyResponse, PageContent, UsageCheckModelType},
}, },
chat::config::{key_config, KeyConfig}, chat::config::{KeyConfig, key_config},
common::utils::{to_base64, token_to_tokeninfo}, common::utils::{to_base64, token_to_tokeninfo},
}; };
use axum::{ use axum::{
Json,
body::Body, body::Body,
extract::Path, extract::Path,
http::{ http::{
header::{AUTHORIZATION, CONTENT_TYPE, LOCATION},
HeaderMap, StatusCode, HeaderMap, StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE, LOCATION},
}, },
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json,
}; };
use prost::Message as _; use prost::Message as _;
pub async fn handle_env_example() -> impl IntoResponse { pub async fn handle_env_example() -> impl IntoResponse {
Response::builder() Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(include_str!("../../../.env.example").to_string()) .body(Body::from(include_str!("../../../.env.example")))
.unwrap() .unwrap()
} }
@@ -33,15 +36,15 @@ pub async fn handle_config_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_CONFIG_PATH).unwrap_or_default() { match AppConfig::get_page_content(ROUTE_CONFIG_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .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() { match AppConfig::get_page_content(ROUTE_SHARED_STYLES_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) | PageContent::Html(content) => Response::builder() PageContent::Text(content) | PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .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() { match AppConfig::get_page_content(ROUTE_SHARED_JS_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) | PageContent::Html(content) => Response::builder() PageContent::Text(content) | PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }
_ => Response::builder() _ => Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body("Not found".to_string()) .body(Body::from("Not found"))
.unwrap(), .unwrap(),
} }
} }
@@ -83,15 +90,15 @@ pub async fn handle_readme() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() { match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }
@@ -105,11 +112,11 @@ pub async fn handle_about() -> impl IntoResponse {
.unwrap(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .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() { match AppConfig::get_page_content(ROUTE_BUILD_KEY_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .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(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone()) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }
@@ -142,7 +151,9 @@ pub async fn handle_build_key(
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)); .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 ( return (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(BuildKeyResponse::Error("Unauthorized".to_owned())), Json(BuildKeyResponse::Error("Unauthorized".to_owned())),
@@ -157,7 +168,7 @@ pub async fn handle_build_key(
return ( return (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(BuildKeyResponse::Error("Invalid auth token".to_owned())), 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 { if let Some(usage_check_models) = request.usage_check_models {
let usage_check = key_config::UsageCheckModel { let usage_check = key_config::UsageCheckModel {
r#type: match usage_check_models.model_type { r#type: match usage_check_models.model_type {
UsageCheckModelType::Default => { UsageCheckModelType::Default => key_config::usage_check_model::Type::Default as i32,
key_config::usage_check_model::Type::Default as i32
}
UsageCheckModelType::Disabled => { UsageCheckModelType::Disabled => {
key_config::usage_check_model::Type::Disabled as i32 key_config::usage_check_model::Type::Disabled as i32
} }

View File

@@ -6,28 +6,28 @@ use crate::{
ROUTE_BASIC_CALIBRATION_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_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_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_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH, ROUTE_STATIC_PATH, ROUTE_TOKEN_TAGS_UPDATE_PATH, ROUTE_TOKENS_ADD_PATH,
ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH, ROUTE_TOKENS_UPDATE_PATH, ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
ROUTE_USER_INFO_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}, model::{AppConfig, AppState, PageContent},
}, },
chat::constant::AVAILABLE_MODELS, chat::constant::Models,
common::model::{ common::model::{
health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats},
ApiStatus, ApiStatus,
health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats},
}, },
}; };
use axum::{ use axum::{
Json,
body::Body, body::Body,
extract::State, extract::State,
http::{ http::{
header::{CONTENT_TYPE, LOCATION},
HeaderMap, StatusCode, HeaderMap, StatusCode,
header::{CONTENT_TYPE, LOCATION},
}, },
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json,
}; };
use chrono::Local; use chrono::Local;
use reqwest::header::AUTHORIZATION; use reqwest::header::AUTHORIZATION;
@@ -44,11 +44,11 @@ pub async fn handle_root() -> impl IntoResponse {
.unwrap(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }
@@ -65,7 +65,7 @@ pub async fn handle_health(
.get(AUTHORIZATION) .get(AUTHORIZATION)
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) .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( let mut sys = System::new_with_specifics(
@@ -93,8 +93,8 @@ pub async fn handle_health(
Some(SystemStats { Some(SystemStats {
started: start_time.to_string(), started: start_time.to_string(),
total_requests: state.total_requests, total_requests: state.request_manager.total_requests,
active_requests: state.active_requests, active_requests: state.request_manager.active_requests,
system: SystemInfo { system: SystemInfo {
memory: MemoryInfo { memory: MemoryInfo {
rss: memory, // 物理内存使用量(字节) rss: memory, // 物理内存使用量(字节)
@@ -113,7 +113,7 @@ pub async fn handle_health(
version: PKG_VERSION, version: PKG_VERSION,
uptime, uptime,
stats, stats,
models: AVAILABLE_MODELS.iter().map(|m| m.id).collect::<Vec<_>>(), models: Models::ids(),
endpoints: vec![ endpoints: vec![
ROUTE_CHAT_PATH.as_str(), ROUTE_CHAT_PATH.as_str(),
ROUTE_MODELS_PATH.as_str(), ROUTE_MODELS_PATH.as_str(),
@@ -122,6 +122,7 @@ pub async fn handle_health(
ROUTE_TOKENS_UPDATE_PATH, ROUTE_TOKENS_UPDATE_PATH,
ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_ADD_PATH,
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_DELETE_PATH,
ROUTE_TOKEN_TAGS_UPDATE_PATH,
ROUTE_LOGS_PATH, ROUTE_LOGS_PATH,
ROUTE_ENV_EXAMPLE_PATH, ROUTE_ENV_EXAMPLE_PATH,
ROUTE_CONFIG_PATH, ROUTE_CONFIG_PATH,

View File

@@ -10,14 +10,14 @@ use crate::{
common::{model::ApiStatus, utils::extract_token}, common::{model::ApiStatus, utils::extract_token},
}; };
use axum::{ use axum::{
Json,
body::Body, body::Body,
extract::State, extract::State,
http::{ http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, StatusCode, HeaderMap, StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE},
}, },
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json,
}; };
use chrono::Local; use chrono::Local;
use std::sync::Arc; 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() { match AppConfig::get_page_content(ROUTE_LOGS_PATH).unwrap_or_default() {
PageContent::Default => Response::builder() PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(Body::from( .body(Body::from(include_str!("../../../static/logs.min.html")))
include_str!("../../../static/logs.min.html").to_string(),
))
.unwrap(), .unwrap(),
PageContent::Text(content) => Response::builder() PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .unwrap(),
PageContent::Html(content) => Response::builder() PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(Body::from(content.clone())) .body(Body::from(content))
.unwrap(), .unwrap(),
} }
} }
@@ -62,10 +60,10 @@ pub async fn handle_logs_post(
if auth_header == auth_token { if auth_header == auth_token {
return Ok(Json(LogsResponse { return Ok(Json(LogsResponse {
status: ApiStatus::Success, status: ApiStatus::Success,
total: state.total_requests, total: state.request_manager.total_requests,
active: Some(state.active_requests), active: Some(state.request_manager.active_requests),
error: Some(state.error_requests), error: Some(state.request_manager.error_requests),
logs: state.request_logs.clone(), logs: state.request_manager.request_logs.clone(),
timestamp: Local::now().to_string(), timestamp: Local::now().to_string(),
})); }));
} }
@@ -75,6 +73,7 @@ pub async fn handle_logs_post(
// 否则筛选出token匹配的日志 // 否则筛选出token匹配的日志
let filtered_logs: Vec<RequestLog> = state let filtered_logs: Vec<RequestLog> = state
.request_manager
.request_logs .request_logs
.iter() .iter()
.filter(|log| log.token_info.token == token_part) .filter(|log| log.token_info.token == token_part)

View File

@@ -1,10 +1,13 @@
use crate::{ use crate::{
chat::constant::ERR_NODATA, 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 axum::Json;
use super::tokens::TokenRequest; use super::token::TokenRequest;
pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> { pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> {
let auth_token = match request.token { let auth_token = match request.token {
@@ -12,7 +15,7 @@ pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUser
None => { None => {
return Json(GetUserInfo::Error { return Json(GetUserInfo::Error {
error: ERR_NODATA.to_string(), error: ERR_NODATA.to_string(),
}) });
} }
}; };
@@ -21,12 +24,12 @@ pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUser
None => { None => {
return Json(GetUserInfo::Error { return Json(GetUserInfo::Error {
error: ERR_NODATA.to_string(), error: ERR_NODATA.to_string(),
}) });
} }
}; };
match get_token_profile(&token).await { match get_token_profile(&token).await {
Some(usage) => Json(GetUserInfo::Usage(usage)), Some(usage) => Json(GetUserInfo::Usage(Box::new(usage))),
None => Json(GetUserInfo::Error { None => Json(GetUserInfo::Error {
error: ERR_NODATA.to_string(), error: ERR_NODATA.to_string(),
}), }),

99
src/chat/route/token.rs Normal file
View 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,
})
}

View File

@@ -1,83 +1,29 @@
use crate::{ use crate::{
app::{ app::{
constant::{ constant::AUTHORIZATION_BEARER_PREFIX,
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, lazy::AUTH_TOKEN,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENS_PATH,
},
lazy::{AUTH_TOKEN, TOKEN_LIST_FILE},
model::{ model::{
AppConfig, AppState, PageContent, TokenAddRequestTokenInfo, TokenInfo, AppState, TokenAddRequest, TokenInfo, TokenInfoResponse, TokenManager,
TokenUpdateRequest, TokensDeleteRequest, TokensDeleteResponse, TokenTagsResponse, TokenTagsUpdateRequest, TokenUpdateRequest, TokensDeleteRequest,
TokensDeleteResponse,
}, },
}, },
common::{ common::{
model::{error::ChatError, ApiStatus, ErrorResponse}, model::{ApiStatus, ErrorResponse, error::ChatError, userinfo::TokenProfile},
utils::{ utils::{
extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_default, generate_checksum_with_repair,
generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens, load_tokens_from_content, parse_token, validate_token,
parse_token, validate_token, validate_token_and_checksum, write_tokens,
}, },
}, },
}; };
use axum::{ use axum::{
extract::{Query, State},
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap,
},
response::{IntoResponse, Response},
Json, Json,
extract::State,
http::{HeaderMap, StatusCode, header::AUTHORIZATION},
}; };
use reqwest::StatusCode; use std::{collections::HashMap, sync::Arc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex; 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( pub async fn handle_get_tokens(
State(state): State<Arc<Mutex<AppState>>>, State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap, headers: HeaderMap,
@@ -93,7 +39,8 @@ pub async fn handle_get_tokens(
return Err(StatusCode::UNAUTHORIZED); 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(); let tokens_count = tokens.len();
Ok(Json(TokenInfoResponse { 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( pub async fn handle_update_tokens(
State(state): State<Arc<Mutex<AppState>>>, State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap, headers: HeaderMap,
@@ -163,19 +67,50 @@ pub async fn handle_update_tokens(
return Err(StatusCode::UNAUTHORIZED); 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)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// 重新加载 tokens
let token_infos = load_tokens();
let tokens_count = token_infos.len();
// 更新应用状态 // 更新应用状态
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
state.token_infos = token_infos; state.token_manager = token_manager;
} }
Ok(Json(TokenInfoResponse { Ok(Json(TokenInfoResponse {
@@ -189,7 +124,7 @@ pub async fn handle_update_tokens(
pub async fn handle_add_tokens( pub async fn handle_add_tokens(
State(state): State<Arc<Mutex<AppState>>>, State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap, headers: HeaderMap,
Json(request): Json<Vec<TokenAddRequestTokenInfo>>, Json(request): Json<TokenAddRequest>,
) -> Result<Json<TokenInfoResponse>, (StatusCode, Json<ErrorResponse>)> { ) -> Result<Json<TokenInfoResponse>, (StatusCode, Json<ErrorResponse>)> {
// 验证 AUTH_TOKEN // 验证 AUTH_TOKEN
let auth_header = headers let auth_header = headers
@@ -208,64 +143,65 @@ pub async fn handle_add_tokens(
)); ));
} }
let token_list_file = TOKEN_LIST_FILE.as_str(); // 获取当前的 token_manager
let mut token_manager = {
// 获取当前的 tokens 并创建新的 token_infos
let mut token_infos = {
let state = state.lock().await; let state = state.lock().await;
state.token_infos.clone() state.token_manager.clone()
}; };
// 创建现有token的集合 // 创建现有token的集合
let existing_tokens: std::collections::HashSet<_> = let existing_tokens: std::collections::HashSet<_> = token_manager
token_infos.iter().map(|info| info.token.as_str()).collect(); .tokens
.iter()
// 预分配容量 .map(|info| info.token.as_str())
let mut new_tokens = Vec::with_capacity(request.len()); .collect();
// 处理新的tokens // 处理新的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); let parsed_token = parse_token(&token_info.token);
if !existing_tokens.contains(parsed_token.as_str()) && validate_token(&parsed_token) { if !existing_tokens.contains(parsed_token.as_str()) && validate_token(&parsed_token) {
new_tokens.push(TokenInfo { new_tokens.push(TokenInfo {
token: parsed_token, token: parsed_token,
// 如果提供了checksum就使用提供的否则生成新的
checksum: token_info checksum: token_info
.checksum .checksum
.as_deref() .as_deref()
.map(generate_checksum_with_repair) .map(generate_checksum_with_repair)
.unwrap_or_else(generate_checksum_with_default), .unwrap_or_else(generate_checksum_with_default),
profile: None, profile: None,
tags: request.tags.clone(),
}); });
} }
} }
// 如果有新tokens才进行后续操作 // 如果有新tokens才进行后续操作
if !new_tokens.is_empty() { if !new_tokens.is_empty() {
// 预分配足够的容量 // 添加新tokens
token_infos.reserve(new_tokens.len()); token_manager.tokens.extend(new_tokens);
token_infos.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, StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Error, status: ApiStatus::Error,
code: None, code: None,
error: Some("Failed to update token list file".to_string()), error: Some("Failed to save token list".to_string()),
message: Some("无法更新token list文件".to_string()), message: Some("无法保存token list".to_string()),
}), }),
) )
})?; })?;
// 获取最终的tokens数量在更新状态之前
let tokens_count = token_infos.len();
// 更新应用状态 // 更新应用状态
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
state.token_infos = token_infos; state.token_manager = token_manager;
} }
Ok(Json(TokenInfoResponse { Ok(Json(TokenInfoResponse {
@@ -275,12 +211,13 @@ pub async fn handle_add_tokens(
message: Some("New tokens have been added and reloaded".to_string()), message: Some("New tokens have been added and reloaded".to_string()),
})) }))
} else { } else {
// 如果没有新tokens使用原始数量 // 如果没有新tokens返回当前状态
let tokens_count = token_infos.len(); let tokens = token_manager.tokens.clone();
let tokens_count = tokens.len();
Ok(Json(TokenInfoResponse { Ok(Json(TokenInfoResponse {
status: ApiStatus::Success, status: ApiStatus::Success,
tokens: None, tokens: Some(tokens),
tokens_count, tokens_count,
message: Some("No new tokens were added".to_string()), 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(); // 获取当前的 token_manager
let original_count = token_infos.len(); // 提前存储原始长度 let mut token_manager = {
let state = state.lock().await;
// 获取token_list文件路径 state.token_manager.clone()
let token_list_file = TOKEN_LIST_FILE.as_str(); };
// 创建要删除的tokens的HashSet提高查找效率 // 创建要删除的tokens的HashSet提高查找效率
let tokens_to_delete: std::collections::HashSet<_> = request.tokens.iter().collect(); let tokens_to_delete: std::collections::HashSet<_> = request.tokens.iter().collect();
@@ -324,7 +261,12 @@ pub async fn handle_delete_tokens(
request request
.tokens .tokens
.iter() .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() .cloned()
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
@@ -332,28 +274,26 @@ pub async fn handle_delete_tokens(
None None
}; };
// 预分配容量并过滤掉要删除的tokens let original_count: usize = token_manager.tokens.len();
let estimated_capacity = original_count.saturating_sub(tokens_to_delete.len());
let mut filtered_token_infos = Vec::with_capacity(estimated_capacity);
// 一次性过滤tokens // 从每个分组中删除指定的tokens
for info in token_infos { token_manager
if !tokens_to_delete.contains(&info.token) { .tokens
filtered_token_infos.push(info); .retain(|token_info| !tokens_to_delete.contains(&token_info.token));
}
} let new_count: usize = token_manager.tokens.len();
// 如果有tokens被删除才进行更新操作 // 如果有tokens被删除才进行更新操作
if filtered_token_infos.len() < original_count { if new_count < original_count {
// 写入文件 // 保存到文件
write_tokens(&filtered_token_infos, token_list_file).map_err(|_| { token_manager.save_tokens().await.map_err(|_| {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { Json(ErrorResponse {
status: ApiStatus::Error, status: ApiStatus::Error,
code: None, code: None,
error: Some("Failed to update token list file".to_string()), error: Some("Failed to save token list".to_string()),
message: Some("无法更新token list文件".to_string()), message: Some("无法保存token list".to_string()),
}), }),
) )
})?; })?;
@@ -361,9 +301,10 @@ pub async fn handle_delete_tokens(
// 如果需要的话计算 updated_tokens // 如果需要的话计算 updated_tokens
let updated_tokens = if request.expectation.needs_updated_tokens() { let updated_tokens = if request.expectation.needs_updated_tokens() {
Some( Some(
filtered_token_infos token_manager
.tokens
.iter() .iter()
.map(|info| info.token.clone()) .map(|t| t.token.clone())
.collect(), .collect(),
) )
} else { } else {
@@ -373,7 +314,7 @@ pub async fn handle_delete_tokens(
// 更新状态 // 更新状态
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
state.token_infos = filtered_token_infos; state.token_manager = token_manager;
} }
Ok(Json(TokensDeleteResponse { Ok(Json(TokensDeleteResponse {
@@ -387,9 +328,10 @@ pub async fn handle_delete_tokens(
status: ApiStatus::Success, status: ApiStatus::Success,
updated_tokens: if request.expectation.needs_updated_tokens() { updated_tokens: if request.expectation.needs_updated_tokens() {
Some( Some(
filtered_token_infos token_manager
.tokens
.iter() .iter()
.map(|info| info.token.clone()) .map(|t| t.token.clone())
.collect(), .collect(),
) )
} else { } else {
@@ -400,82 +342,62 @@ pub async fn handle_delete_tokens(
} }
} }
pub async fn handle_tokens_page() -> impl IntoResponse { pub async fn handle_update_token_tags(
match AppConfig::get_page_content(ROUTE_TOKENS_PATH).unwrap_or_default() { State(state): State<Arc<Mutex<AppState>>>,
PageContent::Default => Response::builder() headers: HeaderMap,
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) Json(request): Json<TokenTagsUpdateRequest>,
.body(include_str!("../../../static/tokens.min.html").to_string()) ) -> Result<Json<TokenTagsResponse>, (StatusCode, Json<ErrorResponse>)> {
.unwrap(), // 验证 AUTH_TOKEN
PageContent::Text(content) => Response::builder() let auth_header = headers
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) .get(AUTHORIZATION)
.body(content.clone()) .and_then(|h| h.to_str().ok())
.unwrap(), .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
PageContent::Html(content) => Response::builder() .ok_or((
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) StatusCode::UNAUTHORIZED,
.body(content.clone()) Json(ChatError::Unauthorized.to_json()),
.unwrap(), ))?;
if auth_header != AUTH_TOKEN.as_str() {
return Err((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
));
} }
}
#[derive(Deserialize)] // 获取并更新 token_manager
pub struct TokenRequest { {
pub token: Option<String>, let mut state = state.lock().await;
} if let Err(e) = state
.token_manager
#[derive(Serialize)] .update_tokens_tags(request.tokens, request.tags)
pub struct BasicCalibrationResponse { {
pub status: ApiStatus, return Err((
pub message: Option<String>, StatusCode::BAD_REQUEST,
#[serde(skip_serializing_if = "Option::is_none")] Json(ErrorResponse {
pub user_id: Option<String>, status: ApiStatus::Error,
#[serde(skip_serializing_if = "Option::is_none")] code: None,
pub create_at: Option<String>, error: Some(e.to_string()),
#[serde(skip_serializing_if = "Option::is_none")] message: Some("更新标签失败".to_string()),
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) { if (state.token_manager.save_tokens().await).is_err() {
Some(parts) => parts, return Err((
None => { StatusCode::INTERNAL_SERVER_ERROR,
return Json(BasicCalibrationResponse { Json(ErrorResponse {
status: ApiStatus::Error, status: ApiStatus::Error,
message: Some("无效令牌或无效校验和".to_string()), code: None,
user_id: None, error: Some("Failed to save token tags".to_string()),
create_at: None, message: Some("无法保存标签信息".to_string()),
checksum_time: None, }),
}) ));
} }
}; }
// 提取用户ID和创建时间 Ok(Json(TokenTagsResponse {
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, status: ApiStatus::Success,
message: Some("校验成功".to_string()), message: Some("标签更新成功".to_string()),
user_id, }))
create_at,
checksum_time,
})
} }

View File

@@ -4,7 +4,10 @@ use crate::{
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION, AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
OBJECT_CHAT_COMPLETION_CHUNK, 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::{ model::{
AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo, AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo,
UsageCheck, UsageCheck,
@@ -12,7 +15,7 @@ use crate::{
}, },
chat::{ chat::{
config::KeyConfig, config::KeyConfig,
constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS}, constant::{Models, USAGE_CHECK_MODELS},
error::StreamError, error::StreamError,
model::{ model::{
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
@@ -21,22 +24,22 @@ use crate::{
}, },
common::{ common::{
client::build_client, client::build_client,
model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse}, model::{ApiStatus, ErrorResponse, error::ChatError, userinfo::MembershipType},
utils::{ utils::{
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token, TrimNewlines as _, format_time_ms, from_base64, get_available_models,
validate_token_and_checksum, TrimNewlines as _, get_token_profile, tokeninfo_to_token, validate_token_and_checksum,
}, },
}, },
}; };
use axum::{ use axum::{
Json,
body::Body, body::Body,
extract::State, extract::State,
http::{ http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, StatusCode, HeaderMap, StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE},
}, },
response::Response, response::Response,
Json,
}; };
use bytes::Bytes; use bytes::Bytes;
use futures::StreamExt; use futures::StreamExt;
@@ -44,17 +47,129 @@ use prost::Message as _;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::{ use std::{
convert::Infallible, convert::Infallible,
sync::{atomic::AtomicBool, Arc}, sync::{Arc, atomic::AtomicBool},
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
use uuid::Uuid; 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> { pub async fn handle_models(
Json(ModelsResponse { State(state): State<Arc<Mutex<AppState>>>,
object: "list", headers: HeaderMap,
data: &AVAILABLE_MODELS, ) -> 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 =
let model_supported = model.is_some(); if Models::exists(&model_name) || (allow_claude && request.model.starts_with("claude")) {
Some(&model_name)
if !(model_supported || allow_claude && request.model.starts_with("claude")) { } else {
return Err(( return Err((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(ChatError::ModelNotSupported(request.model).to_json()), Json(ChatError::ModelNotSupported(request.model).to_json()),
)); ));
} };
let request_time = chrono::Local::now(); let request_time = chrono::Local::now();
@@ -114,7 +229,7 @@ pub async fn handle_chat(
{ {
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0); static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await; let state_guard = state.lock().await;
let token_infos = &state_guard.token_infos; let token_infos = &state_guard.token_manager.tokens;
// 检查是否存在可用的token // 检查是否存在可用的token
if token_infos.is_empty() { if token_infos.is_empty() {
@@ -159,56 +274,85 @@ pub async fn handle_chat(
{ {
let state_clone = state.clone(); let state_clone = state.clone();
let mut state = state.lock().await; let mut state = state.lock().await;
state.total_requests += 1; state.request_manager.total_requests += 1;
state.active_requests += 1; state.request_manager.active_requests += 1;
// 查找最新的相同token的日志,检查使用情况 let mut found_count: u32 = 0;
let need_profile_check = state let mut no_prompt_count: u32 = 0;
.request_logs let mut need_profile_check = false;
.iter()
.rev() for log in state.request_manager.request_logs.iter().rev() {
.find(|log| log.token_info.token == auth_token && log.token_info.profile.is_some()) if log.token_info.token == auth_token {
.and_then(|log| log.token_info.profile.as_ref()) if !LONG_CONTEXT_MODELS.contains(&log.model.as_str()) {
.map(|profile| { found_count += 1;
if profile.stripe.membership_type != MembershipType::Free {
return false;
} }
let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str()); if log.prompt.is_none() {
let standard = &profile.usage.standard; no_prompt_count += 1;
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)
} }
})
.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 { if need_profile_check {
state.active_requests -= 1; state.request_manager.active_requests -= 1;
state.error_requests += 1; state.request_manager.error_requests += 1;
return Err(( return Err((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()), 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; current_id = next_id;
// 如果需要获取用户使用情况,创建后台任务获取profile // 如果需要获取用户使用情况,创建后台任务获取profile
if model if model
.map(|m| { .map(|m| {
m.is_usage_check(UsageCheck::from_proto( Model::is_usage_check(
current_config.usage_check_models.as_ref(), m,
)) UsageCheck::from_proto(current_config.usage_check_models.as_ref()),
)
}) })
.unwrap_or(false) .unwrap_or(false)
{ {
@@ -222,30 +366,35 @@ pub async fn handle_chat(
// 先找到所有需要更新的位置的索引 // 先找到所有需要更新的位置的索引
let token_info_idx = state let token_info_idx = state
.token_infos .token_manager
.tokens
.iter() .iter()
.position(|info| info.token == auth_token_clone); .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) { match (token_info_idx, log_idx) {
(Some(t_idx), Some(l_idx)) => { (Some(t_idx), Some(l_idx)) => {
state.token_infos[t_idx].profile = profile.clone(); state.token_manager.tokens[t_idx].profile = profile.clone();
state.request_logs[l_idx].token_info.profile = profile; state.request_manager.request_logs[l_idx].token_info.profile = profile;
} }
(Some(t_idx), None) => { (Some(t_idx), None) => {
state.token_infos[t_idx].profile = profile; state.token_manager.tokens[t_idx].profile = profile;
} }
(None, Some(l_idx)) => { (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) => {} (None, None) => {}
} }
}); });
} }
state.request_logs.push(RequestLog { state.request_manager.request_logs.push(RequestLog {
id: next_id, id: next_id,
timestamp: request_time, timestamp: request_time,
model: request.model.clone(), model: request.model.clone(),
@@ -253,6 +402,7 @@ pub async fn handle_chat(
token: auth_token.clone(), token: auth_token.clone(),
checksum: checksum.clone(), checksum: checksum.clone(),
profile: None, profile: None,
tags: None,
}, },
prompt: None, prompt: None,
timing: TimingInfo { timing: TimingInfo {
@@ -264,8 +414,10 @@ pub async fn handle_chat(
error: None, error: None,
}); });
if state.request_logs.len() > *REQUEST_LOGS_LIMIT { if !*IS_UNLIMITED_REQUEST_LOGS
state.request_logs.remove(0); && 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) => { Err(e) => {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -291,8 +444,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed; log.status = LogStatus::Failed;
log.error = Some(e.to_string()); log.error = Some(e.to_string());
} }
state.active_requests -= 1; state.request_manager.active_requests -= 1;
state.error_requests += 1; state.request_manager.error_requests += 1;
return Err(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json( 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( let response = tokio::time::timeout(
std::time::Duration::from_secs(*SERVICE_TIMEOUT), std::time::Duration::from_secs(*SERVICE_TIMEOUT),
@@ -319,6 +481,7 @@ pub async fn handle_chat(
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -334,6 +497,7 @@ pub async fn handle_chat(
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -342,8 +506,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed; log.status = LogStatus::Failed;
log.error = Some(e.to_string()); log.error = Some(e.to_string());
} }
state.active_requests -= 1; state.request_manager.active_requests -= 1;
state.error_requests += 1; state.request_manager.error_requests += 1;
} }
return Err(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -356,6 +520,7 @@ pub async fn handle_chat(
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -364,8 +529,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed; log.status = LogStatus::Failed;
log.error = Some("Request timeout".to_string()); log.error = Some("Request timeout".to_string());
} }
state.active_requests -= 1; state.request_manager.active_requests -= 1;
state.error_requests += 1; state.request_manager.error_requests += 1;
} }
return Err(( return Err((
StatusCode::GATEWAY_TIMEOUT, StatusCode::GATEWAY_TIMEOUT,
@@ -377,7 +542,7 @@ pub async fn handle_chat(
// 释放活动请求计数 // 释放活动请求计数
{ {
let mut state = state.lock().await; 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(); 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; let mut state = ctx.state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -494,6 +660,7 @@ pub async fn handle_chat(
StreamMessage::Debug(debug_prompt) => { StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = ctx.state.try_lock() { if let Ok(mut state) = ctx.state.try_lock() {
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -518,11 +685,12 @@ pub async fn handle_chat(
if let Err(StreamError::ChatError(error)) = if let Err(StreamError::ChatError(error)) =
decoder.lock().await.decode(&chunk, convert_web_ref) 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; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -532,12 +700,12 @@ pub async fn handle_chat(
log.error = Some(error_response.native_code()); log.error = Some(error_response.native_code());
log.timing.total = log.timing.total =
format_time_ms(start_time.elapsed().as_secs_f64()); format_time_ms(start_time.elapsed().as_secs_f64());
state.error_requests += 1; state.request_manager.error_requests += 1;
} }
} }
return Err(( return Err((
error_response.status_code(), 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; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -560,7 +729,7 @@ pub async fn handle_chat(
{ {
log.status = LogStatus::Failed; log.status = LogStatus::Failed;
log.error = Some("Empty stream response".to_string()); log.error = Some("Empty stream response".to_string());
state.error_requests += 1; state.request_manager.error_requests += 1;
} }
} }
return Err(( return Err((
@@ -667,6 +836,7 @@ pub async fn handle_chat(
StreamMessage::Debug(debug_prompt) => { StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = state.try_lock() { if let Ok(mut state) = state.try_lock() {
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -681,10 +851,10 @@ pub async fn handle_chat(
} }
} }
Err(StreamError::ChatError(error)) => { Err(StreamError::ChatError(error)) => {
let error_response = error.to_error_response(); let error_response = error.into_error_response();
return Err(( return Err((
error_response.status_code(), error_response.status_code(),
Json(error_response.to_common()), Json(error_response.into_common()),
)); ));
} }
Err(e) => { Err(e) => {
@@ -705,6 +875,7 @@ pub async fn handle_chat(
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()
@@ -712,7 +883,7 @@ pub async fn handle_chat(
{ {
log.status = LogStatus::Failed; log.status = LogStatus::Failed;
log.error = Some("Empty response received".to_string()); log.error = Some("Empty response received".to_string());
state.error_requests += 1; state.request_manager.error_requests += 1;
} }
} }
return Err(( return Err((
@@ -747,6 +918,7 @@ pub async fn handle_chat(
let total_time = format_time_ms(start_time.elapsed().as_secs_f64()); let total_time = format_time_ms(start_time.elapsed().as_secs_f64());
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(log) = state if let Some(log) = state
.request_manager
.request_logs .request_logs
.iter_mut() .iter_mut()
.rev() .rev()

View File

@@ -1,10 +1,10 @@
use crate::chat::{ use crate::chat::{
aiserver::v1::StreamChatResponse, aiserver::v1::{StreamChatResponse, WebReference},
error::{ChatError, StreamError}, error::{ChatError, StreamError},
}; };
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use prost::Message; use prost::Message;
use std::{collections::BTreeMap, io::Read}; use std::io::Read;
// 解压gzip数据 // 解压gzip数据
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> { fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
@@ -24,17 +24,23 @@ pub trait ToMarkdown {
fn to_markdown(&self) -> String; fn to_markdown(&self) -> String;
} }
impl ToMarkdown for BTreeMap<String, String> { impl ToMarkdown for Vec<WebReference> {
fn to_markdown(&self) -> String { fn to_markdown(&self) -> String {
if self.is_empty() { if self.is_empty() {
return String::new(); return String::new();
} }
let mut result = String::from("WebReferences:\n"); let mut result = String::from("WebReferences:\n");
for (i, (url, title)) in self.iter().enumerate() { for (i, web_ref) in self.iter().enumerate() {
result.push_str(&format!("{}. [{}]({})\n", i + 1, title, url)); result.push_str(&format!(
"{}. [{}]({})<{}>\n",
i + 1,
web_ref.title,
web_ref.url,
web_ref.chunk
));
} }
result.push_str("\n"); result.push('\n');
result result
} }
} }
@@ -44,7 +50,7 @@ pub enum StreamMessage {
// 调试 // 调试
Debug(String), Debug(String),
// 网络引用 // 网络引用
WebReference(BTreeMap<String, String>), WebReference(Vec<WebReference>),
// 内容开始标志 // 内容开始标志
ContentStart, ContentStart,
// 消息内容 // 消息内容
@@ -98,11 +104,15 @@ impl StreamDecoder {
self.first_result_ready 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); self.buffer.extend_from_slice(data);
if self.buffer.len() < 5 { if self.buffer.len() < 5 {
if self.buffer.len() == 0 { if self.buffer.is_empty() {
return Err(StreamError::EmptyStream); return Err(StreamError::EmptyStream);
} }
crate::debug_println!("数据长度小于5字节当前数据: {}", hex::encode(&self.buffer)); 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]; let msg_data = &self.buffer[offset + 5..offset + 5 + msg_len];
match self.process_message(msg_type, msg_data)? { if let Some(msg) = self.process_message(msg_type, msg_data)? {
Some(msg) => { if convert_web_ref {
if convert_web_ref { messages.push(msg.convert_web_ref_to_content());
messages.push(msg.convert_web_ref_to_content()); } else {
} else { messages.push(msg);
messages.push(msg);
}
} }
_ => {}
} }
offset += 5 + msg_len; offset += 5 + msg_len;
@@ -157,7 +164,8 @@ impl StreamDecoder {
} }
} }
if !self.first_result_ready { 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) Ok(messages)
} }
@@ -182,17 +190,13 @@ impl StreamDecoder {
fn handle_text_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> { fn handle_text_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> {
if let Ok(response) = StreamChatResponse::decode(msg_data) { 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() { if !response.text.is_empty() {
Ok(Some(StreamMessage::Content(response.text))) Ok(Some(StreamMessage::Content(response.text)))
} else if let Some(filled_prompt) = response.filled_prompt { } else if let Some(filled_prompt) = response.filled_prompt {
Ok(Some(StreamMessage::Debug(filled_prompt))) Ok(Some(StreamMessage::Debug(filled_prompt)))
} else if let Some(web_citation) = response.web_citation { } else if let Some(web_citation) = response.web_citation {
let mut refs = BTreeMap::new(); Ok(Some(StreamMessage::WebReference(web_citation.references)))
for reference in web_citation.references {
refs.insert(reference.url, reference.title);
}
Ok(Some(StreamMessage::WebReference(refs)))
} else { } else {
Ok(None) Ok(None)
} }
@@ -204,17 +208,13 @@ impl StreamDecoder {
fn handle_gzip_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> { fn handle_gzip_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> {
if let Some(text) = decompress_gzip(msg_data) { if let Some(text) = decompress_gzip(msg_data) {
if let Ok(response) = StreamChatResponse::decode(&text[..]) { 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() { if !response.text.is_empty() {
Ok(Some(StreamMessage::Content(response.text))) Ok(Some(StreamMessage::Content(response.text)))
} else if let Some(filled_prompt) = response.filled_prompt { } else if let Some(filled_prompt) = response.filled_prompt {
Ok(Some(StreamMessage::Debug(filled_prompt))) Ok(Some(StreamMessage::Debug(filled_prompt)))
} else if let Some(web_citation) = response.web_citation { } else if let Some(web_citation) = response.web_citation {
let mut refs = BTreeMap::new(); Ok(Some(StreamMessage::WebReference(web_citation.references)))
for reference in web_citation.references {
refs.insert(reference.url, reference.title);
}
Ok(Some(StreamMessage::WebReference(refs)))
} else { } else {
Ok(None) Ok(None)
} }
@@ -231,7 +231,7 @@ impl StreamDecoder {
return Ok(Some(StreamMessage::StreamEnd)); return Ok(Some(StreamMessage::StreamEnd));
} }
if let Ok(text) = String::from_utf8(msg_data.to_vec()) { 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) { if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error)); return Err(StreamError::ChatError(error));
} }
@@ -248,7 +248,7 @@ impl StreamDecoder {
return Ok(Some(StreamMessage::StreamEnd)); return Ok(Some(StreamMessage::StreamEnd));
} }
if let Ok(text) = String::from_utf8(text) { 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) { if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error)); return Err(StreamError::ChatError(error));
} }
@@ -293,8 +293,11 @@ mod tests {
} }
StreamMessage::WebReference(refs) => { StreamMessage::WebReference(refs) => {
println!("网页引用:"); println!("网页引用:");
for (i, (url, title)) in refs.iter().enumerate() { for (i, web_ref) in refs.iter().enumerate() {
println!("{}. {} - {}", i, url, title); println!(
"{}. {} - {} - {}",
i, web_ref.url, web_ref.title, web_ref.chunk
);
} }
} }
StreamMessage::Debug(prompt) => { StreamMessage::Debug(prompt) => {
@@ -375,8 +378,11 @@ mod tests {
} }
StreamMessage::WebReference(refs) => { StreamMessage::WebReference(refs) => {
println!("网页引用 [hex: {}]:", hex_str); println!("网页引用 [hex: {}]:", hex_str);
for (i, (url, title)) in refs.iter().enumerate() { for (i, web_ref) in refs.iter().enumerate() {
println!("{}. {} - {}", i, url, title); println!(
"{}. {} - {} - {}",
i, web_ref.url, web_ref.title, web_ref.chunk
);
} }
} }
StreamMessage::Debug(prompt) => { StreamMessage::Debug(prompt) => {

View File

@@ -1,3 +1,3 @@
pub mod client;
pub mod model; pub mod model;
pub mod utils; pub mod utils;
pub mod client;

View File

@@ -1,17 +1,21 @@
use super::utils::generate_hash; use super::utils::generate_hash;
use crate::{app::{ use crate::{
constant::{ AppConfig,
CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL, app::{
HEADER_NAME_GHOST_MODE, TRUE, 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::{ use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT,
DNT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
}; };
use reqwest::{Client, RequestBuilder}; use reqwest::{Client, RequestBuilder};
use std::sync::LazyLock; use std::sync::LazyLock;
use uuid::Uuid; use uuid::Uuid;
@@ -35,7 +39,10 @@ def_const!(VALUE_LANGUAGE, "zh-CN");
def_const!(EMPTY, "empty"); def_const!(EMPTY, "empty");
def_const!(CORS, "cors"); def_const!(CORS, "cors");
def_const!(NO_CACHE, "no-cache"); 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!(SAME_ORIGIN, "same-origin");
def_const!(KEEP_ALIVE, "keep-alive"); def_const!(KEEP_ALIVE, "keep-alive");
def_const!(TRAILERS, "trailers"); def_const!(TRAILERS, "trailers");
@@ -66,13 +73,13 @@ pub fn rebuild_http_client() {
/// # 返回 /// # 返回
/// ///
/// * `reqwest::RequestBuilder` - 配置好的请求构建器 /// * `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 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 { let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT 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(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_API2_HOST) .header(PROXY_HOST, CURSOR_API2_HOST)
} else { } else {
HTTP_CLIENT HTTP_CLIENT.read().post(url).header(HOST, CURSOR_API2_HOST)
.read()
.post(url)
.header(HOST, CURSOR_API2_HOST)
}; };
client 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) .bearer_auth(auth_token)
.header("connect-accept-encoding", ENCODINGS) .header("connect-accept-encoding", ENCODINGS)
.header("connect-protocol-version", ONE) .header("connect-protocol-version", ONE)

View File

@@ -1,6 +1,6 @@
pub mod config;
pub mod error; pub mod error;
pub mod health; pub mod health;
pub mod config;
pub mod token; pub mod token;
pub mod userinfo; pub mod userinfo;
@@ -16,8 +16,8 @@ pub enum ApiStatus {
Success, Success,
#[serde(rename = "error")] #[serde(rename = "error")]
Error, Error,
#[serde(rename = "failed")] #[serde(rename = "failure")]
Failed, Failure,
} }
// #[derive(Serialize)] // #[derive(Serialize)]

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies}; use crate::app::model::{PageContent, Proxies, UsageCheck, VisionAbility};
#[derive(Serialize)] #[derive(Serialize)]
pub struct ConfigData { pub struct ConfigData {

View File

@@ -25,10 +25,10 @@ impl ChatError {
}; };
ErrorResponse { ErrorResponse {
status: super::ApiStatus::Error, status: super::ApiStatus::Error,
code: None, code: None,
error: Some(error.to_string()), error: Some(error.to_string()),
message: Some(message), message: Some(message),
} }
} }
} }

View File

@@ -9,7 +9,7 @@ pub struct HealthCheckResponse {
pub uptime: i64, pub uptime: i64,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub stats: Option<SystemStats>, pub stats: Option<SystemStats>,
pub models: Vec<&'static str>, pub models: Vec<String>,
pub endpoints: Vec<&'static str>, pub endpoints: Vec<&'static str>,
} }

View File

@@ -1,11 +1,11 @@
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum GetUserInfo { pub enum GetUserInfo {
Usage(TokenProfile), Usage(Box<TokenProfile>),
Error { error: String }, Error { error: String },
} }
@@ -51,7 +51,7 @@ pub struct ModelUsage {
default, default,
skip_serializing_if = "Option::is_none" skip_serializing_if = "Option::is_none"
)] )]
pub requests_total: Option<u32>, pub total_requests: Option<u32>,
#[serde(rename(deserialize = "numTokens", serialize = "tokens"))] #[serde(rename(deserialize = "numTokens", serialize = "tokens"))]
pub num_tokens: u32, pub num_tokens: u32,
#[serde( #[serde(
@@ -74,6 +74,8 @@ pub struct UsageProfile {
pub standard: ModelUsage, pub standard: ModelUsage,
#[serde(rename(deserialize = "gpt-4-32k"))] #[serde(rename(deserialize = "gpt-4-32k"))]
pub unknown: ModelUsage, pub unknown: ModelUsage,
#[serde(rename(deserialize = "startOfMonth"))]
pub start_of_month: DateTime<Local>,
} }
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] #[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]

View File

@@ -1,15 +1,28 @@
mod checksum; 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::*; pub use checksum::*;
mod token; mod token;
use prost::Message as _;
pub use token::*; pub use token::*;
mod base64; mod base64;
pub use base64::*; pub use base64::*;
use super::model::{token::TokenPayload, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}}; use super::model::{
use crate::app::{ token::TokenPayload,
constant::{COMMA, FALSE, TRUE}, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile},
lazy::{TOKEN_DELIMITER, USE_COMMA_DELIMITER}, };
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 { 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) 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)> { pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> {
// 尝试使用自定义分隔符查找 // 尝试使用自定义分隔符查找
let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER); 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 // 组合 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)
} }

View File

@@ -1,9 +1,9 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use rand::Rng; use rand::Rng as _;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
pub fn generate_hash() -> String { 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(); let mut hasher = Sha256::new();
hasher.update(random_bytes); hasher.update(random_bytes);
hex::encode(hasher.finalize()) hex::encode(hasher.finalize())

View File

@@ -1,24 +1,9 @@
use super::generate_checksum_with_repair; use super::generate_checksum_with_repair;
use crate::app::{ use crate::app::{constant::COMMA, model::TokenInfo};
constant::{COMMA, EMPTY_STRING},
lazy::TOKEN_LIST_FILE,
model::TokenInfo,
};
use crate::common::model::token::TokenPayload; 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}; 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 // 解析token
pub fn parse_token(token_part: &str) -> String { pub fn parse_token(token_part: &str) -> String {
// 查找最后一个:或%3A的位置 // 查找最后一个:或%3A的位置
@@ -38,82 +23,41 @@ pub fn parse_token(token_part: &str) -> String {
} }
} }
// Token 加载函数 // Token 加载函数,支持从字符串内容加载
pub fn load_tokens() -> Vec<TokenInfo> { pub fn load_tokens_from_content(content: &str) -> Vec<TokenInfo> {
let token_list_file = TOKEN_LIST_FILE.as_str(); let token_map: std::collections::HashMap<String, String> = content
.lines()
// 确保文件存在 .filter_map(|line| {
if !std::path::Path::new(&token_list_file).exists() { let line = line.trim();
if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) { if line.is_empty() || line.starts_with('#') {
eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e); return None;
}
}
// 读取和规范化 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()
} }
Err(e) => {
eprintln!("警告: 无法读取token-list文件: {}", e); let parts: Vec<&str> = line.split(COMMA).collect();
std::collections::HashMap::new() 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 token_map
.into_iter() .into_iter()
.map(|(token, checksum)| TokenInfo { .map(|(token, checksum)| TokenInfo {
token: token.clone(), token,
checksum, checksum,
profile: None, profile: None,
tags: None,
}) })
.collect() .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 HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
pub(super) const ISSUER: &str = "https://authentication.cursor.sh"; pub(super) const ISSUER: &str = "https://authentication.cursor.sh";
pub(super) const SCOPE: &str = "openid profile email offline_access"; pub(super) const SCOPE: &str = "openid profile email offline_access";

View File

@@ -8,16 +8,16 @@ use app::{
PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, 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_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_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_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKEN_TAGS_UPDATE_PATH,
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_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_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
}, },
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH}, lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
model::*, model::*,
}; };
use axum::{ use axum::{
routing::{get, post},
Router, Router,
routing::{get, post},
}; };
use chat::{ use chat::{
route::{ route::{
@@ -25,12 +25,12 @@ use chat::{
handle_build_key, handle_build_key_page, handle_config_page, handle_delete_tokens, 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_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_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_root, handle_static, handle_tokens_page, handle_update_token_tags,
handle_user_info, handle_update_tokens, handle_user_info,
}, },
service::{handle_chat, handle_models}, 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 std::sync::Arc;
use tokio::signal; use tokio::signal;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -58,11 +58,8 @@ async fn main() {
// 初始化全局配置 // 初始化全局配置
AppConfig::init(); 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() { 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; tokio::time::sleep(std::time::Duration::from_secs(wait_duration)).await;
let mut app_state = state_for_reload.lock().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); // debug_println!("checksum 自动刷新: {}", next_reload);
} }
}); });
@@ -130,12 +127,12 @@ async fn main() {
println!("配置已保存"); println!("配置已保存");
} }
// 保存日志 // 保存状态
let state = state_for_shutdown.lock().await; let state = state_for_shutdown.lock().await;
if let Err(e) = state.save_logs().await { if let Err(e) = state.save_state().await {
eprintln!("保存日志失败: {}", e); eprintln!("保存状态失败: {}", e);
} else { } else {
println!("日志已保存"); println!("状态已保存");
} }
}; };
@@ -146,7 +143,6 @@ async fn main() {
.route(ROUTE_TOKENS_PATH, get(handle_tokens_page)) .route(ROUTE_TOKENS_PATH, get(handle_tokens_page))
.route(ROUTE_MODELS_PATH.as_str(), get(handle_models)) .route(ROUTE_MODELS_PATH.as_str(), get(handle_models))
.route(ROUTE_TOKENS_GET_PATH, post(handle_get_tokens)) .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_UPDATE_PATH, post(handle_update_tokens))
.route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens)) .route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens))
.route(ROUTE_TOKENS_DELETE_PATH, post(handle_delete_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_USER_INFO_PATH, post(handle_user_info))
.route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page)) .route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page))
.route(ROUTE_BUILD_KEY_PATH, post(handle_build_key)) .route(ROUTE_BUILD_KEY_PATH, post(handle_build_key))
.route(ROUTE_TOKEN_TAGS_UPDATE_PATH, post(handle_update_token_tags))
.layer(RequestBodyLimitLayer::new( .layer(RequestBodyLimitLayer::new(
1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2), 1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2),
)) ))

View File

@@ -376,6 +376,23 @@
background: var(--error-color); background: var(--error-color);
color: white; 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> </style>
</head> </head>
@@ -418,6 +435,11 @@
</div> </div>
</div> </div>
<!-- 添加图表容器 -->
<div class="chart-container">
<canvas id="requestsChart"></canvas>
</div>
<div class="table-container"> <div class="table-container">
<table id="logsTable"> <table id="logsTable">
<thead> <thead>
@@ -529,8 +551,12 @@
</div> </div>
</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> <script>
let refreshInterval; let refreshInterval;
let requestsChart;
function updateStats(data) { function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total || 0; 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(''); 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) { function updateTable(data) {
const tbody = document.getElementById('logsBody'); const tbody = document.getElementById('logsBody');
updateStats(data); 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(''); 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('');
} }

View File

@@ -469,7 +469,10 @@
} }
const data = await makeAuthenticatedRequest('/tokens/add', { const data = await makeAuthenticatedRequest('/tokens/add', {
body: JSON.stringify(tokenList) body: JSON.stringify({
tokens: tokenList,
tags: []
})
}); });
if (data) { if (data) {

View File

@@ -15,19 +15,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bitflags" name = "base64"
version = "2.6.0" version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "cc" name = "bitflags"
version = "1.2.5" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
dependencies = [
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -51,7 +48,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
name = "get-token" name = "get-token"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64",
"rusqlite", "rusqlite",
"serde_json",
] ]
[[package]] [[package]]
@@ -72,17 +71,28 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.30.1" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.2" version = "1.20.2"
@@ -97,18 +107,18 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -128,10 +138,42 @@ dependencies = [
] ]
[[package]] [[package]]
name = "shlex" name = "ryu"
version = "1.3.0" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "smallvec" name = "smallvec"
@@ -141,9 +183,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.91" version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -152,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"

View File

@@ -4,7 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [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] [profile.release]
lto = true lto = true

View File

@@ -1,28 +1,150 @@
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use rusqlite::Connection; use rusqlite::Connection;
use std::env; use std::env;
use std::path::PathBuf; 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(&timestamp_bytes)
}
fn main() { fn main() {
let home_dir = env::var("HOME") let db_path = if cfg!(windows) {
.or_else(|_| env::var("USERPROFILE")) let app_data = env::var("APPDATA").unwrap_or_else(|_| {
.unwrap(); let profile = env::var("USERPROFILE").expect("未找到 USERPROFILE 环境变量");
let db_path = if cfg!(target_os = "windows") { PathBuf::from(profile)
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb") .join("AppData")
} else if cfg!(target_os = "linux") { .join("Roaming")
PathBuf::from(home_dir).join(".config/Cursor/User/globalStorage/state.vscdb") .to_string_lossy()
} else { .to_string()
PathBuf::from(home_dir) });
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") .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) { match Connection::open(&db_path) {
Ok(conn) => { Ok(conn) => {
match conn.query_row( let token = conn.query_row(
"SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'", "SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'",
[], [],
|row| row.get::<_, String>(0), |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), Err(err) => eprintln!("获取令牌时出错: {}", err),
} }
} }

219
tools/set-token/Cargo.lock generated Normal file
View 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"

View 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
View 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!("无效选项,请重试"),
}
}
}