first commit

This commit is contained in:
wisdgod
2024-12-23 08:57:28 +08:00
commit 3ab975a5b3
24 changed files with 5037 additions and 0 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
PORT=3000
ROUTE_PREFIX=
AUTH_TOKEN=
TOKEN_FILE=.token
TOKEN_LIST_FILE=.token-list

263
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,263 @@
name: Build
on:
push:
tags:
- 'v*'
jobs:
build:
name: Build ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
targets: x86_64-unknown-linux-gnu
- os: windows-latest
targets: x86_64-pc-windows-msvc
- os: macos-latest
targets: x86_64-apple-darwin,aarch64-apple-darwin
steps:
- uses: actions/checkout@v4.2.2
- name: Setup Node.js
uses: actions/setup-node@v4.1.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'scripts/package.json'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.targets }}
- name: Install Linux dependencies (x86_64)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
protobuf-compiler \
pkg-config \
libssl-dev \
openssl
# 安装 npm 依赖
cd scripts && npm install && cd ..
# 设置 OpenSSL 环境变量
echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=/usr/include/openssl" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
# - name: Set up Docker Buildx
# if: runner.os == 'Linux'
# uses: docker/setup-buildx-action@v3.8.0
# - name: Build Linux arm64
# if: runner.os == 'Linux'
# run: |
# # 启用 QEMU 支持
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# # 创建 Dockerfile
# cat > Dockerfile.arm64 << 'EOF'
# FROM arm64v8/ubuntu:22.04
# ENV DEBIAN_FRONTEND=noninteractive
# RUN apt-get update && apt-get install -y \
# build-essential \
# curl \
# pkg-config \
# libssl-dev \
# protobuf-compiler \
# nodejs \
# npm \
# git \
# && rm -rf /var/lib/apt/lists/*
# # 安装 Rust
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# ENV PATH="/root/.cargo/bin:${PATH}"
# WORKDIR /build
# COPY . .
# # 安装 npm 依赖
# RUN cd scripts && npm install && cd ..
# # 构建动态链接版本
# RUN cargo build --release
# # 构建静态链接版本
# RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --release
# EOF
# # 构建 arm64 版本
# docker buildx build --platform linux/arm64 -f Dockerfile.arm64 -t builder-arm64 .
# # 创建临时容器
# docker create --name temp-container builder-arm64 sh
# # 复制动态链接版本
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-aarch64-unknown-linux-gnu
# # 复制静态链接版本
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-static-aarch64-unknown-linux-gnu
# # 清理临时容器
# docker rm temp-container
- name: Build Linux x86_64 (Dynamic)
if: runner.os == 'Linux'
run: bash scripts/build.sh
- name: Build Linux x86_64 (Static)
if: runner.os == 'Linux'
run: bash scripts/build.sh --static
- name: Install macOS dependencies
if: runner.os == 'macOS'
run: |
brew install \
protobuf \
pkg-config \
openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Windows dependencies
if: runner.os == 'Windows'
run: |
choco install -y protoc
choco install -y openssl
echo "OPENSSL_DIR=C:/Program Files/OpenSSL-Win64" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=C:/Program Files/OpenSSL-Win64/lib/pkgconfig" >> $GITHUB_ENV
cd scripts && npm install && cd ..
- name: Build (Dynamic)
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
run: bash scripts/build.sh
- name: Build (Static)
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
run: bash scripts/build.sh --static
- name: Upload artifacts
uses: actions/upload-artifact@v4.5.0
with:
name: binaries-${{ matrix.os }}
path: release/*
retention-days: 1
build-freebsd:
name: Build FreeBSD
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Build on FreeBSD
uses: vmactions/freebsd-vm@v1.1.5
with:
usesh: true
prepare: |
# 设置持久化的环境变量
echo 'export SSL_CERT_FILE=/etc/ssl/cert.pem' >> /root/.profile
echo 'export PATH="/usr/local/bin:$PATH"' >> /root/.profile
# 安装基础依赖
pkg update
pkg install -y \
git \
curl \
node20 \
www/npm \
protobuf \
ca_root_nss \
bash \
gmake \
pkgconf \
openssl
export SSL_CERT_FILE=/etc/ssl/cert.pem
# 克隆代码(确保在正确的目录)
cd /root
git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
# 然后再进入 scripts 目录
cd scripts && npm install && cd ..
# 安装 rustup 和 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
# 设置持久化的 Rust 环境变量
echo '. "$HOME/.cargo/env"' >> /root/.profile
# 添加所需的目标支持
. /root/.profile
rustup target add x86_64-unknown-freebsd
rustup component add rust-src
run: |
# 加载环境变量
. /root/.profile
# 构建
echo "构建动态链接版本..."
/usr/local/bin/bash scripts/build.sh
echo "构建静态链接版本..."
/usr/local/bin/bash scripts/build.sh --static
- name: Upload artifacts
uses: actions/upload-artifact@v4.5.0
with:
name: binaries-freebsd
path: release/*
retention-days: 1
release:
name: Create Release
needs: [build, build-freebsd]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4.2.2
- name: Download all artifacts
uses: actions/download-artifact@v4.1.8
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir release
cd artifacts
for dir in binaries-*; do
cp -r "$dir"/* ../release/
done
- name: Generate checksums
run: |
cd release
sha256sum * > SHA256SUMS.txt
- name: Create Release
uses: softprops/action-gh-release@v2.2.0
with:
files: |
release/*
draft: false
prerelease: false
generate_release_notes: true
fail_on_unmatched_files: true

56
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Docker Build and Push
on:
workflow_dispatch:
# push:
# tags:
# - 'v*'
env:
IMAGE_NAME: ${{ github.repository_owner }}/cursor-api
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
- name: Get version from Cargo.toml
if: github.event_name == 'workflow_dispatch'
id: cargo_version
run: |
VERSION=$(grep '^version = ' Cargo.toml | cut -d '"' -f2)
echo "version=v${VERSION}" >> $GITHUB_OUTPUT
- name: Log in to Docker Hub
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5.6.1
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ steps.cargo_version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=ref,event=tag,enable=${{ github.event_name == 'push' }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0
with:
driver-opts: |
image=moby/buildkit:latest
- name: Build and push Docker image
uses: docker/build-push-action@v6.10.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
/target
/get-token/target
/*.log
/*.env
/static/tokeninfo.min.html
node_modules
.DS_Store
/.vscode
/.cargo
/.token
/.token-list
/cursor-api
/cursor-api.exe
/release

2086
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

50
Cargo.toml Normal file
View File

@@ -0,0 +1,50 @@
[package]
name = "cursor-api"
version = "0.1.0"
edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"]
[build-dependencies]
prost-build = "0.13.4"
[dependencies]
axum = { version = "0.7.9", features = ["json"] }
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
bytes = "1.9.0"
chrono = { version = "0.4.39", features = ["serde"] }
dotenvy = "0.15.7"
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
prost = "0.13.4"
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
reqwest = { version = "0.12.9", features = ["json", "gzip", "stream"] }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = { version = "1.0.134", features = ["std"] }
sha2 = { version = "0.10.8", default-features = false }
tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
tokio-stream = { version = "0.1.17", features = ["time"] }
tower-http = { version = "0.6.2", features = ["cors"] }
uuid = { version = "1.11.0", features = ["v4"] }
# 优化设置
[profile.release]
lto = true # 启用链接时优化
codegen-units = 1 # 减少并行编译单元以提高优化
panic = 'abort' # 在 panic 时直接终止,减小二进制大小
strip = true # 移除调试符号
opt-level = 3 # 最高优化级别
# 构建脚本设置
[package.metadata.cross.target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main"
[package.metadata.cross.target.aarch64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main"
[package.metadata.cross.target.x86_64-apple-darwin]
image = "ghcr.io/cross-rs/x86_64-apple-darwin:main"
[package.metadata.cross.target.aarch64-apple-darwin]
image = "ghcr.io/cross-rs/aarch64-apple-darwin:main"

48
Dockerfile Normal file
View File

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

172
README.md Normal file
View File

@@ -0,0 +1,172 @@
# cursor-api
## 获取key
1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录(赠送 250 次快速响应,可通过删除账号再注册重置)
2. 在浏览器中打开开发者工具F12
3. 找到 应用-Cookies 中名为 `WorkosCursorSessionToken` 的值并保存(相当于 openai 的密钥)
## 接口说明
### 基础对话(请求格式和响应格式参考 openai
- 接口地址:`/v1/chat/completions`
- 请求方法POST
- 认证方式Bearer Token支持两种认证方式
1. 使用环境变量 `AUTH_TOKEN` 进行认证
2. 使用 `.token` 文件中的令牌列表进行轮询认证
### 获取模型列表
- 接口地址:`/v1/models`
- 请求方法GET
### 获取环境变量中的x-cursor-checksum
- 接口地址:`/env-checksum`
- 请求方法GET
### 获取随机x-cursor-checksum
- 接口地址:`/checksum`
- 请求方法GET
### 健康检查接口
- 接口地址:`/`
- 请求方法GET
### 获取日志接口
- 接口地址:`/logs`
- 请求方法GET
### Token管理接口
- 获取Token信息页面`/tokeninfo`
- 更新Token信息`/update-tokeninfo`
- 获取Token信息`/get-tokeninfo`
## 配置说明
### 环境变量
- `PORT`: 服务器端口号默认3000
- `AUTH_TOKEN`: 认证令牌必须用于API认证
- `ROUTE_PREFIX`: 路由前缀(可选)
- `TOKEN_FILE`: token文件路径默认.token
- `TOKEN_LIST_FILE`: token列表文件路径默认.token-list
### Token文件格式
1. `.token` 文件每行一个token支持以下格式
```
token1
alias::token2
```
2. `.token-list` 文件每行为token和checksum的对应关系
```
token1,checksum1
token2,checksum2
```
## 部署
### 本地部署
#### 从源码编译
需要安装 Rust 工具链和 protobuf 编译器:
```bash
# 安装依赖Debian/Ubuntu
apt-get install -y build-essential protobuf-compiler
# 编译并运行
cargo build --release
./target/release/cursor-api
```
#### 使用预编译二进制
从 [Releases](https://github.com/wisdgod/cursor-api/releases) 下载对应平台的二进制文件。
### Docker 部署
#### Docker 运行示例
```bash
docker run -d -e PORT=3000 -e AUTH_TOKEN=your_token -p 3000:3000 wisdgod/cursor-api:latest
```
#### Docker 构建示例
```bash
docker build -t cursor-api .
docker run -p 3000:3000 cursor-api
```
### huggingface部署
1. duplicate项目:
[huggingface链接](https://huggingface.co/login?next=%2Fspaces%2Fstevenrk%2Fcursor%3Fduplicate%3Dtrue)
2. 配置环境变量
在你的space中点击settings找到`Variables and secrets`添加Variables
- name: `AUTH_TOKEN` (注意大写)
- value: 你随意
3. 重新部署
点击`Factory rebuild`,等待部署完成
4. 接口地址(`Embed this Space`中查看):
```
https://{username}-{space-name}.hf.space/v1/models
```
## 注意事项
1. 请妥善保管您的 AuthToken不要泄露给他人
2. 配置 AUTH_TOKEN 环境变量以增加安全性
3. 本项目仅供学习研究使用,请遵守 Cursor 的使用条款
## 开发
### 跨平台编译
使用提供的构建脚本:
```bash
# 仅编译当前平台
./scripts/build.sh
# 交叉编译所有支持的平台
./scripts/build.sh --cross
```
支持的目标平台:
- x86_64-unknown-linux-gnu
- x86_64-pc-windows-msvc
- aarch64-unknown-linux-gnu
- x86_64-apple-darwin
- aarch64-apple-darwin
### 获取token
- 使用 [get-token](https://github.com/wisdgod/cursor-api/tree/main/get-token) 获取读取当前设备token仅支持windows与macos
## 鸣谢
- [cursor-api](https://github.com/wisdgod/cursor-api)
- [zhx47/cursor-api](https://github.com/zhx47/cursor-api)
- [luolazyandlazy/cursorToApi](https://github.com/luolazyandlazy/cursorToApi)
## 许可证
版权所有 (c) 2024
本软件仅供学习和研究使用。未经授权,不得用于商业用途。
保留所有权利。

60
build.rs Normal file
View File

@@ -0,0 +1,60 @@
use std::io::Result;
use std::path::Path;
use std::process::Command;
fn check_and_install_deps() -> Result<()> {
let scripts_dir = Path::new("scripts");
let node_modules = scripts_dir.join("node_modules");
// 如果 node_modules 不存在,运行 npm install
if !node_modules.exists() {
println!("cargo:warning=Installing HTML minifier dependencies...");
let status = Command::new("npm")
.current_dir(scripts_dir)
.arg("install")
.status()?;
if !status.success() {
panic!("Failed to install npm dependencies");
}
println!("cargo:warning=Dependencies installed successfully");
}
Ok(())
}
fn minify_html() -> Result<()> {
println!("cargo:warning=Minifying HTML files...");
let status = Command::new("node")
.args(&["scripts/minify-html.js"])
.status()?;
if !status.success() {
panic!("HTML minification failed");
}
Ok(())
}
fn main() -> Result<()> {
// Proto 文件处理
println!("cargo:rerun-if-changed=src/message.proto");
let mut config = prost_build::Config::new();
config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
config
.compile_protos(&["src/message.proto"], &["src/"])
.unwrap();
// HTML 文件处理
println!("cargo:rerun-if-changed=static/tokeninfo.html");
println!("cargo:rerun-if-changed=scripts/minify-html.js");
println!("cargo:rerun-if-changed=scripts/package.json");
// 检查并安装依赖
check_and_install_deps()?;
// 运行 HTML 压缩
minify_html()?;
Ok(())
}

189
get-token/Cargo.lock generated Normal file
View File

@@ -0,0 +1,189 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "cc"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "get-token"
version = "0.1.0"
dependencies = [
"rusqlite",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

14
get-token/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "get-token"
version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = { version = "0.32.1", default-features = false, features = ["bundled"] }
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true
opt-level = 3

58
get-token/README.md Normal file
View File

@@ -0,0 +1,58 @@
# Cursor Token 获取工具
这个工具用于从 Cursor 编辑器的本地数据库中获取访问令牌。
## 系统要求
- Rust 编程环境
- Cargo 包管理器
## 构建说明
### Windows
1. 安装 Rust
```powershell
winget install Rustlang.Rust
# 或访问 https://rustup.rs/ 下载安装程序
```
2. 克隆项目并构建
```powershell
git clone <repository-url>
cd get-token
cargo build --release
```
3. 构建完成后,可执行文件位于 `target/release/get-token.exe`
### macOS
1. 安装 Rust
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
2. 克隆项目并构建
```bash
git clone <repository-url>
cd get-token
cargo build --release
```
3. 构建完成后,可执行文件位于 `target/release/get-token`
## 使用方法
直接运行编译好的可执行文件即可:
- Windows: `.\target\release\get-token.exe`
- macOS: `./target/release/get-token`
程序将自动查找并显示 Cursor 编辑器的访问令牌。
## 注意事项
- 确保 Cursor 编辑器已经安装并且至少登录过一次
- Windows 数据库路径:`%USERPROFILE%\AppData\Roaming\Cursor\User\globalStorage\state.vscdb`
- macOS 数据库路径:`~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`

29
get-token/src/main.rs Normal file
View File

@@ -0,0 +1,29 @@
use rusqlite::Connection;
use std::env;
use std::path::PathBuf;
fn main() {
let home_dir = env::var("HOME")
.or_else(|_| env::var("USERPROFILE"))
.unwrap();
let db_path = if cfg!(target_os = "windows") {
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb")
} else {
PathBuf::from(home_dir)
.join("Library/Application Support/Cursor/User/globalStorage/state.vscdb")
};
match Connection::open(&db_path) {
Ok(conn) => {
match conn.query_row(
"SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'",
[],
|row| row.get::<_, String>(0),
) {
Ok(token) => println!("访问令牌: {}", token.trim()),
Err(err) => eprintln!("获取令牌时出错: {}", err),
}
}
Err(err) => eprintln!("无法打开数据库: {}", err),
}
}

158
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,158 @@
# <20><>ɫ<EFBFBD><C9AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Blue }
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Error { Write-Host "[ERROR] $args" -ForegroundColor Red; exit 1 }
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>Ĺ<EFBFBD><C4B9><EFBFBD>
function Test-Requirements {
$tools = @("cargo", "protoc", "npm", "node")
$missing = @()
foreach ($tool in $tools) {
if (!(Get-Command $tool -ErrorAction SilentlyContinue)) {
$missing += $tool
}
}
if ($missing.Count -gt 0) {
Write-Error "ȱ<EFBFBD>ٱ<EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $($missing -join ', ')"
}
}
# <20><> Test-Requirements <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>º<EFBFBD><C2BA><EFBFBD>
function Initialize-VSEnvironment {
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڳ<EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>..."
# ֱ<><D6B1>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD>֪<EFBFBD><D6AA> vcvarsall.bat ·<><C2B7>
$vcvarsallPath = "E:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat"
if (-not (Test-Path $vcvarsallPath)) {
Write-Error "δ<EFBFBD>ҵ<EFBFBD> vcvarsall.bat: $vcvarsallPath"
return
}
Write-Info "ʹ<EFBFBD><EFBFBD> vcvarsall.bat ·<><C2B7>: $vcvarsallPath"
# <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$archArg = "x64"
$command = "`"$vcvarsallPath`" $archArg && set"
try {
$output = cmd /c "$command" 2>&1
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ɹ<EFBFBD>ִ<EFBFBD><D6B4>
if ($LASTEXITCODE -ne 0) {
Write-Error "vcvarsall.bat ִ<><D6B4>ʧ<EFBFBD>ܣ<EFBFBD><DCA3>˳<EFBFBD><CBB3><EFBFBD>: $LASTEXITCODE"
return
}
# <20><><EFBFBD>µ<EFBFBD>ǰ PowerShell <20><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
foreach ($line in $output) {
if ($line -match "^([^=]+)=(.*)$") {
$name = $matches[1]
$value = $matches[2]
if (![string]::IsNullOrEmpty($name)) {
Set-Item -Path "env:$name" -Value $value -ErrorAction SilentlyContinue
}
}
}
Write-Info "Visual Studio <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
}
catch {
Write-Error "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
}
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
function Show-Help {
Write-Host @"
<EFBFBD>÷<EFBFBD>: $(Split-Path $MyInvocation.MyCommand.Path -Leaf) [ѡ<EFBFBD><EFBFBD>]
ѡ<EFBFBD><EFBFBD>:
--static ʹ<EFBFBD>þ<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>Ĭ<EFBFBD>϶<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>
--help <EFBFBD><EFBFBD>ʾ<EFBFBD>˰<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
Ĭ<EFBFBD>ϱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Windows ֧<EFBFBD>ֵļܹ<EFBFBD> (x64 <EFBFBD><EFBFBD> arm64)
"@
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
function New-Target {
param (
[string]$target,
[string]$rustflags
)
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڹ<EFBFBD><EFBFBD><EFBFBD> $target..."
# <20><><EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD>й<EFBFBD><D0B9><EFBFBD>
$env:RUSTFLAGS = $rustflags
cargo build --target $target --release
# <20>ƶ<EFBFBD><C6B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$binaryName = "cursor-api"
if ($UseStatic) {
$binaryName += "-static"
}
$sourcePath = "target/$target/release/cursor-api.exe"
$targetPath = "release/${binaryName}-${target}.exe"
if (Test-Path $sourcePath) {
Copy-Item $sourcePath $targetPath -Force
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD> $target"
}
else {
Write-Warn "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD>: $target"
return $false
}
return $true
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
$UseStatic = $false
foreach ($arg in $args) {
switch ($arg) {
"--static" { $UseStatic = $true }
"--help" { Show-Help; exit 0 }
default { Write-Error "δ֪<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $arg" }
}
}
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
try {
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
Test-Requirements
# <20><>ʼ<EFBFBD><CABC> Visual Studio <20><><EFBFBD><EFBFBD>
Initialize-VSEnvironment
# <20><><EFBFBD><EFBFBD> release Ŀ¼
New-Item -ItemType Directory -Force -Path "release" | Out-Null
# <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ƽ̨
$targets = @(
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc"
)
# <20><><EFBFBD>þ<EFBFBD>̬<EFBFBD><CCAC><EFBFBD>ӱ<EFBFBD>־
$rustflags = ""
if ($UseStatic) {
$rustflags = "-C target-feature=+crt-static"
}
Write-Info "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>
foreach ($target in $targets) {
New-Target -target $target -rustflags $rustflags
}
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
}
catch {
Write-Error "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>з<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
}

192
scripts/build.sh Normal file
View File

@@ -0,0 +1,192 @@
#!/bin/bash
set -euo pipefail
# 颜色输出函数
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
# 检查是否在 Linux 环境
is_linux() {
[ "$(uname -s)" = "Linux" ]
}
# 检查必要的工具
check_requirements() {
local missing_tools=()
# 基础工具检查
for tool in cargo protoc npm node; do
if ! command -v "$tool" &>/dev/null; then
missing_tools+=("$tool")
fi
done
# Linux 特定检查
if [[ $USE_CROSS == true ]] && ! command -v cross &>/dev/null; then
missing_tools+=("cross")
fi
if [[ ${#missing_tools[@]} -gt 0 ]]; then
error "缺少必要工具: ${missing_tools[*]}"
fi
}
# 帮助信息
show_help() {
cat << EOF
用法: $(basename "$0") [选项]
选项:
--cross 使用 cross 进行交叉编译(仅在 Linux 上有效)
--static 使用静态链接(默认动态链接)
--help 显示此帮助信息
不带参数时只编译当前平台
EOF
}
# 并行构建函数
build_target() {
local target=$1
local extension=""
local rustflags="${2:-}"
info "正在构建 $target..."
# 确定文件后缀
[[ $target == *"windows"* ]] && extension=".exe"
# 设置目标特定的环境变量
local build_env=()
if [[ $target == "aarch64-unknown-linux-gnu" ]]; then
build_env+=(
"CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc"
"CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++"
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc"
"PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig"
"PKG_CONFIG_ALLOW_CROSS=1"
"OPENSSL_DIR=/usr"
"OPENSSL_INCLUDE_DIR=/usr/include"
"OPENSSL_LIB_DIR=/usr/lib/aarch64-linux-gnu"
)
fi
# 判断是否使用 cross仅在 Linux 上)
if [[ $target != "$CURRENT_TARGET" ]]; then
env ${build_env[@]+"${build_env[@]}"} RUSTFLAGS="$rustflags" cargo build --target "$target" --release
else
env ${build_env[@]+"${build_env[@]}"} RUSTFLAGS="$rustflags" cargo build --release
fi
# 移动编译产物到 release 目录
local binary_name="cursor-api"
[[ $USE_STATIC == true ]] && binary_name+="-static"
if [[ -f "target/$target/release/cursor-api$extension" ]]; then
cp "target/$target/release/cursor-api$extension" "release/${binary_name}-$target$extension"
info "完成构建 $target"
else
warn "构建产物未找到: $target"
return 1
fi
}
# 获取 CPU 架构
ARCH=$(uname -m | sed 's/^aarch64\|arm64$/aarch64/;s/^x86_64\|x86-64\|x64\|amd64$/x86_64/')
OS=$(uname -s)
# 确定当前系统的目标平台
get_target() {
local arch=$1
local os=$2
case "$os" in
"Darwin") echo "${arch}-apple-darwin" ;;
"Linux") echo "${arch}-unknown-linux-gnu" ;;
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;;
"FreeBSD") echo "x86_64-unknown-freebsd" ;;
*) error "不支持的系统: $os" ;;
esac
}
# 设置当前目标平台
CURRENT_TARGET=$(get_target "$ARCH" "$OS")
# 检查是否成功获取目标平台
[ -z "$CURRENT_TARGET" ] && error "无法确定当前系统的目标平台"
# 获取系统对应的所有目标
get_targets() {
case "$1" in
"linux") echo "x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu" ;;
"windows") echo "x86_64-pc-windows-msvc aarch64-pc-windows-msvc" ;;
"macos") echo "x86_64-apple-darwin aarch64-apple-darwin" ;;
"freebsd") echo "x86_64-unknown-freebsd" ;;
*) error "不支持的系统组: $1" ;;
esac
}
# 解析参数
USE_CROSS=false
USE_STATIC=false
while [[ $# -gt 0 ]]; do
case $1 in
--cross) USE_CROSS=true ;;
--static) USE_STATIC=true ;;
--help) show_help; exit 0 ;;
*) error "未知参数: $1" ;;
esac
shift
done
# 检查依赖
check_requirements
# 确定要构建的目标
if [[ $USE_CROSS == true ]] && is_linux; then
# 只在 Linux 上使用 cross 进行多架构构建
TARGETS=($(get_targets "linux"))
else
# 其他系统或不使用 cross 时只构建当前系统的所有架构
case "$OS" in
"Darwin") TARGETS=($(get_targets "macos")) ;;
"Linux") TARGETS=("$CURRENT_TARGET") ;;
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") TARGETS=($(get_targets "windows")) ;;
"FreeBSD") TARGETS=("$CURRENT_TARGET") ;;
*) error "不支持的系统: $OS" ;;
esac
fi
# 创建 release 目录
mkdir -p release
# 设置静态链接标志
RUSTFLAGS=""
[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static"
# 并行构建所有目标
info "开始构建..."
for target in "${TARGETS[@]}"; do
build_target "$target" "$RUSTFLAGS" &
done
# 等待所有构建完成
wait
# 为 macOS 平台创建通用二进制
if [[ "$OS" == "Darwin" ]] && [[ ${#TARGETS[@]} -gt 1 ]]; then
binary_suffix=""
[[ $USE_STATIC == true ]] && binary_suffix="-static"
if [[ -f "release/cursor-api${binary_suffix}-x86_64-apple-darwin" ]] && \
[[ -f "release/cursor-api${binary_suffix}-aarch64-apple-darwin" ]]; then
info "创建 macOS 通用二进制..."
lipo -create \
"release/cursor-api${binary_suffix}-x86_64-apple-darwin" \
"release/cursor-api${binary_suffix}-aarch64-apple-darwin" \
-output "release/cursor-api${binary_suffix}-universal-apple-darwin"
fi
fi
info "构建完成!"

49
scripts/minify-html.js Normal file
View File

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

265
scripts/package-lock.json generated Normal file
View File

@@ -0,0 +1,265 @@
{
"name": "html-minifier-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "html-minifier-scripts",
"version": "1.0.0",
"dependencies": {
"html-minifier-terser": "^7.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
"license": "MIT",
"dependencies": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
}
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
"license": "MIT",
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/html-minifier-terser": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
"license": "MIT",
"dependencies": {
"camel-case": "^4.1.2",
"clean-css": "~5.3.2",
"commander": "^10.0.0",
"entities": "^4.4.0",
"param-case": "^3.0.4",
"relateurl": "^0.2.7",
"terser": "^5.15.1"
},
"bin": {
"html-minifier-terser": "cli.js"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/terser": {
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}
}
}

11
scripts/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "html-minifier-scripts",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"html-minifier-terser": "^7.2.0"
}
}

31
scripts/setup-windows.ps1 Normal file
View File

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

256
src/lib.rs Normal file
View File

@@ -0,0 +1,256 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use flate2::read::GzDecoder;
use prost::Message;
use rand::{thread_rng, Rng};
use sha2::{Digest, Sha256};
use std::io::Read;
use uuid::Uuid;
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/cursor.rs"));
}
use proto::{ChatMessage, ResMessage};
#[derive(Debug)]
pub struct ChatInput {
pub role: String,
pub content: String,
}
fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_message::Message>) {
// 收集 system 和 developer 指令
let instructions = inputs
.iter()
.filter(|input| input.role == "system" || input.role == "developer")
.map(|input| input.content.clone())
.collect::<Vec<String>>()
.join("\n\n");
// 使用默认指令或收集到的指令
let instructions = if instructions.is_empty() {
"Respond in Chinese by default".to_string()
} else {
instructions
};
// 过滤出 user 和 assistant 对话
let mut chat_inputs: Vec<ChatInput> = inputs
.into_iter()
.filter(|input| input.role == "user" || input.role == "assistant")
.collect();
// 处理空对话情况
if chat_inputs.is_empty() {
return (
instructions,
vec![proto::chat_message::Message {
role: 1, // user
content: " ".to_string(),
message_id: Uuid::new_v4().to_string(),
}],
);
}
// 如果第一条是 assistant插入空的 user 消息
if chat_inputs
.first()
.map_or(false, |input| input.role == "assistant")
{
chat_inputs.insert(
0,
ChatInput {
role: "user".to_string(),
content: " ".to_string(),
},
);
}
// 处理连续相同角色的情况
let mut i = 1;
while i < chat_inputs.len() {
if chat_inputs[i].role == chat_inputs[i - 1].role {
let insert_role = if chat_inputs[i].role == "user" {
"assistant"
} else {
"user"
};
chat_inputs.insert(
i,
ChatInput {
role: insert_role.to_string(),
content: " ".to_string(),
},
);
}
i += 1;
}
// 确保最后一条是 user
if chat_inputs
.last()
.map_or(false, |input| input.role == "assistant")
{
chat_inputs.push(ChatInput {
role: "user".to_string(),
content: " ".to_string(),
});
}
// 转换为 proto messages
let messages = chat_inputs
.into_iter()
.map(|input| proto::chat_message::Message {
role: if input.role == "user" { 1 } else { 2 },
content: input.content,
message_id: Uuid::new_v4().to_string(),
})
.collect();
(instructions, messages)
}
pub async fn encode_chat_message(
inputs: Vec<ChatInput>,
model_name: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let (instructions, messages) = process_chat_inputs(inputs);
let chat = ChatMessage {
messages,
instructions: Some(proto::chat_message::Instructions {
content: instructions,
}),
project_path: "/path/to/project".to_string(),
model: Some(proto::chat_message::Model {
name: model_name.to_string(),
empty: String::new(),
}),
request_id: Uuid::new_v4().to_string(),
summary: String::new(),
conversation_id: Uuid::new_v4().to_string(),
};
let mut encoded = Vec::new();
chat.encode(&mut encoded)?;
let len_prefix = format!("{:010x}", encoded.len()).to_uppercase();
let content = hex::encode_upper(&encoded);
Ok(hex::decode(len_prefix + &content)?)
}
pub async fn decode_response(data: &[u8]) -> String {
if let Ok(decoded) = decode_proto_messages(data) {
if !decoded.is_empty() {
return decoded;
}
}
decompress_response(data).await
}
fn decode_proto_messages(data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
let hex_str = hex::encode(data);
let mut pos = 0;
let mut messages = Vec::new();
while pos + 10 <= hex_str.len() {
let msg_len = i64::from_str_radix(&hex_str[pos..pos + 10], 16)?;
pos += 10;
if pos + (msg_len * 2) as usize > hex_str.len() {
break;
}
let msg_data = &hex_str[pos..pos + (msg_len * 2) as usize];
pos += (msg_len * 2) as usize;
let buffer = hex::decode(msg_data)?;
let response = ResMessage::decode(&buffer[..])?;
messages.push(response.msg);
}
Ok(messages.join(""))
}
async fn decompress_response(data: &[u8]) -> String {
if data.len() <= 5 {
return String::new();
}
let mut decoder = GzDecoder::new(&data[5..]);
let mut text = String::new();
match decoder.read_to_string(&mut text) {
Ok(_) => {
if !text.contains("<|BEGIN_SYSTEM|>") {
text
} else {
String::new()
}
}
Err(_) => String::new(),
}
}
pub fn generate_random_id(
size: usize,
dict_type: Option<&str>,
custom_chars: Option<&str>,
) -> String {
let charset = match (dict_type, custom_chars) {
(_, Some(chars)) => chars,
(Some("alphabet"), _) => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
(Some("max"), _) => "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-",
_ => "0123456789",
};
let mut rng = thread_rng();
(0..size)
.map(|_| {
let idx = rng.gen_range(0..charset.len());
charset.chars().nth(idx).unwrap()
})
.collect()
}
pub fn generate_hash() -> String {
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
let mut hasher = Sha256::new();
hasher.update(random_bytes);
hex::encode(hasher.finalize())
}
fn obfuscate_bytes(bytes: &mut Vec<u8>) {
let mut prev: u8 = 165;
for (idx, byte) in bytes.iter_mut().enumerate() {
let old_value = *byte;
*byte = (old_value ^ prev).wrapping_add((idx % 256) as u8);
prev = *byte;
}
}
pub fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
/ 1_000_000;
let mut timestamp_bytes = vec![
((timestamp >> 40) & 255) as u8,
((timestamp >> 32) & 255) as u8,
((timestamp >> 24) & 255) as u8,
((timestamp >> 16) & 255) as u8,
((timestamp >> 8) & 255) as u8,
(255 & timestamp) as u8,
];
obfuscate_bytes(&mut timestamp_bytes);
let encoded = BASE64.encode(&timestamp_bytes);
match mac_addr {
Some(mac) => format!("{}{}/{}", encoded, device_id, mac),
None => format!("{}{}", encoded, device_id),
}
}

661
src/main.rs Normal file
View File

@@ -0,0 +1,661 @@
use axum::{
body::Body,
extract::State,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use bytes::Bytes;
use chrono::{DateTime, Local, Utc};
use futures::StreamExt;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{convert::Infallible, sync::Arc};
use tokio::sync::Mutex;
use tower_http::cors::CorsLayer;
use uuid::Uuid;
// 应用状态
struct AppState {
start_time: DateTime<Local>,
version: String,
total_requests: u64,
active_requests: u64,
request_logs: Vec<RequestLog>,
route_prefix: String,
token_infos: Vec<TokenInfo>,
}
// 模型定义
#[derive(Serialize, Deserialize, Clone)]
struct Model {
id: String,
created: i64,
object: String,
owned_by: String,
}
// 请求日志
#[derive(Serialize, Clone)]
struct RequestLog {
timestamp: DateTime<Local>,
model: String,
checksum: String,
auth_token: String,
stream: bool,
}
// 聊天请求
#[derive(Deserialize)]
struct ChatRequest {
model: String,
messages: Vec<Message>,
#[serde(default)]
stream: bool,
}
// 添加用于请求的消息结构体
#[derive(Serialize, Deserialize)]
struct Message {
role: String,
content: String,
}
// 支持的模型列表
mod models;
use models::AVAILABLE_MODELS;
// 用于存储 token 信息
#[derive(Debug)]
struct TokenInfo {
token: String,
checksum: String,
}
// TokenUpdateRequest 结构体
#[derive(Deserialize)]
struct TokenUpdateRequest {
tokens: String,
#[serde(default)]
token_list: Option<String>,
}
// 自定义错误类型
#[derive(Debug)]
enum ChatError {
ModelNotSupported(String),
EmptyMessages,
StreamNotSupported(String),
NoTokens,
RequestFailed(String),
Unauthorized,
}
impl ChatError {
fn to_json(&self) -> serde_json::Value {
let (code, message) = match self {
ChatError::ModelNotSupported(model) => (
"model_not_supported",
format!("Model '{}' is not supported", model),
),
ChatError::EmptyMessages => (
"empty_messages",
"Message array cannot be empty".to_string(),
),
ChatError::StreamNotSupported(model) => (
"stream_not_supported",
format!("Streaming is not supported for model '{}'", model),
),
ChatError::NoTokens => ("no_tokens", "No available tokens".to_string()),
ChatError::RequestFailed(err) => ("request_failed", format!("Request failed: {}", err)),
ChatError::Unauthorized => ("unauthorized", "Invalid authorization token".to_string()),
};
serde_json::json!({
"error": {
"code": code,
"message": message
}
})
}
}
#[tokio::main]
async fn main() {
// 加载环境变量
dotenvy::dotenv().ok();
// 处理 token 文件路径
let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string());
// 加载 tokens
let token_infos = load_tokens(&token_file);
// 获取路由前缀配置
let route_prefix = std::env::var("ROUTE_PREFIX").unwrap_or_default();
// 初始化应用状态
let state = Arc::new(Mutex::new(AppState {
start_time: Local::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
total_requests: 0,
active_requests: 0,
request_logs: Vec::new(),
route_prefix: route_prefix.clone(),
token_infos,
}));
// 设置路由
let app = Router::new()
.route("/", get(handle_root))
.route("/tokeninfo", get(handle_tokeninfo_page))
.route(&format!("{}/v1/models", route_prefix), get(handle_models))
.route("/checksum", get(handle_checksum))
.route("/update-tokeninfo", get(handle_update_tokeninfo))
.route("/get-tokeninfo", post(handle_get_tokeninfo))
.route("/update-tokeninfo", post(handle_update_tokeninfo_post))
.route(
&format!("{}/v1/chat/completions", route_prefix),
post(handle_chat),
)
.route("/logs", get(handle_logs))
.layer(CorsLayer::permissive())
.with_state(state);
// 启动服务器
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
let addr = format!("0.0.0.0:{}", port);
println!("服务器运行在端口 {}", port);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// Token 加载函数
fn load_tokens(token_file: &str) -> Vec<TokenInfo> {
let token_list_file =
std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string());
// 读取并规范化 .token 文件
let tokens = if let Ok(content) = std::fs::read_to_string(token_file) {
let normalized = content.replace("\r\n", "\n");
if normalized != content {
std::fs::write(token_file, &normalized).unwrap();
}
normalized
.lines()
.enumerate()
.filter_map(|(idx, line)| {
let parts: Vec<&str> = line.split("::").collect();
match parts.len() {
1 => Some(line.to_string()),
2 => Some(parts[1].to_string()),
_ => {
println!("警告: 第{}行包含多个'::'分隔符,已忽略此行", idx + 1);
None
}
}
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
} else {
eprintln!("警告: 无法读取token文件 '{}'", token_file);
Vec::new()
};
// 读取现有的 token-list
let mut token_map: std::collections::HashMap<String, String> =
if let Ok(content) = std::fs::read_to_string(&token_list_file) {
content
.split('\n')
.filter(|s| !s.is_empty())
.filter_map(|line| {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect()
} else {
std::collections::HashMap::new()
};
// 为新 token 生成 checksum
for token in tokens {
if !token_map.contains_key(&token) {
let checksum = cursor_api::generate_checksum(
&cursor_api::generate_hash(),
Some(&cursor_api::generate_hash()),
);
token_map.insert(token, checksum);
}
}
// 更新 token-list 文件
let token_list_content = token_map
.iter()
.map(|(token, checksum)| format!("{},{}", token, checksum))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(token_list_file, token_list_content).unwrap();
// 转换为 TokenInfo vector
token_map
.into_iter()
.map(|(token, checksum)| TokenInfo { token, checksum })
.collect()
}
// 根路由处理
async fn handle_root(State(state): State<Arc<Mutex<AppState>>>) -> Json<serde_json::Value> {
let state = state.lock().await;
let uptime = (Local::now() - state.start_time).num_seconds();
Json(serde_json::json!({
"status": "healthy",
"version": state.version,
"uptime": uptime,
"stats": {
"started": state.start_time,
"totalRequests": state.total_requests,
"activeRequests": state.active_requests,
"memory": {
"heapTotal": 0,
"heapUsed": 0,
"rss": 0
}
},
"models": AVAILABLE_MODELS.iter().map(|m| &m.id).collect::<Vec<_>>(),
"endpoints": [
&format!("{}/v1/chat/completions", state.route_prefix),
&format!("{}/v1/models", state.route_prefix),
"/checksum",
"/tokeninfo",
"/update-tokeninfo",
"/get-tokeninfo"
]
}))
}
async fn handle_tokeninfo_page() -> impl IntoResponse {
Response::builder()
.header("Content-Type", "text/html")
.body(include_str!("../static/tokeninfo.min.html").to_string())
.unwrap()
}
// 模型列表处理
async fn handle_models() -> Json<serde_json::Value> {
Json(serde_json::json!({
"object": "list",
"data": AVAILABLE_MODELS.to_vec()
}))
}
// Checksum 处理
async fn handle_checksum() -> Json<serde_json::Value> {
let checksum = cursor_api::generate_checksum(
&cursor_api::generate_hash(),
Some(&cursor_api::generate_hash()),
);
Json(serde_json::json!({
"checksum": checksum
}))
}
// 更新 TokenInfo 处理
async fn handle_update_tokeninfo(
State(state): State<Arc<Mutex<AppState>>>,
) -> Json<serde_json::Value> {
// 获取当前的 token 文件路径
let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string());
// 重新加载 tokens
let token_infos = load_tokens(&token_file);
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Json(serde_json::json!({
"status": "success",
"message": "Token list has been reloaded"
}))
}
// 获取 TokenInfo 处理
async fn handle_get_tokeninfo(
State(_state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get("authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let env_token = std::env::var("AUTH_TOKEN").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if auth_header != env_token {
return Err(StatusCode::UNAUTHORIZED);
}
// 获取文件路径
let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string());
let token_list_file =
std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string());
// 读取文件内容
let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new());
let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new());
Ok(Json(serde_json::json!({
"status": "success",
"token_file": token_file,
"token_list_file": token_list_file,
"tokens": tokens,
"token_list": token_list
})))
}
async fn handle_update_tokeninfo_post(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<TokenUpdateRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get("authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let env_token = std::env::var("AUTH_TOKEN").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if auth_header != env_token {
return Err(StatusCode::UNAUTHORIZED);
}
// 获取文件路径
let token_file = std::env::var("TOKEN_FILE").unwrap_or_else(|_| ".token".to_string());
let token_list_file =
std::env::var("TOKEN_LIST_FILE").unwrap_or_else(|_| ".token-list".to_string());
// 写入 .token 文件
std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// 如果提供了 token_list则写入
if let Some(token_list) = request.token_list {
std::fs::write(&token_list_file, token_list)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// 重新加载 tokens
let token_infos = load_tokens(&token_file);
let token_infos_len = token_infos.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Ok(Json(serde_json::json!({
"status": "success",
"message": "Token files have been updated and reloaded",
"token_file": token_file,
"token_list_file": token_list_file,
"token_count": token_infos_len
})))
}
// 日志处理
async fn handle_logs(State(state): State<Arc<Mutex<AppState>>>) -> Json<serde_json::Value> {
let state = state.lock().await;
Json(serde_json::json!({
"total": state.request_logs.len(),
"logs": state.request_logs,
"timestamp": Utc::now(),
"status": "success"
}))
}
// 聊天处理函数的签名
async fn handle_chat(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<ChatRequest>,
) -> Result<Response<Body>, (StatusCode, Json<serde_json::Value>)> {
// 验证模型是否支持
if !AVAILABLE_MODELS.iter().any(|m| m.id == request.model) {
return Err((
StatusCode::BAD_REQUEST,
Json(ChatError::ModelNotSupported(request.model.clone()).to_json()),
));
}
let request_time = Local::now();
// 验证请求
if request.messages.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(ChatError::EmptyMessages.to_json()),
));
}
// 验证 O1 模型不支持流式输出
if request.model.starts_with("o1") && request.stream {
return Err((
StatusCode::BAD_REQUEST,
Json(ChatError::StreamNotSupported(request.model.clone()).to_json()),
));
}
// 获取并处理认证令牌
let auth_token = headers
.get("authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))?;
// 验证环境变量中的 AUTH_TOKEN
if let Ok(env_token) = std::env::var("AUTH_TOKEN") {
if auth_token != env_token {
return Err((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
));
}
}
// 完整的令牌处理逻辑和对应的 checksum
let (auth_token, checksum) = {
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await;
let token_infos = &state_guard.token_infos;
if token_infos.is_empty() {
return Err((
StatusCode::SERVICE_UNAVAILABLE,
Json(ChatError::NoTokens.to_json()),
));
}
let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
let token_info = &token_infos[index];
(token_info.token.clone(), token_info.checksum.clone())
};
// 更新请求日志
{
let mut state = state.lock().await;
state.total_requests += 1;
state.active_requests += 1;
state.request_logs.push(RequestLog {
timestamp: request_time,
model: request.model.clone(),
checksum: checksum.clone(),
auth_token: auth_token.clone(),
stream: request.stream,
});
if state.request_logs.len() > 100 {
state.request_logs.remove(0);
}
}
// 消息转换
let chat_inputs: Vec<cursor_api::ChatInput> = request
.messages
.into_iter()
.map(|m| cursor_api::ChatInput {
role: m.role,
content: m.content,
})
.collect();
// 将消息转换为hex格式
let hex_data = cursor_api::encode_chat_message(chat_inputs, &request.model)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(
ChatError::RequestFailed("Failed to encode chat message".to_string()).to_json(),
),
)
})?;
// 构建请求客户端
let client = Client::new();
let request_id = Uuid::new_v4().to_string();
let response = client
.post("https://api2.cursor.sh/aiserver.v1.AiService/StreamChat")
.header("Content-Type", "application/connect+proto")
.header("Authorization", format!("Bearer {}", auth_token))
.header("connect-accept-encoding", "gzip,br")
.header("connect-protocol-version", "1")
.header("user-agent", "connect-es/1.4.0")
.header("x-amzn-trace-id", format!("Root={}", &request_id))
.header("x-cursor-checksum", &checksum)
.header("x-cursor-client-version", "0.42.5")
.header("x-cursor-timezone", "Asia/Shanghai")
.header("x-ghost-mode", "false")
.header("x-request-id", &request_id)
.header("Host", "api2.cursor.sh")
.body(hex_data)
.send()
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatError::RequestFailed(format!("Request failed: {}", e)).to_json()),
)
})?;
// 释放活动请求计数
{
let mut state = state.lock().await;
state.active_requests -= 1;
}
if request.stream {
let response_id = format!("chatcmpl-{}", Uuid::new_v4());
let stream = response.bytes_stream().then(move |chunk| {
let response_id = response_id.clone();
let model = request.model.clone();
async move {
let chunk = chunk.unwrap_or_default();
let text = cursor_api::decode_response(&chunk).await;
if text.is_empty() {
return Ok::<_, Infallible>(Bytes::from("[DONE]"));
}
let data = serde_json::json!({
"id": &response_id,
"object": "chat.completion.chunk",
"created": chrono::Utc::now().timestamp(),
"model": model,
"choices": [{
"index": 0,
"delta": {
"content": text
}
}]
});
Ok::<_, Infallible>(Bytes::from(format!("data: {}\n\n", data.to_string())))
}
});
Ok(Response::builder()
.header("Content-Type", "text/event-stream")
.header("Cache-Control", "no-cache")
.header("Connection", "keep-alive")
.body(Body::from_stream(stream))
.unwrap())
} else {
// 非流式响应
let mut full_text = String::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(
ChatError::RequestFailed(format!("Failed to read response chunk: {}", e))
.to_json(),
),
)
})?;
full_text.push_str(&cursor_api::decode_response(&chunk).await);
}
// 处理文本
full_text = full_text
.replace(
regex::Regex::new(r"^.*<\|END_USER\|>").unwrap().as_str(),
"",
)
.replace(regex::Regex::new(r"^\n[a-zA-Z]?").unwrap().as_str(), "")
.trim()
.to_string();
let response_data = serde_json::json!({
"id": format!("chatcmpl-{}", Uuid::new_v4()),
"object": "chat.completion",
"created": chrono::Utc::now().timestamp(),
"model": request.model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": full_text
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
});
Ok(Response::new(Body::from(response_data.to_string())))
}
}

53
src/message.proto Normal file
View File

@@ -0,0 +1,53 @@
syntax = "proto3";
package cursor;
message ChatMessage {
message FileContent {
message Position {
int32 line = 1;
int32 column = 2;
}
message Range {
Position start = 1;
Position end = 2;
}
string filename = 1;
string content = 2;
Position position = 3;
string language = 5;
Range range = 6;
int32 length = 8;
int32 type = 9;
int32 error_code = 11;
}
message Message {
string content = 1;
int32 role = 2;
string message_id = 13;
}
message Instructions {
string content = 1;
}
message Model {
string name = 1;
string empty = 4;
}
// repeated FileContent files = 1;
repeated Message messages = 2;
Instructions instructions = 4;
string projectPath = 5;
Model model = 7;
string requestId = 9;
string summary = 11; // 或许是空的,描述会话做了什么事情,但是不是标题 或许可以当作额外的设定来用
string conversationId = 15; // 又来一个uuid
}
message ResMessage {
string msg = 1;
}

133
src/models.rs Normal file
View File

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

184
static/tokeninfo.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token 信息管理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
.container {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.button-group {
display: flex;
gap: 10px;
margin: 10px 0;
}
textarea {
width: 100%;
min-height: 150px;
margin: 10px 0;
font-family: monospace;
resize: vertical;
}
button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #45a049;
}
button.secondary {
background: #607D8B;
}
button.secondary:hover {
background: #546E7A;
}
.error {
color: red;
margin: 10px 0;
}
.success {
color: green;
margin: 10px 0;
}
#authToken {
width: 100%;
padding: 8px;
margin: 10px 0;
}
</style>
</head>
<body>
<h1>Token 信息管理</h1>
<div class="container">
<h2>认证</h2>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
<div class="container">
<h2>Token 配置</h2>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
</div>
<h3>Token 文件内容</h3>
<textarea id="tokens" placeholder="每行一个 token"></textarea>
<h3>Token List 文件内容</h3>
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
</div>
<div id="message"></div>
<script>
function showMessage(text, isError = false) {
const msg = document.getElementById('message');
msg.className = isError ? 'error' : 'success';
msg.textContent = text;
}
async function getTokenInfo() {
const authToken = document.getElementById('authToken').value;
if (!authToken) {
showMessage('请输入 AUTH_TOKEN', true);
return;
}
try {
const response = await fetch('/get-tokeninfo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
document.getElementById('tokens').value = data.tokens;
document.getElementById('tokenList').value = data.token_list;
showMessage('配置获取成功');
} catch (error) {
showMessage(`获取失败: ${error.message}`, true);
}
}
async function updateTokenInfo() {
const authToken = document.getElementById('authToken').value;
const tokens = document.getElementById('tokens').value;
const tokenList = document.getElementById('tokenList').value;
if (!authToken) {
showMessage('请输入 AUTH_TOKEN', true);
return;
}
if (!tokens) {
showMessage('Token 文件内容不能为空', true);
return;
}
try {
const response = await fetch('/update-tokeninfo', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokens: tokens,
token_list: tokenList || undefined
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
showMessage(`更新成功: ${data.message}`);
} catch (error) {
showMessage(`更新失败: ${error.message}`, true);
}
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
updateTokenInfo();
}
});
</script>
</body>
</html>