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_LIST_FILE=.tokens
# 令牌列表文件路径(已弃用)
# TOKEN_LIST_FILE=.tokens
# 实验性是否启用慢速池true/false
ENABLE_SLOW_POOL=false
@@ -93,8 +93,11 @@ SERVICE_TIMEOUT=30
# 包含网络引用
INCLUDE_WEB_REFERENCES=false
# 持久化日志文件路径
LOGS_FILE_PATH=logs.bin
# 持久化日志文件路径(已弃用)
# LOGS_FILE_PATH=logs.bin
# 持久化页面配置文件路径
PAGES_FILE_PATH=pages.bin
# 持久化页面配置文件路径(已弃用)
# PAGES_FILE_PATH=pages.bin
# 程序数据目录
DATA_DIR=data

2
.gitignore vendored
View File

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

View File

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

View File

@@ -1,5 +1,5 @@
ARG TARGETARCH
FROM --platform=linux/${TARGETARCH} rust:1.84.0-slim-bookworm as builder
FROM --platform=linux/${TARGETARCH} rust:1-slim-bookworm as builder
ARG TARGETARCH
@@ -10,13 +10,7 @@ RUN apt-get update && \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN case "$TARGETARCH" in \
amd64) TARGET_CPU="x86-64-v3" ;; \
arm64) TARGET_CPU="neoverse-n1" ;; \
*) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
esac && \
RUSTFLAGS="-C link-arg=-s -C target-cpu=$TARGET_CPU" cargo build --release && \
cp target/release/cursor-api /app/cursor-api
RUN case "$TARGETARCH" in amd64) TARGET_CPU="x86-64-v3" ;; arm64) TARGET_CPU="neoverse-n1" ;; *) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; esac && RUSTFLAGS="-C link-arg=-s -C target-cpu=$TARGET_CPU" cargo build --release && cp target/release/cursor-api /app/cursor-api
# 运行阶段
FROM --platform=linux/${TARGETARCH} debian:bookworm-slim

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,83 +1,122 @@
macro_rules! def_pub_const {
($name:ident, $value:expr) => {
// 单个常量定义
// ($name:ident, $value:expr) => {
// pub const $name: &'static str = $value;
// };
// 批量常量定义
($($name:ident => $value:expr),+ $(,)?) => {
$(
pub const $name: &'static str = $value;
)+
};
}
pub const COMMA: char = ',';
def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION"));
// def_pub_const!(PKG_NAME, env!("CARGO_PKG_NAME"));
// def_pub_const!(PKG_DESCRIPTION, env!("CARGO_PKG_DESCRIPTION"));
// def_pub_const!(PKG_AUTHORS, env!("CARGO_PKG_AUTHORS"));
// def_pub_const!(PKG_REPOSITORY, env!("CARGO_PKG_REPOSITORY"));
def_pub_const!(EMPTY_STRING, "");
def_pub_const!(COMMA_STRING, ",");
def_pub_const!(ROUTE_ROOT_PATH, "/");
def_pub_const!(ROUTE_HEALTH_PATH, "/health");
def_pub_const!(ROUTE_GET_HASH, "/get-hash");
def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum");
def_pub_const!(ROUTE_GET_TIMESTAMP_HEADER, "/get-tsheader");
def_pub_const!(ROUTE_USER_INFO_PATH, "/userinfo");
def_pub_const!(ROUTE_API_PATH, "/api");
def_pub_const!(ROUTE_LOGS_PATH, "/logs");
def_pub_const!(ROUTE_CONFIG_PATH, "/config");
def_pub_const!(ROUTE_TOKENS_PATH, "/tokens");
def_pub_const!(ROUTE_TOKENS_GET_PATH, "/tokens/get");
def_pub_const!(ROUTE_TOKENS_RELOAD_PATH, "/tokens/reload");
def_pub_const!(ROUTE_TOKENS_UPDATE_PATH, "/tokens/update");
def_pub_const!(ROUTE_TOKENS_ADD_PATH, "/tokens/add");
def_pub_const!(ROUTE_TOKENS_DELETE_PATH, "/tokens/delete");
def_pub_const!(ROUTE_ENV_EXAMPLE_PATH, "/env-example");
def_pub_const!(ROUTE_STATIC_PATH, "/static/{path}");
def_pub_const!(ROUTE_SHARED_STYLES_PATH, "/static/shared-styles.css");
def_pub_const!(ROUTE_SHARED_JS_PATH, "/static/shared.js");
def_pub_const!(ROUTE_ABOUT_PATH, "/about");
def_pub_const!(ROUTE_README_PATH, "/readme");
def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration");
def_pub_const!(ROUTE_BUILD_KEY_PATH, "/build-key");
def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".tokens");
def_pub_const!(STATUS_PENDING, "pending");
def_pub_const!(STATUS_SUCCESS, "success");
def_pub_const!(STATUS_FAILED, "failed");
def_pub_const!(HEADER_NAME_GHOST_MODE, "x-ghost-mode");
def_pub_const!(TRUE, "true");
def_pub_const!(FALSE, "false");
// def_pub_const!(CONTENT_TYPE_PROTO, "application/proto");
def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto");
def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8");
// Package related constants
def_pub_const!(
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8,
"text/plain;charset=utf-8"
);
def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8");
def_pub_const!(
CONTENT_TYPE_TEXT_JS_WITH_UTF8,
"text/javascript;charset=utf-8"
PKG_VERSION => env!("CARGO_PKG_VERSION")
// PKG_NAME => env!("CARGO_PKG_NAME"),
// PKG_DESCRIPTION => env!("CARGO_PKG_DESCRIPTION"),
// PKG_AUTHORS => env!("CARGO_PKG_AUTHORS"),
// PKG_REPOSITORY => env!("CARGO_PKG_REPOSITORY")
);
def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer ");
// Basic string constants
def_pub_const!(
EMPTY_STRING => "",
COMMA_STRING => ","
);
def_pub_const!(CURSOR_API2_HOST, "api2.cursor.sh");
def_pub_const!(CURSOR_HOST, "www.cursor.com");
def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings");
// Route related constants
def_pub_const!(
ROUTE_ROOT_PATH => "/",
ROUTE_HEALTH_PATH => "/health",
ROUTE_GET_HASH => "/get-hash",
ROUTE_GET_CHECKSUM => "/get-checksum",
ROUTE_GET_TIMESTAMP_HEADER => "/get-tsheader",
ROUTE_USER_INFO_PATH => "/userinfo",
ROUTE_API_PATH => "/api",
ROUTE_LOGS_PATH => "/logs",
ROUTE_CONFIG_PATH => "/config",
ROUTE_TOKENS_PATH => "/tokens",
ROUTE_TOKENS_GET_PATH => "/tokens/get",
ROUTE_TOKENS_UPDATE_PATH => "/tokens/update",
ROUTE_TOKENS_ADD_PATH => "/tokens/add",
ROUTE_TOKENS_DELETE_PATH => "/tokens/delete",
ROUTE_TOKEN_TAGS_UPDATE_PATH => "/tokens/tags/update",
ROUTE_ENV_EXAMPLE_PATH => "/env-example",
ROUTE_STATIC_PATH => "/static/{path}",
ROUTE_SHARED_STYLES_PATH => "/static/shared-styles.css",
ROUTE_SHARED_JS_PATH => "/static/shared.js",
ROUTE_ABOUT_PATH => "/about",
ROUTE_README_PATH => "/readme",
ROUTE_BASIC_CALIBRATION_PATH => "/basic-calibration",
ROUTE_BUILD_KEY_PATH => "/build-key"
);
def_pub_const!(OBJECT_CHAT_COMPLETION, "chat.completion");
def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk");
// def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME => ".tokens");
// def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat");
// def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo");
// Status constants
def_pub_const!(
STATUS_PENDING => "pending",
STATUS_SUCCESS => "success",
STATUS_FAILED => "failed"
);
def_pub_const!(FINISH_REASON_STOP, "stop");
// Header constants
def_pub_const!(
HEADER_NAME_GHOST_MODE => "x-ghost-mode"
);
def_pub_const!(ERR_INVALID_PATH, "无效的路径");
// Boolean constants
def_pub_const!(
TRUE => "true",
FALSE => "false"
);
// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good");
// Content type constants
def_pub_const!(
CONTENT_TYPE_PROTO => "application/proto",
CONTENT_TYPE_CONNECT_PROTO => "application/connect+proto",
CONTENT_TYPE_TEXT_HTML_WITH_UTF8 => "text/html;charset=utf-8",
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8 => "text/plain;charset=utf-8",
CONTENT_TYPE_TEXT_CSS_WITH_UTF8 => "text/css;charset=utf-8",
CONTENT_TYPE_TEXT_JS_WITH_UTF8 => "text/javascript;charset=utf-8"
);
// Authorization constants
def_pub_const!(
AUTHORIZATION_BEARER_PREFIX => "Bearer "
);
// Cursor related constants
def_pub_const!(
CURSOR_API2_HOST => "api2.cursor.sh",
CURSOR_HOST => "www.cursor.com",
CURSOR_SETTINGS_URL => "https://www.cursor.com/settings"
);
// Object type constants
def_pub_const!(
OBJECT_CHAT_COMPLETION => "chat.completion",
OBJECT_CHAT_COMPLETION_CHUNK => "chat.completion.chunk"
);
// def_pub_const!(
// CURSOR_API2_STREAM_CHAT => "StreamChat",
// CURSOR_API2_GET_USER_INFO => "GetUserInfo"
// );
// Finish reason constants
def_pub_const!(
FINISH_REASON_STOP => "stop"
);
// Error message constants
def_pub_const!(
ERR_INVALID_PATH => "无效的路径"
);
// def_pub_const!(ERR_CHECKSUM_NO_GOOD => "checksum no good");

View File

