├── .github └── workflows │ ├── build-linux.yml │ ├── build.yml │ └── docker.yml ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── README.md ├── build.rs ├── scripts ├── build.ps1 ├── build.sh ├── minify.js ├── package-lock.json ├── package.json ├── setup.ps1 └── setup.sh ├── src ├── app.rs ├── app │ ├── config.rs │ ├── constant.rs │ ├── lazy.rs │ ├── model.rs │ └── model │ │ └── usage_check.rs ├── chat.rs ├── chat │ ├── adapter.rs │ ├── aiserver.rs │ ├── aiserver │ │ ├── v1.rs │ │ └── v1 │ │ │ └── lite.proto │ ├── constant.rs │ ├── error.rs │ ├── model.rs │ ├── route.rs │ ├── route │ │ ├── api.rs │ │ ├── config.rs │ │ ├── health.rs │ │ ├── logs.rs │ │ ├── profile.rs │ │ └── token.rs │ ├── service.rs │ └── stream.rs ├── common.rs ├── common │ ├── client.rs │ ├── models.rs │ ├── models │ │ ├── config.rs │ │ ├── error.rs │ │ ├── health.rs │ │ └── userinfo.rs │ ├── utils.rs │ └── utils │ │ ├── checksum.rs │ │ └── tokens.rs └── main.rs └── static ├── api.html ├── api.min.html ├── config.html ├── config.min.html ├── logs.html ├── logs.min.html ├── readme.html ├── readme.min.html ├── shared-styles.css ├── shared-styles.min.css ├── shared.js ├── shared.min.js ├── tokeninfo.html └── tokeninfo.min.html /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Linux Binaries 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build ${{ matrix.target }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | target: [x86_64-unknown-linux-gnu] 13 | 14 | steps: 15 | - uses: actions/checkout@v4.2.2 16 | 17 | - name: Install Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | targets: ${{ matrix.target }} 21 | 22 | - name: Install dependencies 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install -y protobuf-compiler pkg-config libssl-dev nodejs npm 26 | 27 | - name: Build binary 28 | run: RUSTFLAGS="-C link-arg=-s" cargo build --release --target ${{ matrix.target }} 29 | 30 | - name: Upload artifact 31 | uses: actions/upload-artifact@v4.5.0 32 | with: 33 | name: cursor-api-${{ matrix.target }} 34 | path: target/${{ matrix.target }}/release/cursor-api 35 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | build: 11 | name: Build ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | include: 17 | - os: ubuntu-latest 18 | targets: x86_64-unknown-linux-gnu 19 | - os: windows-latest 20 | targets: x86_64-pc-windows-msvc 21 | - os: macos-latest 22 | targets: x86_64-apple-darwin,aarch64-apple-darwin 23 | 24 | steps: 25 | - uses: actions/checkout@v4.2.2 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4.1.0 29 | with: 30 | node-version: '20' 31 | cache: 'npm' 32 | cache-dependency-path: 'scripts/package-lock.json' 33 | run: cd scripts && npm install && cd .. 34 | 35 | - name: Install Rust 36 | uses: dtolnay/rust-toolchain@stable 37 | with: 38 | targets: ${{ matrix.targets }} 39 | 40 | - name: Install Linux dependencies (x86_64) 41 | if: runner.os == 'Linux' 42 | run: | 43 | sudo apt-get update 44 | sudo apt-get install -y \ 45 | build-essential \ 46 | protobuf-compiler \ 47 | pkg-config \ 48 | libssl-dev \ 49 | openssl \ 50 | musl-tools \ 51 | musl-dev \ 52 | libssl-dev:native \ 53 | linux-libc-dev:native 54 | 55 | # 设置 OpenSSL 环境变量 56 | echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV 57 | echo "OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV 58 | echo "OPENSSL_INCLUDE_DIR=/usr/include/openssl" >> $GITHUB_ENV 59 | echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV 60 | 61 | - name: Build Linux x86_64 (Dynamic) 62 | if: runner.os == 'Linux' 63 | run: bash scripts/build.sh 64 | 65 | - name: Build Linux x86_64 (Static) 66 | if: runner.os == 'Linux' 67 | run: | 68 | # 使用 musl 目标 69 | rustup target remove x86_64-unknown-linux-gnu 70 | rustup target add x86_64-unknown-linux-musl 71 | 72 | # 设置静态编译环境变量 73 | export CC=musl-gcc 74 | 75 | bash scripts/build.sh --static 76 | 77 | - name: Install macOS dependencies 78 | if: runner.os == 'macOS' 79 | run: | 80 | brew install \ 81 | protobuf \ 82 | pkg-config \ 83 | openssl@3 84 | echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV 85 | echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV 86 | 87 | - name: Install Windows dependencies 88 | if: runner.os == 'Windows' 89 | run: | 90 | choco install -y protoc 91 | choco install -y openssl 92 | choco install -y nodejs-lts 93 | 94 | # 刷新环境变量 95 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") 96 | 97 | # 设置 OpenSSL 环境变量 98 | echo "OPENSSL_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV 99 | echo "PKG_CONFIG_PATH=C:\Program Files\OpenSSL\lib\pkgconfig" >> $env:GITHUB_ENV 100 | 101 | - name: Build macOS (Dynamic) 102 | if: runner.os == 'macOS' || runner.os == 'Windows' 103 | run: bash scripts/build.sh 104 | 105 | - name: Build macOS (Static) 106 | if: runner.os == 'macOS' || runner.os == 'Windows' 107 | run: bash scripts/build.sh --static 108 | 109 | # - name: Verify build artifacts 110 | # run: | 111 | # if [ ! -d "release" ] || [ -z "$(ls -A release)" ]; then 112 | # echo "Error: No build artifacts found in release directory" 113 | # exit 1 114 | # fi 115 | 116 | - name: Upload artifacts 117 | uses: actions/upload-artifact@v4.5.0 118 | with: 119 | name: binaries-${{ matrix.os }} 120 | path: release/* 121 | retention-days: 1 122 | 123 | build-freebsd: 124 | name: Build FreeBSD 125 | runs-on: ubuntu-latest 126 | 127 | steps: 128 | - uses: actions/checkout@v4.2.2 129 | 130 | - name: Build on FreeBSD 131 | uses: vmactions/freebsd-vm@v1.1.5 132 | with: 133 | usesh: true 134 | prepare: | 135 | # 设置持久化的环境变量 136 | echo 'export SSL_CERT_FILE=/etc/ssl/cert.pem' >> /root/.profile 137 | echo 'export PATH="/usr/local/bin:$PATH"' >> /root/.profile 138 | 139 | # 安装基础依赖 140 | pkg update 141 | pkg install -y \ 142 | git \ 143 | curl \ 144 | node20 \ 145 | www/npm \ 146 | protobuf \ 147 | ca_root_nss \ 148 | bash \ 149 | gmake \ 150 | pkgconf \ 151 | openssl \ 152 | libressl-devel \ 153 | libiconv \ 154 | gettext-tools \ 155 | gettext-runtime 156 | 157 | export SSL_CERT_FILE=/etc/ssl/cert.pem 158 | 159 | # 克隆代码(确保在正确的目录) 160 | cd /root 161 | git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY . 162 | 163 | # 安装 rustup 和 Rust 164 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain nightly 165 | 166 | # 设置持久化的 Rust 环境变量 167 | echo '. "$HOME/.cargo/env"' >> /root/.profile 168 | 169 | # 添加所需的目标支持 170 | . /root/.profile 171 | rustup target add x86_64-unknown-freebsd 172 | rustup component add rust-src 173 | 174 | run: | 175 | # 加载环境变量 176 | . /root/.profile 177 | 178 | echo "构建动态链接版本..." 179 | /usr/local/bin/bash scripts/build.sh 180 | 181 | echo "构建静态链接版本..." 182 | /usr/local/bin/bash scripts/build.sh --static 183 | 184 | - name: Upload artifacts 185 | uses: actions/upload-artifact@v4.5.0 186 | with: 187 | name: binaries-freebsd 188 | path: release/* 189 | retention-days: 1 190 | 191 | release: 192 | name: Create Release 193 | needs: [build, build-freebsd] 194 | runs-on: ubuntu-latest 195 | permissions: 196 | contents: write 197 | 198 | steps: 199 | - uses: actions/checkout@v4.2.2 200 | 201 | - name: Download all artifacts 202 | uses: actions/download-artifact@v4.1.8 203 | with: 204 | path: artifacts 205 | 206 | - name: Prepare release assets 207 | run: | 208 | mkdir release 209 | cd artifacts 210 | for dir in binaries-*; do 211 | cp -r "$dir"/* ../release/ 212 | done 213 | 214 | - name: Generate checksums 215 | run: | 216 | cd release 217 | sha256sum * > SHA256SUMS.txt 218 | 219 | - name: Create Release 220 | uses: softprops/action-gh-release@v2.2.0 221 | with: 222 | files: | 223 | release/* 224 | draft: false 225 | prerelease: false 226 | generate_release_notes: true 227 | fail_on_unmatched_files: true -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | update_latest: 7 | description: '是否更新 latest 标签' 8 | required: true 9 | type: boolean 10 | default: false 11 | upload_artifacts: 12 | description: '是否上传构建产物' 13 | required: true 14 | type: boolean 15 | default: false 16 | push: 17 | tags: 18 | - 'v*' 19 | 20 | env: 21 | IMAGE_NAME: ${{ github.repository_owner }}/cursor-api 22 | 23 | jobs: 24 | build-and-push: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4.2.2 30 | 31 | - name: Get version from Cargo.toml 32 | if: github.event_name == 'workflow_dispatch' 33 | id: cargo_version 34 | run: | 35 | VERSION=$(grep '^version = ' Cargo.toml | cut -d '"' -f2) 36 | echo "version=v${VERSION}" >> $GITHUB_OUTPUT 37 | 38 | - name: Log in to Docker Hub 39 | uses: docker/login-action@v3.3.0 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USERNAME }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | 44 | - name: Extract metadata for Docker 45 | id: meta 46 | uses: docker/metadata-action@v5.6.1 47 | with: 48 | images: ${{ env.IMAGE_NAME }} 49 | tags: | 50 | type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.update_latest }} 51 | type=raw,value=${{ steps.cargo_version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} 52 | type=ref,event=tag,enable=${{ github.event_name == 'push' }} 53 | 54 | - name: Set up Docker Buildx 55 | uses: docker/setup-buildx-action@v3.8.0 56 | with: 57 | driver-opts: | 58 | image=moby/buildkit:latest 59 | network=host 60 | 61 | - name: Build and push Docker image 62 | uses: docker/build-push-action@v6.11.0 63 | env: 64 | DOCKER_BUILD_RECORD_UPLOAD: false 65 | with: 66 | context: . 67 | push: true 68 | platforms: linux/amd64,linux/arm64 69 | tags: ${{ steps.meta.outputs.tags }} 70 | labels: ${{ steps.meta.outputs.labels }} 71 | cache-from: type=gha 72 | cache-to: type=gha,mode=max 73 | outputs: type=local,dest=./dist,enable=${{ github.event_name == 'workflow_dispatch' && inputs.upload_artifacts }} 74 | 75 | - name: Prepare artifacts 76 | if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts 77 | run: | 78 | mkdir -p artifacts/amd64 artifacts/arm64 79 | cp dist/linux_amd64/app/cursor-api artifacts/amd64/ 80 | cp dist/linux_arm64/app/cursor-api artifacts/arm64/ 81 | 82 | - name: Upload artifacts 83 | if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts 84 | uses: actions/upload-artifact@v4.6.0 85 | with: 86 | name: cursor-api-binaries 87 | path: artifacts/ 88 | retention-days: 7 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursor-api" 3 | version = "0.1.3-rc.3" 4 | edition = "2021" 5 | authors = ["wisdgod "] 6 | description = "OpenAI format compatibility layer for the Cursor API" 7 | repository = "https://github.com/wisdgod/cursor-api" 8 | 9 | [build-dependencies] 10 | prost-build = "0.13.4" 11 | sha2 = { version = "0.10.8", default-features = false } 12 | serde_json = "1.0.134" 13 | 14 | [dependencies] 15 | axum = { version = "0.7.9", features = ["json"] } 16 | base64 = { version = "0.22.1", default-features = false, features = ["std"] } 17 | # brotli = { version = "7.0.0", default-features = false, features = ["std"] } 18 | bytes = "1.9.0" 19 | chrono = { version = "0.4.39", default-features = false, features = ["std", "clock", "now", "serde"] } 20 | dotenvy = "0.15.7" 21 | flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] } 22 | futures = { version = "0.3.31", default-features = false, features = ["std"] } 23 | gif = { version = "0.13.1", default-features = false, features = ["std"] } 24 | hex = { version = "0.4.3", default-features = false, features = ["std"] } 25 | image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] } 26 | paste = "1.0.15" 27 | prost = "0.13.4" 28 | rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } 29 | regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } 30 | reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] } 31 | serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } 32 | serde_json = "1.0.135" 33 | sha2 = { version = "0.10.8", default-features = false } 34 | sysinfo = { version = "0.33.1", default-features = false, features = ["system"] } 35 | tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] } 36 | tokio-stream = { version = "0.1.17", features = ["time"] } 37 | tower-http = { version = "0.6.2", features = ["cors", "limit"] } 38 | urlencoding = "2.1.3" 39 | uuid = { version = "1.11.1", features = ["v4"] } 40 | 41 | [profile.release] 42 | lto = true 43 | codegen-units = 1 44 | panic = 'abort' 45 | strip = true 46 | opt-level = 3 47 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | pre-build = [ 3 | "set -e", 4 | "apt-get update", 5 | "apt-get install -y --no-install-recommends build-essential protobuf-compiler pkg-config libssl-dev nodejs npm", 6 | "rm -rf /var/lib/apt/lists/*" 7 | ] 8 | 9 | [target.x86_64-unknown-freebsd] 10 | pre-build = [ 11 | "pkg update", 12 | "pkg install -y node20 www/npm protobuf ca_root_nss bash gmake pkgconf openssl", 13 | "export SSL_CERT_FILE=/etc/ssl/cert.pem" 14 | ] 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # AMD64 构建阶段 2 | FROM --platform=linux/amd64 rust:1.83.0-slim-bookworm as builder-amd64 3 | WORKDIR /app 4 | RUN apt-get update && \ 5 | apt-get install -y --no-install-recommends \ 6 | build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ 7 | && rm -rf /var/lib/apt/lists/* 8 | COPY . . 9 | ENV RUSTFLAGS="-C link-arg=-s" 10 | RUN cargo build --release && \ 11 | cp target/release/cursor-api /app/cursor-api 12 | 13 | # ARM64 构建阶段 14 | FROM --platform=linux/arm64 rust:1.83.0-slim-bookworm as builder-arm64 15 | WORKDIR /app 16 | RUN apt-get update && \ 17 | apt-get install -y --no-install-recommends \ 18 | build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \ 19 | && rm -rf /var/lib/apt/lists/* 20 | COPY . . 21 | ENV RUSTFLAGS="-C link-arg=-s" 22 | RUN cargo build --release && \ 23 | cp target/release/cursor-api /app/cursor-api 24 | 25 | # AMD64 运行阶段 26 | FROM --platform=linux/amd64 debian:bookworm-slim as run-amd64 27 | WORKDIR /app 28 | ENV TZ=Asia/Shanghai 29 | RUN apt-get update && \ 30 | apt-get install -y --no-install-recommends \ 31 | ca-certificates tzdata \ 32 | && rm -rf /var/lib/apt/lists/* 33 | COPY --from=builder-amd64 /app/cursor-api . 34 | 35 | # ARM64 运行阶段 36 | FROM --platform=linux/arm64 debian:bookworm-slim as run-arm64 37 | WORKDIR /app 38 | ENV TZ=Asia/Shanghai 39 | RUN apt-get update && \ 40 | apt-get install -y --no-install-recommends \ 41 | ca-certificates tzdata \ 42 | && rm -rf /var/lib/apt/lists/* 43 | COPY --from=builder-arm64 /app/cursor-api . 44 | 45 | # 通用配置 46 | FROM run-${TARGETARCH} 47 | ENV PORT=3000 48 | EXPOSE ${PORT} 49 | CMD ["./cursor-api"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cursor-api 2 | 3 | ## 获取key 4 | 5 | 1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录 6 | 2. 在浏览器中打开开发者工具(F12) 7 | 3. 在 Application-Cookies 中查找名为 `WorkosCursorSessionToken` 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式,cookie 的值使用冒号 (:) 进行分隔。 8 | 9 | ## 接口说明 10 | 11 | ### 基础对话(请求格式和响应格式参考 openai) 12 | 13 | - 接口地址:`/v1/chat/completions` 14 | - 请求方法:POST 15 | - 认证方式:Bearer Token 16 | 1. 使用环境变量 `AUTH_TOKEN` 进行认证 17 | 2. 使用 `.token` 文件中的令牌列表进行轮询认证 18 | 19 | ### Token管理接口 20 | 21 | #### 简易Token信息管理页面 22 | - 接口地址:`/tokeninfo` 23 | - 请求方法:GET 24 | - 响应格式:HTML页面 25 | - 功能:获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容 26 | 27 | #### 更新Token信息 28 | - 接口地址:`/update-tokeninfo` 29 | - 请求方法:GET 30 | - 认证方式:不需要 31 | - 功能:请求内容不包括文件内容,直接修改文件,调用重载函数 32 | 33 | #### 更新Token信息 34 | - 接口地址:`/update-tokeninfo` 35 | - 请求方法:POST 36 | - 认证方式:Bearer Token 37 | - 功能:请求内容包括文件内容,间接修改文件,调用重载函数 38 | 39 | #### 获取Token信息 40 | - 接口地址:`/get-tokeninfo` 41 | - 请求方法:POST 42 | - 认证方式:Bearer Token 43 | 44 | ### 其他接口 45 | 46 | #### 获取模型列表 47 | - 接口地址:`/v1/models` 48 | - 请求方法:GET 49 | 50 | #### 获取随机x-cursor-checksum 51 | - 接口地址:`/checksum` 52 | - 请求方法:GET 53 | 54 | #### 健康检查接口 55 | - 接口地址:`/` 56 | - 请求方法:GET 57 | 58 | #### 获取日志接口 59 | - 接口地址:`/logs` 60 | - 请求方法:GET 61 | 62 | ## 配置说明 63 | 64 | ### 环境变量 65 | 66 | - `PORT`: 服务器端口号(默认:3000) 67 | - `AUTH_TOKEN`: 认证令牌(必须,用于API认证) 68 | - `ROUTE_PREFIX`: 路由前缀(可选) 69 | - `TOKEN_FILE`: token文件路径(默认:.token) 70 | - `TOKEN_LIST_FILE`: token列表文件路径(默认:.token-list) 71 | 72 | ### Token文件格式 73 | 74 | 1. `.token` 文件:每行一个token,支持以下格式: 75 | 76 | ``` 77 | # 这是注释 78 | token1 79 | # alias与标签的作用差不多 80 | alias::token2 81 | ``` 82 | 83 | alias 可以是任意值,用于区分不同的 token,更方便管理,WorkosCursorSessionToken 是相同格式 84 | 该文件将自动向.token-list文件中追加token,同时自动生成checksum 85 | 86 | 2. `.token-list` 文件:每行为token和checksum的对应关系: 87 | 88 | ``` 89 | # 这里的#表示这行在下次读取要删除 90 | token1,checksum1 91 | # 支持像.token一样的alias,冲突时以.token为准 92 | alias::token2,checksum2 93 | ``` 94 | 95 | 该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改: 96 | 97 | - 需要删除某个 token 98 | - 需要使用已有 checksum 来对应某一个 token 99 | 100 | ### 模型列表 101 | 102 | 写死了,后续也不会会支持自定义模型列表 103 | ``` 104 | claude-3.5-sonnet 105 | gpt-4 106 | gpt-4o 107 | claude-3-opus 108 | cursor-fast 109 | cursor-small 110 | gpt-3.5-turbo 111 | gpt-4-turbo-2024-04-09 112 | gpt-4o-128k 113 | gemini-1.5-flash-500k 114 | claude-3-haiku-200k 115 | claude-3-5-sonnet-200k 116 | claude-3-5-sonnet-20241022 117 | gpt-4o-mini 118 | o1-mini 119 | o1-preview 120 | o1 121 | claude-3.5-haiku 122 | gemini-exp-1206 123 | gemini-2.0-flash-thinking-exp 124 | gemini-2.0-flash-exp 125 | ``` 126 | 127 | ## 部署 128 | 129 | ### Docker 部署 130 | 131 | #### Docker 运行示例 132 | 133 | ```bash 134 | docker run -d -e PORT=3000 -e AUTH_TOKEN=your_token -p 3000:3000 wisdgod/cursor-api:latest 135 | ``` 136 | 137 | ### huggingface部署 138 | 139 | 前提:一个huggingface账号 140 | 141 | 1. 创建一个Space并创建一个Dockerfile文件,内容如下: 142 | 143 | ```Dockerfile 144 | FROM wisdgod/cursor-api:latest 145 | 146 | # 可能你要覆盖原镜像的环境变量,但都可以在下面的第2步中配置 147 | ENV PORT=7860 148 | ``` 149 | 150 | 2. 配置环境变量 151 | 152 | 在你的 Space 中,点击 Settings,找到 `Variables and secrets`,添加 Variables 153 | 154 | ```env 155 | # 可选,用于配置服务器端口 156 | PORT=3000 157 | # 必选,用于配置路由前缀,比如/api,/hf,/proxy等等 158 | ROUTE_PREFIX= 159 | # 必选,用于API认证 160 | AUTH_TOKEN= 161 | # 可选,用于配置token文件路径 162 | TOKEN_FILE=.token 163 | # 可选,用于配置token列表文件路径 164 | TOKEN_LIST_FILE=.token-list 165 | ``` 166 | 167 | 更多变量示例可访问 /env-example 168 | 169 | 3. 重新部署 170 | 171 | 点击`Factory rebuild`,等待部署完成 172 | 173 | 4. 接口地址(`Embed this Space`中查看): 174 | 175 | ``` 176 | https://{username}-{space-name}.hf.space/v1/models 177 | ``` 178 | 179 | ## 注意事项 180 | 181 | 1. 请妥善保管您的任何 Token,不要泄露给他人。若发现泄露,请及时更改 182 | 2. 请遵守本项目许可证,你仅拥有使用本项目的权利,不得用于商业用途 183 | 3. 本项目仅供学习研究使用,请遵守 Cursor 的使用条款 184 | 185 | ## 开发 186 | 187 | ### 跨平台编译 188 | 189 | 自行配置cross编译环境 190 | 191 | 支持的平台: 192 | 193 | - linux x86_64 194 | - windows x86_64 195 | - macos x86_64 196 | - freebsd x86_64 197 | - docker (only for linux x86_64) 198 | 199 | ### 获取token 200 | 201 | - 使用 [get-token](https://github.com/wisdgod/cursor-api/tree/main/get-token) 获取读取当前设备token,仅支持windows与macos 202 | 203 | ## 鸣谢 204 | 205 | - [cursor-api](https://github.com/wisdgod/cursor-api) 206 | - [zhx47/cursor-api](https://github.com/zhx47/cursor-api) 207 | - [luolazyandlazy/cursorToApi](https://github.com/luolazyandlazy/cursorToApi) 208 | 209 | ## 许可证 210 | 211 | 版权所有 (c) 2024 212 | 213 | 本软件仅供学习和研究使用。未经授权,不得用于商业用途。 214 | 保留所有权利。 -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | use std::collections::HashMap; 3 | use std::fs; 4 | use std::io::Result; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::Command; 7 | 8 | // 支持的文件类型 9 | const SUPPORTED_EXTENSIONS: [&str; 3] = ["html", "js", "css"]; 10 | 11 | fn check_and_install_deps() -> Result<()> { 12 | let scripts_dir = Path::new("scripts"); 13 | let node_modules = scripts_dir.join("node_modules"); 14 | 15 | if !node_modules.exists() { 16 | println!("cargo:warning=Installing minifier dependencies..."); 17 | let status = Command::new("npm") 18 | .current_dir(scripts_dir) 19 | .arg("install") 20 | .status()?; 21 | 22 | if !status.success() { 23 | panic!("Failed to install npm dependencies"); 24 | } 25 | println!("cargo:warning=Dependencies installed successfully"); 26 | } 27 | Ok(()) 28 | } 29 | 30 | fn get_files_hash() -> Result> { 31 | let mut file_hashes = HashMap::new(); 32 | let static_dir = Path::new("static"); 33 | 34 | if static_dir.exists() { 35 | for entry in fs::read_dir(static_dir)? { 36 | let entry = entry?; 37 | let path = entry.path(); 38 | 39 | // 检查是否是支持的文件类型,且不是已经压缩的文件 40 | if let Some(ext) = path.extension().and_then(|e| e.to_str()) { 41 | if SUPPORTED_EXTENSIONS.contains(&ext) && !path.to_string_lossy().contains(".min.") 42 | { 43 | let content = fs::read(&path)?; 44 | let mut hasher = Sha256::new(); 45 | hasher.update(&content); 46 | let hash = format!("{:x}", hasher.finalize()); 47 | file_hashes.insert(path, hash); 48 | } 49 | } 50 | } 51 | } 52 | 53 | Ok(file_hashes) 54 | } 55 | 56 | fn load_saved_hashes() -> Result> { 57 | let hash_file = Path::new("scripts/.asset-hashes.json"); 58 | if hash_file.exists() { 59 | let content = fs::read_to_string(hash_file)?; 60 | let hash_map: HashMap = serde_json::from_str(&content)?; 61 | Ok(hash_map 62 | .into_iter() 63 | .map(|(k, v)| (PathBuf::from(k), v)) 64 | .collect()) 65 | } else { 66 | Ok(HashMap::new()) 67 | } 68 | } 69 | 70 | fn save_hashes(hashes: &HashMap) -> Result<()> { 71 | let hash_file = Path::new("scripts/.asset-hashes.json"); 72 | let string_map: HashMap = hashes 73 | .iter() 74 | .map(|(k, v)| (k.to_string_lossy().into_owned(), v.clone())) 75 | .collect(); 76 | let content = serde_json::to_string_pretty(&string_map)?; 77 | fs::write(hash_file, content)?; 78 | Ok(()) 79 | } 80 | 81 | fn minify_assets() -> Result<()> { 82 | // 获取现有文件的哈希 83 | let current_hashes = get_files_hash()?; 84 | 85 | if current_hashes.is_empty() { 86 | println!("cargo:warning=No files to minify"); 87 | return Ok(()); 88 | } 89 | 90 | // 加载保存的哈希值 91 | let saved_hashes = load_saved_hashes()?; 92 | 93 | // 找出需要更新的文件 94 | let files_to_update: Vec<_> = current_hashes 95 | .iter() 96 | .filter(|(path, current_hash)| { 97 | let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); 98 | let min_path = path.with_file_name(format!( 99 | "{}.min.{}", 100 | path.file_stem().unwrap().to_string_lossy(), 101 | ext 102 | )); 103 | 104 | // 检查压缩后的文件是否存在 105 | if !min_path.exists() { 106 | return true; 107 | } 108 | 109 | // 检查原始文件是否发生变化 110 | saved_hashes 111 | .get(*path) 112 | .map_or(true, |saved_hash| saved_hash != *current_hash) 113 | }) 114 | .map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned()) 115 | .collect(); 116 | 117 | if files_to_update.is_empty() { 118 | println!("cargo:warning=No files need to be updated"); 119 | return Ok(()); 120 | } 121 | 122 | println!("cargo:warning=Minifying {} files...", files_to_update.len()); 123 | 124 | // 运行压缩脚本 125 | let status = Command::new("node") 126 | .arg("scripts/minify.js") 127 | .args(&files_to_update) 128 | .status()?; 129 | 130 | if !status.success() { 131 | panic!("Asset minification failed"); 132 | } 133 | 134 | // 保存新的哈希值 135 | save_hashes(¤t_hashes)?; 136 | 137 | Ok(()) 138 | } 139 | 140 | fn main() -> Result<()> { 141 | // Proto 文件处理 142 | println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto"); 143 | let mut config = prost_build::Config::new(); 144 | // config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); 145 | // config.type_attribute( 146 | // "aiserver.v1.ThrowErrorCheckRequest", 147 | // "#[derive(serde::Serialize, serde::Deserialize)]" 148 | // ); 149 | config 150 | .compile_protos(&["src/chat/aiserver/v1/lite.proto"], &["src/chat/aiserver/v1/"]) 151 | .unwrap(); 152 | 153 | // 静态资源文件处理 154 | println!("cargo:rerun-if-changed=scripts/minify.js"); 155 | println!("cargo:rerun-if-changed=scripts/package.json"); 156 | println!("cargo:rerun-if-changed=static"); 157 | 158 | // 检查并安装依赖 159 | check_and_install_deps()?; 160 | 161 | // 运行资源压缩 162 | minify_assets()?; 163 | 164 | Ok(()) 165 | } 166 | -------------------------------------------------------------------------------- /scripts/build.ps1: -------------------------------------------------------------------------------- 1 | # 参数处理 2 | param( 3 | [switch]$Static, 4 | [switch]$Help, 5 | [ValidateSet("x86_64", "aarch64", "i686")] 6 | [string]$Architecture 7 | ) 8 | 9 | # 设置错误时停止执行 10 | $ErrorActionPreference = "Stop" 11 | 12 | # 颜色输出函数 13 | function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue } 14 | function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow } 15 | function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 } 16 | 17 | # 检查必要的工具 18 | function Check-Requirements { 19 | $tools = @("cargo", "protoc", "npm", "node") 20 | $missing = @() 21 | 22 | foreach ($tool in $tools) { 23 | if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { 24 | $missing += $tool 25 | } 26 | } 27 | 28 | if ($missing.Count -gt 0) { 29 | Write-Error "缺少必要工具: $($missing -join ', ')" 30 | } 31 | } 32 | 33 | # 帮助信息 34 | function Show-Help { 35 | Write-Host @" 36 | 用法: $(Split-Path $MyInvocation.ScriptName -Leaf) [选项] 37 | 38 | 选项: 39 | -Static 使用静态链接(默认动态链接) 40 | -Help 显示此帮助信息 41 | 42 | 不带参数时使用默认配置构建 43 | "@ 44 | } 45 | 46 | # 构建函数 47 | function Build-Target { 48 | param ( 49 | [string]$Target, 50 | [string]$RustFlags 51 | ) 52 | 53 | Write-Info "正在构建 $Target..." 54 | 55 | # 设置环境变量 56 | $env:RUSTFLAGS = $RustFlags 57 | 58 | # 构建 59 | if ($Target -ne (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) { 60 | cargo build --target $Target --release 61 | } else { 62 | cargo build --release 63 | } 64 | 65 | # 移动编译产物到 release 目录 66 | $binaryName = "cursor-api" 67 | if ($Static) { 68 | $binaryName += "-static" 69 | } 70 | 71 | $binaryPath = if ($Target -eq (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) { 72 | "target/release/cursor-api.exe" 73 | } else { 74 | "target/$Target/release/cursor-api.exe" 75 | } 76 | 77 | if (Test-Path $binaryPath) { 78 | Copy-Item $binaryPath "release/$binaryName-$Target.exe" 79 | Write-Info "完成构建 $Target" 80 | } else { 81 | Write-Warn "构建产物未找到: $Target" 82 | Write-Warn "查找路径: $binaryPath" 83 | Write-Warn "当前目录内容:" 84 | Get-ChildItem -Recurse target/ 85 | return $false 86 | } 87 | 88 | return $true 89 | } 90 | 91 | if ($Help) { 92 | Show-Help 93 | exit 0 94 | } 95 | 96 | # 检查依赖 97 | Check-Requirements 98 | 99 | # 创建 release 目录 100 | New-Item -ItemType Directory -Force -Path release | Out-Null 101 | 102 | # 设置静态链接标志 103 | $rustFlags = "" 104 | if ($Static) { 105 | $rustFlags = "-C target-feature=+crt-static" 106 | } 107 | 108 | # 获取目标架构 109 | $arch = if ($Architecture) { 110 | $Architecture 111 | } else { 112 | switch ($env:PROCESSOR_ARCHITECTURE) { 113 | "AMD64" { "x86_64" } 114 | "ARM64" { "aarch64" } 115 | "X86" { "i686" } 116 | default { Write-Error "不支持的架构: $env:PROCESSOR_ARCHITECTURE" } 117 | } 118 | } 119 | $target = "$arch-pc-windows-msvc" 120 | 121 | Write-Info "开始构建..." 122 | if (-not (Build-Target -Target $target -RustFlags $rustFlags)) { 123 | Write-Error "构建失败" 124 | } 125 | 126 | Write-Info "构建完成!" -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # 颜色输出函数 5 | info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } 6 | warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } 7 | error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; } 8 | 9 | # 检查必要的工具 10 | check_requirements() { 11 | local missing_tools=() 12 | 13 | # 基础工具检查 14 | for tool in cargo protoc npm node; do 15 | if ! command -v "$tool" &>/dev/null; then 16 | missing_tools+=("$tool") 17 | fi 18 | done 19 | 20 | if [[ ${#missing_tools[@]} -gt 0 ]]; then 21 | error "缺少必要工具: ${missing_tools[*]}" 22 | fi 23 | } 24 | 25 | # 解析参数 26 | USE_STATIC=false 27 | 28 | while [[ $# -gt 0 ]]; do 29 | case $1 in 30 | --static) USE_STATIC=true ;; 31 | --help) show_help; exit 0 ;; 32 | *) error "未知参数: $1" ;; 33 | esac 34 | shift 35 | done 36 | 37 | # 帮助信息 38 | show_help() { 39 | cat << EOF 40 | 用法: $(basename "$0") [选项] 41 | 42 | 选项: 43 | --static 使用静态链接(默认动态链接) 44 | --help 显示此帮助信息 45 | 46 | 不带参数时只编译当前平台 47 | EOF 48 | } 49 | 50 | # 并行构建函数 51 | build_target() { 52 | local target=$1 53 | local extension="" 54 | local rustflags="${2:-}" 55 | 56 | info "正在构建 $target..." 57 | 58 | # 确定文件后缀 59 | [[ $target == *"windows"* ]] && extension=".exe" 60 | 61 | # 构建 62 | if [[ $target != "$CURRENT_TARGET" ]]; then 63 | env RUSTFLAGS="$rustflags" cargo build --target "$target" --release 64 | else 65 | env RUSTFLAGS="$rustflags" cargo build --release 66 | fi 67 | 68 | # 移动编译产物到 release 目录 69 | local binary_name="cursor-api" 70 | [[ $USE_STATIC == true ]] && binary_name+="-static" 71 | 72 | local binary_path 73 | if [[ $target == "$CURRENT_TARGET" ]]; then 74 | binary_path="target/release/cursor-api$extension" 75 | else 76 | binary_path="target/$target/release/cursor-api$extension" 77 | fi 78 | 79 | if [[ -f "$binary_path" ]]; then 80 | cp "$binary_path" "release/${binary_name}-$target$extension" 81 | info "完成构建 $target" 82 | else 83 | warn "构建产物未找到: $target" 84 | warn "查找路径: $binary_path" 85 | warn "当前目录内容:" 86 | ls -R target/ 87 | return 1 88 | fi 89 | } 90 | 91 | # 获取 CPU 架构和操作系统 92 | ARCH=$(uname -m | sed 's/^aarch64\|arm64$/aarch64/;s/^x86_64\|x86-64\|x64\|amd64$/x86_64/') 93 | OS=$(uname -s) 94 | 95 | # 确定当前系统的目标平台 96 | get_target() { 97 | local arch=$1 98 | local os=$2 99 | case "$os" in 100 | "Darwin") echo "${arch}-apple-darwin" ;; 101 | "Linux") 102 | if [[ $USE_STATIC == true ]]; then 103 | echo "${arch}-unknown-linux-musl" 104 | else 105 | echo "${arch}-unknown-linux-gnu" 106 | fi 107 | ;; 108 | "MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;; 109 | "FreeBSD") echo "${arch}-unknown-freebsd" ;; 110 | *) error "不支持的系统: $os" ;; 111 | esac 112 | } 113 | 114 | # 设置当前目标平台 115 | CURRENT_TARGET=$(get_target "$ARCH" "$OS") 116 | 117 | # 检查是否成功获取目标平台 118 | [ -z "$CURRENT_TARGET" ] && error "无法确定当前系统的目标平台" 119 | 120 | # 获取系统对应的所有目标 121 | get_targets() { 122 | case "$1" in 123 | "linux") 124 | # Linux 只构建当前架构 125 | echo "$CURRENT_TARGET" 126 | ;; 127 | "freebsd") 128 | # FreeBSD 只构建当前架构 129 | echo "$CURRENT_TARGET" 130 | ;; 131 | "windows") 132 | # Windows 只构建当前架构 133 | echo "$CURRENT_TARGET" 134 | ;; 135 | "macos") 136 | # macOS 构建所有 macOS 目标 137 | echo "x86_64-apple-darwin aarch64-apple-darwin" 138 | ;; 139 | *) error "不支持的系统组: $1" ;; 140 | esac 141 | } 142 | 143 | # 检查依赖 144 | check_requirements 145 | 146 | # 确定要构建的目标 147 | case "$OS" in 148 | Darwin) 149 | TARGETS=($(get_targets "macos")) 150 | ;; 151 | Linux) 152 | TARGETS=($(get_targets "linux")) 153 | ;; 154 | FreeBSD) 155 | TARGETS=($(get_targets "freebsd")) 156 | ;; 157 | MINGW*|MSYS*|CYGWIN*|Windows_NT) 158 | TARGETS=($(get_targets "windows")) 159 | ;; 160 | *) error "不支持的系统: $OS" ;; 161 | esac 162 | 163 | # 创建 release 目录 164 | mkdir -p release 165 | 166 | # 设置静态链接标志 167 | RUSTFLAGS="-C link-arg=-s" 168 | [[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-s" 169 | 170 | # 并行构建所有目标 171 | info "开始构建..." 172 | for target in "${TARGETS[@]}"; do 173 | build_target "$target" "$RUSTFLAGS" & 174 | done 175 | 176 | # 等待所有构建完成 177 | wait 178 | 179 | # 为 macOS 平台创建通用二进制 180 | if [[ "$OS" == "Darwin" ]] && [[ ${#TARGETS[@]} -gt 1 ]]; then 181 | binary_suffix="" 182 | [[ $USE_STATIC == true ]] && binary_suffix="-static" 183 | 184 | if [[ -f "release/cursor-api${binary_suffix}-x86_64-apple-darwin" ]] && \ 185 | [[ -f "release/cursor-api${binary_suffix}-aarch64-apple-darwin" ]]; then 186 | info "创建 macOS 通用二进制..." 187 | lipo -create \ 188 | "release/cursor-api${binary_suffix}-x86_64-apple-darwin" \ 189 | "release/cursor-api${binary_suffix}-aarch64-apple-darwin" \ 190 | -output "release/cursor-api${binary_suffix}-universal-apple-darwin" 191 | fi 192 | fi 193 | 194 | info "构建完成!" -------------------------------------------------------------------------------- /scripts/minify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { minify: minifyHtml } = require('html-minifier-terser'); 4 | const { minify: minifyJs } = require('terser'); 5 | const CleanCSS = require('clean-css'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | // 配置选项 10 | const options = { 11 | collapseWhitespace: true, 12 | removeComments: true, 13 | removeEmptyAttributes: true, 14 | removeOptionalTags: true, 15 | removeRedundantAttributes: true, 16 | removeScriptTypeAttributes: true, 17 | removeStyleLinkTypeAttributes: true, 18 | minifyCSS: true, 19 | minifyJS: true, 20 | processScripts: ['application/json'], 21 | }; 22 | 23 | // CSS 压缩选项 24 | const cssOptions = { 25 | level: 2 26 | }; 27 | 28 | // 处理文件 29 | async function minifyFile(inputPath, outputPath) { 30 | try { 31 | const ext = path.extname(inputPath).toLowerCase(); 32 | const content = fs.readFileSync(inputPath, 'utf8'); 33 | let minified; 34 | 35 | switch (ext) { 36 | case '.html': 37 | minified = await minifyHtml(content, options); 38 | break; 39 | case '.js': 40 | const result = await minifyJs(content); 41 | minified = result.code; 42 | break; 43 | case '.css': 44 | minified = new CleanCSS(cssOptions).minify(content).styles; 45 | break; 46 | default: 47 | throw new Error(`Unsupported file type: ${ext}`); 48 | } 49 | 50 | fs.writeFileSync(outputPath, minified); 51 | console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`); 52 | } catch (err) { 53 | console.error(`✗ Error processing ${inputPath}:`, err); 54 | process.exit(1); 55 | } 56 | } 57 | 58 | // 主函数 59 | async function main() { 60 | // 获取命令行参数,跳过前两个参数(node和脚本路径) 61 | const files = process.argv.slice(2); 62 | 63 | if (files.length === 0) { 64 | console.error('No input files specified'); 65 | process.exit(1); 66 | } 67 | 68 | const staticDir = path.join(__dirname, '..', 'static'); 69 | 70 | for (const file of files) { 71 | const inputPath = path.join(staticDir, file); 72 | const ext = path.extname(file); 73 | const outputPath = path.join( 74 | staticDir, 75 | file.replace(ext, `.min${ext}`) 76 | ); 77 | await minifyFile(inputPath, outputPath); 78 | } 79 | } 80 | 81 | main(); -------------------------------------------------------------------------------- /scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-minifier-scripts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "html-minifier-scripts", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "clean-css": "^5.3.3", 12 | "html-minifier-terser": "^7.2.0", 13 | "terser": "^5.37.0" 14 | }, 15 | "engines": { 16 | "node": ">=14.0.0" 17 | } 18 | }, 19 | "node_modules/@jridgewell/gen-mapping": { 20 | "version": "0.3.8", 21 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 22 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@jridgewell/set-array": "^1.2.1", 26 | "@jridgewell/sourcemap-codec": "^1.4.10", 27 | "@jridgewell/trace-mapping": "^0.3.24" 28 | }, 29 | "engines": { 30 | "node": ">=6.0.0" 31 | } 32 | }, 33 | "node_modules/@jridgewell/resolve-uri": { 34 | "version": "3.1.2", 35 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 36 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 37 | "license": "MIT", 38 | "engines": { 39 | "node": ">=6.0.0" 40 | } 41 | }, 42 | "node_modules/@jridgewell/set-array": { 43 | "version": "1.2.1", 44 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 45 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 46 | "license": "MIT", 47 | "engines": { 48 | "node": ">=6.0.0" 49 | } 50 | }, 51 | "node_modules/@jridgewell/source-map": { 52 | "version": "0.3.6", 53 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", 54 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", 55 | "license": "MIT", 56 | "dependencies": { 57 | "@jridgewell/gen-mapping": "^0.3.5", 58 | "@jridgewell/trace-mapping": "^0.3.25" 59 | } 60 | }, 61 | "node_modules/@jridgewell/sourcemap-codec": { 62 | "version": "1.5.0", 63 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 64 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 65 | "license": "MIT" 66 | }, 67 | "node_modules/@jridgewell/trace-mapping": { 68 | "version": "0.3.25", 69 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 70 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 71 | "license": "MIT", 72 | "dependencies": { 73 | "@jridgewell/resolve-uri": "^3.1.0", 74 | "@jridgewell/sourcemap-codec": "^1.4.14" 75 | } 76 | }, 77 | "node_modules/acorn": { 78 | "version": "8.14.0", 79 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 80 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 81 | "license": "MIT", 82 | "bin": { 83 | "acorn": "bin/acorn" 84 | }, 85 | "engines": { 86 | "node": ">=0.4.0" 87 | } 88 | }, 89 | "node_modules/buffer-from": { 90 | "version": "1.1.2", 91 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 92 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 93 | "license": "MIT" 94 | }, 95 | "node_modules/camel-case": { 96 | "version": "4.1.2", 97 | "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", 98 | "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", 99 | "license": "MIT", 100 | "dependencies": { 101 | "pascal-case": "^3.1.2", 102 | "tslib": "^2.0.3" 103 | } 104 | }, 105 | "node_modules/clean-css": { 106 | "version": "5.3.3", 107 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", 108 | "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", 109 | "license": "MIT", 110 | "dependencies": { 111 | "source-map": "~0.6.0" 112 | }, 113 | "engines": { 114 | "node": ">= 10.0" 115 | } 116 | }, 117 | "node_modules/commander": { 118 | "version": "10.0.1", 119 | "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", 120 | "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", 121 | "license": "MIT", 122 | "engines": { 123 | "node": ">=14" 124 | } 125 | }, 126 | "node_modules/dot-case": { 127 | "version": "3.0.4", 128 | "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", 129 | "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", 130 | "license": "MIT", 131 | "dependencies": { 132 | "no-case": "^3.0.4", 133 | "tslib": "^2.0.3" 134 | } 135 | }, 136 | "node_modules/entities": { 137 | "version": "4.5.0", 138 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 139 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 140 | "license": "BSD-2-Clause", 141 | "engines": { 142 | "node": ">=0.12" 143 | }, 144 | "funding": { 145 | "url": "https://github.com/fb55/entities?sponsor=1" 146 | } 147 | }, 148 | "node_modules/html-minifier-terser": { 149 | "version": "7.2.0", 150 | "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", 151 | "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", 152 | "license": "MIT", 153 | "dependencies": { 154 | "camel-case": "^4.1.2", 155 | "clean-css": "~5.3.2", 156 | "commander": "^10.0.0", 157 | "entities": "^4.4.0", 158 | "param-case": "^3.0.4", 159 | "relateurl": "^0.2.7", 160 | "terser": "^5.15.1" 161 | }, 162 | "bin": { 163 | "html-minifier-terser": "cli.js" 164 | }, 165 | "engines": { 166 | "node": "^14.13.1 || >=16.0.0" 167 | } 168 | }, 169 | "node_modules/lower-case": { 170 | "version": "2.0.2", 171 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", 172 | "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", 173 | "license": "MIT", 174 | "dependencies": { 175 | "tslib": "^2.0.3" 176 | } 177 | }, 178 | "node_modules/no-case": { 179 | "version": "3.0.4", 180 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", 181 | "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", 182 | "license": "MIT", 183 | "dependencies": { 184 | "lower-case": "^2.0.2", 185 | "tslib": "^2.0.3" 186 | } 187 | }, 188 | "node_modules/param-case": { 189 | "version": "3.0.4", 190 | "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", 191 | "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", 192 | "license": "MIT", 193 | "dependencies": { 194 | "dot-case": "^3.0.4", 195 | "tslib": "^2.0.3" 196 | } 197 | }, 198 | "node_modules/pascal-case": { 199 | "version": "3.1.2", 200 | "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", 201 | "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", 202 | "license": "MIT", 203 | "dependencies": { 204 | "no-case": "^3.0.4", 205 | "tslib": "^2.0.3" 206 | } 207 | }, 208 | "node_modules/relateurl": { 209 | "version": "0.2.7", 210 | "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", 211 | "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", 212 | "license": "MIT", 213 | "engines": { 214 | "node": ">= 0.10" 215 | } 216 | }, 217 | "node_modules/source-map": { 218 | "version": "0.6.1", 219 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 220 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 221 | "license": "BSD-3-Clause", 222 | "engines": { 223 | "node": ">=0.10.0" 224 | } 225 | }, 226 | "node_modules/source-map-support": { 227 | "version": "0.5.21", 228 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 229 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 230 | "license": "MIT", 231 | "dependencies": { 232 | "buffer-from": "^1.0.0", 233 | "source-map": "^0.6.0" 234 | } 235 | }, 236 | "node_modules/terser": { 237 | "version": "5.37.0", 238 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", 239 | "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", 240 | "license": "BSD-2-Clause", 241 | "dependencies": { 242 | "@jridgewell/source-map": "^0.3.3", 243 | "acorn": "^8.8.2", 244 | "commander": "^2.20.0", 245 | "source-map-support": "~0.5.20" 246 | }, 247 | "bin": { 248 | "terser": "bin/terser" 249 | }, 250 | "engines": { 251 | "node": ">=10" 252 | } 253 | }, 254 | "node_modules/terser/node_modules/commander": { 255 | "version": "2.20.3", 256 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 257 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 258 | "license": "MIT" 259 | }, 260 | "node_modules/tslib": { 261 | "version": "2.8.1", 262 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 263 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 264 | "license": "0BSD" 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-minifier-scripts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=14.0.0" 7 | }, 8 | "dependencies": { 9 | "clean-css": "^5.3.3", 10 | "html-minifier-terser": "^7.2.0", 11 | "terser": "^5.37.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/setup.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykarelle/cursor-api/8afaf10eeabc055191a46a23b6872e2b9b640278/scripts/setup.ps1 -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置错误时退出 4 | set -e 5 | 6 | # 颜色输出 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | BLUE='\033[0;34m' 10 | NC='\033[0m' # No Color 11 | 12 | info() { 13 | echo -e "${BLUE}[INFO] $1${NC}" 14 | } 15 | 16 | error() { 17 | echo -e "${RED}[ERROR] $1${NC}" 18 | exit 1 19 | } 20 | 21 | # 检查是否为 root 用户(FreeBSD 和 Linux) 22 | if [ "$(uname)" != "Darwin" ] && [ "$EUID" -ne 0 ]; then 23 | error "请使用 root 权限运行此脚本 (sudo ./setup.sh)" 24 | fi 25 | 26 | # 检测包管理器 27 | if command -v brew &> /dev/null; then 28 | PKG_MANAGER="brew" 29 | info "检测到 macOS/Homebrew 系统" 30 | elif command -v pkg &> /dev/null; then 31 | PKG_MANAGER="pkg" 32 | info "检测到 FreeBSD 系统" 33 | elif command -v apt-get &> /dev/null; then 34 | PKG_MANAGER="apt-get" 35 | info "检测到 Debian/Ubuntu 系统" 36 | elif command -v dnf &> /dev/null; then 37 | PKG_MANAGER="dnf" 38 | info "检测到 Fedora/RHEL 系统" 39 | elif command -v yum &> /dev/null; then 40 | PKG_MANAGER="yum" 41 | info "检测到 CentOS 系统" 42 | else 43 | error "未检测到支持的包管理器" 44 | fi 45 | 46 | # 更新包管理器缓存 47 | info "更新包管理器缓存..." 48 | case $PKG_MANAGER in 49 | "brew") 50 | brew update 51 | ;; 52 | "pkg") 53 | pkg update 54 | ;; 55 | *) 56 | $PKG_MANAGER update -y 57 | ;; 58 | esac 59 | 60 | # 安装基础构建工具 61 | info "安装基础构建工具..." 62 | case $PKG_MANAGER in 63 | "brew") 64 | brew install \ 65 | protobuf \ 66 | pkg-config \ 67 | openssl \ 68 | curl \ 69 | git \ 70 | node 71 | ;; 72 | "pkg") 73 | pkg install -y \ 74 | gmake \ 75 | protobuf \ 76 | pkgconf \ 77 | openssl \ 78 | curl \ 79 | git \ 80 | node 81 | ;; 82 | "apt-get") 83 | $PKG_MANAGER install -y --no-install-recommends \ 84 | build-essential \ 85 | protobuf-compiler \ 86 | pkg-config \ 87 | libssl-dev \ 88 | ca-certificates \ 89 | curl \ 90 | tzdata \ 91 | git 92 | ;; 93 | *) 94 | $PKG_MANAGER install -y \ 95 | gcc \ 96 | gcc-c++ \ 97 | make \ 98 | protobuf-compiler \ 99 | pkg-config \ 100 | openssl-devel \ 101 | ca-certificates \ 102 | curl \ 103 | tzdata \ 104 | git 105 | ;; 106 | esac 107 | 108 | # 安装 Node.js 和 npm(如果还没有通过包管理器安装) 109 | if ! command -v node &> /dev/null && [ "$PKG_MANAGER" != "brew" ] && [ "$PKG_MANAGER" != "pkg" ]; then 110 | info "安装 Node.js 和 npm..." 111 | if [ "$PKG_MANAGER" = "apt-get" ]; then 112 | curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - 113 | $PKG_MANAGER install -y nodejs 114 | else 115 | curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - 116 | $PKG_MANAGER install -y nodejs 117 | fi 118 | fi 119 | 120 | # 安装 Rust(如果未安装) 121 | if ! command -v rustc &> /dev/null; then 122 | info "安装 Rust..." 123 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 124 | . "$HOME/.cargo/env" 125 | fi 126 | 127 | # 添加目标平台 128 | info "添加 Rust 目标平台..." 129 | case "$(uname)" in 130 | "FreeBSD") 131 | rustup target add x86_64-unknown-freebsd 132 | ;; 133 | "Darwin") 134 | rustup target add x86_64-apple-darwin aarch64-apple-darwin 135 | ;; 136 | *) 137 | rustup target add x86_64-unknown-linux-gnu 138 | ;; 139 | esac 140 | 141 | # 清理包管理器缓存 142 | case $PKG_MANAGER in 143 | "apt-get") 144 | rm -rf /var/lib/apt/lists/* 145 | ;; 146 | "pkg") 147 | pkg clean -y 148 | ;; 149 | esac 150 | 151 | # 设置时区(除了 macOS) 152 | if [ "$(uname)" != "Darwin" ]; then 153 | info "设置时区为 Asia/Shanghai..." 154 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 155 | fi 156 | 157 | echo -e "${GREEN}安装完成!${NC}" -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod constant; 3 | pub mod model; 4 | pub mod lazy; 5 | -------------------------------------------------------------------------------- /src/app/config.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | constant::AUTHORIZATION_BEARER_PREFIX, 3 | lazy::AUTH_TOKEN, 4 | model::AppConfig, 5 | }; 6 | use crate::common::models::{ 7 | config::{ConfigData, ConfigUpdateRequest}, 8 | ApiStatus, ErrorResponse, NormalResponse, 9 | }; 10 | use axum::{ 11 | http::{header::AUTHORIZATION, HeaderMap, StatusCode}, 12 | Json, 13 | }; 14 | 15 | // 定义处理更新操作的宏 16 | macro_rules! handle_update { 17 | ($request:expr, $field:ident, $update_fn:expr, $field_name:expr) => { 18 | if let Some($field) = $request.$field { 19 | if let Err(e) = $update_fn($field) { 20 | return Err(( 21 | StatusCode::INTERNAL_SERVER_ERROR, 22 | Json(ErrorResponse { 23 | status: ApiStatus::Failed, 24 | code: Some(500), 25 | error: Some(format!("更新 {} 失败: {}", $field_name, e)), 26 | message: None, 27 | }), 28 | )); 29 | } 30 | } 31 | }; 32 | } 33 | 34 | // 定义处理重置操作的宏 35 | macro_rules! handle_reset { 36 | ($request:expr, $field:ident, $reset_fn:expr, $field_name:expr) => { 37 | if $request.$field.is_some() { 38 | if let Err(e) = $reset_fn() { 39 | return Err(( 40 | StatusCode::INTERNAL_SERVER_ERROR, 41 | Json(ErrorResponse { 42 | status: ApiStatus::Failed, 43 | code: Some(500), 44 | error: Some(format!("重置 {} 失败: {}", $field_name, e)), 45 | message: None, 46 | }), 47 | )); 48 | } 49 | } 50 | }; 51 | } 52 | 53 | pub async fn handle_config_update( 54 | headers: HeaderMap, 55 | Json(request): Json, 56 | ) -> Result>, (StatusCode, Json)> { 57 | let auth_header = headers 58 | .get(AUTHORIZATION) 59 | .and_then(|h| h.to_str().ok()) 60 | .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) 61 | .ok_or(( 62 | StatusCode::UNAUTHORIZED, 63 | Json(ErrorResponse { 64 | status: ApiStatus::Failed, 65 | code: Some(401), 66 | error: Some("未提供认证令牌".to_string()), 67 | message: None, 68 | }), 69 | ))?; 70 | 71 | if auth_header != AUTH_TOKEN.as_str() { 72 | return Err(( 73 | StatusCode::UNAUTHORIZED, 74 | Json(ErrorResponse { 75 | status: ApiStatus::Failed, 76 | code: Some(401), 77 | error: Some("无效的认证令牌".to_string()), 78 | message: None, 79 | }), 80 | )); 81 | } 82 | 83 | match request.action.as_str() { 84 | "get" => Ok(Json(NormalResponse { 85 | status: ApiStatus::Success, 86 | data: Some(ConfigData { 87 | page_content: AppConfig::get_page_content(&request.path), 88 | enable_stream_check: AppConfig::get_stream_check(), 89 | include_stop_stream: AppConfig::get_stop_stream(), 90 | vision_ability: AppConfig::get_vision_ability(), 91 | enable_slow_pool: AppConfig::get_slow_pool(), 92 | enable_all_claude: AppConfig::get_allow_claude(), 93 | check_usage_models: AppConfig::get_usage_check(), 94 | }), 95 | message: None, 96 | })), 97 | 98 | "update" => { 99 | // 处理页面内容更新 100 | if !request.path.is_empty() && request.content.is_some() { 101 | let content = request.content.unwrap(); 102 | if let Err(e) = AppConfig::update_page_content(&request.path, content) { 103 | return Err(( 104 | StatusCode::INTERNAL_SERVER_ERROR, 105 | Json(ErrorResponse { 106 | status: ApiStatus::Failed, 107 | code: Some(500), 108 | error: Some(format!("更新页面内容失败: {}", e)), 109 | message: None, 110 | }), 111 | )); 112 | } 113 | } 114 | 115 | handle_update!( 116 | request, 117 | enable_stream_check, 118 | AppConfig::update_stream_check, 119 | "enable_stream_check" 120 | ); 121 | handle_update!( 122 | request, 123 | include_stop_stream, 124 | AppConfig::update_stop_stream, 125 | "include_stop_stream" 126 | ); 127 | handle_update!( 128 | request, 129 | vision_ability, 130 | AppConfig::update_vision_ability, 131 | "vision_ability" 132 | ); 133 | handle_update!( 134 | request, 135 | enable_slow_pool, 136 | AppConfig::update_slow_pool, 137 | "enable_slow_pool" 138 | ); 139 | handle_update!( 140 | request, 141 | enable_all_claude, 142 | AppConfig::update_allow_claude, 143 | "enable_all_claude" 144 | ); 145 | handle_update!( 146 | request, 147 | check_usage_models, 148 | AppConfig::update_usage_check, 149 | "check_usage_models" 150 | ); 151 | 152 | Ok(Json(NormalResponse { 153 | status: ApiStatus::Success, 154 | data: None, 155 | message: Some("配置已更新".to_string()), 156 | })) 157 | } 158 | 159 | "reset" => { 160 | // 重置页面内容 161 | if !request.path.is_empty() { 162 | if let Err(e) = AppConfig::reset_page_content(&request.path) { 163 | return Err(( 164 | StatusCode::INTERNAL_SERVER_ERROR, 165 | Json(ErrorResponse { 166 | status: ApiStatus::Failed, 167 | code: Some(500), 168 | error: Some(format!("重置页面内容失败: {}", e)), 169 | message: None, 170 | }), 171 | )); 172 | } 173 | } 174 | 175 | handle_reset!( 176 | request, 177 | enable_stream_check, 178 | AppConfig::reset_stream_check, 179 | "enable_stream_check" 180 | ); 181 | handle_reset!( 182 | request, 183 | include_stop_stream, 184 | AppConfig::reset_stop_stream, 185 | "include_stop_stream" 186 | ); 187 | handle_reset!( 188 | request, 189 | vision_ability, 190 | AppConfig::reset_vision_ability, 191 | "vision_ability" 192 | ); 193 | handle_reset!( 194 | request, 195 | enable_slow_pool, 196 | AppConfig::reset_slow_pool, 197 | "enable_slow_pool" 198 | ); 199 | handle_reset!( 200 | request, 201 | enable_all_claude, 202 | AppConfig::reset_allow_claude, 203 | "enable_all_claude" 204 | ); 205 | handle_reset!( 206 | request, 207 | check_usage_models, 208 | AppConfig::reset_usage_check, 209 | "check_usage_models" 210 | ); 211 | 212 | Ok(Json(NormalResponse { 213 | status: ApiStatus::Success, 214 | data: None, 215 | message: Some("配置已重置".to_string()), 216 | })) 217 | } 218 | 219 | _ => Err(( 220 | StatusCode::BAD_REQUEST, 221 | Json(ErrorResponse { 222 | status: ApiStatus::Failed, 223 | code: Some(400), 224 | error: Some("无效的操作类型".to_string()), 225 | message: None, 226 | }), 227 | )), 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/app/constant.rs: -------------------------------------------------------------------------------- 1 | macro_rules! def_pub_const { 2 | ($name:ident, $value:expr) => { 3 | pub const $name: &'static str = $value; 4 | }; 5 | } 6 | 7 | def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION")); 8 | // def_pub_const!(PKG_NAME, env!("CARGO_PKG_NAME")); 9 | // def_pub_const!(PKG_DESCRIPTION, env!("CARGO_PKG_DESCRIPTION")); 10 | // def_pub_const!(PKG_AUTHORS, env!("CARGO_PKG_AUTHORS")); 11 | // def_pub_const!(PKG_REPOSITORY, env!("CARGO_PKG_REPOSITORY")); 12 | 13 | def_pub_const!(EMPTY_STRING, ""); 14 | 15 | def_pub_const!(ROUTE_ROOT_PATH, "/"); 16 | def_pub_const!(ROUTE_HEALTH_PATH, "/health"); 17 | def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum"); 18 | def_pub_const!(ROUTE_GET_USER_INFO_PATH, "/get-userinfo"); 19 | def_pub_const!(ROUTE_API_PATH, "/api"); 20 | def_pub_const!(ROUTE_LOGS_PATH, "/logs"); 21 | def_pub_const!(ROUTE_CONFIG_PATH, "/config"); 22 | def_pub_const!(ROUTE_TOKENINFO_PATH, "/tokeninfo"); 23 | def_pub_const!(ROUTE_GET_TOKENINFO_PATH, "/get-tokeninfo"); 24 | def_pub_const!(ROUTE_UPDATE_TOKENINFO_PATH, "/update-tokeninfo"); 25 | def_pub_const!(ROUTE_ENV_EXAMPLE_PATH, "/env-example"); 26 | def_pub_const!(ROUTE_STATIC_PATH, "/static/:path"); 27 | def_pub_const!(ROUTE_SHARED_STYLES_PATH, "/static/shared-styles.css"); 28 | def_pub_const!(ROUTE_SHARED_JS_PATH, "/static/shared.js"); 29 | def_pub_const!(ROUTE_ABOUT_PATH, "/about"); 30 | def_pub_const!(ROUTE_README_PATH, "/readme"); 31 | def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration"); 32 | 33 | def_pub_const!(DEFAULT_TOKEN_FILE_NAME, ".token"); 34 | def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".token-list"); 35 | 36 | def_pub_const!(STATUS_PENDING, "pending"); 37 | def_pub_const!(STATUS_SUCCESS, "success"); 38 | def_pub_const!(STATUS_FAILED, "failed"); 39 | 40 | def_pub_const!(HEADER_NAME_GHOST_MODE, "x-ghost-mode"); 41 | 42 | def_pub_const!(TRUE, "true"); 43 | def_pub_const!(FALSE, "false"); 44 | 45 | // def_pub_const!(CONTENT_TYPE_PROTO, "application/proto"); 46 | def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto"); 47 | def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8"); 48 | def_pub_const!(CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, "text/plain;charset=utf-8"); 49 | def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8"); 50 | def_pub_const!(CONTENT_TYPE_TEXT_JS_WITH_UTF8, "text/javascript;charset=utf-8"); 51 | 52 | def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer "); 53 | 54 | def_pub_const!(CURSOR_API2_HOST, "api2.cursor.sh"); 55 | def_pub_const!(CURSOR_HOST, "www.cursor.com"); 56 | def_pub_const!(CURSOR_SETTINGS_URL, "https://www.cursor.com/settings"); 57 | 58 | def_pub_const!(OBJECT_CHAT_COMPLETION, "chat.completion"); 59 | def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk"); 60 | 61 | // def_pub_const!(CURSOR_API2_STREAM_CHAT, "StreamChat"); 62 | // def_pub_const!(CURSOR_API2_GET_USER_INFO, "GetUserInfo"); 63 | 64 | def_pub_const!(FINISH_REASON_STOP, "stop"); 65 | 66 | def_pub_const!(ERR_UPDATE_CONFIG, "无法更新配置"); 67 | def_pub_const!(ERR_RESET_CONFIG, "无法重置配置"); 68 | def_pub_const!(ERR_INVALID_PATH, "无效的路径"); 69 | 70 | // def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good"); 71 | -------------------------------------------------------------------------------- /src/app/lazy.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::constant::{ 3 | CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME, 4 | EMPTY_STRING, 5 | }, 6 | common::utils::parse_string_from_env, 7 | }; 8 | use std::sync::LazyLock; 9 | 10 | macro_rules! def_pub_static { 11 | // 基础版本:直接存储 String 12 | ($name:ident, $value:expr) => { 13 | pub static $name: LazyLock = LazyLock::new(|| $value); 14 | }; 15 | 16 | // 环境变量版本 17 | ($name:ident, env: $env_key:expr, default: $default:expr) => { 18 | pub static $name: LazyLock = 19 | LazyLock::new(|| parse_string_from_env($env_key, $default).trim().to_string()); 20 | }; 21 | } 22 | 23 | // macro_rules! def_pub_static_getter { 24 | // ($name:ident) => { 25 | // paste::paste! { 26 | // pub fn []() -> String { 27 | // (*$name).clone() 28 | // } 29 | // } 30 | // }; 31 | // } 32 | 33 | def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING); 34 | def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING); 35 | def_pub_static!(TOKEN_FILE, env: "TOKEN_FILE", default: DEFAULT_TOKEN_FILE_NAME); 36 | def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME); 37 | def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX)); 38 | def_pub_static!( 39 | ROUTE_CHAT_PATH, 40 | format!("{}/v1/chat/completions", *ROUTE_PREFIX) 41 | ); 42 | 43 | pub static START_TIME: LazyLock> = 44 | LazyLock::new(chrono::Local::now); 45 | 46 | pub fn get_start_time() -> chrono::DateTime { 47 | *START_TIME 48 | } 49 | 50 | def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Respond in Chinese by default"); 51 | 52 | def_pub_static!(REVERSE_PROXY_HOST, env: "REVERSE_PROXY_HOST", default: ""); 53 | 54 | pub static USE_PROXY: LazyLock = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty()); 55 | 56 | pub static CURSOR_API2_CHAT_URL: LazyLock = LazyLock::new(|| { 57 | let host = if *USE_PROXY { 58 | &*REVERSE_PROXY_HOST 59 | } else { 60 | CURSOR_API2_HOST 61 | }; 62 | format!("https://{}/aiserver.v1.AiService/StreamChat", host) 63 | }); 64 | 65 | pub static CURSOR_API2_STRIPE_URL: LazyLock = LazyLock::new(|| { 66 | let host = if *USE_PROXY { 67 | &*REVERSE_PROXY_HOST 68 | } else { 69 | CURSOR_API2_HOST 70 | }; 71 | format!("https://{}/auth/full_stripe_profile", host) 72 | }); 73 | 74 | pub static CURSOR_USAGE_API_URL: LazyLock = LazyLock::new(|| { 75 | let host = if *USE_PROXY { 76 | &*REVERSE_PROXY_HOST 77 | } else { 78 | CURSOR_HOST 79 | }; 80 | format!("https://{}/api/usage", host) 81 | }); 82 | 83 | pub static CURSOR_USER_API_URL: LazyLock = LazyLock::new(|| { 84 | let host = if *USE_PROXY { 85 | &*REVERSE_PROXY_HOST 86 | } else { 87 | CURSOR_HOST 88 | }; 89 | format!("https://{}/api/auth/me", host) 90 | }); 91 | 92 | // pub static DEBUG: LazyLock = LazyLock::new(|| parse_bool_from_env("DEBUG", false)); 93 | 94 | // #[macro_export] 95 | // macro_rules! debug_println { 96 | // ($($arg:tt)*) => { 97 | // if *crate::app::statics::DEBUG { 98 | // println!($($arg)*); 99 | // } 100 | // }; 101 | // } 102 | -------------------------------------------------------------------------------- /src/app/model.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::constant::{ 3 | ERR_INVALID_PATH, ERR_RESET_CONFIG, ERR_UPDATE_CONFIG, ROUTE_ABOUT_PATH, ROUTE_CONFIG_PATH, 4 | ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_SHARED_JS_PATH, 5 | ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENINFO_PATH, ROUTE_API_PATH, 6 | }, 7 | common::models::userinfo::TokenProfile, 8 | }; 9 | use crate::chat::model::Message; 10 | use std::sync::{LazyLock, RwLock}; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | // 页面内容类型枚举 14 | #[derive(Clone, Serialize, Deserialize)] 15 | #[serde(tag = "type", content = "content")] 16 | pub enum PageContent { 17 | #[serde(rename = "default")] 18 | Default, // 默认行为 19 | #[serde(rename = "text")] 20 | Text(String), // 纯文本 21 | #[serde(rename = "html")] 22 | Html(String), // HTML 内容 23 | } 24 | 25 | impl Default for PageContent { 26 | fn default() -> Self { 27 | Self::Default 28 | } 29 | } 30 | 31 | mod usage_check; 32 | pub use usage_check::UsageCheck; 33 | 34 | // 静态配置 35 | #[derive(Clone)] 36 | pub struct AppConfig { 37 | stream_check: bool, 38 | stop_stream: bool, 39 | vision_ability: VisionAbility, 40 | slow_pool: bool, 41 | allow_claude: bool, 42 | pages: Pages, 43 | usage_check: UsageCheck, 44 | } 45 | 46 | #[derive(Serialize, Deserialize, Clone)] 47 | pub enum VisionAbility { 48 | #[serde(rename = "none", alias = "disabled")] 49 | None, 50 | #[serde(rename = "base64", alias = "base64-only")] 51 | Base64, 52 | #[serde(rename = "all", alias = "base64-http")] 53 | All, 54 | } 55 | 56 | impl VisionAbility { 57 | pub fn from_str(s: &str) -> Self { 58 | match s.to_lowercase().as_str() { 59 | "none" | "disabled" => Self::None, 60 | "base64" | "base64-only" => Self::Base64, 61 | "all" | "base64-http" => Self::All, 62 | _ => Self::default(), 63 | } 64 | } 65 | } 66 | 67 | impl Default for VisionAbility { 68 | fn default() -> Self { 69 | Self::Base64 70 | } 71 | } 72 | 73 | #[derive(Clone, Default)] 74 | pub struct Pages { 75 | pub root_content: PageContent, 76 | pub logs_content: PageContent, 77 | pub config_content: PageContent, 78 | pub tokeninfo_content: PageContent, 79 | pub shared_styles_content: PageContent, 80 | pub shared_js_content: PageContent, 81 | pub about_content: PageContent, 82 | pub readme_content: PageContent, 83 | pub api_content: PageContent, 84 | } 85 | 86 | // 运行时状态 87 | pub struct AppState { 88 | pub total_requests: u64, 89 | pub active_requests: u64, 90 | pub error_requests: u64, 91 | pub request_logs: Vec, 92 | pub token_infos: Vec, 93 | } 94 | 95 | // 全局配置实例 96 | pub static APP_CONFIG: LazyLock> = LazyLock::new(|| { 97 | RwLock::new(AppConfig::default()) 98 | }); 99 | 100 | impl Default for AppConfig { 101 | fn default() -> Self { 102 | Self { 103 | stream_check: true, 104 | stop_stream: true, 105 | vision_ability: VisionAbility::Base64, 106 | slow_pool: false, 107 | allow_claude: false, 108 | pages: Pages::default(), 109 | usage_check: UsageCheck::Default, 110 | } 111 | } 112 | } 113 | 114 | macro_rules! config_methods { 115 | ($($field:ident: $type:ty, $default:expr;)*) => { 116 | $( 117 | paste::paste! { 118 | pub fn []() -> $type { 119 | APP_CONFIG 120 | .read() 121 | .map(|config| config.$field.clone()) 122 | .unwrap_or($default) 123 | } 124 | 125 | pub fn [](value: $type) -> Result<(), &'static str> { 126 | if let Ok(mut config) = APP_CONFIG.write() { 127 | config.$field = value; 128 | Ok(()) 129 | } else { 130 | Err(ERR_UPDATE_CONFIG) 131 | } 132 | } 133 | 134 | pub fn []() -> Result<(), &'static str> { 135 | if let Ok(mut config) = APP_CONFIG.write() { 136 | config.$field = $default; 137 | Ok(()) 138 | } else { 139 | Err(ERR_RESET_CONFIG) 140 | } 141 | } 142 | } 143 | )* 144 | }; 145 | } 146 | 147 | impl AppConfig { 148 | pub fn init( 149 | stream_check: bool, 150 | stop_stream: bool, 151 | vision_ability: VisionAbility, 152 | slow_pool: bool, 153 | allow_claude: bool, 154 | ) { 155 | if let Ok(mut config) = APP_CONFIG.write() { 156 | config.stream_check = stream_check; 157 | config.stop_stream = stop_stream; 158 | config.vision_ability = vision_ability; 159 | config.slow_pool = slow_pool; 160 | config.allow_claude = allow_claude; 161 | } 162 | } 163 | 164 | config_methods! { 165 | stream_check: bool, true; 166 | stop_stream: bool, true; 167 | slow_pool: bool, false; 168 | allow_claude: bool, false; 169 | } 170 | 171 | pub fn get_vision_ability() -> VisionAbility { 172 | APP_CONFIG 173 | .read() 174 | .map(|config| config.vision_ability.clone()) 175 | .unwrap_or_default() 176 | } 177 | 178 | pub fn get_page_content(path: &str) -> Option { 179 | APP_CONFIG.read().ok().map(|config| match path { 180 | ROUTE_ROOT_PATH => config.pages.root_content.clone(), 181 | ROUTE_LOGS_PATH => config.pages.logs_content.clone(), 182 | ROUTE_CONFIG_PATH => config.pages.config_content.clone(), 183 | ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content.clone(), 184 | ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content.clone(), 185 | ROUTE_SHARED_JS_PATH => config.pages.shared_js_content.clone(), 186 | ROUTE_ABOUT_PATH => config.pages.about_content.clone(), 187 | ROUTE_README_PATH => config.pages.readme_content.clone(), 188 | ROUTE_API_PATH => config.pages.api_content.clone(), 189 | _ => PageContent::default(), 190 | }) 191 | } 192 | 193 | pub fn get_usage_check() -> UsageCheck { 194 | APP_CONFIG 195 | .read() 196 | .map(|config| config.usage_check.clone()) 197 | .unwrap_or_default() 198 | } 199 | 200 | pub fn update_vision_ability(new_ability: VisionAbility) -> Result<(), &'static str> { 201 | if let Ok(mut config) = APP_CONFIG.write() { 202 | config.vision_ability = new_ability; 203 | Ok(()) 204 | } else { 205 | Err(ERR_UPDATE_CONFIG) 206 | } 207 | } 208 | 209 | pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> { 210 | if let Ok(mut config) = APP_CONFIG.write() { 211 | match path { 212 | ROUTE_ROOT_PATH => config.pages.root_content = content, 213 | ROUTE_LOGS_PATH => config.pages.logs_content = content, 214 | ROUTE_CONFIG_PATH => config.pages.config_content = content, 215 | ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content = content, 216 | ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content, 217 | ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content, 218 | ROUTE_ABOUT_PATH => config.pages.about_content = content, 219 | ROUTE_README_PATH => config.pages.readme_content = content, 220 | ROUTE_API_PATH => config.pages.api_content = content, 221 | _ => return Err(ERR_INVALID_PATH), 222 | } 223 | Ok(()) 224 | } else { 225 | Err(ERR_UPDATE_CONFIG) 226 | } 227 | } 228 | 229 | pub fn update_usage_check(rule: UsageCheck) -> Result<(), &'static str> { 230 | if let Ok(mut config) = APP_CONFIG.write() { 231 | config.usage_check = rule; 232 | Ok(()) 233 | } else { 234 | Err(ERR_UPDATE_CONFIG) 235 | } 236 | } 237 | 238 | pub fn reset_vision_ability() -> Result<(), &'static str> { 239 | if let Ok(mut config) = APP_CONFIG.write() { 240 | config.vision_ability = VisionAbility::Base64; 241 | Ok(()) 242 | } else { 243 | Err(ERR_RESET_CONFIG) 244 | } 245 | } 246 | 247 | pub fn reset_page_content(path: &str) -> Result<(), &'static str> { 248 | if let Ok(mut config) = APP_CONFIG.write() { 249 | match path { 250 | ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(), 251 | ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(), 252 | ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(), 253 | ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content = PageContent::default(), 254 | ROUTE_SHARED_STYLES_PATH => { 255 | config.pages.shared_styles_content = PageContent::default() 256 | } 257 | ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(), 258 | ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(), 259 | ROUTE_README_PATH => config.pages.readme_content = PageContent::default(), 260 | ROUTE_API_PATH => config.pages.api_content = PageContent::default(), 261 | _ => return Err(ERR_INVALID_PATH), 262 | } 263 | Ok(()) 264 | } else { 265 | Err(ERR_RESET_CONFIG) 266 | } 267 | } 268 | 269 | pub fn reset_usage_check() -> Result<(), &'static str> { 270 | if let Ok(mut config) = APP_CONFIG.write() { 271 | config.usage_check = UsageCheck::default(); 272 | Ok(()) 273 | } else { 274 | Err(ERR_RESET_CONFIG) 275 | } 276 | } 277 | } 278 | 279 | impl AppState { 280 | pub fn new(token_infos: Vec) -> Self { 281 | Self { 282 | total_requests: 0, 283 | active_requests: 0, 284 | error_requests: 0, 285 | request_logs: Vec::new(), 286 | token_infos, 287 | } 288 | } 289 | } 290 | 291 | // 请求日志 292 | #[derive(Serialize, Clone)] 293 | pub struct RequestLog { 294 | pub id: u64, 295 | pub timestamp: chrono::DateTime, 296 | pub model: String, 297 | pub token_info: TokenInfo, 298 | #[serde(skip_serializing_if = "Option::is_none")] 299 | pub prompt: Option, 300 | pub timing: TimingInfo, 301 | pub stream: bool, 302 | pub status: &'static str, 303 | #[serde(skip_serializing_if = "Option::is_none")] 304 | pub error: Option, 305 | } 306 | 307 | #[derive(Serialize, Clone)] 308 | pub struct TimingInfo { 309 | pub total: f64, // 总用时(秒) 310 | #[serde(skip_serializing_if = "Option::is_none")] 311 | pub first: Option, // 首字时间(秒) 312 | } 313 | 314 | // 聊天请求 315 | #[derive(Deserialize)] 316 | pub struct ChatRequest { 317 | pub model: String, 318 | pub messages: Vec, 319 | #[serde(default)] 320 | pub stream: bool, 321 | } 322 | 323 | // 用于存储 token 信息 324 | #[derive(Serialize, Clone)] 325 | pub struct TokenInfo { 326 | pub token: String, 327 | pub checksum: String, 328 | #[serde(skip_serializing_if = "Option::is_none")] 329 | pub profile: Option, 330 | } 331 | 332 | // TokenUpdateRequest 结构体 333 | #[derive(Deserialize)] 334 | pub struct TokenUpdateRequest { 335 | pub tokens: String, 336 | #[serde(default)] 337 | pub token_list: Option, 338 | } 339 | -------------------------------------------------------------------------------- /src/app/model/usage_check.rs: -------------------------------------------------------------------------------- 1 | use crate::chat::constant::AVAILABLE_MODELS; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone)] 5 | pub enum UsageCheck { 6 | None, 7 | Default, 8 | All, 9 | Custom(Vec<&'static str>), 10 | } 11 | 12 | impl Default for UsageCheck { 13 | fn default() -> Self { 14 | Self::Default 15 | } 16 | } 17 | 18 | impl Serialize for UsageCheck { 19 | fn serialize(&self, serializer: S) -> Result 20 | where 21 | S: serde::Serializer, 22 | { 23 | use serde::ser::SerializeStruct; 24 | let mut state = serializer.serialize_struct("UsageCheck", 1)?; 25 | match self { 26 | UsageCheck::None => { 27 | state.serialize_field("type", "none")?; 28 | } 29 | UsageCheck::Default => { 30 | state.serialize_field("type", "default")?; 31 | } 32 | UsageCheck::All => { 33 | state.serialize_field("type", "all")?; 34 | } 35 | UsageCheck::Custom(models) => { 36 | state.serialize_field("type", "list")?; 37 | state.serialize_field("content", &models.join(","))?; 38 | } 39 | } 40 | state.end() 41 | } 42 | } 43 | 44 | impl<'de> Deserialize<'de> for UsageCheck { 45 | fn deserialize(deserializer: D) -> Result 46 | where 47 | D: serde::Deserializer<'de>, 48 | { 49 | #[derive(Deserialize)] 50 | #[serde(tag = "type", content = "content")] 51 | enum UsageCheckHelper { 52 | #[serde(rename = "none")] 53 | None, 54 | #[serde(rename = "default")] 55 | Default, 56 | #[serde(rename = "all")] 57 | All, 58 | #[serde(rename = "list")] 59 | Custom(String), 60 | } 61 | 62 | let helper = UsageCheckHelper::deserialize(deserializer)?; 63 | Ok(match helper { 64 | UsageCheckHelper::None => UsageCheck::None, 65 | UsageCheckHelper::Default => UsageCheck::Default, 66 | UsageCheckHelper::All => UsageCheck::All, 67 | UsageCheckHelper::Custom(list) => { 68 | if list.is_empty() { 69 | return Ok(UsageCheck::None); 70 | } 71 | 72 | let models: Vec<&'static str> = list 73 | .split(',') 74 | .filter_map(|model| { 75 | let model = model.trim(); 76 | AVAILABLE_MODELS 77 | .iter() 78 | .find(|m| m.id == model) 79 | .map(|m| m.id) 80 | }) 81 | .collect(); 82 | 83 | if models.is_empty() { 84 | UsageCheck::None 85 | } else { 86 | UsageCheck::Custom(models) 87 | } 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/chat.rs: -------------------------------------------------------------------------------- 1 | pub mod adapter; 2 | pub mod aiserver; 3 | pub mod constant; 4 | pub mod error; 5 | pub mod model; 6 | pub mod route; 7 | pub mod service; 8 | pub mod stream; 9 | -------------------------------------------------------------------------------- /src/chat/aiserver.rs: -------------------------------------------------------------------------------- 1 | pub mod v1; 2 | -------------------------------------------------------------------------------- /src/chat/aiserver/v1.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs")); 2 | -------------------------------------------------------------------------------- /src/chat/constant.rs: -------------------------------------------------------------------------------- 1 | use super::model::Model; 2 | 3 | macro_rules! def_pub_const { 4 | ($name:ident, $value:expr) => { 5 | pub const $name: &'static str = $value; 6 | }; 7 | } 8 | def_pub_const!(ERR_UNSUPPORTED_GIF, "不支持动态 GIF"); 9 | def_pub_const!(ERR_UNSUPPORTED_IMAGE_FORMAT, "不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF"); 10 | def_pub_const!(ERR_NODATA, "No data"); 11 | 12 | const MODEL_OBJECT: &str = "model"; 13 | const CREATED: &i64 = &1706659200; 14 | 15 | def_pub_const!(ANTHROPIC, "anthropic"); 16 | def_pub_const!(CURSOR, "cursor"); 17 | def_pub_const!(GOOGLE, "google"); 18 | def_pub_const!(OPENAI, "openai"); 19 | 20 | def_pub_const!(CLAUDE_3_5_SONNET, "claude-3.5-sonnet"); 21 | def_pub_const!(GPT_4, "gpt-4"); 22 | def_pub_const!(GPT_4O, "gpt-4o"); 23 | def_pub_const!(CLAUDE_3_OPUS, "claude-3-opus"); 24 | def_pub_const!(CURSOR_FAST, "cursor-fast"); 25 | def_pub_const!(CURSOR_SMALL, "cursor-small"); 26 | def_pub_const!(GPT_3_5_TURBO, "gpt-3.5-turbo"); 27 | def_pub_const!(GPT_4_TURBO_2024_04_09, "gpt-4-turbo-2024-04-09"); 28 | def_pub_const!(GPT_4O_128K, "gpt-4o-128k"); 29 | def_pub_const!(GEMINI_1_5_FLASH_500K, "gemini-1.5-flash-500k"); 30 | def_pub_const!(CLAUDE_3_HAIKU_200K, "claude-3-haiku-200k"); 31 | def_pub_const!(CLAUDE_3_5_SONNET_200K, "claude-3-5-sonnet-200k"); 32 | def_pub_const!(CLAUDE_3_5_SONNET_20241022, "claude-3-5-sonnet-20241022"); 33 | def_pub_const!(GPT_4O_MINI, "gpt-4o-mini"); 34 | def_pub_const!(O1_MINI, "o1-mini"); 35 | def_pub_const!(O1_PREVIEW, "o1-preview"); 36 | def_pub_const!(O1, "o1"); 37 | def_pub_const!(CLAUDE_3_5_HAIKU, "claude-3.5-haiku"); 38 | def_pub_const!(GEMINI_EXP_1206, "gemini-exp-1206"); 39 | def_pub_const!( 40 | GEMINI_2_0_FLASH_THINKING_EXP, 41 | "gemini-2.0-flash-thinking-exp" 42 | ); 43 | def_pub_const!(GEMINI_2_0_FLASH_EXP, "gemini-2.0-flash-exp"); 44 | 45 | pub const AVAILABLE_MODELS: [Model; 21] = [ 46 | Model { 47 | id: CLAUDE_3_5_SONNET, 48 | created: CREATED, 49 | object: MODEL_OBJECT, 50 | owned_by: ANTHROPIC, 51 | }, 52 | Model { 53 | id: GPT_4, 54 | created: CREATED, 55 | object: MODEL_OBJECT, 56 | owned_by: OPENAI, 57 | }, 58 | Model { 59 | id: GPT_4O, 60 | created: CREATED, 61 | object: MODEL_OBJECT, 62 | owned_by: OPENAI, 63 | }, 64 | Model { 65 | id: CLAUDE_3_OPUS, 66 | created: CREATED, 67 | object: MODEL_OBJECT, 68 | owned_by: ANTHROPIC, 69 | }, 70 | Model { 71 | id: CURSOR_FAST, 72 | created: CREATED, 73 | object: MODEL_OBJECT, 74 | owned_by: CURSOR, 75 | }, 76 | Model { 77 | id: CURSOR_SMALL, 78 | created: CREATED, 79 | object: MODEL_OBJECT, 80 | owned_by: CURSOR, 81 | }, 82 | Model { 83 | id: GPT_3_5_TURBO, 84 | created: CREATED, 85 | object: MODEL_OBJECT, 86 | owned_by: OPENAI, 87 | }, 88 | Model { 89 | id: GPT_4_TURBO_2024_04_09, 90 | created: CREATED, 91 | object: MODEL_OBJECT, 92 | owned_by: OPENAI, 93 | }, 94 | Model { 95 | id: GPT_4O_128K, 96 | created: CREATED, 97 | object: MODEL_OBJECT, 98 | owned_by: OPENAI, 99 | }, 100 | Model { 101 | id: GEMINI_1_5_FLASH_500K, 102 | created: CREATED, 103 | object: MODEL_OBJECT, 104 | owned_by: GOOGLE, 105 | }, 106 | Model { 107 | id: CLAUDE_3_HAIKU_200K, 108 | created: CREATED, 109 | object: MODEL_OBJECT, 110 | owned_by: ANTHROPIC, 111 | }, 112 | Model { 113 | id: CLAUDE_3_5_SONNET_200K, 114 | created: CREATED, 115 | object: MODEL_OBJECT, 116 | owned_by: ANTHROPIC, 117 | }, 118 | Model { 119 | id: CLAUDE_3_5_SONNET_20241022, 120 | created: CREATED, 121 | object: MODEL_OBJECT, 122 | owned_by: ANTHROPIC, 123 | }, 124 | Model { 125 | id: GPT_4O_MINI, 126 | created: CREATED, 127 | object: MODEL_OBJECT, 128 | owned_by: OPENAI, 129 | }, 130 | Model { 131 | id: O1_MINI, 132 | created: CREATED, 133 | object: MODEL_OBJECT, 134 | owned_by: OPENAI, 135 | }, 136 | Model { 137 | id: O1_PREVIEW, 138 | created: CREATED, 139 | object: MODEL_OBJECT, 140 | owned_by: OPENAI, 141 | }, 142 | Model { 143 | id: O1, 144 | created: CREATED, 145 | object: MODEL_OBJECT, 146 | owned_by: OPENAI, 147 | }, 148 | Model { 149 | id: CLAUDE_3_5_HAIKU, 150 | created: CREATED, 151 | object: MODEL_OBJECT, 152 | owned_by: ANTHROPIC, 153 | }, 154 | Model { 155 | id: GEMINI_EXP_1206, 156 | created: CREATED, 157 | object: MODEL_OBJECT, 158 | owned_by: GOOGLE, 159 | }, 160 | Model { 161 | id: GEMINI_2_0_FLASH_THINKING_EXP, 162 | created: CREATED, 163 | object: MODEL_OBJECT, 164 | owned_by: GOOGLE, 165 | }, 166 | Model { 167 | id: GEMINI_2_0_FLASH_EXP, 168 | created: CREATED, 169 | object: MODEL_OBJECT, 170 | owned_by: GOOGLE, 171 | }, 172 | ]; 173 | 174 | pub const USAGE_CHECK_MODELS: [&str; 11] = [ 175 | CLAUDE_3_5_SONNET_20241022, 176 | CLAUDE_3_5_SONNET, 177 | GEMINI_EXP_1206, 178 | GPT_4, 179 | GPT_4_TURBO_2024_04_09, 180 | GPT_4O, 181 | CLAUDE_3_5_HAIKU, 182 | GPT_4O_128K, 183 | GEMINI_1_5_FLASH_500K, 184 | CLAUDE_3_HAIKU_200K, 185 | CLAUDE_3_5_SONNET_200K, 186 | ]; 187 | 188 | pub const LONG_CONTEXT_MODELS: [&str; 4] = [ 189 | GPT_4O_128K, 190 | GEMINI_1_5_FLASH_500K, 191 | CLAUDE_3_HAIKU_200K, 192 | CLAUDE_3_5_SONNET_200K, 193 | ]; 194 | -------------------------------------------------------------------------------- /src/chat/error.rs: -------------------------------------------------------------------------------- 1 | use super::aiserver::v1::error_details::Error as ErrorType; 2 | use reqwest::StatusCode; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Deserialize)] 6 | pub struct ChatError { 7 | error: ErrorBody, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | pub struct ErrorBody { 12 | code: String, 13 | // message: String, always: Error 14 | details: Vec, 15 | } 16 | 17 | #[derive(Deserialize)] 18 | pub struct ErrorDetail { 19 | // #[serde(rename = "type")] 20 | // error_type: String, always: aiserver.v1.ErrorDetails 21 | debug: ErrorDebug, 22 | value: String, 23 | } 24 | 25 | #[derive(Deserialize)] 26 | pub struct ErrorDebug { 27 | error: String, 28 | details: ErrorDetails, 29 | // #[serde(rename = "isExpected")] 30 | // is_expected: Option, 31 | } 32 | 33 | #[derive(Deserialize)] 34 | pub struct ErrorDetails { 35 | title: String, 36 | detail: String, 37 | // #[serde(rename = "isRetryable")] 38 | // is_retryable: Option, 39 | } 40 | 41 | use crate::common::models::{ApiStatus, ErrorResponse as CommonErrorResponse}; 42 | 43 | impl ChatError { 44 | pub fn to_error_response(&self) -> ErrorResponse { 45 | if self.error.details.is_empty() { 46 | return ErrorResponse { 47 | status: 500, 48 | code: "unknown".to_string(), 49 | error: None, 50 | }; 51 | } 52 | ErrorResponse { 53 | status: self.status_code(), 54 | code: self.error.code.clone(), 55 | error: Some(Error { 56 | message: self.error.details[0].debug.details.title.clone(), 57 | details: self.error.details[0].debug.details.detail.clone(), 58 | value: self.error.details[0].value.clone(), 59 | }), 60 | } 61 | } 62 | 63 | pub fn status_code(&self) -> u16 { 64 | match ErrorType::from_str_name(&self.error.details[0].debug.error) { 65 | Some(error) => match error { 66 | ErrorType::Unspecified => 500, 67 | ErrorType::BadApiKey 68 | | ErrorType::InvalidAuthId 69 | | ErrorType::AuthTokenNotFound 70 | | ErrorType::AuthTokenExpired 71 | | ErrorType::Unauthorized => 401, 72 | ErrorType::NotLoggedIn 73 | | ErrorType::NotHighEnoughPermissions 74 | | ErrorType::AgentRequiresLogin 75 | | ErrorType::ProUserOnly 76 | | ErrorType::TaskNoPermissions => 403, 77 | ErrorType::NotFound 78 | | ErrorType::UserNotFound 79 | | ErrorType::TaskUuidNotFound 80 | | ErrorType::AgentEngineNotFound 81 | | ErrorType::GitgraphNotFound 82 | | ErrorType::FileNotFound => 404, 83 | ErrorType::FreeUserRateLimitExceeded 84 | | ErrorType::ProUserRateLimitExceeded 85 | | ErrorType::OpenaiRateLimitExceeded 86 | | ErrorType::OpenaiAccountLimitExceeded 87 | | ErrorType::GenericRateLimitExceeded 88 | | ErrorType::Gpt4VisionPreviewRateLimit 89 | | ErrorType::ApiKeyRateLimit => 429, 90 | ErrorType::BadRequest 91 | | ErrorType::BadModelName 92 | | ErrorType::SlashEditFileTooLong 93 | | ErrorType::FileUnsupported 94 | | ErrorType::ClaudeImageTooLarge => 400, 95 | ErrorType::Deprecated 96 | | ErrorType::FreeUserUsageLimit 97 | | ErrorType::ProUserUsageLimit 98 | | ErrorType::ResourceExhausted 99 | | ErrorType::Openai 100 | | ErrorType::MaxTokens 101 | | ErrorType::ApiKeyNotSupported 102 | | ErrorType::UserAbortedRequest 103 | | ErrorType::CustomMessage 104 | | ErrorType::OutdatedClient 105 | | ErrorType::Debounced 106 | | ErrorType::RepositoryServiceRepositoryIsNotInitialized => 500, 107 | }, 108 | None => 500, 109 | } 110 | } 111 | 112 | // pub fn is_expected(&self) -> bool { 113 | // self.error.details[0].debug.is_expected.unwrap_or_default() 114 | // } 115 | } 116 | 117 | #[derive(Serialize)] 118 | pub struct ErrorResponse { 119 | pub status: u16, 120 | pub code: String, 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub error: Option, 123 | } 124 | 125 | #[derive(Serialize)] 126 | pub struct Error { 127 | pub message: String, 128 | pub details: String, 129 | pub value: String, 130 | } 131 | 132 | impl ErrorResponse { 133 | // pub fn to_json(&self) -> serde_json::Value { 134 | // serde_json::to_value(self).unwrap() 135 | // } 136 | 137 | pub fn status_code(&self) -> StatusCode { 138 | StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) 139 | } 140 | 141 | pub fn native_code(&self) -> String { 142 | self.code.replace("_", " ") 143 | } 144 | 145 | pub fn to_common(self) -> CommonErrorResponse { 146 | CommonErrorResponse { 147 | status: ApiStatus::Error, 148 | code: Some(self.status), 149 | error: self.error.as_ref().map(|error| error.message.clone()).or(Some(self.code.clone())), 150 | message: self.error.as_ref().map(|error| error.details.clone()), 151 | } 152 | } 153 | } 154 | 155 | pub enum StreamError { 156 | ChatError(ChatError), 157 | DataLengthLessThan5, 158 | EmptyMessage, 159 | } 160 | 161 | impl std::fmt::Display for StreamError { 162 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 | match self { 164 | StreamError::ChatError(error) => write!(f, "{}", error.error.details[0].debug.details.title), 165 | StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"), 166 | StreamError::EmptyMessage => write!(f, "empty message"), 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/chat/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | #[serde(untagged)] 5 | pub enum MessageContent { 6 | Text(String), 7 | Vision(Vec), 8 | } 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct VisionMessageContent { 12 | #[serde(rename = "type")] 13 | pub content_type: String, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub text: Option, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub image_url: Option, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct ImageUrl { 22 | pub url: String, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub detail: Option, 25 | } 26 | 27 | #[derive(Serialize, Deserialize)] 28 | pub struct Message { 29 | pub role: Role, 30 | pub content: MessageContent, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, PartialEq)] 34 | pub enum Role { 35 | #[serde(rename = "system", alias = "developer")] 36 | System, 37 | #[serde(rename = "user", alias = "human")] 38 | User, 39 | #[serde(rename = "assistant", alias = "ai")] 40 | Assistant, 41 | } 42 | 43 | #[derive(Serialize)] 44 | pub struct ChatResponse { 45 | pub id: String, 46 | pub object: String, 47 | pub created: i64, 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub model: Option, 50 | pub choices: Vec, 51 | #[serde(skip_serializing_if = "Option::is_none")] 52 | pub usage: Option, 53 | } 54 | 55 | #[derive(Serialize)] 56 | pub struct Choice { 57 | pub index: i32, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub message: Option, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | pub delta: Option, 62 | pub finish_reason: Option, 63 | } 64 | 65 | #[derive(Serialize)] 66 | pub struct Delta { 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub role: Option, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub content: Option, 71 | } 72 | 73 | #[derive(Serialize)] 74 | pub struct Usage { 75 | pub prompt_tokens: u32, 76 | pub completion_tokens: u32, 77 | pub total_tokens: u32, 78 | } 79 | 80 | // 模型定义 81 | #[derive(Serialize, Clone)] 82 | pub struct Model { 83 | pub id: &'static str, 84 | pub created: &'static i64, 85 | pub object: &'static str, 86 | pub owned_by: &'static str, 87 | } 88 | 89 | use crate::app::model::{AppConfig, UsageCheck}; 90 | use super::constant::USAGE_CHECK_MODELS; 91 | 92 | impl Model { 93 | pub fn is_usage_check(&self) -> bool { 94 | match AppConfig::get_usage_check() { 95 | UsageCheck::None => false, 96 | UsageCheck::Default => USAGE_CHECK_MODELS.contains(&self.id), 97 | UsageCheck::All => true, 98 | UsageCheck::Custom(models) => models.contains(&self.id), 99 | } 100 | } 101 | } 102 | 103 | #[derive(Serialize)] 104 | pub struct ModelsResponse { 105 | pub object: &'static str, 106 | pub data: &'static [Model], 107 | } 108 | -------------------------------------------------------------------------------- /src/chat/route.rs: -------------------------------------------------------------------------------- 1 | mod logs; 2 | pub use logs::{handle_logs, handle_logs_post}; 3 | mod health; 4 | pub use health::{handle_health, handle_root}; 5 | mod token; 6 | pub use token::{ 7 | handle_basic_calibration, handle_get_checksum, handle_get_tokeninfo, handle_tokeninfo_page, 8 | handle_update_tokeninfo, handle_update_tokeninfo_post, 9 | }; 10 | mod profile; 11 | pub use profile::get_user_info; 12 | mod config; 13 | pub use config::{ 14 | handle_about, handle_config_page, handle_env_example, handle_readme, handle_static, 15 | }; 16 | mod api; 17 | pub use api::handle_api_page; 18 | -------------------------------------------------------------------------------- /src/chat/route/api.rs: -------------------------------------------------------------------------------- 1 | use axum::response::{IntoResponse, Response}; 2 | use reqwest::header::CONTENT_TYPE; 3 | 4 | use crate::{ 5 | app::constant::{ 6 | CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_API_PATH, 7 | }, 8 | AppConfig, PageContent, 9 | }; 10 | 11 | pub async fn handle_api_page() -> impl IntoResponse { 12 | match AppConfig::get_page_content(ROUTE_API_PATH).unwrap_or_default() { 13 | PageContent::Default => Response::builder() 14 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 15 | .body(include_str!("../../../static/api.min.html").to_string()) 16 | .unwrap(), 17 | PageContent::Text(content) => Response::builder() 18 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 19 | .body(content.clone()) 20 | .unwrap(), 21 | PageContent::Html(content) => Response::builder() 22 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 23 | .body(content.clone()) 24 | .unwrap(), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/chat/route/config.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ 2 | constant::{ 3 | CONTENT_TYPE_TEXT_CSS_WITH_UTF8, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, 4 | CONTENT_TYPE_TEXT_JS_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH, 5 | ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, 6 | }, 7 | model::{AppConfig, PageContent}, 8 | }; 9 | use axum::{ 10 | body::Body, 11 | extract::Path, 12 | http::{ 13 | header::{CONTENT_TYPE, LOCATION}, 14 | StatusCode, 15 | }, 16 | response::{IntoResponse, Response}, 17 | }; 18 | 19 | pub async fn handle_env_example() -> impl IntoResponse { 20 | Response::builder() 21 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 22 | .body(include_str!("../../../.env.example").to_string()) 23 | .unwrap() 24 | } 25 | 26 | // 配置页面处理函数 27 | pub async fn handle_config_page() -> impl IntoResponse { 28 | match AppConfig::get_page_content(ROUTE_CONFIG_PATH).unwrap_or_default() { 29 | PageContent::Default => Response::builder() 30 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 31 | .body(include_str!("../../../static/config.min.html").to_string()) 32 | .unwrap(), 33 | PageContent::Text(content) => Response::builder() 34 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 35 | .body(content.clone()) 36 | .unwrap(), 37 | PageContent::Html(content) => Response::builder() 38 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 39 | .body(content.clone()) 40 | .unwrap(), 41 | } 42 | } 43 | 44 | pub async fn handle_static(Path(path): Path) -> impl IntoResponse { 45 | match path.as_str() { 46 | "shared-styles.css" => { 47 | match AppConfig::get_page_content(ROUTE_SHARED_STYLES_PATH).unwrap_or_default() { 48 | PageContent::Default => Response::builder() 49 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) 50 | .body(include_str!("../../../static/shared-styles.min.css").to_string()) 51 | .unwrap(), 52 | PageContent::Text(content) | PageContent::Html(content) => Response::builder() 53 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_CSS_WITH_UTF8) 54 | .body(content.clone()) 55 | .unwrap(), 56 | } 57 | } 58 | "shared.js" => { 59 | match AppConfig::get_page_content(ROUTE_SHARED_JS_PATH).unwrap_or_default() { 60 | PageContent::Default => Response::builder() 61 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) 62 | .body(include_str!("../../../static/shared.min.js").to_string()) 63 | .unwrap(), 64 | PageContent::Text(content) | PageContent::Html(content) => Response::builder() 65 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_JS_WITH_UTF8) 66 | .body(content.clone()) 67 | .unwrap(), 68 | } 69 | } 70 | _ => Response::builder() 71 | .status(StatusCode::NOT_FOUND) 72 | .body("Not found".to_string()) 73 | .unwrap(), 74 | } 75 | } 76 | 77 | pub async fn handle_about() -> impl IntoResponse { 78 | match AppConfig::get_page_content(ROUTE_ABOUT_PATH).unwrap_or_default() { 79 | PageContent::Default => Response::builder() 80 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 81 | .body(include_str!("../../../static/readme.min.html").to_string()) 82 | .unwrap(), 83 | PageContent::Text(content) => Response::builder() 84 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 85 | .body(content.clone()) 86 | .unwrap(), 87 | PageContent::Html(content) => Response::builder() 88 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 89 | .body(content.clone()) 90 | .unwrap(), 91 | } 92 | } 93 | 94 | pub async fn handle_readme() -> impl IntoResponse { 95 | match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() { 96 | PageContent::Default => Response::builder() 97 | .status(StatusCode::TEMPORARY_REDIRECT) 98 | .header(LOCATION, ROUTE_ABOUT_PATH) 99 | .body(Body::empty()) 100 | .unwrap(), 101 | PageContent::Text(content) => Response::builder() 102 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 103 | .body(Body::from(content.clone())) 104 | .unwrap(), 105 | PageContent::Html(content) => Response::builder() 106 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 107 | .body(Body::from(content.clone())) 108 | .unwrap(), 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/chat/route/health.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{ 3 | constant::{ 4 | AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, 5 | CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, 6 | ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, 7 | ROUTE_GET_CHECKSUM, ROUTE_GET_TOKENINFO_PATH, ROUTE_GET_USER_INFO_PATH, 8 | ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, 9 | ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH, 10 | }, 11 | lazy::{get_start_time, AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH}, 12 | model::{AppConfig, AppState, PageContent}, 13 | }, 14 | chat::constant::AVAILABLE_MODELS, 15 | common::models::{ 16 | health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats}, 17 | ApiStatus, 18 | }, 19 | }; 20 | use axum::{ 21 | body::Body, 22 | extract::State, 23 | http::{ 24 | header::{CONTENT_TYPE, LOCATION}, 25 | HeaderMap, StatusCode, 26 | }, 27 | response::{IntoResponse, Response}, 28 | Json, 29 | }; 30 | use chrono::Local; 31 | use reqwest::header::AUTHORIZATION; 32 | use std::sync::Arc; 33 | use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; 34 | use tokio::sync::Mutex; 35 | 36 | pub async fn handle_root() -> impl IntoResponse { 37 | match AppConfig::get_page_content(ROUTE_ROOT_PATH).unwrap_or_default() { 38 | PageContent::Default => Response::builder() 39 | .status(StatusCode::TEMPORARY_REDIRECT) 40 | .header(LOCATION, ROUTE_HEALTH_PATH) 41 | .body(Body::empty()) 42 | .unwrap(), 43 | PageContent::Text(content) => Response::builder() 44 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 45 | .body(Body::from(content.clone())) 46 | .unwrap(), 47 | PageContent::Html(content) => Response::builder() 48 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 49 | .body(Body::from(content.clone())) 50 | .unwrap(), 51 | } 52 | } 53 | 54 | pub async fn handle_health( 55 | State(state): State>>, 56 | headers: HeaderMap, 57 | ) -> Json { 58 | let start_time = get_start_time(); 59 | let uptime = (Local::now() - start_time).num_seconds(); 60 | 61 | // 先检查 headers 是否包含有效的认证信息 62 | let stats = if headers 63 | .get(AUTHORIZATION) 64 | .and_then(|h| h.to_str().ok()) 65 | .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) 66 | .map_or(false, |token| token == AUTH_TOKEN.as_str()) 67 | { 68 | // 只有在需要系统信息时才创建实例 69 | let mut sys = System::new_with_specifics( 70 | RefreshKind::nothing() 71 | .with_memory(MemoryRefreshKind::everything()) 72 | .with_cpu(CpuRefreshKind::everything()), 73 | ); 74 | 75 | std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); 76 | 77 | // 刷新 CPU 和内存信息 78 | sys.refresh_memory(); 79 | sys.refresh_cpu_usage(); 80 | 81 | let pid = std::process::id() as usize; 82 | let process = sys.process(pid.into()); 83 | 84 | // 获取内存信息 85 | let memory = process.map(|p| p.memory()).unwrap_or(0); 86 | 87 | // 获取 CPU 使用率 88 | let cpu_usage = sys.global_cpu_usage(); 89 | 90 | let state = state.lock().await; 91 | 92 | Some(SystemStats { 93 | started: start_time.to_string(), 94 | total_requests: state.total_requests, 95 | active_requests: state.active_requests, 96 | system: SystemInfo { 97 | memory: MemoryInfo { 98 | rss: memory, // 物理内存使用量(字节) 99 | }, 100 | cpu: CpuInfo { 101 | usage: cpu_usage, // CPU 使用率(百分比) 102 | }, 103 | }, 104 | }) 105 | } else { 106 | None 107 | }; 108 | 109 | Json(HealthCheckResponse { 110 | status: ApiStatus::Healthy, 111 | version: PKG_VERSION, 112 | uptime, 113 | stats, 114 | models: AVAILABLE_MODELS.iter().map(|m| m.id).collect::>(), 115 | endpoints: vec![ 116 | ROUTE_CHAT_PATH.as_str(), 117 | ROUTE_MODELS_PATH.as_str(), 118 | ROUTE_GET_CHECKSUM, 119 | ROUTE_TOKENINFO_PATH, 120 | ROUTE_UPDATE_TOKENINFO_PATH, 121 | ROUTE_GET_TOKENINFO_PATH, 122 | ROUTE_LOGS_PATH, 123 | ROUTE_ENV_EXAMPLE_PATH, 124 | ROUTE_CONFIG_PATH, 125 | ROUTE_STATIC_PATH, 126 | ROUTE_ABOUT_PATH, 127 | ROUTE_README_PATH, 128 | ROUTE_BASIC_CALIBRATION_PATH, 129 | ROUTE_GET_USER_INFO_PATH, 130 | ROUTE_API_PATH, 131 | ], 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /src/chat/route/logs.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{ 3 | constant::{ 4 | AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, 5 | CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_LOGS_PATH, 6 | }, 7 | lazy::AUTH_TOKEN, 8 | model::{AppConfig, AppState, PageContent, RequestLog}, 9 | }, 10 | common::{models::ApiStatus, utils::extract_token}, 11 | }; 12 | use axum::{ 13 | body::Body, 14 | extract::State, 15 | http::{ 16 | header::{AUTHORIZATION, CONTENT_TYPE}, 17 | HeaderMap, StatusCode, 18 | }, 19 | response::{IntoResponse, Response}, 20 | Json, 21 | }; 22 | use chrono::Local; 23 | use std::sync::Arc; 24 | use tokio::sync::Mutex; 25 | 26 | // 日志处理 27 | pub async fn handle_logs() -> impl IntoResponse { 28 | match AppConfig::get_page_content(ROUTE_LOGS_PATH).unwrap_or_default() { 29 | PageContent::Default => Response::builder() 30 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 31 | .body(Body::from( 32 | include_str!("../../../static/logs.min.html").to_string(), 33 | )) 34 | .unwrap(), 35 | PageContent::Text(content) => Response::builder() 36 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 37 | .body(Body::from(content.clone())) 38 | .unwrap(), 39 | PageContent::Html(content) => Response::builder() 40 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 41 | .body(Body::from(content.clone())) 42 | .unwrap(), 43 | } 44 | } 45 | 46 | pub async fn handle_logs_post( 47 | State(state): State>>, 48 | headers: HeaderMap, 49 | ) -> Result, StatusCode> { 50 | let auth_token = AUTH_TOKEN.as_str(); 51 | 52 | // 获取认证头 53 | let auth_header = headers 54 | .get(AUTHORIZATION) 55 | .and_then(|h| h.to_str().ok()) 56 | .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) 57 | .ok_or(StatusCode::UNAUTHORIZED)?; 58 | 59 | let state = state.lock().await; 60 | 61 | // 如果是管理员token,返回所有日志 62 | if auth_header == auth_token { 63 | return Ok(Json(LogsResponse { 64 | status: ApiStatus::Success, 65 | total: state.total_requests, 66 | active: Some(state.active_requests), 67 | error: Some(state.error_requests), 68 | logs: state.request_logs.clone(), 69 | timestamp: Local::now().to_string(), 70 | })); 71 | } 72 | 73 | // 解析 token 74 | let token_part = extract_token(auth_header).ok_or(StatusCode::UNAUTHORIZED)?; 75 | 76 | // 否则筛选出token匹配的日志 77 | let filtered_logs: Vec = state 78 | .request_logs 79 | .iter() 80 | .filter(|log| log.token_info.token == token_part) 81 | .cloned() 82 | .collect(); 83 | 84 | // 如果没有匹配的日志,返回未授权错误 85 | if filtered_logs.is_empty() { 86 | return Err(StatusCode::UNAUTHORIZED); 87 | } 88 | 89 | Ok(Json(LogsResponse { 90 | status: ApiStatus::Success, 91 | total: filtered_logs.len() as u64, 92 | active: None, 93 | error: None, 94 | logs: filtered_logs, 95 | timestamp: Local::now().to_string(), 96 | })) 97 | } 98 | 99 | #[derive(serde::Serialize)] 100 | pub struct LogsResponse { 101 | pub status: ApiStatus, 102 | pub total: u64, 103 | #[serde(skip_serializing_if = "Option::is_none")] 104 | pub active: Option, 105 | #[serde(skip_serializing_if = "Option::is_none")] 106 | pub error: Option, 107 | pub logs: Vec, 108 | pub timestamp: String, 109 | } 110 | -------------------------------------------------------------------------------- /src/chat/route/profile.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | chat::constant::ERR_NODATA, 3 | common::{models::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}}, 4 | }; 5 | use axum::Json; 6 | 7 | use super::token::TokenRequest; 8 | 9 | pub async fn get_user_info(Json(request): Json) -> Json { 10 | let auth_token = match request.token { 11 | Some(token) => token, 12 | None => { 13 | return Json(GetUserInfo::Error { 14 | error: ERR_NODATA.to_string(), 15 | }) 16 | } 17 | }; 18 | 19 | let token = match extract_token(&auth_token) { 20 | Some(token) => token, 21 | None => { 22 | return Json(GetUserInfo::Error { 23 | error: ERR_NODATA.to_string(), 24 | }) 25 | } 26 | }; 27 | 28 | match get_token_profile(&token).await { 29 | Some(usage) => Json(GetUserInfo::Usage(usage)), 30 | None => Json(GetUserInfo::Error { 31 | error: ERR_NODATA.to_string(), 32 | }), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/chat/route/token.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{ 3 | constant::{ 4 | AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, 5 | CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENINFO_PATH, 6 | }, 7 | lazy::{AUTH_TOKEN, TOKEN_FILE, TOKEN_LIST_FILE}, 8 | model::{AppConfig, AppState, PageContent, TokenUpdateRequest}, 9 | }, 10 | common::{ 11 | models::{ApiStatus, NormalResponseNoData}, 12 | utils::{ 13 | extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, load_tokens, validate_token_and_checksum 14 | }, 15 | }, 16 | }; 17 | use axum::{ 18 | extract::{Query, State}, 19 | http::{ 20 | header::{AUTHORIZATION, CONTENT_TYPE}, 21 | HeaderMap, 22 | }, 23 | response::{IntoResponse, Response}, 24 | Json, 25 | }; 26 | use reqwest::StatusCode; 27 | use serde::{Deserialize, Serialize}; 28 | use std::sync::Arc; 29 | use tokio::sync::Mutex; 30 | 31 | #[derive(Deserialize)] 32 | pub struct ChecksumQuery { 33 | #[serde(default, alias = "checksum")] 34 | pub bad_checksum: Option, 35 | } 36 | 37 | #[derive(Serialize)] 38 | pub struct ChecksumResponse { 39 | pub checksum: String, 40 | } 41 | 42 | pub async fn handle_get_checksum( 43 | Query(query): Query 44 | ) -> Json { 45 | match query.bad_checksum { 46 | None => Json(ChecksumResponse { checksum: generate_checksum_with_default() }), 47 | Some(bad_checksum) => Json(ChecksumResponse { checksum: generate_checksum_with_repair(&bad_checksum) }) 48 | } 49 | } 50 | 51 | // 更新 TokenInfo 处理 52 | pub async fn handle_update_tokeninfo( 53 | State(state): State>>, 54 | ) -> Json { 55 | // 重新加载 tokens 56 | let token_infos = load_tokens(); 57 | 58 | // 更新应用状态 59 | { 60 | let mut state = state.lock().await; 61 | state.token_infos = token_infos; 62 | } 63 | 64 | Json(NormalResponseNoData { 65 | status: ApiStatus::Success, 66 | message: Some("Token list has been reloaded".to_string()), 67 | }) 68 | } 69 | 70 | // 获取 TokenInfo 处理 71 | pub async fn handle_get_tokeninfo( 72 | headers: HeaderMap, 73 | ) -> Result, StatusCode> { 74 | // 验证 AUTH_TOKEN 75 | let auth_header = headers 76 | .get(AUTHORIZATION) 77 | .and_then(|h| h.to_str().ok()) 78 | .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) 79 | .ok_or(StatusCode::UNAUTHORIZED)?; 80 | 81 | if auth_header != AUTH_TOKEN.as_str() { 82 | return Err(StatusCode::UNAUTHORIZED); 83 | } 84 | 85 | let token_file = TOKEN_FILE.as_str(); 86 | let token_list_file = TOKEN_LIST_FILE.as_str(); 87 | 88 | // 读取文件内容 89 | let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new()); 90 | let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new()); 91 | 92 | // 获取 tokens_count 93 | let tokens_count = { 94 | { 95 | tokens.len() 96 | } 97 | }; 98 | 99 | Ok(Json(TokenInfoResponse { 100 | status: ApiStatus::Success, 101 | token_file: token_file.to_string(), 102 | token_list_file: token_list_file.to_string(), 103 | tokens: Some(tokens), 104 | tokens_count: Some(tokens_count), 105 | token_list: Some(token_list), 106 | message: None, 107 | })) 108 | } 109 | 110 | #[derive(Serialize)] 111 | pub struct TokenInfoResponse { 112 | pub status: ApiStatus, 113 | pub token_file: String, 114 | pub token_list_file: String, 115 | #[serde(skip_serializing_if = "Option::is_none")] 116 | pub tokens: Option, 117 | #[serde(skip_serializing_if = "Option::is_none")] 118 | pub tokens_count: Option, 119 | #[serde(skip_serializing_if = "Option::is_none")] 120 | pub token_list: Option, 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub message: Option, 123 | } 124 | 125 | pub async fn handle_update_tokeninfo_post( 126 | State(state): State>>, 127 | headers: HeaderMap, 128 | Json(request): Json, 129 | ) -> Result, StatusCode> { 130 | // 验证 AUTH_TOKEN 131 | let auth_header = headers 132 | .get(AUTHORIZATION) 133 | .and_then(|h| h.to_str().ok()) 134 | .and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX)) 135 | .ok_or(StatusCode::UNAUTHORIZED)?; 136 | 137 | if auth_header != AUTH_TOKEN.as_str() { 138 | return Err(StatusCode::UNAUTHORIZED); 139 | } 140 | 141 | let token_file = TOKEN_FILE.as_str(); 142 | let token_list_file = TOKEN_LIST_FILE.as_str(); 143 | 144 | // 写入文件 145 | std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 146 | 147 | if let Some(token_list) = &request.token_list { 148 | std::fs::write(&token_list_file, token_list) 149 | .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 150 | } 151 | 152 | // 重新加载 tokens 153 | let token_infos = load_tokens(); 154 | let token_infos_len = token_infos.len(); 155 | 156 | // 更新应用状态 157 | { 158 | let mut state = state.lock().await; 159 | state.token_infos = token_infos; 160 | } 161 | 162 | Ok(Json(TokenInfoResponse { 163 | status: ApiStatus::Success, 164 | token_file: token_file.to_string(), 165 | token_list_file: token_list_file.to_string(), 166 | tokens: None, 167 | tokens_count: Some(token_infos_len), 168 | token_list: None, 169 | message: Some("Token files have been updated and reloaded".to_string()), 170 | })) 171 | } 172 | 173 | pub async fn handle_tokeninfo_page() -> impl IntoResponse { 174 | match AppConfig::get_page_content(ROUTE_TOKENINFO_PATH).unwrap_or_default() { 175 | PageContent::Default => Response::builder() 176 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 177 | .body(include_str!("../../../static/tokeninfo.min.html").to_string()) 178 | .unwrap(), 179 | PageContent::Text(content) => Response::builder() 180 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8) 181 | .body(content.clone()) 182 | .unwrap(), 183 | PageContent::Html(content) => Response::builder() 184 | .header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8) 185 | .body(content.clone()) 186 | .unwrap(), 187 | } 188 | } 189 | 190 | #[derive(Deserialize)] 191 | pub struct TokenRequest { 192 | pub token: Option, 193 | } 194 | 195 | #[derive(Serialize)] 196 | pub struct BasicCalibrationResponse { 197 | pub status: ApiStatus, 198 | pub message: Option, 199 | #[serde(skip_serializing_if = "Option::is_none")] 200 | pub user_id: Option, 201 | #[serde(skip_serializing_if = "Option::is_none")] 202 | pub create_at: Option, 203 | #[serde(skip_serializing_if = "Option::is_none")] 204 | pub checksum_time: Option, 205 | } 206 | 207 | pub async fn handle_basic_calibration( 208 | Json(request): Json, 209 | ) -> Json { 210 | // 从请求头中获取并验证 auth token 211 | let auth_token = match request.token { 212 | Some(token) => token, 213 | None => { 214 | return Json(BasicCalibrationResponse { 215 | status: ApiStatus::Error, 216 | message: Some("未提供授权令牌".to_string()), 217 | user_id: None, 218 | create_at: None, 219 | checksum_time: None, 220 | }) 221 | } 222 | }; 223 | 224 | // 校验 token 和 checksum 225 | let (token, checksum) = match validate_token_and_checksum(&auth_token) { 226 | Some(parts) => parts, 227 | None => { 228 | return Json(BasicCalibrationResponse { 229 | status: ApiStatus::Error, 230 | message: Some("无效令牌或无效校验和".to_string()), 231 | user_id: None, 232 | create_at: None, 233 | checksum_time: None, 234 | }) 235 | } 236 | }; 237 | 238 | // 提取用户ID和创建时间 239 | let user_id = extract_user_id(&token); 240 | let create_at = extract_time(&token).map(|dt| dt.to_string()); 241 | let checksum_time = extract_time_ks(&checksum[..8]); 242 | 243 | // 返回校验结果 244 | Json(BasicCalibrationResponse { 245 | status: ApiStatus::Success, 246 | message: Some("校验成功".to_string()), 247 | user_id, 248 | create_at, 249 | checksum_time, 250 | }) 251 | } 252 | -------------------------------------------------------------------------------- /src/chat/stream.rs: -------------------------------------------------------------------------------- 1 | use super::aiserver::v1::StreamChatResponse; 2 | use flate2::read::GzDecoder; 3 | use prost::Message; 4 | use std::io::Read; 5 | 6 | use super::error::{ChatError, StreamError}; 7 | 8 | // 解压gzip数据 9 | fn decompress_gzip(data: &[u8]) -> Option> { 10 | let mut decoder = GzDecoder::new(data); 11 | let mut decompressed = Vec::new(); 12 | 13 | match decoder.read_to_end(&mut decompressed) { 14 | Ok(_) => Some(decompressed), 15 | Err(_) => { 16 | // println!("gzip解压失败: {}", e); 17 | None 18 | } 19 | } 20 | } 21 | 22 | pub enum StreamMessage { 23 | // 未完成 24 | Incomplete, 25 | // 调试 26 | Debug(String), 27 | // 流开始标志 b"\0\0\0\0\0" 28 | StreamStart, 29 | // 消息内容 30 | Content(Vec), 31 | // 流结束标志 b"\x02\0\0\0\x02{}" 32 | StreamEnd, 33 | } 34 | 35 | pub fn parse_stream_data(data: &[u8]) -> Result { 36 | if data.len() < 5 { 37 | return Err(StreamError::DataLengthLessThan5); 38 | } 39 | 40 | // 检查是否为流开始标志 41 | // if data == b"\0\0\0\0\0" { 42 | // return Ok(StreamMessage::StreamStart); 43 | // } 44 | 45 | // 检查是否为流结束标志 46 | // if data == b"\x02\0\0\0\x02{}" { 47 | // return Ok(StreamMessage::StreamEnd); 48 | // } 49 | 50 | let mut messages = Vec::new(); 51 | let mut offset = 0; 52 | 53 | while offset + 5 <= data.len() { 54 | // 获取消息类型和长度 55 | let msg_type = data[offset]; 56 | let msg_len = u32::from_be_bytes([ 57 | data[offset + 1], 58 | data[offset + 2], 59 | data[offset + 3], 60 | data[offset + 4], 61 | ]) as usize; 62 | 63 | // 流开始 64 | if msg_type == 0 && msg_len == 0 { 65 | return Ok(StreamMessage::StreamStart); 66 | } 67 | 68 | // 检查剩余数据长度是否足够 69 | if offset + 5 + msg_len > data.len() { 70 | return Ok(StreamMessage::Incomplete); 71 | } 72 | 73 | let msg_data = &data[offset + 5..offset + 5 + msg_len]; 74 | 75 | match msg_type { 76 | // 文本消息 77 | 0 => { 78 | if let Ok(response) = StreamChatResponse::decode(msg_data) { 79 | // crate::debug_println!("[text] StreamChatResponse: {:?}", response); 80 | if !response.text.is_empty() { 81 | messages.push(response.text); 82 | } else { 83 | // println!("[text] StreamChatResponse: {:?}", response); 84 | return Ok(StreamMessage::Debug( 85 | response.filled_prompt.unwrap_or_default(), 86 | // response.is_using_slow_request, 87 | )); 88 | } 89 | } 90 | } 91 | // gzip压缩消息 92 | 1 => { 93 | if let Some(text) = decompress_gzip(msg_data) { 94 | let response = StreamChatResponse::decode(&text[..]).unwrap_or_default(); 95 | // crate::debug_println!("[gzip] StreamChatResponse: {:?}", response); 96 | if !response.text.is_empty() { 97 | messages.push(response.text); 98 | } else { 99 | // println!("[gzip] StreamChatResponse: {:?}", response); 100 | return Ok(StreamMessage::Debug( 101 | response.filled_prompt.unwrap_or_default(), 102 | // response.is_using_slow_request, 103 | )); 104 | } 105 | } 106 | } 107 | // JSON字符串 108 | 2 => { 109 | if msg_len == 2 { 110 | return Ok(StreamMessage::StreamEnd); 111 | } 112 | if let Ok(text) = String::from_utf8(msg_data.to_vec()) { 113 | // println!("JSON消息: {}", text); 114 | if let Ok(error) = serde_json::from_str::(&text) { 115 | return Err(StreamError::ChatError(error)); 116 | } 117 | // 未预计 118 | // messages.push(text); 119 | } 120 | } 121 | // 其他类型暂不处理 122 | t => eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t), 123 | } 124 | 125 | offset += 5 + msg_len; 126 | } 127 | 128 | if messages.is_empty() { 129 | Err(StreamError::EmptyMessage) 130 | } else { 131 | Ok(StreamMessage::Content(messages)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod utils; 3 | pub mod client; 4 | -------------------------------------------------------------------------------- /src/common/client.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ 2 | constant::{ 3 | CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL, 4 | HEADER_NAME_GHOST_MODE, TRUE, 5 | }, 6 | lazy::{ 7 | CURSOR_API2_CHAT_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL, 8 | REVERSE_PROXY_HOST, USE_PROXY, 9 | }, 10 | }; 11 | use reqwest::header::{ 12 | ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT, 13 | HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT, 14 | }; 15 | use reqwest::{Client, RequestBuilder}; 16 | use uuid::Uuid; 17 | 18 | macro_rules! def_const { 19 | ($name:ident, $value:expr) => { 20 | const $name: &'static str = $value; 21 | }; 22 | } 23 | 24 | def_const!(SEC_FETCH_DEST, "sec-fetch-dest"); 25 | def_const!(SEC_FETCH_MODE, "sec-fetch-mode"); 26 | def_const!(SEC_FETCH_SITE, "sec-fetch-site"); 27 | def_const!(SEC_GPC, "sec-gpc"); 28 | def_const!(PRIORITY, "priority"); 29 | 30 | def_const!(ONE, "1"); 31 | def_const!(ENCODINGS, "gzip,br"); 32 | def_const!(VALUE_ACCEPT, "*/*"); 33 | def_const!(VALUE_LANGUAGE, "zh-CN"); 34 | def_const!(EMPTY, "empty"); 35 | def_const!(CORS, "cors"); 36 | def_const!(NO_CACHE, "no-cache"); 37 | def_const!(UA_WIN, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"); 38 | def_const!(SAME_ORIGIN, "same-origin"); 39 | def_const!(KEEP_ALIVE, "keep-alive"); 40 | def_const!(TRAILERS, "trailers"); 41 | def_const!(U_EQ_4, "u=4"); 42 | 43 | def_const!(PROXY_HOST, "x-co"); 44 | 45 | /// 返回预构建的 Cursor API 客户端 46 | /// 47 | /// # 参数 48 | /// 49 | /// * `auth_token` - 授权令牌 50 | /// * `checksum` - 校验和 51 | /// * `endpoint` - API 端点路径 52 | /// 53 | /// # 返回 54 | /// 55 | /// * `reqwest::RequestBuilder` - 配置好的请求构建器 56 | pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder { 57 | let trace_id = Uuid::new_v4().to_string(); 58 | 59 | let client = if *USE_PROXY { 60 | Client::new() 61 | .post(&*CURSOR_API2_CHAT_URL) 62 | .header(HOST, &*REVERSE_PROXY_HOST) 63 | .header(PROXY_HOST, CURSOR_API2_HOST) 64 | } else { 65 | Client::new() 66 | .post(&*CURSOR_API2_CHAT_URL) 67 | .header(HOST, CURSOR_API2_HOST) 68 | }; 69 | 70 | client 71 | .header(CONTENT_TYPE, CONTENT_TYPE_CONNECT_PROTO) 72 | .bearer_auth(auth_token) 73 | .header("connect-accept-encoding", ENCODINGS) 74 | .header("connect-protocol-version", ONE) 75 | .header(USER_AGENT, "connect-es/1.6.1") 76 | .header("x-amzn-trace-id", format!("Root={}", trace_id)) 77 | // .header("x-client-key", client_key) 78 | .header("x-cursor-checksum", checksum) 79 | .header("x-cursor-client-version", "0.42.5") 80 | .header("x-cursor-timezone", "Asia/Shanghai") 81 | .header(HEADER_NAME_GHOST_MODE, TRUE) 82 | .header("x-request-id", trace_id) 83 | .header(CONNECTION, KEEP_ALIVE) 84 | .header(TRANSFER_ENCODING, "chunked") 85 | } 86 | 87 | /// 返回预构建的获取 Stripe 账户信息的 Cursor API 客户端 88 | /// 89 | /// # 参数 90 | /// 91 | /// * `auth_token` - 授权令牌 92 | /// 93 | /// # 返回 94 | /// 95 | /// * `reqwest::RequestBuilder` - 配置好的请求构建器 96 | pub fn build_profile_client(auth_token: &str) -> RequestBuilder { 97 | let client = if *USE_PROXY { 98 | Client::new() 99 | .get(&*CURSOR_API2_STRIPE_URL) 100 | .header(HOST, &*REVERSE_PROXY_HOST) 101 | .header(PROXY_HOST, CURSOR_API2_HOST) 102 | } else { 103 | Client::new() 104 | .get(&*CURSOR_API2_STRIPE_URL) 105 | .header(HOST, CURSOR_API2_HOST) 106 | }; 107 | 108 | client 109 | .header("sec-ch-ua", "\"Not-A.Brand\";v=\"99\", \"Chromium\";v=\"124\"") 110 | .header(HEADER_NAME_GHOST_MODE, TRUE) 111 | .header("sec-ch-ua-mobile", "?0") 112 | .bearer_auth(auth_token) 113 | .header( 114 | USER_AGENT, 115 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.42.5 Chrome/124.0.6367.243 Electron/30.4.0 Safari/537.36", 116 | ) 117 | .header("sec-ch-ua-platform", "\"Windows\"") 118 | .header(ACCEPT, VALUE_ACCEPT) 119 | .header(ORIGIN, "vscode-file://vscode-app") 120 | .header(SEC_FETCH_SITE, "cross-site") 121 | .header(SEC_FETCH_MODE, CORS) 122 | .header(SEC_FETCH_DEST, EMPTY) 123 | .header(ACCEPT_ENCODING, ENCODINGS) 124 | .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) 125 | .header(PRIORITY, "u=1, i") 126 | } 127 | 128 | /// 返回预构建的获取使用情况的 Cursor API 客户端 129 | /// 130 | /// # 参数 131 | /// 132 | /// * `user_id` - 用户 ID 133 | /// * `auth_token` - 授权令牌 134 | /// 135 | /// # 返回 136 | /// 137 | /// * `reqwest::RequestBuilder` - 配置好的请求构建器 138 | pub fn build_usage_client(user_id: &str, auth_token: &str) -> RequestBuilder { 139 | let session_token = format!("{}%3A%3A{}", user_id, auth_token); 140 | 141 | let client = if *USE_PROXY { 142 | Client::new() 143 | .get(&*CURSOR_USAGE_API_URL) 144 | .header(HOST, &*REVERSE_PROXY_HOST) 145 | .header(PROXY_HOST, CURSOR_HOST) 146 | } else { 147 | Client::new() 148 | .get(&*CURSOR_USAGE_API_URL) 149 | .header(HOST, CURSOR_HOST) 150 | }; 151 | 152 | client 153 | .header(USER_AGENT, UA_WIN) 154 | .header(ACCEPT, VALUE_ACCEPT) 155 | .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) 156 | .header(ACCEPT_ENCODING, ENCODINGS) 157 | .header(REFERER, CURSOR_SETTINGS_URL) 158 | .header(DNT, ONE) 159 | .header(SEC_GPC, ONE) 160 | .header(SEC_FETCH_DEST, EMPTY) 161 | .header(SEC_FETCH_MODE, CORS) 162 | .header(SEC_FETCH_SITE, SAME_ORIGIN) 163 | .header(CONNECTION, KEEP_ALIVE) 164 | .header(PRAGMA, NO_CACHE) 165 | .header(CACHE_CONTROL, NO_CACHE) 166 | .header(TE, TRAILERS) 167 | .header(PRIORITY, U_EQ_4) 168 | .header( 169 | COOKIE, 170 | &format!("WorkosCursorSessionToken={}", session_token), 171 | ) 172 | .query(&[("user", user_id)]) 173 | } 174 | 175 | /// 返回预构建的获取用户信息的 Cursor API 客户端 176 | /// 177 | /// # 参数 178 | /// 179 | /// * `user_id` - 用户 ID 180 | /// * `auth_token` - 授权令牌 181 | /// 182 | /// # 返回 183 | /// 184 | /// * `reqwest::RequestBuilder` - 配置好的请求构建器 185 | pub fn build_userinfo_client(user_id: &str, auth_token: &str) -> RequestBuilder { 186 | let session_token = format!("{}%3A%3A{}", user_id, auth_token); 187 | 188 | let client = if *USE_PROXY { 189 | Client::new() 190 | .get(&*CURSOR_USER_API_URL) 191 | .header(HOST, &*REVERSE_PROXY_HOST) 192 | .header(PROXY_HOST, CURSOR_HOST) 193 | } else { 194 | Client::new() 195 | .get(&*CURSOR_USER_API_URL) 196 | .header(HOST, CURSOR_HOST) 197 | }; 198 | 199 | client 200 | .header(USER_AGENT, UA_WIN) 201 | .header(ACCEPT, VALUE_ACCEPT) 202 | .header(ACCEPT_LANGUAGE, VALUE_LANGUAGE) 203 | .header(ACCEPT_ENCODING, ENCODINGS) 204 | .header(REFERER, CURSOR_SETTINGS_URL) 205 | .header(DNT, ONE) 206 | .header(SEC_GPC, ONE) 207 | .header(SEC_FETCH_DEST, EMPTY) 208 | .header(SEC_FETCH_MODE, CORS) 209 | .header(SEC_FETCH_SITE, SAME_ORIGIN) 210 | .header(CONNECTION, KEEP_ALIVE) 211 | .header(PRAGMA, NO_CACHE) 212 | .header(CACHE_CONTROL, NO_CACHE) 213 | .header(TE, TRAILERS) 214 | .header(PRIORITY, U_EQ_4) 215 | .header( 216 | COOKIE, 217 | &format!("WorkosCursorSessionToken={}", session_token), 218 | ) 219 | .query(&[("user", user_id)]) 220 | } 221 | -------------------------------------------------------------------------------- /src/common/models.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod health; 3 | pub mod config; 4 | pub mod userinfo; 5 | 6 | use config::ConfigData; 7 | 8 | use serde::Serialize; 9 | 10 | #[derive(Serialize)] 11 | pub enum ApiStatus { 12 | #[serde(rename = "healthy")] 13 | Healthy, 14 | #[serde(rename = "success")] 15 | Success, 16 | #[serde(rename = "error")] 17 | Error, 18 | #[serde(rename = "failed")] 19 | Failed, 20 | } 21 | 22 | // #[derive(Serialize)] 23 | // #[serde(untagged)] 24 | // pub enum ApiResponse { 25 | // HealthCheck(HealthCheckResponse), 26 | // ConfigData(NormalResponse), 27 | // Error(ErrorResponse), 28 | // } 29 | 30 | // impl ApiResponse { 31 | // pub fn to_string(&self) -> String { 32 | // serde_json::to_string(self).unwrap() 33 | // } 34 | // } 35 | 36 | #[derive(Serialize)] 37 | pub struct NormalResponse { 38 | pub status: ApiStatus, 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub data: Option, 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub message: Option, 43 | } 44 | 45 | impl std::fmt::Display for NormalResponse { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | write!(f, "{}", serde_json::to_string(self).unwrap()) 48 | } 49 | } 50 | 51 | #[derive(Serialize)] 52 | pub struct NormalResponseNoData { 53 | pub status: ApiStatus, 54 | #[serde(skip_serializing_if = "Option::is_none")] 55 | pub message: Option, 56 | } 57 | 58 | #[derive(Serialize)] 59 | pub struct ErrorResponse { 60 | // status -> 成功 / 失败 61 | pub status: ApiStatus, 62 | // HTTP 请求的状态码 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub code: Option, 65 | // HTTP 请求的错误码 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub error: Option, 68 | // 错误详情 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub message: Option, 71 | } 72 | -------------------------------------------------------------------------------- /src/common/models/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::app::model::{PageContent, UsageCheck, VisionAbility}; 4 | 5 | #[derive(Serialize)] 6 | pub struct ConfigData { 7 | pub page_content: Option, 8 | pub enable_stream_check: bool, 9 | pub include_stop_stream: bool, 10 | pub vision_ability: VisionAbility, 11 | pub enable_slow_pool: bool, 12 | pub enable_all_claude: bool, 13 | pub check_usage_models: UsageCheck, 14 | } 15 | 16 | #[derive(Deserialize)] 17 | pub struct ConfigUpdateRequest { 18 | #[serde(default)] 19 | pub action: String, // "get", "update", "reset" 20 | #[serde(default)] 21 | pub path: String, 22 | #[serde(default)] 23 | pub content: Option, // "default", "text", "html" 24 | #[serde(default)] 25 | pub enable_stream_check: Option, 26 | #[serde(default)] 27 | pub include_stop_stream: Option, 28 | #[serde(default)] 29 | pub vision_ability: Option, 30 | #[serde(default)] 31 | pub enable_slow_pool: Option, 32 | #[serde(default)] 33 | pub enable_all_claude: Option, 34 | #[serde(default)] 35 | pub check_usage_models: Option, 36 | } 37 | -------------------------------------------------------------------------------- /src/common/models/error.rs: -------------------------------------------------------------------------------- 1 | use super::ErrorResponse; 2 | 3 | pub enum ChatError { 4 | ModelNotSupported(String), 5 | EmptyMessages, 6 | NoTokens, 7 | RequestFailed(String), 8 | Unauthorized, 9 | } 10 | 11 | impl ChatError { 12 | pub fn to_json(&self) -> ErrorResponse { 13 | let (error, message) = match self { 14 | ChatError::ModelNotSupported(model) => ( 15 | "model_not_supported", 16 | format!("Model '{}' is not supported", model), 17 | ), 18 | ChatError::EmptyMessages => ( 19 | "empty_messages", 20 | "Message array cannot be empty".to_string(), 21 | ), 22 | ChatError::NoTokens => ("no_tokens", "No available tokens".to_string()), 23 | ChatError::RequestFailed(err) => ("request_failed", format!("Request failed: {}", err)), 24 | ChatError::Unauthorized => ("unauthorized", "Invalid authorization token".to_string()), 25 | }; 26 | 27 | ErrorResponse { 28 | status: super::ApiStatus::Error, 29 | code: None, 30 | error: Some(error.to_string()), 31 | message: Some(message), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/common/models/health.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use super::ApiStatus; 4 | 5 | #[derive(Serialize)] 6 | pub struct HealthCheckResponse { 7 | pub status: ApiStatus, 8 | pub version: &'static str, 9 | pub uptime: i64, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub stats: Option, 12 | pub models: Vec<&'static str>, 13 | pub endpoints: Vec<&'static str>, 14 | } 15 | 16 | #[derive(Serialize)] 17 | pub struct SystemStats { 18 | pub started: String, 19 | pub total_requests: u64, 20 | pub active_requests: u64, 21 | pub system: SystemInfo, 22 | } 23 | 24 | #[derive(Serialize)] 25 | pub struct SystemInfo { 26 | pub memory: MemoryInfo, 27 | pub cpu: CpuInfo, 28 | } 29 | 30 | #[derive(Serialize)] 31 | pub struct MemoryInfo { 32 | pub rss: u64, // 物理内存使用量(字节) 33 | } 34 | 35 | #[derive(Serialize)] 36 | pub struct CpuInfo { 37 | pub usage: f32, // CPU 使用率(百分比) 38 | } 39 | -------------------------------------------------------------------------------- /src/common/models/userinfo.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize)] 5 | #[serde(untagged)] 6 | pub enum GetUserInfo { 7 | Usage(TokenProfile), 8 | Error{ error: String }, 9 | } 10 | 11 | #[derive(Serialize, Clone)] 12 | pub struct TokenProfile { 13 | pub usage: UsageProfile, 14 | pub user: UserProfile, 15 | pub stripe: StripeProfile, 16 | } 17 | 18 | #[derive(Deserialize, Serialize, PartialEq, Clone)] 19 | pub enum MembershipType { 20 | #[serde(rename = "free")] 21 | Free, 22 | #[serde(rename = "free_trial")] 23 | FreeTrial, 24 | #[serde(rename = "pro")] 25 | Pro, 26 | #[serde(rename = "enterprise")] 27 | Enterprise, 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Clone)] 31 | pub struct StripeProfile { 32 | #[serde(rename(deserialize = "membershipType"))] 33 | pub membership_type: MembershipType, 34 | #[serde(rename(deserialize = "paymentId"), default, skip_serializing_if = "Option::is_none")] 35 | pub payment_id: Option, 36 | #[serde(rename(deserialize = "daysRemainingOnTrial"))] 37 | pub days_remaining_on_trial: u32, 38 | } 39 | 40 | #[derive(Deserialize, Serialize, Clone)] 41 | pub struct ModelUsage { 42 | #[serde(rename(deserialize = "numRequests", serialize = "requests"))] 43 | pub num_requests: u32, 44 | #[serde(rename(deserialize = "numTokens", serialize = "tokens"))] 45 | pub num_tokens: u32, 46 | #[serde( 47 | rename(deserialize = "maxRequestUsage"), 48 | skip_serializing_if = "Option::is_none" 49 | )] 50 | pub max_requests: Option, 51 | #[serde( 52 | rename(deserialize = "maxTokenUsage"), 53 | skip_serializing_if = "Option::is_none" 54 | )] 55 | pub max_tokens: Option, 56 | } 57 | 58 | #[derive(Deserialize, Serialize, Clone)] 59 | pub struct UsageProfile { 60 | #[serde(rename(deserialize = "gpt-4"))] 61 | pub premium: ModelUsage, 62 | #[serde(rename(deserialize = "gpt-3.5-turbo"))] 63 | pub standard: ModelUsage, 64 | #[serde(rename(deserialize = "gpt-4-32k"))] 65 | pub unknown: ModelUsage, 66 | } 67 | 68 | #[derive(Deserialize, Serialize, Clone)] 69 | pub struct UserProfile { 70 | pub email: String, 71 | // pub email_verified: bool, 72 | pub name: String, 73 | #[serde(rename(serialize = "id"))] 74 | pub sub: String, 75 | pub updated_at: DateTime, 76 | // pub picture: Option, 77 | } 78 | -------------------------------------------------------------------------------- /src/common/utils.rs: -------------------------------------------------------------------------------- 1 | mod checksum; 2 | pub use checksum::*; 3 | mod tokens; 4 | pub use tokens::*; 5 | 6 | use super::models::userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}; 7 | use crate::app::constant::{FALSE, TRUE}; 8 | 9 | pub fn parse_bool_from_env(key: &str, default: bool) -> bool { 10 | std::env::var(key) 11 | .ok() 12 | .map(|v| match v.to_lowercase().as_str() { 13 | TRUE | "1" => true, 14 | FALSE | "0" => false, 15 | _ => default, 16 | }) 17 | .unwrap_or(default) 18 | } 19 | 20 | pub fn parse_string_from_env(key: &str, default: &str) -> String { 21 | std::env::var(key).unwrap_or_else(|_| default.to_string()) 22 | } 23 | 24 | pub fn parse_usize_from_env(key: &str, default: usize) -> usize { 25 | std::env::var(key) 26 | .ok() 27 | .and_then(|v| v.parse().ok()) 28 | .unwrap_or(default) 29 | } 30 | 31 | pub async fn get_token_profile(auth_token: &str) -> Option { 32 | let user_id = extract_user_id(auth_token)?; 33 | 34 | // 构建请求客户端 35 | let client = super::client::build_usage_client(&user_id, auth_token); 36 | 37 | // 发送请求并获取响应 38 | // let response = client.send().await.ok()?; 39 | // let bytes = response.bytes().await?; 40 | // println!("Raw response bytes: {:?}", bytes); 41 | // let usage = serde_json::from_str::(&text).ok()?; 42 | let usage = client 43 | .send() 44 | .await 45 | .ok()? 46 | .json::() 47 | .await 48 | .ok()?; 49 | 50 | let user = get_user_profile(auth_token).await?; 51 | 52 | // 从 Stripe 获取用户资料 53 | let stripe = get_stripe_profile(auth_token).await?; 54 | 55 | // 映射响应数据到 TokenProfile 56 | Some(TokenProfile { 57 | usage, 58 | user, 59 | stripe, 60 | }) 61 | } 62 | 63 | pub async fn get_stripe_profile(auth_token: &str) -> Option { 64 | let client = super::client::build_profile_client(auth_token); 65 | let response = client 66 | .send() 67 | .await 68 | .ok()? 69 | .json::() 70 | .await 71 | .ok()?; 72 | Some(response) 73 | } 74 | 75 | pub async fn get_user_profile(auth_token: &str) -> Option { 76 | let user_id = extract_user_id(auth_token)?; 77 | 78 | // 构建请求客户端 79 | let client = super::client::build_userinfo_client(&user_id, auth_token); 80 | 81 | // 发送请求并获取响应 82 | let user_profile = client.send().await.ok()?.json::().await.ok()?; 83 | 84 | Some(user_profile) 85 | } 86 | 87 | pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> { 88 | // 找最后一个逗号 89 | let comma_pos = auth_token.rfind(',')?; 90 | let (token_part, checksum) = auth_token.split_at(comma_pos); 91 | let checksum = &checksum[1..]; // 跳过逗号 92 | 93 | // 解析 token - 为了向前兼容,忽略最后一个:或%3A前的内容 94 | let colon_pos = token_part.rfind(':'); 95 | let encoded_colon_pos = token_part.rfind("%3A"); 96 | 97 | let token = match (colon_pos, encoded_colon_pos) { 98 | (None, None) => token_part, // 最简单的构成: token,checksum 99 | (Some(pos1), None) => &token_part[(pos1 + 1)..], 100 | (None, Some(pos2)) => &token_part[(pos2 + 3)..], 101 | (Some(pos1), Some(pos2)) => { 102 | let pos = pos1.max(pos2); 103 | let start = if pos == pos2 { pos + 3 } else { pos + 1 }; 104 | &token_part[start..] 105 | } 106 | }; 107 | 108 | // 验证 token 和 checksum 有效性 109 | if validate_token(token) && validate_checksum(checksum) { 110 | Some((token.to_string(), checksum.to_string())) 111 | } else { 112 | None 113 | } 114 | } 115 | 116 | pub fn extract_token(auth_token: &str) -> Option { 117 | // 解析 token 118 | let token_part = match auth_token.rfind(',') { 119 | Some(pos) => &auth_token[..pos], 120 | None => auth_token 121 | }; 122 | 123 | let colon_pos = token_part.rfind(':'); 124 | let encoded_colon_pos = token_part.rfind("%3A"); 125 | 126 | let token = match (colon_pos, encoded_colon_pos) { 127 | (None, None) => token_part, 128 | (Some(pos1), None) => &token_part[(pos1 + 1)..], 129 | (None, Some(pos2)) => &token_part[(pos2 + 3)..], 130 | (Some(pos1), Some(pos2)) => { 131 | let pos = pos1.max(pos2); 132 | let start = if pos == pos2 { pos + 3 } else { pos + 1 }; 133 | &token_part[start..] 134 | } 135 | }; 136 | 137 | // 验证 token 有效性 138 | if validate_token(token) { 139 | Some(token.to_string()) 140 | } else { 141 | None 142 | } 143 | } 144 | 145 | pub fn format_time_ms(seconds: f64) -> f64 { 146 | (seconds * 1000.0).round() / 1000.0 147 | } 148 | -------------------------------------------------------------------------------- /src/common/utils/checksum.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; 2 | use rand::Rng; 3 | use sha2::{Digest, Sha256}; 4 | 5 | fn generate_hash() -> String { 6 | let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); 7 | let mut hasher = Sha256::new(); 8 | hasher.update(random_bytes); 9 | hex::encode(hasher.finalize()) 10 | } 11 | 12 | fn obfuscate_bytes(bytes: &mut [u8]) { 13 | let mut prev: u8 = 165; 14 | for (idx, byte) in bytes.iter_mut().enumerate() { 15 | let old_value = *byte; 16 | *byte = (old_value ^ prev).wrapping_add((idx % 256) as u8); 17 | prev = *byte; 18 | } 19 | } 20 | 21 | fn deobfuscate_bytes(bytes: &mut [u8]) { 22 | let mut prev: u8 = 165; 23 | for (idx, byte) in bytes.iter_mut().enumerate() { 24 | let temp = *byte; 25 | *byte = (*byte).wrapping_sub((idx % 256) as u8) ^ prev; 26 | prev = temp; 27 | } 28 | } 29 | 30 | fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String { 31 | let timestamp = std::time::SystemTime::now() 32 | .duration_since(std::time::UNIX_EPOCH) 33 | .unwrap() 34 | .as_secs() 35 | / 1_000; 36 | 37 | let mut timestamp_bytes = vec![ 38 | ((timestamp >> 8) & 0xFF) as u8, 39 | (0xFF & timestamp) as u8, 40 | ((timestamp >> 24) & 0xFF) as u8, 41 | ((timestamp >> 16) & 0xFF) as u8, 42 | ((timestamp >> 8) & 0xFF) as u8, 43 | (0xFF & timestamp) as u8, 44 | ]; 45 | 46 | obfuscate_bytes(&mut timestamp_bytes); 47 | let encoded = BASE64.encode(×tamp_bytes); 48 | 49 | match mac_addr { 50 | Some(mac) => format!("{}{}/{}", encoded, device_id, mac), 51 | None => format!("{}{}", encoded, device_id), 52 | } 53 | } 54 | 55 | pub fn generate_checksum_with_default() -> String { 56 | generate_checksum(&generate_hash(), Some(&generate_hash())) 57 | } 58 | 59 | pub fn generate_checksum_with_repair(bad_checksum: &str) -> String { 60 | // 如果是空字符串,直接返回默认值 61 | if bad_checksum.is_empty() { 62 | return generate_checksum_with_default(); 63 | } 64 | 65 | // 尝试修复时间戳头的函数 66 | fn try_fix_timestamp(timestamp_base64: &str) -> Option { 67 | if let Ok(timestamp_bytes) = BASE64.decode(timestamp_base64) { 68 | if timestamp_bytes.len() == 6 { 69 | let mut fixed_bytes = timestamp_bytes.clone(); 70 | deobfuscate_bytes(&mut fixed_bytes); 71 | 72 | // 检查前3位是否为0 73 | if fixed_bytes[0..3].iter().all(|&x| x == 0) { 74 | // 从后四位构建时间戳 75 | let timestamp = ((fixed_bytes[2] as u64) << 24) 76 | | ((fixed_bytes[3] as u64) << 16) 77 | | ((fixed_bytes[4] as u64) << 8) 78 | | (fixed_bytes[5] as u64); 79 | 80 | let current_timestamp = std::time::SystemTime::now() 81 | .duration_since(std::time::UNIX_EPOCH) 82 | .unwrap() 83 | .as_secs() 84 | / 1_000; 85 | 86 | if timestamp <= current_timestamp { 87 | // 修复时间戳字节 88 | fixed_bytes[0] = fixed_bytes[4]; 89 | fixed_bytes[1] = fixed_bytes[5]; 90 | 91 | obfuscate_bytes(&mut fixed_bytes); 92 | return Some(BASE64.encode(&fixed_bytes)); 93 | } 94 | } 95 | } 96 | } 97 | None 98 | } 99 | 100 | if bad_checksum.len() == 8 { 101 | // 尝试修复时间戳头 102 | if let Some(fixed_timestamp) = try_fix_timestamp(bad_checksum) { 103 | return format!("{}{}/{}", fixed_timestamp, generate_hash(), generate_hash()); 104 | } 105 | 106 | // 验证原始时间戳 107 | if let Some(timestamp) = extract_time_ks(bad_checksum) { 108 | let current_timestamp = std::time::SystemTime::now() 109 | .duration_since(std::time::UNIX_EPOCH) 110 | .unwrap() 111 | .as_secs() 112 | / 1_000; 113 | 114 | if timestamp <= current_timestamp { 115 | return format!("{}{}/{}", bad_checksum, generate_hash(), generate_hash()); 116 | } 117 | } 118 | } else if bad_checksum.len() > 8 { 119 | // 处理可能包含hash的情况 120 | let parts: Vec<&str> = bad_checksum.split('/').collect(); 121 | match parts.len() { 122 | 1 => { 123 | let timestamp_base64 = &bad_checksum[..8]; 124 | let device_id = &bad_checksum[8..]; 125 | 126 | if is_valid_hash(device_id) { 127 | // 先尝试修复时间戳 128 | if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { 129 | return format!("{}{}/{}", fixed_timestamp, device_id, generate_hash()); 130 | } 131 | 132 | // 验证原始时间戳 133 | if let Some(timestamp) = extract_time_ks(timestamp_base64) { 134 | let current_timestamp = std::time::SystemTime::now() 135 | .duration_since(std::time::UNIX_EPOCH) 136 | .unwrap() 137 | .as_secs() 138 | / 1_000; 139 | 140 | if timestamp <= current_timestamp { 141 | return format!( 142 | "{}{}/{}", 143 | timestamp_base64, 144 | device_id, 145 | generate_hash() 146 | ); 147 | } 148 | } 149 | } 150 | } 151 | 2 => { 152 | let first_part = parts[0]; 153 | let mac_hash = parts[1]; 154 | 155 | if is_valid_hash(mac_hash) && first_part.len() == mac_hash.len() + 8 { 156 | let timestamp_base64 = &first_part[..8]; 157 | let device_id = &first_part[8..]; 158 | 159 | if is_valid_hash(device_id) { 160 | // 先尝试修复时间戳 161 | if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) { 162 | return format!("{}{}/{}", fixed_timestamp, device_id, mac_hash); 163 | } 164 | 165 | // 验证原始时间戳 166 | if let Some(timestamp) = extract_time_ks(timestamp_base64) { 167 | let current_timestamp = std::time::SystemTime::now() 168 | .duration_since(std::time::UNIX_EPOCH) 169 | .unwrap() 170 | .as_secs() 171 | / 1_000; 172 | 173 | if timestamp <= current_timestamp { 174 | return bad_checksum.to_string(); 175 | } 176 | } 177 | } 178 | } 179 | } 180 | _ => {} 181 | } 182 | } 183 | 184 | // 如果所有修复尝试都失败,返回默认值 185 | generate_checksum_with_default() 186 | } 187 | 188 | pub fn extract_time_ks(timestamp_base64: &str) -> Option { 189 | let mut timestamp_bytes = BASE64.decode(timestamp_base64).ok()?; 190 | 191 | if timestamp_bytes.len() != 6 { 192 | return None; 193 | } 194 | 195 | deobfuscate_bytes(&mut timestamp_bytes); 196 | 197 | if timestamp_bytes[0] != timestamp_bytes[4] || timestamp_bytes[1] != timestamp_bytes[5] { 198 | return None; 199 | } 200 | 201 | // 使用后四位还原 timestamp 202 | Some( 203 | ((timestamp_bytes[2] as u64) << 24) 204 | | ((timestamp_bytes[3] as u64) << 16) 205 | | ((timestamp_bytes[4] as u64) << 8) 206 | | (timestamp_bytes[5] as u64), 207 | ) 208 | } 209 | 210 | pub fn validate_checksum(checksum: &str) -> bool { 211 | if checksum.is_empty() { 212 | return false; 213 | } 214 | // 首先检查是否包含基本的 base64 编码部分和 hash 格式的 device_id 215 | let parts: Vec<&str> = checksum.split('/').collect(); 216 | 217 | match parts.len() { 218 | // 没有 MAC 地址的情况 219 | 1 => { 220 | if checksum.len() < 72 { 221 | // 8 + 64 = 72 222 | return false; 223 | } 224 | 225 | // 解码前8个字符的base64时间戳 226 | let timestamp_base64 = &checksum[..8]; 227 | let timestamp = match extract_time_ks(timestamp_base64) { 228 | Some(ts) => ts, 229 | None => return false, 230 | }; 231 | 232 | let current_timestamp = std::time::SystemTime::now() 233 | .duration_since(std::time::UNIX_EPOCH) 234 | .unwrap() 235 | .as_secs() 236 | / 1_000; 237 | 238 | if current_timestamp < timestamp { 239 | return false; 240 | } 241 | 242 | // 验证 device_id hash 部分 243 | is_valid_hash(&checksum[8..]) 244 | } 245 | // 包含 MAC hash 的情况 246 | 2 => { 247 | let first_part = parts[0]; 248 | let mac_hash = parts[1]; 249 | 250 | // MAC hash 必须是64字符的十六进制 251 | if !is_valid_hash(mac_hash) { 252 | return false; 253 | } 254 | 255 | // 检查第一部分比MAC hash多8个字符 256 | if first_part.len() != mac_hash.len() + 8 { 257 | return false; 258 | } 259 | 260 | // 递归验证第一部分 261 | validate_checksum(first_part) 262 | } 263 | _ => false, 264 | } 265 | } 266 | 267 | fn is_valid_hash(hash: &str) -> bool { 268 | if hash.len() < 64 { 269 | return false; 270 | } 271 | 272 | // 检查是否都是有效的十六进制字符 273 | hash.chars().all(|c| c.is_ascii_hexdigit()) 274 | } 275 | -------------------------------------------------------------------------------- /src/common/utils/tokens.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{ 3 | constant::EMPTY_STRING, 4 | model::TokenInfo, 5 | lazy::{TOKEN_FILE, TOKEN_LIST_FILE}, 6 | }, 7 | common::utils::generate_checksum_with_default, 8 | }; 9 | 10 | // 规范化文件内容并写入 11 | fn normalize_and_write(content: &str, file_path: &str) -> String { 12 | let normalized = content.replace("\r\n", "\n"); 13 | if normalized != content { 14 | if let Err(e) = std::fs::write(file_path, &normalized) { 15 | eprintln!("警告: 无法更新规范化的文件: {}", e); 16 | } 17 | } 18 | normalized 19 | } 20 | 21 | // 解析token 22 | fn parse_token(token_part: &str) -> Option { 23 | // 查找最后一个:或%3A的位置 24 | let colon_pos = token_part.rfind(':'); 25 | let encoded_colon_pos = token_part.rfind("%3A"); 26 | 27 | match (colon_pos, encoded_colon_pos) { 28 | (None, None) => Some(token_part.to_string()), 29 | (Some(pos1), None) => Some(token_part[(pos1 + 1)..].to_string()), 30 | (None, Some(pos2)) => Some(token_part[(pos2 + 3)..].to_string()), 31 | (Some(pos1), Some(pos2)) => { 32 | // 取较大的位置作为分隔点 33 | let pos = pos1.max(pos2); 34 | let start = if pos == pos2 { pos + 3 } else { pos + 1 }; 35 | Some(token_part[start..].to_string()) 36 | } 37 | } 38 | } 39 | 40 | // Token 加载函数 41 | pub fn load_tokens() -> Vec { 42 | let token_file = TOKEN_FILE.as_str(); 43 | let token_list_file = TOKEN_LIST_FILE.as_str(); 44 | 45 | // 确保文件存在 46 | for file in [&token_file, &token_list_file] { 47 | if !std::path::Path::new(file).exists() { 48 | if let Err(e) = std::fs::write(file, EMPTY_STRING) { 49 | eprintln!("警告: 无法创建文件 '{}': {}", file, e); 50 | } 51 | } 52 | } 53 | 54 | // 读取和规范化 token 文件 55 | let token_entries = match std::fs::read_to_string(&token_file) { 56 | Ok(content) => { 57 | let normalized = content.replace("\r\n", "\n"); 58 | normalized 59 | .lines() 60 | .filter_map(|line| { 61 | let line = line.trim(); 62 | if line.is_empty() || line.starts_with('#') || !validate_token(line) { 63 | return None; 64 | } 65 | parse_token(line) 66 | }) 67 | .collect::>() 68 | } 69 | Err(e) => { 70 | eprintln!("警告: 无法读取token文件 '{}': {}", token_file, e); 71 | Vec::new() 72 | } 73 | }; 74 | 75 | // 读取和规范化 token-list 文件 76 | let mut token_map: std::collections::HashMap = 77 | match std::fs::read_to_string(&token_list_file) { 78 | Ok(content) => { 79 | let normalized = normalize_and_write(&content, &token_list_file); 80 | normalized 81 | .lines() 82 | .filter_map(|line| { 83 | let line = line.trim(); 84 | if line.is_empty() || line.starts_with('#') { 85 | return None; 86 | } 87 | 88 | let parts: Vec<&str> = line.split(',').collect(); 89 | match parts[..] { 90 | [token_part, checksum] => { 91 | let token = parse_token(token_part)?; 92 | Some((token, checksum.to_string())) 93 | } 94 | _ => { 95 | eprintln!("警告: 忽略无效的token-list行: {}", line); 96 | None 97 | } 98 | } 99 | }) 100 | .collect() 101 | } 102 | Err(e) => { 103 | eprintln!("警告: 无法读取token-list文件: {}", e); 104 | std::collections::HashMap::new() 105 | } 106 | }; 107 | 108 | // 更新或添加新token 109 | for token in token_entries { 110 | if !token_map.contains_key(&token) { 111 | // 为新token生成checksum 112 | let checksum = generate_checksum_with_default(); 113 | token_map.insert(token, checksum); 114 | } 115 | } 116 | 117 | // 更新 token-list 文件 118 | let token_list_content = token_map 119 | .iter() 120 | .map(|(token, checksum)| { 121 | format!("{},{}", token, checksum) 122 | }) 123 | .collect::>() 124 | .join("\n"); 125 | 126 | if let Err(e) = std::fs::write(&token_list_file, token_list_content) { 127 | eprintln!("警告: 无法更新token-list文件: {}", e); 128 | } 129 | 130 | // 转换为 TokenInfo vector 131 | token_map 132 | .into_iter() 133 | .map(|(token, checksum)| TokenInfo { 134 | token: token.clone(), 135 | checksum, 136 | profile: None, 137 | }) 138 | .collect() 139 | } 140 | 141 | use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 142 | use chrono::{DateTime, Local, TimeZone}; 143 | 144 | // 验证jwt token是否有效 145 | pub fn validate_token(token: &str) -> bool { 146 | // 检查 token 格式 147 | let parts: Vec<&str> = token.split('.').collect(); 148 | if parts.len() != 3 { 149 | return false; 150 | } 151 | 152 | if parts[0] != "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" { 153 | return false; 154 | } 155 | 156 | // 解码 payload 157 | let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { 158 | Ok(decoded) => decoded, 159 | Err(_) => return false, 160 | }; 161 | 162 | // 转换为字符串 163 | let payload_str = match String::from_utf8(payload) { 164 | Ok(s) => s, 165 | Err(_) => return false, 166 | }; 167 | 168 | // 解析 JSON 169 | let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { 170 | Ok(v) => v, 171 | Err(_) => return false, 172 | }; 173 | 174 | // 验证必要字段是否存在且有效 175 | let required_fields = ["sub", "time", "randomness", "exp", "iss", "scope", "aud"]; 176 | for field in required_fields { 177 | if !payload_json.get(field).is_some() { 178 | return false; 179 | } 180 | } 181 | 182 | // 验证 time 字段 183 | if let Some(time) = payload_json["time"].as_str() { 184 | // 验证 time 是否为有效的数字字符串 185 | if let Ok(time_value) = time.parse::() { 186 | let current_time = chrono::Utc::now().timestamp(); 187 | if time_value > current_time { 188 | return false; 189 | } 190 | } else { 191 | return false; 192 | } 193 | } else { 194 | return false; 195 | } 196 | 197 | // 验证 randomness 长度 198 | if let Some(randomness) = payload_json["randomness"].as_str() { 199 | if randomness.len() != 18 { 200 | return false; 201 | } 202 | } else { 203 | return false; 204 | } 205 | 206 | // 验证过期时间 207 | if let Some(exp) = payload_json["exp"].as_i64() { 208 | let current_time = chrono::Utc::now().timestamp(); 209 | if current_time > exp { 210 | return false; 211 | } 212 | } else { 213 | return false; 214 | } 215 | 216 | // 验证发行者 217 | if payload_json["iss"].as_str() != Some("https://authentication.cursor.sh") { 218 | return false; 219 | } 220 | 221 | // 验证授权范围 222 | if payload_json["scope"].as_str() != Some("openid profile email offline_access") { 223 | return false; 224 | } 225 | 226 | // 验证受众 227 | if payload_json["aud"].as_str() != Some("https://cursor.com") { 228 | return false; 229 | } 230 | 231 | true 232 | } 233 | 234 | // 从 JWT token 中提取用户 ID 235 | pub fn extract_user_id(token: &str) -> Option { 236 | // JWT token 由3部分组成,用 . 分隔 237 | let parts: Vec<&str> = token.split('.').collect(); 238 | if parts.len() != 3 { 239 | return None; 240 | } 241 | 242 | // 解码 payload (第二部分) 243 | let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { 244 | Ok(decoded) => decoded, 245 | Err(_) => return None, 246 | }; 247 | 248 | // 将 payload 转换为字符串 249 | let payload_str = match String::from_utf8(payload) { 250 | Ok(s) => s, 251 | Err(_) => return None, 252 | }; 253 | 254 | // 解析 JSON 255 | let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { 256 | Ok(v) => v, 257 | Err(_) => return None, 258 | }; 259 | 260 | // 提取 sub 字段 261 | payload_json["sub"] 262 | .as_str() 263 | .map(|s| s.split('|').nth(1).unwrap_or(s).to_string()) 264 | } 265 | 266 | // 从 JWT token 中提取 time 字段 267 | pub fn extract_time(token: &str) -> Option> { 268 | // JWT token 由3部分组成,用 . 分隔 269 | let parts: Vec<&str> = token.split('.').collect(); 270 | if parts.len() != 3 { 271 | return None; 272 | } 273 | 274 | // 解码 payload (第二部分) 275 | let payload = match URL_SAFE_NO_PAD.decode(parts[1]) { 276 | Ok(decoded) => decoded, 277 | Err(_) => return None, 278 | }; 279 | 280 | // 将 payload 转换为字符串 281 | let payload_str = match String::from_utf8(payload) { 282 | Ok(s) => s, 283 | Err(_) => return None, 284 | }; 285 | 286 | // 解析 JSON 287 | let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) { 288 | Ok(v) => v, 289 | Err(_) => return None, 290 | }; 291 | 292 | // 提取时间戳并转换为本地时间 293 | payload_json["time"] 294 | .as_str() 295 | .and_then(|t| t.parse::().ok()) 296 | .and_then(|timestamp| Local.timestamp_opt(timestamp, 0).single()) 297 | } 298 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod chat; 3 | mod common; 4 | 5 | use app::{ 6 | config::handle_config_update, 7 | constant::{ 8 | EMPTY_STRING, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, 9 | ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_TOKENINFO_PATH, 10 | ROUTE_GET_USER_INFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, 11 | ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH, 12 | }, 13 | lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH}, 14 | model::*, 15 | }; 16 | use axum::{ 17 | routing::{get, post}, 18 | Router, 19 | }; 20 | use chat::{ 21 | route::{ 22 | get_user_info, handle_about, handle_api_page, handle_basic_calibration, handle_config_page, 23 | handle_env_example, handle_get_checksum, handle_get_tokeninfo, handle_health, handle_logs, 24 | handle_logs_post, handle_readme, handle_root, handle_static, handle_tokeninfo_page, 25 | handle_update_tokeninfo, handle_update_tokeninfo_post, 26 | }, 27 | service::{handle_chat, handle_models}, 28 | }; 29 | use common::utils::{ 30 | load_tokens, parse_bool_from_env, parse_string_from_env, parse_usize_from_env, 31 | }; 32 | use std::sync::Arc; 33 | use tokio::sync::Mutex; 34 | use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer}; 35 | 36 | #[tokio::main] 37 | async fn main() { 38 | // 设置自定义 panic hook 39 | std::panic::set_hook(Box::new(|info| { 40 | // std::env::set_var("RUST_BACKTRACE", "1"); 41 | if let Some(msg) = info.payload().downcast_ref::() { 42 | eprintln!("{}", msg); 43 | } else if let Some(msg) = info.payload().downcast_ref::<&str>() { 44 | eprintln!("{}", msg); 45 | } 46 | })); 47 | 48 | // 加载环境变量 49 | dotenvy::dotenv().ok(); 50 | 51 | if AUTH_TOKEN.is_empty() { 52 | panic!("AUTH_TOKEN must be set") 53 | }; 54 | 55 | // 初始化全局配置 56 | AppConfig::init( 57 | parse_bool_from_env("ENABLE_STREAM_CHECK", true), 58 | parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true), 59 | VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)), 60 | parse_bool_from_env("ENABLE_SLOW_POOL", false), 61 | parse_bool_from_env("PASS_ANY_CLAUDE", false), 62 | ); 63 | 64 | // 加载 tokens 65 | let token_infos = load_tokens(); 66 | 67 | // 初始化应用状态 68 | let state = Arc::new(Mutex::new(AppState::new(token_infos))); 69 | 70 | // 设置路由 71 | let app = Router::new() 72 | .route(ROUTE_ROOT_PATH, get(handle_root)) 73 | .route(ROUTE_HEALTH_PATH, get(handle_health)) 74 | .route(ROUTE_TOKENINFO_PATH, get(handle_tokeninfo_page)) 75 | .route(ROUTE_MODELS_PATH.as_str(), get(handle_models)) 76 | .route(ROUTE_GET_CHECKSUM, get(handle_get_checksum)) 77 | .route(ROUTE_UPDATE_TOKENINFO_PATH, get(handle_update_tokeninfo)) 78 | .route(ROUTE_GET_TOKENINFO_PATH, post(handle_get_tokeninfo)) 79 | .route( 80 | ROUTE_UPDATE_TOKENINFO_PATH, 81 | post(handle_update_tokeninfo_post), 82 | ) 83 | .route(ROUTE_CHAT_PATH.as_str(), post(handle_chat)) 84 | .route(ROUTE_LOGS_PATH, get(handle_logs)) 85 | .route(ROUTE_LOGS_PATH, post(handle_logs_post)) 86 | .route(ROUTE_ENV_EXAMPLE_PATH, get(handle_env_example)) 87 | .route(ROUTE_CONFIG_PATH, get(handle_config_page)) 88 | .route(ROUTE_CONFIG_PATH, post(handle_config_update)) 89 | .route(ROUTE_STATIC_PATH, get(handle_static)) 90 | .route(ROUTE_ABOUT_PATH, get(handle_about)) 91 | .route(ROUTE_README_PATH, get(handle_readme)) 92 | .route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration)) 93 | .route(ROUTE_GET_USER_INFO_PATH, post(get_user_info)) 94 | .route(ROUTE_API_PATH, get(handle_api_page)) 95 | .layer(RequestBodyLimitLayer::new( 96 | 1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2), 97 | )) 98 | .layer(CorsLayer::permissive()) 99 | .with_state(state); 100 | 101 | // 启动服务器 102 | let port = parse_string_from_env("PORT", "3000"); 103 | let addr = format!("0.0.0.0:{}", port); 104 | println!("服务器运行在端口 {}", port); 105 | println!("当前版本: v{}", PKG_VERSION); 106 | 107 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 108 | axum::serve(listener, app).await.unwrap(); 109 | } 110 | -------------------------------------------------------------------------------- /static/api.min.html: -------------------------------------------------------------------------------- 1 | API 管理

API 管理

Healthy
-------------------------------------------------------------------------------- /static/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 配置管理 9 | 10 | 11 | 12 | 13 | 14 | 15 |

配置管理

16 | 17 |
18 |
19 | 20 | 31 |
32 | 33 |
34 | 35 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 54 |
55 | 56 |
57 | 58 | 63 |
64 | 65 |
66 | 67 | 73 |
74 | 75 |
76 | 77 | 82 |
83 | 84 |
85 | 86 | 91 |
92 | 93 |
94 | 95 | 102 | 103 |
104 | 105 |
106 | 107 | 108 |
109 | 110 |
111 | 112 | 113 | 114 |
115 |
116 | 117 |
118 | 119 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /static/config.min.html: -------------------------------------------------------------------------------- 1 | 配置管理

配置管理

-------------------------------------------------------------------------------- /static/shared-styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* 基础颜色变量 */ 3 | --primary-color: #2196F3; 4 | --primary-dark: #1976D2; 5 | --primary-color-alpha: rgba(33, 150, 243, 0.1); 6 | --success-color: #4CAF50; 7 | --error-color: #F44336; 8 | --background-color: #F5F5F5; 9 | --card-background: #FFFFFF; 10 | --text-primary: #333333; 11 | --text-secondary: #757575; 12 | --border-color: #e0e0e0; 13 | --disabled-bg: #f5f5f5; 14 | 15 | /* 布局变量 */ 16 | --border-radius: 8px; 17 | --spacing: 20px; 18 | 19 | /* 动画变量 */ 20 | --transition-fast: 0.2s; 21 | --transition-slow: 0.3s; 22 | } 23 | 24 | /* 暗色模式 */ 25 | @media (prefers-color-scheme: dark) { 26 | :root { 27 | --primary-color: #90CAF9; 28 | --primary-dark: #64B5F6; 29 | --background-color: #121212; 30 | --card-background: #1e1e1e; 31 | --text-primary: #e0e0e0; 32 | --text-secondary: #9e9e9e; 33 | --border-color: #404040; 34 | --disabled-bg: #2d2d2d; 35 | color-scheme: dark; 36 | } 37 | } 38 | 39 | /* 基础样式 */ 40 | html { 41 | scroll-behavior: smooth; 42 | box-sizing: border-box; 43 | } 44 | 45 | *, 46 | *:before, 47 | *:after { 48 | box-sizing: inherit; 49 | } 50 | 51 | body { 52 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 53 | max-width: 1200px; 54 | margin: 0 auto; 55 | padding: var(--spacing); 56 | background: var(--background-color); 57 | color: var(--text-primary); 58 | line-height: 1.6; 59 | } 60 | 61 | /* 容器样式 */ 62 | .container { 63 | background: var(--card-background); 64 | padding: var(--spacing); 65 | border-radius: var(--border-radius); 66 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 67 | margin-bottom: var(--spacing); 68 | transition: transform var(--transition-fast); 69 | } 70 | 71 | .container:hover { 72 | transform: translateY(-2px); 73 | } 74 | 75 | /* 标题样式 */ 76 | h1, 77 | h2, 78 | h3 { 79 | color: var(--text-primary); 80 | margin-top: 0; 81 | line-height: 1.2; 82 | } 83 | 84 | /* 表单元素样式 */ 85 | .form-group { 86 | margin-bottom: 20px; 87 | } 88 | 89 | /* 标签样式 */ 90 | label { 91 | display: block; 92 | margin-bottom: 8px; 93 | font-weight: 500; 94 | color: var(--text-primary); 95 | } 96 | 97 | input, 98 | select, 99 | textarea, 100 | .form-control { 101 | width: 100%; 102 | padding: 10px 12px; 103 | border: 1px solid var(--border-color); 104 | border-radius: 4px; 105 | background: var(--card-background); 106 | color: var(--text-primary); 107 | font-size: 14px; 108 | line-height: 1.5; 109 | transition: all var(--transition-fast); 110 | appearance: none; 111 | } 112 | 113 | input[type="checkbox"] { 114 | width: auto; 115 | margin-right: 8px; 116 | cursor: pointer; 117 | appearance: auto; 118 | } 119 | 120 | input[type="checkbox"] + label { 121 | cursor: pointer; 122 | color: var(--text-primary); 123 | user-select: none; 124 | } 125 | 126 | input:hover, 127 | select:hover, 128 | textarea:hover, 129 | .form-control:hover { 130 | border-color: var(--primary-color); 131 | } 132 | 133 | input:focus, 134 | select:focus, 135 | textarea:focus, 136 | .form-control:focus { 137 | border-color: var(--primary-color); 138 | box-shadow: 0 0 0 2px var(--primary-color-alpha); 139 | outline: none; 140 | } 141 | 142 | /* 禁用状态 */ 143 | input:disabled, 144 | select:disabled, 145 | textarea:disabled, 146 | .form-control:disabled { 147 | background-color: var(--disabled-bg); 148 | border-color: var(--border-color); 149 | cursor: not-allowed; 150 | opacity: 0.7; 151 | } 152 | 153 | /* 错误状态 */ 154 | input.error, 155 | select.error, 156 | textarea.error, 157 | .form-control.error { 158 | border-color: var(--error-color); 159 | } 160 | 161 | /* Select 特殊样式 */ 162 | select { 163 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23757575'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E"); 164 | background-repeat: no-repeat; 165 | background-position: right 8px center; 166 | background-size: 20px; 167 | padding-right: 36px; 168 | } 169 | 170 | /* Textarea 特殊样式 */ 171 | textarea { 172 | min-height: 150px; 173 | resize: vertical; 174 | font-family: monospace; 175 | line-height: 1.4; 176 | } 177 | 178 | /* 按钮基础样式 */ 179 | button { 180 | display: inline-flex; 181 | align-items: center; 182 | justify-content: center; 183 | min-height: 44px; 184 | padding: 8px 24px; 185 | border: none; 186 | border-radius: var(--border-radius); 187 | background: var(--primary-color); 188 | color: white; 189 | font-size: 16px; 190 | font-weight: 500; 191 | text-align: center; 192 | text-decoration: none; 193 | cursor: pointer; 194 | transition: all var(--transition-fast); 195 | user-select: none; 196 | -webkit-tap-highlight-color: transparent; 197 | } 198 | 199 | /* 按钮状态 */ 200 | button:hover { 201 | background: var(--primary-dark); 202 | transform: translateY(-1px); 203 | box-shadow: 0 4px 12px var(--primary-color-alpha); 204 | } 205 | 206 | button:active { 207 | transform: translateY(1px); 208 | } 209 | 210 | button:disabled { 211 | background: var(--disabled-bg); 212 | color: var(--text-secondary); 213 | cursor: not-allowed; 214 | transform: none; 215 | box-shadow: none; 216 | } 217 | 218 | /* 次要按钮样式 */ 219 | button.secondary { 220 | background: var(--text-secondary); 221 | } 222 | 223 | /* 按钮组 */ 224 | .button-group { 225 | display: flex; 226 | gap: 10px; 227 | margin: var(--spacing) 0; 228 | } 229 | 230 | /* 消息提示 */ 231 | .message { 232 | padding: 12px; 233 | border-radius: var(--border-radius); 234 | margin: 10px 0; 235 | border: 1px solid transparent; 236 | } 237 | 238 | .success { 239 | background: var(--success-color); 240 | color: #fff; 241 | } 242 | 243 | .error { 244 | background: var(--error-color); 245 | color: #fff; 246 | } 247 | 248 | /* 表格样式 */ 249 | table { 250 | width: 100%; 251 | border-collapse: collapse; 252 | margin-top: var(--spacing); 253 | background: var(--card-background); 254 | border-radius: var(--border-radius); 255 | overflow: hidden; 256 | } 257 | 258 | th, 259 | td { 260 | padding: 12px; 261 | text-align: left; 262 | border-bottom: 1px solid var(--text-secondary); 263 | } 264 | 265 | th { 266 | background: var(--primary-color); 267 | color: white; 268 | font-weight: 500; 269 | } 270 | 271 | tr:nth-child(even) { 272 | background: rgba(0, 0, 0, 0.02); 273 | } 274 | 275 | tr:hover { 276 | background: rgba(0, 0, 0, 0.04); 277 | } 278 | 279 | /* 辅助类 */ 280 | .visually-hidden { 281 | position: absolute; 282 | width: 1px; 283 | height: 1px; 284 | padding: 0; 285 | margin: -1px; 286 | overflow: hidden; 287 | clip: rect(0, 0, 0, 0); 288 | border: 0; 289 | } 290 | 291 | .text-center { 292 | text-align: center; 293 | } 294 | 295 | .help-text { 296 | margin-top: 4px; 297 | font-size: 14px; 298 | color: var(--text-secondary); 299 | } 300 | 301 | .error-text { 302 | color: var(--error-color); 303 | } 304 | 305 | .mt-0 { 306 | margin-top: 0; 307 | } 308 | 309 | .mb-0 { 310 | margin-bottom: 0; 311 | } 312 | 313 | /* 响应式设计 */ 314 | @media (max-width: 768px) { 315 | :root { 316 | --spacing: 16px; 317 | } 318 | 319 | body { 320 | padding: 10px; 321 | } 322 | 323 | .button-group { 324 | flex-direction: column; 325 | } 326 | 327 | button { 328 | width: 100%; 329 | padding: 12px 20px; 330 | } 331 | 332 | input, 333 | select, 334 | textarea, 335 | .form-control { 336 | font-size: 16px; 337 | padding: 14px 16px; 338 | } 339 | 340 | table { 341 | display: block; 342 | overflow-x: auto; 343 | -webkit-overflow-scrolling: touch; 344 | } 345 | 346 | th, 347 | td { 348 | white-space: nowrap; 349 | } 350 | } -------------------------------------------------------------------------------- /static/shared-styles.min.css: -------------------------------------------------------------------------------- 1 | :root{--primary-color:#2196F3;--primary-dark:#1976D2;--primary-color-alpha:rgba(33, 150, 243, 0.1);--success-color:#4CAF50;--error-color:#F44336;--background-color:#F5F5F5;--card-background:#FFFFFF;--text-primary:#333333;--text-secondary:#757575;--border-color:#e0e0e0;--disabled-bg:#f5f5f5;--border-radius:8px;--spacing:20px;--transition-fast:0.2s;--transition-slow:0.3s}@media (prefers-color-scheme:dark){:root{--primary-color:#90CAF9;--primary-dark:#64B5F6;--background-color:#121212;--card-background:#1e1e1e;--text-primary:#e0e0e0;--text-secondary:#9e9e9e;--border-color:#404040;--disabled-bg:#2d2d2d;color-scheme:dark}}html{scroll-behavior:smooth;box-sizing:border-box}*,:after,:before{box-sizing:inherit}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:var(--text-primary);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,.1);margin-bottom:var(--spacing);transition:transform var(--transition-fast)}.container:hover{transform:translateY(-2px)}h1,h2,h3{color:var(--text-primary);margin-top:0;line-height:1.2}.form-group{margin-bottom:20px}label{display:block;margin-bottom:8px;font-weight:500;color:var(--text-primary)}.form-control,input,select,textarea{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:4px;background:var(--card-background);color:var(--text-primary);font-size:14px;line-height:1.5;transition:all var(--transition-fast);appearance:none}input[type=checkbox]{width:auto;margin-right:8px;cursor:pointer;appearance:auto}input[type=checkbox]+label{cursor:pointer;color:var(--text-primary);user-select:none}.form-control:hover,input:hover,select:hover,textarea:hover{border-color:var(--primary-color)}.form-control:focus,input:focus,select:focus,textarea:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px var(--primary-color-alpha);outline:0}.form-control:disabled,input:disabled,select:disabled,textarea:disabled{background-color:var(--disabled-bg);border-color:var(--border-color);cursor:not-allowed;opacity:.7}.form-control.error,input.error,select.error,textarea.error{border-color:var(--error-color)}select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23757575'%3E%3Cpath d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:20px;padding-right:36px}textarea{min-height:150px;resize:vertical;font-family:monospace;line-height:1.4}button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:8px 24px;border:none;border-radius:var(--border-radius);background:var(--primary-color);color:#fff;font-size:16px;font-weight:500;text-align:center;text-decoration:none;cursor:pointer;transition:all var(--transition-fast);user-select:none;-webkit-tap-highlight-color:transparent}button:hover{background:var(--primary-dark);transform:translateY(-1px);box-shadow:0 4px 12px var(--primary-color-alpha)}button:active{transform:translateY(1px)}button:disabled{background:var(--disabled-bg);color:var(--text-secondary);cursor:not-allowed;transform:none;box-shadow:none}button.secondary{background:var(--text-secondary)}.button-group{display:flex;gap:10px;margin:var(--spacing) 0}.message{padding:12px;border-radius:var(--border-radius);margin:10px 0;border:1px solid transparent}.success{background:var(--success-color);color:#fff}.error{background:var(--error-color);color:#fff}table{width:100%;border-collapse:collapse;margin-top:var(--spacing);background:var(--card-background);border-radius:var(--border-radius);overflow:hidden}td,th{padding:12px;text-align:left;border-bottom:1px solid var(--text-secondary)}th{background:var(--primary-color);color:#fff;font-weight:500}tr:nth-child(2n){background:rgba(0,0,0,.02)}tr:hover{background:rgba(0,0,0,.04)}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.text-center{text-align:center}.help-text{margin-top:4px;font-size:14px;color:var(--text-secondary)}.error-text{color:var(--error-color)}.mt-0{margin-top:0}.mb-0{margin-bottom:0}@media (max-width:768px){:root{--spacing:16px}body{padding:10px}.button-group{flex-direction:column}button{width:100%;padding:12px 20px}.form-control,input,select,textarea{font-size:16px;padding:14px 16px}table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}td,th{white-space:nowrap}} -------------------------------------------------------------------------------- /static/shared.js: -------------------------------------------------------------------------------- 1 | // Token 管理功能 2 | function saveAuthToken(token) { 3 | const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期 4 | localStorage.setItem('authToken', token); 5 | localStorage.setItem('authTokenExpiry', expiryTime); 6 | } 7 | 8 | function getAuthToken() { 9 | const token = localStorage.getItem('authToken'); 10 | const expiry = localStorage.getItem('authTokenExpiry'); 11 | 12 | if (!token || !expiry) { 13 | return null; 14 | } 15 | 16 | if (new Date().getTime() > parseInt(expiry)) { 17 | localStorage.removeItem('authToken'); 18 | localStorage.removeItem('authTokenExpiry'); 19 | return null; 20 | } 21 | 22 | return token; 23 | } 24 | 25 | // 消息显示功能 26 | function showMessage(elementId, text, isError = false) { 27 | const msg = document.getElementById(elementId); 28 | msg.className = `message ${isError ? 'error' : 'success'}`; 29 | msg.textContent = text; 30 | } 31 | 32 | function showGlobalMessage(text, isError = false) { 33 | showMessage('message', text, isError); 34 | // 3秒后自动清除消息 35 | setTimeout(() => { 36 | const msg = document.getElementById('message'); 37 | msg.textContent = ''; 38 | msg.className = 'message'; 39 | }, 3000); 40 | } 41 | 42 | // Token 输入框自动填充和事件绑定 43 | function initializeTokenHandling(inputId) { 44 | document.addEventListener('DOMContentLoaded', () => { 45 | const authToken = getAuthToken(); 46 | if (authToken) { 47 | document.getElementById(inputId).value = authToken; 48 | } 49 | }); 50 | 51 | document.getElementById(inputId).addEventListener('change', (e) => { 52 | if (e.target.value) { 53 | saveAuthToken(e.target.value); 54 | } else { 55 | localStorage.removeItem('authToken'); 56 | localStorage.removeItem('authTokenExpiry'); 57 | } 58 | }); 59 | } 60 | 61 | // API 请求通用处理 62 | async function makeAuthenticatedRequest(url, options = {}) { 63 | const tokenId = options.tokenId || 'authToken'; 64 | const token = document.getElementById(tokenId).value; 65 | 66 | if (!token) { 67 | showGlobalMessage('请输入 AUTH_TOKEN', true); 68 | return null; 69 | } 70 | 71 | const defaultOptions = { 72 | method: 'POST', 73 | headers: { 74 | 'Authorization': `Bearer ${token}`, 75 | 'Content-Type': 'application/json' 76 | } 77 | }; 78 | 79 | try { 80 | const response = await fetch(url, { ...defaultOptions, ...options }); 81 | 82 | if (!response.ok) { 83 | throw new Error(`HTTP error! status: ${response.status}`); 84 | } 85 | 86 | return await response.json(); 87 | } catch (error) { 88 | showGlobalMessage(`请求失败: ${error.message}`, true); 89 | return null; 90 | } 91 | } 92 | 93 | /** 94 | * 从字符串解析布尔值 95 | * @param {string} str - 要解析的字符串 96 | * @param {boolean|null} defaultValue - 解析失败时的默认值 97 | * @returns {boolean|null} 解析结果,如果无法解析则返回默认值 98 | */ 99 | function parseBooleanFromString(str, defaultValue = null) { 100 | if (typeof str !== 'string') { 101 | return defaultValue; 102 | } 103 | 104 | const lowercaseStr = str.toLowerCase().trim(); 105 | 106 | if (lowercaseStr === 'true' || lowercaseStr === '1') { 107 | return true; 108 | } else if (lowercaseStr === 'false' || lowercaseStr === '0') { 109 | return false; 110 | } else { 111 | return defaultValue; 112 | } 113 | } 114 | 115 | /** 116 | * 将布尔值转换为字符串 117 | * @param {boolean|undefined|null} value - 要转换的布尔值 118 | * @param {string} defaultValue - 转换失败时的默认值 119 | * @returns {string} 转换结果,如果输入无效则返回默认值 120 | */ 121 | function parseStringFromBoolean(value, defaultValue = null) { 122 | if (typeof value !== 'boolean') { 123 | return defaultValue; 124 | } 125 | 126 | return value ? 'true' : 'false'; 127 | } 128 | 129 | /** 130 | * 解析对话内容 131 | * @param {string} promptStr - 原始prompt字符串 132 | * @returns {Array<{role: string, content: string}>} 解析后的对话数组 133 | */ 134 | function parsePrompt(promptStr) { 135 | if (!promptStr) return []; 136 | 137 | const messages = []; 138 | const lines = promptStr.split('\n'); 139 | let currentRole = ''; 140 | let currentContent = ''; 141 | 142 | const roleMap = { 143 | 'BEGIN_SYSTEM': 'system', 144 | 'BEGIN_USER': 'user', 145 | 'BEGIN_ASSISTANT': 'assistant' 146 | }; 147 | 148 | for (let i = 0; i < lines.length; i++) { 149 | const line = lines[i]; 150 | 151 | // 检查是否是角色标记行 152 | let foundRole = false; 153 | for (const [marker, role] of Object.entries(roleMap)) { 154 | if (line.includes(marker)) { 155 | // 保存之前的消息(如果有) 156 | if (currentRole && currentContent.trim()) { 157 | messages.push({ 158 | role: currentRole, 159 | content: currentContent.trim() 160 | }); 161 | } 162 | // 设置新角色 163 | currentRole = role; 164 | currentContent = ''; 165 | foundRole = true; 166 | break; 167 | } 168 | } 169 | 170 | // 如果不是角色标记行且不是END标记行,则添加到当前内容 171 | if (!foundRole && !line.includes('END_')) { 172 | currentContent += line + '\n'; 173 | } 174 | } 175 | 176 | // 添加最后一条消息 177 | if (currentRole && currentContent.trim()) { 178 | messages.push({ 179 | role: currentRole, 180 | content: currentContent.trim() 181 | }); 182 | } 183 | 184 | return messages; 185 | } 186 | 187 | /** 188 | * 格式化对话内容为HTML表格 189 | * @param {Array<{role: string, content: string}>} messages - 对话消息数组 190 | * @returns {string} HTML表格字符串 191 | */ 192 | function formatPromptToTable(messages) { 193 | if (!messages || messages.length === 0) { 194 | return '

无对话内容

'; 195 | } 196 | 197 | const roleLabels = { 198 | 'system': '系统', 199 | 'user': '用户', 200 | 'assistant': '助手' 201 | }; 202 | 203 | function escapeHtml(content) { 204 | // 先转义HTML特殊字符 205 | const escaped = content 206 | .replace(/&/g, '&') 207 | .replace(//g, '>') 209 | .replace(/"/g, '"') 210 | .replace(/'/g, '''); 211 | 212 | // 将HTML标签文本用引号包裹,使其更易读 213 | // return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"'); 214 | return escaped; 215 | } 216 | 217 | return `${messages.map(msg => ``).join('')}
角色内容
${roleLabels[msg.role] || msg.role}${escapeHtml(msg.content).replace(/\n/g, '
')}
`; 218 | } 219 | 220 | /** 221 | * 安全地显示prompt对话框 222 | * @param {string} promptStr - 原始prompt字符串 223 | */ 224 | function showPromptModal(promptStr) { 225 | try { 226 | const modal = document.getElementById('promptModal'); 227 | const content = document.getElementById('promptContent'); 228 | 229 | if (!modal || !content) { 230 | console.error('Modal elements not found'); 231 | return; 232 | } 233 | 234 | const messages = parsePrompt(promptStr); 235 | content.innerHTML = formatPromptToTable(messages); 236 | modal.style.display = 'block'; 237 | } catch (e) { 238 | console.error('显示prompt对话框失败:', e); 239 | console.error('原始prompt:', promptStr); 240 | } 241 | } -------------------------------------------------------------------------------- /static/shared.min.js: -------------------------------------------------------------------------------- 1 | function saveAuthToken(e){const t=(new Date).getTime()+864e5;localStorage.setItem("authToken",e),localStorage.setItem("authTokenExpiry",t)}function getAuthToken(){const e=localStorage.getItem("authToken"),t=localStorage.getItem("authTokenExpiry");return e&&t?(new Date).getTime()>parseInt(t)?(localStorage.removeItem("authToken"),localStorage.removeItem("authTokenExpiry"),null):e:null}function showMessage(e,t,o=!1){const n=document.getElementById(e);n.className="message "+(o?"error":"success"),n.textContent=t}function showGlobalMessage(e,t=!1){showMessage("message",e,t),setTimeout((()=>{const e=document.getElementById("message");e.textContent="",e.className="message"}),3e3)}function initializeTokenHandling(e){document.addEventListener("DOMContentLoaded",(()=>{const t=getAuthToken();t&&(document.getElementById(e).value=t)})),document.getElementById(e).addEventListener("change",(e=>{e.target.value?saveAuthToken(e.target.value):(localStorage.removeItem("authToken"),localStorage.removeItem("authTokenExpiry"))}))}async function makeAuthenticatedRequest(e,t={}){const o=t.tokenId||"authToken",n=document.getElementById(o).value;if(!n)return showGlobalMessage("请输入 AUTH_TOKEN",!0),null;const r={method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"}};try{const o=await fetch(e,{...r,...t});if(!o.ok)throw new Error(`HTTP error! status: ${o.status}`);return await o.json()}catch(e){return showGlobalMessage(`请求失败: ${e.message}`,!0),null}}function parseBooleanFromString(e,t=null){if("string"!=typeof e)return t;const o=e.toLowerCase().trim();return"true"===o||"1"===o||"false"!==o&&"0"!==o&&t}function parseStringFromBoolean(e,t=null){return"boolean"!=typeof e?t:e?"true":"false"}function parsePrompt(e){if(!e)return[];const t=[],o=e.split("\n");let n="",r="";const a={BEGIN_SYSTEM:"system",BEGIN_USER:"user",BEGIN_ASSISTANT:"assistant"};for(let e=0;e无对话内容

";const t={system:"系统",user:"用户",assistant:"助手"};return`${e.map((e=>{return``;var o})).join("")}
角色内容
${t[e.role]||e.role}${(o=e.content,o.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")).replace(/\n/g,"
")}
`}function showPromptModal(e){try{const t=document.getElementById("promptModal"),o=document.getElementById("promptContent");if(!t||!o)return void console.error("Modal elements not found");const n=parsePrompt(e);o.innerHTML=formatPromptToTable(n),t.style.display="block"}catch(t){console.error("显示prompt对话框失败:",t),console.error("原始prompt:",e)}} -------------------------------------------------------------------------------- /static/tokeninfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Token 信息管理 9 | 10 | 11 | 12 | 41 | 42 | 43 | 44 |

Token 信息管理

45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 |
54 |
55 |

Token 配置

56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 | 64 |
65 | 66 |
67 | 68 | 69 |
70 | 71 |
72 | 快捷键: Ctrl + S 保存更改 73 |
74 |
75 |
76 | 77 |
78 | 79 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /static/tokeninfo.min.html: -------------------------------------------------------------------------------- 1 | Token 信息管理

Token 信息管理

Token 配置

快捷键: Ctrl + S 保存更改
--------------------------------------------------------------------------------