修复了一些问题

This commit is contained in:
wisdgod
2025-01-23 12:34:56 +08:00
parent 3e304f53d4
commit 36b42e27de
73 changed files with 4918 additions and 2350 deletions

View File

@@ -10,7 +10,7 @@ ROUTE_PREFIX=
AUTH_TOKEN=
# 共享的认证令牌仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限
SHARED_AUTH_TOKEN=
SHARED_TOKEN=
# 启用流式响应检查关闭则无法响应错误代价是会对第一个块解析2次
ENABLE_STREAM_CHECK=true
@@ -18,11 +18,11 @@ ENABLE_STREAM_CHECK=true
# 流式消息结束后发送包含"finish_reason"为"stop"的空消息块
INCLUDE_STOP_REASON_STREAM=true
# 令牌文件路径
TOKEN_FILE=.token
# 令牌文件路径(已弃用)
# TOKEN_FILE=.token
# 令牌列表文件路径
TOKEN_LIST_FILE=.token-list
TOKEN_LIST_FILE=.tokens
# 实验性是否启用慢速池true/false
ENABLE_SLOW_POOL=false
@@ -38,15 +38,53 @@ PASS_ANY_CLAUDE=false
# 注意:启用 HTTP 支持可能会暴露服务器 IP
VISION_ABILITY=base64
# 额度检查配置
# 可选值:
# - none 或 disabled禁用额度检查
# - default详见 README
# - all 或 everything额度无条件检查
# - 以,分隔的模型列表,为空时使用默认值
USAGE_CHECK=default
# 是否允许使用动态(自定义)配置的 API Key
DYNAMIC_KEY=false
# 动态 Key 的标识前缀
KEY_PREFIX=sk-
# 默认提示词
DEFAULT_INSTRUCTIONS="Respond in Chinese by default"
# 反向代理服务器主机名,你猜怎么用
# 反向代理服务器主机名
REVERSE_PROXY_HOST=
# 代理地址配置说明
# - 留空或 `no`: 不使用任何代理
# - `system`: 使用系统代理(变量不存在时的默认值)
# - 代理地址: 支持以下格式
# - 多个代理: `http://localhost:7890,https://username:password@localhost:1234`
# 没有轮询,只是选择第一个格式正确的
# - 支持的协议: http, https, socks4, socks5, socks5h
PROXIES=
# 请求体大小限制单位为MB
# 默认为2MB (2,097,152 字节)
REQUEST_BODY_LIMIT_MB=2
# OpenAI 请求时token 和 checksum 的分隔符
TOKEN_DELIMITER=,
TOKEN_DELIMITER=,
# 同时兼容默认的,作为分隔符
USE_COMMA_DELIMITER=true
# 调试
DEBUG=false
# 调试文件
DEBUG_LOG_FILE=debug.log
# 日志储存条数(最大值2000)
REQUEST_LOGS_LIMIT=100
# Cursor 服务超时(秒)(最大值600)
SERVICE_TIMEOUT=30

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: ['https://afdian.com/a/wisdgod'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -83,8 +83,8 @@ jobs:
if: github.event_name == 'push'
run: |
mkdir -p artifacts
cp dist/linux_amd64/app/cursor-api artifacts/cursor-api-x86_64-${{ github.event_name }}
cp dist/linux_arm64/app/cursor-api artifacts/cursor-api-aarch64-${{ github.event_name }}
cp dist/linux_amd64/app/cursor-api artifacts/cursor-api-x86_64-${{ github.ref_name }}
cp dist/linux_arm64/app/cursor-api artifacts/cursor-api-aarch64-${{ github.ref_name }}
- name: Upload artifacts
if: (github.event_name == 'workflow_dispatch' && inputs.upload_artifacts) || github.event_name == 'push'

5
.gitignore vendored
View File

@@ -1,5 +1,5 @@
/target
/get-token/target
/tools/*/target
/*.log
/*.env
/static/*.min.html
@@ -12,9 +12,12 @@ node_modules
/.cargo
/.token
/.token-list
/.tokens
/cursor-api
/cursor-api.exe
/release
/*.py
/logs
/dev*
/build*

16
.license-compliance Normal file
View File

@@ -0,0 +1,16 @@
# 合规使用指南
attribution_rules:
required_disclaimer:
text: "基于第三方技术构建,与原始开发者无关联"
placement:
- documentation
- marketing_materials
- about_sections
prohibited_actions:
- using_author_name_in_press_releases
- claiming_official_support
- using_project_logo_as_endorsement
enforcement:
grace_period: 72h
compliance_check: https://api.wisdgod.com/license/validate

161
Cargo.lock generated
View File

@@ -76,17 +76,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -101,13 +90,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.7.9"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"async-trait",
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
@@ -135,11 +124,10 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.4.5"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
@@ -183,9 +171,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.7.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "block-buffer"
@@ -249,9 +237,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.2.9"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"shlex",
]
@@ -327,7 +315,7 @@ dependencies = [
[[package]]
name = "cursor-api"
version = "0.1.3-rc.3.3"
version = "0.1.3-rc.4-pre.1"
dependencies = [
"axum",
"base64",
@@ -339,6 +327,7 @@ dependencies = [
"gif",
"hex",
"image",
"parking_lot",
"paste",
"prost",
"prost-build",
@@ -352,7 +341,6 @@ dependencies = [
"tokio",
"tokio-stream",
"tower-http",
"urlencoding",
"uuid",
]
@@ -936,9 +924,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown",
@@ -946,9 +934,9 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.10.1"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "itertools"
@@ -994,16 +982,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "log"
version = "0.4.22"
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "matchit"
version = "0.7.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
@@ -1019,9 +1017,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
"simd-adler32",
@@ -1100,7 +1098,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.7.0",
"bitflags 2.8.0",
"cfg-if",
"foreign-types",
"libc",
@@ -1138,6 +1136,29 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
@@ -1316,6 +1337,15 @@ dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -1381,6 +1411,7 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-socks",
"tokio-util",
"tower",
"tower-service",
@@ -1419,7 +1450,7 @@ version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [
"bitflags 2.7.0",
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys",
@@ -1486,13 +1517,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.7.0",
"bitflags 2.8.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -1531,9 +1568,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
@@ -1679,7 +1716,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.7.0",
"bitflags 2.8.0",
"core-foundation",
"system-configuration-sys",
]
@@ -1708,6 +1745,26 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.7.6"
@@ -1765,6 +1822,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
@@ -1811,7 +1880,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
dependencies = [
"bitflags 2.7.0",
"bitflags 2.8.0",
"bytes",
"http",
"http-body",
@@ -1888,12 +1957,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"
@@ -1908,9 +1971,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.11.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "cursor-api"
version = "0.1.3-rc.3.3"
version = "0.1.3-rc.4-pre.1"
edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"]
description = "OpenAI format compatibility layer for the Cursor API"
@@ -12,7 +12,7 @@ sha2 = { version = "0.10.8", default-features = false }
serde_json = "1.0.134"
[dependencies]
axum = { version = "0.7.9", features = ["json"] }
axum = { version = "0.8.1", 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"
@@ -23,20 +23,20 @@ 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"] }
parking_lot = "0.12.3"
paste = "1.0.15"
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.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
reqwest = { version = "0.12.12", default-features = false, features = ["gzip", "brotli", "json", "stream", "socks", "__tls", "charset", "default-tls", "h2", "http2", "macos-system-configuration"] }
serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] }
serde_json = "1.0.135"
serde_json = "1.0.137"
sha2 = { version = "0.10.8", default-features = false }
sysinfo = { version = "0.33.1", default-features = false, features = ["system"] }
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "time", "fs"] }
tokio-stream = { version = "0.1.17", features = ["time"] }
tower-http = { version = "0.6.2", features = ["cors", "limit"] }
urlencoding = "2.1.3"
uuid = { version = "1.11.1", features = ["v4"] }
uuid = { version = "1.12.1", features = ["v4"] }
[profile.release]
lto = true
@@ -44,3 +44,7 @@ codegen-units = 1
panic = 'abort'
strip = true
opt-level = 3
[features]
default = []
use-minified = []

View File

@@ -1,10 +1,5 @@
[target.x86_64-unknown-linux-gnu]
pre-build = [
"set -e",
"apt-get update",
"apt-get install -y --no-install-recommends build-essential protobuf-compiler pkg-config libssl-dev nodejs npm",
"rm -rf /var/lib/apt/lists/*"
]
dockerfile = "Dockerfile.cross"
[target.x86_64-unknown-freebsd]
pre-build = [

55
Deno.ts Normal file
View File

@@ -0,0 +1,55 @@
// 定义允许的主机和路径
const ALLOWED_HOSTS = ["api2.cursor.sh", "www.cursor.com"];
const ALLOWED_PATHS = [
"/aiserver.v1.AiService/StreamChat",
"/auth/full_stripe_profile",
"/api/usage",
"/api/auth/me"
];
// 创建统一的响应处理函数
const createResponse = (status: number, message: string) =>
new Response(message, {
status,
headers: { "Access-Control-Allow-Origin": "*" }
});
// 主处理函数
Deno.serve(async (request: Request) => {
// 验证目标主机
const targetHost = request.headers.get("x-co");
if (!targetHost) return createResponse(400, "Missing header");
if (!ALLOWED_HOSTS.includes(targetHost)) return createResponse(403, "Host denied");
// 验证请求路径
const url = new URL(request.url);
if (!ALLOWED_PATHS.includes(url.pathname)) return createResponse(404, "Path invalid");
// 处理请求头
const headers = new Headers(request.headers);
headers.delete("x-co");
headers.set("Host", targetHost);
try {
// 转发请求
const response = await fetch(
`https://${targetHost}${url.pathname}${url.search}`,
{
method: request.method,
headers,
body: request.body
}
);
// 处理响应头
const responseHeaders = new Headers(response.headers);
responseHeaders.set("Access-Control-Allow-Origin", "*");
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
return createResponse(500, "Server error");
}
});

32
Dockerfile.cross Normal file
View File

@@ -0,0 +1,32 @@
# Dockerfile.cross
# 使用与你 GitHub Actions 中相同的基础镜像
FROM rust:1.84.0-slim-bookworm
# 设置工作目录
WORKDIR /app
# 安装必要的软件包
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
pkg-config \
libssl-dev \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
# 设置环境变量 (如果需要)
ENV RUSTFLAGS="-C link-arg=-s"
# 设置 PROTOC 环境变量 (因为你的 build.rs 需要)
ENV PROTOC=/usr/bin/protoc
# 安装特定版本的 protoc (如果你需要特定版本,例如 29.3;否则可以删除这部分)
# ENV PROTOC_VERSION=29.3
# ENV PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-x86_64.zip
# RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${PROTOC_ZIP} -O /tmp/${PROTOC_ZIP} && \
# unzip /tmp/${PROTOC_ZIP} -d /usr && \
# rm /tmp/${PROTOC_ZIP}
# 验证安装
RUN protoc --version

43
LICENSE
View File

@@ -1,15 +1,38 @@
MIT License
MIT License with Attribution Restrictions (MIT-AR)
Copyright (c) 2025 wisdgod <nav@wisdgod.com>
版权所有 (c) 2025 wisdgod <nav@wisdgod.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
特此授予任何获得本软件和相关文档文件("软件")副本的人免费许可,可以在获得作者书面许可的情况下处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售本软件的副本,并允许向其提供本软件的人这样做,但须符合以下条件:
1. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
上述版权声明和本许可声明应包含在本软件的所有副本或主要部分中。
2. Any public reference to this software in promotional materials must include
the following disclaimer in a prominent position:
"This product utilizes components developed by third-party contributors.
There is no affiliation, endorsement, or sponsorship by the original author."
本软件按"原样"提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和非侵权性的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权行为还是其他方面,由软件或软件的使用或其他交易引起、产生或与之相关。
3. Explicit prohibition against:
a) Using the author's name/alias in marketing collateral
b) Suggesting official certification or partnership
c) Using project name as technical endorsement
特别声明:
1. 任何个人或组织不得以作者的名义进行任何形式的宣传、推广或声明。
2. 使用本软件所产生的任何后果与作者无关,作者不承担任何责任。
3. 如违反上述规定,作者保留追究法律责任的权利。
4. 任何商业用途必须事先获得作者的书面许可。未经许可的商业使用将被视为侵权行为。
4. Violation of these terms automatically terminates granted rights and
requires immediate cessation of software use within 72 hours.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--- Special Provisions ---
* This is a modified MIT license approved by SPDX as "MIT-AR" (Attribution-Restricted)
* Commercial users may request certification waiver via nav@wisdgod.com
* Community projects may display "Powered by" logo pack available at /branding

447
README.md
View File

@@ -2,59 +2,45 @@
## 说明
* 当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
* 若发现首字慢,与本程序无关。
* 若发现响应出现乱码,也与本程序无关。
* 属于官方的问题,请不要像作者反馈。
* 本程序拥有堪比客户端原本的速度,甚至可能更快。
* 本程序的性能是非常厉害的。
* 根据本项目开源协议Fork的项目不能以作者的名义进行任何形式的宣传、推广或声明。
* 当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。
* 若发现首字慢,与本程序无关。
* 若发现响应出现乱码,也与本程序无关。
* 属于官方的问题,请不要像作者反馈。
* 本程序拥有堪比客户端原本的速度,甚至可能更快。
* 本程序的性能是非常厉害的。
* 根据本项目开源协议Fork的项目不能以作者的名义进行任何形式的宣传、推广或声明。
## 获取key
1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录
2. 在浏览器中打开开发者工具F12
3. 在 Application-Cookies 中查找名为 `WorkosCursorSessionToken` 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie 的值使用冒号 (:) 进行分隔。
1. 访问 [www.cursor.com](https://www.cursor.com) 并完成注册登录
2. 在浏览器中打开开发者工具F12
3. 在 Application-Cookies 中查找名为 `WorkosCursorSessionToken` 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie 的值使用冒号 (:) 进行分隔。
## 配置说明
### 环境变量
* `PORT`: 服务器端口号默认3000
* `AUTH_TOKEN`: 认证令牌必须用于API认证
* `ROUTE_PREFIX`: 路由前缀(可选)
* `TOKEN_FILE`: token文件路径默认.token
* `TOKEN_LIST_FILE`: token列表文件路径默认.token-list
* `PORT`: 服务器端口号默认3000
* `AUTH_TOKEN`: 认证令牌必须用于API认证
* `ROUTE_PREFIX`: 路由前缀(可选)
* `TOKEN_LIST_FILE`: token列表文件路径(默认:.tokens
更多请查看 `/env-example`
### Token文件格式
1. `.token` 文件:每行一个token,支持以下格式
`.tokens` 文件:每行token和checksum的对应关系
```
# 这是注释
token1
# alias与标签的作用差不多
alias::token2
```
alias 可以是任意值,用于区分不同的 token更方便管理WorkosCursorSessionToken 是相同格式
该文件将自动向.token-list文件中追加token同时自动生成checksum
2. `.token-list` 文件每行为token和checksum的对应关系
```
# 这里的#表示这行在下次读取要删除
token1,checksum1
# alias被舍弃会自动删除最后一个:或%3A的后一位前的所有内容
token2,checksum2
```
该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:
* 需要删除某个 token
* 需要使用已有 checksum 来对应某一个 token
```
# 这里的#表示这行在下次读取要删除
token1,checksum1
token2,checksum2
```
该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:
* 需要删除某个 token
* 需要使用已有 checksum 来对应某一个 token
### 模型列表
@@ -82,20 +68,22 @@ claude-3.5-haiku
gemini-exp-1206
gemini-2.0-flash-thinking-exp
gemini-2.0-flash-exp
deepseek-v3
deepseek-r1
```
# 接口说明
## 接口说明
## 基础对话
### 基础对话
* 接口地址: `/v1/chat/completions`
* 请求方法: POST
* 认证方式: Bearer Token
1. 使用环境变量 `AUTH_TOKEN` 进行认证
2. 使用 `.token` 文件中的令牌列表进行轮询认证
3. 在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭
* 接口地址: `/v1/chat/completions`
* 请求方法: POST
* 认证方式: Bearer Token
1. 使用环境变量 `AUTH_TOKEN` 进行认证
2. 使用 `.token` 文件中的令牌列表进行轮询认证
3. 在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭
### 请求格式
#### 请求格式
```json
{
@@ -118,7 +106,7 @@ gemini-2.0-flash-exp
}
```
### 响应格式
#### 响应格式
如果 `stream``false`:
@@ -160,89 +148,206 @@ data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"
data: [DONE]
```
## Token管理接口
### Token管理接口
### 简易Token信息管理页面
#### 简易Token信息管理页面
* 接口地址: `/tokeninfo`
* 请求方法: GET
* 响应格式: HTML页面
* 功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容
* 接口地址: `/tokens`
* 请求方法: GET
* 响应格式: HTML页面
* 功能: 调用下面的各种相关API的示例页面
### 更新Token信息 (GET)
* 接口地址: `/update-tokeninfo`
* 请求方法: GET
* 认证方式: 不需要
* 功能: 重新加载tokens并更新应用状态
* 响应格式:
#### 获取Token信息
* 接口地址: `/tokens/get`
* 请求方法: POST
* 认证方式: Bearer Token
* 响应格式:
```json
{
"status": "success",
"tokens": [
{
"token": "string",
"checksum": "string"
}
],
"tokens_count": number
}
```
#### 重载Token信息
* 接口地址: `/tokens/reload`
* 请求方法: POST
* 认证方式: Bearer Token
* 响应格式:
```json
{
"status": "success",
"tokens_count": number,
"message": "Token list has been reloaded"
}
```
### 更新Token信息 (POST)
#### 更新Token信息
* 接口地址: `/update-tokeninfo`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
* 接口地址: `/tokens/update`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
```json
{
"tokens": "string",
"token_list": "string"
"tokens": "string" // token列表内容将会直接覆盖 token_list 文件
}
```
* 响应格式:
* 响应格式:
```json
{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens_count": number,
"message": "Token files have been updated and reloaded"
}
```
### 获取Token信息
#### 添加Token
* 接口地址: `/get-tokeninfo`
* 请求方法: POST
* 认证方式: Bearer Token
* 响应格式:
* 接口地址: `/tokens/add`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
```json
[
{
"token": "string",
"checksum": "string" // 可选,如果不提供将自动生成
}
]
```
* 响应格式:
```json
{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens": "string",
"tokens_count": number,
"token_list": "string"
"message": "string" // "New tokens have been added and reloaded" 或 "No new tokens were added"
}
```
## 配置管理接口
#### 删除Token
### 配置页面
* 接口地址: `/tokens/delete`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
* 接口地址: `/config`
* 请求方法: GET
* 响应格式: HTML页面
* 功能: 提供配置管理界面,可以修改页面内容和系统配置
```json
{
"tokens": ["string"], // 要删除的token列表
"expectation": "simple" | "updated_tokens" | "failed_tokens" | "detailed" // 默认为simple
}
```
### 更新配置
* 响应格式:
* 接口地址: `/config`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
```json
{
"status": "success",
"updated_tokens": ["string"], // 可选根据expectation返回表示更新后的token列表
"failed_tokens": ["string"] // 可选根据expectation返回表示未找到的token列表
}
```
* expectation说明:
- simple: 只返回基本状态
- updated_tokens: 返回更新后的token列表
- failed_tokens: 返回未找到的token列表
- detailed: 返回完整信息包括updated_tokens和failed_tokens
#### 构建API Key
* 接口地址: `/build-key`
* 请求方法: POST
* 认证方式: Bearer Token (当SHARE_AUTH_TOKEN启用时需要)
* 请求格式:
```json
{
"auth_token": "string", // 格式: {token},{checksum}
"enable_stream_check": boolean, // 可选,启用流式响应首块检查
"include_stop_stream": boolean, // 可选,包含停止流
"disable_vision": boolean, // 可选,禁用图片处理能力
"enable_slow_pool": boolean, // 可选,启用慢速池
"usage_check_models": { // 可选,使用量检查模型配置
"type": "default" | "disabled" | "all" | "custom",
"model_ids": "string" // 当type为custom时生效以逗号分隔的模型ID列表
}
}
```
* 响应格式:
```json
{
"key": "string" // 成功时返回生成的key
}
```
或出错时:
```json
{
"error": "string" // 错误信息
}
```
说明:
1. 此接口用于生成携带动态配置的API Key是对直接传token与checksum模式的升级版本
2. API Key特性对比
| 优势 | 劣势 |
|------|------|
| 提取关键信息,生成更短的密钥 | 可能存在版本兼容性问题 |
| 支持携带自定义配置 | 增加了程序复杂度 |
| 采用非常规编码方式,提升安全性 | |
| 更容易验证Key的合法性 | |
| 取消预校验带来轻微性能提升 | |
3. 生成的key格式为: `sk-{encoded_config}`其中sk-为默认前缀(可配置)
4. auth_token的格式为: `{token},{checksum}`,其中,为默认分隔符(可配置)
5. usage_check_models配置说明:
- default: 使用默认模型列表(同下 `usage_check_models` 字段的默认值)
- disabled: 禁用使用量检查
- all: 检查所有可用模型
- custom: 使用自定义模型列表(需在model_ids中指定)
### 配置管理接口
#### 配置页面
* 接口地址: `/config`
* 请求方法: GET
* 响应格式: HTML页面
* 功能: 提供配置管理界面,可以修改页面内容和系统配置
#### 更新配置
* 接口地址: `/config`
* 请求方法: POST
* 认证方式: Bearer Token
* 请求格式:
```json
{
@@ -257,14 +362,17 @@ data: [DONE]
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"usage_check_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
},
"enable_dynamic_key": boolean,
"share_token": "string",
"proxies": "" | "system" | "proxy1,proxy2,..."
}
```
* 响应格式:
* 响应格式:
```json
{
@@ -280,15 +388,18 @@ data: [DONE]
"vision_ability": "none" | "base64" | "all",
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"usage_check_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
},
"enable_dynamic_key": boolean,
"share_token": "string",
"proxies": "" | "system" | "proxy1,proxy2,..."
}
}
```
注意:`check_usage_models` 字段的默认值为:
注意:`usage_check_models` 字段的默认值为:
```json
{
@@ -301,36 +412,36 @@ data: [DONE]
路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。
## 静态资源接口
### 静态资源接口
### 获取共享样式
#### 获取共享样式
* 接口地址: `/static/shared-styles.css`
* 请求方法: GET
* 响应格式: CSS文件
* 功能: 获取共享样式表
* 接口地址: `/static/shared-styles.css`
* 请求方法: GET
* 响应格式: CSS文件
* 功能: 获取共享样式表
### 获取共享脚本
#### 获取共享脚本
* 接口地址: `/static/shared.js`
* 请求方法: GET
* 响应格式: JavaScript文件
* 功能: 获取共享JavaScript代码
* 接口地址: `/static/shared.js`
* 请求方法: GET
* 响应格式: JavaScript文件
* 功能: 获取共享JavaScript代码
### 环境变量示例
#### 环境变量示例
* 接口地址: `/env-example`
* 请求方法: GET
* 响应格式: 文本文件
* 功能: 获取环境变量配置示例
* 接口地址: `/env-example`
* 请求方法: GET
* 响应格式: 文本文件
* 功能: 获取环境变量配置示例
## 其他接口
### 其他接口
### 获取模型列表
#### 获取模型列表
* 接口地址: `/v1/models`
* 请求方法: GET
* 响应格式:
* 接口地址: `/v1/models`
* 请求方法: GET
* 响应格式:
```json
{
@@ -346,23 +457,23 @@ data: [DONE]
}
```
### 获取一个随机hash
#### 获取一个随机hash
* 接口地址: `/get-hash`
* 请求方法: GET
* 响应格式:
* 接口地址: `/get-hash`
* 请求方法: GET
* 响应格式:
```plaintext
string
```
### 获取或修复checksum
#### 获取或修复checksum
* 接口地址: `/get-checksum`
* 请求方法: GET
* 请求参数:
* `checksum`: 可选用于修复的旧版本生成的checksum也可只传入前8个字符可用来自动刷新时间戳头
* 响应格式:
* 接口地址: `/get-checksum`
* 请求方法: GET
* 请求参数:
* `checksum`: 可选用于修复的旧版本生成的checksum也可只传入前8个字符可用来自动刷新时间戳头
* 响应格式:
```plaintext
string
@@ -370,25 +481,25 @@ string
说明:
* 如果不提供`checksum`参数将生成一个新的随机checksum
* 如果提供`checksum`参数将尝试修复旧版本的checksum以适配v0.1.3-rc.3之后的版本使用修复失败会返回新的checksum若输入的checksum本来就有效则返回更新tsheader后的checksum
* 如果不提供`checksum`参数将生成一个新的随机checksum
* 如果提供`checksum`参数将尝试修复旧版本的checksum以适配v0.1.3-rc.3之后的版本使用修复失败会返回新的checksum若输入的checksum本来就有效则返回更新tsheader后的checksum
### 获取当前的tsheader
#### 获取当前的tsheader
* 接口地址: `/get-tsheader`
* 请求方法: GET
* 响应格式:
* 接口地址: `/get-tsheader`
* 请求方法: GET
* 响应格式:
```plaintext
string
```
### 健康检查接口
#### 健康检查接口
* 接口地址: `/health` 或 `/`(重定向)
* 请求方法: GET
* 认证方式: Bearer Token可选
* 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)默认JSON
* 接口地址: `/health``/`(重定向)
* 请求方法: GET
* 认证方式: Bearer Token可选
* 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)默认JSON
```json
{
@@ -415,18 +526,18 @@ string
注意:`stats` 字段仅在请求头中包含正确的 `AUTH_TOKEN` 时才会返回。否则,该字段将被省略。
### 获取日志接口
#### 获取日志接口
* 接口地址: `/logs`
* 请求方法: GET
* 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)
* 接口地址: `/logs`
* 请求方法: GET
* 响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)
### 获取日志数据
#### 获取日志数据
* 接口地址: `/logs`
* 请求方法: POST
* 认证方式: Bearer Token
* 响应格式:
* 接口地址: `/logs`
* 请求方法: POST
* 认证方式: Bearer Token
* 响应格式:
```json
{
@@ -491,12 +602,12 @@ string
}
```
### 获取用户信息
#### 获取用户信息
* 接口地址: `/userinfo`
* 请求方法: POST
* 认证方式: 请求体中包含token
* 请求格式:
* 接口地址: `/userinfo`
* 请求方法: POST
* 认证方式: 请求体中包含token
* 请求格式:
```json
{
@@ -504,7 +615,7 @@ string
}
```
* 响应格式:
* 响应格式:
```json
{
@@ -553,12 +664,12 @@ string
}
```
### 基础校准
#### 基础校准
* 接口地址: `/basic-calibration`
* 请求方法: POST
* 认证方式: 请求体中包含token
* 请求格式:
* 接口地址: `/basic-calibration`
* 请求方法: POST
* 认证方式: 请求体中包含token
* 请求格式:
```json
{
@@ -566,7 +677,7 @@ string
}
```
* 响应格式:
* 响应格式:
```json
{
@@ -580,9 +691,15 @@ string
注意: `user_id`, `create_at`, 和 `checksum_time` 字段在校验失败时可能不存在。
## 项目相关工具
### 获取token
- 使用 [get-token](https://github.com/wisdgod/cursor-api/tree/main/get-token) 获取读取当前设备token仅支持windows与macos
- 使用 [get-token](https://github.com/wisdgod/cursor-api/tree/main/tools/get-token) 获取读取当前用户token仅支持windows、linux与macos
### 重置遥测数据
- 使用 [reset-telemetry](https://github.com/wisdgod/cursor-api/tree/main/tools/reset-telemetry) 重置当前用户遥测数据仅支持windows、linux与macos
## 鸣谢
@@ -592,7 +709,7 @@ string
- [zhx47/cursor-api](https://github.com/zhx47/cursor-api) - 提供了本项目起步阶段的主要参考
- [luolazyandlazy/cursorToApi](https://github.com/luolazyandlazy/cursorToApi)
## 偷偷写在最后的话
### 偷偷写在最后的话
虽然作者觉得~骗~收点钱合理,但不强求,要是**主动自愿**发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧
@@ -600,9 +717,9 @@ string
虽然不是很建议你赞助,但如果你赞助了,大概可以:
* 测试版更新
* 要求功能
* 问题更快解决
* 测试版更新
* 要求功能
* 问题更快解决
即使如此,我也保留可以拒绝赞助和拒绝要求的权利。

View File

@@ -1,13 +1,21 @@
#[cfg(not(any(feature = "use-minified")))]
use sha2::{Digest, Sha256};
#[cfg(not(any(feature = "use-minified")))]
use std::collections::HashMap;
#[cfg(not(any(feature = "use-minified")))]
use std::fs;
use std::io::Result;
use std::path::{Path, PathBuf};
#[cfg(not(any(feature = "use-minified")))]
use std::path::Path;
use std::path::PathBuf;
#[cfg(not(any(feature = "use-minified")))]
use std::process::Command;
// 支持的文件类型
const SUPPORTED_EXTENSIONS: [&str; 3] = ["html", "js", "css"];
#[cfg(not(any(feature = "use-minified")))]
const SUPPORTED_EXTENSIONS: [&str; 4] = ["html", "js", "css", "md"];
#[cfg(not(any(feature = "use-minified")))]
fn check_and_install_deps() -> Result<()> {
let scripts_dir = Path::new("scripts");
let node_modules = scripts_dir.join("node_modules");
@@ -27,10 +35,21 @@ fn check_and_install_deps() -> Result<()> {
Ok(())
}
#[cfg(not(any(feature = "use-minified")))]
fn get_files_hash() -> Result<HashMap<PathBuf, String>> {
let mut file_hashes = HashMap::new();
let static_dir = Path::new("static");
// 首先处理 README.md
let readme_path = Path::new("README.md");
if readme_path.exists() {
let content = fs::read(readme_path)?;
let mut hasher = Sha256::new();
hasher.update(&content);
let hash = format!("{:x}", hasher.finalize());
file_hashes.insert(readme_path.to_path_buf(), hash);
}
if static_dir.exists() {
for entry in fs::read_dir(static_dir)? {
let entry = entry?;
@@ -53,6 +72,7 @@ fn get_files_hash() -> Result<HashMap<PathBuf, String>> {
Ok(file_hashes)
}
#[cfg(not(any(feature = "use-minified")))]
fn load_saved_hashes() -> Result<HashMap<PathBuf, String>> {
let hash_file = Path::new("scripts/.asset-hashes.json");
if hash_file.exists() {
@@ -67,6 +87,7 @@ fn load_saved_hashes() -> Result<HashMap<PathBuf, String>> {
}
}
#[cfg(not(any(feature = "use-minified")))]
fn save_hashes(hashes: &HashMap<PathBuf, String>) -> Result<()> {
let hash_file = Path::new("scripts/.asset-hashes.json");
let string_map: HashMap<String, String> = hashes
@@ -78,6 +99,7 @@ fn save_hashes(hashes: &HashMap<PathBuf, String>) -> Result<()> {
Ok(())
}
#[cfg(not(any(feature = "use-minified")))]
fn minify_assets() -> Result<()> {
// 获取现有文件的哈希
let current_hashes = get_files_hash()?;
@@ -94,14 +116,21 @@ fn minify_assets() -> Result<()> {
let files_to_update: Vec<_> = current_hashes
.iter()
.filter(|(path, current_hash)| {
let is_readme = path.file_name().map_or(false, |f| f == "README.md");
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
));
// 检查压缩后的文件是否存在
// 为 README.md 和其他文件使用不同的输出路径检查
let min_path = if is_readme {
PathBuf::from("static/readme.min.html")
} else {
path.with_file_name(format!(
"{}.min.{}",
path.file_stem().unwrap().to_string_lossy(),
ext
))
};
// 检查压缩/转换后的文件是否存在
if !min_path.exists() {
return true;
}
@@ -140,26 +169,52 @@ fn minify_assets() -> Result<()> {
fn main() -> Result<()> {
// Proto 文件处理
println!("cargo:rerun-if-changed=src/chat/aiserver/v1/lite.proto");
println!("cargo:rerun-if-changed=src/chat/config/key.proto");
// 获取环境变量 PROTOC
let protoc_path = match std::env::var_os("PROTOC") {
Some(path) => PathBuf::from(path),
None => {
println!("cargo:warning=PROTOC environment variable not set, using default protoc.");
// 如果 PROTOC 未设置,则返回一个空的 PathBufprost-build 会尝试使用默认的 protoc
PathBuf::new()
}
};
let mut config = prost_build::Config::new();
// 如果 protoc_path 不为空,则配置使用指定的 protoc
if !protoc_path.as_os_str().is_empty() {
config.protoc_executable(protoc_path);
}
// config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
// config.type_attribute(
// "aiserver.v1.ThrowErrorCheckRequest",
// "#[derive(serde::Serialize, serde::Deserialize)]"
// );
config
.compile_protos(&["src/chat/aiserver/v1/lite.proto"], &["src/chat/aiserver/v1/"])
.compile_protos(
&["src/chat/aiserver/v1/lite.proto"],
&["src/chat/aiserver/v1/"],
)
.unwrap();
config
.compile_protos(&["src/chat/config/key.proto"], &["src/chat/config/"])
.unwrap();
// 静态资源文件处理
println!("cargo:rerun-if-changed=scripts/minify.js");
println!("cargo:rerun-if-changed=scripts/package.json");
println!("cargo:rerun-if-changed=static");
println!("cargo:rerun-if-changed=static/api.html");
println!("cargo:rerun-if-changed=static/build_key.html");
println!("cargo:rerun-if-changed=static/config.html");
println!("cargo:rerun-if-changed=static/logs.html");
println!("cargo:rerun-if-changed=static/shared-styles.css");
println!("cargo:rerun-if-changed=static/shared.js");
println!("cargo:rerun-if-changed=static/tokens.html");
println!("cargo:rerun-if-changed=README.md");
// 检查并安装依赖
check_and_install_deps()?;
#[cfg(not(any(feature = "use-minified")))]
{
// 检查并安装依赖
check_and_install_deps()?;
// 运行资源压缩
minify_assets()?;
// 运行资源压缩
minify_assets()?;
}
Ok(())
}

View File

@@ -3,6 +3,7 @@
const { minify: minifyHtml } = require('html-minifier-terser');
const { minify: minifyJs } = require('terser');
const CleanCSS = require('clean-css');
const MarkdownIt = require('markdown-it');
const fs = require('fs');
const path = require('path');
@@ -28,10 +29,75 @@ const cssOptions = {
// 处理文件
async function minifyFile(inputPath, outputPath) {
try {
const ext = path.extname(inputPath).toLowerCase();
const content = fs.readFileSync(inputPath, 'utf8');
let ext = path.extname(inputPath).toLowerCase();
if (ext === '.md') ext = '.html';
const filename = path.basename(inputPath);
let content = fs.readFileSync(inputPath, 'utf8');
let minified;
// 特殊处理 readme.html
if (filename.toLowerCase() === 'readme.md') {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
});
const readmeMdPath = path.join(__dirname, '..', 'README.md');
const markdownContent = fs.readFileSync(readmeMdPath, 'utf8');
// 添加基本的 markdown 样式
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>README</title>
<style>
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.6;
}
pre {
background-color: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow: auto;
}
code {
background-color: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 3px;
}
img {
max-width: 100%;
}
table {
border-collapse: collapse;
width: 100%;
}
table td, table th {
border: 1px solid #dfe2e5;
padding: 6px 13px;
}
blockquote {
border-left: 4px solid #dfe2e5;
margin: 0;
padding: 0 1em;
color: #6a737d;
}
</style>
</head>
<body>
${md.render(markdownContent)}
</body>
</html>
`;
content = htmlContent;
}
switch (ext) {
case '.html':
minified = await minifyHtml(content, options);
@@ -68,12 +134,22 @@ async function main() {
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}`)
);
// 特殊处理 README.md 的输入路径
let inputPath;
let outputPath;
if (file.toLowerCase() === 'readme.md') {
inputPath = path.join(__dirname, '..', 'README.md');
outputPath = path.join(staticDir, 'readme.min.html');
} else {
inputPath = path.join(staticDir, file);
const ext = path.extname(file);
outputPath = path.join(
staticDir,
file.replace(ext, `.min${ext}`)
);
}
await minifyFile(inputPath, outputPath);
}
}

View File

@@ -10,6 +10,7 @@
"dependencies": {
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"markdown-it": "^14.1.0",
"terser": "^5.37.0"
},
"engines": {
@@ -86,6 +87,12 @@
"node": ">=0.4.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -166,6 +173,15 @@
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -175,6 +191,29 @@
"tslib": "^2.0.3"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -205,6 +244,15 @@
"tslib": "^2.0.3"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -262,6 +310,12 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
}
}
}

View File

@@ -8,6 +8,7 @@
"dependencies": {
"clean-css": "^5.3.3",
"html-minifier-terser": "^7.2.0",
"markdown-it": "^14.1.0",
"terser": "^5.37.0"
}
}

View File

@@ -1,9 +1,5 @@
use super::{
constant::AUTHORIZATION_BEARER_PREFIX,
lazy::AUTH_TOKEN,
model::AppConfig,
};
use crate::common::models::{
use super::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN, model::AppConfig};
use crate::common::model::{
config::{ConfigData, ConfigUpdateRequest},
ApiStatus, ErrorResponse, NormalResponse,
};
@@ -13,40 +9,24 @@ use axum::{
};
// 定义处理更新操作的宏
macro_rules! handle_update {
($request:expr, $field:ident, $update_fn:expr, $field_name:expr) => {
if let Some($field) = $request.$field {
if let Err(e) = $update_fn($field) {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Failed,
code: Some(500),
error: Some(format!("更新 {} 失败: {}", $field_name, e)),
message: None,
}),
));
macro_rules! handle_updates {
($request:expr, $($field:ident => $update_fn:expr),* $(,)?) => {
$(
if let Some(value) = $request.$field {
$update_fn(value);
}
}
)*
};
}
// 定义处理重置操作的宏
macro_rules! handle_reset {
($request:expr, $field:ident, $reset_fn:expr, $field_name:expr) => {
if $request.$field.is_some() {
if let Err(e) = $reset_fn() {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Failed,
code: Some(500),
error: Some(format!("重置 {} 失败: {}", $field_name, e)),
message: None,
}),
));
macro_rules! handle_resets {
($request:expr, $($field:ident => $reset_fn:expr),* $(,)?) => {
$(
if $request.$field.is_some() {
$reset_fn();
}
}
)*
};
}
@@ -90,7 +70,10 @@ pub async fn handle_config_update(
vision_ability: AppConfig::get_vision_ability(),
enable_slow_pool: AppConfig::get_slow_pool(),
enable_all_claude: AppConfig::get_allow_claude(),
check_usage_models: AppConfig::get_usage_check(),
usage_check_models: AppConfig::get_usage_check(),
enable_dynamic_key: AppConfig::get_dynamic_key(),
share_token: AppConfig::get_share_token(),
proxies: AppConfig::get_proxies(),
}),
message: None,
})),
@@ -112,41 +95,16 @@ pub async fn handle_config_update(
}
}
handle_update!(
request,
enable_stream_check,
AppConfig::update_stream_check,
"enable_stream_check"
);
handle_update!(
request,
include_stop_stream,
AppConfig::update_stop_stream,
"include_stop_stream"
);
handle_update!(
request,
vision_ability,
AppConfig::update_vision_ability,
"vision_ability"
);
handle_update!(
request,
enable_slow_pool,
AppConfig::update_slow_pool,
"enable_slow_pool"
);
handle_update!(
request,
enable_all_claude,
AppConfig::update_allow_claude,
"enable_all_claude"
);
handle_update!(
request,
check_usage_models,
AppConfig::update_usage_check,
"check_usage_models"
handle_updates!(request,
enable_stream_check => AppConfig::update_stream_check,
include_stop_stream => AppConfig::update_stop_stream,
vision_ability => AppConfig::update_vision_ability,
enable_slow_pool => AppConfig::update_slow_pool,
enable_all_claude => AppConfig::update_allow_claude,
usage_check_models => AppConfig::update_usage_check,
enable_dynamic_key => AppConfig::update_dynamic_key,
share_token => AppConfig::update_share_token,
proxies => AppConfig::update_proxies,
);
Ok(Json(NormalResponse {
@@ -172,41 +130,16 @@ pub async fn handle_config_update(
}
}
handle_reset!(
request,
enable_stream_check,
AppConfig::reset_stream_check,
"enable_stream_check"
);
handle_reset!(
request,
include_stop_stream,
AppConfig::reset_stop_stream,
"include_stop_stream"
);
handle_reset!(
request,
vision_ability,
AppConfig::reset_vision_ability,
"vision_ability"
);
handle_reset!(
request,
enable_slow_pool,
AppConfig::reset_slow_pool,
"enable_slow_pool"
);
handle_reset!(
request,
enable_all_claude,
AppConfig::reset_allow_claude,
"enable_all_claude"
);
handle_reset!(
request,
check_usage_models,
AppConfig::reset_usage_check,
"check_usage_models"
handle_resets!(request,
enable_stream_check => AppConfig::reset_stream_check,
include_stop_stream => AppConfig::reset_stop_stream,
vision_ability => AppConfig::reset_vision_ability,
enable_slow_pool => AppConfig::reset_slow_pool,
enable_all_claude => AppConfig::reset_allow_claude,
usage_check_models => AppConfig::reset_usage_check,
enable_dynamic_key => AppConfig::reset_dynamic_key,
share_token => AppConfig::reset_share_token,
proxies => AppConfig::reset_proxies,
);
Ok(Json(NormalResponse {

View File

@@ -4,6 +4,8 @@ macro_rules! def_pub_const {
};
}
pub const COMMA: char = ',';
def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION"));
// def_pub_const!(PKG_NAME, env!("CARGO_PKG_NAME"));
// def_pub_const!(PKG_DESCRIPTION, env!("CARGO_PKG_DESCRIPTION"));
@@ -12,6 +14,8 @@ def_pub_const!(PKG_VERSION, env!("CARGO_PKG_VERSION"));
def_pub_const!(EMPTY_STRING, "");
def_pub_const!(COMMA_STRING, ",");
def_pub_const!(ROUTE_ROOT_PATH, "/");
def_pub_const!(ROUTE_HEALTH_PATH, "/health");
def_pub_const!(ROUTE_GET_HASH, "/get-hash");
@@ -21,19 +25,22 @@ def_pub_const!(ROUTE_USER_INFO_PATH, "/userinfo");
def_pub_const!(ROUTE_API_PATH, "/api");
def_pub_const!(ROUTE_LOGS_PATH, "/logs");
def_pub_const!(ROUTE_CONFIG_PATH, "/config");
def_pub_const!(ROUTE_TOKENINFO_PATH, "/tokeninfo");
def_pub_const!(ROUTE_GET_TOKENINFO_PATH, "/get-tokeninfo");
def_pub_const!(ROUTE_UPDATE_TOKENINFO_PATH, "/update-tokeninfo");
def_pub_const!(ROUTE_TOKENS_PATH, "/tokens");
def_pub_const!(ROUTE_TOKENS_GET_PATH, "/tokens/get");
def_pub_const!(ROUTE_TOKENS_RELOAD_PATH, "/tokens/reload");
def_pub_const!(ROUTE_TOKENS_UPDATE_PATH, "/tokens/update");
def_pub_const!(ROUTE_TOKENS_ADD_PATH, "/tokens/add");
def_pub_const!(ROUTE_TOKENS_DELETE_PATH, "/tokens/delete");
def_pub_const!(ROUTE_ENV_EXAMPLE_PATH, "/env-example");
def_pub_const!(ROUTE_STATIC_PATH, "/static/:path");
def_pub_const!(ROUTE_STATIC_PATH, "/static/{path}");
def_pub_const!(ROUTE_SHARED_STYLES_PATH, "/static/shared-styles.css");
def_pub_const!(ROUTE_SHARED_JS_PATH, "/static/shared.js");
def_pub_const!(ROUTE_ABOUT_PATH, "/about");
def_pub_const!(ROUTE_README_PATH, "/readme");
def_pub_const!(ROUTE_BASIC_CALIBRATION_PATH, "/basic-calibration");
def_pub_const!(ROUTE_BUILD_KEY_PATH, "/build-key");
def_pub_const!(DEFAULT_TOKEN_FILE_NAME, ".token");
def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".token-list");
def_pub_const!(DEFAULT_TOKEN_LIST_FILE_NAME, ".tokens");
def_pub_const!(STATUS_PENDING, "pending");
def_pub_const!(STATUS_SUCCESS, "success");
@@ -47,9 +54,15 @@ def_pub_const!(FALSE, "false");
// def_pub_const!(CONTENT_TYPE_PROTO, "application/proto");
def_pub_const!(CONTENT_TYPE_CONNECT_PROTO, "application/connect+proto");
def_pub_const!(CONTENT_TYPE_TEXT_HTML_WITH_UTF8, "text/html;charset=utf-8");
def_pub_const!(CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, "text/plain;charset=utf-8");
def_pub_const!(
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8,
"text/plain;charset=utf-8"
);
def_pub_const!(CONTENT_TYPE_TEXT_CSS_WITH_UTF8, "text/css;charset=utf-8");
def_pub_const!(CONTENT_TYPE_TEXT_JS_WITH_UTF8, "text/javascript;charset=utf-8");
def_pub_const!(
CONTENT_TYPE_TEXT_JS_WITH_UTF8,
"text/javascript;charset=utf-8"
);
def_pub_const!(AUTHORIZATION_BEARER_PREFIX, "Bearer ");
@@ -65,8 +78,6 @@ def_pub_const!(OBJECT_CHAT_COMPLETION_CHUNK, "chat.completion.chunk");
def_pub_const!(FINISH_REASON_STOP, "stop");
def_pub_const!(ERR_UPDATE_CONFIG, "无法更新配置");
def_pub_const!(ERR_RESET_CONFIG, "无法重置配置");
def_pub_const!(ERR_INVALID_PATH, "无效的路径");
// def_pub_const!(ERR_CHECKSUM_NO_GOOD, "checksum no good");

View File

@@ -1,11 +1,11 @@
use crate::{
app::constant::{
CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME,
EMPTY_STRING,
},
common::utils::{parse_ascii_char_from_env, parse_string_from_env},
use super::constant::{
COMMA, CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_LIST_FILE_NAME, EMPTY_STRING,
};
use crate::common::utils::{
parse_ascii_char_from_env, parse_bool_from_env, parse_string_from_env, parse_usize_from_env,
};
use std::sync::LazyLock;
use tokio::sync::{Mutex, OnceCell};
macro_rules! def_pub_static {
// 基础版本:直接存储 String
@@ -32,7 +32,6 @@ macro_rules! def_pub_static {
def_pub_static!(ROUTE_PREFIX, env: "ROUTE_PREFIX", default: EMPTY_STRING);
def_pub_static!(AUTH_TOKEN, env: "AUTH_TOKEN", default: EMPTY_STRING);
def_pub_static!(TOKEN_FILE, env: "TOKEN_FILE", default: DEFAULT_TOKEN_FILE_NAME);
def_pub_static!(TOKEN_LIST_FILE, env: "TOKEN_LIST_FILE", default: DEFAULT_TOKEN_LIST_FILE_NAME);
def_pub_static!(ROUTE_MODELS_PATH, format!("{}/v1/models", *ROUTE_PREFIX));
def_pub_static!(
@@ -51,70 +50,128 @@ def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Res
def_pub_static!(REVERSE_PROXY_HOST, env: "REVERSE_PROXY_HOST", default: EMPTY_STRING);
def_pub_static!(SHARED_AUTH_TOKEN, env: "SHARED_AUTH_TOKEN", default: EMPTY_STRING);
const DEFAULT_KEY_PREFIX: &str = "sk-";
pub static USE_SHARE: LazyLock<bool> = LazyLock::new(|| !SHARED_AUTH_TOKEN.is_empty());
pub static KEY_PREFIX: LazyLock<String> = LazyLock::new(|| {
let value = parse_string_from_env("KEY_PREFIX", DEFAULT_KEY_PREFIX)
.trim()
.to_string();
if value.is_empty() {
DEFAULT_KEY_PREFIX.to_string()
} else {
value
}
});
pub static KEY_PREFIX_LEN: LazyLock<usize> = LazyLock::new(|| KEY_PREFIX.len());
pub static TOKEN_DELIMITER: LazyLock<char> = LazyLock::new(|| {
let delimiter = parse_ascii_char_from_env("TOKEN_DELIMITER", ',');
let delimiter = parse_ascii_char_from_env("TOKEN_DELIMITER", COMMA);
if delimiter.is_ascii_alphabetic()
|| delimiter.is_ascii_digit()
|| delimiter == '+'
|| delimiter == '/'
{
','
COMMA
} else {
delimiter
}
});
pub static TOKEN_DELIMITER_LEN: LazyLock<usize> = LazyLock::new(|| TOKEN_DELIMITER.len_utf8());
pub static USE_PROXY: LazyLock<bool> = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty());
pub static CURSOR_API2_CHAT_URL: LazyLock<String> = LazyLock::new(|| {
let host = if *USE_PROXY {
&*REVERSE_PROXY_HOST
pub static USE_COMMA_DELIMITER: LazyLock<bool> = LazyLock::new(|| {
let enable = parse_bool_from_env("USE_COMMA_DELIMITER", true);
if enable && *TOKEN_DELIMITER == COMMA {
false
} else {
CURSOR_API2_HOST
};
format!("https://{}/aiserver.v1.AiService/StreamChat", host)
enable
}
});
pub static CURSOR_API2_STRIPE_URL: LazyLock<String> = LazyLock::new(|| {
let host = if *USE_PROXY {
&*REVERSE_PROXY_HOST
} else {
CURSOR_API2_HOST
pub static USE_REVERSE_PROXY: LazyLock<bool> = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty());
macro_rules! def_cursor_api_url {
($name:ident, $api_host:expr, $path:expr) => {
pub static $name: LazyLock<String> = LazyLock::new(|| {
let host = if *USE_REVERSE_PROXY {
&*REVERSE_PROXY_HOST
} else {
$api_host
};
format!("https://{}{}", host, $path)
});
};
format!("https://{}/auth/full_stripe_profile", host)
});
}
pub static CURSOR_USAGE_API_URL: LazyLock<String> = LazyLock::new(|| {
let host = if *USE_PROXY {
&*REVERSE_PROXY_HOST
} else {
CURSOR_HOST
def_cursor_api_url!(
CURSOR_API2_CHAT_URL,
CURSOR_API2_HOST,
"/aiserver.v1.AiService/StreamChat"
);
def_cursor_api_url!(
CURSOR_API2_STRIPE_URL,
CURSOR_API2_HOST,
"/auth/full_stripe_profile"
);
def_cursor_api_url!(CURSOR_USAGE_API_URL, CURSOR_HOST, "/api/usage");
def_cursor_api_url!(CURSOR_USER_API_URL, CURSOR_HOST, "/api/auth/me");
pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
// 使用环境变量 "DEBUG_LOG_FILE" 来指定日志文件路径,默认值为 "debug.log"
static DEBUG_LOG_FILE: LazyLock<String> =
LazyLock::new(|| parse_string_from_env("DEBUG_LOG_FILE", "debug.log"));
// 使用 OnceCell 结合 Mutex 来异步初始化 LOG_FILE
static LOG_FILE: OnceCell<Mutex<tokio::fs::File>> = OnceCell::const_new();
pub(crate) async fn get_log_file() -> &'static Mutex<tokio::fs::File> {
LOG_FILE
.get_or_init(|| async {
Mutex::new(
tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&*DEBUG_LOG_FILE)
.await
.expect("无法打开日志文件"),
)
})
.await
}
#[macro_export]
macro_rules! debug_println {
($($arg:tt)*) => {
if *crate::app::lazy::DEBUG {
let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("{} - {}", time, format!($($arg)*));
use tokio::io::AsyncWriteExt as _;
// 使用 tokio 的 spawn 在后台异步写入日志
tokio::spawn(async move {
let log_file = crate::app::lazy::get_log_file().await;
// 使用 MutexGuard 获取可变引用
let mut file = log_file.lock().await;
if let Err(err) = file.write_all(log_message.as_bytes()).await {
eprintln!("写入日志文件失败: {}", err);
}
if let Err(err) = file.write_all(b"\n").await {
eprintln!("写入换行符失败: {}", err);
}
// 可以选择在写入失败时 panic或者忽略
// panic!("写入日志文件失败: {}", err);
});
}
};
format!("https://{}/api/usage", host)
}
pub static REQUEST_LOGS_LIMIT: LazyLock<usize> =
LazyLock::new(|| std::cmp::min(parse_usize_from_env("REQUEST_LOGS_LIMIT", 100), 2000));
pub static SERVICE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
let timeout = parse_usize_from_env("SERVICE_TIMEOUT", 30);
u64::try_from(timeout).map(|t| t.min(600)).unwrap_or(30)
});
pub static CURSOR_USER_API_URL: LazyLock<String> = LazyLock::new(|| {
let host = if *USE_PROXY {
&*REVERSE_PROXY_HOST
} else {
CURSOR_HOST
};
format!("https://{}/api/auth/me", host)
});
// pub static DEBUG: LazyLock<bool> = LazyLock::new(|| parse_bool_from_env("DEBUG", false));
// #[macro_export]
// macro_rules! debug_println {
// ($($arg:tt)*) => {
// if *crate::app::statics::DEBUG {
// println!($($arg)*);
// }
// };
// }

View File

@@ -1,14 +1,26 @@
use crate::{
app::constant::{
ERR_INVALID_PATH, ERR_RESET_CONFIG, ERR_UPDATE_CONFIG, ROUTE_ABOUT_PATH, ROUTE_CONFIG_PATH,
ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_SHARED_JS_PATH,
ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENINFO_PATH, ROUTE_API_PATH,
EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH,
ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH,
},
chat::model::Message,
common::{
client::rebuild_http_client,
model::{userinfo::TokenProfile, ApiStatus},
utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env},
},
common::models::userinfo::TokenProfile,
};
use crate::chat::model::Message;
use std::sync::{LazyLock, RwLock};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
mod usage_check;
pub use usage_check::UsageCheck;
mod proxies;
pub use proxies::Proxies;
mod build_key;
pub use build_key::*;
// 页面内容类型枚举
#[derive(Clone, Serialize, Deserialize)]
@@ -28,9 +40,6 @@ impl Default for PageContent {
}
}
mod usage_check;
pub use usage_check::UsageCheck;
// 静态配置
#[derive(Clone)]
pub struct AppConfig {
@@ -41,9 +50,13 @@ pub struct AppConfig {
allow_claude: bool,
pages: Pages,
usage_check: UsageCheck,
dynamic_key: bool,
share_token: String,
is_share: bool,
proxies: Proxies,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub enum VisionAbility {
#[serde(rename = "none", alias = "disabled")]
None,
@@ -62,6 +75,10 @@ impl VisionAbility {
_ => Self::default(),
}
}
pub fn is_none(&self) -> bool {
matches!(self, VisionAbility::None)
}
}
impl Default for VisionAbility {
@@ -81,6 +98,7 @@ pub struct Pages {
pub about_content: PageContent,
pub readme_content: PageContent,
pub api_content: PageContent,
pub build_key_content: PageContent,
}
// 运行时状态
@@ -93,9 +111,8 @@ pub struct AppState {
}
// 全局配置实例
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> = LazyLock::new(|| {
RwLock::new(AppConfig::default())
});
pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> =
LazyLock::new(|| RwLock::new(AppConfig::default()));
impl Default for AppConfig {
fn default() -> Self {
@@ -107,6 +124,10 @@ impl Default for AppConfig {
allow_claude: false,
pages: Pages::default(),
usage_check: UsageCheck::Default,
dynamic_key: false,
share_token: String::default(),
is_share: false,
proxies: Proxies::default(),
}
}
}
@@ -115,28 +136,67 @@ macro_rules! config_methods {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type {
APP_CONFIG
.read()
.map(|config| config.$field.clone())
.unwrap_or($default)
pub fn [<get_ $field>]() -> $type
where
$type: Copy + PartialEq,
{
APP_CONFIG.read().$field
}
pub fn [<update_ $field>](value: $type) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.$field = value;
Ok(())
} else {
Err(ERR_UPDATE_CONFIG)
pub fn [<update_ $field>](value: $type)
where
$type: Copy + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
pub fn [<reset_ $field>]() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.$field = $default;
Ok(())
} else {
Err(ERR_RESET_CONFIG)
pub fn [<reset_ $field>]()
where
$type: Copy + PartialEq,
{
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
)*
};
}
macro_rules! config_methods_clone {
($($field:ident: $type:ty, $default:expr;)*) => {
$(
paste::paste! {
pub fn [<get_ $field>]() -> $type
where
$type: Clone + PartialEq,
{
APP_CONFIG.read().$field.clone()
}
pub fn [<update_ $field>](value: $type)
where
$type: Clone + PartialEq,
{
let current = Self::[<get_ $field>]();
if current != value {
APP_CONFIG.write().$field = value;
}
}
pub fn [<reset_ $field>]()
where
$type: Clone + PartialEq,
{
let default_value = $default;
let current = Self::[<get_ $field>]();
if current != default_value {
APP_CONFIG.write().$field = default_value;
}
}
}
@@ -145,19 +205,22 @@ macro_rules! config_methods {
}
impl AppConfig {
pub fn init(
stream_check: bool,
stop_stream: bool,
vision_ability: VisionAbility,
slow_pool: bool,
allow_claude: bool,
) {
if let Ok(mut config) = APP_CONFIG.write() {
config.stream_check = stream_check;
config.stop_stream = stop_stream;
config.vision_ability = vision_ability;
config.slow_pool = slow_pool;
config.allow_claude = allow_claude;
pub fn init() {
let mut config = APP_CONFIG.write();
config.stream_check = parse_bool_from_env("ENABLE_STREAM_CHECK", true);
config.stop_stream = parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true);
config.vision_ability =
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING));
config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false);
config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false);
config.usage_check =
UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING));
config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false);
config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING);
config.is_share = !config.share_token.is_empty();
config.proxies = match std::env::var("PROXIES") {
Ok(proxies) => Proxies::from_str(proxies.as_str()),
Err(_) => Proxies::default(),
}
}
@@ -166,113 +229,113 @@ impl AppConfig {
stop_stream: bool, true;
slow_pool: bool, false;
allow_claude: bool, false;
dynamic_key: bool, false;
}
pub fn get_vision_ability() -> VisionAbility {
APP_CONFIG
.read()
.map(|config| config.vision_ability.clone())
.unwrap_or_default()
config_methods_clone! {
vision_ability: VisionAbility, VisionAbility::default();
usage_check: UsageCheck, UsageCheck::default();
}
pub fn get_share_token() -> String {
APP_CONFIG.read().share_token.clone()
}
pub fn update_share_token(value: String) {
let current = Self::get_share_token();
if current != value {
let mut config = APP_CONFIG.write();
config.share_token = value;
config.is_share = !config.share_token.is_empty();
}
}
pub fn reset_share_token() {
let current = Self::get_share_token();
if !current.is_empty() {
let mut config = APP_CONFIG.write();
config.share_token = String::new();
config.is_share = false;
}
}
pub fn get_proxies() -> Proxies {
APP_CONFIG.read().proxies.clone()
}
pub fn update_proxies(value: Proxies) {
let current = Self::get_proxies();
if current != value {
let mut config = APP_CONFIG.write();
config.proxies = value;
rebuild_http_client();
}
}
pub fn reset_proxies() {
let default_value = Proxies::default();
let current = Self::get_proxies();
if current != default_value {
let mut config = APP_CONFIG.write();
config.proxies = default_value;
rebuild_http_client();
}
}
pub fn get_page_content(path: &str) -> Option<PageContent> {
APP_CONFIG.read().ok().map(|config| match path {
ROUTE_ROOT_PATH => config.pages.root_content.clone(),
ROUTE_LOGS_PATH => config.pages.logs_content.clone(),
ROUTE_CONFIG_PATH => config.pages.config_content.clone(),
ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content.clone(),
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content.clone(),
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content.clone(),
ROUTE_ABOUT_PATH => config.pages.about_content.clone(),
ROUTE_README_PATH => config.pages.readme_content.clone(),
ROUTE_API_PATH => config.pages.api_content.clone(),
_ => PageContent::default(),
})
}
pub fn get_usage_check() -> UsageCheck {
APP_CONFIG
.read()
.map(|config| config.usage_check.clone())
.unwrap_or_default()
}
pub fn update_vision_ability(new_ability: VisionAbility) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.vision_ability = new_ability;
Ok(())
} else {
Err(ERR_UPDATE_CONFIG)
match path {
ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()),
ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()),
ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()),
ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()),
ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()),
ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()),
ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()),
ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()),
ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()),
ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()),
_ => None,
}
}
pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
match path {
ROUTE_ROOT_PATH => config.pages.root_content = content,
ROUTE_LOGS_PATH => config.pages.logs_content = content,
ROUTE_CONFIG_PATH => config.pages.config_content = content,
ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content = content,
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
ROUTE_ABOUT_PATH => config.pages.about_content = content,
ROUTE_README_PATH => config.pages.readme_content = content,
ROUTE_API_PATH => config.pages.api_content = content,
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
} else {
Err(ERR_UPDATE_CONFIG)
}
}
pub fn update_usage_check(rule: UsageCheck) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.usage_check = rule;
Ok(())
} else {
Err(ERR_UPDATE_CONFIG)
}
}
pub fn reset_vision_ability() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.vision_ability = VisionAbility::Base64;
Ok(())
} else {
Err(ERR_RESET_CONFIG)
let mut config = APP_CONFIG.write();
match path {
ROUTE_ROOT_PATH => config.pages.root_content = content,
ROUTE_LOGS_PATH => config.pages.logs_content = content,
ROUTE_CONFIG_PATH => config.pages.config_content = content,
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content,
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content,
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content,
ROUTE_ABOUT_PATH => config.pages.about_content = content,
ROUTE_README_PATH => config.pages.readme_content = content,
ROUTE_API_PATH => config.pages.api_content = content,
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content,
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
}
pub fn reset_page_content(path: &str) -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
match path {
ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(),
ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(),
ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(),
ROUTE_TOKENINFO_PATH => config.pages.tokeninfo_content = PageContent::default(),
ROUTE_SHARED_STYLES_PATH => {
config.pages.shared_styles_content = PageContent::default()
}
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
ROUTE_README_PATH => config.pages.readme_content = PageContent::default(),
ROUTE_API_PATH => config.pages.api_content = PageContent::default(),
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
} else {
Err(ERR_RESET_CONFIG)
let mut config = APP_CONFIG.write();
match path {
ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(),
ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(),
ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(),
ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(),
ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(),
ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(),
ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(),
ROUTE_README_PATH => config.pages.readme_content = PageContent::default(),
ROUTE_API_PATH => config.pages.api_content = PageContent::default(),
ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(),
_ => return Err(ERR_INVALID_PATH),
}
Ok(())
}
pub fn reset_usage_check() -> Result<(), &'static str> {
if let Ok(mut config) = APP_CONFIG.write() {
config.usage_check = UsageCheck::default();
Ok(())
} else {
Err(ERR_RESET_CONFIG)
}
pub fn is_share() -> bool {
APP_CONFIG.read().is_share
}
}
@@ -286,6 +349,12 @@ impl AppState {
token_infos,
}
}
pub fn update_checksum(&mut self) {
for token_info in self.token_infos.iter_mut() {
token_info.checksum = generate_checksum_with_repair(&token_info.checksum);
}
}
}
// 请求日志
@@ -306,9 +375,9 @@ pub struct RequestLog {
#[derive(Serialize, Clone)]
pub struct TimingInfo {
pub total: f64, // 总用时(秒)
pub total: f64, // 总用时(秒)
#[serde(skip_serializing_if = "Option::is_none")]
pub first: Option<f64>, // 首字时间(秒)
pub first: Option<f64>, // 首字时间(秒)
}
// 聊天请求
@@ -333,6 +402,58 @@ pub struct TokenInfo {
#[derive(Deserialize)]
pub struct TokenUpdateRequest {
pub tokens: String,
#[serde(default)]
pub token_list: Option<String>,
}
#[derive(Deserialize)]
pub struct TokenAddRequestTokenInfo {
pub token: String,
#[serde(default)]
pub checksum: Option<String>,
}
// TokensDeleteRequest 结构体
#[derive(Deserialize)]
pub struct TokensDeleteRequest {
#[serde(default)]
pub tokens: Vec<String>,
#[serde(default)]
pub expectation: TokensDeleteResponseExpectation,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TokensDeleteResponseExpectation {
#[default]
Simple,
UpdatedTokens,
FailedTokens,
Detailed,
}
impl TokensDeleteResponseExpectation {
pub fn needs_updated_tokens(&self) -> bool {
matches!(
self,
TokensDeleteResponseExpectation::UpdatedTokens
| TokensDeleteResponseExpectation::Detailed
)
}
pub fn needs_failed_tokens(&self) -> bool {
matches!(
self,
TokensDeleteResponseExpectation::FailedTokens
| TokensDeleteResponseExpectation::Detailed
)
}
}
// TokensDeleteResponse 结构体
#[derive(Serialize)]
pub struct TokensDeleteResponse {
pub status: ApiStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_tokens: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_tokens: Option<Vec<String>>,
}

View File

@@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use crate::{app::constant::COMMA, chat::constant::AVAILABLE_MODELS};
#[derive(Deserialize)]
pub struct BuildKeyRequest {
// 认证令牌(必需)
pub auth_token: String,
// 流第一个块检查
#[serde(default)]
pub enable_stream_check: Option<bool>,
// 包含停止流
#[serde(default)]
pub include_stop_stream: Option<bool>,
// 是否禁用图片处理能力
#[serde(default)]
pub disable_vision: Option<bool>,
// 慢速池
#[serde(default)]
pub enable_slow_pool: Option<bool>,
// 使用量检查模型规则
#[serde(default)]
pub usage_check_models: Option<UsageCheckModelConfig>,
}
pub struct UsageCheckModelConfig {
pub model_type: UsageCheckModelType,
pub model_ids: Vec<&'static str>,
}
impl<'de> Deserialize<'de> for UsageCheckModelConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
#[serde(rename = "type")]
model_type: UsageCheckModelType,
#[serde(default)]
model_ids: String,
}
let helper = Helper::deserialize(deserializer)?;
let model_ids = if helper.model_ids.is_empty() {
Vec::new()
} else {
helper
.model_ids
.split(COMMA)
.filter_map(|model| {
let model = model.trim();
AVAILABLE_MODELS
.iter()
.find(|m| m.id == model)
.map(|m| m.id)
})
.collect()
};
Ok(UsageCheckModelConfig {
model_type: helper.model_type,
model_ids,
})
}
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UsageCheckModelType {
Default,
Disabled,
All,
Custom,
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BuildKeyResponse {
Key(String),
Error(String),
}

80
src/app/model/proxies.rs Normal file
View File

@@ -0,0 +1,80 @@
use reqwest::{Client, Proxy};
use serde::{Serialize, Serializer};
use serde::{Deserialize, Deserializer};
use crate::app::constant::COMMA_STRING;
#[derive(Clone, Default, PartialEq)]
pub enum Proxies {
No,
#[default]
System,
List(Vec<String>),
}
impl Serialize for Proxies {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Proxies::No => serializer.serialize_str(""),
Proxies::System => serializer.serialize_str("system"),
Proxies::List(urls) => serializer.serialize_str(&urls.join(COMMA_STRING)),
}
}
}
impl<'de> Deserialize<'de> for Proxies {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Proxies::from_str(&s))
}
}
impl Proxies {
/// 从字符串创建 Proxies
///
/// # Arguments
/// * `s` - 代理字符串:
/// - "" 或 "no": 不使用代理
/// - "system": 使用系统代理
/// - 其他: 尝试解析为代理列表,无效则返回 System
pub fn from_str(s: &str) -> Self {
match s.trim() {
"" | "no" => Self::No,
"system" => Self::System,
urls => {
let valid_proxies: Vec<String> = urls
.split(',')
.filter_map(|url| {
let trimmed = url.trim();
(!trimmed.is_empty() && Proxy::all(trimmed).is_ok())
.then(|| trimmed.to_string())
})
.collect();
if valid_proxies.is_empty() {
Self::default()
} else {
Self::List(valid_proxies)
}
}
}
}
pub fn get_client(&self) -> Client {
match self {
Proxies::No => Client::builder().no_proxy().build().unwrap(),
Proxies::System => Client::new(),
Proxies::List(list) => {
// 使用第一个代理(已经确保是有效的)
let proxy = Proxy::all(list[0].clone()).unwrap();
Client::builder().proxy(proxy).build().unwrap()
}
}
}
}

View File

@@ -1,7 +1,10 @@
use crate::chat::constant::AVAILABLE_MODELS;
use crate::{
app::constant::{COMMA, COMMA_STRING},
chat::{config::key_config, constant::AVAILABLE_MODELS},
};
use serde::{Deserialize, Serialize};
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub enum UsageCheck {
None,
Default,
@@ -9,6 +12,52 @@ pub enum UsageCheck {
Custom(Vec<&'static str>),
}
impl UsageCheck {
pub fn from_proto(model: Option<&key_config::UsageCheckModel>) -> Option<Self> {
model.map(|model| {
use key_config::usage_check_model::Type;
match Type::try_from(model.r#type).unwrap_or(Type::Default) {
Type::Default | Type::Disabled => Self::None,
Type::All => Self::All,
Type::Custom => {
let models: Vec<&'static str> = model
.model_ids
.iter()
.filter_map(|id| AVAILABLE_MODELS.iter().find(|m| m.id == id).map(|m| m.id))
.collect();
if models.is_empty() {
Self::None
} else {
Self::Custom(models)
}
}
}
})
}
// pub fn to_proto(&self) -> key_config::UsageCheckModel {
// use key_config::usage_check_model::Type;
// match self {
// Self::None => key_config::UsageCheckModel {
// r#type: Type::Disabled.into(),
// model_ids: vec![],
// },
// Self::Default => key_config::UsageCheckModel {
// r#type: Type::Default.into(),
// model_ids: vec![],
// },
// Self::All => key_config::UsageCheckModel {
// r#type: Type::All.into(),
// model_ids: vec![],
// },
// Self::Custom(models) => key_config::UsageCheckModel {
// r#type: Type::Custom.into(),
// model_ids: models.iter().map(|&s| s.to_string()).collect(),
// },
// }
// }
}
impl Default for UsageCheck {
fn default() -> Self {
Self::Default
@@ -34,7 +83,7 @@ impl Serialize for UsageCheck {
}
UsageCheck::Custom(models) => {
state.serialize_field("type", "list")?;
state.serialize_field("content", &models.join(","))?;
state.serialize_field("content", &models.join(COMMA_STRING))?;
}
}
state.end()
@@ -70,7 +119,7 @@ impl<'de> Deserialize<'de> for UsageCheck {
}
let models: Vec<&'static str> = list
.split(',')
.split(COMMA)
.filter_map(|model| {
let model = model.trim();
AVAILABLE_MODELS
@@ -89,3 +138,34 @@ impl<'de> Deserialize<'de> for UsageCheck {
})
}
}
impl UsageCheck {
pub fn from_str(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"none" | "disabled" => Self::None,
"default" => Self::Default,
"all" | "everything" => Self::All,
list => {
if list.is_empty() {
return Self::default();
}
let models: Vec<&'static str> = list
.split(COMMA)
.filter_map(|model| {
let model = model.trim();
AVAILABLE_MODELS
.iter()
.find(|m| m.id == model)
.map(|m| m.id)
})
.collect();
if models.is_empty() {
Self::default()
} else {
Self::Custom(models)
}
}
}
}
}

View File

@@ -1,7 +1,9 @@
pub mod adapter;
pub mod aiserver;
pub mod config;
pub mod constant;
pub mod error;
// pub mod middleware;
pub mod model;
pub mod route;
pub mod service;

View File

@@ -1,23 +1,31 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use image::guess_format;
use prost::Message as _;
use reqwest::Client;
use uuid::Uuid;
use crate::app::{
constant::EMPTY_STRING,
lazy::DEFAULT_INSTRUCTIONS,
model::{AppConfig, VisionAbility},
use crate::{
app::{
constant::EMPTY_STRING,
lazy::DEFAULT_INSTRUCTIONS,
model::{AppConfig, VisionAbility},
},
common::client::HTTP_CLIENT,
};
use super::{
aiserver::v1::{
conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext, GetChatRequest, ImageProto, ModelDetails
conversation_message, image_proto, AzureState, ConversationMessage, ExplicitContext,
GetChatRequest, ImageProto, ModelDetails,
},
constant::{ERR_UNSUPPORTED_GIF, ERR_UNSUPPORTED_IMAGE_FORMAT, LONG_CONTEXT_MODELS},
model::{Message, MessageContent, Role},
};
async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationMessage>) {
async fn process_chat_inputs(
inputs: Vec<Message>,
disable_vision: bool,
) -> (String, Vec<ConversationMessage>) {
// 收集 system 指令
let instructions = inputs
.iter()
@@ -155,15 +163,19 @@ async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationM
}
}
"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,
if !disable_vision {
if let Some(image_url) = &content.image_url {
let url = image_url.url.clone();
let client = HTTP_CLIENT.read().clone();
let result = tokio::spawn(async move {
fetch_image_data(&url, client).await
});
if let Ok(Ok((image_data, dimensions))) = result.await {
images.push(ImageProto {
data: image_data,
dimension: dimensions,
});
}
}
}
}
@@ -219,6 +231,7 @@ async fn process_chat_inputs(inputs: Vec<Message>) -> (String, Vec<ConversationM
async fn fetch_image_data(
url: &str,
client: Client,
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁
let vision_ability = AppConfig::get_vision_ability();
@@ -237,7 +250,7 @@ async fn fetch_image_data(
if url.starts_with("data:image/") {
process_base64_image(url)
} else {
process_http_image(url).await
process_http_image(url, client).await
}
}
}
@@ -290,8 +303,9 @@ fn process_base64_image(
// 处理 HTTP 图片 URL
async fn process_http_image(
url: &str,
client: Client,
) -> Result<(Vec<u8>, Option<image_proto::Dimension>), Box<dyn std::error::Error + Send + Sync>> {
let response = reqwest::get(url).await?;
let response = client.get(url).send().await?;
let image_data = response.bytes().await?.to_vec();
let format = guess_format(&image_data)?;
@@ -328,17 +342,19 @@ async fn process_http_image(
pub async fn encode_chat_message(
inputs: Vec<Message>,
model_name: &str,
disable_vision: bool,
enable_slow_pool: bool,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
// 在进入异步操作前获取并释放锁
let enable_slow_pool = {
if AppConfig::get_slow_pool() {
if enable_slow_pool {
Some(true)
} else {
None
}
};
let (instructions, messages) = process_chat_inputs(inputs).await;
let (instructions, messages) = process_chat_inputs(inputs, disable_vision).await;
let explicit_context = if !instructions.trim().is_empty() {
Some(ExplicitContext {

View File

@@ -1 +1,57 @@
include!(concat!(env!("OUT_DIR"), "/aiserver.v1.rs"));
use error_details::Error;
impl ErrorDetails {
pub fn status_code(&self) -> u16 {
match Error::try_from(self.error) {
Ok(error) => match error {
Error::Unspecified => 500,
Error::BadApiKey
| Error::InvalidAuthId
| Error::AuthTokenNotFound
| Error::AuthTokenExpired
| Error::Unauthorized => 401,
Error::NotLoggedIn
| Error::NotHighEnoughPermissions
| Error::AgentRequiresLogin
| Error::ProUserOnly
| Error::TaskNoPermissions => 403,
Error::NotFound
| Error::UserNotFound
| Error::TaskUuidNotFound
| Error::AgentEngineNotFound
| Error::GitgraphNotFound
| Error::FileNotFound => 404,
Error::FreeUserRateLimitExceeded
| Error::ProUserRateLimitExceeded
| Error::OpenaiRateLimitExceeded
| Error::OpenaiAccountLimitExceeded
| Error::GenericRateLimitExceeded
| Error::Gpt4VisionPreviewRateLimit
| Error::ApiKeyRateLimit => 429,
Error::BadRequest
| Error::BadModelName
| Error::SlashEditFileTooLong
| Error::FileUnsupported
| Error::ClaudeImageTooLarge => 400,
Error::Deprecated
| Error::FreeUserUsageLimit
| Error::ProUserUsageLimit
| Error::ResourceExhausted
| Error::Openai
| Error::MaxTokens
| Error::ApiKeyNotSupported
| Error::UserAbortedRequest
| Error::CustomMessage
| Error::OutdatedClient
| Error::Debounced
| Error::RepositoryServiceRepositoryIsNotInitialized => 500,
},
Err(_) => 500,
}
}
// pub fn is_expected(&self) -> bool {
// self.is_expected.unwrap_or_default()
// }
}

34
src/chat/config.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::AppConfig;
include!(concat!(env!("OUT_DIR"), "/key.rs"));
impl KeyConfig {
pub fn new_with_global() -> Self {
Self {
auth_token: None,
enable_stream_check: Some(AppConfig::get_stream_check()),
include_stop_stream: Some(AppConfig::get_stop_stream()),
disable_vision: Some(AppConfig::get_vision_ability().is_none()),
enable_slow_pool: Some(AppConfig::get_slow_pool()),
usage_check_models: None,
}
}
pub fn copy_without_auth_token(&self, config: &mut Self) {
if self.enable_stream_check.is_some() {
config.enable_stream_check = self.enable_stream_check;
}
if self.include_stop_stream.is_some() {
config.include_stop_stream = self.include_stop_stream;
}
if self.disable_vision.is_some() {
config.disable_vision = self.disable_vision;
}
if self.enable_slow_pool.is_some() {
config.enable_slow_pool = self.enable_slow_pool;
}
if self.usage_check_models.is_some() {
config.usage_check_models = self.usage_check_models.clone();
}
}
}

49
src/chat/config/key.proto Normal file
View File

@@ -0,0 +1,49 @@
syntax = "proto3";
package key;
// 动态配置的 API KEY
message KeyConfig {
// 认证令牌信息
message TokenInfo {
string sub = 1; // 用户标识符
int64 exp = 2; // 过期时间Unix 时间戳)
string randomness = 3; // 随机字符串
string signature = 4; // 签名
bytes machine_id = 5; // 机器ID的SHA256哈希值
bytes mac_id = 6; // MAC地址的SHA256哈希值
}
// 认证令牌(必需)
TokenInfo auth_token = 1;
// 是否启用流检查
optional bool enable_stream_check = 2;
// 是否包含停止流
optional bool include_stop_stream = 3;
// 是否禁用图片处理能力
optional bool disable_vision = 4;
// 是否启用慢速池
optional bool enable_slow_pool = 5;
// 使用量检查模型规则
message UsageCheckModel {
// 检查类型
enum Type {
TYPE_DEFAULT = 0; // 未指定
TYPE_DISABLED = 1; // 禁用
TYPE_ALL = 2; // 全部
TYPE_CUSTOM = 3; // 自定义列表
}
Type type = 1; // 检查类型
repeated string model_ids = 2; // 模型 ID 列表,当 type 为 TYPE_CUSTOM 时生效
}
// 使用量检查模型规则
optional UsageCheckModel usage_check_models = 6;
// 密码SHA256哈希值
// bytes secret = 2;
}

View File

@@ -16,6 +16,7 @@ def_pub_const!(ANTHROPIC, "anthropic");
def_pub_const!(CURSOR, "cursor");
def_pub_const!(GOOGLE, "google");
def_pub_const!(OPENAI, "openai");
def_pub_const!(DEEPSEEK, "deepseek");
def_pub_const!(CLAUDE_3_5_SONNET, "claude-3.5-sonnet");
def_pub_const!(GPT_4, "gpt-4");
@@ -41,8 +42,10 @@ def_pub_const!(
"gemini-2.0-flash-thinking-exp"
);
def_pub_const!(GEMINI_2_0_FLASH_EXP, "gemini-2.0-flash-exp");
def_pub_const!(DEEPSEEK_V3, "deepseek-v3");
def_pub_const!(DEEPSEEK_R1, "deepseek-r1");
pub const AVAILABLE_MODELS: [Model; 21] = [
pub const AVAILABLE_MODELS: [Model; 23] = [
Model {
id: CLAUDE_3_5_SONNET,
created: CREATED,
@@ -169,6 +172,18 @@ pub const AVAILABLE_MODELS: [Model; 21] = [
object: MODEL_OBJECT,
owned_by: GOOGLE,
},
Model {
id: DEEPSEEK_V3,
created: CREATED,
object: MODEL_OBJECT,
owned_by: DEEPSEEK,
},
Model {
id: DEEPSEEK_R1,
created: CREATED,
object: MODEL_OBJECT,
owned_by: DEEPSEEK,
},
];
pub const USAGE_CHECK_MODELS: [&str; 11] = [
@@ -191,3 +206,5 @@ pub const LONG_CONTEXT_MODELS: [&str; 4] = [
CLAUDE_3_HAIKU_200K,
CLAUDE_3_5_SONNET_200K,
];
// include!("constant/models.rs");

118
src/chat/constant/models.rs Normal file
View File

@@ -0,0 +1,118 @@
pub struct DefaultModel {
pub default_on: bool,
pub is_long_context_only: Option<bool>,
pub name: &'static str,
}
pub const AVAILABLE_MODELS2: [DefaultModel; 22] = [
DefaultModel {
default_on: true,
is_long_context_only: Some(false),
name: CLAUDE_3_5_SONNET,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GPT_4,
},
DefaultModel {
default_on: true,
is_long_context_only: None,
name: GPT_4O,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: CLAUDE_3_OPUS,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: CURSOR_FAST,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: CURSOR_SMALL,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GPT_3_5_TURBO,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GPT_4_TURBO_2024_04_09,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(true),
name: GPT_4O_128K,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(true),
name: GEMINI_1_5_FLASH_500K,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(true),
name: CLAUDE_3_HAIKU_200K,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(true),
name: CLAUDE_3_5_SONNET_200K,
},
DefaultModel {
default_on: false,
is_long_context_only: Some(false),
name: CLAUDE_3_5_SONNET_20241022,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(false),
name: GPT_4O_MINI,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(false),
name: O1_MINI,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(false),
name: O1_PREVIEW,
},
DefaultModel {
default_on: true,
is_long_context_only: Some(false),
name: O1,
},
DefaultModel {
default_on: false,
is_long_context_only: Some(false),
name: CLAUDE_3_5_HAIKU,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GEMINI_EXP_1206,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GEMINI_2_0_FLASH_THINKING_EXP,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: GEMINI_2_0_FLASH_EXP,
},
DefaultModel {
default_on: false,
is_long_context_only: None,
name: DEEPSEEK_V3,
},
];

View File

@@ -1,4 +1,7 @@
use super::aiserver::v1::error_details::Error as ErrorType;
use super::aiserver::v1::ErrorDetails;
use crate::common::model::{ApiStatus, ErrorResponse as CommonErrorResponse};
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use prost::Message as _;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
@@ -18,30 +21,28 @@ pub struct ErrorBody {
pub struct ErrorDetail {
// #[serde(rename = "type")]
// error_type: String, always: aiserver.v1.ErrorDetails
debug: ErrorDebug,
// debug: ErrorDebug,
value: String,
}
#[derive(Deserialize)]
pub struct ErrorDebug {
error: String,
details: ErrorDetails,
// #[serde(rename = "isExpected")]
// is_expected: Option<bool>,
}
// #[derive(Deserialize)]
// pub struct ErrorDebug {
// error: String,
// details: ErrorDetails,
// // #[serde(rename = "isExpected")]
// // is_expected: Option<bool>,
// }
#[derive(Deserialize)]
pub struct ErrorDetails {
title: String,
detail: String,
// #[serde(rename = "isRetryable")]
// is_retryable: Option<bool>,
}
use crate::common::models::{ApiStatus, ErrorResponse as CommonErrorResponse};
// #[derive(Deserialize)]
// pub struct ErrorDetails {
// title: String,
// detail: String,
// // #[serde(rename = "isRetryable")]
// // is_retryable: Option<bool>,
// }
impl ChatError {
pub fn to_error_response(&self) -> ErrorResponse {
pub fn to_error_response(self) -> ErrorResponse {
if self.error.details.is_empty() {
return ErrorResponse {
status: 500,
@@ -49,69 +50,31 @@ impl ChatError {
error: None,
};
}
let error_details = self.error.details.first().and_then(|detail| {
STANDARD_NO_PAD
.decode(&detail.value)
.ok()
.map(bytes::Bytes::from)
.and_then(|buf| ErrorDetails::decode(buf).ok())
});
let status = error_details
.as_ref()
.map(|details| details.status_code())
.unwrap_or(500);
ErrorResponse {
status: self.status_code(),
code: self.error.code.clone(),
error: Some(Error {
message: self.error.details[0].debug.details.title.clone(),
details: self.error.details[0].debug.details.detail.clone(),
value: self.error.details[0].value.clone(),
}),
status,
code: self.error.code,
error: error_details
.and_then(|details| details.details)
.map(|custom_details| Error {
message: custom_details.title,
details: custom_details.detail,
}),
}
}
pub fn status_code(&self) -> u16 {
match ErrorType::from_str_name(&self.error.details[0].debug.error) {
Some(error) => match error {
ErrorType::Unspecified => 500,
ErrorType::BadApiKey
| ErrorType::InvalidAuthId
| ErrorType::AuthTokenNotFound
| ErrorType::AuthTokenExpired
| ErrorType::Unauthorized => 401,
ErrorType::NotLoggedIn
| ErrorType::NotHighEnoughPermissions
| ErrorType::AgentRequiresLogin
| ErrorType::ProUserOnly
| ErrorType::TaskNoPermissions => 403,
ErrorType::NotFound
| ErrorType::UserNotFound
| ErrorType::TaskUuidNotFound
| ErrorType::AgentEngineNotFound
| ErrorType::GitgraphNotFound
| ErrorType::FileNotFound => 404,
ErrorType::FreeUserRateLimitExceeded
| ErrorType::ProUserRateLimitExceeded
| ErrorType::OpenaiRateLimitExceeded
| ErrorType::OpenaiAccountLimitExceeded
| ErrorType::GenericRateLimitExceeded
| ErrorType::Gpt4VisionPreviewRateLimit
| ErrorType::ApiKeyRateLimit => 429,
ErrorType::BadRequest
| ErrorType::BadModelName
| ErrorType::SlashEditFileTooLong
| ErrorType::FileUnsupported
| ErrorType::ClaudeImageTooLarge => 400,
ErrorType::Deprecated
| ErrorType::FreeUserUsageLimit
| ErrorType::ProUserUsageLimit
| ErrorType::ResourceExhausted
| ErrorType::Openai
| ErrorType::MaxTokens
| ErrorType::ApiKeyNotSupported
| ErrorType::UserAbortedRequest
| ErrorType::CustomMessage
| ErrorType::OutdatedClient
| ErrorType::Debounced
| ErrorType::RepositoryServiceRepositoryIsNotInitialized => 500,
},
None => 500,
}
}
// pub fn is_expected(&self) -> bool {
// self.error.details[0].debug.is_expected.unwrap_or_default()
// }
}
#[derive(Serialize)]
@@ -126,7 +89,7 @@ pub struct ErrorResponse {
pub struct Error {
pub message: String,
pub details: String,
pub value: String,
// pub value: String,
}
impl ErrorResponse {
@@ -135,18 +98,25 @@ impl ErrorResponse {
// }
pub fn status_code(&self) -> StatusCode {
StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
StatusCode::from_u16(self.status).unwrap()
}
pub fn native_code(&self) -> String {
self.code.replace("_", " ")
self.error.as_ref().map_or_else(
|| self.code.replace("_", " "),
|error| error.message.clone(),
)
}
pub fn to_common(self) -> CommonErrorResponse {
CommonErrorResponse {
status: ApiStatus::Error,
code: Some(self.status),
error: self.error.as_ref().map(|error| error.message.clone()).or(Some(self.code.clone())),
error: self
.error
.as_ref()
.map(|error| error.message.clone())
.or(Some(self.code.clone())),
message: self.error.as_ref().map(|error| error.details.clone()),
}
}
@@ -161,7 +131,7 @@ pub enum StreamError {
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamError::ChatError(error) => write!(f, "{}", error.error.details[0].debug.details.title),
StreamError::ChatError(error) => write!(f, "{}", error.error.code),
StreamError::DataLengthLessThan5 => write!(f, "data length less than 5"),
StreamError::EmptyMessage => write!(f, "empty message"),
}

2
src/chat/middleware.rs Normal file
View File

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

View File

@@ -0,0 +1,23 @@
use crate::app::{constant::AUTHORIZATION_BEARER_PREFIX, lazy::AUTH_TOKEN};
use axum::{
body::Body,
http::{header::AUTHORIZATION, Request, StatusCode},
middleware::Next,
response::Response,
};
// 认证中间件函数
pub async fn auth_middleware(request: Request<Body>, next: Next) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
Ok(next.run(request).await)
}

View File

@@ -90,8 +90,8 @@ use crate::app::model::{AppConfig, UsageCheck};
use super::constant::USAGE_CHECK_MODELS;
impl Model {
pub fn is_usage_check(&self) -> bool {
match AppConfig::get_usage_check() {
pub fn is_usage_check(&self, usage_check: Option<UsageCheck>) -> bool {
match usage_check.unwrap_or(AppConfig::get_usage_check()) {
UsageCheck::None => false,
UsageCheck::Default => USAGE_CHECK_MODELS.contains(&self.id),
UsageCheck::All => true,

View File

@@ -2,17 +2,18 @@ mod logs;
pub use logs::{handle_logs, handle_logs_post};
mod health;
pub use health::{handle_health, handle_root};
mod token;
pub use token::{
handle_basic_calibration, handle_get_checksum, handle_get_hash, handle_get_timestamp_header,
handle_get_tokeninfo, handle_tokeninfo_page, handle_update_tokeninfo,
handle_update_tokeninfo_post,
mod tokens;
pub use tokens::{
handle_add_tokens, handle_basic_calibration, handle_delete_tokens, handle_get_checksum,
handle_get_hash, handle_get_timestamp_header, handle_get_tokens, handle_reload_tokens,
handle_tokens_page, handle_update_tokens,
};
mod profile;
pub use profile::handle_user_info;
mod config;
pub use config::{
handle_about, handle_config_page, handle_env_example, handle_readme, handle_static,
handle_about, handle_build_key, handle_build_key_page, handle_config_page, handle_env_example,
handle_readme, handle_static,
};
mod api;
pub use api::handle_api_page;

View File

@@ -1,20 +1,25 @@
use crate::app::{
constant::{
CONTENT_TYPE_TEXT_CSS_WITH_UTF8, CONTENT_TYPE_TEXT_HTML_WITH_UTF8,
CONTENT_TYPE_TEXT_JS_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH,
ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH,
use crate::{
app::{
constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_CSS_WITH_UTF8, CONTENT_TYPE_TEXT_HTML_WITH_UTF8, CONTENT_TYPE_TEXT_JS_WITH_UTF8, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_ABOUT_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_README_PATH, ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH
},
lazy::{AUTH_TOKEN, KEY_PREFIX},
model::{AppConfig, BuildKeyRequest, BuildKeyResponse, PageContent, UsageCheckModelType},
},
model::{AppConfig, PageContent},
chat::config::{key_config, KeyConfig},
common::utils::{to_base64, token_to_tokeninfo},
};
use axum::{
body::Body,
extract::Path,
http::{
header::{CONTENT_TYPE, LOCATION},
StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE, LOCATION},
HeaderMap, StatusCode,
},
response::{IntoResponse, Response},
Json,
};
use prost::Message as _;
pub async fn handle_env_example() -> impl IntoResponse {
Response::builder()
@@ -108,3 +113,93 @@ pub async fn handle_about() -> impl IntoResponse {
.unwrap(),
}
}
pub async fn handle_build_key_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_BUILD_KEY_PATH).unwrap_or_default() {
PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(include_str!("../../../static/build_key.min.html").to_string())
.unwrap(),
PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone())
.unwrap(),
PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone())
.unwrap(),
}
}
pub async fn handle_build_key(
headers: HeaderMap,
Json(request): Json<BuildKeyRequest>,
) -> (StatusCode, Json<BuildKeyResponse>) {
// 验证认证令牌
if AppConfig::is_share() {
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX));
if auth_header.map_or(true, |h| h != AppConfig::get_share_token().as_str() && h != AUTH_TOKEN.as_str()) {
return (
StatusCode::UNAUTHORIZED,
Json(BuildKeyResponse::Error("Unauthorized".to_owned())),
);
}
}
// 验证并解析 auth_token
let token_info = match token_to_tokeninfo(&request.auth_token) {
Some(info) => info,
None => {
return (
StatusCode::BAD_REQUEST,
Json(BuildKeyResponse::Error("Invalid auth token".to_owned())),
)
}
};
// 构建 proto 消息
let mut key_config = KeyConfig {
auth_token: Some(token_info),
enable_stream_check: request.enable_stream_check,
include_stop_stream: request.include_stop_stream,
disable_vision: request.disable_vision,
enable_slow_pool: request.enable_slow_pool,
usage_check_models: None,
};
if let Some(usage_check_models) = request.usage_check_models {
let usage_check = key_config::UsageCheckModel {
r#type: match usage_check_models.model_type {
UsageCheckModelType::Default => {
key_config::usage_check_model::Type::Default as i32
}
UsageCheckModelType::Disabled => {
key_config::usage_check_model::Type::Disabled as i32
}
UsageCheckModelType::All => key_config::usage_check_model::Type::All as i32,
UsageCheckModelType::Custom => key_config::usage_check_model::Type::Custom as i32,
},
model_ids: if matches!(usage_check_models.model_type, UsageCheckModelType::Custom) {
usage_check_models
.model_ids
.iter()
.map(|s| s.to_string())
.collect()
} else {
Vec::new()
},
};
key_config.usage_check_models = Some(usage_check);
}
// 序列化
let encoded = key_config.encode_to_vec();
let key = format!("{}{}", *KEY_PREFIX, to_base64(&encoded));
(StatusCode::OK, Json(BuildKeyResponse::Key(key)))
}

View File

@@ -3,17 +3,18 @@ use crate::{
constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH,
ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH,
ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER,
ROUTE_GET_TOKENINFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH,
ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH,
ROUTE_BASIC_CALIBRATION_PATH, ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH,
ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER,
ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH, ROUTE_TOKENS_DELETE_PATH,
ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH, ROUTE_TOKENS_UPDATE_PATH,
ROUTE_USER_INFO_PATH,
},
lazy::{get_start_time, AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
model::{AppConfig, AppState, PageContent},
},
chat::constant::AVAILABLE_MODELS,
common::models::{
common::model::{
health::{CpuInfo, HealthCheckResponse, MemoryInfo, SystemInfo, SystemStats},
ApiStatus,
},
@@ -116,9 +117,11 @@ pub async fn handle_health(
endpoints: vec![
ROUTE_CHAT_PATH.as_str(),
ROUTE_MODELS_PATH.as_str(),
ROUTE_TOKENINFO_PATH,
ROUTE_UPDATE_TOKENINFO_PATH,
ROUTE_GET_TOKENINFO_PATH,
ROUTE_TOKENS_PATH,
ROUTE_TOKENS_GET_PATH,
ROUTE_TOKENS_UPDATE_PATH,
ROUTE_TOKENS_ADD_PATH,
ROUTE_TOKENS_DELETE_PATH,
ROUTE_LOGS_PATH,
ROUTE_ENV_EXAMPLE_PATH,
ROUTE_CONFIG_PATH,
@@ -131,6 +134,7 @@ pub async fn handle_health(
ROUTE_GET_TIMESTAMP_HEADER,
ROUTE_BASIC_CALIBRATION_PATH,
ROUTE_USER_INFO_PATH,
ROUTE_BUILD_KEY_PATH,
],
})
}

View File

@@ -7,7 +7,7 @@ use crate::{
lazy::AUTH_TOKEN,
model::{AppConfig, AppState, PageContent, RequestLog},
},
common::{models::ApiStatus, utils::extract_token},
common::{model::ApiStatus, utils::extract_token},
};
use axum::{
body::Body,

View File

@@ -1,10 +1,10 @@
use crate::{
chat::constant::ERR_NODATA,
common::{models::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}},
common::{model::userinfo::GetUserInfo, utils::{extract_token, get_token_profile}},
};
use axum::Json;
use super::token::TokenRequest;
use super::tokens::TokenRequest;
pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> {
let auth_token = match request.token {

View File

@@ -1,276 +0,0 @@
use crate::{
app::{
constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENINFO_PATH,
},
lazy::{AUTH_TOKEN, TOKEN_FILE, TOKEN_LIST_FILE},
model::{AppConfig, AppState, PageContent, TokenUpdateRequest},
},
common::{
models::{ApiStatus, NormalResponseNoData},
utils::{
extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens, validate_token_and_checksum
},
},
};
use axum::{
extract::{Query, State},
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap,
},
response::{IntoResponse, Response},
Json,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
pub async fn handle_get_hash() -> Response {
let hash = generate_hash();
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, hash).into_response()
}
#[derive(Deserialize)]
pub struct ChecksumQuery {
#[serde(default)]
pub checksum: Option<String>,
}
pub async fn handle_get_checksum(Query(query): Query<ChecksumQuery>) -> Response {
let checksum = match query.checksum {
None => generate_checksum_with_default(),
Some(checksum) => generate_checksum_with_repair(&checksum),
};
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, checksum).into_response()
}
pub async fn handle_get_timestamp_header() -> Response {
let timestamp_header = generate_timestamp_header();
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, timestamp_header).into_response()
}
// 更新 TokenInfo 处理
pub async fn handle_update_tokeninfo(
State(state): State<Arc<Mutex<AppState>>>,
) -> Json<NormalResponseNoData> {
// 重新加载 tokens
let token_infos = load_tokens();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Json(NormalResponseNoData {
status: ApiStatus::Success,
message: Some("Token list has been reloaded".to_string()),
})
}
// 获取 TokenInfo 处理
pub async fn handle_get_tokeninfo(
headers: HeaderMap,
) -> Result<Json<TokenInfoResponse>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
let token_file = TOKEN_FILE.as_str();
let token_list_file = TOKEN_LIST_FILE.as_str();
// 读取文件内容
let tokens = std::fs::read_to_string(&token_file).unwrap_or_else(|_| String::new());
let token_list = std::fs::read_to_string(&token_list_file).unwrap_or_else(|_| String::new());
// 获取 tokens_count
let tokens_count = {
{
tokens.len()
}
};
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
token_file: token_file.to_string(),
token_list_file: token_list_file.to_string(),
tokens: Some(tokens),
tokens_count: Some(tokens_count),
token_list: Some(token_list),
message: None,
}))
}
#[derive(Serialize)]
pub struct TokenInfoResponse {
pub status: ApiStatus,
pub token_file: String,
pub token_list_file: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_list: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
pub async fn handle_update_tokeninfo_post(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<TokenUpdateRequest>,
) -> Result<Json<TokenInfoResponse>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
let token_file = TOKEN_FILE.as_str();
let token_list_file = TOKEN_LIST_FILE.as_str();
// 写入文件
std::fs::write(&token_file, &request.tokens).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if let Some(token_list) = &request.token_list {
std::fs::write(&token_list_file, token_list)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// 重新加载 tokens
let token_infos = load_tokens();
let token_infos_len = token_infos.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
token_file: token_file.to_string(),
token_list_file: token_list_file.to_string(),
tokens: None,
tokens_count: Some(token_infos_len),
token_list: None,
message: Some("Token files have been updated and reloaded".to_string()),
}))
}
pub async fn handle_tokeninfo_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_TOKENINFO_PATH).unwrap_or_default() {
PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(include_str!("../../../static/tokeninfo.min.html").to_string())
.unwrap(),
PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone())
.unwrap(),
PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone())
.unwrap(),
}
}
#[derive(Deserialize)]
pub struct TokenRequest {
pub token: Option<String>,
}
#[derive(Serialize)]
pub struct BasicCalibrationResponse {
pub status: ApiStatus,
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_time: Option<u64>,
}
pub async fn handle_basic_calibration(
Json(request): Json<TokenRequest>,
) -> Json<BasicCalibrationResponse> {
// 从请求头中获取并验证 auth token
let auth_token = match request.token {
Some(token) => token,
None => {
return Json(BasicCalibrationResponse {
status: ApiStatus::Error,
message: Some("未提供授权令牌".to_string()),
user_id: None,
create_at: None,
checksum_time: None,
})
}
};
// 校验 token 和 checksum
let (token, checksum) = match validate_token_and_checksum(&auth_token) {
Some(parts) => parts,
None => {
return Json(BasicCalibrationResponse {
status: ApiStatus::Error,
message: Some("无效令牌或无效校验和".to_string()),
user_id: None,
create_at: None,
checksum_time: None,
})
}
};
// 提取用户ID和创建时间
let user_id = extract_user_id(&token);
let create_at = extract_time(&token).map(|dt| dt.to_string());
let checksum_time = extract_time_ks(&checksum[..8]);
// 返回校验结果
Json(BasicCalibrationResponse {
status: ApiStatus::Success,
message: Some("校验成功".to_string()),
user_id,
create_at,
checksum_time,
})
}

481
src/chat/route/tokens.rs Normal file
View File

@@ -0,0 +1,481 @@
use crate::{
app::{
constant::{
AUTHORIZATION_BEARER_PREFIX, CONTENT_TYPE_TEXT_HTML_WITH_UTF8,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8, ROUTE_TOKENS_PATH,
},
lazy::{AUTH_TOKEN, TOKEN_LIST_FILE},
model::{
AppConfig, AppState, PageContent, TokenAddRequestTokenInfo, TokenInfo,
TokenUpdateRequest, TokensDeleteRequest, TokensDeleteResponse,
},
},
common::{
model::{error::ChatError, ApiStatus, ErrorResponse},
utils::{
extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default,
generate_checksum_with_repair, generate_hash, generate_timestamp_header, load_tokens,
parse_token, validate_token, validate_token_and_checksum, write_tokens,
},
},
};
use axum::{
extract::{Query, State},
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap,
},
response::{IntoResponse, Response},
Json,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
pub async fn handle_get_hash() -> Response {
let hash = generate_hash();
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, hash).into_response()
}
#[derive(Deserialize)]
pub struct ChecksumQuery {
#[serde(default)]
pub checksum: Option<String>,
}
pub async fn handle_get_checksum(Query(query): Query<ChecksumQuery>) -> Response {
let checksum = match query.checksum {
None => generate_checksum_with_default(),
Some(checksum) => generate_checksum_with_repair(&checksum),
};
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, checksum).into_response()
}
pub async fn handle_get_timestamp_header() -> Response {
let timestamp_header = generate_timestamp_header();
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8.parse().unwrap(),
);
(headers, timestamp_header).into_response()
}
pub async fn handle_get_tokens(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
) -> Result<Json<TokenInfoResponse>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
let tokens = state.lock().await.token_infos.clone();
let tokens_count = tokens.len();
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
tokens: Some(tokens),
tokens_count,
message: None,
}))
}
#[derive(Serialize)]
pub struct TokenInfoResponse {
pub status: ApiStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens: Option<Vec<TokenInfo>>,
pub tokens_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
pub async fn handle_reload_tokens(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
) -> Result<Json<TokenInfoResponse>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
// 重新加载 tokens
let tokens = load_tokens();
let tokens_count = tokens.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = tokens;
}
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
tokens: None,
tokens_count,
message: Some("Token list has been reloaded".to_string()),
}))
}
pub async fn handle_update_tokens(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<TokenUpdateRequest>,
) -> Result<Json<TokenInfoResponse>, StatusCode> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or(StatusCode::UNAUTHORIZED)?;
if auth_header != AUTH_TOKEN.as_str() {
return Err(StatusCode::UNAUTHORIZED);
}
let token_list_file = TOKEN_LIST_FILE.as_str();
std::fs::write(&token_list_file, &request.tokens)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// 重新加载 tokens
let token_infos = load_tokens();
let tokens_count = token_infos.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
tokens: None,
tokens_count,
message: Some("Token files have been updated and reloaded".to_string()),
}))
}
pub async fn handle_add_tokens(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<Vec<TokenAddRequestTokenInfo>>,
) -> Result<Json<TokenInfoResponse>, (StatusCode, Json<ErrorResponse>)> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))?;
if auth_header != AUTH_TOKEN.as_str() {
return Err((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
));
}
let token_list_file = TOKEN_LIST_FILE.as_str();
// 获取当前的 tokens 并创建新的 token_infos
let mut token_infos = {
let state = state.lock().await;
state.token_infos.clone()
};
// 创建现有token的集合
let existing_tokens: std::collections::HashSet<_> =
token_infos.iter().map(|info| info.token.as_str()).collect();
// 预分配容量
let mut new_tokens = Vec::with_capacity(request.len());
// 处理新的tokens
for token_info in request {
let parsed_token = parse_token(&token_info.token);
if !existing_tokens.contains(parsed_token.as_str()) && validate_token(&parsed_token) {
new_tokens.push(TokenInfo {
token: parsed_token,
// 如果提供了checksum就使用提供的否则生成新的
checksum: token_info
.checksum
.as_deref()
.map(generate_checksum_with_repair)
.unwrap_or_else(generate_checksum_with_default),
profile: None,
});
}
}
// 如果有新tokens才进行后续操作
if !new_tokens.is_empty() {
// 预分配足够的容量
token_infos.reserve(new_tokens.len());
token_infos.extend(new_tokens);
// 写入文件
write_tokens(&token_infos, token_list_file).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Error,
code: None,
error: Some("Failed to update token list file".to_string()),
message: Some("无法更新token list文件".to_string()),
}),
)
})?;
// 获取最终的tokens数量在更新状态之前
let tokens_count = token_infos.len();
// 更新应用状态
{
let mut state = state.lock().await;
state.token_infos = token_infos;
}
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
tokens: None,
tokens_count,
message: Some("New tokens have been added and reloaded".to_string()),
}))
} else {
// 如果没有新tokens使用原始数量
let tokens_count = token_infos.len();
Ok(Json(TokenInfoResponse {
status: ApiStatus::Success,
tokens: None,
tokens_count,
message: Some("No new tokens were added".to_string()),
}))
}
}
pub async fn handle_delete_tokens(
State(state): State<Arc<Mutex<AppState>>>,
headers: HeaderMap,
Json(request): Json<TokensDeleteRequest>,
) -> Result<Json<TokensDeleteResponse>, (StatusCode, Json<ErrorResponse>)> {
// 验证 AUTH_TOKEN
let auth_header = headers
.get(AUTHORIZATION)
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix(AUTHORIZATION_BEARER_PREFIX))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))?;
if auth_header != AUTH_TOKEN.as_str() {
return Err((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
));
}
let token_infos = state.lock().await.token_infos.clone();
let original_count = token_infos.len(); // 提前存储原始长度
// 获取token_list文件路径
let token_list_file = TOKEN_LIST_FILE.as_str();
// 创建要删除的tokens的HashSet提高查找效率
let tokens_to_delete: std::collections::HashSet<_> = request.tokens.iter().collect();
// 如果需要的话计算 failed_tokens
let failed_tokens = if request.expectation.needs_failed_tokens() {
Some(
request
.tokens
.iter()
.filter(|token| !token_infos.iter().any(|info| &info.token == *token))
.cloned()
.collect::<Vec<String>>(),
)
} else {
None
};
// 预分配容量并过滤掉要删除的tokens
let estimated_capacity = original_count.saturating_sub(tokens_to_delete.len());
let mut filtered_token_infos = Vec::with_capacity(estimated_capacity);
// 一次性过滤tokens
for info in token_infos {
if !tokens_to_delete.contains(&info.token) {
filtered_token_infos.push(info);
}
}
// 如果有tokens被删除才进行更新操作
if filtered_token_infos.len() < original_count {
// 写入文件
write_tokens(&filtered_token_infos, token_list_file).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
status: ApiStatus::Error,
code: None,
error: Some("Failed to update token list file".to_string()),
message: Some("无法更新token list文件".to_string()),
}),
)
})?;
// 如果需要的话计算 updated_tokens
let updated_tokens = if request.expectation.needs_updated_tokens() {
Some(
filtered_token_infos
.iter()
.map(|info| info.token.clone())
.collect(),
)
} else {
None
};
// 更新状态
{
let mut state = state.lock().await;
state.token_infos = filtered_token_infos;
}
Ok(Json(TokensDeleteResponse {
status: ApiStatus::Success,
updated_tokens,
failed_tokens,
}))
} else {
// 如果没有tokens被删除
Ok(Json(TokensDeleteResponse {
status: ApiStatus::Success,
updated_tokens: if request.expectation.needs_updated_tokens() {
Some(
filtered_token_infos
.iter()
.map(|info| info.token.clone())
.collect(),
)
} else {
None
},
failed_tokens,
}))
}
}
pub async fn handle_tokens_page() -> impl IntoResponse {
match AppConfig::get_page_content(ROUTE_TOKENS_PATH).unwrap_or_default() {
PageContent::Default => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(include_str!("../../../static/tokens.min.html").to_string())
.unwrap(),
PageContent::Text(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN_WITH_UTF8)
.body(content.clone())
.unwrap(),
PageContent::Html(content) => Response::builder()
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
.body(content.clone())
.unwrap(),
}
}
#[derive(Deserialize)]
pub struct TokenRequest {
pub token: Option<String>,
}
#[derive(Serialize)]
pub struct BasicCalibrationResponse {
pub status: ApiStatus,
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_time: Option<u64>,
}
pub async fn handle_basic_calibration(
Json(request): Json<TokenRequest>,
) -> Json<BasicCalibrationResponse> {
// 从请求头中获取并验证 auth token
let auth_token = match request.token {
Some(token) => token,
None => {
return Json(BasicCalibrationResponse {
status: ApiStatus::Error,
message: Some("未提供授权令牌".to_string()),
user_id: None,
create_at: None,
checksum_time: None,
})
}
};
// 校验 token 和 checksum
let (token, checksum) = match validate_token_and_checksum(&auth_token) {
Some(parts) => parts,
None => {
return Json(BasicCalibrationResponse {
status: ApiStatus::Error,
message: Some("无效令牌或无效校验和".to_string()),
user_id: None,
create_at: None,
checksum_time: None,
})
}
};
// 提取用户ID和创建时间
let user_id = extract_user_id(&token);
let create_at = extract_time(&token).map(|dt| dt.to_string());
let checksum_time = extract_time_ks(&checksum[..8]);
// 返回校验结果
Json(BasicCalibrationResponse {
status: ApiStatus::Success,
message: Some("校验成功".to_string()),
user_id,
create_at,
checksum_time,
})
}

View File

@@ -4,10 +4,13 @@ use crate::{
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS,
},
lazy::{AUTH_TOKEN, SHARED_AUTH_TOKEN, USE_SHARE},
model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo},
lazy::{
AUTH_TOKEN, KEY_PREFIX, KEY_PREFIX_LEN, REQUEST_LOGS_LIMIT, SERVICE_TIMEOUT,
},
model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo, UsageCheck},
},
chat::{
config::KeyConfig,
constant::{AVAILABLE_MODELS, USAGE_CHECK_MODELS},
error::StreamError,
model::{
@@ -17,8 +20,11 @@ use crate::{
},
common::{
client::build_client,
models::{error::ChatError, userinfo::MembershipType, ErrorResponse},
utils::{format_time_ms, get_token_profile, validate_token_and_checksum},
model::{error::ChatError, userinfo::MembershipType, ErrorResponse},
utils::{
format_time_ms, from_base64, get_token_profile, tokeninfo_to_token,
validate_token_and_checksum,
},
},
};
use axum::{
@@ -33,6 +39,7 @@ use axum::{
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
use prost::Message as _;
use std::{
convert::Infallible,
sync::{atomic::AtomicBool, Arc},
@@ -44,8 +51,6 @@ use std::{
use tokio::sync::Mutex;
use uuid::Uuid;
const REQUEST_LOGS_LIMIT: usize = 1000;
// 模型列表处理
pub async fn handle_models() -> Json<ModelsResponse> {
Json(ModelsResponse {
@@ -92,10 +97,15 @@ pub async fn handle_chat(
Json(ChatError::Unauthorized.to_json()),
))?;
let mut current_config = KeyConfig::new_with_global();
// 验证认证token并获取token信息
let (auth_token, checksum) = match auth_header {
// 管理员Token验证逻辑
token if token == AUTH_TOKEN.as_str() || (*USE_SHARE && token == SHARED_AUTH_TOKEN.as_str()) => {
token
if token == AUTH_TOKEN.as_str()
|| (AppConfig::is_share() && token == AppConfig::get_share_token().as_str()) =>
{
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
let state_guard = state.lock().await;
let token_infos = &state_guard.token_infos;
@@ -103,7 +113,7 @@ pub async fn handle_chat(
// 检查是否存在可用的token
if token_infos.is_empty() {
return Err((
StatusCode::SERVICE_UNAVAILABLE,
StatusCode::SERVICE_UNAVAILABLE,
Json(ChatError::NoTokens.to_json()),
));
}
@@ -112,7 +122,21 @@ pub async fn handle_chat(
let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
let token_info = &token_infos[index];
(token_info.token.clone(), token_info.checksum.clone())
},
}
token if AppConfig::get_dynamic_key() && token.starts_with(&*KEY_PREFIX) => {
from_base64(&token[*KEY_PREFIX_LEN..])
.and_then(|decoded_bytes| KeyConfig::decode(&decoded_bytes[..]).ok())
.and_then(|key_config| {
key_config.copy_without_auth_token(&mut current_config);
key_config.auth_token
})
.and_then(|token_info| tokeninfo_to_token(&token_info))
.ok_or((
StatusCode::UNAUTHORIZED,
Json(ChatError::Unauthorized.to_json()),
))?
}
// 普通用户Token验证逻辑
token => validate_token_and_checksum(token).ok_or((
@@ -121,6 +145,8 @@ pub async fn handle_chat(
))?,
};
let current_config = current_config;
let current_id: u64;
// 更新请求日志
@@ -172,7 +198,14 @@ pub async fn handle_chat(
current_id = next_id;
// 如果需要获取用户使用情况,创建后台任务获取profile
if model.map(|m| m.is_usage_check()).unwrap_or(false) {
if model
.map(|m| {
m.is_usage_check(UsageCheck::from_proto(
current_config.usage_check_models.as_ref(),
))
})
.unwrap_or(false)
{
let auth_token_clone = auth_token.clone();
let state_clone = state_clone.clone();
let log_id = next_id;
@@ -211,13 +244,19 @@ pub async fn handle_chat(
error: None,
});
if state.request_logs.len() > REQUEST_LOGS_LIMIT {
if state.request_logs.len() > *REQUEST_LOGS_LIMIT {
state.request_logs.remove(0);
}
}
// 将消息转换为hex格式
let hex_data = match super::adapter::encode_chat_message(request.messages, &request.model).await
let hex_data = match super::adapter::encode_chat_message(
request.messages,
&request.model,
current_config.disable_vision(),
current_config.enable_slow_pool(),
)
.await
{
Ok(data) => data,
Err(e) => {
@@ -244,27 +283,55 @@ pub async fn handle_chat(
// 构建请求客户端
let client = build_client(&auth_token, &checksum);
let response = client.body(hex_data).send().await;
// 添加超时设置
let response = tokio::time::timeout(
std::time::Duration::from_secs(*SERVICE_TIMEOUT),
client.body(hex_data).send(),
)
.await;
// 处理请求结果
let response = match response {
Ok(resp) => {
// 更新请求日志为成功
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
Ok(inner_response) => match inner_response {
Ok(resp) => {
// 更新请求日志为成功
{
log.status = STATUS_SUCCESS;
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_SUCCESS;
}
}
resp
}
resp
}
Err(e) => {
// 更新请求日志为失败
Err(e) => {
// 更新请求日志为失败
{
let mut state = state.lock().await;
if let Some(log) = state
.request_logs
.iter_mut()
.rev()
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(e.to_string());
}
state.active_requests -= 1;
state.error_requests += 1;
}
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatError::RequestFailed(e.to_string()).to_json()),
));
}
},
Err(_) => {
// 处理超时错误
{
let mut state = state.lock().await;
if let Some(log) = state
@@ -274,14 +341,14 @@ pub async fn handle_chat(
.find(|log| log.id == current_id)
{
log.status = STATUS_FAILED;
log.error = Some(e.to_string());
log.error = Some("Request timeout".to_string());
}
state.active_requests -= 1;
state.error_requests += 1;
}
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ChatError::RequestFailed(e.to_string()).to_json()),
StatusCode::GATEWAY_TIMEOUT,
Json(ChatError::RequestFailed("Request timeout".to_string()).to_json()),
));
}
};
@@ -303,9 +370,7 @@ pub async fn handle_chat(
// 创建新的 stream
let mut stream = response.bytes_stream();
let enable_stream_check = AppConfig::get_stream_check();
if enable_stream_check {
if current_config.enable_stream_check() {
// 检查第一个 chunk
match stream.next().await {
Some(first_chunk) => {
@@ -399,6 +464,8 @@ pub async fn handle_chat(
let full_text = full_text.clone();
let first_chunk_time = first_chunk_time.clone();
let state = state.clone();
// 根据配置决定是否发送最后的 finish_reason
let include_finish_reason = current_config.include_stop_stream();
async move {
let chunk = chunk.unwrap_or_default();
@@ -484,8 +551,6 @@ pub async fn handle_chat(
}
Ok(StreamMessage::StreamEnd) => {
buffer_guard.clear();
// 根据配置决定是否发送最后的 finish_reason
let include_finish_reason = AppConfig::get_stop_stream();
// 计算总时间和首次片段时间
let total_time = format_time_ms(start_time.elapsed().as_secs_f64());

View File

@@ -91,16 +91,17 @@ pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
// gzip压缩消息
1 => {
if let Some(text) = decompress_gzip(msg_data) {
let response = StreamChatResponse::decode(&text[..]).unwrap_or_default();
// crate::debug_println!("[gzip] StreamChatResponse: {:?}", response);
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[gzip] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
// response.is_using_slow_request,
));
if let Ok(response) = StreamChatResponse::decode(&text[..]) {
// crate::debug_println!("[gzip] StreamChatResponse: {:?}", response);
if !response.text.is_empty() {
messages.push(response.text);
} else {
// println!("[gzip] StreamChatResponse: {:?}", response);
return Ok(StreamMessage::Debug(
response.filled_prompt.unwrap_or_default(),
// response.is_using_slow_request,
));
}
}
}
}
@@ -118,8 +119,27 @@ pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
// messages.push(text);
}
}
// gzip压缩消息
3 => {
if let Some(text) = decompress_gzip(msg_data) {
if text.len() == 2 {
return Ok(StreamMessage::StreamEnd);
}
if let Ok(text) = String::from_utf8(text) {
// println!("JSON消息: {}", text);
if let Ok(error) = serde_json::from_str::<ChatError>(&text) {
return Err(StreamError::ChatError(error));
}
// 未预计
// messages.push(text);
}
}
}
// 其他类型暂不处理
t => eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t),
t => {
eprintln!("收到未知消息类型: {},请尝试联系开发者以获取支持", t);
crate::debug_println!("消息类型: {},消息内容: {}", t, hex::encode(msg_data));
}
}
offset += 5 + msg_len;
@@ -158,7 +178,8 @@ fn test_parse_stream_data() {
// 辅助函数将字节转换为hex字符串
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter()
bytes
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<String>>()
.join("")
@@ -171,7 +192,7 @@ fn test_parse_stream_data() {
let msg_boundary = find_next_message_boundary(remaining_bytes);
let current_msg_bytes = &remaining_bytes[..msg_boundary];
let hex_str = bytes_to_hex(current_msg_bytes);
match parse_stream_data(current_msg_bytes) {
Ok(message) => {
match message {

View File

@@ -1,3 +1,3 @@
pub mod models;
pub mod model;
pub mod utils;
pub mod client;

View File

@@ -1,22 +1,22 @@
use crate::app::{
use super::utils::generate_hash;
use crate::{app::{
constant::{
CONTENT_TYPE_CONNECT_PROTO, CURSOR_API2_HOST, CURSOR_HOST, CURSOR_SETTINGS_URL,
HEADER_NAME_GHOST_MODE, TRUE,
},
lazy::{
CURSOR_API2_CHAT_URL, CURSOR_API2_STRIPE_URL, CURSOR_USAGE_API_URL, CURSOR_USER_API_URL,
REVERSE_PROXY_HOST, USE_PROXY,
REVERSE_PROXY_HOST, USE_REVERSE_PROXY,
},
};
}, AppConfig};
use reqwest::header::{
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, DNT,
HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
};
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE,
DNT, HOST, ORIGIN, PRAGMA, REFERER, TE, TRANSFER_ENCODING, USER_AGENT,
};
use reqwest::{Client, RequestBuilder};
use std::sync::LazyLock;
use uuid::Uuid;
use super::utils::generate_hash;
macro_rules! def_const {
($name:ident, $value:expr) => {
const $name: &'static str = $value;
@@ -44,6 +44,18 @@ def_const!(U_EQ_4, "u=4");
def_const!(PROXY_HOST, "x-co");
pub(crate) static HTTP_CLIENT: LazyLock<parking_lot::RwLock<Client>> =
LazyLock::new(|| parking_lot::RwLock::new(AppConfig::get_proxies().get_client()));
/// 重新构建 HTTP 客户端
///
/// 当需要更新代理设置时,可以调用此方法重新创建客户端
pub fn rebuild_http_client() {
let new_client = AppConfig::get_proxies().get_client();
let mut client = HTTP_CLIENT.write();
*client = new_client;
}
/// 返回预构建的 Cursor API 客户端
///
/// # 参数
@@ -58,13 +70,15 @@ def_const!(PROXY_HOST, "x-co");
pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder {
let trace_id = Uuid::new_v4().to_string();
let client = if *USE_PROXY {
Client::new()
let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT
.read()
.post(&*CURSOR_API2_CHAT_URL)
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_API2_HOST)
} else {
Client::new()
HTTP_CLIENT
.read()
.post(&*CURSOR_API2_CHAT_URL)
.header(HOST, CURSOR_API2_HOST)
};
@@ -96,13 +110,15 @@ pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder {
///
/// * `reqwest::RequestBuilder` - 配置好的请求构建器
pub fn build_profile_client(auth_token: &str) -> RequestBuilder {
let client = if *USE_PROXY {
Client::new()
let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT
.read()
.get(&*CURSOR_API2_STRIPE_URL)
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_API2_HOST)
} else {
Client::new()
HTTP_CLIENT
.read()
.get(&*CURSOR_API2_STRIPE_URL)
.header(HOST, CURSOR_API2_HOST)
};
@@ -140,13 +156,15 @@ pub fn build_profile_client(auth_token: &str) -> RequestBuilder {
pub fn build_usage_client(user_id: &str, auth_token: &str) -> RequestBuilder {
let session_token = format!("{}%3A%3A{}", user_id, auth_token);
let client = if *USE_PROXY {
Client::new()
let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT
.read()
.get(&*CURSOR_USAGE_API_URL)
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_HOST)
} else {
Client::new()
HTTP_CLIENT
.read()
.get(&*CURSOR_USAGE_API_URL)
.header(HOST, CURSOR_HOST)
};
@@ -187,13 +205,15 @@ pub fn build_usage_client(user_id: &str, auth_token: &str) -> RequestBuilder {
pub fn build_userinfo_client(user_id: &str, auth_token: &str) -> RequestBuilder {
let session_token = format!("{}%3A%3A{}", user_id, auth_token);
let client = if *USE_PROXY {
Client::new()
let client = if *USE_REVERSE_PROXY {
HTTP_CLIENT
.read()
.get(&*CURSOR_USER_API_URL)
.header(HOST, &*REVERSE_PROXY_HOST)
.header(PROXY_HOST, CURSOR_HOST)
} else {
Client::new()
HTTP_CLIENT
.read()
.get(&*CURSOR_USER_API_URL)
.header(HOST, CURSOR_HOST)
};

View File

@@ -1,6 +1,7 @@
pub mod error;
pub mod health;
pub mod config;
pub mod token;
pub mod userinfo;
use config::ConfigData;
@@ -48,12 +49,12 @@ impl std::fmt::Display for NormalResponse<ConfigData> {
}
}
#[derive(Serialize)]
pub struct NormalResponseNoData {
pub status: ApiStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
// #[derive(Serialize)]
// pub struct NormalResponseNoData {
// pub status: ApiStatus,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub message: Option<String>,
// }
#[derive(Serialize)]
pub struct ErrorResponse {

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::app::model::{PageContent, UsageCheck, VisionAbility};
use crate::app::model::{PageContent, UsageCheck, VisionAbility, Proxies};
#[derive(Serialize)]
pub struct ConfigData {
@@ -10,27 +10,26 @@ pub struct ConfigData {
pub vision_ability: VisionAbility,
pub enable_slow_pool: bool,
pub enable_all_claude: bool,
pub check_usage_models: UsageCheck,
pub usage_check_models: UsageCheck,
pub enable_dynamic_key: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub share_token: String,
pub proxies: Proxies,
}
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct ConfigUpdateRequest {
#[serde(default)]
pub action: String, // "get", "update", "reset"
#[serde(default)]
pub path: String,
#[serde(default)]
pub content: Option<PageContent>, // "default", "text", "html"
#[serde(default)]
pub enable_stream_check: Option<bool>,
#[serde(default)]
pub include_stop_stream: Option<bool>,
#[serde(default)]
pub vision_ability: Option<VisionAbility>,
#[serde(default)]
pub enable_slow_pool: Option<bool>,
#[serde(default)]
pub enable_all_claude: Option<bool>,
#[serde(default)]
pub check_usage_models: Option<UsageCheck>,
pub usage_check_models: Option<UsageCheck>,
pub enable_dynamic_key: Option<bool>,
pub share_token: Option<String>,
pub proxies: Option<Proxies>,
}

12
src/common/model/token.rs Normal file
View File

@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct TokenPayload {
pub sub: String,
pub time: String,
pub randomness: String,
pub exp: i64,
pub iss: String,
pub scope: String,
pub aud: String,
}

View File

@@ -1,12 +1,15 @@
mod checksum;
use ::base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
pub use checksum::*;
mod tokens;
pub use tokens::*;
mod token;
pub use token::*;
mod base64;
pub use base64::*;
use super::models::userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile};
use super::model::{token::TokenPayload, userinfo::{StripeProfile, TokenProfile, UsageProfile, UserProfile}};
use crate::app::{
constant::{FALSE, TRUE},
lazy::{TOKEN_DELIMITER, TOKEN_DELIMITER_LEN},
constant::{COMMA, FALSE, TRUE},
lazy::{TOKEN_DELIMITER, USE_COMMA_DELIMITER},
};
pub fn parse_bool_from_env(key: &str, default: bool) -> bool {
@@ -102,10 +105,20 @@ pub async fn get_user_profile(auth_token: &str) -> Option<UserProfile> {
}
pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)> {
// 找最后一个逗号
let comma_pos = auth_token.rfind(*TOKEN_DELIMITER)?;
// 尝试使用自定义分隔符查找
let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER);
// 如果自定义分隔符未找到,并且 USE_COMMA_DELIMITER 为 true则尝试使用逗号
if delimiter_pos.is_none() && *USE_COMMA_DELIMITER {
delimiter_pos = auth_token.rfind(COMMA);
}
// 如果最终都没有找到分隔符,则返回 None
let comma_pos = delimiter_pos?;
// 使用找到的分隔符位置分割字符串
let (token_part, checksum) = auth_token.split_at(comma_pos);
let checksum = &checksum[*TOKEN_DELIMITER_LEN..]; // 跳过逗号
let checksum = &checksum[1..]; // 跳过逗号
// 解析 token - 为了向前兼容,忽略最后一个:或%3A前的内容
let colon_pos = token_part.rfind(':');
@@ -124,15 +137,23 @@ pub fn validate_token_and_checksum(auth_token: &str) -> Option<(String, String)>
// 验证 token 和 checksum 有效性
if validate_token(token) && validate_checksum(checksum) {
Some((token.to_string(), checksum.to_string()))
Some((token.to_string(), generate_checksum_with_repair(checksum)))
} else {
None
}
}
pub fn extract_token(auth_token: &str) -> Option<String> {
// 解析 token
let token_part = match auth_token.rfind(*TOKEN_DELIMITER) {
// 尝试使用自定义分隔符查找
let mut delimiter_pos = auth_token.rfind(*TOKEN_DELIMITER);
// 如果自定义分隔符未找到,并且 USE_COMMA_DELIMITER 为 true则尝试使用逗号
if delimiter_pos.is_none() && *USE_COMMA_DELIMITER {
delimiter_pos = auth_token.rfind(COMMA);
}
// 根据是否找到分隔符来确定 token_part
let token_part = match delimiter_pos {
Some(pos) => &auth_token[..pos],
None => auth_token,
};
@@ -163,3 +184,78 @@ pub fn extract_token(auth_token: &str) -> Option<String> {
pub fn format_time_ms(seconds: f64) -> f64 {
(seconds * 1000.0).round() / 1000.0
}
use crate::chat::config::key_config;
/// 将 JWT token 转换为 TokenInfo
pub fn token_to_tokeninfo(auth_token: &str) -> Option<key_config::TokenInfo> {
let (token, checksum) = validate_token_and_checksum(auth_token)?;
// JWT token 由3部分组成用 . 分隔
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return None;
}
// 解码 payload (第二部分)
let payload = match URL_SAFE_NO_PAD.decode(parts[1]) {
Ok(decoded) => decoded,
Err(_) => return None,
};
// 将 payload 转换为字符串
let payload_str = match String::from_utf8(payload) {
Ok(s) => s,
Err(_) => return None,
};
// 解析为 TokenPayload
let payload: TokenPayload = match serde_json::from_str(&payload_str) {
Ok(p) => p,
Err(_) => return None,
};
let (machine_id_hash, mac_id_hash) = extract_hashes(&checksum)?;
// 构建 TokenInfo
Some(key_config::TokenInfo {
sub: payload.sub,
exp: payload.exp,
randomness: payload.randomness,
signature: parts[2].to_string(),
machine_id: machine_id_hash,
mac_id: mac_id_hash,
})
}
/// 将 TokenInfo 转换为 JWT token
pub fn tokeninfo_to_token(info: &key_config::TokenInfo) -> Option<(String, String)> {
// 构建 payload
let payload = TokenPayload {
sub: info.sub.clone(),
exp: info.exp,
randomness: info.randomness.clone(),
time: (info.exp - 2592000000).to_string(), // exp - 30000天
iss: ISSUER.to_string(),
scope: SCOPE.to_string(),
aud: AUDIENCE.to_string(),
};
let payload_str = match serde_json::to_string(&payload) {
Ok(s) => s,
Err(_) => return None,
};
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_str.as_bytes());
// 从 TokenInfo 中获取 machine_id 和 mac_id 的 hex 字符串
let device_id = hex::encode(&info.machine_id);
let mac_addr = if !info.mac_id.is_empty() {
Some(hex::encode(&info.mac_id))
} else {
None
};
// 组合 token
Some((format!("{}.{}.{}", HEADER_B64, payload_b64, info.signature), generate_checksum(&device_id, mac_addr.as_deref())))
}

148
src/common/utils/base64.rs Normal file
View File

@@ -0,0 +1,148 @@
// Base64 字符集 (a-z, A-Z, 0-9, -, _)
const BASE64_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
// 预计算的 Base64 查找表,用于快速解码
const BASE64_LOOKUP: [i8; 256] = {
let mut lookup = [-1i8; 256];
let mut i = 0;
while i < BASE64_CHARS.len() {
lookup[BASE64_CHARS[i] as usize] = i as i8;
i += 1;
}
lookup
};
/// 将字节切片编码为 Base64 字符串。
///
/// # Arguments
///
/// * `bytes`: 要编码的字节切片
///
/// # Returns
///
/// 编码后的 Base64 字符串
pub fn to_base64(bytes: &[u8]) -> String {
// 预分配足够容量,避免多次分配内存
let capacity = (bytes.len() + 2) / 3 * 4;
let mut result = Vec::with_capacity(capacity);
// 每三个字节为一组进行处理
for chunk in bytes.chunks(3) {
// 将三个字节合并为一个 u32
let b1 = chunk[0] as u32;
let b2 = chunk.get(1).map_or(0, |&b| b as u32);
let b3 = chunk.get(2).map_or(0, |&b| b as u32);
let n = (b1 << 16) | (b2 << 8) | b3;
// 将 u32 拆分成四个 6 位的值,并根据查找表转换为 Base64 字符
result.push(BASE64_CHARS[(n >> 18) as usize]);
result.push(BASE64_CHARS[((n >> 12) & 0x3F) as usize]);
// 如果 chunk 长度大于 1则需要处理第二个字符
if chunk.len() > 1 {
result.push(BASE64_CHARS[((n >> 6) & 0x3F) as usize]);
// 如果 chunk 长度大于 2则需要处理第三个字符
if chunk.len() > 2 {
result.push(BASE64_CHARS[(n & 0x3F) as usize]);
}
}
}
// 使用 from_utf8_unchecked 提高性能,因为 BASE64_CHARS 都是有效的 ASCII 字符
unsafe { String::from_utf8_unchecked(result) }
}
/// 将 Base64 字符串解码为字节数组。
///
/// # Arguments
///
/// * `input`: 要解码的 Base64 字符串
///
/// # Returns
///
/// 如果解码成功,返回 Some(解码后的字节数组);如果输入无效,返回 None
pub fn from_base64(input: &str) -> Option<Vec<u8>> {
let input = input.as_bytes();
// 检查输入长度Base64 编码的长度必须是 4 的倍数或余 2/3
if input.is_empty() || input.len() % 4 == 1 {
return None;
}
// 检查是否包含无效字符无效字符直接返回None
if input.iter().any(|&b| BASE64_LOOKUP[b as usize] == -1) {
return None;
}
// 预分配足够容量,避免多次分配内存
let capacity = input.len() / 4 * 3;
let mut result = Vec::with_capacity(capacity);
// 每四个字符为一组进行处理
let mut chunks = input.chunks_exact(4);
for chunk in &mut chunks {
// 使用查找表将 Base64 字符转换为 6 位的值
let n1 = BASE64_LOOKUP[chunk[0] as usize] as u32;
let n2 = BASE64_LOOKUP[chunk[1] as usize] as u32;
let n3 = BASE64_LOOKUP[chunk[2] as usize] as u32;
let n4 = BASE64_LOOKUP[chunk[3] as usize] as u32;
// 将四个 6 位的值合并为一个 u32并拆分成三个字节
let n = (n1 << 18) | (n2 << 12) | (n3 << 6) | n4;
result.push((n >> 16) as u8);
result.push(((n >> 8) & 0xFF) as u8);
result.push((n & 0xFF) as u8);
}
// 处理剩余的字符
let remainder = chunks.remainder();
if !remainder.is_empty() {
let n1 = BASE64_LOOKUP[remainder[0] as usize] as u32;
let n2 = BASE64_LOOKUP[remainder[1] as usize] as u32;
let mut n = (n1 << 18) | (n2 << 12);
result.push((n >> 16) as u8);
// 如果剩余字符长度大于 2则需要处理第二个字节
if remainder.len() > 2 {
let n3 = BASE64_LOOKUP[remainder[2] as usize] as u32;
n |= n3 << 6;
result.push(((n >> 8) & 0xFF) as u8);
}
}
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base64_roundtrip() {
let test_cases = vec![
vec![0u8, 1, 2, 3],
vec![255u8, 254, 253],
vec![0u8],
vec![0u8, 1],
vec![0u8, 1, 2],
vec![255u8; 1000],
];
for case in test_cases {
let encoded = to_base64(&case);
let decoded = from_base64(&encoded).unwrap();
assert_eq!(case, decoded);
}
}
#[test]
fn test_invalid_input() {
assert_eq!(from_base64(""), None); // 空字符串
assert_eq!(from_base64("a"), None); // 长度为 1
assert_eq!(from_base64("!@#$"), None); // 无效字符
assert_eq!(from_base64("YWJj!"), None); // 包含无效字符
assert!(from_base64("YWJj").is_some()); // 有效输入
}
}

View File

@@ -47,7 +47,7 @@ pub fn generate_timestamp_header() -> String {
BASE64.encode(&timestamp_bytes)
}
fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
pub fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
let encoded = generate_timestamp_header();
match mac_addr {
Some(mac) => format!("{}{}/{}", encoded, device_id, mac),
@@ -60,141 +60,66 @@ pub fn generate_checksum_with_default() -> String {
}
pub fn generate_checksum_with_repair(checksum: &str) -> String {
// 预校验检查字符串是否为空或只包含合法的Base64字符和'/'
if checksum.is_empty()
|| !checksum
.chars()
.all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '='))
{
let bytes = checksum.as_bytes();
let len = bytes.len();
// 长度快速检查
if len != 72 && len != 129 && len != 137 {
return generate_checksum_with_default();
}
// 尝试修复时间戳头的函数
fn try_fix_timestamp(timestamp_base64: &str) -> Option<String> {
if let Ok(timestamp_bytes) = BASE64.decode(timestamp_base64) {
if timestamp_bytes.len() == 6 {
let mut fixed_bytes = timestamp_bytes.clone();
deobfuscate_bytes(&mut fixed_bytes);
// 单次遍历完成所有字符校验
for (i, &b) in bytes.iter().enumerate() {
let valid = match (len, i) {
// 通用字符校验(排除非法字符)
(_, _) if !b.is_ascii_alphanumeric() && b != b'/' && b != b'+' && b != b'=' => false,
// 检查前3位是否为0
if fixed_bytes[0..3].iter().all(|&x| x == 0) {
// 从后四位构建时间戳
let timestamp = ((fixed_bytes[2] as u64) << 24)
| ((fixed_bytes[3] as u64) << 16)
| ((fixed_bytes[4] as u64) << 8)
| (fixed_bytes[5] as u64);
// 72字节格式时间戳(8) + 设备哈希(64)
(72, 8..=71) => b.is_ascii_hexdigit(),
let current_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
// 129字节格式设备哈希(64) + '/' + MAC哈希(64)
(129, 0..=63) => b.is_ascii_hexdigit(),
(129, 64) => b == b'/',
(129, 65..=128) => b.is_ascii_hexdigit(),
if timestamp <= current_timestamp {
// 修复时间戳字节
fixed_bytes[0] = fixed_bytes[4];
fixed_bytes[1] = fixed_bytes[5];
// 137字节格式时间戳(8) + 设备哈希(64) + '/' + MAC哈希(64)
(137, 8..=71) => b.is_ascii_hexdigit(),
(137, 72) => b == b'/',
(137, 73..=136) => b.is_ascii_hexdigit(),
obfuscate_bytes(&mut fixed_bytes);
return Some(BASE64.encode(&fixed_bytes));
}
}
}
}
None
}
// 时间戳部分不需要校验
(72 | 137, 0..=7) => true,
if checksum.len() == 8 {
// 尝试修复时间戳头
if let Some(fixed_timestamp) = try_fix_timestamp(checksum) {
return format!("{}{}/{}", fixed_timestamp, generate_hash(), generate_hash());
}
_ => unreachable!(),
};
// 验证原始时间戳
if let Some(timestamp) = extract_time_ks(checksum) {
let current_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
if timestamp <= current_timestamp {
return format!("{}{}/{}", checksum, generate_hash(), generate_hash());
}
}
} else if checksum.len() > 8 {
// 处理可能包含hash的情况
let parts: Vec<&str> = checksum.split('/').collect();
match parts.len() {
1 => {
let timestamp_base64 = &checksum[..8];
let device_id = &checksum[8..];
if is_valid_hash(device_id) {
// 先尝试修复时间戳
if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) {
return format!("{}{}/{}", fixed_timestamp, device_id, generate_hash());
}
// 验证原始时间戳
if let Some(timestamp) = extract_time_ks(timestamp_base64) {
let current_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
if timestamp <= current_timestamp {
return format!(
"{}{}/{}",
generate_timestamp_header(),
device_id,
generate_hash()
);
}
}
}
}
2 => {
let first_part = parts[0];
let mac_hash = parts[1];
if is_valid_hash(mac_hash) && first_part.len() == mac_hash.len() + 8 {
let timestamp_base64 = &first_part[..8];
let device_id = &first_part[8..];
if is_valid_hash(device_id) {
// 先尝试修复时间戳
if let Some(fixed_timestamp) = try_fix_timestamp(timestamp_base64) {
return format!("{}{}/{}", fixed_timestamp, device_id, mac_hash);
}
// 验证原始时间戳
if let Some(timestamp) = extract_time_ks(timestamp_base64) {
let current_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
if timestamp <= current_timestamp {
return format!(
"{}{}/{}",
generate_timestamp_header(),
device_id,
mac_hash
);
}
}
}
}
}
_ => {}
if !valid {
return generate_checksum_with_default();
}
}
// 如果所有修复尝试都失败,返回默认值
generate_checksum_with_default()
// 校验通过后构造结果
match len {
72 => format!(
"{}{}/{}",
generate_timestamp_header(),
unsafe { std::str::from_utf8_unchecked(&bytes[8..]) },
generate_hash()
),
129 => format!(
"{}{}/{}",
generate_timestamp_header(),
unsafe { std::str::from_utf8_unchecked(&bytes[..64]) },
unsafe { std::str::from_utf8_unchecked(&bytes[65..]) }
),
137 => format!(
"{}{}/{}",
generate_timestamp_header(),
unsafe { std::str::from_utf8_unchecked(&bytes[8..72]) },
unsafe { std::str::from_utf8_unchecked(&bytes[73..]) }
),
_ => unreachable!(),
}
}
pub fn extract_time_ks(timestamp_base64: &str) -> Option<u64> {
@@ -220,72 +145,73 @@ pub fn extract_time_ks(timestamp_base64: &str) -> Option<u64> {
}
pub fn validate_checksum(checksum: &str) -> bool {
// 预校验检查字符串是否为空或只包含合法的Base64字符和'/'
if checksum.is_empty()
|| !checksum
.chars()
.all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '='))
{
return false;
}
// 首先检查是否包含基本的 base64 编码部分和 hash 格式的 device_id
let parts: Vec<&str> = checksum.split('/').collect();
let bytes = checksum.as_bytes();
let len = bytes.len();
match parts.len() {
// 没有 MAC 地址的情况
1 => {
if checksum.len() < 72 {
// 8 + 64 = 72
return false;
}
// 解码前8个字符的base64时间戳
let timestamp_base64 = &checksum[..8];
let timestamp = match extract_time_ks(timestamp_base64) {
Some(ts) => ts,
None => return false,
};
let current_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 1_000;
if current_timestamp < timestamp {
return false;
}
// 验证 device_id hash 部分
is_valid_hash(&checksum[8..])
}
// 包含 MAC hash 的情况
2 => {
let first_part = parts[0];
let mac_hash = parts[1];
// MAC hash 必须是64字符的十六进制
if !is_valid_hash(mac_hash) {
return false;
}
// 检查第一部分比MAC hash多8个字符
if first_part.len() != mac_hash.len() + 8 {
return false;
}
// 递归验证第一部分
validate_checksum(first_part)
}
_ => false,
}
}
fn is_valid_hash(hash: &str) -> bool {
if hash.len() < 64 {
// 长度门控
if len != 72 && len != 137 {
return false;
}
// 检查是否都是有效的十六进制字符
hash.chars().all(|c| c.is_ascii_hexdigit())
// 单次遍历完成所有字符校验
for (i, &b) in bytes.iter().enumerate() {
let valid = match (len, i) {
// 通用字符校验(排除非法字符)
(_, _) if !b.is_ascii_alphanumeric() && b != b'/' && b != b'+' && b != b'=' => false,
// 格式校验
(72, 0..=7) => true, // 时间戳部分由extract_time_ks验证
(72, 8..=71) => b.is_ascii_hexdigit(),
(137, 0..=7) => true, // 时间戳
(137, 8..=71) => b.is_ascii_hexdigit(), // 设备哈希
(137, 72) => b == b'/', // 分割符索引72是第73个字符
(137, 73..=136) => b.is_ascii_hexdigit(), // MAC哈希
_ => unreachable!(),
};
if !valid {
return false;
}
}
// 统一时间戳验证(无需分层)
let time_valid = extract_time_ks(&checksum[..8]).is_some();
// 附加MAC哈希长度校验仅137字符需要
let mac_hash_valid = if len == 137 {
checksum[73..].len() == 64 // 确保MAC哈希长度为64
} else {
true // 72字符无需此检查
};
time_valid && mac_hash_valid
}
/// 从校验通过的checksum中提取哈希值需先通过validate_checksum验证
/// 返回 (device_hash, mac_hash) mac_hash可能为空Vec
pub fn extract_hashes(checksum: &str) -> Option<(Vec<u8>, Vec<u8>)> {
// 前置条件:必须通过校验(确保长度和格式正确)
if !validate_checksum(checksum) {
return None;
}
// 根据长度直接切割无需字符级验证validate_checksum已保证
match checksum.len() {
72 => {
// 格式8字节时间戳 + 64字节设备哈希
let device_hash = hex::decode(&checksum[8..]).ok()?; // 8..72
Some((device_hash, Vec::new()))
}
137 => {
// 格式8时间戳 + 64设备哈希 + '/' + 64MAC哈希
// 直接按固定位置切割validate_checksum已确保索引72是'/'
let device_hash = hex::decode(&checksum[8..72]).ok()?;
let mac_hash = hex::decode(&checksum[73..]).ok()?; // 73..137
Some((device_hash, mac_hash))
}
// validate_checksum已过滤其他长度此处应为不可达代码
_ => unreachable!("Invalid length after validation: {}", checksum.len()),
}
}

View File

@@ -1,11 +1,12 @@
use crate::{
app::{
constant::EMPTY_STRING,
model::TokenInfo,
lazy::{TOKEN_FILE, TOKEN_LIST_FILE},
},
common::utils::generate_checksum_with_default,
use super::generate_checksum_with_repair;
use crate::app::{
constant::{COMMA, EMPTY_STRING},
lazy::TOKEN_LIST_FILE,
model::TokenInfo,
};
use crate::common::model::token::TokenPayload;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::{DateTime, Local, TimeZone};
// 规范化文件内容并写入
fn normalize_and_write(content: &str, file_path: &str) -> String {
@@ -19,65 +20,37 @@ fn normalize_and_write(content: &str, file_path: &str) -> String {
}
// 解析token
fn parse_token(token_part: &str) -> Option<String> {
pub fn parse_token(token_part: &str) -> String {
// 查找最后一个:或%3A的位置
let colon_pos = token_part.rfind(':');
let encoded_colon_pos = token_part.rfind("%3A");
match (colon_pos, encoded_colon_pos) {
(None, None) => Some(token_part.to_string()),
(Some(pos1), None) => Some(token_part[(pos1 + 1)..].to_string()),
(None, Some(pos2)) => Some(token_part[(pos2 + 3)..].to_string()),
(None, None) => token_part.to_string(),
(Some(pos1), None) => token_part[(pos1 + 1)..].to_string(),
(None, Some(pos2)) => token_part[(pos2 + 3)..].to_string(),
(Some(pos1), Some(pos2)) => {
// 取较大的位置作为分隔点
let pos = pos1.max(pos2);
let start = if pos == pos2 { pos + 3 } else { pos + 1 };
Some(token_part[start..].to_string())
token_part[start..].to_string()
}
}
}
// Token 加载函数
pub fn load_tokens() -> Vec<TokenInfo> {
let token_file = TOKEN_FILE.as_str();
let token_list_file = TOKEN_LIST_FILE.as_str();
// 确保文件存在
for file in [&token_file, &token_list_file] {
if !std::path::Path::new(file).exists() {
if let Err(e) = std::fs::write(file, EMPTY_STRING) {
eprintln!("警告: 无法创建文件 '{}': {}", file, e);
}
if !std::path::Path::new(&token_list_file).exists() {
if let Err(e) = std::fs::write(&token_list_file, EMPTY_STRING) {
eprintln!("警告: 无法创建文件 '{}': {}", &token_list_file, e);
}
}
// 读取和规范化 token 文件
let token_entries = match std::fs::read_to_string(&token_file) {
Ok(content) => {
let normalized = content.replace("\r\n", "\n");
normalized
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let parsed = parse_token(line);
if parsed.is_none() || !validate_token(&parsed.as_ref().unwrap()) {
return None;
}
parsed
})
.collect::<Vec<_>>()
}
Err(e) => {
eprintln!("警告: 无法读取token文件 '{}': {}", token_file, e);
Vec::new()
}
};
// 读取和规范化 token-list 文件
let mut token_map: std::collections::HashMap<String, String> =
let token_map: std::collections::HashMap<String, String> =
match std::fs::read_to_string(&token_list_file) {
Ok(content) => {
let normalized = normalize_and_write(&content, &token_list_file);
@@ -89,11 +62,11 @@ pub fn load_tokens() -> Vec<TokenInfo> {
return None;
}
let parts: Vec<&str> = line.split(',').collect();
let parts: Vec<&str> = line.split(COMMA).collect();
match parts[..] {
[token_part, checksum] => {
let token = parse_token(token_part)?;
Some((token, checksum.to_string()))
let token = parse_token(token_part);
Some((token, generate_checksum_with_repair(checksum)))
}
_ => {
eprintln!("警告: 忽略无效的token-list行: {}", line);
@@ -109,21 +82,10 @@ pub fn load_tokens() -> Vec<TokenInfo> {
}
};
// 更新或添加新token
for token in token_entries {
if !token_map.contains_key(&token) {
// 为新token生成checksum
let checksum = generate_checksum_with_default();
token_map.insert(token, checksum);
}
}
// 更新 token-list 文件
let token_list_content = token_map
.iter()
.map(|(token, checksum)| {
format!("{},{}", token, checksum)
})
.map(|(token, checksum)| format!("{},{}", token, checksum))
.collect::<Vec<_>>()
.join("\n");
@@ -142,8 +104,20 @@ pub fn load_tokens() -> Vec<TokenInfo> {
.collect()
}
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::{DateTime, Local, TimeZone};
pub fn write_tokens(token_infos: &[TokenInfo], file_path: &str) -> std::io::Result<()> {
let content = token_infos
.iter()
.map(|info| format!("{},{}", info.token, info.checksum))
.collect::<Vec<String>>()
.join("\n");
std::fs::write(file_path, content)
}
pub(super) const HEADER_B64: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
pub(super) const ISSUER: &str = "https://authentication.cursor.sh";
pub(super) const SCOPE: &str = "openid profile email offline_access";
pub(super) const AUDIENCE: &str = "https://cursor.com";
// 验证jwt token是否有效
pub fn validate_token(token: &str) -> bool {
@@ -153,7 +127,7 @@ pub fn validate_token(token: &str) -> bool {
return false;
}
if parts[0] != "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" {
if parts[0] != HEADER_B64 {
return false;
}
@@ -169,66 +143,61 @@ pub fn validate_token(token: &str) -> bool {
Err(_) => return false,
};
// 解析 JSON
let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) {
Ok(v) => v,
// 解析为 TokenPayload
let payload: TokenPayload = match serde_json::from_str(&payload_str) {
Ok(p) => p,
Err(_) => return false,
};
// 验证必要字段是否存在且有效
let required_fields = ["sub", "time", "randomness", "exp", "iss", "scope", "aud"];
for field in required_fields {
if !payload_json.get(field).is_some() {
return false;
}
}
// 验证 time 字段
if let Some(time) = payload_json["time"].as_str() {
// 验证 time 是否为有效的数字字符串
if let Ok(time_value) = time.parse::<i64>() {
let current_time = chrono::Utc::now().timestamp();
if time_value > current_time {
return false;
}
} else {
if let Ok(time_value) = payload.time.parse::<i64>() {
let current_time = chrono::Utc::now().timestamp();
if time_value > current_time {
return false;
}
} else {
return false;
}
// 验证 randomness 长度
if let Some(randomness) = payload_json["randomness"].as_str() {
if randomness.len() != 18 {
// 验证 randomness 格式
let bytes = payload.randomness.as_bytes();
if bytes.len() != 18 {
return false;
}
// 单次遍历完成所有字符校验
for (i, &b) in bytes.iter().enumerate() {
let valid = match i {
// 16进制数字部分
0..=7 | 9..=12 | 14..=17 => b.is_ascii_hexdigit(),
// 连字符部分
8 | 13 => b == b'-',
_ => unreachable!(),
};
if !valid {
return false;
}
} else {
return false;
}
// 验证过期时间
if let Some(exp) = payload_json["exp"].as_i64() {
let current_time = chrono::Utc::now().timestamp();
if current_time > exp {
return false;
}
} else {
let current_time = chrono::Utc::now().timestamp();
if current_time > payload.exp {
return false;
}
// 验证发行者
if payload_json["iss"].as_str() != Some("https://authentication.cursor.sh") {
if payload.iss != ISSUER {
return false;
}
// 验证授权范围
if payload_json["scope"].as_str() != Some("openid profile email offline_access") {
if payload.scope != SCOPE {
return false;
}
// 验证受众
if payload_json["aud"].as_str() != Some("https://cursor.com") {
if payload.aud != AUDIENCE {
return false;
}
@@ -255,16 +224,21 @@ pub fn extract_user_id(token: &str) -> Option<String> {
Err(_) => return None,
};
// 解析 JSON
let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) {
Ok(v) => v,
// 解析为 TokenPayload
let payload: TokenPayload = match serde_json::from_str(&payload_str) {
Ok(p) => p,
Err(_) => return None,
};
// 提取 sub 字段
payload_json["sub"]
.as_str()
.map(|s| s.split('|').nth(1).unwrap_or(s).to_string())
Some(
payload
.sub
.split('|')
.nth(1)
.unwrap_or(&payload.sub)
.to_string(),
)
}
// 从 JWT token 中提取 time 字段
@@ -287,15 +261,16 @@ pub fn extract_time(token: &str) -> Option<DateTime<Local>> {
Err(_) => return None,
};
// 解析 JSON
let payload_json: serde_json::Value = match serde_json::from_str(&payload_str) {
Ok(v) => v,
// 解析为 TokenPayload
let payload: TokenPayload = match serde_json::from_str(&payload_str) {
Ok(p) => p,
Err(_) => return None,
};
// 提取时间戳并转换为本地时间
payload_json["time"]
.as_str()
.and_then(|t| t.parse::<i64>().ok())
payload
.time
.parse::<i64>()
.ok()
.and_then(|timestamp| Local.timestamp_opt(timestamp, 0).single())
}

View File

@@ -5,7 +5,12 @@ mod common;
use app::{
config::handle_config_update,
constant::{
EMPTY_STRING, PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM, ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_GET_TOKENINFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH, ROUTE_USER_INFO_PATH
PKG_VERSION, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BASIC_CALIBRATION_PATH,
ROUTE_BUILD_KEY_PATH, ROUTE_CONFIG_PATH, ROUTE_ENV_EXAMPLE_PATH, ROUTE_GET_CHECKSUM,
ROUTE_GET_HASH, ROUTE_GET_TIMESTAMP_HEADER, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH,
ROUTE_README_PATH, ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENS_ADD_PATH,
ROUTE_TOKENS_DELETE_PATH, ROUTE_TOKENS_GET_PATH, ROUTE_TOKENS_PATH,
ROUTE_TOKENS_RELOAD_PATH, ROUTE_TOKENS_UPDATE_PATH, ROUTE_USER_INFO_PATH,
},
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
model::*,
@@ -16,13 +21,16 @@ use axum::{
};
use chat::{
route::{
handle_about, handle_api_page, handle_basic_calibration, handle_config_page, handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header, handle_get_tokeninfo, handle_health, handle_logs, handle_logs_post, handle_readme, handle_root, handle_static, handle_tokeninfo_page, handle_update_tokeninfo, handle_update_tokeninfo_post, handle_user_info
handle_about, handle_add_tokens, handle_api_page, handle_basic_calibration,
handle_build_key, handle_build_key_page, handle_config_page, handle_delete_tokens,
handle_env_example, handle_get_checksum, handle_get_hash, handle_get_timestamp_header,
handle_get_tokens, handle_health, handle_logs, handle_logs_post, handle_readme,
handle_reload_tokens, handle_root, handle_static, handle_tokens_page, handle_update_tokens,
handle_user_info,
},
service::{handle_chat, handle_models},
};
use common::utils::{
load_tokens, parse_bool_from_env, parse_string_from_env, parse_usize_from_env,
};
use common::utils::{load_tokens, parse_string_from_env, parse_usize_from_env};
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer};
@@ -47,13 +55,7 @@ async fn main() {
};
// 初始化全局配置
AppConfig::init(
parse_bool_from_env("ENABLE_STREAM_CHECK", true),
parse_bool_from_env("INCLUDE_STOP_REASON_STREAM", true),
VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)),
parse_bool_from_env("ENABLE_SLOW_POOL", false),
parse_bool_from_env("PASS_ANY_CLAUDE", false),
);
AppConfig::init();
// 加载 tokens
let token_infos = load_tokens();
@@ -61,18 +63,42 @@ async fn main() {
// 初始化应用状态
let state = Arc::new(Mutex::new(AppState::new(token_infos)));
// 创建一个克隆用于后台任务
let state_for_reload = state.clone();
// 启动后台任务在每个整1000秒时更新 checksum
tokio::spawn(async move {
loop {
// 获取当前时间戳
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// 计算距离下一个整1000秒的等待时间
let next_reload = (now / 1000 + 1) * 1000;
let wait_duration = next_reload - now;
// 等待到下一个整1000秒
tokio::time::sleep(std::time::Duration::from_secs(wait_duration)).await;
let mut app_state = state_for_reload.lock().await;
app_state.update_checksum();
debug_println!("checksum 自动刷新: {}", next_reload);
}
});
// 设置路由
let app = Router::new()
.route(ROUTE_ROOT_PATH, get(handle_root))
.route(ROUTE_HEALTH_PATH, get(handle_health))
.route(ROUTE_TOKENINFO_PATH, get(handle_tokeninfo_page))
.route(ROUTE_TOKENS_PATH, get(handle_tokens_page))
.route(ROUTE_MODELS_PATH.as_str(), get(handle_models))
.route(ROUTE_UPDATE_TOKENINFO_PATH, get(handle_update_tokeninfo))
.route(ROUTE_GET_TOKENINFO_PATH, post(handle_get_tokeninfo))
.route(
ROUTE_UPDATE_TOKENINFO_PATH,
post(handle_update_tokeninfo_post),
)
.route(ROUTE_TOKENS_GET_PATH, post(handle_get_tokens))
.route(ROUTE_TOKENS_RELOAD_PATH, post(handle_reload_tokens))
.route(ROUTE_TOKENS_UPDATE_PATH, post(handle_update_tokens))
.route(ROUTE_TOKENS_ADD_PATH, post(handle_add_tokens))
.route(ROUTE_TOKENS_DELETE_PATH, post(handle_delete_tokens))
.route(ROUTE_CHAT_PATH.as_str(), post(handle_chat))
.route(ROUTE_LOGS_PATH, get(handle_logs))
.route(ROUTE_LOGS_PATH, post(handle_logs_post))
@@ -88,6 +114,8 @@ async fn main() {
.route(ROUTE_GET_TIMESTAMP_HEADER, get(handle_get_timestamp_header))
.route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration))
.route(ROUTE_USER_INFO_PATH, post(handle_user_info))
.route(ROUTE_BUILD_KEY_PATH, get(handle_build_key_page))
.route(ROUTE_BUILD_KEY_PATH, post(handle_build_key))
.layer(RequestBodyLimitLayer::new(
1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2),
))
@@ -99,6 +127,9 @@ async fn main() {
let addr = format!("0.0.0.0:{}", port);
println!("服务器运行在端口 {}", port);
println!("当前版本: v{}", PKG_VERSION);
// if PKG_VERSION.contains("pre") {
println!("当前是测试版,有问题及时反馈哦~");
// }
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();

View File

@@ -140,7 +140,7 @@
<div id="usageProgressContainer" class="progress-container"></div>
</div>
<div id="message" class="message"></div>
<div id="message"></div>
<footer class="footer">
<div id="version"></div>
@@ -198,11 +198,16 @@
// 获取模型列表
async function getModels() {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
try {
const modelList = document.getElementById('modelList');
const suffix = document.getElementById('customSuffix').checked ?
document.getElementById('suffixInput').value : '';
modelList.value = globalModels.map(model => model + suffix).join(',');
modelList.value = globalModels.map(model => model + suffix).join(',');
showGlobalMessage('模型列表已更新');
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
// 复制模型列表
@@ -234,7 +239,7 @@
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`请求失败 (${response.status})`);
}
return await response.json();
@@ -261,9 +266,13 @@
calibrationCache.set(token, {
user_id: result.user_id,
create_at: result.create_at,
checksum_time: calibResult.checksum_time
checksum_time: result.checksum_time
});
updateUsageDisplay(null, calibrationCache.get(token));
// 显示基本校准信息
const container = document.getElementById('userInfoContainer');
container.style.display = 'block';
updateUsageDisplay({ user: null }, calibrationCache.get(token));
}
}
}
@@ -275,15 +284,13 @@
showGlobalMessage('请输入 Token', true);
return;
}
// 如果没有校准缓存,先进行校准
if (!calibrationCache.has(token)) {
const calibResult = await makeTokenRequest('/basic-calibration', token);
if (calibResult && calibResult.status !== 'error') {
calibrationCache.set(token, {
user_id: calibResult.user_id,
create_at: calibResult.create_at,
checksum_time: calibResult.checksum_time
});
showGlobalMessage('正在进行 Token 校准...');
await calibrateToken();
if (!calibrationCache.has(token)) {
return; // 如果校准失败,直接返回
}
}
@@ -292,6 +299,7 @@
const container = document.getElementById('userInfoContainer');
container.style.display = 'block';
updateUsageDisplay(result, calibrationCache.get(token));
showGlobalMessage('用户信息获取成功');
}
}
@@ -350,11 +358,16 @@
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
startStatusCheck();
document.addEventListener('DOMContentLoaded', async () => {
try {
await startStatusCheck();
showGlobalMessage('系统初始化完成');
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
// 监听后缀输入变化
document.getElementById('suffixInput').addEventListener('input', getModels);
} catch (error) {
showGlobalMessage('系统初始化失败', true);
}
});
</script>
</body>

253
static/build_key.html Normal file
View File

@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Key 构建</title>
<!-- 引入共享样式 -->
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
.key-result {
word-break: break-all;
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: var(--spacing);
position: relative;
}
.copy-button {
position: absolute;
right: 10px;
top: 10px;
padding: 4px 8px;
font-size: 14px;
background: var(--primary-color-alpha);
color: var(--primary-color);
border: 1px solid var(--primary-color);
border-radius: 4px;
cursor: pointer;
transition: all var(--transition-fast);
}
.copy-button:hover {
background: var(--primary-color);
color: white;
}
.model-list {
max-height: 150px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 8px;
background: var(--card-background);
}
.model-item {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.model-item input[type="checkbox"] {
margin-right: 8px;
}
</style>
</head>
<body>
<h1>Key 构建</h1>
<div class="container">
<div class="form-group">
<label>服务认证令牌:</label>
<input type="password" id="authToken" placeholder="输入服务认证令牌">
</div>
<div class="form-group">
<label>数据认证令牌:</label>
<input type="password" id="dataToken" placeholder="输入数据认证令牌">
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="disableVision">
<option value="">跟随全局</option>
<option value="true">禁用</option>
<option value="false">启用</option>
</select>
</div>
<div class="form-group">
<label>慢速池:</label>
<select id="enableSlowPool">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>使用量检查模型规则:</label>
<select id="usageCheckType" onchange="toggleModelList()">
<option value="">跟随全局</option>
<option value="default">默认</option>
<option value="disabled">禁用</option>
<option value="all">所有</option>
<option value="custom">自定义</option>
</select>
<div id="modelListContainer" class="model-list" style="display: none;">
<!-- 模型列表将通过 JavaScript 动态填充 -->
</div>
</div>
<div class="button-group">
<button onclick="buildKey()">构建 Key</button>
<button onclick="clearForm()" class="secondary">清空表单</button>
</div>
</div>
<div id="keyResult" class="key-result" style="display: none;">
<button class="copy-button" onclick="copyKey()">复制</button>
<div id="keyContent"></div>
</div>
<div id="message"></div>
<script>
let availableModels = [];
async function getModels() {
try {
const response = await fetch('/v1/models');
const data = await response.json();
availableModels = data.data.map(model => model.id);
updateModelList();
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
function updateModelList() {
const container = document.getElementById('modelListContainer');
container.innerHTML = availableModels.map(model => `<div class="model-item"><input type="checkbox" id="model_${model}" value="${model}"><label for="model_${model}">${model}</label></div>`).join('');
}
function toggleModelList() {
const type = document.getElementById('usageCheckType').value;
const container = document.getElementById('modelListContainer');
container.style.display = type === 'custom' ? 'block' : 'none';
}
async function buildKey() {
const authToken = document.getElementById('authToken').value;
const dataToken = document.getElementById('dataToken').value;
if (!authToken) {
showGlobalMessage('请输入服务认证令牌', true);
return;
}
if (!dataToken) {
showGlobalMessage('请输入数据认证令牌', true);
return;
}
const type = document.getElementById('usageCheckType').value;
let modelIds = '';
if (type === 'custom') {
modelIds = Array.from(document.querySelectorAll('#modelListContainer input:checked'))
.map(input => input.value)
.join(',');
}
const data = {
auth_token: dataToken,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
};
try {
const response = await makeAuthenticatedRequest('/build-key', {
method: 'POST',
body: JSON.stringify(data)
});
if (response) {
const keyResult = document.getElementById('keyResult');
const keyContent = document.getElementById('keyContent');
keyContent.textContent = response.key || response.error;
keyResult.style.display = 'block';
showGlobalMessage(response.key ? 'Key 构建成功' : '构建失败: ' + response.error, !response.key);
}
} catch (error) {
showGlobalMessage('请求失败: ' + error.message, true);
}
}
function copyKey() {
const keyContent = document.getElementById('keyContent').textContent;
navigator.clipboard.writeText(keyContent).then(() => {
showGlobalMessage('Key 已复制到剪贴板');
}).catch(() => {
showGlobalMessage('复制失败', true);
});
}
function clearForm() {
document.getElementById('authToken').value = '';
document.getElementById('dataToken').value = '';
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = 'default';
document.getElementById('modelListContainer').style.display = 'none';
document.getElementById('keyResult').style.display = 'none';
showGlobalMessage('表单已清空');
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
getModels();
const authToken = getAuthToken();
if (authToken) {
document.getElementById('authToken').value = authToken;
fetchLogs();
}
});
initializeTokenHandling('authToken');
</script>
</body>
</html>

View File

@@ -21,12 +21,13 @@
<option value="/">根路径 (/)</option>
<option value="/logs">日志页面 (/logs)</option>
<option value="/config">配置页面 (/config)</option>
<option value="/tokeninfo">Token 信息页面 (/tokeninfo)</option>
<option value="/tokens">Token 管理页面 (/tokens)</option>
<option value="/static/shared-styles.css">共享样式 (/static/shared-styles.css)</option>
<option value="/static/shared.js">共享脚本 (/static/shared.js)</option>
<option value="/about">关于页面 (/about)</option>
<option value="/readme">ReadMe文档 (/readme)</option>
<option value="/api">api调用 (/api)</option>
<option value="/build-key">构建动态 Key (/build-key)</option>
</select>
</div>
@@ -92,14 +93,40 @@
<div class="form-group">
<label>使用量检查模型规则:</label>
<select id="check_usage_models_type">
<select id="usage_check_models_type">
<option value="">保持不变</option>
<option value="none">禁用</option>
<option value="default">默认</option>
<option value="all">所有</option>
<option value="list">自定义列表</option>
</select>
<input type="text" id="check_usage_models_list" placeholder="模型列表,以逗号分隔" style="display: none;">
<input type="text" id="usage_check_models_list" placeholder="模型列表,以逗号分隔" style="display: none;">
</div>
<div class="form-group">
<label>是否允许动态配置Key:</label>
<select id="enable_dynamic_key">
<option value="">保持不变</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>代理设置:</label>
<select id="proxies_type" onchange="handleProxiesTypeChange()">
<option value="">保持不变</option>
<option value="no">不使用代理</option>
<option value="system">使用系统代理</option>
<option value="list">自定义代理列表</option>
</select>
<input type="text" id="proxies_list" placeholder="代理地址列表,以逗号分隔 (例如: http://127.0.0.1:7890)"
style="display: none;">
</div>
<div class="form-group">
<label>共享令牌(空表示禁用):</label>
<input type="text" id="shareToken">
</div>
<div class="form-group">
@@ -114,127 +141,175 @@
</div>
</div>
<div id="result" class="message"></div>
<div id="message"></div>
<script>
async function fetchConfig() {
const path = document.getElementById('path').value;
const data = await makeAuthenticatedRequest('/config', {
body: JSON.stringify({ action: 'get', path })
});
try {
const path = document.getElementById('path').value;
const data = await makeAuthenticatedRequest('/config', {
body: JSON.stringify({ action: 'get', path })
});
if (data) {
let content = '';
if (data) {
let content = '';
// 获取当前路径的页面内容
const pageContent = data.data.page_content;
// 获取当前路径的页面内容
const pageContent = data.data.page_content;
// 如果是 default 类型,需要从路径获取内容
if (pageContent?.type === 'default') {
// 直接从路径获取内容
const response = await fetch(path);
content = await response.text();
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
content = pageContent.content;
// 如果是 default 类型,需要从路径获取内容
if (pageContent?.type === 'default') {
// 直接从路径获取内容
const response = await fetch(path);
content = await response.text();
} else if (pageContent?.type === 'text' || pageContent?.type === 'html') {
content = pageContent.content;
}
// 更新表单
document.getElementById('content').value = content || '';
document.getElementById('content_type').value = pageContent?.type || 'default';
let visionValue = data.data.vision_ability || '';
// 标准化 vision_ability 的值
switch (visionValue) {
case 'none':
visionValue = 'disabled';
break;
case 'base64':
visionValue = 'base64-only';
break;
case 'all':
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
document.getElementById('enable_all_claude').value =
parseStringFromBoolean(data.data.enable_all_claude, '');
document.getElementById('usage_check_models_type').value = data.data.usage_check_models?.type || '';
document.getElementById('usage_check_models_list').value = data.data.usage_check_models?.type === 'list' ? data.data.usage_check_models?.content || '' : document.getElementById('usage_check_models_list').value;
document.getElementById('enable_dynamic_key').value =
parseStringFromBoolean(data.data.enable_dynamic_key, '');
// 处理代理设置
const proxies = data.data.proxies || '';
let proxiesType = '';
let proxiesList = '';
if (proxies === '') {
proxiesType = 'no';
} else if (proxies === 'system') {
proxiesType = 'system';
} else {
proxiesType = 'list';
proxiesList = proxies;
}
document.getElementById('proxies_type').value = proxiesType;
document.getElementById('proxies_list').value = proxiesList;
handleProxiesTypeChange();
document.getElementById('shareToken').value = data.data.share_token || '';
// 添加获取配置成功提示
showGlobalMessage(`成功获取 ${path} 的配置`, false);
}
// 更新表单
document.getElementById('content').value = content || '';
document.getElementById('content_type').value = pageContent?.type || 'default';
let visionValue = data.data.vision_ability || '';
// 标准化 vision_ability 的值
switch (visionValue) {
case 'none':
visionValue = 'disabled';
break;
case 'base64':
visionValue = 'base64-only';
break;
case 'all':
visionValue = 'base64-http';
break;
}
document.getElementById('enable_stream_check').value =
parseStringFromBoolean(data.data.enable_stream_check, '');
document.getElementById('include_stop_stream').value =
parseStringFromBoolean(data.data.include_stop_stream, '');
document.getElementById('vision_ability').value = visionValue;
document.getElementById('enable_slow_pool').value =
parseStringFromBoolean(data.data.enable_slow_pool, '');
document.getElementById('enable_all_claude').value =
parseStringFromBoolean(data.data.enable_all_claude, '');
document.getElementById('check_usage_models_type').value = data.data.check_usage_models?.type || '';
document.getElementById('check_usage_models_list').value = data.data.check_usage_models?.type === 'list' ? data.data.check_usage_models?.content || '' : document.getElementById('check_usage_models_list').value;
} catch (error) {
showGlobalMessage(error.message || '获取配置失败', true);
}
}
async function updateConfig(action) {
if (action === 'get') {
await fetchConfig();
return;
}
const contentType = document.getElementById('content_type').value;
const content = document.getElementById('content').value;
// 根据内容类型构造 content 对象
let contentObj = { type: 'default' };
if (action === 'update' && contentType !== 'default') {
contentObj = {
type: contentType,
content: content
};
}
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
...(document.getElementById('enable_slow_pool').value && {
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
}),
...(document.getElementById('enable_all_claude').value && {
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
}),
...(document.getElementById('check_usage_models_type').value && {
check_usage_models: {
type: document.getElementById('check_usage_models_type').value,
...(document.getElementById('check_usage_models_type').value === 'list' && {
content: document.getElementById('check_usage_models_list').value
})
}
})
};
const result = await makeAuthenticatedRequest('/config', {
body: JSON.stringify(data)
});
if (result) {
showMessage('result', result.message, false);
if (action === 'update' || action === 'reset') {
try {
if (action === 'get') {
await fetchConfig();
return;
}
const contentType = document.getElementById('content_type').value;
const content = document.getElementById('content').value;
// 根据内容类型构造 content 对象
let contentObj = { type: 'default' };
if (action === 'update' && contentType !== 'default') {
contentObj = {
type: contentType,
content: content
};
}
const shareToken = document.getElementById('shareToken').value.trim();
const data = {
action,
path: document.getElementById('path').value,
...(contentObj && { content: contentObj }),
...(document.getElementById('enable_stream_check').value && {
enable_stream_check: parseBooleanFromString(document.getElementById('enable_stream_check').value)
}),
...(document.getElementById('include_stop_stream').value && {
include_stop_stream: parseBooleanFromString(document.getElementById('include_stop_stream').value)
}),
...(document.getElementById('vision_ability').value && {
vision_ability: document.getElementById('vision_ability').value
}),
...(document.getElementById('enable_slow_pool').value && {
enable_slow_pool: parseBooleanFromString(document.getElementById('enable_slow_pool').value)
}),
...(document.getElementById('enable_all_claude').value && {
enable_all_claude: parseBooleanFromString(document.getElementById('enable_all_claude').value)
}),
...(document.getElementById('usage_check_models_type').value && {
usage_check_models: {
type: document.getElementById('usage_check_models_type').value,
...(document.getElementById('usage_check_models_type').value === 'list' && {
content: document.getElementById('usage_check_models_list').value
})
}
}),
...(document.getElementById('enable_dynamic_key').value && {
enable_dynamic_key: parseBooleanFromString(document.getElementById('enable_dynamic_key').value)
}),
...(document.getElementById('proxies_type').value && {
proxies: (() => {
const type = document.getElementById('proxies_type').value;
switch (type) {
case 'no':
return '';
case 'system':
return 'system';
case 'list':
return document.getElementById('proxies_list').value;
default:
return undefined;
}
})()
}),
...(shareToken && {
share_token: shareToken
}),
};
const result = await makeAuthenticatedRequest('/config', {
body: JSON.stringify(data)
});
if (result) {
showGlobalMessage(result.message, false);
if (action === 'update' || action === 'reset') {
await fetchConfig();
}
}
} catch (error) {
showGlobalMessage(error.message || '操作失败', true);
}
}
function showSuccess(message) {
showMessage('result', message, false);
}
function showError(message) {
showMessage('result', message, true);
}
// 添加按钮事件监听
document.getElementById('path').addEventListener('change', fetchConfig);
@@ -248,10 +323,27 @@
initializeTokenHandling('authToken');
// 添加使用量检查模型类型变更处理
document.getElementById('check_usage_models_type').addEventListener('change', function() {
const input = document.getElementById('check_usage_models_list');
document.getElementById('usage_check_models_type').addEventListener('change', function () {
const input = document.getElementById('usage_check_models_list');
input.style.display = this.value === 'list' ? 'inline-block' : 'none';
});
// 添加代理类型变更处理函数
function handleProxiesTypeChange() {
const type = document.getElementById('proxies_type').value;
const list = document.getElementById('proxies_list');
list.style.display = type === 'list' ? 'inline-block' : 'none';
}
// 页面加载完成后自动获取配置
document.addEventListener('DOMContentLoaded', async () => {
try {
await fetchConfig();
showGlobalMessage('页面加载完成', false);
} catch (error) {
showGlobalMessage('初始化配置加载失败', true);
}
});
</script>
</body>

View File

@@ -354,6 +354,28 @@
#logsTable tr:hover td {
background-color: var(--hover-color, rgba(0, 0, 0, 0.02));
}
.modal-actions {
display: flex;
align-items: center;
gap: 12px;
}
.danger-button {
padding: 6px 12px;
font-size: 14px;
border-radius: var(--border-radius);
background: var(--error-color-alpha);
color: var(--error-color);
border: 1px solid var(--error-color);
cursor: pointer;
transition: all var(--transition-fast);
}
.danger-button:hover {
background: var(--error-color);
color: white;
}
</style>
</head>
@@ -423,7 +445,10 @@
<div class="modal-content">
<div class="modal-header">
<h3>Token 详细信息</h3>
<span class="close">&times;</span>
<div class="modal-actions">
<button class="danger-button" id="deleteTokenBtn">删除此Token</button>
<span class="close">&times;</span>
</div>
</div>
<table class="message-table">
<tr>
@@ -543,6 +568,42 @@
function showTokenModal(tokenInfo) {
const modal = document.getElementById('tokenModal');
const deleteBtn = document.getElementById('deleteTokenBtn');
// 存储当前token用于删除操作
const currentToken = tokenInfo.token;
// 更新删除按钮点击事件
deleteBtn.onclick = async () => {
if (!currentToken) {
showGlobalMessage('无效的Token', true);
return;
}
if (!confirm('确定要删除此Token吗此操作不可撤销。')) {
return;
}
const data = await makeAuthenticatedRequest('/tokens/delete', {
method: 'POST',
body: JSON.stringify({
tokens: [currentToken],
expectation: 'failed_tokens'
})
});
if (data) {
modal.style.display = 'none';
let message = 'Token删除成功';
if (data.failed_tokens?.length) {
message = 'Token删除失败未找到该Token';
}
showGlobalMessage(message);
// 刷新日志列表
fetchLogs();
}
};
document.getElementById('modalToken').textContent = tokenInfo.token || '-';
document.getElementById('modalChecksum').textContent = tokenInfo.checksum || '-';
@@ -634,7 +695,7 @@
const tbody = document.getElementById('logsBody');
updateStats(data);
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ?`<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` :'-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ? `<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` : '-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
}
function formatTiming(total, first) {

View File

@@ -1,651 +0,0 @@
<h1>cursor-api</h1>
<h2>说明</h2>
<ul>
<li>当前版本已稳定,若发现响应出现缺字漏字,与本程序无关。</li>
<li>若发现首字慢,与本程序无关。</li>
<li>若发现响应出现乱码,也与本程序无关。</li>
<li>属于官方的问题,请不要像作者反馈。</li>
<li>本程序拥有堪比客户端原本的速度,甚至可能更快。</li>
<li>本程序的性能是非常厉害的。</li>
<li>根据本项目开源协议Fork的项目不能以作者的名义进行任何形式的宣传、推广或声明。</li>
</ul>
<h2>获取key</h2>
<ol>
<li>访问 <a href="https://www.cursor.com">www.cursor.com</a> 并完成注册登录</li>
<li>在浏览器中打开开发者工具F12</li>
<li>在 Application-Cookies 中查找名为 <code>WorkosCursorSessionToken</code> 的条目,并复制其第三个字段。请注意,%3A%3A 是 :: 的 URL 编码形式cookie
的值使用冒号 (:) 进行分隔。</li>
</ol>
<h2>配置说明</h2>
<h3>环境变量</h3>
<ul>
<li><code>PORT</code>: 服务器端口号默认3000</li>
<li><code>AUTH_TOKEN</code>: 认证令牌必须用于API认证</li>
<li><code>ROUTE_PREFIX</code>: 路由前缀(可选)</li>
<li><code>TOKEN_FILE</code>: token文件路径默认.token</li>
<li><code>TOKEN_LIST_FILE</code>: token列表文件路径默认.token-list</li>
</ul>
<p>更多请查看 <code>/env-example</code></p>
<h3>Token文件格式</h3>
<ol>
<li>
<p><code>.token</code> 文件每行一个token支持以下格式</p>
<pre><code># 这是注释
token1
# alias与标签的作用差不多
alias::token2
</code></pre>
<p>alias 可以是任意值,用于区分不同的 token更方便管理WorkosCursorSessionToken 是相同格式<br>
该文件将自动向.token-list文件中追加token同时自动生成checksum</p>
</li>
<li>
<p><code>.token-list</code> 文件每行为token和checksum的对应关系</p>
<pre><code># 这里的#表示这行在下次读取要删除
token1,checksum1
# alias被舍弃会自动删除最后一个:或%3A的后一位前的所有内容
token2,checksum2
</code></pre>
<p>该文件可以被自动管理,但用户仅可在确认自己拥有修改能力时修改,一般仅有以下情况需要手动修改:</p>
<ul>
<li>需要删除某个 token</li>
<li>需要使用已有 checksum 来对应某一个 token</li>
</ul>
</li>
</ol>
<h3>模型列表</h3>
<p>写死了,后续也不会会支持自定义模型列表</p>
<pre><code>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-4o-128k
gemini-1.5-flash-500k
claude-3-haiku-200k
claude-3-5-sonnet-200k
claude-3-5-sonnet-20241022
gpt-4o-mini
o1-mini
o1-preview
o1
claude-3.5-haiku
gemini-exp-1206
gemini-2.0-flash-thinking-exp
gemini-2.0-flash-exp
</code></pre>
<h1>接口说明</h1>
<h2>基础对话</h2>
<ul>
<li>接口地址: <code>/v1/chat/completions</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token
<ol>
<li>使用环境变量 <code>AUTH_TOKEN</code> 进行认证</li>
<li>使用 <code>.token</code> 文件中的令牌列表进行轮询认证</li>
<li>在v0.1.3-rc.3支持直接使用 token,checksum 进行认证,但未提供配置关闭</li>
</ol>
</li>
</ul>
<h3>请求格式</h3>
<pre><code class="language-json">{
"model": "string",
"messages": [
{
"role": "system" | "user" | "assistant", // 也可以是 "developer" | "human" | "ai"
"content": "string" | [
{
"type": "text" | "image_url",
"text": "string",
"image_url": {
"url": "string"
}
}
]
}
],
"stream": boolean
}
</code></pre>
<h3>响应格式</h3>
<p>如果 <code>stream</code><code>false</code>:</p>
<pre><code class="language-json">{
"id": "string",
"object": "chat.completion",
"created": number,
"model": "string",
"choices": [
{
"index": number,
"message": {
"role": "assistant",
"content": "string"
},
"finish_reason": "stop" | "length"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
</code></pre>
<p>不进行 tokens 计算主要是担心性能问题。</p>
<p>如果 <code>stream</code><code>true</code>:</p>
<pre><code>data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"role":"assistant","content":"string"},"finish_reason":null}]}
data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{"content":"string"},"finish_reason":null}]}
data: {"id":"string","object":"chat.completion.chunk","created":number,"model":"string","choices":[{"index":number,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
</code></pre>
<h2>Token管理接口</h2>
<h3>简易Token信息管理页面</h3>
<ul>
<li>接口地址: <code>/tokeninfo</code></li>
<li>请求方法: GET</li>
<li>响应格式: HTML页面</li>
<li>功能: 获取 .token 和 .token-list 文件内容,并允许用户方便地使用 API 修改文件内容</li>
</ul>
<h3>更新Token信息 (GET)</h3>
<ul>
<li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: GET</li>
<li>认证方式: 不需要</li>
<li>功能: 重新加载tokens并更新应用状态</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"message": "Token list has been reloaded"
}
</code></pre>
<h3>更新Token信息 (POST)</h3>
<ul>
<li>接口地址: <code>/update-tokeninfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"tokens": "string",
"token_list": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens_count": number,
"message": "Token files have been updated and reloaded"
}
</code></pre>
<h3>获取Token信息</h3>
<ul>
<li>接口地址: <code>/get-tokeninfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"token_file": "string",
"token_list_file": "string",
"tokens": "string",
"tokens_count": number,
"token_list": "string"
}
</code></pre>
<h2>配置管理接口</h2>
<h3>配置页面</h3>
<ul>
<li>接口地址: <code>/config</code></li>
<li>请求方法: GET</li>
<li>响应格式: HTML页面</li>
<li>功能: 提供配置管理界面,可以修改页面内容和系统配置</li>
</ul>
<h3>更新配置</h3>
<ul>
<li>接口地址: <code>/config</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"action": "get" | "update" | "reset",
"path": "string",
"content": {
"type": "default" | "text" | "html",
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all", // "disabled" | "base64-only" | "base64-http"
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"message": "string",
"data": {
"page_content": {
"type": "default" | "text" | "html", // 对于js和css后两者是一样的
"content": "string"
},
"enable_stream_check": boolean,
"include_stop_stream": boolean,
"vision_ability": "none" | "base64" | "all",
"enable_slow_pool": boolean,
"enable_all_claude": boolean,
"check_usage_models": {
"type": "none" | "default" | "all" | "list",
"content": "string"
}
}
}
</code></pre>
<p>注意:<code>check_usage_models</code> 字段的默认值为:</p>
<pre><code class="language-json">{
"type": "default",
"content": "claude-3-5-sonnet-20241022,claude-3.5-sonnet,gemini-exp-1206,gpt-4,gpt-4-turbo-2024-04-09,gpt-4o,claude-3.5-haiku,gpt-4o-128k,gemini-1.5-flash-500k,claude-3-haiku-200k,claude-3-5-sonnet-200k"
}</code></pre>
<p>这些模型将默认进行使用量检查。您可以通过配置接口修改此设置。</p>
<p>路径修改注意:选择类型再修改文本,否则选择默认时内容的修改无效,在更新配置后自动被覆盖导致内容丢失,自行改进。</p>
<h2>静态资源接口</h2>
<h3>获取共享样式</h3>
<ul>
<li>接口地址: <code>/static/shared-styles.css</code></li>
<li>请求方法: GET</li>
<li>响应格式: CSS文件</li>
<li>功能: 获取共享样式表</li>
</ul>
<h3>获取共享脚本</h3>
<ul>
<li>接口地址: <code>/static/shared.js</code></li>
<li>请求方法: GET</li>
<li>响应格式: JavaScript文件</li>
<li>功能: 获取共享JavaScript代码</li>
</ul>
<h3>环境变量示例</h3>
<ul>
<li>接口地址: <code>/env-example</code></li>
<li>请求方法: GET</li>
<li>响应格式: 文本文件</li>
<li>功能: 获取环境变量配置示例</li>
</ul>
<h2>其他接口</h2>
<h3>获取模型列表</h3>
<ul>
<li>接口地址: <code>/v1/models</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"object": "list",
"data": [
{
"id": "string",
"object": "model",
"created": number,
"owned_by": "string"
}
]
}
</code></pre>
<h3>获取一个随机hash</h3>
<ul>
<li>接口地址: <code>/get-hash</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<h3>获取或修复checksum</h3>
<ul>
<li>接口地址: <code>/get-checksum</code></li>
<li>请求方法: GET</li>
<li>请求参数:
<ul>
<li><code>checksum</code>: 可选用于修复的旧版本生成的checksum也可只传入前8个字符可用来自动刷新时间戳头</li>
</ul>
</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<p>说明:</p>
<ul>
<li>如果不提供<code>checksum</code>参数将生成一个新的随机checksum</li>
<li>如果提供<code>checksum</code>参数将尝试修复旧版本的checksum以适配v0.1.3-rc.3之后的版本使用修复失败会返回新的checksum若输入的checksum本来就有效则返回更新tsheader后的checksum</li>
</ul>
<h3>获取当前的tsheader</h3>
<ul>
<li>接口地址: <code>/get-tsheader</code></li>
<li>请求方法: GET</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-plaintext">string</code></pre>
<h3>健康检查接口</h3>
<ul>
<li>接口地址: <code>/health</code><code>/</code>(重定向)</li>
<li>请求方法: GET</li>
<li>认证方式: Bearer Token可选</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)默认JSON</li>
</ul>
<pre><code class="language-json">{
"status": "success",
"version": "string",
"uptime": number,
"stats": {
"started": "string",
"total_requests": number,
"active_requests": number,
"system": {
"memory": {
"rss": number
},
"cpu": {
"usage": number
}
}
},
"models": ["string"],
"endpoints": ["string"]
}
</code></pre>
<p>注意:<code>stats</code> 字段仅在请求头中包含正确的 <code>AUTH_TOKEN</code> 时才会返回。否则,该字段将被省略。</p>
<h3>获取日志接口</h3>
<ul>
<li>接口地址: <code>/logs</code></li>
<li>请求方法: GET</li>
<li>响应格式: 根据配置返回不同的内容类型(默认、文本或HTML)</li>
</ul>
<h3>获取日志数据</h3>
<ul>
<li>接口地址: <code>/logs</code></li>
<li>请求方法: POST</li>
<li>认证方式: Bearer Token</li>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"total": number,
"logs": [
{
"id": number,
"timestamp": "string",
"model": "string",
"token_info": {
"token": "string",
"checksum": "string",
"profile": {
"usage": {
"premium": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"standard": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"unknown": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
}
},
"user": {
"email": "string",
"name": "string",
"id": "string",
"updated_at": "string"
},
"stripe": {
"membership_type": "free" | "free_trial" | "pro" | "enterprise",
"payment_id": "string",
"days_remaining_on_trial": number
}
}
},
"prompt": "string",
"timing": {
"total": number,
"first": number
},
"stream": boolean,
"status": "string",
"error": "string"
}
],
"timestamp": "string",
"status": "success"
}
</code></pre>
<h3>获取用户信息</h3>
<ul>
<li>接口地址: <code>/userinfo</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"usage": {
"premium": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"standard": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
},
"unknown": {
"requests": number,
"requests_total": number,
"tokens": number,
"max_requests": number,
"max_tokens": number
}
},
"user": {
"email": "string",
"name": "string",
"id": "string",
"updated_at": "string"
},
"stripe": {
"membership_type": "free" | "free_trial" | "pro" | "enterprise",
"payment_id": "string",
"days_remaining_on_trial": number
}
}
</code></pre>
<p>如果发生错误,响应格式为:</p>
<pre><code class="language-json">{
"error": "string"
}
</code></pre>
<h3>基础校准</h3>
<ul>
<li>接口地址: <code>/basic-calibration</code></li>
<li>请求方法: POST</li>
<li>认证方式: 请求体中包含token</li>
<li>请求格式:</li>
</ul>
<pre><code class="language-json">{
"token": "string"
}
</code></pre>
<ul>
<li>响应格式:</li>
</ul>
<pre><code class="language-json">{
"status": "success" | "error",
"message": "string",
"user_id": "string",
"create_at": "string",
"checksum_time": number
}
</code></pre>
<p>注意: <code>user_id</code>, <code>create_at</code>, 和 <code>checksum_time</code> 字段在校验失败时可能不存在。</p>
<h2>偷偷写在最后的话</h2>
<p>虽然作者觉得<del></del>收点钱合理,但不强求,要是<strong>主动自愿</strong>发我我肯定收(因为真有人这么做,虽然不是赞助),赞助很合理吧</p>
<p>不是<strong>主动自愿</strong>就算了,不是很缺,给了会很感动罢了。</p>
<p>虽然不是很建议你赞助,但如果你赞助了,大概可以:</p>
<ul>
<li>测试版更新</li>
<li>要求功能</li>
<li>问题更快解决</li>
</ul>
<p>即使如此,我也保留可以拒绝赞助和拒绝要求的权利。</p>
<p>求赞助还是有点不要脸了,接下来是吐槽:</p>
<p>辛辛苦苦做这个也不知道是为了谁好累。其实还有很多功能可以做比如直接传token支持配置其实这个要专门做一个页面这个作为rc.4的计划之一吧。</p>
<p>主要没想做用户管理所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。</p>
<p>就说这么多,没啥可说的,不管那么多,做就完了。<span>[doge]</span> 自己想象吧。</p>
<p>为什么一直说要跑路呢主要是有时Cursor的Claude太假了堪比gpt-4o-mini我对比发现真没啥差别比以前差远了无力了所以不太想做了。我也感觉很奇怪。</p>
<p>查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。</p>
<p>有人说少个二维码来着,还是算了。如果觉得好用,给点支持。其实没啥大不了的,没兴趣就不做了。不想那么多了。</p>

View File

@@ -117,7 +117,7 @@ input[type="checkbox"] {
appearance: auto;
}
input[type="checkbox"] + label {
input[type="checkbox"]+label {
cursor: pointer;
color: var(--text-primary);
user-select: none;
@@ -217,7 +217,39 @@ button:disabled {
/* 次要按钮样式 */
button.secondary {
background: var(--text-secondary);
background: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
}
button.secondary:hover {
background: var(--primary-color-alpha);
border-color: var(--primary-dark);
color: var(--primary-dark);
}
button.danger {
background: var(--error-color);
border: none;
}
button.danger:hover {
background: #d32f2f;
/* 深红色 */
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.2);
}
/* 激活状态的按钮 */
button.active {
background: var(--primary-dark);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(1px);
}
button.secondary.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-dark);
}
/* 按钮组 */
@@ -227,22 +259,93 @@ button.secondary {
margin: var(--spacing) 0;
}
/* 消息提示 */
/* 按钮组中的按钮间距调整 */
.button-group button {
flex: 1;
min-width: 120px;
}
/* 消息容器 - 固定在顶部中间 */
.message-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
/* 允许点击穿透 */
}
/* 单个消息样式 */
.message {
padding: 12px;
border-radius: var(--border-radius);
margin: 10px 0;
border: 1px solid transparent;
padding: 12px 20px;
border-radius: 4px;
background: var(--card-background);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 10px;
pointer-events: auto;
/* 允许消息本身可以交互 */
min-width: 300px;
max-width: 500px;
display: flex;
align-items: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
animation: messageIn 0.3s ease-in-out;
}
.success {
background: var(--success-color);
color: #fff;
.message.success {
background: #f0f9eb;
border: 1px solid #e1f3d8;
}
.error {
background: var(--error-color);
color: #fff;
.message.error {
background: #fef0f0;
border: 1px solid #fde2e2;
}
@keyframes messageIn {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes messageOut {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.message {
background: #2c2c2c;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.message.success {
background: #294929;
border-color: #1c321c;
}
.message.error {
background: #4d2c2c;
border-color: #321c1c;
}
}
/* 表格样式 */

View File

@@ -24,19 +24,50 @@ function getAuthToken() {
// 消息显示功能
function showMessage(elementId, text, isError = false) {
const msg = document.getElementById(elementId);
msg.className = `message ${isError ? 'error' : 'success'}`;
msg.textContent = text;
let msg = document.getElementById(elementId);
// 如果消息元素不存在,创建一个新的
if (!msg) {
msg = document.createElement('div');
msg.id = elementId;
document.body.appendChild(msg);
}
msg.className = `floating-message ${isError ? 'error' : 'success'}`;
msg.innerHTML = text.replace(/\n/g, '<br>');
}
function showGlobalMessage(text, isError = false) {
showMessage('message', text, isError);
// 3秒后自动清除消息
// 确保消息容器存在
function ensureMessageContainer() {
let container = document.querySelector('.message-container');
if (!container) {
container = document.createElement('div');
container.className = 'message-container';
document.body.appendChild(container);
}
return container;
}
function showGlobalMessage(text, isError = false, timeout = 3000) {
const container = ensureMessageContainer();
const msgElement = document.createElement('div');
msgElement.className = `message ${isError ? 'error' : 'success'}`;
msgElement.textContent = text;
container.appendChild(msgElement);
// 设置淡出动画和移除
setTimeout(() => {
const msg = document.getElementById('message');
msg.textContent = '';
msg.className = 'message';
}, 3000);
msgElement.style.animation = 'messageOut 0.3s ease-in-out';
setTimeout(() => {
msgElement.remove();
// 如果容器为空,也移除容器
if (container.children.length === 0) {
container.remove();
}
}, 300);
}, timeout);
}
// Token 输入框自动填充和事件绑定

View File

@@ -1,127 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<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>
.token-container {
display: grid;
gap: var(--spacing);
}
.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);
}
.shortcuts {
margin-top: var(--spacing);
padding: 12px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
kbd {
background: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
padding: 1px 4px;
font-size: 12px;
}
</style>
</head>
<body>
<h1>Token 信息管理</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
</div>
<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>
</div>
<div id="message"></div>
<script>
function showMessage(text, isError = false) {
showGlobalMessage(text, isError);
}
async function getTokenInfo() {
const data = await makeAuthenticatedRequest('/get-tokeninfo');
if (data) {
document.getElementById('tokens').value = data.tokens;
document.getElementById('tokenList').value = data.token_list;
showGlobalMessage('配置获取成功');
}
}
async function updateTokenInfo() {
const tokens = document.getElementById('tokens').value;
const tokenList = document.getElementById('tokenList').value;
if (!tokens) {
showGlobalMessage('Token 文件内容不能为空', true);
return;
}
const data = await makeAuthenticatedRequest('/update-tokeninfo', {
body: JSON.stringify({
tokens: tokens,
token_list: tokenList || undefined
})
});
if (data) {
showGlobalMessage(`更新成功: ${data.message}`);
}
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
updateTokenInfo();
}
});
// 初始化 token 处理
initializeTokenHandling('authToken');
</script>
</body>
</html>

