v0.1.3-rc.1

This commit is contained in:
wisdgod
2024-12-30 23:29:37 +08:00
parent ea1acb555f
commit 5505ccc6cb
41 changed files with 9626 additions and 1215 deletions

View File

@@ -1,5 +1,36 @@
# 当前配置为默认值,请根据需要修改
# 服务器监听端口
PORT=3000
# 路由前缀,必须以 / 开头(如果不为空)
ROUTE_PREFIX=
# 认证令牌,必填
AUTH_TOKEN=
# 启用流式响应检查关闭则无法响应错误代价是会对第一个块解析2次
ENABLE_STREAM_CHECK=true
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块
INCLUDE_STOP_REASON_STREAM=true
# 令牌文件路径
TOKEN_FILE=.token
# 令牌列表文件路径
TOKEN_LIST_FILE=.token-list
# 实验性是否启用慢速池true/false
ENABLE_SLOW_POOL=false
# 允许claude开头的模型请求绕过内置模型限制true/false
PASS_ANY_CLAUDE=false
# 图片处理能力配置
# 可选值:
# - none 或 disabled禁用图片功能
# - base64 或 base64-only仅支持 base64 编码的图片
# - all 或 base64-http支持 base64 和 HTTP 图片
# 注意:启用 HTTP 支持可能会暴露服务器 IP
VISION_ABILITY=base64

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
# 统一使用 LF
* text=auto eol=lf
# 对特定文件类型设置
*.bat text eol=crlf
*.ps1 text eol=crlf

View File

@@ -1,6 +1,7 @@
name: Build
on:
workflow_dispatch:
push:
tags:
- 'v*'
@@ -28,7 +29,8 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'scripts/package.json'
cache-dependency-path: 'scripts/package-lock.json'
run: cd scripts && npm install && cd ..
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -44,10 +46,11 @@ jobs:
protobuf-compiler \
pkg-config \
libssl-dev \
openssl
# 安装 npm 依赖
cd scripts && npm install && cd ..
openssl \
musl-tools \
musl-dev \
libssl-dev:native \
linux-libc-dev:native
# 设置 OpenSSL 环境变量
echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV
@@ -55,72 +58,21 @@ jobs:
echo "OPENSSL_INCLUDE_DIR=/usr/include/openssl" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
# - name: Set up Docker Buildx
# if: runner.os == 'Linux'
# uses: docker/setup-buildx-action@v3.8.0
# - name: Build Linux arm64
# if: runner.os == 'Linux'
# run: |
# # 启用 QEMU 支持
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# # 创建 Dockerfile
# cat > Dockerfile.arm64 << 'EOF'
# FROM arm64v8/ubuntu:22.04
# ENV DEBIAN_FRONTEND=noninteractive
# RUN apt-get update && apt-get install -y \
# build-essential \
# curl \
# pkg-config \
# libssl-dev \
# protobuf-compiler \
# nodejs \
# npm \
# git \
# && rm -rf /var/lib/apt/lists/*
# # 安装 Rust
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# ENV PATH="/root/.cargo/bin:${PATH}"
# WORKDIR /build
# COPY . .
# # 安装 npm 依赖
# RUN cd scripts && npm install && cd ..
# # 构建动态链接版本
# RUN cargo build --release
# # 构建静态链接版本
# RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --release
# EOF
# # 构建 arm64 版本
# docker buildx build --platform linux/arm64 -f Dockerfile.arm64 -t builder-arm64 .
# # 创建临时容器
# docker create --name temp-container builder-arm64 sh
# # 复制动态链接版本
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-aarch64-unknown-linux-gnu
# # 复制静态链接版本
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-static-aarch64-unknown-linux-gnu
# # 清理临时容器
# docker rm temp-container
- name: Build Linux x86_64 (Dynamic)
if: runner.os == 'Linux'
run: bash scripts/build.sh
- name: Build Linux x86_64 (Static)
if: runner.os == 'Linux'
run: bash scripts/build.sh --static
run: |
# 使用 musl 目标
rustup target remove x86_64-unknown-linux-gnu
rustup target add x86_64-unknown-linux-musl
# 设置静态编译环境变量
export CC=musl-gcc
bash scripts/build.sh --static
- name: Install macOS dependencies
if: runner.os == 'macOS'
@@ -137,18 +89,30 @@ jobs:
run: |
choco install -y protoc
choco install -y openssl
echo "OPENSSL_DIR=C:/Program Files/OpenSSL-Win64" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=C:/Program Files/OpenSSL-Win64/lib/pkgconfig" >> $GITHUB_ENV
cd scripts && npm install && cd ..
choco install -y nodejs-lts
- name: Build (Dynamic)
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
# 刷新环境变量
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# 设置 OpenSSL 环境变量
echo "OPENSSL_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV
echo "PKG_CONFIG_PATH=C:\Program Files\OpenSSL\lib\pkgconfig" >> $env:GITHUB_ENV
- name: Build macOS (Dynamic)
if: runner.os == 'macOS' || runner.os == 'Windows'
run: bash scripts/build.sh
- name: Build (Static)
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
- name: Build macOS (Static)
if: runner.os == 'macOS' || runner.os == 'Windows'
run: bash scripts/build.sh --static
# - name: Verify build artifacts
# run: |
# if [ ! -d "release" ] || [ -z "$(ls -A release)" ]; then
# echo "Error: No build artifacts found in release directory"
# exit 1
# fi
- name: Upload artifacts
uses: actions/upload-artifact@v4.5.0
with:
@@ -184,7 +148,11 @@ jobs:
bash \
gmake \
pkgconf \
openssl
openssl \
libressl-devel \
libiconv \
gettext-tools \
gettext-runtime
export SSL_CERT_FILE=/etc/ssl/cert.pem
@@ -192,11 +160,8 @@ jobs:
cd /root
git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
# 然后再进入 scripts 目录
cd scripts && npm install && cd ..
# 安装 rustup 和 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain nightly
# 设置持久化的 Rust 环境变量
echo '. "$HOME/.cargo/env"' >> /root/.profile
@@ -210,7 +175,6 @@ jobs:
# 加载环境变量
. /root/.profile
# 构建
echo "构建动态链接版本..."
/usr/local/bin/bash scripts/build.sh

View File

