v0.1.3-rc.4正式版

This commit is contained in:
wisdgod
2025-01-27 14:03:46 +08:00
parent 76d5b55b5a
commit c58f2697f0
41 changed files with 1956 additions and 964 deletions

View File

@@ -12,11 +12,12 @@ AUTH_TOKEN=
# 共享的认证令牌仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限
SHARED_TOKEN=
# 启用流式响应检查关闭则无法响应错误代价是会对第一个块解析2次
ENABLE_STREAM_CHECK=true
# 启用流式响应检查关闭则无法响应错误代价是会对第一个块解析2次(已弃用)
# 新版本已经完成优化
# ENABLE_STREAM_CHECK=true
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块
INCLUDE_STOP_REASON_STREAM=true
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块(已弃用)
# INCLUDE_STOP_REASON_STREAM=true
# 令牌文件路径(已弃用)
# TOKEN_FILE=.token
@@ -88,3 +89,12 @@ REQUEST_LOGS_LIMIT=100
# Cursor 服务超时(秒)(最大值600)
SERVICE_TIMEOUT=30
# 包含网络引用
INCLUDE_WEB_REFERENCES=false
# 持久化日志文件路径
LOGS_FILE_PATH=logs.bin
# 持久化页面配置文件路径
PAGES_FILE_PATH=pages.bin

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@ node_modules
/logs
/dev*
/build*
/*.bin
/result.txt

421
Cargo.lock generated
View File

@@ -17,6 +17,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -175,6 +186,18 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -211,6 +234,28 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta 0.1.4",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.21.0"
@@ -259,6 +304,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"rkyv 0.7.45",
"serde",
"windows-targets",
]
@@ -315,7 +361,7 @@ dependencies = [
[[package]]
name = "cursor-api"
version = "0.1.3-rc.4-pre.1"
version = "0.1.3-rc.4"
dependencies = [
"axum",
"base64",
@@ -327,6 +373,7 @@ dependencies = [
"gif",
"hex",
"image",
"memmap2",
"parking_lot",
"paste",
"prost",
@@ -334,13 +381,16 @@ dependencies = [
"rand",
"regex",
"reqwest",
"rkyv 0.7.45",
"serde",
"serde_json",
"sha2",
"sonic-rs",
"sysinfo",
"tokio",
"tokio-stream",
"tower-http",
"url",
"uuid",
]
@@ -362,7 +412,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -408,6 +458,18 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "faststr"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9154486833a83cb5d99de8c4d831314b8ae810dd4ef18d89ceb7a9c7c728dd74"
dependencies = [
"bytes",
"rkyv 0.8.10",
"serde",
"simdutf8",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -463,6 +525,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
@@ -507,7 +575,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -596,6 +664,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
@@ -871,7 +948,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -929,7 +1006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.2",
]
[[package]]
@@ -1009,6 +1086,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
dependencies = [
"libc",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -1042,6 +1128,26 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "munge"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64142d38c84badf60abf06ff9bd80ad2174306a5b11bd4706535090a30a419df"
dependencies = [
"munge_macro",
]
[[package]]
name = "munge_macro"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "native-tls"
version = "0.2.12"
@@ -1115,7 +1221,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1228,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1266,7 +1372,7 @@ dependencies = [
"prost",
"prost-types",
"regex",
"syn",
"syn 2.0.96",
"tempfile",
]
@@ -1280,7 +1386,7 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1292,6 +1398,46 @@ dependencies = [
"prost",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive 0.1.4",
]
[[package]]
name = "ptr_meta"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90"
dependencies = [
"ptr_meta_derive 0.3.0",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -1307,6 +1453,21 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rancor"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947"
dependencies = [
"ptr_meta 0.3.0",
]
[[package]]
name = "rand"
version = "0.8.5"
@@ -1346,6 +1507,26 @@ dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "ref-cast"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -1375,6 +1556,21 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "rend"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215"
[[package]]
name = "reqwest"
version = "0.12.12"
@@ -1438,6 +1634,64 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta 0.1.4",
"rend 0.4.2",
"rkyv_derive 0.7.45",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e147371c75553e1e2fcdb483944a8540b8438c31426279553b9a8182a9b7b65"
dependencies = [
"bytes",
"hashbrown 0.15.2",
"indexmap",
"munge",
"ptr_meta 0.3.0",
"rancor",
"rend 0.5.2",
"rkyv_derive 0.8.10",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rkyv_derive"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -1523,6 +1777,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
@@ -1563,7 +1823,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1617,12 +1877,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.9"
@@ -1648,6 +1923,44 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "sonic-number"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a74044c092f4f43ca7a6cfd62854cf9fb5ac8502b131347c990bf22bef1dfe"
dependencies = [
"cfg-if",
]
[[package]]
name = "sonic-rs"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0275f9f2f07d47556fe60c2759da8bc4be6083b047b491b2d476aa0bfa558eb1"
dependencies = [
"bumpalo",
"bytes",
"cfg-if",
"faststr",
"itoa",
"ref-cast",
"ryu",
"serde",
"simdutf8",
"sonic-number",
"sonic-simd",
"thiserror 2.0.11",
]
[[package]]
name = "sonic-simd"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940a24e82c9a97483ef66cef06b92160a8fa5cd74042c57c10b24d99d169d2fc"
dependencies = [
"cfg-if",
]
[[package]]
name = "spin"
version = "0.9.8"
@@ -1666,6 +1979,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.96"
@@ -1694,7 +2018,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1731,6 +2055,12 @@ dependencies = [
"libc",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.15.0"
@@ -1751,7 +2081,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.11",
]
[[package]]
@@ -1762,7 +2101,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
@@ -1775,6 +2125,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.43.0"
@@ -1786,6 +2151,7 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
@@ -1799,7 +2165,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -1830,7 +2196,7 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror",
"thiserror 1.0.69",
"tokio",
]
@@ -2027,7 +2393,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
"wasm-bindgen-shared",
]
@@ -2062,7 +2428,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2166,7 +2532,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -2177,7 +2543,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -2313,6 +2679,15 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "yoke"
version = "0.7.5"
@@ -2333,7 +2708,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
"synstructure",
]
@@ -2355,7 +2730,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]
@@ -2375,7 +2750,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
"synstructure",
]
@@ -2404,7 +2779,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.96",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "cursor-api"
version = "0.1.3-rc.4-pre.1"
version = "0.1.3-rc.4"
edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"]
description = "OpenAI format compatibility layer for the Cursor API"
@@ -16,26 +16,30 @@ 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"] }
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 = "1.0.137"
serde_json = { package = "sonic-rs", version = "0.3.17" }
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"] }
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"] }
[profile.release]

View File

@@ -1,9 +1,5 @@
[target.x86_64-unknown-linux-gnu]
dockerfile = "Dockerfile.cross"
[target.x86_64-unknown-freebsd]
pre-build = [
"pkg update",
"pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl",
"export SSL_CERT_FILE=/etc/ssl/cert.pem"
]
[target.aarch64-unknown-linux-gnu]
dockerfile = "Dockerfile.cross.arm64"

179
Cursor API.md Normal file
View File

@@ -0,0 +1,179 @@
# Cursor API
## 项目说明
### 版本声明
- 当前版本已进入稳定阶段
- 以下问题与程序无关,请勿反馈:
- 响应缺字漏字
- 首字延迟现象
- 响应出现乱码
- 性能优势:
- 达到原生客户端响应速度
- 部分场景下表现更优
- 开源协议要求:
- Fork 项目禁止以原作者名义进行宣传推广
- 禁止发布任何形式的官方声明
![Cursor API 架构示意图](https://via.placeholder.com/800x400.png?text=Cursor+API+Architecture)
## 快速入门
### 密钥获取
1. 访问 [Cursor 官网](https://www.cursor.com) 完成注册登录
2. 开启浏览器开发者工具 (F12)
3. 在 Application → Cookies 中定位 `WorkosCursorSessionToken`
4. 复制第三个字段值(注意:`%3A%3A``::` 的 URL 编码形式)
## 配置指南
### 环境变量
| 变量名 | 类型 | 默认值 | 说明 |
|--------|------|--------|-----|
| PORT | int | 3000 | 服务端口号 |
| AUTH_TOKEN | string | 无 | 认证令牌(必需) |
| ROUTE_PREFIX | string | 无 | 路由前缀 |
| TOKEN_LIST_FILE | string | .tokens | Token 存储文件 |
完整配置参见 [env-example](/env-example)
### Token 文件规范
`.tokens` 文件格式:
```plaintext
# 注释行将在下次读取时自动删除
token1,checksum1
token2,checksum2
```
文件管理原则:
- 系统自动维护文件内容
- 仅以下情况需要手动编辑:
- 删除特定 token
- 绑定已有 checksum 到指定 token
## 模型支持列表
```json
[
"claude-3.5-sonnet",
"gpt-4",
"gpt-4o",
"cursor-fast",
"gpt-4o-mini",
"deepseek-v3"
]
```
*注:模型列表为固定配置,暂不支持自定义扩展*
## API 文档
### 基础对话接口
**Endpoint**
`POST /v1/chat/completions`
**认证方式**
`Bearer Token` 三级认证机制:
1. 环境变量 `AUTH_TOKEN`
2. `.token` 文件轮询
3. 直接 token,checksum 认证v0.1.3-rc.3+
**请求示例**
```json
{
"model": "gpt-4",
"messages": [
{
"role": "user",
"content": "解释量子计算的基本原理"
}
],
"stream": false
}
```
**响应示例(非流式)**
```json
{
"id": "chatcmpl-9Xy...",
"object": "chat.completion",
"created": 1628063500,
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "量子计算基于量子比特..."
},
"finish_reason": "stop"
}]
}
```
### Token 管理接口
| 端点 | 方法 | 功能 |
|------|------|-----|
| `/tokens` | GET | Token 信息管理界面 |
| `/tokens/update` | POST | 批量更新 Token 列表 |
| `/tokens/add` | POST | 增量添加 Token |
| `/tokens/delete` | POST | 删除指定 Token |
```mermaid
sequenceDiagram
participant Client
participant API
Client->>API: POST /tokens/add
API->>API: 验证Token有效性
API->>File: 写入.tokens
API-->>Client: 返回更新结果
```
## 高级功能
### 动态密钥生成
**Endpoint**
`POST /build-key`
**优势对比**
| 特性 | 传统模式 | 动态密钥 |
|------|---------|---------|
| 密钥长度 | 较长 | 优化缩短 |
| 配置扩展 | 无 | 支持自定义 |
| 安全等级 | 基础 | 增强编码 |
| 验证效率 | 预校验耗时 | 即时验证 |
## 系统监控
### 健康检查
**Endpoint**
`GET /health`
**响应示例**
```json
{
"status": "success",
"version": "1.2.0",
"uptime": 86400,
"models": ["gpt-4", "claude-3.5"],
"endpoints": ["/v1/chat", "/tokens"]
}
```
## 生态工具
### 开发辅助工具
- [Token 获取工具](https://github.com/wisdgod/cursor-api/tree/main/tools/get-token)
支持 Windows/Linux/macOS 系统
- [遥测数据重置工具](https://github.com/wisdgod/cursor-api/tree/main/tools/reset-telemetry)
清除用户使用数据记录
## 致谢声明
本项目的发展离不开以下开源项目的启发:
- [zhx47/cursor-api](https://github.com/zhx47/cursor-api) - 基础架构参考
- [cursorToApi](https://github.com/luolazyandlazy/cursorToApi) - 认证机制优化方案
---
> **项目维护说明**
> 我们欢迎社区贡献,但请注意:
> 1. 功能请求需附带使用场景说明
> 2. Bug 报告请提供复现步骤和环境信息
> 3. 重要变更需通过 CI/CD 测试流程

55
Deno.ts
View File

@@ -1,55 +0,0 @@
// 定义允许的主机和路径
const ALLOWED_HOSTS = ["api2.cursor.sh", "www.cursor.com"];
const ALLOWED_PATHS = [
"/aiserver.v1.AiService/StreamChat",
"/auth/full_stripe_profile",
"/api/usage",
"/api/auth/me"
];
// 创建统一的响应处理函数
const createResponse = (status: number, message: string) =>
new Response(message, {
status,
headers: { "Access-Control-Allow-Origin": "*" }
});
// 主处理函数
Deno.serve(async (request: Request) => {
// 验证目标主机
const targetHost = request.headers.get("x-co");
if (!targetHost) return createResponse(400, "Missing header");
if (!ALLOWED_HOSTS.includes(targetHost)) return createResponse(403, "Host denied");
// 验证请求路径
const url = new URL(request.url);
if (!ALLOWED_PATHS.includes(url.pathname)) return createResponse(404, "Path invalid");
// 处理请求头
const headers = new Headers(request.headers);
headers.delete("x-co");
headers.set("Host", targetHost);
try {
// 转发请求
const response = await fetch(
`https://${targetHost}${url.pathname}${url.search}`,
{
method: request.method,
headers,
body: request.body
}
);
// 处理响应头
const responseHeaders = new Headers(response.headers);
responseHeaders.set("Access-Control-Allow-Origin", "*");
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return createResponse(500, "Server error");
}
});

View File

@@ -6,7 +6,7 @@ RUN apt-get update && \
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
&& rm -rf /var/lib/apt/lists/*
COPY . .
ENV RUSTFLAGS="-C link-arg=-s"
ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native"
RUN cargo build --release && \
cp target/release/cursor-api /app/cursor-api
@@ -18,7 +18,7 @@ RUN apt-get update && \
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
&& rm -rf /var/lib/apt/lists/*
COPY . .
ENV RUSTFLAGS="-C link-arg=-s"
ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native"
RUN cargo build --release && \
cp target/release/cursor-api /app/cursor-api

View File

@@ -16,7 +16,7 @@ RUN apt-get update && \
&& rm -rf /var/lib/apt/lists/*
# 设置环境变量 (如果需要)
ENV RUSTFLAGS="-C link-arg=-s"
# ENV RUSTFLAGS="-C link-arg=-s"
# 设置 PROTOC 环境变量 (因为你的 build.rs 需要)
ENV PROTOC=/usr/bin/protoc

30
Dockerfile.cross.arm64 Normal file
View File

@@ -0,0 +1,30 @@
# Dockerfile.cross
FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm
WORKDIR /app
# 安装必要的软件包
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
pkg-config \
libssl-dev \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
# 设置环境变量 (如果需要)
# ENV RUSTFLAGS="-C link-arg=-s"
# 设置 PROTOC 环境变量 (因为你的 build.rs 需要)
ENV PROTOC=/usr/bin/protoc
# 安装特定版本的 protoc (如果你需要特定版本,例如 29.3;否则可以删除这部分)
# ENV PROTOC_VERSION=29.3
# ENV PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-x86_64.zip
# RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP} -O /tmp/${PROTOC_ZIP} && \
# unzip /tmp/${PROTOC_ZIP} -d /usr && \
# rm /tmp/${PROTOC_ZIP}
# 验证安装
RUN protoc --version

View File

@@ -171,7 +171,43 @@ data: [DONE]
"tokens": [
{
"token": "string",
"checksum": "string"
"checksum": "string",
"profile": { // 可能存在
"usage": {
"premium": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"standard": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"unknown": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
}
},
"user": {
"email": "string",
"name": "string",
"id": "string",
"updated_at": "string"
},
"stripe": {
"membership_type": "free" | "free_trial" | "pro" | "enterprise",
"payment_id": "string",
"days_remaining_on_trial": number
}
}
}
],
"tokens_count": number
@@ -282,14 +318,13 @@ data: [DONE]
```json
{
"auth_token": "string", // 格式: {token},{checksum}
"enable_stream_check": boolean, // 可选,启用流式响应首块检查
"include_stop_stream": boolean, // 可选,包含停止流
"disable_vision": boolean, // 可选,禁用图片处理能力
"enable_slow_pool": boolean, // 可选,启用慢速池
"usage_check_models": { // 可选,使用量检查模型配置
"type": "default" | "disabled" | "all" | "custom",
"model_ids": "string" // 当type为custom时生效以逗号分隔的模型ID列表
}
},
"include_web_references": boolean
}
```
@@ -357,8 +392,6 @@ data: [DONE]
"type": "default" | "text" | "html",
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
@@ -368,7 +401,8 @@ data: [DONE]
},
"enable_dynamic_key": boolean,
"share_token": "string",
"proxies": "" | "system" | "proxy1,proxy2,..."
"proxies": "" | "system" | "proxy1,proxy2,...",
"include_web_references": boolean
}
```
@@ -383,8 +417,6 @@ data: [DONE]
"type": "default" | "text" | "html", // 对于js和css后两者是一样的
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all",
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
@@ -394,7 +426,8 @@ data: [DONE]
},
"enable_dynamic_key": boolean,
"share_token": "string",
"proxies": "" | "system" | "proxy1,proxy2,..."
"proxies": "" | "system" | "proxy1,proxy2,...",
"include_web_references": boolean
}
}
```

1
serve.ts Normal file
View File

@@ -0,0 +1 @@
Deno.serve(async(r:Request)=>{const rs=(s:number,m:string)=>new Response(m,{status:s,headers:{"Access-Control-Allow-Origin":"*"}});const h=r.headers.get("x-co");if(!h)return rs(400,"Missing header");const a=["api2.cursor.sh","www.cursor.com"];if(!a.includes(h))return rs(403,"Host denied");const u=new URL(r.url),p=["/aiserver.v1.AiService/StreamChat","/aiserver.v1.AiService/StreamChatWeb","/auth/full_stripe_profile","/api/usage","/api/auth/me"];if(!p.includes(u.pathname))return rs(404,"Path invalid");const hd=new Headers(r.headers);hd.delete("x-co");hd.set("Host",h);try{const f=await fetch(`https://${h}${u.pathname}${u.search}`,{method:r.method,headers:hd,body:r.body});const fh=new Headers(f.headers);fh.set("Access-Control-Allow-Origin","*");return new Response(f.body,{status:f.status,headers:fh})}catch(e){return rs(500,"Server error")}});

View File

@@ -65,8 +65,6 @@ pub async fn handle_config_update(
status: ApiStatus::Success,
data: Some(ConfigData {
page_content: AppConfig::get_page_content(&request.path),
enable_stream_check: AppConfig::get_stream_check(),
include_stop_stream: AppConfig::get_stop_stream(),
vision_ability: AppConfig::get_vision_ability(),
enable_slow_pool: AppConfig::get_slow_pool(),
enable_all_claude: AppConfig::get_allow_claude(),
@@ -74,6 +72,7 @@ pub async fn handle_config_update(
enable_dynamic_key: AppConfig::get_dynamic_key(),
share_token: AppConfig::get_share_token(),
proxies: AppConfig::get_proxies(),
include_web_references: AppConfig::get_web_refs(),
}),
message: None,
})),
@@ -96,8 +95,6 @@ pub async fn handle_config_update(
}
handle_updates!(request,
enable_stream_check => AppConfig::update_stream_check,
include_stop_stream => AppConfig::update_stop_stream,
vision_ability => AppConfig::update_vision_ability,
enable_slow_pool => AppConfig::update_slow_pool,
enable_all_claude => AppConfig::update_allow_claude,
@@ -105,6 +102,7 @@ pub async fn handle_config_update(
enable_dynamic_key => AppConfig::update_dynamic_key,
share_token => AppConfig::update_share_token,
proxies => AppConfig::update_proxies,
include_web_references => AppConfig::update_web_refs,
);
Ok(Json(NormalResponse {
@@ -131,8 +129,6 @@ pub async fn handle_config_update(
}
handle_resets!(request,
enable_stream_check => AppConfig::reset_stream_check,
include_stop_stream => AppConfig::reset_stop_stream,
vision_ability => AppConfig::reset_vision_ability,
enable_slow_pool => AppConfig::reset_slow_pool,
enable_all_claude => AppConfig::reset_allow_claude,
@@ -140,6 +136,7 @@ pub async fn handle_config_update(
enable_dynamic_key => AppConfig::reset_dynamic_key,
share_token => AppConfig::reset_share_token,
proxies => AppConfig::reset_proxies,
include_web_references => AppConfig::reset_web_refs,
);
Ok(Json(NormalResponse {

View File

@@ -108,6 +108,12 @@ def_cursor_api_url!(
"/aiserver.v1.AiService/StreamChat"
);
def_cursor_api_url!(
CURSOR_API2_CHAT_WEB_URL,
CURSOR_API2_HOST,
"/aiserver.v1.AiService/StreamChatWeb"
);
def_cursor_api_url!(
CURSOR_API2_STRIPE_URL,
CURSOR_API2_HOST,
@@ -118,6 +124,12 @@ 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"));
pub(super) static PAGES_FILE_PATH: LazyLock<String> =
LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin"));
pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
// 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log"

View File

@@ -12,18 +12,22 @@ use crate::{
},
};
use parking_lot::RwLock;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
mod usage_check;
pub use usage_check::UsageCheck;
mod config;
mod proxies;
pub use proxies::Proxies;
mod build_key;
pub use build_key::*;
use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS};
// 页面内容类型枚举
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)]
#[serde(tag = "type", content = "content")]
pub enum PageContent {
#[serde(rename = "default")]
@@ -41,10 +45,8 @@ impl Default for PageContent {
}
// 静态配置
#[derive(Clone)]
#[derive(Default, Clone)]
pub struct AppConfig {
stream_check: bool,
stop_stream: bool,
vision_ability: VisionAbility,
slow_pool: bool,
allow_claude: bool,
@@ -54,9 +56,10 @@ pub struct AppConfig {
share_token: String,
is_share: bool,
proxies: Proxies,
web_refs: bool,
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum VisionAbility {
#[serde(rename = "none", alias = "disabled")]
None,
@@ -87,7 +90,7 @@ impl Default for VisionAbility {
}
}
#[derive(Clone, Default)]
#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct Pages {
pub root_content: PageContent,
pub logs_content: PageContent,
@@ -114,24 +117,6 @@ pub struct AppState {
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
LazyLock::new(|| RwLock::new(AppConfig::default()));
impl Default for AppConfig {
fn default() -> Self {
Self {
stream_check: true,
stop_stream: true,
vision_ability: VisionAbility::Base64,
slow_pool: false,
allow_claude: false,
pages: Pages::default(),
usage_check: UsageCheck::Default,
dynamic_key: false,
share_token: String::default(),
is_share: false,
proxies: Proxies::default(),
}
}
}
macro_rules! config_methods {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
@@ -207,8 +192,6 @@ macro_rules! config_methods_clone {
impl AppConfig {
pub fn init() {
let mut config = APP_CONFIG.write();
config.stream_check = parse_bool_from_env("ENABLE_STREAM_CHECK", true);
config.stop_stream = parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true);
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);
@@ -221,15 +204,15 @@ impl AppConfig {
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! {
stream_check: bool, true;
stop_stream: bool, true;
slow_pool: bool, false;
allow_claude: bool, false;
dynamic_key: bool, false;
web_refs: bool, false;
}
config_methods_clone! {
@@ -341,11 +324,20 @@ impl AppConfig {
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() })
});
Self {
total_requests: 0,
total_requests: request_logs.len() as u64,
active_requests: 0,
error_requests: 0,
request_logs: Vec::new(),
error_requests: request_logs
.iter()
.filter(|log| matches!(log.status, LogStatus::Failed))
.count() as u64,
request_logs,
token_infos,
}
}
@@ -357,8 +349,43 @@ impl AppState {
}
}
#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub enum LogStatus {
Pending,
Success,
Failed,
}
impl Serialize for LogStatus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str_name())
}
}
impl LogStatus {
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Pending => STATUS_PENDING,
Self::Success => STATUS_SUCCESS,
Self::Failed => STATUS_FAILED,
}
}
pub fn from_str_name(s: &str) -> Option<Self> {
match s {
STATUS_PENDING => Some(Self::Pending),
STATUS_SUCCESS => Some(Self::Success),
STATUS_FAILED => Some(Self::Failed),
_ => None,
}
}
}
// 请求日志
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct RequestLog {
pub id: u64,
pub timestamp: chrono::DateTime<chrono::Local>,
@@ -368,12 +395,12 @@ pub struct RequestLog {
pub prompt: Option<String>,
pub timing: TimingInfo,
pub stream: bool,
pub status: &'static str,
pub status: LogStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct TimingInfo {
pub total: f64, // 总用时(秒)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -390,7 +417,7 @@ pub struct ChatRequest {
}
// 用于存储 token 信息
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct TokenInfo {
pub token: String,
pub checksum: String,

View File

@@ -4,23 +4,15 @@ use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS};
#[derive(Deserialize)]
pub struct BuildKeyRequest {
// 认证令牌(必需)
pub auth_token: String,
// 流第一个块检查
#[serde(default)]
pub enable_stream_check: Option<bool>,
// 包含停止流
#[serde(default)]
pub include_stop_stream: Option<bool>,
// 是否禁用图片处理能力
#[serde(default)]
pub disable_vision: Option<bool>,
// 慢速池
#[serde(default)]
pub enable_slow_pool: Option<bool>,
// 使用量检查模型规则
#[serde(default)]
pub usage_check_models: Option<UsageCheckModelConfig>,
#[serde(default)]
pub include_web_references: Option<bool>,
}
pub struct UsageCheckModelConfig {
pub model_type: UsageCheckModelType,

114
src/app/model/config.rs Normal file
View File

@@ -0,0 +1,114 @@
use memmap2::{MmapMut, MmapOptions};
use rkyv::{archived_root, Deserialize as _};
use std::fs::OpenOptions;
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)),
};
// 添加文件大小检查
if file.metadata()?.len() > usize::MAX as u64 {
return Err("日志文件过大".into());
}
// 创建只读内存映射
let mmap = unsafe { MmapOptions::new().map(&file)? };
// 验证并反序列化数据
let archived = unsafe { archived_root::<Vec<RequestLog>>(&mmap) };
Ok(archived.deserialize(&mut rkyv::Infallible)?)
}
}
impl AppConfig {
pub fn save_config() -> Result<(), Box<dyn std::error::Error>> {
let pages = APP_CONFIG.read().pages.clone();
let bytes = rkyv::to_bytes::<_, 256>(&pages)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(PAGES_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 fn load_saved_config() -> Result<(), Box<dyn std::error::Error>> {
let file = match OpenOptions::new().read(true).open(PAGES_FILE_PATH.as_str()) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(());
}
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 { archived_root::<Pages>(&mmap) };
let pages = archived.deserialize(&mut rkyv::Infallible)?;
let mut config = APP_CONFIG.write();
config.pages = pages;
Ok(())
}
}

View File

@@ -1,6 +1,7 @@
use reqwest::{Client, Proxy};
use serde::{Serialize, Serializer};
use serde::{Deserialize, Deserializer};
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use crate::app::constant::COMMA_STRING;
@@ -30,7 +31,7 @@ impl<'de> Deserialize<'de> for Proxies {
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
Ok(Proxies::from_str(&s))
}
}

View File

@@ -3,6 +3,7 @@ use crate::{
chat::{config::key_config, constant::AVAILABLE_MODELS},
};
use serde::{Deserialize, Serialize};
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
#[derive(Clone, PartialEq)]
pub enum UsageCheck {
@@ -108,7 +109,7 @@ impl<'de> Deserialize<'de> for UsageCheck {
Custom(String),
}
let helper = UsageCheckHelper::deserialize(deserializer)?;
let helper = <UsageCheckHelper as serde::Deserialize>::deserialize(deserializer)?;
Ok(match helper {
UsageCheckHelper::None => UsageCheck::None,
UsageCheckHelper::Default => UsageCheck::Default,

View File

@@ -15,8 +15,7 @@ use crate::{
use super::{
aiserver::v1::{
conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext,
GetChatRequest, ImageProto, ModelDetails,
conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
},
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
model::{Message, MessageContent, Role},
@@ -25,7 +24,7 @@ use super::{
async fn process_chat_inputs(
inputs: Vec<Message>,
disable_vision: bool,
) -> (String, Vec<ConversationMessage>) {
) -> (String, Vec<ConversationMessage>, Vec<String>) {
// 收集 system 指令
let instructions = inputs
.iter()
@@ -98,9 +97,25 @@ async fn process_chat_inputs(
file_diff_trajectories: vec![],
conversation_summary: None,
}],
vec![],
);
}
// 处理 WebReferences 开头的 assistant 消息
chat_inputs = chat_inputs
.into_iter()
.map(|mut input| {
if let (Role::Assistant, MessageContent::Text(text)) = (&input.role, &input.content) {
if text.starts_with("WebReferences:") {
if let Some(pos) = text.find("\n\n") {
input.content = MessageContent::Text(text[pos + 2..].to_owned());
}
}
}
input
})
.collect();
// 如果第一条是 assistant插入空的 user 消息
if chat_inputs
.first()
@@ -226,7 +241,32 @@ async fn process_chat_inputs(
});
}
(instructions, messages)
let mut urls = Vec::new();
if let Some(last_msg) = messages.last() {
if last_msg.r#type == conversation_message::MessageType::Human as i32 {
let text = &last_msg.text;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '@' {
let mut url = String::new();
while let Some(&next_char) = chars.peek() {
if next_char.is_whitespace() {
break;
}
url.push(chars.next().unwrap());
}
if let Ok(parsed_url) = url::Url::parse(&url) {
if parsed_url.scheme() == "http" || parsed_url.scheme() == "https" {
urls.push(url);
}
}
}
}
}
}
(instructions, messages, urls)
}
async fn fetch_image_data(
@@ -344,6 +384,7 @@ pub async fn encode_chat_message(
model_name: &str,
disable_vision: bool,
enable_slow_pool: bool,
is_search: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁
let enable_slow_pool = {
@@ -354,7 +395,7 @@ pub async fn encode_chat_message(
}
};
let (instructions, messages) = process_chat_inputs(inputs, disable_vision).await;
let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await;
let explicit_context = if !instructions.trim().is_empty() {
Some(ExplicitContext {
@@ -365,6 +406,15 @@ pub async fn encode_chat_message(
None
};
let base_uuid = rand::random::<u16>();
let external_links = urls.into_iter().enumerate().map(|(i, url)| {
let uuid = base_uuid.wrapping_add(i as u16);
ChatExternalLink {
url,
uuid: uuid.to_string(),
}
}).collect();
let chat = GetChatRequest {
current_file: None,
conversation: messages,
@@ -394,11 +444,15 @@ pub async fn encode_chat_message(
is_bash: Some(false),
conversation_id: Uuid::new_v4().to_string(),
can_handle_filenames_after_language_ids: Some(true),
use_web: None,
use_web: if is_search {
Some("full_search".to_string())
} else {
None
},
quotes: vec![],
debug_info: None,
workspace_id: None,
external_links: vec![],
external_links,
commit_notes: vec![],
long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)),
is_eval: Some(false),

View File

@@ -6,21 +6,14 @@ impl KeyConfig {
pub fn new_with_global() -> Self {
Self {
auth_token: None,
enable_stream_check: Some(AppConfig::get_stream_check()),
include_stop_stream: Some(AppConfig::get_stop_stream()),
disable_vision: Some(AppConfig::get_vision_ability().is_none()),
enable_slow_pool: Some(AppConfig::get_slow_pool()),
usage_check_models: None,
include_web_references: Some(AppConfig::get_web_refs()),
}
}
pub fn copy_without_auth_token(&self, config: &mut Self) {
if self.enable_stream_check.is_some() {
config.enable_stream_check = self.enable_stream_check;
}
if self.include_stop_stream.is_some() {
config.include_stop_stream = self.include_stop_stream;
}
if self.disable_vision.is_some() {
config.disable_vision = self.disable_vision;
}
@@ -30,5 +23,8 @@ impl KeyConfig {
if self.usage_check_models.is_some() {
config.usage_check_models = self.usage_check_models.clone();
}
if self.include_web_references.is_some() {
config.include_web_references = self.include_web_references;
}
}
}

View File

@@ -17,12 +17,6 @@ message KeyConfig {
// 认证令牌(必需)
TokenInfo auth_token = 1;
// 是否启用流检查
optional bool enable_stream_check = 2;
// 是否包含停止流
optional bool include_stop_stream = 3;
// 是否禁用图片处理能力
optional bool disable_vision = 4;
@@ -44,6 +38,9 @@ message KeyConfig {
// 使用量检查模型规则
optional UsageCheckModel usage_check_models = 6;
// 包含网络引用
optional bool include_web_references = 7;
// 密码SHA256哈希值
// bytes secret = 2;
}

View File

@@ -6,7 +6,10 @@ macro_rules! def_pub_const {
};
}
def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF");
def_pub_const!(ERR_UNSUPPORTED_IMAGE_FORMAT, "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF");
def_pub_const!(
ERR_UNSUPPORTED_IMAGE_FORMAT,
"不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF"
);
def_pub_const!(ERR_NODATA, "No data");
const MODEL_OBJECT: &str = "model";
@@ -45,146 +48,137 @@ 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");
pub const AVAILABLE_MODELS: [Model; 23] = [
#[derive(Clone, PartialEq, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
pub enum ModelType {
Claude35Sonnet,
Gpt4,
Gpt4o,
Claude3Opus,
CursorFast,
CursorSmall,
Gpt35Turbo,
Gpt4Turbo202404,
Gpt4o128k,
Gemini15Flash500k,
Claude3Haiku200k,
Claude35Sonnet200k,
Claude35Sonnet20241022,
Gpt4oMini,
O1Mini,
O1Preview,
O1,
Claude35Haiku,
GeminiExp1206,
Gemini20FlashThinkingExp,
Gemini20FlashExp,
DeepseekV3,
DeepseekR1,
}
macro_rules! create_model {
($($id:expr, $owner:expr),* $(,)?) => {
pub const AVAILABLE_MODELS: [Model; count!($( ($id, $owner) )*)] = [
$(
Model {
id: CLAUDE_3_5_SONNET,
id: $id,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
owned_by: $owner,
},
Model {
id: GPT_4,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: GPT_4O,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: CLAUDE_3_OPUS,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
},
Model {
id: CURSOR_FAST,
created: CREATED,
object: MODEL_OBJECT,
owned_by: CURSOR,
},
Model {
id: CURSOR_SMALL,
created: CREATED,
object: MODEL_OBJECT,
owned_by: CURSOR,
},
Model {
id: GPT_3_5_TURBO,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: GPT_4_TURBO_2024_04_09,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: GPT_4O_128K,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: GEMINI_1_5_FLASH_500K,
created: CREATED,
object: MODEL_OBJECT,
owned_by: GOOGLE,
},
Model {
id: CLAUDE_3_HAIKU_200K,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
},
Model {
id: CLAUDE_3_5_SONNET_200K,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
},
Model {
id: CLAUDE_3_5_SONNET_20241022,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
},
Model {
id: GPT_4O_MINI,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: O1_MINI,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: O1_PREVIEW,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: O1,
created: CREATED,
object: MODEL_OBJECT,
owned_by: OPENAI,
},
Model {
id: CLAUDE_3_5_HAIKU,
created: CREATED,
object: MODEL_OBJECT,
owned_by: ANTHROPIC,
},
Model {
id: GEMINI_EXP_1206,
created: CREATED,
object: MODEL_OBJECT,
owned_by: GOOGLE,
},
Model {
id: GEMINI_2_0_FLASH_THINKING_EXP,
created: CREATED,
object: MODEL_OBJECT,
owned_by: GOOGLE,
},
Model {
id: GEMINI_2_0_FLASH_EXP,
created: CREATED,
object: MODEL_OBJECT,
owned_by: GOOGLE,
},
Model {
id: DEEPSEEK_V3,
created: CREATED,
object: MODEL_OBJECT,
owned_by: DEEPSEEK,
},
Model {
id: DEEPSEEK_R1,
created: CREATED,
object: MODEL_OBJECT,
owned_by: DEEPSEEK,
},
];
)*
];
};
}
macro_rules! count {
() => (0);
(($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
}
impl ModelType {
pub fn as_str_name(&self) -> &'static str {
match self {
ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET,
ModelType::Gpt4 => GPT_4,
ModelType::Gpt4o => GPT_4O,
ModelType::Claude3Opus => CLAUDE_3_OPUS,
ModelType::CursorFast => CURSOR_FAST,
ModelType::CursorSmall => CURSOR_SMALL,
ModelType::Gpt35Turbo => GPT_3_5_TURBO,
ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09,
ModelType::Gpt4o128k => GPT_4O_128K,
ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K,
ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K,
ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K,
ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022,
ModelType::Gpt4oMini => GPT_4O_MINI,
ModelType::O1Mini => O1_MINI,
ModelType::O1Preview => O1_PREVIEW,
ModelType::O1 => O1,
ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU,
ModelType::GeminiExp1206 => GEMINI_EXP_1206,
ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP,
ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP,
ModelType::DeepseekV3 => DEEPSEEK_V3,
ModelType::DeepseekR1 => DEEPSEEK_R1,
}
}
pub 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,
}
}
}
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,
);
pub const USAGE_CHECK_MODELS: [&str; 11] = [
CLAUDE_3_5_SONNET_20241022,

View File

@@ -125,7 +125,6 @@ impl ErrorResponse {
pub enum StreamError {
ChatError(ChatError),
DataLengthLessThan5,
EmptyMessage,
}
impl std::fmt::Display for StreamError {
@@ -133,7 +132,6 @@ impl std::fmt::Display for StreamError {
match self {
StreamError::ChatError(error) => write!(f, "{}", error.error.code),
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
StreamError::EmptyMessage => write!(f, "empty message"),
}
}
}

View File

@@ -86,8 +86,8 @@ pub struct Model {
pub owned_by: &'static str,
}
use crate::app::model::{AppConfig, UsageCheck};
use super::constant::USAGE_CHECK_MODELS;
use crate::app::model::{AppConfig, UsageCheck};
impl Model {
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {

View File

@@ -164,11 +164,10 @@ pub async fn handle_build_key(
// 构建 proto 消息
let mut key_config = KeyConfig {
auth_token: Some(token_info),
enable_stream_check: request.enable_stream_check,
include_stop_stream: request.include_stop_stream,
disable_vision: request.disable_vision,
enable_slow_pool: request.enable_slow_pool,
usage_check_models: None,
include_web_references: request.include_web_references,
};
if let Some(usage_check_models) = request.usage_check_models {

View File

@@ -2,12 +2,13 @@ use crate::{
app::{
constant::{
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS,
OBJECT_CHAT_COMPLETION_CHUNK,
},
lazy::{
AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT,
lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT},
model::{
AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo,
UsageCheck,
},
model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo, UsageCheck},
},
chat::{
config::KeyConfig,
@@ -16,11 +17,11 @@ use crate::{
model::{
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
},
stream::{parse_stream_data, StreamMessage},
stream::{StreamDecoder, StreamMessage},
},
common::{
client::build_client,
model::{error::ChatError, userinfo::MembershipType, ErrorResponse},
model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse},
utils::{
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
validate_token_and_checksum,
@@ -38,16 +39,13 @@ use axum::{
Json,
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
use futures::StreamExt;
use prost::Message as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{
convert::Infallible,
sync::{atomic::AtomicBool, Arc},
};
use std::{
pin::Pin,
sync::atomic::{AtomicUsize, Ordering},
};
use tokio::sync::Mutex;
use uuid::Uuid;
@@ -66,8 +64,16 @@ pub async fn handle_chat(
Json(request): Json<ChatRequest>,
) -> Result<Response<Body>, (StatusCode, Json<ErrorResponse>)> {
let allow_claude = AppConfig::get_allow_claude();
let is_search = request.model.ends_with("-online");
let model_name = if is_search {
request.model[..request.model.len() - 7].to_string()
} else {
request.model.clone()
};
// 验证模型是否支持并获取模型信息
let model = AVAILABLE_MODELS.iter().find(|m| m.id == request.model);
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")) {
@@ -168,7 +174,7 @@ pub async fn handle_chat(
return false;
}
let is_premium = USAGE_CHECK_MODELS.contains(&request.model.as_str());
let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str());
let standard = &profile.usage.standard;
let premium = &profile.usage.premium;
@@ -213,14 +219,28 @@ pub async fn handle_chat(
tokio::spawn(async move {
let profile = get_token_profile(&auth_token_clone).await;
let mut state = state_clone.lock().await;
// 根据id查找对应的日志
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == log_id)
{
log.token_info.profile = profile;
// 先找到所有需要更新的位置的索引
let token_info_idx = state
.token_infos
.iter()
.position(|info| info.token == auth_token_clone);
let log_idx = state.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;
}
(Some(t_idx), None) => {
state.token_infos[t_idx].profile = profile;
}
(None, Some(l_idx)) => {
state.request_logs[l_idx].token_info.profile = profile;
}
(None, None) => {}
}
});
}
@@ -240,7 +260,7 @@ pub async fn handle_chat(
first: None,
},
stream: request.stream,
status: STATUS_PENDING,
status: LogStatus::Pending,
error: None,
});
@@ -252,9 +272,10 @@ pub async fn handle_chat(
// 将消息转换为hex格式
let hex_data = match super::adapter::encode_chat_message(
request.messages,
&request.model,
&model_name,
current_config.disable_vision(),
current_config.enable_slow_pool(),
is_search,
)
.await
{
@@ -267,7 +288,7 @@ pub async fn handle_chat(
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.status = LogStatus::Failed;
log.error = Some(e.to_string());
}
state.active_requests -= 1;
@@ -282,7 +303,7 @@ pub async fn handle_chat(
};
// 构建请求客户端
let client = build_client(&auth_token, &checksum);
let client = build_client(&auth_token, &checksum, is_search);
// 添加超时设置
let response = tokio::time::timeout(
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
@@ -303,7 +324,7 @@ pub async fn handle_chat(
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_SUCCESS;
log.status = LogStatus::Success;
}
}
resp
@@ -318,7 +339,7 @@ pub async fn handle_chat(
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.status = LogStatus::Failed;
log.error = Some(e.to_string());
}
state.active_requests -= 1;
@@ -340,7 +361,7 @@ pub async fn handle_chat(
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.status = LogStatus::Failed;
log.error = Some("Request timeout".to_string());
}
state.active_requests -= 1;
@@ -359,149 +380,60 @@ pub async fn handle_chat(
state.active_requests -= 1;
}
let convert_web_ref = current_config.include_web_references();
if request.stream {
let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple());
let full_text = Arc::new(Mutex::new(String::with_capacity(1024)));
let is_start = Arc::new(AtomicBool::new(true));
let start_time = std::time::Instant::now();
let first_chunk_time = Arc::new(Mutex::new(None));
let first_chunk_time = Arc::new(Mutex::new(None::<f64>));
let decoder = Arc::new(Mutex::new(StreamDecoder::new()));
let stream = {
// 创建新的 stream
let mut stream = response.bytes_stream();
// 定义消息处理器的上下文结构体
struct MessageProcessContext<'a> {
response_id: &'a str,
model: &'a str,
is_start: &'a AtomicBool,
first_chunk_time: &'a Mutex<Option<f64>>,
start_time: std::time::Instant,
state: &'a Mutex<AppState>,
current_id: u64,
}
if current_config.enable_stream_check() {
// 检查第一个 chunk
match stream.next().await {
Some(first_chunk) => {
let chunk = first_chunk.map_err(|e| {
let error_message = format!("Failed to read response chunk: {}", e);
// 理论上,若程序正常,必定成功,因为前面判断过了
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatError::RequestFailed(error_message).to_json()),
)
})?;
match parse_stream_data(&chunk) {
Err(StreamError::ChatError(error)) => {
let error_respone = error.to_error_response();
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(error_respone.native_code());
log.timing.total =
format_time_ms(start_time.elapsed().as_secs_f64());
state.error_requests += 1;
}
}
return Err((
error_respone.status_code(),
Json(error_respone.to_common()),
));
}
Ok(_) | Err(_) => {
// 创建一个包含第一个 chunk 的 stream
Box::pin(
futures::stream::once(async move { Ok(chunk) }).chain(stream),
)
as Pin<
Box<
dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send,
>,
>
}
}
}
None => {
// Box::pin(stream)
// as Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some("Empty stream response".to_string());
state.error_requests += 1;
}
}
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(
ChatError::RequestFailed("Empty stream response".to_string())
.to_json(),
),
));
}
}
} else {
Box::pin(stream)
as Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>
}
}
.then({
let buffer = Arc::new(Mutex::new(Vec::new()));
let first_chunk_time = first_chunk_time.clone();
let state = state.clone();
move |chunk| {
let buffer = buffer.clone();
let response_id = response_id.clone();
let model = request.model.clone();
let is_start = is_start.clone();
let full_text = full_text.clone();
let first_chunk_time = first_chunk_time.clone();
let state = state.clone();
// 根据配置决定是否发送最后的 finish_reason
let include_finish_reason = current_config.include_stop_stream();
async move {
let chunk = chunk.unwrap_or_default();
let mut buffer_guard = buffer.lock().await;
buffer_guard.extend_from_slice(&chunk);
match parse_stream_data(&buffer_guard) {
Ok(StreamMessage::Content(texts)) => {
buffer_guard.clear();
// 处理消息并生成响应数据的辅助函数
async fn process_messages(
messages: Vec<StreamMessage>,
ctx: &MessageProcessContext<'_>,
) -> String {
let mut response_data = String::new();
for message in messages {
match message {
StreamMessage::Content(text) => {
// 记录首字时间(如果还未记录)
if let Ok(mut first_time) = first_chunk_time.try_lock() {
if let Ok(mut first_time) = ctx.first_chunk_time.try_lock() {
if first_time.is_none() {
*first_time =
Some(format_time_ms(start_time.elapsed().as_secs_f64()));
*first_time = Some(ctx.start_time.elapsed().as_secs_f64());
}
}
// 处理文本内容
for text in texts {
let mut text_guard = full_text.lock().await;
text_guard.push_str(&text);
let is_first = is_start.load(Ordering::SeqCst);
let is_first = ctx.is_start.load(Ordering::SeqCst);
let response = ChatResponse {
id: response_id.clone(),
id: ctx.response_id.to_string(),
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
created: chrono::Utc::now().timestamp(),
model: if is_first { Some(model.clone()) } else { None },
model: if is_first {
Some(ctx.model.to_string())
} else {
None
},
choices: vec![Choice {
index: 0,
message: None,
delta: Some(Delta {
role: if is_first {
is_start.store(false, Ordering::SeqCst);
ctx.is_start.store(false, Ordering::SeqCst);
Some(Role::Assistant)
} else {
None
@@ -518,60 +450,26 @@ pub async fn handle_chat(
serde_json::to_string(&response).unwrap()
));
}
Ok::<_, Infallible>(Bytes::from(response_data))
}
Ok(StreamMessage::StreamStart) => {
buffer_guard.clear();
// 发送初始响应,包含模型信息
let response = ChatResponse {
id: response_id.clone(),
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
created: chrono::Utc::now().timestamp(),
model: {
is_start.store(true, Ordering::SeqCst);
Some(model.clone())
},
choices: vec![Choice {
index: 0,
message: None,
delta: Some(Delta {
role: Some(Role::Assistant),
content: Some(String::new()),
}),
finish_reason: None,
}],
usage: None,
};
Ok(Bytes::from(format!(
"data: {}\n\n",
serde_json::to_string(&response).unwrap()
)))
}
Ok(StreamMessage::StreamEnd) => {
buffer_guard.clear();
StreamMessage::StreamEnd => {
// 计算总时间和首次片段时间
let total_time = format_time_ms(start_time.elapsed().as_secs_f64());
let first_time = first_chunk_time.lock().await.unwrap_or(total_time);
let total_time = ctx.start_time.elapsed().as_secs_f64();
let first_time = ctx.first_chunk_time.lock().await.unwrap_or(total_time);
{
let mut state = state.lock().await;
let mut state = ctx.state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
.find(|log| log.id == ctx.current_id)
{
log.timing.total = total_time;
log.timing.first = Some(first_time);
log.timing.total = format_time_ms(total_time);
log.timing.first = Some(format_time_ms(first_time));
}
}
if include_finish_reason {
let response = ChatResponse {
id: response_id.clone(),
id: ctx.response_id.to_string(),
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
created: chrono::Utc::now().timestamp(),
model: None,
@@ -586,36 +484,150 @@ pub async fn handle_chat(
}],
usage: None,
};
Ok(Bytes::from(format!(
response_data.push_str(&format!(
"data: {}\n\ndata: [DONE]\n\n",
serde_json::to_string(&response).unwrap()
)))
} else {
Ok(Bytes::from("data: [DONE]\n\n"))
));
}
}
Ok(StreamMessage::Incomplete) => {
// 保持buffer中的数据以待下一个chunk
Ok(Bytes::new())
}
Ok(StreamMessage::Debug(debug_prompt)) => {
buffer_guard.clear();
if let Ok(mut state) = state.try_lock() {
StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = ctx.state.try_lock() {
if let Some(last_log) = state.request_logs.last_mut() {
last_log.prompt = Some(debug_prompt.clone());
last_log.prompt = Some(debug_prompt);
}
}
Ok(Bytes::new())
}
_ => {} // 忽略其他消息类型
}
}
response_data
}
let stream = {
let mut stream = response.bytes_stream();
// 处理第一个chunk并获取first_result
while decoder.lock().await.has_no_first_result() {
match stream.next().await {
Some(first_chunk) => {
let chunk = first_chunk.map_err(|e| {
let error_message = format!("Failed to read response chunk: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatError::RequestFailed(error_message).to_json()),
)
})?;
if let Err(StreamError::ChatError(error)) =
decoder.lock().await.decode(&chunk, convert_web_ref)
{
let error_response = error.to_error_response();
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = LogStatus::Failed;
log.error = Some(error_response.native_code());
log.timing.total =
format_time_ms(start_time.elapsed().as_secs_f64());
state.error_requests += 1;
}
}
return Err((
error_response.status_code(),
Json(error_response.to_common()),
));
}
}
None => {
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = LogStatus::Failed;
log.error = Some("Empty stream response".to_string());
state.error_requests += 1;
}
}
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(
ChatError::RequestFailed("Empty stream response".to_string())
.to_json(),
),
));
}
}
}
// 处理后续的stream
stream.then({
let decoder = decoder.clone();
let response_id = response_id.clone();
let model = request.model.clone();
let is_start = is_start.clone();
let first_chunk_time = first_chunk_time.clone();
let state = state.clone();
move |chunk| {
let decoder = decoder.clone();
let response_id = response_id.clone();
let model = model.clone();
let is_start = is_start.clone();
let first_chunk_time = first_chunk_time.clone();
let state = state.clone();
async move {
let chunk = chunk.unwrap_or_default();
let ctx = MessageProcessContext {
response_id: &response_id,
model: &model,
is_start: &is_start,
first_chunk_time: &first_chunk_time,
start_time,
state: &state,
current_id,
};
// 使用decoder处理chunk
let messages = match decoder.lock().await.decode(&chunk, convert_web_ref) {
Ok(msgs) => msgs,
Err(e) => {
buffer_guard.clear();
eprintln!("[警告] Stream error: {}", e);
Ok(Bytes::new())
return Ok::<_, Infallible>(Bytes::new());
}
};
let mut response_data = String::new();
if let Some(first_msg) = decoder.lock().await.take_first_result() {
let first_response = process_messages(first_msg, &ctx).await;
if !first_response.is_empty() {
response_data.push_str(&first_response);
}
}
let current_response = process_messages(messages, &ctx).await;
if !current_response.is_empty() {
response_data.push_str(&current_response);
}
Ok(Bytes::from(response_data))
}
}
});
})
};
Ok(Response::builder()
.header("Cache-Control", "no-cache")
@@ -626,81 +638,62 @@ pub async fn handle_chat(
} else {
// 非流式响应
let start_time = std::time::Instant::now();
let mut first_chunk_received = false;
let mut first_chunk_time = 0.0;
let mut first_chunk_time = None::<f64>;
let mut decoder = StreamDecoder::new();
let mut full_text = String::with_capacity(1024);
let mut stream = response.bytes_stream();
let mut prompt = None;
let mut all_chunks = Vec::new();
let mut buffer = Vec::new();
// 收集所有的chunks
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| {
// 更新请求日志为失败
if let Ok(mut state) = state.try_lock() {
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(format!("Failed to read response chunk: {}", e));
state.error_requests += 1;
}
}
let error_message = format!("Failed to read response chunk: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(
ChatError::RequestFailed(format!("Failed to read response chunk: {}", e))
.to_json(),
),
Json(ChatError::RequestFailed(error_message).to_json()),
)
})?;
buffer.extend_from_slice(&chunk);
match parse_stream_data(&buffer) {
Ok(StreamMessage::Content(texts)) => {
if !first_chunk_received {
first_chunk_time = format_time_ms(start_time.elapsed().as_secs_f64());
first_chunk_received = true;
all_chunks.extend(chunk);
}
// 一次性解码所有数据
let messages = match decoder.decode(&all_chunks, convert_web_ref) {
Ok(msgs) => msgs,
Err(StreamError::ChatError(error)) => {
let error_response = error.to_error_response();
return Err((
error_response.status_code(),
Json(error_response.to_common()),
));
}
Err(e) => {
let error_response = ErrorResponse {
status: ApiStatus::Error,
code: Some(500),
error: Some(e.to_string()),
message: None,
};
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
}
};
// 处理所有消息
for message in messages {
match message {
StreamMessage::Content(text) => {
if first_chunk_time.is_none() {
first_chunk_time = Some(start_time.elapsed().as_secs_f64());
}
for text in texts {
full_text.push_str(&text);
}
buffer.clear();
}
Ok(StreamMessage::Incomplete) => continue,
Ok(StreamMessage::Debug(debug_prompt)) => {
prompt = Some(debug_prompt);
buffer.clear();
}
Ok(StreamMessage::StreamStart) | Ok(StreamMessage::StreamEnd) => {
buffer.clear();
}
Err(StreamError::ChatError(error)) => {
let error = error.to_error_response();
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(error.native_code());
log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64());
state.error_requests += 1;
StreamMessage::Debug(debug_prompt) => {
if let Ok(mut state) = state.try_lock() {
if let Some(last_log) = state.request_logs.last_mut() {
last_log.prompt = Some(debug_prompt);
}
}
return Err((error.status_code(), Json(error.to_common())));
}
Err(_) => {
buffer.clear();
continue;
}
_ => {}
}
}
@@ -715,11 +708,8 @@ pub async fn handle_chat(
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.status = LogStatus::Failed;
log.error = Some("Empty response received".to_string());
if let Some(p) = prompt {
log.prompt = Some(p);
}
state.error_requests += 1;
}
}
@@ -761,9 +751,8 @@ pub async fn handle_chat(
.find(|log| log.id == current_id)
{
log.timing.total = total_time;
log.timing.first = Some(first_chunk_time);
log.prompt = prompt;
log.status = STATUS_SUCCESS;
log.timing.first = first_chunk_time;
log.status = LogStatus::Success;
}
}

View File

@@ -1,230 +1,2 @@
use super::aiserver::v1::StreamChatResponse;
use flate2::read::GzDecoder;
use prost::Message;
use std::io::Read;
use super::error::{ChatError, StreamError};
// 解压gzip数据
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
match decoder.read_to_end(&mut decompressed) {
Ok(_) => Some(decompressed),
Err(_) => {
// println!("gzip解压失败: {}", e);
None
}
}
}
pub enum StreamMessage {
// 未完成
Incomplete,
// 调试
Debug(String),
// 流开始标志 b"\0\0\0\0\0"
StreamStart,
// 消息内容
Content(Vec<String>),
// 流结束标志 b"\x02\0\0\0\x02{}"
StreamEnd,
}
pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
if data.len() < 5 {
return Err(StreamError::DataLengthLessThan5);
}
// 检查是否为流开始标志
// if data == b"\0\0\0\0\0" {
// return Ok(StreamMessage::StreamStart);
// }
// 检查是否为流结束标志
// if data == b"\x02\0\0\0\x02{}" {
// return Ok(StreamMessage::StreamEnd);
// }
let mut messages = Vec::new();
let mut offset = 0;
while offset + 5 <= data.len() {
// 获取消息类型和长度
let msg_type = data[offset];
let msg_len = u32::from_be_bytes([
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
]) as usize;
// 流开始
if msg_type == 0 && msg_len == 0 {
return Ok(StreamMessage::StreamStart);
}
// 检查剩余数据长度是否足够
if offset + 5 + msg_len > data.len() {
return Ok(StreamMessage::Incomplete);
}
let msg_data = &data[offset + 5..offset + 5 + msg_len];
match msg_type {
// 文本消息
0 => {
if let Ok(response) = StreamChatResponse::decode(msg_data) {
// crate::debug_println!("[text] StreamChatResponse: {:?}", response);
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[text] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
// response.is_using_slow_request,
));
}
}
}
// gzip压缩消息
1 => {
if let Some(text) = decompress_gzip(msg_data) {
if let Ok(response) = StreamChatResponse::decode(&text[..]) {
// crate::debug_println!("[gzip] StreamChatResponse: {:?}", response);
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[gzip] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
// response.is_using_slow_request,
));
}
}
}
}
// JSON字符串
2 => {
if msg_len == 2 {
return Ok(StreamMessage::StreamEnd);
}
if let Ok(text) = String::from_utf8(msg_data.to_vec()) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
// 未预计
// messages.push(text);
}
}
// gzip压缩消息
3 => {
if let Some(text) = decompress_gzip(msg_data) {
if text.len() == 2 {
return Ok(StreamMessage::StreamEnd);
}
if let Ok(text) = String::from_utf8(text) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
// 未预计
// messages.push(text);
}
}
}
// 其他类型暂不处理
t => {
eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t);
crate::debug_println!("消息类型: {},消息内容: {}", t, hex::encode(msg_data));
}
}
offset += 5 + msg_len;
}
if messages.is_empty() {
Err(StreamError::EmptyMessage)
} else {
Ok(StreamMessage::Content(messages))
}
}
#[test]
fn test_parse_stream_data() {
// 使用include_str!加载测试数据文件
let stream_data = include_str!("../../tests/data/stream_data.txt");
// 将整个字符串按每两个字符分割成字节
let bytes: Vec<u8> = stream_data
.as_bytes()
.chunks(2)
.map(|chunk| {
let hex_str = std::str::from_utf8(chunk).unwrap();
u8::from_str_radix(hex_str, 16).unwrap()
})
.collect();
// 辅助函数:找到下一个消息边界
fn find_next_message_boundary(bytes: &[u8]) -> usize {
if bytes.len() < 5 {
return bytes.len();
}
let msg_len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
5 + msg_len
}
// 辅助函数将字节转换为hex字符串
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<String>>()
.join("")
}
// 多次解析数据
let mut offset = 0;
while offset < bytes.len() {
let remaining_bytes = &bytes[offset..];
let msg_boundary = find_next_message_boundary(remaining_bytes);
let current_msg_bytes = &remaining_bytes[..msg_boundary];
let hex_str = bytes_to_hex(current_msg_bytes);
match parse_stream_data(current_msg_bytes) {
Ok(message) => {
match message {
StreamMessage::Content(messages) => {
print!("消息内容 [hex: {}]:", hex_str);
for msg in messages {
println!(" {}", msg);
}
offset += msg_boundary;
}
StreamMessage::Debug(_) => {
// println!("调试信息 [hex: {}]: {}", hex_str, prompt);
offset += msg_boundary;
}
StreamMessage::StreamEnd => {
println!("流结束 [hex: {}]", hex_str);
break;
}
StreamMessage::StreamStart => {
println!("流开始 [hex: {}]", hex_str);
offset += msg_boundary;
}
StreamMessage::Incomplete => {
println!("数据不完整 [hex: {}]", hex_str);
break;
}
}
}
Err(e) => {
println!("解析错误 [hex: {}]: {}", hex_str, e);
break;
}
}
}
}
mod decoder;
pub use decoder::*;

398
src/chat/stream/decoder.rs Normal file
View File

@@ -0,0 +1,398 @@
use crate::chat::{
aiserver::v1::StreamChatResponse,
error::{ChatError, StreamError},
};
use flate2::read::GzDecoder;
use prost::Message;
use std::{collections::BTreeMap, io::Read};
// 解压gzip数据
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
match decoder.read_to_end(&mut decompressed) {
Ok(_) => Some(decompressed),
Err(_) => {
// println!("gzip解压失败: {}", e);
None
}
}
}
pub trait ToMarkdown {
fn to_markdown(&self) -> String;
}
impl ToMarkdown for BTreeMap<String, String> {
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));
}
result.push_str("\n");
result
}
}
#[derive(PartialEq, Clone)]
pub enum StreamMessage {
// 调试
Debug(String),
// 网络引用
WebReference(BTreeMap<String, String>),
// 内容开始标志
ContentStart,
// 消息内容
Content(String),
// 流结束标志
StreamEnd,
}
impl StreamMessage {
fn convert_web_ref_to_content(self) -> Self {
match self {
StreamMessage::WebReference(refs) => StreamMessage::Content(refs.to_markdown()),
other => other,
}
}
}
pub struct StreamDecoder {
buffer: Vec<u8>,
first_result: Option<Vec<StreamMessage>>,
first_result_taken: bool,
}
impl StreamDecoder {
pub fn new() -> Self {
Self {
buffer: Vec::new(),
first_result: None,
first_result_taken: false,
}
}
// 获取第一个结果的引用
pub fn take_first_result(&mut self) -> Option<Vec<StreamMessage>> {
if self.is_incomplete() {
return None;
}
if self.first_result.is_some() {
self.first_result_taken = true;
}
self.first_result.take()
}
fn is_incomplete(&self) -> bool {
!self.buffer.is_empty()
}
pub fn has_no_first_result(&self) -> bool {
self.first_result.is_none()
}
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 {
crate::debug_println!("数据长度小于5字节当前数据: {}", hex::encode(&self.buffer));
return Err(StreamError::DataLengthLessThan5);
}
let mut messages = Vec::new();
let mut offset = 0;
while offset + 5 <= self.buffer.len() {
let msg_type = self.buffer[offset];
let msg_len = u32::from_be_bytes([
self.buffer[offset + 1],
self.buffer[offset + 2],
self.buffer[offset + 3],
self.buffer[offset + 4],
]) as usize;
if msg_len == 0 {
offset += 5;
messages.push(StreamMessage::ContentStart);
continue;
}
if offset + 5 + msg_len > self.buffer.len() {
break;
}
let msg_data = &self.buffer[offset + 5..offset + 5 + msg_len];
match self.process_message(msg_type, msg_data)? {
Some(msg) => {
if convert_web_ref {
messages.push(msg.convert_web_ref_to_content());
} else {
messages.push(msg);
}
}
_ => {}
}
offset += 5 + msg_len;
}
self.buffer.drain(..offset);
if !self.first_result_taken && !messages.is_empty() {
if self.first_result.is_none() {
self.first_result = Some(messages.clone());
} else {
self.first_result.as_mut().unwrap().extend(messages.clone());
}
}
Ok(messages)
}
fn process_message(
&self,
msg_type: u8,
msg_data: &[u8],
) -> Result<Option<StreamMessage>, StreamError> {
match msg_type {
0 => self.handle_text_message(msg_data),
1 => self.handle_gzip_message(msg_data),
2 => self.handle_json_message(msg_data),
3 => self.handle_gzip_json_message(msg_data),
t => {
eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t);
crate::debug_println!("消息类型: {},消息内容: {}", t, hex::encode(msg_data));
Ok(None)
}
}
}
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);
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)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
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);
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)))
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn handle_json_message(&self, msg_data: &[u8]) -> Result<Option<StreamMessage>, StreamError> {
if msg_data.len() == 2 {
return Ok(Some(StreamMessage::StreamEnd));
}
if let Ok(text) = String::from_utf8(msg_data.to_vec()) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
}
Ok(None)
}
fn handle_gzip_json_message(
&self,
msg_data: &[u8],
) -> Result<Option<StreamMessage>, StreamError> {
if let Some(text) = decompress_gzip(msg_data) {
if text.len() == 2 {
return Ok(Some(StreamMessage::StreamEnd));
}
if let Ok(text) = String::from_utf8(text) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_chunk() {
// 使用include_str!加载测试数据文件
let stream_data = include_str!("../../../tests/data/stream_data.txt");
// 将整个字符串按每两个字符分割成字节
let bytes: Vec<u8> = stream_data
.as_bytes()
.chunks(2)
.map(|chunk| {
let hex_str = std::str::from_utf8(chunk).unwrap();
u8::from_str_radix(hex_str, 16).unwrap()
})
.collect();
// 创建解码器
let mut decoder = StreamDecoder::new();
match decoder.decode(&bytes, false) {
Ok(messages) => {
for message in messages {
match message {
StreamMessage::StreamEnd => {
println!("流结束");
break;
}
StreamMessage::Content(msg) => {
println!("消息内容: {}", msg);
}
StreamMessage::WebReference(refs) => {
println!("网页引用:");
for (i, (url, title)) in refs.iter().enumerate() {
println!("{}. {} - {}", i, url, title);
}
}
StreamMessage::Debug(prompt) => {
println!("调试信息: {}", prompt);
}
StreamMessage::ContentStart => {
println!("流开始");
}
}
}
}
Err(e) => {
println!("解析错误: {}", e);
}
}
if decoder.is_incomplete() {
println!("数据不完整");
}
}
#[test]
fn test_multiple_chunks() {
// 使用include_str!加载测试数据文件
let stream_data = include_str!("../../../tests/data/stream_data.txt");
// 将整个字符串按每两个字符分割成字节
let bytes: Vec<u8> = stream_data
.as_bytes()
.chunks(2)
.map(|chunk| {
let hex_str = std::str::from_utf8(chunk).unwrap();
u8::from_str_radix(hex_str, 16).unwrap()
})
.collect();
// 创建解码器
let mut decoder = StreamDecoder::new();
// 辅助函数:找到下一个消息边界
fn find_next_message_boundary(bytes: &[u8]) -> usize {
if bytes.len() < 5 {
return bytes.len();
}
let msg_len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
5 + msg_len
}
// 辅助函数将字节转换为hex字符串
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<String>>()
.join("")
}
// 多次解析数据
let mut offset = 0;
let mut should_break = false;
while offset < bytes.len() {
let remaining_bytes = &bytes[offset..];
let msg_boundary = find_next_message_boundary(remaining_bytes);
let current_msg_bytes = &remaining_bytes[..msg_boundary];
let hex_str = bytes_to_hex(current_msg_bytes);
match decoder.decode(current_msg_bytes, false) {
Ok(messages) => {
for message in messages {
match message {
StreamMessage::StreamEnd => {
println!("流结束 [hex: {}]", hex_str);
should_break = true;
break;
}
StreamMessage::Content(msg) => {
println!("消息内容 [hex: {}]: {}", hex_str, msg);
}
StreamMessage::WebReference(refs) => {
println!("网页引用 [hex: {}]:", hex_str);
for (i, (url, title)) in refs.iter().enumerate() {
println!("{}. {} - {}", i, url, title);
}
}
StreamMessage::Debug(prompt) => {
println!("调试信息 [hex: {}]: {}", hex_str, prompt);
}
StreamMessage::ContentStart => {
println!("流开始 [hex: {}]", hex_str);
}
}
}
if should_break {
break;
}
if decoder.is_incomplete() {
println!("数据不完整 [hex: {}]", hex_str);
break;
}
offset += msg_boundary;
}
Err(e) => {
println!("解析错误 [hex: {}]: {}", hex_str, e);
break;
}
}
}
}
}

View File

@@ -5,8 +5,7 @@ use crate::{app::{
HEADER_NAME_GHOST_MODE, TRUE,
},
lazy::{
CURSOR_API2_CHAT_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL,
REVERSE_PROXY_HOST, USE_REVERSE_PROXY,
CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST, USE_REVERSE_PROXY
},
}, AppConfig};
use reqwest::header::{
@@ -67,19 +66,24 @@ pub fn rebuild_http_client() {
/// # 返回
///
/// * `reqwest::RequestBuilder` - 配置好的请求构建器
pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder {
pub fn build_client(auth_token: &str, checksum: &str, is_search: 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
.read()
.post(&*CURSOR_API2_CHAT_URL)
.post(url)
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_API2_HOST)
} else {
HTTP_CLIENT
.read()
.post(&*CURSOR_API2_CHAT_URL)
.post(url)
.header(HOST, CURSOR_API2_HOST)
};

View File

@@ -5,8 +5,6 @@ use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies};
#[derive(Serialize)]
pub struct ConfigData {
pub page_content: Option<PageContent>,
pub enable_stream_check: bool,
pub include_stop_stream: bool,
pub vision_ability: VisionAbility,
pub enable_slow_pool: bool,
pub enable_all_claude: bool,
@@ -15,6 +13,7 @@ pub struct ConfigData {
#[serde(skip_serializing_if = "String::is_empty")]
pub share_token: String,
pub proxies: Proxies,
pub include_web_references: bool,
}
#[derive(Deserialize, Default)]
@@ -23,8 +22,6 @@ pub struct ConfigUpdateRequest {
pub action: String, // "get", "update", "reset"
pub path: String,
pub content: Option<PageContent>, // "default", "text", "html"
pub enable_stream_check: Option<bool>,
pub include_stop_stream: Option<bool>,
pub vision_ability: Option<VisionAbility>,
pub enable_slow_pool: Option<bool>,
pub enable_all_claude: Option<bool>,
@@ -32,4 +29,5 @@ pub struct ConfigUpdateRequest {
pub enable_dynamic_key: Option<bool>,
pub share_token: Option<String>,
pub proxies: Option<Proxies>,
pub include_web_references: Option<bool>,
}

View File

@@ -1,5 +1,6 @@
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
#[derive(Serialize)]
#[serde(untagged)]
@@ -8,14 +9,14 @@ pub enum GetUserInfo {
Error { error: String },
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct TokenProfile {
pub usage: UsageProfile,
pub user: UserProfile,
pub stripe: StripeProfile,
}
#[derive(Deserialize, Serialize, PartialEq, Clone)]
#[derive(Deserialize, Serialize, PartialEq, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub enum MembershipType {
#[serde(rename = "free")]
Free,
@@ -27,7 +28,7 @@ pub enum MembershipType {
Enterprise,
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct StripeProfile {
#[serde(rename(deserialize = "membershipType"))]
pub membership_type: MembershipType,
@@ -41,7 +42,7 @@ pub struct StripeProfile {
pub days_remaining_on_trial: u32,
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct ModelUsage {
#[serde(rename(deserialize = "numRequests", serialize = "requests"))]
pub num_requests: u32,
@@ -65,7 +66,7 @@ pub struct ModelUsage {
pub max_tokens: Option<u32>,
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct UsageProfile {
#[serde(rename(deserialize = "gpt-4"))]
pub premium: ModelUsage,
@@ -75,7 +76,7 @@ pub struct UsageProfile {
pub unknown: ModelUsage,
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
pub struct UserProfile {
pub email: String,
// pub email_verified: bool,

View File

@@ -32,6 +32,7 @@ use chat::{
};
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
use std::sync::Arc;
use tokio::signal;
use tokio::sync::Mutex;
use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer};
@@ -63,6 +64,11 @@ async fn main() {
// 初始化应用状态
let state = Arc::new(Mutex::new(AppState::new(token_infos)));
// 尝试加载保存的配置
if let Err(e) = AppConfig::load_saved_config() {
eprintln!("加载保存的配置失败: {}", e);
}
// 创建一个克隆用于后台任务
let state_for_reload = state.clone();
@@ -84,10 +90,55 @@ async fn main() {
let mut app_state = state_for_reload.lock().await;
app_state.update_checksum();
debug_println!("checksum 自动刷新: {}", next_reload);
// debug_println!("checksum 自动刷新: {}", next_reload);
}
});
// 创建一个克隆用于信号处理
let state_for_shutdown = state.clone();
// 设置关闭信号处理
let shutdown_signal = async move {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("正在关闭服务器...");
// 保存配置
if let Err(e) = AppConfig::save_config() {
eprintln!("保存配置失败: {}", e);
} else {
println!("配置已保存");
}
// 保存日志
let state = state_for_shutdown.lock().await;
if let Err(e) = state.save_logs().await {
eprintln!("保存日志失败: {}", e);
} else {
println!("日志已保存");
}
};
// 设置路由
let app = Router::new()
.route(ROUTE_ROOT_PATH, get(handle_root))
@@ -128,9 +179,19 @@ async fn main() {
println!("服务器运行在端口 {}", port);
println!("当前版本: v{}", PKG_VERSION);
// if PKG_VERSION.contains("pre") {
println!("当前是测试版,有问题及时反馈哦~");
// println!("当前是测试版,有问题及时反馈哦~");
// }
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
let server = axum::serve(listener, app);
tokio::select! {
result = server => {
if let Err(e) = result {
eprintln!("服务器错误: {}", e);
}
}
_ = shutdown_signal => {
println!("服务器已关闭");
}
}
}

View File

@@ -118,7 +118,7 @@
<div class="button-group">
<button onclick="calibrateToken()">校准 Token</button>
<button onclick="getUserInfo()">获取用户信息</button>
<button onclick="getModels()">获取模型列表</button>
<button onclick="getModels(true)">获取模型列表</button>
</div>
<div class="form-group model-input-container">
@@ -130,7 +130,7 @@
<div class="form-group custom-suffix">
<input type="checkbox" id="customSuffix" onchange="toggleCustomSuffix()">
<label for="customSuffix">添加自定义后缀</label>
<input type="text" id="suffixInput" placeholder="@OpenAI" style="display: none;">
<input type="text" id="suffixInput" placeholder="-online@OpenAI" style="display: none;">
</div>
</div>
@@ -197,14 +197,16 @@
}
// 获取模型列表
async function getModels() {
async function getModels(showMessage = false) {
try {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
modelList.value = globalModels.map(model => model + suffix).join(',');
if (showMessage) {
showGlobalMessage('模型列表已更新');
}
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
@@ -223,7 +225,7 @@
const suffixInput = document.getElementById('suffixInput');
suffixInput.style.display = document.getElementById('customSuffix').checked ? 'block' : 'none';
if (document.getElementById('customSuffix').checked) {
getModels();
getModels(false);
}
}
@@ -363,8 +365,13 @@
await startStatusCheck();
showGlobalMessage('系统初始化完成');
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
// 使用防抖处理后缀输入事件
const suffixInput = document.getElementById('suffixInput');
let debounceTimer;
suffixInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => getModels(false), 300);
});
} catch (error) {
showGlobalMessage('系统初始化失败', true);
}

View File

@@ -75,24 +75,6 @@
<input type="password" id="dataToken" placeholder="输入数据认证令牌">
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="disableVision">
@@ -125,6 +107,15 @@
</div>
</div>
<div class="form-group">
<label>包含网络引用:</label>
<select id="includeWebReferences">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="button-group">
<button onclick="buildKey()">构建 Key</button>
<button onclick="clearForm()" class="secondary">清空表单</button>
@@ -187,14 +178,13 @@
const data = {
auth_token: dataToken,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
} : undefined,
include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined)
};
try {
@@ -227,11 +217,10 @@
function clearForm() {
document.getElementById('authToken').value = '';
document.getElementById('dataToken').value = '';
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = 'default';
document.getElementById('includeWebReferences').value = '';
document.getElementById('modelListContainer').style.display = 'none';
document.getElementById('keyResult').style.display = 'none';
showGlobalMessage('表单已清空');

View File

@@ -45,24 +45,6 @@
<textarea id="content"></textarea>
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enable_stream_check">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="include_stop_stream">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="vision_ability">
@@ -182,10 +164,6 @@
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
@@ -243,18 +221,10 @@
};
}
const shareToken = document.getElementById('shareToken').value.trim();
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
@@ -290,9 +260,7 @@
}
})()
}),
...(shareToken && {
share_token: shareToken
}),
share_token: document.getElementById('shareToken').value.trim(),
};
const result = await makeAuthenticatedRequest('/config', {

View File

@@ -551,21 +551,6 @@
return 'high';
}
function formatMembershipType(type) {
if (!type) return '-';
switch (type) {
case 'free_trial': return 'Pro Trial';
case 'pro': return 'Pro';
case 'free': return 'Free';
case 'enterprise': return 'Business';
default: return type
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
}
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
const deleteBtn = document.getElementById('deleteTokenBtn');

View File

@@ -1,10 +1,19 @@
// Token 管理功能
/**
* 保存认证令牌到本地存储
* @param {string} token - 要保存的认证令牌
* @returns {void}
*/
function saveAuthToken(token) {
const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期
localStorage.setItem('authToken', token);
localStorage.setItem('authTokenExpiry', expiryTime);
}
/**
* 获取存储的认证令牌
* @returns {string|null} 如果令牌有效则返回令牌,否则返回 null
*/
function getAuthToken() {
const token = localStorage.getItem('authToken');
const expiry = localStorage.getItem('authTokenExpiry');
@@ -23,6 +32,13 @@ function getAuthToken() {
}
// 消息显示功能
/**
* 在指定元素中显示消息
* @param {string} elementId - 目标元素的 ID
* @param {string} text - 要显示的消息文本
* @param {boolean} [isError=false] - 是否为错误消息
* @returns {void}
*/
function showMessage(elementId, text, isError = false) {
let msg = document.getElementById(elementId);
@@ -38,6 +54,10 @@ function showMessage(elementId, text, isError = false) {
}
// 确保消息容器存在
/**
* 确保消息容器存在于 DOM 中
* @returns {HTMLElement} 消息容器元素
*/
function ensureMessageContainer() {
let container = document.querySelector('.message-container');
if (!container) {
@@ -48,6 +68,13 @@ function ensureMessageContainer() {
return container;
}
/**
* 显示全局消息提示
* @param {string} text - 要显示的消息文本
* @param {boolean} [isError=false] - 是否为错误消息
* @param {number} [timeout=3000] - 消息显示时长(毫秒)
* @returns {void}
*/
function showGlobalMessage(text, isError = false, timeout = 3000) {
const container = ensureMessageContainer();
@@ -270,3 +297,27 @@ function showPromptModal(promptStr) {
console.error('原始prompt:', promptStr);
}
}
/**
* 将会员类型代码转换为显示名称
* @param {string|null} type - 会员类型代码,如 'free_trial', 'pro', 'free', 'enterprise' 等
* @returns {string} 格式化后的会员类型显示名称
* @example
* formatMembershipType('free_trial') // 返回 'Pro Trial'
* formatMembershipType('pro') // 返回 'Pro'
* formatMembershipType(null) // 返回 '-'
* formatMembershipType('custom_type') // 返回 'Custom Type'
*/
function formatMembershipType(type) {
if (!type) return '-';
switch (type) {
case 'free_trial': return 'Pro Trial';
case 'pro': return 'Pro';
case 'free': return 'Free';
case 'enterprise': return 'Business';
default: return type
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
}

View File

@@ -274,6 +274,7 @@
<h3>Token 管理</h3>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="reloadTokens()" class="secondary">重载Token</button>
<button onclick="addTokens()" class="secondary">添加Token</button>
<button onclick="deleteTokens()" class="danger">删除Token</button>
</div>
@@ -295,6 +296,10 @@
<tr>
<th>Token</th>
<th>Checksum</th>
<th>邮箱</th>
<th>会员类型</th>
<th>Premium用量</th>
<th>试用剩余</th>
<th class="action-cell">操作</th>
</tr>
</thead>
@@ -317,22 +322,6 @@
<div class="modal-header">
<h3>生成动态Key</h3>
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="disableVision">
@@ -362,6 +351,14 @@
<!-- 模型列表将通过 JavaScript 动态填充 -->
</div>
</div>
<div class="form-group">
<label>包含网络引用:</label>
<select id="includeWebReferences">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="key-result" id="keyResult" style="display: none;" onclick="copyGeneratedKey()">
<div class="key-content" id="keyContent"></div>
</div>
@@ -376,7 +373,15 @@
const data = await makeAuthenticatedRequest('/tokens/get');
if (data) {
const tableBody = document.getElementById('tokenTableBody');
tableBody.innerHTML = data.tokens.map(t => `<tr><td title="${t.token}">${t.token}</td><td title="${t.checksum}">${t.checksum}</td><td class="action-cell"><button onclick="showKeyModal('${t.token}','${t.checksum}')" class="secondary">生成Key</button></td></tr>`).join('');
tableBody.innerHTML = data.tokens.map(t => {
const profile = t.profile || {};
const user = profile.user || {};
const stripe = profile.stripe || {};
const usage = profile.usage || {};
const premium = usage.premium || {};
return `<tr><td title="${t.token}">${t.token}</td><td title="${t.checksum}">${t.checksum}</td><td>${user.email || '-'}</td><td>${formatMembershipType(stripe.membership_type)}</td><td>${premium.requests || 0}/${premium.max_requests || '∞'}</td><td>${stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}` : '-'}</td><td class="action-cell"><button onclick="showKeyModal('${t.token}','${t.checksum}')" class="secondary">生成Key</button></td></tr>`;
}).join('');
showGlobalMessage('配置获取成功');
}
}
@@ -397,6 +402,14 @@
});
}
async function reloadTokens() {
const data = await makeAuthenticatedRequest('/tokens/reload');
if (data) {
showGlobalMessage(`Token重载成功: ${data.message}`);
getTokenInfo(); // 刷新当前配置
}
}
async function addTokens() {
const tokensInput = document.getElementById('tokenInput').value;
@@ -508,12 +521,11 @@
});
// 重置所有选项
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = '';
document.getElementById('modelListContainer').style.display = 'none';
document.getElementById('includeWebReferences').value = '';
}
function closeKeyModal() {
@@ -537,14 +549,13 @@
const payload = {
auth_token: `${currentToken},${currentChecksum}`,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
} : undefined,
include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined)
};
const data = await makeAuthenticatedRequest('/build-key', {

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@ async function handleRequest(request) {
const allowedHosts = ["api2.cursor.sh", "www.cursor.com"];
const allowedPaths = [
"/aiserver.v1.AiService/StreamChat",
"/aiserver.v1.AiService/StreamChatWeb",
"/auth/full_stripe_profile",
"/api/usage",
"/api/auth/me"