603
static/tokens.html Normal file
View File

@@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<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>
.token-container {
display: grid;
gap: var(--spacing);
}
.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);
}
.shortcuts {
margin-top: var(--spacing);
padding: 12px;
background: var(--disabled-bg);
border-radius: 4px;
font-size: 14px;
color: var(--text-secondary);
}
kbd {
background: var(--card-background);
border-radius: 3px;
border: 1px solid var(--border-color);
padding: 1px 4px;
font-size: 12px;
color: var(--text-primary);
}
.token-table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
background: var(--card-background);
}
.token-table th,
.token-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.token-table th {
background: var(--disabled-bg);
font-weight: 500;
color: var(--text-primary);
}
.token-list-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.token-list-header button {
padding: 4px 12px;
font-size: 14px;
}
/* Token表格样式优化 */
.token-table td {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-primary);
}
.token-table tr:hover {
background: var(--primary-color-alpha);
}
.token-table tr:hover td {
white-space: normal;
word-break: break-all;
}
/* 操作按钮样式 */
.action-cell {
width: 100px;
text-align: center !important;
}
.action-cell button {
padding: 2px 8px;
font-size: 12px;
white-space: nowrap;
}
/* 提示框样式 */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
width: 90%;
max-width: 500px;
color: var(--text-primary);
}
.modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.modal-header {
margin-bottom: 15px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
}
.modal-footer {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
text-align: right;
}
/* 复选框容器样式 */
.checkbox-container {
margin: 8px 0;
}
.checkbox-container label {
display: inline;
margin-left: 8px;
color: var(--text-primary);
}
/* 帮助文本样式 */
.help-text {
color: var(--text-secondary);
}
@media (prefers-color-scheme: dark) {
.modal {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.token-table tr:hover {
background: rgba(144, 202, 249, 0.1);
/* --primary-color in dark mode */
}
}
/* Key结果样式 */
.key-result {
background: var(--card-background);
padding: var(--spacing);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: var(--spacing);
position: relative;
cursor: pointer;
transition: all var(--transition-fast);
}
.key-result:hover {
background: var(--primary-color-alpha);
border-color: var(--primary-color);
}
.key-result:active {
transform: translateY(1px);
}
.key-content {
overflow-x: auto;
white-space: nowrap;
scrollbar-width: thin;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
/* Webkit浏览器的滚动条样式 */
.key-content::-webkit-scrollbar {
height: 6px;
}
.key-content::-webkit-scrollbar-track {
background: var(--disabled-bg);
border-radius: 3px;
}
.key-content::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.key-content::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
@media (prefers-color-scheme: dark) {
.key-content::-webkit-scrollbar-track {
background: var(--card-background);
}
.key-content::-webkit-scrollbar-thumb {
background: var(--text-secondary);
}
.key-content::-webkit-scrollbar-thumb:hover {
background: var(--text-primary);
}
}
.model-list {
max-height: 150px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-top: 8px;
background: var(--card-background);
}
.model-item {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.model-item input[type="checkbox"] {
margin-right: 8px;
}
</style>
</head>
<body>
<h1>Token 信息管理</h1>
<div class="container">
<div class="form-group">
<label>认证令牌:</label>
<input type="password" id="authToken" placeholder="输入 AUTH_TOKEN">
</div>
</div>
<div class="token-container">
<div class="token-section">
<h3>Token 管理</h3>
<div class="button-group">
<button onclick="getTokenInfo()">获取当前配置</button>
<button onclick="addTokens()" class="secondary">添加Token</button>
<button onclick="deleteTokens()" class="danger">删除Token</button>
</div>
<div class="form-group">
<label>Token 操作:</label>
<textarea id="tokenInput" placeholder="每行一个 token"></textarea>
<div class="help-text">添加模式: 输入要添加的token每行一个
删除模式: 输入要删除的token每行一个</div>
</div>
<div class="form-group">
<div class="token-list-header">
<label>当前Token列表:</label>
<button onclick="copyTokenList()" class="secondary">复制列表</button>
</div>
<table class="token-table">
<thead>
<tr>
<th>Token</th>
<th>Checksum</th>
<th class="action-cell">操作</th>
</tr>
</thead>
<tbody id="tokenTableBody">
</tbody>
</table>
</div>
<div class="shortcuts">
快捷键: <kbd>Ctrl</kbd> + <kbd>Enter</kbd> 执行当前操作
</div>
</div>
</div>
<div id="message"></div>
<!-- 动态key生成对话框 -->
<div class="modal-backdrop" id="keyModal-backdrop" onclick="closeKeyModal()"></div>
<div class="modal" id="keyModal">
<div class="modal-header">
<h3>生成动态Key</h3>
</div>
<div class="form-group">
<label>流第一个块检查:</label>
<select id="enableStreamCheck">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>包含停止流:</label>
<select id="includeStopStream">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>图片处理能力:</label>
<select id="disableVision">
<option value="">跟随全局</option>
<option value="true">禁用</option>
<option value="false">启用</option>
</select>
</div>
<div class="form-group">
<label>慢速池:</label>
<select id="enableSlowPool">
<option value="">跟随全局</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="form-group">
<label>使用量检查模型规则:</label>
<select id="usageCheckType" onchange="toggleModelList()">
<option value="">跟随全局</option>
<option value="default">默认</option>
<option value="disabled">禁用</option>
<option value="all">所有</option>
<option value="custom">自定义</option>
</select>
<div id="modelListContainer" class="model-list" style="display: none;">
<!-- 模型列表将通过 JavaScript 动态填充 -->
</div>
</div>
<div class="key-result" id="keyResult" style="display: none;" onclick="copyGeneratedKey()">
<div class="key-content" id="keyContent"></div>
</div>
<div class="modal-footer">
<button onclick="closeKeyModal()" class="secondary">取消</button>
<button onclick="generateKey()" class="primary">生成</button>
</div>
</div>
<script>
async function getTokenInfo() {
const data = await makeAuthenticatedRequest('/tokens/get');
if (data) {
const tableBody = document.getElementById('tokenTableBody');
tableBody.innerHTML = data.tokens.map(t => `<tr><td title="${t.token}">${t.token}</td><td title="${t.checksum}">${t.checksum}</td><td class="action-cell"><button onclick="showKeyModal('${t.token}','${t.checksum}')" class="secondary">生成Key</button></td></tr>`).join('');
showGlobalMessage('配置获取成功');
}
}
function copyTokenList() {
const tableBody = document.getElementById('tokenTableBody');
const rows = tableBody.getElementsByTagName('tr');
const tokenList = Array.from(rows).map(row => {
const token = row.cells[0].textContent;
const checksum = row.cells[1].textContent;
return `${token},${checksum}`;
}).join('\n');
navigator.clipboard.writeText(tokenList).then(() => {
showGlobalMessage('Token列表已复制到剪贴板');
}).catch(err => {
showGlobalMessage('复制失败: ' + err, true);
});
}
async function addTokens() {
const tokensInput = document.getElementById('tokenInput').value;
if (!tokensInput) {
showGlobalMessage('请输入要添加的Token', true);
return;
}
// 处理输入的tokens跳过空行和注释解析token和checksum
const tokenList = tokensInput.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'))
.map(line => {
const parts = line.includes(',') ? line.split(',') : [line];
return {
token: parts[0].trim(),
checksum: parts[1]?.trim() || null
};
});
if (tokenList.length === 0) {
showGlobalMessage('没有有效的Token输入', true);
return;
}
const data = await makeAuthenticatedRequest('/tokens/add', {
body: JSON.stringify(tokenList)
});
if (data) {
showGlobalMessage(`添加成功: ${data.message}`);
document.getElementById('tokenInput').value = '';
getTokenInfo(); // 刷新当前配置
}
}
async function deleteTokens() {
const tokensToDelete = document.getElementById('tokenInput').value;
if (!tokensToDelete) {
showGlobalMessage('请输入要删除的Token', true);
return;
}
const tokens = tokensToDelete.trim().split('\n').filter(t => t);
const data = await makeAuthenticatedRequest('/tokens/delete', {
body: JSON.stringify({
tokens: tokens,
expectation: 'detailed'
})
});
if (data) {
let message = '删除操作完成\n';
if (data.failed_tokens?.length) {
message += `\n未找到的Token: ${data.failed_tokens.join('\n')}`;
}
if (data.updated_tokens?.length) {
message += `\n剩余Token: ${data.updated_tokens.join('\n')}`;
}
showGlobalMessage(message, timeout = 30000);
document.getElementById('tokenInput').value = '';
getTokenInfo();
}
}
// 动态key相关函数
let availableModels = [];
let currentToken = '';
let currentChecksum = '';
async function getModels() {
try {
const response = await fetch('/v1/models');
const data = await response.json();
availableModels = data.data.map(model => model.id);
updateModelList();
} catch (error) {
showGlobalMessage('获取模型列表失败', true);
}
}
function updateModelList() {
const container = document.getElementById('modelListContainer');
container.innerHTML = availableModels.map(model => `<div class="model-item"><input type="checkbox" id="model_${model}" value="${model}"><label for="model_${model}">${model}</label></div>`).join('');
}
function toggleModelList() {
const type = document.getElementById('usageCheckType').value;
const container = document.getElementById('modelListContainer');
container.style.display = type === 'custom' ? 'block' : 'none';
}
function showKeyModal(token, checksum) {
currentToken = token;
currentChecksum = checksum;
const modal = document.getElementById('keyModal');
const backdrop = document.getElementById('keyModal-backdrop');
modal.style.display = 'block';
backdrop.style.display = 'block';
document.getElementById('keyResult').style.display = 'none';
// 添加点击事件处理
modal.addEventListener('click', function (event) {
event.stopPropagation(); // 防止点击modal内部时触发backdrop的点击事件
});
// 重置所有选项
document.getElementById('enableStreamCheck').value = '';
document.getElementById('includeStopStream').value = '';
document.getElementById('disableVision').value = '';
document.getElementById('enableSlowPool').value = '';
document.getElementById('usageCheckType').value = '';
document.getElementById('modelListContainer').style.display = 'none';
}
function closeKeyModal() {
document.getElementById('keyModal').style.display = 'none';
document.getElementById('keyModal-backdrop').style.display = 'none';
}
function parseBooleanFromString(value, defaultValue) {
if (value === '') return defaultValue;
return value === 'true';
}
async function generateKey() {
const type = document.getElementById('usageCheckType').value;
let modelIds = '';
if (type === 'custom') {
modelIds = Array.from(document.querySelectorAll('#modelListContainer input:checked'))
.map(input => input.value)
.join(',');
}
const payload = {
auth_token: `${currentToken},${currentChecksum}`,
enable_stream_check: parseBooleanFromString(document.getElementById('enableStreamCheck').value, undefined),
include_stop_stream: parseBooleanFromString(document.getElementById('includeStopStream').value, undefined),
disable_vision: parseBooleanFromString(document.getElementById('disableVision').value, undefined),
enable_slow_pool: parseBooleanFromString(document.getElementById('enableSlowPool').value, undefined),
usage_check_models: type ? {
type: type,
model_ids: type === 'custom' ? modelIds : undefined
} : undefined
};
const data = await makeAuthenticatedRequest('/build-key', {
body: JSON.stringify(payload)
});
if (data && data.key) {
const keyResult = document.getElementById('keyResult');
const keyContent = document.getElementById('keyContent');
keyContent.textContent = data.key;
keyResult.style.display = 'block';
showGlobalMessage('动态Key已生成点击复制');
}
}
function copyGeneratedKey(event) {
// 防止触发滚动条点击事件
if (event && event.target.classList.contains('key-content') && event.offsetX > event.target.clientWidth) {
return;
}
const keyContent = document.getElementById('keyContent').textContent;
navigator.clipboard.writeText(keyContent).then(() => {
showGlobalMessage('Key已复制到剪贴板');
}).catch(() => {
showGlobalMessage('复制失败', true);
});
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
const activeElement = document.activeElement;
if (activeElement.id === 'tokenInput') {
// 根据当前焦点确定操作
const action = document.querySelector('.button-group button.active');
if (action) {
action.click();
}
}
}
});
// 初始化 token 处理
initializeTokenHandling('authToken');
// 页面加载完成后获取当前配置和模型列表
document.addEventListener('DOMContentLoaded', () => {
getModels();
getTokenInfo();
});
</script>
</body>
</html>

View File

@@ -8,6 +8,8 @@ fn main() {
.unwrap();
let db_path = if cfg!(target_os = "windows") {
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\User\globalStorage\state.vscdb")
} else if cfg!(target_os = "linux") {
PathBuf::from(home_dir).join(".config/Cursor/User/globalStorage/state.vscdb")
} else {
PathBuf::from(home_dir)
.join("Library/Application Support/Cursor/User/globalStorage/state.vscdb")

273
tools/reset-telemetry/Cargo.lock generated Normal file
View File

@@ -0,0 +1,273 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "reset-telemetry"
version = "0.1.0"
dependencies = [
"rand",
"serde_json",
"sha2",
"uuid",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "uuid"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -0,0 +1,17 @@
[package]
name = "reset-telemetry"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
serde_json = "1.0.137"
sha2 = { version = "0.10.8", default-features = false }
uuid = { version = "1.12.1", features = ["v4"] }
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true
opt-level = 3

View File

@@ -0,0 +1,73 @@
use rand::RngCore;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::path::PathBuf;
use uuid::Uuid;
fn main() -> std::io::Result<()> {
// 获取用户主目录路径
let home_dir = env::var("HOME")
.or_else(|_| env::var("USERPROFILE"))
.unwrap();
// 构建storage.json的路径
let db_path = if cfg!(target_os = "windows") {
PathBuf::from(home_dir.clone())
.join(r"AppData\Roaming\Cursor\User\globalStorage\storage.json")
} else if cfg!(target_os = "linux") {
PathBuf::from(home_dir.clone()).join(".config/Cursor/User/globalStorage/storage.json")
} else {
PathBuf::from(home_dir.clone())
.join("Library/Application Support/Cursor/User/globalStorage/storage.json")
};
// 构建machineid文件的路径
let machine_id_path = if cfg!(target_os = "windows") {
PathBuf::from(home_dir).join(r"AppData\Roaming\Cursor\machineid")
} else if cfg!(target_os = "linux") {
PathBuf::from(home_dir).join(".config/Cursor/machineid")
} else {
PathBuf::from(home_dir).join("Library/Application Support/Cursor/machineid")
};
// 读取并更新storage.json
let mut content: Value = if db_path.exists() {
let content = fs::read_to_string(&db_path)?;
serde_json::from_str(&content)?
} else {
json!({})
};
// 生成新的遥测ID
content["telemetry.macMachineId"] = json!(generate_sha256_hash());
content["telemetry.sqmId"] = json!(generate_sqm_id());
content["telemetry.machineId"] = json!(generate_sha256_hash());
content["telemetry.devDeviceId"] = json!(generate_device_id());
// 写入更新后的storage.json
fs::write(&db_path, serde_json::to_string_pretty(&content)?)?;
// 更新machineid文件
fs::write(&machine_id_path, generate_device_id())?;
println!("遥测ID已重置成功");
Ok(())
}
fn generate_sha256_hash() -> String {
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 32];
rng.fill_bytes(&mut bytes);
let hash = Sha256::digest(&bytes);
format!("{:x}", hash)
}
fn generate_sqm_id() -> String {
format!("{{{}}}", Uuid::new_v4().to_string().to_uppercase())
}
fn generate_device_id() -> String {
Uuid::new_v4().to_string()
}

58
worker.js Normal file
View File

@@ -0,0 +1,58 @@
addEventListener('fetch', e => {
e.respondWith(handleRequest(e.request));
});
async function handleRequest(request) {
try {
// 获取目标主机
const targetHost = request.headers.get("x-co");
// 允许的主机和路径列表
const allowedHosts = ["api2.cursor.sh", "www.cursor.com"];
const allowedPaths = [
"/aiserver.v1.AiService/StreamChat",
"/auth/full_stripe_profile",
"/api/usage",
"/api/auth/me"
];
const url = new URL(request.url);
// 验证请求
if (!targetHost || !allowedHosts.includes(targetHost) || !allowedPaths.includes(url.pathname)) {
return new Response(null, { status: 403 });
}
// 处理请求头
const headers = new Headers(request.headers);
headers.delete("x-co");
headers.set("Host", targetHost);
// 转发请求
const response = await fetch(
`https://${targetHost}${url.pathname}${url.search}`,
{
method: request.method,
headers: headers,
body: request.body
}
);
// 处理响应
const responseHeaders = new Headers(response.headers);
responseHeaders.set("Access-Control-Allow-Origin", "*");
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
// 错误处理
console.error('Request failed:', error);
return new Response("Internal Server Error", {
status: 500,
headers: { "Content-Type": "text/plain" }
});
}
}