@@ -2,6 +2,12 @@ name: Docker Build and Push
on:
workflow_dispatch:
inputs:
update_latest:
description: '是否更新 latest 标签'
required: true
type: boolean
default: false
push:
tags:
- 'v*'
@@ -36,6 +42,7 @@ jobs:
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.update_latest }}
type=raw,value=${{ steps.cargo_version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=ref,event=tag,enable=${{ github.event_name == 'push' }}
@@ -44,12 +51,14 @@ jobs:
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- name: Build and push Docker image
uses: docker/build-push-action@v6.10.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha

9
.gitignore vendored
View File

@@ -2,7 +2,10 @@
/get-token/target
/*.log
/*.env
/static/tokeninfo.min.html
/static/*.min.html
/static/*.min.css
/static/*.min.js
/scripts/.asset-hashes.json
node_modules
.DS_Store
/.vscode
@@ -12,3 +15,7 @@ node_modules
/cursor-api
/cursor-api.exe
/release
/static/readme.html
/*.py
/src/decoder.rs

273
Cargo.lock generated
View File

@@ -159,6 +159,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
@@ -180,12 +186,24 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.9.0"
@@ -194,9 +212,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.2.5"
version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
dependencies = [
"shlex",
]
@@ -222,6 +240,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -268,7 +292,7 @@ dependencies = [
[[package]]
name = "cursor-api"
version = "0.1.1"
version = "0.1.3-rc.1"
dependencies = [
"axum",
"base64",
@@ -277,7 +301,10 @@ dependencies = [
"dotenvy",
"flate2",
"futures",
"gif",
"hex",
"image",
"lazy_static",
"prost",
"prost-build",
"rand",
@@ -286,6 +313,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"sysinfo",
"tokio",
"tokio-stream",
"tower-http",
@@ -356,6 +384,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -500,6 +537,16 @@ dependencies = [
"wasi",
]
[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -673,7 +720,7 @@ dependencies = [
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
"windows-core 0.52.0",
]
[[package]]
@@ -824,6 +871,33 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "indexmap"
version = "2.7.0"
@@ -865,6 +939,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
@@ -914,6 +994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
@@ -950,6 +1031,15 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -980,7 +1070,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"cfg-if",
"foreign-types",
"libc",
@@ -1052,6 +1142,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -1133,10 +1236,16 @@ dependencies = [
]
[[package]]
name = "quote"
version = "1.0.37"
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
@@ -1202,9 +1311,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.9"
version = "0.12.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
checksum = "7fe060fe50f524be480214aba758c71f99f90ee8c83c5a36b5e9e1d568eb4eb3"
dependencies = [
"async-compression",
"base64",
@@ -1237,6 +1346,7 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-util",
"tower",
"tower-service",
"url",
"wasm-bindgen",
@@ -1273,7 +1383,7 @@ version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
@@ -1321,9 +1431,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
@@ -1346,7 +1456,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -1365,18 +1475,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -1434,6 +1544,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "slab"
version = "0.4.9"
@@ -1479,9 +1595,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.91"
version = "2.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126"
dependencies = [
"proc-macro2",
"quote",
@@ -1508,13 +1624,26 @@ dependencies = [
"syn",
]
[[package]]
name = "sysinfo"
version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
dependencies = [
"core-foundation-sys",
"libc",
"memchr",
"ntapi",
"windows",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys",
]
@@ -1645,7 +1774,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"bytes",
"http",
"pin-project-lite",
@@ -1858,6 +1987,44 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "weezl"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -1867,17 +2034,60 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result 0.1.2",
"windows-targets",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-result 0.2.0",
"windows-strings",
"windows-targets",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-result"
version = "0.2.0"
@@ -1893,7 +2103,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-result 0.2.0",
"windows-targets",
]
@@ -2084,3 +2294,18 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
dependencies = [
"zune-core",
]

View File

@@ -1,28 +1,39 @@
[package]
name = "cursor-api"
version = "0.1.1"
version = "0.1.3-rc.1"
edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"]
# license = "MIT"
# copyright = "Copyright (c) 2024 wisdgod"
description = "OpenAI format compatibility layer for the Cursor API"
repository = "https://github.com/wisdgod/cursor-api"
[build-dependencies]
prost-build = "0.13.4"
sha2 = { version = "0.10.8", default-features = false }
serde_json = "1.0.134"
[dependencies]
axum = { version = "0.7.9", features = ["json"] }
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
# brotli = { version = "7.0.0", default-features = false, features = ["std"] }
bytes = "1.9.0"
chrono = { version = "0.4.39", features = ["serde"] }
dotenvy = "0.15.7"
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
gif = { version = "0.13.1", default-features = false, features = ["std"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
lazy_static = "1.5.0"
prost = "0.13.4"
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
reqwest = { version = "0.12.9", features = ["json", "gzip", "stream"] }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = { version = "1.0.134", features = ["std"] }
reqwest = { version = "0.12.11", default-features = false, features = ["gzip", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] }
serde_json = "1.0.134"
sha2 = { version = "0.10.8", default-features = false }
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
tokio-stream = { version = "0.1.17", features = ["time"] }
tower-http = { version = "0.6.2", features = ["cors"] }

View File

@@ -1,48 +1,47 @@
# 构建阶段
FROM rust:1.83.0-slim-bookworm as builder
# AMD64 构建阶段
FROM --platform=linux/amd64 rust:1.83.0-slim-bookworm as builder-amd64
WORKDIR /app
# 安装构建依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends \
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/*
# 复制项目文件
COPY . .
RUN cargo build --release && \
cp target/release/cursor-api /app/cursor-api
# 构建
RUN rustup target add x86_64-unknown-linux-gnu && \
cargo build --target x86_64-unknown-linux-gnu --release && \
cp target/x86_64-unknown-linux-gnu/release/cursor-api /app/cursor-api
# 运行阶段
FROM debian:bookworm-slim
# ARM64 构建阶段
FROM --platform=linux/arm64 rust:1.83.0-slim-bookworm as builder-arm64
WORKDIR /app
ENV TZ=Asia/Shanghai
# 安装运行时依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
tzdata \
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN cargo build --release && \
cp target/release/cursor-api /app/cursor-api
# 复制构建产物
COPY --from=builder /app/cursor-api .
# AMD64 运行阶段
FROM --platform=linux/amd64 debian:bookworm-slim as run-amd64
WORKDIR /app
ENV TZ=Asia/Shanghai
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder-amd64 /app/cursor-api .
# 设置默认端口
# ARM64 运行阶段
FROM --platform=linux/arm64 debian:bookworm-slim as run-arm64
WORKDIR /app
ENV TZ=Asia/Shanghai
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder-arm64 /app/cursor-api .
# 通用配置
FROM run-${TARGETARCH}
ENV PORT=3000
# 动态暴露端口
EXPOSE ${PORT}
CMD ["./cursor-api"]

View File

@@ -4,7 +4,7 @@
1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录(赠送 250 次快速响应,可通过删除账号再注册重置)
2. 在浏览器中打开开发者工具F12
3. 找到 Application-Cookies 中名为 `WorkosCursorSessionToken` 的值并保存(相当于 openai 的密钥)
3. 找到 Application-Cookies 中名为 `WorkosCursorSessionToken` 的值并复制其第3个字段%3A%3A是::的编码cookie用:分隔值
## 接口说明
@@ -74,7 +74,9 @@
1. `.token` 文件每行一个token支持以下格式
```
# 这是注释
token1
# alias与标签的作用差不多
alias::token2
```
@@ -84,8 +86,10 @@
2. `.token-list` 文件每行为token和checksum的对应关系
```
# 这里的#表示这行在下次读取要删除
token1,checksum1
token2,checksum2
# 支持像.token一样的alias冲突时以.token为准
alias::token2,checksum2
```
该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:
@@ -97,17 +101,19 @@
写死了,后续也不会会支持自定义模型列表
```
cursor-small
claude-3.5-sonnet
gpt-4
gpt-4o
claude-3-opus
cursor-fast
cursor-small
gpt-3.5
gpt-3.5-turbo
gpt-4-turbo-2024-04-09
gpt-4
gpt-4o-128k
gemini-1.5-flash-500k
claude-3-haiku-200k
claude-3-5-sonnet-200k
claude-3-5-sonnet-20240620
claude-3-5-sonnet-20241022
gpt-4o-mini
o1-mini
@@ -121,30 +127,6 @@ gemini-2.0-flash-exp
## 部署
### 本地部署
#### 从源码编译
需要安装 Rust 工具链和依赖:
```bash
# 安装rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安装依赖Debian/Ubuntu
apt-get install -y build-essential protobuf-compiler pkg-config libssl-dev nodejs npm
# 原生编译
cargo build --release
# 交叉编译以x86_64-unknown-linux-gnu为例老实说这也算原生编译因为使用了docker
cross build --target x86_64-unknown-linux-gnu --release
```
#### 使用预编译二进制
从 [Releases](https://github.com/wisdgod/cursor-api/releases) 下载对应平台的二进制文件。
### Docker 部署
#### Docker 运行示例
@@ -153,13 +135,6 @@ cross build --target x86_64-unknown-linux-gnu --release
docker run -d -e PORT=3000 -e AUTH_TOKEN=your_token -p 3000:3000 wisdgod/cursor-api:latest
```
#### Docker 构建示例
```bash
docker build -t cursor-api .
docker run -p 3000:3000 cursor-api
```
### huggingface部署
前提一个huggingface账号
@@ -190,6 +165,8 @@ docker run -p 3000:3000 cursor-api
TOKEN_LIST_FILE=.token-list
```
更多变量示例可访问 /env-example
3. 重新部署
点击`Factory rebuild`,等待部署完成
@@ -210,15 +187,7 @@ docker run -p 3000:3000 cursor-api
### 跨平台编译
使用提供的构建脚本:
```bash
# 仅编译当前平台
./scripts/build.sh
# 交叉编译所有支持的平台
./scripts/build.sh --cross
```
自行配置cross编译环境
支持的平台:

137
build.rs
View File

@@ -1,15 +1,19 @@
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::io::Result;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
// 支持的文件类型
const SUPPORTED_EXTENSIONS: [&str; 3] = ["html", "js", "css"];
fn check_and_install_deps() -> Result<()> {
let scripts_dir = Path::new("scripts");
let node_modules = scripts_dir.join("node_modules");
// 如果 node_modules 不存在,运行 npm install
if !node_modules.exists() {
println!("cargo:warning=Installing HTML minifier dependencies...");
println!("cargo:warning=Installing minifier dependencies...");
let status = Command::new("npm")
.current_dir(scripts_dir)
.arg("install")
@@ -23,38 +27,139 @@ fn check_and_install_deps() -> Result<()> {
Ok(())
}
fn minify_html() -> Result<()> {
println!("cargo:warning=Minifying HTML files...");
fn get_files_hash() -> Result<HashMap<PathBuf, String>> {
let mut file_hashes = HashMap::new();
let static_dir = Path::new("static");
if static_dir.exists() {
for entry in fs::read_dir(static_dir)? {
let entry = entry?;
let path = entry.path();
// 检查是否是支持的文件类型,且不是已经压缩的文件
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if SUPPORTED_EXTENSIONS.contains(&ext) && !path.to_string_lossy().contains(".min.")
{
let content = fs::read(&path)?;
let mut hasher = Sha256::new();
hasher.update(&content);
let hash = format!("{:x}", hasher.finalize());
file_hashes.insert(path, hash);
}
}
}
}
Ok(file_hashes)
}
fn load_saved_hashes() -> Result<HashMap<PathBuf, String>> {
let hash_file = Path::new("scripts/.asset-hashes.json");
if hash_file.exists() {
let content = fs::read_to_string(hash_file)?;
let hash_map: HashMap<String, String> = serde_json::from_str(&content)?;
Ok(hash_map
.into_iter()
.map(|(k, v)| (PathBuf::from(k), v))
.collect())
} else {
Ok(HashMap::new())
}
}
fn save_hashes(hashes: &HashMap<PathBuf, String>) -> Result<()> {
let hash_file = Path::new("scripts/.asset-hashes.json");
let string_map: HashMap<String, String> = hashes
.iter()
.map(|(k, v)| (k.to_string_lossy().into_owned(), v.clone()))
.collect();
let content = serde_json::to_string_pretty(&string_map)?;
fs::write(hash_file, content)?;
Ok(())
}
fn minify_assets() -> Result<()> {
// 获取现有文件的哈希
let current_hashes = get_files_hash()?;
if current_hashes.is_empty() {
println!("cargo:warning=No files to minify");
return Ok(());
}
// 加载保存的哈希值
let saved_hashes = load_saved_hashes()?;
// 找出需要更新的文件
let files_to_update: Vec<_> = current_hashes
.iter()
.filter(|(path, current_hash)| {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let min_path = path.with_file_name(format!(
"{}.min.{}",
path.file_stem().unwrap().to_string_lossy(),
ext
));
// 检查压缩后的文件是否存在
if !min_path.exists() {
return true;
}
// 检查原始文件是否发生变化
saved_hashes
.get(*path)
.map_or(true, |saved_hash| saved_hash != *current_hash)
})
.map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned())
.collect();
if files_to_update.is_empty() {
println!("cargo:warning=No files need to be updated");
return Ok(());
}
println!("cargo:warning=Minifying {} files...", files_to_update.len());
// 运行压缩脚本
let status = Command::new("node")
.args(&["scripts/minify-html.js"])
.arg("scripts/minify.js")
.args(&files_to_update)
.status()?;
if !status.success() {
panic!("HTML minification failed");
panic!("Asset minification failed");
}
// 保存新的哈希值
save_hashes(&current_hashes)?;
Ok(())
}
fn main() -> Result<()> {
// Proto 文件处理
println!("cargo:rerun-if-changed=src/message.proto");
println!("cargo:rerun-if-changed=src/aiserver/v1/aiserver.proto");
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(
// "aiserver.v1.ThrowErrorCheckRequest",
// "#[derive(serde::Serialize, serde::Deserialize)]"
// );
config
.compile_protos(&["src/message.proto"], &["src/"])
.compile_protos(&["src/aiserver/v1/aiserver.proto"], &["src/aiserver/v1/"])
.unwrap();
// HTML 文件处理
println!("cargo:rerun-if-changed=static/tokeninfo.html");
println!("cargo:rerun-if-changed=scripts/minify-html.js");
// 静态资源文件处理
println!("cargo:rerun-if-changed=scripts/minify.js");
println!("cargo:rerun-if-changed=scripts/package.json");
println!("cargo:rerun-if-changed=static");
// 检查并安装依赖
check_and_install_deps()?;
// 运行 HTML 压缩
minify_html()?;
// 运行资源压缩
minify_assets()?;
Ok(())
}

View File

@@ -1,158 +1,126 @@
# <EFBFBD><EFBFBD>ɫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Blue }
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Error { Write-Host "[ERROR] $args" -ForegroundColor Red; exit 1 }
# 参数处理
param(
[switch]$Static,
[switch]$Help,
[ValidateSet("x86_64", "aarch64", "i686")]
[string]$Architecture
)
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>Ĺ<EFBFBD><EFBFBD><EFBFBD>
function Test-Requirements {
$tools = @("cargo", "protoc", "npm", "node")
$missing = @()
# 设置错误时停止执行
$ErrorActionPreference = "Stop"
foreach ($tool in $tools) {
if (!(Get-Command $tool -ErrorAction SilentlyContinue)) {
$missing += $tool
}
}
# 颜色输出函数
function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue }
function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 }
if ($missing.Count -gt 0) {
Write-Error "ȱ<EFBFBD>ٱ<EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $($missing -join ', ')"
}
}
# 检查必要的工具
function Check-Requirements {
$tools = @("cargo", "protoc", "npm", "node")
$missing = @()
# <20><> Test-Requirements <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>º<EFBFBD><C2BA><EFBFBD>
function Initialize-VSEnvironment {
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڳ<EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>..."
# ֱ<><D6B1>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD>֪<EFBFBD><D6AA> vcvarsall.bat ·<><C2B7>
$vcvarsallPath = "E:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat"
if (-not (Test-Path $vcvarsallPath)) {
Write-Error "δ<EFBFBD>ҵ<EFBFBD> vcvarsall.bat: $vcvarsallPath"
return
}
Write-Info "ʹ<EFBFBD><EFBFBD> vcvarsall.bat ·<><C2B7>: $vcvarsallPath"
# <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$archArg = "x64"
$command = "`"$vcvarsallPath`" $archArg && set"
try {
$output = cmd /c "$command" 2>&1
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ɹ<EFBFBD>ִ<EFBFBD><D6B4>
if ($LASTEXITCODE -ne 0) {
Write-Error "vcvarsall.bat ִ<><D6B4>ʧ<EFBFBD>ܣ<EFBFBD><DCA3>˳<EFBFBD><CBB3><EFBFBD>: $LASTEXITCODE"
return
foreach ($tool in $tools) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
$missing += $tool
}
# <20><><EFBFBD>µ<EFBFBD>ǰ PowerShell <20><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
foreach ($line in $output) {
if ($line -match "^([^=]+)=(.*)$") {
$name = $matches[1]
$value = $matches[2]
if (![string]::IsNullOrEmpty($name)) {
Set-Item -Path "env:$name" -Value $value -ErrorAction SilentlyContinue
}
}
}
Write-Info "Visual Studio <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
}
catch {
Write-Error "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
if ($missing.Count -gt 0) {
Write-Error "缺少必要工具: $($missing -join ', ')"
}
}
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
# 帮助信息
function Show-Help {
Write-Host @"
<EFBFBD>÷<EFBFBD>: $(Split-Path $MyInvocation.MyCommand.Path -Leaf) [ѡ<EFBFBD><EFBFBD>]
Write-Host @"
: $(Split-Path $MyInvocation.ScriptName -Leaf) []
ѡ<EFBFBD><EFBFBD>:
--static ʹ<EFBFBD>þ<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>Ĭ<EFBFBD>϶<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>
--help <EFBFBD><EFBFBD>ʾ<EFBFBD>˰<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
:
-Static 使
-Help
Ĭ<EFBFBD>ϱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Windows ֧<EFBFBD>ֵļܹ<EFBFBD> (x64 <EFBFBD><EFBFBD> arm64)
使
"@
}
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function New-Target {
param (
[string]$target,
[string]$rustflags
)
# 构建函数
function Build-Target {
param (
[string]$Target,
[string]$RustFlags
)
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڹ<EFBFBD><EFBFBD><EFBFBD> $target..."
Write-Info "正在构建 $Target..."
# <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD>й<EFBFBD><EFBFBD><EFBFBD>
$env:RUSTFLAGS = $rustflags
cargo build --target $target --release
# 设置环境变量
$env:RUSTFLAGS = $RustFlags
# <EFBFBD>ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$binaryName = "cursor-api"
if ($UseStatic) {
$binaryName += "-static"
}
# 构建
if ($Target -ne (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) {
cargo build --target $Target --release
} else {
cargo build --release
}
$sourcePath = "target/$target/release/cursor-api.exe"
$targetPath = "release/${binaryName}-${target}.exe"
# 移动编译产物到 release 目录
$binaryName = "cursor-api"
if ($Static) {
$binaryName += "-static"
}
if (Test-Path $sourcePath) {
Copy-Item $sourcePath $targetPath -Force
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD> $target"
}
else {
Write-Warn "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD>: $target"
return $false
}
return $true
$binaryPath = if ($Target -eq (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) {
"target/release/cursor-api.exe"
} else {
"target/$Target/release/cursor-api.exe"
}
if (Test-Path $binaryPath) {
Copy-Item $binaryPath "release/$binaryName-$Target.exe"
Write-Info "完成构建 $Target"
} else {
Write-Warn "构建产物未找到: $Target"
Write-Warn "查找路径: $binaryPath"
Write-Warn "当前目录内容:"
Get-ChildItem -Recurse target/
return $false
}
return $true
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$UseStatic = $false
foreach ($arg in $args) {
switch ($arg) {
"--static" { $UseStatic = $true }
"--help" { Show-Help; exit 0 }
default { Write-Error "δ֪<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $arg" }
}
if ($Help) {
Show-Help
exit 0
}
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
try {
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
Test-Requirements
# 检查依赖
Check-Requirements
# <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>
Initialize-VSEnvironment
# 创建 release 目录
New-Item -ItemType Directory -Force -Path release | Out-Null
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD> release Ŀ¼
New-Item -ItemType Directory -Force -Path "release" | Out-Null
# <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ƽ̨
$targets = @(
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc"
)
# <20><><EFBFBD>þ<EFBFBD>̬<EFBFBD><CCAC><EFBFBD>ӱ<EFBFBD>־
$rustflags = ""
if ($UseStatic) {
$rustflags = "-C target-feature=+crt-static"
}
Write-Info "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>
foreach ($target in $targets) {
New-Target -target $target -rustflags $rustflags
}
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
# 设置静态链接标志
$rustFlags = ""
if ($Static) {
$rustFlags = "-C target-feature=+crt-static"
}
catch {
Write-Error "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>з<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
}
# 获取目标架构
$arch = if ($Architecture) {
$Architecture
} else {
switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "x86_64" }
"ARM64" { "aarch64" }
"X86" { "i686" }
default { Write-Error "不支持的架构: $env:PROCESSOR_ARCHITECTURE" }
}
}
$target = "$arch-pc-windows-msvc"
Write-Info "开始构建..."
if (-not (Build-Target -Target $target -RustFlags $rustFlags)) {
Write-Error "构建失败"
}
Write-Info "构建完成!"

View File

@@ -6,11 +6,6 @@ info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
# 检查是否在 Linux 环境
is_linux() {
[ "$(uname -s)" = "Linux" ]
}
# 检查必要的工具
check_requirements() {
local missing_tools=()
@@ -22,23 +17,29 @@ check_requirements() {
fi
done
# cross 工具检查(仅在 Linux 上需要)
if [[ "$OS" == "Linux" ]] && ! command -v cross &>/dev/null; then
missing_tools+=("cross")
fi
if [[ ${#missing_tools[@]} -gt 0 ]]; then
error "缺少必要工具: ${missing_tools[*]}"
fi
}
# 解析参数
USE_STATIC=false
while [[ $# -gt 0 ]]; do
case $1 in
--static) USE_STATIC=true ;;
--help) show_help; exit 0 ;;
*) error "未知参数: $1" ;;
esac
shift
done
# 帮助信息
show_help() {
cat << EOF
用法: $(basename "$0") [选项]
选项:
--cross 使用 cross 进行交叉编译(仅在 Linux 上有效)
--static 使用静态链接(默认动态链接)
--help 显示此帮助信息
@@ -46,22 +47,6 @@ show_help() {
EOF
}
# 判断是否使用 cross
should_use_cross() {
local target=$1
# 如果不是 Linux 环境,直接返回 false
if [[ "$OS" != "Linux" ]]; then
return 1
fi
# 在 Linux 环境下,以下目标不使用 cross
# 1. Linux 上的 x86_64-unknown-linux-gnu
if [[ "$target" == "x86_64-unknown-linux-gnu" ]]; then
return 1
fi
return 0
}
# 并行构建函数
build_target() {
local target=$1
@@ -73,15 +58,11 @@ build_target() {
# 确定文件后缀
[[ $target == *"windows"* ]] && extension=".exe"
# 判断是否使用 cross
if should_use_cross "$target"; then
env RUSTFLAGS="$rustflags" cross build --target "$target" --release
# 构建
if [[ $target != "$CURRENT_TARGET" ]]; then
env RUSTFLAGS="$rustflags" cargo build --target "$target" --release
else
if [[ $target != "$CURRENT_TARGET" ]]; then
env RUSTFLAGS="$rustflags" cargo build --target "$target" --release
else
env RUSTFLAGS="$rustflags" cargo build --release
fi
env RUSTFLAGS="$rustflags" cargo build --release
fi
# 移动编译产物到 release 目录
@@ -117,7 +98,13 @@ get_target() {
local os=$2
case "$os" in
"Darwin") echo "${arch}-apple-darwin" ;;
"Linux") echo "${arch}-unknown-linux-gnu" ;;
"Linux")
if [[ $USE_STATIC == true ]]; then
echo "${arch}-unknown-linux-musl"
else
echo "${arch}-unknown-linux-gnu"
fi
;;
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;;
"FreeBSD") echo "${arch}-unknown-freebsd" ;;
*) error "不支持的系统: $os" ;;
@@ -134,16 +121,16 @@ CURRENT_TARGET=$(get_target "$ARCH" "$OS")
get_targets() {
case "$1" in
"linux")
# Linux 构建所有 Linux 目标和 FreeBSD 目标
echo "x86_64-unknown-linux-gnu x86_64-unknown-freebsd"
# Linux 构建当前架构
echo "$CURRENT_TARGET"
;;
"freebsd")
# FreeBSD 只构建当前架构的 FreeBSD 目标
echo "${ARCH}-unknown-freebsd"
# FreeBSD 只构建当前架构
echo "$CURRENT_TARGET"
;;
"windows")
# Windows 构建所有 Windows 目标
echo "x86_64-pc-windows-msvc"
# Windows 构建当前架构
echo "$CURRENT_TARGET"
;;
"macos")
# macOS 构建所有 macOS 目标
@@ -153,33 +140,21 @@ get_targets() {
esac
}
# 解析参数
USE_STATIC=false
while [[ $# -gt 0 ]]; do
case $1 in
--static) USE_STATIC=true ;;
--help) show_help; exit 0 ;;
*) error "未知参数: $1" ;;
esac
shift
done
# 检查依赖
check_requirements
# 确定要构建的目标
case "$OS" in
"Darwin")
Darwin)
TARGETS=($(get_targets "macos"))
;;
"Linux")
Linux)
TARGETS=($(get_targets "linux"))
;;
"FreeBSD")
FreeBSD)
TARGETS=($(get_targets "freebsd"))
;;
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT")
MINGW*|MSYS*|CYGWIN*|Windows_NT)
TARGETS=($(get_targets "windows"))
;;
*) error "不支持的系统: $OS" ;;
@@ -189,8 +164,8 @@ esac
mkdir -p release
# 设置静态链接标志
RUSTFLAGS=""
[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static"
RUSTFLAGS="-C link-arg=-s"
[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-s"
# 并行构建所有目标
info "开始构建..."

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env node
const { minify } = require('html-minifier-terser');
const fs = require('fs');
const path = require('path');
// 配置选项
const options = {
collapseWhitespace: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: true,
processScripts: ['application/json'],
};
// 处理文件
async function minifyFile(inputPath, outputPath) {
try {
const html = fs.readFileSync(inputPath, 'utf8');
const minified = await minify(html, options);
fs.writeFileSync(outputPath, minified);
console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
} catch (err) {
console.error(`✗ Error processing ${inputPath}:`, err);
process.exit(1);
}
}
// 主函数
async function main() {
const staticDir = path.join(__dirname, '..', 'static');
const files = [
['tokeninfo.html', 'tokeninfo.min.html'],
];
for (const [input, output] of files) {
await minifyFile(
path.join(staticDir, input),
path.join(staticDir, output)
);
}
}
main();

81
scripts/minify.js Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
const { minify: minifyHtml } = require('html-minifier-terser');
const { minify: minifyJs } = require('terser');
const CleanCSS = require('clean-css');
const fs = require('fs');
const path = require('path');
// 配置选项
const options = {
collapseWhitespace: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: true,
processScripts: ['application/json'],
};
// CSS 压缩选项
const cssOptions = {
level: 2
};
// 处理文件
async function minifyFile(inputPath, outputPath) {
try {
const ext = path.extname(inputPath).toLowerCase();
const content = fs.readFileSync(inputPath, 'utf8');
let minified;
switch (ext) {
case '.html':
minified = await minifyHtml(content, options);
break;
case '.js':
const result = await minifyJs(content);
minified = result.code;
break;
case '.css':
minified = new CleanCSS(cssOptions).minify(content).styles;
break;
default:
throw new Error(`Unsupported file type: ${ext}`);
}
fs.writeFileSync(outputPath, minified);
console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
} catch (err) {
console.error(`✗ Error processing ${inputPath}:`, err);
process.exit(1);
}
}
// 主函数
async function main() {
// 获取命令行参数跳过前两个参数node和脚本路径
const files = process.argv.slice(2);
if (files.length === 0) {
console.error('No input files specified');
process.exit(1);
}
const staticDir = path.join(__dirname, '..', 'static');
for (const file of files) {
const inputPath = path.join(staticDir, file);
const ext = path.extname(file);
const outputPath = path.join(
staticDir,
file.replace(ext, `.min${ext}`)
);
await minifyFile(inputPath, outputPath);
}
}
main();

View File

@@ -8,7 +8,9 @@
"name": "html-minifier-scripts",
"version": "1.0.0",
"dependencies": {
"html-minifier-terser": "^7.2.0"
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"terser": "^5.37.0"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -6,6 +6,8 @@
"node": ">=14.0.0"
},
"dependencies": {
"html-minifier-terser": "^7.2.0"
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"terser": "^5.37.0"
}
}
}

View File

@@ -1,31 +0,0 @@
# <20><><EFBFBD><EFBFBD> PowerShell <20><><EFBFBD><EFBFBD>Ϊ UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
# <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>Թ<EFBFBD><D4B9><EFBFBD>ԱȨ<D4B1><C8A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Warning "<EFBFBD><EFBFBD><EFBFBD>Թ<EFBFBD><EFBFBD><EFBFBD>ԱȨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д˽ű<EFBFBD>"
exit 1
}
# <20><><EFBFBD><EFBFBD><E9B2A2>װ Chocolatey
if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ Chocolatey..."
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
# <20><>װ<EFBFBD><D7B0>Ҫ<EFBFBD>Ĺ<EFBFBD><C4B9><EFBFBD>
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD>Ĺ<EFBFBD><EFBFBD><EFBFBD>..."
choco install -y mingw
choco install -y protoc
choco install -y git
# <20><>װ Rust <20><><EFBFBD><EFBFBD>
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ Rust <20><><EFBFBD><EFBFBD>..."
rustup target add x86_64-pc-windows-msvc
rustup target add x86_64-unknown-linux-gnu
cargo install cross
Write-Output "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"

179
scripts/setup.ps1 Normal file
View File

@@ -0,0 +1,179 @@
# <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD>ʱִֹͣ<D6B9><D6B4>
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue" # <20>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ٶ<EFBFBD>
# <20><>ɫ<EFBFBD><C9AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue }
function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
function Write-Success { param($Message) Write-Host "[SUCCESS] $Message" -ForegroundColor Green }
function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 }
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԱȨ<D4B1><C8A8>
function Test-Administrator {
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal $user
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-Administrator)) {
Write-Error "<EFBFBD><EFBFBD><EFBFBD>Թ<EFBFBD><EFBFBD><EFBFBD>ԱȨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д˽ű<EFBFBD>"
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
function Show-Help {
Write-Host @"
<EFBFBD>÷<EFBFBD>: $(Split-Path $MyInvocation.ScriptName -Leaf) [ѡ<EFBFBD><EFBFBD>]
ѡ<EFBFBD><EFBFBD>:
-NoVS <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Visual Studio Build Tools
-NoRust <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Rust
-NoNode <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Node.js
-Help <EFBFBD><EFBFBD>ʾ<EFBFBD>˰<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
ʾ<EFBFBD><EFBFBD>:
.\setup.ps1
.\setup.ps1 -NoVS
.\setup.ps1 -NoRust -NoNode
"@
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
param(
[switch]$NoVS,
[switch]$NoRust,
[switch]$NoNode,
[switch]$Help
)
if ($Help) {
Show-Help
exit 0
}
# <20><><EFBFBD><EFBFBD><E9B2A2>װ Chocolatey
function Install-Chocolatey {
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Chocolatey..."
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Info "<EFBFBD><EFBFBD>װ Chocolatey..."
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
try {
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
catch {
Write-Error "<EFBFBD><EFBFBD>װ Chocolatey ʧ<><CAA7>: $_"
}
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
}
# <20><>װ Visual Studio Build Tools
function Install-VSBuildTools {
if ($NoVS) {
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Visual Studio Build Tools <20><>װ"
return
}
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Visual Studio Build Tools..."
$vsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vsPath)) {
Write-Info "<EFBFBD><EFBFBD>װ Visual Studio Build Tools..."
try {
# <20><><EFBFBD>ذ<EFBFBD>װ<EFBFBD><D7B0><EFBFBD><EFBFBD>
$vsInstallerUrl = "https://aka.ms/vs/17/release/vs_BuildTools.exe"
$vsInstallerPath = "$env:TEMP\vs_BuildTools.exe"
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath
# <20><>װ
$process = Start-Process -FilePath $vsInstallerPath -ArgumentList `
"--quiet", "--wait", "--norestart", "--nocache", `
"--installPath", "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools", `
"--add", "Microsoft.VisualStudio.Workload.VCTools" `
-NoNewWindow -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Error "Visual Studio Build Tools <20><>װʧ<D7B0><CAA7>"
}
Remove-Item $vsInstallerPath -Force
}
catch {
Write-Error "<EFBFBD><EFBFBD>װ Visual Studio Build Tools ʧ<><CAA7>: $_"
}
}
else {
Write-Info "Visual Studio Build Tools <20>Ѱ<EFBFBD>װ"
}
}
# <20><>װ Rust
function Install-Rust {
if ($NoRust) {
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust <20><>װ"
return
}
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust..."
if (-not (Get-Command rustc -ErrorAction SilentlyContinue)) {
Write-Info "<EFBFBD><EFBFBD>װ Rust..."
try {
$rustupInit = "$env:TEMP\rustup-init.exe"
Invoke-WebRequest -Uri "https://win.rustup.rs" -OutFile $rustupInit
Start-Process -FilePath $rustupInit -ArgumentList "-y" -Wait
Remove-Item $rustupInit -Force
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
catch {
Write-Error "<EFBFBD><EFBFBD>װ Rust ʧ<><CAA7>: $_"
}
}
# <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ƽ̨
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust Ŀ<><C4BF>ƽ̨..."
$arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" }
rustup target add "$arch-pc-windows-msvc"
}
# <20><>װ<EFBFBD><D7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function Install-Tools {
Write-Info "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
# <20><>װ protoc
if (-not (Get-Command protoc -ErrorAction SilentlyContinue)) {
Write-Info "<EFBFBD><EFBFBD>װ Protocol Buffers..."
choco install -y protoc
}
# <20><>װ Git
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Write-Info "<EFBFBD><EFBFBD>װ Git..."
choco install -y git
}
# <20><>װ Node.js
if (-not $NoNode -and -not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Info "<EFBFBD><EFBFBD>װ Node.js..."
choco install -y nodejs
}
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
try {
Write-Info "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
Install-Chocolatey
Install-VSBuildTools
Install-Rust
Install-Tools
Write-Success "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
}
catch {
Write-Error "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>г<EFBFBD><EFBFBD>ִ<EFBFBD><EFBFBD><EFBFBD>: $_"
}

157
scripts/setup.sh Normal file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# 设置错误时退出
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
info() {
echo -e "${BLUE}[INFO] $1${NC}"
}
error() {
echo -e "${RED}[ERROR] $1${NC}"
exit 1
}
# 检查是否为 root 用户FreeBSD 和 Linux
if [ "$(uname)" != "Darwin" ] && [ "$EUID" -ne 0 ]; then
error "请使用 root 权限运行此脚本 (sudo ./setup.sh)"
fi
# 检测包管理器
if command -v brew &> /dev/null; then
PKG_MANAGER="brew"
info "检测到 macOS/Homebrew 系统"
elif command -v pkg &> /dev/null; then
PKG_MANAGER="pkg"
info "检测到 FreeBSD 系统"
elif command -v apt-get &> /dev/null; then
PKG_MANAGER="apt-get"
info "检测到 Debian/Ubuntu 系统"
elif command -v dnf &> /dev/null; then
PKG_MANAGER="dnf"
info "检测到 Fedora/RHEL 系统"
elif command -v yum &> /dev/null; then
PKG_MANAGER="yum"
info "检测到 CentOS 系统"
else
error "未检测到支持的包管理器"
fi
# 更新包管理器缓存
info "更新包管理器缓存..."
case $PKG_MANAGER in
"brew")
brew update
;;
"pkg")
pkg update
;;
*)
$PKG_MANAGER update -y
;;
esac
# 安装基础构建工具
info "安装基础构建工具..."
case $PKG_MANAGER in
"brew")
brew install \
protobuf \
pkg-config \
openssl \
curl \
git \
node
;;
"pkg")
pkg install -y \
gmake \
protobuf \
pkgconf \
openssl \
curl \
git \
node
;;
"apt-get")
$PKG_MANAGER install -y --no-install-recommends \
build-essential \
protobuf-compiler \
pkg-config \
libssl-dev \
ca-certificates \
curl \
tzdata \
git
;;
*)
$PKG_MANAGER install -y \
gcc \
gcc-c++ \
make \
protobuf-compiler \
pkg-config \
openssl-devel \
ca-certificates \
curl \
tzdata \
git
;;
esac
# 安装 Node.js 和 npm如果还没有通过包管理器安装
if ! command -v node &> /dev/null && [ "$PKG_MANAGER" != "brew" ] && [ "$PKG_MANAGER" != "pkg" ]; then
info "安装 Node.js 和 npm..."
if [ "$PKG_MANAGER" = "apt-get" ]; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
$PKG_MANAGER install -y nodejs
else
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
$PKG_MANAGER install -y nodejs
fi
fi
# 安装 Rust如果未安装
if ! command -v rustc &> /dev/null; then
info "安装 Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"
fi
# 添加目标平台
info "添加 Rust 目标平台..."
case "$(uname)" in
"FreeBSD")
rustup target add x86_64-unknown-freebsd
;;
"Darwin")
rustup target add x86_64-apple-darwin aarch64-apple-darwin
;;
*)
rustup target add x86_64-unknown-linux-gnu
;;
esac
# 清理包管理器缓存
case $PKG_MANAGER in
"apt-get")
rm -rf /var/lib/apt/lists/*
;;
"pkg")
pkg clean -y
;;
esac
# 设置时区(除了 macOS
if [ "$(uname)" != "Darwin" ]; then
info "设置时区为 Asia/Shanghai..."
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
fi
echo -e "${GREEN}安装完成!${NC}"

1
src/aiserver.rs Normal file
View File

@@ -0,0 +1 @@
pub mod v1;

1
src/aiserver/v1.rs Normal file
View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));

File diff suppressed because it is too large Load Diff

5
src/app.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod models;
pub mod constant;
pub mod token;
pub mod utils;
pub mod client;

29
src/app/client.rs Normal file
View File

@@ -0,0 +1,29 @@
use crate::app::constant::*;
use reqwest::Client;
use uuid::Uuid;
/// 返回预构建的 Cursor API 客户端
pub fn build_client(auth_token: &str, checksum: &str, endpoint: &str) -> reqwest::RequestBuilder {
let client = Client::new();
let trace_id = Uuid::new_v4().to_string();
let content_type = if endpoint == CURSOR_API2_STREAM_CHAT {
CONTENT_TYPE_CONNECT_PROTO
} else {
CONTENT_TYPE_PROTO
};
client
.post(format!("{}{}", CURSOR_API2_BASE_URL, endpoint))
.header(HEADER_NAME_CONTENT_TYPE, content_type)
.header(HEADER_NAME_AUTHORIZATION, 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("x-amzn-trace-id", format!("Root={}", trace_id))
.header("x-cursor-checksum", checksum)
.header("x-cursor-client-version", "0.42.5")
.header("x-cursor-timezone", "Asia/Shanghai")
.header("x-ghost-mode", "false")
.header("x-request-id", trace_id)
.header("Host", CURSOR_API2_HOST)
}

64
src/app/constant.rs Normal file
View File

@@ -0,0 +1,64 @@
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
pub const PKG_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
pub const PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
pub const ROUTER_ROOT_PATH: &str = "/";
pub const ROUTER_HEALTH_PATH: &str = "/health";
pub const ROUTER_GET_CHECKSUM: &str = "/get-checksum";
pub const ROUTER_GET_USER_INFO_PATH: &str = "/get-user-info";
pub const ROUTER_LOGS_PATH: &str = "/logs";
pub const ROUTER_CONFIG_PATH: &str = "/config";
pub const ROUTER_TOKENINFO_PATH: &str = "/tokeninfo";
pub const ROUTER_GET_TOKENINFO_PATH: &str = "/get-tokeninfo";
pub const ROUTER_UPDATE_TOKENINFO_PATH: &str = "/update-tokeninfo";
pub const ROUTER_ENV_EXAMPLE_PATH: &str = "/env-example";
pub const ROUTER_SHARED_STYLES_PATH: &str = "/static/shared-styles.css";
pub const ROUTER_SHARED_JS_PATH: &str = "/static/shared.js";
pub const STATUS: &str = "status";
pub const MESSAGE: &str = "message";
pub const ERROR: &str = "error";
pub const TOKEN_FILE: &str = "token_file";
pub const TOKEN_LIST_FILE: &str = "token_list_file";
pub const TOKENS: &str = "tokens";
pub const TOKEN_LIST: &str = "token_list";
pub const STATUS_SUCCESS: &str = "success";
pub const STATUS_FAILED: &str = "failed";
pub const HEADER_NAME_CONTENT_TYPE: &str = "content-type";
pub const HEADER_NAME_AUTHORIZATION: &str = "Authorization";
pub const CONTENT_TYPE_PROTO: &str = "application/proto";
pub const CONTENT_TYPE_CONNECT_PROTO: &str = "application/connect+proto";
pub const CONTENT_TYPE_TEXT_HTML_WITH_UTF8: &str = "text/html;charset=utf-8";
pub const CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8: &str = "text/plain;charset=utf-8";
pub const AUTHORIZATION_BEARER_PREFIX: &str = "Bearer ";
pub const OBJECT_CHAT_COMPLETION: &str = "chat.completion";
pub const OBJECT_CHAT_COMPLETION_CHUNK: &str = "chat.completion.chunk";
pub const CURSOR_API2_HOST: &str = "api2.cursor.sh";
pub const CURSOR_API2_BASE_URL: &str = "https://api2.cursor.sh/aiserver.v1.AiService/";
pub const CURSOR_API2_STREAM_CHAT: &str = "StreamChat";
pub const CURSOR_API2_GET_USER_INFO: &str = "GetUserInfo";
pub const FINISH_REASON_STOP: &str = "stop";
pub const LONG_CONTEXT_MODELS: [&str; 4] = [
"gpt-4o-128k",
"gemini-1.5-flash-500k",
"claude-3-haiku-200k",
"claude-3-5-sonnet-200k",
];
pub const MODEL_OBJECT: &str = "model";
pub const ANTHROPIC: &str = "anthropic";
pub const CURSOR: &str = "cursor";
pub const GOOGLE: &str = "google";
pub const OPENAI: &str = "openai";

447
src/app/models.rs Normal file
View File

@@ -0,0 +1,447 @@
use super::{constant::*, token::UserUsageInfo};
use crate::message::Message;
use chrono::{DateTime, Local};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
// 页面内容类型枚举
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
pub enum PageContent {
#[serde(rename = "default")]
Default, // 默认行为
#[serde(rename = "text")]
Text(String), // 纯文本
#[serde(rename = "html")]
Html(String), // HTML 内容
}
impl Default for PageContent {
fn default() -> Self {
Self::Default
}
}
// 静态配置
#[derive(Clone)]
pub struct AppConfig {
enable_stream_check: bool,
include_stop_stream: bool,
vision_ability: VisionAbility,
enable_slow_pool: bool,
allow_claude: bool,
auth_token: String,
token_file: String,
token_list_file: String,
route_prefix: String,
pub start_time: chrono::DateTime<chrono::Local>,
pages: Pages,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum VisionAbility {
#[serde(rename = "none", alias = "disabled")]
None,
#[serde(rename = "base64", alias = "base64-only")]
Base64,
#[serde(rename = "all", alias = "base64-http")]
All,
}
impl VisionAbility {
pub fn from_str(s: &str) -> Result<Self, &'static str> {
match s.to_lowercase().as_str() {
"none" | "disabled" => Ok(Self::None),
"base64" | "base64-only" => Ok(Self::Base64),
"all" | "base64-http" => Ok(Self::All),
_ => Err("Invalid VisionAbility value"),
}
}
}
impl Default for VisionAbility {
fn default() -> Self {
Self::Base64
}
}
#[derive(Clone)]
pub struct Pages {
pub root_content: PageContent,
pub logs_content: PageContent,
pub config_content: PageContent,
pub tokeninfo_content: PageContent,
pub shared_styles_content: PageContent,
pub shared_js_content: PageContent,
}
impl Default for Pages {
fn default() -> Self {
Self {
root_content: PageContent::Default,
logs_content: PageContent::Default,
config_content: PageContent::Default,
tokeninfo_content: PageContent::Default,
shared_styles_content: PageContent::Default,
shared_js_content: PageContent::Default,
}
}
}
// 运行时状态
pub struct AppState {
pub total_requests: u64,
pub active_requests: u64,
pub request_logs: Vec<RequestLog>,
pub token_infos: Vec<TokenInfo>,
}
// 全局配置实例
lazy_static! {
pub static ref APP_CONFIG: RwLock<AppConfig> = RwLock::new(AppConfig::default());
}
impl Default for AppConfig {
fn default() -> Self {
Self {
enable_stream_check: true,
include_stop_stream: true,
vision_ability: VisionAbility::Base64,
enable_slow_pool: false,
allow_claude: false,
auth_token: String::new(),
token_file: ".token".to_string(),
token_list_file: ".token-list".to_string(),
route_prefix: String::new(),
start_time: chrono::Local::now(),
pages: Pages::default(),
}
}
}
impl AppConfig {
pub fn init(
enable_stream_check: bool,
include_stop_stream: bool,
vision_ability: VisionAbility,
enable_slow_pool: bool,
allow_claude: bool,
auth_token: String,
token_file: String,
token_list_file: String,
route_prefix: String,
) {
if let Ok(mut config) = APP_CONFIG.write() {
config.enable_stream_check = enable_stream_check;
config.include_stop_stream = include_stop_stream;
config.vision_ability = vision_ability;
config.enable_slow_pool = enable_slow_pool;
config.allow_claude = allow_claude;
config.auth_token = auth_token;
config.token_file = token_file;
config.token_list_file = token_list_file;
config.route_prefix = route_prefix;
}
}
pub fn get_stream_check() -> bool {
APP_CONFIG
.read()
.map(|config| config.enable_stream_check)
.unwrap_or(true)
}
pub fn get_stop_stream() -> bool {
APP_CONFIG
.read()
.map(|config| config.include_stop_stream)
.unwrap_or(true)
}
pub fn get_vision_ability() -> VisionAbility {
APP_CONFIG
.read()
.map(|config| config.vision_ability.clone())
.unwrap_or_default()
}
pub fn get_slow_pool() -> bool {
APP_CONFIG
.read()
.map(|config| config.enable_slow_pool)
.unwrap_or(false)
}
pub fn get_allow_claude() -> bool {
APP_CONFIG
.read()
.map(|config| config.allow_claude)
.unwrap_or(false)
}
pub fn get_auth_token() -> String {
APP_CONFIG
.read()
.map(|config| config.auth_token.clone())
.unwrap_or_default()
}
pub fn get_token_file() -> String {
APP_CONFIG
.read()
.map(|config| config.token_file.clone())
.unwrap_or_default()
}
pub fn get_token_list_file() -> String {
APP_CONFIG
.read()
.map(|config| config.token_list_file.clone())
.unwrap_or_default()
}
pub fn get_route_prefix() -> String {
APP_CONFIG
.read()
.map(|config| config.route_prefix.clone())
.unwrap_or_default()
}
pub fn get_page_content(path: &str) -> Option<PageContent> {
APP_CONFIG.read().ok().map(|config| match path {
ROUTER_ROOT_PATH => config.pages.root_content.clone(),
ROUTER_LOGS_PATH => config.pages.logs_content.clone(),
ROUTER_CONFIG_PATH => config.pages.config_content.clone(),
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content.clone(),
ROUTER_SHARED_STYLES_PATH => config.pages.shared_styles_content.clone(),
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content.clone(),
_ => PageContent::Default,
})
}
pub fn update_stream_check(enable: bool) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.enable_stream_check = enable;
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn update_stop_stream(enable: bool) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.include_stop_stream = enable;
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn update_vision_ability(new_ability: VisionAbility) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.vision_ability = new_ability;
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn update_slow_pool(enable: bool) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.enable_slow_pool = enable;
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn update_allow_claude(enable: bool) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.allow_claude = enable;
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
match path {
ROUTER_ROOT_PATH => config.pages.root_content = content,
ROUTER_LOGS_PATH => config.pages.logs_content = content,
ROUTER_CONFIG_PATH => config.pages.config_content = content,
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content = content,
ROUTER_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content = content,
_ => return Err("无效的路径"),
}
Ok(())
} else {
Err("无法更新配置")
}
}
pub fn reset_stream_check() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.enable_stream_check = true;
Ok(())
} else {
Err("无法重置配置")
}
}
pub fn reset_stop_stream() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.include_stop_stream = true;
Ok(())
} else {
Err("无法重置配置")
}
}
pub fn reset_vision_ability() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.vision_ability = VisionAbility::Base64;
Ok(())
} else {
Err("无法重置配置")
}
}
pub fn reset_slow_pool() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.enable_slow_pool = false;
Ok(())
} else {
Err("无法重置配置")
}
}
pub fn reset_allow_claude() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.allow_claude = false;
Ok(())
} else {
Err("无法重置配置")
}
}
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
match path {
ROUTER_ROOT_PATH => config.pages.root_content = PageContent::Default,
ROUTER_LOGS_PATH => config.pages.logs_content = PageContent::Default,
ROUTER_CONFIG_PATH => config.pages.config_content = PageContent::Default,
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content = PageContent::Default,
ROUTER_SHARED_STYLES_PATH => {
config.pages.shared_styles_content = PageContent::Default
}
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::Default,
_ => return Err("无效的路径"),
}
Ok(())
} else {
Err("无法重置配置")
}
}
}
impl AppState {
pub fn new(token_infos: Vec<TokenInfo>) -> Self {
Self {
total_requests: 0,
active_requests: 0,
request_logs: Vec::new(),
token_infos,
}
}
pub fn update_token_infos(&mut self, token_infos: Vec<TokenInfo>) {
self.token_infos = token_infos;
}
}
// 模型定义
#[derive(Serialize, Clone)]
pub struct Model {
pub id: String,
pub created: i64,
pub object: String,
pub owned_by: String,
}
// impl Model {
// pub fn is_pesticide(&self) -> bool {
// !(self.owned_by.as_str() == CURSOR || self.id.as_str() == "gpt-4o-mini")
// }
// }
// 请求日志
#[derive(Serialize, Clone)]
pub struct RequestLog {
pub timestamp: DateTime<Local>,
pub model: String,
pub token_info: TokenInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
pub stream: bool,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
// pub struct PromptList(Option<String>);
// impl PromptList {
// pub fn to_vec(&self) -> Vec<>
// }
// 聊天请求
#[derive(Deserialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(default)]
pub stream: bool,
}
// 用于存储 token 信息
#[derive(Serialize, Clone)]
pub struct TokenInfo {
pub token: String,
pub checksum: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<UserUsageInfo>,
}
// TokenUpdateRequest 结构体
#[derive(Deserialize)]
pub struct TokenUpdateRequest {
pub tokens: String,
#[serde(default)]
pub token_list: Option<String>,
}
// 添加用于接收更新请求的结构体
#[derive(Deserialize)]
pub struct ConfigUpdateRequest {
#[serde(default)]
pub action: String, // "get", "update", "reset"
#[serde(default)]
pub path: String,
#[serde(default)]
pub content: Option<PageContent>, // "default", "text", "html"
#[serde(default)]
pub enable_stream_check: Option<bool>,
#[serde(default)]
pub include_stop_stream: Option<bool>,
#[serde(default)]
pub vision_ability: Option<VisionAbility>,
#[serde(default)]
pub enable_slow_pool: Option<bool>,
#[serde(default)]
pub enable_all_claude: Option<bool>,
}

313
src/app/token.rs Normal file
View File

@@ -0,0 +1,313 @@
use super::{
constant::*,
models::{AppConfig, AppState, TokenInfo, TokenUpdateRequest},
utils::i32_to_u32,
};
use crate::aiserver::v1::GetUserInfoResponse;
use axum::http::HeaderMap;
use axum::{
extract::{Query, State},
Json,
};
use image::EncodableLayout;
use prost::Message;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
// 规范化文件内容并写入
fn normalize_and_write(content: &str, file_path: &str) -> String {
let normalized = content.replace("\r\n", "\n");
if normalized != content {
if let Err(e) = std::fs::write(file_path, &normalized) {
eprintln!("警告: 无法更新规范化的文件: {}", e);
}
}
normalized
}
// 解析token和别名
fn parse_token_alias(token_part: &str, line: &str) -> Option<(String, Option<String>)> {
match token_part.split("::").collect::<Vec<_>>() {
parts if parts.len() == 1 => Some((parts[0].to_string(), None)),
parts if parts.len() == 2 => Some((parts[1].to_string(), Some(parts[0].to_string()))),
_ => {
eprintln!("警告: 忽略无效的行: {}", line);
None
}
}
}
// Token 加载函数
pub fn load_tokens() -> Vec<TokenInfo> {
let token_file = AppConfig::get_token_file();
let token_list_file = AppConfig::get_token_list_file();
// 确保文件存在
for file in [&token_file, &token_list_file] {
if !std::path::Path::new(file).exists() {
if let Err(e) = std::fs::write(file, "") {
eprintln!("警告: 无法创建文件 '{}': {}", file, e);
}
}
}
// 读取和规范化 token 文件
let token_entries = match std::fs::read_to_string(&token_file) {
Ok(content) => {
let normalized = normalize_and_write(&content, &token_file);
normalized
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
parse_token_alias(line, line)
})
.collect::<Vec<_>>()
}
Err(e) => {
eprintln!("警告: 无法读取token文件 '{}': {}", token_file, e);
Vec::new()
}
};
// 读取和规范化 token-list 文件
let mut token_map: std::collections::HashMap<String, (String, Option<String>)> =
match std::fs::read_to_string(&token_list_file) {
Ok(content) => {
let normalized = normalize_and_write(&content, &token_list_file);
normalized
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let parts: Vec<&str> = line.split(',').collect();
match parts[..] {
[token_part, checksum] => {
let (token, alias) = parse_token_alias(token_part, line)?;
Some((token, (checksum.to_string(), alias)))
}
_ => {
eprintln!("警告: 忽略无效的token-list行: {}", line);
None
}
}
})
.collect()
}
Err(e) => {
eprintln!("警告: 无法读取token-list文件: {}", e);
std::collections::HashMap::new()
}
};
// 更新或添加新token
for (token, alias) in token_entries {
if let Some((_, existing_alias)) = token_map.get(&token) {
// 只在alias不同时更新已存在的token
if alias != *existing_alias {
if let Some((checksum, _)) = token_map.get(&token) {
token_map.insert(token.clone(), (checksum.clone(), alias));
}
}
} else {
// 为新token生成checksum
let checksum =
crate::generate_checksum(&crate::generate_hash(), Some(&crate::generate_hash()));
token_map.insert(token, (checksum, alias));
}
}
// 更新 token-list 文件
let token_list_content = token_map
.iter()
.map(|(token, (checksum, alias))| {
if let Some(alias) = alias {
format!("{}::{},{}", alias, token, checksum)
} else {
format!("{},{}", token, checksum)
}
})
.collect::<Vec<_>>()
.join("\n");
if let Err(e) = std::fs::write(&token_list_file, token_list_content) {
eprintln!("警告: 无法更新token-list文件: {}", e);
}
// 转换为 TokenInfo vector
token_map
.into_iter()
.map(|(token, (checksum, alias))| TokenInfo {
token,
checksum,
alias,
usage: None,
})
.collect()
}
// 更新 TokenInfo 处理
pub async fn handle_update_tokeninfo(
State(state): State<Arc<Mutex<AppState>>>,
) -> Json<serde_json::Value> {
// 重新加载 tokens
let token_infos = load_tokens();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Json(serde_json::json!({
STATUS: STATUS_SUCCESS,
MESSAGE: "Token list has been reloaded"
}))
}
// 获取 TokenInfo 处理
pub async fn handle_get_tokeninfo(
State(_state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
let auth_token = AppConfig::get_auth_token();
let token_file = AppConfig::get_token_file();
let token_list_file = AppConfig::get_token_list_file();
// 验证 AUTH_TOKEN
let auth_header = headers
.get(HEADER_NAME_AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != auth_token {
return Err(StatusCode::UNAUTHORIZED);
}
// 读取文件内容
let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new());
let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new());
Ok(Json(serde_json::json!({
STATUS: STATUS_SUCCESS,
"token_file": token_file,
"token_list_file": token_list_file,
"tokens": tokens,
"token_list": token_list
})))
}
pub async fn handle_update_tokeninfo_post(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<TokenUpdateRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let auth_token = AppConfig::get_auth_token();
let token_file = AppConfig::get_token_file();
let token_list_file = AppConfig::get_token_list_file();
// 验证 AUTH_TOKEN
let auth_header = headers
.get(HEADER_NAME_AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != auth_token {
return Err(StatusCode::UNAUTHORIZED);
}
// 写入 .token 文件
std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// 如果提供了 token_list则写入
if let Some(token_list) = request.token_list {
std::fs::write(&token_list_file, token_list)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// 重新加载 tokens
let token_infos = load_tokens();
let token_infos_len = token_infos.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Ok(Json(serde_json::json!({
STATUS: STATUS_SUCCESS,
MESSAGE: "Token files have been updated and reloaded",
"token_file": token_file,
"token_list_file": token_list_file,
"token_count": token_infos_len
})))
}
#[derive(Deserialize)]
pub struct GetUserInfoQuery {
alias: String,
}
pub async fn get_user_info(
State(state): State<Arc<Mutex<AppState>>>,
Query(query): Query<GetUserInfoQuery>,
) -> Json<GetUserInfo> {
let (auth_token, checksum) = match {
let app_token_infos = &state.lock().await.token_infos;
app_token_infos
.iter()
.find(|token_info| token_info.alias == Some(query.alias.clone()))
.map(|token_info| (token_info.token.clone(), token_info.checksum.clone()))
} {
Some(token) => token,
None => return Json(GetUserInfo::Error("No data".to_string())),
};
match get_user_usage(&auth_token, &checksum).await {
Some(usage) => Json(GetUserInfo::Usage(usage)),
None => Json(GetUserInfo::Error("No data".to_string())),
}
}
pub async fn get_user_usage(auth_token: &str, checksum: &str) -> Option<UserUsageInfo> {
// 构建请求客户端
let client = super::client::build_client(auth_token, checksum, CURSOR_API2_GET_USER_INFO);
let response = client
.body(Vec::new())
.send()
.await
.ok()?
.bytes()
.await
.ok()?;
let user_info = GetUserInfoResponse::decode(response.as_bytes()).ok()?;
user_info.usage.map(|user_usage| UserUsageInfo {
fast_requests: i32_to_u32(user_usage.gpt4_requests),
max_fast_requests: i32_to_u32(user_usage.gpt4_max_requests),
})
}
#[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,
}

22
src/app/utils.rs Normal file
View File

@@ -0,0 +1,22 @@
pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
std::env::var(key)
.ok()
.map(|v| match v.to_lowercase().as_str() {
"true" | "1" => true,
"false" | "0" => false,
_ => default,
})
.unwrap_or(default)
}
pub fn parse_string_from_env(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
pub fn i32_to_u32(value: i32) -> u32 {
if value < 0 {
0
} else {
value as u32
}
}

2
src/chat.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod stream;
pub mod error;

154
src/chat/error.rs Normal file
View File

@@ -0,0 +1,154 @@
use crate::aiserver::v1::throw_error_check_request::Error as ErrorType;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ChatError {
pub error: ErrorBody,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorBody {
pub code: String,
pub message: String,
pub details: Vec<ErrorDetail>,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorDetail {
#[serde(rename = "type")]
pub error_type: String,
pub debug: ErrorDebug,
pub value: String,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorDebug {
pub error: String,
pub details: ErrorDetails,
#[serde(rename = "isExpected")]
pub is_expected: bool,
}
impl ErrorDebug {
pub fn is_valid(&self) -> bool {
ErrorType::from_str_name(&self.error).is_some()
}
pub fn status_code(&self) -> u16 {
match ErrorType::from_str_name(&self.error) {
Some(error) => match error {
ErrorType::Unspecified => 500,
ErrorType::BadApiKey
| ErrorType::BadUserApiKey
| ErrorType::InvalidAuthId
| ErrorType::AuthTokenNotFound
| ErrorType::AuthTokenExpired
| ErrorType::Unauthorized => 401,
ErrorType::NotLoggedIn
| ErrorType::NotHighEnoughPermissions
| ErrorType::AgentRequiresLogin
| ErrorType::ProUserOnly
| ErrorType::TaskNoPermissions => 403,
ErrorType::NotFound
| ErrorType::UserNotFound
| ErrorType::TaskUuidNotFound
| ErrorType::AgentEngineNotFound
| ErrorType::GitgraphNotFound
| ErrorType::FileNotFound => 404,
ErrorType::FreeUserRateLimitExceeded
| ErrorType::ProUserRateLimitExceeded
| ErrorType::OpenaiRateLimitExceeded
| ErrorType::OpenaiAccountLimitExceeded
| ErrorType::GenericRateLimitExceeded
| ErrorType::Gpt4VisionPreviewRateLimit
| ErrorType::ApiKeyRateLimit => 429,
ErrorType::BadRequest
| ErrorType::BadModelName
| ErrorType::SlashEditFileTooLong
| ErrorType::FileUnsupported
| ErrorType::ClaudeImageTooLarge => 400,
_ => 500,
},
None => 500,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct ErrorDetails {
pub title: String,
pub detail: String,
#[serde(rename = "isRetryable")]
pub is_retryable: bool,
}
impl ChatError {
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap()
}
pub fn to_error_response(&self) -> ErrorResponse {
if self.error.details.is_empty() {
return ErrorResponse {
status: 500,
code: "ERROR_UNKNOWN".to_string(),
error: None,
};
}
ErrorResponse {
status: self.error.details[0].debug.status_code(),
code: self.error.details[0].debug.error.clone(),
error: Some(Error {
message: self.error.details[0].debug.details.title.clone(),
details: self.error.details[0].debug.details.detail.clone(),
value: self.error.details[0].value.clone(),
}),
}
}
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub status: u16,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<Error>,
}
#[derive(Serialize)]
pub struct Error {
pub message: String,
pub details: String,
pub value: String,
}
impl ErrorResponse {
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap()
}
pub fn status_code(&self) -> StatusCode {
StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
pub fn native_code(&self) -> String {
self.code.replace("_", " ").to_lowercase()
}
}
pub enum StreamError {
ChatError(ChatError),
DataLengthLessThan5,
EmptyMessage,
}
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamError::ChatError(error) => write!(f, "{}", serde_json::to_string(error).unwrap()),
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
StreamError::EmptyMessage => write!(f, "empty message"),
}
}
}

127
src/chat/stream.rs Normal file
View File

@@ -0,0 +1,127 @@
use crate::aiserver::v1::StreamChatResponse;
use flate2::read::GzDecoder;
use prost::Message;
use std::io::Read;
use super::error::{ChatError, StreamError};
// 解压gzip数据
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
match decoder.read_to_end(&mut decompressed) {
Ok(_) => Some(decompressed),
Err(_) => {
// println!("gzip解压失败: {}", e);
None
}
}
}
pub enum StreamMessage {
// 调试
Debug(String),
// 流开始标志 b"\0\0\0\0\0"
StreamStart,
// 消息内容
Content(Vec<String>),
// 流结束标志 b"\x02\0\0\0\x02{}"
StreamEnd,
}
pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
if data.len() < 5 {
return Err(StreamError::DataLengthLessThan5);
}
// 检查是否为流开始标志
// if data == b"\0\0\0\0\0" {
// return Ok(StreamMessage::StreamStart);
// }
// 检查是否为流结束标志
// if data == b"\x02\0\0\0\x02{}" {
// return Ok(StreamMessage::StreamEnd);
// }
let mut messages = Vec::new();
let mut offset = 0;
while offset + 5 <= data.len() {
// 获取消息类型和长度
let msg_type = data[offset];
let msg_len = u32::from_be_bytes([
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
]) as usize;
// 流开始
if msg_type == 0 && msg_len == 0 {
return Ok(StreamMessage::StreamStart);
}
// 检查剩余数据长度是否足够
if offset + 5 + msg_len > data.len() {
break;
}
let msg_data = &data[offset + 5..offset + 5 + msg_len];
match msg_type {
// 文本消息
0 => {
if let Ok(response) = StreamChatResponse::decode(msg_data) {
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[text] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
));
}
}
}
// gzip压缩消息
1 => {
if let Some(text) = decompress_gzip(msg_data) {
let response = StreamChatResponse::decode(&text[..]).unwrap_or_default();
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[gzip] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
));
}
}
}
// JSON字符串
2 => {
if msg_len == 2 {
return Ok(StreamMessage::StreamEnd);
}
if let Ok(text) = String::from_utf8(msg_data.to_vec()) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
// 未预计
// messages.push(text);
}
}
// 其他类型暂不处理
t => eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t),
}
offset += 5 + msg_len;
}
if messages.is_empty() {
Err(StreamError::EmptyMessage)
} else {
Ok(StreamMessage::Content(messages))
}
}

View File

@@ -1,29 +1,40 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use flate2::read::GzDecoder;
use prost::Message;
use rand::{thread_rng, Rng};
use image::guess_format;
use prost::Message as _;
use rand::Rng;
use sha2::{Digest, Sha256};
use std::io::Read;
use uuid::Uuid;
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/cursor.rs"));
}
mod aiserver;
use aiserver::v1::*;
use proto::{ChatMessage, ResMessage};
pub mod message;
use message::*;
#[derive(Debug)]
pub struct ChatInput {
pub role: String,
pub content: String,
}
pub mod app;
use app::{models::*,constant::*};
fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_message::Message>) {
pub mod chat;
async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationMessage>) {
// 收集 system 和 developer 指令
let instructions = inputs
.iter()
.filter(|input| input.role == "system" || input.role == "developer")
.map(|input| input.content.clone())
.filter(|input| input.role == Role::System)
.map(|input| match &input.content {
MessageContent::Text(text) => text.clone(),
MessageContent::Vision(contents) => contents
.iter()
.filter_map(|content| {
if content.content_type == "text" {
content.text.clone()
} else {
None
}
})
.collect::<Vec<String>>()
.join("\n"),
})
.collect::<Vec<String>>()
.join("\n\n");
@@ -35,19 +46,48 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
};
// 过滤出 user 和 assistant 对话
let mut chat_inputs: Vec<ChatInput> = inputs
let mut chat_inputs: Vec<Message> = inputs
.into_iter()
.filter(|input| input.role == "user" || input.role == "assistant")
.filter(|input| input.role == Role::User || input.role == Role::Assistant)
.collect();
// 处理空对话情况
if chat_inputs.is_empty() {
return (
instructions,
vec![proto::chat_message::Message {
role: 1, // user
content: " ".to_string(),
message_id: Uuid::new_v4().to_string(),
vec![ConversationMessage {
text: " ".to_string(),
r#type: conversation_message::MessageType::Human as i32,
attached_code_chunks: vec![],
codebase_context_chunks: vec![],
commits: vec![],
pull_requests: vec![],
git_diffs: vec![],
assistant_suggested_diffs: vec![],
interpreter_results: vec![],
images: vec![],
attached_folders: vec![],
approximate_lint_errors: vec![],
bubble_id: Uuid::new_v4().to_string(),
server_bubble_id: None,
attached_folders_new: vec![],
lints: vec![],
user_responses_to_suggested_code_blocks: vec![],
relevant_files: vec![],
tool_results: vec![],
notepads: vec![],
is_capability_iteration: Some(false),
capabilities: vec![],
edit_trail_contexts: vec![],
suggested_code_blocks: vec![],
diffs_for_compressing_files: vec![],
multi_file_linter_errors: vec![],
diff_histories: vec![],
recently_viewed_files: vec![],
recent_locations_history: vec![],
is_agentic: false,
file_diff_trajectories: vec![],
conversation_summary: None,
}],
);
}
@@ -55,13 +95,13 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
// 如果第一条是 assistant插入空的 user 消息
if chat_inputs
.first()
.map_or(false, |input| input.role == "assistant")
.map_or(false, |input| input.role == Role::Assistant)
{
chat_inputs.insert(
0,
ChatInput {
role: "user".to_string(),
content: " ".to_string(),
Message {
role: Role::User,
content: MessageContent::Text(" ".to_string()),
},
);
}
@@ -70,16 +110,16 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
let mut i = 1;
while i < chat_inputs.len() {
if chat_inputs[i].role == chat_inputs[i - 1].role {
let insert_role = if chat_inputs[i].role == "user" {
"assistant"
let insert_role = if chat_inputs[i].role == Role::User {
Role::Assistant
} else {
"user"
Role::User
};
chat_inputs.insert(
i,
ChatInput {
role: insert_role.to_string(),
content: " ".to_string(),
Message {
role: insert_role,
content: MessageContent::Text(" ".to_string()),
},
);
}
@@ -89,46 +129,266 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
// 确保最后一条是 user
if chat_inputs
.last()
.map_or(false, |input| input.role == "assistant")
.map_or(false, |input| input.role == Role::Assistant)
{
chat_inputs.push(ChatInput {
role: "user".to_string(),
content: " ".to_string(),
chat_inputs.push(Message {
role: Role::User,
content: MessageContent::Text(" ".to_string()),
});
}
// 转换为 proto messages
let messages = chat_inputs
.into_iter()
.map(|input| proto::chat_message::Message {
role: if input.role == "user" { 1 } else { 2 },
content: input.content,
message_id: Uuid::new_v4().to_string(),
})
.collect();
let mut messages = Vec::new();
for input in chat_inputs {
let (text, images) = match input.content {
MessageContent::Text(text) => (text, vec![]),
MessageContent::Vision(contents) => {
let mut text_parts = Vec::new();
let mut images = Vec::new();
for content in contents {
match content.content_type.as_str() {
"text" => {
if let Some(text) = content.text {
text_parts.push(text);
}
}
"image_url" => {
if let Some(image_url) = &content.image_url {
let url = image_url.url.clone();
let result =
tokio::spawn(async move { fetch_image_data(&url).await });
if let Ok(Ok((image_data, dimensions))) = result.await {
images.push(ImageProto {
data: image_data,
dimension: dimensions,
});
}
}
}
_ => {}
}
}
(text_parts.join("\n"), images)
}
};
messages.push(ConversationMessage {
text,
r#type: if input.role == Role::User {
conversation_message::MessageType::Human as i32
} else {
conversation_message::MessageType::Ai as i32
},
attached_code_chunks: vec![],
codebase_context_chunks: vec![],
commits: vec![],
pull_requests: vec![],
git_diffs: vec![],
assistant_suggested_diffs: vec![],
interpreter_results: vec![],
images,
attached_folders: vec![],
approximate_lint_errors: vec![],
bubble_id: Uuid::new_v4().to_string(),
server_bubble_id: None,
attached_folders_new: vec![],
lints: vec![],
user_responses_to_suggested_code_blocks: vec![],
relevant_files: vec![],
tool_results: vec![],
notepads: vec![],
is_capability_iteration: Some(false),
capabilities: vec![],
edit_trail_contexts: vec![],
suggested_code_blocks: vec![],
diffs_for_compressing_files: vec![],
multi_file_linter_errors: vec![],
diff_histories: vec![],
recently_viewed_files: vec![],
recent_locations_history: vec![],
is_agentic: false,
file_diff_trajectories: vec![],
conversation_summary: None,
});
}
(instructions, messages)
}
async fn fetch_image_data(
url: &str,
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁
let vision_ability = AppConfig::get_vision_ability();
match vision_ability {
VisionAbility::None => Err("图片功能已禁用".into()),
VisionAbility::Base64 => {
if !url.starts_with("data:image/") {
return Err("仅支持 base64 编码的图片".into());
}
process_base64_image(url)
}
VisionAbility::All => {
if url.starts_with("data:image/") {
process_base64_image(url)
} else {
process_http_image(url).await
}
}
}
}
// 处理 base64 编码的图片
fn process_base64_image(
url: &str,
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
let parts: Vec<&str> = url.split("base64,").collect();
if parts.len() != 2 {
return Err("无效的 base64 图片格式".into());
}
// 检查图片格式
let format = parts[0].to_lowercase();
if !format.contains("png")
&& !format.contains("jpeg")
&& !format.contains("jpg")
&& !format.contains("webp")
&& !format.contains("gif")
{
return Err("不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF".into());
}
let image_data = BASE64.decode(parts[1])?;
// 检查是否为动态 GIF
if format.contains("gif") {
if let Ok(frames) = gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data)) {
if frames.into_iter().count() > 1 {
return Err("不支持动态 GIF".into());
}
}
}
// 获取图片尺寸
let dimensions = if let Ok(img) = image::load_from_memory(&image_data) {
Some(image_proto::Dimension {
width: img.width() as i32,
height: img.height() as i32,
})
} else {
None
};
Ok((image_data, dimensions))
}
// 处理 HTTP 图片 URL
async fn process_http_image(
url: &str,
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
let response = reqwest::get(url).await?;
let image_data = response.bytes().await?.to_vec();
let format = guess_format(&image_data)?;
// 检查图片格式
match format {
image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP => {
// 这些格式都支持
}
image::ImageFormat::Gif => {
if let Ok(frames) =
gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data))
{
if frames.into_iter().count() > 1 {
return Err("不支持动态 GIF".into());
}
}
}
_ => return Err("不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF".into()),
}
// 获取图片尺寸
let dimensions = if let Ok(img) = image::load_from_memory_with_format(&image_data, format) {
Some(image_proto::Dimension {
width: img.width() as i32,
height: img.height() as i32,
})
} else {
None
};
Ok((image_data, dimensions))
}
pub async fn encode_chat_message(
inputs: Vec<ChatInput>,
inputs: Vec<Message>,
model_name: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let (instructions, messages) = process_chat_inputs(inputs);
// 在进入异步操作前获取并释放锁
let enable_slow_pool = {
if AppConfig::get_slow_pool() {
Some(true)
} else {
None
}
};
let chat = ChatMessage {
messages,
instructions: Some(proto::chat_message::Instructions {
content: instructions,
}),
project_path: "/path/to/project".to_string(),
model: Some(proto::chat_message::Model {
name: model_name.to_string(),
empty: String::new(),
let (instructions, messages) = process_chat_inputs(inputs).await;
let explicit_context = if !instructions.trim().is_empty() {
Some(ExplicitContext {
context: instructions,
repo_context: None,
})
} else {
None
};
let chat = GetChatRequest {
current_file: None,
conversation: messages,
repositories: vec![],
explicit_context,
workspace_root_path: None,
code_blocks: vec![],
model_details: Some(ModelDetails {
model_name: Some(model_name.to_string()),
api_key: None,
enable_ghost_mode: None,
azure_state: None,
enable_slow_pool,
openai_api_base_url: None,
}),
documentation_identifiers: vec![],
request_id: Uuid::new_v4().to_string(),
summary: String::new(),
linter_errors: None,
summary: None,
summary_up_until_index: None,
allow_long_file_scan: None,
is_bash: None,
conversation_id: Uuid::new_v4().to_string(),
can_handle_filenames_after_language_ids: None,
use_web: None,
quotes: vec![],
debug_info: None,
workspace_id: None,
external_links: vec![],
commit_notes: vec![],
long_context_mode: if LONG_CONTEXT_MODELS.contains(&model_name) {
Some(true)
} else {
None
},
is_eval: None,
desired_max_tokens: None,
context_ast: None,
is_composer: None,
runnable_code_blocks: None,
should_cache: None,
};
let mut encoded = Vec::new();
@@ -140,78 +400,6 @@ pub async fn encode_chat_message(
Ok(hex::decode(len_prefix + &content)?)
}
pub async fn decode_response(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
match decode_proto_messages(data) {
Ok(decoded) if !decoded.is_empty() => Ok(decoded),
_ => decompress_response(data).await
}
}
fn decode_proto_messages(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let hex_str = hex::encode(data);
let mut pos = 0;
let mut messages = Vec::new();
while pos + 10 <= hex_str.len() {
let msg_len = i64::from_str_radix(&hex_str[pos..pos + 10], 16)?;
pos += 10;
if pos + (msg_len * 2) as usize > hex_str.len() {
break;
}
let msg_data = &hex_str[pos..pos + (msg_len * 2) as usize];
pos += (msg_len * 2) as usize;
let buffer = hex::decode(msg_data)?;
let response = ResMessage::decode(&buffer[..])?;
messages.push(response.msg);
}
Ok(messages.join(""))
}
async fn decompress_response(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if data.len() <= 5 {
return Ok(String::new());
}
let mut decoder = GzDecoder::new(&data[5..]);
let mut text = String::new();
match decoder.read_to_string(&mut text) {
Ok(_) => {
if !text.contains("<|BEGIN_SYSTEM|>") {
Ok(text)
} else {
Ok(String::new())
}
},
Err(e) => Err(Box::new(e))
}
}
pub fn generate_random_id(
size: usize,
dict_type: Option<&str>,
custom_chars: Option<&str>,
) -> String {
let charset = match (dict_type, custom_chars) {
(_, Some(chars)) => chars,
(Some("alphabet"), _) => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
(Some("max"), _) => "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-",
_ => "0123456789",
};
let mut rng = thread_rng();
(0..size)
.map(|_| {
let idx = rng.gen_range(0..charset.len());
charset.chars().nth(idx).unwrap()
})
.collect()
}
pub fn generate_hash() -> String {
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
let mut hasher = Sha256::new();
@@ -219,7 +407,7 @@ pub fn generate_hash() -> String {
hex::encode(hasher.finalize())
}
fn obfuscate_bytes(bytes: &mut Vec<u8>) {
fn obfuscate_bytes(bytes: &mut [u8]) {
let mut prev: u8 = 165;
for (idx, byte) in bytes.iter_mut().enumerate() {
let old_value = *byte;

File diff suppressed because it is too large Load Diff

78
src/message.rs Normal file
View File

@@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Vision(Vec<VisionMessageContent>),
}
#[derive(Serialize, Deserialize)]
pub struct VisionMessageContent {
#[serde(rename = "type")]
pub content_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<ImageUrl>,
}
#[derive(Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: MessageContent,
}
#[derive(Serialize, Deserialize, PartialEq)]
pub enum Role {
#[serde(rename = "system", alias = "developer")]
System,
#[serde(rename = "user", alias = "human")]
User,
#[serde(rename = "assistant", alias = "ai")]
Assistant,
}
#[derive(Serialize)]
pub struct ChatResponse {
pub id: String,
pub object: String,
pub created: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub choices: Vec<Choice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Serialize)]
pub struct Choice {
pub index: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<Delta>,
pub finish_reason: Option<String>,
}
#[derive(Serialize)]
pub struct Delta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
}
#[derive(Serialize)]
pub struct Usage {
pub prompt_tokens: i32,
pub completion_tokens: i32,
pub total_tokens: i32,
}

View File

@@ -1,133 +1,141 @@
use std::sync::LazyLock;
use crate::Model;
use std::sync::LazyLock;
const MODEL_OBJECT: &str = "model";
const ANTHROPIC: &str = "anthropic";
const CURSOR: &str = "cursor";
const GOOGLE: &str = "google";
const OPENAI: &str = "openai";
use super::{ANTHROPIC, CURSOR, GOOGLE, MODEL_OBJECT, OPENAI};
pub static AVAILABLE_MODELS: LazyLock<Vec<Model>> = LazyLock::new(|| {
vec![
Model {
id: "cursor-small".into(),
id: "claude-3.5-sonnet".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: CURSOR.into()
owned_by: ANTHROPIC.into(),
},
Model {
id: "claude-3-opus".into(),
id: "gpt-3.5".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
},
Model {
id: "cursor-fast".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: CURSOR.into()
},
Model {
id: "gpt-3.5-turbo".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
},
Model {
id: "gpt-4-turbo-2024-04-09".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "gpt-4".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "gpt-4o".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into(),
},
Model {
id: "claude-3-opus".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into(),
},
Model {
id: "cursor-fast".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: CURSOR.into(),
},
Model {
id: "cursor-small".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: CURSOR.into(),
},
Model {
id: "gpt-3.5-turbo".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into(),
},
Model {
id: "gpt-4-turbo-2024-04-09".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into(),
},
Model {
id: "gpt-4o-128k".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "gemini-1.5-flash-500k".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: GOOGLE.into()
owned_by: GOOGLE.into(),
},
Model {
id: "claude-3-haiku-200k".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
owned_by: ANTHROPIC.into(),
},
Model {
id: "claude-3-5-sonnet-200k".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
},
Model {
id: "claude-3-5-sonnet-20240620".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
owned_by: ANTHROPIC.into(),
},
Model {
id: "claude-3-5-sonnet-20241022".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
owned_by: ANTHROPIC.into(),
},
Model {
id: "gpt-4o-mini".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "o1-mini".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "o1-preview".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "o1".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: OPENAI.into()
owned_by: OPENAI.into(),
},
Model {
id: "claude-3.5-haiku".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: ANTHROPIC.into()
owned_by: ANTHROPIC.into(),
},
Model {
id: "gemini-exp-1206".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: GOOGLE.into()
owned_by: GOOGLE.into(),
},
Model {
id: "gemini-2.0-flash-thinking-exp".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: GOOGLE.into()
owned_by: GOOGLE.into(),
},
Model {
id: "gemini-2.0-flash-exp".into(),
created: 1706659200,
object: MODEL_OBJECT.into(),
owned_by: GOOGLE.into()
}
owned_by: GOOGLE.into(),
},
]
});
});

6
start_instruction Normal file
View File

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

226
static/config.html Normal file
View File

@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置管理</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
</head>
<body>
<h1>配置管理</h1>
<div class="container">
<div class="form-group">
<label>路径:</label>
<select id="path">
<option value="/">根路径 (/)</option>
<option value="/logs">日志页面 (/logs)</option>
<option value="/config">配置页面 (/config)</option>
<option value="/tokeninfo">Token 信息页面 (/tokeninfo)</option>
<option value="/static/shared-styles.css">共享样式 (/static/shared-styles.css)</option>
<option value="/static/shared.js">共享脚本 (/static/shared.js)</option>
</select>
</div>
<div class="form-group">
<label>内容类型:</label>
<select id="content_type">
<option value="default">默认行为</option>
<option value="text">纯文本</option>
<option value="html">HTML</option>
</select>
</div>
<div class="form-group">
<label>内容:</label>
<textarea id="content"></textarea>
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enable_stream_check">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="include_stop_stream">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="vision_ability">
<option value="">保持不变</option>
<option value="disabled">禁用</option>
<option value="base64-only">仅 Base64</option>
<option value="base64-http">Base64 + HTTP</option>
</select>
</div>
<div class="form-group">
<label>慢速池:</label>
<select id="enable_slow_pool">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>允许所有Claude模型:</label>
<select id="enable_all_claude">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken">
</div>
<div class="button-group">
<button onclick="updateConfig('get')">获取配置</button>
<button onclick="updateConfig('update')">更新配置</button>
<button onclick="updateConfig('reset')" class="secondary">重置配置</button>
</div>
</div>
<div id="result" class="message"></div>
<script>
async function fetchConfig() {
const path = document.getElementById('path').value;
const data = await makeAuthenticatedRequest('/config', {
body: JSON.stringify({ action: 'get', path })
});
if (data) {
let content = '';
// 获取当前路径的页面内容
const pageContent = data.data.page_content;
// 如果是 default 类型,需要从路径获取内容
if (pageContent?.type === 'default') {
// 直接从路径获取内容
const response = await fetch(path);
content = await response.text();
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
content = pageContent.content;
}
// 更新表单
document.getElementById('content').value = content || '';
document.getElementById('content_type').value = pageContent?.type || 'default';
let visionValue = data.data.vision_ability || '';
// 标准化 vision_ability 的值
switch (visionValue) {
case 'none':
visionValue = 'disabled';
break;
case 'base64':
visionValue = 'base64-only';
break;
case 'all':
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
document.getElementById('enable_all_claude').value =
parseStringFromBoolean(data.data.enable_all_claude, '');
}
}
async function updateConfig(action) {
if (action === 'get') {
await fetchConfig();
return;
}
const contentType = document.getElementById('content_type').value;
const content = document.getElementById('content').value;
// 根据内容类型构造 content 对象
let contentObj = { type: 'default' };
if (action === 'update' && contentType !== 'default') {
contentObj = {
type: contentType,
content: content
};
}
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
...(document.getElementById('enable_slow_pool').value && {
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
}),
...(document.getElementById('enable_all_claude').value && {
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
})
};
const result = await makeAuthenticatedRequest('/config', {
body: JSON.stringify(data)
});
if (result) {
showMessage('result', result.message, false);
if (action === 'update' || action === 'reset') {
await fetchConfig();
}
}
}
function showSuccess(message) {
showMessage('result', message, false);
}
function showError(message) {
showMessage('result', message, true);
}
// 添加按钮事件监听
document.getElementById('path').addEventListener('change', fetchConfig);
// 更新内容类型变更处理
document.getElementById('content_type').addEventListener('change', function () {
const textarea = document.getElementById('content');
textarea.disabled = this.value === 'default';
});
// 初始化 token 处理
initializeTokenHandling('authToken');
</script>
</body>
</html>

382
static/logs.html Normal file
View File

@@ -0,0 +1,382 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求日志查看</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
/* 日志页面特定样式 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: var(--spacing);
}
.stat-card {
background: var(--card-background);
padding: 15px;
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-card h4 {
margin: 0 0 8px 0;
color: var(--primary-color);
}
.stat-value {
font-size: 24px;
font-weight: 500;
}
.refresh-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 8px;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
overflow-y: auto;
}
.modal-content {
background-color: var(--card-background);
margin: 5% auto;
padding: 20px;
border-radius: var(--border-radius);
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
}
.close {
float: right;
cursor: pointer;
font-size: 28px;
}
.info-button {
background: var(--primary-color);
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
.message-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.message-table th,
.message-table td {
border: 1px solid var(--border-color);
padding: 12px;
vertical-align: top;
}
.message-table td {
word-break: break-word;
}
.message-table td:nth-child(2) {
max-width: 600px;
}
.message-table td:first-child {
width: 80px;
white-space: nowrap;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.modal-header h3 {
margin: 0;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 5px 10px;
}
.close:hover {
color: var(--primary-color);
}
.usage-progress-container {
margin-top: 20px;
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.usage-progress-bar {
height: 100%;
width: 0%;
transition: width 0.3s ease;
background: linear-gradient(to right,
#4caf50 0%,
/* 绿色 */
#8bc34a 25%,
/* 浅绿 */
#ffeb3b 50%,
/* 黄色 */
#ff9800 75%,
/* 橙色 */
#f44336 100%
/* 红色 */
);
}
</style>
</head>
<body>
<h1>请求日志查看</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
<div class="refresh-container">
<div class="button-group">
<button onclick="fetchLogs()">刷新日志</button>
</div>
<div class="auto-refresh">
<input type="checkbox" id="autoRefresh" checked>
<label for="autoRefresh">自动刷新 (60秒)</label>
</div>
</div>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<h4>总请求数</h4>
<div id="totalRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>活跃请求数</h4>
<div id="activeRequests" class="stat-value">-</div>
</div>
<div class="stat-card">
<h4>最后更新</h4>
<div id="lastUpdate" class="stat-value">-</div>
</div>
</div>
<div class="table-container">
<table id="logsTable">
<thead>
<tr>
<th>时间</th>
<th>模型</th>
<th>Token信息</th>
<th>Prompt</th>
<th>流式响应</th>
<th>状态</th>
<th>错误信息</th>
</tr>
</thead>
<tbody id="logsBody"></tbody>
</table>
</div>
</div>
<div id="message"></div>
<!-- 添加弹窗组件 -->
<div id="tokenModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Token 详细信息</h3>
<table>
<tr>
<td>Token:</td>
<td id="modalToken"></td>
</tr>
<tr>
<td>校验和:</td>
<td id="modalChecksum"></td>
</tr>
<tr>
<td>别名:</td>
<td id="modalAlias"></td>
</tr>
<tr>
<td>使用情况:</td>
<td id="modalUsage"></td>
</tr>
</table>
<!-- 添加进度条容器 -->
<div class="usage-progress-container">
<div id="modalUsageBar" class="usage-progress-bar"></div>
</div>
</div>
</div>
<div id="promptModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>对话内容</h3>
<span class="close">&times;</span>
</div>
<div id="promptContent"></div>
</div>
</div>
<script>
let refreshInterval;
function updateStats(data) {
document.getElementById('totalRequests').textContent = data.total;
document.getElementById('activeRequests').textContent = data.active || 0;
document.getElementById('lastUpdate').textContent =
new Date(data.timestamp).toLocaleTimeString();
}
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
document.getElementById('modalAlias').textContent = tokenInfo.alias || '-';
// 获取进度条容器
const progressContainer = document.querySelector('.usage-progress-container');
// 处理使用情况和进度条
if (tokenInfo.usage) {
const current = tokenInfo.usage.fast_requests;
const max = tokenInfo.usage.max_fast_requests;
const percentage = (current / max * 100).toFixed(1);
document.getElementById('modalUsage').textContent =
`${current}/${max} (${percentage}%)`;
// 显示进度条容器
progressContainer.style.display = 'block';
// 更新进度条
const progressBar = document.getElementById('modalUsageBar');
progressBar.style.width = `${percentage}%`;
} else {
document.getElementById('modalUsage').textContent = '-';
// 隐藏进度条容器
progressContainer.style.display = 'none';
}
modal.style.display = 'block';
}
function updateTable(data) {
const tbody = document.getElementById('logsBody');
updateStats(data);
tbody.innerHTML = data.logs.map(log => `
<tr>
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td>${log.model}</td>
<td>
<button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>
查看详情
</button>
</td>
<td>
${log.prompt ?
`<button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">
查看对话
</button>` :
'-'
}
</td>
<td>${log.stream ? '是' : '否'}</td>
<td>${log.status}</td>
<td>${log.error || '-'}</td>
</tr>
`).join('');
}
async function fetchLogs() {
const data = await makeAuthenticatedRequest('/logs');
if (data) {
updateTable(data);
showGlobalMessage('日志获取成功');
}
}
// 自动刷新控制
document.getElementById('autoRefresh').addEventListener('change', function (e) {
if (e.target.checked) {
refreshInterval = setInterval(fetchLogs, 60000);
} else {
clearInterval(refreshInterval);
}
});
// 页面加载完成后自动获取日志
document.addEventListener('DOMContentLoaded', () => {
const authToken = getAuthToken();
if (authToken) {
document.getElementById('authToken').value = authToken;
fetchLogs();
}
// 启动自动刷新
refreshInterval = setInterval(fetchLogs, 60000);
});
// 初始化 token 处理
initializeTokenHandling('authToken');
// 添加清理逻辑
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
// 添加模态框关闭逻辑
document.querySelectorAll('.modal .close').forEach(closeBtn => {
closeBtn.onclick = function () {
this.closest('.modal').style.display = 'none';
}
});
window.onclick = function (event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
</script>
</body>
</html>

169
static/shared-styles.css Normal file
View File

@@ -0,0 +1,169 @@
:root {
--primary-color: #2196F3;
--primary-dark: #1976D2;
--success-color: #4CAF50;
--error-color: #F44336;
--background-color: #F5F5F5;
--card-background: #FFFFFF;
--border-radius: 8px;
--spacing: 20px;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing);
background: var(--background-color);
color: #333;
line-height: 1.6;
}
.container {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: var(--spacing);
}
h1,
h2,
h3 {
color: #1a1a1a;
margin-top: 0;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input[type="text"],
input[type="password"],
select,
textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
input[type="text"]:focus,
input[type="password"]:focus,
select:focus,
textarea:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
}
textarea {
min-height: 150px;
font-family: monospace;
resize: vertical;
}
.button-group {
display: flex;
gap: 10px;
margin: var(--spacing) 0;
}
button {
background: var(--primary-color);
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s, transform 0.1s;
}
button:hover {
background: var(--primary-dark);
}
button:active {
transform: translateY(1px);
}
button.secondary {
background: #757575;
}
button.secondary:hover {
background: #616161;
}
.message {
padding: 12px;
border-radius: var(--border-radius);
margin: 10px 0;
}
.success {
background: #E8F5E9;
color: #2E7D32;
border: 1px solid #A5D6A7;
}
.error {
background: #FFEBEE;
color: #C62828;
border: 1px solid #FFCDD2;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: var(--spacing);
background: var(--card-background);
border-radius: var(--border-radius);
overflow: hidden;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: var(--primary-color);
color: white;
font-weight: 500;
}
tr:nth-child(even) {
background: #f8f9fa;
}
tr:hover {
background: #f1f3f4;
}
/* 响应式设计 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.button-group {
flex-direction: column;
}
table {
display: block;
overflow-x: auto;
}
}

252
static/shared.js Normal file
View File

@@ -0,0 +1,252 @@
// Token 管理功能
function saveAuthToken(token) {
const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期
localStorage.setItem('authToken', token);
localStorage.setItem('authTokenExpiry', expiryTime);
}
function getAuthToken() {
const token = localStorage.getItem('authToken');
const expiry = localStorage.getItem('authTokenExpiry');
if (!token || !expiry) {
return null;
}
if (new Date().getTime() > parseInt(expiry)) {
localStorage.removeItem('authToken');
localStorage.removeItem('authTokenExpiry');
return null;
}
return token;
}
// 消息显示功能
function showMessage(elementId, text, isError = false) {
const msg = document.getElementById(elementId);
msg.className = `message ${isError ? 'error' : 'success'}`;
msg.textContent = text;
}
function showGlobalMessage(text, isError = false) {
showMessage('message', text, isError);
}
// Token 输入框自动填充和事件绑定
function initializeTokenHandling(inputId) {
document.addEventListener('DOMContentLoaded', () => {
const authToken = getAuthToken();
if (authToken) {
document.getElementById(inputId).value = authToken;
}
});
document.getElementById(inputId).addEventListener('change', (e) => {
if (e.target.value) {
saveAuthToken(e.target.value);
} else {
localStorage.removeItem('authToken');
localStorage.removeItem('authTokenExpiry');
}
});
}
// API 请求通用处理
async function makeAuthenticatedRequest(url, options = {}) {
const tokenId = options.tokenId || 'authToken';
const token = document.getElementById(tokenId).value;
if (!token) {
showGlobalMessage('请输入 AUTH_TOKEN', true);
return null;
}
const defaultOptions = {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
try {
const response = await fetch(url, { ...defaultOptions, ...options });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
showGlobalMessage(`请求失败: ${error.message}`, true);
return null;
}
}
/**
* 从字符串解析布尔值
* @param {string} str - 要解析的字符串
* @param {boolean|null} defaultValue - 解析失败时的默认值
* @returns {boolean|null} 解析结果,如果无法解析则返回默认值
*/
function parseBooleanFromString(str, defaultValue = null) {
if (typeof str !== 'string') {
return defaultValue;
}
const lowercaseStr = str.toLowerCase().trim();
if (lowercaseStr === 'true' || lowercaseStr === '1') {
return true;
} else if (lowercaseStr === 'false' || lowercaseStr === '0') {
return false;
} else {
return defaultValue;
}
}
/**
* 将布尔值转换为字符串
* @param {boolean|undefined|null} value - 要转换的布尔值
* @param {string} defaultValue - 转换失败时的默认值
* @returns {string} 转换结果,如果输入无效则返回默认值
*/
function parseStringFromBoolean(value, defaultValue = null) {
if (typeof value !== 'boolean') {
return defaultValue;
}
return value ? 'true' : 'false';
}
/**
* 解析对话内容
* @param {string} promptStr - 原始prompt字符串
* @returns {Array<{role: string, content: string}>} 解析后的对话数组
*/
function parsePrompt(promptStr) {
if (!promptStr) return [];
const messages = [];
const lines = promptStr.split('\n');
let currentRole = '';
let currentContent = '';
const roleMap = {
'BEGIN_SYSTEM': 'system',
'BEGIN_USER': 'user',
'BEGIN_ASSISTANT': 'assistant'
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检查是否是角色标记行
let foundRole = false;
for (const [marker, role] of Object.entries(roleMap)) {
if (line.includes(marker)) {
// 保存之前的消息(如果有)
if (currentRole && currentContent.trim()) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
// 设置新角色
currentRole = role;
currentContent = '';
foundRole = true;
break;
}
}
// 如果不是角色标记行且不是END标记行则添加到当前内容
if (!foundRole && !line.includes('END_')) {
currentContent += line + '\n';
}
}
// 添加最后一条消息
if (currentRole && currentContent.trim()) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
return messages;
}
/**
* 格式化对话内容为HTML表格
* @param {Array<{role: string, content: string}>} messages - 对话消息数组
* @returns {string} HTML表格字符串
*/
function formatPromptToTable(messages) {
if (!messages || messages.length === 0) {
return '<p>无对话内容</p>';
}
const roleLabels = {
'system': '系统',
'user': '用户',
'assistant': '助手'
};
function escapeHtml(content) {
// 先转义HTML特殊字符
const escaped = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// 将HTML标签文本用引号包裹使其更易读
// return escaped.replace(/&lt;(\/?[^>]+)&gt;/g, '"<$1>"');
return escaped;
}
return `
<table class="message-table">
<thead>
<tr>
<th>角色</th>
<th>内容</th>
</tr>
</thead>
<tbody>
${messages.map(msg => `
<tr>
<td>${roleLabels[msg.role] || msg.role}</td>
<td>${escapeHtml(msg.content).replace(/\n/g, '<br>')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
/**
* 安全地显示prompt对话框
* @param {string} promptStr - 原始prompt字符串
*/
function showPromptModal(promptStr) {
try {
const modal = document.getElementById('promptModal');
const content = document.getElementById('promptContent');
if (!modal || !content) {
console.error('Modal elements not found');
return;
}
const messages = parsePrompt(promptStr);
content.innerHTML = formatPromptToTable(messages);
modal.style.display = 'block';
} catch (e) {
console.error('显示prompt对话框失败:', e);
console.error('原始prompt:', promptStr);
}
}

View File

@@ -5,70 +5,36 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token 信息管理</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
.token-container {
display: grid;
gap: var(--spacing);
}
.container {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
.token-section {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.button-group {
display: flex;
gap: 10px;
margin: 10px 0;
}
textarea {
width: 100%;
min-height: 150px;
margin: 10px 0;
font-family: monospace;
resize: vertical;
}
button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
.shortcuts {
margin-top: var(--spacing);
padding: 12px;
background: #f8f9fa;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #45a049;
}
button.secondary {
background: #607D8B;
}
button.secondary:hover {
background: #546E7A;
}
.error {
color: red;
margin: 10px 0;
}
.success {
color: green;
margin: 10px 0;
}
#authToken {
width: 100%;
padding: 8px;
margin: 10px 0;
kbd {
background: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
padding: 1px 4px;
font-size: 12px;
}
</style>
</head>
@@ -77,97 +43,70 @@
<h1>Token 信息管理</h1>
<div class="container">
<h2>认证</h2>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
</div>
<div class="container">
<h2>Token 配置</h2>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
<div class="token-container">
<div class="token-section">
<h3>Token 配置</h3>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
</div>
<div class="form-group">
<label>Token 文件内容:</label>
<textarea id="tokens" placeholder="每行一个 token"></textarea>
</div>
<div class="form-group">
<label>Token List 文件内容:</label>
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
</div>
<div class="shortcuts">
快捷键: <kbd>Ctrl</kbd> + <kbd>S</kbd> 保存更改
</div>
</div>
<h3>Token 文件内容</h3>
<textarea id="tokens" placeholder="每行一个 token"></textarea>
<h3>Token List 文件内容</h3>
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
</div>
<div id="message"></div>
<script>
function showMessage(text, isError = false) {
const msg = document.getElementById('message');
msg.className = isError ? 'error' : 'success';
msg.textContent = text;
showGlobalMessage(text, isError);
}
async function getTokenInfo() {
const authToken = document.getElementById('authToken').value;
if (!authToken) {
showMessage('请输入 AUTH_TOKEN', true);
return;
}
try {
const response = await fetch('/get-tokeninfo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const data = await makeAuthenticatedRequest('/get-tokeninfo');
if (data) {
document.getElementById('tokens').value = data.tokens;
document.getElementById('tokenList').value = data.token_list;
showMessage('配置获取成功');
} catch (error) {
showMessage(`获取失败: ${error.message}`, true);
showGlobalMessage('配置获取成功');
}
}
async function updateTokenInfo() {
const authToken = document.getElementById('authToken').value;
const tokens = document.getElementById('tokens').value;
const tokenList = document.getElementById('tokenList').value;
if (!authToken) {
showMessage('请输入 AUTH_TOKEN', true);
return;
}
if (!tokens) {
showMessage('Token 文件内容不能为空', true);
showGlobalMessage('Token 文件内容不能为空', true);
return;
}
try {
const response = await fetch('/update-tokeninfo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokens: tokens,
token_list: tokenList || undefined
})
});
const data = await makeAuthenticatedRequest('/update-tokeninfo', {
body: JSON.stringify({
tokens: tokens,
token_list: tokenList || undefined
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
showMessage(`更新成功: ${data.message}`);
} catch (error) {
showMessage(`更新失败: ${error.message}`, true);
if (data) {
showGlobalMessage(`更新成功: ${data.message}`);
}
}
@@ -178,6 +117,9 @@
updateTokenInfo();
}
});
// 初始化 token 处理
initializeTokenHandling('authToken');
</script>
</body>