mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-06 07:06:51 +08:00
v0.1.3-rc.3
This commit is contained in:
@@ -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
|
||||||
|
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@@ -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
|
||||||
|
27
.github/workflows/docker.yml
vendored
27
.github/workflows/docker.yml
vendored
@@ -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
|
||||||
@@ -62,4 +69,20 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
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
231
Cargo.lock
generated
@@ -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",
|
||||||
|
17
Cargo.toml
17
Cargo.toml
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
4
build.rs
4
build.rs
@@ -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();
|
||||||
|
|
||||||
// 静态资源文件处理
|
// 静态资源文件处理
|
||||||
|
@@ -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>)> {
|
||||||
|
@@ -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");
|
||||||
|
@@ -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));
|
||||||
|
@@ -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 结构体
|
||||||
|
@@ -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();
|
||||||
|
1156
src/chat/aiserver/v1/lite.proto
Normal file
1156
src/chat/aiserver/v1/lite.proto
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
@@ -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
26
src/chat/route/api.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
@@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -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
34
src/chat/route/profile.rs
Normal 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(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -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())),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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()))
|
||||||
|
@@ -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)])
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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,
|
|
||||||
}
|
|
77
src/common/models/userinfo.rs
Normal file
77
src/common/models/userinfo.rs
Normal 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>,
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
18
src/main.rs
18
src/main.rs
@@ -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
381
static/api.html
Normal 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>
|
@@ -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>
|
||||||
|
533
static/logs.html
533
static/logs.html
@@ -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">×</span>
|
<div class="modal-header">
|
||||||
<h3>Token 详细信息</h3>
|
<h3>Token 详细信息</h3>
|
||||||
<table>
|
<span class="close">×</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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.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) {
|
||||||
|
@@ -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>
|
@@ -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 输入框自动填充和事件绑定
|
||||||
@@ -94,9 +100,9 @@ function parseBooleanFromString(str, defaultValue = null) {
|
|||||||
if (typeof str !== 'string') {
|
if (typeof str !== 'string') {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowercaseStr = str.toLowerCase().trim();
|
const lowercaseStr = str.toLowerCase().trim();
|
||||||
|
|
||||||
if (lowercaseStr === 'true' || lowercaseStr === '1') {
|
if (lowercaseStr === 'true' || lowercaseStr === '1') {
|
||||||
return true;
|
return true;
|
||||||
} else if (lowercaseStr === 'false' || lowercaseStr === '0') {
|
} else if (lowercaseStr === 'false' || lowercaseStr === '0') {
|
||||||
@@ -202,7 +208,7 @@ function formatPromptToTable(messages) {
|
|||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
// 将HTML标签文本用引号包裹,使其更易读
|
// 将HTML标签文本用引号包裹,使其更易读
|
||||||
// return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"');
|
// return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"');
|
||||||
return escaped;
|
return escaped;
|
||||||
|
@@ -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>
|
||||||
<!-- 引入共享样式 -->
|
<!-- 引入共享样式 -->
|
||||||
|
Reference in New Issue
Block a user