From 061156fb79a8811b873acafadab3aaa1a5132494 Mon Sep 17 00:00:00 2001 From: wisdgod Date: Tue, 14 Jan 2025 09:13:13 +0800 Subject: [PATCH] v0.1.3-rc.3 --- .env.example | 8 +- .github/workflows/build-linux.yml | 2 +- .github/workflows/docker.yml | 27 +- Cargo.lock | 231 +++--- Cargo.toml | 17 +- Dockerfile | 2 + build.rs | 4 +- src/app/config.rs | 6 +- src/app/constant.rs | 16 +- src/app/lazy.rs | 50 +- src/app/model.rs | 37 +- src/chat/adapter.rs | 34 +- src/chat/aiserver/v1/lite.proto | 1156 +++++++++++++++++++++++++++++ src/chat/error.rs | 3 +- src/chat/route.rs | 17 +- src/chat/route/api.rs | 26 + src/chat/route/health.rs | 3 +- src/chat/route/logs.rs | 43 +- src/chat/route/profile.rs | 34 + src/chat/route/token.rs | 88 +-- src/chat/route/usage.rs | 48 -- src/chat/service.rs | 336 +++++++-- src/common/client.rs | 227 +++++- src/common/models.rs | 2 +- src/common/models/usage.rs | 25 - src/common/models/userinfo.rs | 77 ++ src/common/utils.rs | 149 ++-- src/common/utils/checksum.rs | 219 +++++- src/common/utils/tokens.rs | 92 +-- src/main.rs | 18 +- static/api.html | 381 ++++++++++ static/config.html | 12 +- static/logs.html | 533 +++++++++++-- static/readme.html | 436 ++++++++--- static/shared.js | 12 +- static/tokeninfo.html | 1 + 36 files changed, 3585 insertions(+), 787 deletions(-) create mode 100644 src/chat/aiserver/v1/lite.proto create mode 100644 src/chat/route/api.rs create mode 100644 src/chat/route/profile.rs delete mode 100644 src/chat/route/usage.rs delete mode 100644 src/common/models/usage.rs create mode 100644 src/common/models/userinfo.rs create mode 100644 static/api.html diff --git a/.env.example b/.env.example index 472c252..2e07dce 100644 --- a/.env.example +++ b/.env.example @@ -38,5 +38,9 @@ VISION_ABILITY=base64 # 默认提示词 DEFAULT_INSTRUCTIONS="Respond in Chinese by default" -# 反向代理服务器主机名 -CURSOR_API2_HOST= +# 反向代理服务器主机名,你猜怎么用 +REVERSE_PROXY_HOST= + +# 请求体大小限制(单位为MB) +# 默认为2MB (2,097,152 字节) +REQUEST_BODY_LIMIT_MB=2 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index a4c2b57..597ddb6 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -25,7 +25,7 @@ jobs: sudo apt-get install -y protobuf-compiler pkg-config libssl-dev nodejs npm - name: Build binary - run: cargo build --release --target ${{ matrix.target }} + run: RUSTFLAGS="-C link-arg=-s" cargo build --release --target ${{ matrix.target }} - name: Upload artifact uses: actions/upload-artifact@v4.5.0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d755a17..3684641 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,6 +8,11 @@ on: required: true type: boolean default: false + upload_artifacts: + description: '是否上传构建产物' + required: true + type: boolean + default: true push: tags: - 'v*' @@ -54,7 +59,9 @@ jobs: network=host - name: Build and push Docker image - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_RECORD_UPLOAD: false with: context: . push: true @@ -62,4 +69,20 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max + outputs: type=local,dest=./dist,enable=${{ github.event_name == 'workflow_dispatch' && inputs.upload_artifacts }} + + - name: Prepare artifacts + if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts + run: | + mkdir -p artifacts/amd64 artifacts/arm64 + cp dist/linux_amd64/app/cursor-api artifacts/amd64/ + cp dist/linux_arm64/app/cursor-api artifacts/arm64/ + + - name: Upload artifacts + if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts + uses: actions/upload-artifact@v4.6.0 + with: + name: cursor-api-binaries + path: artifacts/ + retention-days: 7 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6eaceb0..06d07d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -38,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -65,6 +68,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ + "brotli", "flate2", "futures-core", "memchr", @@ -74,9 +78,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.84" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -179,9 +183,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "block-buffer" @@ -192,6 +196,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -224,9 +249,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.7" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ "shlex", ] @@ -245,10 +270,8 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-targets", ] @@ -304,9 +327,8 @@ dependencies = [ [[package]] name = "cursor-api" -version = "0.1.3" +version = "0.1.3-rc.3" dependencies = [ - "anyhow", "axum", "base64", "bytes", @@ -317,14 +339,12 @@ dependencies = [ "gif", "hex", "image", - "lazy_static", "paste", "prost", "prost-build", "rand", "regex", "reqwest", - "rusqlite", "serde", "serde_json", "sha2", @@ -394,18 +414,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.3.0" @@ -600,30 +608,12 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "heck" version = "0.5.0" @@ -936,9 +926,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" dependencies = [ "byteorder-lite", "quick-error", @@ -951,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown", ] [[package]] @@ -977,42 +967,25 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -1127,7 +1100,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "foreign-types", "libc", @@ -1189,9 +1162,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1229,9 +1202,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", "syn", @@ -1239,9 +1212,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1434,20 +1407,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags 2.6.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1456,11 +1415,11 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", @@ -1469,9 +1428,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "once_cell", "rustls-pki-types", @@ -1533,7 +1492,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "core-foundation", "core-foundation-sys", "libc", @@ -1542,9 +1501,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -1572,9 +1531,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -1672,9 +1631,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -1720,7 +1679,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "core-foundation", "system-configuration-sys", ] @@ -1761,9 +1720,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -1777,9 +1736,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -1852,9 +1811,11 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "bytes", "http", + "http-body", + "http-body-util", "pin-project-lite", "tower-layer", "tower-service", @@ -1947,9 +1908,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "getrandom", ] @@ -1983,20 +1944,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -2008,9 +1970,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -2021,9 +1983,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2031,9 +1993,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -2044,9 +2006,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -2063,9 +2028,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 9736b19..bb89bd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cursor-api" -version = "0.1.3" +version = "0.1.3-rc.3" edition = "2021" authors = ["wisdgod "] description = "OpenAI format compatibility layer for the Cursor API" @@ -12,34 +12,31 @@ sha2 = { version = "0.10.8", default-features = false } serde_json = "1.0.134" [dependencies] -anyhow = "1.0.95" axum = { version = "0.7.9", 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", features = ["serde"] } +chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde"] } 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"] } -lazy_static = "1.5.0" 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", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } -rusqlite = { version = "0.32.1", features = ["bundled"], optional = true } +reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } -serde_json = "1.0.134" +serde_json = "1.0.135" sha2 = { version = "0.10.8", default-features = false } sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } -tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } +tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } tokio-stream = { version = "0.1.17", features = ["time"] } -tower-http = { version = "0.6.2", features = ["cors"] } +tower-http = { version = "0.6.2", features = ["cors", "limit"] } urlencoding = "2.1.3" -uuid = { version = "1.11.0", features = ["v4"] } +uuid = { version = "1.11.1", features = ["v4"] } [profile.release] lto = true diff --git a/Dockerfile b/Dockerfile index 047ef11..75a927c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +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" RUN cargo build --release && \ cp target/release/cursor-api /app/cursor-api @@ -17,6 +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" RUN cargo build --release && \ cp target/release/cursor-api /app/cursor-api diff --git a/build.rs b/build.rs index 7477417..a6557a5 100644 --- a/build.rs +++ b/build.rs @@ -139,7 +139,7 @@ fn minify_assets() -> Result<()> { fn main() -> Result<()> { // Proto 文件处理 - println!("cargo:rerun-if-changed=src/chat/aiserver/v1/aiserver.proto"); + println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto"); let mut config = prost_build::Config::new(); // config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); // config.type_attribute( @@ -147,7 +147,7 @@ fn main() -> Result<()> { // "#[derive(serde::Serialize, serde::Deserialize)]" // ); config - .compile_protos(&["src/chat/aiserver/v1/aiserver.proto"], &["src/chat/aiserver/v1/"]) + .compile_protos(&["src/chat/aiserver/v1/lite.proto"], &["src/chat/aiserver/v1/"]) .unwrap(); // 静态资源文件处理 diff --git a/src/app/config.rs b/src/app/config.rs index 6b20554..f990f96 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,19 +1,16 @@ use super::{ constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, - model::{AppConfig, AppState}, + model::AppConfig, }; use crate::common::models::{ config::{ConfigData, ConfigUpdateRequest}, ApiStatus, ErrorResponse, NormalResponse, }; use axum::{ - extract::State, http::{header::AUTHORIZATION, HeaderMap, StatusCode}, Json, }; -use std::sync::Arc; -use tokio::sync::Mutex; // 定义处理更新操作的宏 macro_rules! handle_update { @@ -54,7 +51,6 @@ macro_rules! handle_reset { } pub async fn handle_config_update( - State(_state): State>>, headers: HeaderMap, Json(request): Json, ) -> Result>, (StatusCode, Json)> { diff --git a/src/app/constant.rs b/src/app/constant.rs index 4b291b9..eddd6df 100644 --- a/src/app/constant.rs +++ b/src/app/constant.rs @@ -15,7 +15,8 @@ def_pub_const!(EMPTY_STRING, ""); def_pub_const!(ROUTE_ROOT_PATH, "/"); def_pub_const!(ROUTE_HEALTH_PATH, "/health"); def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum"); -def_pub_const!(ROUTE_GET_USER_INFO_PATH, "/get-user-info"); +def_pub_const!(ROUTE_GET_USER_INFO_PATH, "/get-userinfo"); +def_pub_const!(ROUTE_API_PATH, "/api"); def_pub_const!(ROUTE_LOGS_PATH, "/logs"); def_pub_const!(ROUTE_CONFIG_PATH, "/config"); def_pub_const!(ROUTE_TOKENINFO_PATH, "/tokeninfo"); @@ -32,6 +33,7 @@ def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration"); def_pub_const!(DEFAULT_TOKEN_FILE_NAME, ".token"); def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".token-list"); +def_pub_const!(STATUS_PENDING, "pending"); def_pub_const!(STATUS_SUCCESS, "success"); def_pub_const!(STATUS_FAILED, "failed"); @@ -40,7 +42,7 @@ def_pub_const!(HEADER_NAME_GHOST_MODE, "x-ghost-mode"); def_pub_const!(TRUE, "true"); def_pub_const!(FALSE, "false"); -def_pub_const!(CONTENT_TYPE_PROTO, "application/proto"); +// def_pub_const!(CONTENT_TYPE_PROTO, "application/proto"); def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto"); def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8"); def_pub_const!(CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, "text/plain;charset=utf-8"); @@ -49,14 +51,20 @@ def_pub_const!(CONTENT_TYPE_TEXT_JS_WITH_UTF8, "text/javascript;charset=utf-8"); def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer "); +def_pub_const!(CURSOR_API2_HOST, "api2.cursor.sh"); +def_pub_const!(CURSOR_HOST, "www.cursor.com"); +def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings"); + def_pub_const!(OBJECT_CHAT_COMPLETION, "chat.completion"); def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk"); -def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); -def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); +// def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); +// def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); def_pub_const!(FINISH_REASON_STOP, "stop"); def_pub_const!(ERR_UPDATE_CONFIG, "无法更新配置"); def_pub_const!(ERR_RESET_CONFIG, "无法重置配置"); def_pub_const!(ERR_INVALID_PATH, "无效的路径"); + +// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good"); diff --git a/src/app/lazy.rs b/src/app/lazy.rs index 74ab67e..33a05f9 100644 --- a/src/app/lazy.rs +++ b/src/app/lazy.rs @@ -1,5 +1,8 @@ use crate::{ - app::constant::{DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING}, + app::constant::{ + CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME, + EMPTY_STRING, + }, common::utils::parse_string_from_env, }; use std::sync::LazyLock; @@ -31,10 +34,7 @@ def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING); def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING); def_pub_static!(TOKEN_FILE, env: "TOKEN_FILE", default: DEFAULT_TOKEN_FILE_NAME); def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME); -def_pub_static!( - ROUTE_MODELS_PATH, - format!("{}/v1/models", *ROUTE_PREFIX) -); +def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX)); def_pub_static!( ROUTE_CHAT_PATH, format!("{}/v1/chat/completions", *ROUTE_PREFIX) @@ -49,10 +49,44 @@ pub fn get_start_time() -> chrono::DateTime { def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Respond in Chinese by default"); -def_pub_static!(CURSOR_API2_HOST, env: "REVERSE_PROXY_HOST", default: "api2.cursor.sh"); +def_pub_static!(REVERSE_PROXY_HOST, env: "REVERSE_PROXY_HOST", default: ""); -pub static CURSOR_API2_BASE_URL: LazyLock = LazyLock::new(|| { - format!("https://{}/aiserver.v1.AiService/", *CURSOR_API2_HOST) +pub static USE_PROXY: LazyLock = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty()); + +pub static CURSOR_API2_CHAT_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_API2_HOST + }; + format!("https://{}/aiserver.v1.AiService/StreamChat", host) +}); + +pub static CURSOR_API2_STRIPE_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_API2_HOST + }; + format!("https://{}/auth/full_stripe_profile", host) +}); + +pub static CURSOR_USAGE_API_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_HOST + }; + format!("https://{}/api/usage", host) +}); + +pub static CURSOR_USER_API_URL: LazyLock = LazyLock::new(|| { + let host = if *USE_PROXY { + &*REVERSE_PROXY_HOST + } else { + CURSOR_HOST + }; + format!("https://{}/api/auth/me", host) }); // pub static DEBUG: LazyLock = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); diff --git a/src/app/model.rs b/src/app/model.rs index 9f2a3c9..ef82e60 100644 --- a/src/app/model.rs +++ b/src/app/model.rs @@ -2,14 +2,13 @@ use crate::{ app::constant::{ ERR_INVALID_PATH, ERR_RESET_CONFIG, ERR_UPDATE_CONFIG, ROUTE_ABOUT_PATH, ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_SHARED_JS_PATH, - ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENINFO_PATH, + ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENINFO_PATH, ROUTE_API_PATH, }, - common::models::usage::UserUsageInfo, + common::models::userinfo::TokenProfile, }; use crate::chat::model::Message; -use lazy_static::lazy_static; +use std::sync::{LazyLock, RwLock}; use serde::{Deserialize, Serialize}; -use std::sync::RwLock; // 页面内容类型枚举 #[derive(Clone, Serialize, Deserialize)] @@ -81,20 +80,22 @@ pub struct Pages { pub shared_js_content: PageContent, pub about_content: PageContent, pub readme_content: PageContent, + pub api_content: PageContent, } // 运行时状态 pub struct AppState { pub total_requests: u64, pub active_requests: u64, + pub error_requests: u64, pub request_logs: Vec, pub token_infos: Vec, } // 全局配置实例 -lazy_static! { - pub static ref APP_CONFIG: RwLock = RwLock::new(AppConfig::default()); -} +pub static APP_CONFIG: LazyLock> = LazyLock::new(|| { + RwLock::new(AppConfig::default()) +}); impl Default for AppConfig { fn default() -> Self { @@ -105,7 +106,7 @@ impl Default for AppConfig { slow_pool: false, allow_claude: false, pages: Pages::default(), - usage_check: UsageCheck::default(), + usage_check: UsageCheck::Default, } } } @@ -184,6 +185,7 @@ impl AppConfig { ROUTE_SHARED_JS_PATH => config.pages.shared_js_content.clone(), ROUTE_ABOUT_PATH => config.pages.about_content.clone(), ROUTE_README_PATH => config.pages.readme_content.clone(), + ROUTE_API_PATH => config.pages.api_content.clone(), _ => PageContent::default(), }) } @@ -215,6 +217,7 @@ impl AppConfig { ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content, ROUTE_ABOUT_PATH => config.pages.about_content = content, ROUTE_README_PATH => config.pages.readme_content = content, + ROUTE_API_PATH => config.pages.api_content = content, _ => return Err(ERR_INVALID_PATH), } Ok(()) @@ -254,6 +257,7 @@ impl AppConfig { ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(), ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(), ROUTE_README_PATH => config.pages.readme_content = PageContent::default(), + ROUTE_API_PATH => config.pages.api_content = PageContent::default(), _ => return Err(ERR_INVALID_PATH), } Ok(()) @@ -277,6 +281,7 @@ impl AppState { Self { total_requests: 0, active_requests: 0, + error_requests: 0, request_logs: Vec::new(), token_infos, } @@ -292,17 +297,19 @@ pub struct RequestLog { pub token_info: TokenInfo, #[serde(skip_serializing_if = "Option::is_none")] pub prompt: Option, + pub timing: TimingInfo, pub stream: bool, pub status: &'static str, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } -// pub struct PromptList(Option); - -// impl PromptList { -// pub fn to_vec(&self) -> Vec<> -// } +#[derive(Serialize, Clone)] +pub struct TimingInfo { + pub total: f64, // 总用时(秒) + #[serde(skip_serializing_if = "Option::is_none")] + pub first: Option, // 首字时间(秒) +} // 聊天请求 #[derive(Deserialize)] @@ -319,9 +326,7 @@ pub struct TokenInfo { pub token: String, pub checksum: String, #[serde(skip_serializing_if = "Option::is_none")] - pub alias: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub usage: Option, + pub profile: Option, } // TokenUpdateRequest 结构体 diff --git a/src/chat/adapter.rs b/src/chat/adapter.rs index f57f0ea..17e53c1 100644 --- a/src/chat/adapter.rs +++ b/src/chat/adapter.rs @@ -5,14 +5,13 @@ use uuid::Uuid; use crate::app::{ constant::EMPTY_STRING, - model::{AppConfig, VisionAbility}, lazy::DEFAULT_INSTRUCTIONS, + model::{AppConfig, VisionAbility}, }; use super::{ aiserver::v1::{ - conversation_message, image_proto, ConversationMessage, ExplicitContext, GetChatRequest, - ImageProto, ModelDetails, + conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails }, constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS}, model::{Message, MessageContent, Role}, @@ -200,7 +199,7 @@ async fn process_chat_inputs(inputs: Vec) -> (String, Vec, model_name: &str, -) -> Result, Box> { +) -> Result, Box> { // 在进入异步操作前获取并释放锁 let enable_slow_pool = { if AppConfig::get_slow_pool() { @@ -361,7 +360,12 @@ pub async fn encode_chat_message( model_name: Some(model_name.to_string()), api_key: None, enable_ghost_mode: None, - azure_state: None, + azure_state: Some(AzureState { + api_key: String::new(), + base_url: String::new(), + deployment: String::new(), + use_azure: false, + }), enable_slow_pool, openai_api_base_url: None, }), @@ -370,27 +374,23 @@ pub async fn encode_chat_message( linter_errors: None, summary: None, summary_up_until_index: None, - allow_long_file_scan: None, - is_bash: None, + allow_long_file_scan: Some(false), + is_bash: Some(false), conversation_id: Uuid::new_v4().to_string(), - can_handle_filenames_after_language_ids: None, + can_handle_filenames_after_language_ids: Some(true), use_web: None, quotes: vec![], debug_info: None, workspace_id: None, external_links: vec![], commit_notes: vec![], - long_context_mode: if LONG_CONTEXT_MODELS.contains(&model_name) { - Some(true) - } else { - None - }, - is_eval: None, + long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)), + is_eval: Some(false), desired_max_tokens: None, context_ast: None, is_composer: None, - runnable_code_blocks: None, - should_cache: None, + runnable_code_blocks: Some(false), + should_cache: Some(false), }; let mut encoded = Vec::new(); diff --git a/src/chat/aiserver/v1/lite.proto b/src/chat/aiserver/v1/lite.proto new file mode 100644 index 0000000..7a6cc24 --- /dev/null +++ b/src/chat/aiserver/v1/lite.proto @@ -0,0 +1,1156 @@ +syntax = "proto3"; +package aiserver.v1; +enum ClientSideToolV2 { // aiserver.v1.ClientSideToolV2 + CLIENT_SIDE_TOOL_V2_UNSPECIFIED = 0; + CLIENT_SIDE_TOOL_V2_READ_SEMSEARCH_FILES = 1; + CLIENT_SIDE_TOOL_V2_READ_FILE_FOR_IMPORTS = 2; + CLIENT_SIDE_TOOL_V2_RIPGREP_SEARCH = 3; + CLIENT_SIDE_TOOL_V2_RUN_TERMINAL_COMMAND = 4; + CLIENT_SIDE_TOOL_V2_READ_FILE = 5; + CLIENT_SIDE_TOOL_V2_LIST_DIR = 6; + CLIENT_SIDE_TOOL_V2_EDIT_FILE = 7; + CLIENT_SIDE_TOOL_V2_FILE_SEARCH = 8; + CLIENT_SIDE_TOOL_V2_SEMANTIC_SEARCH_FULL = 9; + CLIENT_SIDE_TOOL_V2_CREATE_FILE = 10; + CLIENT_SIDE_TOOL_V2_DELETE_FILE = 11; +} +enum EmbeddingModel { // aiserver.v1.EmbeddingModel + EMBEDDING_MODEL_UNSPECIFIED = 0; + EMBEDDING_MODEL_VOYAGE_CODE_2 = 1; + EMBEDDING_MODEL_TEXT_EMBEDDINGS_LARGE_3 = 2; + EMBEDDING_MODEL_QWEN_1_5B_CUSTOM = 3; +} +enum ChunkType { // aiserver.v1.ChunkType + CHUNK_TYPE_UNSPECIFIED = 0; + CHUNK_TYPE_CODEBASE = 1; + CHUNK_TYPE_LONG_FILE = 2; + CHUNK_TYPE_DOCS = 3; +} +enum FastApplySource { // aiserver.v1.FastApplySource + FAST_APPLY_SOURCE_UNSPECIFIED = 0; + FAST_APPLY_SOURCE_COMPOSER = 1; + FAST_APPLY_SOURCE_CLICKED_APPLY = 2; + FAST_APPLY_SOURCE_CACHED_APPLY = 3; +} +enum BuiltinTool { // aiserver.v1.BuiltinTool + BUILTIN_TOOL_UNSPECIFIED = 0; + BUILTIN_TOOL_SEARCH = 1; + BUILTIN_TOOL_READ_CHUNK = 2; + BUILTIN_TOOL_GOTODEF = 3; + BUILTIN_TOOL_EDIT = 4; + BUILTIN_TOOL_UNDO_EDIT = 5; + BUILTIN_TOOL_END = 6; + BUILTIN_TOOL_NEW_FILE = 7; + BUILTIN_TOOL_ADD_TEST = 8; + BUILTIN_TOOL_RUN_TEST = 9; + BUILTIN_TOOL_DELETE_TEST = 10; + BUILTIN_TOOL_SAVE_FILE = 11; + BUILTIN_TOOL_GET_TESTS = 12; + BUILTIN_TOOL_GET_SYMBOLS = 13; + BUILTIN_TOOL_SEMANTIC_SEARCH = 14; + BUILTIN_TOOL_GET_PROJECT_STRUCTURE = 15; + BUILTIN_TOOL_CREATE_RM_FILES = 16; + BUILTIN_TOOL_RUN_TERMINAL_COMMANDS = 17; + BUILTIN_TOOL_NEW_EDIT = 18; + BUILTIN_TOOL_READ_WITH_LINTER = 19; +} +enum FeatureType { // aiserver.v1.FeatureType + FEATURE_TYPE_UNSPECIFIED = 0; + FEATURE_TYPE_EDIT = 1; + FEATURE_TYPE_GENERATE = 2; + FEATURE_TYPE_INLINE_LONG_COMPLETION = 3; +} +enum TaskStatus { // aiserver.v1.TaskStatus + TASK_STATUS_UNSPECIFIED = 0; + TASK_STATUS_RUNNING = 1; + TASK_STATUS_PAUSED = 2; + TASK_STATUS_DONE = 3; + TASK_STATUS_NOT_STARTED = 4; +} +enum RerankerAlgorithm { // aiserver.v1.RerankerAlgorithm + RERANKER_ALGORITHM_UNSPECIFIED = 0; + RERANKER_ALGORITHM_LULEA = 1; + RERANKER_ALGORITHM_UMEA = 2; + RERANKER_ALGORITHM_NONE = 3; + RERANKER_ALGORITHM_LLAMA = 4; + RERANKER_ALGORITHM_STARCODER_V1 = 5; + RERANKER_ALGORITHM_GPT_3_5_LOGPROBS = 6; + RERANKER_ALGORITHM_LULEA_HAIKU = 7; + RERANKER_ALGORITHM_COHERE = 8; + RERANKER_ALGORITHM_VOYAGE = 9; + RERANKER_ALGORITHM_VOYAGE_EMBEDS = 10; + RERANKER_ALGORITHM_IDENTITY = 11; + RERANKER_ALGORITHM_ADA_EMBEDS = 12; +} +enum RechunkerChoice { // aiserver.v1.RechunkerChoice + RECHUNKER_CHOICE_UNSPECIFIED = 0; + RECHUNKER_CHOICE_IDENTITY = 1; + RECHUNKER_CHOICE_600_TOKS = 2; + RECHUNKER_CHOICE_2400_TOKS = 3; + RECHUNKER_CHOICE_4000_TOKS = 4; +} +enum LintGenerator { // aiserver.v1.LintGenerator + LINT_GENERATOR_UNSPECIFIED = 0; + LINT_GENERATOR_NAIVE = 1; + LINT_GENERATOR_COMMENT_PIPELINE = 2; + LINT_GENERATOR_SIMPLE_BUG = 3; + LINT_GENERATOR_SIMPLE_LINT_RULES = 4; +} +enum LintDiscriminator { // aiserver.v1.LintDiscriminator + LINT_DISCRIMINATOR_UNSPECIFIED = 0; + LINT_DISCRIMINATOR_SPECIFIC_RULES = 1; + LINT_DISCRIMINATOR_COMPILE_ERRORS = 2; + LINT_DISCRIMINATOR_CHANGE_BEHAVIOR = 3; + LINT_DISCRIMINATOR_RELEVANCE = 4; + LINT_DISCRIMINATOR_USER_AWARENESS = 5; + LINT_DISCRIMINATOR_CORRECTNESS = 6; + LINT_DISCRIMINATOR_CHUNKING = 7; + LINT_DISCRIMINATOR_TYPO = 8; + LINT_DISCRIMINATOR_CONFIDENCE = 9; + LINT_DISCRIMINATOR_DISMISSED_BUGS = 10; +} +enum CppSource { // aiserver.v1.CppSource + CPP_SOURCE_UNSPECIFIED = 0; + CPP_SOURCE_LINE_CHANGE = 1; + CPP_SOURCE_TYPING = 2; + CPP_SOURCE_OPTION_HOLD = 3; + CPP_SOURCE_LINTER_ERRORS = 4; + CPP_SOURCE_PARAMETER_HINTS = 5; + CPP_SOURCE_CURSOR_PREDICTION = 6; + CPP_SOURCE_MANUAL_TRIGGER = 7; + CPP_SOURCE_EDITOR_CHANGE = 8; +} +enum ChunkingStrategy { // aiserver.v1.ChunkingStrategy + CHUNKING_STRATEGY_UNSPECIFIED = 0; + CHUNKING_STRATEGY_DEFAULT = 1; +} +message ErrorDetails { // aiserver.v1.ErrorDetails + enum Error { // aiserver.v1.ErrorDetails.Error + ERROR_UNSPECIFIED = 0; + ERROR_BAD_API_KEY = 1; + ERROR_NOT_LOGGED_IN = 2; + ERROR_INVALID_AUTH_ID = 3; + ERROR_NOT_HIGH_ENOUGH_PERMISSIONS = 4; + ERROR_BAD_MODEL_NAME = 5; + ERROR_USER_NOT_FOUND = 6; + ERROR_FREE_USER_RATE_LIMIT_EXCEEDED = 7; + ERROR_PRO_USER_RATE_LIMIT_EXCEEDED = 8; + ERROR_FREE_USER_USAGE_LIMIT = 9; + ERROR_PRO_USER_USAGE_LIMIT = 10; + ERROR_AUTH_TOKEN_NOT_FOUND = 11; + ERROR_AUTH_TOKEN_EXPIRED = 12; + ERROR_OPENAI = 13; + ERROR_OPENAI_RATE_LIMIT_EXCEEDED = 14; + ERROR_OPENAI_ACCOUNT_LIMIT_EXCEEDED = 15; + ERROR_TASK_UUID_NOT_FOUND = 16; + ERROR_TASK_NO_PERMISSIONS = 17; + ERROR_AGENT_REQUIRES_LOGIN = 18; + ERROR_AGENT_ENGINE_NOT_FOUND = 19; + ERROR_MAX_TOKENS = 20; + ERROR_USER_ABORTED_REQUEST = 21; + ERROR_GENERIC_RATE_LIMIT_EXCEEDED = 22; + ERROR_PRO_USER_ONLY = 23; + ERROR_API_KEY_NOT_SUPPORTED = 24; + ERROR_SLASH_EDIT_FILE_TOO_LONG = 26; + ERROR_FILE_UNSUPPORTED = 27; + ERROR_GPT_4_VISION_PREVIEW_RATE_LIMIT = 28; + ERROR_CUSTOM_MESSAGE = 29; + ERROR_OUTDATED_CLIENT = 30; + ERROR_CLAUDE_IMAGE_TOO_LARGE = 31; + ERROR_GITGRAPH_NOT_FOUND = 32; + ERROR_FILE_NOT_FOUND = 33; + ERROR_API_KEY_RATE_LIMIT = 34; + ERROR_DEBOUNCED = 35; + ERROR_BAD_REQUEST = 36; + ERROR_REPOSITORY_SERVICE_REPOSITORY_IS_NOT_INITIALIZED = 37; + ERROR_UNAUTHORIZED = 38; + ERROR_NOT_FOUND = 39; + ERROR_DEPRECATED = 40; + ERROR_RESOURCE_EXHAUSTED = 41; + } + Error error = 1; + CustomErrorDetails details = 2; + optional bool is_expected = 3; +} + +message CustomErrorDetails { // aiserver.v1.CustomErrorDetails + string title = 1; + string detail = 2; + optional bool allow_command_links_potentially_unsafe_please_only_use_for_handwritten_trusted_markdown = 3; + optional bool is_retryable = 4; + optional bool show_request_id = 5; +} + +message GetChatRequest { // aiserver.v1.GetChatRequest + CurrentFileInfo current_file = 1; + repeated ConversationMessage conversation = 2; + repeated RepositoryInfo repositories = 3; + ExplicitContext explicit_context = 4; + optional string workspace_root_path = 5; + repeated CodeBlock code_blocks = 6; + ModelDetails model_details = 7; + repeated string documentation_identifiers = 8; + string request_id = 9; + LinterErrors linter_errors = 10; + optional string summary = 11; + optional int32 summary_up_until_index = 12; + optional bool allow_long_file_scan = 13; + optional bool is_bash = 14; + string conversation_id = 15; + optional bool can_handle_filenames_after_language_ids = 16; + optional string use_web = 17; + repeated ChatQuote quotes = 18; + optional DebugInfo debug_info = 19; + optional string workspace_id = 20; + repeated ChatExternalLink external_links = 21; + repeated CommitNote commit_notes = 23; + optional bool long_context_mode = 22; + optional bool is_eval = 24; + optional int32 desired_max_tokens = 26; + ContextAST context_ast = 25; + optional bool is_composer = 27; + optional bool runnable_code_blocks = 28; + optional bool should_cache = 29; +} +message CurrentFileInfo { // aiserver.v1.CurrentFileInfo + message NotebookCell { // aiserver.v1.CurrentFileInfo.NotebookCell + } + string relative_workspace_path = 1; + string contents = 2; + bool rely_on_filesync = 18; + optional string sha_256_hash = 17; + repeated NotebookCell cells = 16; + repeated BM25Chunk top_chunks = 10; + int32 contents_start_at_line = 9; + CursorPosition cursor_position = 3; + repeated DataframeInfo dataframes = 4; + int32 total_number_of_lines = 8; + string language_id = 5; + CursorRange selection = 6; + optional int32 alternative_version_id = 11; + repeated Diagnostic diagnostics = 7; + optional int32 file_version = 14; + repeated int32 cell_start_lines = 15; + string workspace_root_path = 19; +} +message BM25Chunk { // aiserver.v1.BM25Chunk + string content = 1; + SimplestRange range = 2; + int32 score = 3; + string relative_path = 4; +} +message SimplestRange { // aiserver.v1.SimplestRange + int32 start_line = 1; + int32 end_line_inclusive = 2; +} +message CursorPosition { // aiserver.v1.CursorPosition + int32 line = 1; + int32 column = 2; +} +message DataframeInfo { // aiserver.v1.DataframeInfo + message Column { // aiserver.v1.DataframeInfo.Column + string key = 1; + string type = 2; + } + string name = 1; + string shape = 2; + int32 data_dimensionality = 3; + repeated Column columns = 6; + int32 row_count = 7; + string index_column = 8; +} +message CursorRange { // aiserver.v1.CursorRange + CursorPosition start_position = 1; + CursorPosition end_position = 2; +} +message Diagnostic { // aiserver.v1.Diagnostic + enum DiagnosticSeverity { // aiserver.v1.Diagnostic.DiagnosticSeverity + DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; + DIAGNOSTIC_SEVERITY_ERROR = 1; + DIAGNOSTIC_SEVERITY_WARNING = 2; + DIAGNOSTIC_SEVERITY_INFORMATION = 3; + DIAGNOSTIC_SEVERITY_HINT = 4; + } + message RelatedInformation { // aiserver.v1.Diagnostic.RelatedInformation + string message = 1; + CursorRange range = 2; + } + string message = 1; + CursorRange range = 2; + DiagnosticSeverity severity = 3; + repeated RelatedInformation related_information = 4; +} +message ConversationMessage { // aiserver.v1.ConversationMessage + enum MessageType { // aiserver.v1.ConversationMessage.MessageType + MESSAGE_TYPE_UNSPECIFIED = 0; + MESSAGE_TYPE_HUMAN = 1; + MESSAGE_TYPE_AI = 2; + } + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + message ApproximateLintError { // aiserver.v1.ConversationMessage.ApproximateLintError + string message = 1; + string value = 2; + int32 start_line = 3; + int32 end_line = 4; + int32 start_column = 5; + int32 end_column = 6; + } + message Lints { // aiserver.v1.ConversationMessage.Lints + GetLintsForChangeResponse lints = 1; + string chat_codeblock_model_value = 2; + } + message ToolResult { // aiserver.v1.ConversationMessage.ToolResult + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + string tool_call_id = 1; + string tool_name = 2; + uint32 tool_index = 3; + string args = 4; + string raw_args = 5; + repeated CodeChunk attached_code_chunks = 6; + optional string content = 7; + ClientSideToolV2Result result = 8; + optional ToolResultError error = 9; + } + message NotepadContext { // aiserver.v1.ConversationMessage.NotepadContext + message CodeChunk { // aiserver.v1.ConversationMessage.CodeChunk + enum SummarizationStrategy { // aiserver.v1.ConversationMessage.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.ConversationMessage.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + INTENT_RECENTLY_VIEWED_FILE = 3; + INTENT_OUTLINE = 4; + INTENT_MENTIONED_FILE = 5; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; + optional bool contents_are_missing = 9; + } + string name = 1; + string text = 2; + repeated CodeChunk attached_code_chunks = 3; + repeated string attached_folders = 4; + repeated Commit commits = 5; + repeated PullRequest pull_requests = 6; + repeated GitDiff git_diffs = 7; + repeated ImageProto images = 8; + } + message EditTrailContext { // aiserver.v1.ConversationMessage.EditTrailContext + message EditLocation { // aiserver.v1.ConversationMessage.EditLocation + string relative_workspace_path = 1; + SimplestRange range = 3; + SimplestRange initial_range = 4; + string context_lines = 5; + string text = 6; + SimplestRange text_range = 7; + } + string unique_id = 1; + repeated EditLocation edit_trail_sorted = 2; + } + message RecentLocation { // aiserver.v1.ConversationMessage.RecentLocation + string relative_workspace_path = 1; + int32 line_number = 2; + } + string text = 1; + MessageType type = 2; + repeated CodeChunk attached_code_chunks = 3; + repeated CodeBlock codebase_context_chunks = 4; + repeated Commit commits = 5; + repeated PullRequest pull_requests = 6; + repeated GitDiff git_diffs = 7; + repeated SimpleFileDiff assistant_suggested_diffs = 8; + repeated InterpreterResult interpreter_results = 9; + repeated ImageProto images = 10; + repeated string attached_folders = 11; + repeated ApproximateLintError approximate_lint_errors = 12; + string bubble_id = 13; + optional string server_bubble_id = 32; + repeated FolderInfo attached_folders_new = 14; + repeated Lints lints = 15; + repeated UserResponseToSuggestedCodeBlock user_responses_to_suggested_code_blocks = 16; + repeated string relevant_files = 17; + repeated ToolResult tool_results = 18; + repeated NotepadContext notepads = 19; + optional bool is_capability_iteration = 20; + repeated ComposerCapabilityRequest capabilities = 21; + repeated EditTrailContext edit_trail_contexts = 22; + repeated SuggestedCodeBlock suggested_code_blocks = 23; + repeated RedDiff diffs_for_compressing_files = 24; + repeated LinterErrorsWithoutFileContents multi_file_linter_errors = 25; + repeated DiffHistoryData diff_histories = 26; + repeated CodeChunk recently_viewed_files = 27; + repeated RecentLocation recent_locations_history = 28; + bool is_agentic = 29; + repeated ComposerFileDiffHistory file_diff_trajectories = 30; + optional ConversationSummary conversation_summary = 31; +} +message CodeBlock { // aiserver.v1.CodeBlock + message Signatures { // aiserver.v1.CodeBlock.Signatures + repeated CursorRange ranges = 1; + } + string relative_workspace_path = 1; + optional string file_contents = 2; + CursorRange range = 3; + string contents = 4; + Signatures signatures = 5; + optional string override_contents = 6; + optional string original_contents = 7; + repeated DetailedLine detailed_lines = 8; +} +message DetailedLine { // aiserver.v1.DetailedLine + string text = 1; + float line_number = 2; + bool is_signature = 3; +} +message Commit { // aiserver.v1.Commit + string sha = 1; + string message = 2; + string description = 3; + repeated FileDiff diff = 4; + string author = 5; + string date = 6; +} +message FileDiff { // aiserver.v1.FileDiff + message Chunk { // aiserver.v1.FileDiff.Chunk + string content = 1; + repeated string lines = 2; + int32 old_start = 3; + int32 old_lines = 4; + int32 new_start = 5; + int32 new_lines = 6; + } + string from = 1; + string to = 2; + repeated Chunk chunks = 3; +} +message PullRequest { // aiserver.v1.PullRequest + string title = 1; + string body = 2; + repeated FileDiff diff = 3; +} +message GitDiff { // aiserver.v1.GitDiff + enum DiffType { // aiserver.v1.GitDiff.DiffType + DIFF_TYPE_UNSPECIFIED = 0; + DIFF_TYPE_DIFF_TO_HEAD = 1; + DIFF_TYPE_DIFF_FROM_BRANCH_TO_MAIN = 2; + } + repeated FileDiff diffs = 1; + DiffType diff_type = 2; +} +message SimpleFileDiff { // aiserver.v1.SimpleFileDiff + message Chunk { // aiserver.v1.SimpleFileDiff.Chunk + repeated string old_lines = 1; + repeated string new_lines = 2; + LineRange old_range = 3; + LineRange new_range = 4; + } + string relative_workspace_path = 1; + repeated Chunk chunks = 3; +} +message LineRange { // aiserver.v1.LineRange + int32 start_line_number = 1; + int32 end_line_number_inclusive = 2; +} +message InterpreterResult { // aiserver.v1.InterpreterResult + string output = 1; + bool success = 2; +} +message ImageProto { // aiserver.v1.ImageProto + message Dimension { // aiserver.v1.ImageProto.Dimension + int32 width = 1; + int32 height = 2; + } + bytes data = 1; + Dimension dimension = 2; +} +message FolderInfo { // aiserver.v1.FolderInfo + string relative_path = 1; + repeated FolderFileInfo files = 2; +} +message FolderFileInfo { // aiserver.v1.FolderFileInfo + string relative_path = 1; + string content = 2; + bool truncated = 3; + float score = 4; +} +message GetLintsForChangeResponse { // aiserver.v1.GetLintsForChangeResponse + message Lint { // aiserver.v1.GetLintsForChangeResponse.Lint + message QuickFix { // aiserver.v1.GetLintsForChangeResponse.Lint.QuickFix + message Edit { // aiserver.v1.GetLintsForChangeResponse.Lint.QuickFix.Edit + string relative_workspace_path = 1; + string text = 2; + int32 start_line_number_one_indexed = 3; + int32 start_column_one_indexed = 4; + int32 end_line_number_inclusive_one_indexed = 5; + int32 end_column_one_indexed = 6; + } + string message = 1; + string kind = 2; + bool is_preferred = 3; + repeated Edit edits = 4; + } + string message = 1; + string severity = 2; + string relative_workspace_path = 3; + int32 start_line_number_one_indexed = 4; + int32 start_column_one_indexed = 5; + int32 end_line_number_inclusive_one_indexed = 6; + int32 end_column_one_indexed = 7; + repeated QuickFix quick_fixes = 9; + } + repeated Lint lints = 1; +} +message UserResponseToSuggestedCodeBlock { // aiserver.v1.UserResponseToSuggestedCodeBlock + enum UserResponseType { // aiserver.v1.UserResponseToSuggestedCodeBlock.UserResponseType + USER_RESPONSE_TYPE_UNSPECIFIED = 0; + USER_RESPONSE_TYPE_ACCEPT = 1; + USER_RESPONSE_TYPE_REJECT = 2; + USER_RESPONSE_TYPE_MODIFY = 3; + } + UserResponseType user_response_type = 1; + string file_path = 2; + optional FileDiff user_modifications_to_suggested_code_blocks = 3; +} +message ClientSideToolV2Result { // aiserver.v1.ClientSideToolV2Result + ClientSideToolV2 tool = 1; + ReadSemsearchFilesResult read_semsearch_files_result = 2; + ReadFileForImportsResult read_file_for_imports_result = 3; + RipgrepSearchResult ripgrep_search_result = 4; + RunTerminalCommandResult run_terminal_command_result = 5; + ReadFileResult read_file_result = 6; + ListDirResult list_dir_result = 9; + EditFileResult edit_file_result = 10; + ToolCallFileSearchResult file_search_result = 11; + SemanticSearchFullResult semantic_search_full_result = 18; + CreateFileResult create_file_result = 19; + DeleteFileResult delete_file_result = 20; + optional ToolResultError error = 8; +} +message ReadSemsearchFilesResult { // aiserver.v1.ReadSemsearchFilesResult + repeated CodeResult code_results = 1; +} +message CodeResult { // aiserver.v1.CodeResult + CodeBlock code_block = 1; + float score = 2; +} +message ReadFileForImportsResult { // aiserver.v1.ReadFileForImportsResult + string contents = 1; +} +message RipgrepSearchResult { // aiserver.v1.RipgrepSearchResult + RipgrepSearchResultInternal internal = 1; +} +message RipgrepSearchResultInternal { // aiserver.v1.RipgrepSearchResultInternal + message IFileMatch { // aiserver.v1.RipgrepSearchResultInternal.IFileMatch + message ITextSearchResult { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchResult + message ITextSearchMatch { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchMatch + message ISearchRangeSetPairing { // aiserver.v1.RipgrepSearchResultInternal.ISearchRangeSetPairing + message ISearchRange { // aiserver.v1.RipgrepSearchResultInternal.ISearchRange + int32 start_line_number = 1; + int32 start_column = 2; + int32 end_line_number = 3; + int32 end_column = 4; + } + ISearchRange source = 1; + ISearchRange preview = 2; + } + optional string uri = 1; + repeated ISearchRangeSetPairing range_locations = 2; + string preview_text = 3; + optional int32 webview_index = 4; + optional string cell_fragment = 5; + } + message ITextSearchContext { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchContext + optional string uri = 1; + string text = 2; + int32 line_number = 3; + } + ITextSearchMatch match = 1; + ITextSearchContext context = 2; + } + string resource = 1; + repeated ITextSearchResult results = 2; + } + enum SearchCompletionExitCode { // aiserver.v1.RipgrepSearchResultInternal.SearchCompletionExitCode + SEARCH_COMPLETION_EXIT_CODE_UNSPECIFIED = 0; + SEARCH_COMPLETION_EXIT_CODE_NORMAL = 1; + SEARCH_COMPLETION_EXIT_CODE_NEW_SEARCH_STARTED = 2; + } + message ITextSearchCompleteMessage { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchCompleteMessage + enum TextSearchCompleteMessageType { // aiserver.v1.RipgrepSearchResultInternal.TextSearchCompleteMessageType + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_UNSPECIFIED = 0; + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_INFORMATION = 1; + TEXT_SEARCH_COMPLETE_MESSAGE_TYPE_WARNING = 2; + } + string text = 1; + TextSearchCompleteMessageType type = 2; + optional bool trusted = 3; + } + message IFileSearchStats { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchStats + message ISearchEngineStats { // aiserver.v1.RipgrepSearchResultInternal.ISearchEngineStats + int32 file_walk_time = 1; + int32 directories_walked = 2; + int32 files_walked = 3; + int32 cmd_time = 4; + optional int32 cmd_result_count = 5; + } + message ICachedSearchStats { // aiserver.v1.RipgrepSearchResultInternal.ICachedSearchStats + bool cache_was_resolved = 1; + int32 cache_lookup_time = 2; + int32 cache_filter_time = 3; + int32 cache_entry_count = 4; + } + message IFileSearchProviderStats { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchProviderStats + int32 provider_time = 1; + int32 post_process_time = 2; + } + enum FileSearchProviderType { // aiserver.v1.RipgrepSearchResultInternal.IFileSearchStats.FileSearchProviderType + FILE_SEARCH_PROVIDER_TYPE_UNSPECIFIED = 0; + FILE_SEARCH_PROVIDER_TYPE_FILE_SEARCH_PROVIDER = 1; + FILE_SEARCH_PROVIDER_TYPE_SEARCH_PROCESS = 2; + } + bool from_cache = 1; + ISearchEngineStats search_engine_stats = 2; + ICachedSearchStats cached_search_stats = 3; + IFileSearchProviderStats file_search_provider_stats = 4; + int32 result_count = 5; + FileSearchProviderType type = 6; + optional int32 sorting_time = 7; + } + message ITextSearchStats { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchStats + enum TextSearchProviderType { // aiserver.v1.RipgrepSearchResultInternal.ITextSearchStats.TextSearchProviderType + TEXT_SEARCH_PROVIDER_TYPE_UNSPECIFIED = 0; + TEXT_SEARCH_PROVIDER_TYPE_TEXT_SEARCH_PROVIDER = 1; + TEXT_SEARCH_PROVIDER_TYPE_SEARCH_PROCESS = 2; + TEXT_SEARCH_PROVIDER_TYPE_AI_TEXT_SEARCH_PROVIDER = 3; + } + TextSearchProviderType type = 1; + } + repeated IFileMatch results = 1; + optional SearchCompletionExitCode exit = 2; + optional bool limit_hit = 3; + repeated ITextSearchCompleteMessage messages = 4; + IFileSearchStats file_search_stats = 5; + ITextSearchStats text_search_stats = 6; +} +message RunTerminalCommandResult { // aiserver.v1.RunTerminalCommandResult + string output = 1; + int32 exit_code = 2; + optional bool rejected = 3; + bool popped_out_into_background = 4; +} +message ReadFileResult { // aiserver.v1.ReadFileResult + string contents = 1; + bool did_downgrade_to_line_range = 2; + bool did_shorten_line_range = 3; + bool did_set_default_line_range = 4; + optional string full_file_contents = 5; + optional string outline = 6; + optional int32 start_line_one_indexed = 7; + optional int32 end_line_one_indexed_inclusive = 8; + string relative_workspace_path = 9; + bool did_shorten_char_range = 10; +} +message ListDirResult { // aiserver.v1.ListDirResult + message File { // aiserver.v1.ListDirResult.File + message Timestamp { // google.protobuf.Timestamp + int64 seconds = 1; + int32 nanos = 2; + } + string name = 1; + bool is_directory = 2; + optional int64 size = 3; + optional Timestamp last_modified = 4; + optional int32 num_children = 5; + optional int32 num_lines = 6; + } + repeated File files = 1; + string directory_relative_workspace_path = 2; +} +message EditFileResult { // aiserver.v1.EditFileResult + message FileDiff { // aiserver.v1.EditFileResult.FileDiff + message ChunkDiff { // aiserver.v1.EditFileResult.FileDiff.ChunkDiff + string diff_string = 1; + int32 old_start = 2; + int32 new_start = 3; + int32 old_lines = 4; + int32 new_lines = 5; + int32 lines_removed = 6; + int32 lines_added = 7; + } + enum Editor { // aiserver.v1.EditFileResult.FileDiff.Editor + EDITOR_UNSPECIFIED = 0; + EDITOR_AI = 1; + EDITOR_HUMAN = 2; + } + repeated ChunkDiff chunks = 1; + Editor editor = 2; + bool hit_timeout = 3; + } + FileDiff diff = 1; + bool is_applied = 2; + bool apply_failed = 3; +} +message ToolCallFileSearchResult { // aiserver.v1.ToolCallFileSearchResult + message File { // aiserver.v1.ToolCallFileSearchResult.File + string uri = 1; + } + repeated File files = 1; + optional bool limit_hit = 2; + int32 num_results = 3; +} +message SemanticSearchFullResult { // aiserver.v1.SemanticSearchFullResult + repeated CodeResult code_results = 1; +} +message CreateFileResult { // aiserver.v1.CreateFileResult + bool file_created_successfully = 1; + bool file_already_exists = 2; +} +message DeleteFileResult { // aiserver.v1.DeleteFileResult + bool rejected = 1; + bool file_non_existent = 2; + bool file_deleted_successfully = 3; +} +message ToolResultError { // aiserver.v1.ToolResultError + string client_visible_error_message = 1; + string model_visible_error_message = 2; +} +message ComposerCapabilityRequest { // aiserver.v1.ComposerCapabilityRequest + enum ComposerCapabilityType { // aiserver.v1.ComposerCapabilityRequest.ComposerCapabilityType + COMPOSER_CAPABILITY_TYPE_UNSPECIFIED = 0; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_LINTS = 1; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_TESTS = 2; + COMPOSER_CAPABILITY_TYPE_MEGA_PLANNER = 3; + COMPOSER_CAPABILITY_TYPE_LOOP_ON_COMMAND = 4; + COMPOSER_CAPABILITY_TYPE_TOOL_CALL = 5; + COMPOSER_CAPABILITY_TYPE_DIFF_REVIEW = 6; + COMPOSER_CAPABILITY_TYPE_CONTEXT_PICKING = 7; + COMPOSER_CAPABILITY_TYPE_EDIT_TRAIL = 8; + COMPOSER_CAPABILITY_TYPE_AUTO_CONTEXT = 9; + COMPOSER_CAPABILITY_TYPE_CONTEXT_PLANNER = 10; + COMPOSER_CAPABILITY_TYPE_DIFF_HISTORY = 11; + COMPOSER_CAPABILITY_TYPE_REMEMBER_THIS = 12; + COMPOSER_CAPABILITY_TYPE_DECOMPOSER = 13; + COMPOSER_CAPABILITY_TYPE_USES_CODEBASE = 14; + COMPOSER_CAPABILITY_TYPE_TOOL_FORMER = 15; + } + message LoopOnLintsCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnLintsCapability + repeated LinterErrors linter_errors = 1; + optional string custom_instructions = 2; + } + message LoopOnTestsCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnTestsCapability + repeated string test_names = 1; + optional string custom_instructions = 2; + } + message MegaPlannerCapability { // aiserver.v1.ComposerCapabilityRequest.MegaPlannerCapability + optional string custom_instructions = 1; + } + message LoopOnCommandCapability { // aiserver.v1.ComposerCapabilityRequest.LoopOnCommandCapability + string command = 1; + optional string custom_instructions = 2; + optional string output = 3; + optional int32 exit_code = 4; + } + message ToolCallCapability { // aiserver.v1.ComposerCapabilityRequest.ToolCallCapability + message ToolSchema { // aiserver.v1.ComposerCapabilityRequest.ToolSchema + enum ToolType { // aiserver.v1.ComposerCapabilityRequest.ToolType + TOOL_TYPE_UNSPECIFIED = 0; + TOOL_TYPE_ADD_FILE_TO_CONTEXT = 1; + TOOL_TYPE_RUN_TERMINAL_COMMAND = 2; + TOOL_TYPE_ITERATE = 3; + TOOL_TYPE_REMOVE_FILE_FROM_CONTEXT = 4; + TOOL_TYPE_SEMANTIC_SEARCH_CODEBASE = 5; + } + ToolType type = 1; + string name = 2; + repeated string required = 4; + } + optional string custom_instructions = 1; + repeated ToolSchema tool_schemas = 2; + repeated string relevant_files = 3; + repeated string files_in_context = 4; + repeated string semantic_search_files = 5; + } + message DiffReviewCapability { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability + message SimpleFileDiff { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability.SimpleFileDiff + message Chunk { // aiserver.v1.ComposerCapabilityRequest.DiffReviewCapability.SimpleFileDiff.Chunk + repeated string old_lines = 1; + repeated string new_lines = 2; + LineRange old_range = 3; + LineRange new_range = 4; + } + string relative_workspace_path = 1; + repeated Chunk chunks = 3; + } + optional string custom_instructions = 1; + repeated SimpleFileDiff diffs = 2; + } + message ContextPickingCapability { // aiserver.v1.ComposerCapabilityRequest.ContextPickingCapability + optional string custom_instructions = 1; + repeated string potential_context_files = 2; + repeated CodeChunk potential_context_code_chunks = 3; + repeated string files_in_context = 4; + } + message EditTrailCapability { // aiserver.v1.ComposerCapabilityRequest.EditTrailCapability + optional string custom_instructions = 1; + } + message AutoContextCapability { // aiserver.v1.ComposerCapabilityRequest.AutoContextCapability + optional string custom_instructions = 1; + repeated string additional_files = 2; + } + message ContextPlannerCapability { // aiserver.v1.ComposerCapabilityRequest.ContextPlannerCapability + optional string custom_instructions = 1; + repeated CodeChunk attached_code_chunks = 2; + } + message RememberThisCapability { // aiserver.v1.ComposerCapabilityRequest.RememberThisCapability + optional string custom_instructions = 1; + string memory = 2; + } + message DecomposerCapability { // aiserver.v1.ComposerCapabilityRequest.DecomposerCapability + optional string custom_instructions = 1; + } + ComposerCapabilityType type = 1; + LoopOnLintsCapability loop_on_lints = 2; + LoopOnTestsCapability loop_on_tests = 3; + MegaPlannerCapability mega_planner = 4; + LoopOnCommandCapability loop_on_command = 5; + ToolCallCapability tool_call = 6; + DiffReviewCapability diff_review = 7; + ContextPickingCapability context_picking = 8; + EditTrailCapability edit_trail = 9; + AutoContextCapability auto_context = 10; + ContextPlannerCapability context_planner = 11; + RememberThisCapability remember_this = 12; + DecomposerCapability decomposer = 13; +} +message LinterErrors { // aiserver.v1.LinterErrors + string relative_workspace_path = 1; + repeated LinterError errors = 2; + string file_contents = 3; +} +message LinterError { // aiserver.v1.LinterError + message RelatedInformation { // aiserver.v1.Diagnostic.RelatedInformation + string message = 1; + CursorRange range = 2; + } + enum DiagnosticSeverity { // aiserver.v1.Diagnostic.DiagnosticSeverity + DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0; + DIAGNOSTIC_SEVERITY_ERROR = 1; + DIAGNOSTIC_SEVERITY_WARNING = 2; + DIAGNOSTIC_SEVERITY_INFORMATION = 3; + DIAGNOSTIC_SEVERITY_HINT = 4; + } + string message = 1; + CursorRange range = 2; + optional string source = 3; + repeated RelatedInformation related_information = 4; + optional DiagnosticSeverity severity = 5; +} +message CodeChunk { // aiserver.v1.CodeChunk + enum SummarizationStrategy { // aiserver.v1.CodeChunk.SummarizationStrategy + SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED = 0; + SUMMARIZATION_STRATEGY_SUMMARIZED = 1; + SUMMARIZATION_STRATEGY_EMBEDDED = 2; + } + enum Intent { // aiserver.v1.CodeChunk.Intent + INTENT_UNSPECIFIED = 0; + INTENT_COMPOSER_FILE = 1; + INTENT_COMPRESSED_COMPOSER_FILE = 2; + } + string relative_workspace_path = 1; + int32 start_line_number = 2; + repeated string lines = 3; + optional SummarizationStrategy summarization_strategy = 4; + string language_identifier = 5; + optional Intent intent = 6; + optional bool is_final_version = 7; + optional bool is_first_version = 8; +} +message SuggestedCodeBlock { // aiserver.v1.SuggestedCodeBlock + string relative_workspace_path = 1; +} +message RedDiff { // aiserver.v1.RedDiff + string relative_workspace_path = 1; + repeated SimplestRange red_ranges = 2; + repeated SimplestRange red_ranges_reversed = 3; + string start_hash = 4; + string end_hash = 5; +} +message LinterErrorsWithoutFileContents { // aiserver.v1.LinterErrorsWithoutFileContents + string relative_workspace_path = 1; + repeated LinterError errors = 2; +} +message DiffHistoryData { // aiserver.v1.DiffHistoryData + string relative_workspace_path = 1; + repeated ComposerFileDiff diffs = 2; + double timestamp = 3; + string unique_id = 4; + ComposerFileDiff start_to_end_diff = 5; +} +message ComposerFileDiff { // aiserver.v1.ComposerFileDiff + message ChunkDiff { // aiserver.v1.ComposerFileDiff.ChunkDiff + string diff_string = 1; + int32 old_start = 2; + int32 new_start = 3; + int32 old_lines = 4; + int32 new_lines = 5; + int32 lines_removed = 6; + int32 lines_added = 7; + } + enum Editor { // aiserver.v1.ComposerFileDiff.Editor + EDITOR_UNSPECIFIED = 0; + EDITOR_AI = 1; + EDITOR_HUMAN = 2; + } + repeated ChunkDiff chunks = 1; + Editor editor = 2; + bool hit_timeout = 3; +} +message ComposerFileDiffHistory { // aiserver.v1.ComposerFileDiffHistory + string file_name = 1; + repeated string diff_history = 2; + repeated double diff_history_timestamps = 3; +} +message ConversationSummary { // aiserver.v1.ConversationSummary + string summary = 1; + string truncation_last_bubble_id_inclusive = 2; + string client_should_start_sending_from_inclusive_bubble_id = 3; +} +message RepositoryInfo { // aiserver.v1.RepositoryInfo + string relative_workspace_path = 1; + repeated string remote_urls = 2; + repeated string remote_names = 3; + string repo_name = 4; + string repo_owner = 5; + bool is_tracked = 6; + bool is_local = 7; + optional int32 num_files = 8; + optional double orthogonal_transform_seed = 9; + optional EmbeddingModel preferred_embedding_model = 10; +} +message ExplicitContext { // aiserver.v1.ExplicitContext + string context = 1; + optional string repo_context = 2; +} +message ModelDetails { // aiserver.v1.ModelDetails + optional string model_name = 1; + optional string api_key = 2; + optional bool enable_ghost_mode = 3; + optional AzureState azure_state = 4; + optional bool enable_slow_pool = 5; + optional string openai_api_base_url = 6; +} +message AzureState { // aiserver.v1.AzureState + string api_key = 1; + string base_url = 2; + string deployment = 3; + bool use_azure = 4; +} +message ChatQuote { // aiserver.v1.ChatQuote + string markdown = 1; + string bubble_id = 2; + int32 section_index = 3; +} +message DebugInfo { // aiserver.v1.DebugInfo + message Breakpoint { // aiserver.v1.DebugInfo.Breakpoint + string relative_workspace_path = 1; + int32 line_number = 2; + repeated string lines_before_breakpoint = 3; + repeated string lines_after_breakpoint = 4; + optional string exception_info = 5; + } + message CallStackFrame { // aiserver.v1.DebugInfo.CallStackFrame + message Scope { // aiserver.v1.DebugInfo.Scope + message Variable { // aiserver.v1.DebugInfo.Variable + string name = 1; + string value = 2; + optional string type = 3; + } + string name = 1; + repeated Variable variables = 2; + } + string relative_workspace_path = 1; + int32 line_number = 2; + string function_name = 3; + repeated Scope scopes = 4; + } + Breakpoint breakpoint = 1; + repeated CallStackFrame call_stack = 2; + repeated CodeBlock history = 3; +} +message ChatExternalLink { // aiserver.v1.ChatExternalLink + string url = 1; + string uuid = 2; +} +message CommitNote { // aiserver.v1.CommitNote + string note = 1; + string commit_hash = 2; +} +message ContextAST { // aiserver.v1.ContextAST + repeated ContainerTree files = 1; +} +message ContainerTree { // aiserver.v1.ContainerTree + string relative_workspace_path = 1; + repeated ContainerTreeNode nodes = 2; +} +message ContainerTreeNode { // aiserver.v1.ContainerTreeNode + message Container { // aiserver.v1.ContainerTreeNode.Container + message Reference { // aiserver.v1.ContainerTreeNode.Reference + string value = 1; + string relative_workspace_path = 2; + } + string doc_string = 1; + string header = 2; + string trailer = 3; + repeated ContainerTreeNode children = 5; + repeated Reference references = 6; + double score = 7; + } + message Blob { // aiserver.v1.ContainerTreeNode.Blob + optional string value = 1; + } + message Symbol { // aiserver.v1.ContainerTreeNode.Symbol + message Reference { // aiserver.v1.ContainerTreeNode.Reference + string value = 1; + string relative_workspace_path = 2; + } + string doc_string = 1; + string value = 2; + repeated Reference references = 6; + double score = 7; + } + Container container = 1; + Blob blob = 2; + Symbol symbol = 3; +} +message StreamChatResponse { // aiserver.v1.StreamChatResponse + message ChunkIdentity { // aiserver.v1.StreamChatResponse.ChunkIdentity + string file_name = 1; + int32 start_line = 2; + int32 end_line = 3; + string text = 4; + ChunkType chunk_type = 5; + } + string text = 1; + optional string server_bubble_id = 22; + optional string debugging_only_chat_prompt = 2; + optional int32 debugging_only_token_count = 3; + DocumentationCitation document_citation = 4; + optional string filled_prompt = 5; + optional bool is_big_file = 6; + optional string intermediate_text = 7; + optional bool is_using_slow_request = 10; + optional ChunkIdentity chunk_identity = 8; + optional DocsReference docs_reference = 9; + optional WebCitation web_citation = 11; + optional StatusUpdates status_updates = 12; + optional ServerTimingInfo timing_info = 13; + optional SymbolLink symbol_link = 14; + optional FileLink file_link = 15; + optional ConversationSummary conversation_summary = 16; + optional ServiceStatusUpdate service_status_update = 17; +} +message DocumentationCitation { // aiserver.v1.DocumentationCitation + repeated DocumentationChunk chunks = 1; +} +message DocumentationChunk { // aiserver.v1.DocumentationChunk + string doc_name = 1; + string page_url = 2; + string documentation_chunk = 3; + float score = 4; + string page_title = 5; +} +message DocsReference { // aiserver.v1.DocsReference + string title = 1; + string url = 2; +} +message WebCitation { // aiserver.v1.WebCitation + repeated WebReference references = 1; +} +message WebReference { // aiserver.v1.WebReference + string title = 2; + string url = 1; +} +message StatusUpdates { // aiserver.v1.StatusUpdates + repeated StatusUpdate updates = 1; +} +message StatusUpdate { // aiserver.v1.StatusUpdate + string message = 1; + optional string metadata = 2; +} +message ServerTimingInfo { // aiserver.v1.ServerTimingInfo + double server_start_time = 1; + double server_first_token_time = 2; + double server_request_sent_time = 3; + double server_end_time = 4; +} +message SymbolLink { // aiserver.v1.SymbolLink + string symbol_name = 1; + string symbol_search_string = 2; + string relative_workspace_path = 3; + int32 rough_line_number = 4; +} +message FileLink { // aiserver.v1.FileLink + string display_name = 1; + string relative_workspace_path = 2; +} +message ServiceStatusUpdate { // aiserver.v1.ServiceStatusUpdate + string message = 1; + string codicon = 2; + optional bool allow_command_links_potentially_unsafe_please_only_use_for_handwritten_trusted_markdown = 3; +} diff --git a/src/chat/error.rs b/src/chat/error.rs index 75e530d..be4db01 100644 --- a/src/chat/error.rs +++ b/src/chat/error.rs @@ -1,4 +1,4 @@ -use super::aiserver::v1::throw_error_check_request::Error as ErrorType; +use super::aiserver::v1::error_details::Error as ErrorType; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; @@ -65,7 +65,6 @@ impl ChatError { Some(error) => match error { ErrorType::Unspecified => 500, ErrorType::BadApiKey - | ErrorType::BadUserApiKey | ErrorType::InvalidAuthId | ErrorType::AuthTokenNotFound | ErrorType::AuthTokenExpired diff --git a/src/chat/route.rs b/src/chat/route.rs index 956b11f..48731f8 100644 --- a/src/chat/route.rs +++ b/src/chat/route.rs @@ -1,10 +1,17 @@ mod logs; pub use logs::{handle_logs, handle_logs_post}; mod health; -pub use health::{handle_root, handle_health}; +pub use health::{handle_health, handle_root}; mod token; -pub use token::{handle_get_checksum, handle_update_tokeninfo, handle_get_tokeninfo, handle_update_tokeninfo_post, handle_tokeninfo_page, handle_basic_calibration}; -mod usage; -pub use usage::get_user_info; +pub use token::{ + handle_basic_calibration, handle_get_checksum, handle_get_tokeninfo, handle_tokeninfo_page, + handle_update_tokeninfo, handle_update_tokeninfo_post, +}; +mod profile; +pub use profile::get_user_info; mod config; -pub use config::{handle_env_example, handle_config_page, handle_static, handle_readme, handle_about}; +pub use config::{ + handle_about, handle_config_page, handle_env_example, handle_readme, handle_static, +}; +mod api; +pub use api::handle_api_page; diff --git a/src/chat/route/api.rs b/src/chat/route/api.rs new file mode 100644 index 0000000..109d5f9 --- /dev/null +++ b/src/chat/route/api.rs @@ -0,0 +1,26 @@ +use axum::response::{IntoResponse, Response}; +use reqwest::header::CONTENT_TYPE; + +use crate::{ + app::constant::{ + CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH, + }, + AppConfig, PageContent, +}; + +pub async fn handle_api_page() -> impl IntoResponse { + match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() { + PageContent::Default => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(include_str!("../../../static/api.min.html").to_string()) + .unwrap(), + PageContent::Text(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) + .body(content.clone()) + .unwrap(), + PageContent::Html(content) => Response::builder() + .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) + .body(content.clone()) + .unwrap(), + } +} diff --git a/src/chat/route/health.rs b/src/chat/route/health.rs index f27c255..bee3097 100644 --- a/src/chat/route/health.rs +++ b/src/chat/route/health.rs @@ -2,7 +2,7 @@ use crate::{ app::{ constant::{ AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, - CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION, ROUTE_ABOUT_PATH, + CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_TOKENINFO_PATH, ROUTE_GET_USER_INFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, @@ -127,6 +127,7 @@ pub async fn handle_health( ROUTE_README_PATH, ROUTE_BASIC_CALIBRATION_PATH, ROUTE_GET_USER_INFO_PATH, + ROUTE_API_PATH, ], }) } diff --git a/src/chat/route/logs.rs b/src/chat/route/logs.rs index a71315b..0076b7e 100644 --- a/src/chat/route/logs.rs +++ b/src/chat/route/logs.rs @@ -7,7 +7,7 @@ use crate::{ lazy::AUTH_TOKEN, model::{AppConfig, AppState, PageContent, RequestLog}, }, - common::models::ApiStatus, + common::{models::ApiStatus, utils::extract_token}, }; use axum::{ body::Body, @@ -62,37 +62,16 @@ pub async fn handle_logs_post( if auth_header == auth_token { return Ok(Json(LogsResponse { status: ApiStatus::Success, - total: state.request_logs.len(), + total: state.total_requests, + active: Some(state.active_requests), + error: Some(state.error_requests), logs: state.request_logs.clone(), timestamp: Local::now().to_string(), })); } - // 解析 token 和 checksum - let token_part = if let Some(pos) = auth_header.find("::") { - let (_, rest) = auth_header.split_at(pos + 2); - if let Some(comma_pos) = rest.find(',') { - let (token, _) = rest.split_at(comma_pos); - token - } else { - rest - } - } else if let Some(pos) = auth_header.find("%3A%3A") { - let (_, rest) = auth_header.split_at(pos + 6); - if let Some(comma_pos) = rest.find(',') { - let (token, _) = rest.split_at(comma_pos); - token - } else { - rest - } - } else { - if let Some(comma_pos) = auth_header.find(',') { - let (token, _) = auth_header.split_at(comma_pos); - token - } else { - auth_header - } - }; + // 解析 token + let token_part = extract_token(auth_header).ok_or(StatusCode::UNAUTHORIZED)?; // 否则筛选出token匹配的日志 let filtered_logs: Vec = state @@ -109,7 +88,9 @@ pub async fn handle_logs_post( Ok(Json(LogsResponse { status: ApiStatus::Success, - total: filtered_logs.len(), + total: filtered_logs.len() as u64, + active: None, + error: None, logs: filtered_logs, timestamp: Local::now().to_string(), })) @@ -118,7 +99,11 @@ pub async fn handle_logs_post( #[derive(serde::Serialize)] pub struct LogsResponse { pub status: ApiStatus, - pub total: usize, + pub total: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub active: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, pub logs: Vec, pub timestamp: String, } diff --git a/src/chat/route/profile.rs b/src/chat/route/profile.rs new file mode 100644 index 0000000..86e6363 --- /dev/null +++ b/src/chat/route/profile.rs @@ -0,0 +1,34 @@ +use crate::{ + chat::constant::ERR_NODATA, + common::{models::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}}, +}; +use axum::Json; + +use super::token::TokenRequest; + +pub async fn get_user_info(Json(request): Json) -> Json { + let auth_token = match request.token { + Some(token) => token, + None => { + return Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }) + } + }; + + let token = match extract_token(&auth_token) { + Some(token) => token, + None => { + return Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }) + } + }; + + match get_token_profile(&token).await { + Some(usage) => Json(GetUserInfo::Usage(usage)), + None => Json(GetUserInfo::Error { + error: ERR_NODATA.to_string(), + }), + } +} diff --git a/src/chat/route/token.rs b/src/chat/route/token.rs index 20b3506..b9476b1 100644 --- a/src/chat/route/token.rs +++ b/src/chat/route/token.rs @@ -10,13 +10,12 @@ use crate::{ common::{ models::{ApiStatus, NormalResponseNoData}, utils::{ - extract_time, extract_user_id, generate_checksum_with_default, load_tokens, - validate_checksum, validate_token, + extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, load_tokens, validate_token_and_checksum }, }, }; use axum::{ - extract::State, + extract::{Query, State}, http::{ header::{AUTHORIZATION, CONTENT_TYPE}, HeaderMap, @@ -29,14 +28,24 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; +#[derive(Deserialize)] +pub struct ChecksumQuery { + #[serde(default, alias = "checksum")] + pub bad_checksum: Option, +} + #[derive(Serialize)] pub struct ChecksumResponse { pub checksum: String, } -pub async fn handle_get_checksum() -> Json { - let checksum = generate_checksum_with_default(); - Json(ChecksumResponse { checksum }) +pub async fn handle_get_checksum( + Query(query): Query +) -> Json { + match query.bad_checksum { + None => Json(ChecksumResponse { checksum: generate_checksum_with_default() }), + Some(bad_checksum) => Json(ChecksumResponse { checksum: generate_checksum_with_repair(&bad_checksum) }) + } } // 更新 TokenInfo 处理 @@ -191,6 +200,8 @@ pub struct BasicCalibrationResponse { pub user_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub create_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub checksum_time: Option, } pub async fn handle_basic_calibration( @@ -205,65 +216,36 @@ pub async fn handle_basic_calibration( message: Some("未提供授权令牌".to_string()), user_id: None, create_at: None, + checksum_time: None, }) } }; - // 解析 token 和 checksum - let (token_part, checksum) = if let Some(pos) = auth_token.find("::") { - let (_, rest) = auth_token.split_at(pos + 2); - if let Some(comma_pos) = rest.find(',') { - let (token, checksum) = rest.split_at(comma_pos); - (token, &checksum[1..]) - } else { - (rest, "") - } - } else if let Some(pos) = auth_token.find("%3A%3A") { - let (_, rest) = auth_token.split_at(pos + 6); - if let Some(comma_pos) = rest.find(',') { - let (token, checksum) = rest.split_at(comma_pos); - (token, &checksum[1..]) - } else { - (rest, "") - } - } else { - if let Some(comma_pos) = auth_token.find(',') { - let (token, checksum) = auth_token.split_at(comma_pos); - (token, &checksum[1..]) - } else { - (&auth_token[..], "") + // 校验 token 和 checksum + let (token, checksum) = match validate_token_and_checksum(&auth_token) { + Some(parts) => parts, + None => { + return Json(BasicCalibrationResponse { + status: ApiStatus::Error, + message: Some("无效令牌或无效校验和".to_string()), + user_id: None, + create_at: None, + checksum_time: None, + }) } }; - // 验证 token 有效性 - if !validate_token(token_part) { - return Json(BasicCalibrationResponse { - status: ApiStatus::Error, - message: Some("无效的授权令牌".to_string()), - user_id: None, - create_at: None, - }); - } - - // 验证 checksum - if !validate_checksum(checksum) { - return Json(BasicCalibrationResponse { - status: ApiStatus::Error, - message: Some("无效的校验和".to_string()), - user_id: None, - create_at: None, - }); - } - // 提取用户ID和创建时间 - let user_id = extract_user_id(token_part); - let create_at = extract_time(token_part).map(|dt| dt.to_string()); + let user_id = extract_user_id(&token); + let create_at = extract_time(&token).map(|dt| dt.to_string()); + let checksum_time = extract_time_ks(&checksum[..8]); - // 返回校准结果 + // 返回校验结果 Json(BasicCalibrationResponse { status: ApiStatus::Success, - message: Some("校准成功".to_string()), + message: Some("校验成功".to_string()), user_id, create_at, + checksum_time, }) } diff --git a/src/chat/route/usage.rs b/src/chat/route/usage.rs deleted file mode 100644 index ffd95e6..0000000 --- a/src/chat/route/usage.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ - chat::constant::ERR_NODATA, - common::{ - models::usage::GetUserInfo, - utils::{generate_checksum_with_default, get_user_usage}, - }, -}; -use axum::Json; - -use super::token::TokenRequest; - -pub async fn get_user_info(Json(request): Json) -> Json { - let auth_token = match request.token { - Some(token) => token, - None => return Json(GetUserInfo::Error(ERR_NODATA.to_string())), - }; - - // 解析 token 和 checksum - let (token_part, checksum) = if let Some(pos) = auth_token.find("::") { - let (_, rest) = auth_token.split_at(pos + 2); - if let Some(comma_pos) = rest.find(',') { - let (token, checksum) = rest.split_at(comma_pos); - (token, checksum[1..].to_string()) - } else { - (rest, generate_checksum_with_default()) - } - } else if let Some(pos) = auth_token.find("%3A%3A") { - let (_, rest) = auth_token.split_at(pos + 6); - if let Some(comma_pos) = rest.find(',') { - let (token, checksum) = rest.split_at(comma_pos); - (token, checksum[1..].to_string()) - } else { - (rest, generate_checksum_with_default()) - } - } else { - if let Some(comma_pos) = auth_token.find(',') { - let (token, checksum) = auth_token.split_at(comma_pos); - (token, checksum[1..].to_string()) - } else { - (&auth_token[..], generate_checksum_with_default()) - } - }; - - match get_user_usage(&token_part, &checksum).await { - Some(usage) => Json(GetUserInfo::Usage(usage)), - None => Json(GetUserInfo::Error(ERR_NODATA.to_string())), - } -} diff --git a/src/chat/service.rs b/src/chat/service.rs index 28b1fa3..1fcd4f5 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -1,14 +1,15 @@ -use super::constant::AVAILABLE_MODELS; use crate::{ app::{ constant::{ - AUTHORIZATION_BEARER_PREFIX, CURSOR_API2_STREAM_CHAT, FINISH_REASON_STOP, - OBJECT_CHAT_COMPLETION, OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_SUCCESS, + AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, + OBJECT_CHAT_COMPLETION, OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, + STATUS_SUCCESS, }, lazy::AUTH_TOKEN, - model::{AppConfig, AppState, ChatRequest, RequestLog, TokenInfo}, + model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo}, }, chat::{ + constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS}, error::StreamError, model::{ ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, @@ -17,8 +18,10 @@ use crate::{ }, common::{ client::build_client, - models::{error::ChatError, ErrorResponse}, - utils::{get_user_usage, validate_token_and_checksum}, + models::{error::ChatError, userinfo::MembershipType, ErrorResponse}, + utils::{ + format_time_ms, get_token_profile, validate_token_and_checksum, + }, }, }; use axum::{ @@ -93,7 +96,7 @@ pub async fn handle_chat( ))?; // 验证 AuthToken 和 获取 token 信息 - let (auth_token, checksum, alias) = if auth_header == AUTH_TOKEN.as_str() { + let (auth_token, checksum) = if auth_header == AUTH_TOKEN.as_str() { // 如果是管理员Token,使用原有逻辑 static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0); let state_guard = state.lock().await; @@ -108,11 +111,7 @@ pub async fn handle_chat( let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len(); let token_info = &token_infos[index]; - ( - token_info.token.clone(), - token_info.checksum.clone(), - token_info.alias.clone(), - ) + (token_info.token.clone(), token_info.checksum.clone()) } else { // 否则尝试解析token validate_token_and_checksum(auth_header).ok_or(( @@ -121,6 +120,8 @@ pub async fn handle_chat( ))? }; + let current_id: u64; + // 更新请求日志 { let state_clone = state.clone(); @@ -128,29 +129,68 @@ pub async fn handle_chat( state.total_requests += 1; state.active_requests += 1; - // 如果有model且需要获取使用情况,创建后台任务获取 - if let Some(model) = model { - if model.is_usage_check() { - let auth_token_clone = auth_token.clone(); - let checksum_clone = checksum.clone(); - let state_clone = state_clone.clone(); + // 查找最新的相同token的日志,检查使用情况 + let need_profile_check = state + .request_logs + .iter() + .rev() + .find(|log| log.token_info.token == auth_token && log.token_info.profile.is_some()) + .and_then(|log| log.token_info.profile.as_ref()) + .map(|profile| { + if profile.stripe.membership_type != MembershipType::Free { + return false; + } - tokio::spawn(async move { - let usage = get_user_usage(&auth_token_clone, &checksum_clone).await; - let mut state = state_clone.lock().await; - // 根据时间戳找到对应的日志 - if let Some(log) = state - .request_logs - .iter_mut() - .find(|log| log.timestamp == request_time) - { - log.token_info.usage = usage; - } - }); - } + let is_premium = USAGE_CHECK_MODELS.contains(&request.model.as_str()); + let standard = &profile.usage.standard; + let premium = &profile.usage.premium; + + if is_premium { + premium + .max_requests + .map_or(false, |max| premium.num_requests >= max) + } else { + standard + .max_requests + .map_or(false, |max| standard.num_requests >= max) + } + }) + .unwrap_or(false); + + // 如果达到限制,直接返回未授权错误 + if need_profile_check { + state.active_requests -= 1; + state.error_requests += 1; + return Err(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + )); } let next_id = state.request_logs.last().map_or(1, |log| log.id + 1); + current_id = next_id; + + // 如果需要获取用户使用情况,创建后台任务获取profile + if model.map(|m| m.is_usage_check()).unwrap_or(false) { + let auth_token_clone = auth_token.clone(); + let state_clone = state_clone.clone(); + let log_id = next_id; + + 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; + } + }); + } + state.request_logs.push(RequestLog { id: next_id, timestamp: request_time, @@ -158,12 +198,15 @@ pub async fn handle_chat( token_info: TokenInfo { token: auth_token.clone(), checksum: checksum.clone(), - alias: alias.clone(), - usage: None, + profile: None, }, prompt: None, + timing: TimingInfo { + total: 0.0, + first: None, + }, stream: request.stream, - status: "pending", + status: STATUS_PENDING, error: None, }); @@ -173,19 +216,54 @@ pub async fn handle_chat( } // 将消息转换为hex格式 - let hex_data = super::adapter::encode_chat_message(request.messages, &request.model) - .await - .map_err(|_| { - ( + let hex_data = match super::adapter::encode_chat_message(request.messages, &request.model).await + { + Ok(data) => data, + Err(e) => { + 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(e.to_string()); + } + state.active_requests -= 1; + state.error_requests += 1; + return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json( ChatError::RequestFailed("Failed to encode chat message".to_string()).to_json(), ), - ) - })?; + )); + } + }; // 构建请求客户端 - let client = build_client(&auth_token, &checksum, CURSOR_API2_STREAM_CHAT); + // let client_key = match generate_client_key(&checksum) { + // Some(key) => key, + // None => { + // let mut state = state.lock().await; + // if let Some(log) = state + // .request_logs + // .iter_mut() + // .rev() + // .find(|log| log.id == current_id) + // { + // log.status = STATUS_FAILED; + // log.error = Some(ERR_CHECKSUM_NO_GOOD.to_string()); + // } + // state.active_requests -= 1; + // state.error_requests += 1; + // return Err(( + // StatusCode::BAD_REQUEST, + // Json(ChatError::RequestFailed(ERR_CHECKSUM_NO_GOOD.to_string()).to_json()), + // )); + // } + // }; + let client = build_client(&auth_token, &checksum); let response = client.body(hex_data).send().await; // 处理请求结果 @@ -194,7 +272,14 @@ pub async fn handle_chat( // 更新请求日志为成功 { let mut state = state.lock().await; - state.request_logs.last_mut().unwrap().status = STATUS_SUCCESS; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_SUCCESS; + } } resp } @@ -202,10 +287,17 @@ pub async fn handle_chat( // 更新请求日志为失败 { let mut state = state.lock().await; - if let Some(last_log) = state.request_logs.last_mut() { - last_log.status = STATUS_FAILED; - last_log.error = Some(e.to_string()); + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_FAILED; + log.error = Some(e.to_string()); } + state.active_requests -= 1; + state.error_requests += 1; } return Err(( StatusCode::INTERNAL_SERVER_ERROR, @@ -224,6 +316,8 @@ pub async fn handle_chat( 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 stream = { // 创建新的 stream @@ -250,9 +344,16 @@ pub async fn handle_chat( // 更新请求日志为失败 { let mut state = state.lock().await; - if let Some(last_log) = state.request_logs.last_mut() { - last_log.status = STATUS_FAILED; - last_log.error = Some(error_respone.native_code()); + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_FAILED; + log.error = Some(error_respone.native_code()); + log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64()); + state.error_requests += 1; } } return Err(( @@ -279,9 +380,15 @@ pub async fn handle_chat( // 更新请求日志为失败 { let mut state = state.lock().await; - if let Some(last_log) = state.request_logs.last_mut() { - last_log.status = STATUS_FAILED; - last_log.error = Some("Empty stream response".to_string()); + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_FAILED; + log.error = Some("Empty stream response".to_string()); + state.error_requests += 1; } } return Err(( @@ -299,7 +406,9 @@ pub async fn handle_chat( } } .then({ - let buffer = Arc::new(Mutex::new(Vec::new())); // 创建共享的buffer + 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(); @@ -307,6 +416,7 @@ pub async fn handle_chat( 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(); async move { @@ -319,6 +429,14 @@ pub async fn handle_chat( buffer_guard.clear(); let mut response_data = String::new(); + // 记录首字时间(如果还未记录) + 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())); + } + } + + // 处理文本内容 for text in texts { let mut text_guard = full_text.lock().await; text_guard.push_str(&text); @@ -387,6 +505,23 @@ pub async fn handle_chat( // 根据配置决定是否发送最后的 finish_reason let include_finish_reason = AppConfig::get_stop_stream(); + // 计算总时间和首次片段时间 + 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(), @@ -443,13 +578,29 @@ pub async fn handle_chat( .unwrap()) } else { // 非流式响应 - let mut full_text = String::with_capacity(1024); // 预分配合适的容量 + let start_time = std::time::Instant::now(); + let mut first_chunk_received = false; + let mut first_chunk_time = 0.0; + let mut full_text = String::with_capacity(1024); let mut stream = response.bytes_stream(); let mut prompt = None; let mut buffer = Vec::new(); 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; + } + } ( StatusCode::INTERNAL_SERVER_ERROR, Json( @@ -463,14 +614,16 @@ pub async fn handle_chat( 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 text in texts { full_text.push_str(&text); } buffer.clear(); } - Ok(StreamMessage::Incomplete) => { - continue; - } + Ok(StreamMessage::Incomplete) => continue, Ok(StreamMessage::Debug(debug_prompt)) => { prompt = Some(debug_prompt); buffer.clear(); @@ -479,11 +632,23 @@ pub async fn handle_chat( buffer.clear(); } Err(StreamError::ChatError(error)) => { - return Err(( - StatusCode::from_u16(error.status_code()) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - Json(error.to_error_response().to_common()), - )); + let error = error.to_error_response(); + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_FAILED; + log.error = Some(error.native_code()); + log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64()); + state.error_requests += 1; + } + } + return Err((error.status_code(), Json(error.to_common()))); } Err(_) => { buffer.clear(); @@ -492,21 +657,23 @@ pub async fn handle_chat( } } - let prompt_tokens = prompt.as_ref().map(|p| p.len() as u32).unwrap_or(0); - let completion_tokens = full_text.len() as u32; - let total_tokens = prompt_tokens + completion_tokens; - // 检查响应是否为空 if full_text.is_empty() { // 更新请求日志为失败 { let mut state = state.lock().await; - if let Some(last_log) = state.request_logs.last_mut() { - last_log.status = STATUS_FAILED; - last_log.error = Some("Empty response received".to_string()); + if let Some(log) = state + .request_logs + .iter_mut() + .rev() + .find(|log| log.id == current_id) + { + log.status = STATUS_FAILED; + log.error = Some("Empty response received".to_string()); if let Some(p) = prompt { - last_log.prompt = Some(p); + log.prompt = Some(p); } + state.error_requests += 1; } } return Err(( @@ -515,14 +682,6 @@ pub async fn handle_chat( )); } - // 更新请求日志提示词 - { - let mut state = state.lock().await; - if let Some(last_log) = state.request_logs.last_mut() { - last_log.prompt = prompt; - } - } - let response_data = ChatResponse { id: format!("chatcmpl-{}", Uuid::new_v4().simple()), object: OBJECT_CHAT_COMPLETION.to_string(), @@ -538,12 +697,29 @@ pub async fn handle_chat( finish_reason: Some(FINISH_REASON_STOP.to_string()), }], usage: Some(Usage { - prompt_tokens, - completion_tokens, - total_tokens, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, }), }; + { + // 更新请求日志时间信息和状态 + let total_time = format_time_ms(start_time.elapsed().as_secs_f64()); + 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_chunk_time); + log.prompt = prompt; + log.status = STATUS_SUCCESS; + } + } + Ok(Response::builder() .header(CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_string(&response_data).unwrap())) diff --git a/src/common/client.rs b/src/common/client.rs index 25732c3..2301997 100644 --- a/src/common/client.rs +++ b/src/common/client.rs @@ -1,65 +1,220 @@ use crate::app::{ constant::{ - AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_CONNECT_PROTO, CONTENT_TYPE_PROTO, - CURSOR_API2_STREAM_CHAT, HEADER_NAME_GHOST_MODE, - TRUE, FALSE + CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL, + 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_PROXY, }, - lazy::{CURSOR_API2_BASE_URL, CURSOR_API2_HOST}, }; -use reqwest::{header::{CONTENT_TYPE,AUTHORIZATION,USER_AGENT,HOST}, Client}; +use reqwest::header::{ + ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT, + HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT, +}; +use reqwest::{Client, RequestBuilder}; use uuid::Uuid; +macro_rules! def_const { + ($name:ident, $value:expr) => { + const $name: &'static str = $value; + }; +} + +def_const!(SEC_FETCH_DEST, "sec-fetch-dest"); +def_const!(SEC_FETCH_MODE, "sec-fetch-mode"); +def_const!(SEC_FETCH_SITE, "sec-fetch-site"); +def_const!(SEC_GPC, "sec-gpc"); +def_const!(PRIORITY, "priority"); + +def_const!(ONE, "1"); +def_const!(ENCODINGS, "gzip,br"); +def_const!(VALUE_ACCEPT, "*/*"); +def_const!(VALUE_LANGUAGE, "zh-CN"); +def_const!(EMPTY, "empty"); +def_const!(CORS, "cors"); +def_const!(NO_CACHE, "no-cache"); +def_const!(UA_WIN, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"); +def_const!(SAME_ORIGIN, "same-origin"); +def_const!(KEEP_ALIVE, "keep-alive"); +def_const!(TRAILERS, "trailers"); +def_const!(U_EQ_4, "u=4"); + +def_const!(PROXY_HOST, "x-co"); + /// 返回预构建的 Cursor API 客户端 -pub fn build_client(auth_token: &str, checksum: &str, endpoint: &str) -> reqwest::RequestBuilder { - let client = Client::new(); +/// +/// # 参数 +/// +/// * `auth_token` - 授权令牌 +/// * `checksum` - 校验和 +/// * `endpoint` - API 端点路径 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder { let trace_id = Uuid::new_v4().to_string(); - let content_type = if endpoint == CURSOR_API2_STREAM_CHAT { - CONTENT_TYPE_CONNECT_PROTO + + let client = if *USE_PROXY { + Client::new() + .post(&*CURSOR_API2_CHAT_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) } else { - CONTENT_TYPE_PROTO + Client::new() + .post(&*CURSOR_API2_CHAT_URL) + .header(HOST, CURSOR_API2_HOST) }; client - .post(format!("{}{}", *CURSOR_API2_BASE_URL, endpoint)) - .header(CONTENT_TYPE, content_type) - .header( - AUTHORIZATION, - format!("{}{}", AUTHORIZATION_BEARER_PREFIX, auth_token), - ) - .header("connect-accept-encoding", "gzip,br") - .header("connect-protocol-version", "1") + .header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO) + .bearer_auth(auth_token) + .header("connect-accept-encoding", ENCODINGS) + .header("connect-protocol-version", ONE) .header(USER_AGENT, "connect-es/1.6.1") .header("x-amzn-trace-id", format!("Root={}", trace_id)) + // .header("x-client-key", client_key) .header("x-cursor-checksum", checksum) .header("x-cursor-client-version", "0.42.5") .header("x-cursor-timezone", "Asia/Shanghai") - .header(HEADER_NAME_GHOST_MODE, FALSE) + .header(HEADER_NAME_GHOST_MODE, TRUE) .header("x-request-id", trace_id) - .header(HOST, CURSOR_API2_HOST.clone()) + .header(CONNECTION, KEEP_ALIVE) + .header(TRANSFER_ENCODING, "chunked") } /// 返回预构建的获取 Stripe 账户信息的 Cursor API 客户端 -pub fn build_profile_client(auth_token: &str) -> reqwest::RequestBuilder { - let client = Client::new(); +/// +/// # 参数 +/// +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_profile_client(auth_token: &str) -> RequestBuilder { + let client = if *USE_PROXY { + Client::new() + .get(&*CURSOR_API2_STRIPE_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_API2_HOST) + } else { + Client::new() + .get(&*CURSOR_API2_STRIPE_URL) + .header(HOST, CURSOR_API2_HOST) + }; client - .get(format!("https://{}/auth/full_stripe_profile", *CURSOR_API2_HOST)) - .header(HOST, CURSOR_API2_HOST.clone()) .header("sec-ch-ua", "\"Not-A.Brand\";v=\"99\", \"Chromium\";v=\"124\"") - .header(HEADER_NAME_GHOST_MODE, TRUE) + .header(HEADER_NAME_GHOST_MODE, TRUE) .header("sec-ch-ua-mobile", "?0") + .bearer_auth(auth_token) .header( - AUTHORIZATION, - format!("{}{}", AUTHORIZATION_BEARER_PREFIX, auth_token), + USER_AGENT, + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.42.5 Chrome/124.0.6367.243 Electron/30.4.0 Safari/537.36", ) - .header(USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.42.5 Chrome/124.0.6367.243 Electron/30.4.0 Safari/537.36") .header("sec-ch-ua-platform", "\"Windows\"") - .header("accept", "*/*") - .header("origin", "vscode-file://vscode-app") - .header("sec-fetch-site", "cross-site") - .header("sec-fetch-mode", "cors") - .header("sec-fetch-dest", "empty") - .header("accept-encoding", "gzip, deflate, br") - .header("accept-language", "zh-CN") - .header("priority", "u=1, i") + .header(ACCEPT, VALUE_ACCEPT) + .header(ORIGIN, "vscode-file://vscode-app") + .header(SEC_FETCH_SITE, "cross-site") + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_DEST, EMPTY) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(PRIORITY, "u=1, i") +} + +/// 返回预构建的获取使用情况的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `user_id` - 用户 ID +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_usage_client(user_id: &str, auth_token: &str) -> RequestBuilder { + let session_token = format!("{}%3A%3A{}", user_id, auth_token); + + let client = if *USE_PROXY { + Client::new() + .get(&*CURSOR_USAGE_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + Client::new() + .get(&*CURSOR_USAGE_API_URL) + .header(HOST, CURSOR_HOST) + }; + + client + .header(USER_AGENT, UA_WIN) + .header(ACCEPT, VALUE_ACCEPT) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(REFERER, CURSOR_SETTINGS_URL) + .header(DNT, ONE) + .header(SEC_GPC, ONE) + .header(SEC_FETCH_DEST, EMPTY) + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_SITE, SAME_ORIGIN) + .header(CONNECTION, KEEP_ALIVE) + .header(PRAGMA, NO_CACHE) + .header(CACHE_CONTROL, NO_CACHE) + .header(TE, TRAILERS) + .header(PRIORITY, U_EQ_4) + .header( + COOKIE, + &format!("WorkosCursorSessionToken={}", session_token), + ) + .query(&[("user", user_id)]) +} + +/// 返回预构建的获取用户信息的 Cursor API 客户端 +/// +/// # 参数 +/// +/// * `user_id` - 用户 ID +/// * `auth_token` - 授权令牌 +/// +/// # 返回 +/// +/// * `reqwest::RequestBuilder` - 配置好的请求构建器 +pub fn build_userinfo_client(user_id: &str, auth_token: &str) -> RequestBuilder { + let session_token = format!("{}%3A%3A{}", user_id, auth_token); + + let client = if *USE_PROXY { + Client::new() + .get(&*CURSOR_USER_API_URL) + .header(HOST, &*REVERSE_PROXY_HOST) + .header(PROXY_HOST, CURSOR_HOST) + } else { + Client::new() + .get(&*CURSOR_USER_API_URL) + .header(HOST, CURSOR_HOST) + }; + + client + .header(USER_AGENT, UA_WIN) + .header(ACCEPT, VALUE_ACCEPT) + .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) + .header(ACCEPT_ENCODING, ENCODINGS) + .header(REFERER, CURSOR_SETTINGS_URL) + .header(DNT, ONE) + .header(SEC_GPC, ONE) + .header(SEC_FETCH_DEST, EMPTY) + .header(SEC_FETCH_MODE, CORS) + .header(SEC_FETCH_SITE, SAME_ORIGIN) + .header(CONNECTION, KEEP_ALIVE) + .header(PRAGMA, NO_CACHE) + .header(CACHE_CONTROL, NO_CACHE) + .header(TE, TRAILERS) + .header(PRIORITY, U_EQ_4) + .header( + COOKIE, + &format!("WorkosCursorSessionToken={}", session_token), + ) + .query(&[("user", user_id)]) } diff --git a/src/common/models.rs b/src/common/models.rs index bd00ac0..9333c5c 100644 --- a/src/common/models.rs +++ b/src/common/models.rs @@ -1,7 +1,7 @@ pub mod error; pub mod health; pub mod config; -pub mod usage; +pub mod userinfo; use config::ConfigData; diff --git a/src/common/models/usage.rs b/src/common/models/usage.rs deleted file mode 100644 index 4aedcf9..0000000 --- a/src/common/models/usage.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize)] -pub enum GetUserInfo { - #[serde(rename = "usage")] - Usage(UserUsageInfo), - #[serde(rename = "error")] - Error(String), -} - -#[derive(Serialize, Clone)] -pub struct UserUsageInfo { - pub fast_requests: u32, - pub max_fast_requests: u32, - pub mtype: String, - pub trial_days: u32, -} - -#[derive(Deserialize)] -pub struct StripeProfile { - #[serde(rename = "membershipType")] - pub membership_type: String, - #[serde(rename = "daysRemainingOnTrial")] - pub days_remaining_on_trial: i32, -} diff --git a/src/common/models/userinfo.rs b/src/common/models/userinfo.rs new file mode 100644 index 0000000..1feca79 --- /dev/null +++ b/src/common/models/userinfo.rs @@ -0,0 +1,77 @@ +use chrono::{DateTime, Local}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +#[serde(untagged)] +pub enum GetUserInfo { + Usage(TokenProfile), + Error{ error: String }, +} + +#[derive(Serialize, Clone)] +pub struct TokenProfile { + pub usage: UsageProfile, + pub user: UserProfile, + pub stripe: StripeProfile, +} + +#[derive(Deserialize, Serialize, PartialEq, Clone)] +pub enum MembershipType { + #[serde(rename = "free")] + Free, + #[serde(rename = "free_trial")] + FreeTrial, + #[serde(rename = "pro")] + Pro, + #[serde(rename = "enterprise")] + Enterprise, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct StripeProfile { + #[serde(rename(deserialize = "membershipType"))] + pub membership_type: MembershipType, + #[serde(rename(deserialize = "paymentId"), default, skip_serializing_if = "Option::is_none")] + pub payment_id: Option, + #[serde(rename(deserialize = "daysRemainingOnTrial"))] + pub days_remaining_on_trial: u32, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct ModelUsage { + #[serde(rename(deserialize = "numRequests", serialize = "requests"))] + pub num_requests: u32, + #[serde(rename(deserialize = "numTokens", serialize = "tokens"))] + pub num_tokens: u32, + #[serde( + rename(deserialize = "maxRequestUsage"), + skip_serializing_if = "Option::is_none" + )] + pub max_requests: Option, + #[serde( + rename(deserialize = "maxTokenUsage"), + skip_serializing_if = "Option::is_none" + )] + pub max_tokens: Option, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct UsageProfile { + #[serde(rename(deserialize = "gpt-4"))] + pub premium: ModelUsage, + #[serde(rename(deserialize = "gpt-3.5-turbo"))] + pub standard: ModelUsage, + #[serde(rename(deserialize = "gpt-4-32k"))] + pub unknown: ModelUsage, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct UserProfile { + pub email: String, + // pub email_verified: bool, + pub name: String, + #[serde(rename(serialize = "id"))] + pub sub: String, + pub updated_at: DateTime, + // pub picture: Option, +} diff --git a/src/common/utils.rs b/src/common/utils.rs index bbf390a..ef24055 100644 --- a/src/common/utils.rs +++ b/src/common/utils.rs @@ -2,18 +2,16 @@ mod checksum; pub use checksum::*; mod tokens; pub use tokens::*; -use prost::Message as _; -use crate::{app::constant::CURSOR_API2_GET_USER_INFO, chat::aiserver::v1::GetUserInfoResponse}; - -use super::models::usage::{StripeProfile, UserUsageInfo}; +use super::models::userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}; +use crate::app::constant::{FALSE, TRUE}; pub fn parse_bool_from_env(key: &str, default: bool) -> bool { std::env::var(key) .ok() .map(|v| match v.to_lowercase().as_str() { - "true" | "1" => true, - "false" | "0" => false, + TRUE | "1" => true, + FALSE | "0" => false, _ => default, }) .unwrap_or(default) @@ -23,70 +21,127 @@ pub fn parse_string_from_env(key: &str, default: &str) -> String { std::env::var(key).unwrap_or_else(|_| default.to_string()) } -pub fn i32_to_u32(value: i32) -> u32 { - if value < 0 { - 0 - } else { - value as u32 - } +pub fn parse_usize_from_env(key: &str, default: usize) -> usize { + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) } -pub async fn get_user_usage(auth_token: &str, checksum: &str) -> Option { +pub async fn get_token_profile(auth_token: &str) -> Option { + let user_id = extract_user_id(auth_token)?; + // 构建请求客户端 - let client = super::client::build_client(auth_token, checksum, CURSOR_API2_GET_USER_INFO); - let response = client - .body(Vec::new()) + let client = super::client::build_usage_client(&user_id, auth_token); + + // 发送请求并获取响应 + // let response = client.send().await.ok()?; + // let bytes = response.bytes().await?; + // println!("Raw response bytes: {:?}", bytes); + // let usage = serde_json::from_str::(&text).ok()?; + let usage = client .send() .await .ok()? - .bytes() + .json::() .await .ok()?; - let user_info = GetUserInfoResponse::decode(response.as_ref()).ok()?; - let (mtype, trial_days) = get_stripe_profile(auth_token).await?; + let user = get_user_profile(auth_token).await?; - user_info.usage.map(|user_usage| UserUsageInfo { - fast_requests: i32_to_u32(user_usage.gpt4_requests), - max_fast_requests: i32_to_u32(user_usage.gpt4_max_requests), - mtype, - trial_days, + // 从 Stripe 获取用户资料 + let stripe = get_stripe_profile(auth_token).await?; + + // 映射响应数据到 TokenProfile + Some(TokenProfile { + usage, + user, + stripe, }) } -pub async fn get_stripe_profile(auth_token: &str) -> Option<(String, u32)> { +pub async fn get_stripe_profile(auth_token: &str) -> Option { let client = super::client::build_profile_client(auth_token); - let response = client.send().await.ok()?.json::().await.ok()?; - Some((response.membership_type, i32_to_u32(response.days_remaining_on_trial))) + let response = client + .send() + .await + .ok()? + .json::() + .await + .ok()?; + Some(response) } -pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String, Option)> { - // 提取 token、checksum 和可能的 alias - let (token, checksum, alias) = { - // 先尝试提取 alias - let (token_part, alias) = if let Some(pos) = auth_token.find("::") { - let (alias, rest) = auth_token.split_at(pos); - (&rest[2..], Some(alias)) - } else if let Some(pos) = auth_token.find("%3A%3A") { - let (alias, rest) = auth_token.split_at(pos); - (&rest[6..], Some(alias)) - } else { - (auth_token, None) - }; +pub async fn get_user_profile(auth_token: &str) -> Option { + let user_id = extract_user_id(auth_token)?; - // 提取 token 和 checksum - if let Some(comma_pos) = token_part.find(',') { - let (token, checksum) = token_part.split_at(comma_pos); - (token, &checksum[1..], alias) - } else { - return None; // 缺少必要的 checksum + // 构建请求客户端 + let client = super::client::build_userinfo_client(&user_id, auth_token); + + // 发送请求并获取响应 + let user_profile = client.send().await.ok()?.json::().await.ok()?; + + Some(user_profile) +} + +pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> { + // 找最后一个逗号 + let comma_pos = auth_token.rfind(',')?; + let (token_part, checksum) = auth_token.split_at(comma_pos); + let checksum = &checksum[1..]; // 跳过逗号 + + // 解析 token - 为了向前兼容,忽略最后一个:或%3A前的内容 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + let token = match (colon_pos, encoded_colon_pos) { + (None, None) => token_part, // 最简单的构成: token,checksum + (Some(pos1), None) => &token_part[(pos1 + 1)..], + (None, Some(pos2)) => &token_part[(pos2 + 3)..], + (Some(pos1), Some(pos2)) => { + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + &token_part[start..] } }; // 验证 token 和 checksum 有效性 if validate_token(token) && validate_checksum(checksum) { - Some((token.to_string(), checksum.to_string(), alias.map(String::from))) + Some((token.to_string(), checksum.to_string())) } else { None } } + +pub fn extract_token(auth_token: &str) -> Option { + // 解析 token + let token_part = match auth_token.rfind(',') { + Some(pos) => &auth_token[..pos], + None => auth_token + }; + + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + let token = match (colon_pos, encoded_colon_pos) { + (None, None) => token_part, + (Some(pos1), None) => &token_part[(pos1 + 1)..], + (None, Some(pos2)) => &token_part[(pos2 + 3)..], + (Some(pos1), Some(pos2)) => { + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + &token_part[start..] + } + }; + + // 验证 token 有效性 + if validate_token(token) { + Some(token.to_string()) + } else { + None + } +} + +pub fn format_time_ms(seconds: f64) -> f64 { + (seconds * 1000.0).round() / 1000.0 +} diff --git a/src/common/utils/checksum.rs b/src/common/utils/checksum.rs index 01d857b..d97b53f 100644 --- a/src/common/utils/checksum.rs +++ b/src/common/utils/checksum.rs @@ -18,20 +18,29 @@ fn obfuscate_bytes(bytes: &mut [u8]) { } } +fn deobfuscate_bytes(bytes: &mut [u8]) { + let mut prev: u8 = 165; + for (idx, byte) in bytes.iter_mut().enumerate() { + let temp = *byte; + *byte = (*byte).wrapping_sub((idx % 256) as u8) ^ prev; + prev = temp; + } +} + fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() - .as_millis() - / 1_000_000; + .as_secs() + / 1_000; let mut timestamp_bytes = vec![ - ((timestamp >> 40) & 255) as u8, - ((timestamp >> 32) & 255) as u8, - ((timestamp >> 24) & 255) as u8, - ((timestamp >> 16) & 255) as u8, - ((timestamp >> 8) & 255) as u8, - (255 & timestamp) as u8, + ((timestamp >> 8) & 0xFF) as u8, + (0xFF & timestamp) as u8, + ((timestamp >> 24) & 0xFF) as u8, + ((timestamp >> 16) & 0xFF) as u8, + ((timestamp >> 8) & 0xFF) as u8, + (0xFF & timestamp) as u8, ]; obfuscate_bytes(&mut timestamp_bytes); @@ -47,22 +56,200 @@ pub fn generate_checksum_with_default() -> String { generate_checksum(&generate_hash(), Some(&generate_hash())) } +pub fn generate_checksum_with_repair(bad_checksum: &str) -> String { + // 预校验:检查字符串是否为空或只包含合法的Base64字符和'/' + if bad_checksum.is_empty() + || !bad_checksum + .chars() + .all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=')) + { + return generate_checksum_with_default(); + } + + // 尝试修复时间戳头的函数 + fn try_fix_timestamp(timestamp_base64: &str) -> Option { + if let Ok(timestamp_bytes) = BASE64.decode(timestamp_base64) { + if timestamp_bytes.len() == 6 { + let mut fixed_bytes = timestamp_bytes.clone(); + deobfuscate_bytes(&mut fixed_bytes); + + // 检查前3位是否为0 + if fixed_bytes[0..3].iter().all(|&x| x == 0) { + // 从后四位构建时间戳 + let timestamp = ((fixed_bytes[2] as u64) << 24) + | ((fixed_bytes[3] as u64) << 16) + | ((fixed_bytes[4] as u64) << 8) + | (fixed_bytes[5] as u64); + + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + // 修复时间戳字节 + fixed_bytes[0] = fixed_bytes[4]; + fixed_bytes[1] = fixed_bytes[5]; + + obfuscate_bytes(&mut fixed_bytes); + return Some(BASE64.encode(&fixed_bytes)); + } + } + } + } + None + } + + if bad_checksum.len() == 8 { + // 尝试修复时间戳头 + if let Some(fixed_timestamp) = try_fix_timestamp(bad_checksum) { + return format!("{}{}/{}", fixed_timestamp, generate_hash(), generate_hash()); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(bad_checksum) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return format!("{}{}/{}", bad_checksum, generate_hash(), generate_hash()); + } + } + } else if bad_checksum.len() > 8 { + // 处理可能包含hash的情况 + let parts: Vec<&str> = bad_checksum.split('/').collect(); + match parts.len() { + 1 => { + let timestamp_base64 = &bad_checksum[..8]; + let device_id = &bad_checksum[8..]; + + if is_valid_hash(device_id) { + // 先尝试修复时间戳 + if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { + return format!("{}{}/{}", fixed_timestamp, device_id, generate_hash()); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(timestamp_base64) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return format!( + "{}{}/{}", + timestamp_base64, + device_id, + generate_hash() + ); + } + } + } + } + 2 => { + let first_part = parts[0]; + let mac_hash = parts[1]; + + if is_valid_hash(mac_hash) && first_part.len() == mac_hash.len() + 8 { + let timestamp_base64 = &first_part[..8]; + let device_id = &first_part[8..]; + + if is_valid_hash(device_id) { + // 先尝试修复时间戳 + if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { + return format!("{}{}/{}", fixed_timestamp, device_id, mac_hash); + } + + // 验证原始时间戳 + if let Some(timestamp) = extract_time_ks(timestamp_base64) { + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if timestamp <= current_timestamp { + return bad_checksum.to_string(); + } + } + } + } + } + _ => {} + } + } + + // 如果所有修复尝试都失败,返回默认值 + generate_checksum_with_default() +} + +pub fn extract_time_ks(timestamp_base64: &str) -> Option { + let mut timestamp_bytes = BASE64.decode(timestamp_base64).ok()?; + + if timestamp_bytes.len() != 6 { + return None; + } + + deobfuscate_bytes(&mut timestamp_bytes); + + if timestamp_bytes[0] != timestamp_bytes[4] || timestamp_bytes[1] != timestamp_bytes[5] { + return None; + } + + // 使用后四位还原 timestamp + Some( + ((timestamp_bytes[2] as u64) << 24) + | ((timestamp_bytes[3] as u64) << 16) + | ((timestamp_bytes[4] as u64) << 8) + | (timestamp_bytes[5] as u64), + ) +} + pub fn validate_checksum(checksum: &str) -> bool { + // 预校验:检查字符串是否为空或只包含合法的Base64字符和'/' + if checksum.is_empty() + || !checksum + .chars() + .all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=')) + { + return false; + } // 首先检查是否包含基本的 base64 编码部分和 hash 格式的 device_id let parts: Vec<&str> = checksum.split('/').collect(); match parts.len() { // 没有 MAC 地址的情况 1 => { - // 检查是否包含 BASE64 编码的 timestamp (8字符) + 64字符的hash - if checksum.len() != 72 { + if checksum.len() < 72 { // 8 + 64 = 72 return false; } + // 解码前8个字符的base64时间戳 + let timestamp_base64 = &checksum[..8]; + let timestamp = match extract_time_ks(timestamp_base64) { + Some(ts) => ts, + None => return false, + }; + + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + / 1_000; + + if current_timestamp < timestamp { + return false; + } + // 验证 device_id hash 部分 - let device_hash = &checksum[8..]; - is_valid_hash(device_hash) + is_valid_hash(&checksum[8..]) } // 包含 MAC hash 的情况 2 => { @@ -74,6 +261,11 @@ pub fn validate_checksum(checksum: &str) -> bool { return false; } + // 检查第一部分比MAC hash多8个字符 + if first_part.len() != mac_hash.len() + 8 { + return false; + } + // 递归验证第一部分 validate_checksum(first_part) } @@ -82,8 +274,7 @@ pub fn validate_checksum(checksum: &str) -> bool { } fn is_valid_hash(hash: &str) -> bool { - // 检查长度是否为64 - if hash.len() != 64 { + if hash.len() < 64 { return false; } diff --git a/src/common/utils/tokens.rs b/src/common/utils/tokens.rs index 107fdf7..d4b4770 100644 --- a/src/common/utils/tokens.rs +++ b/src/common/utils/tokens.rs @@ -18,14 +18,21 @@ fn normalize_and_write(content: &str, file_path: &str) -> String { normalized } -// 解析token和别名 -fn parse_token_alias(token_part: &str, line: &str) -> Option<(String, Option)> { - match token_part.split("::").collect::>() { - parts if parts.len() == 1 => Some((parts[0].to_string(), None)), - parts if parts.len() == 2 => Some((parts[1].to_string(), Some(parts[0].to_string()))), - _ => { - eprintln!("警告: 忽略无效的行: {}", line); - None +// 解析token +fn parse_token(token_part: &str) -> Option { + // 查找最后一个:或%3A的位置 + let colon_pos = token_part.rfind(':'); + let encoded_colon_pos = token_part.rfind("%3A"); + + match (colon_pos, encoded_colon_pos) { + (None, None) => Some(token_part.to_string()), + (Some(pos1), None) => Some(token_part[(pos1 + 1)..].to_string()), + (None, Some(pos2)) => Some(token_part[(pos2 + 3)..].to_string()), + (Some(pos1), Some(pos2)) => { + // 取较大的位置作为分隔点 + let pos = pos1.max(pos2); + let start = if pos == pos2 { pos + 3 } else { pos + 1 }; + Some(token_part[start..].to_string()) } } } @@ -47,15 +54,15 @@ pub fn load_tokens() -> Vec { // 读取和规范化 token 文件 let token_entries = match std::fs::read_to_string(&token_file) { Ok(content) => { - let normalized = normalize_and_write(&content, &token_file); + let normalized = content.replace("\r\n", "\n"); normalized .lines() .filter_map(|line| { let line = line.trim(); - if line.is_empty() || line.starts_with('#') { + if line.is_empty() || line.starts_with('#') || !validate_token(line) { return None; } - parse_token_alias(line, line) + parse_token(line) }) .collect::>() } @@ -66,7 +73,7 @@ pub fn load_tokens() -> Vec { }; // 读取和规范化 token-list 文件 - let mut token_map: std::collections::HashMap)> = + let mut token_map: std::collections::HashMap = match std::fs::read_to_string(&token_list_file) { Ok(content) => { let normalized = normalize_and_write(&content, &token_list_file); @@ -81,8 +88,8 @@ pub fn load_tokens() -> Vec { let parts: Vec<&str> = line.split(',').collect(); match parts[..] { [token_part, checksum] => { - let (token, alias) = parse_token_alias(token_part, line)?; - Some((token, (checksum.to_string(), alias))) + let token = parse_token(token_part)?; + Some((token, checksum.to_string())) } _ => { eprintln!("警告: 忽略无效的token-list行: {}", line); @@ -99,30 +106,19 @@ pub fn load_tokens() -> Vec { }; // 更新或添加新token - for (token, alias) in token_entries { - if let Some((_, existing_alias)) = token_map.get(&token) { - // 只在alias不同时更新已存在的token - if alias != *existing_alias { - if let Some((checksum, _)) = token_map.get(&token) { - token_map.insert(token.clone(), (checksum.clone(), alias)); - } - } - } else { + for token in token_entries { + if !token_map.contains_key(&token) { // 为新token生成checksum let checksum = generate_checksum_with_default(); - token_map.insert(token, (checksum, alias)); + token_map.insert(token, checksum); } } // 更新 token-list 文件 let token_list_content = token_map .iter() - .map(|(token, (checksum, alias))| { - if let Some(alias) = alias { - format!("{}::{},{}", alias, token, checksum) - } else { - format!("{},{}", token, checksum) - } + .map(|(token, checksum)| { + format!("{},{}", token, checksum) }) .collect::>() .join("\n"); @@ -134,11 +130,10 @@ pub fn load_tokens() -> Vec { // 转换为 TokenInfo vector token_map .into_iter() - .map(|(token, (checksum, alias))| TokenInfo { - token, + .map(|(token, checksum)| TokenInfo { + token: token.clone(), checksum, - alias, - usage: None, + profile: None, }) .collect() } @@ -154,6 +149,10 @@ pub fn validate_token(token: &str) -> bool { return false; } + if parts[0] != "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" { + return false; + } + // 解码 payload let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { Ok(decoded) => decoded, @@ -173,22 +172,13 @@ pub fn validate_token(token: &str) -> bool { }; // 验证必要字段是否存在且有效 - let required_fields = ["sub", "exp", "iss", "aud", "randomness", "time"]; + let required_fields = ["sub", "time", "randomness", "exp", "iss", "scope", "aud"]; for field in required_fields { if !payload_json.get(field).is_some() { return false; } } - // 验证 randomness 长度 - if let Some(randomness) = payload_json["randomness"].as_str() { - if randomness.len() != 18 { - return false; - } - } else { - return false; - } - // 验证 time 字段 if let Some(time) = payload_json["time"].as_str() { // 验证 time 是否为有效的数字字符串 @@ -204,6 +194,15 @@ pub fn validate_token(token: &str) -> bool { return false; } + // 验证 randomness 长度 + if let Some(randomness) = payload_json["randomness"].as_str() { + if randomness.len() != 18 { + return false; + } + } else { + return false; + } + // 验证过期时间 if let Some(exp) = payload_json["exp"].as_i64() { let current_time = chrono::Utc::now().timestamp(); @@ -219,6 +218,11 @@ pub fn validate_token(token: &str) -> bool { return false; } + // 验证授权范围 + if payload_json["scope"].as_str() != Some("openid profile email offline_access") { + return false; + } + // 验证受众 if payload_json["aud"].as_str() != Some("https://cursor.com") { return false; diff --git a/src/main.rs b/src/main.rs index b400e9c..483f1d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod common; use app::{ config::handle_config_update, constant::{ - EMPTY_STRING, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_BASIC_CALIBRATION_PATH, + EMPTY_STRING, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_TOKENINFO_PATH, ROUTE_GET_USER_INFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH, @@ -19,17 +19,19 @@ use axum::{ }; use chat::{ route::{ - get_user_info, handle_about, handle_basic_calibration, handle_config_page, + get_user_info, handle_about, handle_api_page, handle_basic_calibration, handle_config_page, handle_env_example, handle_get_checksum, handle_get_tokeninfo, handle_health, handle_logs, handle_logs_post, handle_readme, handle_root, handle_static, handle_tokeninfo_page, handle_update_tokeninfo, handle_update_tokeninfo_post, }, service::{handle_chat, handle_models}, }; -use common::utils::{load_tokens, parse_bool_from_env, parse_string_from_env}; +use common::utils::{ + load_tokens, parse_bool_from_env, parse_string_from_env, parse_usize_from_env, +}; use std::sync::Arc; use tokio::sync::Mutex; -use tower_http::cors::CorsLayer; +use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer}; #[tokio::main] async fn main() { @@ -87,8 +89,12 @@ async fn main() { .route(ROUTE_STATIC_PATH, get(handle_static)) .route(ROUTE_ABOUT_PATH, get(handle_about)) .route(ROUTE_README_PATH, get(handle_readme)) - .route(ROUTE_BASIC_CALIBRATION_PATH, get(handle_basic_calibration)) - .route(ROUTE_GET_USER_INFO_PATH, get(get_user_info)) + .route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration)) + .route(ROUTE_GET_USER_INFO_PATH, post(get_user_info)) + .route(ROUTE_API_PATH, get(handle_api_page)) + .layer(RequestBodyLimitLayer::new( + 1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2), + )) .layer(CorsLayer::permissive()) .with_state(state); diff --git a/static/api.html b/static/api.html new file mode 100644 index 0000000..e504334 --- /dev/null +++ b/static/api.html @@ -0,0 +1,381 @@ + + + + + + + + API 管理 + + + + + + +
+
+

