From c58f2697f0f4684d6e98d5ac9058b4c346a90828 Mon Sep 17 00:00:00 2001 From: wisdgod Date: Mon, 27 Jan 2025 14:03:46 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.3-rc.4=E6=AD=A3=E5=BC=8F=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 20 +- .gitignore | 2 + Cargo.lock | 421 ++++++++++++++++++++++++-- Cargo.toml | 12 +- Cross.toml | 8 +- Cursor API.md | 179 +++++++++++ Deno.ts | 55 ---- Dockerfile | 4 +- Dockerfile.cross | 2 +- Dockerfile.cross.arm64 | 30 ++ README.md | 53 +++- serve.ts | 1 + src/app/config.rs | 9 +- src/app/lazy.rs | 12 + src/app/model.rs | 99 +++--- src/app/model/build_key.rs | 12 +- src/app/model/config.rs | 114 +++++++ src/app/model/proxies.rs | 3 +- src/app/model/usage_check.rs | 3 +- src/chat/adapter.rs | 68 ++++- src/chat/config.rs | 12 +- src/chat/config/key.proto | 9 +- src/chat/constant.rs | 276 +++++++++-------- src/chat/error.rs | 2 - src/chat/model.rs | 2 +- src/chat/route/config.rs | 3 +- src/chat/service.rs | 563 +++++++++++++++++------------------ src/chat/stream.rs | 232 +-------------- src/chat/stream/decoder.rs | 398 +++++++++++++++++++++++++ src/common/client.rs | 14 +- src/common/model/config.rs | 6 +- src/common/model/userinfo.rs | 13 +- src/main.rs | 67 ++++- static/api.html | 21 +- static/build_key.html | 35 +-- static/config.html | 34 +-- static/logs.html | 15 - static/shared.js | 53 +++- static/tokens.html | 55 ++-- tests/data/stream_data.txt | 2 +- worker.js | 1 + 41 files changed, 1956 insertions(+), 964 deletions(-) create mode 100644 Cursor API.md delete mode 100644 Deno.ts create mode 100644 Dockerfile.cross.arm64 create mode 100644 serve.ts create mode 100644 src/app/model/config.rs create mode 100644 src/chat/stream/decoder.rs diff --git a/.env.example b/.env.example index 82f908a..235777d 100644 --- a/.env.example +++ b/.env.example @@ -12,11 +12,12 @@ AUTH_TOKEN= # 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限 SHARED_TOKEN= -# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次 -ENABLE_STREAM_CHECK=true +# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次(已弃用) +# 新版本已经完成优化 +# ENABLE_STREAM_CHECK=true -# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块 -INCLUDE_STOP_REASON_STREAM=true +# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块(已弃用) +# INCLUDE_STOP_REASON_STREAM=true # 令牌文件路径(已弃用) # TOKEN_FILE=.token @@ -87,4 +88,13 @@ DEBUG_LOG_FILE=debug.log REQUEST_LOGS_LIMIT=100 # Cursor 服务超时(秒)(最大值600) -SERVICE_TIMEOUT=30 \ No newline at end of file +SERVICE_TIMEOUT=30 + +# 包含网络引用 +INCLUDE_WEB_REFERENCES=false + +# 持久化日志文件路径 +LOGS_FILE_PATH=logs.bin + +# 持久化页面配置文件路径 +PAGES_FILE_PATH=pages.bin \ No newline at end of file diff --git a/.gitignore b/.gitignore index e512410..e57501b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ node_modules /logs /dev* /build* +/*.bin +/result.txt diff --git a/Cargo.lock b/Cargo.lock index e8f6f86..e0dae0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -175,6 +186,18 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -211,6 +234,28 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta 0.1.4", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.21.0" @@ -259,6 +304,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "rkyv 0.7.45", "serde", "windows-targets", ] @@ -315,7 +361,7 @@ dependencies = [ [[package]] name = "cursor-api" -version = "0.1.3-rc.4-pre.1" +version = "0.1.3-rc.4" dependencies = [ "axum", "base64", @@ -327,6 +373,7 @@ dependencies = [ "gif", "hex", "image", + "memmap2", "parking_lot", "paste", "prost", @@ -334,13 +381,16 @@ dependencies = [ "rand", "regex", "reqwest", + "rkyv 0.7.45", "serde", "serde_json", "sha2", + "sonic-rs", "sysinfo", "tokio", "tokio-stream", "tower-http", + "url", "uuid", ] @@ -362,7 +412,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -408,6 +458,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "faststr" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9154486833a83cb5d99de8c4d831314b8ae810dd4ef18d89ceb7a9c7c728dd74" +dependencies = [ + "bytes", + "rkyv 0.8.10", + "serde", + "simdutf8", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -463,6 +525,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -507,7 +575,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -596,6 +664,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -871,7 +948,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -929,7 +1006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1009,6 +1086,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -1042,6 +1128,26 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "munge" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64142d38c84badf60abf06ff9bd80ad2174306a5b11bd4706535090a30a419df" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -1115,7 +1221,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1228,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.96", ] [[package]] @@ -1266,7 +1372,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn", + "syn 2.0.96", "tempfile", ] @@ -1280,7 +1386,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1292,6 +1398,46 @@ dependencies = [ "prost", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive 0.1.4", +] + +[[package]] +name = "ptr_meta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +dependencies = [ + "ptr_meta_derive 0.3.0", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -1307,6 +1453,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +dependencies = [ + "ptr_meta 0.3.0", +] + [[package]] name = "rand" version = "0.8.5" @@ -1346,6 +1507,26 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "regex" version = "1.11.1" @@ -1375,6 +1556,21 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rend" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" + [[package]] name = "reqwest" version = "0.12.12" @@ -1438,6 +1634,64 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta 0.1.4", + "rend 0.4.2", + "rkyv_derive 0.7.45", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e147371c75553e1e2fcdb483944a8540b8438c31426279553b9a8182a9b7b65" +dependencies = [ + "bytes", + "hashbrown 0.15.2", + "indexmap", + "munge", + "ptr_meta 0.3.0", + "rancor", + "rend 0.5.2", + "rkyv_derive 0.8.10", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1523,6 +1777,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -1563,7 +1823,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1617,12 +1877,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.9" @@ -1648,6 +1923,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "sonic-number" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a74044c092f4f43ca7a6cfd62854cf9fb5ac8502b131347c990bf22bef1dfe" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "sonic-rs" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0275f9f2f07d47556fe60c2759da8bc4be6083b047b491b2d476aa0bfa558eb1" +dependencies = [ + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "ryu", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror 2.0.11", +] + +[[package]] +name = "sonic-simd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940a24e82c9a97483ef66cef06b92160a8fa5cd74042c57c10b24d99d169d2fc" +dependencies = [ + "cfg-if", +] + [[package]] name = "spin" version = "0.9.8" @@ -1666,6 +1979,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.96" @@ -1694,7 +2018,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1731,6 +2055,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.15.0" @@ -1751,7 +2081,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -1762,7 +2101,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -1775,6 +2125,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.43.0" @@ -1786,6 +2151,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -1799,7 +2165,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -1830,7 +2196,7 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -2027,7 +2393,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-shared", ] @@ -2062,7 +2428,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2166,7 +2532,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -2177,7 +2543,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -2313,6 +2679,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.7.5" @@ -2333,7 +2708,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", "synstructure", ] @@ -2355,7 +2730,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] @@ -2375,7 +2750,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", "synstructure", ] @@ -2404,7 +2779,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.96", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d3b2493..c6f84c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cursor-api" -version = "0.1.3-rc.4-pre.1" +version = "0.1.3-rc.4" edition = "2021" authors = ["wisdgod "] description = "OpenAI format compatibility layer for the Cursor API" @@ -16,26 +16,30 @@ axum = { version = "0.8.1", features = ["json"] } base64 = { version = "0.22.1", default-features = false, features = ["std"] } # brotli = { version = "7.0.0", default-features = false, features = ["std"] } bytes = "1.9.0" -chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde"] } +chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde", "rkyv-64"] } dotenvy = "0.15.7" flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] } futures = { version = "0.3.31", default-features = false, features = ["std"] } gif = { version = "0.13.1", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] } +memmap2 = "0.9.5" +# openssl = { version = "0.10.68", features = ["vendored"] } parking_lot = "0.12.3" paste = "1.0.15" prost = "0.13.4" rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } +rkyv = { version = "0.7.45", default-features = false, features = ["alloc", "std", "bytecheck", "size_64", "validation", "std"] } serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } -serde_json = "1.0.137" +serde_json = { package = "sonic-rs", version = "0.3.17" } sha2 = { version = "0.10.8", default-features = false } sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } -tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs"] } +tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs", "signal"] } tokio-stream = { version = "0.1.17", features = ["time"] } tower-http = { version = "0.6.2", features = ["cors", "limit"] } +url = { version = "2.5.4", default-features = false } uuid = { version = "1.12.1", features = ["v4"] } [profile.release] diff --git a/Cross.toml b/Cross.toml index f2be4ef..259aa0d 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,9 +1,5 @@ [target.x86_64-unknown-linux-gnu] dockerfile = "Dockerfile.cross" -[target.x86_64-unknown-freebsd] -pre-build = [ - "pkg update", - "pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl", - "export SSL_CERT_FILE=/etc/ssl/cert.pem" -] +[target.aarch64-unknown-linux-gnu] +dockerfile = "Dockerfile.cross.arm64" diff --git a/Cursor API.md b/Cursor API.md new file mode 100644 index 0000000..5015110 --- /dev/null +++ b/Cursor API.md @@ -0,0 +1,179 @@ +# Cursor API + +## 项目说明 + +### 版本声明 +- 当前版本已进入稳定阶段 +- 以下问题与程序无关,请勿反馈: + - 响应缺字漏字 + - 首字延迟现象 + - 响应出现乱码 +- 性能优势: + - 达到原生客户端响应速度 + - 部分场景下表现更优 +- 开源协议要求: + - Fork 项目禁止以原作者名义进行宣传推广 + - 禁止发布任何形式的官方声明 + +![Cursor API 架构示意图](https://via.placeholder.com/800x400.png?text=Cursor+API+Architecture) + +## 快速入门 + +### 密钥获取 +1. 访问 [Cursor 官网](https://www.cursor.com) 完成注册登录 +2. 开启浏览器开发者工具 (F12) +3. 在 Application → Cookies 中定位 `WorkosCursorSessionToken` +4. 复制第三个字段值(注意:`%3A%3A` 为 `::` 的 URL 编码形式) + +## 配置指南 + +### 环境变量 +| 变量名 | 类型 | 默认值 | 说明 | +|--------|------|--------|-----| +| PORT | int | 3000 | 服务端口号 | +| AUTH_TOKEN | string | 无 | 认证令牌(必需) | +| ROUTE_PREFIX | string | 无 | 路由前缀 | +| TOKEN_LIST_FILE | string | .tokens | Token 存储文件 | + +完整配置参见 [env-example](/env-example) + +### Token 文件规范 +`.tokens` 文件格式: +```plaintext +# 注释行将在下次读取时自动删除 +token1,checksum1 +token2,checksum2 +``` + +文件管理原则: +- 系统自动维护文件内容 +- 仅以下情况需要手动编辑: + - 删除特定 token + - 绑定已有 checksum 到指定 token + +## 模型支持列表 +```json +[ + "claude-3.5-sonnet", + "gpt-4", + "gpt-4o", + "cursor-fast", + "gpt-4o-mini", + "deepseek-v3" +] +``` +*注:模型列表为固定配置,暂不支持自定义扩展* + +## API 文档 + +### 基础对话接口 +**Endpoint** +`POST /v1/chat/completions` + +**认证方式** +`Bearer Token` 三级认证机制: +1. 环境变量 `AUTH_TOKEN` +2. `.token` 文件轮询 +3. 直接 token,checksum 认证(v0.1.3-rc.3+) + +**请求示例** +```json +{ + "model": "gpt-4", + "messages": [ + { + "role": "user", + "content": "解释量子计算的基本原理" + } + ], + "stream": false +} +``` + +**响应示例(非流式)** +```json +{ + "id": "chatcmpl-9Xy...", + "object": "chat.completion", + "created": 1628063500, + "model": "gpt-4", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "量子计算基于量子比特..." + }, + "finish_reason": "stop" + }] +} +``` + +### Token 管理接口 +| 端点 | 方法 | 功能 | +|------|------|-----| +| `/tokens` | GET | Token 信息管理界面 | +| `/tokens/update` | POST | 批量更新 Token 列表 | +| `/tokens/add` | POST | 增量添加 Token | +| `/tokens/delete` | POST | 删除指定 Token | + +```mermaid +sequenceDiagram + participant Client + participant API + Client->>API: POST /tokens/add + API->>API: 验证Token有效性 + API->>File: 写入.tokens + API-->>Client: 返回更新结果 +``` + +## 高级功能 + +### 动态密钥生成 +**Endpoint** +`POST /build-key` + +**优势对比** +| 特性 | 传统模式 | 动态密钥 | +|------|---------|---------| +| 密钥长度 | 较长 | 优化缩短 | +| 配置扩展 | 无 | 支持自定义 | +| 安全等级 | 基础 | 增强编码 | +| 验证效率 | 预校验耗时 | 即时验证 | + +## 系统监控 + +### 健康检查 +**Endpoint** +`GET /health` + +**响应示例** +```json +{ + "status": "success", + "version": "1.2.0", + "uptime": 86400, + "models": ["gpt-4", "claude-3.5"], + "endpoints": ["/v1/chat", "/tokens"] +} +``` + +## 生态工具 + +### 开发辅助工具 +- [Token 获取工具](https://github.com/wisdgod/cursor-api/tree/main/tools/get-token) + 支持 Windows/Linux/macOS 系统 +- [遥测数据重置工具](https://github.com/wisdgod/cursor-api/tree/main/tools/reset-telemetry) + 清除用户使用数据记录 + +## 致谢声明 +本项目的发展离不开以下开源项目的启发: +- [zhx47/cursor-api](https://github.com/zhx47/cursor-api) - 基础架构参考 +- [cursorToApi](https://github.com/luolazyandlazy/cursorToApi) - 认证机制优化方案 + +--- + +> **项目维护说明** +> 我们欢迎社区贡献,但请注意: +> 1. 功能请求需附带使用场景说明 +> 2. Bug 报告请提供复现步骤和环境信息 +> 3. 重要变更需通过 CI/CD 测试流程 \ No newline at end of file diff --git a/Deno.ts b/Deno.ts deleted file mode 100644 index ab2da11..0000000 --- a/Deno.ts +++ /dev/null @@ -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"); - } -}); diff --git a/Dockerfile b/Dockerfile index 012409b..3bf5683 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update && \ build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ && rm -rf /var/lib/apt/lists/* COPY . . -ENV RUSTFLAGS="-C link-arg=-s" +ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native" RUN cargo build --release && \ cp target/release/cursor-api /app/cursor-api @@ -18,7 +18,7 @@ RUN apt-get update && \ build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ && rm -rf /var/lib/apt/lists/* COPY . . -ENV RUSTFLAGS="-C link-arg=-s" +ENV RUSTFLAGS="-C link-arg=-s -C target-cpu=native" RUN cargo build --release && \ cp target/release/cursor-api /app/cursor-api diff --git a/Dockerfile.cross b/Dockerfile.cross index 3097738..1e71cba 100644 --- a/Dockerfile.cross +++ b/Dockerfile.cross @@ -16,7 +16,7 @@ RUN apt-get update && \ && rm -rf /var/lib/apt/lists/* # 设置环境变量 (如果需要) -ENV RUSTFLAGS="-C link-arg=-s" +# ENV RUSTFLAGS="-C link-arg=-s" # 设置 PROTOC 环境变量 (因为你的 build.rs 需要) ENV PROTOC=/usr/bin/protoc diff --git a/Dockerfile.cross.arm64 b/Dockerfile.cross.arm64 new file mode 100644 index 0000000..713e1f3 --- /dev/null +++ b/Dockerfile.cross.arm64 @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 3f542ee..e0906d0 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,43 @@ data: [DONE] "tokens": [ { "token": "string", - "checksum": "string" + "checksum": "string", + "profile": { // 可能存在 + "usage": { + "premium": { + "requests": number, + "requests_total": number, + "tokens": number, + "max_requests": number, + "max_tokens": number + }, + "standard": { + "requests": number, + "requests_total": number, + "tokens": number, + "max_requests": number, + "max_tokens": number + }, + "unknown": { + "requests": number, + "requests_total": number, + "tokens": number, + "max_requests": number, + "max_tokens": number + } + }, + "user": { + "email": "string", + "name": "string", + "id": "string", + "updated_at": "string" + }, + "stripe": { + "membership_type": "free" | "free_trial" | "pro" | "enterprise", + "payment_id": "string", + "days_remaining_on_trial": number + } + } } ], "tokens_count": number @@ -282,14 +318,13 @@ data: [DONE] ```json { "auth_token": "string", // 格式: {token},{checksum} - "enable_stream_check": boolean, // 可选,启用流式响应首块检查 - "include_stop_stream": boolean, // 可选,包含停止流 "disable_vision": boolean, // 可选,禁用图片处理能力 "enable_slow_pool": boolean, // 可选,启用慢速池 "usage_check_models": { // 可选,使用量检查模型配置 "type": "default" | "disabled" | "all" | "custom", "model_ids": "string" // 当type为custom时生效,以逗号分隔的模型ID列表 - } + }, + "include_web_references": boolean } ``` @@ -357,8 +392,6 @@ data: [DONE] "type": "default" | "text" | "html", "content": "string" }, - "enable_stream_check": boolean, - "include_stop_stream": boolean, "vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http" "enable_slow_pool": boolean, "enable_all_claude": boolean, @@ -368,7 +401,8 @@ data: [DONE] }, "enable_dynamic_key": boolean, "share_token": "string", - "proxies": "" | "system" | "proxy1,proxy2,..." + "proxies": "" | "system" | "proxy1,proxy2,...", + "include_web_references": boolean } ``` @@ -383,8 +417,6 @@ data: [DONE] "type": "default" | "text" | "html", // 对于js和css后两者是一样的 "content": "string" }, - "enable_stream_check": boolean, - "include_stop_stream": boolean, "vision_ability": "none" | "base64" | "all", "enable_slow_pool": boolean, "enable_all_claude": boolean, @@ -394,7 +426,8 @@ data: [DONE] }, "enable_dynamic_key": boolean, "share_token": "string", - "proxies": "" | "system" | "proxy1,proxy2,..." + "proxies": "" | "system" | "proxy1,proxy2,...", + "include_web_references": boolean } } ``` diff --git a/serve.ts b/serve.ts new file mode 100644 index 0000000..31bc485 --- /dev/null +++ b/serve.ts @@ -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")}}); \ No newline at end of file diff --git a/src/app/config.rs b/src/app/config.rs index c9a2bfb..2154fa5 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -65,8 +65,6 @@ pub async fn handle_config_update( status: ApiStatus::Success, data: Some(ConfigData { page_content: AppConfig::get_page_content(&request.path), - enable_stream_check: AppConfig::get_stream_check(), - include_stop_stream: AppConfig::get_stop_stream(), vision_ability: AppConfig::get_vision_ability(), enable_slow_pool: AppConfig::get_slow_pool(), enable_all_claude: AppConfig::get_allow_claude(), @@ -74,6 +72,7 @@ pub async fn handle_config_update( enable_dynamic_key: AppConfig::get_dynamic_key(), share_token: AppConfig::get_share_token(), proxies: AppConfig::get_proxies(), + include_web_references: AppConfig::get_web_refs(), }), message: None, })), @@ -96,8 +95,6 @@ pub async fn handle_config_update( } handle_updates!(request, - enable_stream_check => AppConfig::update_stream_check, - include_stop_stream => AppConfig::update_stop_stream, vision_ability => AppConfig::update_vision_ability, enable_slow_pool => AppConfig::update_slow_pool, enable_all_claude => AppConfig::update_allow_claude, @@ -105,6 +102,7 @@ pub async fn handle_config_update( enable_dynamic_key => AppConfig::update_dynamic_key, share_token => AppConfig::update_share_token, proxies => AppConfig::update_proxies, + include_web_references => AppConfig::update_web_refs, ); Ok(Json(NormalResponse { @@ -131,8 +129,6 @@ pub async fn handle_config_update( } handle_resets!(request, - enable_stream_check => AppConfig::reset_stream_check, - include_stop_stream => AppConfig::reset_stop_stream, vision_ability => AppConfig::reset_vision_ability, enable_slow_pool => AppConfig::reset_slow_pool, enable_all_claude => AppConfig::reset_allow_claude, @@ -140,6 +136,7 @@ pub async fn handle_config_update( enable_dynamic_key => AppConfig::reset_dynamic_key, share_token => AppConfig::reset_share_token, proxies => AppConfig::reset_proxies, + include_web_references => AppConfig::reset_web_refs, ); Ok(Json(NormalResponse { diff --git a/src/app/lazy.rs b/src/app/lazy.rs index e98a851..8f659dd 100644 --- a/src/app/lazy.rs +++ b/src/app/lazy.rs @@ -108,6 +108,12 @@ def_cursor_api_url!( "/aiserver.v1.AiService/StreamChat" ); +def_cursor_api_url!( + CURSOR_API2_CHAT_WEB_URL, + CURSOR_API2_HOST, + "/aiserver.v1.AiService/StreamChatWeb" +); + def_cursor_api_url!( CURSOR_API2_STRIPE_URL, CURSOR_API2_HOST, @@ -118,6 +124,12 @@ def_cursor_api_url!(CURSOR_USAGE_API_URL, CURSOR_HOST, "/api/usage"); def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me"); +pub(super) static LOGS_FILE_PATH: LazyLock = + LazyLock::new(|| parse_string_from_env("LOGS_FILE_PATH", "logs.bin")); + +pub(super) static PAGES_FILE_PATH: LazyLock = + LazyLock::new(|| parse_string_from_env("PAGES_FILE_PATH", "pages.bin")); + pub static DEBUG: LazyLock = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); // 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log" diff --git a/src/app/model.rs b/src/app/model.rs index 2fbc88e..1bf836b 100644 --- a/src/app/model.rs +++ b/src/app/model.rs @@ -12,18 +12,22 @@ use crate::{ }, }; use parking_lot::RwLock; +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; mod usage_check; pub use usage_check::UsageCheck; +mod config; mod proxies; pub use proxies::Proxies; mod build_key; pub use build_key::*; +use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS}; + // 页面内容类型枚举 -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)] #[serde(tag = "type", content = "content")] pub enum PageContent { #[serde(rename = "default")] @@ -41,10 +45,8 @@ impl Default for PageContent { } // 静态配置 -#[derive(Clone)] +#[derive(Default, Clone)] pub struct AppConfig { - stream_check: bool, - stop_stream: bool, vision_ability: VisionAbility, slow_pool: bool, allow_claude: bool, @@ -54,9 +56,10 @@ pub struct AppConfig { share_token: String, is_share: bool, proxies: Proxies, + web_refs: bool, } -#[derive(Serialize, Deserialize, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)] pub enum VisionAbility { #[serde(rename = "none", alias = "disabled")] None, @@ -87,7 +90,7 @@ impl Default for VisionAbility { } } -#[derive(Clone, Default)] +#[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)] pub struct Pages { pub root_content: PageContent, pub logs_content: PageContent, @@ -114,24 +117,6 @@ pub struct AppState { pub static APP_CONFIG: LazyLock> = LazyLock::new(|| RwLock::new(AppConfig::default())); -impl Default for AppConfig { - fn default() -> Self { - Self { - stream_check: true, - stop_stream: true, - vision_ability: VisionAbility::Base64, - slow_pool: false, - allow_claude: false, - pages: Pages::default(), - usage_check: UsageCheck::Default, - dynamic_key: false, - share_token: String::default(), - is_share: false, - proxies: Proxies::default(), - } - } -} - macro_rules! config_methods { ($($field:ident: $type:ty, $default:expr;)*) => { $( @@ -207,8 +192,6 @@ macro_rules! config_methods_clone { impl AppConfig { pub fn init() { let mut config = APP_CONFIG.write(); - config.stream_check = parse_bool_from_env("ENABLE_STREAM_CHECK", true); - config.stop_stream = parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true); config.vision_ability = VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)); config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false); @@ -221,15 +204,15 @@ impl AppConfig { config.proxies = match std::env::var("PROXIES") { Ok(proxies) => Proxies::from_str(proxies.as_str()), Err(_) => Proxies::default(), - } + }; + config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false) } config_methods! { - stream_check: bool, true; - stop_stream: bool, true; slow_pool: bool, false; allow_claude: bool, false; dynamic_key: bool, false; + web_refs: bool, false; } config_methods_clone! { @@ -341,11 +324,20 @@ impl AppConfig { impl AppState { pub fn new(token_infos: Vec) -> Self { + // 尝试加载保存的日志 + let request_logs = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async { Self::load_saved_logs().await.unwrap_or_default() }) + }); + Self { - total_requests: 0, + total_requests: request_logs.len() as u64, active_requests: 0, - error_requests: 0, - request_logs: Vec::new(), + error_requests: request_logs + .iter() + .filter(|log| matches!(log.status, LogStatus::Failed)) + .count() as u64, + request_logs, token_infos, } } @@ -357,8 +349,43 @@ impl AppState { } } +#[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)] +pub enum LogStatus { + Pending, + Success, + Failed, +} + +impl Serialize for LogStatus { + fn serialize(&self, serializer: S) -> Result + 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 { + match s { + STATUS_PENDING => Some(Self::Pending), + STATUS_SUCCESS => Some(Self::Success), + STATUS_FAILED => Some(Self::Failed), + _ => None, + } + } +} + // 请求日志 -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct RequestLog { pub id: u64, pub timestamp: chrono::DateTime, @@ -368,12 +395,12 @@ pub struct RequestLog { pub prompt: Option, pub timing: TimingInfo, pub stream: bool, - pub status: &'static str, + pub status: LogStatus, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct TimingInfo { pub total: f64, // 总用时(秒) #[serde(skip_serializing_if = "Option::is_none")] @@ -390,7 +417,7 @@ pub struct ChatRequest { } // 用于存储 token 信息 -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct TokenInfo { pub token: String, pub checksum: String, diff --git a/src/app/model/build_key.rs b/src/app/model/build_key.rs index 98966a1..46f3f31 100644 --- a/src/app/model/build_key.rs +++ b/src/app/model/build_key.rs @@ -4,23 +4,15 @@ use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS}; #[derive(Deserialize)] pub struct BuildKeyRequest { - // 认证令牌(必需) pub auth_token: String, - // 流第一个块检查 - #[serde(default)] - pub enable_stream_check: Option, - // 包含停止流 - #[serde(default)] - pub include_stop_stream: Option, - // 是否禁用图片处理能力 #[serde(default)] pub disable_vision: Option, - // 慢速池 #[serde(default)] pub enable_slow_pool: Option, - // 使用量检查模型规则 #[serde(default)] pub usage_check_models: Option, + #[serde(default)] + pub include_web_references: Option, } pub struct UsageCheckModelConfig { pub model_type: UsageCheckModelType, diff --git a/src/app/model/config.rs b/src/app/model/config.rs new file mode 100644 index 0000000..f4f5244 --- /dev/null +++ b/src/app/model/config.rs @@ -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> { + // 序列化日志 + 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, Box> { + 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::>(&mmap) }; + Ok(archived.deserialize(&mut rkyv::Infallible)?) + } +} + +impl AppConfig { + pub fn save_config() -> Result<(), Box> { + 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> { + 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::(&mmap) }; + let pages = archived.deserialize(&mut rkyv::Infallible)?; + let mut config = APP_CONFIG.write(); + config.pages = pages; + + Ok(()) + } +} diff --git a/src/app/model/proxies.rs b/src/app/model/proxies.rs index bf73b17..b117e4a 100644 --- a/src/app/model/proxies.rs +++ b/src/app/model/proxies.rs @@ -1,6 +1,7 @@ use reqwest::{Client, Proxy}; use serde::{Serialize, Serializer}; use serde::{Deserialize, Deserializer}; +// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; use crate::app::constant::COMMA_STRING; @@ -30,7 +31,7 @@ impl<'de> Deserialize<'de> for Proxies { where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; + let s = ::deserialize(deserializer)?; Ok(Proxies::from_str(&s)) } } diff --git a/src/app/model/usage_check.rs b/src/app/model/usage_check.rs index c186070..7e04498 100644 --- a/src/app/model/usage_check.rs +++ b/src/app/model/usage_check.rs @@ -3,6 +3,7 @@ use crate::{ chat::{config::key_config, constant::AVAILABLE_MODELS}, }; use serde::{Deserialize, Serialize}; +// use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; #[derive(Clone, PartialEq)] pub enum UsageCheck { @@ -108,7 +109,7 @@ impl<'de> Deserialize<'de> for UsageCheck { Custom(String), } - let helper = UsageCheckHelper::deserialize(deserializer)?; + let helper = ::deserialize(deserializer)?; Ok(match helper { UsageCheckHelper::None => UsageCheck::None, UsageCheckHelper::Default => UsageCheck::Default, diff --git a/src/chat/adapter.rs b/src/chat/adapter.rs index a05f6cf..2499451 100644 --- a/src/chat/adapter.rs +++ b/src/chat/adapter.rs @@ -15,8 +15,7 @@ use crate::{ use super::{ aiserver::v1::{ - conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext, - GetChatRequest, ImageProto, ModelDetails, + conversation_message, image_proto, AzureState, ChatExternalLink, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails }, constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS}, model::{Message, MessageContent, Role}, @@ -25,7 +24,7 @@ use super::{ async fn process_chat_inputs( inputs: Vec, disable_vision: bool, -) -> (String, Vec) { +) -> (String, Vec, Vec) { // 收集 system 指令 let instructions = inputs .iter() @@ -98,9 +97,25 @@ async fn process_chat_inputs( file_diff_trajectories: vec![], conversation_summary: None, }], + vec![], ); } + // 处理 WebReferences 开头的 assistant 消息 + chat_inputs = chat_inputs + .into_iter() + .map(|mut input| { + if let (Role::Assistant, MessageContent::Text(text)) = (&input.role, &input.content) { + if text.starts_with("WebReferences:") { + if let Some(pos) = text.find("\n\n") { + input.content = MessageContent::Text(text[pos + 2..].to_owned()); + } + } + } + input + }) + .collect(); + // 如果第一条是 assistant,插入空的 user 消息 if chat_inputs .first() @@ -226,7 +241,32 @@ async fn process_chat_inputs( }); } - (instructions, messages) + let mut urls = Vec::new(); + if let Some(last_msg) = messages.last() { + if last_msg.r#type == conversation_message::MessageType::Human as i32 { + let text = &last_msg.text; + let mut chars = text.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '@' { + let mut url = String::new(); + while let Some(&next_char) = chars.peek() { + if next_char.is_whitespace() { + break; + } + url.push(chars.next().unwrap()); + } + if let Ok(parsed_url) = url::Url::parse(&url) { + if parsed_url.scheme() == "http" || parsed_url.scheme() == "https" { + urls.push(url); + } + } + } + } + } + } + + (instructions, messages, urls) } async fn fetch_image_data( @@ -344,6 +384,7 @@ pub async fn encode_chat_message( model_name: &str, disable_vision: bool, enable_slow_pool: bool, + is_search: bool, ) -> Result, Box> { // 在进入异步操作前获取并释放锁 let enable_slow_pool = { @@ -354,7 +395,7 @@ pub async fn encode_chat_message( } }; - let (instructions, messages) = process_chat_inputs(inputs, disable_vision).await; + let (instructions, messages, urls) = process_chat_inputs(inputs, disable_vision).await; let explicit_context = if !instructions.trim().is_empty() { Some(ExplicitContext { @@ -365,6 +406,15 @@ pub async fn encode_chat_message( None }; + let base_uuid = rand::random::(); + let external_links = urls.into_iter().enumerate().map(|(i, url)| { + let uuid = base_uuid.wrapping_add(i as u16); + ChatExternalLink { + url, + uuid: uuid.to_string(), + } + }).collect(); + let chat = GetChatRequest { current_file: None, conversation: messages, @@ -394,11 +444,15 @@ pub async fn encode_chat_message( is_bash: Some(false), conversation_id: Uuid::new_v4().to_string(), can_handle_filenames_after_language_ids: Some(true), - use_web: None, + use_web: if is_search { + Some("full_search".to_string()) + } else { + None + }, quotes: vec![], debug_info: None, workspace_id: None, - external_links: vec![], + external_links, commit_notes: vec![], long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)), is_eval: Some(false), diff --git a/src/chat/config.rs b/src/chat/config.rs index de61eb5..e649b7b 100644 --- a/src/chat/config.rs +++ b/src/chat/config.rs @@ -6,21 +6,14 @@ impl KeyConfig { pub fn new_with_global() -> Self { Self { auth_token: None, - enable_stream_check: Some(AppConfig::get_stream_check()), - include_stop_stream: Some(AppConfig::get_stop_stream()), disable_vision: Some(AppConfig::get_vision_ability().is_none()), enable_slow_pool: Some(AppConfig::get_slow_pool()), usage_check_models: None, + include_web_references: Some(AppConfig::get_web_refs()), } } pub fn copy_without_auth_token(&self, config: &mut Self) { - if self.enable_stream_check.is_some() { - config.enable_stream_check = self.enable_stream_check; - } - if self.include_stop_stream.is_some() { - config.include_stop_stream = self.include_stop_stream; - } if self.disable_vision.is_some() { config.disable_vision = self.disable_vision; } @@ -30,5 +23,8 @@ impl KeyConfig { if self.usage_check_models.is_some() { config.usage_check_models = self.usage_check_models.clone(); } + if self.include_web_references.is_some() { + config.include_web_references = self.include_web_references; + } } } diff --git a/src/chat/config/key.proto b/src/chat/config/key.proto index 4806294..2542c7d 100644 --- a/src/chat/config/key.proto +++ b/src/chat/config/key.proto @@ -17,12 +17,6 @@ message KeyConfig { // 认证令牌(必需) TokenInfo auth_token = 1; - // 是否启用流检查 - optional bool enable_stream_check = 2; - - // 是否包含停止流 - optional bool include_stop_stream = 3; - // 是否禁用图片处理能力 optional bool disable_vision = 4; @@ -44,6 +38,9 @@ message KeyConfig { // 使用量检查模型规则 optional UsageCheckModel usage_check_models = 6; + // 包含网络引用 + optional bool include_web_references = 7; + // 密码SHA256哈希值 // bytes secret = 2; } \ No newline at end of file diff --git a/src/chat/constant.rs b/src/chat/constant.rs index bbc10b6..bf14e40 100644 --- a/src/chat/constant.rs +++ b/src/chat/constant.rs @@ -6,7 +6,10 @@ macro_rules! def_pub_const { }; } def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF"); -def_pub_const!(ERR_UNSUPPORTED_IMAGE_FORMAT, "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF"); +def_pub_const!( + ERR_UNSUPPORTED_IMAGE_FORMAT, + "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF" +); def_pub_const!(ERR_NODATA, "No data"); const MODEL_OBJECT: &str = "model"; @@ -45,146 +48,137 @@ def_pub_const!(GEMINI_2_0_FLASH_EXP, "gemini-2.0-flash-exp"); def_pub_const!(DEEPSEEK_V3, "deepseek-v3"); def_pub_const!(DEEPSEEK_R1, "deepseek-r1"); -pub const AVAILABLE_MODELS: [Model; 23] = [ - Model { - id: CLAUDE_3_5_SONNET, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: GPT_4, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: GPT_4O, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: CLAUDE_3_OPUS, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: CURSOR_FAST, - created: CREATED, - object: MODEL_OBJECT, - owned_by: CURSOR, - }, - Model { - id: CURSOR_SMALL, - created: CREATED, - object: MODEL_OBJECT, - owned_by: CURSOR, - }, - Model { - id: GPT_3_5_TURBO, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: GPT_4_TURBO_2024_04_09, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: GPT_4O_128K, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: GEMINI_1_5_FLASH_500K, - created: CREATED, - object: MODEL_OBJECT, - owned_by: GOOGLE, - }, - Model { - id: CLAUDE_3_HAIKU_200K, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: CLAUDE_3_5_SONNET_200K, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: CLAUDE_3_5_SONNET_20241022, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: GPT_4O_MINI, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: O1_MINI, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: O1_PREVIEW, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: O1, - created: CREATED, - object: MODEL_OBJECT, - owned_by: OPENAI, - }, - Model { - id: CLAUDE_3_5_HAIKU, - created: CREATED, - object: MODEL_OBJECT, - owned_by: ANTHROPIC, - }, - Model { - id: GEMINI_EXP_1206, - created: CREATED, - object: MODEL_OBJECT, - owned_by: GOOGLE, - }, - Model { - id: GEMINI_2_0_FLASH_THINKING_EXP, - created: CREATED, - object: MODEL_OBJECT, - owned_by: GOOGLE, - }, - Model { - id: GEMINI_2_0_FLASH_EXP, - created: CREATED, - object: MODEL_OBJECT, - owned_by: GOOGLE, - }, - Model { - id: DEEPSEEK_V3, - created: CREATED, - object: MODEL_OBJECT, - owned_by: DEEPSEEK, - }, - Model { - id: DEEPSEEK_R1, - created: CREATED, - object: MODEL_OBJECT, - owned_by: DEEPSEEK, - }, -]; +#[derive(Clone, PartialEq, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] +pub enum ModelType { + Claude35Sonnet, + Gpt4, + Gpt4o, + Claude3Opus, + CursorFast, + CursorSmall, + Gpt35Turbo, + Gpt4Turbo202404, + Gpt4o128k, + Gemini15Flash500k, + Claude3Haiku200k, + Claude35Sonnet200k, + Claude35Sonnet20241022, + Gpt4oMini, + O1Mini, + O1Preview, + O1, + Claude35Haiku, + GeminiExp1206, + Gemini20FlashThinkingExp, + Gemini20FlashExp, + DeepseekV3, + DeepseekR1, +} + +macro_rules! create_model { + ($($id:expr, $owner:expr),* $(,)?) => { + pub const AVAILABLE_MODELS: [Model; count!($( ($id, $owner) )*)] = [ + $( + Model { + id: $id, + created: CREATED, + object: MODEL_OBJECT, + owned_by: $owner, + }, + )* + ]; + }; +} + +macro_rules! count { + () => (0); + (($id:expr, $owner:expr) $( ($id2:expr, $owner2:expr) )*) => (1 + count!($( ($id2, $owner2) )*)); +} + +impl ModelType { + pub fn as_str_name(&self) -> &'static str { + match self { + ModelType::Claude35Sonnet => CLAUDE_3_5_SONNET, + ModelType::Gpt4 => GPT_4, + ModelType::Gpt4o => GPT_4O, + ModelType::Claude3Opus => CLAUDE_3_OPUS, + ModelType::CursorFast => CURSOR_FAST, + ModelType::CursorSmall => CURSOR_SMALL, + ModelType::Gpt35Turbo => GPT_3_5_TURBO, + ModelType::Gpt4Turbo202404 => GPT_4_TURBO_2024_04_09, + ModelType::Gpt4o128k => GPT_4O_128K, + ModelType::Gemini15Flash500k => GEMINI_1_5_FLASH_500K, + ModelType::Claude3Haiku200k => CLAUDE_3_HAIKU_200K, + ModelType::Claude35Sonnet200k => CLAUDE_3_5_SONNET_200K, + ModelType::Claude35Sonnet20241022 => CLAUDE_3_5_SONNET_20241022, + ModelType::Gpt4oMini => GPT_4O_MINI, + ModelType::O1Mini => O1_MINI, + ModelType::O1Preview => O1_PREVIEW, + ModelType::O1 => O1, + ModelType::Claude35Haiku => CLAUDE_3_5_HAIKU, + ModelType::GeminiExp1206 => GEMINI_EXP_1206, + ModelType::Gemini20FlashThinkingExp => GEMINI_2_0_FLASH_THINKING_EXP, + ModelType::Gemini20FlashExp => GEMINI_2_0_FLASH_EXP, + ModelType::DeepseekV3 => DEEPSEEK_V3, + ModelType::DeepseekR1 => DEEPSEEK_R1, + } + } + + pub fn from_str_name(id :&str) -> Option { + match id { + CLAUDE_3_5_SONNET => Some(ModelType::Claude35Sonnet), + GPT_4 => Some(ModelType::Gpt4), + GPT_4O => Some(ModelType::Gpt4o), + CLAUDE_3_OPUS => Some(ModelType::Claude3Opus), + CURSOR_FAST => Some(ModelType::CursorFast), + CURSOR_SMALL => Some(ModelType::CursorSmall), + GPT_3_5_TURBO => Some(ModelType::Gpt35Turbo), + GPT_4_TURBO_2024_04_09 => Some(ModelType::Gpt4Turbo202404), + GPT_4O_128K => Some(ModelType::Gpt4o128k), + GEMINI_1_5_FLASH_500K => Some(ModelType::Gemini15Flash500k), + CLAUDE_3_HAIKU_200K => Some(ModelType::Claude3Haiku200k), + CLAUDE_3_5_SONNET_200K => Some(ModelType::Claude35Sonnet200k), + CLAUDE_3_5_SONNET_20241022 => Some(ModelType::Claude35Sonnet20241022), + GPT_4O_MINI => Some(ModelType::Gpt4oMini), + O1_MINI => Some(ModelType::O1Mini), + O1_PREVIEW => Some(ModelType::O1Preview), + O1 => Some(ModelType::O1), + CLAUDE_3_5_HAIKU => Some(ModelType::Claude35Haiku), + GEMINI_EXP_1206 => Some(ModelType::GeminiExp1206), + GEMINI_2_0_FLASH_THINKING_EXP => Some(ModelType::Gemini20FlashThinkingExp), + GEMINI_2_0_FLASH_EXP => Some(ModelType::Gemini20FlashExp), + DEEPSEEK_V3 => Some(ModelType::DeepseekV3), + DEEPSEEK_R1 => Some(ModelType::DeepseekR1), + _ => None, + } + } +} + +create_model!( + CLAUDE_3_5_SONNET, ANTHROPIC, + GPT_4, OPENAI, + GPT_4O, OPENAI, + CLAUDE_3_OPUS, ANTHROPIC, + CURSOR_FAST, CURSOR, + CURSOR_SMALL, CURSOR, + GPT_3_5_TURBO, OPENAI, + GPT_4_TURBO_2024_04_09, OPENAI, + GPT_4O_128K, OPENAI, + GEMINI_1_5_FLASH_500K, GOOGLE, + CLAUDE_3_HAIKU_200K, ANTHROPIC, + CLAUDE_3_5_SONNET_200K, ANTHROPIC, + CLAUDE_3_5_SONNET_20241022, ANTHROPIC, + GPT_4O_MINI, OPENAI, + O1_MINI, OPENAI, + O1_PREVIEW, OPENAI, + O1, OPENAI, + CLAUDE_3_5_HAIKU, ANTHROPIC, + GEMINI_EXP_1206, GOOGLE, + GEMINI_2_0_FLASH_THINKING_EXP, GOOGLE, + GEMINI_2_0_FLASH_EXP, GOOGLE, + DEEPSEEK_V3, DEEPSEEK, + DEEPSEEK_R1, DEEPSEEK, +); pub const USAGE_CHECK_MODELS: [&str; 11] = [ CLAUDE_3_5_SONNET_20241022, diff --git a/src/chat/error.rs b/src/chat/error.rs index 2ae3f44..5fcc1e0 100644 --- a/src/chat/error.rs +++ b/src/chat/error.rs @@ -125,7 +125,6 @@ impl ErrorResponse { pub enum StreamError { ChatError(ChatError), DataLengthLessThan5, - EmptyMessage, } impl std::fmt::Display for StreamError { @@ -133,7 +132,6 @@ impl std::fmt::Display for StreamError { match self { StreamError::ChatError(error) => write!(f, "{}", error.error.code), StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"), - StreamError::EmptyMessage => write!(f, "empty message"), } } } diff --git a/src/chat/model.rs b/src/chat/model.rs index bda8024..24576c0 100644 --- a/src/chat/model.rs +++ b/src/chat/model.rs @@ -86,8 +86,8 @@ pub struct Model { pub owned_by: &'static str, } -use crate::app::model::{AppConfig, UsageCheck}; use super::constant::USAGE_CHECK_MODELS; +use crate::app::model::{AppConfig, UsageCheck}; impl Model { pub fn is_usage_check(&self, usage_check: Option) -> bool { diff --git a/src/chat/route/config.rs b/src/chat/route/config.rs index 3629cfc..eb94dc2 100644 --- a/src/chat/route/config.rs +++ b/src/chat/route/config.rs @@ -164,11 +164,10 @@ pub async fn handle_build_key( // 构建 proto 消息 let mut key_config = KeyConfig { auth_token: Some(token_info), - enable_stream_check: request.enable_stream_check, - include_stop_stream: request.include_stop_stream, disable_vision: request.disable_vision, enable_slow_pool: request.enable_slow_pool, usage_check_models: None, + include_web_references: request.include_web_references, }; if let Some(usage_check_models) = request.usage_check_models { diff --git a/src/chat/service.rs b/src/chat/service.rs index 00376f9..426d6e6 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -2,12 +2,13 @@ use crate::{ app::{ constant::{ AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION, - OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS, + OBJECT_CHAT_COMPLETION_CHUNK, }, - lazy::{ - AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT, + lazy::{AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT}, + model::{ + AppConfig, AppState, ChatRequest, LogStatus, RequestLog, TimingInfo, TokenInfo, + UsageCheck, }, - model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo, UsageCheck}, }, chat::{ config::KeyConfig, @@ -16,11 +17,11 @@ use crate::{ model::{ ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, }, - stream::{parse_stream_data, StreamMessage}, + stream::{StreamDecoder, StreamMessage}, }, common::{ client::build_client, - model::{error::ChatError, userinfo::MembershipType, ErrorResponse}, + model::{error::ChatError, userinfo::MembershipType, ApiStatus, ErrorResponse}, utils::{ format_time_ms, from_base64, get_token_profile, tokeninfo_to_token, validate_token_and_checksum, @@ -38,16 +39,13 @@ use axum::{ Json, }; use bytes::Bytes; -use futures::{Stream, StreamExt}; +use futures::StreamExt; use prost::Message as _; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ convert::Infallible, sync::{atomic::AtomicBool, Arc}, }; -use std::{ - pin::Pin, - sync::atomic::{AtomicUsize, Ordering}, -}; use tokio::sync::Mutex; use uuid::Uuid; @@ -66,8 +64,16 @@ pub async fn handle_chat( Json(request): Json, ) -> Result, (StatusCode, Json)> { let allow_claude = AppConfig::get_allow_claude(); + + let is_search = request.model.ends_with("-online"); + let model_name = if is_search { + request.model[..request.model.len() - 7].to_string() + } else { + request.model.clone() + }; + // 验证模型是否支持并获取模型信息 - let model = AVAILABLE_MODELS.iter().find(|m| m.id == request.model); + let model = AVAILABLE_MODELS.iter().find(|m| m.id == model_name); let model_supported = model.is_some(); if !(model_supported || allow_claude && request.model.starts_with("claude")) { @@ -168,7 +174,7 @@ pub async fn handle_chat( return false; } - let is_premium = USAGE_CHECK_MODELS.contains(&request.model.as_str()); + let is_premium = USAGE_CHECK_MODELS.contains(&model_name.as_str()); let standard = &profile.usage.standard; let premium = &profile.usage.premium; @@ -213,14 +219,28 @@ pub async fn handle_chat( tokio::spawn(async move { let profile = get_token_profile(&auth_token_clone).await; let mut state = state_clone.lock().await; - // 根据id查找对应的日志 - if let Some(log) = state - .request_logs - .iter_mut() - .rev() - .find(|log| log.id == log_id) - { - log.token_info.profile = profile; + + // 先找到所有需要更新的位置的索引 + let token_info_idx = state + .token_infos + .iter() + .position(|info| info.token == auth_token_clone); + + let log_idx = state.request_logs.iter().rposition(|log| log.id == log_id); + + // 根据索引更新 + match (token_info_idx, log_idx) { + (Some(t_idx), Some(l_idx)) => { + state.token_infos[t_idx].profile = profile.clone(); + state.request_logs[l_idx].token_info.profile = profile; + } + (Some(t_idx), None) => { + state.token_infos[t_idx].profile = profile; + } + (None, Some(l_idx)) => { + state.request_logs[l_idx].token_info.profile = profile; + } + (None, None) => {} } }); } @@ -240,7 +260,7 @@ pub async fn handle_chat( first: None, }, stream: request.stream, - status: STATUS_PENDING, + status: LogStatus::Pending, error: None, }); @@ -252,9 +272,10 @@ pub async fn handle_chat( // 将消息转换为hex格式 let hex_data = match super::adapter::encode_chat_message( request.messages, - &request.model, + &model_name, current_config.disable_vision(), current_config.enable_slow_pool(), + is_search, ) .await { @@ -267,7 +288,7 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_FAILED; + log.status = LogStatus::Failed; log.error = Some(e.to_string()); } state.active_requests -= 1; @@ -282,7 +303,7 @@ pub async fn handle_chat( }; // 构建请求客户端 - let client = build_client(&auth_token, &checksum); + let client = build_client(&auth_token, &checksum, is_search); // 添加超时设置 let response = tokio::time::timeout( std::time::Duration::from_secs(*SERVICE_TIMEOUT), @@ -303,7 +324,7 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_SUCCESS; + log.status = LogStatus::Success; } } resp @@ -318,7 +339,7 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_FAILED; + log.status = LogStatus::Failed; log.error = Some(e.to_string()); } state.active_requests -= 1; @@ -340,7 +361,7 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_FAILED; + log.status = LogStatus::Failed; log.error = Some("Request timeout".to_string()); } state.active_requests -= 1; @@ -359,70 +380,171 @@ pub async fn handle_chat( state.active_requests -= 1; } + let convert_web_ref = current_config.include_web_references(); + if request.stream { let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple()); - let full_text = Arc::new(Mutex::new(String::with_capacity(1024))); let is_start = Arc::new(AtomicBool::new(true)); let start_time = std::time::Instant::now(); - let first_chunk_time = Arc::new(Mutex::new(None)); + let first_chunk_time = Arc::new(Mutex::new(None::)); + let decoder = Arc::new(Mutex::new(StreamDecoder::new())); + + // 定义消息处理器的上下文结构体 + struct MessageProcessContext<'a> { + response_id: &'a str, + model: &'a str, + is_start: &'a AtomicBool, + first_chunk_time: &'a Mutex>, + start_time: std::time::Instant, + state: &'a Mutex, + current_id: u64, + } + + // 处理消息并生成响应数据的辅助函数 + async fn process_messages( + messages: Vec, + ctx: &MessageProcessContext<'_>, + ) -> String { + let mut response_data = String::new(); + + for message in messages { + match message { + StreamMessage::Content(text) => { + // 记录首字时间(如果还未记录) + if let Ok(mut first_time) = ctx.first_chunk_time.try_lock() { + if first_time.is_none() { + *first_time = Some(ctx.start_time.elapsed().as_secs_f64()); + } + } + + let is_first = ctx.is_start.load(Ordering::SeqCst); + + let response = ChatResponse { + id: ctx.response_id.to_string(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: if is_first { + Some(ctx.model.to_string()) + } else { + None + }, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: if is_first { + ctx.is_start.store(false, Ordering::SeqCst); + Some(Role::Assistant) + } else { + None + }, + content: Some(text), + }), + finish_reason: None, + }], + usage: None, + }; + + response_data.push_str(&format!( + "data: {}\n\n", + serde_json::to_string(&response).unwrap() + )); + } + StreamMessage::StreamEnd => { + // 计算总时间和首次片段时间 + let total_time = ctx.start_time.elapsed().as_secs_f64(); + let first_time = ctx.first_chunk_time.lock().await.unwrap_or(total_time); + + { + let mut state = ctx.state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == ctx.current_id) + { + log.timing.total = format_time_ms(total_time); + log.timing.first = Some(format_time_ms(first_time)); + } + } + + let response = ChatResponse { + id: ctx.response_id.to_string(), + object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), + created: chrono::Utc::now().timestamp(), + model: None, + choices: vec![Choice { + index: 0, + message: None, + delta: Some(Delta { + role: None, + content: None, + }), + finish_reason: Some(FINISH_REASON_STOP.to_string()), + }], + usage: None, + }; + response_data.push_str(&format!( + "data: {}\n\ndata: [DONE]\n\n", + serde_json::to_string(&response).unwrap() + )); + } + StreamMessage::Debug(debug_prompt) => { + if let Ok(mut state) = ctx.state.try_lock() { + if let Some(last_log) = state.request_logs.last_mut() { + last_log.prompt = Some(debug_prompt); + } + } + } + _ => {} // 忽略其他消息类型 + } + } + + response_data + } let stream = { - // 创建新的 stream let mut stream = response.bytes_stream(); - if current_config.enable_stream_check() { - // 检查第一个 chunk + // 处理第一个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()), ) })?; - match parse_stream_data(&chunk) { - Err(StreamError::ChatError(error)) => { - let error_respone = error.to_error_response(); - // 更新请求日志为失败 + 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) { - 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; - } + 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_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> + Send, - >, - > } + return Err(( + error_response.status_code(), + Json(error_response.to_common()), + )); } } None => { - // Box::pin(stream) - // as Pin> + Send>> // 更新请求日志为失败 { let mut state = state.lock().await; @@ -432,7 +554,7 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_FAILED; + log.status = LogStatus::Failed; log.error = Some("Empty stream response".to_string()); state.error_requests += 1; } @@ -446,176 +568,66 @@ pub async fn handle_chat( )); } } - } else { - Box::pin(stream) - as Pin> + 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(); + // 处理后续的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 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); + 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(); - match parse_stream_data(&buffer_guard) { - Ok(StreamMessage::Content(texts)) => { - buffer_guard.clear(); - let mut response_data = String::new(); + async move { + let chunk = chunk.unwrap_or_default(); - // 记录首字时间(如果还未记录) - if let Ok(mut first_time) = first_chunk_time.try_lock() { - if first_time.is_none() { - *first_time = - Some(format_time_ms(start_time.elapsed().as_secs_f64())); - } + 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) => { + eprintln!("[警告] Stream error: {}", e); + return Ok::<_, Infallible>(Bytes::new()); } + }; - // 处理文本内容 - 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 mut response_data = String::new(); - let response = ChatResponse { - id: response_id.clone(), - object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), - created: chrono::Utc::now().timestamp(), - model: if is_first { Some(model.clone()) } else { None }, - choices: vec![Choice { - index: 0, - message: None, - delta: Some(Delta { - role: if is_first { - is_start.store(false, Ordering::SeqCst); - Some(Role::Assistant) - } else { - None - }, - content: Some(text), - }), - finish_reason: None, - }], - usage: None, - }; - - response_data.push_str(&format!( - "data: {}\n\n", - serde_json::to_string(&response).unwrap() - )); - } - - Ok::<_, Infallible>(Bytes::from(response_data)) - } - Ok(StreamMessage::StreamStart) => { - buffer_guard.clear(); - // 发送初始响应,包含模型信息 - let response = ChatResponse { - id: response_id.clone(), - object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), - created: chrono::Utc::now().timestamp(), - model: { - is_start.store(true, Ordering::SeqCst); - Some(model.clone()) - }, - choices: vec![Choice { - index: 0, - message: None, - delta: Some(Delta { - role: Some(Role::Assistant), - content: Some(String::new()), - }), - finish_reason: None, - }], - usage: None, - }; - - Ok(Bytes::from(format!( - "data: {}\n\n", - serde_json::to_string(&response).unwrap() - ))) - } - Ok(StreamMessage::StreamEnd) => { - buffer_guard.clear(); - - // 计算总时间和首次片段时间 - let total_time = format_time_ms(start_time.elapsed().as_secs_f64()); - let first_time = first_chunk_time.lock().await.unwrap_or(total_time); - - { - let mut state = state.lock().await; - if let Some(log) = state - .request_logs - .iter_mut() - .rev() - .find(|log| log.id == current_id) - { - log.timing.total = total_time; - log.timing.first = Some(first_time); - } - } - - if include_finish_reason { - let response = ChatResponse { - id: response_id.clone(), - object: OBJECT_CHAT_COMPLETION_CHUNK.to_string(), - created: chrono::Utc::now().timestamp(), - model: None, - choices: vec![Choice { - index: 0, - message: None, - delta: Some(Delta { - role: None, - content: None, - }), - finish_reason: Some(FINISH_REASON_STOP.to_string()), - }], - usage: None, - }; - Ok(Bytes::from(format!( - "data: {}\n\ndata: [DONE]\n\n", - serde_json::to_string(&response).unwrap() - ))) - } else { - Ok(Bytes::from("data: [DONE]\n\n")) + 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); } } - Ok(StreamMessage::Incomplete) => { - // 保持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() { - last_log.prompt = Some(debug_prompt.clone()); - } - } - Ok(Bytes::new()) - } - Err(e) => { - buffer_guard.clear(); - eprintln!("[警告] Stream error: {}", e); - Ok(Bytes::new()) + + 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() .header("Cache-Control", "no-cache") @@ -626,81 +638,62 @@ pub async fn handle_chat( } else { // 非流式响应 let start_time = std::time::Instant::now(); - let mut first_chunk_received = false; - let mut first_chunk_time = 0.0; + let mut first_chunk_time = None::; + let mut decoder = StreamDecoder::new(); let mut full_text = String::with_capacity(1024); let mut stream = response.bytes_stream(); - let mut prompt = None; + let mut all_chunks = Vec::new(); - let mut buffer = Vec::new(); + // 收集所有的chunks while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e| { - // 更新请求日志为失败 - if let Ok(mut state) = state.try_lock() { - if let Some(log) = state - .request_logs - .iter_mut() - .rev() - .find(|log| log.id == current_id) - { - log.status = STATUS_FAILED; - log.error = Some(format!("Failed to read response chunk: {}", e)); - state.error_requests += 1; - } - } + let error_message = format!("Failed to read response chunk: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, - Json( - ChatError::RequestFailed(format!("Failed to read response chunk: {}", e)) - .to_json(), - ), + Json(ChatError::RequestFailed(error_message).to_json()), ) })?; + all_chunks.extend(chunk); + } - buffer.extend_from_slice(&chunk); + // 一次性解码所有数据 + let messages = match decoder.decode(&all_chunks, convert_web_ref) { + Ok(msgs) => msgs, + Err(StreamError::ChatError(error)) => { + let error_response = error.to_error_response(); + return Err(( + error_response.status_code(), + Json(error_response.to_common()), + )); + } + Err(e) => { + let error_response = ErrorResponse { + status: ApiStatus::Error, + code: Some(500), + error: Some(e.to_string()), + message: None, + }; + return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))); + } + }; - match parse_stream_data(&buffer) { - Ok(StreamMessage::Content(texts)) => { - if !first_chunk_received { - first_chunk_time = format_time_ms(start_time.elapsed().as_secs_f64()); - first_chunk_received = true; + // 处理所有消息 + for message in messages { + match message { + StreamMessage::Content(text) => { + if first_chunk_time.is_none() { + first_chunk_time = Some(start_time.elapsed().as_secs_f64()); } - for text in texts { - full_text.push_str(&text); - } - buffer.clear(); + full_text.push_str(&text); } - Ok(StreamMessage::Incomplete) => continue, - Ok(StreamMessage::Debug(debug_prompt)) => { - prompt = Some(debug_prompt); - buffer.clear(); - } - Ok(StreamMessage::StreamStart) | Ok(StreamMessage::StreamEnd) => { - buffer.clear(); - } - Err(StreamError::ChatError(error)) => { - let error = error.to_error_response(); - // 更新请求日志为失败 - { - let mut state = state.lock().await; - if let Some(log) = state - .request_logs - .iter_mut() - .rev() - .find(|log| log.id == current_id) - { - log.status = STATUS_FAILED; - log.error = Some(error.native_code()); - log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64()); - state.error_requests += 1; + StreamMessage::Debug(debug_prompt) => { + if let Ok(mut state) = state.try_lock() { + if let Some(last_log) = state.request_logs.last_mut() { + last_log.prompt = Some(debug_prompt); } } - return Err((error.status_code(), Json(error.to_common()))); - } - Err(_) => { - buffer.clear(); - continue; } + _ => {} } } @@ -715,11 +708,8 @@ pub async fn handle_chat( .rev() .find(|log| log.id == current_id) { - log.status = STATUS_FAILED; + log.status = LogStatus::Failed; log.error = Some("Empty response received".to_string()); - if let Some(p) = prompt { - log.prompt = Some(p); - } state.error_requests += 1; } } @@ -761,9 +751,8 @@ pub async fn handle_chat( .find(|log| log.id == current_id) { log.timing.total = total_time; - log.timing.first = Some(first_chunk_time); - log.prompt = prompt; - log.status = STATUS_SUCCESS; + log.timing.first = first_chunk_time; + log.status = LogStatus::Success; } } diff --git a/src/chat/stream.rs b/src/chat/stream.rs index 84f67bf..953526d 100644 --- a/src/chat/stream.rs +++ b/src/chat/stream.rs @@ -1,230 +1,2 @@ -use super::aiserver::v1::StreamChatResponse; -use flate2::read::GzDecoder; -use prost::Message; -use std::io::Read; - -use super::error::{ChatError, StreamError}; - -// 解压gzip数据 -fn decompress_gzip(data: &[u8]) -> Option> { - 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), - // 流结束标志 b"\x02\0\0\0\x02{}" - StreamEnd, -} - -pub fn parse_stream_data(data: &[u8]) -> Result { - 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::(&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::(&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 = 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::>() - .join("") - } - - // 多次解析数据 - let mut offset = 0; - while offset < bytes.len() { - let remaining_bytes = &bytes[offset..]; - let msg_boundary = find_next_message_boundary(remaining_bytes); - let current_msg_bytes = &remaining_bytes[..msg_boundary]; - let hex_str = bytes_to_hex(current_msg_bytes); - - match parse_stream_data(current_msg_bytes) { - Ok(message) => { - match message { - StreamMessage::Content(messages) => { - print!("消息内容 [hex: {}]:", hex_str); - for msg in messages { - println!(" {}", msg); - } - offset += msg_boundary; - } - StreamMessage::Debug(_) => { - // println!("调试信息 [hex: {}]: {}", hex_str, prompt); - offset += msg_boundary; - } - StreamMessage::StreamEnd => { - println!("流结束 [hex: {}]", hex_str); - break; - } - StreamMessage::StreamStart => { - println!("流开始 [hex: {}]", hex_str); - offset += msg_boundary; - } - StreamMessage::Incomplete => { - println!("数据不完整 [hex: {}]", hex_str); - break; - } - } - } - Err(e) => { - println!("解析错误 [hex: {}]: {}", hex_str, e); - break; - } - } - } -} +mod decoder; +pub use decoder::*; diff --git a/src/chat/stream/decoder.rs b/src/chat/stream/decoder.rs new file mode 100644 index 0000000..936df7e --- /dev/null +++ b/src/chat/stream/decoder.rs @@ -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> { + 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 { + 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), + // 内容开始标志 + 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, + first_result: Option>, + 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> { + 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, 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, 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, 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, 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, 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::(&text) { + return Err(StreamError::ChatError(error)); + } + } + Ok(None) + } + + fn handle_gzip_json_message( + &self, + msg_data: &[u8], + ) -> Result, 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::(&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 = 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 = 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::>() + .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; + } + } + } + } +} diff --git a/src/common/client.rs b/src/common/client.rs index 11851cb..2028885 100644 --- a/src/common/client.rs +++ b/src/common/client.rs @@ -5,8 +5,7 @@ use crate::{app::{ HEADER_NAME_GHOST_MODE, TRUE, }, lazy::{ - CURSOR_API2_CHAT_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, - REVERSE_PROXY_HOST, USE_REVERSE_PROXY, + CURSOR_API2_CHAT_URL, CURSOR_API2_CHAT_WEB_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, REVERSE_PROXY_HOST, USE_REVERSE_PROXY }, }, AppConfig}; use reqwest::header::{ @@ -67,19 +66,24 @@ pub fn rebuild_http_client() { /// # 返回 /// /// * `reqwest::RequestBuilder` - 配置好的请求构建器 -pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder { +pub fn build_client(auth_token: &str, checksum: &str, is_search: bool) -> RequestBuilder { let trace_id = Uuid::new_v4().to_string(); + let url = if is_search { + &*CURSOR_API2_CHAT_WEB_URL + } else { + &*CURSOR_API2_CHAT_URL + }; let client = if *USE_REVERSE_PROXY { HTTP_CLIENT .read() - .post(&*CURSOR_API2_CHAT_URL) + .post(url) .header(HOST, &*REVERSE_PROXY_HOST) .header(PROXY_HOST, CURSOR_API2_HOST) } else { HTTP_CLIENT .read() - .post(&*CURSOR_API2_CHAT_URL) + .post(url) .header(HOST, CURSOR_API2_HOST) }; diff --git a/src/common/model/config.rs b/src/common/model/config.rs index 493fad6..2112f35 100644 --- a/src/common/model/config.rs +++ b/src/common/model/config.rs @@ -5,8 +5,6 @@ use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies}; #[derive(Serialize)] pub struct ConfigData { pub page_content: Option, - pub enable_stream_check: bool, - pub include_stop_stream: bool, pub vision_ability: VisionAbility, pub enable_slow_pool: bool, pub enable_all_claude: bool, @@ -15,6 +13,7 @@ pub struct ConfigData { #[serde(skip_serializing_if = "String::is_empty")] pub share_token: String, pub proxies: Proxies, + pub include_web_references: bool, } #[derive(Deserialize, Default)] @@ -23,8 +22,6 @@ pub struct ConfigUpdateRequest { pub action: String, // "get", "update", "reset" pub path: String, pub content: Option, // "default", "text", "html" - pub enable_stream_check: Option, - pub include_stop_stream: Option, pub vision_ability: Option, pub enable_slow_pool: Option, pub enable_all_claude: Option, @@ -32,4 +29,5 @@ pub struct ConfigUpdateRequest { pub enable_dynamic_key: Option, pub share_token: Option, pub proxies: Option, + pub include_web_references: Option, } diff --git a/src/common/model/userinfo.rs b/src/common/model/userinfo.rs index ad8554a..5a1466b 100644 --- a/src/common/model/userinfo.rs +++ b/src/common/model/userinfo.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; #[derive(Serialize)] #[serde(untagged)] @@ -8,14 +9,14 @@ pub enum GetUserInfo { Error { error: String }, } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct TokenProfile { pub usage: UsageProfile, pub user: UserProfile, pub stripe: StripeProfile, } -#[derive(Deserialize, Serialize, PartialEq, Clone)] +#[derive(Deserialize, Serialize, PartialEq, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub enum MembershipType { #[serde(rename = "free")] Free, @@ -27,7 +28,7 @@ pub enum MembershipType { Enterprise, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct StripeProfile { #[serde(rename(deserialize = "membershipType"))] pub membership_type: MembershipType, @@ -41,7 +42,7 @@ pub struct StripeProfile { pub days_remaining_on_trial: u32, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct ModelUsage { #[serde(rename(deserialize = "numRequests", serialize = "requests"))] pub num_requests: u32, @@ -65,7 +66,7 @@ pub struct ModelUsage { pub max_tokens: Option, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct UsageProfile { #[serde(rename(deserialize = "gpt-4"))] pub premium: ModelUsage, @@ -75,7 +76,7 @@ pub struct UsageProfile { pub unknown: ModelUsage, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] pub struct UserProfile { pub email: String, // pub email_verified: bool, diff --git a/src/main.rs b/src/main.rs index 0c36c7b..2d0658a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use chat::{ }; use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env}; use std::sync::Arc; +use tokio::signal; use tokio::sync::Mutex; use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer}; @@ -63,6 +64,11 @@ async fn main() { // 初始化应用状态 let state = Arc::new(Mutex::new(AppState::new(token_infos))); + // 尝试加载保存的配置 + if let Err(e) = AppConfig::load_saved_config() { + eprintln!("加载保存的配置失败: {}", e); + } + // 创建一个克隆用于后台任务 let state_for_reload = state.clone(); @@ -84,10 +90,55 @@ async fn main() { let mut app_state = state_for_reload.lock().await; app_state.update_checksum(); - debug_println!("checksum 自动刷新: {}", next_reload); + // debug_println!("checksum 自动刷新: {}", next_reload); } }); + // 创建一个克隆用于信号处理 + let state_for_shutdown = state.clone(); + + // 设置关闭信号处理 + let shutdown_signal = async move { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("正在关闭服务器..."); + + // 保存配置 + if let Err(e) = AppConfig::save_config() { + eprintln!("保存配置失败: {}", e); + } else { + println!("配置已保存"); + } + + // 保存日志 + let state = state_for_shutdown.lock().await; + if let Err(e) = state.save_logs().await { + eprintln!("保存日志失败: {}", e); + } else { + println!("日志已保存"); + } + }; + // 设置路由 let app = Router::new() .route(ROUTE_ROOT_PATH, get(handle_root)) @@ -128,9 +179,19 @@ async fn main() { println!("服务器运行在端口 {}", port); println!("当前版本: v{}", PKG_VERSION); // if PKG_VERSION.contains("pre") { - println!("当前是测试版,有问题及时反馈哦~"); + // println!("当前是测试版,有问题及时反馈哦~"); // } let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); + let server = axum::serve(listener, app); + tokio::select! { + result = server => { + if let Err(e) = result { + eprintln!("服务器错误: {}", e); + } + } + _ = shutdown_signal => { + println!("服务器已关闭"); + } + } } diff --git a/static/api.html b/static/api.html index 58bccee..850ed6d 100644 --- a/static/api.html +++ b/static/api.html @@ -118,7 +118,7 @@
- +
@@ -130,7 +130,7 @@
- +
@@ -197,14 +197,16 @@ } // 获取模型列表 - async function getModels() { + async function getModels(showMessage = false) { try { const modelList = document.getElementById('modelList'); const suffix = document.getElementById('customSuffix').checked ? document.getElementById('suffixInput').value : ''; modelList.value = globalModels.map(model => model + suffix).join(','); - showGlobalMessage('模型列表已更新'); + if (showMessage) { + showGlobalMessage('模型列表已更新'); + } } catch (error) { showGlobalMessage('获取模型列表失败', true); } @@ -223,7 +225,7 @@ const suffixInput = document.getElementById('suffixInput'); suffixInput.style.display = document.getElementById('customSuffix').checked ? 'block' : 'none'; if (document.getElementById('customSuffix').checked) { - getModels(); + getModels(false); } } @@ -363,8 +365,13 @@ await startStatusCheck(); showGlobalMessage('系统初始化完成'); - // 监听后缀输入变化 - document.getElementById('suffixInput').addEventListener('input', getModels); + // 使用防抖处理后缀输入事件 + const suffixInput = document.getElementById('suffixInput'); + let debounceTimer; + suffixInput.addEventListener('input', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => getModels(false), 300); + }); } catch (error) { showGlobalMessage('系统初始化失败', true); } diff --git a/static/build_key.html b/static/build_key.html index fc84cf6..f3dbbc1 100644 --- a/static/build_key.html +++ b/static/build_key.html @@ -75,24 +75,6 @@ -
- - -
- -
- - -
-
+ + + + +
+
@@ -187,14 +178,13 @@ const data = { auth_token: dataToken, - enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined), - include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined), disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined), enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined), usage_check_models: type ? { type: type, model_ids: type === 'custom' ? modelIds : undefined - } : undefined + } : undefined, + include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined) }; try { @@ -227,11 +217,10 @@ function clearForm() { document.getElementById('authToken').value = ''; document.getElementById('dataToken').value = ''; - document.getElementById('enableStreamCheck').value = ''; - document.getElementById('includeStopStream').value = ''; document.getElementById('disableVision').value = ''; document.getElementById('enableSlowPool').value = ''; document.getElementById('usageCheckType').value = 'default'; + document.getElementById('includeWebReferences').value = ''; document.getElementById('modelListContainer').style.display = 'none'; document.getElementById('keyResult').style.display = 'none'; showGlobalMessage('表单已清空'); diff --git a/static/config.html b/static/config.html index 250cdb1..69f1299 100644 --- a/static/config.html +++ b/static/config.html @@ -45,24 +45,6 @@
-
- - -
- -
- - -
-
- - - - -
-
- - -
+ + + + +
@@ -376,7 +373,15 @@ const data = await makeAuthenticatedRequest('/tokens/get'); if (data) { const tableBody = document.getElementById('tokenTableBody'); - tableBody.innerHTML = data.tokens.map(t => `${t.token}${t.checksum}`).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 `${t.token}${t.checksum}${user.email || '-'}${formatMembershipType(stripe.membership_type)}${premium.requests || 0}/${premium.max_requests || '∞'}${stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}天` : '-'}`; + }).join(''); showGlobalMessage('配置获取成功'); } } @@ -397,6 +402,14 @@ }); } + async function reloadTokens() { + const data = await makeAuthenticatedRequest('/tokens/reload'); + if (data) { + showGlobalMessage(`Token重载成功: ${data.message}`); + getTokenInfo(); // 刷新当前配置 + } + } + async function addTokens() { const tokensInput = document.getElementById('tokenInput').value; @@ -508,12 +521,11 @@ }); // 重置所有选项 - document.getElementById('enableStreamCheck').value = ''; - document.getElementById('includeStopStream').value = ''; document.getElementById('disableVision').value = ''; document.getElementById('enableSlowPool').value = ''; document.getElementById('usageCheckType').value = ''; document.getElementById('modelListContainer').style.display = 'none'; + document.getElementById('includeWebReferences').value = ''; } function closeKeyModal() { @@ -537,14 +549,13 @@ const payload = { auth_token: `${currentToken},${currentChecksum}`, - enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined), - include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined), disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined), enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined), usage_check_models: type ? { type: type, model_ids: type === 'custom' ? modelIds : undefined - } : undefined + } : undefined, + include_web_references: parseBooleanFromString(document.getElementById('includeWebReferences').value, undefined) }; const data = await makeAuthenticatedRequest('/build-key', { diff --git a/tests/data/stream_data.txt b/tests/data/stream_data.txt index 4fc1df7..9318aca 100644 --- a/tests/data/stream_data.txt +++ b/tests/data/stream_data.txt @@ -1 +1 @@ -01000005231F8B080000000000000385553D8F1C451025EE981F509060A3F5AC6C0C81395932F6D95C6063F9CEB21CF96A676A669AEDA91EF7C7EE0DBE937046CE0F3004C4C40810093FE50422E22FA0EA9EB9DD8593894E37DBF5EAEBBD57EFBFF3E1EFEFEE9D7EB6FFE0E0D18BC3E78747FB0F4F6FABE736023A0264D01CC818DD1007E89D6D1C761DB919F4764D8E2A580CF0E0F1D1B59B163ACDBA8029B4C5BE1F205868C9F480ECD7E400798097917CD0963D8416038496207A72D0A2872BD147346690AF03ACB531B020C0858D014A5B696EAE164A5D2FE0594BBC09D51ED02F3537505B0754E9E0257368493B89A319F486D013D818FA1800C1EBAE37BAD654C18A9CD796C1D60951DEC3C2D872990B6C75D31ADDB482293FB7C80D79602AC97B740320578055E5A1B45D479C736BAE74898160DD9223889CE3AA0C2FBD2E8818FC52F73D5505DC97C24FB0EB0DDD52C7C7C706B989D8D0AD1E433B0F765E6B436A3E87A228804EB40FD26E022B8A42BD7A95DA7E711DCECEFEFFD58DB7BE3A3E3E5647D3684B64F044A975E2A01D815432036FF3927A47353969D9B219C0115679317D8581A63D24F402BEA843DA9BF679B91D216F68E003BA3027AEA65548A60B168CA39AC1228614F38107BBC4E13D78426BA7C37F6ACC05E91A7C4FA5AE759998E5281150667EC7AC71F0C2EA95AE0810164E530D74D21B640C5B9C189B99416443DE6F987719B4CF892FBA56EA4601F72CB00D60348175D0E15240A1C632F842A98F0A38A801336627BC128A0D368266406135E9866122C5059D1DF9DE7225AFD214A70785523713A73A0C02E3C6979EE469876E59D935174A7D3C4A4906282410A131ADB744E02F92E55E7363532238B807580721402BD83A6834B0C07219748A357A49E0ED2D105AF543682D0B0D13FED959E29AFAE49222B60A48A246DE30351370AC0A8DB73BA5A5D58B6ADE565812AD231F70E44D47A1B515CCA134E87D9E99D4908D6041C6722364DE74B4A3526F3B9ADBD092CB42AD23978940770EEEB6183ED73E58375CB90AAF14002499C9DFAD494C9FF34CF64EF71FDDDBF8B19A1CFAE9E1FE93D3DBEAF156EFB535C6AEA589B45D1F5C2CB3BF0A778C110EEF50C08B261C195A2107D167974CD90D053CB2C044957CC472C9766DA86AE832EC4A3B2A83E88B77D10BB557461F6CF762FBFD6DF56443D5BBAD66015C0C50518DD104A5F61E3E3D3CBA682C7ACAA81D9EE82E7662AD7D0C499368F2ED492B0C76490C46773A649A80D7DC18DA940347ADE625B4E82AF26196A0D3C2AD0F1074470927FD979350B62C09AA887AF2A150EA482C4BFB4D64EF6C6DA3383F5729D0D0C9C569836B133B276F49CDC825211FE06544A3C3301E45A9901C89C66749F2D306881B5198E654472E49733393DB423D71252779F3B514F59468B63EA5DA1C61D02BBAF85C28F58CA08FCE47023A098E3A828AFAD0824361B05809838F3DB95A974936E2EAA1FD14D624C76009E4BD182D1A2145BE8DBBB1AEC692AE195A9101E2D8914BBB4B08636ACD6C57BBA5ED80B4B8D021A20147A5CEAB2F60E4871424E7D9D9D8B42047174D66417AE767D0D98536FA2B4A0A18D5BC4D2147DE46578AA7CB942AEAACD055EC20BD0E2E8A7D37621C2BCAD8E2D4F7AD6B68F4D4DE9A3C723340699D08426E18973403479D5D4D58537B65CEA1398C6917E8A902CBD0C60E1918437499904234DBA7B17103BDD35CEADE909F2ED2DA3A53CDB66E97233429D22EBEA432153DF24BEDCDB3B8F6E6972A73349BD15AFE6D354A9DFFFADD9F6F7EF8EBF5B7E73F7F7DFED337E7BF7DFFC7EB1FFFFEE5CD6EDC3FD3E84DBB490A0000000000000000000000050A03E6889100000000080A06E68BA5E69C8900000000080A06E585B3E4BA8E00000000050A03E7BC9600000000050A03E7A88B00000000050A03E3808100000000080A06E8BDAFE4BBB600000000080A06E5BC80E58F9100000000050A03E3808100000000080A06E7AE97E6B39500000000050A03E3808100000000080A06E695B0E68DAE00000000080A06E7BB93E69E8400000000050A03E3808100000000080A06E69CBAE599A800000000080A06E5ADA6E4B9A000000000050A03E3808100000000080A06E4BABAE5B7A500000000080A06E699BAE883BD00000000050A03E7AD8900000000080A06E5A49AE4B8AA00000000080A06E9A286E59F9F000000000B0A09E79A84E4BFA1E681AF00000000050A03E3808200000000080A06E68891E79A8400000000080A06E79FA5E8AF8600000000050A03E6B6B500000000050A03E79B9600000000050A03E4BA8600000000050A03E5A49A00000000050A03E7A78D00000000050A03E7BC9600000000050A03E7A88B00000000080A06E8AFADE8A88000000000050A03EFBC8800000000050A03E5A68200000000080A06507974686F6E00000000050A03E3808100000000060A044A61766100000000050A03E3808100000000060A044A61766100000000080A0653637269707400000000050A03E3808100000000030A014300000000040A022B2B00000000050A03E7AD8900000000080A06EFBC89E3808100000000080A06E5BC80E58F9100000000050A03E6A18600000000050A03E69EB600000000050A03E3808100000000080A06E5B7A5E585B700000000050A03E5928C00000000080A06E69C80E4BDB300000000080A06E5AE9EE8B7B5000000000B0A09E38082E6ADA4E5A49600000000080A06EFBC8CE6889100000000050A03E8BF9800000000080A06E58FAFE4BBA500000000080A06E68F90E4BE9B00000000080A06E585B3E4BA8E00000000080A06E8AEA1E7AE9700000000050A03E69CBA00000000080A06E7A791E5ADA600000000050A03E79A8400000000080A06E79086E8AEBA00000000080A06E79FA5E8AF8600000000050A03E3808100000000080A06E68A80E69CAF00000000080A06E8B68BE58ABF00000000050A03E5928C00000000080A06E8A18CE4B89A00000000080A06E58AA8E68081000000000B0A09E79A84E4BFA1E681AF00000000070A05E380820A0A00000000080A06E5A682E69E9C00000000050A03E4BDA000000000050A03E69C8900000000080A06E585B7E4BD93000000000B0A09E79A84E997AEE9A29800000000050A03E6889600000000080A06E99C80E8A68100000000080A06E5B8AEE58AA900000000050A03E79A8400000000080A06E59CB0E696B900000000080A06EFBC8CE8AFB700000000080A06E5918AE8AF8900000000050A03E6889100000000080A06EFBC8CE6889100000000050A03E4BC9A00000000050A03E5B0BD00000000050A03E58A9B00000000080A06E68F90E4BE9B00000000080A06E8AFA6E7BB8600000000050A03E5928C00000000080A06E6B7B1E585A500000000050A03E79A8400000000050A03E8A7A300000000050A03E7AD9400000000050A03E3808202000000027B7D \ No newline at end of file +000000001362110A0F0A0D52656164696E67206C696E6B730100000F021F8B0800000000000003D55A4F731BC96EBFB32A971CDF09555BA9DDD1CA3239FCCFCB2B59966DAD2D2F234AEBEC562EE00C346C6BA69BDBDD439AFA00EF9C4F91AA7CAD7C8A1C5340CF0CA97F96F4D6FB52B9D844FF01D0C00F68A047BFFDD7AFADFFFCB5F59785F74B3779F9D279F4A53B304BD2A80E1253BCFCE77FFA7949FAF0046632F597FFF9B7296604B9C90CB45AF085FF794B1EA8409583365E5DAA04BD32DAC17A419A5664A162B1B79758424F6E6F6F1FF6F6CA651A083016F6F62C3993AF84460D4A272A25ED0F5AAD63618D696AC9B909B45AC7DA33D3F3291367E448A74C81D213004789D1A963BD5EAB547FEFC152426A45E017C4CBFEBAB3A3D582F38572E09427500E96D6784A3CA530DF80A5A3C3E9F9D1BB43409DCAEEB7C66439C1D4AA15261B989A5C251B993D275B3830973023BB5209012E97F9E6A0328EA72F1E0A728E4DF7641B3D629623536A6F37909894D8108797D902B5721E35FCF0E3B81BC1613E47AD107EF8B1DBEF3399911532EEF06C415625A8618685E1D14E04873A35D6CA8EE180C9CCE4B2A1D713AA5479DE2CF52A2B514EFF0AEDBC4CEB099B91F64A33D9E76DB6A04A8BA190E55C588E87111C96CE5BCC15BE3C3289712F8F1656395FA083139733E71F7E1C74AA65C2A2C78A5F939DA3FA1CCE39EE45F00A17C89B44FE2B5C58543237EC32A9B31C53720BF8E1C7D1A81D89B6989A7A39E5684B27EAF585CC545930190BA5AE890FD296A55AF8C6F198095B34677E6572B51205FB63211D9F980FF08EEC35656615ECD11D0D79D6BB350A1D0F98B678AD72DECBF26DA949F1B945F932CFD0562E64A1A5BD624E6FD01951642063A54E794BDC1F467084C5DCA4B265C40C8FB0206B8CE8DD95798DB5DA47B824F8856C4A323BE2914D81BAB27E65A123D2EC2438BC64BCC8CA4104470B4CE537CB58A85CAC24E3E1A823FE6D7253CC835D58B4298C15BBC78331933A9353F4E28AD887D754C0192D65B4CBA3CE239C05B1FD36B3B4067D30C888256F9695F344C035250BDE5FCE7395305CE27604AF491768AF98EC47F0FAB39A9BD2077B7523786D0AA58380CE96D2BB5C3A111C679BA5E72DED088E739861BEC2D458D1AA1BC1F1EF257A6315E6F0B6549A42CCF05AE74D0DFE388263BF50661962B0DF89E00D5A433BD68EC7A308DEA8CF010063FEAD79EEE5A144C37661B7CF0B2DEA84EDDEED32413A59B0F800AD3E0746353835F9469313B983D13882B7380F88E87598A87C14F3E9DE92B19990E3719F495BA0DEB0F178DFA2C22D4B7CABE616738F56F4E1AD9628E85311BA8AE1783C96812DF2DE9698526ECAA5C066DC9691A299F354601E7C1E473B268D7B4C6DEA13C611BC4325BEECB7C711BC333A2DAD24827EBB277406EFF91F0E065E5DEA0C2D9F86217C9250A5609713D5890E61C3117CA253D3582C8EE0C4E2EF3C35E07576BBAD1BC189B348B9A41B5EE83117FEE3087EC2021B64FD844B495823FE6D6C1AB217F37E4F7A1310D18BE0BDB184FB5BF4994BDEC3AB8C332B23B8EF46F0BE5CA3F2C2A11FC1FB8DCD36D775E61F0F22F8801267A3BEFCF6AB0A829D083ED01CB509C28574C62F240C07BC56CDEB3BA2DB1132A8D6193141C9C293769E240DF6E22E0FFA4559DF32C376041FCA2F54CC4D693331501CC12926684499AE1094D621C1503CC514337489C02866954E31C7B5C4279F8DA94DE588B690A95A9113FD032D4B63E19DFB902A074C58AFB4FABD0C089391D22A5FE91AC7713DA22481C40CDA53FAA212D655F4361A85E80E874265260F00E1BBF1D4684F9A321B7C12D67B47D6A20F1E3F35D6248979F9899C27CBF7EC026DB0A6ACBEE6B80BFAC51CCE1FB150552032CC3ED21203AC864CF805D926FA3B3CB286DF08EB5B9237A8042DF29DCCF0EFF3404662D5785811956779B1B16B94B01E46F07351A17114C114AF9A1A22663227C78644CD458E55DE48FC8CD9D753D458047143A644FA46ECDD8F604AB6E4DF9D08A60B95ABE5526971DCA01B714A0A9AF758A6B1BECCE4B45D595E92F586F3BE09A6FC570C6966CC15C419955A19FDF21437C67B311F87D199292ADF72E23D2B9D53F8F23D5EE3D5A23ACF3082B335EA34045B3B6A2A9F015F2533D40C1AA583CB473C52A60A0E2D06AF8C394066EC73D1348E3B4CDA798565A136C982F25C4E19F3C9668A01011FC8E870C7C63CA6335C1ACB038CF0596E56781534179EB959D51513DFA23353FAC5CEED3B8C60B60C150E7B7266157C407D252A32EDE1BDF2DE49F9F19156AABAC6671E3E9489C286FA45E984B4AF1273E51B9E2A43728A39E3CF4AAB3416218858F29A8B952AAD335E666B4A49D201ABBA56FE3AE094473A119CA35A87BCC74739C7CF6A0B2FC6D739EAEB3A24B95C395F60CD9ECD7D6E4289108FF8B7CEC45B1C7EE7566995621AAA6F33C7AC82CA79A995ABAA5C5E57DA2B12C0B60311EC72842A31EE669D739135D818447071C555A4DC665C345E68C56D0183018E0B65B9441740769AB9F74A67A9E14B8CABE56A90DBA6DAB017B66CE26314C1C5F59C768C318AE017D2745D52B8F99856E4B55C8BA31E5336534D6DB60FAF38774959DBB93B7971303B0833BF524121BA8711FCD6DCF59C3C7F53C51CE7EB1042DD085AADE98281AACB624E969B8AA305EAAC1E80AF355EDDF6735BAFD3AA21627FA4E81182510BDC54DD13BCDA802BE72EB16AAE74061B5302669608BC1196873E470E737DA7FFDA6F7AB6ED9A9B6DDBC13FA4F3ABFB35A89A5DD952A0E2BB438AB7D071D7CDA103A56196637275D06ACDAAA313F0F52DA3D06A3DDD2647B929EFD34FA780C99536EB9CD26C67C3F7EEB689AA43AC69BE30E6EA199DEB4EA77AB3D3BF35717F731B1ADF44A0275390986269344F552F14ADD6A74AA98BB30F8CC6F305F14F5813B88529F31404696C914A7D07DEDCF398F089BECFF3B0986D19DE31D425131648A74BA3B4874B54B9FB07BC16FCA29CF2C0A25DB95C1AEB4558F38E20FE3505BC214AD94A67B399FCE60587790EB38DF354387689153761CED178B1F4AA2030EC2DE6B144E761DC861437EE80B3CC1A16CA710393600EA5AC3EE08D87D313F82BDC62576D04CC4CAB351E1FF48700FF52ED6AB5CE4D8A9B568BFB43FF767AFEF8F66EE7BEED3363F1F1BD837BF74E73DC64D6943A7D9CC368701F870F38777FEFDE9D3D9C0829B39852CAD5D0A5E15E2A215691AB53CCE1E7D263C603A7F8D9D85DB24911CDA49149D8D9BD1DF96820356B2D6EB69418CB228D06CF784D51B0C56B38CDD217E5BC834B636F4C733F8F50EC88DA8E2E6F083C68B5BEFBEE3B38A31C3DA5ADD647D384AE6354EEE6371B1685ECC4C22A2D0F5AAD29A3F0A4D9D86AFD841AE2C13EC4EDB87F8BAB250E06E113D495B5FDAFAEAD57F59EB4AA5BAF3AD19CC71CA540D61A1B0C55A3B9D5DADB3B0B492BDDDB837F7F2109413957122CD081366B981369A8325B7A00AFC8AF79A43789BB5848FC0F279D3616309D9DEF43523A6F0AB20EE8CB92AC229DB0E89C5662B74A07A31B15005A506BDC1E4DDA7DE6238A5D489A15B53E11A025480CBFC4957C51780385D15CBECB81B8A7BF2C2D7715417D777093F370D2E9349C4FC34EA533E17E0897EA8B9C57CEAA8A65CE4506EBCBC75B07E145B349928E2557E6FEAE98F6F8C907581B7BC5A64091DF0058F4BFC57730E9F61ABE27EC7275A92A9F7D625FAD0854331C1434C64382A5234ECD5BCEA196106D4A6B49FB7C239A544A35A70F6ADDD2A33FE975B77AE815375319FADA92F5311BC60D0898FB4D1454F5D10E1C0FE0CE7EB52BE28E2E9DB8D2E5B8E67C2C9CCFD0D30D9CF38939F9FF01BC7727DD2E1C9E0A2BC63EFF7E32E2ABB3EEC0BEA9E7A627B78ED5DB35F19F87D4DEA4DB7E08515C7D6C8D12446DC125F01145144FB2981D45EE8889074F0E88ADB7E9E150E84DDAED6FCBB13BE90FBF2DA895DE3AF75998EE4EFA759CCB585CA7F19929084AC730E38E620B35FE76A57495F41861CB1C3D5FCD3B9FBD1EC27D5DA0D64EDEA2BE512ADE87CE70D2E9FF3988BC900355156E083EC0792ED57F38972B93849CBB2CF37C53F704D3FA8437D51C4C7AFD8710FDC7CC775B4C9D76DEF0778272991B4CA5A4E6F371DAE154236D7915EC41F0234E583CE285CEA43DFA635ED84270EB0FC1E75AE53997FF2B95724355757760D83EEBEA76498CBE54B6A82E975BD9E17E95DBA3491C3F27B06E4486B4502266BD204B70B935B6937DBB16778D7F6FA9309CF4C7BB01D5A903EA9DCA16772EA2E3624E69AA74E6BE765D3CC5639D7DE8F427FD073DD65CDABBAE122FD6B7F34ECC34F152FB4AFABB67384AB987748C9F53713DB160A9F876BE09DF9B6C6F04C1B753B7DDDEE6DDFBEAABAFE4FFC5E3386AEE80DBF84EABC833CEEF78131D38FEC2898E679C9AE7F7681BEF82BAFD9496A0337E00FAA271CF84CAC6BCE02F86509894F27B52565D0D0D26FD3E4C4335148A76A986AA4CB7E6787DFC965C2BBFA8EF49F6D3BD3A88F1ACC973EE004BBF1324CA33AEF9D6B88BED70D8DD7BEB594E4DEB3677B96D732B00D157D4FC7A36FB3BBCCDC7E88CB7D8BCCF73AF31CF5FD01FC85522A47FA3C1786EAABE2FAE846D6FD2EFEE20B5337A1252874F5AF5F5D6BA5ED534D527E18A57C51293CA236E4DD6EDDFB8594277242396BC55B4C2FC668FF2580FF19095FBFBD0694F7A4FAF5D9FDADF569C3BFF8FAA83BB2A3FA790E7E7185B28EF19D8D58B0226D63877A3937C40EF9D80C39BC1D6DF87F6F8D15EC0CC1DD915A5E012F49E2CA5A0C9CB5D13CE5DA992AACB4BB2525EF303CC7E089242650B5F811030CF6FD688FB55ADC346DCA951AB87A690939A4CE33C1FF0816E420E339AB46F445FF374540B550E4A5D17DC736BD68EEA3766815DB97CE1CD8B34185D1221BF043E18034F49363DE972E2F69FDBE256623ACFEAEB6EE734BA93D2986BBCADEA64AC79693B944EA56E25729365D21AEB70CFCD663F3FF6F4F694C0E986DA6AF0CDCC77A7B6BC753FD561CE7F44544579D0AB6428F0FBA8BADC09FA2559C7EFB1B754EEEC3E6A3CAF69A7DD67AB67C77457124DF7FF4C787B3C89475F05E1F79600D728518F7EA79311E516B812143D0D54F7A6F38732447870ED0D76C11C3F76A7FEF7DFFEA379E48677F29585BF05AD250FCE373BDFEBC25FD82EC38BFEE18ABF83CF55AEFC060ABE589370BEE6251C0574986596C4F639AD28AF7329274AAF244186722B382454427EB3E4EB90FFDE69A5D212F3E6490EF08658DCC08AFF5C2AA52569AECEC3270552B6FE06B9144CB3A4094C0F7F7DBB0FB304737A71AEC8EECB172AAA92FF112E31517EB3CF6E5F539EF3FF8C17B7A4843F2B06459B97C74B425FDAF034543A3A80FF05840B634FAC2C00000000000002620001000014201F8B0800000000000003D55BCD6E244772F6B99E222CC1D03487D3EC5F92DD1076CDE15023EEFCB5D91C095A181845574557A7589559CACCEA9E9E95005DBCF0CD7BDB8B0DFBE23DAECF8BB5E1771124C03EF9E0173022B2AABAC92167382BED2E3C074E6756666464C41791115159EFFDC5CEFF7EF9E157F74F1E9E3E7D31FD6C7A7EF2E4AB9F449F9912D012A006A53D65994A497B28AC492DE639D95D28CC8A2C25305BC3C3C9F9BD81815C69D5867AEA028B620DDEC082B20250BB155940BD862F4B725E19EDC02FD0835F10948E2C2CD0C19DD29598656BEE5DC34A6519CC0870664A0FB149944E5BED28EAB6E1D305E9CD54E500DD85D229CC8D054A9477BCB25F90B23C8F76A1C8081D81297D517A40702A2F32355794C092AC534683990B451E0FB3CCC41781C1854A17994A174C931F2F50A7E440534CCEA15D03EA0430491CC426CF4987B5954E548C9E60B5204B50EA302F09E479AF33220DEE421505256DF888197F897991D138FAFCF3CF33D46989298D0BF48B3D6FF6E62AA3686F0FDAED36D04BE53C6F5788B5DBEDE817BF906DBFE8C2D75FBF7D54EF8DA33EFFFCF3E8BC166D8C1A1C916C9DB457968039D9056782920A4B73B2BC65A3B33558C22428A648D053AD07A1DE8667732F7A532E283727D41B18388FD6EF914E6A55F04A0D0A2A51EDC2ACF432E70307E602D77F0967B4B2CABFC6636048CDC11514ABB98A05599604802CF3A36C856BC7A85EAA8400616615CD815E16196AF45B98A836B30BA5CEC8B90DF2AE23EDC2C2CDAE2396A62311BDF408B85C30B0CC9920B3D91A1084F8B6C5D53080DC2494ED426CB2CC7C592A598F57A5206D2C8A6C5D0FF2A662B811621BCE2B430C0C07EE84A14A3F9708AC4DD9887D499657B2342F3306B38694345914CCD47BACCCC580369EF9B726296302CC672A2D955F07B720246B0630CBB6ACC2522A2EE18EA0902DAA36A6D61530F8057CFBCD3FEEEDC1B7DFFCE60A78BFFDE637DF7EF34FF54CC8D15E90756D386FF046DA9596AE6E383C63D613CAC8D386EC15B3357663E3220A96602D8720E7669F4C8E47328AAEACD78EA25E1B1E0469654AE8E678C1308339C6DEB5A3A8DF86D339604059CE9E869D0E2B466940F673A452DDE0A37170965C6174C2A3C4AEEA01ED281A8897C9D133195B8D74C44359548959E976140D2BE7CA26C55B63D7AB69B5E5165DB3D836981AA49E3E009C7B76090BA6ADBCC20C66185F782573337541E0CC18D8D1146BBF309A1D93D0FFFA6BF13ED1FE354C6C31206E1EF5464FC125555C894D6DB326C8613FFA26C60474969CAFED2627BF3009EC419CA17341661BACCF28333A65F7B6D9D125BFED4C4E7BC62FC806D73D2F752C60383A3D5EA0FF58396FECFA4E0B7E110180385EFE7F4B127577259303418423B4F182192D331F7C48E5BF9260B76E61CA2C6970808E3DA0785CA5E7A27E664239B8D066A5D966D7A66CC319CD2D2A0D736B7270B866A9BE770A9843A97196110F9C59B3725489CF93D5E4DFDBE55189D11F7858E09200E3581CA4B93ACC5878EFF4836D72B5DFB584D93DAF72629CB9DAD7B6DF8349AD4F71D331BB780E2032A51902CCA89C1A972522A80F7612676522F0BE11EF1024D28E3EFCEAE4E9834DFC13D511D1F3E9C9D9573F89265BC89AB30B5EF1D242CB795BC6219E61CBCC32F6579716140D58CA68895AFC6D2E41905DB7E1A9014DECC20D60CC0AC92849E93ADA89B2147B3ECFAE6CA71D7D1897CE9BFCC5F6F89F44671B4770BC509A09CED690D01CCBCC471FEE5D3BA99243B5EBAB52781F4E75517A1745EFBFFF3E4C8C67F72627D159BDBB4F6956E9E32CE8239258F20650865D6E50C968FE716019E0760B60BE332EDFABCE1355B9849AD2151CB6DF8C5E06A8044EA4634A3660C69959BE06E95B2138BA17FE4511EB80577B7EF6780C0BEF0B37DEDB63BF56BAB62948A36AC726DF6BC6C546B326C7D184DD7766520351042FF9CF43F240392A39CE24CE0968E4C38F9664E15941FAE8147676624B2C9D9D9D5DD8D9A944B5B3C3D2DDD9B1E44CB694B62414B14A4833CB27421A93C492736388A21316263C3B9F70E38C1CC782CFCE27A0F418C0516C74E298AF072A61C55A8A492D83EA9F9D4F7EBA35238A829A64874A823C4FB10F098BA5E3A3C9F9F1C747E2F479F64363D28C6062D512E3354C4CA6E210D99F93CD1DDBF494EC52C5D541DEAE84E3E9A5AF8FE7DBCBE82D623936A5F61C759984581047F374815A398F1AEEDC1DF55B7094CD502B843B77FBC3213753B2D2EC75F9694E5671E03EC5DC706FB705473A31D6CA8C837D6EA62693098381B44A9565CD50AFD21265F7F7D1CECAA47E603926559A9B439E6673AAB8389066391392A383161C95CE5BCC14EE1D9BD8B8BDE38555CEE7E8E0D4654CF9CEDDFD6E354C480C98F1576467A8BE08FB1C0D5A701F17C89364FDFBB8106770E7EEE8A0CF4D9D6698905BC09DBB87879D96708B89A9875386B674C2DE509AA92A736EF6A4A55E116FA42343B5D0EDF546DCB079B3E7FB26534B61703892A6E31DF3063E26FB8A52B30CF2E81F1EF053EF5628EDDE3EB72DBE5219CFE5F56DA949F1BE85F9324BD1562AE4454B7BC1943E426784917DE92B75C2537AC383161C633E33894C396482C798933546F8EECB738D35DBC758107C423621797AC83DEB1C7525FD4A42C7A45949703467BCC8C8FD161C2F3091DFBCC68263A73B7787D21FB67AC8BF4D66F259900B2F6D726345EEBDFD1137752ABB18F4AAC62E3CA01CCEA890DE3EF73A8F7016961D7698A435E883400E79E57551294F16784572A614E52C5331C3A5D769C103D2EC07B9396CC1832F14170A82BCFA2D7860B822115712A95B7A9B4AB70527E9BAF03CA5D382930CA6982D313156B8EAB7E0E4CB12BDB11C2A3E2C95A660333CD6795383BFD78213BF50A6083638ECB6E023B486B6A4DD1B1DB6E023F54500C0887F6B7EB67724D6B019D81FF2408B3A66B9F7FBDC201D2F78F900AD211B46D53931D95A939375F70F472D7888B3808841971B958E7ABCBB87646C2ACDD168C84D9BF38174E7EE80E72D2ADCF28A0FD5CC62E6D10A3F3CD512057EAA86AE6CB8371A49C706790F4B4C28336521B01975A4276F9E79CA310B3AEFB5B644DA1B706B5DEFB0D7828F51892E879D510B3E363A29AD3882616720ED141EF11F36061E5DEA948B3177EE32844F63AA18ECB3A33AD5C16CD8824F75621A89F55A706AF14B7EB4CFE3EC665ABF05A7CE2265E26E78A0C74CE88F5AF033CCB141D6CFB0108775C8BF8D4D82F762DA8F48AF0322062D78642CE1EE067D66CE73789471666904F7FD163C2A57A8BC5018B6E0D1DAA6EB57B5E71FEDB7E0318A9D1D0EE5B75F5610ECB6E031CD509BB0B8349DF10B31C37D1EAB66F519D1EF4A33B0D63DE406C50B4FDA79123738E8F5B9D32FCAFA9439E8B4E071F992F299296D2A02EAB5E009C6688499BE3428A94D82A1F804134CD1C502A31EB3F404335C897DF2DEB8B5AE14D19166A296E484FFD096A13DA19DF9E02AF7B961BDD2EACB32204C7A4AAB7CC56BAFD7AB7B9438901E83F609BD5431F32A7C1B8DD2E81F1C482B355900089F8D4F241EA2D4069D84F1DE91B5E883C69F186BE2D8EC7DCA8524CBE7EC026D90A68C7EC57617F8EBB1393FC55C5586C8307B4A0506581D708373C4C6FABBDCB3829F13D6A7244F50315AE43399E13FE48E9444AABD83AA516996071BBB4231EB83163CCB2B341EB66082174D0CD1E366C68558AD5073906315E7A3C255871F6ACCC37207DC92D5D722EF610B26644BFEDD6DC164A13255149C6030B3FD16BBA4C0F980D734D697A9ECB62FC34BB2DEB0DF3741947F83C1CD8C388238A3522BA3F79EE0DA782FE263333A3379A55B76BC67A5730AF71EE12BBC5854FB3968C1D90A75128CADD36A229F7D3E4AA6A819344A07951F724F992838B218B432620399B2CE85D35EAFCB4D3BABB02CAD75BCA02C935DF6786753C58080C7647438637BDCA7532C8CE50E46F834334BBC089C0BCDCC2CEB88894FD1A929FD62EBF43D68C1B408110E6B726A153C467D212C72DBC323E57D28553CA5A5AA8EF1A987C765ACB0697DA274CC15B0E0982BDDF0A33238A71E7BFC696995C63C1811AFBCE260A572EB8C97E98A121277C0ACAE947F1570CA3DDD169CA35A05BFC75B39C72FD4065E8CAF73D4AF6A93E470E57C81357916F7B9092142EF907FEB54B4C5E6776E9556092621FA36334C2BA89C975AB92ACAE571A5BD20016C2734825C8E51C5C65D8E739EA70D36F65BF0FC82A34839CD38687CAE15A7050C0638C99595AA3503B2DB3C7BA4749A183EC4385AAE3AA7BE1AC9E36CD9D8C7610B9EBF9AD196300E5BF009697A555238F9B8ADC86B39160F07DCB2A96A62B35DB8CFBE4BC2DAEEEB0F9FB7A7EDF0E433CA2958F7410B7EDE9CF5EC3C7FAEF219CE56C184FA2D88A2C98281AACB7C4696938A63296C561DF0A6C4ABDF79D7D4EB499510B13E12F40841A839AEABEC09EEAFC19533175B35E3949ED3614C2D519D9F1F792EBA55AEE952FEB5DBE46C9B3197D3B6F69F24F3ABF3B5E645074FC991CB015A82B790716F6AB74AC334C3F8A21D45D36AEB047C7C4B2F44D1ED65729C99F23AFEB8B4B255466A267CE0AE8AA8DAC48A660B632EDE2173DDCA542F67FA571E5C9FDC86C4B77A85C68FB89E5E18CD8FAA0A85D42F8429A95FC8CB13FE092BAACB4882349648C53E5775AE29267C4A1FF0FB061ECCB20C750C5515E748278551DAC31C55E6FE04D5824F94531E7869571685B15E166BEA08A25F93C3475C0E3416CEA653F9CD038EB20CA66BE72977AC122B6AC28CADF17921052AB3ACCADA053A0FA30E24B8766DF6322B5848A5995F514129A3DB3CF168720A3F852BE4AA8980A989A2D1A83D3C00F8AB6A56149D9B04D751C4F9A17F38397FFBF47EF7BAE95363F1ED73F7AF9D3BC9709D5A53EAE4ED140EF7AFA3F01867EE0F9DBB35871D21A516134A381A92229C8E8959E4E8143378567A4CB9E3097E61EC76B37111CD43230F616BF6A6E7A901AEF3899A2DC5C6F292A67A899AA0608BC7B09B95F721E1EDC8F663CEE711F2ADA536BDC5A505DBA1B47B46197A4AA2E8A9694CD785B7541BFF66C3A0E09D78B18ACB76144D1885A7CDC428FA196AE8EDEF42AFD31B5EA16A898D41E8047665ECF08D63EB51835B8DEAD7A34E35FB31470990B5C60641D5688EA29D9DB3E0B4929D1DF8DB7B5595D795E175BD36ABF0CABEF26C491BEE935F71CF60DCEB632EF67F30EE763087C9F47C1742859DACE337CA645528F54AA5DC6F7830BA610120829AE3CEE1B833643AC2D87371B3C2D6A72465722EDA2A5DCACB4703B9D11CBE57EFC5D6302F2D6715817DD7BE4CF960DCED36949F84994AA742FD08E6EAE5E67A025F92E02083F9E5EDADC2E27933499C4E53F2BEB24C6774EB0DAC8CBD6051A0ACDF0058F8BF42777FDC1F34744F59E5728DA3A62DC57ED57407068DF11063C91740E65B94432C21DC94D692E6F72BCC49C554B3FBC0D6153E86E3417FC3875E723295CA6BF14BDB6C08372060EA975150C5475B706CC36BF3D5F612AFF1D2ED55BC9CD4944F84F219BFCEDCC639EF989DFF0FC07B7FDCEFC3D11321C5D8E7DFB7467CB5D72DD837F1DCE4F4CAB606DB22FEE3217530EE776E4214471F1BA184A536E012F808238A1FF2325B8CBCB64C6FFFD606B1D136DD6C0A8371A7F3E352EC8F87073F2EA895DE28F79D30DD1F0F6B3B97BE5EEDC6A7260FB76F4246B1811ABFBB52BA727A8CB02243CF47F3D66BAF9B705F07A8B59237A86F98EAED42F760DC1DFE7110F95C365445B8C1F8A07E2319F6E54A7985392FE5825CC80926F50E2FB3B93F1E0C6F42F40F13DFD5656AB7F311BF27288BCC602221757D198F5D8DA4E595B18785DFA284C55BB4D01D770E7F98163610DCE843F02977779A1B61BABE4865583EABEA74898D9E2B9B5787CB15EF703DCB9DC371AFF72E8675C93224859265C26DC2F946D8E10AC8B6C45DA3DF2B2C1C8C87A36D83EAD606F5B14A17AF1D4427F98C12BE71E9DE745CDC4663DD5DE80EC7C31B35D61CDADBAA122DD6A7F396CD34F652EB4AF2BB7750947237F1D87B9788EB96014B45B7FBA3D0BD4CF69211FC78EC763A1BBF7B5D7CF506FFBF783B8E9A33E02ABE93CAF28CF35BDA44078EDF70A2E3274ECDB26BB8ED6D83BA739B94A03BBA01FAC2F1C084C8C6DCE33786E1CADE352EAB8E86F6C7C3214C423414827689862A4FC797A46F714ACABDC6EA9C643D5DCB8308CF1AB9F4694ABF6524CA33AEF9D4781DDB61B3DBE7D63B2935A9D3DC6293E65600A237B0F9666FF607689BB7D11D6DB0799DE61E6096DDA31FE0AB6491E1A504E35D5DF57576256407E3617F0BA9DDC35B21F5E056A3DE9C5AD7A39AA4FA341CF12A2F30AE34C2D7F3DDEEA593256447D263C95B454BCC2EE7286FCB216E92F27017BA9DF1E0F6B1EB6DF3DB8A72F7FF5174F03ACBEF12C8CBEDB35C79BEC055571430B6C6B94B99E40D7C6F191C5E36B6E12E74466FCD05CCCC915D52022E46EFE573104D5ECE9AB0EF8A9544CDE5AA9BE7DDA0DB0D4692F347151508E5E2E4A51871B78A75E4A2FA2646AD0A4DC127359EC679DEE00DD9846CE670DCB9647D4DE9A85E54B9EBAF14AE6826B02B8B7BDEDC4B82D09B2B8B37DAC06D9CCD40B29C5EE78F9BE256CB74DF29AFBBEAD3E83597C6547B9BA84EFA9A4ADB51B86C59A512994953498D7538E7A6D3676F2BBDDDC670FA21B6DAFFD1C4F75A6C79E57CAACD9C2F1155561EF82AC3B722B6BAD55AF15EF07745EE922AFA12B76D1535DE2D69A7EDB2D53BDB745F1C4DFFCFB6786734EE1DBE11841FF017312B14AB47BF95C9545F942D0545B703D5B5EEFC260F110AAE83FD6D30F7DE76A67EFBCB5F35456EA8EEF3C364F359DCE67DDD545E6D15A1A27FB4E4F7E0339529BFE68F0CAC8AC3FE9A4A380AE8304D2D89EC335A5256FB5276945E89830CE15650488884FCBAE0E390EF3B2D555262D694E4002F2D8B6B58F275A9840AD2897C62A1ABEFE5AA779045F87245F1DBE2C9D1670F77611A6346F7CE157FFDC79545AA9CFF3116182BBFDE65B5AF28CBF87FB9995F7D1C557D6553571EE784BEB4A134543A7E1DF5E2C58B1711FF7BE3F5F328FAEE3FFEE5FB7FF8B7EF7EFFAFFFF5DBFFFCEF5FFFF6AF6FBEDC0CDFFFEAD7FFF3EFFFFCDDEFBFF9EE777FFFFD2FFFEEFBDFFEEE32F1FF03DD0AD4E6F8380000000000000000000000050A03E6889100000000080A06E697A0E6B39500000000080A06E79BB4E68EA500000000080A06E8AEBFE997AE00000000080A06E7BD91E7AB9900000000080A06EFBC8CE4BD8600000000050A03E6889100000000080A06E58FAFE4BBA500000000080A06E5918AE8AF8900000000050A03E4BDA000000000080A06E585B3E4BA8E00000000050A03E8AFA500000000080A06E7BD91E7AB9900000000080A06E79A84E4B88000000000050A03E4BA9B00000000080A06E4BFA1E681AF00000000050A03E3808200000000080A06E6A0B9E68DAE00000000050A03E6889100000000050A03E6898000000000050A03E79FA500000000050A03EFBC8C00000000060A044F70656E00000000040A02414900000000050A03E79A8400000000080A06E78AB6E6808100000000080A06E9A1B5E99DA200000000050A03EFBC8800000000030A015B00000000080A0673746174757300000000070A052E6F70656E00000000040A02616900000000060A042E636F6D00000000040A025D2800000000070A05687474707300000000050A033A2F2F00000000080A0673746174757300000000070A052E6F70656E00000000040A02616900000000060A042E636F6D00000000040A022F2900000000050A03EFBC8900000000080A06E68F90E4BE9B00000000050A03E4BA8600000000080A06E69C89E585B300000000060A044F70656E00000000040A02414900000000080A06E69C8DE58AA100000000050A03E79A8400000000080A06E8BF90E8A18C00000000080A06E78AB6E6808100000000050A03E5928C00000000080A06E7BBB4E68AA400000000080A06E4BFA1E681AF00000000050A03E3808200000000080A06E794A8E688B700000000080A06E58FAFE4BBA500000000080A06E69FA5E79C8B00000000080A06E69C8DE58AA100000000050A03E79A8400000000080A06E6ADA3E5B8B800000000080A06E8BF90E8A18C00000000080A06E697B6E997B400000000050A03E3808100000000050A03E6958500000000050A03E99A9C00000000080A06E68AA5E5918A00000000080A06E4BBA5E58F8A00000000080A06E7BBB4E68AA400000000080A06E9809AE79FA500000000050A03E7AD8900000000080A06E58685E5AEB9000000000B0A09E38082E6ADA4E5A49600000000050A03EFBC8C00000000080A06E794A8E688B700000000050A03E8BF9800000000080A06E58FAFE4BBA500000000050A03E8AEA200000000050A03E9988500000000080A06E794B5E5AD9000000000080A06E982AEE4BBB600000000050A03E6889600000000080A06E79FADE4BFA100000000080A06E9809AE79FA500000000080A06EFBC8CE4BBA500000000050A03E4BEBF00000000050A03E59CA800000000080A06E58F91E7949F00000000080A06E4BA8BE4BBB600000000050A03E697B600000000080A06E58F8AE697B600000000080A06E88EB7E58F9600000000080A06E69BB4E696B000000000050A03E3808202000000027B7D \ No newline at end of file diff --git a/worker.js b/worker.js index 9d8b6f5..bf733a3 100644 --- a/worker.js +++ b/worker.js @@ -11,6 +11,7 @@ async function handleRequest(request) { const allowedHosts = ["api2.cursor.sh", "www.cursor.com"]; const allowedPaths = [ "/aiserver.v1.AiService/StreamChat", + "/aiserver.v1.AiService/StreamChatWeb", "/auth/full_stripe_profile", "/api/usage", "/api/auth/me"