349 lines
10 KiB
Rust
349 lines
10 KiB
Rust
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use std::fs;
|
||
|
|
use std::path::{Path, PathBuf};
|
||
|
|
use anyhow::{Result, Context};
|
||
|
|
use tracing::{info, warn, error};
|
||
|
|
|
||
|
|
/// LLM API配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct LlmApiConfig {
|
||
|
|
pub api_key: String,
|
||
|
|
pub base_url: String,
|
||
|
|
pub model: String,
|
||
|
|
pub timeout: u64,
|
||
|
|
pub retry_times: u32,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for LlmApiConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
api_key: String::new(),
|
||
|
|
base_url: "https://open.bigmodel.cn/api/paas/v4".to_string(),
|
||
|
|
model: "glm-4.7-flash".to_string(),
|
||
|
|
timeout: 120,
|
||
|
|
retry_times: 3,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 爬虫配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct SpiderConfig {
|
||
|
|
pub target_url: String,
|
||
|
|
pub xpath: String,
|
||
|
|
pub user_agent: String,
|
||
|
|
pub fetch_interval: u64,
|
||
|
|
pub retry_times: u32,
|
||
|
|
pub retry_interval: u64,
|
||
|
|
pub chrome_path: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for SpiderConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
target_url: "https://example.com".to_string(),
|
||
|
|
xpath: "//a[contains(@class, 'linkblack')]".to_string(),
|
||
|
|
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36".to_string(),
|
||
|
|
fetch_interval: 60,
|
||
|
|
retry_times: 3,
|
||
|
|
retry_interval: 5,
|
||
|
|
chrome_path: String::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// UI配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct UiConfig {
|
||
|
|
pub opacity: f32,
|
||
|
|
pub is_on_top: bool,
|
||
|
|
pub thresholds: Thresholds,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct Thresholds {
|
||
|
|
pub cold: i32,
|
||
|
|
pub warm: i32,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for Thresholds {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
cold: 30,
|
||
|
|
warm: 70,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for UiConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
opacity: 0.9,
|
||
|
|
is_on_top: true,
|
||
|
|
thresholds: Thresholds::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 数据库配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct DatabaseConfig {
|
||
|
|
pub path: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for DatabaseConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
path: "guba.db".to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 日志配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct LoggingConfig {
|
||
|
|
pub level: String,
|
||
|
|
pub path: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for LoggingConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
level: "INFO".to_string(),
|
||
|
|
path: "guba.log".to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 应用配置
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct AppConfig {
|
||
|
|
pub llm_api: LlmApiConfig,
|
||
|
|
pub spider: SpiderConfig,
|
||
|
|
pub ui: UiConfig,
|
||
|
|
pub database: DatabaseConfig,
|
||
|
|
pub logging: LoggingConfig,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for AppConfig {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self {
|
||
|
|
llm_api: LlmApiConfig::default(),
|
||
|
|
spider: SpiderConfig::default(),
|
||
|
|
ui: UiConfig::default(),
|
||
|
|
database: DatabaseConfig::default(),
|
||
|
|
logging: LoggingConfig::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 配置管理器
|
||
|
|
pub struct ConfigManager {
|
||
|
|
config_path: PathBuf,
|
||
|
|
config: AppConfig,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ConfigManager {
|
||
|
|
/// 创建新的配置管理器
|
||
|
|
pub fn new(config_path: &str) -> Result<Self> {
|
||
|
|
let config_path = Self::get_config_path(config_path)?;
|
||
|
|
info!("配置管理器初始化,配置文件路径: {:?}", config_path);
|
||
|
|
|
||
|
|
let config = if config_path.exists() {
|
||
|
|
Self::load_from_file(&config_path)?
|
||
|
|
} else {
|
||
|
|
warn!("配置文件不存在,使用默认配置");
|
||
|
|
AppConfig::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
Ok(Self {
|
||
|
|
config_path,
|
||
|
|
config,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 获取配置文件路径
|
||
|
|
fn get_config_path(config_path: &str) -> Result<PathBuf> {
|
||
|
|
// 检查是否是打包后的环境
|
||
|
|
let exe_path = std::env::current_exe()?;
|
||
|
|
let exe_dir = exe_path.parent()
|
||
|
|
.context("无法获取可执行文件目录")?;
|
||
|
|
|
||
|
|
// 优先使用可执行文件所在目录
|
||
|
|
let path = exe_dir.join(config_path);
|
||
|
|
Ok(path)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 从文件加载配置
|
||
|
|
fn load_from_file(path: &Path) -> Result<AppConfig> {
|
||
|
|
let content = fs::read_to_string(path)
|
||
|
|
.with_context(|| format!("读取配置文件失败: {:?}", path))?;
|
||
|
|
|
||
|
|
let loaded: serde_json::Value = serde_json::from_str(&content)
|
||
|
|
.with_context(|| "解析配置文件失败")?;
|
||
|
|
|
||
|
|
// 合并默认配置和加载的配置
|
||
|
|
let default_config = serde_json::to_value(AppConfig::default())?;
|
||
|
|
let merged = Self::merge_config(&default_config, &loaded);
|
||
|
|
|
||
|
|
let config: AppConfig = serde_json::from_value(merged)?;
|
||
|
|
info!("配置加载成功");
|
||
|
|
Ok(config)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 递归合并配置
|
||
|
|
fn merge_config(default: &serde_json::Value, loaded: &serde_json::Value) -> serde_json::Value {
|
||
|
|
match (default, loaded) {
|
||
|
|
(serde_json::Value::Object(mut default_map), serde_json::Value::Object(loaded_map)) => {
|
||
|
|
for (key, value) in loaded_map {
|
||
|
|
if let Some(default_value) = default_map.get(key) {
|
||
|
|
default_map.insert(
|
||
|
|
key.clone(),
|
||
|
|
Self::merge_config(default_value, value),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
default_map.insert(key.clone(), value.clone());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
serde_json::Value::Object(default_map)
|
||
|
|
}
|
||
|
|
_ => loaded.clone(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 保存配置到文件
|
||
|
|
pub fn save(&self) -> Result<()> {
|
||
|
|
let content = serde_json::to_string_pretty(&self.config)?;
|
||
|
|
fs::write(&self.config_path, content)
|
||
|
|
.with_context(|| format!("保存配置文件失败: {:?}", self.config_path))?;
|
||
|
|
info!("配置保存成功");
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 获取嵌套配置值
|
||
|
|
pub fn get(&self, keys: &[&str]) -> Option<serde_json::Value> {
|
||
|
|
let mut value = serde_json::to_value(&self.config).ok()?;
|
||
|
|
for key in keys {
|
||
|
|
value = value.get(key)?.clone();
|
||
|
|
}
|
||
|
|
Some(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 设置嵌套配置值
|
||
|
|
pub fn set(&mut self, keys: &[&str], value: serde_json::Value) -> Result<()> {
|
||
|
|
let mut config_value = serde_json::to_value(&self.config)?;
|
||
|
|
let mut current = &mut config_value;
|
||
|
|
|
||
|
|
for key in &keys[..keys.len() - 1] {
|
||
|
|
current = current
|
||
|
|
.as_object_mut()
|
||
|
|
.context("配置不是对象")?
|
||
|
|
.get_mut(*key)
|
||
|
|
.context("配置键不存在")?;
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(last_key) = keys.last() {
|
||
|
|
current
|
||
|
|
.as_object_mut()
|
||
|
|
.context("配置不是对象")?
|
||
|
|
.insert(last_key.to_string(), value);
|
||
|
|
}
|
||
|
|
|
||
|
|
self.config = serde_json::from_value(config_value)?;
|
||
|
|
self.save()?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
// Getters
|
||
|
|
pub fn llm_api_config(&self) -> &LlmApiConfig {
|
||
|
|
&self.config.llm_api
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn spider_config(&self) -> &SpiderConfig {
|
||
|
|
&self.config.spider
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn ui_config(&self) -> &UiConfig {
|
||
|
|
&self.config.ui
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn database_config(&self) -> &DatabaseConfig {
|
||
|
|
&self.config.database
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn logging_config(&self) -> &LoggingConfig {
|
||
|
|
&self.config.logging
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update methods
|
||
|
|
pub fn update_llm_api(&mut self, api_key: Option<String>, base_url: Option<String>,
|
||
|
|
model: Option<String>, timeout: Option<u64>,
|
||
|
|
retry_times: Option<u32>) -> Result<()> {
|
||
|
|
if let Some(api_key) = api_key {
|
||
|
|
self.config.llm_api.api_key = api_key;
|
||
|
|
}
|
||
|
|
if let Some(base_url) = base_url {
|
||
|
|
self.config.llm_api.base_url = base_url;
|
||
|
|
}
|
||
|
|
if let Some(model) = model {
|
||
|
|
self.config.llm_api.model = model;
|
||
|
|
}
|
||
|
|
if let Some(timeout) = timeout {
|
||
|
|
self.config.llm_api.timeout = timeout;
|
||
|
|
}
|
||
|
|
if let Some(retry_times) = retry_times {
|
||
|
|
self.config.llm_api.retry_times = retry_times;
|
||
|
|
}
|
||
|
|
info!("LLM API配置已更新");
|
||
|
|
self.save()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn update_spider(&mut self, target_url: Option<String>, xpath: Option<String>,
|
||
|
|
user_agent: Option<String>, fetch_interval: Option<u64>,
|
||
|
|
retry_times: Option<u32>, retry_interval: Option<u64>,
|
||
|
|
chrome_path: Option<String>) -> Result<()> {
|
||
|
|
if let Some(target_url) = target_url {
|
||
|
|
self.config.spider.target_url = target_url;
|
||
|
|
}
|
||
|
|
if let Some(xpath) = xpath {
|
||
|
|
self.config.spider.xpath = xpath;
|
||
|
|
}
|
||
|
|
if let Some(user_agent) = user_agent {
|
||
|
|
self.config.spider.user_agent = user_agent;
|
||
|
|
}
|
||
|
|
if let Some(fetch_interval) = fetch_interval {
|
||
|
|
self.config.spider.fetch_interval = fetch_interval;
|
||
|
|
}
|
||
|
|
if let Some(retry_times) = retry_times {
|
||
|
|
self.config.spider.retry_times = retry_times;
|
||
|
|
}
|
||
|
|
if let Some(retry_interval) = retry_interval {
|
||
|
|
self.config.spider.retry_interval = retry_interval;
|
||
|
|
}
|
||
|
|
if let Some(chrome_path) = chrome_path {
|
||
|
|
self.config.spider.chrome_path = chrome_path;
|
||
|
|
}
|
||
|
|
info!("爬虫配置已更新");
|
||
|
|
self.save()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn update_ui(&mut self, opacity: Option<f32>, is_on_top: Option<bool>,
|
||
|
|
cold_threshold: Option<i32>, warm_threshold: Option<i32>) -> Result<()> {
|
||
|
|
if let Some(opacity) = opacity {
|
||
|
|
self.config.ui.opacity = opacity.clamp(0.3, 1.0);
|
||
|
|
}
|
||
|
|
if let Some(is_on_top) = is_on_top {
|
||
|
|
self.config.ui.is_on_top = is_on_top;
|
||
|
|
}
|
||
|
|
if let Some(cold) = cold_threshold {
|
||
|
|
self.config.ui.thresholds.cold = cold;
|
||
|
|
}
|
||
|
|
if let Some(warm) = warm_threshold {
|
||
|
|
self.config.ui.thresholds.warm = warm;
|
||
|
|
}
|
||
|
|
info!("UI配置已更新");
|
||
|
|
self.save()
|
||
|
|
}
|
||
|
|
}
|