v0.1.3-rc.3

This commit is contained in:
wisdgod
2025-01-14 09:13:13 +08:00
parent 732cfbc58e
commit 061156fb79
36 changed files with 3585 additions and 787 deletions

View File

@@ -38,5 +38,9 @@ VISION_ABILITY=base64
# 默认提示词 # 默认提示词
DEFAULT_INSTRUCTIONS="Respond in Chinese by default" DEFAULT_INSTRUCTIONS="Respond in Chinese by default"
# 反向代理服务器主机名 # 反向代理服务器主机名,你猜怎么用
CURSOR_API2_HOST= REVERSE_PROXY_HOST=
# 请求体大小限制单位为MB
# 默认为2MB (2,097,152 字节)
REQUEST_BODY_LIMIT_MB=2

View File

@@ -25,7 +25,7 @@ jobs:
sudo apt-get install -y protobuf-compiler pkg-config libssl-dev nodejs npm sudo apt-get install -y protobuf-compiler pkg-config libssl-dev nodejs npm
- name: Build binary - 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 - name: Upload artifact
uses: actions/upload-artifact@v4.5.0 uses: actions/upload-artifact@v4.5.0

View File

@@ -8,6 +8,11 @@ on:
required: true required: true
type: boolean type: boolean
default: false default: false
upload_artifacts:
description: '是否上传构建产物'
required: true
type: boolean
default: true
push: push:
tags: tags:
- 'v*' - 'v*'
@@ -54,7 +59,9 @@ jobs:
network=host network=host
- name: Build and push Docker image - 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: with:
context: . context: .
push: true push: true
@@ -63,3 +70,19 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max 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

231
Cargo.lock generated
View File

@@ -17,18 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@@ -38,6 +26,21 @@ dependencies = [
"memchr", "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]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -65,6 +68,7 @@ version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [ dependencies = [
"brotli",
"flate2", "flate2",
"futures-core", "futures-core",
"memchr", "memchr",
@@ -74,9 +78,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.84" version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -179,9 +183,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@@ -192,6 +196,27 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@@ -224,9 +249,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.7" version = "1.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -245,10 +270,8 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-targets", "windows-targets",
] ]
@@ -304,9 +327,8 @@ dependencies = [
[[package]] [[package]]
name = "cursor-api" name = "cursor-api"
version = "0.1.3" version = "0.1.3-rc.3"
dependencies = [ dependencies = [
"anyhow",
"axum", "axum",
"base64", "base64",
"bytes", "bytes",
@@ -317,14 +339,12 @@ dependencies = [
"gif", "gif",
"hex", "hex",
"image", "image",
"lazy_static",
"paste", "paste",
"prost", "prost",
"prost-build", "prost-build",
"rand", "rand",
"regex", "regex",
"reqwest", "reqwest",
"rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -394,18 +414,6 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -600,30 +608,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 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]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -936,9 +926,9 @@ dependencies = [
[[package]] [[package]]
name = "image-webp" name = "image-webp"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [ dependencies = [
"byteorder-lite", "byteorder-lite",
"quick-error", "quick-error",
@@ -951,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.2", "hashbrown",
] ]
[[package]] [[package]]
@@ -977,42 +967,25 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.76" version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "litemap" name = "litemap"
@@ -1127,7 +1100,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"cfg-if", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
@@ -1189,9 +1162,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -1229,9 +1202,9 @@ dependencies = [
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.25" version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn", "syn",
@@ -1239,9 +1212,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1434,20 +1407,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@@ -1456,11 +1415,11 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.42" version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -1469,9 +1428,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.20" version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@@ -1533,7 +1492,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -1542,9 +1501,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.13.0" version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -1572,9 +1531,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.134" version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1672,9 +1631,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.95" version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1720,7 +1679,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -1761,9 +1720,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.42.0" version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -1777,9 +1736,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1852,9 +1811,11 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"bytes", "bytes",
"http", "http",
"http-body",
"http-body-util",
"pin-project-lite", "pin-project-lite",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@@ -1947,9 +1908,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]
@@ -1983,20 +1944,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"rustversion",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -2008,9 +1970,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.49" version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -2021,9 +1983,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2031,9 +1993,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2044,9 +2006,12 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
@@ -2063,9 +2028,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.76" version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cursor-api" name = "cursor-api"
version = "0.1.3" version = "0.1.3-rc.3"
edition = "2021" edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"] authors = ["wisdgod <nav@wisdgod.com>"]
description = "OpenAI format compatibility layer for the Cursor API" description = "OpenAI format compatibility layer for the Cursor API"
@@ -12,34 +12,31 @@ sha2 = { version = "0.10.8", default-features = false }
serde_json = "1.0.134" serde_json = "1.0.134"
[dependencies] [dependencies]
anyhow = "1.0.95"
axum = { version = "0.7.9", features = ["json"] } axum = { version = "0.7.9", features = ["json"] }
base64 = { version = "0.22.1", default-features = false, features = ["std"] } base64 = { version = "0.22.1", default-features = false, features = ["std"] }
# brotli = { version = "7.0.0", default-features = false, features = ["std"] } # brotli = { version = "7.0.0", default-features = false, features = ["std"] }
bytes = "1.9.0" bytes = "1.9.0"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] } flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] } futures = { version = "0.3.31", default-features = false, features = ["std"] }
gif = { version = "0.13.1", default-features = false, features = ["std"] } gif = { version = "0.13.1", default-features = false, features = ["std"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["std"] }
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] } image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
lazy_static = "1.5.0"
paste = "1.0.15" paste = "1.0.15"
prost = "0.13.4" prost = "0.13.4"
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
rusqlite = { version = "0.32.1", features = ["bundled"], optional = true }
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] }
serde_json = "1.0.134" serde_json = "1.0.135"
sha2 = { version = "0.10.8", default-features = false } sha2 = { version = "0.10.8", default-features = false }
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
tokio = { version = "1.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"] } 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" urlencoding = "2.1.3"
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.1", features = ["v4"] }
[profile.release] [profile.release]
lto = true lto = true

View File

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

View File

@@ -139,7 +139,7 @@ fn minify_assets() -> Result<()> {
fn main() -> Result<()> { fn main() -> Result<()> {
// Proto 文件处理 // 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(); let mut config = prost_build::Config::new();
// config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); // config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
// config.type_attribute( // config.type_attribute(
@@ -147,7 +147,7 @@ fn main() -> Result<()> {
// "#[derive(serde::Serialize, serde::Deserialize)]" // "#[derive(serde::Serialize, serde::Deserialize)]"
// ); // );
config 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(); .unwrap();
// 静态资源文件处理 // 静态资源文件处理

View File

@@ -1,19 +1,16 @@
use super::{ use super::{
constant::AUTHORIZATION_BEARER_PREFIX, constant::AUTHORIZATION_BEARER_PREFIX,
lazy::AUTH_TOKEN, lazy::AUTH_TOKEN,
model::{AppConfig, AppState}, model::AppConfig,
}; };
use crate::common::models::{ use crate::common::models::{
config::{ConfigData, ConfigUpdateRequest}, config::{ConfigData, ConfigUpdateRequest},
ApiStatus, ErrorResponse, NormalResponse, ApiStatus, ErrorResponse, NormalResponse,
}; };
use axum::{ use axum::{
extract::State,
http::{header::AUTHORIZATION, HeaderMap, StatusCode}, http::{header::AUTHORIZATION, HeaderMap, StatusCode},
Json, Json,
}; };
use std::sync::Arc;
use tokio::sync::Mutex;
// 定义处理更新操作的宏 // 定义处理更新操作的宏
macro_rules! handle_update { macro_rules! handle_update {
@@ -54,7 +51,6 @@ macro_rules! handle_reset {
} }
pub async fn handle_config_update( pub async fn handle_config_update(
State(_state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap, headers: HeaderMap,
Json(request): Json<ConfigUpdateRequest>, Json(request): Json<ConfigUpdateRequest>,
) -> Result<Json<NormalResponse<ConfigData>>, (StatusCode, Json<ErrorResponse>)> { ) -> Result<Json<NormalResponse<ConfigData>>, (StatusCode, Json<ErrorResponse>)> {

View File

@@ -15,7 +15,8 @@ def_pub_const!(EMPTY_STRING, "");
def_pub_const!(ROUTE_ROOT_PATH, "/"); def_pub_const!(ROUTE_ROOT_PATH, "/");
def_pub_const!(ROUTE_HEALTH_PATH, "/health"); def_pub_const!(ROUTE_HEALTH_PATH, "/health");
def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum"); 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_LOGS_PATH, "/logs");
def_pub_const!(ROUTE_CONFIG_PATH, "/config"); def_pub_const!(ROUTE_CONFIG_PATH, "/config");
def_pub_const!(ROUTE_TOKENINFO_PATH, "/tokeninfo"); 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_FILE_NAME, ".token");
def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".token-list"); 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_SUCCESS, "success");
def_pub_const!(STATUS_FAILED, "failed"); 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!(TRUE, "true");
def_pub_const!(FALSE, "false"); 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_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_HTML_WITH_UTF8, "text/html;charset=utf-8");
def_pub_const!(CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, "text/plain;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!(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, "chat.completion");
def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk"); def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk");
def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); // def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat");
def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); // def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo");
def_pub_const!(FINISH_REASON_STOP, "stop"); def_pub_const!(FINISH_REASON_STOP, "stop");
def_pub_const!(ERR_UPDATE_CONFIG, "无法更新配置"); def_pub_const!(ERR_UPDATE_CONFIG, "无法更新配置");
def_pub_const!(ERR_RESET_CONFIG, "无法重置配置"); def_pub_const!(ERR_RESET_CONFIG, "无法重置配置");
def_pub_const!(ERR_INVALID_PATH, "无效的路径"); def_pub_const!(ERR_INVALID_PATH, "无效的路径");
// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good");

View File

@@ -1,5 +1,8 @@
use crate::{ 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, common::utils::parse_string_from_env,
}; };
use std::sync::LazyLock; 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!(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_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!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME);
def_pub_static!( def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX));
ROUTE_MODELS_PATH,
format!("{}/v1/models", *ROUTE_PREFIX)
);
def_pub_static!( def_pub_static!(
ROUTE_CHAT_PATH, ROUTE_CHAT_PATH,
format!("{}/v1/chat/completions", *ROUTE_PREFIX) format!("{}/v1/chat/completions", *ROUTE_PREFIX)
@@ -49,10 +49,44 @@ pub fn get_start_time() -> chrono::DateTime<chrono::Local> {
def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Respond in Chinese by default"); 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<String> = LazyLock::new(|| { pub static USE_PROXY: LazyLock<bool> = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty());
format!("https://{}/aiserver.v1.AiService/", *CURSOR_API2_HOST)
pub static CURSOR_API2_CHAT_URL: LazyLock<String> = 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<String> = 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<String> = 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<String> = LazyLock::new(|| {
let host = if *USE_PROXY {
&*REVERSE_PROXY_HOST
} else {
CURSOR_HOST
};
format!("https://{}/api/auth/me", host)
}); });
// pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); // pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));

View File

@@ -2,14 +2,13 @@ use crate::{
app::constant::{ app::constant::{
ERR_INVALID_PATH, ERR_RESET_CONFIG, ERR_UPDATE_CONFIG, ROUTE_ABOUT_PATH, ROUTE_CONFIG_PATH, 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_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 crate::chat::model::Message;
use lazy_static::lazy_static; use std::sync::{LazyLock, RwLock};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::RwLock;
// 页面内容类型枚举 // 页面内容类型枚举
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@@ -81,20 +80,22 @@ pub struct Pages {
pub shared_js_content: PageContent, pub shared_js_content: PageContent,
pub about_content: PageContent, pub about_content: PageContent,
pub readme_content: PageContent, pub readme_content: PageContent,
pub api_content: PageContent,
} }
// 运行时状态 // 运行时状态
pub struct AppState { pub struct AppState {
pub total_requests: u64, pub total_requests: u64,
pub active_requests: u64, pub active_requests: u64,
pub error_requests: u64,
pub request_logs: Vec<RequestLog>, pub request_logs: Vec<RequestLog>,
pub token_infos: Vec<TokenInfo>, pub token_infos: Vec<TokenInfo>,
} }
// 全局配置实例 // 全局配置实例
lazy_static! { pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> = LazyLock::new(|| {
pub static ref APP_CONFIG: RwLock<AppConfig> = RwLock::new(AppConfig::default()); RwLock::new(AppConfig::default())
} });
impl Default for AppConfig { impl Default for AppConfig {
fn default() -> Self { fn default() -> Self {
@@ -105,7 +106,7 @@ impl Default for AppConfig {
slow_pool: false, slow_pool: false,
allow_claude: false, allow_claude: false,
pages: Pages::default(), 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_SHARED_JS_PATH => config.pages.shared_js_content.clone(),
ROUTE_ABOUT_PATH => config.pages.about_content.clone(), ROUTE_ABOUT_PATH => config.pages.about_content.clone(),
ROUTE_README_PATH => config.pages.readme_content.clone(), ROUTE_README_PATH => config.pages.readme_content.clone(),
ROUTE_API_PATH => config.pages.api_content.clone(),
_ => PageContent::default(), _ => PageContent::default(),
}) })
} }
@@ -215,6 +217,7 @@ impl AppConfig {
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content, ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
ROUTE_ABOUT_PATH => config.pages.about_content = content, ROUTE_ABOUT_PATH => config.pages.about_content = content,
ROUTE_README_PATH => config.pages.readme_content = content, ROUTE_README_PATH => config.pages.readme_content = content,
ROUTE_API_PATH => config.pages.api_content = content,
_ => return Err(ERR_INVALID_PATH), _ => return Err(ERR_INVALID_PATH),
} }
Ok(()) Ok(())
@@ -254,6 +257,7 @@ impl AppConfig {
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(), ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(), ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
ROUTE_README_PATH => config.pages.readme_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), _ => return Err(ERR_INVALID_PATH),
} }
Ok(()) Ok(())
@@ -277,6 +281,7 @@ impl AppState {
Self { Self {
total_requests: 0, total_requests: 0,
active_requests: 0, active_requests: 0,
error_requests: 0,
request_logs: Vec::new(), request_logs: Vec::new(),
token_infos, token_infos,
} }
@@ -292,17 +297,19 @@ pub struct RequestLog {
pub token_info: TokenInfo, pub token_info: TokenInfo,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>, pub prompt: Option<String>,
pub timing: TimingInfo,
pub stream: bool, pub stream: bool,
pub status: &'static str, pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>, pub error: Option<String>,
} }
// pub struct PromptList(Option<String>); #[derive(Serialize, Clone)]
pub struct TimingInfo {
// impl PromptList { pub total: f64, // 总用时(秒)
// pub fn to_vec(&self) -> Vec<> #[serde(skip_serializing_if = "Option::is_none")]
// } pub first: Option<f64>, // 首字时间(秒)
}
// 聊天请求 // 聊天请求
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -319,9 +326,7 @@ pub struct TokenInfo {
pub token: String, pub token: String,
pub checksum: String, pub checksum: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>, pub profile: Option<TokenProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<UserUsageInfo>,
} }
// TokenUpdateRequest 结构体 // TokenUpdateRequest 结构体