API 管理

+
Healthy
+
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/static/config.html b/static/config.html index 48585a6..705a7e3 100644 --- a/static/config.html +++ b/static/config.html @@ -3,6 +3,7 @@ + 配置管理 @@ -25,6 +26,7 @@ + @@ -88,11 +90,6 @@ -
- - -
-
+
+ + +
+
diff --git a/static/logs.html b/static/logs.html index 08c77d0..e3526fa 100644 --- a/static/logs.html +++ b/static/logs.html @@ -3,13 +3,14 @@ + 请求日志查看 @@ -190,6 +386,10 @@

活跃请求数

-
+
+

错误请求数

+
-
+

最后更新

-
@@ -200,11 +400,12 @@ - + + @@ -220,9 +421,11 @@
id 时间 模型 Token信息 Prompt用时/首字 流式响应 状态 错误信息
+ +
@@ -232,26 +435,62 @@ - - + + + + + + + + + + + + + + + + + + + + - + + + + + - - + + + + + + + + + + + + +
Token:
别名: + 用户信息 +
邮箱:
用户名:
用户ID:
更新时间:
+ 会员信息 +
会员类型:
试用剩余天数:支付ID:
试用剩余:
使用情况: + 使用量统计 (最近30天) +
Premium models:
Standard models:
Unknown models:
- -
-
-
+
@@ -269,54 +508,135 @@ let refreshInterval; function updateStats(data) { - document.getElementById('totalRequests').textContent = data.total; + document.getElementById('totalRequests').textContent = data.total || 0; document.getElementById('activeRequests').textContent = data.active || 0; + if (data.error) { + document.getElementById('errorRequests').textContent = data.error; + document.getElementById('errorRequests').parentElement.style.display = ''; + } else { + document.getElementById('errorRequests').parentElement.style.display = 'none'; + } document.getElementById('lastUpdate').textContent = new Date(data.timestamp).toLocaleTimeString(); } + function getProgressBarClass(percentage) { + if (percentage <= 60) return 'low'; + if (percentage <= 85) return 'medium'; + return 'high'; + } + + function formatMembershipType(type) { + if (!type) return '-'; + + switch (type) { + case 'free_trial': return 'Pro Trial'; + case 'pro': return 'Pro'; + case 'free': return 'Free'; + case 'enterprise': return 'Business'; + default: return type + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + } + function showTokenModal(tokenInfo) { const modal = document.getElementById('tokenModal'); document.getElementById('modalToken').textContent = tokenInfo.token || '-'; document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-'; - document.getElementById('modalAlias').textContent = tokenInfo.alias || '-'; - // 添加会员类型和试用天数显示 - if (tokenInfo.usage) { - document.getElementById('modalMemberType').textContent = tokenInfo.usage.mtype || '-'; + if (tokenInfo.profile) { + const { user, stripe, usage } = tokenInfo.profile; + + // 设置用户信息 + document.getElementById('modalEmail').textContent = user.email || '-'; + document.getElementById('modalName').textContent = user.name || '-'; + document.getElementById('modalId').textContent = user.id || '-'; + document.getElementById('modalUpdatedAt').textContent = user.updated_at ? new Date(user.updated_at).toLocaleString() : '-'; + + // 设置会员信息 + document.getElementById('modalMemberType').textContent = + formatMembershipType(stripe.membership_type); + document.getElementById('modalPaymentId').textContent = stripe.payment_id || '-'; document.getElementById('modalTrialDays').textContent = - tokenInfo.usage.trial_days > 0 ? `${tokenInfo.usage.trial_days}天` : '-'; + stripe.days_remaining_on_trial > 0 ? `${stripe.days_remaining_on_trial}天` : '-'; + + // 处理使用量信息 + const container = document.getElementById('usageProgressContainer'); + container.innerHTML = ''; + + const models = { + 'modalPremiumUsage': usage.premium, + 'modalStandardUsage': usage.standard, + 'modalUnknownUsage': usage.unknown + }; + + Object.entries(models).forEach(([elementId, modelData]) => { + const element = document.getElementById(elementId); + if (modelData) { + const { requests, tokens, max_requests } = modelData; + + if (max_requests) { + const percentage = (requests / max_requests * 100).toFixed(1); + element.textContent = `${requests}/${max_requests} requests (${percentage}%), ${tokens} tokens`; + + const progressDiv = document.createElement('div'); + progressDiv.className = 'usage-progress-container'; + const colorClass = getProgressBarClass(parseFloat(percentage)); + progressDiv.innerHTML = ` +
+ `; + container.appendChild(progressDiv); + } else { + element.textContent = `${requests} requests, ${tokens} tokens`; + } + } else { + element.textContent = '-'; + } + }); } else { - document.getElementById('modalMemberType').textContent = '-'; - document.getElementById('modalTrialDays').textContent = '-'; - } - - // 获取进度条容器 - const progressContainer = document.querySelector('.usage-progress-container'); - - // 处理使用情况和进度条 - if (tokenInfo.usage) { - const current = tokenInfo.usage.fast_requests; - const max = tokenInfo.usage.max_fast_requests; - const percentage = (current / max * 100).toFixed(1); - - document.getElementById('modalUsage').textContent = - `${current}/${max} (${percentage}%)`; - - // 显示进度条容器 - progressContainer.style.display = 'block'; - // 更新进度条 - const progressBar = document.getElementById('modalUsageBar'); - progressBar.style.width = `${percentage}%`; - } else { - document.getElementById('modalUsage').textContent = '-'; - // 隐藏进度条容器 - progressContainer.style.display = 'none'; + // 如果没有 profile 信息,清空所有字段 + [ + 'modalEmail', + 'modalName', + 'modalId', + 'modalUpdatedAt', + 'modalMemberType', + 'modalPaymentId', + 'modalTrialDays', + 'modalPremiumUsage', + 'modalStandardUsage', + 'modalUnknownUsage' + ].forEach(id => document.getElementById(id).textContent = '-'); + document.getElementById('usageProgressContainer').innerHTML = ''; } modal.style.display = 'block'; } + function formatSimpleTokenInfo(tokenInfo) { + if (!tokenInfo.profile) return '无用户信息'; + + const { user, stripe, usage } = tokenInfo.profile; + const premiumUsage = usage.premium ? + `${usage.premium.requests}/${usage.premium.max_requests}` : '-'; + + const rows = [ + ['邮箱', user.email || '-'], + ...(user.name ? [['用户名', user.name]] : []), + ['会员', formatMembershipType(stripe.membership_type)], + ['Premium', premiumUsage] + ]; + + return rows.map(([label, value]) => ` +
+ ${label}: + ${value} +
+ `).join(''); + } + function updateTable(data) { const tbody = document.getElementById('logsBody'); updateStats(data); @@ -327,18 +647,31 @@ ${new Date(log.timestamp).toLocaleString()} ${log.model} - +
+ +
${log.prompt ? - `` : + `
+ +
` : '-' } + + ${formatTiming(log.timing.total, log.timing.first)} + ${log.stream ? '是' : '否'} ${log.status} ${log.error || '-'} @@ -346,6 +679,42 @@ `).join(''); } + function formatTiming(total, first) { + const formattedTotal = total.toFixed(2); + const formattedFirst = first !== null && first !== undefined ? `${first.toFixed(2)}s` : '-'; + return `${formattedTotal}s / ${formattedFirst}`; + } + + function formatPromptPreview(promptStr) { + try { + const messages = parsePrompt(promptStr); + if (!messages || messages.length === 0) { + return '无对话内容'; + } + + // 获取最后一条消息 + const lastMessage = messages[messages.length - 1]; + const roleLabels = { + 'system': '系统', + 'user': '用户', + 'assistant': '助手' + }; + + return ` +
最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):
+
${lastMessage.content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
') + }
+ `; + } catch (e) { + console.error('预览对话内容失败:', e); + return '无法解析对话内容'; + } + } + async function fetchLogs() { const data = await makeAuthenticatedRequest('/logs'); if (data) { diff --git a/static/readme.html b/static/readme.html index 2d9d327..a288425 100644 --- a/static/readme.html +++ b/static/readme.html @@ -3,20 +3,21 @@

说明

    -
  • 当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
  • -
  • 若发现首字慢,与本程序无关。
  • -
  • 若发现响应出现乱码,也与本程序无关。
  • -
  • 属于官方的问题,请不要像作者反馈。
  • -
  • 本程序拥有堪比客户端原本的速度,甚至可能更快。
  • -
  • 本程序的性能是非常厉害的。
  • +
  • 当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
  • +
  • 若发现首字慢,与本程序无关。
  • +
  • 若发现响应出现乱码,也与本程序无关。
  • +
  • 属于官方的问题,请不要像作者反馈。
  • +
  • 本程序拥有堪比客户端原本的速度,甚至可能更快。
  • +
  • 本程序的性能是非常厉害的。

获取key

    -
  1. 访问 www.cursor.com 并完成注册登录
  2. -
  3. 在浏览器中打开开发者工具(F12)
  4. -
  5. 在 Application-Cookies 中查找名为 WorkosCursorSessionToken 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式,cookie 的值使用冒号 (:) 进行分隔。
  6. +
  7. 访问 www.cursor.com 并完成注册登录
  8. +
  9. 在浏览器中打开开发者工具(F12)
  10. +
  11. 在 Application-Cookies 中查找名为 WorkosCursorSessionToken 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式,cookie + 的值使用冒号 (:) 进行分隔。

配置说明

@@ -24,11 +25,11 @@

环境变量

    -
  • PORT: 服务器端口号(默认:3000)
  • -
  • AUTH_TOKEN: 认证令牌(必须,用于API认证)
  • -
  • ROUTE_PREFIX: 路由前缀(可选)
  • -
  • TOKEN_FILE: token文件路径(默认:.token)
  • -
  • TOKEN_LIST_FILE: token列表文件路径(默认:.token-list)
  • +
  • PORT: 服务器端口号(默认:3000)
  • +
  • AUTH_TOKEN: 认证令牌(必须,用于API认证)
  • +
  • ROUTE_PREFIX: 路由前缀(可选)
  • +
  • TOKEN_FILE: token文件路径(默认:.token)
  • +
  • TOKEN_LIST_FILE: token列表文件路径(默认:.token-list)

更多请查看 /env-example

@@ -36,35 +37,35 @@

Token文件格式

    -
  1. -

    .token 文件:每行一个token,支持以下格式:

    +
  2. +

    .token 文件:每行一个token,支持以下格式:

    -
    # 这是注释
    +    
    # 这是注释
     token1
     # alias与标签的作用差不多
     alias::token2
     
    -

    alias 可以是任意值,用于区分不同的 token,更方便管理,WorkosCursorSessionToken 是相同格式
    -该文件将自动向.token-list文件中追加token,同时自动生成checksum

    -
  3. +

    alias 可以是任意值,用于区分不同的 token,更方便管理,WorkosCursorSessionToken 是相同格式
    + 该文件将自动向.token-list文件中追加token,同时自动生成checksum

    + -
  4. -

    .token-list 文件:每行为token和checksum的对应关系:

    +
  5. +

    .token-list 文件:每行为token和checksum的对应关系:

    -
    # 这里的#表示这行在下次读取要删除
    +    
    # 这里的#表示这行在下次读取要删除
     token1,checksum1
    -# 支持像.token一样的alias,冲突时以.token为准
    -alias::token2,checksum2
    +# alias被舍弃,会自动删除最后一个:或%3A的后一位前的所有内容
    +token2,checksum2
     
    -

    该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:

    +

    该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:

    -
      -
    • 需要删除某个 token
    • -
    • 需要使用已有 checksum 来对应某一个 token
    • -
    -
  6. +
      +
    • 需要删除某个 token
    • +
    • 需要使用已有 checksum 来对应某一个 token
    • +
    +

模型列表

@@ -99,13 +100,15 @@ gemini-2.0-flash-exp

基础对话

    -
  • 接口地址: /v1/chat/completions
  • -
  • 请求方法: POST
  • -
  • 认证方式: Bearer Token -
      -
    1. 使用环境变量 AUTH_TOKEN 进行认证
    2. -
    3. 使用 .token 文件中的令牌列表进行轮询认证
    4. -
  • +
  • 接口地址: /v1/chat/completions
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token +
      +
    1. 使用环境变量 AUTH_TOKEN 进行认证
    2. +
    3. 使用 .token 文件中的令牌列表进行轮询认证
    4. +
    5. 在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭
    6. +
    +

请求格式

@@ -150,9 +153,9 @@ gemini-2.0-flash-exp } ], "usage": { - "prompt_tokens": number, - "completion_tokens": number, - "total_tokens": number + "prompt_tokens": number, // 0 + "completion_tokens": number, // 0 + "total_tokens": number // 0 } }
@@ -173,28 +176,35 @@ data: [DONE]

简易Token信息管理页面

    -
  • 接口地址: /tokeninfo
  • -
  • 请求方法: GET
  • -
  • 响应格式: HTML页面
  • -
  • 功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容
  • +
  • 接口地址: /tokeninfo
  • +
  • 请求方法: GET
  • +
  • 响应格式: HTML页面
  • +
  • 功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容

更新Token信息 (GET)

    -
  • 接口地址: /update-tokeninfo
  • -
  • 请求方法: GET
  • -
  • 认证方式: 不需要
  • -
  • 功能: 请求内容不包括文件内容,直接修改文件,调用重载函数
  • +
  • 接口地址: /update-tokeninfo
  • +
  • 请求方法: GET
  • +
  • 认证方式: 不需要
  • +
  • 功能: 重新加载tokens并更新应用状态
  • +
  • 响应格式:
+
{
+  "status": "success",
+  "message": "Token list has been reloaded"
+}
+
+

更新Token信息 (POST)

    -
  • 接口地址: /update-tokeninfo
  • -
  • 请求方法: POST
  • -
  • 认证方式: Bearer Token
  • -
  • 请求格式:
  • +
  • 接口地址: /update-tokeninfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 请求格式:
{
@@ -204,25 +214,25 @@ data: [DONE]
 
    -
  • 响应格式:
  • +
  • 响应格式:
{
   "status": "success",
-  "message": "Token files have been updated and reloaded",
   "token_file": "string",
   "token_list_file": "string",
-  "token_count": number
+  "tokens_count": number,
+  "message": "Token files have been updated and reloaded"
 }
 

获取Token信息

    -
  • 接口地址: /get-tokeninfo
  • -
  • 请求方法: POST
  • -
  • 认证方式: Bearer Token
  • -
  • 响应格式:
  • +
  • 接口地址: /get-tokeninfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 响应格式:
{
@@ -230,6 +240,7 @@ data: [DONE]
   "token_file": "string",
   "token_list_file": "string",
   "tokens": "string",
+  "tokens_count": number,
   "token_list": "string"
 }
 
@@ -239,36 +250,42 @@ data: [DONE]

配置页面

    -
  • 接口地址: /config
  • -
  • 请求方法: GET
  • -
  • 响应格式: HTML页面
  • -
  • 功能: 提供配置管理界面,可以修改页面内容和系统配置
  • +
  • 接口地址: /config
  • +
  • 请求方法: GET
  • +
  • 响应格式: HTML页面
  • +
  • 功能: 提供配置管理界面,可以修改页面内容和系统配置

更新配置

    -
  • 接口地址: /config
  • -
  • 请求方法: POST
  • -
  • 认证方式: Bearer Token
  • -
  • 请求格式:
  • +
  • 接口地址: /config
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 请求格式:
{
   "action": "get" | "update" | "reset",
   "path": "string",
-  "content": "string",
-  "content_type": "default" | "text" | "html",
-  "enable_stream_check": boolean,
+  "content": {
+    "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_slow_pool": boolean
+  "enable_all_claude": boolean,
+  "check_usage_models": {
+    "type": "none" | "default" | "all" | "list",
+    "content": "string"
+  }
 }
 
    -
  • 响应格式:
  • +
  • 响应格式:
{
@@ -276,43 +293,60 @@ data: [DONE]
   "message": "string",
   "data": {
     "page_content": {
-      "type": "default" | "text" | "html",
+      "type": "default" | "text" | "html", // 对于js和css后两者是一样的
       "content": "string"
     },
     "enable_stream_check": boolean,
-    "vision_ability": "base64" | "url" | "none", 
-    "enable_slow_pool": boolean
+    "include_stop_stream": boolean,
+    "vision_ability": "none" | "base64" | "all",
+    "enable_slow_pool": boolean,
+    "enable_all_claude": boolean,
+    "check_usage_models": {
+      "type": "none" | "default" | "all" | "list",
+      "content": "string"
+    }
   }
 }
 
+

注意:check_usage_models 字段的默认值为:

+ +
{
+  "type": "default",
+  "content": "claude-3-5-sonnet-20241022,claude-3.5-sonnet,gemini-exp-1206,gpt-4,gpt-4-turbo-2024-04-09,gpt-4o,claude-3.5-haiku,gpt-4o-128k,gemini-1.5-flash-500k,claude-3-haiku-200k,claude-3-5-sonnet-200k"
+}
+ +

这些模型将默认进行使用量检查。您可以通过配置接口修改此设置。

+ +

路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。

+

静态资源接口

获取共享样式

    -
  • 接口地址: /static/shared-styles.css
  • -
  • 请求方法: GET
  • -
  • 响应格式: CSS文件
  • -
  • 功能: 获取共享样式表
  • +
  • 接口地址: /static/shared-styles.css
  • +
  • 请求方法: GET
  • +
  • 响应格式: CSS文件
  • +
  • 功能: 获取共享样式表

获取共享脚本

    -
  • 接口地址: /static/shared.js
  • -
  • 请求方法: GET
  • -
  • 响应格式: JavaScript文件
  • -
  • 功能: 获取共享JavaScript代码
  • +
  • 接口地址: /static/shared.js
  • +
  • 请求方法: GET
  • +
  • 响应格式: JavaScript文件
  • +
  • 功能: 获取共享JavaScript代码

环境变量示例

    -
  • 接口地址: /env-example
  • -
  • 请求方法: GET
  • -
  • 响应格式: 文本文件
  • -
  • 功能: 获取环境变量配置示例
  • +
  • 接口地址: /env-example
  • +
  • 请求方法: GET
  • +
  • 响应格式: 文本文件
  • +
  • 功能: 获取环境变量配置示例

其他接口

@@ -320,9 +354,9 @@ data: [DONE]

获取模型列表

    -
  • 接口地址: /v1/models
  • -
  • 请求方法: GET
  • -
  • 响应格式:
  • +
  • 接口地址: /v1/models
  • +
  • 请求方法: GET
  • +
  • 响应格式:
{
@@ -338,12 +372,17 @@ data: [DONE]
 }
 
-

获取随机checksum

+

获取或修复checksum

    -
  • 接口地址: /checksum
  • -
  • 请求方法: GET
  • -
  • 响应格式:
  • +
  • 接口地址: /get-checksum
  • +
  • 请求方法: GET
  • +
  • 请求参数: +
      +
    • bad_checksum: 可选,用于修复的旧版本生成的checksum,也可只传入前8个字符,可用别名checksum
    • +
    +
  • +
  • 响应格式:
{
@@ -351,43 +390,111 @@ data: [DONE]
 }
 
+

说明:

+
    +
  • 如果不提供bad_checksum参数,将生成一个新的随机checksum
  • +
  • 如果提供bad_checksum参数,将尝试修复旧版本的checksum以适配当前版本(v0.1.3-rc.3)使用,修复失败会返回新的checksum
  • +
+

健康检查接口

    -
  • 接口地址: /health/(重定向)
  • -
  • 请求方法: GET
  • -
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)
  • +
  • 接口地址: /health/(重定向)
  • +
  • 请求方法: GET
  • +
  • 认证方式: Bearer Token(可选)
  • +
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML),默认JSON
+
{
+  "status": "success",
+  "version": "string",
+  "uptime": number,
+  "stats": {
+    "started": "string",
+    "total_requests": number,
+    "active_requests": number,
+    "system": {
+      "memory": {
+        "rss": number
+      },
+      "cpu": {
+        "usage": number
+      }
+    }
+  },
+  "models": ["string"],
+  "endpoints": ["string"]
+}
+
+ +

注意:stats 字段仅在请求头中包含正确的 AUTH_TOKEN 时才会返回。否则,该字段将被省略。

+

获取日志接口

    -
  • 接口地址: /logs
  • -
  • 请求方法: GET
  • -
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)
  • +
  • 接口地址: /logs
  • +
  • 请求方法: GET
  • +
  • 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)

获取日志数据

    -
  • 接口地址: /logs
  • -
  • 请求方法: POST
  • -
  • 认证方式: Bearer Token
  • -
  • 响应格式:
  • +
  • 接口地址: /logs
  • +
  • 请求方法: POST
  • +
  • 认证方式: Bearer Token
  • +
  • 响应格式:
{
   "total": number,
   "logs": [
     {
+      "id": number,
       "timestamp": "string",
       "model": "string",
       "token_info": {
         "token": "string",
         "checksum": "string",
-        "alias": "string"
+        "profile": {
+          "usage": {
+            "premium": {
+              "requests": number,
+              "tokens": number,
+              "max_requests": number,
+              "max_tokens": number
+            },
+            "standard": {
+              "requests": number,
+              "tokens": number,
+              "max_requests": number,
+              "max_tokens": number
+            },
+            "unknown": {
+              "requests": 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
+          }
+        }
       },
       "prompt": "string",
+      "timing": {
+        "total": number,
+        "first": number
+      },
       "stream": boolean,
       "status": "string",
       "error": "string"
@@ -397,3 +504,120 @@ data: [DONE]
   "status": "success"
 }
 
+ +

获取用户信息

+ +
    +
  • 接口地址: /get-userinfo
  • +
  • 请求方法: POST
  • +
  • 认证方式: 请求体中包含token
  • +
  • 请求格式:
  • +
+ +
{
+  "token": "string"
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "usage": {
+    "premium": {
+      "requests": number,
+      "tokens": number,
+      "max_requests": number,
+      "max_tokens": number
+    },
+    "standard": {
+      "requests": number,
+      "tokens": number,
+      "max_requests": number,
+      "max_tokens": number
+    },
+    "unknown": {
+      "requests": 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
+  }
+}
+
+ +

如果发生错误,响应格式为:

+ +
{
+  "error": "string"
+}
+
+ +

基础校准

+ +
    +
  • 接口地址: /basic-calibration
  • +
  • 请求方法: POST
  • +
  • 认证方式: 请求体中包含token
  • +
  • 请求格式:
  • +
+ +
{
+  "token": "string"
+}
+
+ +
    +
  • 响应格式:
  • +
+ +
{
+  "status": "success" | "error",
+  "message": "string",
+  "user_id": "string",
+  "create_at": "string",
+  "checksum_time": number
+}
+
+ +

注意: user_id, create_at, 和 checksum_time 字段在校验失败时可能不存在。

+ +

偷偷写在最后的话

+ +

虽然作者觉得收点钱合理,但不强求,要是主动自愿发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧

+ +

不是主动自愿就算了,不是很缺,给了会很感动罢了。

+ +

虽然不是很建议你赞助,但如果你赞助了,大概可以:

+ +
    +
  • 测试版更新
  • +
  • 要求功能
  • +
  • 问题更快解决
  • +
+ +

即使如此,我也保留可以拒绝赞助和拒绝要求的权利。

+ +

求赞助还是有点不要脸了,接下来是吐槽:

+ +

辛辛苦苦做这个也不知道是为了谁,好累。其实还有很多功能可以做,比如直接传token支持配置(其实这个要专门做一个页面)。

+ +

主要没想做用户管理,所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。

+ +

就说这么多,没啥可说的,不管那么多,做就完了。[doge] 自己想象吧。

+ +

为什么一直说要跑路呢?主要是有时Cursor的Claude太假了,堪比gpt-4o-mini,我对比发现真没啥差别,比以前差远了,无力了,所以不太想做了。我也感觉很奇怪。

+ +

查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。

\ No newline at end of file diff --git a/static/shared.js b/static/shared.js index 144771b..1628e1b 100644 --- a/static/shared.js +++ b/static/shared.js @@ -31,6 +31,12 @@ function showMessage(elementId, text, isError = false) { function showGlobalMessage(text, isError = false) { showMessage('message', text, isError); + // 3秒后自动清除消息 + setTimeout(() => { + const msg = document.getElementById('message'); + msg.textContent = ''; + msg.className = 'message'; + }, 3000); } // Token 输入框自动填充和事件绑定 @@ -94,9 +100,9 @@ function parseBooleanFromString(str, defaultValue = null) { if (typeof str !== 'string') { return defaultValue; } - + const lowercaseStr = str.toLowerCase().trim(); - + if (lowercaseStr === 'true' || lowercaseStr === '1') { return true; } else if (lowercaseStr === 'false' || lowercaseStr === '0') { @@ -202,7 +208,7 @@ function formatPromptToTable(messages) { .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); - + // 将HTML标签文本用引号包裹,使其更易读 // return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"'); return escaped; diff --git a/static/tokeninfo.html b/static/tokeninfo.html index 0dd5b26..a285e0f 100644 --- a/static/tokeninfo.html +++ b/static/tokeninfo.html @@ -3,6 +3,7 @@ + Token 信息管理