@@ -1,10 +1,9 @@
use super::constant::{
COMMA, CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING,
};
use super::constant::{COMMA, CURSOR_API2_HOST, CURSOR_HOST, EMPTY_STRING};
use crate::common::utils::{
parse_ascii_char_from_env, parse_bool_from_env, parse_string_from_env, parse_usize_from_env,
};
use std::sync::LazyLock;
use chrono::Local;
use std::{path::PathBuf, sync::LazyLock};
use tokio::sync::{Mutex, OnceCell};
macro_rules! def_pub_static {
@@ -32,17 +31,15 @@ macro_rules! def_pub_static {
def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING);
def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING);
def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME);
def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX));
def_pub_static!(
ROUTE_CHAT_PATH,
format!("{}/v1/chat/completions", *ROUTE_PREFIX)
);
pub static START_TIME: LazyLock<chrono::DateTime<chrono::Local>> =
LazyLock::new(chrono::Local::now);
pub static START_TIME: LazyLock<chrono::DateTime<Local>> = LazyLock::new(Local::now);
pub fn get_start_time() -> chrono::DateTime<chrono::Local> {
pub fn get_start_time() -> chrono::DateTime<Local> {
*START_TIME
}
@@ -114,6 +111,12 @@ def_cursor_api_url!(
"/aiserver.v1.AiService/StreamChatWeb"
);
def_cursor_api_url!(
CURSOR_API2_CHAT_MODELS_URL,
CURSOR_API2_HOST,
"/aiserver.v1.AiService/AvailableModels"
);
def_cursor_api_url!(
CURSOR_API2_STRIPE_URL,
CURSOR_API2_HOST,
@@ -124,11 +127,26 @@ def_cursor_api_url!(CURSOR_USAGE_API_URL, CURSOR_HOST, "/api/usage");
def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me");
pub(super) static LOGS_FILE_PATH: LazyLock<String> =
LazyLock::new(|| parse_string_from_env("LOGS_FILE_PATH", "logs.bin"));
static DATA_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
let data_dir = parse_string_from_env("DATA_DIR", "data");
let path = std::env::current_exe()
.ok()
.and_then(|exe_path| exe_path.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."))
.join(data_dir);
if !path.exists() {
std::fs::create_dir_all(&path).expect("无法创建数据目录");
}
path
});
pub(super) static PAGES_FILE_PATH: LazyLock<String> =
LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin"));
pub(super) static CONFIG_FILE_PATH: LazyLock<PathBuf> =
LazyLock::new(|| DATA_DIR.join("config.bin"));
pub(super) static LOGS_FILE_PATH: LazyLock<PathBuf> = LazyLock::new(|| DATA_DIR.join("logs.bin"));
pub(super) static TOKENS_FILE_PATH: LazyLock<PathBuf> =
LazyLock::new(|| DATA_DIR.join("tokens.bin"));
pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
@@ -157,14 +175,14 @@ pub(crate) async fn get_log_file() -> &'static Mutex<tokio::fs::File> {
#[macro_export]
macro_rules! debug_println {
($($arg:tt)*) => {
if *crate::app::lazy::DEBUG {
if *$crate::app::lazy::DEBUG {
let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("{} - {}", time, format!($($arg)*));
use tokio::io::AsyncWriteExt as _;
// 使用 tokio 的 spawn 在后台异步写入日志
tokio::spawn(async move {
let log_file = crate::app::lazy::get_log_file().await;
let log_file = $crate::app::lazy::get_log_file().await;
// 使用 MutexGuard 获取可变引用
let mut file = log_file.lock().await;
if let Err(err) = file.write_all(log_message.as_bytes()).await {
@@ -183,6 +201,8 @@ macro_rules! debug_println {
pub static REQUEST_LOGS_LIMIT: LazyLock<usize> =
LazyLock::new(|| std::cmp::min(parse_usize_from_env("REQUEST_LOGS_LIMIT", 100), 2000));
pub static IS_UNLIMITED_REQUEST_LOGS: LazyLock<bool> = LazyLock::new(|| *REQUEST_LOGS_LIMIT == 0);
pub static SERVICE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30);
u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30)

View File

@@ -1,30 +1,30 @@
use crate::{
app::constant::{
EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH,
ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH,
},
chat::model::Message,
common::{
client::rebuild_http_client,
model::{userinfo::TokenProfile, ApiStatus},
utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env},
model::{ApiStatus, userinfo::TokenProfile},
utils::{generate_checksum_with_repair, get_token_profile},
},
};
use parking_lot::RwLock;
use memmap2::{MmapMut, MmapOptions};
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use std::{collections::HashSet, fs::OpenOptions};
mod usage_check;
pub use usage_check::UsageCheck;
mod vision_ability;
pub use vision_ability::VisionAbility;
mod config;
pub use config::AppConfig;
mod proxies;
pub use proxies::Proxies;
mod build_key;
pub use build_key::*;
use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS};
use super::{
constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS},
lazy::{LOGS_FILE_PATH, TOKENS_FILE_PATH},
};
// 页面内容类型枚举
#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)]
@@ -44,52 +44,6 @@ impl Default for PageContent {
}
}
// 静态配置
#[derive(Default, Clone)]
pub struct AppConfig {
vision_ability: VisionAbility,
slow_pool: bool,
allow_claude: bool,
pages: Pages,
usage_check: UsageCheck,
dynamic_key: bool,
share_token: String,
is_share: bool,
proxies: Proxies,
web_refs: bool,
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum VisionAbility {
#[serde(rename = "none", alias = "disabled")]
None,
#[serde(rename = "base64", alias = "base64-only")]
Base64,
#[serde(rename = "all", alias = "base64-http")]
All,
}
impl VisionAbility {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"none" | "disabled" => Self::None,
"base64" | "base64-only" => Self::Base64,
"all" | "base64-http" => Self::All,
_ => Self::default(),
}
}
pub fn is_none(&self) -> bool {
matches!(self, VisionAbility::None)
}
}
impl Default for VisionAbility {
fn default() -> Self {
Self::Base64
}
}
#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct Pages {
pub root_content: PageContent,
@@ -104,232 +58,140 @@ pub struct Pages {
pub build_key_content: PageContent,
}
// 运行时状态
pub struct AppState {
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct TokenGroup {
pub index: u16,
pub name: String,
pub tokens: Vec<TokenInfo>,
#[serde(default)]
pub enabled: bool,
}
// Token管理器
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct TokenManager {
pub tokens: Vec<TokenInfo>,
pub tags: HashSet<String>, // 存储所有已使用的标签
}
// 请求统计管理器
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct RequestStatsManager {
pub total_requests: u64,
pub active_requests: u64,
pub error_requests: u64,
pub request_logs: Vec<RequestLog>,
pub token_infos: Vec<TokenInfo>,
}
// 全局配置实例
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
LazyLock::new(|| RwLock::new(AppConfig::default()));
macro_rules! config_methods {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Copy + PartialEq,
{
APP_CONFIG.read().$field
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct AppState {
pub token_manager: TokenManager,
pub request_manager: RequestStatsManager,
}
pub fn [<update_ $field>](value: $type)
where
$type: Copy + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
impl TokenManager {
pub fn new(tokens: Vec<TokenInfo>) -> Self {
let mut tags = HashSet::new();
for token in &tokens {
if let Some(token_tags) = &token.tags {
tags.extend(token_tags.iter().cloned());
}
}
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;
}
}
}
)*
};
Self { tokens, tags }
}
macro_rules! config_methods_clone {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Clone + PartialEq,
{
APP_CONFIG.read().$field.clone()
pub fn update_global_tags(&mut self, new_tags: &[String]) {
// 将新标签添加到全局标签集合中
self.tags.extend(new_tags.iter().cloned());
}
pub fn [<update_ $field>](value: $type)
where
$type: Clone + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
pub fn update_tokens_tags(
&mut self,
tokens: Vec<String>,
new_tags: Vec<String>,
) -> Result<(), &'static str> {
// 创建tokens的HashSet用于快速查找
let tokens_set: HashSet<_> = tokens.iter().collect();
// 更新指定tokens的标签
for token_info in &mut self.tokens {
if tokens_set.contains(&token_info.token) {
token_info.tags = Some(new_tags.clone());
}
}
pub fn [<reset_ $field>]()
where
$type: Clone + PartialEq,
{
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
)*
};
}
// 更新全局标签集合
self.tags = self
.tokens
.iter()
.filter_map(|t| t.tags.clone())
.flatten()
.collect();
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),
pub fn get_tokens_by_tag(&self, tag: &str) -> Vec<&TokenInfo> {
self.tokens
.iter()
.filter(|t| {
t.tags
.as_ref()
.is_some_and(|tags| tags.contains(&tag.to_string()))
})
.collect()
}
pub fn update_checksum(&mut self) {
for token_info in self.tokens.iter_mut() {
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
}
}
pub async fn save_tokens(&self) -> Result<(), Box<dyn std::error::Error>> {
let bytes = rkyv::to_bytes::<_, 256>(self)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&*TOKENS_FILE_PATH)?;
if bytes.len() > usize::MAX / 2 {
return Err("Token数据过大".into());
}
file.set_len(bytes.len() as u64)?;
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
mmap.copy_from_slice(&bytes);
mmap.flush()?;
Ok(())
}
pub fn is_share() -> bool {
APP_CONFIG.read().is_share
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)),
};
if file.metadata()?.len() > usize::MAX as u64 {
return Err("Token文件过大".into());
}
let mmap = unsafe { MmapOptions::new().map(&file)? };
let archived = unsafe { rkyv::archived_root::<Self>(&mmap) };
Ok(archived.deserialize(&mut rkyv::Infallible)?)
}
}
impl AppState {
pub fn new(token_infos: Vec<TokenInfo>) -> Self {
// 尝试加载保存的日志
let request_logs = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(async { Self::load_saved_logs().await.unwrap_or_default() })
});
impl RequestStatsManager {
pub fn new(request_logs: Vec<RequestLog>) -> Self {
Self {
total_requests: request_logs.len() as u64,
active_requests: 0,
@@ -338,14 +200,96 @@ impl AppState {
.filter(|log| matches!(log.status, LogStatus::Failed))
.count() as u64,
request_logs,
token_infos,
}
}
pub fn update_checksum(&mut self) {
for token_info in self.token_infos.iter_mut() {
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
pub async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&*LOGS_FILE_PATH)?;
if bytes.len() > usize::MAX / 2 {
return Err("日志数据过大".into());
}
file.set_len(bytes.len() as u64)?;
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
mmap.copy_from_slice(&bytes);
mmap.flush()?;
Ok(())
}
pub async fn load_logs() -> Result<Vec<RequestLog>, Box<dyn std::error::Error>> {
let file = match OpenOptions::new().read(true).open(&*LOGS_FILE_PATH) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => return Err(Box::new(e)),
};
if file.metadata()?.len() > usize::MAX as u64 {
return Err("日志文件过大".into());
}
let mmap = unsafe { MmapOptions::new().map(&file)? };
let archived = unsafe { rkyv::archived_root::<Vec<RequestLog>>(&mmap) };
Ok(archived.deserialize(&mut rkyv::Infallible)?)
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}
impl AppState {
pub fn new() -> Self {
// 尝试加载保存的数据
let (request_logs, mut token_manager) = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let logs = RequestStatsManager::load_logs().await.unwrap_or_default();
let token_manager = TokenManager::load_tokens()
.await
.unwrap_or_else(|_| TokenManager::new(Vec::new()));
(logs, token_manager)
})
});
// 查询缺失的 token profiles
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
for token_info in token_manager.tokens.iter_mut() {
if token_info.profile.is_none() {
token_info.profile = get_token_profile(&token_info.token).await;
}
}
})
});
Self {
token_manager,
request_manager: RequestStatsManager::new(request_logs),
}
}
pub async fn save_state(&self) -> Result<(), Box<dyn std::error::Error>> {
// 并行保存 logs 和 tokens
let (logs_result, tokens_result) = tokio::join!(
self.request_manager.save_logs(),
self.token_manager.save_tokens()
);
logs_result?;
tokens_result?;
Ok(())
}
}
@@ -417,12 +361,13 @@ pub struct ChatRequest {
}
// 用于存储 token 信息
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
#[derive(Clone, Serialize, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct TokenInfo {
pub token: String,
pub checksum: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<TokenProfile>,
pub tags: Option<Vec<String>>,
}
// TokenUpdateRequest 结构体
@@ -431,6 +376,13 @@ pub struct TokenUpdateRequest {
pub tokens: String,
}
#[derive(Deserialize)]
pub struct TokenAddRequest {
pub tokens: Vec<TokenAddRequestTokenInfo>,
#[serde(default)]
pub tags: Option<Vec<String>>,
}
#[derive(Deserialize)]
pub struct TokenAddRequestTokenInfo {
pub token: String,
@@ -484,3 +436,26 @@ pub struct TokensDeleteResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_tokens: Option<Vec<String>>,
}
#[derive(Serialize)]
pub struct TokenInfoResponse {
pub status: ApiStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens: Option<Vec<TokenInfo>>,
pub tokens_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
// 标签相关的请求/响应结构体
#[derive(Deserialize)]
pub struct TokenTagsUpdateRequest {
pub tokens: Vec<String>,
pub tags: Vec<String>,
}
#[derive(Serialize)]
pub struct TokenTagsResponse {
pub status: ApiStatus,
pub message: Option<String>,
}

View File

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

View File

@@ -1,69 +1,248 @@
use memmap2::{MmapMut, MmapOptions};
use rkyv::{archived_root, Deserialize as _};
use std::fs::OpenOptions;
use parking_lot::RwLock;
use rkyv::{Deserialize as _, archived_root};
use std::{fs::OpenOptions, sync::LazyLock};
use crate::app::lazy::{LOGS_FILE_PATH, PAGES_FILE_PATH};
use super::{AppConfig, AppState, Pages, RequestLog, APP_CONFIG};
impl AppState {
// 保存日志的方法
pub(crate) async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
// 序列化日志
let bytes = rkyv::to_bytes::<_, 256>(&self.request_logs)?;
// 创建或打开文件
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(LOGS_FILE_PATH.as_str())?;
// 添加大小检查
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(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)),
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},
},
};
// 添加文件大小检查
if file.metadata()?.len() > usize::MAX as u64 {
return Err("日志文件过大".into());
use super::{PageContent, Pages, Proxies, UsageCheck, VisionAbility};
// 静态配置
#[derive(Default, Clone)]
pub struct AppConfig {
vision_ability: VisionAbility,
slow_pool: bool,
allow_claude: bool,
pages: Pages,
usage_check: UsageCheck,
dynamic_key: bool,
share_token: String,
is_share: bool,
proxies: Proxies,
web_refs: bool,
}
// 创建只读内存映射
let mmap = unsafe { MmapOptions::new().map(&file)? };
// 全局配置实例
static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
LazyLock::new(|| RwLock::new(AppConfig::default()));
// 验证并反序列化数据
let archived = unsafe { archived_root::<Vec<RequestLog>>(&mmap) };
Ok(archived.deserialize(&mut rkyv::Infallible)?)
macro_rules! config_methods {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Copy + PartialEq,
{
APP_CONFIG.read().$field
}
pub fn [<update_ $field>](value: $type)
where
$type: Copy + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
pub fn [<reset_ $field>]()
where
$type: Copy + PartialEq,
{
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
)*
};
}
macro_rules! config_methods_clone {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Clone + PartialEq,
{
APP_CONFIG.read().$field.clone()
}
pub fn [<update_ $field>](value: $type)
where
$type: Clone + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
pub fn [<reset_ $field>]()
where
$type: Clone + PartialEq,
{
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
)*
};
}
impl AppConfig {
pub fn init() {
let mut config = APP_CONFIG.write();
config.vision_ability =
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false);
config.usage_check =
UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING));
config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false);
config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING);
config.is_share = !config.share_token.is_empty();
config.proxies = match std::env::var("PROXIES") {
Ok(proxies) => Proxies::from_str(proxies.as_str()),
Err(_) => Proxies::default(),
};
config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false)
}
config_methods! {
slow_pool: bool, false;
allow_claude: bool, false;
dynamic_key: bool, false;
web_refs: bool, false;
}
config_methods_clone! {
vision_ability: VisionAbility, VisionAbility::default();
usage_check: UsageCheck, UsageCheck::default();
}
pub fn get_share_token() -> String {
APP_CONFIG.read().share_token.clone()
}
pub fn update_share_token(value: String) {
let current = Self::get_share_token();
if current != value {
let mut config = APP_CONFIG.write();
config.share_token = value;
config.is_share = !config.share_token.is_empty();
}
}
pub fn reset_share_token() {
let current = Self::get_share_token();
if !current.is_empty() {
let mut config = APP_CONFIG.write();
config.share_token = String::new();
config.is_share = false;
}
}
pub fn get_proxies() -> Proxies {
APP_CONFIG.read().proxies.clone()
}
pub fn update_proxies(value: Proxies) {
let current = Self::get_proxies();
if current != value {
let mut config = APP_CONFIG.write();
config.proxies = value;
rebuild_http_client();
}
}
pub fn reset_proxies() {
let default_value = Proxies::default();
let current = Self::get_proxies();
if current != default_value {
let mut config = APP_CONFIG.write();
config.proxies = default_value;
rebuild_http_client();
}
}
pub fn get_page_content(path: &str) -> Option<PageContent> {
match path {
ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()),
ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()),
ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()),
ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()),
ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()),
ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()),
ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()),
ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()),
ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()),
ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()),
_ => None,
}
}
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
let mut config = APP_CONFIG.write();
match path {
ROUTE_ROOT_PATH => config.pages.root_content = content,
ROUTE_LOGS_PATH => config.pages.logs_content = content,
ROUTE_CONFIG_PATH => config.pages.config_content = content,
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content,
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
ROUTE_ABOUT_PATH => config.pages.about_content = content,
ROUTE_README_PATH => config.pages.readme_content = content,
ROUTE_API_PATH => config.pages.api_content = content,
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content,
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
}
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
let mut config = APP_CONFIG.write();
match path {
ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(),
ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(),
ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(),
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(),
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(),
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
ROUTE_README_PATH => config.pages.readme_content = PageContent::default(),
ROUTE_API_PATH => config.pages.api_content = PageContent::default(),
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(),
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
}
pub fn is_share() -> bool {
APP_CONFIG.read().is_share
}
pub fn save_config() -> Result<(), Box<dyn std::error::Error>> {
let pages = APP_CONFIG.read().pages.clone();
let bytes = rkyv::to_bytes::<_, 256>(&pages)?;
@@ -72,7 +251,8 @@ impl AppConfig {
.read(true)
.write(true)
.create(true)
.open(PAGES_FILE_PATH.as_str())?;
.truncate(true)
.open(&*CONFIG_FILE_PATH)?;
// 添加大小检查
if bytes.len() > usize::MAX / 2 {
@@ -89,7 +269,7 @@ impl AppConfig {
}
pub fn load_saved_config() -> Result<(), Box<dyn std::error::Error>> {
let file = match OpenOptions::new().read(true).open(PAGES_FILE_PATH.as_str()) {
let file = match OpenOptions::new().read(true).open(&*CONFIG_FILE_PATH) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(());

View File

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

View File

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

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 prost::Message as _;
use rand::Rng as _;
use reqwest::Client;
use uuid::Uuid;
@@ -10,17 +10,79 @@ use crate::{
lazy::DEFAULT_INSTRUCTIONS,
model::{AppConfig, VisionAbility},
},
common::client::HTTP_CLIENT,
common::{client::HTTP_CLIENT, utils::encode_message},
};
use super::{
aiserver::v1::{
conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest,
ImageProto, ModelDetails, WebReference, conversation_message, image_proto,
},
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
model::{Message, MessageContent, Role},
};
fn parse_web_references(text: &str) -> Vec<WebReference> {
let mut web_refs = Vec::new();
let lines = text.lines().skip(1); // 跳过 "WebReferences:" 行
for line in lines {
let line = line.trim();
if line.is_empty() {
break;
}
// 跳过序号和空格
let mut chars = line.chars();
for c in chars.by_ref() {
if c == '.' {
break;
}
}
let remaining = chars.as_str().trim_start();
// 解析 [title](url) 部分
if !remaining.starts_with('[') {
continue;
}
let mut title = String::new();
let mut url = String::new();
let mut chunk = String::new();
let mut current = &mut title;
let mut state = 0; // 0: title, 1: url, 2: chunk
let mut chars = remaining.chars();
chars.next(); // 跳过 '['
while let Some(c) = chars.next() {
match (state, c) {
(0, ']') => {
state = 1;
if chars.next() != Some('(') {
break;
}
current = &mut url;
}
(1, ')') => {
state = 2;
if chars.next() == Some('<') {
current = &mut chunk;
} else {
break;
}
}
(2, '>') => break,
(_, c) => current.push(c),
}
}
web_refs.push(WebReference { title, url, chunk });
}
web_refs
}
async fn process_chat_inputs(
inputs: Vec<Message>,
disable_vision: bool,
@@ -96,6 +158,17 @@ async fn process_chat_inputs(
is_agentic: false,
file_diff_trajectories: vec![],
conversation_summary: None,
existed_subsequent_terminal_command: false,
existed_previous_terminal_command: false,
docs_references: vec![],
web_references: vec![],
git_context: None,
attached_folders_list_dir_results: vec![],
cached_conversation_summary: None,
human_changes: vec![],
attached_human_changes: false,
summarized_composers: vec![],
cursor_rules: vec![],
}],
vec![],
);
@@ -119,7 +192,7 @@ async fn process_chat_inputs(
// 如果第一条是 assistant插入空的 user 消息
if chat_inputs
.first()
.map_or(false, |input| input.role == Role::Assistant)
.is_some_and(|input| input.role == Role::Assistant)
{
chat_inputs.insert(
0,
@@ -153,7 +226,7 @@ async fn process_chat_inputs(
// 确保最后一条是 user
if chat_inputs
.last()
.map_or(false, |input| input.role == Role::Assistant)
.is_some_and(|input| input.role == Role::Assistant)
{
chat_inputs.push(Message {
role: Role::User,
@@ -201,6 +274,21 @@ async fn process_chat_inputs(
}
};
let (text, web_references) =
if input.role == Role::Assistant && text.starts_with("WebReferences:") {
if let Some(pos) = text.find("\n\n") {
let (web_refs_text, content_text) = text.split_at(pos);
(
content_text[2..].to_string(), // 跳过 "\n\n"
parse_web_references(web_refs_text),
)
} else {
(text, vec![])
}
} else {
(text, vec![])
};
messages.push(ConversationMessage {
text,
r#type: if input.role == Role::User {
@@ -238,6 +326,17 @@ async fn process_chat_inputs(
is_agentic: false,
file_diff_trajectories: vec![],
conversation_summary: None,
existed_subsequent_terminal_command: false,
existed_previous_terminal_command: false,
docs_references: vec![],
web_references,
git_context: None,
attached_folders_list_dir_results: vec![],
cached_conversation_summary: None,
human_changes: vec![],
attached_human_changes: false,
summarized_composers: vec![],
cursor_rules: vec![],
});
}
@@ -387,13 +486,7 @@ pub async fn encode_chat_message(
is_search: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁
let enable_slow_pool = {
if enable_slow_pool {
Some(true)
} else {
None
}
};
let enable_slow_pool = { if enable_slow_pool { Some(true) } else { None } };
let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await;
@@ -401,19 +494,24 @@ pub async fn encode_chat_message(
Some(ExplicitContext {
context: instructions,
repo_context: None,
rules: vec![],
})
} else {
None
};
let base_uuid = rand::random::<u16>();
let external_links = urls.into_iter().enumerate().map(|(i, url)| {
let base_uuid = rand::rng().random::<u16>();
let external_links = urls
.into_iter()
.enumerate()
.map(|(i, url)| {
let uuid = base_uuid.wrapping_add(i as u16);
ChatExternalLink {
url,
uuid: uuid.to_string(),
}
}).collect();
})
.collect();
let chat = GetChatRequest {
current_file: None,
@@ -461,13 +559,9 @@ pub async fn encode_chat_message(
is_composer: None,
runnable_code_blocks: Some(false),
should_cache: Some(false),
allow_model_fallbacks: None,
number_of_times_shown_fallback_model_warning: None,
};
let mut encoded = Vec::new();
chat.encode(&mut encoded)?;
let len_prefix = format!("{:010x}", encoded.len()).to_uppercase();
let content = hex::encode_upper(&encoded);
Ok(hex::decode(len_prefix + &content)?)
encode_message(&chat, true)
}

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

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;
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;
)+
};
}
def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF");
// 错误信息
def_pub_const!(
ERR_UNSUPPORTED_IMAGE_FORMAT,
"不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF"
ERR_UNSUPPORTED_GIF => "不支持动态 GIF",
ERR_UNSUPPORTED_IMAGE_FORMAT => "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF",
ERR_NODATA => "No data",
);
def_pub_const!(ERR_NODATA, "No data");
const MODEL_OBJECT: &str = "model";
const CREATED: &i64 = &1706659200;
// 系统常量
pub const MODEL_OBJECT: &str = "model";
pub const CREATED: &i64 = &1706659200;
def_pub_const!(ANTHROPIC, "anthropic");
def_pub_const!(CURSOR, "cursor");
def_pub_const!(GOOGLE, "google");
def_pub_const!(OPENAI, "openai");
def_pub_const!(DEEPSEEK, "deepseek");
def_pub_const!(CLAUDE_3_5_SONNET, "claude-3.5-sonnet");
def_pub_const!(GPT_4, "gpt-4");
def_pub_const!(GPT_4O, "gpt-4o");
def_pub_const!(CLAUDE_3_OPUS, "claude-3-opus");
def_pub_const!(CURSOR_FAST, "cursor-fast");
def_pub_const!(CURSOR_SMALL, "cursor-small");
def_pub_const!(GPT_3_5_TURBO, "gpt-3.5-turbo");
def_pub_const!(GPT_4_TURBO_2024_04_09, "gpt-4-turbo-2024-04-09");
def_pub_const!(GPT_4O_128K, "gpt-4o-128k");
def_pub_const!(GEMINI_1_5_FLASH_500K, "gemini-1.5-flash-500k");
def_pub_const!(CLAUDE_3_HAIKU_200K, "claude-3-haiku-200k");
def_pub_const!(CLAUDE_3_5_SONNET_200K, "claude-3-5-sonnet-200k");
def_pub_const!(CLAUDE_3_5_SONNET_20241022, "claude-3-5-sonnet-20241022");
def_pub_const!(GPT_4O_MINI, "gpt-4o-mini");
def_pub_const!(O1_MINI, "o1-mini");
def_pub_const!(O1_PREVIEW, "o1-preview");
def_pub_const!(O1, "o1");
def_pub_const!(CLAUDE_3_5_HAIKU, "claude-3.5-haiku");
def_pub_const!(GEMINI_EXP_1206, "gemini-exp-1206");
// AI 服务商
def_pub_const!(
GEMINI_2_0_FLASH_THINKING_EXP,
"gemini-2.0-flash-thinking-exp"
ANTHROPIC => "anthropic",
CURSOR => "cursor",
GOOGLE => "google",
OPENAI => "openai",
DEEPSEEK => "deepseek",
XAI => "xai",
UNKNOWN => "unknown",
);
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,
// }
// 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",
macro_rules! create_model {
($($id:expr, $owner:expr),* $(,)?) => {
pub const AVAILABLE_MODELS: [Model; count!($( ($id, $owner) )*)] = [
// 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: $id,
id: $model.into(),
created: CREATED,
object: MODEL_OBJECT,
owned_by: $owner,
},
)*
];
]),
last_update: Instant::now() - Duration::from_secs(30 * 60),
})
});
};
}
macro_rules! count {
() => (0);
(($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
pub struct Models {
pub models: Arc<Vec<Model>>,
last_update: Instant,
}
// impl ModelType {
// pub fn as_str_name(&self) -> &'static str {
// match self {
// ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET,
// ModelType::Gpt4 => GPT_4,
// ModelType::Gpt4o => GPT_4O,
// ModelType::Claude3Opus => CLAUDE_3_OPUS,
// ModelType::CursorFast => CURSOR_FAST,
// ModelType::CursorSmall => CURSOR_SMALL,
// ModelType::Gpt35Turbo => GPT_3_5_TURBO,
// ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09,
// ModelType::Gpt4o128k => GPT_4O_128K,
// ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K,
// ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K,
// ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K,
// ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022,
// ModelType::Gpt4oMini => GPT_4O_MINI,
// ModelType::O1Mini => O1_MINI,
// ModelType::O1Preview => O1_PREVIEW,
// ModelType::O1 => O1,
// ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU,
// ModelType::GeminiExp1206 => GEMINI_EXP_1206,
// ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP,
// ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP,
// ModelType::DeepseekV3 => DEEPSEEK_V3,
// ModelType::DeepseekR1 => DEEPSEEK_R1,
// }
impl Models {
// 返回读锁
pub fn read() -> parking_lot::RwLockReadGuard<'static, Models> {
INSTANCE.read()
}
// 返回 Arc 的克隆
pub fn to_arc() -> Arc<Vec<Model>> {
INSTANCE.read().models.clone()
}
// 克隆所有模型
// pub fn cloned() -> Vec<Model> {
// INSTANCE.read().models.as_ref().clone()
// }
// pub fn from_str_name(id :&str) -> Option<ModelType> {
// match id {
// CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet),
// GPT_4 => Some(ModelType::Gpt4),
// GPT_4O => Some(ModelType::Gpt4o),
// CLAUDE_3_OPUS => Some(ModelType::Claude3Opus),
// CURSOR_FAST => Some(ModelType::CursorFast),
// CURSOR_SMALL => Some(ModelType::CursorSmall),
// GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo),
// GPT_4_TURBO_2024_04_09 => Some(ModelType::Gpt4Turbo202404),
// GPT_4O_128K => Some(ModelType::Gpt4o128k),
// GEMINI_1_5_FLASH_500K => Some(ModelType::Gemini15Flash500k),
// CLAUDE_3_HAIKU_200K => Some(ModelType::Claude3Haiku200k),
// CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k),
// CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022),
// GPT_4O_MINI => Some(ModelType::Gpt4oMini),
// O1_MINI => Some(ModelType::O1Mini),
// O1_PREVIEW => Some(ModelType::O1Preview),
// O1 => Some(ModelType::O1),
// CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku),
// GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206),
// GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp),
// GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp),
// DEEPSEEK_V3 => Some(ModelType::DeepseekV3),
// DEEPSEEK_R1 => Some(ModelType::DeepseekR1),
// _ => None,
// }
// }
// 检查模型是否存在
pub fn exists(model_id: &str) -> bool {
Self::read().models.iter().any(|m| m.id == model_id)
}
// 查找模型并返回其 ID
pub fn find_id(model: &str) -> Option<String> {
Self::read()
.models
.iter()
.find(|m| m.id == model)
.map(|m| m.id.clone())
}
// 返回所有模型 ID 的列表
pub fn ids() -> Vec<String> {
Self::read()
.models
.iter()
.map(|m| m.id.clone())
.collect()
}
// 写入方法
pub fn update(new_models: Vec<Model>) -> Result<(), &'static str> {
if new_models.is_empty() {
return Err("Models list cannot be empty");
}
let mut data = INSTANCE.write();
// 检查时间间隔30分钟
if data.last_update.elapsed() < Duration::from_secs(30 * 60) {
return Err("Cannot update models more frequently than every 30 minutes");
}
// 检查内容是否有变化
if *data.models == new_models {
return Err("No changes in models");
}
// 更新数据和时间戳
data.models = Arc::new(new_models);
data.last_update = Instant::now();
Ok(())
}
}
// macro_rules! count {
// () => (0);
// (($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
// }
create_model!(
CLAUDE_3_5_SONNET, ANTHROPIC,
GPT_4, OPENAI,
GPT_4O, OPENAI,
CLAUDE_3_OPUS, ANTHROPIC,
CURSOR_FAST, CURSOR,
CURSOR_SMALL, CURSOR,
GPT_3_5_TURBO, OPENAI,
GPT_4_TURBO_2024_04_09, OPENAI,
GPT_4O_128K, OPENAI,
GEMINI_1_5_FLASH_500K, GOOGLE,
CLAUDE_3_HAIKU_200K, ANTHROPIC,
CLAUDE_3_5_SONNET_200K, ANTHROPIC,
CLAUDE_3_5_SONNET_20241022, ANTHROPIC,
GPT_4O_MINI, OPENAI,
O1_MINI, OPENAI,
O1_PREVIEW, OPENAI,
O1, OPENAI,
CLAUDE_3_5_HAIKU, ANTHROPIC,
GEMINI_EXP_1206, GOOGLE,
GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE,
GEMINI_2_0_FLASH_EXP, GOOGLE,
DEEPSEEK_V3, DEEPSEEK,
DEEPSEEK_R1, DEEPSEEK,
create_models!(
CLAUDE_3_5_SONNET => ANTHROPIC,
GPT_4 => OPENAI,
GPT_4O => OPENAI,
CLAUDE_3_OPUS => ANTHROPIC,
CURSOR_FAST => CURSOR,
CURSOR_SMALL => CURSOR,
GPT_3_5_TURBO => OPENAI,
GPT_4_TURBO_2024_04_09 => OPENAI,
GPT_4O_128K => OPENAI,
GEMINI_1_5_FLASH_500K => GOOGLE,
CLAUDE_3_HAIKU_200K => ANTHROPIC,
CLAUDE_3_5_SONNET_200K => ANTHROPIC,
GPT_4O_MINI => OPENAI,
O1_MINI => OPENAI,
O1_PREVIEW => OPENAI,
O1 => OPENAI,
CLAUDE_3_5_HAIKU => ANTHROPIC,
GEMINI_2_0_PRO_EXP => GOOGLE,
GEMINI_2_0_FLASH_THINKING_EXP => GOOGLE,
GEMINI_2_0_FLASH => GOOGLE,
DEEPSEEK_V3 => DEEPSEEK,
DEEPSEEK_R1 => DEEPSEEK,
O3_MINI => OPENAI,
GROK_2 => XAI,
);
pub const USAGE_CHECK_MODELS: [&str; 11] = [
@@ -200,5 +222,3 @@ pub const LONG_CONTEXT_MODELS: [&str; 4] = [
CLAUDE_3_HAIKU_200K,
CLAUDE_3_5_SONNET_200K,
];
// include!("constant/models.rs");

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

View File

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

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
@@ -80,22 +82,28 @@ pub struct Usage {
// 模型定义
#[derive(Serialize, Clone)]
pub struct Model {
pub id: &'static str,
pub id: String,
pub created: &'static i64,
pub object: &'static str,
pub owned_by: &'static str,
}
use super::constant::USAGE_CHECK_MODELS;
impl PartialEq for Model {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
use super::constant::{Models, USAGE_CHECK_MODELS};
use crate::app::model::{AppConfig, UsageCheck};
impl Model {
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {
pub fn is_usage_check(model_id: &String, usage_check: Option<UsageCheck>) -> bool {
match usage_check.unwrap_or(AppConfig::get_usage_check()) {
UsageCheck::None => false,
UsageCheck::Default => USAGE_CHECK_MODELS.contains(&self.id),
UsageCheck::Default => USAGE_CHECK_MODELS.contains(&model_id.as_str()),
UsageCheck::All => true,
UsageCheck::Custom(models) => models.contains(&self.id),
UsageCheck::Custom(models) => models.contains(model_id),
}
}
}
@@ -103,5 +111,18 @@ impl Model {
#[derive(Serialize)]
pub struct ModelsResponse {
pub object: &'static str,
pub data: &'static [Model],
pub data: Arc<Vec<Model>>,
}
impl ModelsResponse {
pub(super) fn new(data: Arc<Vec<Model>>) -> Self {
Self {
object: "list",
data,
}
}
pub(super) fn with_default_models() -> Self {
Self::new(Models::to_arc())
}
}

View File

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

View File

@@ -1,26 +1,29 @@
use axum::response::{IntoResponse, Response};
use axum::{
body::Body,
response::{IntoResponse, Response},
};
use reqwest::header::CONTENT_TYPE;
use crate::{
AppConfig, PageContent,
app::constant::{
CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH,
},
AppConfig, PageContent,
};
pub async fn handle_api_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() {
PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(include_str!("../../../static/api.min.html").to_string())
.body(Body::from(include_str!("../../../static/api.min.html")))
.unwrap(),
PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone())
.body(Body::from(content))
.unwrap(),
PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone())
.body(Body::from(content))
.unwrap(),
}
}

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

View File

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

View File

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

View File

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

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

View File

@@ -4,7 +4,10 @@ use crate::{
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
OBJECT_CHAT_COMPLETION_CHUNK,
},
lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT},
lazy::{
AUTH_TOKEN, CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, IS_UNLIMITED_REQUEST_LOGS,
KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT,
},
model::{
AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo,
UsageCheck,
@@ -12,7 +15,7 @@ use crate::{
},
chat::{
config::KeyConfig,
constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS},
constant::{Models, USAGE_CHECK_MODELS},
error::StreamError,
model::{
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
@@ -21,22 +24,22 @@ use crate::{
},
common::{
client::build_client,
model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse},
model::{ApiStatus, ErrorResponse, error::ChatError, userinfo::MembershipType},
utils::{
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
validate_token_and_checksum, TrimNewlines as _,
TrimNewlines as _, format_time_ms, from_base64, get_available_models,
get_token_profile, tokeninfo_to_token, validate_token_and_checksum,
},
},
};
use axum::{
Json,
body::Body,
extract::State,
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE},
},
response::Response,
Json,
};
use bytes::Bytes;
use futures::StreamExt;
@@ -44,17 +47,129 @@ use prost::Message as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{
convert::Infallible,
sync::{atomic::AtomicBool, Arc},
sync::{Arc, atomic::AtomicBool},
};
use tokio::sync::Mutex;
use uuid::Uuid;
use super::{constant::LONG_CONTEXT_MODELS, model::Model};
// 辅助函数提取认证token
fn extract_auth_token(headers: &HeaderMap) -> Result<&str, (StatusCode, Json<ErrorResponse>)> {
headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))
}
// 辅助函数解析token信息
async fn resolve_token_info(
auth_header: &str,
state: &Arc<Mutex<AppState>>,
) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
match auth_header {
// 管理员Token处理
token if is_admin_token(token) => resolve_admin_token(state).await,
// 动态密钥处理
token if is_dynamic_key(token) => resolve_dynamic_key(token),
// 普通用户Token处理
token => validate_token_and_checksum(token).ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
)),
}
}
// 辅助函数检查是否为管理员token
fn is_admin_token(token: &str) -> bool {
token == AUTH_TOKEN.as_str()
|| (AppConfig::is_share() && token == AppConfig::get_share_token().as_str())
}
// 辅助函数:检查是否为动态密钥
fn is_dynamic_key(token: &str) -> bool {
AppConfig::get_dynamic_key() && token.starts_with(&*KEY_PREFIX)
}
// 辅助函数处理管理员token
async fn resolve_admin_token(
state: &Arc<Mutex<AppState>>,
) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await;
let token_infos = &state_guard.token_manager.tokens;
if token_infos.is_empty() {
return Err((
StatusCode::SERVICE_UNAVAILABLE,
Json(ChatError::NoTokens.to_json()),
));
}
let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
let token_info = &token_infos[index];
Ok((token_info.token.clone(), token_info.checksum.clone()))
}
// 辅助函数:处理动态密钥
fn resolve_dynamic_key(token: &str) -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
from_base64(&token[*KEY_PREFIX_LEN..])
.and_then(|decoded_bytes| KeyConfig::decode(&decoded_bytes[..]).ok())
.and_then(|key_config| key_config.auth_token)
.and_then(|token_info| tokeninfo_to_token(&token_info))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))
}
// 模型列表处理
pub async fn handle_models() -> Json<ModelsResponse> {
Json(ModelsResponse {
object: "list",
data: &AVAILABLE_MODELS,
})
pub async fn handle_models(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
) -> Result<Json<ModelsResponse>, (StatusCode, Json<ErrorResponse>)> {
// 如果没有认证头,返回默认可用模型
if headers.get(AUTHORIZATION).is_none() {
return Ok(Json(ModelsResponse::with_default_models()));
}
// 提取和验证认证token
let auth_token = extract_auth_token(&headers)?;
let (token, checksum) = resolve_token_info(auth_token, &state).await?;
// 获取可用模型列表
let models = get_available_models(&token, &checksum).await.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Failure,
code: Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16()),
error: Some("Failed to fetch available models".to_string()),
message: Some("Unable to get available models".to_string()),
}),
))?;
// 更新模型列表
if let Err(e) = Models::update(models) {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Failure,
code: Some(StatusCode::INTERNAL_SERVER_ERROR.as_u16()),
error: Some("Failed to update models".to_string()),
message: Some(e.to_string()),
}),
));
}
Ok(Json(ModelsResponse::new(Models::to_arc())))
}
// 聊天处理函数的签名
@@ -73,15 +188,15 @@ pub async fn handle_chat(
};
// 验证模型是否支持并获取模型信息
let model = AVAILABLE_MODELS.iter().find(|m| m.id == model_name);
let model_supported = model.is_some();
if !(model_supported || allow_claude && request.model.starts_with("claude")) {
let model =
if Models::exists(&model_name) || (allow_claude && request.model.starts_with("claude")) {
Some(&model_name)
} else {
return Err((
StatusCode::BAD_REQUEST,
Json(ChatError::ModelNotSupported(request.model).to_json()),
));
}
};
let request_time = chrono::Local::now();
@@ -114,7 +229,7 @@ pub async fn handle_chat(
{
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await;
let token_infos = &state_guard.token_infos;
let token_infos = &state_guard.token_manager.tokens;
// 检查是否存在可用的token
if token_infos.is_empty() {
@@ -159,56 +274,85 @@ pub async fn handle_chat(
{
let state_clone = state.clone();
let mut state = state.lock().await;
state.total_requests += 1;
state.active_requests += 1;
state.request_manager.total_requests += 1;
state.request_manager.active_requests += 1;
// 查找最新的相同token的日志,检查使用情况
let need_profile_check = state
.request_logs
.iter()
.rev()
.find(|log| log.token_info.token == auth_token && log.token_info.profile.is_some())
.and_then(|log| log.token_info.profile.as_ref())
.map(|profile| {
if profile.stripe.membership_type != MembershipType::Free {
return false;
let mut found_count: u32 = 0;
let mut no_prompt_count: u32 = 0;
let mut need_profile_check = false;
for log in state.request_manager.request_logs.iter().rev() {
if log.token_info.token == auth_token {
if !LONG_CONTEXT_MODELS.contains(&log.model.as_str()) {
found_count += 1;
}
if log.prompt.is_none() {
no_prompt_count += 1;
}
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());
let standard = &profile.usage.standard;
let premium = &profile.usage.premium;
need_profile_check =
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)
}
profile.usage.premium.max_requests.is_some_and(|max| {
profile.usage.premium.num_requests >= max
})
.unwrap_or(false);
} else {
profile.usage.standard.max_requests.is_some_and(|max| {
profile.usage.standard.num_requests >= max
})
};
}
}
}
// 如果达到限制,直接返回未授权错误
if found_count == 2 {
break;
}
}
}
if found_count == 2 && no_prompt_count == 2 {
state.request_manager.active_requests -= 1;
state.request_manager.error_requests += 1;
return Err((
StatusCode::TOO_MANY_REQUESTS,
Json(ErrorResponse {
status: ApiStatus::Error,
code: Some(429),
error: Some("rate_limit_exceeded".to_string()),
message: Some("Too many requests without prompt".to_string()),
}),
));
}
// 处理检查结果
if need_profile_check {
state.active_requests -= 1;
state.error_requests += 1;
state.request_manager.active_requests -= 1;
state.request_manager.error_requests += 1;
return Err((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
));
}
let next_id = state.request_logs.last().map_or(1, |log| log.id + 1);
let next_id = state
.request_manager
.request_logs
.last()
.map_or(1, |log| log.id + 1);
current_id = next_id;
// 如果需要获取用户使用情况,创建后台任务获取profile
if model
.map(|m| {
m.is_usage_check(UsageCheck::from_proto(
current_config.usage_check_models.as_ref(),
))
Model::is_usage_check(
m,
UsageCheck::from_proto(current_config.usage_check_models.as_ref()),
)
})
.unwrap_or(false)
{
@@ -222,30 +366,35 @@ pub async fn handle_chat(
// 先找到所有需要更新的位置的索引
let token_info_idx = state
.token_infos
.token_manager
.tokens
.iter()
.position(|info| info.token == auth_token_clone);
let log_idx = state.request_logs.iter().rposition(|log| log.id == log_id);
let log_idx = state
.request_manager
.request_logs
.iter()
.rposition(|log| log.id == log_id);
// 根据索引更新
match (token_info_idx, log_idx) {
(Some(t_idx), Some(l_idx)) => {
state.token_infos[t_idx].profile = profile.clone();
state.request_logs[l_idx].token_info.profile = profile;
state.token_manager.tokens[t_idx].profile = profile.clone();
state.request_manager.request_logs[l_idx].token_info.profile = profile;
}
(Some(t_idx), None) => {
state.token_infos[t_idx].profile = profile;
state.token_manager.tokens[t_idx].profile = profile;
}
(None, Some(l_idx)) => {
state.request_logs[l_idx].token_info.profile = profile;
state.request_manager.request_logs[l_idx].token_info.profile = profile;
}
(None, None) => {}
}
});
}
state.request_logs.push(RequestLog {
state.request_manager.request_logs.push(RequestLog {
id: next_id,
timestamp: request_time,
model: request.model.clone(),
@@ -253,6 +402,7 @@ pub async fn handle_chat(
token: auth_token.clone(),
checksum: checksum.clone(),
profile: None,
tags: None,
},
prompt: None,
timing: TimingInfo {
@@ -264,8 +414,10 @@ pub async fn handle_chat(
error: None,
});
if state.request_logs.len() > *REQUEST_LOGS_LIMIT {
state.request_logs.remove(0);
if !*IS_UNLIMITED_REQUEST_LOGS
&& state.request_manager.request_logs.len() > *REQUEST_LOGS_LIMIT
{
state.request_manager.request_logs.remove(0);
}
}
@@ -283,6 +435,7 @@ pub async fn handle_chat(
Err(e) => {
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -291,8 +444,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed;
log.error = Some(e.to_string());
}
state.active_requests -= 1;
state.error_requests += 1;
state.request_manager.active_requests -= 1;
state.request_manager.error_requests += 1;
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(
@@ -303,7 +456,16 @@ pub async fn handle_chat(
};
// 构建请求客户端
let client = build_client(&auth_token, &checksum, is_search);
let client = build_client(
&auth_token,
&checksum,
if is_search {
&CURSOR_API2_CHAT_WEB_URL
} else {
&CURSOR_API2_CHAT_URL
},
true,
);
// 添加超时设置
let response = tokio::time::timeout(
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
@@ -319,6 +481,7 @@ pub async fn handle_chat(
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -334,6 +497,7 @@ pub async fn handle_chat(
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -342,8 +506,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed;
log.error = Some(e.to_string());
}
state.active_requests -= 1;
state.error_requests += 1;
state.request_manager.active_requests -= 1;
state.request_manager.error_requests += 1;
}
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
@@ -356,6 +520,7 @@ pub async fn handle_chat(
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -364,8 +529,8 @@ pub async fn handle_chat(
log.status = LogStatus::Failed;
log.error = Some("Request timeout".to_string());
}
state.active_requests -= 1;
state.error_requests += 1;
state.request_manager.active_requests -= 1;
state.request_manager.error_requests += 1;
}
return Err((
StatusCode::GATEWAY_TIMEOUT,
@@ -377,7 +542,7 @@ pub async fn handle_chat(
// 释放活动请求计数
{
let mut state = state.lock().await;
state.active_requests -= 1;
state.request_manager.active_requests -= 1;
}
let convert_web_ref = current_config.include_web_references();
@@ -460,6 +625,7 @@ pub async fn handle_chat(
{
let mut state = ctx.state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -494,6 +660,7 @@ pub async fn handle_chat(
StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = ctx.state.try_lock() {
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -518,11 +685,12 @@ pub async fn handle_chat(
if let Err(StreamError::ChatError(error)) =
decoder.lock().await.decode(&chunk, convert_web_ref)
{
let error_response = error.to_error_response();
let error_response = error.into_error_response();
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -532,12 +700,12 @@ pub async fn handle_chat(
log.error = Some(error_response.native_code());
log.timing.total =
format_time_ms(start_time.elapsed().as_secs_f64());
state.error_requests += 1;
state.request_manager.error_requests += 1;
}
}
return Err((
error_response.status_code(),
Json(error_response.to_common()),
Json(error_response.into_common()),
));
}
}
@@ -553,6 +721,7 @@ pub async fn handle_chat(
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -560,7 +729,7 @@ pub async fn handle_chat(
{
log.status = LogStatus::Failed;
log.error = Some("Empty stream response".to_string());
state.error_requests += 1;
state.request_manager.error_requests += 1;
}
}
return Err((
@@ -667,6 +836,7 @@ pub async fn handle_chat(
StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = state.try_lock() {
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -681,10 +851,10 @@ pub async fn handle_chat(
}
}
Err(StreamError::ChatError(error)) => {
let error_response = error.to_error_response();
let error_response = error.into_error_response();
return Err((
error_response.status_code(),
Json(error_response.to_common()),
Json(error_response.into_common()),
));
}
Err(e) => {
@@ -705,6 +875,7 @@ pub async fn handle_chat(
{
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()
@@ -712,7 +883,7 @@ pub async fn handle_chat(
{
log.status = LogStatus::Failed;
log.error = Some("Empty response received".to_string());
state.error_requests += 1;
state.request_manager.error_requests += 1;
}
}
return Err((
@@ -747,6 +918,7 @@ pub async fn handle_chat(
let total_time = format_time_ms(start_time.elapsed().as_secs_f64());
let mut state = state.lock().await;
if let Some(log) = state
.request_manager
.request_logs
.iter_mut()
.rev()

View File

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

View File

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

View File

@@ -1,16 +1,20 @@
use super::utils::generate_hash;
use crate::{app::{
use crate::{
AppConfig,
app::{
constant::{
CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL,
HEADER_NAME_GHOST_MODE, TRUE,
CONTENT_TYPE_CONNECT_PROTO, CONTENT_TYPE_PROTO, CURSOR_API2_HOST, CURSOR_HOST,
CURSOR_SETTINGS_URL, HEADER_NAME_GHOST_MODE, TRUE,
},
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
CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST,
USE_REVERSE_PROXY,
},
}, AppConfig};
},
};
use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE,
DNT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT,
HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
};
use reqwest::{Client, RequestBuilder};
use std::sync::LazyLock;
@@ -35,7 +39,10 @@ def_const!(VALUE_LANGUAGE, "zh-CN");
def_const!(EMPTY, "empty");
def_const!(CORS, "cors");
def_const!(NO_CACHE, "no-cache");
def_const!(UA_WIN, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
def_const!(
UA_WIN,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
);
def_const!(SAME_ORIGIN, "same-origin");
def_const!(KEEP_ALIVE, "keep-alive");
def_const!(TRAILERS, "trailers");
@@ -66,13 +73,13 @@ pub fn rebuild_http_client() {
/// # 返回
///
/// * `reqwest::RequestBuilder` - 配置好的请求构建器
pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> RequestBuilder {
pub fn build_client(
auth_token: &str,
checksum: &str,
url: &str,
is_stream: bool,
) -> RequestBuilder {
let trace_id = Uuid::new_v4().to_string();
let url = if is_search {
&*CURSOR_API2_CHAT_WEB_URL
} else {
&*CURSOR_API2_CHAT_URL
};
let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT
@@ -81,14 +88,18 @@ pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> Reques
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_API2_HOST)
} else {
HTTP_CLIENT
.read()
.post(url)
.header(HOST, CURSOR_API2_HOST)
HTTP_CLIENT.read().post(url).header(HOST, CURSOR_API2_HOST)
};
client
.header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO)
.header(
CONTENT_TYPE,
if is_stream {
CONTENT_TYPE_CONNECT_PROTO
} else {
CONTENT_TYPE_PROTO
},
)
.bearer_auth(auth_token)
.header("connect-accept-encoding", ENCODINGS)
.header("connect-protocol-version", ONE)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,28 @@
mod checksum;
use ::base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use ::base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
pub use checksum::*;
mod token;
use prost::Message as _;
pub use token::*;
mod base64;
pub use base64::*;
use super::model::{token::TokenPayload, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}};
use crate::app::{
use super::model::{
token::TokenPayload,
userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile},
};
use crate::{
app::{
constant::{COMMA, FALSE, TRUE},
lazy::{TOKEN_DELIMITER, USE_COMMA_DELIMITER},
lazy::{CURSOR_API2_CHAT_MODELS_URL, TOKEN_DELIMITER, USE_COMMA_DELIMITER},
},
chat::{
aiserver::v1::{AvailableModelsRequest, AvailableModelsResponse},
constant::{
ANTHROPIC, CREATED, CURSOR, DEEPSEEK, GOOGLE, MODEL_OBJECT, OPENAI, UNKNOWN, XAI,
},
model::Model,
},
};
pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
@@ -124,6 +137,59 @@ pub async fn get_user_profile(auth_token: &str) -> Option<UserProfile> {
Some(user_profile)
}
pub async fn get_available_models(auth_token: &str, checksum: &str) -> Option<Vec<Model>> {
let client =
super::client::build_client(auth_token, checksum, &CURSOR_API2_CHAT_MODELS_URL, false);
let request = AvailableModelsRequest {
is_nightly: true,
include_long_context_models: true,
};
let response = client
.body(encode_message(&request, false).unwrap())
.send()
.await
.ok()?
.bytes()
.await
.ok()?;
let available_models = AvailableModelsResponse::decode(response.as_ref()).ok()?;
Some(
available_models
.models
.into_iter()
.map(|model| Model {
id: model.name.clone(),
created: CREATED,
object: MODEL_OBJECT,
owned_by: {
let mut chars = model.name.chars();
match chars.next() {
Some('g') => match chars.next() {
Some('p') => OPENAI, // g + p → "gp" (gpt)
Some('e') => GOOGLE, // g + e → "ge" (gemini)
Some('r') => XAI, // g + r → "ge" (grok)
_ => UNKNOWN,
},
Some('o') => match chars.next() {
// o 开头需要二次判断
Some('1') | Some('3') => OPENAI, // o1/o3 系列
_ => UNKNOWN,
},
Some('c') => match chars.next() {
Some('l') => ANTHROPIC, // c + l → "cl" (claude)
Some('u') => CURSOR, // c + u → "cu" (cursor)
_ => UNKNOWN,
},
Some('d') if chars.next() == Some('e') => DEEPSEEK, // d + e → "de" (deepseek)
// 其他情况
_ => UNKNOWN,
}
},
})
.collect(),
)
}
pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> {
// 尝试使用自定义分隔符查找
let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER);
@@ -277,5 +343,33 @@ pub fn tokeninfo_to_token(info: &key_config::TokenInfo) -> Option<(String, Strin
};
// 组合 token
Some((format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature), generate_checksum(&device_id, mac_addr.as_deref())))
Some((
format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature),
generate_checksum(&device_id, mac_addr.as_deref()),
))
}
pub fn encode_message(
message: &impl prost::Message,
with_gzip: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let mut encoded = Vec::new();
message.encode(&mut encoded)?;
if !with_gzip {
return Ok(encoded);
}
// 构造 5 字节头部 [0x00, len_be_bytes...]
let mut header = Vec::with_capacity(5);
header.push(0x00); // 压缩标记位
// 将长度转换为 u32 大端字节(显式长度检查)
let len = u32::try_from(encoded.len()).map_err(|_| "Message length exceeds u32::MAX")?; // 明确错误类型
header.extend_from_slice(&len.to_be_bytes());
// 组合最终数据
let mut result = header;
result.extend(encoded);
Ok(result)
}

View File

@@ -1,9 +1,9 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use rand::Rng;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use rand::Rng as _;
use sha2::{Digest, Sha256};
pub fn generate_hash() -> String {
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
let random_bytes = rand::rng().random::<[u8; 32]>();
let mut hasher = Sha256::new();
hasher.update(random_bytes);
hex::encode(hasher.finalize())

View File

@@ -1,24 +1,9 @@
use super::generate_checksum_with_repair;
use crate::app::{
constant::{COMMA, EMPTY_STRING},
lazy::TOKEN_LIST_FILE,
model::TokenInfo,
};
use crate::app::{constant::COMMA, model::TokenInfo};
use crate::common::model::token::TokenPayload;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Local, TimeZone};
// 规范化文件内容并写入
fn normalize_and_write(content: &str, file_path: &str) -> String {
let normalized = content.replace("\r\n", "\n");
if normalized != content {
if let Err(e) = std::fs::write(file_path, &normalized) {
eprintln!("警告: 无法更新规范化的文件: {}", e);
}
}
normalized
}
// 解析token
pub fn parse_token(token_part: &str) -> String {
// 查找最后一个:或%3A的位置
@@ -38,23 +23,9 @@ pub fn parse_token(token_part: &str) -> String {
}
}
// Token 加载函数
pub fn load_tokens() -> Vec<TokenInfo> {
let token_list_file = TOKEN_LIST_FILE.as_str();
// 确保文件存在
if !std::path::Path::new(&token_list_file).exists() {
if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) {
eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e);
}
}
// 读取和规范化 token-list 文件
let token_map: std::collections::HashMap<String, String> =
match std::fs::read_to_string(&token_list_file) {
Ok(content) => {
let normalized = normalize_and_write(&content, &token_list_file);
normalized
// Token 加载函数,支持从字符串内容加载
pub fn load_tokens_from_content(content: &str) -> Vec<TokenInfo> {
let token_map: std::collections::HashMap<String, String> = content
.lines()
.filter_map(|line| {
let line = line.trim();
@@ -74,46 +45,19 @@ pub fn load_tokens() -> Vec<TokenInfo> {
}
}
})
.collect()
}
Err(e) => {
eprintln!("警告: 无法读取token-list文件: {}", e);
std::collections::HashMap::new()
}
};
.collect();
// 更新 token-list 文件
let token_list_content = token_map
.iter()
.map(|(token, checksum)| format!("{},{}", token, checksum))
.collect::<Vec<_>>()
.join("\n");
if let Err(e) = std::fs::write(&token_list_file, token_list_content) {
eprintln!("警告: 无法更新token-list文件: {}", e);
}
// 转换为 TokenInfo vector
token_map
.into_iter()
.map(|(token, checksum)| TokenInfo {
token: token.clone(),
token,
checksum,
profile: None,
tags: None,
})
.collect()
}
pub fn write_tokens(token_infos: &[TokenInfo], file_path: &str) -> std::io::Result<()> {
let content = token_infos
.iter()
.map(|info| format!("{},{}", info.token, info.checksum))
.collect::<Vec<String>>()
.join("\n");
std::fs::write(file_path, content)
}
pub(super) const HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
pub(super) const ISSUER: &str = "https://authentication.cursor.sh";
pub(super) const SCOPE: &str = "openid profile email offline_access";

View File

@@ -8,16 +8,16 @@ use app::{
PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH,
ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM,
ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH,
ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH,
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
ROUTE_TOKENS_RELOAD_PATH, ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKEN_TAGS_UPDATE_PATH,
ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
},
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
model::*,
};
use axum::{
routing::{get, post},
Router,
routing::{get, post},
};
use chat::{
route::{
@@ -25,12 +25,12 @@ use chat::{
handle_build_key, handle_build_key_page, handle_config_page, handle_delete_tokens,
handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header,
handle_get_tokens, handle_health, handle_logs, handle_logs_post, handle_readme,
handle_reload_tokens, handle_root, handle_static, handle_tokens_page, handle_update_tokens,
handle_user_info,
handle_root, handle_static, handle_tokens_page, handle_update_token_tags,
handle_update_tokens, handle_user_info,
},
service::{handle_chat, handle_models},
};
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
use common::utils::{parse_string_from_env, parse_usize_from_env};
use std::sync::Arc;
use tokio::signal;
use tokio::sync::Mutex;
@@ -58,11 +58,8 @@ async fn main() {
// 初始化全局配置
AppConfig::init();
// 加载 tokens
let token_infos = load_tokens();
// 初始化应用状态
let state = Arc::new(Mutex::new(AppState::new(token_infos)));
let state = Arc::new(Mutex::new(AppState::new()));
// 尝试加载保存的配置
if let Err(e) = AppConfig::load_saved_config() {
@@ -89,7 +86,7 @@ async fn main() {
tokio::time::sleep(std::time::Duration::from_secs(wait_duration)).await;
let mut app_state = state_for_reload.lock().await;
app_state.update_checksum();
app_state.token_manager.update_checksum();
// debug_println!("checksum 自动刷新: {}", next_reload);
}
});
@@ -130,12 +127,12 @@ async fn main() {
println!("配置已保存");
}
// 保存日志
// 保存状态
let state = state_for_shutdown.lock().await;
if let Err(e) = state.save_logs().await {
eprintln!("保存日志失败: {}", e);
if let Err(e) = state.save_state().await {
eprintln!("保存状态失败: {}", e);
} else {
println!("日志已保存");
println!("状态已保存");
}
};
@@ -146,7 +143,6 @@ async fn main() {
.route(ROUTE_TOKENS_PATH, get(handle_tokens_page))
.route(ROUTE_MODELS_PATH.as_str(), get(handle_models))
.route(ROUTE_TOKENS_GET_PATH, post(handle_get_tokens))
.route(ROUTE_TOKENS_RELOAD_PATH, post(handle_reload_tokens))
.route(ROUTE_TOKENS_UPDATE_PATH, post(handle_update_tokens))
.route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens))
.route(ROUTE_TOKENS_DELETE_PATH, post(handle_delete_tokens))
@@ -167,6 +163,7 @@ async fn main() {
.route(ROUTE_USER_INFO_PATH, post(handle_user_info))
.route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page))
.route(ROUTE_BUILD_KEY_PATH, post(handle_build_key))
.route(ROUTE_TOKEN_TAGS_UPDATE_PATH, post(handle_update_token_tags))
.layer(RequestBodyLimitLayer::new(
1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2),
))

View File

@@ -376,6 +376,23 @@
background: var(--error-color);
color: white;
}
/* 图表样式 */
.chart-container {
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: var(--spacing);
border: 1px solid var(--border-color);
height: 300px;
}
@media (max-width: 768px) {
.chart-container {
height: 200px;
padding: 16px;
}
}
</style>
</head>
@@ -418,6 +435,11 @@
</div>
</div>
<!-- 添加图表容器 -->
<div class="chart-container">
<canvas id="requestsChart"></canvas>
</div>
<div class="table-container">
<table id="logsTable">
<thead>
@@ -529,8 +551,12 @@
</div>
</div>
<!-- 添加 Chart.js 库 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.js" integrity="sha512-0im+NZpDrlsC+p6iSc13cqlMNPqdT6e0hUF8NAaxdaGOmPuV9DdVpWYOCHHrMQNVDb2TByQoDbHx34MT6g16ZA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let refreshInterval;
let requestsChart;
function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total || 0;
@@ -676,9 +702,91 @@
return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');
}
function initChart() {
const ctx = document.getElementById('requestsChart').getContext('2d');
requestsChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '每小时请求数',
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 10
}
}
},
plugins: {
title: {
display: true,
text: '24小时请求统计'
}
}
}
});
}
// 更新图表数据
function updateChart(data) {
if (!requestsChart) {
initChart();
}
// 按小时统计请求数量
const hourlyStats = new Map();
const now = new Date();
const past24Hours = new Date(now - 24 * 60 * 60 * 1000);
// 初始化24小时的时间段
for (let i = 0; i < 24; i++) {
const hour = new Date(now - i * 60 * 60 * 1000);
const hourKey = hour.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: 'numeric'
});
hourlyStats.set(hourKey, 0);
}
// 统计每小时的请求数
data.logs.forEach(log => {
const logTime = new Date(log.timestamp);
if (logTime >= past24Hours) {
const hourKey = logTime.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: 'numeric'
});
if (hourlyStats.has(hourKey)) {
hourlyStats.set(hourKey, hourlyStats.get(hourKey) + 1);
}
}
});
// 转换为图表数据
const sortedHours = Array.from(hourlyStats.keys()).reverse();
const counts = sortedHours.map(hour => hourlyStats.get(hour));
requestsChart.data.labels = sortedHours;
requestsChart.data.datasets[0].data = counts;
requestsChart.update();
}
function updateTable(data) {
const tbody = document.getElementById('logsBody');
updateStats(data);
updateChart(data);
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ? `<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` : '-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
}

View File

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

View File

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

View File

@@ -4,7 +4,10 @@ version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] }
base64 = "0.22.1"
# rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] }
rusqlite = "0.32.1"
serde_json = "1.0.138"
[profile.release]
lto = true

