mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-06 15:16:51 +08:00
first commit
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal 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
263
.github/workflows/build.yml
vendored
Normal 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
56
.github/workflows/docker.yml
vendored
Normal 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
14
.gitignore
vendored
Normal 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
2086
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
Normal file
50
Cargo.toml
Normal 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
48
Dockerfile
Normal 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
172
README.md
Normal 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
60
build.rs
Normal 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
189
get-token/Cargo.lock
generated
Normal 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
14
get-token/Cargo.toml
Normal 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
58
get-token/README.md
Normal 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
29
get-token/src/main.rs
Normal 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
158
scripts/build.ps1
Normal 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
192
scripts/build.sh
Normal 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
49
scripts/minify-html.js
Normal 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
265
scripts/package-lock.json
generated
Normal 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
11
scripts/package.json
Normal 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
31
scripts/setup-windows.ps1
Normal 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
256
src/lib.rs
Normal 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(×tamp_bytes);
|
||||
|
||||
match mac_addr {
|
||||
Some(mac) => format!("{}{}/{}", encoded, device_id, mac),
|
||||
None => format!("{}{}", encoded, device_id),
|
||||
}
|
||||
}
|
661
src/main.rs
Normal file
661
src/main.rs
Normal 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
53
src/message.proto
Normal 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
133
src/models.rs
Normal 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
184
static/tokeninfo.html
Normal 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>
|
Reference in New Issue
Block a user