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:
@@ -67,14 +67,17 @@ async fn verify_api_token(state: &AppState, raw_token: &str, client_ip: Option<S
|
||||
}
|
||||
}
|
||||
|
||||
// 异步更新 last_used_at(不阻塞请求)
|
||||
let db = state.db.clone();
|
||||
tokio::spawn(async move {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = sqlx::query("UPDATE api_tokens SET last_used_at = $1 WHERE token_hash = $2")
|
||||
.bind(&now).bind(&token_hash)
|
||||
.execute(&db).await;
|
||||
});
|
||||
// 异步更新 last_used_at — 通过 Worker 通道派发,受 SpawnLimiter 门控
|
||||
// 替换原来的 tokio::spawn(DB UPDATE),消除每请求无限制 spawn
|
||||
{
|
||||
use crate::workers::update_last_used::UpdateLastUsedArgs;
|
||||
let args = UpdateLastUsedArgs {
|
||||
token_hash: token_hash.to_string(),
|
||||
};
|
||||
if let Err(e) = state.worker_dispatcher.dispatch("update_last_used", args).await {
|
||||
tracing::debug!("Failed to dispatch update_last_used: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthContext {
|
||||
account_id,
|
||||
@@ -84,23 +87,43 @@ async fn verify_api_token(state: &AppState, raw_token: &str, client_ip: Option<S
|
||||
})
|
||||
}
|
||||
|
||||
/// 从请求中提取客户端 IP
|
||||
fn extract_client_ip(req: &Request) -> Option<String> {
|
||||
// 优先从 ConnectInfo 获取
|
||||
if let Some(ConnectInfo(addr)) = req.extensions().get::<ConnectInfo<SocketAddr>>() {
|
||||
return Some(addr.ip().to_string());
|
||||
/// 从请求中提取客户端 IP(安全版:仅对 trusted_proxies 解析 XFF)
|
||||
fn extract_client_ip(req: &Request, trusted_proxies: &[String]) -> Option<String> {
|
||||
// 优先从 ConnectInfo 获取直接连接 IP
|
||||
let connect_ip = req.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|ConnectInfo(addr)| addr.ip().to_string());
|
||||
|
||||
// 仅当直接连接 IP 在 trusted_proxies 中时,才信任 XFF/X-Real-IP
|
||||
if let Some(ref ip) = connect_ip {
|
||||
if trusted_proxies.iter().any(|p| p == ip) {
|
||||
// 受信代理 → 从 XFF 取真实客户端 IP
|
||||
if let Some(forwarded) = req.headers()
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
if let Some(client) = forwarded.split(',').next() {
|
||||
let trimmed = client.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 尝试 X-Real-IP
|
||||
if let Some(real_ip) = req.headers()
|
||||
.get("x-real-ip")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
let trimmed = real_ip.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 回退到 X-Forwarded-For / X-Real-IP
|
||||
if let Some(forwarded) = req.headers()
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
return Some(forwarded.split(',').next()?.trim().to_string());
|
||||
}
|
||||
req.headers()
|
||||
.get("x-real-ip")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
|
||||
// 非受信来源或无代理头 → 返回直接连接 IP
|
||||
connect_ip
|
||||
}
|
||||
|
||||
/// 认证中间件: 从 JWT Cookie / Authorization Header / API Token 提取身份
|
||||
@@ -110,7 +133,10 @@ pub async fn auth_middleware(
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let client_ip = extract_client_ip(&req);
|
||||
let client_ip = {
|
||||
let config = state.config.read().await;
|
||||
extract_client_ip(&req, &config.server.trusted_proxies)
|
||||
};
|
||||
let auth_header = req.headers()
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
Reference in New Issue
Block a user