View File

@@ -1,28 +1,150 @@
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use rusqlite::Connection;
use std::env;
use std::path::PathBuf;
use std::process::Command;
fn get_machine_id() -> String {
if cfg!(windows) {
let output = Command::new("REG")
.args(&[
"QUERY",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
"/v",
"MachineGuid",
])
.output()
.expect("无法执行 REG 命令");
String::from_utf8_lossy(&output.stdout)
.lines()
.find(|line| line.contains("MachineGuid"))
.and_then(|line| line.split_whitespace().last())
.unwrap_or("unknown")
.to_string()
} else if cfg!(target_os = "macos") {
let output = Command::new("ioreg")
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
.expect("无法执行 ioreg 命令");
String::from_utf8_lossy(&output.stdout)
.lines()
.find(|line| line.contains("IOPlatformUUID"))
.and_then(|line| line.split("\"").nth(3))
.unwrap_or("unknown")
.to_string()
} else if cfg!(target_os = "linux") {
let output = Command::new("sh")
.arg("-c")
.arg("( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :")
.output()
.expect("无法获取 machine-id");
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else if cfg!(target_os = "freebsd") {
let output = Command::new("sh")
.arg("-c")
.arg("kenv -q smbios.system.uuid || sysctl -n kern.hostuuid")
.output()
.expect("无法获取 UUID");
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
"unknown".to_string()
}
}
fn obfuscate_bytes(bytes: &mut [u8]) {
let mut prev: u8 = 165;
for (idx, byte) in bytes.iter_mut().enumerate() {
let old_value = *byte;
*byte = (old_value ^ prev).wrapping_add((idx % 256) as u8);
prev = *byte;
}
}
fn generate_timestamp_header() -> String {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
let mut timestamp_bytes = vec![
((timestamp >> 8) & 0xFF) as u8,
(timestamp & 0xFF) as u8,
((timestamp >> 24) & 0xFF) as u8,
((timestamp >> 16) & 0xFF) as u8,
((timestamp >> 8) & 0xFF) as u8,
(timestamp & 0xFF) as u8,
];
obfuscate_bytes(&mut timestamp_bytes);
BASE64.encode(&timestamp_bytes)
}
fn main() {
let home_dir = env::var("HOME")
.or_else(|_| env::var("USERPROFILE"))
.unwrap();
let db_path = if cfg!(target_os = "windows") {
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb")
} else if cfg!(target_os = "linux") {
PathBuf::from(home_dir).join(".config/Cursor/User/globalStorage/state.vscdb")
} else {
PathBuf::from(home_dir)
let db_path = if cfg!(windows) {
let app_data = env::var("APPDATA").unwrap_or_else(|_| {
let profile = env::var("USERPROFILE").expect("未找到 USERPROFILE 环境变量");
PathBuf::from(profile)
.join("AppData")
.join("Roaming")
.to_string_lossy()
.to_string()
});
PathBuf::from(app_data).join(r"Cursor\User\globalStorage\state.vscdb")
} else if cfg!(target_os = "macos") {
let home = env::var("HOME").expect("未找到 HOME 环境变量");
PathBuf::from(home)
.join("Library/Application Support/Cursor/User/globalStorage/state.vscdb")
} else if cfg!(target_os = "linux") {
let config_home = env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").expect("未找到 HOME 环境变量");
format!("{}/.config", home)
});
PathBuf::from(config_home).join("Cursor/User/globalStorage/state.vscdb")
} else {
panic!("不支持的操作系统平台")
};
match Connection::open(&db_path) {
Ok(conn) => {
match conn.query_row(
let token = conn.query_row(
"SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'",
[],
|row| row.get::<_, String>(0),
);
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(),
) {
Ok(token) => println!("访问令牌: {}", token.trim()),
println!(
"校验和: {}{}/{}",
generate_timestamp_header(),
machine_id,
mac_machine_id
);
}
}
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!("无效选项,请重试"),
}
}
}