View File

@@ -5,14 +5,13 @@ use uuid::Uuid;
use crate::app::{ use crate::app::{
constant::EMPTY_STRING, constant::EMPTY_STRING,
model::{AppConfig, VisionAbility},
lazy::DEFAULT_INSTRUCTIONS, lazy::DEFAULT_INSTRUCTIONS,
model::{AppConfig, VisionAbility},
}; };
use super::{ use super::{
aiserver::v1::{ aiserver::v1::{
conversation_message, image_proto, ConversationMessage, ExplicitContext, GetChatRequest, conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
ImageProto, ModelDetails,
}, },
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS}, constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
model::{Message, MessageContent, Role}, model::{Message, MessageContent, Role},
@@ -200,7 +199,7 @@ async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationM
relevant_files: vec![], relevant_files: vec![],
tool_results: vec![], tool_results: vec![],
notepads: vec![], notepads: vec![],
is_capability_iteration: Some(false), is_capability_iteration: None,
capabilities: vec![], capabilities: vec![],
edit_trail_contexts: vec![], edit_trail_contexts: vec![],
suggested_code_blocks: vec![], suggested_code_blocks: vec![],
@@ -329,7 +328,7 @@ async fn process_http_image(
pub async fn encode_chat_message( pub async fn encode_chat_message(
inputs: Vec<Message>, inputs: Vec<Message>,
model_name: &str, model_name: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁 // 在进入异步操作前获取并释放锁
let enable_slow_pool = { let enable_slow_pool = {
if AppConfig::get_slow_pool() { if AppConfig::get_slow_pool() {
@@ -361,7 +360,12 @@ pub async fn encode_chat_message(
model_name: Some(model_name.to_string()), model_name: Some(model_name.to_string()),
api_key: None, api_key: None,
enable_ghost_mode: 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, enable_slow_pool,
openai_api_base_url: None, openai_api_base_url: None,
}), }),
@@ -370,27 +374,23 @@ pub async fn encode_chat_message(
linter_errors: None, linter_errors: None,
summary: None, summary: None,
summary_up_until_index: None, summary_up_until_index: None,
allow_long_file_scan: None, allow_long_file_scan: Some(false),
is_bash: None, is_bash: Some(false),
conversation_id: Uuid::new_v4().to_string(), 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, use_web: None,
quotes: vec![], quotes: vec![],
debug_info: None, debug_info: None,
workspace_id: None, workspace_id: None,
external_links: vec![], external_links: vec![],
commit_notes: vec![], commit_notes: vec![],
long_context_mode: if LONG_CONTEXT_MODELS.contains(&model_name) { long_context_mode: Some(LONG_CONTEXT_MODELS.contains(&model_name)),
Some(true) is_eval: Some(false),
} else {
None
},
is_eval: None,
desired_max_tokens: None, desired_max_tokens: None,
context_ast: None, context_ast: None,
is_composer: None, is_composer: None,
runnable_code_blocks: None, runnable_code_blocks: Some(false),
should_cache: None, should_cache: Some(false),
}; };
let mut encoded = Vec::new(); let mut encoded = Vec::new();

File diff suppressed because it is too large Load Diff

View File

@@ -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 reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -65,7 +65,6 @@ impl ChatError {
Some(error) => match error { Some(error) => match error {
ErrorType::Unspecified => 500, ErrorType::Unspecified => 500,
ErrorType::BadApiKey ErrorType::BadApiKey
| ErrorType::BadUserApiKey
| ErrorType::InvalidAuthId | ErrorType::InvalidAuthId
| ErrorType::AuthTokenNotFound | ErrorType::AuthTokenNotFound
| ErrorType::AuthTokenExpired | ErrorType::AuthTokenExpired

View File

@@ -1,10 +1,17 @@
mod logs; mod logs;
pub use logs::{handle_logs, handle_logs_post}; pub use logs::{handle_logs, handle_logs_post};
mod health; mod health;
pub use health::{handle_root, handle_health}; pub use health::{handle_health, handle_root};
mod token; mod token;
pub use token::{handle_get_checksum, handle_update_tokeninfo, handle_get_tokeninfo, handle_update_tokeninfo_post, handle_tokeninfo_page, handle_basic_calibration}; pub use token::{
mod usage; handle_basic_calibration, handle_get_checksum, handle_get_tokeninfo, handle_tokeninfo_page,
pub use usage::get_user_info; handle_update_tokeninfo, handle_update_tokeninfo_post,
};
mod profile;
pub use profile::get_user_info;
mod config; 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;

26
src/chat/route/api.rs Normal file
View File

@@ -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(),
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
app::{ app::{
constant::{ constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, 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_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH,
ROUTE_GET_CHECKSUM, ROUTE_GET_TOKENINFO_PATH, ROUTE_GET_USER_INFO_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_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
@@ -127,6 +127,7 @@ pub async fn handle_health(
ROUTE_README_PATH, ROUTE_README_PATH,
ROUTE_BASIC_CALIBRATION_PATH, ROUTE_BASIC_CALIBRATION_PATH,
ROUTE_GET_USER_INFO_PATH, ROUTE_GET_USER_INFO_PATH,
ROUTE_API_PATH,
], ],
}) })
} }

View File

@@ -7,7 +7,7 @@ use crate::{
lazy::AUTH_TOKEN, lazy::AUTH_TOKEN,
model::{AppConfig, AppState, PageContent, RequestLog}, model::{AppConfig, AppState, PageContent, RequestLog},
}, },
common::models::ApiStatus, common::{models::ApiStatus, utils::extract_token},
}; };
use axum::{ use axum::{
body::Body, body::Body,
@@ -62,37 +62,16 @@ pub async fn handle_logs_post(
if auth_header == auth_token { if auth_header == auth_token {
return Ok(Json(LogsResponse { return Ok(Json(LogsResponse {
status: ApiStatus::Success, 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(), logs: state.request_logs.clone(),
timestamp: Local::now().to_string(), timestamp: Local::now().to_string(),
})); }));
} }
// 解析 token 和 checksum // 解析 token
let token_part = if let Some(pos) = auth_header.find("::") { let token_part = extract_token(auth_header).ok_or(StatusCode::UNAUTHORIZED)?;
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匹配的日志 // 否则筛选出token匹配的日志
let filtered_logs: Vec<RequestLog> = state let filtered_logs: Vec<RequestLog> = state
@@ -109,7 +88,9 @@ pub async fn handle_logs_post(
Ok(Json(LogsResponse { Ok(Json(LogsResponse {
status: ApiStatus::Success, status: ApiStatus::Success,
total: filtered_logs.len(), total: filtered_logs.len() as u64,
active: None,
error: None,
logs: filtered_logs, logs: filtered_logs,
timestamp: Local::now().to_string(), timestamp: Local::now().to_string(),
})) }))
@@ -118,7 +99,11 @@ pub async fn handle_logs_post(
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct LogsResponse { pub struct LogsResponse {
pub status: ApiStatus, pub status: ApiStatus,
pub total: usize, pub total: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<u64>,
pub logs: Vec<RequestLog>, pub logs: Vec<RequestLog>,
pub timestamp: String, pub timestamp: String,
} }

34
src/chat/route/profile.rs Normal file
View File

@@ -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<TokenRequest>) -> Json<GetUserInfo> {
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(),
}),
}
}

View File

@@ -10,13 +10,12 @@ use crate::{
common::{ common::{
models::{ApiStatus, NormalResponseNoData}, models::{ApiStatus, NormalResponseNoData},
utils::{ utils::{
extract_time, extract_user_id, generate_checksum_with_default, load_tokens, extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, load_tokens, validate_token_and_checksum
validate_checksum, validate_token,
}, },
}, },
}; };
use axum::{ use axum::{
extract::State, extract::{Query, State},
http::{ http::{
header::{AUTHORIZATION, CONTENT_TYPE}, header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, HeaderMap,
@@ -29,14 +28,24 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[derive(Deserialize)]
pub struct ChecksumQuery {
#[serde(default, alias = "checksum")]
pub bad_checksum: Option<String>,
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct ChecksumResponse { pub struct ChecksumResponse {
pub checksum: String, pub checksum: String,
} }
pub async fn handle_get_checksum() -> Json<ChecksumResponse> { pub async fn handle_get_checksum(
let checksum = generate_checksum_with_default(); Query(query): Query<ChecksumQuery>
Json(ChecksumResponse { checksum }) ) -> Json<ChecksumResponse> {
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 处理 // 更新 TokenInfo 处理
@@ -191,6 +200,8 @@ pub struct BasicCalibrationResponse {
pub user_id: Option<String>, pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub create_at: Option<String>, pub create_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_time: Option<u64>,
} }
pub async fn handle_basic_calibration( pub async fn handle_basic_calibration(
@@ -205,65 +216,36 @@ pub async fn handle_basic_calibration(
message: Some("未提供授权令牌".to_string()), message: Some("未提供授权令牌".to_string()),
user_id: None, user_id: None,
create_at: None, create_at: None,
checksum_time: None,
}) })
} }
}; };
// 解析 token 和 checksum // 校验 token 和 checksum
let (token_part, checksum) = if let Some(pos) = auth_token.find("::") { let (token, checksum) = match validate_token_and_checksum(&auth_token) {
let (_, rest) = auth_token.split_at(pos + 2); Some(parts) => parts,
if let Some(comma_pos) = rest.find(',') { None => {
let (token, checksum) = rest.split_at(comma_pos); return Json(BasicCalibrationResponse {
(token, &checksum[1..]) status: ApiStatus::Error,
} else { message: Some("无效令牌或无效校验和".to_string()),
(rest, "") user_id: None,
} create_at: None,
} else if let Some(pos) = auth_token.find("%3A%3A") { checksum_time: None,
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 有效性
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和创建时间 // 提取用户ID和创建时间
let user_id = extract_user_id(token_part); let user_id = extract_user_id(&token);
let create_at = extract_time(token_part).map(|dt| dt.to_string()); let create_at = extract_time(&token).map(|dt| dt.to_string());
let checksum_time = extract_time_ks(&checksum[..8]);
// 返回校结果 // 返回校结果
Json(BasicCalibrationResponse { Json(BasicCalibrationResponse {
status: ApiStatus::Success, status: ApiStatus::Success,
message: Some("成功".to_string()), message: Some("成功".to_string()),
user_id, user_id,
create_at, create_at,
checksum_time,
}) })
} }

View File

@@ -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<TokenRequest>) -> Json<GetUserInfo> {
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())),
}
}

View File

@@ -1,14 +1,15 @@
use super::constant::AVAILABLE_MODELS;
use crate::{ use crate::{
app::{ app::{
constant::{ constant::{
AUTHORIZATION_BEARER_PREFIX, CURSOR_API2_STREAM_CHAT, FINISH_REASON_STOP, AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP,
OBJECT_CHAT_COMPLETION, OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_SUCCESS, OBJECT_CHAT_COMPLETION, OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING,
STATUS_SUCCESS,
}, },
lazy::AUTH_TOKEN, lazy::AUTH_TOKEN,
model::{AppConfig, AppState, ChatRequest, RequestLog, TokenInfo}, model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo},
}, },
chat::{ chat::{
constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS},
error::StreamError, error::StreamError,
model::{ model::{
ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage, ChatResponse, Choice, Delta, Message, MessageContent, ModelsResponse, Role, Usage,
@@ -17,8 +18,10 @@ use crate::{
}, },
common::{ common::{
client::build_client, client::build_client,
models::{error::ChatError, ErrorResponse}, models::{error::ChatError, userinfo::MembershipType, ErrorResponse},
utils::{get_user_usage, validate_token_and_checksum}, utils::{
format_time_ms, get_token_profile, validate_token_and_checksum,
},
}, },
}; };
use axum::{ use axum::{
@@ -93,7 +96,7 @@ pub async fn handle_chat(
))?; ))?;
// 验证 AuthToken 和 获取 token 信息 // 验证 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,使用原有逻辑 // 如果是管理员Token,使用原有逻辑
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0); static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await; 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 index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
let token_info = &token_infos[index]; let token_info = &token_infos[index];
( (token_info.token.clone(), token_info.checksum.clone())
token_info.token.clone(),
token_info.checksum.clone(),
token_info.alias.clone(),
)
} else { } else {
// 否则尝试解析token // 否则尝试解析token
validate_token_and_checksum(auth_header).ok_or(( 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(); let state_clone = state.clone();
@@ -128,29 +129,68 @@ pub async fn handle_chat(
state.total_requests += 1; state.total_requests += 1;
state.active_requests += 1; state.active_requests += 1;
// 如果有model且需要获取使用情况,创建后台任务获取 // 查找最新的相同token的日志,检查使用情况
if let Some(model) = model { let need_profile_check = state
if model.is_usage_check() { .request_logs
let auth_token_clone = auth_token.clone(); .iter()
let checksum_clone = checksum.clone(); .rev()
let state_clone = state_clone.clone(); .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 is_premium = USAGE_CHECK_MODELS.contains(&request.model.as_str());
let usage = get_user_usage(&auth_token_clone, &checksum_clone).await; let standard = &profile.usage.standard;
let mut state = state_clone.lock().await; let premium = &profile.usage.premium;
// 根据时间戳找到对应的日志
if let Some(log) = state if is_premium {
.request_logs premium
.iter_mut() .max_requests
.find(|log| log.timestamp == request_time) .map_or(false, |max| premium.num_requests >= max)
{ } else {
log.token_info.usage = usage; 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); 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 { state.request_logs.push(RequestLog {
id: next_id, id: next_id,
timestamp: request_time, timestamp: request_time,
@@ -158,12 +198,15 @@ pub async fn handle_chat(
token_info: TokenInfo { token_info: TokenInfo {
token: auth_token.clone(), token: auth_token.clone(),
checksum: checksum.clone(), checksum: checksum.clone(),
alias: alias.clone(), profile: None,
usage: None,
}, },
prompt: None, prompt: None,
timing: TimingInfo {
total: 0.0,
first: None,
},
stream: request.stream, stream: request.stream,
status: "pending", status: STATUS_PENDING,
error: None, error: None,
}); });
@@ -173,19 +216,54 @@ pub async fn handle_chat(
} }
// 将消息转换为hex格式 // 将消息转换为hex格式
let hex_data = super::adapter::encode_chat_message(request.messages, &request.model) let hex_data = match super::adapter::encode_chat_message(request.messages, &request.model).await
.await {
.map_err(|_| { 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, StatusCode::INTERNAL_SERVER_ERROR,
Json( Json(
ChatError::RequestFailed("Failed to encode chat message".to_string()).to_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; let response = client.body(hex_data).send().await;
// 处理请求结果 // 处理请求结果
@@ -194,7 +272,14 @@ pub async fn handle_chat(
// 更新请求日志为成功 // 更新请求日志为成功
{ {
let mut state = state.lock().await; 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 resp
} }
@@ -202,10 +287,17 @@ pub async fn handle_chat(
// 更新请求日志为失败 // 更新请求日志为失败
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(last_log) = state.request_logs.last_mut() { if let Some(log) = state
last_log.status = STATUS_FAILED; .request_logs
last_log.error = Some(e.to_string()); .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(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -224,6 +316,8 @@ pub async fn handle_chat(
let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple()); let response_id = format!("chatcmpl-{}", Uuid::new_v4().simple());
let full_text = Arc::new(Mutex::new(String::with_capacity(1024))); let full_text = Arc::new(Mutex::new(String::with_capacity(1024)));
let is_start = Arc::new(AtomicBool::new(true)); let is_start = Arc::new(AtomicBool::new(true));
let start_time = std::time::Instant::now();
let first_chunk_time = Arc::new(Mutex::new(None));
let stream = { let stream = {
// 创建新的 stream // 创建新的 stream
@@ -250,9 +344,16 @@ pub async fn handle_chat(
// 更新请求日志为失败 // 更新请求日志为失败
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(last_log) = state.request_logs.last_mut() { if let Some(log) = state
last_log.status = STATUS_FAILED; .request_logs
last_log.error = Some(error_respone.native_code()); .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(( return Err((
@@ -279,9 +380,15 @@ pub async fn handle_chat(
// 更新请求日志为失败 // 更新请求日志为失败
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(last_log) = state.request_logs.last_mut() { if let Some(log) = state
last_log.status = STATUS_FAILED; .request_logs
last_log.error = Some("Empty stream response".to_string()); .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(( return Err((
@@ -299,7 +406,9 @@ pub async fn handle_chat(
} }
} }
.then({ .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| { move |chunk| {
let buffer = buffer.clone(); let buffer = buffer.clone();
@@ -307,6 +416,7 @@ pub async fn handle_chat(
let model = request.model.clone(); let model = request.model.clone();
let is_start = is_start.clone(); let is_start = is_start.clone();
let full_text = full_text.clone(); let full_text = full_text.clone();
let first_chunk_time = first_chunk_time.clone();
let state = state.clone(); let state = state.clone();
async move { async move {
@@ -319,6 +429,14 @@ pub async fn handle_chat(
buffer_guard.clear(); buffer_guard.clear();
let mut response_data = String::new(); 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 { for text in texts {
let mut text_guard = full_text.lock().await; let mut text_guard = full_text.lock().await;
text_guard.push_str(&text); text_guard.push_str(&text);
@@ -387,6 +505,23 @@ pub async fn handle_chat(
// 根据配置决定是否发送最后的 finish_reason // 根据配置决定是否发送最后的 finish_reason
let include_finish_reason = AppConfig::get_stop_stream(); 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 { if include_finish_reason {
let response = ChatResponse { let response = ChatResponse {
id: response_id.clone(), id: response_id.clone(),
@@ -443,13 +578,29 @@ pub async fn handle_chat(
.unwrap()) .unwrap())
} else { } 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 stream = response.bytes_stream();
let mut prompt = None; let mut prompt = None;
let mut buffer = Vec::new(); let mut buffer = Vec::new();
while let Some(chunk) = stream.next().await { while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| { let chunk = chunk.map_err(|e| {
// 更新请求日志为失败
if let Ok(mut state) = state.try_lock() {
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(format!("Failed to read response chunk: {}", e));
state.error_requests += 1;
}
}
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json( Json(
@@ -463,14 +614,16 @@ pub async fn handle_chat(
match parse_stream_data(&buffer) { match parse_stream_data(&buffer) {
Ok(StreamMessage::Content(texts)) => { 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 { for text in texts {
full_text.push_str(&text); full_text.push_str(&text);
} }
buffer.clear(); buffer.clear();
} }
Ok(StreamMessage::Incomplete) => { Ok(StreamMessage::Incomplete) => continue,
continue;
}
Ok(StreamMessage::Debug(debug_prompt)) => { Ok(StreamMessage::Debug(debug_prompt)) => {
prompt = Some(debug_prompt); prompt = Some(debug_prompt);
buffer.clear(); buffer.clear();
@@ -479,11 +632,23 @@ pub async fn handle_chat(
buffer.clear(); buffer.clear();
} }
Err(StreamError::ChatError(error)) => { Err(StreamError::ChatError(error)) => {
return Err(( let error = error.to_error_response();
StatusCode::from_u16(error.status_code()) // 更新请求日志为失败
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), {
Json(error.to_error_response().to_common()), 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(_) => { Err(_) => {
buffer.clear(); 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() { if full_text.is_empty() {
// 更新请求日志为失败 // 更新请求日志为失败
{ {
let mut state = state.lock().await; let mut state = state.lock().await;
if let Some(last_log) = state.request_logs.last_mut() { if let Some(log) = state
last_log.status = STATUS_FAILED; .request_logs
last_log.error = Some("Empty response received".to_string()); .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 { if let Some(p) = prompt {
last_log.prompt = Some(p); log.prompt = Some(p);
} }
state.error_requests += 1;
} }
} }
return Err(( 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 { let response_data = ChatResponse {
id: format!("chatcmpl-{}", Uuid::new_v4().simple()), id: format!("chatcmpl-{}", Uuid::new_v4().simple()),
object: OBJECT_CHAT_COMPLETION.to_string(), object: OBJECT_CHAT_COMPLETION.to_string(),
@@ -538,12 +697,29 @@ pub async fn handle_chat(
finish_reason: Some(FINISH_REASON_STOP.to_string()), finish_reason: Some(FINISH_REASON_STOP.to_string()),
}], }],
usage: Some(Usage { usage: Some(Usage {
prompt_tokens, prompt_tokens: 0,
completion_tokens, completion_tokens: 0,
total_tokens, 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() Ok(Response::builder()
.header(CONTENT_TYPE, "application/json") .header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&response_data).unwrap())) .body(Body::from(serde_json::to_string(&response_data).unwrap()))

View File

@@ -1,65 +1,220 @@
use crate::app::{ use crate::app::{
constant::{ constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_CONNECT_PROTO, CONTENT_TYPE_PROTO, CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL,
CURSOR_API2_STREAM_CHAT, HEADER_NAME_GHOST_MODE, HEADER_NAME_GHOST_MODE, TRUE,
TRUE, FALSE },
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; 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 客户端 /// 返回预构建的 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 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 { } else {
CONTENT_TYPE_PROTO Client::new()
.post(&*CURSOR_API2_CHAT_URL)
.header(HOST, CURSOR_API2_HOST)
}; };
client client
.post(format!("{}{}", *CURSOR_API2_BASE_URL, endpoint)) .header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO)
.header(CONTENT_TYPE, content_type) .bearer_auth(auth_token)
.header( .header("connect-accept-encoding", ENCODINGS)
AUTHORIZATION, .header("connect-protocol-version", ONE)
format!("{}{}", AUTHORIZATION_BEARER_PREFIX, auth_token),
)
.header("connect-accept-encoding", "gzip,br")
.header("connect-protocol-version", "1")
.header(USER_AGENT, "connect-es/1.6.1") .header(USER_AGENT, "connect-es/1.6.1")
.header("x-amzn-trace-id", format!("Root={}", trace_id)) .header("x-amzn-trace-id", format!("Root={}", trace_id))
// .header("x-client-key", client_key)
.header("x-cursor-checksum", checksum) .header("x-cursor-checksum", checksum)
.header("x-cursor-client-version", "0.42.5") .header("x-cursor-client-version", "0.42.5")
.header("x-cursor-timezone", "Asia/Shanghai") .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("x-request-id", trace_id)
.header(HOST, CURSOR_API2_HOST.clone()) .header(CONNECTION, KEEP_ALIVE)
.header(TRANSFER_ENCODING, "chunked")
} }
/// 返回预构建的获取 Stripe 账户信息的 Cursor API 客户端 /// 返回预构建的获取 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 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("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") .header("sec-ch-ua-mobile", "?0")
.bearer_auth(auth_token)
.header( .header(
AUTHORIZATION, USER_AGENT,
format!("{}{}", AUTHORIZATION_BEARER_PREFIX, auth_token), "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("sec-ch-ua-platform", "\"Windows\"")
.header("accept", "*/*") .header(ACCEPT, VALUE_ACCEPT)
.header("origin", "vscode-file://vscode-app") .header(ORIGIN, "vscode-file://vscode-app")
.header("sec-fetch-site", "cross-site") .header(SEC_FETCH_SITE, "cross-site")
.header("sec-fetch-mode", "cors") .header(SEC_FETCH_MODE, CORS)
.header("sec-fetch-dest", "empty") .header(SEC_FETCH_DEST, EMPTY)
.header("accept-encoding", "gzip, deflate, br") .header(ACCEPT_ENCODING, ENCODINGS)
.header("accept-language", "zh-CN") .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE)
.header("priority", "u=1, i") .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)])
} }

View File

@@ -1,7 +1,7 @@
pub mod error; pub mod error;
pub mod health; pub mod health;
pub mod config; pub mod config;
pub mod usage; pub mod userinfo;
use config::ConfigData; use config::ConfigData;

View File

@@ -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,
}

View File

@@ -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<String>,
#[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<u32>,
#[serde(
rename(deserialize = "maxTokenUsage"),
skip_serializing_if = "Option::is_none"
)]
pub max_tokens: Option<u32>,
}
#[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<Local>,
// pub picture: Option<String>,
}

View File

@@ -2,18 +2,16 @@ mod checksum;
pub use checksum::*; pub use checksum::*;
mod tokens; mod tokens;
pub use tokens::*; pub use tokens::*;
use prost::Message as _;
use crate::{app::constant::CURSOR_API2_GET_USER_INFO, chat::aiserver::v1::GetUserInfoResponse}; use super::models::userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile};
use crate::app::constant::{FALSE, TRUE};
use super::models::usage::{StripeProfile, UserUsageInfo};
pub fn parse_bool_from_env(key: &str, default: bool) -> bool { pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
std::env::var(key) std::env::var(key)
.ok() .ok()
.map(|v| match v.to_lowercase().as_str() { .map(|v| match v.to_lowercase().as_str() {
"true" | "1" => true, TRUE | "1" => true,
"false" | "0" => false, FALSE | "0" => false,
_ => default, _ => default,
}) })
.unwrap_or(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()) std::env::var(key).unwrap_or_else(|_| default.to_string())
} }
pub fn i32_to_u32(value: i32) -> u32 { pub fn parse_usize_from_env(key: &str, default: usize) -> usize {
if value < 0 { std::env::var(key)
0 .ok()
} else { .and_then(|v| v.parse().ok())
value as u32 .unwrap_or(default)
}
} }
pub async fn get_user_usage(auth_token: &str, checksum: &str) -> Option<UserUsageInfo> { pub async fn get_token_profile(auth_token: &str) -> Option<TokenProfile> {
let user_id = extract_user_id(auth_token)?;
// 构建请求客户端 // 构建请求客户端
let client = super::client::build_client(auth_token, checksum, CURSOR_API2_GET_USER_INFO); let client = super::client::build_usage_client(&user_id, auth_token);
let response = client
.body(Vec::new()) // 发送请求并获取响应
// let response = client.send().await.ok()?;
// let bytes = response.bytes().await?;
// println!("Raw response bytes: {:?}", bytes);
// let usage = serde_json::from_str::<UsageProfile>(&text).ok()?;
let usage = client
.send() .send()
.await .await
.ok()? .ok()?
.bytes() .json::<UsageProfile>()
.await .await
.ok()?; .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 { // 从 Stripe 获取用户资料
fast_requests: i32_to_u32(user_usage.gpt4_requests), let stripe = get_stripe_profile(auth_token).await?;
max_fast_requests: i32_to_u32(user_usage.gpt4_max_requests),
mtype, // 映射响应数据到 TokenProfile
trial_days, 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<StripeProfile> {
let client = super::client::build_profile_client(auth_token); let client = super::client::build_profile_client(auth_token);
let response = client.send().await.ok()?.json::<StripeProfile>().await.ok()?; let response = client
Some((response.membership_type, i32_to_u32(response.days_remaining_on_trial))) .send()
.await
.ok()?
.json::<StripeProfile>()
.await
.ok()?;
Some(response)
} }
pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String, Option<String>)> { pub async fn get_user_profile(auth_token: &str) -> Option<UserProfile> {
// 提取 token、checksum 和可能的 alias let user_id = extract_user_id(auth_token)?;
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)
};
// 提取 token 和 checksum // 构建请求客户端
if let Some(comma_pos) = token_part.find(',') { let client = super::client::build_userinfo_client(&user_id, auth_token);
let (token, checksum) = token_part.split_at(comma_pos);
(token, &checksum[1..], alias) // 发送请求并获取响应
} else { let user_profile = client.send().await.ok()?.json::<UserProfile>().await.ok()?;
return None; // 缺少必要的 checksum
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 有效性 // 验证 token 和 checksum 有效性
if validate_token(token) && validate_checksum(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 { } else {
None None
} }
} }
pub fn extract_token(auth_token: &str) -> Option<String> {
// 解析 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
}

View File

@@ -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 { fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis() .as_secs()
/ 1_000_000; / 1_000;
let mut timestamp_bytes = vec![ let mut timestamp_bytes = vec![
((timestamp >> 40) & 255) as u8, ((timestamp >> 8) & 0xFF) as u8,
((timestamp >> 32) & 255) as u8, (0xFF & timestamp) as u8,
((timestamp >> 24) & 255) as u8, ((timestamp >> 24) & 0xFF) as u8,
((timestamp >> 16) & 255) as u8, ((timestamp >> 16) & 0xFF) as u8,
((timestamp >> 8) & 255) as u8, ((timestamp >> 8) & 0xFF) as u8,
(255 & timestamp) as u8, (0xFF & timestamp) as u8,
]; ];
obfuscate_bytes(&mut timestamp_bytes); obfuscate_bytes(&mut timestamp_bytes);
@@ -47,22 +56,200 @@ pub fn generate_checksum_with_default() -> String {
generate_checksum(&generate_hash(), Some(&generate_hash())) 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<String> {
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<u64> {
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 { 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 // 首先检查是否包含基本的 base64 编码部分和 hash 格式的 device_id
let parts: Vec<&str> = checksum.split('/').collect(); let parts: Vec<&str> = checksum.split('/').collect();
match parts.len() { match parts.len() {
// 没有 MAC 地址的情况 // 没有 MAC 地址的情况
1 => { 1 => {
// 检查是否包含 BASE64 编码的 timestamp (8字符) + 64字符的hash if checksum.len() < 72 {
if checksum.len() != 72 {
// 8 + 64 = 72 // 8 + 64 = 72
return false; 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 部分 // 验证 device_id hash 部分
let device_hash = &checksum[8..]; is_valid_hash(&checksum[8..])
is_valid_hash(device_hash)
} }
// 包含 MAC hash 的情况 // 包含 MAC hash 的情况
2 => { 2 => {
@@ -74,6 +261,11 @@ pub fn validate_checksum(checksum: &str) -> bool {
return false; return false;
} }
// 检查第一部分比MAC hash多8个字符
if first_part.len() != mac_hash.len() + 8 {
return false;
}
// 递归验证第一部分 // 递归验证第一部分
validate_checksum(first_part) validate_checksum(first_part)
} }
@@ -82,8 +274,7 @@ pub fn validate_checksum(checksum: &str) -> bool {
} }
fn is_valid_hash(hash: &str) -> bool { fn is_valid_hash(hash: &str) -> bool {
// 检查长度是否为64 if hash.len() < 64 {
if hash.len() != 64 {
return false; return false;
} }

View File

@@ -18,14 +18,21 @@ fn normalize_and_write(content: &str, file_path: &str) -> String {
normalized normalized
} }
// 解析token和别名 // 解析token
fn parse_token_alias(token_part: &str, line: &str) -> Option<(String, Option<String>)> { fn parse_token(token_part: &str) -> Option<String> {
match token_part.split("::").collect::<Vec<_>>() { // 查找最后一个:或%3A的位置
parts if parts.len() == 1 => Some((parts[0].to_string(), None)), let colon_pos = token_part.rfind(':');
parts if parts.len() == 2 => Some((parts[1].to_string(), Some(parts[0].to_string()))), let encoded_colon_pos = token_part.rfind("%3A");
_ => {
eprintln!("警告: 忽略无效的行: {}", line); match (colon_pos, encoded_colon_pos) {
None (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<TokenInfo> {
// 读取和规范化 token 文件 // 读取和规范化 token 文件
let token_entries = match std::fs::read_to_string(&token_file) { let token_entries = match std::fs::read_to_string(&token_file) {
Ok(content) => { Ok(content) => {
let normalized = normalize_and_write(&content, &token_file); let normalized = content.replace("\r\n", "\n");
normalized normalized
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
let line = line.trim(); let line = line.trim();
if line.is_empty() || line.starts_with('#') { if line.is_empty() || line.starts_with('#') || !validate_token(line) {
return None; return None;
} }
parse_token_alias(line, line) parse_token(line)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
@@ -66,7 +73,7 @@ pub fn load_tokens() -> Vec<TokenInfo> {
}; };
// 读取和规范化 token-list 文件 // 读取和规范化 token-list 文件
let mut token_map: std::collections::HashMap<String, (String, Option<String>)> = let mut token_map: std::collections::HashMap<String, String> =
match std::fs::read_to_string(&token_list_file) { match std::fs::read_to_string(&token_list_file) {
Ok(content) => { Ok(content) => {
let normalized = normalize_and_write(&content, &token_list_file); let normalized = normalize_and_write(&content, &token_list_file);
@@ -81,8 +88,8 @@ pub fn load_tokens() -> Vec<TokenInfo> {
let parts: Vec<&str> = line.split(',').collect(); let parts: Vec<&str> = line.split(',').collect();
match parts[..] { match parts[..] {
[token_part, checksum] => { [token_part, checksum] => {
let (token, alias) = parse_token_alias(token_part, line)?; let token = parse_token(token_part)?;
Some((token, (checksum.to_string(), alias))) Some((token, checksum.to_string()))
} }
_ => { _ => {
eprintln!("警告: 忽略无效的token-list行: {}", line); eprintln!("警告: 忽略无效的token-list行: {}", line);
@@ -99,30 +106,19 @@ pub fn load_tokens() -> Vec<TokenInfo> {
}; };
// 更新或添加新token // 更新或添加新token
for (token, alias) in token_entries { for token in token_entries {
if let Some((_, existing_alias)) = token_map.get(&token) { if !token_map.contains_key(&token) {
// 只在alias不同时更新已存在的token
if alias != *existing_alias {
if let Some((checksum, _)) = token_map.get(&token) {
token_map.insert(token.clone(), (checksum.clone(), alias));
}
}
} else {
// 为新token生成checksum // 为新token生成checksum
let checksum = generate_checksum_with_default(); let checksum = generate_checksum_with_default();
token_map.insert(token, (checksum, alias)); token_map.insert(token, checksum);
} }
} }
// 更新 token-list 文件 // 更新 token-list 文件
let token_list_content = token_map let token_list_content = token_map
.iter() .iter()
.map(|(token, (checksum, alias))| { .map(|(token, checksum)| {
if let Some(alias) = alias { format!("{},{}", token, checksum)
format!("{}::{},{}", alias, token, checksum)
} else {
format!("{},{}", token, checksum)
}
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
@@ -134,11 +130,10 @@ pub fn load_tokens() -> Vec<TokenInfo> {
// 转换为 TokenInfo vector // 转换为 TokenInfo vector
token_map token_map
.into_iter() .into_iter()
.map(|(token, (checksum, alias))| TokenInfo { .map(|(token, checksum)| TokenInfo {
token, token: token.clone(),
checksum, checksum,
alias, profile: None,
usage: None,
}) })
.collect() .collect()
} }
@@ -154,6 +149,10 @@ pub fn validate_token(token: &str) -> bool {
return false; return false;
} }
if parts[0] != "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" {
return false;
}
// 解码 payload // 解码 payload
let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { let payload = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(decoded) => decoded, 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 { for field in required_fields {
if !payload_json.get(field).is_some() { if !payload_json.get(field).is_some() {
return false; return false;
} }
} }
// 验证 randomness 长度
if let Some(randomness) = payload_json["randomness"].as_str() {
if randomness.len() != 18 {
return false;
}
} else {
return false;
}
// 验证 time 字段 // 验证 time 字段
if let Some(time) = payload_json["time"].as_str() { if let Some(time) = payload_json["time"].as_str() {
// 验证 time 是否为有效的数字字符串 // 验证 time 是否为有效的数字字符串
@@ -204,6 +194,15 @@ pub fn validate_token(token: &str) -> bool {
return false; 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() { if let Some(exp) = payload_json["exp"].as_i64() {
let current_time = chrono::Utc::now().timestamp(); let current_time = chrono::Utc::now().timestamp();
@@ -219,6 +218,11 @@ pub fn validate_token(token: &str) -> bool {
return false; 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") { if payload_json["aud"].as_str() != Some("https://cursor.com") {
return false; return false;

View File

@@ -5,7 +5,7 @@ mod common;
use app::{ use app::{
config::handle_config_update, config::handle_config_update,
constant::{ 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_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_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, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH,
@@ -19,17 +19,19 @@ use axum::{
}; };
use chat::{ use chat::{
route::{ 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_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_logs_post, handle_readme, handle_root, handle_static, handle_tokeninfo_page,
handle_update_tokeninfo, handle_update_tokeninfo_post, handle_update_tokeninfo, handle_update_tokeninfo_post,
}, },
service::{handle_chat, handle_models}, 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 std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::cors::CorsLayer; use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -87,8 +89,12 @@ async fn main() {
.route(ROUTE_STATIC_PATH, get(handle_static)) .route(ROUTE_STATIC_PATH, get(handle_static))
.route(ROUTE_ABOUT_PATH, get(handle_about)) .route(ROUTE_ABOUT_PATH, get(handle_about))
.route(ROUTE_README_PATH, get(handle_readme)) .route(ROUTE_README_PATH, get(handle_readme))
.route(ROUTE_BASIC_CALIBRATION_PATH, get(handle_basic_calibration)) .route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration))
.route(ROUTE_GET_USER_INFO_PATH, get(get_user_info)) .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()) .layer(CorsLayer::permissive())
.with_state(state); .with_state(state);

381
static/api.html Normal file
View File

@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 管理</title>
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
.status-healthy {
color: var(--success-color);
animation: pulse 2s infinite;
}
.status-error {
color: var(--error-color);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.footer {
margin-top: 2rem;
color: var(--text-secondary);
font-size: 0.9rem;
text-align: center;
}
.copy-button {
position: absolute;
right: 0px;
top: 0px;
padding: 4px;
background: transparent;
min-height: auto;
}
.model-input-container {
position: relative;
}
.custom-suffix {
margin-top: 1rem;
}
.progress-container {
margin-top: 1rem;
}
.usage-progress-container {
width: 100%;
height: 8px;
background: var(--border-color);
border-radius: 4px;
margin: 8px 0;
overflow: hidden;
}
.usage-progress-bar {
height: 100%;
transition: width 0.3s ease;
}
.progress-low {
background: var(--success-color);
}
.progress-medium {
background: #FFA726;
}
.progress-high {
background: var(--error-color);
}
.usage-progress-bar.unlimited {
background: repeating-linear-gradient(45deg,
var(--success-color),
var(--success-color) 10px,
transparent 10px,
transparent 20px);
opacity: 0.5;
}
@media (max-width: 768px) {
.copy-button {
width: auto !important;
}
}
</style>
</head>
<body>
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1>API 管理</h1>
<div id="serverStatus" class="status-healthy">Healthy</div>
</div>
<div class="form-group">
<label for="authToken">AUTH Token</label>
<input type="text" id="authToken" placeholder="请输入 AUTH Token">
</div>
<div class="button-group">
<button onclick="calibrateToken()">校准 Token</button>
<button onclick="getUserInfo()">获取用户信息</button>
<button onclick="getModels()">获取模型列表</button>
</div>
<div class="form-group model-input-container">
<label for="modelList">模型列表</label>
<input type="text" id="modelList" readonly>
<button class="copy-button" onclick="copyModelList()">📋</button>
</div>
<div class="form-group custom-suffix">
<input type="checkbox" id="customSuffix" onchange="toggleCustomSuffix()">
<label for="customSuffix">添加自定义后缀</label>
<input type="text" id="suffixInput" placeholder="@OpenAI" style="display: none;">
</div>
</div>
<div id="userInfoContainer" class="container" style="display: none;">
<h2>用户信息</h2>
<div id="userDetails"></div>
<div id="usageProgressContainer" class="progress-container"></div>
</div>
<div id="message" class="message"></div>
<footer class="footer">
<div id="version"></div>
<div id="uptime"></div>
</footer>
<script>
// 全局变量
let globalModels = [];
// Token校准结果缓存
const calibrationCache = new Map();
// 服务器状态检查
async function checkServerStatus() {
try {
const response = await fetch('/health');
const data = await response.json();
// 更新状态显示
const statusElement = document.getElementById('serverStatus');
statusElement.className = data.status === 'healthy' ? 'status-healthy' : 'status-error';
statusElement.textContent = data.status === 'healthy' ? 'Healthy' : 'Error';
// 更新版本和运行时间
document.getElementById('version').textContent = `版本: ${data.version}`;
document.getElementById('uptime').textContent = formatUptime(data.uptime);
// 保存模型列表
globalModels = data.models || [];
return true;
} catch (error) {
const statusElement = document.getElementById('serverStatus');
statusElement.className = 'status-error';
statusElement.textContent = 'Error';
showGlobalMessage('服务器状态检查失败', true);
return false;
}
}
// 定时检查服务器状态5分钟
function startStatusCheck() {
checkServerStatus();
setInterval(checkServerStatus, 5 * 60 * 1000);
}
// 格式化运行时间
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `运行时间: ${days}${hours}${minutes}`;
}
// 获取模型列表
async function getModels() {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
modelList.value = globalModels.map(model => model + suffix).join(',');
}
// 复制模型列表
function copyModelList() {
const modelList = document.getElementById('modelList');
navigator.clipboard.writeText(modelList.value)
.then(() => showGlobalMessage('已复制到剪贴板'))
.catch(() => showGlobalMessage('复制失败', true));
}
// 切换自定义后缀输入框
function toggleCustomSuffix() {
const suffixInput = document.getElementById('suffixInput');
suffixInput.style.display = document.getElementById('customSuffix').checked ? 'block' : 'none';
if (document.getElementById('customSuffix').checked) {
getModels();
}
}
// Token相关请求
async function makeTokenRequest(url, token) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
showGlobalMessage(`请求失败: ${error.message}`, true);
return null;
}
}
// Token 校准
async function calibrateToken() {
const token = document.getElementById('authToken').value;
if (!token) {
showGlobalMessage('请输入 AUTH Token', true);
return;
}
const result = await makeTokenRequest('/basic-calibration', token);
if (result) {
if (result.status === 'error') {
showGlobalMessage(result.message, true);
} else {
showGlobalMessage('校准成功');
// 缓存校准结果
calibrationCache.set(token, {
user_id: result.user_id,
create_at: result.create_at,
checksum_time: calibResult.checksum_time
});
updateUsageDisplay(null, calibrationCache.get(token));
}
}
}
// 获取用户信息
async function getUserInfo() {
const token = document.getElementById('authToken').value;
if (!token) {
showGlobalMessage('请输入 Token', true);
return;
}
// 如果没有校准缓存,先进行校准
if (!calibrationCache.has(token)) {
const calibResult = await makeTokenRequest('/basic-calibration', token);
if (calibResult && calibResult.status !== 'error') {
calibrationCache.set(token, {
user_id: calibResult.user_id,
create_at: calibResult.create_at,
checksum_time: calibResult.checksum_time
});
}
}
const result = await makeTokenRequest('/get-userinfo', token);
if (result) {
const container = document.getElementById('userInfoContainer');
container.style.display = 'block';
updateUsageDisplay(result, calibrationCache.get(token));
}
}
// 更新使用情况显示
function updateUsageDisplay(tokenInfo, calibInfo) {
const userDetails = document.getElementById('userDetails');
const progressContainer = document.getElementById('usageProgressContainer');
// 清空现有内容
userDetails.innerHTML = '';
progressContainer.innerHTML = '';
// 添加用户基本信息
if (tokenInfo.user || calibInfo) {
const user = tokenInfo.user || {};
userDetails.innerHTML += `
<p>用户ID: ${calibInfo ? calibInfo.user_id : user.id}</p>
<p>邮箱: ${user.email || ''}</p>
<p>用户名: ${user.name || ''}</p>
${user.updated_at ? `<p>更新时间: ${new Date(user.updated_at).toLocaleString()}</p>` : ''}
${calibInfo ? `<p>令牌创建时间: ${new Date(calibInfo.create_at).toLocaleString()}</p>` : ''}
${calibInfo && calibInfo.checksum_time ? `<p>校验和时间区间: ${new Date(calibInfo.checksum_time * 1e6).toLocaleString()} - ${new Date((calibInfo.checksum_time + 1) * 1e6 - 1).toLocaleString()}</p>` : ''}
`;
}
// 添加 Stripe 会员信息
if (tokenInfo.stripe) {
const stripe = tokenInfo.stripe;
userDetails.innerHTML += `
<p>会员类型: ${stripe.membership_type}</p>
${stripe.payment_id ? `<p>付款 ID: ${stripe.payment_id}</p>` : ''}
<p>试用剩余: ${stripe.days_remaining_on_trial} 天</p>
`;
}
// 添加使用情况进度条
if (tokenInfo.usage) {
const usage = tokenInfo.usage;
const models = {
'高级模型': usage.premium,
'标准模型': usage.standard,
'未知模型': usage.unknown
};
Object.entries(models).forEach(([modelName, data]) => {
if (data) {
const isUnlimited = !data.max_requests;
const percentage = isUnlimited ? 100 : (data.requests / data.max_requests * 100).toFixed(1);
const progressClass = isUnlimited ? 'unlimited' : getProgressBarClass(parseFloat(percentage));
progressContainer.innerHTML += `
<div>
<p>${modelName}: ${data.requests}/${isUnlimited ? '∞' : data.max_requests} 请求
${isUnlimited ? '' : `(${percentage}%)`}, ${data.tokens} tokens</p>
<div class="usage-progress-container">
<div class="usage-progress-bar ${progressClass}" style="width: ${percentage}%"></div>
</div>
</div>
`;
}
});
}
}
// 获取进度条样式
function getProgressBarClass(percentage) {
if (percentage < 50) return 'progress-low';
if (percentage < 80) return 'progress-medium';
return 'progress-high';
}
// Token变更时清除缓存
document.getElementById('authToken').addEventListener('change', (e) => {
calibrationCache.delete(e.target.value); // 清除对应的缓存
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
startStatusCheck();
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
});
</script>
</body>
</html>

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置管理</title> <title>配置管理</title>
<!-- 引入共享样式 --> <!-- 引入共享样式 -->
@@ -25,6 +26,7 @@
<option value="/static/shared.js">共享脚本 (/static/shared.js)</option> <option value="/static/shared.js">共享脚本 (/static/shared.js)</option>
<option value="/about">关于页面 (/about)</option> <option value="/about">关于页面 (/about)</option>
<option value="/readme">ReadMe文档 (/readme)</option> <option value="/readme">ReadMe文档 (/readme)</option>
<option value="/api">api调用 (/api)</option>
</select> </select>
</div> </div>
@@ -88,11 +90,6 @@
</select> </select>
</div> </div>
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken">
</div>
<div class="form-group"> <div class="form-group">
<label>使用量检查模型规则:</label> <label>使用量检查模型规则:</label>
<select id="check_usage_models_type"> <select id="check_usage_models_type">
@@ -105,6 +102,11 @@
<input type="text" id="check_usage_models_list" placeholder="模型列表,以逗号分隔" style="display: none;"> <input type="text" id="check_usage_models_list" placeholder="模型列表,以逗号分隔" style="display: none;">
</div> </div>
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken">
</div>
<div class="button-group"> <div class="button-group">
<button onclick="updateConfig('get')">获取配置</button> <button onclick="updateConfig('get')">获取配置</button>
<button onclick="updateConfig('update')">更新配置</button> <button onclick="updateConfig('update')">更新配置</button>

View File

@@ -3,13 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求日志查看</title> <title>请求日志查看</title>
<!-- 引入共享样式 --> <!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css"> <link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script> <script src="/static/shared.js"></script>
<style> <style>
/* 日志页面特定样式 */ /* 创建正确的堆叠上下文 */
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -19,9 +20,16 @@
.stat-card { .stat-card {
background: var(--card-background); background: var(--card-background);
padding: 15px; padding: 20px;
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all var(--transition-fast);
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
} }
.stat-card h4 { .stat-card h4 {
@@ -30,8 +38,10 @@
} }
.stat-value { .stat-value {
font-size: 24px; font-size: 28px;
font-weight: 500; font-weight: 600;
color: var(--primary-color);
margin-top: 4px;
} }
.refresh-container { .refresh-container {
@@ -44,18 +54,22 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: var(--card-background);
padding: 8px 16px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
} }
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1; z-index: 1000;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
overflow-y: auto; overflow-y: hidden;
} }
.modal-content { .modal-content {
@@ -65,8 +79,10 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
width: 90%; width: 90%;
max-width: 800px; max-width: 800px;
max-height: 80vh; max-height: 85vh;
overflow-y: auto; overflow-y: auto;
border: 1px solid var(--border-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
} }
.close { .close {
@@ -76,25 +92,33 @@
} }
.info-button { .info-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
transition: all var(--transition-fast);
background: var(--primary-color-alpha);
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.info-button:hover {
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
} }
.message-table { .message-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 10px; margin-top: 10px;
margin: 0;
border: 1px solid var(--border-color);
} }
.message-table th, .message-table th,
.message-table td { .message-table td {
border: 1px solid var(--border-color); padding: 12px 16px;
padding: 12px; border-bottom: 1px solid var(--border-color);
vertical-align: top; transition: background-color var(--transition-fast);
} }
.message-table td { .message-table td {
@@ -133,11 +157,10 @@
} }
.usage-progress-container { .usage-progress-container {
margin-top: 20px; margin: 16px 0;
width: 100%; height: 8px;
height: 20px; background-color: var(--border-color);
background-color: #f0f0f0; border-radius: 4px;
border-radius: 10px;
overflow: hidden; overflow: hidden;
} }
@@ -145,18 +168,191 @@
height: 100%; height: 100%;
width: 0%; width: 0%;
transition: width 0.3s ease; transition: width 0.3s ease;
background: linear-gradient(to right, background-color: var(--primary-color);
#4caf50 0%, border-radius: 4px;
/* 绿色 */ }
#8bc34a 25%,
/* 浅绿 */ /* 根据使用比例改变颜色 */
#ffeb3b 50%, .usage-progress-bar.low {
/* 黄色 */ background-color: #4caf50;
#ff9800 75%, /* 绿色 */
/* 橙色 */ }
#f44336 100%
/* 红色 */ .usage-progress-bar.medium {
); background-color: #ff9800;
/* 橙色 */
}
.usage-progress-bar.high {
background-color: #f44336;
/* 红色 */
}
/* Token 信息和对话预览的通用样式 */
.token-info-tooltip {
position: relative;
display: inline-block;
}
.token-info-tooltip .tooltip-content {
visibility: hidden;
position: absolute;
z-index: 1002;
background-color: var(--card-background);
padding: 12px 15px;
border-radius: var(--border-radius);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 280px;
left: 50%;
transform: translateX(-50%);
bottom: calc(100% + 8px);
opacity: 0;
transition: opacity 0.3s, visibility 0.3s;
text-align: left;
line-height: 1.6;
border: 1px solid var(--border-color);
pointer-events: none;
}
.token-info-tooltip:hover .tooltip-content {
visibility: visible;
opacity: 1;
}
/* 添加小三角形指示器 */
.token-info-tooltip .tooltip-content::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
border-width: 8px;
border-style: solid;
border-color: var(--card-background) transparent transparent transparent;
}
/* 添加不可见的连接区域 */
.token-info-tooltip::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100%;
width: 100%;
height: 10px;
background: transparent;
}
/* Token 信息特定样式 */
.token-info-tooltip .tooltip-info-row {
display: flex;
justify-content: space-between;
margin: 2px 0;
}
.token-info-tooltip .tooltip-info-row .label {
color: var(--text-secondary);
margin-right: 10px;
}
.token-info-tooltip .tooltip-info-row .value {
font-weight: 500;
word-break: break-word;
}
/* 对话预览特定样式 */
.prompt-preview .tooltip-content {
width: 320px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
word-break: break-word;
}
.prompt-preview .tooltip-content .message-meta {
font-size: 0.8em;
color: var(--text-secondary);
padding: 0;
margin: 0 0 4px 0;
}
.prompt-preview .tooltip-content .last-message {
font-size: 0.9em;
line-height: 1.5;
color: var(--text-primary);
margin: 0;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
/* 优化滚动条样式 */
.prompt-preview .tooltip-content::-webkit-scrollbar {
width: 6px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 3px;
}
.prompt-preview .tooltip-content::-webkit-scrollbar-track {
background-color: var(--card-background);
}
/* 优化表格样式 */
.table-container {
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--border-color);
}
#logsTable {
position: relative;
z-index: 1;
}
#logsTable th {
position: sticky;
top: 0;
z-index: 2;
background: var(--primary-color);
white-space: nowrap;
transition: background-color 0.2s ease;
}
#logsTable td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
transition: background-color var(--transition-fast);
}
/* 响应式优化 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
padding: 16px;
}
.stat-value {
font-size: 24px;
}
.modal-content {
margin: 2% auto;
width: 95%;
padding: 16px;
}
}
/* 优化表格悬停效果 */
#logsTable tr:hover td {
background-color: var(--hover-color, rgba(0, 0, 0, 0.02));
} }
</style> </style>
</head> </head>
@@ -190,6 +386,10 @@
<h4>活跃请求数</h4> <h4>活跃请求数</h4>
<div id="activeRequests" class="stat-value">-</div> <div id="activeRequests" class="stat-value">-</div>
</div> </div>
<div class="stat-card">
<h4>错误请求数</h4>
<div id="errorRequests" class="stat-value">-</div>
</div>
<div class="stat-card"> <div class="stat-card">
<h4>最后更新</h4> <h4>最后更新</h4>
<div id="lastUpdate" class="stat-value">-</div> <div id="lastUpdate" class="stat-value">-</div>
@@ -200,11 +400,12 @@
<table id="logsTable"> <table id="logsTable">
<thead> <thead>
<tr> <tr>
<th>id</th> <th></th>
<th>时间</th> <th>时间</th>
<th>模型</th> <th>模型</th>
<th>Token信息</th> <th>Token信息</th>
<th>Prompt</th> <th>Prompt</th>
<th>用时/首字</th>
<th>流式响应</th> <th>流式响应</th>
<th>状态</th> <th>状态</th>
<th>错误信息</th> <th>错误信息</th>
@@ -220,9 +421,11 @@
<!-- 添加弹窗组件 --> <!-- 添加弹窗组件 -->
<div id="tokenModal" class="modal"> <div id="tokenModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close">&times;</span> <div class="modal-header">
<h3>Token 详细信息</h3> <h3>Token 详细信息</h3>
<table> <span class="close">&times;</span>
</div>
<table class="message-table">
<tr> <tr>
<td>Token:</td> <td>Token:</td>
<td id="modalToken"></td> <td id="modalToken"></td>
@@ -232,26 +435,62 @@
<td id="modalChecksum"></td> <td id="modalChecksum"></td>
</tr> </tr>
<tr> <tr>
<td>别名:</td> <td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
<td id="modalAlias"></td> 用户信息
</td>
</tr>
<tr>
<td>邮箱:</td>
<td id="modalEmail"></td>
</tr>
<tr>
<td>用户名:</td>
<td id="modalName"></td>
</tr>
<tr>
<td>用户ID:</td>
<td id="modalId"></td>
</tr>
<tr>
<td>更新时间:</td>
<td id="modalUpdatedAt"></td>
</tr>
<tr>
<td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
会员信息
</td>
</tr> </tr>
<tr> <tr>
<td>会员类型:</td> <td>会员类型:</td>
<td id="modalMemberType"></td> <td id="modalMemberType"></td>
</tr> </tr>
<tr> <tr>
<td>试用剩余天数:</td> <td>支付ID:</td>
<td id="modalPaymentId"></td>
</tr>
<tr>
<td>试用剩余:</td>
<td id="modalTrialDays"></td> <td id="modalTrialDays"></td>
</tr> </tr>
<tr> <tr>
<td>使用情况:</td> <td colspan="2" style="text-align: center; font-weight: bold; background-color: var(--border-color);">
<td id="modalUsage"></td> 使用量统计 (最近30天)
</td>
</tr>
<tr>
<td>Premium models:</td>
<td id="modalPremiumUsage"></td>
</tr>
<tr>
<td>Standard models:</td>
<td id="modalStandardUsage"></td>
</tr>
<tr>
<td>Unknown models:</td>
<td id="modalUnknownUsage"></td>
</tr> </tr>
</table> </table>
<!-- 添加进度条容器 --> <div id="usageProgressContainer"></div>
<div class="usage-progress-container">
<div id="modalUsageBar" class="usage-progress-bar"></div>
</div>
</div> </div>
</div> </div>
@@ -269,54 +508,135 @@
let refreshInterval; let refreshInterval;
function updateStats(data) { function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total; document.getElementById('totalRequests').textContent = data.total || 0;
document.getElementById('activeRequests').textContent = data.active || 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 = document.getElementById('lastUpdate').textContent =
new Date(data.timestamp).toLocaleTimeString(); 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) { function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal'); const modal = document.getElementById('tokenModal');
document.getElementById('modalToken').textContent = tokenInfo.token || '-'; document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-'; document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
document.getElementById('modalAlias').textContent = tokenInfo.alias || '-';
// 添加会员类型和试用天数显示 if (tokenInfo.profile) {
if (tokenInfo.usage) { const { user, stripe, usage } = tokenInfo.profile;
document.getElementById('modalMemberType').textContent = tokenInfo.usage.mtype || '-';
// 设置用户信息
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 = 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 = `
<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>
`;
container.appendChild(progressDiv);
} else {
element.textContent = `${requests} requests, ${tokens} tokens`;
}
} else {
element.textContent = '-';
}
});
} else { } else {
document.getElementById('modalMemberType').textContent = '-'; // 如果没有 profile 信息,清空所有字段
document.getElementById('modalTrialDays').textContent = '-'; [
} 'modalEmail',
'modalName',
// 获取进度条容器 'modalId',
const progressContainer = document.querySelector('.usage-progress-container'); 'modalUpdatedAt',
'modalMemberType',
// 处理使用情况和进度条 'modalPaymentId',
if (tokenInfo.usage) { 'modalTrialDays',
const current = tokenInfo.usage.fast_requests; 'modalPremiumUsage',
const max = tokenInfo.usage.max_fast_requests; 'modalStandardUsage',
const percentage = (current / max * 100).toFixed(1); 'modalUnknownUsage'
].forEach(id => document.getElementById(id).textContent = '-');
document.getElementById('modalUsage').textContent = document.getElementById('usageProgressContainer').innerHTML = '';
`${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';
} }
modal.style.display = 'block'; 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]) => `
<div class="tooltip-info-row">
<span class="label">${label}:</span>
<span class="value">${value}</span>
</div>
`).join('');
}
function updateTable(data) { function updateTable(data) {
const tbody = document.getElementById('logsBody'); const tbody = document.getElementById('logsBody');
updateStats(data); updateStats(data);
@@ -327,18 +647,31 @@
<td>${new Date(log.timestamp).toLocaleString()}</td> <td>${new Date(log.timestamp).toLocaleString()}</td>
<td>${log.model}</td> <td>${log.model}</td>
<td> <td>
<button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'> <div class="token-info-tooltip">
查看详情 <button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>
</button> 查看详情
<div class="tooltip-content">
${formatSimpleTokenInfo(log.token_info)}
</div>
</button>
</div>
</td> </td>
<td> <td>
${log.prompt ? ${log.prompt ?
`<button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))"> `<div class="token-info-tooltip prompt-preview">
查看对话 <button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">
</button>` : 查看对话
<div class="tooltip-content">
${formatPromptPreview(log.prompt)}
</div>
</button>
</div>` :
'-' '-'
} }
</td> </td>
<td>
${formatTiming(log.timing.total, log.timing.first)}
</td>
<td>${log.stream ? '是' : '否'}</td> <td>${log.stream ? '是' : '否'}</td>
<td>${log.status}</td> <td>${log.status}</td>
<td>${log.error || '-'}</td> <td>${log.error || '-'}</td>
@@ -346,6 +679,42 @@
`).join(''); `).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 `
<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div>
<div class="last-message">${lastMessage.content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
}</div>
`;
} catch (e) {
console.error('预览对话内容失败:', e);
return '无法解析对话内容';
}
}
async function fetchLogs() { async function fetchLogs() {
const data = await makeAuthenticatedRequest('/logs'); const data = await makeAuthenticatedRequest('/logs');
if (data) { if (data) {

View File

@@ -3,20 +3,21 @@
<h2>说明</h2> <h2>说明</h2>
<ul> <ul>
<li>当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。</li> <li>当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。</li>
<li>若发现首字慢,与本程序无关。</li> <li>若发现首字慢,与本程序无关。</li>
<li>若发现响应出现乱码,也与本程序无关。</li> <li>若发现响应出现乱码,也与本程序无关。</li>
<li>属于官方的问题,请不要像作者反馈。</li> <li>属于官方的问题,请不要像作者反馈。</li>
<li>本程序拥有堪比客户端原本的速度,甚至可能更快。</li> <li>本程序拥有堪比客户端原本的速度,甚至可能更快。</li>
<li>本程序的性能是非常厉害的。</li> <li>本程序的性能是非常厉害的。</li>
</ul> </ul>
<h2>获取key</h2> <h2>获取key</h2>
<ol> <ol>
<li>访问 <a href="https://www.cursor.com">www.cursor.com</a> 并完成注册登录</li> <li>访问 <a href="https://www.cursor.com">www.cursor.com</a> 并完成注册登录</li>
<li>在浏览器中打开开发者工具F12</li> <li>在浏览器中打开开发者工具F12</li>
<li>在 Application-Cookies 中查找名为 <code>WorkosCursorSessionToken</code> 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie 的值使用冒号 (:) 进行分隔。</li> <li>在 Application-Cookies 中查找名为 <code>WorkosCursorSessionToken</code> 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie
的值使用冒号 (:) 进行分隔。</li>
</ol> </ol>
<h2>配置说明</h2> <h2>配置说明</h2>
@@ -24,11 +25,11 @@
<h3>环境变量</h3> <h3>环境变量</h3>
<ul> <ul>
<li><code>PORT</code>: 服务器端口号默认3000</li> <li><code>PORT</code>: 服务器端口号默认3000</li>
<li><code>AUTH_TOKEN</code>: 认证令牌必须用于API认证</li> <li><code>AUTH_TOKEN</code>: 认证令牌必须用于API认证</li>
<li><code>ROUTE_PREFIX</code>: 路由前缀(可选)</li> <li><code>ROUTE_PREFIX</code>: 路由前缀(可选)</li>
<li><code>TOKEN_FILE</code>: token文件路径默认.token</li> <li><code>TOKEN_FILE</code>: token文件路径默认.token</li>
<li><code>TOKEN_LIST_FILE</code>: token列表文件路径默认.token-list</li> <li><code>TOKEN_LIST_FILE</code>: token列表文件路径默认.token-list</li>
</ul> </ul>
<p>更多请查看 <code>/env-example</code></p> <p>更多请查看 <code>/env-example</code></p>
@@ -36,35 +37,35 @@
<h3>Token文件格式</h3> <h3>Token文件格式</h3>
<ol> <ol>
<li> <li>
<p><code>.token</code> 文件每行一个token支持以下格式</p> <p><code>.token</code> 文件每行一个token支持以下格式</p>
<pre><code># 这是注释 <pre><code># 这是注释
token1 token1
# alias与标签的作用差不多 # alias与标签的作用差不多
alias::token2 alias::token2
</code></pre> </code></pre>
<p>alias 可以是任意值,用于区分不同的 token更方便管理WorkosCursorSessionToken 是相同格式<br> <p>alias 可以是任意值,用于区分不同的 token更方便管理WorkosCursorSessionToken 是相同格式<br>
该文件将自动向.token-list文件中追加token同时自动生成checksum</p> 该文件将自动向.token-list文件中追加token同时自动生成checksum</p>
</li> </li>
<li> <li>
<p><code>.token-list</code> 文件每行为token和checksum的对应关系</p> <p><code>.token-list</code> 文件每行为token和checksum的对应关系</p>
<pre><code># 这里的#表示这行在下次读取要删除 <pre><code># 这里的#表示这行在下次读取要删除
token1,checksum1 token1,checksum1
# 支持像.token一样的alias冲突时以.token为准 # alias被舍弃会自动删除最后一个:或%3A的后一位前的所有内容
alias::token2,checksum2 token2,checksum2
</code></pre> </code></pre>
<p>该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:</p> <p>该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:</p>
<ul> <ul>
<li>需要删除某个 token</li> <li>需要删除某个 token</li>
<li>需要使用已有 checksum 来对应某一个 token</li> <li>需要使用已有 checksum 来对应某一个 token</li>
</ul> </ul>
</li> </li>
</ol> </ol>
<h3>模型列表</h3> <h3>模型列表</h3>
@@ -99,13 +100,15 @@ gemini-2.0-flash-exp
<h2>基础对话</h2> <h2>基础对话</h2>
<ul> <ul>
<li>接口地址: <code>/v1/chat/completions</code></li> <li>接口地址: <code>/v1/chat/completions</code></li>
<li>请求方法: POST</li> <li>请求方法: POST</li>
<li>认证方式: Bearer Token <li>认证方式: Bearer Token
<ol> <ol>
<li>使用环境变量 <code>AUTH_TOKEN</code> 进行认证</li> <li>使用环境变量 <code>AUTH_TOKEN</code> 进行认证</li>
<li>使用 <code>.token</code> 文件中的令牌列表进行轮询认证</li> <li>使用 <code>.token</code> 文件中的令牌列表进行轮询认证</li>
</ol></li> <li>在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭</li>
</ol>
</li>
</ul> </ul>
<h3>请求格式</h3> <h3>请求格式</h3>
@@ -150,9 +153,9 @@ gemini-2.0-flash-exp
} }
], ],
"usage": { "usage": {
"prompt_tokens": number, "prompt_tokens": number, // 0
"completion_tokens": number, "completion_tokens": number, // 0
"total_tokens": number "total_tokens": number // 0
} }
} }
</code></pre> </code></pre>
@@ -173,28 +176,35 @@ data: [DONE]
<h3>简易Token信息管理页面</h3> <h3>简易Token信息管理页面</h3>
<ul> <ul>
<li>接口地址: <code>/tokeninfo</code></li> <li>接口地址: <code>/tokeninfo</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: HTML页面</li> <li>响应格式: HTML页面</li>
<li>功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容</li> <li>功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容</li>
</ul> </ul>
<h3>更新Token信息 (GET)</h3> <h3>更新Token信息 (GET)</h3>
<ul> <ul>
<li>接口地址: <code>/update-tokeninfo</code></li> <li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>认证方式: 不需要</li> <li>认证方式: 不需要</li>
<li>功能: 请求内容不包括文件内容,直接修改文件,调用重载函数</li> <li>功能: 重新加载tokens并更新应用状态</li>
<li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{
"status": "success",
"message": "Token list has been reloaded"
}
</code></pre>
<h3>更新Token信息 (POST)</h3> <h3>更新Token信息 (POST)</h3>
<ul> <ul>
<li>接口地址: <code>/update-tokeninfo</code></li> <li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: POST</li> <li>请求方法: POST</li>
<li>认证方式: Bearer Token</li> <li>认证方式: Bearer Token</li>
<li>请求格式:</li> <li>请求格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
@@ -204,25 +214,25 @@ data: [DONE]
</code></pre> </code></pre>
<ul> <ul>
<li>响应格式:</li> <li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
"status": "success", "status": "success",
"message": "Token files have been updated and reloaded",
"token_file": "string", "token_file": "string",
"token_list_file": "string", "token_list_file": "string",
"token_count": number "tokens_count": number,
"message": "Token files have been updated and reloaded"
} }
</code></pre> </code></pre>
<h3>获取Token信息</h3> <h3>获取Token信息</h3>
<ul> <ul>
<li>接口地址: <code>/get-tokeninfo</code></li> <li>接口地址: <code>/get-tokeninfo</code></li>
<li>请求方法: POST</li> <li>请求方法: POST</li>
<li>认证方式: Bearer Token</li> <li>认证方式: Bearer Token</li>
<li>响应格式:</li> <li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
@@ -230,6 +240,7 @@ data: [DONE]
"token_file": "string", "token_file": "string",
"token_list_file": "string", "token_list_file": "string",
"tokens": "string", "tokens": "string",
"tokens_count": number,
"token_list": "string" "token_list": "string"
} }
</code></pre> </code></pre>
@@ -239,36 +250,42 @@ data: [DONE]
<h3>配置页面</h3> <h3>配置页面</h3>
<ul> <ul>
<li>接口地址: <code>/config</code></li> <li>接口地址: <code>/config</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: HTML页面</li> <li>响应格式: HTML页面</li>
<li>功能: 提供配置管理界面,可以修改页面内容和系统配置</li> <li>功能: 提供配置管理界面,可以修改页面内容和系统配置</li>
</ul> </ul>
<h3>更新配置</h3> <h3>更新配置</h3>
<ul> <ul>
<li>接口地址: <code>/config</code></li> <li>接口地址: <code>/config</code></li>
<li>请求方法: POST</li> <li>请求方法: POST</li>
<li>认证方式: Bearer Token</li> <li>认证方式: Bearer Token</li>
<li>请求格式:</li> <li>请求格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
"action": "get" | "update" | "reset", "action": "get" | "update" | "reset",
"path": "string", "path": "string",
"content": "string", "content": {
"content_type": "default" | "text" | "html", "type": "default" | "text" | "html",
"enable_stream_check": boolean, "content": "string"
},
"enable_stream_check": boolean, "enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http" "vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
"enable_slow_pool": boolean, "enable_slow_pool": boolean,
"enable_slow_pool": boolean "enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
} }
</code></pre> </code></pre>
<ul> <ul>
<li>响应格式:</li> <li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
@@ -276,43 +293,60 @@ data: [DONE]
"message": "string", "message": "string",
"data": { "data": {
"page_content": { "page_content": {
"type": "default" | "text" | "html", "type": "default" | "text" | "html", // 对于js和css后两者是一样的
"content": "string" "content": "string"
}, },
"enable_stream_check": boolean, "enable_stream_check": boolean,
"vision_ability": "base64" | "url" | "none", "include_stop_stream": boolean,
"enable_slow_pool": boolean "vision_ability": "none" | "base64" | "all",
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
} }
} }
</code></pre> </code></pre>
<p>注意:<code>check_usage_models</code> 字段的默认值为:</p>
<pre><code class="language-json">{
"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"
}</code></pre>
<p>这些模型将默认进行使用量检查。您可以通过配置接口修改此设置。</p>
<p>路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。</p>
<h2>静态资源接口</h2> <h2>静态资源接口</h2>
<h3>获取共享样式</h3> <h3>获取共享样式</h3>
<ul> <ul>
<li>接口地址: <code>/static/shared-styles.css</code></li> <li>接口地址: <code>/static/shared-styles.css</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: CSS文件</li> <li>响应格式: CSS文件</li>
<li>功能: 获取共享样式表</li> <li>功能: 获取共享样式表</li>
</ul> </ul>
<h3>获取共享脚本</h3> <h3>获取共享脚本</h3>
<ul> <ul>
<li>接口地址: <code>/static/shared.js</code></li> <li>接口地址: <code>/static/shared.js</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: JavaScript文件</li> <li>响应格式: JavaScript文件</li>
<li>功能: 获取共享JavaScript代码</li> <li>功能: 获取共享JavaScript代码</li>
</ul> </ul>
<h3>环境变量示例</h3> <h3>环境变量示例</h3>
<ul> <ul>
<li>接口地址: <code>/env-example</code></li> <li>接口地址: <code>/env-example</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: 文本文件</li> <li>响应格式: 文本文件</li>
<li>功能: 获取环境变量配置示例</li> <li>功能: 获取环境变量配置示例</li>
</ul> </ul>
<h2>其他接口</h2> <h2>其他接口</h2>
@@ -320,9 +354,9 @@ data: [DONE]
<h3>获取模型列表</h3> <h3>获取模型列表</h3>
<ul> <ul>
<li>接口地址: <code>/v1/models</code></li> <li>接口地址: <code>/v1/models</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式:</li> <li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
@@ -338,12 +372,17 @@ data: [DONE]
} }
</code></pre> </code></pre>
<h3>获取随机checksum</h3> <h3>获取或修复checksum</h3>
<ul> <ul>
<li>接口地址: <code>/checksum</code></li> <li>接口地址: <code>/get-checksum</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式:</li> <li>请求参数:
<ul>
<li><code>bad_checksum</code>: 可选用于修复的旧版本生成的checksum也可只传入前8个字符可用别名checksum</li>
</ul>
</li>
<li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
@@ -351,43 +390,111 @@ data: [DONE]
} }
</code></pre> </code></pre>
<p>说明:</p>
<ul>
<li>如果不提供<code>bad_checksum</code>参数将生成一个新的随机checksum</li>
<li>如果提供<code>bad_checksum</code>参数将尝试修复旧版本的checksum以适配当前版本(v0.1.3-rc.3)使用修复失败会返回新的checksum</li>
</ul>
<h3>健康检查接口</h3> <h3>健康检查接口</h3>
<ul> <ul>
<li>接口地址: <code>/health</code><code>/</code>(重定向)</li> <li>接口地址: <code>/health</code><code>/</code>(重定向)</li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)</li> <li>认证方式: Bearer Token可选</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)默认JSON</li>
</ul> </ul>
<pre><code class="language-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"]
}
</code></pre>
<p>注意:<code>stats</code> 字段仅在请求头中包含正确的 <code>AUTH_TOKEN</code> 时才会返回。否则,该字段将被省略。</p>
<h3>获取日志接口</h3> <h3>获取日志接口</h3>
<ul> <ul>
<li>接口地址: <code>/logs</code></li> <li>接口地址: <code>/logs</code></li>
<li>请求方法: GET</li> <li>请求方法: GET</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)</li> <li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)</li>
</ul> </ul>
<h3>获取日志数据</h3> <h3>获取日志数据</h3>
<ul> <ul>
<li>接口地址: <code>/logs</code></li> <li>接口地址: <code>/logs</code></li>
<li>请求方法: POST</li> <li>请求方法: POST</li>
<li>认证方式: Bearer Token</li> <li>认证方式: Bearer Token</li>
<li>响应格式:</li> <li>响应格式:</li>
</ul> </ul>
<pre><code class="language-json">{ <pre><code class="language-json">{
"total": number, "total": number,
"logs": [ "logs": [
{ {
"id": number,
"timestamp": "string", "timestamp": "string",
"model": "string", "model": "string",
"token_info": { "token_info": {
"token": "string", "token": "string",
"checksum": "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", "prompt": "string",
"timing": {
"total": number,
"first": number
},
"stream": boolean, "stream": boolean,
"status": "string", "status": "string",
"error": "string" "error": "string"
@@ -397,3 +504,120 @@ data: [DONE]
"status": "success" "status": "success"
} }
</code></pre> </code></pre>
<h3>获取用户信息</h3>
<ul>
<li>接口地址: <code>/get-userinfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"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
}
}
</code></pre>
<p>如果发生错误,响应格式为:</p>
<pre><code class="language-json">{
"error": "string"
}
</code></pre>
<h3>基础校准</h3>
<ul>
<li>接口地址: <code>/basic-calibration</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success" | "error",
"message": "string",
"user_id": "string",
"create_at": "string",
"checksum_time": number
}
</code></pre>
<p>注意: <code>user_id</code>, <code>create_at</code>, 和 <code>checksum_time</code> 字段在校验失败时可能不存在。</p>
<h2>偷偷写在最后的话</h2>
<p>虽然作者觉得<del></del>收点钱合理,但不强求,要是<strong>主动自愿</strong>发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧</p>
<p>不是<strong>主动自愿</strong>就算了,不是很缺,给了会很感动罢了。</p>
<p>虽然不是很建议你赞助,但如果你赞助了,大概可以:</p>
<ul>
<li>测试版更新</li>
<li>要求功能</li>
<li>问题更快解决</li>
</ul>
<p>即使如此,我也保留可以拒绝赞助和拒绝要求的权利。</p>
<p>求赞助还是有点不要脸了,接下来是吐槽:</p>
<p>辛辛苦苦做这个也不知道是为了谁好累。其实还有很多功能可以做比如直接传token支持配置其实这个要专门做一个页面</p>
<p>主要没想做用户管理所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。</p>
<p>就说这么多,没啥可说的,不管那么多,做就完了。<span>[doge]</span> 自己想象吧。</p>
<p>为什么一直说要跑路呢主要是有时Cursor的Claude太假了堪比gpt-4o-mini我对比发现真没啥差别比以前差远了无力了所以不太想做了。我也感觉很奇怪。</p>
<p>查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。</p>

View File

@@ -31,6 +31,12 @@ function showMessage(elementId, text, isError = false) {
function showGlobalMessage(text, isError = false) { function showGlobalMessage(text, isError = false) {
showMessage('message', text, isError); showMessage('message', text, isError);
// 3秒后自动清除消息
setTimeout(() => {
const msg = document.getElementById('message');
msg.textContent = '';
msg.className = 'message';
}, 3000);
} }
// Token 输入框自动填充和事件绑定 // Token 输入框自动填充和事件绑定

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token 信息管理</title> <title>Token 信息管理</title>
<!-- 引入共享样式 --> <!-- 引入共享样式 -->