mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
refactor: 迁移 server 模块到 proxycast-server crate
- 创建 proxycast-server crate,包含完整的 HTTP 服务器逻辑 - 迁移 server/handlers/ 下所有 8 个处理器模块 - 迁移 client_detector 模块 - 主 crate server/mod.rs 替换为 re-export 层 - 升级 proxycast-core logger:完整的日志轮转、压缩归档、正则脱敏 - 提取 network 模块到 proxycast-core(从 network_cmd 中分离纯逻辑) - 修复 bootstrap.rs/state.rs 中 LogStore::with_config 调用 - 修复 provider_calls.rs doctest 代码块标记
This commit is contained in:
Generated
+87
@@ -6669,14 +6669,18 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"proptest",
|
||||
"proxycast-agent",
|
||||
"proxycast-config",
|
||||
"proxycast-core",
|
||||
"proxycast-credential",
|
||||
"proxycast-infra",
|
||||
"proxycast-mcp",
|
||||
"proxycast-processor",
|
||||
"proxycast-providers",
|
||||
"proxycast-server",
|
||||
"proxycast-server-utils",
|
||||
"proxycast-services",
|
||||
"proxycast-skills",
|
||||
"proxycast-terminal",
|
||||
"proxycast-websocket",
|
||||
"rand 0.8.5",
|
||||
@@ -6724,6 +6728,23 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-agent"
|
||||
version = "0.60.0"
|
||||
dependencies = [
|
||||
"aster",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"proxycast-core",
|
||||
"proxycast-mcp",
|
||||
"rmcp 0.6.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-config"
|
||||
version = "0.60.0"
|
||||
@@ -6752,11 +6773,13 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"flate2",
|
||||
"futures",
|
||||
"if-addrs",
|
||||
"indexmap 2.13.0",
|
||||
"notify 6.1.1",
|
||||
"parking_lot",
|
||||
"proptest",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
@@ -6815,6 +6838,21 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-mcp"
|
||||
version = "0.60.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"glob",
|
||||
"proxycast-core",
|
||||
"rmcp 0.6.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-processor"
|
||||
version = "0.60.0"
|
||||
@@ -6868,6 +6906,43 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-server"
|
||||
version = "0.60.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"parking_lot",
|
||||
"proptest",
|
||||
"proxycast-config",
|
||||
"proxycast-core",
|
||||
"proxycast-credential",
|
||||
"proxycast-infra",
|
||||
"proxycast-processor",
|
||||
"proxycast-providers",
|
||||
"proxycast-server-utils",
|
||||
"proxycast-services",
|
||||
"proxycast-websocket",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"subtle",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-server-utils"
|
||||
version = "0.60.0"
|
||||
@@ -6921,6 +6996,18 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-skills"
|
||||
version = "0.60.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dirs 5.0.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-terminal"
|
||||
version = "0.60.0"
|
||||
|
||||
@@ -21,6 +21,10 @@ proxycast-credential = { path = "crates/credential" }
|
||||
proxycast-websocket = { path = "crates/websocket" }
|
||||
proxycast-processor = { path = "crates/processor" }
|
||||
proxycast-server-utils = { path = "crates/server-utils" }
|
||||
proxycast-server = { path = "crates/server" }
|
||||
proxycast-skills = { path = "crates/skills" }
|
||||
proxycast-mcp = { path = "crates/mcp" }
|
||||
proxycast-agent = { path = "crates/agent" }
|
||||
voice-core = { path = "crates/voice-core" }
|
||||
|
||||
# 序列化
|
||||
@@ -201,6 +205,10 @@ proxycast-credential.workspace = true
|
||||
proxycast-websocket.workspace = true
|
||||
proxycast-processor.workspace = true
|
||||
proxycast-server-utils.workspace = true
|
||||
proxycast-server.workspace = true
|
||||
proxycast-skills.workspace = true
|
||||
proxycast-mcp.workspace = true
|
||||
proxycast-agent.workspace = true
|
||||
voice-core.workspace = true
|
||||
|
||||
# Tauri
|
||||
|
||||
@@ -51,12 +51,18 @@ flate2.workspace = true
|
||||
tar.workspace = true
|
||||
zip.workspace = true
|
||||
|
||||
# 正则表达式(logger 脱敏需要)
|
||||
regex.workspace = true
|
||||
|
||||
# YAML 配置
|
||||
serde_yaml.workspace = true
|
||||
|
||||
# 数据库(errors 模块需要 rusqlite::Error)
|
||||
rusqlite.workspace = true
|
||||
|
||||
# 网络接口(network 模块需要)
|
||||
if-addrs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
tempfile.workspace = true
|
||||
@@ -47,6 +47,12 @@ pub mod processor;
|
||||
// WebSocket 核心类型
|
||||
pub mod websocket;
|
||||
|
||||
// 事件发射抽象(供独立 crate 解耦 Tauri 依赖)
|
||||
pub mod event_emit;
|
||||
|
||||
// 网络工具
|
||||
pub mod network;
|
||||
|
||||
// 数据层
|
||||
pub mod content;
|
||||
pub mod database;
|
||||
@@ -54,6 +60,7 @@ pub mod memory;
|
||||
pub mod workspace;
|
||||
|
||||
// 重新导出常用类型
|
||||
pub use event_emit::{DynEmitter, EventEmit, NoOpEmitter};
|
||||
pub use logger::{LogEntry, LogStore, LogStoreConfig, SharedLogStore};
|
||||
pub use models::provider_type::ProviderType;
|
||||
pub use models::*;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! 日志管理模块
|
||||
use chrono::{Duration, Local, Utc};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -42,19 +43,13 @@ pub struct LogStore {
|
||||
|
||||
impl Default for LogStore {
|
||||
fn default() -> Self {
|
||||
// 默认日志文件路径: ~/.proxycast/logs/proxycast.log
|
||||
let log_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".proxycast")
|
||||
.join("logs");
|
||||
|
||||
// 创建日志目录
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
|
||||
let log_file = log_dir.join("proxycast.log");
|
||||
|
||||
let config = LogStoreConfig::default();
|
||||
|
||||
Self {
|
||||
logs: VecDeque::new(),
|
||||
max_logs: config.max_logs,
|
||||
@@ -86,24 +81,18 @@ impl LogStore {
|
||||
level: level.to_string(),
|
||||
message: sanitized.clone(),
|
||||
};
|
||||
|
||||
self.logs.push_back(entry.clone());
|
||||
|
||||
// 写入日志文件
|
||||
if self.config.enable_file_logging {
|
||||
if let Some(ref path) = self.log_file_path {
|
||||
self.rotate_log_file_if_needed(path);
|
||||
let local_time = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
let log_line = format!("{} [{}] {}\n", local_time, level.to_uppercase(), sanitized);
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = file.write_all(log_line.as_bytes());
|
||||
}
|
||||
self.prune_old_logs(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 保持日志数量在限制内
|
||||
if self.logs.len() > self.max_logs {
|
||||
self.logs.pop_front();
|
||||
}
|
||||
@@ -115,7 +104,6 @@ impl LogStore {
|
||||
let log_dir = log_path.parent().unwrap_or(std::path::Path::new("."));
|
||||
let raw_file = log_dir.join(format!("raw_response_{request_id}.txt"));
|
||||
let sanitized = sanitize_log_message(body);
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
@@ -145,26 +133,22 @@ impl LogStore {
|
||||
let Ok(metadata) = fs::metadata(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if metadata.len() <= self.config.max_file_size {
|
||||
return;
|
||||
}
|
||||
|
||||
let suffix = Local::now().format("%Y%m%d-%H%M%S");
|
||||
let rotated = path.with_file_name(format!(
|
||||
"{}.{}",
|
||||
path.file_name().unwrap_or_default().to_string_lossy(),
|
||||
suffix
|
||||
));
|
||||
|
||||
let _ = fs::rename(path, &rotated);
|
||||
self.prune_old_logs(path);
|
||||
}
|
||||
|
||||
fn prune_old_logs(&self, path: &std::path::Path) {
|
||||
let Some(dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
fn prune_old_logs(&self, path: &PathBuf) {
|
||||
let Some(dir) = path.parent() else { return };
|
||||
self.archive_old_logs(path);
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
@@ -173,7 +157,6 @@ impl LogStore {
|
||||
"{}.",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
);
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
@@ -192,26 +175,106 @@ impl LogStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn archive_old_logs(&self, path: &PathBuf) {
|
||||
let Some(dir) = path.parent() else { return };
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
let archive_cutoff = Utc::now() - Duration::days(7);
|
||||
let delete_cutoff = Utc::now() - Duration::days(30);
|
||||
let prefix = format!(
|
||||
"{}.",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
);
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if !file_name.starts_with(&prefix) {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
let Ok(metadata) = entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
continue;
|
||||
};
|
||||
let modified = chrono::DateTime::<Utc>::from(modified);
|
||||
// 删除超过 30 天的 gz 文件
|
||||
if file_name.ends_with(".gz") {
|
||||
if modified < delete_cutoff {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 跳过不到 7 天的文件
|
||||
if modified >= archive_cutoff {
|
||||
continue;
|
||||
}
|
||||
// 压缩超过 7 天的日志文件
|
||||
let mut input = Vec::new();
|
||||
if let Ok(mut file) = fs::File::open(&path) {
|
||||
if file.read_to_end(&mut input).is_err() {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
let gz_path = path.with_extension(format!(
|
||||
"{}.gz",
|
||||
path.extension().unwrap_or_default().to_string_lossy()
|
||||
));
|
||||
if let Ok(gz_file) = fs::File::create(&gz_path) {
|
||||
let mut encoder =
|
||||
flate2::write::GzEncoder::new(gz_file, flate2::Compression::default());
|
||||
if encoder.write_all(&input).is_ok() && encoder.finish().is_ok() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 简化的共享日志存储类型(使用 parking_lot)
|
||||
pub type SharedLogStore = Arc<parking_lot::RwLock<LogStore>>;
|
||||
|
||||
/// P2 安全修复:扩展日志脱敏规则,覆盖更多敏感字段
|
||||
pub fn sanitize_log_message(message: &str) -> String {
|
||||
// 简化版本:使用字符串替换而不是正则表达式
|
||||
let patterns = [
|
||||
(r"Bearer\s+[A-Za-z0-9._-]+", "Bearer ***"),
|
||||
(
|
||||
r#"api[_-]?key["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"api_key: ***",
|
||||
),
|
||||
(r#"token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#, "token: ***"),
|
||||
(
|
||||
r#"access[_-]?token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"access_token: ***",
|
||||
),
|
||||
(
|
||||
r#"refresh[_-]?token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"refresh_token: ***",
|
||||
),
|
||||
(
|
||||
r#"client[_-]?secret["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"client_secret: ***",
|
||||
),
|
||||
(
|
||||
r#"[Aa]uthorization["']?\s*[:=]\s*["']?[A-Za-z0-9._\s-]+"#,
|
||||
"authorization: ***",
|
||||
),
|
||||
(r#"password["']?\s*[:=]\s*["']?[^\s"',}]+"#, "password: ***"),
|
||||
(
|
||||
r#"secret["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"secret: ***",
|
||||
),
|
||||
];
|
||||
let mut sanitized = message.to_string();
|
||||
|
||||
// Bearer token
|
||||
if let Some(pos) = sanitized.find("Bearer ") {
|
||||
let start = pos + 7;
|
||||
if let Some(end) =
|
||||
sanitized[start..].find(|c: char| c.is_whitespace() || c == '"' || c == '\'')
|
||||
{
|
||||
sanitized.replace_range(start..start + end, "***");
|
||||
for (pattern, replacement) in patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
sanitized = re.replace_all(&sanitized, replacement).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
sanitized
|
||||
}
|
||||
|
||||
@@ -221,11 +284,52 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_bearer_token() {
|
||||
let input = "Authorization: Bearer abcDEF123 end";
|
||||
let input = "Authorization: Bearer abcDEF123._-XYZ";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(!output.contains("abcDEF123"));
|
||||
assert!(output.contains("***"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_api_key() {
|
||||
let input = r#"request api_key="sk-test_123.456-ABC" end"#;
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("api_key: ***"));
|
||||
assert!(!output.contains("sk-test_123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_access_token() {
|
||||
let input = "access_token=atk_12345";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("access_token: ***"));
|
||||
assert!(!output.contains("atk_12345"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_refresh_token() {
|
||||
let input = "refresh_token: rtk_ABCDE-123";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("refresh_token: ***"));
|
||||
assert!(!output.contains("rtk_ABCDE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_client_secret() {
|
||||
let input = "client_secret = \"cs_SeCreT-999\"";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("client_secret: ***"));
|
||||
assert!(!output.contains("cs_SeCreT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_password() {
|
||||
let input = r#"{"password":"p@ssW0rd!"}"#;
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("password: ***"));
|
||||
assert!(!output.contains("p@ssW0rd!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plain_text_unchanged() {
|
||||
let input = "这是一段普通日志,不包含任何敏感字段。";
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! 网络工具模块
|
||||
//!
|
||||
//! 提供获取本地网络接口信息的功能。
|
||||
//! 从主 crate 的 commands/network_cmd.rs 迁移而来。
|
||||
|
||||
use serde::Serialize;
|
||||
use std::net::{IpAddr, UdpSocket};
|
||||
|
||||
/// 网络接口信息
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NetworkInfo {
|
||||
/// 本地回环地址
|
||||
pub localhost: String,
|
||||
/// 内网 IP 地址(局域网)
|
||||
pub lan_ip: Option<String>,
|
||||
/// 所有可用的网络接口 IP 地址
|
||||
pub all_ips: Vec<String>,
|
||||
}
|
||||
|
||||
/// 获取本地网络信息
|
||||
///
|
||||
/// 返回 localhost 和内网 IP 地址,用于客户端连接
|
||||
pub fn get_network_info() -> Result<NetworkInfo, String> {
|
||||
let lan_ip = get_local_ip();
|
||||
let all_ips = get_all_local_ips();
|
||||
|
||||
Ok(NetworkInfo {
|
||||
localhost: "127.0.0.1".to_string(),
|
||||
lan_ip,
|
||||
all_ips,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取本机内网 IP 地址
|
||||
///
|
||||
/// 通过创建 UDP socket 连接外部地址来获取本机的内网 IP
|
||||
fn get_local_ip() -> Option<String> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||
socket.connect("8.8.8.8:80").ok()?;
|
||||
let local_addr = socket.local_addr().ok()?;
|
||||
let ip_str = local_addr.ip().to_string();
|
||||
|
||||
if let IpAddr::V4(ipv4) = local_addr.ip() {
|
||||
if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) {
|
||||
let all_ips = get_all_local_ips();
|
||||
if let Some(ip) = all_ips.iter().find(|ip| ip.starts_with("192.168.")) {
|
||||
return Some(ip.clone());
|
||||
}
|
||||
if let Some(ip) = all_ips.first() {
|
||||
return Some(ip.clone());
|
||||
}
|
||||
return Some("127.0.0.1".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Some(ip_str)
|
||||
}
|
||||
|
||||
/// 获取所有本地网络接口的 IP 地址
|
||||
///
|
||||
/// 返回所有非回环的 IPv4 私有地址
|
||||
fn get_all_local_ips() -> Vec<String> {
|
||||
let mut ips = Vec::new();
|
||||
|
||||
if let Ok(interfaces) = if_addrs::get_if_addrs() {
|
||||
for iface in interfaces {
|
||||
if let IpAddr::V4(ipv4) = iface.ip() {
|
||||
if ipv4.is_loopback() {
|
||||
continue;
|
||||
}
|
||||
if ipv4.octets()[0] == 169 && ipv4.octets()[1] == 254 {
|
||||
continue;
|
||||
}
|
||||
if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) {
|
||||
continue;
|
||||
}
|
||||
let is_private = ipv4.octets()[0] == 10
|
||||
|| (ipv4.octets()[0] == 172
|
||||
&& (ipv4.octets()[1] >= 16 && ipv4.octets()[1] <= 31))
|
||||
|| (ipv4.octets()[0] == 192 && ipv4.octets()[1] == 168);
|
||||
|
||||
if is_private {
|
||||
ips.push(ipv4.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// 根据监听地址生成可访问的 host
|
||||
pub fn get_accessible_host(listen_host: &str) -> String {
|
||||
match listen_host {
|
||||
"0.0.0.0" => get_network_info()
|
||||
.ok()
|
||||
.and_then(|info| info.lan_ip)
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||
"localhost" => "127.0.0.1".to_string(),
|
||||
_ => listen_host.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据监听地址生成可访问的 URL
|
||||
pub fn get_accessible_url(listen_host: &str, port: u16) -> String {
|
||||
let host = get_accessible_host(listen_host);
|
||||
format!("http://{host}:{port}")
|
||||
}
|
||||
|
||||
/// 根据监听地址生成本地访问的 URL
|
||||
#[allow(dead_code)]
|
||||
pub fn get_local_url(listen_host: &str, port: u16) -> String {
|
||||
let host = match listen_host {
|
||||
"0.0.0.0" | "localhost" => "127.0.0.1".to_string(),
|
||||
_ => listen_host.to_string(),
|
||||
};
|
||||
format!("http://{host}:{port}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_host_localhost() {
|
||||
assert_eq!(get_accessible_host("127.0.0.1"), "127.0.0.1");
|
||||
assert_eq!(get_accessible_host("localhost"), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_host_specific_ip() {
|
||||
assert_eq!(get_accessible_host("192.168.1.100"), "192.168.1.100");
|
||||
assert_eq!(get_accessible_host("10.0.0.1"), "10.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_local_url() {
|
||||
assert_eq!(get_local_url("0.0.0.0", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(get_local_url("127.0.0.1", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(get_local_url("localhost", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(
|
||||
get_local_url("192.168.1.100", 8999),
|
||||
"http://192.168.1.100:8999"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_url_specific_ip() {
|
||||
assert_eq!(
|
||||
get_accessible_url("192.168.1.100", 8999),
|
||||
"http://192.168.1.100:8999"
|
||||
);
|
||||
assert_eq!(
|
||||
get_accessible_url("127.0.0.1", 8999),
|
||||
"http://127.0.0.1:8999"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "proxycast-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
proxycast-core.workspace = true
|
||||
proxycast-config.workspace = true
|
||||
proxycast-infra.workspace = true
|
||||
proxycast-providers.workspace = true
|
||||
proxycast-services.workspace = true
|
||||
proxycast-credential.workspace = true
|
||||
proxycast-websocket.workspace = true
|
||||
proxycast-processor.workspace = true
|
||||
proxycast-server-utils.workspace = true
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
futures.workspace = true
|
||||
axum.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
rusqlite.workspace = true
|
||||
chrono.workspace = true
|
||||
uuid.workspace = true
|
||||
base64.workspace = true
|
||||
bytes.workspace = true
|
||||
regex.workspace = true
|
||||
subtle.workspace = true
|
||||
async-stream.workspace = true
|
||||
urlencoding.workspace = true
|
||||
parking_lot.workspace = true
|
||||
tokio-util.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
+1
-1
@@ -7,8 +7,8 @@ pub use proxycast_core::models::client_type::*;
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use crate::config::EndpointProvidersConfig;
|
||||
use proptest::prelude::*;
|
||||
use proxycast_core::config::EndpointProvidersConfig;
|
||||
|
||||
fn arb_client_type() -> impl Strategy<Value = ClientType> {
|
||||
prop_oneof![
|
||||
+51
-45
@@ -25,18 +25,18 @@ use axum::{
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use crate::models::anthropic::AnthropicMessagesRequest;
|
||||
use crate::models::openai::ChatCompletionRequest;
|
||||
use crate::processor::RequestContext;
|
||||
use crate::server::client_detector::ClientType;
|
||||
use crate::server::{record_request_telemetry, record_token_usage, AppState};
|
||||
use crate::server_utils::{
|
||||
use crate::client_detector::ClientType;
|
||||
use crate::{record_request_telemetry, record_token_usage, AppState};
|
||||
use proxycast_core::models::anthropic::AnthropicMessagesRequest;
|
||||
use proxycast_core::models::openai::ChatCompletionRequest;
|
||||
use proxycast_core::ProviderType;
|
||||
use proxycast_processor::RequestContext;
|
||||
use proxycast_providers::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use proxycast_providers::streaming::StreamFormat as StreamingFormat;
|
||||
use proxycast_server_utils::{
|
||||
build_anthropic_response, build_anthropic_stream_response, message_content_len,
|
||||
parse_cw_response, safe_truncate,
|
||||
};
|
||||
use crate::streaming::StreamFormat as StreamingFormat;
|
||||
use crate::ProviderType;
|
||||
|
||||
use super::{call_provider_anthropic, call_provider_openai};
|
||||
|
||||
@@ -331,13 +331,14 @@ pub async fn chat_completions(
|
||||
let credential = if credential.is_none() {
|
||||
eprintln!("[CHAT_COMPLETIONS] Provider Pool 中未找到凭证,尝试 API Key Provider...");
|
||||
|
||||
use crate::database::dao::api_key_provider::ApiProviderType;
|
||||
use proxycast_core::database::dao::api_key_provider::ApiProviderType;
|
||||
let provider_id_lower = selected_provider.to_lowercase();
|
||||
|
||||
// 策略 1: 优先按 provider_id 直接查找(支持 deepseek, moonshot 等 60+ Provider)
|
||||
// 这些 Provider 在 API Key Provider 中有独立配置
|
||||
let mut found_credential: Option<crate::models::provider_pool_model::ProviderCredential> =
|
||||
None;
|
||||
let mut found_credential: Option<
|
||||
proxycast_core::models::provider_pool_model::ProviderCredential,
|
||||
> = None;
|
||||
|
||||
if let Some(db) = &state.db {
|
||||
// 先尝试按 provider_id 直接查找
|
||||
@@ -347,7 +348,7 @@ pub async fn chat_completions(
|
||||
.api_key_service
|
||||
.get_fallback_credential(
|
||||
db,
|
||||
&crate::models::provider_pool_model::PoolProviderType::OpenAI,
|
||||
&proxycast_core::models::provider_pool_model::PoolProviderType::OpenAI,
|
||||
Some(&provider_id_lower),
|
||||
Some(&client_type),
|
||||
)
|
||||
@@ -408,36 +409,40 @@ pub async fn chat_completions(
|
||||
};
|
||||
|
||||
let provider_type = match provider_info.provider_type {
|
||||
ApiProviderType::Anthropic => crate::ProviderType::Anthropic,
|
||||
ApiProviderType::Openai | ApiProviderType::OpenaiResponse => {
|
||||
crate::ProviderType::OpenAI
|
||||
ApiProviderType::Anthropic => {
|
||||
proxycast_core::ProviderType::Anthropic
|
||||
}
|
||||
ApiProviderType::Gemini => crate::ProviderType::GeminiApiKey,
|
||||
_ => crate::ProviderType::OpenAI,
|
||||
ApiProviderType::Openai | ApiProviderType::OpenaiResponse => {
|
||||
proxycast_core::ProviderType::OpenAI
|
||||
}
|
||||
ApiProviderType::Gemini => {
|
||||
proxycast_core::ProviderType::GeminiApiKey
|
||||
}
|
||||
_ => proxycast_core::ProviderType::OpenAI,
|
||||
};
|
||||
|
||||
let credential_data = match provider_type {
|
||||
crate::ProviderType::Anthropic => {
|
||||
crate::models::provider_pool_model::CredentialData::AnthropicKey {
|
||||
proxycast_core::ProviderType::Anthropic => {
|
||||
proxycast_core::models::provider_pool_model::CredentialData::AnthropicKey {
|
||||
api_key: api_key.clone(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
crate::ProviderType::GeminiApiKey => {
|
||||
crate::models::provider_pool_model::CredentialData::GeminiApiKey {
|
||||
proxycast_core::ProviderType::GeminiApiKey => {
|
||||
proxycast_core::models::provider_pool_model::CredentialData::GeminiApiKey {
|
||||
api_key: api_key.clone(),
|
||||
base_url,
|
||||
excluded_models: vec![],
|
||||
}
|
||||
}
|
||||
_ => crate::models::provider_pool_model::CredentialData::OpenAIKey {
|
||||
_ => proxycast_core::models::provider_pool_model::CredentialData::OpenAIKey {
|
||||
api_key: api_key.clone(),
|
||||
base_url,
|
||||
},
|
||||
};
|
||||
|
||||
let mut cred =
|
||||
crate::models::provider_pool_model::ProviderCredential::new(
|
||||
proxycast_core::models::provider_pool_model::ProviderCredential::new(
|
||||
provider_type,
|
||||
credential_data,
|
||||
);
|
||||
@@ -522,9 +527,9 @@ pub async fn chat_completions(
|
||||
let is_success = response.status().is_success();
|
||||
let _status_code = response.status().as_u16();
|
||||
let status = if is_success {
|
||||
crate::telemetry::RequestStatus::Success
|
||||
proxycast_infra::telemetry::RequestStatus::Success
|
||||
} else {
|
||||
crate::telemetry::RequestStatus::Failed
|
||||
proxycast_infra::telemetry::RequestStatus::Failed
|
||||
};
|
||||
record_request_telemetry(&state, &ctx, status, None);
|
||||
|
||||
@@ -674,7 +679,7 @@ pub async fn chat_completions(
|
||||
record_request_telemetry(
|
||||
&state,
|
||||
&ctx,
|
||||
crate::telemetry::RequestStatus::Success,
|
||||
proxycast_infra::telemetry::RequestStatus::Success,
|
||||
None,
|
||||
);
|
||||
// 记录 Token 使用量
|
||||
@@ -693,7 +698,7 @@ pub async fn chat_completions(
|
||||
record_request_telemetry(
|
||||
&state,
|
||||
&ctx,
|
||||
crate::telemetry::RequestStatus::Failed,
|
||||
proxycast_infra::telemetry::RequestStatus::Failed,
|
||||
Some(e.to_string()),
|
||||
);
|
||||
// 标记 Flow 失败
|
||||
@@ -1059,8 +1064,9 @@ pub async fn anthropic_messages(
|
||||
eprintln!("[ANTHROPIC_MESSAGES] Provider Pool 中未找到凭证,尝试 API Key Provider...");
|
||||
|
||||
// 策略 1: 优先按 provider_id 直接查找(支持自定义 Provider)
|
||||
let mut found_credential: Option<crate::models::provider_pool_model::ProviderCredential> =
|
||||
None;
|
||||
let mut found_credential: Option<
|
||||
proxycast_core::models::provider_pool_model::ProviderCredential,
|
||||
> = None;
|
||||
|
||||
if let Some(db) = &state.db {
|
||||
eprintln!("[ANTHROPIC_MESSAGES] 尝试按 provider_id '{selected_provider}' 直接查找凭证");
|
||||
@@ -1069,7 +1075,7 @@ pub async fn anthropic_messages(
|
||||
.api_key_service
|
||||
.get_fallback_credential(
|
||||
db,
|
||||
&crate::models::provider_pool_model::PoolProviderType::Anthropic,
|
||||
&proxycast_core::models::provider_pool_model::PoolProviderType::Anthropic,
|
||||
Some(&selected_provider),
|
||||
Some(&client_type),
|
||||
)
|
||||
@@ -1133,9 +1139,9 @@ pub async fn anthropic_messages(
|
||||
// 记录请求统计
|
||||
let is_success = response.status().is_success();
|
||||
let status = if is_success {
|
||||
crate::telemetry::RequestStatus::Success
|
||||
proxycast_infra::telemetry::RequestStatus::Success
|
||||
} else {
|
||||
crate::telemetry::RequestStatus::Failed
|
||||
proxycast_infra::telemetry::RequestStatus::Failed
|
||||
};
|
||||
record_request_telemetry(&state, &ctx, status, None);
|
||||
|
||||
@@ -1552,9 +1558,9 @@ fn get_target_stream_format(path: &str) -> StreamingFormat {
|
||||
/// 当前所有 Provider 都返回 false,因为 StreamingProvider trait 尚未实现。
|
||||
/// 一旦任务 6 完成,此函数将根据凭证类型返回适当的值。
|
||||
fn should_use_true_streaming(
|
||||
credential: &crate::models::provider_pool_model::ProviderCredential,
|
||||
credential: &proxycast_core::models::provider_pool_model::ProviderCredential,
|
||||
) -> bool {
|
||||
use crate::models::provider_pool_model::CredentialData;
|
||||
use proxycast_core::models::provider_pool_model::CredentialData;
|
||||
|
||||
// TODO: 当 StreamingProvider trait 实现后,根据凭证类型返回 true
|
||||
// 目前所有 Provider 都使用伪流式模式
|
||||
@@ -1665,10 +1671,10 @@ fn map_to_api_key_provider_id(provider_type: &str) -> String {
|
||||
|
||||
/// 根据 API Provider 类型构建额外的请求头
|
||||
fn build_api_key_headers(
|
||||
provider_type: &crate::database::dao::api_key_provider::ApiProviderType,
|
||||
provider_type: &proxycast_core::database::dao::api_key_provider::ApiProviderType,
|
||||
api_key: &str,
|
||||
) -> HashMap<String, String> {
|
||||
use crate::database::dao::api_key_provider::ApiProviderType;
|
||||
use proxycast_core::database::dao::api_key_provider::ApiProviderType;
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
@@ -1693,9 +1699,9 @@ fn build_api_key_headers(
|
||||
|
||||
/// 获取默认的 API Host
|
||||
fn get_default_api_host(
|
||||
provider_type: &crate::database::dao::api_key_provider::ApiProviderType,
|
||||
provider_type: &proxycast_core::database::dao::api_key_provider::ApiProviderType,
|
||||
) -> String {
|
||||
use crate::database::dao::api_key_provider::ApiProviderType;
|
||||
use proxycast_core::database::dao::api_key_provider::ApiProviderType;
|
||||
|
||||
match provider_type {
|
||||
ApiProviderType::Openai | ApiProviderType::OpenaiResponse => {
|
||||
@@ -1718,11 +1724,11 @@ fn convert_openai_to_anthropic(request: &ChatCompletionRequest) -> serde_json::V
|
||||
// 提取 system prompt
|
||||
if let Some(content) = &msg.content {
|
||||
system_prompt = Some(match content {
|
||||
crate::models::openai::MessageContent::Text(s) => s.clone(),
|
||||
crate::models::openai::MessageContent::Parts(parts) => parts
|
||||
proxycast_core::models::openai::MessageContent::Text(s) => s.clone(),
|
||||
proxycast_core::models::openai::MessageContent::Parts(parts) => parts
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
if let crate::models::openai::ContentPart::Text { text } = p {
|
||||
if let proxycast_core::models::openai::ContentPart::Text { text } = p {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -1736,11 +1742,11 @@ fn convert_openai_to_anthropic(request: &ChatCompletionRequest) -> serde_json::V
|
||||
// 转换其他消息
|
||||
let content = match &msg.content {
|
||||
Some(c) => match c {
|
||||
crate::models::openai::MessageContent::Text(s) => s.clone(),
|
||||
crate::models::openai::MessageContent::Parts(parts) => parts
|
||||
proxycast_core::models::openai::MessageContent::Text(s) => s.clone(),
|
||||
proxycast_core::models::openai::MessageContent::Parts(parts) => parts
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
if let crate::models::openai::ContentPart::Text { text } = p {
|
||||
if let proxycast_core::models::openai::ContentPart::Text { text } = p {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
+8
-8
@@ -16,10 +16,10 @@ use axum::{
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::dao::api_key_provider::{ApiKeyProviderDao, ApiProviderType};
|
||||
use crate::database::dao::provider_pool::ProviderPoolDao;
|
||||
use crate::models::provider_pool_model::PoolProviderType;
|
||||
use crate::server::AppState;
|
||||
use crate::AppState;
|
||||
use proxycast_core::database::dao::api_key_provider::{ApiKeyProviderDao, ApiProviderType};
|
||||
use proxycast_core::database::dao::provider_pool::ProviderPoolDao;
|
||||
use proxycast_core::models::provider_pool_model::PoolProviderType;
|
||||
|
||||
/// 选择凭证请求参数
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -155,7 +155,7 @@ pub async fn credentials_select(
|
||||
/// 尝试从 OAuth 凭证池选择凭证
|
||||
async fn try_select_oauth_credential(
|
||||
state: &AppState,
|
||||
db: &crate::database::DbConnection,
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
request: &SelectCredentialRequest,
|
||||
) -> Result<Option<CredentialResponse>, CredentialApiError> {
|
||||
// 使用 ProviderPoolService 智能选择凭证
|
||||
@@ -208,7 +208,7 @@ async fn try_select_oauth_credential(
|
||||
/// 尝试从 API Key Provider 选择凭证
|
||||
async fn try_select_api_key_credential(
|
||||
state: &AppState,
|
||||
db: &crate::database::DbConnection,
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
request: &SelectCredentialRequest,
|
||||
) -> Result<Option<CredentialResponse>, CredentialApiError> {
|
||||
// 将 provider_type 映射到 API Key Provider ID
|
||||
@@ -363,7 +363,7 @@ pub async fn credentials_get_token(
|
||||
/// 尝试从 OAuth 凭证池获取 Token
|
||||
async fn try_get_oauth_token(
|
||||
state: &AppState,
|
||||
db: &crate::database::DbConnection,
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
uuid: &str,
|
||||
) -> Result<Option<CredentialResponse>, CredentialApiError> {
|
||||
// 查询凭证
|
||||
@@ -468,7 +468,7 @@ async fn try_get_oauth_token(
|
||||
/// 尝试从 API Key Provider 获取 Token
|
||||
async fn try_get_api_key_token(
|
||||
state: &AppState,
|
||||
db: &crate::database::DbConnection,
|
||||
db: &proxycast_core::database::DbConnection,
|
||||
uuid: &str,
|
||||
) -> Result<Option<CredentialResponse>, CredentialApiError> {
|
||||
let conn = db.lock().map_err(|e| CredentialApiError {
|
||||
+6
-6
@@ -23,14 +23,14 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::converter::openai_to_antigravity::{
|
||||
use crate::handlers::verify_api_key;
|
||||
use crate::AppState;
|
||||
use proxycast_core::models::openai::ImageGenerationRequest;
|
||||
use proxycast_core::models::provider_pool_model::CredentialData;
|
||||
use proxycast_providers::converter::openai_to_antigravity::{
|
||||
convert_antigravity_image_response, convert_image_request_to_antigravity,
|
||||
};
|
||||
use crate::models::openai::ImageGenerationRequest;
|
||||
use crate::models::provider_pool_model::CredentialData;
|
||||
use crate::providers::AntigravityProvider;
|
||||
use crate::server::handlers::verify_api_key;
|
||||
use crate::server::AppState;
|
||||
use proxycast_providers::providers::AntigravityProvider;
|
||||
|
||||
/// 处理图像生成请求
|
||||
///
|
||||
+5
-3
@@ -15,9 +15,11 @@ use axum::{
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::dao::provider_pool::ProviderPoolDao;
|
||||
use crate::models::provider_pool_model::{CachedTokenInfo, PoolProviderType, ProviderCredential};
|
||||
use crate::server::AppState;
|
||||
use crate::AppState;
|
||||
use proxycast_core::database::dao::provider_pool::ProviderPoolDao;
|
||||
use proxycast_core::models::provider_pool_model::{
|
||||
CachedTokenInfo, PoolProviderType, ProviderCredential,
|
||||
};
|
||||
|
||||
/// 可用凭证信息
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
+4
-4
@@ -7,8 +7,8 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::dao::provider_pool::ProviderPoolDao;
|
||||
use crate::server::AppState;
|
||||
use crate::AppState;
|
||||
use proxycast_core::database::dao::provider_pool::ProviderPoolDao;
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
@@ -201,7 +201,7 @@ pub async fn management_add_credential(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<AddCredentialRequest>,
|
||||
) -> impl IntoResponse {
|
||||
use crate::models::provider_pool_model::{
|
||||
use proxycast_core::models::provider_pool_model::{
|
||||
CredentialData, PoolProviderType, ProviderCredential,
|
||||
};
|
||||
|
||||
@@ -538,7 +538,7 @@ pub async fn management_update_config(
|
||||
// 更新默认 Provider
|
||||
if let Some(provider) = request.default_provider {
|
||||
// 验证 provider 类型
|
||||
if provider.parse::<crate::ProviderType>().is_ok() {
|
||||
if provider.parse::<proxycast_core::ProviderType>().is_ok() {
|
||||
let mut dp = state.default_provider.write().await;
|
||||
*dp = provider.clone();
|
||||
tracing::info!("[MANAGEMENT] Updated default_provider to: {}", provider);
|
||||
+33
-28
@@ -49,29 +49,29 @@ use axum::{
|
||||
};
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use crate::converter::openai_to_antigravity::{
|
||||
use crate::AppState;
|
||||
use proxycast_core::models::anthropic::AnthropicMessagesRequest;
|
||||
use proxycast_core::models::openai::ChatCompletionRequest;
|
||||
use proxycast_core::models::provider_pool_model::{CredentialData, ProviderCredential};
|
||||
use proxycast_providers::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use proxycast_providers::converter::openai_to_antigravity::{
|
||||
convert_antigravity_to_openai_response, convert_openai_to_antigravity_with_context,
|
||||
};
|
||||
use crate::models::anthropic::AnthropicMessagesRequest;
|
||||
use crate::models::openai::ChatCompletionRequest;
|
||||
use crate::models::provider_pool_model::{CredentialData, ProviderCredential};
|
||||
use crate::providers::{
|
||||
use proxycast_providers::providers::{
|
||||
AntigravityProvider, ClaudeCustomProvider, CodexProvider, KiroProvider, OpenAICustomProvider,
|
||||
VertexProvider,
|
||||
};
|
||||
use crate::server::AppState;
|
||||
use crate::server_utils::{
|
||||
build_anthropic_response, build_anthropic_stream_response, build_error_response,
|
||||
build_error_response_with_status, parse_cw_response, safe_truncate, CWParsedResponse,
|
||||
};
|
||||
use crate::session::store_thought_signature;
|
||||
use crate::stream::{PipelineConfig, StreamPipeline};
|
||||
use crate::streaming::traits::StreamingProvider;
|
||||
use crate::streaming::{
|
||||
use proxycast_providers::session::store_thought_signature;
|
||||
use proxycast_providers::stream::{PipelineConfig, StreamPipeline};
|
||||
use proxycast_providers::streaming::traits::StreamingProvider;
|
||||
use proxycast_providers::streaming::{
|
||||
StreamConfig, StreamContext, StreamError, StreamFormat as StreamingFormat, StreamManager,
|
||||
StreamResponse,
|
||||
};
|
||||
use proxycast_server_utils::{
|
||||
build_anthropic_response, build_anthropic_stream_response, build_error_response,
|
||||
build_error_response_with_status, parse_cw_response, safe_truncate, CWParsedResponse,
|
||||
};
|
||||
|
||||
/// 根据凭证调用 Provider (Anthropic 格式)
|
||||
///
|
||||
@@ -1658,9 +1658,9 @@ pub async fn call_provider_openai(
|
||||
|
||||
// 创建 StreamConverter 将 Anthropic SSE 转换为 OpenAI SSE
|
||||
let converter = std::sync::Arc::new(tokio::sync::Mutex::new(
|
||||
crate::streaming::converter::StreamConverter::with_model(
|
||||
crate::streaming::converter::StreamFormat::AnthropicSse,
|
||||
crate::streaming::converter::StreamFormat::OpenAiSse,
|
||||
proxycast_providers::streaming::converter::StreamConverter::with_model(
|
||||
proxycast_providers::streaming::converter::StreamFormat::AnthropicSse,
|
||||
proxycast_providers::streaming::converter::StreamFormat::OpenAiSse,
|
||||
&request.model,
|
||||
),
|
||||
));
|
||||
@@ -1681,7 +1681,7 @@ pub async fn call_provider_openai(
|
||||
};
|
||||
|
||||
for sse_str in sse_events {
|
||||
yield Ok::<String, crate::streaming::StreamError>(sse_str);
|
||||
yield Ok::<String, proxycast_providers::streaming::StreamError>(sse_str);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1699,7 +1699,7 @@ pub async fn call_provider_openai(
|
||||
};
|
||||
|
||||
for sse_str in final_events {
|
||||
yield Ok::<String, crate::streaming::StreamError>(sse_str);
|
||||
yield Ok::<String, proxycast_providers::streaming::StreamError>(sse_str);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2257,9 +2257,14 @@ pub async fn handle_streaming_response_with_timeout(
|
||||
// 获取 flow_id 的克隆用于回调
|
||||
|
||||
// 创建带超时的流式处理,使用 BoxStream 统一类型
|
||||
let timeout_stream: BoxStream<'static, Result<String, crate::streaming::StreamError>> = {
|
||||
let timeout_stream: BoxStream<
|
||||
'static,
|
||||
Result<String, proxycast_providers::streaming::StreamError>,
|
||||
> = {
|
||||
let stream = manager.handle_stream(context, source_stream);
|
||||
Box::pin(crate::streaming::with_timeout(stream, &config))
|
||||
Box::pin(proxycast_providers::streaming::with_timeout(
|
||||
stream, &config,
|
||||
))
|
||||
};
|
||||
|
||||
// 转换为 Body 流
|
||||
@@ -2299,7 +2304,7 @@ pub async fn handle_streaming_response_with_timeout(
|
||||
/// # 返回
|
||||
/// 统一的流式响应类型
|
||||
pub fn response_to_stream(response: reqwest::Response) -> StreamResponse {
|
||||
crate::streaming::reqwest_stream_to_stream_response(response)
|
||||
proxycast_providers::streaming::reqwest_stream_to_stream_response(response)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -2355,7 +2360,7 @@ pub async fn handle_streaming_with_disconnect_detection(
|
||||
// 创建流式处理
|
||||
let managed_stream: futures::stream::BoxStream<
|
||||
'static,
|
||||
Result<String, crate::streaming::StreamError>,
|
||||
Result<String, proxycast_providers::streaming::StreamError>,
|
||||
> = Box::pin(manager.handle_stream(context, source_stream));
|
||||
|
||||
// 如果有取消令牌,创建一个可取消的流
|
||||
@@ -2624,8 +2629,8 @@ pub async fn handle_kiro_stream(
|
||||
// 检查是否是 401/403 错误或 Token 过期,需要刷新 token 重试(需求 4.1)
|
||||
let needs_token_refresh = matches!(
|
||||
&e,
|
||||
crate::providers::ProviderError::AuthenticationError(_)
|
||||
| crate::providers::ProviderError::TokenExpired(_)
|
||||
proxycast_providers::providers::ProviderError::AuthenticationError(_)
|
||||
| proxycast_providers::providers::ProviderError::TokenExpired(_)
|
||||
);
|
||||
|
||||
if needs_token_refresh {
|
||||
@@ -3109,7 +3114,7 @@ fn build_sse_response(
|
||||
/// ```
|
||||
///
|
||||
/// OpenAI SSE 格式:
|
||||
/// ```
|
||||
/// ```text
|
||||
/// data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
|
||||
/// ```
|
||||
fn convert_gemini_chunk_to_openai_sse(json: &serde_json::Value, model: &str) -> Option<String> {
|
||||
@@ -3207,7 +3212,7 @@ fn convert_gemini_chunk_to_openai_sse(json: &serde_json::Value, model: &str) ->
|
||||
|
||||
/// 将 OpenAI ChatCompletionResponse 转换为 Anthropic MessagesResponse 格式
|
||||
fn convert_openai_response_to_anthropic(
|
||||
openai_resp: &crate::models::openai::ChatCompletionResponse,
|
||||
openai_resp: &proxycast_core::models::openai::ChatCompletionResponse,
|
||||
model: &str,
|
||||
) -> serde_json::Value {
|
||||
// 提取第一个 choice 的内容
|
||||
+12
-12
@@ -16,20 +16,20 @@ use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use crate::converter::openai_to_antigravity::{
|
||||
use crate::AppState;
|
||||
use proxycast_core::models::anthropic::AnthropicMessagesRequest;
|
||||
use proxycast_core::models::openai::ChatCompletionRequest;
|
||||
use proxycast_core::models::provider_pool_model::ProviderCredential;
|
||||
use proxycast_processor::RequestContext;
|
||||
use proxycast_providers::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use proxycast_providers::converter::openai_to_antigravity::{
|
||||
convert_antigravity_to_openai_response, convert_openai_to_antigravity_with_context,
|
||||
};
|
||||
use crate::models::anthropic::AnthropicMessagesRequest;
|
||||
use crate::models::openai::ChatCompletionRequest;
|
||||
use crate::models::provider_pool_model::ProviderCredential;
|
||||
use crate::processor::RequestContext;
|
||||
use crate::providers::{
|
||||
use proxycast_providers::providers::{
|
||||
AntigravityProvider, ClaudeCustomProvider, KiroProvider, OpenAICustomProvider,
|
||||
};
|
||||
use crate::server::AppState;
|
||||
use crate::server_utils::parse_cw_response;
|
||||
use crate::websocket::{
|
||||
use proxycast_server_utils::parse_cw_response;
|
||||
use proxycast_websocket::{
|
||||
WsApiRequest, WsApiResponse, WsEndpoint, WsError, WsMessage as WsProtoMessage,
|
||||
};
|
||||
|
||||
@@ -447,7 +447,7 @@ pub async fn call_provider_openai_for_ws(
|
||||
credential: &ProviderCredential,
|
||||
request: &ChatCompletionRequest,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
use crate::models::provider_pool_model::CredentialData;
|
||||
use proxycast_core::models::provider_pool_model::CredentialData;
|
||||
|
||||
match &credential.credential {
|
||||
CredentialData::KiroOAuth { creds_file_path } => {
|
||||
@@ -726,7 +726,7 @@ pub async fn call_provider_anthropic_for_ws(
|
||||
credential: &ProviderCredential,
|
||||
request: &AnthropicMessagesRequest,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
use crate::models::provider_pool_model::CredentialData;
|
||||
use proxycast_core::models::provider_pool_model::CredentialData;
|
||||
|
||||
match &credential.credential {
|
||||
CredentialData::ClaudeKey { api_key, base_url } => {
|
||||
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,9 @@ pub struct AppStates {
|
||||
pub fn init_states(config: &Config) -> Result<AppStates, String> {
|
||||
// 核心状态
|
||||
let state: AppState = Arc::new(RwLock::new(server::ServerState::new(config.clone())));
|
||||
let logs: LogState = Arc::new(RwLock::new(logger::LogStore::with_config(&config.logging)));
|
||||
let logs: LogState = Arc::new(RwLock::new(logger::create_log_store_from_config(
|
||||
&config.logging,
|
||||
)));
|
||||
|
||||
// 数据库
|
||||
let db = database::init_database().map_err(|e| format!("数据库初始化失败: {e}"))?;
|
||||
|
||||
@@ -33,7 +33,9 @@ use crate::server;
|
||||
/// 初始化核心应用状态
|
||||
pub fn init_core_state(config: Config) -> (AppState, LogState) {
|
||||
let state: AppState = Arc::new(RwLock::new(server::ServerState::new(config.clone())));
|
||||
let logs: LogState = Arc::new(RwLock::new(logger::LogStore::with_config(&config.logging)));
|
||||
let logs: LogState = Arc::new(RwLock::new(logger::create_log_store_from_config(
|
||||
&config.logging,
|
||||
)));
|
||||
(state, logs)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,208 +1,14 @@
|
||||
//! 网络相关命令
|
||||
//!
|
||||
//! 提供获取本地网络接口信息的功能
|
||||
//! 核心逻辑已迁移到 proxycast-core::network,本文件保留 Tauri 命令包装。
|
||||
|
||||
use serde::Serialize;
|
||||
use std::net::{IpAddr, UdpSocket};
|
||||
// 重新导出核心类型
|
||||
pub use proxycast_core::network::{
|
||||
get_accessible_host, get_accessible_url, get_local_url, NetworkInfo,
|
||||
};
|
||||
|
||||
/// 网络接口信息
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NetworkInfo {
|
||||
/// 本地回环地址
|
||||
pub localhost: String,
|
||||
/// 内网 IP 地址(局域网)
|
||||
pub lan_ip: Option<String>,
|
||||
/// 所有可用的网络接口 IP 地址
|
||||
pub all_ips: Vec<String>,
|
||||
}
|
||||
|
||||
/// 获取本地网络信息
|
||||
///
|
||||
/// 返回 localhost 和内网 IP 地址,用于客户端连接
|
||||
/// 获取本地网络信息(Tauri 命令包装)
|
||||
#[tauri::command]
|
||||
pub fn get_network_info() -> Result<NetworkInfo, String> {
|
||||
let lan_ip = get_local_ip();
|
||||
let all_ips = get_all_local_ips();
|
||||
|
||||
Ok(NetworkInfo {
|
||||
localhost: "127.0.0.1".to_string(),
|
||||
lan_ip,
|
||||
all_ips,
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取本机内网 IP 地址
|
||||
///
|
||||
/// 通过创建 UDP socket 连接外部地址来获取本机的内网 IP
|
||||
/// 如果获取到的是 VPN 地址,则从 all_ips 中选择一个合适的
|
||||
fn get_local_ip() -> Option<String> {
|
||||
// 创建一个 UDP socket 并连接到外部地址(不会真正发送数据)
|
||||
// 这样可以获取到本机用于出站连接的 IP 地址
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||
socket.connect("8.8.8.8:80").ok()?;
|
||||
let local_addr = socket.local_addr().ok()?;
|
||||
let ip_str = local_addr.ip().to_string();
|
||||
|
||||
// 检查是否是 VPN 地址 (198.18.x.x)
|
||||
if let IpAddr::V4(ipv4) = local_addr.ip() {
|
||||
if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) {
|
||||
// 是 VPN 地址,尝试从 all_ips 中获取真实的局域网 IP
|
||||
let all_ips = get_all_local_ips();
|
||||
// 优先选择 192.168.x.x
|
||||
if let Some(ip) = all_ips.iter().find(|ip| ip.starts_with("192.168.")) {
|
||||
return Some(ip.clone());
|
||||
}
|
||||
// 其次选择任意私有 IP
|
||||
if let Some(ip) = all_ips.first() {
|
||||
return Some(ip.clone());
|
||||
}
|
||||
// 如果没有私有 IP,返回 127.0.0.1
|
||||
return Some("127.0.0.1".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Some(ip_str)
|
||||
}
|
||||
|
||||
/// 获取所有本地网络接口的 IP 地址
|
||||
///
|
||||
/// 返回所有非回环的 IPv4 地址,过滤掉 VPN 和虚拟网卡
|
||||
fn get_all_local_ips() -> Vec<String> {
|
||||
let mut ips = Vec::new();
|
||||
|
||||
// 使用 if-addrs crate 获取所有网络接口
|
||||
if let Ok(interfaces) = if_addrs::get_if_addrs() {
|
||||
for iface in interfaces {
|
||||
// 只处理 IPv4 地址
|
||||
if let IpAddr::V4(ipv4) = iface.ip() {
|
||||
// 过滤掉回环地址
|
||||
if ipv4.is_loopback() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 过滤掉链路本地地址 (169.254.x.x)
|
||||
if ipv4.octets()[0] == 169 && ipv4.octets()[1] == 254 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 过滤掉常见的 VPN 地址段
|
||||
// 198.18.0.0/15 (用于基准测试)
|
||||
if ipv4.octets()[0] == 198 && (ipv4.octets()[1] == 18 || ipv4.octets()[1] == 19) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只保留私有网络地址
|
||||
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||||
let is_private = ipv4.octets()[0] == 10
|
||||
|| (ipv4.octets()[0] == 172
|
||||
&& (ipv4.octets()[1] >= 16 && ipv4.octets()[1] <= 31))
|
||||
|| (ipv4.octets()[0] == 192 && ipv4.octets()[1] == 168);
|
||||
|
||||
if is_private {
|
||||
ips.push(ipv4.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// 根据监听地址生成可访问的 URL
|
||||
///
|
||||
/// 用于生成客户端配置中的 API URL。
|
||||
///
|
||||
/// # 参数
|
||||
/// - `listen_host`: 服务器监听地址
|
||||
/// - `port`: 服务器端口
|
||||
///
|
||||
/// # 返回
|
||||
/// - 如果监听地址为 `0.0.0.0`,返回局域网 IP 或 `127.0.0.1`
|
||||
/// - 如果监听地址为 `127.0.0.1` 或 `localhost`,返回 `127.0.0.1`
|
||||
/// - 其他情况返回原始地址
|
||||
pub fn get_accessible_host(listen_host: &str) -> String {
|
||||
match listen_host {
|
||||
"0.0.0.0" => {
|
||||
// 获取局域网 IP,如果没有则使用 127.0.0.1
|
||||
get_network_info()
|
||||
.ok()
|
||||
.and_then(|info| info.lan_ip)
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
}
|
||||
"localhost" => "127.0.0.1".to_string(),
|
||||
_ => listen_host.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据监听地址生成可访问的 URL
|
||||
///
|
||||
/// # 参数
|
||||
/// - `listen_host`: 服务器监听地址
|
||||
/// - `port`: 服务器端口
|
||||
///
|
||||
/// # 返回
|
||||
/// 格式为 `http://{host}:{port}` 的 URL
|
||||
pub fn get_accessible_url(listen_host: &str, port: u16) -> String {
|
||||
let host = get_accessible_host(listen_host);
|
||||
format!("http://{host}:{port}")
|
||||
}
|
||||
|
||||
/// 根据监听地址生成本地访问的 URL
|
||||
///
|
||||
/// 用于 Agent 等本地组件访问服务器。
|
||||
/// 对于 `0.0.0.0`,返回 `127.0.0.1`(本地访问)。
|
||||
///
|
||||
/// # 参数
|
||||
/// - `listen_host`: 服务器监听地址
|
||||
/// - `port`: 服务器端口
|
||||
///
|
||||
/// # 返回
|
||||
/// 格式为 `http://{host}:{port}` 的 URL
|
||||
#[allow(dead_code)]
|
||||
pub fn get_local_url(listen_host: &str, port: u16) -> String {
|
||||
let host = match listen_host {
|
||||
"0.0.0.0" | "localhost" => "127.0.0.1".to_string(),
|
||||
_ => listen_host.to_string(),
|
||||
};
|
||||
format!("http://{host}:{port}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_host_localhost() {
|
||||
assert_eq!(get_accessible_host("127.0.0.1"), "127.0.0.1");
|
||||
assert_eq!(get_accessible_host("localhost"), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_host_specific_ip() {
|
||||
assert_eq!(get_accessible_host("192.168.1.100"), "192.168.1.100");
|
||||
assert_eq!(get_accessible_host("10.0.0.1"), "10.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_local_url() {
|
||||
assert_eq!(get_local_url("0.0.0.0", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(get_local_url("127.0.0.1", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(get_local_url("localhost", 8999), "http://127.0.0.1:8999");
|
||||
assert_eq!(
|
||||
get_local_url("192.168.1.100", 8999),
|
||||
"http://192.168.1.100:8999"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_accessible_url_specific_ip() {
|
||||
assert_eq!(
|
||||
get_accessible_url("192.168.1.100", 8999),
|
||||
"http://192.168.1.100:8999"
|
||||
);
|
||||
assert_eq!(
|
||||
get_accessible_url("127.0.0.1", 8999),
|
||||
"http://127.0.0.1:8999"
|
||||
);
|
||||
}
|
||||
proxycast_core::network::get_network_info()
|
||||
}
|
||||
|
||||
+7
-370
@@ -1,375 +1,12 @@
|
||||
//! 日志管理模块
|
||||
use chrono::{Duration, Local, Utc};
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
//!
|
||||
//! 核心逻辑已迁移到 proxycast-core crate,本文件保留扩展函数。
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogStoreConfig {
|
||||
pub max_logs: usize,
|
||||
pub retention_days: u32,
|
||||
pub max_file_size: u64,
|
||||
pub enable_file_logging: bool,
|
||||
}
|
||||
pub use proxycast_core::logger::*;
|
||||
|
||||
impl Default for LogStoreConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_logs: 1000,
|
||||
retention_days: 7,
|
||||
max_file_size: 10 * 1024 * 1024,
|
||||
enable_file_logging: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::config::LoggingConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: String,
|
||||
pub level: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub struct LogStore {
|
||||
logs: VecDeque<LogEntry>,
|
||||
max_logs: usize,
|
||||
config: LogStoreConfig,
|
||||
log_file_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for LogStore {
|
||||
fn default() -> Self {
|
||||
// 默认日志文件路径: ~/.proxycast/logs/proxycast.log
|
||||
let log_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".proxycast")
|
||||
.join("logs");
|
||||
|
||||
// 创建日志目录
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
|
||||
let log_file = log_dir.join("proxycast.log");
|
||||
|
||||
let config = LogStoreConfig::default();
|
||||
|
||||
Self {
|
||||
logs: VecDeque::new(),
|
||||
max_logs: config.max_logs,
|
||||
config,
|
||||
log_file_path: Some(log_file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_config(logging: &crate::config::LoggingConfig) -> Self {
|
||||
let mut store = Self::default();
|
||||
store.config.retention_days = logging.retention_days;
|
||||
store.config.enable_file_logging = logging.enabled;
|
||||
store.max_logs = store.config.max_logs;
|
||||
store
|
||||
}
|
||||
|
||||
pub fn add(&mut self, level: &str, message: &str) {
|
||||
let sanitized = sanitize_log_message(message);
|
||||
let now = Utc::now();
|
||||
let entry = LogEntry {
|
||||
timestamp: now.to_rfc3339(),
|
||||
level: level.to_string(),
|
||||
message: sanitized.clone(),
|
||||
};
|
||||
|
||||
self.logs.push_back(entry.clone());
|
||||
|
||||
// 写入日志文件
|
||||
if self.config.enable_file_logging {
|
||||
if let Some(ref path) = self.log_file_path {
|
||||
self.rotate_log_file_if_needed(path);
|
||||
let local_time = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
let log_line = format!("{} [{}] {}\n", local_time, level.to_uppercase(), sanitized);
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = file.write_all(log_line.as_bytes());
|
||||
}
|
||||
self.prune_old_logs(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 保持日志数量在限制内
|
||||
if self.logs.len() > self.max_logs {
|
||||
self.logs.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录原始响应到单独的文件(用于调试)
|
||||
pub fn log_raw_response(&self, request_id: &str, body: &str) {
|
||||
if let Some(ref log_path) = self.log_file_path {
|
||||
let log_dir = log_path.parent().unwrap_or(std::path::Path::new("."));
|
||||
let raw_file = log_dir.join(format!("raw_response_{request_id}.txt"));
|
||||
let sanitized = sanitize_log_message(body);
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&raw_file)
|
||||
{
|
||||
let _ = file.write_all(sanitized.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_logs(&self) -> Vec<LogEntry> {
|
||||
self.logs.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.logs.clear();
|
||||
}
|
||||
|
||||
pub fn get_log_file_path(&self) -> Option<String> {
|
||||
self.log_file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
fn rotate_log_file_if_needed(&self, path: &PathBuf) {
|
||||
let Ok(metadata) = fs::metadata(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if metadata.len() <= self.config.max_file_size {
|
||||
return;
|
||||
}
|
||||
|
||||
let suffix = Local::now().format("%Y%m%d-%H%M%S");
|
||||
let rotated = path.with_file_name(format!(
|
||||
"{}.{}",
|
||||
path.file_name().unwrap_or_default().to_string_lossy(),
|
||||
suffix
|
||||
));
|
||||
|
||||
let _ = fs::rename(path, &rotated);
|
||||
self.prune_old_logs(path);
|
||||
}
|
||||
|
||||
fn prune_old_logs(&self, path: &PathBuf) {
|
||||
let Some(dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
self.archive_old_logs(path);
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
let cutoff = Utc::now() - Duration::days(self.config.retention_days as i64);
|
||||
let prefix = format!(
|
||||
"{}.",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
);
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if !file_name.starts_with(&prefix) {
|
||||
continue;
|
||||
}
|
||||
let Ok(metadata) = entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
continue;
|
||||
};
|
||||
let modified = chrono::DateTime::<Utc>::from(modified);
|
||||
if modified < cutoff {
|
||||
let _ = fs::remove_file(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn archive_old_logs(&self, path: &PathBuf) {
|
||||
let Some(dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
let archive_cutoff = Utc::now() - Duration::days(7);
|
||||
let delete_cutoff = Utc::now() - Duration::days(30);
|
||||
let prefix = format!(
|
||||
"{}.",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
);
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
if !file_name.starts_with(&prefix) {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
let Ok(metadata) = entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
continue;
|
||||
};
|
||||
let modified = chrono::DateTime::<Utc>::from(modified);
|
||||
|
||||
if file_name.ends_with(".gz") {
|
||||
if modified < delete_cutoff {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if modified >= archive_cutoff {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut input = Vec::new();
|
||||
if let Ok(mut file) = fs::File::open(&path) {
|
||||
if file.read_to_end(&mut input).is_err() {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let gz_path = path.with_extension(format!(
|
||||
"{}.gz",
|
||||
path.extension().unwrap_or_default().to_string_lossy()
|
||||
));
|
||||
if let Ok(gz_file) = fs::File::create(&gz_path) {
|
||||
let mut encoder = GzEncoder::new(gz_file, Compression::default());
|
||||
if encoder.write_all(&input).is_ok() && encoder.finish().is_ok() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub type SharedLogStore = Arc<RwLock<LogStore>>;
|
||||
|
||||
/// P2 安全修复:扩展日志脱敏规则,覆盖更多敏感字段
|
||||
pub fn sanitize_log_message(message: &str) -> String {
|
||||
let patterns = [
|
||||
// Bearer token
|
||||
(r"Bearer\s+[A-Za-z0-9._-]+", "Bearer ***"),
|
||||
// API key 各种格式
|
||||
(
|
||||
r#"api[_-]?key["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"api_key: ***",
|
||||
),
|
||||
// 通用 token
|
||||
(r#"token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#, "token: ***"),
|
||||
// P2 新增:access_token
|
||||
(
|
||||
r#"access[_-]?token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"access_token: ***",
|
||||
),
|
||||
// P2 新增:refresh_token
|
||||
(
|
||||
r#"refresh[_-]?token["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"refresh_token: ***",
|
||||
),
|
||||
// P2 新增:client_secret
|
||||
(
|
||||
r#"client[_-]?secret["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"client_secret: ***",
|
||||
),
|
||||
// P2 新增:authorization header
|
||||
(
|
||||
r#"[Aa]uthorization["']?\s*[:=]\s*["']?[A-Za-z0-9._\s-]+"#,
|
||||
"authorization: ***",
|
||||
),
|
||||
// P2 新增:password
|
||||
(r#"password["']?\s*[:=]\s*["']?[^\s"',}]+"#, "password: ***"),
|
||||
// P2 新增:secret
|
||||
(
|
||||
r#"secret["']?\s*[:=]\s*["']?[A-Za-z0-9._-]+"#,
|
||||
"secret: ***",
|
||||
),
|
||||
];
|
||||
|
||||
let mut sanitized = message.to_string();
|
||||
for (pattern, replacement) in patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
sanitized = re.replace_all(&sanitized, replacement).to_string();
|
||||
}
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::sanitize_log_message;
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_bearer_token() {
|
||||
let input = "Authorization: Bearer abcDEF123._-XYZ";
|
||||
let output = sanitize_log_message(input);
|
||||
// 验证敏感 token 被脱敏
|
||||
assert!(!output.contains("abcDEF123"));
|
||||
assert!(output.contains("***"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_api_key() {
|
||||
let input = r#"request api_key="sk-test_123.456-ABC" end"#;
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("api_key: ***"));
|
||||
assert!(!output.contains("sk-test_123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_access_token() {
|
||||
let input = "access_token=atk_12345";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("access_token: ***"));
|
||||
assert!(!output.contains("atk_12345"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_refresh_token() {
|
||||
let input = "refresh_token: rtk_ABCDE-123";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("refresh_token: ***"));
|
||||
assert!(!output.contains("rtk_ABCDE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_client_secret() {
|
||||
let input = "client_secret = \"cs_SeCreT-999\"";
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("client_secret: ***"));
|
||||
assert!(!output.contains("cs_SeCreT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_password() {
|
||||
let input = r#"{"password":"p@ssW0rd!"}"#;
|
||||
let output = sanitize_log_message(input);
|
||||
assert!(output.contains("password: ***"));
|
||||
assert!(!output.contains("p@ssW0rd!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plain_text_unchanged() {
|
||||
let input = "这是一段普通日志,不包含任何敏感字段。";
|
||||
let output = sanitize_log_message(input);
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
/// 使用 LoggingConfig 创建 LogStore
|
||||
pub fn create_log_store_from_config(logging: &LoggingConfig) -> LogStore {
|
||||
LogStore::with_custom_config(logging.retention_days, logging.enabled)
|
||||
}
|
||||
|
||||
+3
-1836
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user