mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-12-24 13:38:01 +08:00
优化并添加图像识别
This commit is contained in:
22
.env.example
22
.env.example
@@ -1,5 +1,27 @@
|
||||
# 当前配置为默认值,请根据需要修改
|
||||
|
||||
# 服务器监听端口
|
||||
PORT=3000
|
||||
|
||||
# 路由前缀,必须以 / 开头(如果不为空)
|
||||
ROUTE_PREFIX=
|
||||
|
||||
# 认证令牌,必填
|
||||
AUTH_TOKEN=
|
||||
|
||||
# 令牌文件路径
|
||||
TOKEN_FILE=.token
|
||||
|
||||
# 令牌列表文件路径
|
||||
TOKEN_LIST_FILE=.token-list
|
||||
|
||||
# (实验性)是否启用慢速池(true/false)
|
||||
ENABLE_SLOW_POOL=
|
||||
|
||||
# 图片处理能力配置
|
||||
# 可选值:
|
||||
# - none 或 disabled:禁用图片功能
|
||||
# - base64 或 base64-only:仅支持 base64 编码的图片
|
||||
# - base64-http 或 all:支持 base64 和 HTTP 图片
|
||||
# 注意:启用 HTTP 支持可能会暴露服务器 IP
|
||||
VISION_ABILITY=base64
|
||||
|
||||
98
.github/workflows/build.yml
vendored
98
.github/workflows/build.yml
vendored
@@ -46,74 +46,12 @@ jobs:
|
||||
libssl-dev \
|
||||
openssl
|
||||
|
||||
# 安装 npm 依赖
|
||||
cd scripts && npm install && cd ..
|
||||
|
||||
# 设置 OpenSSL 环境变量
|
||||
echo "OPENSSL_DIR=/usr" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=/usr/include/openssl" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# if: runner.os == 'Linux'
|
||||
# uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
# - name: Build Linux arm64
|
||||
# if: runner.os == 'Linux'
|
||||
# run: |
|
||||
# # 启用 QEMU 支持
|
||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# # 创建 Dockerfile
|
||||
# cat > Dockerfile.arm64 << 'EOF'
|
||||
# FROM arm64v8/ubuntu:22.04
|
||||
|
||||
# ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# RUN apt-get update && apt-get install -y \
|
||||
# build-essential \
|
||||
# curl \
|
||||
# pkg-config \
|
||||
# libssl-dev \
|
||||
# protobuf-compiler \
|
||||
# nodejs \
|
||||
# npm \
|
||||
# git \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# # 安装 Rust
|
||||
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# WORKDIR /build
|
||||
# COPY . .
|
||||
|
||||
# # 安装 npm 依赖
|
||||
# RUN cd scripts && npm install && cd ..
|
||||
|
||||
# # 构建动态链接版本
|
||||
# RUN cargo build --release
|
||||
|
||||
# # 构建静态链接版本
|
||||
# RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --release
|
||||
# EOF
|
||||
|
||||
# # 构建 arm64 版本
|
||||
# docker buildx build --platform linux/arm64 -f Dockerfile.arm64 -t builder-arm64 .
|
||||
|
||||
# # 创建临时容器
|
||||
# docker create --name temp-container builder-arm64 sh
|
||||
|
||||
# # 复制动态链接版本
|
||||
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-aarch64-unknown-linux-gnu
|
||||
|
||||
# # 复制静态链接版本
|
||||
# docker cp temp-container:/build/target/release/cursor-api ./release/cursor-api-static-aarch64-unknown-linux-gnu
|
||||
|
||||
# # 清理临时容器
|
||||
# docker rm temp-container
|
||||
|
||||
- name: Build Linux x86_64 (Dynamic)
|
||||
if: runner.os == 'Linux'
|
||||
run: bash scripts/build.sh
|
||||
@@ -137,18 +75,39 @@ 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 ..
|
||||
|
||||
# 刷新环境变量
|
||||
$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 (Dynamic)
|
||||
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
|
||||
run: bash scripts/build.sh
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./scripts/build.ps1
|
||||
|
||||
- name: Build (Static)
|
||||
if: runner.os != 'Linux' && runner.os != 'FreeBSD'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./scripts/build.ps1 -Static
|
||||
|
||||
- name: Build macOS (Dynamic)
|
||||
if: runner.os == 'macOS'
|
||||
run: bash scripts/build.sh
|
||||
|
||||
- name: Build macOS (Static)
|
||||
if: runner.os == 'macOS'
|
||||
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:
|
||||
@@ -192,9 +151,6 @@ 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
|
||||
|
||||
|
||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -2,9 +2,9 @@ name: Docker Build and Push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
# push:
|
||||
# tags:
|
||||
# - 'v*'
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/cursor-api
|
||||
|
||||
5
.gitignore
vendored
5
.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
|
||||
|
||||
175
Cargo.lock
generated
175
Cargo.lock
generated
@@ -26,6 +26,21 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -159,6 +174,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"
|
||||
@@ -174,18 +195,51 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
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"
|
||||
@@ -222,6 +276,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,16 +328,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cursor-api"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"flate2",
|
||||
"futures",
|
||||
"gif",
|
||||
"hex",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"rand",
|
||||
@@ -356,6 +420,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 +573,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"
|
||||
@@ -824,6 +907,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 +975,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 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -980,7 +1097,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 +1169,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"
|
||||
@@ -1132,6 +1262,12 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
@@ -1273,7 +1409,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",
|
||||
@@ -1346,7 +1482,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",
|
||||
@@ -1434,6 +1570,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"
|
||||
@@ -1514,7 +1656,7 @@ 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 +1787,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 +2000,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -2084,3 +2232,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",
|
||||
]
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,27 +1,33 @@
|
||||
[package]
|
||||
name = "cursor-api"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
authors = ["wisdgod <nav@wisdgod.com>"]
|
||||
|
||||
[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.9", default-features = false, features = ["gzip", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
|
||||
serde = { version = "1.0.216", default-features = false, features = ["std", "derive"] }
|
||||
serde_json = "1.0.134"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["time"] }
|
||||
|
||||
19
README.md
19
README.md
@@ -97,17 +97,18 @@
|
||||
|
||||
写死了,后续也不会会支持自定义模型列表
|
||||
```
|
||||
cursor-small
|
||||
claude-3.5-sonnet
|
||||
gpt-4
|
||||
gpt-4o
|
||||
claude-3-opus
|
||||
cursor-fast
|
||||
cursor-small
|
||||
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
|
||||
@@ -137,7 +138,7 @@ apt-get install -y build-essential protobuf-compiler pkg-config libssl-dev nodej
|
||||
# 原生编译
|
||||
cargo build --release
|
||||
|
||||
# 交叉编译,以x86_64-unknown-linux-gnu为例,老实说,这也算原生编译,因为使用了docker
|
||||
# 交叉编译,以x86_64-unknown-linux-gnu为例
|
||||
cross build --target x86_64-unknown-linux-gnu --release
|
||||
```
|
||||
|
||||
@@ -210,15 +211,7 @@ docker run -p 3000:3000 cursor-api
|
||||
|
||||
### 跨平台编译
|
||||
|
||||
使用提供的构建脚本:
|
||||
|
||||
```bash
|
||||
# 仅编译当前平台
|
||||
./scripts/build.sh
|
||||
|
||||
# 交叉编译所有支持的平台
|
||||
./scripts/build.sh --cross
|
||||
```
|
||||
自行配置cross编译环境
|
||||
|
||||
支持的平台:
|
||||
|
||||
|
||||
133
build.rs
133
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,135 @@ 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
|
||||
.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,11 +17,6 @@ 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
|
||||
@@ -38,7 +28,6 @@ show_help() {
|
||||
用法: $(basename "$0") [选项]
|
||||
|
||||
选项:
|
||||
--cross 使用 cross 进行交叉编译(仅在 Linux 上有效)
|
||||
--static 使用静态链接(默认动态链接)
|
||||
--help 显示此帮助信息
|
||||
|
||||
@@ -46,22 +35,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 +46,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 目录
|
||||
@@ -118,7 +87,7 @@ get_target() {
|
||||
case "$os" in
|
||||
"Darwin") echo "${arch}-apple-darwin" ;;
|
||||
"Linux") echo "${arch}-unknown-linux-gnu" ;;
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-msvc" ;;
|
||||
"MINGW"*|"MSYS"*|"CYGWIN"*|"Windows_NT") echo "${arch}-pc-windows-gnu" ;;
|
||||
"FreeBSD") echo "${arch}-unknown-freebsd" ;;
|
||||
*) error "不支持的系统: $os" ;;
|
||||
esac
|
||||
@@ -134,16 +103,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 目标
|
||||
@@ -170,16 +139,16 @@ 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" ;;
|
||||
|
||||
@@ -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}"
|
||||
2
src/aiserver.rs
Normal file
2
src/aiserver.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod proto;
|
||||
pub use proto::*;
|
||||
1
src/aiserver/proto.rs
Normal file
1
src/aiserver/proto.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
244
src/app.rs
Normal file
244
src/app.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use super::message::Message;
|
||||
use chrono::{DateTime, Local};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::RwLock;
|
||||
|
||||
// 页面内容类型枚举
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum PageContent {
|
||||
Default, // 默认行为
|
||||
Text(String), // 纯文本
|
||||
Html(String), // HTML 内容
|
||||
}
|
||||
|
||||
impl Default for PageContent {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
// 静态配置
|
||||
#[derive(Clone)]
|
||||
pub struct AppConfig {
|
||||
pub vision_ability: String,
|
||||
pub enable_slow_pool: Option<bool>,
|
||||
pub auth_token: String,
|
||||
pub token_file: String,
|
||||
pub token_list_file: String,
|
||||
pub route_prefix: String,
|
||||
pub version: String,
|
||||
pub start_time: chrono::DateTime<chrono::Local>,
|
||||
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,
|
||||
}
|
||||
|
||||
// 运行时状态
|
||||
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 {
|
||||
vision_ability: "base64".to_string(),
|
||||
enable_slow_pool: None,
|
||||
auth_token: String::new(),
|
||||
token_file: ".token".to_string(),
|
||||
token_list_file: ".token-list".to_string(),
|
||||
route_prefix: String::new(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
start_time: chrono::Local::now(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn init(
|
||||
vision_ability: String,
|
||||
enable_slow_pool: Option<bool>,
|
||||
auth_token: String,
|
||||
token_file: String,
|
||||
token_list_file: String,
|
||||
route_prefix: String,
|
||||
) {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.vision_ability = vision_ability;
|
||||
config.enable_slow_pool = enable_slow_pool;
|
||||
config.auth_token = auth_token;
|
||||
config.token_file = token_file;
|
||||
config.token_list_file = token_list_file;
|
||||
config.route_prefix = route_prefix;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_vision_ability(&self, new_ability: String) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.vision_ability = new_ability;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_slow_pool(&self, enable: bool) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_slow_pool = Some(enable);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_page_content(
|
||||
&self,
|
||||
path: &str,
|
||||
content: PageContent,
|
||||
) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
match path {
|
||||
"/" => config.root_content = content,
|
||||
"/logs" => config.logs_content = content,
|
||||
"/config" => config.config_content = content,
|
||||
"/tokeninfo" => config.tokeninfo_content = content,
|
||||
"/static/shared-styles.css" => config.shared_styles_content = content,
|
||||
"/static/shared.js" => config.shared_js_content = content,
|
||||
_ => return Err("无效的路径"),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法更新配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_page_content(&self, path: &str) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
match path {
|
||||
"/" => config.root_content = PageContent::Default,
|
||||
"/logs" => config.logs_content = PageContent::Default,
|
||||
"/config" => config.config_content = PageContent::Default,
|
||||
"/tokeninfo" => config.tokeninfo_content = PageContent::Default,
|
||||
"/static/shared-styles.css" => config.shared_styles_content = PageContent::Default,
|
||||
"/static/shared.js" => config.shared_js_content = PageContent::Default,
|
||||
_ => return Err("无效的路径"),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_vision_ability(&self) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.vision_ability = "base64".to_string();
|
||||
Ok(())
|
||||
} else {
|
||||
Err("无法重置配置")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_slow_pool(&self) -> Result<(), &'static str> {
|
||||
if let Ok(mut config) = APP_CONFIG.write() {
|
||||
config.enable_slow_pool = None;
|
||||
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, Deserialize, Clone)]
|
||||
pub struct Model {
|
||||
pub id: String,
|
||||
pub created: i64,
|
||||
pub object: String,
|
||||
pub owned_by: String,
|
||||
}
|
||||
|
||||
// 请求日志
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct RequestLog {
|
||||
pub timestamp: DateTime<Local>,
|
||||
pub model: String,
|
||||
pub checksum: String,
|
||||
pub auth_token: String,
|
||||
pub alias: String,
|
||||
pub stream: bool,
|
||||
pub status: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
// 聊天请求
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default)]
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
// 用于存储 token 信息
|
||||
pub struct TokenInfo {
|
||||
pub token: String,
|
||||
pub checksum: String,
|
||||
pub alias: Option<String>,
|
||||
}
|
||||
|
||||
// TokenUpdateRequest 结构体
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenUpdateRequest {
|
||||
pub tokens: String,
|
||||
#[serde(default)]
|
||||
pub token_list: Option<String>,
|
||||
}
|
||||
|
||||
// 添加用于接收更新请求的结构体
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConfigUpdateRequest {
|
||||
pub action: String, // "get", "update", "reset"
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub content_type: Option<String>, // "default", "text", "html"
|
||||
#[serde(default)]
|
||||
pub content: String,
|
||||
#[serde(default)]
|
||||
pub vision_ability: Option<String>,
|
||||
#[serde(default)]
|
||||
pub enable_slow_pool: Option<bool>,
|
||||
}
|
||||
114
src/decoder.rs
Normal file
114
src/decoder.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use crate::StreamChatResponse;
|
||||
use brotli::Decompressor;
|
||||
use flate2::read::GzDecoder;
|
||||
use prost::Message as _;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
pub struct StreamDecoder;
|
||||
|
||||
impl Default for StreamDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamDecoder {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn process_chunk(&self, data: &[u8]) -> Result<String, Box<dyn StdError + Send + Sync>> {
|
||||
// 1. 首先尝试 proto 解码
|
||||
let hex = hex::encode(data);
|
||||
let mut offset = 0;
|
||||
let mut results = Vec::new();
|
||||
|
||||
while offset + 10 <= hex.len() {
|
||||
match i64::from_str_radix(&hex[offset..offset + 10], 16) {
|
||||
Ok(data_length) => {
|
||||
offset += 10;
|
||||
if offset + (data_length * 2) as usize > hex.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let message_hex = &hex[offset..offset + (data_length * 2) as usize];
|
||||
offset += (data_length * 2) as usize;
|
||||
|
||||
if let Ok(message_buffer) = hex::decode(message_hex) {
|
||||
if let Ok(message) = StreamChatResponse::decode(&message_buffer[..]) {
|
||||
results.push(message.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
if !results.is_empty() {
|
||||
return Ok(results.join(""));
|
||||
}
|
||||
|
||||
// 2. 如果 proto 解码失败,尝试 gzip 解压
|
||||
if data.len() > 5 && data[0] == 0x1f {
|
||||
let mut decoder = GzDecoder::new(&data[5..]);
|
||||
let mut text = String::new();
|
||||
if decoder.read_to_string(&mut text).is_ok() && !text.contains("<|BEGIN_SYSTEM|>") {
|
||||
return Ok(text);
|
||||
}
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// 3. 如果 gzip 失败,尝试 brotli 解压
|
||||
if data.len() > 5 && data[0] == 0x0b {
|
||||
let mut decoder = Decompressor::new(
|
||||
Cursor::new(&data[5..]),
|
||||
4096, // 默认的缓冲区大小
|
||||
);
|
||||
let mut text = String::new();
|
||||
if decoder.read_to_string(&mut text).is_ok() && !text.contains("<|BEGIN_SYSTEM|>") {
|
||||
return Ok(text);
|
||||
}
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
// 4. 如果所有解码方式都失败,返回空字符串
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DecoderError {
|
||||
InvalidLength,
|
||||
HexDecode(hex::FromHexError),
|
||||
ProtoDecode(prost::DecodeError),
|
||||
Decompress(std::io::Error),
|
||||
Utf8(std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
// 实现 Display trait
|
||||
impl fmt::Display for DecoderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidLength => write!(f, "Invalid message length"),
|
||||
Self::HexDecode(e) => write!(f, "Hex decode error: {}", e),
|
||||
Self::ProtoDecode(e) => write!(f, "Proto decode error: {}", e),
|
||||
Self::Decompress(e) => write!(f, "Decompression error: {}", e),
|
||||
Self::Utf8(e) => write!(f, "UTF-8 decode error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 Error trait
|
||||
impl StdError for DecoderError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
Self::InvalidLength => None,
|
||||
Self::HexDecode(e) => Some(e),
|
||||
Self::ProtoDecode(e) => Some(e),
|
||||
Self::Decompress(e) => Some(e),
|
||||
Self::Utf8(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
381
src/lib.rs
381
src/lib.rs
@@ -1,29 +1,51 @@
|
||||
use std::io::Read as _;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use flate2::read::GzDecoder;
|
||||
use prost::Message;
|
||||
use image::guess_format;
|
||||
use prost::Message as _;
|
||||
use rand::{thread_rng, Rng};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/cursor.rs"));
|
||||
}
|
||||
pub mod aiserver;
|
||||
use aiserver::proto::*;
|
||||
|
||||
use proto::{ChatMessage, ResMessage};
|
||||
pub mod message;
|
||||
use message::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChatInput {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
// pub mod decoder;
|
||||
// use decoder::*;
|
||||
|
||||
fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_message::Message>) {
|
||||
pub mod app;
|
||||
use app::*;
|
||||
|
||||
const LONG_CONTEXT_MODELS: [&str; 4] = [
|
||||
"gpt-4o-128k",
|
||||
"gemini-1.5-flash-500k",
|
||||
"claude-3-haiku-200k",
|
||||
"claude-3-5-sonnet-200k",
|
||||
];
|
||||
|
||||
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())
|
||||
.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,7 +57,7 @@ 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")
|
||||
.collect();
|
||||
@@ -44,10 +66,39 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
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,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -59,9 +110,9 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
{
|
||||
chat_inputs.insert(
|
||||
0,
|
||||
ChatInput {
|
||||
Message {
|
||||
role: "user".to_string(),
|
||||
content: " ".to_string(),
|
||||
content: MessageContent::Text(" ".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -77,9 +128,9 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
};
|
||||
chat_inputs.insert(
|
||||
i,
|
||||
ChatInput {
|
||||
Message {
|
||||
role: insert_role.to_string(),
|
||||
content: " ".to_string(),
|
||||
content: MessageContent::Text(" ".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -91,44 +142,266 @@ fn process_chat_inputs(inputs: Vec<ChatInput>) -> (String, Vec<proto::chat_messa
|
||||
.last()
|
||||
.map_or(false, |input| input.role == "assistant")
|
||||
{
|
||||
chat_inputs.push(ChatInput {
|
||||
chat_inputs.push(Message {
|
||||
role: "user".to_string(),
|
||||
content: " ".to_string(),
|
||||
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 == "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 = {
|
||||
let config = APP_CONFIG.read().unwrap();
|
||||
config.vision_ability.clone()
|
||||
};
|
||||
|
||||
match vision_ability.as_str() {
|
||||
"none" | "disabled" => Err("图片功能已禁用".into()),
|
||||
|
||||
"base64" | "base64-only" => {
|
||||
if !url.starts_with("data:image/") {
|
||||
return Err("仅支持 base64 编码的图片".into());
|
||||
}
|
||||
process_base64_image(url)
|
||||
}
|
||||
|
||||
"base64-http" | "all" => {
|
||||
if url.starts_with("data:image/") {
|
||||
process_base64_image(url)
|
||||
} else {
|
||||
process_http_image(url).await
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err("无效的 VISION_ABILITY 配置".into()),
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 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 = {
|
||||
let config = APP_CONFIG.read().unwrap();
|
||||
config.enable_slow_pool
|
||||
};
|
||||
|
||||
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,10 +413,12 @@ 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>> {
|
||||
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
|
||||
_ => decompress_response(data).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,14 +439,16 @@ fn decode_proto_messages(data: &[u8]) -> Result<String, Box<dyn std::error::Erro
|
||||
pos += (msg_len * 2) as usize;
|
||||
|
||||
let buffer = hex::decode(msg_data)?;
|
||||
let response = ResMessage::decode(&buffer[..])?;
|
||||
messages.push(response.msg);
|
||||
let response = StreamChatResponse::decode(&buffer[..])?;
|
||||
messages.push(response.text);
|
||||
}
|
||||
|
||||
Ok(messages.join(""))
|
||||
}
|
||||
|
||||
async fn decompress_response(data: &[u8]) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
async fn decompress_response(
|
||||
data: &[u8],
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
if data.len() <= 5 {
|
||||
return Ok(String::new());
|
||||
}
|
||||
@@ -186,8 +463,8 @@ async fn decompress_response(data: &[u8]) -> Result<String, Box<dyn std::error::
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
},
|
||||
Err(e) => Err(Box::new(e))
|
||||
}
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +496,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;
|
||||
|
||||
834
src/main.rs
834
src/main.rs
File diff suppressed because it is too large
Load Diff
65
src/message.rs
Normal file
65
src/message.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
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: String,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: i32,
|
||||
pub completion_tokens: i32,
|
||||
pub total_tokens: i32,
|
||||
}
|
||||
104
src/models.rs
104
src/models.rs
@@ -1,5 +1,5 @@
|
||||
use std::sync::LazyLock;
|
||||
use crate::Model;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const MODEL_OBJECT: &str = "model";
|
||||
const ANTHROPIC: &str = "anthropic";
|
||||
@@ -10,124 +10,130 @@ const OPENAI: &str = "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()
|
||||
},
|
||||
Model {
|
||||
id: "claude-3-opus".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: ANTHROPIC.into()
|
||||
},
|
||||
Model {
|
||||
id: "cursor-fast".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: CURSOR.into()
|
||||
},
|
||||
Model {
|
||||
id: "gpt-3.5-turbo".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
},
|
||||
Model {
|
||||
id: "gpt-4-turbo-2024-04-09".into(),
|
||||
created: 1706659200,
|
||||
object: MODEL_OBJECT.into(),
|
||||
owned_by: OPENAI.into()
|
||||
owned_by: ANTHROPIC.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(),
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
169
static/config.html
Normal file
169
static/config.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!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="vision_ability">
|
||||
<option value="">保持不变</option>
|
||||
<option value="none">禁用</option>
|
||||
<option value="base64">仅 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>认证令牌:</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 data = await makeAuthenticatedRequest('/config', {
|
||||
body: JSON.stringify({ action: 'get' })
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const path = document.getElementById('path').value;
|
||||
let content = '';
|
||||
|
||||
// 根据不同路径获取对应内容
|
||||
const contentMap = {
|
||||
'/': data.data.root_content,
|
||||
'/logs': data.data.logs_content,
|
||||
'/config': data.data.config_content,
|
||||
'/tokeninfo': data.data.tokeninfo_content,
|
||||
'/static/shared-styles.css': data.data.shared_styles_content,
|
||||
'/static/shared.js': data.data.shared_js_content
|
||||
};
|
||||
|
||||
const pageContent = contentMap[path];
|
||||
|
||||
// 如果是 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?.toLowerCase() || 'default';
|
||||
document.getElementById('vision_ability').value = data.data.vision_ability || '';
|
||||
document.getElementById('enable_slow_pool').value =
|
||||
data.data.enable_slow_pool === null ? '' : data.data.enable_slow_pool.toString();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateConfig(action) {
|
||||
if (action === 'get') {
|
||||
await fetchConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
action,
|
||||
path: document.getElementById('path').value,
|
||||
content_type: action === 'update' ? document.getElementById('content_type').value : undefined,
|
||||
content: action === 'update' ? document.getElementById('content').value : '',
|
||||
...(document.getElementById('vision_ability').value && {
|
||||
vision_ability: document.getElementById('vision_ability').value
|
||||
}),
|
||||
...(document.getElementById('enable_slow_pool').value && {
|
||||
enable_slow_pool: document.getElementById('enable_slow_pool').value === 'true' || null
|
||||
})
|
||||
};
|
||||
|
||||
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>
|
||||
179
static/logs.html
Normal file
179
static/logs.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!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;
|
||||
}
|
||||
</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>校验和</th>
|
||||
<th>认证令牌</th>
|
||||
<th>别名</th>
|
||||
<th>流式响应</th>
|
||||
<th>状态</th>
|
||||
<th>错误信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
|
||||
<script>
|
||||
let refreshInterval;
|
||||
|
||||
function showMessage(text, isError = false) {
|
||||
showGlobalMessage(text, isError);
|
||||
}
|
||||
|
||||
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 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>${log.checksum}</td>
|
||||
<td>${log.auth_token}</td>
|
||||
<td>${log.alias || '-'}</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);
|
||||
}
|
||||
});
|
||||
</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;
|
||||
}
|
||||
}
|
||||
85
static/shared.js
Normal file
85
static/shared.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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