mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-09-26 18:51:11 +08:00
v0.1.3-rc.1
This commit is contained in:
31
.env.example
31
.env.example
@@ -1,5 +1,36 @@
|
||||
# 当前配置为默认值,请根据需要修改
|
||||
|
||||
# 服务器监听端口
|
||||
PORT=3000
|
||||
|
||||
# 路由前缀,必须以 / 开头(如果不为空)
|
||||
ROUTE_PREFIX=
|
||||
|
||||
# 认证令牌,必填
|
||||
AUTH_TOKEN=
|
||||
|
||||
# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次
|
||||
ENABLE_STREAM_CHECK=true
|
||||
|
||||
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块
|
||||
INCLUDE_STOP_REASON_STREAM=true
|
||||
|
||||
# 令牌文件路径
|
||||
TOKEN_FILE=.token
|
||||
|
||||
# 令牌列表文件路径
|
||||
TOKEN_LIST_FILE=.token-list
|
||||
|
||||
# (实验性)是否启用慢速池(true/false)
|
||||
ENABLE_SLOW_POOL=false
|
||||
|
||||
# 允许claude开头的模型请求绕过内置模型限制(true/false)
|
||||
PASS_ANY_CLAUDE=false
|
||||
|
||||
# 图片处理能力配置
|
||||
# 可选值:
|
||||
# - none 或 disabled:禁用图片功能
|
||||
# - base64 或 base64-only:仅支持 base64 编码的图片
|
||||
# - all 或 base64-http:支持 base64 和 HTTP 图片
|
||||
# 注意:启用 HTTP 支持可能会暴露服务器 IP
|
||||
VISION_ABILITY=base64
|
||||
|
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# 统一使用 LF
|
||||
* text=auto eol=lf
|
||||
|
||||
# 对特定文件类型设置
|
||||
*.bat text eol=crlf
|
||||
*.ps1 text eol=crlf
|
120
.github/workflows/build.yml
vendored
120
.github/workflows/build.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
@@ -28,7 +29,8 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'scripts/package.json'
|
||||
cache-dependency-path: 'scripts/package-lock.json'
|
||||
run: cd scripts && npm install && cd ..
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -44,10 +46,11 @@ jobs:
|
||||
protobuf-compiler \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
openssl
|
||||
|
||||
# 安装 npm 依赖
|
||||
cd scripts && npm install && cd ..
|
||||
openssl \
|
||||
musl-tools \
|
||||
musl-dev \
|
||||
libssl-dev:native \
|
||||
linux-libc-dev:native
|
||||
|
||||
# 设置 OpenSSL 环境变量
|
||||
echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV
|
||||
@@ -55,72 +58,21 @@ jobs:
|
||||
echo "OPENSSL_INCLUDE_DIR=/usr/include/openssl" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# if: runner.os == 'Linux'
|
||||
# uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
# - name: Build Linux arm64
|
||||
# if: runner.os == 'Linux'
|
||||
# run: |
|
||||
# # 启用 QEMU 支持
|
||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# # 创建 Dockerfile
|
||||
# cat > Dockerfile.arm64 << 'EOF'
|
||||
# FROM arm64v8/ubuntu:22.04
|
||||
|
||||
# ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# RUN apt-get update && apt-get install -y \
|
||||
# build-essential \
|
||||
# curl \
|
||||
# pkg-config \
|
||||
# libssl-dev \
|
||||
# protobuf-compiler \
|
||||
# nodejs \
|
||||
# npm \
|
||||
# git \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# # 安装 Rust
|
||||
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# WORKDIR /build
|
||||
# COPY . .
|
||||
|
||||
# # 安装 npm 依赖
|
||||
# RUN cd scripts && npm install && cd ..
|
||||
|
||||
# # 构建动态链接版本
|
||||
# RUN cargo build --release
|
||||
|
||||
# # 构建静态链接版本
|
||||
# RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --release
|
||||
# EOF
|
||||
|
||||
# # 构建 arm64 版本
|
||||
# docker buildx build --platform linux/arm64 -f Dockerfile.arm64 -t builder-arm64 .
|
||||
|
||||
# # 创建临时容器
|
||||
# docker create --name temp-container builder-arm64 sh
|
||||
|
||||
# # 复制动态链接版本
|
||||
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-aarch64-unknown-linux-gnu
|
||||
|
||||
# # 复制静态链接版本
|
||||
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-static-aarch64-unknown-linux-gnu
|
||||
|
||||
# # 清理临时容器
|
||||
# docker rm temp-container
|
||||
|
||||
- name: Build Linux x86_64 (Dynamic)
|
||||
if: runner.os == 'Linux'
|
||||
run: bash scripts/build.sh
|
||||
|
||||
- name: Build Linux x86_64 (Static)
|
||||
if: runner.os == 'Linux'
|
||||
run: bash scripts/build.sh --static
|
||||
run: |
|
||||
# 使用 musl 目标
|
||||
rustup target remove x86_64-unknown-linux-gnu
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
# 设置静态编译环境变量
|
||||
export CC=musl-gcc
|
||||
|
||||
bash scripts/build.sh --static
|
||||
|
||||
- name: Install macOS dependencies
|
||||
if: runner.os == 'macOS'
|
||||
@@ -137,18 +89,30 @@ jobs:
|
||||
run: |
|
||||
choco install -y protoc
|
||||
choco install -y openssl
|
||||
echo "OPENSSL_DIR=C:/Program Files/OpenSSL-Win64" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=C:/Program Files/OpenSSL-Win64/lib/pkgconfig" >> $GITHUB_ENV
|
||||
cd scripts && npm install && cd ..
|
||||
choco install -y nodejs-lts
|
||||
|
||||
- name: Build (Dynamic)
|
||||
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
|
||||
# 刷新环境变量
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
# 设置 OpenSSL 环境变量
|
||||
echo "OPENSSL_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=C:\Program Files\OpenSSL\lib\pkgconfig" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Build macOS (Dynamic)
|
||||
if: runner.os == 'macOS' || runner.os == 'Windows'
|
||||
run: bash scripts/build.sh
|
||||
|
||||
- name: Build (Static)
|
||||
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
|
||||
- name: Build macOS (Static)
|
||||
if: runner.os == 'macOS' || runner.os == 'Windows'
|
||||
run: bash scripts/build.sh --static
|
||||
|
||||
# - name: Verify build artifacts
|
||||
# run: |
|
||||
# if [ ! -d "release" ] || [ -z "$(ls -A release)" ]; then
|
||||
# echo "Error: No build artifacts found in release directory"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
@@ -184,7 +148,11 @@ jobs:
|
||||
bash \
|
||||
gmake \
|
||||
pkgconf \
|
||||
openssl
|
||||
openssl \
|
||||
libressl-devel \
|
||||
libiconv \
|
||||
gettext-tools \
|
||||
gettext-runtime
|
||||
|
||||
export SSL_CERT_FILE=/etc/ssl/cert.pem
|
||||
|
||||
@@ -192,11 +160,8 @@ jobs:
|
||||
cd /root
|
||||
git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
|
||||
|
||||
# 然后再进入 scripts 目录
|
||||
cd scripts && npm install && cd ..
|
||||
|
||||
# 安装 rustup 和 Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain nightly
|
||||
|
||||
# 设置持久化的 Rust 环境变量
|
||||
echo '. "$HOME/.cargo/env"' >> /root/.profile
|
||||
@@ -210,7 +175,6 @@ jobs:
|
||||
# 加载环境变量
|
||||
. /root/.profile
|
||||
|
||||
# 构建
|
||||
echo "构建动态链接版本..."
|
||||
/usr/local/bin/bash scripts/build.sh
|
||||
|
||||
|
9
.github/workflows/docker.yml
vendored
9
.github/workflows/docker.yml
vendored
@@ -2,6 +2,12 @@ name: Docker Build and Push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
update_latest:
|
||||
description: '是否更新 latest 标签'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
@@ -36,6 +42,7 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.update_latest }}
|
||||
type=raw,value=${{ steps.cargo_version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
type=ref,event=tag,enable=${{ github.event_name == 'push' }}
|
||||
|
||||
@@ -44,12 +51,14 @@ jobs:
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:latest
|
||||
network=host
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -2,7 +2,10 @@
|
||||
/get-token/target
|
||||
/*.log
|
||||
/*.env
|
||||
/static/tokeninfo.min.html
|
||||
/static/*.min.html
|
||||
/static/*.min.css
|
||||
/static/*.min.js
|
||||
/scripts/.asset-hashes.json
|
||||
node_modules
|
||||
.DS_Store
|
||||
/.vscode
|
||||
@@ -12,3 +15,7 @@ node_modules
|
||||
/cursor-api
|
||||
/cursor-api.exe
|
||||
/release
|
||||
|
||||
/static/readme.html
|
||||
/*.py
|
||||
/src/decoder.rs
|
273
Cargo.lock
generated
273
Cargo.lock
generated
@@ -159,6 +159,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
@@ -180,12 +186,24 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.9.0"
|
||||
@@ -194,9 +212,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.5"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
|
||||
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -222,6 +240,12 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -268,7 +292,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cursor-api"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3-rc.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
@@ -277,7 +301,10 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"flate2",
|
||||
"futures",
|
||||
"gif",
|
||||
"hex",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"rand",
|
||||
@@ -286,6 +313,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower-http",
|
||||
@@ -356,6 +384,15 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
@@ -500,6 +537,16 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -673,7 +720,7 @@ dependencies = [
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -824,6 +871,33 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.7.0"
|
||||
@@ -865,6 +939,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.169"
|
||||
@@ -914,6 +994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -950,6 +1031,15 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -980,7 +1070,7 @@ version = "0.10.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -1052,6 +1142,19 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@@ -1133,10 +1236,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1202,9 +1311,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.9"
|
||||
version = "0.12.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
|
||||
checksum = "7fe060fe50f524be480214aba758c71f99f90ee8c83c5a36b5e9e1d568eb4eb3"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64",
|
||||
@@ -1237,6 +1346,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -1273,7 +1383,7 @@ version = "0.38.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -1321,9 +1431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.18"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
||||
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1346,7 +1456,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -1365,18 +1475,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.216"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.216"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1434,6 +1544,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -1479,9 +1595,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.91"
|
||||
version = "2.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
|
||||
checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1508,13 +1624,26 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
@@ -1645,7 +1774,7 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
@@ -1858,6 +1987,44 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -1867,17 +2034,60 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
@@ -1893,7 +2103,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
@@ -2084,3 +2294,18 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
19
Cargo.toml
19
Cargo.toml
@@ -1,28 +1,39 @@
|
||||
[package]
|
||||
name = "cursor-api"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3-rc.1"
|
||||
edition = "2021"
|
||||
authors = ["wisdgod <nav@wisdgod.com>"]
|
||||
# license = "MIT"
|
||||
# copyright = "Copyright (c) 2024 wisdgod"
|
||||
description = "OpenAI format compatibility layer for the Cursor API"
|
||||
repository = "https://github.com/wisdgod/cursor-api"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.4"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
serde_json = "1.0.134"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.9", features = ["json"] }
|
||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||
# brotli = { version = "7.0.0", default-features = false, features = ["std"] }
|
||||
bytes = "1.9.0"
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
dotenvy = "0.15.7"
|
||||
flate2 = { version = "1.0.35", default-features = false, features = ["rust_backend"] }
|
||||
futures = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
gif = { version = "0.13.1", default-features = false, features = ["std"] }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
|
||||
lazy_static = "1.5.0"
|
||||
prost = "0.13.4"
|
||||
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
||||
regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] }
|
||||
reqwest = { version = "0.12.9", features = ["json", "gzip", "stream"] }
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde_json = { version = "1.0.134", features = ["std"] }
|
||||
reqwest = { version = "0.12.11", default-features = false, features = ["gzip", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
|
||||
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] }
|
||||
serde_json = "1.0.134"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
|
||||
tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["time"] }
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
|
65
Dockerfile
65
Dockerfile
@@ -1,48 +1,47 @@
|
||||
# 构建阶段
|
||||
FROM rust:1.83.0-slim-bookworm as builder
|
||||
|
||||
# AMD64 构建阶段
|
||||
FROM --platform=linux/amd64 rust:1.83.0-slim-bookworm as builder-amd64
|
||||
WORKDIR /app
|
||||
|
||||
# 安装构建依赖
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
protobuf-compiler \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
nodejs \
|
||||
npm \
|
||||
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
RUN cargo build --release && \
|
||||
cp target/release/cursor-api /app/cursor-api
|
||||
|
||||
# 构建
|
||||
RUN rustup target add x86_64-unknown-linux-gnu && \
|
||||
cargo build --target x86_64-unknown-linux-gnu --release && \
|
||||
cp target/x86_64-unknown-linux-gnu/release/cursor-api /app/cursor-api
|
||||
|
||||
# 运行阶段
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# ARM64 构建阶段
|
||||
FROM --platform=linux/arm64 rust:1.83.0-slim-bookworm as builder-arm64
|
||||
WORKDIR /app
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
build-essential protobuf-compiler pkg-config libssl-dev nodejs npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY . .
|
||||
RUN cargo build --release && \
|
||||
cp target/release/cursor-api /app/cursor-api
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/cursor-api .
|
||||
# AMD64 运行阶段
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim as run-amd64
|
||||
WORKDIR /app
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder-amd64 /app/cursor-api .
|
||||
|
||||
# 设置默认端口
|
||||
# ARM64 运行阶段
|
||||
FROM --platform=linux/arm64 debian:bookworm-slim as run-arm64
|
||||
WORKDIR /app
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder-arm64 /app/cursor-api .
|
||||
|
||||
# 通用配置
|
||||
FROM run-${TARGETARCH}
|
||||
ENV PORT=3000
|
||||
|
||||
# 动态暴露端口
|
||||
EXPOSE ${PORT}
|
||||
|
||||
CMD ["./cursor-api"]
|
59
README.md
59
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录(赠送 250 次快速响应,可通过删除账号再注册重置)
|
||||
2. 在浏览器中打开开发者工具(F12)
|
||||
3. 找到 Application-Cookies 中名为 `WorkosCursorSessionToken` 的值并保存(相当于 openai 的密钥)
|
||||
3. 找到 Application-Cookies 中名为 `WorkosCursorSessionToken` 的值并复制其第3个字段,%3A%3A是::的编码,cookie用:分隔值
|
||||
|
||||
## 接口说明
|
||||
|
||||
@@ -74,7 +74,9 @@
|
||||
1. `.token` 文件:每行一个token,支持以下格式:
|
||||
|
||||
```
|
||||
# 这是注释
|
||||
token1
|
||||
# alias与标签的作用差不多
|
||||
alias::token2
|
||||
```
|
||||
|
||||
@@ -84,8 +86,10 @@
|
||||
2. `.token-list` 文件:每行为token和checksum的对应关系:
|
||||
|
||||
```
|
||||
# 这里的#表示这行在下次读取要删除
|
||||
token1,checksum1
|
||||
token2,checksum2
|
||||
# 支持像.token一样的alias,冲突时以.token为准
|
||||
alias::token2,checksum2
|
||||
```
|
||||
|
||||
该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:
|
||||
@@ -97,17 +101,19 @@
|
||||
|
||||
写死了,后续也不会会支持自定义模型列表
|
||||
```
|
||||
cursor-small
|
||||
claude-3.5-sonnet
|
||||
gpt-4
|
||||
gpt-4o
|
||||
claude-3-opus
|
||||
cursor-fast
|
||||
cursor-small
|
||||
gpt-3.5
|
||||
gpt-3.5-turbo
|
||||
gpt-4-turbo-2024-04-09
|
||||
gpt-4
|
||||
gpt-4o-128k
|
||||
gemini-1.5-flash-500k
|
||||
claude-3-haiku-200k
|
||||
claude-3-5-sonnet-200k
|
||||
claude-3-5-sonnet-20240620
|
||||
claude-3-5-sonnet-20241022
|
||||
gpt-4o-mini
|
||||
o1-mini
|
||||
@@ -121,30 +127,6 @@ gemini-2.0-flash-exp
|
||||
|
||||
## 部署
|
||||
|
||||
### 本地部署
|
||||
|
||||
#### 从源码编译
|
||||
|
||||
需要安装 Rust 工具链和依赖:
|
||||
|
||||
```bash
|
||||
# 安装rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# 安装依赖(Debian/Ubuntu)
|
||||
apt-get install -y build-essential protobuf-compiler pkg-config libssl-dev nodejs npm
|
||||
|
||||
# 原生编译
|
||||
cargo build --release
|
||||
|
||||
# 交叉编译,以x86_64-unknown-linux-gnu为例,老实说,这也算原生编译,因为使用了docker
|
||||
cross build --target x86_64-unknown-linux-gnu --release
|
||||
```
|
||||
|
||||
#### 使用预编译二进制
|
||||
|
||||
从 [Releases](https://github.com/wisdgod/cursor-api/releases) 下载对应平台的二进制文件。
|
||||
|
||||
### Docker 部署
|
||||
|
||||
#### Docker 运行示例
|
||||
@@ -153,13 +135,6 @@ cross build --target x86_64-unknown-linux-gnu --release
|
||||
docker run -d -e PORT=3000 -e AUTH_TOKEN=your_token -p 3000:3000 wisdgod/cursor-api:latest
|
||||
```
|
||||
|
||||
#### Docker 构建示例
|
||||
|
||||
```bash
|
||||
docker build -t cursor-api .
|
||||
docker run -p 3000:3000 cursor-api
|
||||
```
|
||||
|
||||
### huggingface部署
|
||||
|
||||
前提:一个huggingface账号
|
||||
@@ -190,6 +165,8 @@ docker run -p 3000:3000 cursor-api
|
||||
TOKEN_LIST_FILE=.token-list
|
||||
```
|
||||
|
||||
更多变量示例可访问 /env-example
|
||||
|
||||
3. 重新部署
|
||||
|
||||
点击`Factory rebuild`,等待部署完成
|
||||
@@ -210,15 +187,7 @@ docker run -p 3000:3000 cursor-api
|
||||
|
||||
### 跨平台编译
|
||||
|
||||
使用提供的构建脚本:
|
||||
|
||||
```bash
|
||||
# 仅编译当前平台
|
||||
./scripts/build.sh
|
||||
|
||||
# 交叉编译所有支持的平台
|
||||
./scripts/build.sh --cross
|
||||
```
|
||||
自行配置cross编译环境
|
||||
|
||||
支持的平台:
|
||||
|
||||
|
137
build.rs
137
build.rs
@@ -1,15 +1,19 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
// 支持的文件类型
|
||||
const SUPPORTED_EXTENSIONS: [&str; 3] = ["html", "js", "css"];
|
||||
|
||||
fn check_and_install_deps() -> Result<()> {
|
||||
let scripts_dir = Path::new("scripts");
|
||||
let node_modules = scripts_dir.join("node_modules");
|
||||
|
||||
// 如果 node_modules 不存在,运行 npm install
|
||||
if !node_modules.exists() {
|
||||
println!("cargo:warning=Installing HTML minifier dependencies...");
|
||||
|
||||
println!("cargo:warning=Installing minifier dependencies...");
|
||||
let status = Command::new("npm")
|
||||
.current_dir(scripts_dir)
|
||||
.arg("install")
|
||||
@@ -23,38 +27,139 @@ fn check_and_install_deps() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn minify_html() -> Result<()> {
|
||||
println!("cargo:warning=Minifying HTML files...");
|
||||
fn get_files_hash() -> Result<HashMap<PathBuf, String>> {
|
||||
let mut file_hashes = HashMap::new();
|
||||
let static_dir = Path::new("static");
|
||||
|
||||
if static_dir.exists() {
|
||||
for entry in fs::read_dir(static_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// 检查是否是支持的文件类型,且不是已经压缩的文件
|
||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
if SUPPORTED_EXTENSIONS.contains(&ext) && !path.to_string_lossy().contains(".min.")
|
||||
{
|
||||
let content = fs::read(&path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&content);
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
file_hashes.insert(path, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(file_hashes)
|
||||
}
|
||||
|
||||
fn load_saved_hashes() -> Result<HashMap<PathBuf, String>> {
|
||||
let hash_file = Path::new("scripts/.asset-hashes.json");
|
||||
if hash_file.exists() {
|
||||
let content = fs::read_to_string(hash_file)?;
|
||||
let hash_map: HashMap<String, String> = serde_json::from_str(&content)?;
|
||||
Ok(hash_map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (PathBuf::from(k), v))
|
||||
.collect())
|
||||
} else {
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn save_hashes(hashes: &HashMap<PathBuf, String>) -> Result<()> {
|
||||
let hash_file = Path::new("scripts/.asset-hashes.json");
|
||||
let string_map: HashMap<String, String> = hashes
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string_lossy().into_owned(), v.clone()))
|
||||
.collect();
|
||||
let content = serde_json::to_string_pretty(&string_map)?;
|
||||
fs::write(hash_file, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn minify_assets() -> Result<()> {
|
||||
// 获取现有文件的哈希
|
||||
let current_hashes = get_files_hash()?;
|
||||
|
||||
if current_hashes.is_empty() {
|
||||
println!("cargo:warning=No files to minify");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 加载保存的哈希值
|
||||
let saved_hashes = load_saved_hashes()?;
|
||||
|
||||
// 找出需要更新的文件
|
||||
let files_to_update: Vec<_> = current_hashes
|
||||
.iter()
|
||||
.filter(|(path, current_hash)| {
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
let min_path = path.with_file_name(format!(
|
||||
"{}.min.{}",
|
||||
path.file_stem().unwrap().to_string_lossy(),
|
||||
ext
|
||||
));
|
||||
|
||||
// 检查压缩后的文件是否存在
|
||||
if !min_path.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查原始文件是否发生变化
|
||||
saved_hashes
|
||||
.get(*path)
|
||||
.map_or(true, |saved_hash| saved_hash != *current_hash)
|
||||
})
|
||||
.map(|(path, _)| path.file_name().unwrap().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
|
||||
if files_to_update.is_empty() {
|
||||
println!("cargo:warning=No files need to be updated");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("cargo:warning=Minifying {} files...", files_to_update.len());
|
||||
|
||||
// 运行压缩脚本
|
||||
let status = Command::new("node")
|
||||
.args(&["scripts/minify-html.js"])
|
||||
.arg("scripts/minify.js")
|
||||
.args(&files_to_update)
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
panic!("HTML minification failed");
|
||||
panic!("Asset minification failed");
|
||||
}
|
||||
|
||||
// 保存新的哈希值
|
||||
save_hashes(¤t_hashes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Proto 文件处理
|
||||
println!("cargo:rerun-if-changed=src/message.proto");
|
||||
println!("cargo:rerun-if-changed=src/aiserver/v1/aiserver.proto");
|
||||
let mut config = prost_build::Config::new();
|
||||
config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
|
||||
// config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
|
||||
// config.type_attribute(
|
||||
// "aiserver.v1.ThrowErrorCheckRequest",
|
||||
// "#[derive(serde::Serialize, serde::Deserialize)]"
|
||||
// );
|
||||
config
|
||||
.compile_protos(&["src/message.proto"], &["src/"])
|
||||
.compile_protos(&["src/aiserver/v1/aiserver.proto"], &["src/aiserver/v1/"])
|
||||
.unwrap();
|
||||
|
||||
// HTML 文件处理
|
||||
println!("cargo:rerun-if-changed=static/tokeninfo.html");
|
||||
println!("cargo:rerun-if-changed=scripts/minify-html.js");
|
||||
// 静态资源文件处理
|
||||
println!("cargo:rerun-if-changed=scripts/minify.js");
|
||||
println!("cargo:rerun-if-changed=scripts/package.json");
|
||||
println!("cargo:rerun-if-changed=static");
|
||||
|
||||
// 检查并安装依赖
|
||||
check_and_install_deps()?;
|
||||
|
||||
// 运行 HTML 压缩
|
||||
minify_html()?;
|
||||
// 运行资源压缩
|
||||
minify_assets()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,158 +1,126 @@
|
||||
# <EFBFBD><EFBFBD>ɫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Blue }
|
||||
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
|
||||
function Write-Error { Write-Host "[ERROR] $args" -ForegroundColor Red; exit 1 }
|
||||
# 参数处理
|
||||
param(
|
||||
[switch]$Static,
|
||||
[switch]$Help,
|
||||
[ValidateSet("x86_64", "aarch64", "i686")]
|
||||
[string]$Architecture
|
||||
)
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>Ĺ<EFBFBD><EFBFBD><EFBFBD>
|
||||
function Test-Requirements {
|
||||
$tools = @("cargo", "protoc", "npm", "node")
|
||||
$missing = @()
|
||||
# 设置错误时停止执行
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
foreach ($tool in $tools) {
|
||||
if (!(Get-Command $tool -ErrorAction SilentlyContinue)) {
|
||||
$missing += $tool
|
||||
}
|
||||
}
|
||||
# 颜色输出函数
|
||||
function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue }
|
||||
function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
|
||||
function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 }
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
Write-Error "ȱ<EFBFBD>ٱ<EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $($missing -join ', ')"
|
||||
}
|
||||
}
|
||||
# 检查必要的工具
|
||||
function Check-Requirements {
|
||||
$tools = @("cargo", "protoc", "npm", "node")
|
||||
$missing = @()
|
||||
|
||||
# <20><> Test-Requirements <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>º<EFBFBD><C2BA><EFBFBD>
|
||||
function Initialize-VSEnvironment {
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڳ<EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>..."
|
||||
|
||||
# ֱ<><D6B1>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD>֪<EFBFBD><D6AA> vcvarsall.bat ·<><C2B7>
|
||||
$vcvarsallPath = "E:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat"
|
||||
|
||||
if (-not (Test-Path $vcvarsallPath)) {
|
||||
Write-Error "δ<EFBFBD>ҵ<EFBFBD> vcvarsall.bat: $vcvarsallPath"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Info "ʹ<EFBFBD><EFBFBD> vcvarsall.bat ·<><C2B7>: $vcvarsallPath"
|
||||
|
||||
# <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$archArg = "x64"
|
||||
$command = "`"$vcvarsallPath`" $archArg && set"
|
||||
|
||||
try {
|
||||
$output = cmd /c "$command" 2>&1
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ɹ<EFBFBD>ִ<EFBFBD><D6B4>
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "vcvarsall.bat ִ<><D6B4>ʧ<EFBFBD>ܣ<EFBFBD><DCA3>˳<EFBFBD><CBB3><EFBFBD>: $LASTEXITCODE"
|
||||
return
|
||||
foreach ($tool in $tools) {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
|
||||
$missing += $tool
|
||||
}
|
||||
|
||||
# <20><><EFBFBD>µ<EFBFBD>ǰ PowerShell <20>Ự<EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
foreach ($line in $output) {
|
||||
if ($line -match "^([^=]+)=(.*)$") {
|
||||
$name = $matches[1]
|
||||
$value = $matches[2]
|
||||
if (![string]::IsNullOrEmpty($name)) {
|
||||
Set-Item -Path "env:$name" -Value $value -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Info "Visual Studio <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
Write-Error "缺少必要工具: $($missing -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
# 帮助信息
|
||||
function Show-Help {
|
||||
Write-Host @"
|
||||
<EFBFBD>÷<EFBFBD>: $(Split-Path $MyInvocation.MyCommand.Path -Leaf) [ѡ<EFBFBD><EFBFBD>]
|
||||
Write-Host @"
|
||||
用法: $(Split-Path $MyInvocation.ScriptName -Leaf) [选项]
|
||||
|
||||
ѡ<EFBFBD><EFBFBD>:
|
||||
--static ʹ<EFBFBD>þ<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>Ĭ<EFBFBD>϶<EFBFBD>̬<EFBFBD><EFBFBD><EFBFBD>ӣ<EFBFBD>
|
||||
--help <EFBFBD><EFBFBD>ʾ<EFBFBD>˰<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
选项:
|
||||
-Static 使用静态链接(默认动态链接)
|
||||
-Help 显示此帮助信息
|
||||
|
||||
Ĭ<EFBFBD>ϱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Windows ֧<EFBFBD>ֵļܹ<EFBFBD> (x64 <EFBFBD><EFBFBD> arm64)
|
||||
不带参数时使用默认配置构建
|
||||
"@
|
||||
}
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
function New-Target {
|
||||
param (
|
||||
[string]$target,
|
||||
[string]$rustflags
|
||||
)
|
||||
# 构建函数
|
||||
function Build-Target {
|
||||
param (
|
||||
[string]$Target,
|
||||
[string]$RustFlags
|
||||
)
|
||||
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ڹ<EFBFBD><EFBFBD><EFBFBD> $target..."
|
||||
Write-Info "正在构建 $Target..."
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD>й<EFBFBD><EFBFBD><EFBFBD>
|
||||
$env:RUSTFLAGS = $rustflags
|
||||
cargo build --target $target --release
|
||||
# 设置环境变量
|
||||
$env:RUSTFLAGS = $RustFlags
|
||||
|
||||
# <EFBFBD>ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$binaryName = "cursor-api"
|
||||
if ($UseStatic) {
|
||||
$binaryName += "-static"
|
||||
}
|
||||
# 构建
|
||||
if ($Target -ne (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) {
|
||||
cargo build --target $Target --release
|
||||
} else {
|
||||
cargo build --release
|
||||
}
|
||||
|
||||
$sourcePath = "target/$target/release/cursor-api.exe"
|
||||
$targetPath = "release/${binaryName}-${target}.exe"
|
||||
# 移动编译产物到 release 目录
|
||||
$binaryName = "cursor-api"
|
||||
if ($Static) {
|
||||
$binaryName += "-static"
|
||||
}
|
||||
|
||||
if (Test-Path $sourcePath) {
|
||||
Copy-Item $sourcePath $targetPath -Force
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD> $target"
|
||||
}
|
||||
else {
|
||||
Write-Warn "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD>: $target"
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
$binaryPath = if ($Target -eq (rustc -Vv | Select-String "host: (.*)" | ForEach-Object { $_.Matches.Groups[1].Value })) {
|
||||
"target/release/cursor-api.exe"
|
||||
} else {
|
||||
"target/$Target/release/cursor-api.exe"
|
||||
}
|
||||
|
||||
if (Test-Path $binaryPath) {
|
||||
Copy-Item $binaryPath "release/$binaryName-$Target.exe"
|
||||
Write-Info "完成构建 $Target"
|
||||
} else {
|
||||
Write-Warn "构建产物未找到: $Target"
|
||||
Write-Warn "查找路径: $binaryPath"
|
||||
Write-Warn "当前目录内容:"
|
||||
Get-ChildItem -Recurse target/
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$UseStatic = $false
|
||||
|
||||
foreach ($arg in $args) {
|
||||
switch ($arg) {
|
||||
"--static" { $UseStatic = $true }
|
||||
"--help" { Show-Help; exit 0 }
|
||||
default { Write-Error "δ֪<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $arg" }
|
||||
}
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
exit 0
|
||||
}
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
try {
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
Test-Requirements
|
||||
# 检查依赖
|
||||
Check-Requirements
|
||||
|
||||
# <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD> Visual Studio <20><><EFBFBD><EFBFBD>
|
||||
Initialize-VSEnvironment
|
||||
# 创建 release 目录
|
||||
New-Item -ItemType Directory -Force -Path release | Out-Null
|
||||
|
||||
# <EFBFBD><EFBFBD><EFBFBD><EFBFBD> release Ŀ¼
|
||||
New-Item -ItemType Directory -Force -Path "release" | Out-Null
|
||||
|
||||
# <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ƽ̨
|
||||
$targets = @(
|
||||
"x86_64-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc"
|
||||
)
|
||||
|
||||
# <20><><EFBFBD>þ<EFBFBD>̬<EFBFBD><CCAC><EFBFBD>ӱ<EFBFBD>־
|
||||
$rustflags = ""
|
||||
if ($UseStatic) {
|
||||
$rustflags = "-C target-feature=+crt-static"
|
||||
}
|
||||
|
||||
Write-Info "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>
|
||||
foreach ($target in $targets) {
|
||||
New-Target -target $target -rustflags $rustflags
|
||||
}
|
||||
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
|
||||
# 设置静态链接标志
|
||||
$rustFlags = ""
|
||||
if ($Static) {
|
||||
$rustFlags = "-C target-feature=+crt-static"
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>з<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: $_"
|
||||
}
|
||||
|
||||
# 获取目标架构
|
||||
$arch = if ($Architecture) {
|
||||
$Architecture
|
||||
} else {
|
||||
switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||
"AMD64" { "x86_64" }
|
||||
"ARM64" { "aarch64" }
|
||||
"X86" { "i686" }
|
||||
default { Write-Error "不支持的架构: $env:PROCESSOR_ARCHITECTURE" }
|
||||
}
|
||||
}
|
||||
$target = "$arch-pc-windows-msvc"
|
||||
|
||||
Write-Info "开始构建..."
|
||||
if (-not (Build-Target -Target $target -RustFlags $rustFlags)) {
|
||||
Write-Error "构建失败"
|
||||
}
|
||||
|
||||
Write-Info "构建完成!"
|
@@ -6,11 +6,6 @@ info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||
|
||||
# 检查是否在 Linux 环境
|
||||
is_linux() {
|
||||
[ "$(uname -s)" = "Linux" ]
|
||||
}
|
||||
|
||||
# 检查必要的工具
|
||||
check_requirements() {
|
||||
local missing_tools=()
|
||||
@@ -22,23 +17,29 @@ check_requirements() {
|
||||
fi
|
||||
done
|
||||
|
||||
# cross 工具检查(仅在 Linux 上需要)
|
||||
if [[ "$OS" == "Linux" ]] && ! command -v cross &>/dev/null; then
|
||||
missing_tools+=("cross")
|
||||
fi
|
||||
|
||||
if [[ ${#missing_tools[@]} -gt 0 ]]; then
|
||||
error "缺少必要工具: ${missing_tools[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
USE_STATIC=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--static) USE_STATIC=true ;;
|
||||
--help) show_help; exit 0 ;;
|
||||
*) error "未知参数: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# 帮助信息
|
||||
show_help() {
|
||||
cat << EOF
|
||||
用法: $(basename "$0") [选项]
|
||||
|
||||
选项:
|
||||
--cross 使用 cross 进行交叉编译(仅在 Linux 上有效)
|
||||
--static 使用静态链接(默认动态链接)
|
||||
--help 显示此帮助信息
|
||||
|
||||
@@ -46,22 +47,6 @@ show_help() {
|
||||
EOF
|
||||
}
|
||||
|
||||
# 判断是否使用 cross
|
||||
should_use_cross() {
|
||||
local target=$1
|
||||
# 如果不是 Linux 环境,直接返回 false
|
||||
if [[ "$OS" != "Linux" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 在 Linux 环境下,以下目标不使用 cross:
|
||||
# 1. Linux 上的 x86_64-unknown-linux-gnu
|
||||
if [[ "$target" == "x86_64-unknown-linux-gnu" ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 并行构建函数
|
||||
build_target() {
|
||||
local target=$1
|
||||
@@ -73,15 +58,11 @@ build_target() {
|
||||
# 确定文件后缀
|
||||
[[ $target == *"windows"* ]] && extension=".exe"
|
||||
|
||||
# 判断是否使用 cross
|
||||
if should_use_cross "$target"; then
|
||||
env RUSTFLAGS="$rustflags" cross build --target "$target" --release
|
||||
# 构建
|
||||
if [[ $target != "$CURRENT_TARGET" ]]; then
|
||||
env RUSTFLAGS="$rustflags" cargo build --target "$target" --release
|
||||
else
|
||||
if [[ $target != "$CURRENT_TARGET" ]]; then
|
||||
env RUSTFLAGS="$rustflags" cargo build --target "$target" --release
|
||||
else
|
||||
env RUSTFLAGS="$rustflags" cargo build --release
|
||||
fi
|
||||
env RUSTFLAGS="$rustflags" cargo build --release
|
||||
fi
|
||||
|
||||
# 移动编译产物到 release 目录
|
||||
@@ -117,7 +98,13 @@ get_target() {
|
||||
local os=$2
|
||||
case "$os" in
|
||||
"Darwin") echo "${arch}-apple-darwin" ;;
|
||||
"Linux") echo "${arch}-unknown-linux-gnu" ;;
|
||||
"Linux")
|
||||
if [[ $USE_STATIC == true ]]; then
|
||||
echo "${arch}-unknown-linux-musl"
|
||||
else
|
||||
echo "${arch}-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;;
|
||||
"FreeBSD") echo "${arch}-unknown-freebsd" ;;
|
||||
*) error "不支持的系统: $os" ;;
|
||||
@@ -134,16 +121,16 @@ CURRENT_TARGET=$(get_target "$ARCH" "$OS")
|
||||
get_targets() {
|
||||
case "$1" in
|
||||
"linux")
|
||||
# Linux 构建所有 Linux 目标和 FreeBSD 目标
|
||||
echo "x86_64-unknown-linux-gnu x86_64-unknown-freebsd"
|
||||
# Linux 只构建当前架构
|
||||
echo "$CURRENT_TARGET"
|
||||
;;
|
||||
"freebsd")
|
||||
# FreeBSD 只构建当前架构的 FreeBSD 目标
|
||||
echo "${ARCH}-unknown-freebsd"
|
||||
# FreeBSD 只构建当前架构
|
||||
echo "$CURRENT_TARGET"
|
||||
;;
|
||||
"windows")
|
||||
# Windows 构建所有 Windows 目标
|
||||
echo "x86_64-pc-windows-msvc"
|
||||
# Windows 只构建当前架构
|
||||
echo "$CURRENT_TARGET"
|
||||
;;
|
||||
"macos")
|
||||
# macOS 构建所有 macOS 目标
|
||||
@@ -153,33 +140,21 @@ get_targets() {
|
||||
esac
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
USE_STATIC=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--static) USE_STATIC=true ;;
|
||||
--help) show_help; exit 0 ;;
|
||||
*) error "未知参数: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# 检查依赖
|
||||
check_requirements
|
||||
|
||||
# 确定要构建的目标
|
||||
case "$OS" in
|
||||
"Darwin")
|
||||
Darwin)
|
||||
TARGETS=($(get_targets "macos"))
|
||||
;;
|
||||
"Linux")
|
||||
Linux)
|
||||
TARGETS=($(get_targets "linux"))
|
||||
;;
|
||||
"FreeBSD")
|
||||
FreeBSD)
|
||||
TARGETS=($(get_targets "freebsd"))
|
||||
;;
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT")
|
||||
MINGW*|MSYS*|CYGWIN*|Windows_NT)
|
||||
TARGETS=($(get_targets "windows"))
|
||||
;;
|
||||
*) error "不支持的系统: $OS" ;;
|
||||
@@ -189,8 +164,8 @@ esac
|
||||
mkdir -p release
|
||||
|
||||
# 设置静态链接标志
|
||||
RUSTFLAGS=""
|
||||
[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static"
|
||||
RUSTFLAGS="-C link-arg=-s"
|
||||
[[ $USE_STATIC == true ]] && RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-s"
|
||||
|
||||
# 并行构建所有目标
|
||||
info "开始构建..."
|
||||
|
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { minify } = require('html-minifier-terser');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 配置选项
|
||||
const options = {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeOptionalTags: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
processScripts: ['application/json'],
|
||||
};
|
||||
|
||||
// 处理文件
|
||||
async function minifyFile(inputPath, outputPath) {
|
||||
try {
|
||||
const html = fs.readFileSync(inputPath, 'utf8');
|
||||
const minified = await minify(html, options);
|
||||
fs.writeFileSync(outputPath, minified);
|
||||
console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
|
||||
} catch (err) {
|
||||
console.error(`✗ Error processing ${inputPath}:`, err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const staticDir = path.join(__dirname, '..', 'static');
|
||||
const files = [
|
||||
['tokeninfo.html', 'tokeninfo.min.html'],
|
||||
];
|
||||
|
||||
for (const [input, output] of files) {
|
||||
await minifyFile(
|
||||
path.join(staticDir, input),
|
||||
path.join(staticDir, output)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
81
scripts/minify.js
Normal file
81
scripts/minify.js
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { minify: minifyHtml } = require('html-minifier-terser');
|
||||
const { minify: minifyJs } = require('terser');
|
||||
const CleanCSS = require('clean-css');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 配置选项
|
||||
const options = {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeOptionalTags: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
processScripts: ['application/json'],
|
||||
};
|
||||
|
||||
// CSS 压缩选项
|
||||
const cssOptions = {
|
||||
level: 2
|
||||
};
|
||||
|
||||
// 处理文件
|
||||
async function minifyFile(inputPath, outputPath) {
|
||||
try {
|
||||
const ext = path.extname(inputPath).toLowerCase();
|
||||
const content = fs.readFileSync(inputPath, 'utf8');
|
||||
let minified;
|
||||
|
||||
switch (ext) {
|
||||
case '.html':
|
||||
minified = await minifyHtml(content, options);
|
||||
break;
|
||||
case '.js':
|
||||
const result = await minifyJs(content);
|
||||
minified = result.code;
|
||||
break;
|
||||
case '.css':
|
||||
minified = new CleanCSS(cssOptions).minify(content).styles;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported file type: ${ext}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, minified);
|
||||
console.log(`✓ Minified ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
|
||||
} catch (err) {
|
||||
console.error(`✗ Error processing ${inputPath}:`, err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
// 获取命令行参数,跳过前两个参数(node和脚本路径)
|
||||
const files = process.argv.slice(2);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error('No input files specified');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const staticDir = path.join(__dirname, '..', 'static');
|
||||
|
||||
for (const file of files) {
|
||||
const inputPath = path.join(staticDir, file);
|
||||
const ext = path.extname(file);
|
||||
const outputPath = path.join(
|
||||
staticDir,
|
||||
file.replace(ext, `.min${ext}`)
|
||||
);
|
||||
await minifyFile(inputPath, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
4
scripts/package-lock.json
generated
4
scripts/package-lock.json
generated
@@ -8,7 +8,9 @@
|
||||
"name": "html-minifier-scripts",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"html-minifier-terser": "^7.2.0"
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"terser": "^5.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
@@ -6,6 +6,8 @@
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-minifier-terser": "^7.2.0"
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"terser": "^5.37.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
# <20><><EFBFBD><EFBFBD> PowerShell <20><><EFBFBD><EFBFBD>Ϊ UTF-8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>Թ<EFBFBD><D4B9><EFBFBD>ԱȨ<D4B1><C8A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
Write-Warning "<EFBFBD><EFBFBD><EFBFBD>Թ<EFBFBD><EFBFBD><EFBFBD>ԱȨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д˽ű<EFBFBD>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# <20><><EFBFBD>鲢<EFBFBD><E9B2A2>װ Chocolatey
|
||||
if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ Chocolatey..."
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
}
|
||||
|
||||
# <20><>װ<EFBFBD><D7B0>Ҫ<EFBFBD>Ĺ<EFBFBD><C4B9><EFBFBD>
|
||||
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD>Ĺ<EFBFBD><EFBFBD><EFBFBD>..."
|
||||
choco install -y mingw
|
||||
choco install -y protoc
|
||||
choco install -y git
|
||||
|
||||
# <20><>װ Rust <20><><EFBFBD><EFBFBD>
|
||||
Write-Output "<EFBFBD><EFBFBD><EFBFBD>ڰ<EFBFBD>װ Rust <20><><EFBFBD><EFBFBD>..."
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
rustup target add x86_64-unknown-linux-gnu
|
||||
cargo install cross
|
||||
|
||||
Write-Output "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
|
179
scripts/setup.ps1
Normal file
179
scripts/setup.ps1
Normal file
@@ -0,0 +1,179 @@
|
||||
# <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD>ʱִֹͣ<D6B9><D6B4>
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue" # <20>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ٶ<EFBFBD>
|
||||
|
||||
# <20><>ɫ<EFBFBD><C9AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue }
|
||||
function Write-Warn { param($Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
|
||||
function Write-Success { param($Message) Write-Host "[SUCCESS] $Message" -ForegroundColor Green }
|
||||
function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԱȨ<D4B1><C8A8>
|
||||
function Test-Administrator {
|
||||
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal $user
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "<EFBFBD><EFBFBD><EFBFBD>Թ<EFBFBD><EFBFBD><EFBFBD>ԱȨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>д˽ű<EFBFBD>"
|
||||
}
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
function Show-Help {
|
||||
Write-Host @"
|
||||
<EFBFBD>÷<EFBFBD>: $(Split-Path $MyInvocation.ScriptName -Leaf) [ѡ<EFBFBD><EFBFBD>]
|
||||
|
||||
ѡ<EFBFBD><EFBFBD>:
|
||||
-NoVS <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Visual Studio Build Tools
|
||||
-NoRust <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Rust
|
||||
-NoNode <EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ Node.js
|
||||
-Help <EFBFBD><EFBFBD>ʾ<EFBFBD>˰<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ
|
||||
|
||||
ʾ<EFBFBD><EFBFBD>:
|
||||
.\setup.ps1
|
||||
.\setup.ps1 -NoVS
|
||||
.\setup.ps1 -NoRust -NoNode
|
||||
"@
|
||||
}
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
param(
|
||||
[switch]$NoVS,
|
||||
[switch]$NoRust,
|
||||
[switch]$NoNode,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
exit 0
|
||||
}
|
||||
|
||||
# <20><><EFBFBD>鲢<EFBFBD><E9B2A2>װ Chocolatey
|
||||
function Install-Chocolatey {
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Chocolatey..."
|
||||
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Chocolatey..."
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
try {
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD>װ Chocolatey ʧ<><CAA7>: $_"
|
||||
}
|
||||
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
}
|
||||
}
|
||||
|
||||
# <20><>װ Visual Studio Build Tools
|
||||
function Install-VSBuildTools {
|
||||
if ($NoVS) {
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Visual Studio Build Tools <20><>װ"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Visual Studio Build Tools..."
|
||||
$vsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vsPath)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Visual Studio Build Tools..."
|
||||
try {
|
||||
# <20><><EFBFBD>ذ<EFBFBD>װ<EFBFBD><D7B0><EFBFBD><EFBFBD>
|
||||
$vsInstallerUrl = "https://aka.ms/vs/17/release/vs_BuildTools.exe"
|
||||
$vsInstallerPath = "$env:TEMP\vs_BuildTools.exe"
|
||||
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath
|
||||
|
||||
# <20><>װ
|
||||
$process = Start-Process -FilePath $vsInstallerPath -ArgumentList `
|
||||
"--quiet", "--wait", "--norestart", "--nocache", `
|
||||
"--installPath", "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools", `
|
||||
"--add", "Microsoft.VisualStudio.Workload.VCTools" `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
|
||||
if ($process.ExitCode -ne 0) {
|
||||
Write-Error "Visual Studio Build Tools <20><>װʧ<D7B0><CAA7>"
|
||||
}
|
||||
|
||||
Remove-Item $vsInstallerPath -Force
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD>װ Visual Studio Build Tools ʧ<><CAA7>: $_"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Info "Visual Studio Build Tools <20>Ѱ<EFBFBD>װ"
|
||||
}
|
||||
}
|
||||
|
||||
# <20><>װ Rust
|
||||
function Install-Rust {
|
||||
if ($NoRust) {
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust <20><>װ"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust..."
|
||||
if (-not (Get-Command rustc -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Rust..."
|
||||
try {
|
||||
$rustupInit = "$env:TEMP\rustup-init.exe"
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs" -OutFile $rustupInit
|
||||
Start-Process -FilePath $rustupInit -ArgumentList "-y" -Wait
|
||||
Remove-Item $rustupInit -Force
|
||||
|
||||
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD>װ Rust ʧ<><CAA7>: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ƽ̨
|
||||
Write-Info "<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rust Ŀ<><C4BF>ƽ̨..."
|
||||
$arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" }
|
||||
rustup target add "$arch-pc-windows-msvc"
|
||||
}
|
||||
|
||||
# <20><>װ<EFBFBD><D7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
function Install-Tools {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
|
||||
|
||||
# <20><>װ protoc
|
||||
if (-not (Get-Command protoc -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Protocol Buffers..."
|
||||
choco install -y protoc
|
||||
}
|
||||
|
||||
# <20><>װ Git
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Git..."
|
||||
choco install -y git
|
||||
}
|
||||
|
||||
# <20><>װ Node.js
|
||||
if (-not $NoNode -and -not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "<EFBFBD><EFBFBD>װ Node.js..."
|
||||
choco install -y nodejs
|
||||
}
|
||||
|
||||
# ˢ<>»<EFBFBD><C2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
}
|
||||
|
||||
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
try {
|
||||
Write-Info "<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>..."
|
||||
|
||||
Install-Chocolatey
|
||||
Install-VSBuildTools
|
||||
Install-Rust
|
||||
Install-Tools
|
||||
|
||||
Write-Success "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>"
|
||||
}
|
||||
catch {
|
||||
Write-Error "<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>г<EFBFBD><EFBFBD>ִ<EFBFBD><EFBFBD><EFBFBD>: $_"
|
||||
}
|
157
scripts/setup.sh
Normal file
157
scripts/setup.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 设置错误时退出
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}[INFO] $1${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}[ERROR] $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查是否为 root 用户(FreeBSD 和 Linux)
|
||||
if [ "$(uname)" != "Darwin" ] && [ "$EUID" -ne 0 ]; then
|
||||
error "请使用 root 权限运行此脚本 (sudo ./setup.sh)"
|
||||
fi
|
||||
|
||||
# 检测包管理器
|
||||
if command -v brew &> /dev/null; then
|
||||
PKG_MANAGER="brew"
|
||||
info "检测到 macOS/Homebrew 系统"
|
||||
elif command -v pkg &> /dev/null; then
|
||||
PKG_MANAGER="pkg"
|
||||
info "检测到 FreeBSD 系统"
|
||||
elif command -v apt-get &> /dev/null; then
|
||||
PKG_MANAGER="apt-get"
|
||||
info "检测到 Debian/Ubuntu 系统"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
PKG_MANAGER="dnf"
|
||||
info "检测到 Fedora/RHEL 系统"
|
||||
elif command -v yum &> /dev/null; then
|
||||
PKG_MANAGER="yum"
|
||||
info "检测到 CentOS 系统"
|
||||
else
|
||||
error "未检测到支持的包管理器"
|
||||
fi
|
||||
|
||||
# 更新包管理器缓存
|
||||
info "更新包管理器缓存..."
|
||||
case $PKG_MANAGER in
|
||||
"brew")
|
||||
brew update
|
||||
;;
|
||||
"pkg")
|
||||
pkg update
|
||||
;;
|
||||
*)
|
||||
$PKG_MANAGER update -y
|
||||
;;
|
||||
esac
|
||||
|
||||
# 安装基础构建工具
|
||||
info "安装基础构建工具..."
|
||||
case $PKG_MANAGER in
|
||||
"brew")
|
||||
brew install \
|
||||
protobuf \
|
||||
pkg-config \
|
||||
openssl \
|
||||
curl \
|
||||
git \
|
||||
node
|
||||
;;
|
||||
"pkg")
|
||||
pkg install -y \
|
||||
gmake \
|
||||
protobuf \
|
||||
pkgconf \
|
||||
openssl \
|
||||
curl \
|
||||
git \
|
||||
node
|
||||
;;
|
||||
"apt-get")
|
||||
$PKG_MANAGER install -y --no-install-recommends \
|
||||
build-essential \
|
||||
protobuf-compiler \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
curl \
|
||||
tzdata \
|
||||
git
|
||||
;;
|
||||
*)
|
||||
$PKG_MANAGER install -y \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
make \
|
||||
protobuf-compiler \
|
||||
pkg-config \
|
||||
openssl-devel \
|
||||
ca-certificates \
|
||||
curl \
|
||||
tzdata \
|
||||
git
|
||||
;;
|
||||
esac
|
||||
|
||||
# 安装 Node.js 和 npm(如果还没有通过包管理器安装)
|
||||
if ! command -v node &> /dev/null && [ "$PKG_MANAGER" != "brew" ] && [ "$PKG_MANAGER" != "pkg" ]; then
|
||||
info "安装 Node.js 和 npm..."
|
||||
if [ "$PKG_MANAGER" = "apt-get" ]; then
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
$PKG_MANAGER install -y nodejs
|
||||
else
|
||||
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
|
||||
$PKG_MANAGER install -y nodejs
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安装 Rust(如果未安装)
|
||||
if ! command -v rustc &> /dev/null; then
|
||||
info "安装 Rust..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
. "$HOME/.cargo/env"
|
||||
fi
|
||||
|
||||
# 添加目标平台
|
||||
info "添加 Rust 目标平台..."
|
||||
case "$(uname)" in
|
||||
"FreeBSD")
|
||||
rustup target add x86_64-unknown-freebsd
|
||||
;;
|
||||
"Darwin")
|
||||
rustup target add x86_64-apple-darwin aarch64-apple-darwin
|
||||
;;
|
||||
*)
|
||||
rustup target add x86_64-unknown-linux-gnu
|
||||
;;
|
||||
esac
|
||||
|
||||
# 清理包管理器缓存
|
||||
case $PKG_MANAGER in
|
||||
"apt-get")
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
;;
|
||||
"pkg")
|
||||
pkg clean -y
|
||||
;;
|
||||
esac
|
||||
|
||||
# 设置时区(除了 macOS)
|
||||
if [ "$(uname)" != "Darwin" ]; then
|
||||
info "设置时区为 Asia/Shanghai..."
|
||||
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}安装完成!${NC}"
|
1
src/aiserver.rs
Normal file
1
src/aiserver.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod v1;
|
1
src/aiserver/v1.rs
Normal file
1
src/aiserver/v1.rs
Normal file
@@ -0,0 +1 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));
|
5051
src/aiserver/v1/aiserver.proto
Normal file
5051
src/aiserver/v1/aiserver.proto
Normal file
File diff suppressed because it is too large
Load Diff
5
src/app.rs
Normal file
5
src/app.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod models;
|
||||
pub mod constant;
|
||||
pub mod token;
|
||||
pub mod utils;
|
||||
pub mod client;
|
29
src/app/client.rs
Normal file
29
src/app/client.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::app::constant::*;
|
||||
use reqwest::Client;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 返回预构建的 Cursor API 客户端
|
||||
pub fn build_client(auth_token: &str, checksum: &str, endpoint: &str) -> reqwest::RequestBuilder {
|
||||
let client = Client::new();
|
||||
let trace_id = Uuid::new_v4().to_string();
|
||||
let content_type = if endpoint == CURSOR_API2_STREAM_CHAT {
|
||||
CONTENT_TYPE_CONNECT_PROTO
|
||||
} else {
|
||||
CONTENT_TYPE_PROTO
|
||||
};
|
||||
|
||||
client
|
||||
.post(format!("{}{}", CURSOR_API2_BASE_URL, endpoint))
|
||||
.header(HEADER_NAME_CONTENT_TYPE, content_type)
|
||||
.header(HEADER_NAME_AUTHORIZATION, format!("{}{}", AUTHORIZATION_BEARER_PREFIX, auth_token))
|
||||
.header("connect-accept-encoding", "gzip,br")
|
||||
.header("connect-protocol-version", "1")
|
||||
.header("user-agent", "connect-es/1.6.1")
|
||||
.header("x-amzn-trace-id", format!("Root={}", trace_id))
|
||||
.header("x-cursor-checksum", checksum)
|
||||
.header("x-cursor-client-version", "0.42.5")
|
||||
.header("x-cursor-timezone", "Asia/Shanghai")
|
||||
.header("x-ghost-mode", "false")
|
||||
.header("x-request-id", trace_id)
|
||||
.header("Host", CURSOR_API2_HOST)
|
||||
}
|
64
src/app/constant.rs
Normal file
64
src/app/constant.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
pub const PKG_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
pub const PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
|
||||
pub const ROUTER_ROOT_PATH: &str = "/";
|
||||
pub const ROUTER_HEALTH_PATH: &str = "/health";
|
||||
pub const ROUTER_GET_CHECKSUM: &str = "/get-checksum";
|
||||
pub const ROUTER_GET_USER_INFO_PATH: &str = "/get-user-info";
|
||||
pub const ROUTER_LOGS_PATH: &str = "/logs";
|
||||
pub const ROUTER_CONFIG_PATH: &str = "/config";
|
||||
pub const ROUTER_TOKENINFO_PATH: &str = "/tokeninfo";
|
||||
pub const ROUTER_GET_TOKENINFO_PATH: &str = "/get-tokeninfo";
|
||||
pub const ROUTER_UPDATE_TOKENINFO_PATH: &str = "/update-tokeninfo";
|
||||
pub const ROUTER_ENV_EXAMPLE_PATH: &str = "/env-example";
|
||||
pub const ROUTER_SHARED_STYLES_PATH: &str = "/static/shared-styles.css";
|
||||
pub const ROUTER_SHARED_JS_PATH: &str = "/static/shared.js";
|
||||
|
||||
pub const STATUS: &str = "status";
|
||||
pub const MESSAGE: &str = "message";
|
||||
pub const ERROR: &str = "error";
|
||||
|
||||
pub const TOKEN_FILE: &str = "token_file";
|
||||
pub const TOKEN_LIST_FILE: &str = "token_list_file";
|
||||
pub const TOKENS: &str = "tokens";
|
||||
pub const TOKEN_LIST: &str = "token_list";
|
||||
|
||||
pub const STATUS_SUCCESS: &str = "success";
|
||||
pub const STATUS_FAILED: &str = "failed";
|
||||
|
||||
pub const HEADER_NAME_CONTENT_TYPE: &str = "content-type";
|
||||
pub const HEADER_NAME_AUTHORIZATION: &str = "Authorization";
|
||||
|
||||
pub const CONTENT_TYPE_PROTO: &str = "application/proto";
|
||||
pub const CONTENT_TYPE_CONNECT_PROTO: &str = "application/connect+proto";
|
||||
pub const CONTENT_TYPE_TEXT_HTML_WITH_UTF8: &str = "text/html;charset=utf-8";
|
||||
pub const CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8: &str = "text/plain;charset=utf-8";
|
||||
|
||||
pub const AUTHORIZATION_BEARER_PREFIX: &str = "Bearer ";
|
||||
|
||||
pub const OBJECT_CHAT_COMPLETION: &str = "chat.completion";
|
||||
pub const OBJECT_CHAT_COMPLETION_CHUNK: &str = "chat.completion.chunk";
|
||||
|
||||
pub const CURSOR_API2_HOST: &str = "api2.cursor.sh";
|
||||
pub const CURSOR_API2_BASE_URL: &str = "https://api2.cursor.sh/aiserver.v1.AiService/";
|
||||
|
||||
pub const CURSOR_API2_STREAM_CHAT: &str = "StreamChat";
|
||||
pub const CURSOR_API2_GET_USER_INFO: &str = "GetUserInfo";
|
||||
|
||||
pub const FINISH_REASON_STOP: &str = "stop";
|
||||
|
||||
pub const LONG_CONTEXT_MODELS: [&str; 4] = [
|
||||
"gpt-4o-128k",
|
||||
"gemini-1.5-flash-500k",
|
||||
"claude-3-haiku-200k",
|
||||
"claude-3-5-sonnet-200k",
|
||||
];
|
||||
|
||||
pub const MODEL_OBJECT: &str = "model";
|
||||
pub const ANTHROPIC: &str = "anthropic";
|
||||
pub const CURSOR: &str = "cursor";
|
||||
pub const GOOGLE: &str = "google";
|
||||
pub const OPENAI: &str = "openai";
|
447
src/app/models.rs
Normal file
447
src/app/models.rs
Normal file
@@ -0,0 +1,447 @@
|
||||
use super::{constant::*, token::UserUsageInfo};
|
||||
use crate::message::Message;
|
||||
use chrono::{DateTime, Local};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::RwLock;
|
||||
|
||||
// 页面内容类型枚举
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum PageContent {
|
||||
#[serde(rename = "default")]
|
||||
Default, // 默认行为
|
||||
#[serde(rename = "text")]
|
||||
Text(String), // 纯文本
|
||||
#[serde(rename = "html")]
|
||||
Html(String), // HTML 内容
|
||||
}
|
||||
|
||||
impl Default for PageContent {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
// 静态配置
|
||||
#[derive(Clone)]
|
||||
pub struct AppConfig {
|
||||
enable_stream_check: bool,
|
||||
include_stop_stream: bool,
|
||||
vision_ability: VisionAbility,
|
||||
enable_slow_pool: bool,
|
||||
allow_claude: bool,
|
||||
auth_token: String,
|
||||
token_file: String,
|
||||
token_list_file: String,
|
||||
route_prefix: String,
|
||||
pub start_time: chrono::DateTime<chrono::Local>,
|
||||
pages: Pages,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum VisionAbility {
|
||||
#[serde(rename = "none", alias = "disabled")]
|
||||
None,
|
||||
#[serde(rename = "base64", alias = "base64-only")]
|
||||
Base64,
|
||||
#[serde(rename = "all", alias = "base64-http")]
|
||||
All,
|
||||
}
|
||||
|
||||
impl VisionAbility {
|
||||
pub fn from_str(s: &str) -> Result<Self, &'static str> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"none" | "disabled" => Ok(Self::None),
|
||||
"base64" | "base64-only" => Ok(Self::Base64),
|
||||
"all" | "base64-http" => Ok(Self::All),
|
||||
_ => Err("Invalid VisionAbility value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VisionAbility {
|
||||
fn default() -> Self {
|
||||
Self::Base64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pages {
|
||||
pub root_content: PageContent,
|
||||
pub logs_content: PageContent,
|
||||
pub config_content: PageContent,
|
||||
pub tokeninfo_content: PageContent,
|
||||
pub shared_styles_content: PageContent,
|
||||
pub shared_js_content: PageContent,
|
||||
}
|
||||
|
||||
impl Default for Pages {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root_content: PageContent::Default,
|
||||
logs_content: PageContent::Default,
|
||||
config_content: PageContent::Default,
|
||||
tokeninfo_content: PageContent::Default,
|
||||
shared_styles_content: PageContent::Default,
|
||||
shared_js_content: PageContent::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运行时状态
|
||||
pub struct AppState {
|
||||
pub total_requests: u64,
|
||||
pub active_requests: u64,
|
||||
pub request_logs: Vec<RequestLog>,
|
||||
pub token_infos: Vec<TokenInfo>,
|
||||
}
|
||||
|
||||
// 全局配置实例
|
||||
lazy_static! {
|
||||
pub static ref APP_CONFIG: RwLock<AppConfig> = RwLock::new(AppConfig::default());
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_stream_check: true,
|
||||
include_stop_stream: true,
|
||||
vision_ability: VisionAbility::Base64,
|
||||
enable_slow_pool: false,
|
||||
allow_claude: false,
|
||||
auth_token: String::new(),
|
||||
token_file: ".token".to_string(),
|
||||
token_list_file: ".token-list".to_string(),
|
||||
route_prefix: String::new(),
|
||||
start_time: chrono::Local::now(),
|
||||
pages: Pages::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn init(
|
||||
enable_stream_check: bool,
|
||||
include_stop_stream: bool,
|
||||
vision_ability: VisionAbility,
|
||||
enable_slow_pool: bool,
|
||||
allow_claude: bool,
|
||||
auth_token: String,
|
||||
token_file: String,
|
||||
token_list_file: String,
|
||||
route_prefix: String,
|
||||
) {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_stream_check = enable_stream_check;
|
||||
config.include_stop_stream = include_stop_stream;
|
||||
config.vision_ability = vision_ability;
|
||||
config.enable_slow_pool = enable_slow_pool;
|
||||
config.allow_claude = allow_claude;
|
||||
config.auth_token = auth_token;
|
||||
config.token_file = token_file;
|
||||
config.token_list_file = token_list_file;
|
||||
config.route_prefix = route_prefix;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_stream_check() -> bool {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.enable_stream_check)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn get_stop_stream() -> bool {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.include_stop_stream)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn get_vision_ability() -> VisionAbility {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.vision_ability.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_slow_pool() -> bool {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.enable_slow_pool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_allow_claude() -> bool {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.allow_claude)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_auth_token() -> String {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.auth_token.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_token_file() -> String {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.token_file.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_token_list_file() -> String {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.token_list_file.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_route_prefix() -> String {
|
||||
APP_CONFIG
|
||||
.read()
|
||||
.map(|config| config.route_prefix.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_page_content(path: &str) -> Option<PageContent> {
|
||||
APP_CONFIG.read().ok().map(|config| match path {
|
||||
ROUTER_ROOT_PATH => config.pages.root_content.clone(),
|
||||
ROUTER_LOGS_PATH => config.pages.logs_content.clone(),
|
||||
ROUTER_CONFIG_PATH => config.pages.config_content.clone(),
|
||||
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content.clone(),
|
||||
ROUTER_SHARED_STYLES_PATH => config.pages.shared_styles_content.clone(),
|
||||
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content.clone(),
|
||||
_ => PageContent::Default,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_stream_check(enable: bool) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_stream_check = enable;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_stop_stream(enable: bool) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.include_stop_stream = enable;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_vision_ability(new_ability: VisionAbility) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.vision_ability = new_ability;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_slow_pool(enable: bool) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_slow_pool = enable;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_allow_claude(enable: bool) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.allow_claude = enable;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
match path {
|
||||
ROUTER_ROOT_PATH => config.pages.root_content = content,
|
||||
ROUTER_LOGS_PATH => config.pages.logs_content = content,
|
||||
ROUTER_CONFIG_PATH => config.pages.config_content = content,
|
||||
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content = content,
|
||||
ROUTER_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
|
||||
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content = content,
|
||||
_ => return Err("无效的路径"),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_stream_check() -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_stream_check = true;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_stop_stream() -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.include_stop_stream = true;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_vision_ability() -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.vision_ability = VisionAbility::Base64;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_slow_pool() -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_slow_pool = false;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_allow_claude() -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.allow_claude = false;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
match path {
|
||||
ROUTER_ROOT_PATH => config.pages.root_content = PageContent::Default,
|
||||
ROUTER_LOGS_PATH => config.pages.logs_content = PageContent::Default,
|
||||
ROUTER_CONFIG_PATH => config.pages.config_content = PageContent::Default,
|
||||
ROUTER_TOKENINFO_PATH => config.pages.tokeninfo_content = PageContent::Default,
|
||||
ROUTER_SHARED_STYLES_PATH => {
|
||||
config.pages.shared_styles_content = PageContent::Default
|
||||
}
|
||||
ROUTER_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::Default,
|
||||
_ => return Err("无效的路径"),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(token_infos: Vec<TokenInfo>) -> Self {
|
||||
Self {
|
||||
total_requests: 0,
|
||||
active_requests: 0,
|
||||
request_logs: Vec::new(),
|
||||
token_infos,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_token_infos(&mut self, token_infos: Vec<TokenInfo>) {
|
||||
self.token_infos = token_infos;
|
||||
}
|
||||
}
|
||||
|
||||
// 模型定义
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Model {
|
||||
pub id: String,
|
||||
pub created: i64,
|
||||
pub object: String,
|
||||
pub owned_by: String,
|
||||
}
|
||||
|
||||
// impl Model {
|
||||
// pub fn is_pesticide(&self) -> bool {
|
||||
// !(self.owned_by.as_str() == CURSOR || self.id.as_str() == "gpt-4o-mini")
|
||||
// }
|
||||
// }
|
||||
|
||||
// 请求日志
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct RequestLog {
|
||||
pub timestamp: DateTime<Local>,
|
||||
pub model: String,
|
||||
pub token_info: TokenInfo,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt: Option<String>,
|
||||
pub stream: bool,
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
// pub struct PromptList(Option<String>);
|
||||
|
||||
// impl PromptList {
|
||||
// pub fn to_vec(&self) -> Vec<>
|
||||
// }
|
||||
|
||||
// 聊天请求
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default)]
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
// 用于存储 token 信息
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct TokenInfo {
|
||||
pub token: String,
|
||||
pub checksum: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alias: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<UserUsageInfo>,
|
||||
}
|
||||
|
||||
// TokenUpdateRequest 结构体
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenUpdateRequest {
|
||||
pub tokens: String,
|
||||
#[serde(default)]
|
||||
pub token_list: Option<String>,
|
||||
}
|
||||
|
||||
// 添加用于接收更新请求的结构体
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConfigUpdateRequest {
|
||||
#[serde(default)]
|
||||
pub action: String, // "get", "update", "reset"
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub content: Option<PageContent>, // "default", "text", "html"
|
||||
#[serde(default)]
|
||||
pub enable_stream_check: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub include_stop_stream: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub vision_ability: Option<VisionAbility>,
|
||||
#[serde(default)]
|
||||
pub enable_slow_pool: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub enable_all_claude: Option<bool>,
|
||||
}
|
313
src/app/token.rs
Normal file
313
src/app/token.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use super::{
|
||||
constant::*,
|
||||
models::{AppConfig, AppState, TokenInfo, TokenUpdateRequest},
|
||||
utils::i32_to_u32,
|
||||
};
|
||||
use crate::aiserver::v1::GetUserInfoResponse;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
Json,
|
||||
};
|
||||
use image::EncodableLayout;
|
||||
use prost::Message;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
// 规范化文件内容并写入
|
||||
fn normalize_and_write(content: &str, file_path: &str) -> String {
|
||||
let normalized = content.replace("\r\n", "\n");
|
||||
if normalized != content {
|
||||
if let Err(e) = std::fs::write(file_path, &normalized) {
|
||||
eprintln!("警告: 无法更新规范化的文件: {}", e);
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
// 解析token和别名
|
||||
fn parse_token_alias(token_part: &str, line: &str) -> Option<(String, Option<String>)> {
|
||||
match token_part.split("::").collect::<Vec<_>>() {
|
||||
parts if parts.len() == 1 => Some((parts[0].to_string(), None)),
|
||||
parts if parts.len() == 2 => Some((parts[1].to_string(), Some(parts[0].to_string()))),
|
||||
_ => {
|
||||
eprintln!("警告: 忽略无效的行: {}", line);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token 加载函数
|
||||
pub fn load_tokens() -> Vec<TokenInfo> {
|
||||
let token_file = AppConfig::get_token_file();
|
||||
let token_list_file = AppConfig::get_token_list_file();
|
||||
|
||||
// 确保文件存在
|
||||
for file in [&token_file, &token_list_file] {
|
||||
if !std::path::Path::new(file).exists() {
|
||||
if let Err(e) = std::fs::write(file, "") {
|
||||
eprintln!("警告: 无法创建文件 '{}': {}", file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取和规范化 token 文件
|
||||
let token_entries = match std::fs::read_to_string(&token_file) {
|
||||
Ok(content) => {
|
||||
let normalized = normalize_and_write(&content, &token_file);
|
||||
normalized
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
parse_token_alias(line, line)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("警告: 无法读取token文件 '{}': {}", token_file, e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
// 读取和规范化 token-list 文件
|
||||
let mut token_map: std::collections::HashMap<String, (String, Option<String>)> =
|
||||
match std::fs::read_to_string(&token_list_file) {
|
||||
Ok(content) => {
|
||||
let normalized = normalize_and_write(&content, &token_list_file);
|
||||
normalized
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
match parts[..] {
|
||||
[token_part, checksum] => {
|
||||
let (token, alias) = parse_token_alias(token_part, line)?;
|
||||
Some((token, (checksum.to_string(), alias)))
|
||||
}
|
||||
_ => {
|
||||
eprintln!("警告: 忽略无效的token-list行: {}", line);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("警告: 无法读取token-list文件: {}", e);
|
||||
std::collections::HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
// 更新或添加新token
|
||||
for (token, alias) in token_entries {
|
||||
if let Some((_, existing_alias)) = token_map.get(&token) {
|
||||
// 只在alias不同时更新已存在的token
|
||||
if alias != *existing_alias {
|
||||
if let Some((checksum, _)) = token_map.get(&token) {
|
||||
token_map.insert(token.clone(), (checksum.clone(), alias));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 为新token生成checksum
|
||||
let checksum =
|
||||
crate::generate_checksum(&crate::generate_hash(), Some(&crate::generate_hash()));
|
||||
token_map.insert(token, (checksum, alias));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 token-list 文件
|
||||
let token_list_content = token_map
|
||||
.iter()
|
||||
.map(|(token, (checksum, alias))| {
|
||||
if let Some(alias) = alias {
|
||||
format!("{}::{},{}", alias, token, checksum)
|
||||
} else {
|
||||
format!("{},{}", token, checksum)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
if let Err(e) = std::fs::write(&token_list_file, token_list_content) {
|
||||
eprintln!("警告: 无法更新token-list文件: {}", e);
|
||||
}
|
||||
|
||||
// 转换为 TokenInfo vector
|
||||
token_map
|
||||
.into_iter()
|
||||
.map(|(token, (checksum, alias))| TokenInfo {
|
||||
token,
|
||||
checksum,
|
||||
alias,
|
||||
usage: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 更新 TokenInfo 处理
|
||||
pub async fn handle_update_tokeninfo(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
) -> Json<serde_json::Value> {
|
||||
// 重新加载 tokens
|
||||
let token_infos = load_tokens();
|
||||
|
||||
// 更新应用状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = token_infos;
|
||||
}
|
||||
|
||||
Json(serde_json::json!({
|
||||
STATUS: STATUS_SUCCESS,
|
||||
MESSAGE: "Token list has been reloaded"
|
||||
}))
|
||||
}
|
||||
|
||||
// 获取 TokenInfo 处理
|
||||
pub async fn handle_get_tokeninfo(
|
||||
State(_state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let auth_token = AppConfig::get_auth_token();
|
||||
let token_file = AppConfig::get_token_file();
|
||||
let token_list_file = AppConfig::get_token_list_file();
|
||||
|
||||
// 验证 AUTH_TOKEN
|
||||
let auth_header = headers
|
||||
.get(HEADER_NAME_AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if auth_header != auth_token {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new());
|
||||
let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new());
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
STATUS: STATUS_SUCCESS,
|
||||
"token_file": token_file,
|
||||
"token_list_file": token_list_file,
|
||||
"tokens": tokens,
|
||||
"token_list": token_list
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn handle_update_tokeninfo_post(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
headers: HeaderMap,
|
||||
Json(request): Json<TokenUpdateRequest>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let auth_token = AppConfig::get_auth_token();
|
||||
let token_file = AppConfig::get_token_file();
|
||||
let token_list_file = AppConfig::get_token_list_file();
|
||||
|
||||
// 验证 AUTH_TOKEN
|
||||
let auth_header = headers
|
||||
.get(HEADER_NAME_AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if auth_header != auth_token {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 写入 .token 文件
|
||||
std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// 如果提供了 token_list,则写入
|
||||
if let Some(token_list) = request.token_list {
|
||||
std::fs::write(&token_list_file, token_list)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
// 重新加载 tokens
|
||||
let token_infos = load_tokens();
|
||||
let token_infos_len = token_infos.len();
|
||||
|
||||
// 更新应用状态
|
||||
{
|
||||
let mut state = state.lock().await;
|
||||
state.token_infos = token_infos;
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
STATUS: STATUS_SUCCESS,
|
||||
MESSAGE: "Token files have been updated and reloaded",
|
||||
"token_file": token_file,
|
||||
"token_list_file": token_list_file,
|
||||
"token_count": token_infos_len
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetUserInfoQuery {
|
||||
alias: String,
|
||||
}
|
||||
|
||||
pub async fn get_user_info(
|
||||
State(state): State<Arc<Mutex<AppState>>>,
|
||||
Query(query): Query<GetUserInfoQuery>,
|
||||
) -> Json<GetUserInfo> {
|
||||
let (auth_token, checksum) = match {
|
||||
let app_token_infos = &state.lock().await.token_infos;
|
||||
app_token_infos
|
||||
.iter()
|
||||
.find(|token_info| token_info.alias == Some(query.alias.clone()))
|
||||
.map(|token_info| (token_info.token.clone(), token_info.checksum.clone()))
|
||||
} {
|
||||
Some(token) => token,
|
||||
None => return Json(GetUserInfo::Error("No data".to_string())),
|
||||
};
|
||||
|
||||
match get_user_usage(&auth_token, &checksum).await {
|
||||
Some(usage) => Json(GetUserInfo::Usage(usage)),
|
||||
None => Json(GetUserInfo::Error("No data".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_usage(auth_token: &str, checksum: &str) -> Option<UserUsageInfo> {
|
||||
// 构建请求客户端
|
||||
let client = super::client::build_client(auth_token, checksum, CURSOR_API2_GET_USER_INFO);
|
||||
let response = client
|
||||
.body(Vec::new())
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.bytes()
|
||||
.await
|
||||
.ok()?;
|
||||
let user_info = GetUserInfoResponse::decode(response.as_bytes()).ok()?;
|
||||
|
||||
user_info.usage.map(|user_usage| UserUsageInfo {
|
||||
fast_requests: i32_to_u32(user_usage.gpt4_requests),
|
||||
max_fast_requests: i32_to_u32(user_usage.gpt4_max_requests),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub enum GetUserInfo {
|
||||
#[serde[rename = "usage"]]
|
||||
Usage(UserUsageInfo),
|
||||
#[serde[rename = "error"]]
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct UserUsageInfo {
|
||||
pub fast_requests: u32,
|
||||
pub max_fast_requests: u32,
|
||||
}
|
22
src/app/utils.rs
Normal file
22
src/app/utils.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.map(|v| match v.to_lowercase().as_str() {
|
||||
"true" | "1" => true,
|
||||
"false" | "0" => false,
|
||||
_ => default,
|
||||
})
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn parse_string_from_env(key: &str, default: &str) -> String {
|
||||
std::env::var(key).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
pub fn i32_to_u32(value: i32) -> u32 {
|
||||
if value < 0 {
|
||||
0
|
||||
} else {
|
||||
value as u32
|
||||
}
|
||||
}
|
2
src/chat.rs
Normal file
2
src/chat.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod stream;
|
||||
pub mod error;
|
154
src/chat/error.rs
Normal file
154
src/chat/error.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use crate::aiserver::v1::throw_error_check_request::Error as ErrorType;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ChatError {
|
||||
pub error: ErrorBody,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ErrorBody {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub details: Vec<ErrorDetail>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ErrorDetail {
|
||||
#[serde(rename = "type")]
|
||||
pub error_type: String,
|
||||
pub debug: ErrorDebug,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ErrorDebug {
|
||||
pub error: String,
|
||||
pub details: ErrorDetails,
|
||||
#[serde(rename = "isExpected")]
|
||||
pub is_expected: bool,
|
||||
}
|
||||
|
||||
impl ErrorDebug {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
ErrorType::from_str_name(&self.error).is_some()
|
||||
}
|
||||
|
||||
pub fn status_code(&self) -> u16 {
|
||||
match ErrorType::from_str_name(&self.error) {
|
||||
Some(error) => match error {
|
||||
ErrorType::Unspecified => 500,
|
||||
ErrorType::BadApiKey
|
||||
| ErrorType::BadUserApiKey
|
||||
| ErrorType::InvalidAuthId
|
||||
| ErrorType::AuthTokenNotFound
|
||||
| ErrorType::AuthTokenExpired
|
||||
| ErrorType::Unauthorized => 401,
|
||||
ErrorType::NotLoggedIn
|
||||
| ErrorType::NotHighEnoughPermissions
|
||||
| ErrorType::AgentRequiresLogin
|
||||
| ErrorType::ProUserOnly
|
||||
| ErrorType::TaskNoPermissions => 403,
|
||||
ErrorType::NotFound
|
||||
| ErrorType::UserNotFound
|
||||
| ErrorType::TaskUuidNotFound
|
||||
| ErrorType::AgentEngineNotFound
|
||||
| ErrorType::GitgraphNotFound
|
||||
| ErrorType::FileNotFound => 404,
|
||||
ErrorType::FreeUserRateLimitExceeded
|
||||
| ErrorType::ProUserRateLimitExceeded
|
||||
| ErrorType::OpenaiRateLimitExceeded
|
||||
| ErrorType::OpenaiAccountLimitExceeded
|
||||
| ErrorType::GenericRateLimitExceeded
|
||||
| ErrorType::Gpt4VisionPreviewRateLimit
|
||||
| ErrorType::ApiKeyRateLimit => 429,
|
||||
ErrorType::BadRequest
|
||||
| ErrorType::BadModelName
|
||||
| ErrorType::SlashEditFileTooLong
|
||||
| ErrorType::FileUnsupported
|
||||
| ErrorType::ClaudeImageTooLarge => 400,
|
||||
_ => 500,
|
||||
},
|
||||
None => 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ErrorDetails {
|
||||
pub title: String,
|
||||
pub detail: String,
|
||||
#[serde(rename = "isRetryable")]
|
||||
pub is_retryable: bool,
|
||||
}
|
||||
|
||||
impl ChatError {
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
serde_json::to_value(self).unwrap()
|
||||
}
|
||||
|
||||
pub fn to_error_response(&self) -> ErrorResponse {
|
||||
if self.error.details.is_empty() {
|
||||
return ErrorResponse {
|
||||
status: 500,
|
||||
code: "ERROR_UNKNOWN".to_string(),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
ErrorResponse {
|
||||
status: self.error.details[0].debug.status_code(),
|
||||
code: self.error.details[0].debug.error.clone(),
|
||||
error: Some(Error {
|
||||
message: self.error.details[0].debug.details.title.clone(),
|
||||
details: self.error.details[0].debug.details.detail.clone(),
|
||||
value: self.error.details[0].value.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub status: u16,
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Error {
|
||||
pub message: String,
|
||||
pub details: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl ErrorResponse {
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
serde_json::to_value(self).unwrap()
|
||||
}
|
||||
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
pub fn native_code(&self) -> String {
|
||||
self.code.replace("_", " ").to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum StreamError {
|
||||
ChatError(ChatError),
|
||||
DataLengthLessThan5,
|
||||
EmptyMessage,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StreamError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StreamError::ChatError(error) => write!(f, "{}", serde_json::to_string(error).unwrap()),
|
||||
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
|
||||
StreamError::EmptyMessage => write!(f, "empty message"),
|
||||
}
|
||||
}
|
||||
}
|
127
src/chat/stream.rs
Normal file
127
src/chat/stream.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use crate::aiserver::v1::StreamChatResponse;
|
||||
use flate2::read::GzDecoder;
|
||||
use prost::Message;
|
||||
use std::io::Read;
|
||||
|
||||
use super::error::{ChatError, StreamError};
|
||||
|
||||
// 解压gzip数据
|
||||
fn decompress_gzip(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut decoder = GzDecoder::new(data);
|
||||
let mut decompressed = Vec::new();
|
||||
|
||||
match decoder.read_to_end(&mut decompressed) {
|
||||
Ok(_) => Some(decompressed),
|
||||
Err(_) => {
|
||||
// println!("gzip解压失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum StreamMessage {
|
||||
// 调试
|
||||
Debug(String),
|
||||
// 流开始标志 b"\0\0\0\0\0"
|
||||
StreamStart,
|
||||
// 消息内容
|
||||
Content(Vec<String>),
|
||||
// 流结束标志 b"\x02\0\0\0\x02{}"
|
||||
StreamEnd,
|
||||
}
|
||||
|
||||
pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
|
||||
if data.len() < 5 {
|
||||
return Err(StreamError::DataLengthLessThan5);
|
||||
}
|
||||
|
||||
// 检查是否为流开始标志
|
||||
// if data == b"\0\0\0\0\0" {
|
||||
// return Ok(StreamMessage::StreamStart);
|
||||
// }
|
||||
|
||||
// 检查是否为流结束标志
|
||||
// if data == b"\x02\0\0\0\x02{}" {
|
||||
// return Ok(StreamMessage::StreamEnd);
|
||||
// }
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
||||
while offset + 5 <= data.len() {
|
||||
// 获取消息类型和长度
|
||||
let msg_type = data[offset];
|
||||
let msg_len = u32::from_be_bytes([
|
||||
data[offset + 1],
|
||||
data[offset + 2],
|
||||
data[offset + 3],
|
||||
data[offset + 4],
|
||||
]) as usize;
|
||||
|
||||
// 流开始
|
||||
if msg_type == 0 && msg_len == 0 {
|
||||
return Ok(StreamMessage::StreamStart);
|
||||
}
|
||||
|
||||
// 检查剩余数据长度是否足够
|
||||
if offset + 5 + msg_len > data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let msg_data = &data[offset + 5..offset + 5 + msg_len];
|
||||
|
||||
match msg_type {
|
||||
// 文本消息
|
||||
0 => {
|
||||
if let Ok(response) = StreamChatResponse::decode(msg_data) {
|
||||
if !response.text.is_empty() {
|
||||
messages.push(response.text);
|
||||
} else {
|
||||
// println!("[text] StreamChatResponse: {:?}", response);
|
||||
return Ok(StreamMessage::Debug(
|
||||
response.filled_prompt.unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// gzip压缩消息
|
||||
1 => {
|
||||
if let Some(text) = decompress_gzip(msg_data) {
|
||||
let response = StreamChatResponse::decode(&text[..]).unwrap_or_default();
|
||||
if !response.text.is_empty() {
|
||||
messages.push(response.text);
|
||||
} else {
|
||||
// println!("[gzip] StreamChatResponse: {:?}", response);
|
||||
return Ok(StreamMessage::Debug(
|
||||
response.filled_prompt.unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// JSON字符串
|
||||
2 => {
|
||||
if msg_len == 2 {
|
||||
return Ok(StreamMessage::StreamEnd);
|
||||
}
|
||||
if let Ok(text) = String::from_utf8(msg_data.to_vec()) {
|
||||
// println!("JSON消息: {}", text);
|
||||
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
|
||||
return Err(StreamError::ChatError(error));
|
||||
}
|
||||
// 未预计
|
||||
// messages.push(text);
|
||||
}
|
||||
}
|
||||
// 其他类型暂不处理
|
||||
t => eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t),
|
||||
}
|
||||
|
||||
offset += 5 + msg_len;
|
||||
}
|
||||
|
||||
if messages.is_empty() {
|
||||
Err(StreamError::EmptyMessage)
|
||||
} else {
|
||||
Ok(StreamMessage::Content(messages))
|
||||
}
|
||||
}
|
446
src/lib.rs
446
src/lib.rs
@@ -1,29 +1,40 @@
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use flate2::read::GzDecoder;
|
||||
use prost::Message;
|
||||
use rand::{thread_rng, Rng};
|
||||
use image::guess_format;
|
||||
use prost::Message as _;
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/cursor.rs"));
|
||||
}
|
||||
mod aiserver;
|
||||
use aiserver::v1::*;
|
||||
|
||||
use proto::{ChatMessage, ResMessage};
|
||||
pub mod message;
|
||||
use message::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChatInput {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
pub mod app;
|
||||
use app::{models::*,constant::*};
|
||||
|
||||
fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_message::Message>) {
|
||||
pub mod chat;
|
||||
|
||||
async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationMessage>) {
|
||||
// 收集 system 和 developer 指令
|
||||
let instructions = inputs
|
||||
.iter()
|
||||
.filter(|input| input.role == "system" || input.role == "developer")
|
||||
.map(|input| input.content.clone())
|
||||
.filter(|input| input.role == Role::System)
|
||||
.map(|input| match &input.content {
|
||||
MessageContent::Text(text) => text.clone(),
|
||||
MessageContent::Vision(contents) => contents
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
if content.content_type == "text" {
|
||||
content.text.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
@@ -35,19 +46,48 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
};
|
||||
|
||||
// 过滤出 user 和 assistant 对话
|
||||
let mut chat_inputs: Vec<ChatInput> = inputs
|
||||
let mut chat_inputs: Vec<Message> = inputs
|
||||
.into_iter()
|
||||
.filter(|input| input.role == "user" || input.role == "assistant")
|
||||
.filter(|input| input.role == Role::User || input.role == Role::Assistant)
|
||||
.collect();
|
||||
|
||||
// 处理空对话情况
|
||||
if chat_inputs.is_empty() {
|
||||
return (
|
||||
instructions,
|
||||
vec![proto::chat_message::Message {
|
||||
role: 1, // user
|
||||
content: " ".to_string(),
|
||||
message_id: Uuid::new_v4().to_string(),
|
||||
vec![ConversationMessage {
|
||||
text: " ".to_string(),
|
||||
r#type: conversation_message::MessageType::Human as i32,
|
||||
attached_code_chunks: vec![],
|
||||
codebase_context_chunks: vec![],
|
||||
commits: vec![],
|
||||
pull_requests: vec![],
|
||||
git_diffs: vec![],
|
||||
assistant_suggested_diffs: vec![],
|
||||
interpreter_results: vec![],
|
||||
images: vec![],
|
||||
attached_folders: vec![],
|
||||
approximate_lint_errors: vec![],
|
||||
bubble_id: Uuid::new_v4().to_string(),
|
||||
server_bubble_id: None,
|
||||
attached_folders_new: vec![],
|
||||
lints: vec![],
|
||||
user_responses_to_suggested_code_blocks: vec![],
|
||||
relevant_files: vec![],
|
||||
tool_results: vec![],
|
||||
notepads: vec![],
|
||||
is_capability_iteration: Some(false),
|
||||
capabilities: vec![],
|
||||
edit_trail_contexts: vec![],
|
||||
suggested_code_blocks: vec![],
|
||||
diffs_for_compressing_files: vec![],
|
||||
multi_file_linter_errors: vec![],
|
||||
diff_histories: vec![],
|
||||
recently_viewed_files: vec![],
|
||||
recent_locations_history: vec![],
|
||||
is_agentic: false,
|
||||
file_diff_trajectories: vec![],
|
||||
conversation_summary: None,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -55,13 +95,13 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
// 如果第一条是 assistant,插入空的 user 消息
|
||||
if chat_inputs
|
||||
.first()
|
||||
.map_or(false, |input| input.role == "assistant")
|
||||
.map_or(false, |input| input.role == Role::Assistant)
|
||||
{
|
||||
chat_inputs.insert(
|
||||
0,
|
||||
ChatInput {
|
||||
role: "user".to_string(),
|
||||
content: " ".to_string(),
|
||||
Message {
|
||||
role: Role::User,
|
||||
content: MessageContent::Text(" ".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -70,16 +110,16 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
let mut i = 1;
|
||||
while i < chat_inputs.len() {
|
||||
if chat_inputs[i].role == chat_inputs[i - 1].role {
|
||||
let insert_role = if chat_inputs[i].role == "user" {
|
||||
"assistant"
|
||||
let insert_role = if chat_inputs[i].role == Role::User {
|
||||
Role::Assistant
|
||||
} else {
|
||||
"user"
|
||||
Role::User
|
||||
};
|
||||
chat_inputs.insert(
|
||||
i,
|
||||
ChatInput {
|
||||
role: insert_role.to_string(),
|
||||
content: " ".to_string(),
|
||||
Message {
|
||||
role: insert_role,
|
||||
content: MessageContent::Text(" ".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -89,46 +129,266 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
// 确保最后一条是 user
|
||||
if chat_inputs
|
||||
.last()
|
||||
.map_or(false, |input| input.role == "assistant")
|
||||
.map_or(false, |input| input.role == Role::Assistant)
|
||||
{
|
||||
chat_inputs.push(ChatInput {
|
||||
role: "user".to_string(),
|
||||
content: " ".to_string(),
|
||||
chat_inputs.push(Message {
|
||||
role: Role::User,
|
||||
content: MessageContent::Text(" ".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// 转换为 proto messages
|
||||
let messages = chat_inputs
|
||||
.into_iter()
|
||||
.map(|input| proto::chat_message::Message {
|
||||
role: if input.role == "user" { 1 } else { 2 },
|
||||
content: input.content,
|
||||
message_id: Uuid::new_v4().to_string(),
|
||||
})
|
||||
.collect();
|
||||
let mut messages = Vec::new();
|
||||
for input in chat_inputs {
|
||||
let (text, images) = match input.content {
|
||||
MessageContent::Text(text) => (text, vec![]),
|
||||
MessageContent::Vision(contents) => {
|
||||
let mut text_parts = Vec::new();
|
||||
let mut images = Vec::new();
|
||||
|
||||
for content in contents {
|
||||
match content.content_type.as_str() {
|
||||
"text" => {
|
||||
if let Some(text) = content.text {
|
||||
text_parts.push(text);
|
||||
}
|
||||
}
|
||||
"image_url" => {
|
||||
if let Some(image_url) = &content.image_url {
|
||||
let url = image_url.url.clone();
|
||||
let result =
|
||||
tokio::spawn(async move { fetch_image_data(&url).await });
|
||||
if let Ok(Ok((image_data, dimensions))) = result.await {
|
||||
images.push(ImageProto {
|
||||
data: image_data,
|
||||
dimension: dimensions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(text_parts.join("\n"), images)
|
||||
}
|
||||
};
|
||||
|
||||
messages.push(ConversationMessage {
|
||||
text,
|
||||
r#type: if input.role == Role::User {
|
||||
conversation_message::MessageType::Human as i32
|
||||
} else {
|
||||
conversation_message::MessageType::Ai as i32
|
||||
},
|
||||
attached_code_chunks: vec![],
|
||||
codebase_context_chunks: vec![],
|
||||
commits: vec![],
|
||||
pull_requests: vec![],
|
||||
git_diffs: vec![],
|
||||
assistant_suggested_diffs: vec![],
|
||||
interpreter_results: vec![],
|
||||
images,
|
||||
attached_folders: vec![],
|
||||
approximate_lint_errors: vec![],
|
||||
bubble_id: Uuid::new_v4().to_string(),
|
||||
server_bubble_id: None,
|
||||
attached_folders_new: vec![],
|
||||
lints: vec![],
|
||||
user_responses_to_suggested_code_blocks: vec![],
|
||||
relevant_files: vec![],
|
||||
tool_results: vec![],
|
||||
notepads: vec![],
|
||||
is_capability_iteration: Some(false),
|
||||
capabilities: vec![],
|
||||
edit_trail_contexts: vec![],
|
||||
suggested_code_blocks: vec![],
|
||||
diffs_for_compressing_files: vec![],
|
||||
multi_file_linter_errors: vec![],
|
||||
diff_histories: vec![],
|
||||
recently_viewed_files: vec![],
|
||||
recent_locations_history: vec![],
|
||||
is_agentic: false,
|
||||
file_diff_trajectories: vec![],
|
||||
conversation_summary: None,
|
||||
});
|
||||
}
|
||||
|
||||
(instructions, messages)
|
||||
}
|
||||
|
||||
async fn fetch_image_data(
|
||||
url: &str,
|
||||
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 在进入异步操作前获取并释放锁
|
||||
let vision_ability = AppConfig::get_vision_ability();
|
||||
|
||||
match vision_ability {
|
||||
VisionAbility::None => Err("图片功能已禁用".into()),
|
||||
|
||||
VisionAbility::Base64 => {
|
||||
if !url.starts_with("data:image/") {
|
||||
return Err("仅支持 base64 编码的图片".into());
|
||||
}
|
||||
process_base64_image(url)
|
||||
}
|
||||
|
||||
VisionAbility::All => {
|
||||
if url.starts_with("data:image/") {
|
||||
process_base64_image(url)
|
||||
} else {
|
||||
process_http_image(url).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 base64 编码的图片
|
||||
fn process_base64_image(
|
||||
url: &str,
|
||||
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let parts: Vec<&str> = url.split("base64,").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("无效的 base64 图片格式".into());
|
||||
}
|
||||
|
||||
// 检查图片格式
|
||||
let format = parts[0].to_lowercase();
|
||||
if !format.contains("png")
|
||||
&& !format.contains("jpeg")
|
||||
&& !format.contains("jpg")
|
||||
&& !format.contains("webp")
|
||||
&& !format.contains("gif")
|
||||
{
|
||||
return Err("不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF".into());
|
||||
}
|
||||
|
||||
let image_data = BASE64.decode(parts[1])?;
|
||||
|
||||
// 检查是否为动态 GIF
|
||||
if format.contains("gif") {
|
||||
if let Ok(frames) = gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data)) {
|
||||
if frames.into_iter().count() > 1 {
|
||||
return Err("不支持动态 GIF".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
let dimensions = if let Ok(img) = image::load_from_memory(&image_data) {
|
||||
Some(image_proto::Dimension {
|
||||
width: img.width() as i32,
|
||||
height: img.height() as i32,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((image_data, dimensions))
|
||||
}
|
||||
|
||||
// 处理 HTTP 图片 URL
|
||||
async fn process_http_image(
|
||||
url: &str,
|
||||
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let image_data = response.bytes().await?.to_vec();
|
||||
let format = guess_format(&image_data)?;
|
||||
|
||||
// 检查图片格式
|
||||
match format {
|
||||
image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP => {
|
||||
// 这些格式都支持
|
||||
}
|
||||
image::ImageFormat::Gif => {
|
||||
if let Ok(frames) =
|
||||
gif::DecodeOptions::new().read_info(std::io::Cursor::new(&image_data))
|
||||
{
|
||||
if frames.into_iter().count() > 1 {
|
||||
return Err("不支持动态 GIF".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err("不支持的图片格式,仅支持 PNG、JPEG、WEBP 和非动态 GIF".into()),
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
let dimensions = if let Ok(img) = image::load_from_memory_with_format(&image_data, format) {
|
||||
Some(image_proto::Dimension {
|
||||
width: img.width() as i32,
|
||||
height: img.height() as i32,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((image_data, dimensions))
|
||||
}
|
||||
|
||||
pub async fn encode_chat_message(
|
||||
inputs: Vec<ChatInput>,
|
||||
inputs: Vec<Message>,
|
||||
model_name: &str,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let (instructions, messages) = process_chat_inputs(inputs);
|
||||
// 在进入异步操作前获取并释放锁
|
||||
let enable_slow_pool = {
|
||||
if AppConfig::get_slow_pool() {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let chat = ChatMessage {
|
||||
messages,
|
||||
instructions: Some(proto::chat_message::Instructions {
|
||||
content: instructions,
|
||||
}),
|
||||
project_path: "/path/to/project".to_string(),
|
||||
model: Some(proto::chat_message::Model {
|
||||
name: model_name.to_string(),
|
||||
empty: String::new(),
|
||||
let (instructions, messages) = process_chat_inputs(inputs).await;
|
||||
|
||||
let explicit_context = if !instructions.trim().is_empty() {
|
||||
Some(ExplicitContext {
|
||||
context: instructions,
|
||||
repo_context: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat = GetChatRequest {
|
||||
current_file: None,
|
||||
conversation: messages,
|
||||
repositories: vec![],
|
||||
explicit_context,
|
||||
workspace_root_path: None,
|
||||
code_blocks: vec![],
|
||||
model_details: Some(ModelDetails {
|
||||
model_name: Some(model_name.to_string()),
|
||||
api_key: None,
|
||||
enable_ghost_mode: None,
|
||||
azure_state: None,
|
||||
enable_slow_pool,
|
||||
openai_api_base_url: None,
|
||||
}),
|
||||
documentation_identifiers: vec![],
|
||||
request_id: Uuid::new_v4().to_string(),
|
||||
summary: String::new(),
|
||||
linter_errors: None,
|
||||
summary: None,
|
||||
summary_up_until_index: None,
|
||||
allow_long_file_scan: None,
|
||||
is_bash: None,
|
||||
conversation_id: Uuid::new_v4().to_string(),
|
||||
can_handle_filenames_after_language_ids: None,
|
||||
use_web: None,
|
||||
quotes: vec![],
|
||||
debug_info: None,
|
||||
workspace_id: None,
|
||||
external_links: vec![],
|
||||
commit_notes: vec![],
|
||||
long_context_mode: if LONG_CONTEXT_MODELS.contains(&model_name) {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_eval: None,
|
||||
desired_max_tokens: None,
|
||||
context_ast: None,
|
||||
is_composer: None,
|
||||
runnable_code_blocks: None,
|
||||
should_cache: None,
|
||||
};
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
@@ -140,78 +400,6 @@ pub async fn encode_chat_message(
|
||||
Ok(hex::decode(len_prefix + &content)?)
|
||||
}
|
||||
|
||||
pub async fn decode_response(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match decode_proto_messages(data) {
|
||||
Ok(decoded) if !decoded.is_empty() => Ok(decoded),
|
||||
_ => decompress_response(data).await
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_proto_messages(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let hex_str = hex::encode(data);
|
||||
let mut pos = 0;
|
||||
let mut messages = Vec::new();
|
||||
|
||||
while pos + 10 <= hex_str.len() {
|
||||
let msg_len = i64::from_str_radix(&hex_str[pos..pos + 10], 16)?;
|
||||
pos += 10;
|
||||
|
||||
if pos + (msg_len * 2) as usize > hex_str.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let msg_data = &hex_str[pos..pos + (msg_len * 2) as usize];
|
||||
pos += (msg_len * 2) as usize;
|
||||
|
||||
let buffer = hex::decode(msg_data)?;
|
||||
let response = ResMessage::decode(&buffer[..])?;
|
||||
messages.push(response.msg);
|
||||
}
|
||||
|
||||
Ok(messages.join(""))
|
||||
}
|
||||
|
||||
async fn decompress_response(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
if data.len() <= 5 {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut decoder = GzDecoder::new(&data[5..]);
|
||||
let mut text = String::new();
|
||||
|
||||
match decoder.read_to_string(&mut text) {
|
||||
Ok(_) => {
|
||||
if !text.contains("<|BEGIN_SYSTEM|>") {
|
||||
Ok(text)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
},
|
||||
Err(e) => Err(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_random_id(
|
||||
size: usize,
|
||||
dict_type: Option<&str>,
|
||||
custom_chars: Option<&str>,
|
||||
) -> String {
|
||||
let charset = match (dict_type, custom_chars) {
|
||||
(_, Some(chars)) => chars,
|
||||
(Some("alphabet"), _) => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
(Some("max"), _) => "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-",
|
||||
_ => "0123456789",
|
||||
};
|
||||
|
||||
let mut rng = thread_rng();
|
||||
(0..size)
|
||||
.map(|_| {
|
||||
let idx = rng.gen_range(0..charset.len());
|
||||
charset.chars().nth(idx).unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn generate_hash() -> String {
|
||||
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let mut hasher = Sha256::new();
|
||||
@@ -219,7 +407,7 @@ pub fn generate_hash() -> String {
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
fn obfuscate_bytes(bytes: &mut Vec<u8>) {
|
||||
fn obfuscate_bytes(bytes: &mut [u8]) {
|
||||
let mut prev: u8 = 165;
|
||||
for (idx, byte) in bytes.iter_mut().enumerate() {
|
||||
let old_value = *byte;
|
||||
|
1214
src/main.rs
1214
src/main.rs
File diff suppressed because it is too large
Load Diff
78
src/message.rs
Normal file
78
src/message.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum MessageContent {
|
||||
Text(String),
|
||||
Vision(Vec<VisionMessageContent>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VisionMessageContent {
|
||||
#[serde(rename = "type")]
|
||||
pub content_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_url: Option<ImageUrl>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ImageUrl {
|
||||
pub url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub role: Role,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq)]
|
||||
pub enum Role {
|
||||
#[serde(rename = "system", alias = "developer")]
|
||||
System,
|
||||
#[serde(rename = "user", alias = "human")]
|
||||
User,
|
||||
#[serde(rename = "assistant", alias = "ai")]
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChatResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
pub choices: Vec<Choice>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Choice {
|
||||
pub index: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<Message>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delta: Option<Delta>,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Delta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<Role>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: i32,
|
||||
pub completion_tokens: i32,
|
||||
pub total_tokens: i32,
|
||||
}
|
108
src/models.rs
108
src/models.rs
@@ -1,133 +1,141 @@
|
||||
use std::sync::LazyLock;
|
||||
use crate::Model;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const MODEL_OBJECT: &str = "model";
|
||||
const ANTHROPIC: &str = "anthropic";
|
||||
const CURSOR: &str = "cursor";
|
||||
const GOOGLE: &str = "google";
|
||||
const OPENAI: &str = "openai";
|
||||
use super::{ANTHROPIC, CURSOR, GOOGLE, MODEL_OBJECT, OPENAI};
|
||||
|
||||
pub static AVAILABLE_MODELS: LazyLock<Vec<Model>> = LazyLock::new(|| {
|
||||
vec![
|
||||
Model {
|
||||
id: "cursor-small".into(),
|
||||
id: "claude-3.5-sonnet".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: CURSOR.into()
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-opus".into(),
|
||||
id: "gpt-3.5".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
},
|
||||
Model {
|
||||
id: "cursor-fast".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: CURSOR.into()
|
||||
},
|
||||
Model {
|
||||
id: "gpt-3.5-turbo".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4-turbo-2024-04-09".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4o".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-opus".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "cursor-fast".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: CURSOR.into(),
|
||||
},
|
||||
Model {
|
||||
id: "cursor-small".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: CURSOR.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-3.5-turbo".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4-turbo-2024-04-09".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4o-128k".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gemini-1.5-flash-500k".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: GOOGLE.into()
|
||||
owned_by: GOOGLE.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-haiku-200k".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-5-sonnet-200k".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-5-sonnet-20240620".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-5-sonnet-20241022".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4o-mini".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "o1-mini".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "o1-preview".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "o1".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: OPENAI.into(),
|
||||
},
|
||||
Model {
|
||||
id: "claude-3.5-haiku".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
owned_by: ANTHROPIC.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gemini-exp-1206".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: GOOGLE.into()
|
||||
owned_by: GOOGLE.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gemini-2.0-flash-thinking-exp".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: GOOGLE.into()
|
||||
owned_by: GOOGLE.into(),
|
||||
},
|
||||
Model {
|
||||
id: "gemini-2.0-flash-exp".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: GOOGLE.into()
|
||||
}
|
||||
owned_by: GOOGLE.into(),
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
6
start_instruction
Normal file
6
start_instruction
Normal file
@@ -0,0 +1,6 @@
|
||||
当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
|
||||
若发现首字慢,与本程序无关。
|
||||
若发现响应出现乱码,也与本程序无关。
|
||||
属于官方的问题,请不要像作者反馈。
|
||||
本程序拥有堪比客户端原本的速度,甚至可能更快。
|
||||
本程序的性能是非常厉害的。
|
226
static/config.html
Normal file
226
static/config.html
Normal file
@@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>配置管理</title>
|
||||
<!-- 引入共享样式 -->
|
||||
<link rel="stylesheet" href="/static/shared-styles.css">
|
||||
<script src="/static/shared.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>配置管理</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-group">
|
||||
<label>路径:</label>
|
||||
<select id="path">
|
||||
<option value="/">根路径 (/)</option>
|
||||
<option value="/logs">日志页面 (/logs)</option>
|
||||
<option value="/config">配置页面 (/config)</option>
|
||||
<option value="/tokeninfo">Token 信息页面 (/tokeninfo)</option>
|
||||
<option value="/static/shared-styles.css">共享样式 (/static/shared-styles.css)</option>
|
||||
<option value="/static/shared.js">共享脚本 (/static/shared.js)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>内容类型:</label>
|
||||
<select id="content_type">
|
||||
<option value="default">默认行为</option>
|
||||
<option value="text">纯文本</option>
|
||||
<option value="html">HTML</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>内容:</label>
|
||||
<textarea id="content"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>流第一个块检查:</label>
|
||||
<select id="enable_stream_check">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>包含停止流:</label>
|
||||
<select id="include_stop_stream">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>图片处理能力:</label>
|
||||
<select id="vision_ability">
|
||||
<option value="">保持不变</option>
|
||||
<option value="disabled">禁用</option>
|
||||
<option value="base64-only">仅 Base64</option>
|
||||
<option value="base64-http">Base64 + HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>慢速池:</label>
|
||||
<select id="enable_slow_pool">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>允许所有Claude模型:</label>
|
||||
<select id="enable_all_claude">
|
||||
<option value="">保持不变</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>认证令牌:</label>
|
||||
<input type="password" id="authToken">
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="updateConfig('get')">获取配置</button>
|
||||
<button onclick="updateConfig('update')">更新配置</button>
|
||||
<button onclick="updateConfig('reset')" class="secondary">重置配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" class="message"></div>
|
||||
|
||||
<script>
|
||||
async function fetchConfig() {
|
||||
const path = document.getElementById('path').value;
|
||||
const data = await makeAuthenticatedRequest('/config', {
|
||||
body: JSON.stringify({ action: 'get', path })
|
||||
});
|
||||
|
||||
if (data) {
|
||||
let content = '';
|
||||
|
||||
// 获取当前路径的页面内容
|
||||
const pageContent = data.data.page_content;
|
||||
|
||||
// 如果是 default 类型,需要从路径获取内容
|
||||
if (pageContent?.type === 'default') {
|
||||
// 直接从路径获取内容
|
||||
const response = await fetch(path);
|
||||
content = await response.text();
|
||||
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
|
||||
content = pageContent.content;
|
||||
}
|
||||
|
||||
// 更新表单
|
||||
document.getElementById('content').value = content || '';
|
||||
document.getElementById('content_type').value = pageContent?.type || 'default';
|
||||
let visionValue = data.data.vision_ability || '';
|
||||
// 标准化 vision_ability 的值
|
||||
switch (visionValue) {
|
||||
case 'none':
|
||||
visionValue = 'disabled';
|
||||
break;
|
||||
case 'base64':
|
||||
visionValue = 'base64-only';
|
||||
break;
|
||||
case 'all':
|
||||
visionValue = 'base64-http';
|
||||
break;
|
||||
}
|
||||
document.getElementById('enable_stream_check').value =
|
||||
parseStringFromBoolean(data.data.enable_stream_check, '');
|
||||
document.getElementById('include_stop_stream').value =
|
||||
parseStringFromBoolean(data.data.include_stop_stream, '');
|
||||
document.getElementById('vision_ability').value = visionValue;
|
||||
document.getElementById('enable_slow_pool').value =
|
||||
parseStringFromBoolean(data.data.enable_slow_pool, '');
|
||||
document.getElementById('enable_all_claude').value =
|
||||
parseStringFromBoolean(data.data.enable_all_claude, '');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateConfig(action) {
|
||||
if (action === 'get') {
|
||||
await fetchConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = document.getElementById('content_type').value;
|
||||
const content = document.getElementById('content').value;
|
||||
|
||||
// 根据内容类型构造 content 对象
|
||||
let contentObj = { type: 'default' };
|
||||
if (action === 'update' && contentType !== 'default') {
|
||||
contentObj = {
|
||||
type: contentType,
|
||||
content: content
|
||||
};
|
||||
}
|
||||
|
||||
const data = {
|
||||
action,
|
||||
path: document.getElementById('path').value,
|
||||
...(contentObj && { content: contentObj }),
|
||||
...(document.getElementById('enable_stream_check').value && {
|
||||
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
|
||||
}),
|
||||
...(document.getElementById('include_stop_stream').value && {
|
||||
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
|
||||
}),
|
||||
...(document.getElementById('vision_ability').value && {
|
||||
vision_ability: document.getElementById('vision_ability').value
|
||||
}),
|
||||
...(document.getElementById('enable_slow_pool').value && {
|
||||
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
|
||||
}),
|
||||
...(document.getElementById('enable_all_claude').value && {
|
||||
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
|
||||
})
|
||||
};
|
||||
|
||||
const result = await makeAuthenticatedRequest('/config', {
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (result) {
|
||||
showMessage('result', result.message, false);
|
||||
if (action === 'update' || action === 'reset') {
|
||||
await fetchConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
showMessage('result', message, false);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showMessage('result', message, true);
|
||||
}
|
||||
|
||||
// 添加按钮事件监听
|
||||
document.getElementById('path').addEventListener('change', fetchConfig);
|
||||
|
||||
// 更新内容类型变更处理
|
||||
document.getElementById('content_type').addEventListener('change', function () {
|
||||
const textarea = document.getElementById('content');
|
||||
textarea.disabled = this.value === 'default';
|
||||
});
|
||||
|
||||
// 初始化 token 处理
|
||||
initializeTokenHandling('authToken');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
382
static/logs.html
Normal file
382
static/logs.html
Normal file
@@ -0,0 +1,382 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>请求日志查看</title>
|
||||
<!-- 引入共享样式 -->
|
||||
<link rel="stylesheet" href="/static/shared-styles.css">
|
||||
<script src="/static/shared.js"></script>
|
||||
<style>
|
||||
/* 日志页面特定样式 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--card-background);
|
||||
padding: 15px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.refresh-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auto-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--card-background);
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.message-table th,
|
||||
.message-table td {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.message-table td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-table td:nth-child(2) {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.message-table td:first-child {
|
||||
width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.usage-progress-container {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.usage-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
background: linear-gradient(to right,
|
||||
#4caf50 0%,
|
||||
/* 绿色 */
|
||||
#8bc34a 25%,
|
||||
/* 浅绿 */
|
||||
#ffeb3b 50%,
|
||||
/* 黄色 */
|
||||
#ff9800 75%,
|
||||
/* 橙色 */
|
||||
#f44336 100%
|
||||
/* 红色 */
|
||||
);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>请求日志查看</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-group">
|
||||
<label>认证令牌:</label>
|
||||
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
|
||||
</div>
|
||||
<div class="refresh-container">
|
||||
<div class="button-group">
|
||||
<button onclick="fetchLogs()">刷新日志</button>
|
||||
</div>
|
||||
<div class="auto-refresh">
|
||||
<input type="checkbox" id="autoRefresh" checked>
|
||||
<label for="autoRefresh">自动刷新 (60秒)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h4>总请求数</h4>
|
||||
<div id="totalRequests" class="stat-value">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>活跃请求数</h4>
|
||||
<div id="activeRequests" class="stat-value">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h4>最后更新</h4>
|
||||
<div id="lastUpdate" class="stat-value">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>模型</th>
|
||||
<th>Token信息</th>
|
||||
<th>Prompt</th>
|
||||
<th>流式响应</th>
|
||||
<th>状态</th>
|
||||
<th>错误信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
|
||||
<!-- 添加弹窗组件 -->
|
||||
<div id="tokenModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3>Token 详细信息</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Token:</td>
|
||||
<td id="modalToken"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>校验和:</td>
|
||||
<td id="modalChecksum"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>别名:</td>
|
||||
<td id="modalAlias"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>使用情况:</td>
|
||||
<td id="modalUsage"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- 添加进度条容器 -->
|
||||
<div class="usage-progress-container">
|
||||
<div id="modalUsageBar" class="usage-progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="promptModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>对话内容</h3>
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div id="promptContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let refreshInterval;
|
||||
|
||||
function updateStats(data) {
|
||||
document.getElementById('totalRequests').textContent = data.total;
|
||||
document.getElementById('activeRequests').textContent = data.active || 0;
|
||||
document.getElementById('lastUpdate').textContent =
|
||||
new Date(data.timestamp).toLocaleTimeString();
|
||||
}
|
||||
|
||||
function showTokenModal(tokenInfo) {
|
||||
const modal = document.getElementById('tokenModal');
|
||||
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
|
||||
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
|
||||
document.getElementById('modalAlias').textContent = tokenInfo.alias || '-';
|
||||
|
||||
// 获取进度条容器
|
||||
const progressContainer = document.querySelector('.usage-progress-container');
|
||||
|
||||
// 处理使用情况和进度条
|
||||
if (tokenInfo.usage) {
|
||||
const current = tokenInfo.usage.fast_requests;
|
||||
const max = tokenInfo.usage.max_fast_requests;
|
||||
const percentage = (current / max * 100).toFixed(1);
|
||||
|
||||
document.getElementById('modalUsage').textContent =
|
||||
`${current}/${max} (${percentage}%)`;
|
||||
|
||||
// 显示进度条容器
|
||||
progressContainer.style.display = 'block';
|
||||
// 更新进度条
|
||||
const progressBar = document.getElementById('modalUsageBar');
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
} else {
|
||||
document.getElementById('modalUsage').textContent = '-';
|
||||
// 隐藏进度条容器
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
function updateTable(data) {
|
||||
const tbody = document.getElementById('logsBody');
|
||||
updateStats(data);
|
||||
|
||||
tbody.innerHTML = data.logs.map(log => `
|
||||
<tr>
|
||||
<td>${new Date(log.timestamp).toLocaleString()}</td>
|
||||
<td>${log.model}</td>
|
||||
<td>
|
||||
<button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>
|
||||
查看详情
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
${log.prompt ?
|
||||
`<button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">
|
||||
查看对话
|
||||
</button>` :
|
||||
'-'
|
||||
}
|
||||
</td>
|
||||
<td>${log.stream ? '是' : '否'}</td>
|
||||
<td>${log.status}</td>
|
||||
<td>${log.error || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function fetchLogs() {
|
||||
const data = await makeAuthenticatedRequest('/logs');
|
||||
if (data) {
|
||||
updateTable(data);
|
||||
showGlobalMessage('日志获取成功');
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新控制
|
||||
document.getElementById('autoRefresh').addEventListener('change', function (e) {
|
||||
if (e.target.checked) {
|
||||
refreshInterval = setInterval(fetchLogs, 60000);
|
||||
} else {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后自动获取日志
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const authToken = getAuthToken();
|
||||
if (authToken) {
|
||||
document.getElementById('authToken').value = authToken;
|
||||
fetchLogs();
|
||||
}
|
||||
// 启动自动刷新
|
||||
refreshInterval = setInterval(fetchLogs, 60000);
|
||||
});
|
||||
|
||||
// 初始化 token 处理
|
||||
initializeTokenHandling('authToken');
|
||||
|
||||
// 添加清理逻辑
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加模态框关闭逻辑
|
||||
document.querySelectorAll('.modal .close').forEach(closeBtn => {
|
||||
closeBtn.onclick = function () {
|
||||
this.closest('.modal').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
window.onclick = function (event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
169
static/shared-styles.css
Normal file
169
static/shared-styles.css
Normal file
@@ -0,0 +1,169 @@
|
||||
:root {
|
||||
--primary-color: #2196F3;
|
||||
--primary-dark: #1976D2;
|
||||
--success-color: #4CAF50;
|
||||
--error-color: #F44336;
|
||||
--background-color: #F5F5F5;
|
||||
--card-background: #FFFFFF;
|
||||
--border-radius: 8px;
|
||||
--spacing: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing);
|
||||
background: var(--background-color);
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--card-background);
|
||||
padding: var(--spacing);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #1a1a1a;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #E8F5E9;
|
||||
color: #2E7D32;
|
||||
border: 1px solid #A5D6A7;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFEBEE;
|
||||
color: #C62828;
|
||||
border: 1px solid #FFCDD2;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: var(--spacing);
|
||||
background: var(--card-background);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
252
static/shared.js
Normal file
252
static/shared.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// Token 管理功能
|
||||
function saveAuthToken(token) {
|
||||
const expiryTime = new Date().getTime() + (24 * 60 * 60 * 1000); // 24小时后过期
|
||||
localStorage.setItem('authToken', token);
|
||||
localStorage.setItem('authTokenExpiry', expiryTime);
|
||||
}
|
||||
|
||||
function getAuthToken() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const expiry = localStorage.getItem('authTokenExpiry');
|
||||
|
||||
if (!token || !expiry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (new Date().getTime() > parseInt(expiry)) {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authTokenExpiry');
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// 消息显示功能
|
||||
function showMessage(elementId, text, isError = false) {
|
||||
const msg = document.getElementById(elementId);
|
||||
msg.className = `message ${isError ? 'error' : 'success'}`;
|
||||
msg.textContent = text;
|
||||
}
|
||||
|
||||
function showGlobalMessage(text, isError = false) {
|
||||
showMessage('message', text, isError);
|
||||
}
|
||||
|
||||
// Token 输入框自动填充和事件绑定
|
||||
function initializeTokenHandling(inputId) {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const authToken = getAuthToken();
|
||||
if (authToken) {
|
||||
document.getElementById(inputId).value = authToken;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById(inputId).addEventListener('change', (e) => {
|
||||
if (e.target.value) {
|
||||
saveAuthToken(e.target.value);
|
||||
} else {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authTokenExpiry');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// API 请求通用处理
|
||||
async function makeAuthenticatedRequest(url, options = {}) {
|
||||
const tokenId = options.tokenId || 'authToken';
|
||||
const token = document.getElementById(tokenId).value;
|
||||
|
||||
if (!token) {
|
||||
showGlobalMessage('请输入 AUTH_TOKEN', true);
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
showGlobalMessage(`请求失败: ${error.message}`, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字符串解析布尔值
|
||||
* @param {string} str - 要解析的字符串
|
||||
* @param {boolean|null} defaultValue - 解析失败时的默认值
|
||||
* @returns {boolean|null} 解析结果,如果无法解析则返回默认值
|
||||
*/
|
||||
function parseBooleanFromString(str, defaultValue = null) {
|
||||
if (typeof str !== 'string') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const lowercaseStr = str.toLowerCase().trim();
|
||||
|
||||
if (lowercaseStr === 'true' || lowercaseStr === '1') {
|
||||
return true;
|
||||
} else if (lowercaseStr === 'false' || lowercaseStr === '0') {
|
||||
return false;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将布尔值转换为字符串
|
||||
* @param {boolean|undefined|null} value - 要转换的布尔值
|
||||
* @param {string} defaultValue - 转换失败时的默认值
|
||||
* @returns {string} 转换结果,如果输入无效则返回默认值
|
||||
*/
|
||||
function parseStringFromBoolean(value, defaultValue = null) {
|
||||
if (typeof value !== 'boolean') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析对话内容
|
||||
* @param {string} promptStr - 原始prompt字符串
|
||||
* @returns {Array<{role: string, content: string}>} 解析后的对话数组
|
||||
*/
|
||||
function parsePrompt(promptStr) {
|
||||
if (!promptStr) return [];
|
||||
|
||||
const messages = [];
|
||||
const lines = promptStr.split('\n');
|
||||
let currentRole = '';
|
||||
let currentContent = '';
|
||||
|
||||
const roleMap = {
|
||||
'BEGIN_SYSTEM': 'system',
|
||||
'BEGIN_USER': 'user',
|
||||
'BEGIN_ASSISTANT': 'assistant'
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// 检查是否是角色标记行
|
||||
let foundRole = false;
|
||||
for (const [marker, role] of Object.entries(roleMap)) {
|
||||
if (line.includes(marker)) {
|
||||
// 保存之前的消息(如果有)
|
||||
if (currentRole && currentContent.trim()) {
|
||||
messages.push({
|
||||
role: currentRole,
|
||||
content: currentContent.trim()
|
||||
});
|
||||
}
|
||||
// 设置新角色
|
||||
currentRole = role;
|
||||
currentContent = '';
|
||||
foundRole = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是角色标记行且不是END标记行,则添加到当前内容
|
||||
if (!foundRole && !line.includes('END_')) {
|
||||
currentContent += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一条消息
|
||||
if (currentRole && currentContent.trim()) {
|
||||
messages.push({
|
||||
role: currentRole,
|
||||
content: currentContent.trim()
|
||||
});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化对话内容为HTML表格
|
||||
* @param {Array<{role: string, content: string}>} messages - 对话消息数组
|
||||
* @returns {string} HTML表格字符串
|
||||
*/
|
||||
function formatPromptToTable(messages) {
|
||||
if (!messages || messages.length === 0) {
|
||||
return '<p>无对话内容</p>';
|
||||
}
|
||||
|
||||
const roleLabels = {
|
||||
'system': '系统',
|
||||
'user': '用户',
|
||||
'assistant': '助手'
|
||||
};
|
||||
|
||||
function escapeHtml(content) {
|
||||
// 先转义HTML特殊字符
|
||||
const escaped = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// 将HTML标签文本用引号包裹,使其更易读
|
||||
// return escaped.replace(/<(\/?[^>]+)>/g, '"<$1>"');
|
||||
return escaped;
|
||||
}
|
||||
|
||||
return `
|
||||
<table class="message-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>角色</th>
|
||||
<th>内容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${messages.map(msg => `
|
||||
<tr>
|
||||
<td>${roleLabels[msg.role] || msg.role}</td>
|
||||
<td>${escapeHtml(msg.content).replace(/\n/g, '<br>')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地显示prompt对话框
|
||||
* @param {string} promptStr - 原始prompt字符串
|
||||
*/
|
||||
function showPromptModal(promptStr) {
|
||||
try {
|
||||
const modal = document.getElementById('promptModal');
|
||||
const content = document.getElementById('promptContent');
|
||||
|
||||
if (!modal || !content) {
|
||||
console.error('Modal elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = parsePrompt(promptStr);
|
||||
content.innerHTML = formatPromptToTable(messages);
|
||||
modal.style.display = 'block';
|
||||
} catch (e) {
|
||||
console.error('显示prompt对话框失败:', e);
|
||||
console.error('原始prompt:', promptStr);
|
||||
}
|
||||
}
|
@@ -5,70 +5,36 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Token 信息管理</title>
|
||||
<!-- 引入共享样式 -->
|
||||
<link rel="stylesheet" href="/static/shared-styles.css">
|
||||
<script src="/static/shared.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
.token-container {
|
||||
display: grid;
|
||||
gap: var(--spacing);
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
.token-section {
|
||||
background: var(--card-background);
|
||||
padding: var(--spacing);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
.shortcuts {
|
||||
margin-top: var(--spacing);
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #607D8B;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #546E7A;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#authToken {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin: 10px 0;
|
||||
kbd {
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #b4b4b4;
|
||||
padding: 1px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -77,97 +43,70 @@
|
||||
<h1>Token 信息管理</h1>
|
||||
|
||||
<div class="container">
|
||||
<h2>认证</h2>
|
||||
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
|
||||
<div class="form-group">
|
||||
<label>认证令牌:</label>
|
||||
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Token 配置</h2>
|
||||
<div class="button-group">
|
||||
<button onclick="getTokenInfo()">获取当前配置</button>
|
||||
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
|
||||
<div class="token-container">
|
||||
<div class="token-section">
|
||||
<h3>Token 配置</h3>
|
||||
<div class="button-group">
|
||||
<button onclick="getTokenInfo()">获取当前配置</button>
|
||||
<button onclick="updateTokenInfo()" class="secondary">保存更改</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Token 文件内容:</label>
|
||||
<textarea id="tokens" placeholder="每行一个 token"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Token List 文件内容:</label>
|
||||
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts">
|
||||
快捷键: <kbd>Ctrl</kbd> + <kbd>S</kbd> 保存更改
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Token 文件内容</h3>
|
||||
<textarea id="tokens" placeholder="每行一个 token"></textarea>
|
||||
|
||||
<h3>Token List 文件内容</h3>
|
||||
<textarea id="tokenList" placeholder="token,checksum 格式,每行一对"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
|
||||
<script>
|
||||
function showMessage(text, isError = false) {
|
||||
const msg = document.getElementById('message');
|
||||
msg.className = isError ? 'error' : 'success';
|
||||
msg.textContent = text;
|
||||
showGlobalMessage(text, isError);
|
||||
}
|
||||
|
||||
async function getTokenInfo() {
|
||||
const authToken = document.getElementById('authToken').value;
|
||||
if (!authToken) {
|
||||
showMessage('请输入 AUTH_TOKEN', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/get-tokeninfo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await makeAuthenticatedRequest('/get-tokeninfo');
|
||||
if (data) {
|
||||
document.getElementById('tokens').value = data.tokens;
|
||||
document.getElementById('tokenList').value = data.token_list;
|
||||
showMessage('配置获取成功');
|
||||
} catch (error) {
|
||||
showMessage(`获取失败: ${error.message}`, true);
|
||||
showGlobalMessage('配置获取成功');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTokenInfo() {
|
||||
const authToken = document.getElementById('authToken').value;
|
||||
const tokens = document.getElementById('tokens').value;
|
||||
const tokenList = document.getElementById('tokenList').value;
|
||||
|
||||
if (!authToken) {
|
||||
showMessage('请输入 AUTH_TOKEN', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokens) {
|
||||
showMessage('Token 文件内容不能为空', true);
|
||||
showGlobalMessage('Token 文件内容不能为空', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/update-tokeninfo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tokens: tokens,
|
||||
token_list: tokenList || undefined
|
||||
})
|
||||
});
|
||||
const data = await makeAuthenticatedRequest('/update-tokeninfo', {
|
||||
body: JSON.stringify({
|
||||
tokens: tokens,
|
||||
token_list: tokenList || undefined
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
showMessage(`更新成功: ${data.message}`);
|
||||
} catch (error) {
|
||||
showMessage(`更新失败: ${error.message}`, true);
|
||||
if (data) {
|
||||
showGlobalMessage(`更新成功: ${data.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +117,9 @@
|
||||
updateTokenInfo();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 token 处理
|
||||
initializeTokenHandling('authToken');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
Reference in New Issue
Block a user