优化并添加图像识别

This commit is contained in:
wisdgod
2024-12-27 06:05:53 +08:00
parent ea1acb555f
commit fc6b65abe6
31 changed files with 8024 additions and 828 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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",
]

View File

@@ -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"] }

View File

@@ -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
View File

@@ -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(&current_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(())
}

View File

@@ -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 "构建完成!"

View File

@@ -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" ;;

View File

@@ -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
View 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();

View File

@@ -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"

View File

@@ -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"
}
}
}

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,2 @@
pub mod proto;
pub use proto::*;

1
src/aiserver/proto.rs Normal file
View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));

File diff suppressed because it is too large Load Diff

244
src/app.rs Normal file
View 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
View 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),
}
}
}

View File

@@ -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;

File diff suppressed because it is too large Load Diff

65
src/message.rs Normal file
View 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,
}

View File

@@ -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
View 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
View 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
View 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
View 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;
}
}

View File

@@ -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>