mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-06 07:06:51 +08:00
v0.1.3-rc.4正式版
This commit is contained in:
18
.env.example
18
.env.example
@@ -12,11 +12,12 @@ AUTH_TOKEN=
|
|||||||
# 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限
|
# 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限
|
||||||
SHARED_TOKEN=
|
SHARED_TOKEN=
|
||||||
|
|
||||||
# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次
|
# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次(已弃用)
|
||||||
ENABLE_STREAM_CHECK=true
|
# 新版本已经完成优化
|
||||||
|
# ENABLE_STREAM_CHECK=true
|
||||||
|
|
||||||
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块
|
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块(已弃用)
|
||||||
INCLUDE_STOP_REASON_STREAM=true
|
# INCLUDE_STOP_REASON_STREAM=true
|
||||||
|
|
||||||
# 令牌文件路径(已弃用)
|
# 令牌文件路径(已弃用)
|
||||||
# TOKEN_FILE=.token
|
# TOKEN_FILE=.token
|
||||||
@@ -88,3 +89,12 @@ REQUEST_LOGS_LIMIT=100
|
|||||||
|
|
||||||
# Cursor 服务超时(秒)(最大值600)
|
# Cursor 服务超时(秒)(最大值600)
|
||||||
SERVICE_TIMEOUT=30
|
SERVICE_TIMEOUT=30
|
||||||
|
|
||||||
|
# 包含网络引用
|
||||||
|
INCLUDE_WEB_REFERENCES=false
|
||||||
|
|
||||||
|
# 持久化日志文件路径
|
||||||
|
LOGS_FILE_PATH=logs.bin
|
||||||
|
|
||||||
|
# 持久化页面配置文件路径
|
||||||
|
PAGES_FILE_PATH=pages.bin
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ node_modules
|
|||||||
/logs
|
/logs
|
||||||
/dev*
|
/dev*
|
||||||
/build*
|
/build*
|
||||||
|
/*.bin
|
||||||
|
/result.txt
|
||||||
|
421
Cargo.lock
generated
421
Cargo.lock
generated
@@ -17,6 +17,17 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -175,6 +186,18 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
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]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -211,6 +234,28 @@ version = "3.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
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]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.21.0"
|
version = "1.21.0"
|
||||||
@@ -259,6 +304,7 @@ dependencies = [
|
|||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"rkyv 0.7.45",
|
||||||
"serde",
|
"serde",
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
@@ -315,7 +361,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cursor-api"
|
name = "cursor-api"
|
||||||
version = "0.1.3-rc.4-pre.1"
|
version = "0.1.3-rc.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -327,6 +373,7 @@ dependencies = [
|
|||||||
"gif",
|
"gif",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
|
"memmap2",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paste",
|
"paste",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -334,13 +381,16 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rkyv 0.7.45",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"sonic-rs",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -362,7 +412,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -408,6 +458,18 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
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]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -463,6 +525,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -507,7 +575,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -596,6 +664,15 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
@@ -871,7 +948,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -929,7 +1006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1009,6 +1086,15 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -1042,6 +1128,26 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
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]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1115,7 +1221,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1228,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
|
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1266,7 +1372,7 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"regex",
|
"regex",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1280,7 +1386,7 @@ dependencies = [
|
|||||||
"itertools",
|
"itertools",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1292,6 +1398,46 @@ dependencies = [
|
|||||||
"prost",
|
"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]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -1307,6 +1453,21 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -1346,6 +1507,26 @@ dependencies = [
|
|||||||
"bitflags 2.8.0",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1375,6 +1556,21 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
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]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.12"
|
version = "0.12.12"
|
||||||
@@ -1438,6 +1634,64 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@@ -1523,6 +1777,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seahash"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -1563,7 +1823,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1617,12 +1877,27 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
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]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -1648,6 +1923,44 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -1666,6 +1979,17 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.96"
|
version = "2.0.96"
|
||||||
@@ -1694,7 +2018,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1731,6 +2055,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.15.0"
|
version = "3.15.0"
|
||||||
@@ -1751,7 +2081,16 @@ version = "1.0.69"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
@@ -1762,7 +2101,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"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]]
|
[[package]]
|
||||||
@@ -1775,6 +2125,21 @@ dependencies = [
|
|||||||
"zerovec",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.43.0"
|
version = "1.43.0"
|
||||||
@@ -1786,6 +2151,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@@ -1799,7 +2165,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1830,7 +2196,7 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2027,7 +2393,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2062,7 +2428,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@@ -2166,7 +2532,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2177,7 +2543,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2313,6 +2679,15 @@ version = "0.5.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"tap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -2333,7 +2708,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2355,7 +2730,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2375,7 +2750,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2404,7 +2779,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cursor-api"
|
name = "cursor-api"
|
||||||
version = "0.1.3-rc.4-pre.1"
|
version = "0.1.3-rc.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["wisdgod <nav@wisdgod.com>"]
|
authors = ["wisdgod <nav@wisdgod.com>"]
|
||||||
description = "OpenAI format compatibility layer for the Cursor API"
|
description = "OpenAI format compatibility layer for the Cursor API"
|
||||||
@@ -16,26 +16,30 @@ axum = { version = "0.8.1", features = ["json"] }
|
|||||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||||
# brotli = { version = "7.0.0", default-features = false, features = ["std"] }
|
# brotli = { version = "7.0.0", default-features = false, features = ["std"] }
|
||||||
bytes = "1.9.0"
|
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"
|
dotenvy = "0.15.7"
|
||||||
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
|
||||||
futures = { version = "0.3.31", default-features = false, features = ["std"] }
|
futures = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||||
gif = { version = "0.13.1", 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"] }
|
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||||
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
|
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"
|
parking_lot = "0.12.3"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
prost = "0.13.4"
|
prost = "0.13.4"
|
||||||
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
||||||
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
|
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"] }
|
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 = { 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 }
|
sha2 = { version = "0.10.8", default-features = false }
|
||||||
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
|
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"] }
|
tokio-stream = { version = "0.1.17", features = ["time"] }
|
||||||
tower-http = { version = "0.6.2", features = ["cors", "limit"] }
|
tower-http = { version = "0.6.2", features = ["cors", "limit"] }
|
||||||
|
url = { version = "2.5.4", default-features = false }
|
||||||
uuid = { version = "1.12.1", features = ["v4"] }
|
uuid = { version = "1.12.1", features = ["v4"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
@@ -1,9 +1,5 @@
|
|||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
dockerfile = "Dockerfile.cross"
|
dockerfile = "Dockerfile.cross"
|
||||||
|
|
||||||
[target.x86_64-unknown-freebsd]
|
[target.aarch64-unknown-linux-gnu]
|
||||||
pre-build = [
|
dockerfile = "Dockerfile.cross.arm64"
|
||||||
"pkg update",
|
|
||||||
"pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl",
|
|
||||||
"export SSL_CERT_FILE=/etc/ssl/cert.pem"
|
|
||||||
]
|
|
||||||
|
179
Cursor API.md
Normal file
179
Cursor API.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Cursor API
|
||||||
|
|
||||||
|
## 项目说明
|
||||||
|
|
||||||
|
### 版本声明
|
||||||
|
- 当前版本已进入稳定阶段
|
||||||
|
- 以下问题与程序无关,请勿反馈:
|
||||||
|
- 响应缺字漏字
|
||||||
|
- 首字延迟现象
|
||||||
|
- 响应出现乱码
|
||||||
|
- 性能优势:
|
||||||
|
- 达到原生客户端响应速度
|
||||||
|
- 部分场景下表现更优
|
||||||
|
- 开源协议要求:
|
||||||
|
- Fork 项目禁止以原作者名义进行宣传推广
|
||||||
|
- 禁止发布任何形式的官方声明
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 快速入门
|
||||||
|
|
||||||
|
### 密钥获取
|
||||||
|
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
55
Deno.ts
@@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
@@ -6,7 +6,7 @@ RUN apt-get update && \
|
|||||||
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
|
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV RUSTFLAGS="-C link-arg=-s"
|
ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native"
|
||||||
RUN cargo build --release && \
|
RUN cargo build --release && \
|
||||||
cp target/release/cursor-api /app/cursor-api
|
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 \
|
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV RUSTFLAGS="-C link-arg=-s"
|
ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native"
|
||||||
RUN cargo build --release && \
|
RUN cargo build --release && \
|
||||||
cp target/release/cursor-api /app/cursor-api
|
cp target/release/cursor-api /app/cursor-api
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ RUN apt-get update && \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 设置环境变量 (如果需要)
|
# 设置环境变量 (如果需要)
|
||||||
ENV RUSTFLAGS="-C link-arg=-s"
|
# ENV RUSTFLAGS="-C link-arg=-s"
|
||||||
|
|
||||||
# 设置 PROTOC 环境变量 (因为你的 build.rs 需要)
|
# 设置 PROTOC 环境变量 (因为你的 build.rs 需要)
|
||||||
ENV PROTOC=/usr/bin/protoc
|
ENV PROTOC=/usr/bin/protoc
|
||||||
|
30
Dockerfile.cross.arm64
Normal file
30
Dockerfile.cross.arm64
Normal 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
|
53
README.md
53
README.md
@@ -171,7 +171,43 @@ data: [DONE]
|
|||||||
"tokens": [
|
"tokens": [
|
||||||
{
|
{
|
||||||
"token": "string",
|
"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
|
"tokens_count": number
|
||||||
@@ -282,14 +318,13 @@ data: [DONE]
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"auth_token": "string", // 格式: {token},{checksum}
|
"auth_token": "string", // 格式: {token},{checksum}
|
||||||
"enable_stream_check": boolean, // 可选,启用流式响应首块检查
|
|
||||||
"include_stop_stream": boolean, // 可选,包含停止流
|
|
||||||
"disable_vision": boolean, // 可选,禁用图片处理能力
|
"disable_vision": boolean, // 可选,禁用图片处理能力
|
||||||
"enable_slow_pool": boolean, // 可选,启用慢速池
|
"enable_slow_pool": boolean, // 可选,启用慢速池
|
||||||
"usage_check_models": { // 可选,使用量检查模型配置
|
"usage_check_models": { // 可选,使用量检查模型配置
|
||||||
"type": "default" | "disabled" | "all" | "custom",
|
"type": "default" | "disabled" | "all" | "custom",
|
||||||
"model_ids": "string" // 当type为custom时生效,以逗号分隔的模型ID列表
|
"model_ids": "string" // 当type为custom时生效,以逗号分隔的模型ID列表
|
||||||
}
|
},
|
||||||
|
"include_web_references": boolean
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -357,8 +392,6 @@ data: [DONE]
|
|||||||
"type": "default" | "text" | "html",
|
"type": "default" | "text" | "html",
|
||||||
"content": "string"
|
"content": "string"
|
||||||
},
|
},
|
||||||
"enable_stream_check": boolean,
|
|
||||||
"include_stop_stream": boolean,
|
|
||||||
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
|
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
|
||||||
"enable_slow_pool": boolean,
|
"enable_slow_pool": boolean,
|
||||||
"enable_all_claude": boolean,
|
"enable_all_claude": boolean,
|
||||||
@@ -368,7 +401,8 @@ data: [DONE]
|
|||||||
},
|
},
|
||||||
"enable_dynamic_key": boolean,
|
"enable_dynamic_key": boolean,
|
||||||
"share_token": "string",
|
"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后两者是一样的
|
"type": "default" | "text" | "html", // 对于js和css后两者是一样的
|
||||||
"content": "string"
|
"content": "string"
|
||||||
},
|
},
|
||||||
"enable_stream_check": boolean,
|
|
||||||
"include_stop_stream": boolean,
|
|
||||||
"vision_ability": "none" | "base64" | "all",
|
"vision_ability": "none" | "base64" | "all",
|
||||||
"enable_slow_pool": boolean,
|
"enable_slow_pool": boolean,
|
||||||
"enable_all_claude": boolean,
|
"enable_all_claude": boolean,
|
||||||
@@ -394,7 +426,8 @@ data: [DONE]
|
|||||||
},
|
},
|
||||||
"enable_dynamic_key": boolean,
|
"enable_dynamic_key": boolean,
|
||||||
"share_token": "string",
|
"share_token": "string",
|
||||||
"proxies": "" | "system" | "proxy1,proxy2,..."
|
"proxies": "" | "system" | "proxy1,proxy2,...",
|
||||||
|
"include_web_references": boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
1
serve.ts
Normal file
1
serve.ts
Normal 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")}});
|
@@ -65,8 +65,6 @@ pub async fn handle_config_update(
|
|||||||
status: ApiStatus::Success,
|
status: ApiStatus::Success,
|
||||||
data: Some(ConfigData {
|
data: Some(ConfigData {
|
||||||
page_content: AppConfig::get_page_content(&request.path),
|
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(),
|
vision_ability: AppConfig::get_vision_ability(),
|
||||||
enable_slow_pool: AppConfig::get_slow_pool(),
|
enable_slow_pool: AppConfig::get_slow_pool(),
|
||||||
enable_all_claude: AppConfig::get_allow_claude(),
|
enable_all_claude: AppConfig::get_allow_claude(),
|
||||||
@@ -74,6 +72,7 @@ pub async fn handle_config_update(
|
|||||||
enable_dynamic_key: AppConfig::get_dynamic_key(),
|
enable_dynamic_key: AppConfig::get_dynamic_key(),
|
||||||
share_token: AppConfig::get_share_token(),
|
share_token: AppConfig::get_share_token(),
|
||||||
proxies: AppConfig::get_proxies(),
|
proxies: AppConfig::get_proxies(),
|
||||||
|
include_web_references: AppConfig::get_web_refs(),
|
||||||
}),
|
}),
|
||||||
message: None,
|
message: None,
|
||||||
})),
|
})),
|
||||||
@@ -96,8 +95,6 @@ pub async fn handle_config_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_updates!(request,
|
handle_updates!(request,
|
||||||
enable_stream_check => AppConfig::update_stream_check,
|
|
||||||
include_stop_stream => AppConfig::update_stop_stream,
|
|
||||||
vision_ability => AppConfig::update_vision_ability,
|
vision_ability => AppConfig::update_vision_ability,
|
||||||
enable_slow_pool => AppConfig::update_slow_pool,
|
enable_slow_pool => AppConfig::update_slow_pool,
|
||||||
enable_all_claude => AppConfig::update_allow_claude,
|
enable_all_claude => AppConfig::update_allow_claude,
|
||||||
@@ -105,6 +102,7 @@ pub async fn handle_config_update(
|
|||||||
enable_dynamic_key => AppConfig::update_dynamic_key,
|
enable_dynamic_key => AppConfig::update_dynamic_key,
|
||||||
share_token => AppConfig::update_share_token,
|
share_token => AppConfig::update_share_token,
|
||||||
proxies => AppConfig::update_proxies,
|
proxies => AppConfig::update_proxies,
|
||||||
|
include_web_references => AppConfig::update_web_refs,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Json(NormalResponse {
|
Ok(Json(NormalResponse {
|
||||||
@@ -131,8 +129,6 @@ pub async fn handle_config_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_resets!(request,
|
handle_resets!(request,
|
||||||
enable_stream_check => AppConfig::reset_stream_check,
|
|
||||||
include_stop_stream => AppConfig::reset_stop_stream,
|
|
||||||
vision_ability => AppConfig::reset_vision_ability,
|
vision_ability => AppConfig::reset_vision_ability,
|
||||||
enable_slow_pool => AppConfig::reset_slow_pool,
|
enable_slow_pool => AppConfig::reset_slow_pool,
|
||||||
enable_all_claude => AppConfig::reset_allow_claude,
|
enable_all_claude => AppConfig::reset_allow_claude,
|
||||||
@@ -140,6 +136,7 @@ pub async fn handle_config_update(
|
|||||||
enable_dynamic_key => AppConfig::reset_dynamic_key,
|
enable_dynamic_key => AppConfig::reset_dynamic_key,
|
||||||
share_token => AppConfig::reset_share_token,
|
share_token => AppConfig::reset_share_token,
|
||||||
proxies => AppConfig::reset_proxies,
|
proxies => AppConfig::reset_proxies,
|
||||||
|
include_web_references => AppConfig::reset_web_refs,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Json(NormalResponse {
|
Ok(Json(NormalResponse {
|
||||||
|
@@ -108,6 +108,12 @@ def_cursor_api_url!(
|
|||||||
"/aiserver.v1.AiService/StreamChat"
|
"/aiserver.v1.AiService/StreamChat"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
def_cursor_api_url!(
|
||||||
|
CURSOR_API2_CHAT_WEB_URL,
|
||||||
|
CURSOR_API2_HOST,
|
||||||
|
"/aiserver.v1.AiService/StreamChatWeb"
|
||||||
|
);
|
||||||
|
|
||||||
def_cursor_api_url!(
|
def_cursor_api_url!(
|
||||||
CURSOR_API2_STRIPE_URL,
|
CURSOR_API2_STRIPE_URL,
|
||||||
CURSOR_API2_HOST,
|
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");
|
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));
|
pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
|
||||||
|
|
||||||
// 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log"
|
// 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log"
|
||||||
|
@@ -12,18 +12,22 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
mod usage_check;
|
mod usage_check;
|
||||||
pub use usage_check::UsageCheck;
|
pub use usage_check::UsageCheck;
|
||||||
|
mod config;
|
||||||
mod proxies;
|
mod proxies;
|
||||||
pub use proxies::Proxies;
|
pub use proxies::Proxies;
|
||||||
mod build_key;
|
mod build_key;
|
||||||
pub use build_key::*;
|
pub use build_key::*;
|
||||||
|
|
||||||
|
use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS};
|
||||||
|
|
||||||
// 页面内容类型枚举
|
// 页面内容类型枚举
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum PageContent {
|
pub enum PageContent {
|
||||||
#[serde(rename = "default")]
|
#[serde(rename = "default")]
|
||||||
@@ -41,10 +45,8 @@ impl Default for PageContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 静态配置
|
// 静态配置
|
||||||
#[derive(Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
stream_check: bool,
|
|
||||||
stop_stream: bool,
|
|
||||||
vision_ability: VisionAbility,
|
vision_ability: VisionAbility,
|
||||||
slow_pool: bool,
|
slow_pool: bool,
|
||||||
allow_claude: bool,
|
allow_claude: bool,
|
||||||
@@ -54,9 +56,10 @@ pub struct AppConfig {
|
|||||||
share_token: String,
|
share_token: String,
|
||||||
is_share: bool,
|
is_share: bool,
|
||||||
proxies: Proxies,
|
proxies: Proxies,
|
||||||
|
web_refs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||||
pub enum VisionAbility {
|
pub enum VisionAbility {
|
||||||
#[serde(rename = "none", alias = "disabled")]
|
#[serde(rename = "none", alias = "disabled")]
|
||||||
None,
|
None,
|
||||||
@@ -87,7 +90,7 @@ impl Default for VisionAbility {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct Pages {
|
pub struct Pages {
|
||||||
pub root_content: PageContent,
|
pub root_content: PageContent,
|
||||||
pub logs_content: PageContent,
|
pub logs_content: PageContent,
|
||||||
@@ -114,24 +117,6 @@ pub struct AppState {
|
|||||||
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
|
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
|
||||||
LazyLock::new(|| RwLock::new(AppConfig::default()));
|
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 {
|
macro_rules! config_methods {
|
||||||
($($field:ident: $type:ty, $default:expr;)*) => {
|
($($field:ident: $type:ty, $default:expr;)*) => {
|
||||||
$(
|
$(
|
||||||
@@ -207,8 +192,6 @@ macro_rules! config_methods_clone {
|
|||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
let mut config = APP_CONFIG.write();
|
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 =
|
config.vision_ability =
|
||||||
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
|
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
|
||||||
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
|
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
|
||||||
@@ -221,15 +204,15 @@ impl AppConfig {
|
|||||||
config.proxies = match std::env::var("PROXIES") {
|
config.proxies = match std::env::var("PROXIES") {
|
||||||
Ok(proxies) => Proxies::from_str(proxies.as_str()),
|
Ok(proxies) => Proxies::from_str(proxies.as_str()),
|
||||||
Err(_) => Proxies::default(),
|
Err(_) => Proxies::default(),
|
||||||
}
|
};
|
||||||
|
config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
config_methods! {
|
config_methods! {
|
||||||
stream_check: bool, true;
|
|
||||||
stop_stream: bool, true;
|
|
||||||
slow_pool: bool, false;
|
slow_pool: bool, false;
|
||||||
allow_claude: bool, false;
|
allow_claude: bool, false;
|
||||||
dynamic_key: bool, false;
|
dynamic_key: bool, false;
|
||||||
|
web_refs: bool, false;
|
||||||
}
|
}
|
||||||
|
|
||||||
config_methods_clone! {
|
config_methods_clone! {
|
||||||
@@ -341,11 +324,20 @@ impl AppConfig {
|
|||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(token_infos: Vec<TokenInfo>) -> Self {
|
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 {
|
Self {
|
||||||
total_requests: 0,
|
total_requests: request_logs.len() as u64,
|
||||||
active_requests: 0,
|
active_requests: 0,
|
||||||
error_requests: 0,
|
error_requests: request_logs
|
||||||
request_logs: Vec::new(),
|
.iter()
|
||||||
|
.filter(|log| matches!(log.status, LogStatus::Failed))
|
||||||
|
.count() as u64,
|
||||||
|
request_logs,
|
||||||
token_infos,
|
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 struct RequestLog {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub timestamp: chrono::DateTime<chrono::Local>,
|
pub timestamp: chrono::DateTime<chrono::Local>,
|
||||||
@@ -368,12 +395,12 @@ pub struct RequestLog {
|
|||||||
pub prompt: Option<String>,
|
pub prompt: Option<String>,
|
||||||
pub timing: TimingInfo,
|
pub timing: TimingInfo,
|
||||||
pub stream: bool,
|
pub stream: bool,
|
||||||
pub status: &'static str,
|
pub status: LogStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct TimingInfo {
|
pub struct TimingInfo {
|
||||||
pub total: f64, // 总用时(秒)
|
pub total: f64, // 总用时(秒)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -390,7 +417,7 @@ pub struct ChatRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 用于存储 token 信息
|
// 用于存储 token 信息
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct TokenInfo {
|
pub struct TokenInfo {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub checksum: String,
|
pub checksum: String,
|
||||||
|
@@ -4,23 +4,15 @@ use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS};
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct BuildKeyRequest {
|
pub struct BuildKeyRequest {
|
||||||
// 认证令牌(必需)
|
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
// 流第一个块检查
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_stream_check: Option<bool>,
|
|
||||||
// 包含停止流
|
|
||||||
#[serde(default)]
|
|
||||||
pub include_stop_stream: Option<bool>,
|
|
||||||
// 是否禁用图片处理能力
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub disable_vision: Option<bool>,
|
pub disable_vision: Option<bool>,
|
||||||
// 慢速池
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enable_slow_pool: Option<bool>,
|
pub enable_slow_pool: Option<bool>,
|
||||||
// 使用量检查模型规则
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub usage_check_models: Option<UsageCheckModelConfig>,
|
pub usage_check_models: Option<UsageCheckModelConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub include_web_references: Option<bool>,
|
||||||
}
|
}
|
||||||
pub struct UsageCheckModelConfig {
|
pub struct UsageCheckModelConfig {
|
||||||
pub model_type: UsageCheckModelType,
|
pub model_type: UsageCheckModelType,
|
||||||
|
114
src/app/model/config.rs
Normal file
114
src/app/model/config.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
use reqwest::{Client, Proxy};
|
use reqwest::{Client, Proxy};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||||
|
|
||||||
use crate::app::constant::COMMA_STRING;
|
use crate::app::constant::COMMA_STRING;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ impl<'de> Deserialize<'de> for Proxies {
|
|||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let s = String::deserialize(deserializer)?;
|
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
Ok(Proxies::from_str(&s))
|
Ok(Proxies::from_str(&s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ use crate::{
|
|||||||
chat::{config::key_config, constant::AVAILABLE_MODELS},
|
chat::{config::key_config, constant::AVAILABLE_MODELS},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum UsageCheck {
|
pub enum UsageCheck {
|
||||||
@@ -108,7 +109,7 @@ impl<'de> Deserialize<'de> for UsageCheck {
|
|||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
let helper = UsageCheckHelper::deserialize(deserializer)?;
|
let helper = <UsageCheckHelper as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
Ok(match helper {
|
Ok(match helper {
|
||||||
UsageCheckHelper::None => UsageCheck::None,
|
UsageCheckHelper::None => UsageCheck::None,
|
||||||
UsageCheckHelper::Default => UsageCheck::Default,
|
UsageCheckHelper::Default => UsageCheck::Default,
|
||||||
|
@@ -15,8 +15,7 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
aiserver::v1::{
|
aiserver::v1::{
|
||||||
conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext,
|
conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
|
||||||
GetChatRequest, ImageProto, ModelDetails,
|
|
||||||
},
|
},
|
||||||
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
|
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
|
||||||
model::{Message, MessageContent, Role},
|
model::{Message, MessageContent, Role},
|
||||||
@@ -25,7 +24,7 @@ use super::{
|
|||||||
async fn process_chat_inputs(
|
async fn process_chat_inputs(
|
||||||
inputs: Vec<Message>,
|
inputs: Vec<Message>,
|
||||||
disable_vision: bool,
|
disable_vision: bool,
|
||||||
) -> (String, Vec<ConversationMessage>) {
|
) -> (String, Vec<ConversationMessage>, Vec<String>) {
|
||||||
// 收集 system 指令
|
// 收集 system 指令
|
||||||
let instructions = inputs
|
let instructions = inputs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -98,9 +97,25 @@ async fn process_chat_inputs(
|
|||||||
file_diff_trajectories: vec![],
|
file_diff_trajectories: vec![],
|
||||||
conversation_summary: None,
|
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 消息
|
// 如果第一条是 assistant,插入空的 user 消息
|
||||||
if chat_inputs
|
if chat_inputs
|
||||||
.first()
|
.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(
|
async fn fetch_image_data(
|
||||||
@@ -344,6 +384,7 @@ pub async fn encode_chat_message(
|
|||||||
model_name: &str,
|
model_name: &str,
|
||||||
disable_vision: bool,
|
disable_vision: bool,
|
||||||
enable_slow_pool: bool,
|
enable_slow_pool: bool,
|
||||||
|
is_search: bool,
|
||||||
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// 在进入异步操作前获取并释放锁
|
// 在进入异步操作前获取并释放锁
|
||||||
let enable_slow_pool = {
|
let enable_slow_pool = {
|
||||||
@@ -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() {
|
let explicit_context = if !instructions.trim().is_empty() {
|
||||||
Some(ExplicitContext {
|
Some(ExplicitContext {
|
||||||
@@ -365,6 +406,15 @@ pub async fn encode_chat_message(
|
|||||||
None
|
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 {
|
let chat = GetChatRequest {
|
||||||
current_file: None,
|
current_file: None,
|
||||||
conversation: messages,
|
conversation: messages,
|
||||||
@@ -394,11 +444,15 @@ pub async fn encode_chat_message(
|
|||||||
is_bash: Some(false),
|
is_bash: Some(false),
|
||||||
conversation_id: Uuid::new_v4().to_string(),
|
conversation_id: Uuid::new_v4().to_string(),
|
||||||
can_handle_filenames_after_language_ids: Some(true),
|
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![],
|
quotes: vec![],
|
||||||
debug_info: None,
|
debug_info: None,
|
||||||
workspace_id: None,
|
workspace_id: None,
|
||||||
external_links: vec![],
|
external_links,
|
||||||
commit_notes: vec![],
|
commit_notes: vec![],
|
||||||
long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)),
|
long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)),
|
||||||
is_eval: Some(false),
|
is_eval: Some(false),
|
||||||
|
@@ -6,21 +6,14 @@ impl KeyConfig {
|
|||||||
pub fn new_with_global() -> Self {
|
pub fn new_with_global() -> Self {
|
||||||
Self {
|
Self {
|
||||||
auth_token: None,
|
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()),
|
disable_vision: Some(AppConfig::get_vision_ability().is_none()),
|
||||||
enable_slow_pool: Some(AppConfig::get_slow_pool()),
|
enable_slow_pool: Some(AppConfig::get_slow_pool()),
|
||||||
usage_check_models: None,
|
usage_check_models: None,
|
||||||
|
include_web_references: Some(AppConfig::get_web_refs()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_without_auth_token(&self, config: &mut Self) {
|
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() {
|
if self.disable_vision.is_some() {
|
||||||
config.disable_vision = self.disable_vision;
|
config.disable_vision = self.disable_vision;
|
||||||
}
|
}
|
||||||
@@ -30,5 +23,8 @@ impl KeyConfig {
|
|||||||
if self.usage_check_models.is_some() {
|
if self.usage_check_models.is_some() {
|
||||||
config.usage_check_models = self.usage_check_models.clone();
|
config.usage_check_models = self.usage_check_models.clone();
|
||||||
}
|
}
|
||||||
|
if self.include_web_references.is_some() {
|
||||||
|
config.include_web_references = self.include_web_references;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,12 +17,6 @@ message KeyConfig {
|
|||||||
// 认证令牌(必需)
|
// 认证令牌(必需)
|
||||||
TokenInfo auth_token = 1;
|
TokenInfo auth_token = 1;
|
||||||
|
|
||||||
// 是否启用流检查
|
|
||||||
optional bool enable_stream_check = 2;
|
|
||||||
|
|
||||||
// 是否包含停止流
|
|
||||||
optional bool include_stop_stream = 3;
|
|
||||||
|
|
||||||
// 是否禁用图片处理能力
|
// 是否禁用图片处理能力
|
||||||
optional bool disable_vision = 4;
|
optional bool disable_vision = 4;
|
||||||
|
|
||||||
@@ -44,6 +38,9 @@ message KeyConfig {
|
|||||||
// 使用量检查模型规则
|
// 使用量检查模型规则
|
||||||
optional UsageCheckModel usage_check_models = 6;
|
optional UsageCheckModel usage_check_models = 6;
|
||||||
|
|
||||||
|
// 包含网络引用
|
||||||
|
optional bool include_web_references = 7;
|
||||||
|
|
||||||
// 密码SHA256哈希值
|
// 密码SHA256哈希值
|
||||||
// bytes secret = 2;
|
// bytes secret = 2;
|
||||||
}
|
}
|
@@ -6,7 +6,10 @@ macro_rules! def_pub_const {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF");
|
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");
|
def_pub_const!(ERR_NODATA, "No data");
|
||||||
|
|
||||||
const MODEL_OBJECT: &str = "model";
|
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_V3, "deepseek-v3");
|
||||||
def_pub_const!(DEEPSEEK_R1, "deepseek-r1");
|
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 {
|
Model {
|
||||||
id: CLAUDE_3_5_SONNET,
|
id: $id,
|
||||||
created: CREATED,
|
created: CREATED,
|
||||||
object: MODEL_OBJECT,
|
object: MODEL_OBJECT,
|
||||||
owned_by: ANTHROPIC,
|
owned_by: $owner,
|
||||||
},
|
},
|
||||||
Model {
|
)*
|
||||||
id: GPT_4,
|
];
|
||||||
created: CREATED,
|
};
|
||||||
object: MODEL_OBJECT,
|
}
|
||||||
owned_by: OPENAI,
|
|
||||||
},
|
macro_rules! count {
|
||||||
Model {
|
() => (0);
|
||||||
id: GPT_4O,
|
(($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*));
|
||||||
created: CREATED,
|
}
|
||||||
object: MODEL_OBJECT,
|
|
||||||
owned_by: OPENAI,
|
impl ModelType {
|
||||||
},
|
pub fn as_str_name(&self) -> &'static str {
|
||||||
Model {
|
match self {
|
||||||
id: CLAUDE_3_OPUS,
|
ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET,
|
||||||
created: CREATED,
|
ModelType::Gpt4 => GPT_4,
|
||||||
object: MODEL_OBJECT,
|
ModelType::Gpt4o => GPT_4O,
|
||||||
owned_by: ANTHROPIC,
|
ModelType::Claude3Opus => CLAUDE_3_OPUS,
|
||||||
},
|
ModelType::CursorFast => CURSOR_FAST,
|
||||||
Model {
|
ModelType::CursorSmall => CURSOR_SMALL,
|
||||||
id: CURSOR_FAST,
|
ModelType::Gpt35Turbo => GPT_3_5_TURBO,
|
||||||
created: CREATED,
|
ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09,
|
||||||
object: MODEL_OBJECT,
|
ModelType::Gpt4o128k => GPT_4O_128K,
|
||||||
owned_by: CURSOR,
|
ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K,
|
||||||
},
|
ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K,
|
||||||
Model {
|
ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K,
|
||||||
id: CURSOR_SMALL,
|
ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022,
|
||||||
created: CREATED,
|
ModelType::Gpt4oMini => GPT_4O_MINI,
|
||||||
object: MODEL_OBJECT,
|
ModelType::O1Mini => O1_MINI,
|
||||||
owned_by: CURSOR,
|
ModelType::O1Preview => O1_PREVIEW,
|
||||||
},
|
ModelType::O1 => O1,
|
||||||
Model {
|
ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU,
|
||||||
id: GPT_3_5_TURBO,
|
ModelType::GeminiExp1206 => GEMINI_EXP_1206,
|
||||||
created: CREATED,
|
ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP,
|
||||||
object: MODEL_OBJECT,
|
ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP,
|
||||||
owned_by: OPENAI,
|
ModelType::DeepseekV3 => DEEPSEEK_V3,
|
||||||
},
|
ModelType::DeepseekR1 => DEEPSEEK_R1,
|
||||||
Model {
|
}
|
||||||
id: GPT_4_TURBO_2024_04_09,
|
}
|
||||||
created: CREATED,
|
|
||||||
object: MODEL_OBJECT,
|
pub fn from_str_name(id :&str) -> Option<ModelType> {
|
||||||
owned_by: OPENAI,
|
match id {
|
||||||
},
|
CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet),
|
||||||
Model {
|
GPT_4 => Some(ModelType::Gpt4),
|
||||||
id: GPT_4O_128K,
|
GPT_4O => Some(ModelType::Gpt4o),
|
||||||
created: CREATED,
|
CLAUDE_3_OPUS => Some(ModelType::Claude3Opus),
|
||||||
object: MODEL_OBJECT,
|
CURSOR_FAST => Some(ModelType::CursorFast),
|
||||||
owned_by: OPENAI,
|
CURSOR_SMALL => Some(ModelType::CursorSmall),
|
||||||
},
|
GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo),
|
||||||
Model {
|
GPT_4_TURBO_2024_04_09 => Some(ModelType::Gpt4Turbo202404),
|
||||||
id: GEMINI_1_5_FLASH_500K,
|
GPT_4O_128K => Some(ModelType::Gpt4o128k),
|
||||||
created: CREATED,
|
GEMINI_1_5_FLASH_500K => Some(ModelType::Gemini15Flash500k),
|
||||||
object: MODEL_OBJECT,
|
CLAUDE_3_HAIKU_200K => Some(ModelType::Claude3Haiku200k),
|
||||||
owned_by: GOOGLE,
|
CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k),
|
||||||
},
|
CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022),
|
||||||
Model {
|
GPT_4O_MINI => Some(ModelType::Gpt4oMini),
|
||||||
id: CLAUDE_3_HAIKU_200K,
|
O1_MINI => Some(ModelType::O1Mini),
|
||||||
created: CREATED,
|
O1_PREVIEW => Some(ModelType::O1Preview),
|
||||||
object: MODEL_OBJECT,
|
O1 => Some(ModelType::O1),
|
||||||
owned_by: ANTHROPIC,
|
CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku),
|
||||||
},
|
GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206),
|
||||||
Model {
|
GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp),
|
||||||
id: CLAUDE_3_5_SONNET_200K,
|
GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp),
|
||||||
created: CREATED,
|
DEEPSEEK_V3 => Some(ModelType::DeepseekV3),
|
||||||
object: MODEL_OBJECT,
|
DEEPSEEK_R1 => Some(ModelType::DeepseekR1),
|
||||||
owned_by: ANTHROPIC,
|
_ => None,
|
||||||
},
|
}
|
||||||
Model {
|
}
|
||||||
id: CLAUDE_3_5_SONNET_20241022,
|
}
|
||||||
created: CREATED,
|
|
||||||
object: MODEL_OBJECT,
|
create_model!(
|
||||||
owned_by: ANTHROPIC,
|
CLAUDE_3_5_SONNET, ANTHROPIC,
|
||||||
},
|
GPT_4, OPENAI,
|
||||||
Model {
|
GPT_4O, OPENAI,
|
||||||
id: GPT_4O_MINI,
|
CLAUDE_3_OPUS, ANTHROPIC,
|
||||||
created: CREATED,
|
CURSOR_FAST, CURSOR,
|
||||||
object: MODEL_OBJECT,
|
CURSOR_SMALL, CURSOR,
|
||||||
owned_by: OPENAI,
|
GPT_3_5_TURBO, OPENAI,
|
||||||
},
|
GPT_4_TURBO_2024_04_09, OPENAI,
|
||||||
Model {
|
GPT_4O_128K, OPENAI,
|
||||||
id: O1_MINI,
|
GEMINI_1_5_FLASH_500K, GOOGLE,
|
||||||
created: CREATED,
|
CLAUDE_3_HAIKU_200K, ANTHROPIC,
|
||||||
object: MODEL_OBJECT,
|
CLAUDE_3_5_SONNET_200K, ANTHROPIC,
|
||||||
owned_by: OPENAI,
|
CLAUDE_3_5_SONNET_20241022, ANTHROPIC,
|
||||||
},
|
GPT_4O_MINI, OPENAI,
|
||||||
Model {
|
O1_MINI, OPENAI,
|
||||||
id: O1_PREVIEW,
|
O1_PREVIEW, OPENAI,
|
||||||
created: CREATED,
|
O1, OPENAI,
|
||||||
object: MODEL_OBJECT,
|
CLAUDE_3_5_HAIKU, ANTHROPIC,
|
||||||
owned_by: OPENAI,
|
GEMINI_EXP_1206, GOOGLE,
|
||||||
},
|
GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE,
|
||||||
Model {
|
GEMINI_2_0_FLASH_EXP, GOOGLE,
|
||||||
id: O1,
|
DEEPSEEK_V3, DEEPSEEK,
|
||||||
created: CREATED,
|
DEEPSEEK_R1, DEEPSEEK,
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
pub const USAGE_CHECK_MODELS: [&str; 11] = [
|
pub const USAGE_CHECK_MODELS: [&str; 11] = [
|
||||||
CLAUDE_3_5_SONNET_20241022,
|
CLAUDE_3_5_SONNET_20241022,
|
||||||
|
@@ -125,7 +125,6 @@ impl ErrorResponse {
|
|||||||
pub enum StreamError {
|
pub enum StreamError {
|
||||||
ChatError(ChatError),
|
ChatError(ChatError),
|
||||||
DataLengthLessThan5,
|
DataLengthLessThan5,
|
||||||
EmptyMessage,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for StreamError {
|
impl std::fmt::Display for StreamError {
|
||||||
@@ -133,7 +132,6 @@ impl std::fmt::Display for StreamError {
|
|||||||
match self {
|
match self {
|
||||||
StreamError::ChatError(error) => write!(f, "{}", error.error.code),
|
StreamError::ChatError(error) => write!(f, "{}", error.error.code),
|
||||||
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
|
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
|
||||||
StreamError::EmptyMessage => write!(f, "empty message"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -86,8 +86,8 @@ pub struct Model {
|
|||||||
pub owned_by: &'static str,
|
pub owned_by: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::app::model::{AppConfig, UsageCheck};
|
|
||||||
use super::constant::USAGE_CHECK_MODELS;
|
use super::constant::USAGE_CHECK_MODELS;
|
||||||
|
use crate::app::model::{AppConfig, UsageCheck};
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {
|
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {
|
||||||
|
@@ -164,11 +164,10 @@ pub async fn handle_build_key(
|
|||||||
// 构建 proto 消息
|
// 构建 proto 消息
|
||||||
let mut key_config = KeyConfig {
|
let mut key_config = KeyConfig {
|
||||||
auth_token: Some(token_info),
|
auth_token: Some(token_info),
|
||||||
enable_stream_check: request.enable_stream_check,
|
|
||||||
include_stop_stream: request.include_stop_stream,
|
|
||||||
disable_vision: request.disable_vision,
|
disable_vision: request.disable_vision,
|
||||||
enable_slow_pool: request.enable_slow_pool,
|
enable_slow_pool: request.enable_slow_pool,
|
||||||
usage_check_models: None,
|
usage_check_models: None,
|
||||||
|
include_web_references: request.include_web_references,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(usage_check_models) = request.usage_check_models {
|
if let Some(usage_check_models) = request.usage_check_models {
|
||||||
|
@@ -2,12 +2,13 @@ use crate::{
|
|||||||
app::{
|
app::{
|
||||||
constant::{
|
constant::{
|
||||||
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
|
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
|
||||||
OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS,
|
OBJECT_CHAT_COMPLETION_CHUNK,
|
||||||
},
|
},
|
||||||
lazy::{
|
lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT},
|
||||||
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::{
|
chat::{
|
||||||
config::KeyConfig,
|
config::KeyConfig,
|
||||||
@@ -16,11 +17,11 @@ use crate::{
|
|||||||
model::{
|
model::{
|
||||||
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
|
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
|
||||||
},
|
},
|
||||||
stream::{parse_stream_data, StreamMessage},
|
stream::{StreamDecoder, StreamMessage},
|
||||||
},
|
},
|
||||||
common::{
|
common::{
|
||||||
client::build_client,
|
client::build_client,
|
||||||
model::{error::ChatError, userinfo::MembershipType, ErrorResponse},
|
model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse},
|
||||||
utils::{
|
utils::{
|
||||||
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
|
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
|
||||||
validate_token_and_checksum,
|
validate_token_and_checksum,
|
||||||
@@ -38,16 +39,13 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::StreamExt;
|
||||||
use prost::Message as _;
|
use prost::Message as _;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use std::{
|
|
||||||
pin::Pin,
|
|
||||||
sync::atomic::{AtomicUsize, Ordering},
|
|
||||||
};
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -66,8 +64,16 @@ pub async fn handle_chat(
|
|||||||
Json(request): Json<ChatRequest>,
|
Json(request): Json<ChatRequest>,
|
||||||
) -> Result<Response<Body>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Response<Body>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
let allow_claude = AppConfig::get_allow_claude();
|
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();
|
let model_supported = model.is_some();
|
||||||
|
|
||||||
if !(model_supported || allow_claude && request.model.starts_with("claude")) {
|
if !(model_supported || allow_claude && request.model.starts_with("claude")) {
|
||||||
@@ -168,7 +174,7 @@ pub async fn handle_chat(
|
|||||||
return false;
|
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 standard = &profile.usage.standard;
|
||||||
let premium = &profile.usage.premium;
|
let premium = &profile.usage.premium;
|
||||||
|
|
||||||
@@ -213,14 +219,28 @@ pub async fn handle_chat(
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let profile = get_token_profile(&auth_token_clone).await;
|
let profile = get_token_profile(&auth_token_clone).await;
|
||||||
let mut state = state_clone.lock().await;
|
let mut state = state_clone.lock().await;
|
||||||
// 根据id查找对应的日志
|
|
||||||
if let Some(log) = state
|
// 先找到所有需要更新的位置的索引
|
||||||
.request_logs
|
let token_info_idx = state
|
||||||
.iter_mut()
|
.token_infos
|
||||||
.rev()
|
.iter()
|
||||||
.find(|log| log.id == log_id)
|
.position(|info| info.token == auth_token_clone);
|
||||||
{
|
|
||||||
log.token_info.profile = profile;
|
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,
|
first: None,
|
||||||
},
|
},
|
||||||
stream: request.stream,
|
stream: request.stream,
|
||||||
status: STATUS_PENDING,
|
status: LogStatus::Pending,
|
||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,9 +272,10 @@ pub async fn handle_chat(
|
|||||||
// 将消息转换为hex格式
|
// 将消息转换为hex格式
|
||||||
let hex_data = match super::adapter::encode_chat_message(
|
let hex_data = match super::adapter::encode_chat_message(
|
||||||
request.messages,
|
request.messages,
|
||||||
&request.model,
|
&model_name,
|
||||||
current_config.disable_vision(),
|
current_config.disable_vision(),
|
||||||
current_config.enable_slow_pool(),
|
current_config.enable_slow_pool(),
|
||||||
|
is_search,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -267,7 +288,7 @@ pub async fn handle_chat(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.status = STATUS_FAILED;
|
log.status = LogStatus::Failed;
|
||||||
log.error = Some(e.to_string());
|
log.error = Some(e.to_string());
|
||||||
}
|
}
|
||||||
state.active_requests -= 1;
|
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(
|
let response = tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
|
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
|
||||||
@@ -303,7 +324,7 @@ pub async fn handle_chat(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.status = STATUS_SUCCESS;
|
log.status = LogStatus::Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp
|
resp
|
||||||
@@ -318,7 +339,7 @@ pub async fn handle_chat(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.status = STATUS_FAILED;
|
log.status = LogStatus::Failed;
|
||||||
log.error = Some(e.to_string());
|
log.error = Some(e.to_string());
|
||||||
}
|
}
|
||||||
state.active_requests -= 1;
|
state.active_requests -= 1;
|
||||||
@@ -340,7 +361,7 @@ pub async fn handle_chat(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.status = STATUS_FAILED;
|
log.status = LogStatus::Failed;
|
||||||
log.error = Some("Request timeout".to_string());
|
log.error = Some("Request timeout".to_string());
|
||||||
}
|
}
|
||||||
state.active_requests -= 1;
|
state.active_requests -= 1;
|
||||||
@@ -359,149 +380,60 @@ pub async fn handle_chat(
|
|||||||
state.active_requests -= 1;
|
state.active_requests -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let convert_web_ref = current_config.include_web_references();
|
||||||
|
|
||||||
if request.stream {
|
if request.stream {
|
||||||
let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple());
|
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 is_start = Arc::new(AtomicBool::new(true));
|
||||||
let start_time = std::time::Instant::now();
|
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
|
struct MessageProcessContext<'a> {
|
||||||
let mut stream = response.bytes_stream();
|
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
|
async fn process_messages(
|
||||||
match stream.next().await {
|
messages: Vec<StreamMessage>,
|
||||||
Some(first_chunk) => {
|
ctx: &MessageProcessContext<'_>,
|
||||||
let chunk = first_chunk.map_err(|e| {
|
) -> String {
|
||||||
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();
|
|
||||||
let mut response_data = String::new();
|
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() {
|
if first_time.is_none() {
|
||||||
*first_time =
|
*first_time = Some(ctx.start_time.elapsed().as_secs_f64());
|
||||||
Some(format_time_ms(start_time.elapsed().as_secs_f64()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理文本内容
|
let is_first = ctx.is_start.load(Ordering::SeqCst);
|
||||||
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 response = ChatResponse {
|
let response = ChatResponse {
|
||||||
id: response_id.clone(),
|
id: ctx.response_id.to_string(),
|
||||||
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
|
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
|
||||||
created: chrono::Utc::now().timestamp(),
|
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 {
|
choices: vec![Choice {
|
||||||
index: 0,
|
index: 0,
|
||||||
message: None,
|
message: None,
|
||||||
delta: Some(Delta {
|
delta: Some(Delta {
|
||||||
role: if is_first {
|
role: if is_first {
|
||||||
is_start.store(false, Ordering::SeqCst);
|
ctx.is_start.store(false, Ordering::SeqCst);
|
||||||
Some(Role::Assistant)
|
Some(Role::Assistant)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -518,60 +450,26 @@ pub async fn handle_chat(
|
|||||||
serde_json::to_string(&response).unwrap()
|
serde_json::to_string(&response).unwrap()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
StreamMessage::StreamEnd => {
|
||||||
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();
|
|
||||||
|
|
||||||
// 计算总时间和首次片段时间
|
// 计算总时间和首次片段时间
|
||||||
let total_time = format_time_ms(start_time.elapsed().as_secs_f64());
|
let total_time = ctx.start_time.elapsed().as_secs_f64();
|
||||||
let first_time = first_chunk_time.lock().await.unwrap_or(total_time);
|
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
|
if let Some(log) = state
|
||||||
.request_logs
|
.request_logs
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == ctx.current_id)
|
||||||
{
|
{
|
||||||
log.timing.total = total_time;
|
log.timing.total = format_time_ms(total_time);
|
||||||
log.timing.first = Some(first_time);
|
log.timing.first = Some(format_time_ms(first_time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if include_finish_reason {
|
|
||||||
let response = ChatResponse {
|
let response = ChatResponse {
|
||||||
id: response_id.clone(),
|
id: ctx.response_id.to_string(),
|
||||||
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
|
object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(),
|
||||||
created: chrono::Utc::now().timestamp(),
|
created: chrono::Utc::now().timestamp(),
|
||||||
model: None,
|
model: None,
|
||||||
@@ -586,36 +484,150 @@ pub async fn handle_chat(
|
|||||||
}],
|
}],
|
||||||
usage: None,
|
usage: None,
|
||||||
};
|
};
|
||||||
Ok(Bytes::from(format!(
|
response_data.push_str(&format!(
|
||||||
"data: {}\n\ndata: [DONE]\n\n",
|
"data: {}\n\ndata: [DONE]\n\n",
|
||||||
serde_json::to_string(&response).unwrap()
|
serde_json::to_string(&response).unwrap()
|
||||||
)))
|
));
|
||||||
} else {
|
|
||||||
Ok(Bytes::from("data: [DONE]\n\n"))
|
|
||||||
}
|
}
|
||||||
}
|
StreamMessage::Debug(debug_prompt) => {
|
||||||
Ok(StreamMessage::Incomplete) => {
|
if let Ok(mut state) = ctx.state.try_lock() {
|
||||||
// 保持buffer中的数据以待下一个chunk
|
|
||||||
Ok(Bytes::new())
|
|
||||||
}
|
|
||||||
Ok(StreamMessage::Debug(debug_prompt)) => {
|
|
||||||
buffer_guard.clear();
|
|
||||||
if let Ok(mut state) = state.try_lock() {
|
|
||||||
if let Some(last_log) = state.request_logs.last_mut() {
|
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) => {
|
Err(e) => {
|
||||||
buffer_guard.clear();
|
|
||||||
eprintln!("[警告] Stream error: {}", e);
|
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(¤t_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Bytes::from(response_data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.header("Cache-Control", "no-cache")
|
.header("Cache-Control", "no-cache")
|
||||||
@@ -626,81 +638,62 @@ pub async fn handle_chat(
|
|||||||
} else {
|
} else {
|
||||||
// 非流式响应
|
// 非流式响应
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
let mut first_chunk_received = false;
|
let mut first_chunk_time = None::<f64>;
|
||||||
let mut first_chunk_time = 0.0;
|
let mut decoder = StreamDecoder::new();
|
||||||
let mut full_text = String::with_capacity(1024);
|
let mut full_text = String::with_capacity(1024);
|
||||||
let mut stream = response.bytes_stream();
|
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 {
|
while let Some(chunk) = stream.next().await {
|
||||||
let chunk = chunk.map_err(|e| {
|
let chunk = chunk.map_err(|e| {
|
||||||
// 更新请求日志为失败
|
let error_message = format!("Failed to read response chunk: {}", 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(
|
Json(ChatError::RequestFailed(error_message).to_json()),
|
||||||
ChatError::RequestFailed(format!("Failed to read response chunk: {}", e))
|
|
||||||
.to_json(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
all_chunks.extend(chunk);
|
||||||
buffer.extend_from_slice(&chunk);
|
}
|
||||||
|
|
||||||
match parse_stream_data(&buffer) {
|
// 一次性解码所有数据
|
||||||
Ok(StreamMessage::Content(texts)) => {
|
let messages = match decoder.decode(&all_chunks, convert_web_ref) {
|
||||||
if !first_chunk_received {
|
Ok(msgs) => msgs,
|
||||||
first_chunk_time = format_time_ms(start_time.elapsed().as_secs_f64());
|
Err(StreamError::ChatError(error)) => {
|
||||||
first_chunk_received = true;
|
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);
|
full_text.push_str(&text);
|
||||||
}
|
}
|
||||||
buffer.clear();
|
StreamMessage::Debug(debug_prompt) => {
|
||||||
}
|
if let Ok(mut state) = state.try_lock() {
|
||||||
Ok(StreamMessage::Incomplete) => continue,
|
if let Some(last_log) = state.request_logs.last_mut() {
|
||||||
Ok(StreamMessage::Debug(debug_prompt)) => {
|
last_log.prompt = Some(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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Err((error.status_code(), Json(error.to_common())));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
buffer.clear();
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,11 +708,8 @@ pub async fn handle_chat(
|
|||||||
.rev()
|
.rev()
|
||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.status = STATUS_FAILED;
|
log.status = LogStatus::Failed;
|
||||||
log.error = Some("Empty response received".to_string());
|
log.error = Some("Empty response received".to_string());
|
||||||
if let Some(p) = prompt {
|
|
||||||
log.prompt = Some(p);
|
|
||||||
}
|
|
||||||
state.error_requests += 1;
|
state.error_requests += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -761,9 +751,8 @@ pub async fn handle_chat(
|
|||||||
.find(|log| log.id == current_id)
|
.find(|log| log.id == current_id)
|
||||||
{
|
{
|
||||||
log.timing.total = total_time;
|
log.timing.total = total_time;
|
||||||
log.timing.first = Some(first_chunk_time);
|
log.timing.first = first_chunk_time;
|
||||||
log.prompt = prompt;
|
log.status = LogStatus::Success;
|
||||||
log.status = STATUS_SUCCESS;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,230 +1,2 @@
|
|||||||
use super::aiserver::v1::StreamChatResponse;
|
mod decoder;
|
||||||
use flate2::read::GzDecoder;
|
pub use decoder::*;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
398
src/chat/stream/decoder.rs
Normal file
398
src/chat/stream/decoder.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,8 +5,7 @@ use crate::{app::{
|
|||||||
HEADER_NAME_GHOST_MODE, TRUE,
|
HEADER_NAME_GHOST_MODE, TRUE,
|
||||||
},
|
},
|
||||||
lazy::{
|
lazy::{
|
||||||
CURSOR_API2_CHAT_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL,
|
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
|
||||||
REVERSE_PROXY_HOST, USE_REVERSE_PROXY,
|
|
||||||
},
|
},
|
||||||
}, AppConfig};
|
}, AppConfig};
|
||||||
use reqwest::header::{
|
use reqwest::header::{
|
||||||
@@ -67,19 +66,24 @@ pub fn rebuild_http_client() {
|
|||||||
/// # 返回
|
/// # 返回
|
||||||
///
|
///
|
||||||
/// * `reqwest::RequestBuilder` - 配置好的请求构建器
|
/// * `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 trace_id = Uuid::new_v4().to_string();
|
||||||
|
let url = if is_search {
|
||||||
|
&*CURSOR_API2_CHAT_WEB_URL
|
||||||
|
} else {
|
||||||
|
&*CURSOR_API2_CHAT_URL
|
||||||
|
};
|
||||||
|
|
||||||
let client = if *USE_REVERSE_PROXY {
|
let client = if *USE_REVERSE_PROXY {
|
||||||
HTTP_CLIENT
|
HTTP_CLIENT
|
||||||
.read()
|
.read()
|
||||||
.post(&*CURSOR_API2_CHAT_URL)
|
.post(url)
|
||||||
.header(HOST, &*REVERSE_PROXY_HOST)
|
.header(HOST, &*REVERSE_PROXY_HOST)
|
||||||
.header(PROXY_HOST, CURSOR_API2_HOST)
|
.header(PROXY_HOST, CURSOR_API2_HOST)
|
||||||
} else {
|
} else {
|
||||||
HTTP_CLIENT
|
HTTP_CLIENT
|
||||||
.read()
|
.read()
|
||||||
.post(&*CURSOR_API2_CHAT_URL)
|
.post(url)
|
||||||
.header(HOST, CURSOR_API2_HOST)
|
.header(HOST, CURSOR_API2_HOST)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -5,8 +5,6 @@ use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies};
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ConfigData {
|
pub struct ConfigData {
|
||||||
pub page_content: Option<PageContent>,
|
pub page_content: Option<PageContent>,
|
||||||
pub enable_stream_check: bool,
|
|
||||||
pub include_stop_stream: bool,
|
|
||||||
pub vision_ability: VisionAbility,
|
pub vision_ability: VisionAbility,
|
||||||
pub enable_slow_pool: bool,
|
pub enable_slow_pool: bool,
|
||||||
pub enable_all_claude: bool,
|
pub enable_all_claude: bool,
|
||||||
@@ -15,6 +13,7 @@ pub struct ConfigData {
|
|||||||
#[serde(skip_serializing_if = "String::is_empty")]
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
pub share_token: String,
|
pub share_token: String,
|
||||||
pub proxies: Proxies,
|
pub proxies: Proxies,
|
||||||
|
pub include_web_references: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
@@ -23,8 +22,6 @@ pub struct ConfigUpdateRequest {
|
|||||||
pub action: String, // "get", "update", "reset"
|
pub action: String, // "get", "update", "reset"
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub content: Option<PageContent>, // "default", "text", "html"
|
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 vision_ability: Option<VisionAbility>,
|
||||||
pub enable_slow_pool: Option<bool>,
|
pub enable_slow_pool: Option<bool>,
|
||||||
pub enable_all_claude: Option<bool>,
|
pub enable_all_claude: Option<bool>,
|
||||||
@@ -32,4 +29,5 @@ pub struct ConfigUpdateRequest {
|
|||||||
pub enable_dynamic_key: Option<bool>,
|
pub enable_dynamic_key: Option<bool>,
|
||||||
pub share_token: Option<String>,
|
pub share_token: Option<String>,
|
||||||
pub proxies: Option<Proxies>,
|
pub proxies: Option<Proxies>,
|
||||||
|
pub include_web_references: Option<bool>,
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@@ -8,14 +9,14 @@ pub enum GetUserInfo {
|
|||||||
Error { error: String },
|
Error { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct TokenProfile {
|
pub struct TokenProfile {
|
||||||
pub usage: UsageProfile,
|
pub usage: UsageProfile,
|
||||||
pub user: UserProfile,
|
pub user: UserProfile,
|
||||||
pub stripe: StripeProfile,
|
pub stripe: StripeProfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, PartialEq, Clone)]
|
#[derive(Deserialize, Serialize, PartialEq, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub enum MembershipType {
|
pub enum MembershipType {
|
||||||
#[serde(rename = "free")]
|
#[serde(rename = "free")]
|
||||||
Free,
|
Free,
|
||||||
@@ -27,7 +28,7 @@ pub enum MembershipType {
|
|||||||
Enterprise,
|
Enterprise,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct StripeProfile {
|
pub struct StripeProfile {
|
||||||
#[serde(rename(deserialize = "membershipType"))]
|
#[serde(rename(deserialize = "membershipType"))]
|
||||||
pub membership_type: MembershipType,
|
pub membership_type: MembershipType,
|
||||||
@@ -41,7 +42,7 @@ pub struct StripeProfile {
|
|||||||
pub days_remaining_on_trial: u32,
|
pub days_remaining_on_trial: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct ModelUsage {
|
pub struct ModelUsage {
|
||||||
#[serde(rename(deserialize = "numRequests", serialize = "requests"))]
|
#[serde(rename(deserialize = "numRequests", serialize = "requests"))]
|
||||||
pub num_requests: u32,
|
pub num_requests: u32,
|
||||||
@@ -65,7 +66,7 @@ pub struct ModelUsage {
|
|||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct UsageProfile {
|
pub struct UsageProfile {
|
||||||
#[serde(rename(deserialize = "gpt-4"))]
|
#[serde(rename(deserialize = "gpt-4"))]
|
||||||
pub premium: ModelUsage,
|
pub premium: ModelUsage,
|
||||||
@@ -75,7 +76,7 @@ pub struct UsageProfile {
|
|||||||
pub unknown: ModelUsage,
|
pub unknown: ModelUsage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)]
|
||||||
pub struct UserProfile {
|
pub struct UserProfile {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
// pub email_verified: bool,
|
// pub email_verified: bool,
|
||||||
|
67
src/main.rs
67
src/main.rs
@@ -32,6 +32,7 @@ use chat::{
|
|||||||
};
|
};
|
||||||
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
|
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::signal;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer};
|
use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer};
|
||||||
|
|
||||||
@@ -63,6 +64,11 @@ async fn main() {
|
|||||||
// 初始化应用状态
|
// 初始化应用状态
|
||||||
let state = Arc::new(Mutex::new(AppState::new(token_infos)));
|
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();
|
let state_for_reload = state.clone();
|
||||||
|
|
||||||
@@ -84,10 +90,55 @@ async fn main() {
|
|||||||
|
|
||||||
let mut app_state = state_for_reload.lock().await;
|
let mut app_state = state_for_reload.lock().await;
|
||||||
app_state.update_checksum();
|
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()
|
let app = Router::new()
|
||||||
.route(ROUTE_ROOT_PATH, get(handle_root))
|
.route(ROUTE_ROOT_PATH, get(handle_root))
|
||||||
@@ -128,9 +179,19 @@ async fn main() {
|
|||||||
println!("服务器运行在端口 {}", port);
|
println!("服务器运行在端口 {}", port);
|
||||||
println!("当前版本: v{}", PKG_VERSION);
|
println!("当前版本: v{}", PKG_VERSION);
|
||||||
// if PKG_VERSION.contains("pre") {
|
// if PKG_VERSION.contains("pre") {
|
||||||
println!("当前是测试版,有问题及时反馈哦~");
|
// println!("当前是测试版,有问题及时反馈哦~");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
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!("服务器已关闭");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button onclick="calibrateToken()">校准 Token</button>
|
<button onclick="calibrateToken()">校准 Token</button>
|
||||||
<button onclick="getUserInfo()">获取用户信息</button>
|
<button onclick="getUserInfo()">获取用户信息</button>
|
||||||
<button onclick="getModels()">获取模型列表</button>
|
<button onclick="getModels(true)">获取模型列表</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group model-input-container">
|
<div class="form-group model-input-container">
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<div class="form-group custom-suffix">
|
<div class="form-group custom-suffix">
|
||||||
<input type="checkbox" id="customSuffix" onchange="toggleCustomSuffix()">
|
<input type="checkbox" id="customSuffix" onchange="toggleCustomSuffix()">
|
||||||
<label for="customSuffix">添加自定义后缀</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,14 +197,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型列表
|
// 获取模型列表
|
||||||
async function getModels() {
|
async function getModels(showMessage = false) {
|
||||||
try {
|
try {
|
||||||
const modelList = document.getElementById('modelList');
|
const modelList = document.getElementById('modelList');
|
||||||
const suffix = document.getElementById('customSuffix').checked ?
|
const suffix = document.getElementById('customSuffix').checked ?
|
||||||
document.getElementById('suffixInput').value : '';
|
document.getElementById('suffixInput').value : '';
|
||||||
|
|
||||||
modelList.value = globalModels.map(model => model + suffix).join(',');
|
modelList.value = globalModels.map(model => model + suffix).join(',');
|
||||||
|
if (showMessage) {
|
||||||
showGlobalMessage('模型列表已更新');
|
showGlobalMessage('模型列表已更新');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showGlobalMessage('获取模型列表失败', true);
|
showGlobalMessage('获取模型列表失败', true);
|
||||||
}
|
}
|
||||||
@@ -223,7 +225,7 @@
|
|||||||
const suffixInput = document.getElementById('suffixInput');
|
const suffixInput = document.getElementById('suffixInput');
|
||||||
suffixInput.style.display = document.getElementById('customSuffix').checked ? 'block' : 'none';
|
suffixInput.style.display = document.getElementById('customSuffix').checked ? 'block' : 'none';
|
||||||
if (document.getElementById('customSuffix').checked) {
|
if (document.getElementById('customSuffix').checked) {
|
||||||
getModels();
|
getModels(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,8 +365,13 @@
|
|||||||
await startStatusCheck();
|
await startStatusCheck();
|
||||||
showGlobalMessage('系统初始化完成');
|
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) {
|
} catch (error) {
|
||||||
showGlobalMessage('系统初始化失败', true);
|
showGlobalMessage('系统初始化失败', true);
|
||||||
}
|
}
|
||||||
|
@@ -75,24 +75,6 @@
|
|||||||
<input type="password" id="dataToken" placeholder="输入数据认证令牌">
|
<input type="password" id="dataToken" placeholder="输入数据认证令牌">
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>图片处理能力:</label>
|
<label>图片处理能力:</label>
|
||||||
<select id="disableVision">
|
<select id="disableVision">
|
||||||
@@ -125,6 +107,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="button-group">
|
||||||
<button onclick="buildKey()">构建 Key</button>
|
<button onclick="buildKey()">构建 Key</button>
|
||||||
<button onclick="clearForm()" class="secondary">清空表单</button>
|
<button onclick="clearForm()" class="secondary">清空表单</button>
|
||||||
@@ -187,14 +178,13 @@
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
auth_token: dataToken,
|
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),
|
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
|
||||||
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
|
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
|
||||||
usage_check_models: type ? {
|
usage_check_models: type ? {
|
||||||
type: type,
|
type: type,
|
||||||
model_ids: type === 'custom' ? modelIds : undefined
|
model_ids: type === 'custom' ? modelIds : undefined
|
||||||
} : undefined
|
} : undefined,
|
||||||
|
include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined)
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -227,11 +217,10 @@
|
|||||||
function clearForm() {
|
function clearForm() {
|
||||||
document.getElementById('authToken').value = '';
|
document.getElementById('authToken').value = '';
|
||||||
document.getElementById('dataToken').value = '';
|
document.getElementById('dataToken').value = '';
|
||||||
document.getElementById('enableStreamCheck').value = '';
|
|
||||||
document.getElementById('includeStopStream').value = '';
|
|
||||||
document.getElementById('disableVision').value = '';
|
document.getElementById('disableVision').value = '';
|
||||||
document.getElementById('enableSlowPool').value = '';
|
document.getElementById('enableSlowPool').value = '';
|
||||||
document.getElementById('usageCheckType').value = 'default';
|
document.getElementById('usageCheckType').value = 'default';
|
||||||
|
document.getElementById('includeWebReferences').value = '';
|
||||||
document.getElementById('modelListContainer').style.display = 'none';
|
document.getElementById('modelListContainer').style.display = 'none';
|
||||||
document.getElementById('keyResult').style.display = 'none';
|
document.getElementById('keyResult').style.display = 'none';
|
||||||
showGlobalMessage('表单已清空');
|
showGlobalMessage('表单已清空');
|
||||||
|
@@ -45,24 +45,6 @@
|
|||||||
<textarea id="content"></textarea>
|
<textarea id="content"></textarea>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>图片处理能力:</label>
|
<label>图片处理能力:</label>
|
||||||
<select id="vision_ability">
|
<select id="vision_ability">
|
||||||
@@ -182,10 +164,6 @@
|
|||||||
visionValue = 'base64-http';
|
visionValue = 'base64-http';
|
||||||
break;
|
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('vision_ability').value = visionValue;
|
||||||
document.getElementById('enable_slow_pool').value =
|
document.getElementById('enable_slow_pool').value =
|
||||||
parseStringFromBoolean(data.data.enable_slow_pool, '');
|
parseStringFromBoolean(data.data.enable_slow_pool, '');
|
||||||
@@ -243,18 +221,10 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareToken = document.getElementById('shareToken').value.trim();
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
action,
|
action,
|
||||||
path: document.getElementById('path').value,
|
path: document.getElementById('path').value,
|
||||||
...(contentObj && { content: contentObj }),
|
...(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 && {
|
...(document.getElementById('vision_ability').value && {
|
||||||
vision_ability: document.getElementById('vision_ability').value
|
vision_ability: document.getElementById('vision_ability').value
|
||||||
}),
|
}),
|
||||||
@@ -290,9 +260,7 @@
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}),
|
}),
|
||||||
...(shareToken && {
|
share_token: document.getElementById('shareToken').value.trim(),
|
||||||
share_token: shareToken
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await makeAuthenticatedRequest('/config', {
|
const result = await makeAuthenticatedRequest('/config', {
|
||||||
|
@@ -551,21 +551,6 @@
|
|||||||
return 'high';
|
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) {
|
function showTokenModal(tokenInfo) {
|
||||||
const modal = document.getElementById('tokenModal');
|
const modal = document.getElementById('tokenModal');
|
||||||
const deleteBtn = document.getElementById('deleteTokenBtn');
|
const deleteBtn = document.getElementById('deleteTokenBtn');
|
||||||
|
@@ -1,10 +1,19 @@
|
|||||||
// Token 管理功能
|
// Token 管理功能
|
||||||
|
/**
|
||||||
|
* 保存认证令牌到本地存储
|
||||||
|
* @param {string} token - 要保存的认证令牌
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function saveAuthToken(token) {
|
function saveAuthToken(token) {
|
||||||
const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期
|
const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期
|
||||||
localStorage.setItem('authToken', token);
|
localStorage.setItem('authToken', token);
|
||||||
localStorage.setItem('authTokenExpiry', expiryTime);
|
localStorage.setItem('authTokenExpiry', expiryTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储的认证令牌
|
||||||
|
* @returns {string|null} 如果令牌有效则返回令牌,否则返回 null
|
||||||
|
*/
|
||||||
function getAuthToken() {
|
function getAuthToken() {
|
||||||
const token = localStorage.getItem('authToken');
|
const token = localStorage.getItem('authToken');
|
||||||
const expiry = localStorage.getItem('authTokenExpiry');
|
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) {
|
function showMessage(elementId, text, isError = false) {
|
||||||
let msg = document.getElementById(elementId);
|
let msg = document.getElementById(elementId);
|
||||||
|
|
||||||
@@ -38,6 +54,10 @@ function showMessage(elementId, text, isError = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保消息容器存在
|
// 确保消息容器存在
|
||||||
|
/**
|
||||||
|
* 确保消息容器存在于 DOM 中
|
||||||
|
* @returns {HTMLElement} 消息容器元素
|
||||||
|
*/
|
||||||
function ensureMessageContainer() {
|
function ensureMessageContainer() {
|
||||||
let container = document.querySelector('.message-container');
|
let container = document.querySelector('.message-container');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
@@ -48,6 +68,13 @@ function ensureMessageContainer() {
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示全局消息提示
|
||||||
|
* @param {string} text - 要显示的消息文本
|
||||||
|
* @param {boolean} [isError=false] - 是否为错误消息
|
||||||
|
* @param {number} [timeout=3000] - 消息显示时长(毫秒)
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function showGlobalMessage(text, isError = false, timeout = 3000) {
|
function showGlobalMessage(text, isError = false, timeout = 3000) {
|
||||||
const container = ensureMessageContainer();
|
const container = ensureMessageContainer();
|
||||||
|
|
||||||
@@ -270,3 +297,27 @@ function showPromptModal(promptStr) {
|
|||||||
console.error('原始prompt:', 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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -274,6 +274,7 @@
|
|||||||
<h3>Token 管理</h3>
|
<h3>Token 管理</h3>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button onclick="getTokenInfo()">获取当前配置</button>
|
<button onclick="getTokenInfo()">获取当前配置</button>
|
||||||
|
<button onclick="reloadTokens()" class="secondary">重载Token</button>
|
||||||
<button onclick="addTokens()" class="secondary">添加Token</button>
|
<button onclick="addTokens()" class="secondary">添加Token</button>
|
||||||
<button onclick="deleteTokens()" class="danger">删除Token</button>
|
<button onclick="deleteTokens()" class="danger">删除Token</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,6 +296,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Token</th>
|
<th>Token</th>
|
||||||
<th>Checksum</th>
|
<th>Checksum</th>
|
||||||
|
<th>邮箱</th>
|
||||||
|
<th>会员类型</th>
|
||||||
|
<th>Premium用量</th>
|
||||||
|
<th>试用剩余</th>
|
||||||
<th class="action-cell">操作</th>
|
<th class="action-cell">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -317,22 +322,6 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>生成动态Key</h3>
|
<h3>生成动态Key</h3>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>图片处理能力:</label>
|
<label>图片处理能力:</label>
|
||||||
<select id="disableVision">
|
<select id="disableVision">
|
||||||
@@ -362,6 +351,14 @@
|
|||||||
<!-- 模型列表将通过 JavaScript 动态填充 -->
|
<!-- 模型列表将通过 JavaScript 动态填充 -->
|
||||||
</div>
|
</div>
|
||||||
</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-result" id="keyResult" style="display: none;" onclick="copyGeneratedKey()">
|
||||||
<div class="key-content" id="keyContent"></div>
|
<div class="key-content" id="keyContent"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,7 +373,15 @@
|
|||||||
const data = await makeAuthenticatedRequest('/tokens/get');
|
const data = await makeAuthenticatedRequest('/tokens/get');
|
||||||
if (data) {
|
if (data) {
|
||||||
const tableBody = document.getElementById('tokenTableBody');
|
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('配置获取成功');
|
showGlobalMessage('配置获取成功');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,6 +402,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reloadTokens() {
|
||||||
|
const data = await makeAuthenticatedRequest('/tokens/reload');
|
||||||
|
if (data) {
|
||||||
|
showGlobalMessage(`Token重载成功: ${data.message}`);
|
||||||
|
getTokenInfo(); // 刷新当前配置
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function addTokens() {
|
async function addTokens() {
|
||||||
const tokensInput = document.getElementById('tokenInput').value;
|
const tokensInput = document.getElementById('tokenInput').value;
|
||||||
|
|
||||||
@@ -508,12 +521,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 重置所有选项
|
// 重置所有选项
|
||||||
document.getElementById('enableStreamCheck').value = '';
|
|
||||||
document.getElementById('includeStopStream').value = '';
|
|
||||||
document.getElementById('disableVision').value = '';
|
document.getElementById('disableVision').value = '';
|
||||||
document.getElementById('enableSlowPool').value = '';
|
document.getElementById('enableSlowPool').value = '';
|
||||||
document.getElementById('usageCheckType').value = '';
|
document.getElementById('usageCheckType').value = '';
|
||||||
document.getElementById('modelListContainer').style.display = 'none';
|
document.getElementById('modelListContainer').style.display = 'none';
|
||||||
|
document.getElementById('includeWebReferences').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeKeyModal() {
|
function closeKeyModal() {
|
||||||
@@ -537,14 +549,13 @@
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
auth_token: `${currentToken},${currentChecksum}`,
|
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),
|
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
|
||||||
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
|
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
|
||||||
usage_check_models: type ? {
|
usage_check_models: type ? {
|
||||||
type: type,
|
type: type,
|
||||||
model_ids: type === 'custom' ? modelIds : undefined
|
model_ids: type === 'custom' ? modelIds : undefined
|
||||||
} : undefined
|
} : undefined,
|
||||||
|
include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined)
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await makeAuthenticatedRequest('/build-key', {
|
const data = await makeAuthenticatedRequest('/build-key', {
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ async function handleRequest(request) {
|
|||||||
const allowedHosts = ["api2.cursor.sh", "www.cursor.com"];
|
const allowedHosts = ["api2.cursor.sh", "www.cursor.com"];
|
||||||
const allowedPaths = [
|
const allowedPaths = [
|
||||||
"/aiserver.v1.AiService/StreamChat",
|
"/aiserver.v1.AiService/StreamChat",
|
||||||
|
"/aiserver.v1.AiService/StreamChatWeb",
|
||||||
"/auth/full_stripe_profile",
|
"/auth/full_stripe_profile",
|
||||||
"/api/usage",
|
"/api/usage",
|
||||||
"/api/auth/me"
|
"/api/auth/me"
|
||||||
|
Reference in New Issue
Block a user