mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-10-05 14:46:53 +08:00
v0.1.3-rc.3.2
This commit is contained in:
@@ -6,9 +6,12 @@ PORT=3000
|
||||
# 路由前缀,必须以 / 开头(如果不为空)
|
||||
ROUTE_PREFIX=
|
||||
|
||||
# 认证令牌,必填
|
||||
# 最高权限的认证令牌,必填
|
||||
AUTH_TOKEN=
|
||||
|
||||
# 共享的认证令牌,仅Chat端点权限(轮询与AUTH_TOKEN同步),无其余权限
|
||||
SHARED_AUTH_TOKEN=
|
||||
|
||||
# 启用流式响应检查,关闭则无法响应错误,代价是会对第一个块解析2次
|
||||
ENABLE_STREAM_CHECK=true
|
||||
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -327,7 +327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cursor-api"
|
||||
version = "0.1.3-rc.3"
|
||||
version = "0.1.3-rc.3.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cursor-api"
|
||||
version = "0.1.3-rc.3"
|
||||
version = "0.1.3-rc.3.2"
|
||||
edition = "2021"
|
||||
authors = ["wisdgod <nav@wisdgod.com>"]
|
||||
description = "OpenAI format compatibility layer for the Cursor API"
|
||||
|
@@ -14,8 +14,10 @@ def_pub_const!(EMPTY_STRING, "");
|
||||
|
||||
def_pub_const!(ROUTE_ROOT_PATH, "/");
|
||||
def_pub_const!(ROUTE_HEALTH_PATH, "/health");
|
||||
def_pub_const!(ROUTE_GET_HASH, "/get-hash");
|
||||
def_pub_const!(ROUTE_GET_CHECKSUM, "/get-checksum");
|
||||
def_pub_const!(ROUTE_GET_USER_INFO_PATH, "/get-userinfo");
|
||||
def_pub_const!(ROUTE_GET_TIMESTAMP_HEADER, "/get-tsheader");
|
||||
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");
|
||||
|
@@ -51,6 +51,10 @@ def_pub_static!(DEFAULT_INSTRUCTIONS, env: "DEFAULT_INSTRUCTIONS", default: "Res
|
||||
|
||||
def_pub_static!(REVERSE_PROXY_HOST, env: "REVERSE_PROXY_HOST", default: "");
|
||||
|
||||
def_pub_static!(SHARED_AUTH_TOKEN, env: "SHARED_AUTH_TOKEN", default: EMPTY_STRING);
|
||||
|
||||
pub static USE_SHARE: LazyLock<bool> = LazyLock::new(|| !SHARED_AUTH_TOKEN.is_empty());
|
||||
|
||||
pub static USE_PROXY: LazyLock<bool> = LazyLock::new(|| !REVERSE_PROXY_HOST.is_empty());
|
||||
|
||||
pub static CURSOR_API2_CHAT_URL: LazyLock<String> = LazyLock::new(|| {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,12 @@ mod health;
|
||||
pub use health::{handle_health, handle_root};
|
||||
mod token;
|
||||
pub use token::{
|
||||
handle_basic_calibration, handle_get_checksum, handle_get_tokeninfo, handle_tokeninfo_page,
|
||||
handle_update_tokeninfo, handle_update_tokeninfo_post,
|
||||
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 profile;
|
||||
pub use profile::get_user_info;
|
||||
pub use profile::handle_user_info;
|
||||
mod config;
|
||||
pub use config::{
|
||||
handle_about, handle_config_page, handle_env_example, handle_readme, handle_static,
|
||||
|
@@ -74,8 +74,8 @@ pub async fn handle_static(Path(path): Path<String>) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_about() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_ABOUT_PATH).unwrap_or_default() {
|
||||
pub async fn handle_readme() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.header(CONTENT_TYPE, CONTENT_TYPE_TEXT_HTML_WITH_UTF8)
|
||||
.body(include_str!("../../../static/readme.min.html").to_string())
|
||||
@@ -91,11 +91,11 @@ pub async fn handle_about() -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_readme() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_README_PATH).unwrap_or_default() {
|
||||
pub async fn handle_about() -> impl IntoResponse {
|
||||
match AppConfig::get_page_content(ROUTE_ABOUT_PATH).unwrap_or_default() {
|
||||
PageContent::Default => Response::builder()
|
||||
.status(StatusCode::TEMPORARY_REDIRECT)
|
||||
.header(LOCATION, ROUTE_ABOUT_PATH)
|
||||
.header(LOCATION, ROUTE_README_PATH)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
PageContent::Text(content) => Response::builder()
|
||||
|
@@ -4,9 +4,10 @@ use crate::{
|
||||
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_TOKENINFO_PATH, ROUTE_GET_USER_INFO_PATH,
|
||||
ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH,
|
||||
ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_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,
|
||||
},
|
||||
lazy::{get_start_time, AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
|
||||
model::{AppConfig, AppState, PageContent},
|
||||
@@ -115,7 +116,6 @@ pub async fn handle_health(
|
||||
endpoints: vec![
|
||||
ROUTE_CHAT_PATH.as_str(),
|
||||
ROUTE_MODELS_PATH.as_str(),
|
||||
ROUTE_GET_CHECKSUM,
|
||||
ROUTE_TOKENINFO_PATH,
|
||||
ROUTE_UPDATE_TOKENINFO_PATH,
|
||||
ROUTE_GET_TOKENINFO_PATH,
|
||||
@@ -125,9 +125,12 @@ pub async fn handle_health(
|
||||
ROUTE_STATIC_PATH,
|
||||
ROUTE_ABOUT_PATH,
|
||||
ROUTE_README_PATH,
|
||||
ROUTE_BASIC_CALIBRATION_PATH,
|
||||
ROUTE_GET_USER_INFO_PATH,
|
||||
ROUTE_API_PATH,
|
||||
ROUTE_GET_HASH,
|
||||
ROUTE_GET_CHECKSUM,
|
||||
ROUTE_GET_TIMESTAMP_HEADER,
|
||||
ROUTE_BASIC_CALIBRATION_PATH,
|
||||
ROUTE_USER_INFO_PATH,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ use axum::Json;
|
||||
|
||||
use super::token::TokenRequest;
|
||||
|
||||
pub async fn get_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> {
|
||||
pub async fn handle_user_info(Json(request): Json<TokenRequest>) -> Json<GetUserInfo> {
|
||||
let auth_token = match request.token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
|
@@ -10,7 +10,7 @@ use crate::{
|
||||
common::{
|
||||
models::{ApiStatus, NormalResponseNoData},
|
||||
utils::{
|
||||
extract_time, extract_time_ks, extract_user_id, generate_checksum_with_default, generate_checksum_with_repair, load_tokens, validate_token_and_checksum
|
||||
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
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -28,24 +28,49 @@ 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, alias = "checksum")]
|
||||
pub bad_checksum: Option<String>,
|
||||
#[serde(default)]
|
||||
pub checksum: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChecksumResponse {
|
||||
pub checksum: 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_checksum(
|
||||
Query(query): Query<ChecksumQuery>
|
||||
) -> Json<ChecksumResponse> {
|
||||
match query.bad_checksum {
|
||||
None => Json(ChecksumResponse { checksum: generate_checksum_with_default() }),
|
||||
Some(bad_checksum) => Json(ChecksumResponse { checksum: generate_checksum_with_repair(&bad_checksum) })
|
||||
}
|
||||
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 处理
|
||||
|
@@ -1,11 +1,10 @@
|
||||
use crate::{
|
||||
app::{
|
||||
constant::{
|
||||
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP,
|
||||
OBJECT_CHAT_COMPLETION, OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING,
|
||||
STATUS_SUCCESS,
|
||||
AUTHORIZATION_BEARER_PREFIX, FINISH_REASON_STOP, OBJECT_CHAT_COMPLETION,
|
||||
OBJECT_CHAT_COMPLETION_CHUNK, STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS,
|
||||
},
|
||||
lazy::AUTH_TOKEN,
|
||||
lazy::{AUTH_TOKEN, SHARED_AUTH_TOKEN, USE_SHARE},
|
||||
model::{AppConfig, AppState, ChatRequest, RequestLog, TimingInfo, TokenInfo},
|
||||
},
|
||||
chat::{
|
||||
@@ -19,9 +18,7 @@ use crate::{
|
||||
common::{
|
||||
client::build_client,
|
||||
models::{error::ChatError, userinfo::MembershipType, ErrorResponse},
|
||||
utils::{
|
||||
format_time_ms, get_token_profile, validate_token_and_checksum,
|
||||
},
|
||||
utils::{format_time_ms, get_token_profile, validate_token_and_checksum},
|
||||
},
|
||||
};
|
||||
use axum::{
|
||||
@@ -95,29 +92,33 @@ pub async fn handle_chat(
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
))?;
|
||||
|
||||
// 验证 AuthToken 和 获取 token 信息
|
||||
let (auth_token, checksum) = if auth_header == AUTH_TOKEN.as_str() {
|
||||
// 如果是管理员Token,使用原有逻辑
|
||||
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
let state_guard = state.lock().await;
|
||||
let token_infos = &state_guard.token_infos;
|
||||
// 验证认证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()) => {
|
||||
static CURRENT_KEY_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
let state_guard = state.lock().await;
|
||||
let token_infos = &state_guard.token_infos;
|
||||
|
||||
if token_infos.is_empty() {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(ChatError::NoTokens.to_json()),
|
||||
));
|
||||
}
|
||||
// 检查是否存在可用的token
|
||||
if token_infos.is_empty() {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(ChatError::NoTokens.to_json()),
|
||||
));
|
||||
}
|
||||
|
||||
let index = CURRENT_KEY_INDEX.fetch_add(1, Ordering::SeqCst) % token_infos.len();
|
||||
let token_info = &token_infos[index];
|
||||
(token_info.token.clone(), token_info.checksum.clone())
|
||||
} else {
|
||||
// 否则尝试解析token
|
||||
validate_token_and_checksum(auth_header).ok_or((
|
||||
// 轮询选择token
|
||||
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验证逻辑
|
||||
token => validate_token_and_checksum(token).ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ChatError::Unauthorized.to_json()),
|
||||
))?
|
||||
))?,
|
||||
};
|
||||
|
||||
let current_id: u64;
|
||||
@@ -352,7 +353,8 @@ pub async fn handle_chat(
|
||||
{
|
||||
log.status = STATUS_FAILED;
|
||||
log.error = Some(error_respone.native_code());
|
||||
log.timing.total = format_time_ms(start_time.elapsed().as_secs_f64());
|
||||
log.timing.total =
|
||||
format_time_ms(start_time.elapsed().as_secs_f64());
|
||||
state.error_requests += 1;
|
||||
}
|
||||
}
|
||||
@@ -432,7 +434,8 @@ pub async fn handle_chat(
|
||||
// 记录首字时间(如果还未记录)
|
||||
if let Ok(mut first_time) = first_chunk_time.try_lock() {
|
||||
if first_time.is_none() {
|
||||
*first_time = Some(format_time_ms(start_time.elapsed().as_secs_f64()));
|
||||
*first_time =
|
||||
Some(format_time_ms(start_time.elapsed().as_secs_f64()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -131,3 +131,79 @@ pub fn parse_stream_data(data: &[u8]) -> Result<StreamMessage, StreamError> {
|
||||
Ok(StreamMessage::Content(messages))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_stream_data() {
|
||||
// 使用include_str!加载测试数据文件
|
||||
let stream_data = include_str!("../../tests/data/stream_data.txt");
|
||||
|
||||
// 将整个字符串按每两个字符分割成字节
|
||||
let bytes: Vec<u8> = stream_data
|
||||
.as_bytes()
|
||||
.chunks(2)
|
||||
.map(|chunk| {
|
||||
let hex_str = std::str::from_utf8(chunk).unwrap();
|
||||
u8::from_str_radix(hex_str, 16).unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 辅助函数:找到下一个消息边界
|
||||
fn find_next_message_boundary(bytes: &[u8]) -> usize {
|
||||
if bytes.len() < 5 {
|
||||
return bytes.len();
|
||||
}
|
||||
let msg_len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
|
||||
5 + msg_len
|
||||
}
|
||||
|
||||
// 辅助函数:将字节转换为hex字符串
|
||||
fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
bytes.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
// 多次解析数据
|
||||
let mut offset = 0;
|
||||
while offset < bytes.len() {
|
||||
let remaining_bytes = &bytes[offset..];
|
||||
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 {
|
||||
StreamMessage::Content(messages) => {
|
||||
print!("消息内容 [hex: {}]:", hex_str);
|
||||
for msg in messages {
|
||||
println!(" {}", msg);
|
||||
}
|
||||
offset += msg_boundary;
|
||||
}
|
||||
StreamMessage::Debug(_) => {
|
||||
// println!("调试信息 [hex: {}]: {}", hex_str, prompt);
|
||||
offset += msg_boundary;
|
||||
}
|
||||
StreamMessage::StreamEnd => {
|
||||
println!("流结束 [hex: {}]", hex_str);
|
||||
break;
|
||||
}
|
||||
StreamMessage::StreamStart => {
|
||||
println!("流开始 [hex: {}]", hex_str);
|
||||
offset += msg_boundary;
|
||||
}
|
||||
StreamMessage::Incomplete => {
|
||||
println!("数据不完整 [hex: {}]", hex_str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("解析错误 [hex: {}]: {}", hex_str, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ use reqwest::header::{
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::utils::generate_hash;
|
||||
|
||||
macro_rules! def_const {
|
||||
($name:ident, $value:expr) => {
|
||||
const $name: &'static str = $value;
|
||||
@@ -74,7 +76,7 @@ pub fn build_client(auth_token: &str, checksum: &str) -> RequestBuilder {
|
||||
.header("connect-protocol-version", ONE)
|
||||
.header(USER_AGENT, "connect-es/1.6.1")
|
||||
.header("x-amzn-trace-id", format!("Root={}", trace_id))
|
||||
// .header("x-client-key", client_key)
|
||||
.header("x-client-key", generate_hash())
|
||||
.header("x-cursor-checksum", checksum)
|
||||
.header("x-cursor-client-version", "0.42.5")
|
||||
.header("x-cursor-timezone", "Asia/Shanghai")
|
||||
|
@@ -2,7 +2,7 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn generate_hash() -> String {
|
||||
pub fn generate_hash() -> String {
|
||||
let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(random_bytes);
|
||||
@@ -27,7 +27,7 @@ fn deobfuscate_bytes(bytes: &mut [u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
|
||||
pub fn generate_timestamp_header() -> String {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -44,8 +44,11 @@ fn generate_checksum(device_id: &str, mac_addr: Option<&str>) -> String {
|
||||
];
|
||||
|
||||
obfuscate_bytes(&mut timestamp_bytes);
|
||||
let encoded = BASE64.encode(×tamp_bytes);
|
||||
BASE64.encode(×tamp_bytes)
|
||||
}
|
||||
|
||||
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),
|
||||
None => format!("{}{}", encoded, device_id),
|
||||
@@ -56,10 +59,10 @@ pub fn generate_checksum_with_default() -> String {
|
||||
generate_checksum(&generate_hash(), Some(&generate_hash()))
|
||||
}
|
||||
|
||||
pub fn generate_checksum_with_repair(bad_checksum: &str) -> String {
|
||||
pub fn generate_checksum_with_repair(checksum: &str) -> String {
|
||||
// 预校验:检查字符串是否为空或只包含合法的Base64字符和'/'
|
||||
if bad_checksum.is_empty()
|
||||
|| !bad_checksum
|
||||
if checksum.is_empty()
|
||||
|| !checksum
|
||||
.chars()
|
||||
.all(|c| (c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '='))
|
||||
{
|
||||
@@ -101,14 +104,14 @@ pub fn generate_checksum_with_repair(bad_checksum: &str) -> String {
|
||||
None
|
||||
}
|
||||
|
||||
if bad_checksum.len() == 8 {
|
||||
if checksum.len() == 8 {
|
||||
// 尝试修复时间戳头
|
||||
if let Some(fixed_timestamp) = try_fix_timestamp(bad_checksum) {
|
||||
if let Some(fixed_timestamp) = try_fix_timestamp(checksum) {
|
||||
return format!("{}{}/{}", fixed_timestamp, generate_hash(), generate_hash());
|
||||
}
|
||||
|
||||
// 验证原始时间戳
|
||||
if let Some(timestamp) = extract_time_ks(bad_checksum) {
|
||||
if let Some(timestamp) = extract_time_ks(checksum) {
|
||||
let current_timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -116,16 +119,16 @@ pub fn generate_checksum_with_repair(bad_checksum: &str) -> String {
|
||||
/ 1_000;
|
||||
|
||||
if timestamp <= current_timestamp {
|
||||
return format!("{}{}/{}", bad_checksum, generate_hash(), generate_hash());
|
||||
return format!("{}{}/{}", checksum, generate_hash(), generate_hash());
|
||||
}
|
||||
}
|
||||
} else if bad_checksum.len() > 8 {
|
||||
} else if checksum.len() > 8 {
|
||||
// 处理可能包含hash的情况
|
||||
let parts: Vec<&str> = bad_checksum.split('/').collect();
|
||||
let parts: Vec<&str> = checksum.split('/').collect();
|
||||
match parts.len() {
|
||||
1 => {
|
||||
let timestamp_base64 = &bad_checksum[..8];
|
||||
let device_id = &bad_checksum[8..];
|
||||
let timestamp_base64 = &checksum[..8];
|
||||
let device_id = &checksum[8..];
|
||||
|
||||
if is_valid_hash(device_id) {
|
||||
// 先尝试修复时间戳
|
||||
@@ -144,7 +147,7 @@ pub fn generate_checksum_with_repair(bad_checksum: &str) -> String {
|
||||
if timestamp <= current_timestamp {
|
||||
return format!(
|
||||
"{}{}/{}",
|
||||
timestamp_base64,
|
||||
generate_timestamp_header(),
|
||||
device_id,
|
||||
generate_hash()
|
||||
);
|
||||
@@ -175,7 +178,12 @@ pub fn generate_checksum_with_repair(bad_checksum: &str) -> String {
|
||||
/ 1_000;
|
||||
|
||||
if timestamp <= current_timestamp {
|
||||
return bad_checksum.to_string();
|
||||
return format!(
|
||||
"{}{}/{}",
|
||||
generate_timestamp_header(),
|
||||
device_id,
|
||||
mac_hash
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -59,10 +59,14 @@ pub fn load_tokens() -> Vec<TokenInfo> {
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') || !validate_token(line) {
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
return None;
|
||||
}
|
||||
parse_token(line)
|
||||
let parsed = parse_token(line);
|
||||
if parsed.is_none() || !validate_token(&parsed.as_ref().unwrap()) {
|
||||
return None;
|
||||
}
|
||||
parsed
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
18
src/main.rs
18
src/main.rs
@@ -5,10 +5,7 @@ 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_TOKENINFO_PATH,
|
||||
ROUTE_GET_USER_INFO_PATH, ROUTE_HEALTH_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH,
|
||||
ROUTE_ROOT_PATH, ROUTE_STATIC_PATH, ROUTE_TOKENINFO_PATH, ROUTE_UPDATE_TOKENINFO_PATH,
|
||||
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
|
||||
},
|
||||
lazy::{AUTH_TOKEN, ROUTE_CHAT_PATH, ROUTE_MODELS_PATH},
|
||||
model::*,
|
||||
@@ -19,10 +16,7 @@ use axum::{
|
||||
};
|
||||
use chat::{
|
||||
route::{
|
||||
get_user_info, handle_about, handle_api_page, handle_basic_calibration, handle_config_page,
|
||||
handle_env_example, handle_get_checksum, 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_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
|
||||
},
|
||||
service::{handle_chat, handle_models},
|
||||
};
|
||||
@@ -73,7 +67,6 @@ async fn main() {
|
||||
.route(ROUTE_HEALTH_PATH, get(handle_health))
|
||||
.route(ROUTE_TOKENINFO_PATH, get(handle_tokeninfo_page))
|
||||
.route(ROUTE_MODELS_PATH.as_str(), get(handle_models))
|
||||
.route(ROUTE_GET_CHECKSUM, get(handle_get_checksum))
|
||||
.route(ROUTE_UPDATE_TOKENINFO_PATH, get(handle_update_tokeninfo))
|
||||
.route(ROUTE_GET_TOKENINFO_PATH, post(handle_get_tokeninfo))
|
||||
.route(
|
||||
@@ -89,9 +82,12 @@ async fn main() {
|
||||
.route(ROUTE_STATIC_PATH, get(handle_static))
|
||||
.route(ROUTE_ABOUT_PATH, get(handle_about))
|
||||
.route(ROUTE_README_PATH, get(handle_readme))
|
||||
.route(ROUTE_BASIC_CALIBRATION_PATH, post(handle_basic_calibration))
|
||||
.route(ROUTE_GET_USER_INFO_PATH, post(get_user_info))
|
||||
.route(ROUTE_API_PATH, get(handle_api_page))
|
||||
.route(ROUTE_GET_HASH, get(handle_get_hash))
|
||||
.route(ROUTE_GET_CHECKSUM, get(handle_get_checksum))
|
||||
.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))
|
||||
.layer(RequestBodyLimitLayer::new(
|
||||
1024 * 1024 * parse_usize_from_env("REQUEST_BODY_LIMIT_MB", 2),
|
||||
))
|
||||
|
@@ -287,7 +287,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const result = await makeTokenRequest('/get-userinfo', token);
|
||||
const result = await makeTokenRequest('/userinfo', token);
|
||||
if (result) {
|
||||
const container = document.getElementById('userInfoContainer');
|
||||
container.style.display = 'block';
|
||||
|
@@ -153,13 +153,15 @@ gemini-2.0-flash-exp
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": number, // 0
|
||||
"completion_tokens": number, // 0
|
||||
"total_tokens": number // 0
|
||||
"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}]}
|
||||
@@ -372,6 +374,16 @@ data: [DONE]
|
||||
}
|
||||
</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>
|
||||
@@ -379,23 +391,30 @@ data: [DONE]
|
||||
<li>请求方法: GET</li>
|
||||
<li>请求参数:
|
||||
<ul>
|
||||
<li><code>bad_checksum</code>: 可选,用于修复的旧版本生成的checksum,也可只传入前8个字符,可用别名checksum</li>
|
||||
<li><code>checksum</code>: 可选,用于修复的旧版本生成的checksum,也可只传入前8个字符;可用来自动刷新时间戳头</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>响应格式:</li>
|
||||
</ul>
|
||||
|
||||
<pre><code class="language-json">{
|
||||
"checksum": "string"
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code class="language-plaintext">string</code></pre>
|
||||
|
||||
<p>说明:</p>
|
||||
<ul>
|
||||
<li>如果不提供<code>bad_checksum</code>参数,将生成一个新的随机checksum</li>
|
||||
<li>如果提供<code>bad_checksum</code>参数,将尝试修复旧版本的checksum以适配当前版本(v0.1.3-rc.3)使用,修复失败会返回新的checksum</li>
|
||||
<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>
|
||||
@@ -508,7 +527,7 @@ data: [DONE]
|
||||
<h3>获取用户信息</h3>
|
||||
|
||||
<ul>
|
||||
<li>接口地址: <code>/get-userinfo</code></li>
|
||||
<li>接口地址: <code>/userinfo</code></li>
|
||||
<li>请求方法: POST</li>
|
||||
<li>认证方式: 请求体中包含token</li>
|
||||
<li>请求格式:</li>
|
||||
@@ -612,7 +631,7 @@ data: [DONE]
|
||||
|
||||
<p>求赞助还是有点不要脸了,接下来是吐槽:</p>
|
||||
|
||||
<p>辛辛苦苦做这个也不知道是为了谁,好累。其实还有很多功能可以做,比如直接传token支持配置(其实这个要专门做一个页面)。</p>
|
||||
<p>辛辛苦苦做这个也不知道是为了谁,好累。其实还有很多功能可以做,比如直接传token支持配置(其实这个要专门做一个页面),这个作为rc.4的计划之一吧。</p>
|
||||
|
||||
<p>主要没想做用户管理,所以不存在是否接入LinuxDo的问题。虽然那个半成品公益版做好了就是了。</p>
|
||||
|
||||
@@ -620,4 +639,6 @@ data: [DONE]
|
||||
|
||||
<p>为什么一直说要跑路呢?主要是有时Cursor的Claude太假了,堪比gpt-4o-mini,我对比发现真没啥差别,比以前差远了,无力了,所以不太想做了。我也感觉很奇怪。</p>
|
||||
|
||||
<p>查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。</p>
|
||||
<p>查询额度会在一开始检测导致和完成时的额度有些差别,但是懒得改了,反正差别不大,对话也没响应内容,恰好完成了统一。</p>
|
||||
|
||||
<p>有人说少个二维码来着,还是算了。如果觉得好用,给点支持。其实没啥大不了的,没兴趣就不做了。不想那么多了。</p>
|
1
tests/data/stream_data.txt
Normal file
1
tests/data/stream_data.txt
Normal file
@@ -0,0 +1 @@
|
||||
01000005231F8B080000000000000385553D8F1C451025EE981F509060A3F5AC6C0C81395932F6D95C6063F9CEB21CF96A676A669AEDA91EF7C7EE0DBE937046CE0F3004C4C40810093FE50422E22FA0EA9EB9DD8593894E37DBF5EAEBBD57EFBFF3E1EFEFEE9D7EB6FFE0E0D18BC3E78747FB0F4F6FABE736023A0264D01CC818DD1007E89D6D1C761DB919F4764D8E2A580CF0E0F1D1B59B163ACDBA8029B4C5BE1F205868C9F480ECD7E400798097917CD0963D8416038496207A72D0A2872BD147346690AF03ACB531B020C0858D014A5B696EAE164A5D2FE0594BBC09D51ED02F3537505B0754E9E0257368493B89A319F486D013D818FA1800C1EBAE37BAD654C18A9CD796C1D60951DEC3C2D872990B6C75D31ADDB482293FB7C80D79602AC97B740320578055E5A1B45D479C736BAE74898160DD9223889CE3AA0C2FBD2E8818FC52F73D5505DC97C24FB0EB0DDD52C7C7C706B989D8D0AD1E433B0F765E6B436A3E87A228804EB40FD26E022B8A42BD7A95DA7E711DCECEFEFFD58DB7BE3A3E3E5647D3684B64F044A975E2A01D815432036FF3927A47353969D9B219C0115679317D8581A63D24F402BEA843DA9BF679B91D216F68E003BA3027AEA65548A60B168CA39AC1228614F38107BBC4E13D78426BA7C37F6ACC05E91A7C4FA5AE759998E5281150667EC7AC71F0C2EA95AE0810164E530D74D21B640C5B9C189B99416443DE6F987719B4CF892FBA56EA4601F72CB00D60348175D0E15240A1C632F842A98F0A38A801336627BC128A0D368266406135E9866122C5059D1DF9DE7225AFD214A70785523713A73A0C02E3C6979EE469876E59D935174A7D3C4A4906282410A131ADB744E02F92E55E7363532238B807580721402BD83A6834B0C07219748A357A49E0ED2D105AF543682D0B0D13FED959E29AFAE49222B60A48A246DE30351370AC0A8DB73BA5A5D58B6ADE565812AD231F70E44D47A1B515CCA134E87D9E99D4908D6041C6722364DE74B4A3526F3B9ADBD092CB42AD23978940770EEEB6183ED73E58375CB90AAF14002499C9DFAD494C9FF34CF64EF71FDDDBF8B19A1CFAE9E1FE93D3DBEAF156EFB535C6AEA589B45D1F5C2CB3BF0A778C110EEF50C08B261C195A2107D167974CD90D053CB2C044957CC472C9766DA86AE832EC4A3B2A83E88B77D10BB557461F6CF762FBFD6DF56443D5BBAD66015C0C50518DD104A5F61E3E3D3CBA682C7ACAA81D9EE82E7662AD7D0C499368F2ED492B0C76490C46773A649A80D7DC18DA940347ADE625B4E82AF26196A0D3C2AD0F1074470927FD979350B62C09AA887AF2A150EA482C4BFB4D64EF6C6DA3383F5729D0D0C9C569836B133B276F49CDC825211FE06544A3C3301E45A9901C89C66749F2D306881B5198E654472E49733393DB423D71252779F3B514F59468B63EA5DA1C61D02BBAF85C28F58CA08FCE47023A098E3A828AFAD0824361B05809838F3DB95A974936E2EAA1FD14D624C76009E4BD182D1A2145BE8DBBB1AEC692AE195A9101E2D8914BBB4B08636ACD6C57BBA5ED80B4B8D021A20147A5CEAB2F60E4871424E7D9D9D8B42047174D66417AE767D0D98536FA2B4A0A18D5BC4D2147DE46578AA7CB942AEAACD055EC20BD0E2E8A7D37621C2BCAD8E2D4F7AD6B68F4D4DE9A3C723340699D08426E18973403479D5D4D58537B65CEA1398C6917E8A902CBD0C60E1918437499904234DBA7B17103BDD35CEADE909F2ED2DA3A53CDB66E97233429D22EBEA432153DF24BEDCDB3B8F6E6972A73349BD15AFE6D354A9DFFFADD9F6F7EF8EBF5B7E73F7F7DFED337E7BF7DFFC7EB1FFFFEE5CD6EDC3FD3E84DBB490A0000000000000000000000050A03E6889100000000080A06E68BA5E69C8900000000080A06E585B3E4BA8E00000000050A03E7BC9600000000050A03E7A88B00000000050A03E3808100000000080A06E8BDAFE4BBB600000000080A06E5BC80E58F9100000000050A03E3808100000000080A06E7AE97E6B39500000000050A03E3808100000000080A06E695B0E68DAE00000000080A06E7BB93E69E8400000000050A03E3808100000000080A06E69CBAE599A800000000080A06E5ADA6E4B9A000000000050A03E3808100000000080A06E4BABAE5B7A500000000080A06E699BAE883BD00000000050A03E7AD8900000000080A06E5A49AE4B8AA00000000080A06E9A286E59F9F000000000B0A09E79A84E4BFA1E681AF00000000050A03E3808200000000080A06E68891E79A8400000000080A06E79FA5E8AF8600000000050A03E6B6B500000000050A03E79B9600000000050A03E4BA8600000000050A03E5A49A00000000050A03E7A78D00000000050A03E7BC9600000000050A03E7A88B00000000080A06E8AFADE8A88000000000050A03EFBC8800000000050A03E5A68200000000080A06507974686F6E00000000050A03E3808100000000060A044A61766100000000050A03E3808100000000060A044A61766100000000080A0653637269707400000000050A03E3808100000000030A014300000000040A022B2B00000000050A03E7AD8900000000080A06EFBC89E3808100000000080A06E5BC80E58F9100000000050A03E6A18600000000050A03E69EB600000000050A03E3808100000000080A06E5B7A5E585B700000000050A03E5928C00000000080A06E69C80E4BDB300000000080A06E5AE9EE8B7B5000000000B0A09E38082E6ADA4E5A49600000000080A06EFBC8CE6889100000000050A03E8BF9800000000080A06E58FAFE4BBA500000000080A06E68F90E4BE9B00000000080A06E585B3E4BA8E00000000080A06E8AEA1E7AE9700000000050A03E69CBA00000000080A06E7A791E5ADA600000000050A03E79A8400000000080A06E79086E8AEBA00000000080A06E79FA5E8AF8600000000050A03E3808100000000080A06E68A80E69CAF00000000080A06E8B68BE58ABF00000000050A03E5928C00000000080A06E8A18CE4B89A00000000080A06E58AA8E68081000000000B0A09E79A84E4BFA1E681AF00000000070A05E380820A0A00000000080A06E5A682E69E9C00000000050A03E4BDA000000000050A03E69C8900000000080A06E585B7E4BD93000000000B0A09E79A84E997AEE9A29800000000050A03E6889600000000080A06E99C80E8A68100000000080A06E5B8AEE58AA900000000050A03E79A8400000000080A06E59CB0E696B900000000080A06EFBC8CE8AFB700000000080A06E5918AE8AF8900000000050A03E6889100000000080A06EFBC8CE6889100000000050A03E4BC9A00000000050A03E5B0BD00000000050A03E58A9B00000000080A06E68F90E4BE9B00000000080A06E8AFA6E7BB8600000000050A03E5928C00000000080A06E6B7B1E585A500000000050A03E79A8400000000050A03E8A7A300000000050A03E7AD9400000000050A03E3808202000000027B7D
|
Reference in New Issue
Block a user