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:
coso
2026-02-08 22:34:56 +08:00
parent a32133490e
commit 146db0d110
22 changed files with 2417 additions and 2550 deletions
+87
View File
@@ -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"
+8
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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::*;
+138 -34
View File
@@ -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 = "这是一段普通日志,不包含任何敏感字段。";
+158
View File
@@ -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"
);
}
}
+40
View File
@@ -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
@@ -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![
@@ -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
@@ -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 {
@@ -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;
/// 处理图像生成请求
///
@@ -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)]
@@ -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);
@@ -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 的内容
@@ -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
+3 -1
View File
@@ -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}"))?;
+3 -1
View File
@@ -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)
}
+7 -201
View File
@@ -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
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff