From ea1acb555faa644596d050b327032dae7e59a16a Mon Sep 17 00:00:00 2001 From: wisdgod Date: Wed, 25 Dec 2024 07:02:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker.yml | 6 +- Cargo.lock | 2 +- Cargo.toml | 13 +- Cross.toml | 14 ++ README.md | 152 ++++++++++++++----- scripts/build.sh | 115 ++++++++------ src/lib.rs | 24 ++- src/main.rs | 281 ++++++++++++++++++++--------------- 8 files changed, 384 insertions(+), 223 deletions(-) create mode 100644 Cross.toml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 76a9bf1..251069a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,9 +2,9 @@ name: Docker Build and Push on: workflow_dispatch: - # push: - # tags: - # - 'v*' + push: + tags: + - 'v*' env: IMAGE_NAME: ${{ github.repository_owner }}/cursor-api diff --git a/Cargo.lock b/Cargo.lock index 9a776b3..ec146e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,7 +268,7 @@ dependencies = [ [[package]] name = "cursor-api" -version = "0.1.0" +version = "0.1.1" dependencies = [ "axum", "base64", diff --git a/Cargo.toml b/Cargo.toml index 3e13bbd..9847e92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cursor-api" -version = "0.1.0" +version = "0.1.1" edition = "2021" authors = ["wisdgod "] @@ -28,13 +28,12 @@ tokio-stream = { version = "0.1.17", features = ["time"] } tower-http = { version = "0.6.2", features = ["cors"] } uuid = { version = "1.11.0", features = ["v4"] } -# 优化设置 [profile.release] -lto = true # 启用链接时优化 -codegen-units = 1 # 减少并行编译单元以提高优化 -panic = 'abort' # 在 panic 时直接终止,减小二进制大小 -strip = true # 移除调试符号 -opt-level = 3 # 最高优化级别 +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +opt-level = 3 # 构建脚本设置 [package.metadata.cross.target.x86_64-unknown-linux-gnu] diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..d7cea17 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,14 @@ +[target.x86_64-unknown-linux-gnu] +pre-build = [ + "set -e", + "apt-get update", + "apt-get install -y --no-install-recommends build-essential protobuf-compiler pkg-config libssl-dev nodejs npm", + "rm -rf /var/lib/apt/lists/*" +] + +[target.x86_64-unknown-freebsd] +pre-build = [ + "pkg update", + "pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl", + "export SSL_CERT_FILE=/etc/ssl/cert.pem" +] diff --git a/README.md b/README.md index 41f3614..8591cf5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录(赠送 250 次快速响应,可通过删除账号再注册重置) 2. 在浏览器中打开开发者工具(F12) -3. 找到 应用-Cookies 中名为 `WorkosCursorSessionToken` 的值并保存(相当于 openai 的密钥) +3. 找到 Application-Cookies 中名为 `WorkosCursorSessionToken` 的值并保存(相当于 openai 的密钥) ## 接口说明 @@ -12,41 +12,53 @@ - 接口地址:`/v1/chat/completions` - 请求方法:POST -- 认证方式:Bearer Token(支持两种认证方式) +- 认证方式:Bearer Token 1. 使用环境变量 `AUTH_TOKEN` 进行认证 2. 使用 `.token` 文件中的令牌列表进行轮询认证 -### 获取模型列表 +### Token管理接口 +#### 简易Token信息管理页面 +- 接口地址:`/tokeninfo` +- 请求方法:GET +- 响应格式:HTML页面 +- 功能:获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容 + +#### 更新Token信息 +- 接口地址:`/update-tokeninfo` +- 请求方法:GET +- 认证方式:不需要 +- 功能:请求内容不包括文件内容,直接修改文件,调用重载函数 + +#### 更新Token信息 +- 接口地址:`/update-tokeninfo` +- 请求方法:POST +- 认证方式:Bearer Token +- 功能:请求内容包括文件内容,间接修改文件,调用重载函数 + +#### 获取Token信息 +- 接口地址:`/get-tokeninfo` +- 请求方法:POST +- 认证方式:Bearer Token + +### 其他接口 + +#### 获取模型列表 - 接口地址:`/v1/models` - 请求方法:GET -### 获取环境变量中的x-cursor-checksum - -- 接口地址:`/env-checksum` -- 请求方法:GET - -### 获取随机x-cursor-checksum - +#### 获取随机x-cursor-checksum - 接口地址:`/checksum` - 请求方法:GET -### 健康检查接口 - +#### 健康检查接口 - 接口地址:`/` - 请求方法:GET -### 获取日志接口 - +#### 获取日志接口 - 接口地址:`/logs` - 请求方法:GET -### Token管理接口 - -- 获取Token信息页面:`/tokeninfo` -- 更新Token信息:`/update-tokeninfo` -- 获取Token信息:`/get-tokeninfo` - ## 配置说明 ### 环境变量 @@ -60,32 +72,73 @@ ### Token文件格式 1. `.token` 文件:每行一个token,支持以下格式: + ``` token1 alias::token2 ``` + alias 可以是任意值,用于区分不同的 token,更方便管理,WorkosCursorSessionToken 是相同格式 + 该文件将自动向.token-list文件中追加token,同时自动生成checksum + 2. `.token-list` 文件:每行为token和checksum的对应关系: + ``` token1,checksum1 token2,checksum2 ``` + 该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改: + + - 需要删除某个 token + - 需要使用已有 checksum 来对应某一个 token + +### 模型列表 + +写死了,后续也不会会支持自定义模型列表 +``` +cursor-small +claude-3-opus +cursor-fast +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 +o1-preview +o1 +claude-3.5-haiku +gemini-exp-1206 +gemini-2.0-flash-thinking-exp +gemini-2.0-flash-exp +``` + ## 部署 ### 本地部署 #### 从源码编译 -需要安装 Rust 工具链和 protobuf 编译器: +需要安装 Rust 工具链和依赖: ```bash -# 安装依赖(Debian/Ubuntu) -apt-get install -y build-essential protobuf-compiler +# 安装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 -./target/release/cursor-api + +# 交叉编译,以x86_64-unknown-linux-gnu为例,老实说,这也算原生编译,因为使用了docker +cross build --target x86_64-unknown-linux-gnu --release ``` #### 使用预编译二进制 @@ -109,28 +162,48 @@ docker run -p 3000:3000 cursor-api ### huggingface部署 -1. duplicate项目: - [huggingface链接](https://huggingface.co/login?next=%2Fspaces%2Fstevenrk%2Fcursor%3Fduplicate%3Dtrue) +前提:一个huggingface账号 + +1. 创建一个Space并创建一个Dockerfile文件,内容如下: + + ```Dockerfile + FROM wisdgod/cursor-api:latest + + # 可能你要覆盖原镜像的环境变量,但都可以在下面的第2步中配置 + ENV PORT=7860 + ``` 2. 配置环境变量 - 在你的space中,点击settings,找到`Variables and secrets`,添加Variables - - name: `AUTH_TOKEN` (注意大写) - - value: 你随意 + 在你的 Space 中,点击 Settings,找到 `Variables and secrets`,添加 Variables + + ```env + # 可选,用于配置服务器端口 + PORT=3000 + # 必选,用于配置路由前缀,比如/api,/hf,/proxy等等 + ROUTE_PREFIX= + # 必选,用于API认证 + AUTH_TOKEN= + # 可选,用于配置token文件路径 + TOKEN_FILE=.token + # 可选,用于配置token列表文件路径 + TOKEN_LIST_FILE=.token-list + ``` 3. 重新部署 - + 点击`Factory rebuild`,等待部署完成 4. 接口地址(`Embed this Space`中查看): + ``` https://{username}-{space-name}.hf.space/v1/models ``` ## 注意事项 -1. 请妥善保管您的 AuthToken,不要泄露给他人 -2. 配置 AUTH_TOKEN 环境变量以增加安全性 +1. 请妥善保管您的任何 Token,不要泄露给他人。若发现泄露,请及时更改 +2. 请遵守本项目许可证,你仅拥有使用本项目的权利,不得用于商业用途 3. 本项目仅供学习研究使用,请遵守 Cursor 的使用条款 ## 开发 @@ -147,12 +220,13 @@ docker run -p 3000:3000 cursor-api ./scripts/build.sh --cross ``` -支持的目标平台: -- x86_64-unknown-linux-gnu -- x86_64-pc-windows-msvc -- aarch64-unknown-linux-gnu -- x86_64-apple-darwin -- aarch64-apple-darwin +支持的平台: + +- linux x86_64 +- windows x86_64 +- macos x86_64 +- freebsd x86_64 +- docker (only for linux x86_64) ### 获取token diff --git a/scripts/build.sh b/scripts/build.sh index c9b62fa..c310861 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -22,8 +22,8 @@ check_requirements() { fi done - # Linux 特定检查 - if [[ $USE_CROSS == true ]] && ! command -v cross &>/dev/null; then + # cross 工具检查(仅在 Linux 上需要) + if [[ "$OS" == "Linux" ]] && ! command -v cross &>/dev/null; then missing_tools+=("cross") fi @@ -46,6 +46,22 @@ 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 @@ -57,42 +73,41 @@ build_target() { # 确定文件后缀 [[ $target == *"windows"* ]] && extension=".exe" - # 设置目标特定的环境变量 - local build_env=() - if [[ $target == "aarch64-unknown-linux-gnu" ]]; then - build_env+=( - "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" - "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" - "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" - "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" - "PKG_CONFIG_ALLOW_CROSS=1" - "OPENSSL_DIR=/usr" - "OPENSSL_INCLUDE_DIR=/usr/include" - "OPENSSL_LIB_DIR=/usr/lib/aarch64-linux-gnu" - ) - fi - - # 判断是否使用 cross(仅在 Linux 上) - if [[ $target != "$CURRENT_TARGET" ]]; then - env ${build_env[@]+"${build_env[@]}"} RUSTFLAGS="$rustflags" cargo build --target "$target" --release + # 判断是否使用 cross + if should_use_cross "$target"; then + env RUSTFLAGS="$rustflags" cross build --target "$target" --release else - env ${build_env[@]+"${build_env[@]}"} RUSTFLAGS="$rustflags" cargo build --release + if [[ $target != "$CURRENT_TARGET" ]]; then + env RUSTFLAGS="$rustflags" cargo build --target "$target" --release + else + env RUSTFLAGS="$rustflags" cargo build --release + fi fi # 移动编译产物到 release 目录 local binary_name="cursor-api" [[ $USE_STATIC == true ]] && binary_name+="-static" - if [[ -f "target/$target/release/cursor-api$extension" ]]; then - cp "target/$target/release/cursor-api$extension" "release/${binary_name}-$target$extension" + local binary_path + if [[ $target == "$CURRENT_TARGET" ]]; then + binary_path="target/release/cursor-api$extension" + else + binary_path="target/$target/release/cursor-api$extension" + fi + + if [[ -f "$binary_path" ]]; then + cp "$binary_path" "release/${binary_name}-$target$extension" info "完成构建 $target" else warn "构建产物未找到: $target" + warn "查找路径: $binary_path" + warn "当前目录内容:" + ls -R target/ return 1 fi } -# 获取 CPU 架构 +# 获取 CPU 架构和操作系统 ARCH=$(uname -m | sed 's/^aarch64\|arm64$/aarch64/;s/^x86_64\|x86-64\|x64\|amd64$/x86_64/') OS=$(uname -s) @@ -104,7 +119,7 @@ get_target() { "Darwin") echo "${arch}-apple-darwin" ;; "Linux") echo "${arch}-unknown-linux-gnu" ;; "MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;; - "FreeBSD") echo "x86_64-unknown-freebsd" ;; + "FreeBSD") echo "${arch}-unknown-freebsd" ;; *) error "不支持的系统: $os" ;; esac } @@ -118,21 +133,31 @@ CURRENT_TARGET=$(get_target "$ARCH" "$OS") # 获取系统对应的所有目标 get_targets() { case "$1" in - "linux") echo "x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu" ;; - "windows") echo "x86_64-pc-windows-msvc aarch64-pc-windows-msvc" ;; - "macos") echo "x86_64-apple-darwin aarch64-apple-darwin" ;; - "freebsd") echo "x86_64-unknown-freebsd" ;; + "linux") + # Linux 构建所有 Linux 目标和 FreeBSD 目标 + echo "x86_64-unknown-linux-gnu x86_64-unknown-freebsd" + ;; + "freebsd") + # FreeBSD 只构建当前架构的 FreeBSD 目标 + echo "${ARCH}-unknown-freebsd" + ;; + "windows") + # Windows 构建所有 Windows 目标 + echo "x86_64-pc-windows-msvc" + ;; + "macos") + # macOS 构建所有 macOS 目标 + echo "x86_64-apple-darwin aarch64-apple-darwin" + ;; *) error "不支持的系统组: $1" ;; esac } # 解析参数 -USE_CROSS=false USE_STATIC=false while [[ $# -gt 0 ]]; do case $1 in - --cross) USE_CROSS=true ;; --static) USE_STATIC=true ;; --help) show_help; exit 0 ;; *) error "未知参数: $1" ;; @@ -144,19 +169,21 @@ done check_requirements # 确定要构建的目标 -if [[ $USE_CROSS == true ]] && is_linux; then - # 只在 Linux 上使用 cross 进行多架构构建 - TARGETS=($(get_targets "linux")) -else - # 其他系统或不使用 cross 时只构建当前系统的所有架构 - case "$OS" in - "Darwin") TARGETS=($(get_targets "macos")) ;; - "Linux") TARGETS=("$CURRENT_TARGET") ;; - "MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") TARGETS=($(get_targets "windows")) ;; - "FreeBSD") TARGETS=("$CURRENT_TARGET") ;; - *) error "不支持的系统: $OS" ;; - esac -fi +case "$OS" in + "Darwin") + TARGETS=($(get_targets "macos")) + ;; + "Linux") + TARGETS=($(get_targets "linux")) + ;; + "FreeBSD") + TARGETS=($(get_targets "freebsd")) + ;; + "MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") + TARGETS=($(get_targets "windows")) + ;; + *) error "不支持的系统: $OS" ;; +esac # 创建 release 目录 mkdir -p release diff --git a/src/lib.rs b/src/lib.rs index 574be9f..de59635 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,16 +140,14 @@ pub async fn encode_chat_message( Ok(hex::decode(len_prefix + &content)?) } -pub async fn decode_response(data: &[u8]) -> String { - if let Ok(decoded) = decode_proto_messages(data) { - if !decoded.is_empty() { - return decoded; - } +pub async fn decode_response(data: &[u8]) -> Result> { + match decode_proto_messages(data) { + Ok(decoded) if !decoded.is_empty() => Ok(decoded), + _ => decompress_response(data).await } - decompress_response(data).await } -fn decode_proto_messages(data: &[u8]) -> Result> { +fn decode_proto_messages(data: &[u8]) -> Result> { let hex_str = hex::encode(data); let mut pos = 0; let mut messages = Vec::new(); @@ -173,9 +171,9 @@ fn decode_proto_messages(data: &[u8]) -> Result String { +async fn decompress_response(data: &[u8]) -> Result> { if data.len() <= 5 { - return String::new(); + return Ok(String::new()); } let mut decoder = GzDecoder::new(&data[5..]); @@ -184,12 +182,12 @@ async fn decompress_response(data: &[u8]) -> String { match decoder.read_to_string(&mut text) { Ok(_) => { if !text.contains("<|BEGIN_SYSTEM|>") { - text + Ok(text) } else { - String::new() + Ok(String::new()) } - } - Err(_) => String::new(), + }, + Err(e) => Err(Box::new(e)) } } diff --git a/src/main.rs b/src/main.rs index d8b048e..1faa6e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,20 +11,51 @@ use chrono::{DateTime, Local, Utc}; use futures::StreamExt; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + LazyLock, +}; use std::{convert::Infallible, sync::Arc}; use tokio::sync::Mutex; use tower_http::cors::CorsLayer; use uuid::Uuid; -// 应用状态 -struct AppState { - start_time: DateTime, +struct AppConfig { + auth_token: String, + token_file: String, + token_list_file: String, + route_prefix: String, version: String, + start_time: DateTime, +} + +static APP_CONFIG: LazyLock = LazyLock::new(|| { + // 加载环境变量 + if let Err(e) = dotenvy::dotenv() { + eprintln!("警告: 无法加载 .env 文件: {}", e); + } + + let auth_token = std::env::var("AUTH_TOKEN").unwrap_or_else(|_| "".to_string()); + if auth_token.is_empty() { + eprintln!("错误: AUTH_TOKEN 未设置"); + std::process::exit(1); + } + + AppConfig { + auth_token, + token_file: std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string()), + token_list_file: std::env::var("TOKEN_LIST_FILE") + .unwrap_or_else(|_| ".token-list".to_string()), + route_prefix: std::env::var("ROUTE_PREFIX").unwrap_or_default(), + version: env!("CARGO_PKG_VERSION").to_string(), + start_time: Local::now(), + } +}); + +struct AppState { total_requests: u64, active_requests: u64, request_logs: Vec, - route_prefix: String, token_infos: Vec, } @@ -45,6 +76,8 @@ struct RequestLog { checksum: String, auth_token: String, stream: bool, + status: String, + error: Option, } // 聊天请求 @@ -68,7 +101,6 @@ mod models; use models::AVAILABLE_MODELS; // 用于存储 token 信息 -#[derive(Debug)] struct TokenInfo { token: String, checksum: String, @@ -83,7 +115,6 @@ struct TokenUpdateRequest { } // 自定义错误类型 -#[derive(Debug)] enum ChatError { ModelNotSupported(String), EmptyMessages, @@ -124,26 +155,14 @@ impl ChatError { #[tokio::main] async fn main() { - // 加载环境变量 - dotenvy::dotenv().ok(); - - // 处理 token 文件路径 - let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string()); - // 加载 tokens - let token_infos = load_tokens(&token_file); + let token_infos = load_tokens(); - // 获取路由前缀配置 - let route_prefix = std::env::var("ROUTE_PREFIX").unwrap_or_default(); - - // 初始化应用状态 + // 初始化需要互斥访问的状态 let state = Arc::new(Mutex::new(AppState { - start_time: Local::now(), - version: env!("CARGO_PKG_VERSION").to_string(), total_requests: 0, active_requests: 0, request_logs: Vec::new(), - route_prefix: route_prefix.clone(), token_infos, })); @@ -151,13 +170,16 @@ async fn main() { let app = Router::new() .route("/", get(handle_root)) .route("/tokeninfo", get(handle_tokeninfo_page)) - .route(&format!("{}/v1/models", route_prefix), get(handle_models)) + .route( + &format!("{}/v1/models", APP_CONFIG.route_prefix), + get(handle_models), + ) .route("/checksum", get(handle_checksum)) .route("/update-tokeninfo", get(handle_update_tokeninfo)) .route("/get-tokeninfo", post(handle_get_tokeninfo)) .route("/update-tokeninfo", post(handle_update_tokeninfo_post)) .route( - &format!("{}/v1/chat/completions", route_prefix), + &format!("{}/v1/chat/completions", APP_CONFIG.route_prefix), post(handle_chat), ) .route("/logs", get(handle_logs)) @@ -174,54 +196,69 @@ async fn main() { } // Token 加载函数 -fn load_tokens(token_file: &str) -> Vec { - let token_list_file = - std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string()); - - // 读取并规范化 .token 文件 - let tokens = if let Ok(content) = std::fs::read_to_string(token_file) { - let normalized = content.replace("\r\n", "\n"); - if normalized != content { - std::fs::write(token_file, &normalized).unwrap(); - } - normalized - .lines() - .enumerate() - .filter_map(|(idx, line)| { - let parts: Vec<&str> = line.split("::").collect(); - match parts.len() { - 1 => Some(line.to_string()), - 2 => Some(parts[1].to_string()), - _ => { - println!("警告: 第{}行包含多个'::'分隔符,已忽略此行", idx + 1); - None - } +fn load_tokens() -> Vec { + // 读取 .token 文件并解析 + let tokens = match std::fs::read_to_string(&APP_CONFIG.token_file) { + Ok(content) => { + let normalized = content.replace("\r\n", "\n"); + // 如果内容被规范化,则更新文件 + if normalized != content { + if let Err(e) = std::fs::write(&APP_CONFIG.token_file, &normalized) { + eprintln!("警告: 无法更新规范化的token文件: {}", e); } - }) - .filter(|s| !s.is_empty()) - .collect::>() - } else { - eprintln!("警告: 无法读取token文件 '{}'", token_file); - Vec::new() + } + + normalized + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() { + return None; + } + + // 处理 alias::token 格式 + match line.split("::").collect::>() { + parts if parts.len() == 1 => Some(line.to_string()), + parts if parts.len() == 2 => Some(parts[1].to_string()), + _ => { + eprintln!("警告: 忽略无效的token行: {}", line); + None + } + } + }) + .collect::>() + } + Err(e) => { + eprintln!("警告: 无法读取token文件 '{}': {}", APP_CONFIG.token_file, e); + Vec::new() + } }; // 读取现有的 token-list let mut token_map: std::collections::HashMap = - if let Ok(content) = std::fs::read_to_string(&token_list_file) { - content - .split('\n') - .filter(|s| !s.is_empty()) + match std::fs::read_to_string(&APP_CONFIG.token_list_file) { + Ok(content) => content + .lines() .filter_map(|line| { + let line = line.trim(); + if line.is_empty() { + return None; + } + let parts: Vec<&str> = line.split(',').collect(); - if parts.len() == 2 { - Some((parts[0].to_string(), parts[1].to_string())) - } else { - None + match parts[..] { + [token, checksum] => Some((token.to_string(), checksum.to_string())), + _ => { + eprintln!("警告: 忽略无效的token-list行: {}", line); + None + } } }) - .collect() - } else { - std::collections::HashMap::new() + .collect(), + Err(e) => { + eprintln!("警告: 无法读取token-list文件: {}", e); + std::collections::HashMap::new() + } }; // 为新 token 生成 checksum @@ -241,7 +278,10 @@ fn load_tokens(token_file: &str) -> Vec { .map(|(token, checksum)| format!("{},{}", token, checksum)) .collect::>() .join("\n"); - std::fs::write(token_list_file, token_list_content).unwrap(); + + if let Err(e) = std::fs::write(&APP_CONFIG.token_list_file, token_list_content) { + eprintln!("警告: 无法更新token-list文件: {}", e); + } // 转换为 TokenInfo vector token_map @@ -253,14 +293,14 @@ fn load_tokens(token_file: &str) -> Vec { // 根路由处理 async fn handle_root(State(state): State>>) -> Json { let state = state.lock().await; - let uptime = (Local::now() - state.start_time).num_seconds(); + let uptime = (Local::now() - APP_CONFIG.start_time).num_seconds(); Json(serde_json::json!({ "status": "healthy", - "version": state.version, + "version": APP_CONFIG.version, "uptime": uptime, "stats": { - "started": state.start_time, + "started": APP_CONFIG.start_time, "totalRequests": state.total_requests, "activeRequests": state.active_requests, "memory": { @@ -271,8 +311,8 @@ async fn handle_root(State(state): State>>) -> Json>(), "endpoints": [ - &format!("{}/v1/chat/completions", state.route_prefix), - &format!("{}/v1/models", state.route_prefix), + &format!("{}/v1/chat/completions", APP_CONFIG.route_prefix), + &format!("{}/v1/models", APP_CONFIG.route_prefix), "/checksum", "/tokeninfo", "/update-tokeninfo", @@ -311,11 +351,8 @@ async fn handle_checksum() -> Json { async fn handle_update_tokeninfo( State(state): State>>, ) -> Json { - // 获取当前的 token 文件路径 - let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string()); - // 重新加载 tokens - let token_infos = load_tokens(&token_file); + let token_infos = load_tokens(); // 更新应用状态 { @@ -341,25 +378,19 @@ async fn handle_get_tokeninfo( .and_then(|h| h.strip_prefix("Bearer ")) .ok_or(StatusCode::UNAUTHORIZED)?; - let env_token = std::env::var("AUTH_TOKEN").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - if auth_header != env_token { + if auth_header != APP_CONFIG.auth_token { return Err(StatusCode::UNAUTHORIZED); } - // 获取文件路径 - let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string()); - let token_list_file = - std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string()); - // 读取文件内容 - 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()); + let tokens = std::fs::read_to_string(&APP_CONFIG.token_file).unwrap_or_else(|_| String::new()); + let token_list = + std::fs::read_to_string(&APP_CONFIG.token_list_file).unwrap_or_else(|_| String::new()); Ok(Json(serde_json::json!({ "status": "success", - "token_file": token_file, - "token_list_file": token_list_file, + "token_file": APP_CONFIG.token_file, + "token_list_file": APP_CONFIG.token_list_file, "tokens": tokens, "token_list": token_list }))) @@ -377,28 +408,22 @@ async fn handle_update_tokeninfo_post( .and_then(|h| h.strip_prefix("Bearer ")) .ok_or(StatusCode::UNAUTHORIZED)?; - let env_token = std::env::var("AUTH_TOKEN").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - if auth_header != env_token { + if auth_header != APP_CONFIG.auth_token { return Err(StatusCode::UNAUTHORIZED); } - // 获取文件路径 - let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string()); - let token_list_file = - std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string()); - // 写入 .token 文件 - std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + std::fs::write(&APP_CONFIG.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) + std::fs::write(&APP_CONFIG.token_list_file, token_list) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } // 重新加载 tokens - let token_infos = load_tokens(&token_file); + let token_infos = load_tokens(); let token_infos_len = token_infos.len(); // 更新应用状态 @@ -410,8 +435,8 @@ async fn handle_update_tokeninfo_post( Ok(Json(serde_json::json!({ "status": "success", "message": "Token files have been updated and reloaded", - "token_file": token_file, - "token_list_file": token_list_file, + "token_file": APP_CONFIG.token_file, + "token_list_file": APP_CONFIG.token_list_file, "token_count": token_infos_len }))) } @@ -469,14 +494,12 @@ async fn handle_chat( Json(ChatError::Unauthorized.to_json()), ))?; - // 验证环境变量中的 AUTH_TOKEN - if let Ok(env_token) = std::env::var("AUTH_TOKEN") { - if auth_token != env_token { - return Err(( - StatusCode::UNAUTHORIZED, - Json(ChatError::Unauthorized.to_json()), - )); - } + // 验证 AuthToken + if auth_token != APP_CONFIG.auth_token { + return Err(( + StatusCode::UNAUTHORIZED, + Json(ChatError::Unauthorized.to_json()), + )); } // 完整的令牌处理逻辑和对应的 checksum @@ -508,6 +531,8 @@ async fn handle_chat( checksum: checksum.clone(), auth_token: auth_token.clone(), stream: request.stream, + status: "pending".to_string(), + error: None, }); if state.request_logs.len() > 100 { @@ -556,13 +581,33 @@ async fn handle_chat( .header("Host", "api2.cursor.sh") .body(hex_data) .send() - .await - .map_err(|e| { - ( + .await; + + // 处理请求结果 + let response = match response { + Ok(resp) => { + // 更新请求日志为成功 + { + let mut state = state.lock().await; + state.request_logs.last_mut().unwrap().status = "success".to_string(); + } + resp + } + Err(e) => { + // 更新请求日志为失败 + { + let mut state = state.lock().await; + if let Some(last_log) = state.request_logs.last_mut() { + last_log.status = "failed".to_string(); + last_log.error = Some(e.to_string()); + } + } + return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ChatError::RequestFailed(format!("Request failed: {}", e)).to_json()), - ) - })?; + )); + } + }; // 释放活动请求计数 { @@ -579,11 +624,11 @@ async fn handle_chat( async move { let chunk = chunk.unwrap_or_default(); - let text = cursor_api::decode_response(&chunk).await; - - if text.is_empty() { - return Ok::<_, Infallible>(Bytes::from("[DONE]")); - } + let text = match cursor_api::decode_response(&chunk).await { + Ok(text) if text.is_empty() => return Ok(Bytes::from("data: [DONE]\n\n")), + Ok(text) => text, + Err(_) => return Ok(Bytes::new()), + }; let data = serde_json::json!({ "id": &response_id, @@ -623,7 +668,11 @@ async fn handle_chat( ), ) })?; - full_text.push_str(&cursor_api::decode_response(&chunk).await); + full_text.push_str( + &cursor_api::decode_response(&chunk) + .await + .unwrap_or_default(), + ); } // 处理文本