fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,15 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use secrecy::SecretString;
|
||||
|
||||
/// 当前期望的配置版本
|
||||
const CURRENT_CONFIG_VERSION: u32 = 1;
|
||||
|
||||
/// SaaS 服务器完整配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SaaSConfig {
|
||||
/// Configuration schema version
|
||||
#[serde(default = "default_config_version")]
|
||||
pub config_version: u32,
|
||||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub auth: AuthConfig,
|
||||
@@ -15,6 +21,8 @@ pub struct SaaSConfig {
|
||||
pub rate_limit: RateLimitConfig,
|
||||
#[serde(default)]
|
||||
pub scheduler: SchedulerConfig,
|
||||
#[serde(default)]
|
||||
pub payment: PaymentConfig,
|
||||
}
|
||||
|
||||
/// Scheduler 定时任务配置
|
||||
@@ -66,6 +74,30 @@ pub struct ServerConfig {
|
||||
pub struct DatabaseConfig {
|
||||
#[serde(default = "default_db_url")]
|
||||
pub url: String,
|
||||
/// 连接池最大连接数
|
||||
#[serde(default = "default_max_connections")]
|
||||
pub max_connections: u32,
|
||||
/// 连接池最小连接数
|
||||
#[serde(default = "default_min_connections")]
|
||||
pub min_connections: u32,
|
||||
/// 获取连接超时 (秒)
|
||||
#[serde(default = "default_acquire_timeout")]
|
||||
pub acquire_timeout_secs: u64,
|
||||
/// 空闲连接回收超时 (秒)
|
||||
#[serde(default = "default_idle_timeout")]
|
||||
pub idle_timeout_secs: u64,
|
||||
/// 连接最大生命周期 (秒)
|
||||
#[serde(default = "default_max_lifetime")]
|
||||
pub max_lifetime_secs: u64,
|
||||
/// Worker 并发上限 (Semaphore permits)
|
||||
#[serde(default = "default_worker_concurrency")]
|
||||
pub worker_concurrency: usize,
|
||||
/// 限流事件批量 flush 间隔 (秒)
|
||||
#[serde(default = "default_rate_limit_batch_interval")]
|
||||
pub rate_limit_batch_interval_secs: u64,
|
||||
/// 限流事件批量 flush 最大条目数
|
||||
#[serde(default = "default_rate_limit_batch_max")]
|
||||
pub rate_limit_batch_max_size: usize,
|
||||
}
|
||||
|
||||
/// 认证配置
|
||||
@@ -97,12 +129,21 @@ pub struct RelayConfig {
|
||||
pub max_attempts: u32,
|
||||
}
|
||||
|
||||
fn default_config_version() -> u32 { 1 }
|
||||
fn default_host() -> String { "0.0.0.0".into() }
|
||||
fn default_port() -> u16 { 8080 }
|
||||
fn default_db_url() -> String { "postgres://localhost:5432/zclaw".into() }
|
||||
fn default_jwt_hours() -> i64 { 24 }
|
||||
fn default_totp_issuer() -> String { "ZCLAW SaaS".into() }
|
||||
fn default_refresh_hours() -> i64 { 168 }
|
||||
fn default_max_connections() -> u32 { 100 }
|
||||
fn default_min_connections() -> u32 { 5 }
|
||||
fn default_acquire_timeout() -> u64 { 8 }
|
||||
fn default_idle_timeout() -> u64 { 180 }
|
||||
fn default_max_lifetime() -> u64 { 900 }
|
||||
fn default_worker_concurrency() -> usize { 20 }
|
||||
fn default_rate_limit_batch_interval() -> u64 { 5 }
|
||||
fn default_rate_limit_batch_max() -> usize { 500 }
|
||||
fn default_max_queue() -> usize { 1000 }
|
||||
fn default_max_concurrent() -> usize { 5 }
|
||||
fn default_batch_window() -> u64 { 50 }
|
||||
@@ -132,15 +173,115 @@ impl Default for RateLimitConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 支付配置
|
||||
///
|
||||
/// 支付宝和微信支付商户配置。所有字段通过环境变量传入(不写入 TOML 文件)。
|
||||
/// 字段缺失时自动降级为 mock 支付模式。
|
||||
///
|
||||
/// 注意:自定义 Debug 和 Serialize 实现会隐藏敏感字段。
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentConfig {
|
||||
/// 支付宝 App ID(来自支付宝开放平台)
|
||||
#[serde(default)]
|
||||
pub alipay_app_id: Option<String>,
|
||||
/// 支付宝商户私钥(RSA2)— 敏感,不序列化
|
||||
#[serde(default, skip_serializing)]
|
||||
pub alipay_private_key: Option<String>,
|
||||
/// 支付宝公钥证书路径(用于验签)
|
||||
#[serde(default)]
|
||||
pub alipay_cert_path: Option<String>,
|
||||
/// 支付宝回调通知 URL
|
||||
#[serde(default)]
|
||||
pub alipay_notify_url: Option<String>,
|
||||
/// 支付宝公钥(用于回调验签,PEM 格式)— 敏感,不序列化
|
||||
#[serde(default, skip_serializing)]
|
||||
pub alipay_public_key: Option<String>,
|
||||
|
||||
/// 微信支付商户号
|
||||
#[serde(default)]
|
||||
pub wechat_mch_id: Option<String>,
|
||||
/// 微信支付商户证书序列号
|
||||
#[serde(default)]
|
||||
pub wechat_serial_no: Option<String>,
|
||||
/// 微信支付商户私钥路径
|
||||
#[serde(default)]
|
||||
pub wechat_private_key_path: Option<String>,
|
||||
/// 微信支付 API v3 密钥 — 敏感,不序列化
|
||||
#[serde(default, skip_serializing)]
|
||||
pub wechat_api_v3_key: Option<String>,
|
||||
/// 微信支付回调通知 URL
|
||||
#[serde(default)]
|
||||
pub wechat_notify_url: Option<String>,
|
||||
/// 微信支付 App ID(公众号/小程序)
|
||||
#[serde(default)]
|
||||
pub wechat_app_id: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PaymentConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PaymentConfig")
|
||||
.field("alipay_app_id", &self.alipay_app_id)
|
||||
.field("alipay_private_key", &self.alipay_private_key.as_ref().map(|_| "***REDACTED***"))
|
||||
.field("alipay_cert_path", &self.alipay_cert_path)
|
||||
.field("alipay_notify_url", &self.alipay_notify_url)
|
||||
.field("alipay_public_key", &self.alipay_public_key.as_ref().map(|_| "***REDACTED***"))
|
||||
.field("wechat_mch_id", &self.wechat_mch_id)
|
||||
.field("wechat_serial_no", &self.wechat_serial_no)
|
||||
.field("wechat_private_key_path", &self.wechat_private_key_path)
|
||||
.field("wechat_api_v3_key", &self.wechat_api_v3_key.as_ref().map(|_| "***REDACTED***"))
|
||||
.field("wechat_notify_url", &self.wechat_notify_url)
|
||||
.field("wechat_app_id", &self.wechat_app_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaymentConfig {
|
||||
fn default() -> Self {
|
||||
// 优先从环境变量读取,未配置则降级 mock
|
||||
Self {
|
||||
alipay_app_id: std::env::var("ALIPAY_APP_ID").ok(),
|
||||
alipay_private_key: std::env::var("ALIPAY_PRIVATE_KEY").ok(),
|
||||
alipay_cert_path: std::env::var("ALIPAY_CERT_PATH").ok(),
|
||||
alipay_notify_url: std::env::var("ALIPAY_NOTIFY_URL").ok(),
|
||||
alipay_public_key: std::env::var("ALIPAY_PUBLIC_KEY").ok(),
|
||||
wechat_mch_id: std::env::var("WECHAT_PAY_MCH_ID").ok(),
|
||||
wechat_serial_no: std::env::var("WECHAT_PAY_SERIAL_NO").ok(),
|
||||
wechat_private_key_path: std::env::var("WECHAT_PAY_PRIVATE_KEY_PATH").ok(),
|
||||
wechat_api_v3_key: std::env::var("WECHAT_PAY_API_V3_KEY").ok(),
|
||||
wechat_notify_url: std::env::var("WECHAT_PAY_NOTIFY_URL").ok(),
|
||||
wechat_app_id: std::env::var("WECHAT_PAY_APP_ID").ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaymentConfig {
|
||||
/// 支付宝是否已完整配置
|
||||
pub fn alipay_configured(&self) -> bool {
|
||||
self.alipay_app_id.is_some()
|
||||
&& self.alipay_private_key.is_some()
|
||||
&& self.alipay_notify_url.is_some()
|
||||
}
|
||||
|
||||
/// 微信支付是否已完整配置
|
||||
pub fn wechat_configured(&self) -> bool {
|
||||
self.wechat_mch_id.is_some()
|
||||
&& self.wechat_serial_no.is_some()
|
||||
&& self.wechat_private_key_path.is_some()
|
||||
&& self.wechat_notify_url.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SaaSConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config_version: 1,
|
||||
server: ServerConfig::default(),
|
||||
database: DatabaseConfig::default(),
|
||||
auth: AuthConfig::default(),
|
||||
relay: RelayConfig::default(),
|
||||
rate_limit: RateLimitConfig::default(),
|
||||
scheduler: SchedulerConfig::default(),
|
||||
payment: PaymentConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +299,17 @@ impl Default for ServerConfig {
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self { url: default_db_url() }
|
||||
Self {
|
||||
url: default_db_url(),
|
||||
max_connections: default_max_connections(),
|
||||
min_connections: default_min_connections(),
|
||||
acquire_timeout_secs: default_acquire_timeout(),
|
||||
idle_timeout_secs: default_idle_timeout(),
|
||||
max_lifetime_secs: default_max_lifetime(),
|
||||
worker_concurrency: default_worker_concurrency(),
|
||||
rate_limit_batch_interval_secs: default_rate_limit_batch_interval(),
|
||||
rate_limit_batch_max_size: default_rate_limit_batch_max(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +371,26 @@ impl SaaSConfig {
|
||||
SaaSConfig::default()
|
||||
};
|
||||
|
||||
// 配置版本兼容性检查
|
||||
if config.config_version < CURRENT_CONFIG_VERSION {
|
||||
tracing::warn!(
|
||||
"[Config] config_version ({}) is below current version ({}). \
|
||||
Some features may not work correctly. \
|
||||
Please update your saas-config.toml. \
|
||||
See docs for migration guide.",
|
||||
config.config_version,
|
||||
CURRENT_CONFIG_VERSION
|
||||
);
|
||||
} else if config.config_version > CURRENT_CONFIG_VERSION {
|
||||
tracing::error!(
|
||||
"[Config] config_version ({}) is ahead of supported version ({}). \
|
||||
This server version may not support all configured features. \
|
||||
Consider upgrading the server.",
|
||||
config.config_version,
|
||||
CURRENT_CONFIG_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
// 环境变量覆盖数据库 URL (避免在配置文件中存储密码)
|
||||
if let Ok(db_url) = std::env::var("ZCLAW_DATABASE_URL") {
|
||||
config.database.url = db_url;
|
||||
|
||||
Reference in New Issue
Block a user