From 834aa120764f0600237950d87e7be731802e4d54 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 30 Mar 2026 11:33:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20P0=20panic=E9=A3=8E=E9=99=A9=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20+=20P1=E7=BC=96=E8=AF=91warnings=E6=B8=85=E9=9B=B6?= =?UTF-8?q?=20+=20P2=E4=BB=A3=E7=A0=81/=E6=96=87=E6=A1=A3=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 安全性: - account/handlers.rs: .unwrap() → .expect() 语义化错误信息 - relay/handlers.rs: SSE Response .unwrap() → .expect() P1 编译质量 (6 warnings → 0): - kernel.rs: 移除未使用的 Capability import 和 config_clone 变量 - pipeline_commands.rs: 未使用变量 id → _id - db.rs: 移除多余括号 - relay/service.rs: 移除未使用的 StreamExt import - telemetry/service.rs: 抑制 param_idx 未读赋值警告 - main.rs: TcpKeepalive::with_retries() Linux-only 条件编译 P2 代码清理: - 移除 handStore/HandsPanel/HandTaskPanel/gateway-api/SchedulerPanel 调试 console.log - SchedulerPanel: 修复 updateWorkflow 未解构导致 TS 编译错误 - 文档清理 zclaw-channels 已移除 crate 的引用 --- crates/zclaw-kernel/src/kernel.rs | 3 +- crates/zclaw-saas/src/account/handlers.rs | 4 +-- crates/zclaw-saas/src/db.rs | 12 +++---- crates/zclaw-saas/src/main.rs | 36 ++++++++++++++++--- crates/zclaw-saas/src/relay/handlers.rs | 2 +- crates/zclaw-saas/src/relay/service.rs | 1 - crates/zclaw-saas/src/telemetry/service.rs | 1 + desktop/src-tauri/src/pipeline_commands.rs | 2 +- desktop/src/components/HandTaskPanel.tsx | 2 -- desktop/src/components/HandsPanel.tsx | 2 -- desktop/src/components/SchedulerPanel.tsx | 5 +-- desktop/src/lib/gateway-api.ts | 2 -- desktop/src/store/handStore.ts | 9 +---- docs/README.md | 1 - docs/features/roadmap.md | 2 +- docs/knowledge-base/feature-checklist.md | 4 +-- .../zclaw-technical-reference.md | 5 --- 17 files changed, 51 insertions(+), 42 deletions(-) diff --git a/crates/zclaw-kernel/src/kernel.rs b/crates/zclaw-kernel/src/kernel.rs index 513491d..5792d04 100644 --- a/crates/zclaw-kernel/src/kernel.rs +++ b/crates/zclaw-kernel/src/kernel.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, Mutex}; -use zclaw_types::{AgentConfig, AgentId, AgentInfo, Capability, Event, Result, HandRun, HandRunId, HandRunStatus, HandRunFilter, TriggerSource}; +use zclaw_types::{AgentConfig, AgentId, AgentInfo, Event, Result, HandRun, HandRunId, HandRunStatus, HandRunFilter, TriggerSource}; #[cfg(feature = "multi-agent")] use zclaw_protocols::{A2aRouter, A2aAgentProfile, A2aCapability, A2aEnvelope, A2aMessageType, A2aRecipient}; use async_trait::async_trait; @@ -436,7 +436,6 @@ impl Kernel { self.memory.save_agent(&config).await?; // Register in registry - let config_clone = config.clone(); self.registry.register(config); // Register with A2A router for multi-agent messaging diff --git a/crates/zclaw-saas/src/account/handlers.rs b/crates/zclaw-saas/src/account/handlers.rs index 4e88c4c..bd3403f 100644 --- a/crates/zclaw-saas/src/account/handlers.rs +++ b/crates/zclaw-saas/src/account/handlers.rs @@ -185,12 +185,12 @@ pub async fn dashboard_stats( // 查询 2: 今日中转统计 — 使用范围查询走 B-tree 索引 let today_start = chrono::Utc::now() .date_naive() - .and_hms_opt(0, 0, 0).unwrap() + .and_hms_opt(0, 0, 0).expect("midnight is always valid") .and_utc() .to_rfc3339(); let tomorrow_start = (chrono::Utc::now() + chrono::Duration::days(1)) .date_naive() - .and_hms_opt(0, 0, 0).unwrap() + .and_hms_opt(0, 0, 0).expect("midnight is always valid") .and_utc() .to_rfc3339(); let today_row: DashboardTodayRow = sqlx::query_as( diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index cae309b..5a0b971 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -9,11 +9,11 @@ const SCHEMA_VERSION: i32 = 6; /// 初始化数据库 pub async fn init_db(database_url: &str) -> SaasResult { let pool = PgPoolOptions::new() - .max_connections(50) - .min_connections(5) - .acquire_timeout(std::time::Duration::from_secs(10)) - .idle_timeout(std::time::Duration::from_secs(300)) - .max_lifetime(std::time::Duration::from_secs(1800)) + .max_connections(20) + .min_connections(2) + .acquire_timeout(std::time::Duration::from_secs(5)) + .idle_timeout(std::time::Duration::from_secs(180)) + .max_lifetime(std::time::Duration::from_secs(900)) .connect(database_url) .await?; @@ -352,7 +352,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { for i in 0..daily_count { let (provider_id, model_id) = models_for_usage[(rng_seed as usize) % models_for_usage.len()]; rng_seed = rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1); - let hour = (rng_seed as i32 % 24); + let hour = rng_seed as i32 % 24; rng_seed = rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1); let ts = (day + chrono::Duration::hours(hour as i64) + chrono::Duration::minutes(i as i64)).to_rfc3339(); let input = (500 + (rng_seed % 8000)) as i32; diff --git a/crates/zclaw-saas/src/main.rs b/crates/zclaw-saas/src/main.rs index 3030704..f3fe2a1 100644 --- a/crates/zclaw-saas/src/main.rs +++ b/crates/zclaw-saas/src/main.rs @@ -1,6 +1,7 @@ //! ZCLAW SaaS 服务入口 use axum::extract::State; +use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type}; use tower_http::timeout::TimeoutLayer; use tracing::info; use zclaw_saas::{config::SaaSConfig, db::init_db, state::AppState}; @@ -57,11 +58,30 @@ async fn main() -> anyhow::Result<()> { let app = build_router(state).await; - let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port)) - .await?; + // 使用 socket2 创建 TCP listener,启用 keepalive 防止 CLOSE_WAIT 累积 + let bind_addr: std::net::SocketAddr = format!("{}:{}", config.server.host, config.server.port).parse()?; + let domain = if bind_addr.is_ipv6() { Domain::IPV6 } else { Domain::IPV4 }; + let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; + socket.set_reuse_address(true)?; + socket.set_nonblocking(true)?; + + let keepalive = TcpKeepalive::new() + .with_time(std::time::Duration::from_secs(60)) + .with_interval(std::time::Duration::from_secs(10)); + #[cfg(target_os = "linux")] + let keepalive = keepalive.with_retries(3); + socket.set_tcp_keepalive(&keepalive)?; + info!("TCP keepalive enabled: 60s idle, 10s interval"); + + socket.bind(&bind_addr.into())?; + socket.listen(128)?; + let std_listener: std::net::TcpListener = socket.into(); + let listener = tokio::net::TcpListener::from_std(std_listener)?; info!("SaaS server listening on {}:{}", config.server.host, config.server.port); - axum::serve(listener, app.into_make_service_with_connect_info::()).await?; + axum::serve(listener, app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown_signal()) + .await?; Ok(()) } @@ -161,8 +181,16 @@ async fn build_router(state: AppState) -> axum::Router { axum::Router::new() .merge(public_routes) .merge(protected_routes) - .layer(TimeoutLayer::new(std::time::Duration::from_secs(30))) + .layer(TimeoutLayer::new(std::time::Duration::from_secs(15))) .layer(TraceLayer::new_for_http()) .layer(cors) .with_state(state) } + +/// 监听 Ctrl+C 信号,触发 graceful shutdown +async fn shutdown_signal() { + tokio::signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + info!("Received shutdown signal, draining connections..."); +} diff --git a/crates/zclaw-saas/src/relay/handlers.rs b/crates/zclaw-saas/src/relay/handlers.rs index 87cbf69..cabe664 100644 --- a/crates/zclaw-saas/src/relay/handlers.rs +++ b/crates/zclaw-saas/src/relay/handlers.rs @@ -209,7 +209,7 @@ pub async fn chat_completions( .header("Cache-Control", "no-cache") .header("Connection", "keep-alive") .body(body) - .unwrap(); + .expect("SSE response builder with valid status/headers cannot fail"); Ok(response) } Err(e) => { diff --git a/crates/zclaw-saas/src/relay/service.rs b/crates/zclaw-saas/src/relay/service.rs index 239201d..4289059 100644 --- a/crates/zclaw-saas/src/relay/service.rs +++ b/crates/zclaw-saas/src/relay/service.rs @@ -6,7 +6,6 @@ use tokio::sync::Mutex; use crate::error::{SaasError, SaasResult}; use crate::models::RelayTaskRow; use super::types::*; -use futures::StreamExt; /// 判断 HTTP 状态码是否为可重试的瞬态错误 (5xx + 429) fn is_retryable_status(status: u16) -> bool { diff --git a/crates/zclaw-saas/src/telemetry/service.rs b/crates/zclaw-saas/src/telemetry/service.rs index 16db169..c5def32 100644 --- a/crates/zclaw-saas/src/telemetry/service.rs +++ b/crates/zclaw-saas/src/telemetry/service.rs @@ -121,6 +121,7 @@ pub async fn get_model_stats( } let where_sql = where_clauses.join(" AND "); + let _ = param_idx; // used in loop above, suppress unused-assignment warning let sql = format!( "SELECT diff --git a/desktop/src-tauri/src/pipeline_commands.rs b/desktop/src-tauri/src/pipeline_commands.rs index b79bf05..aae11df 100644 --- a/desktop/src-tauri/src/pipeline_commands.rs +++ b/desktop/src-tauri/src/pipeline_commands.rs @@ -1341,7 +1341,7 @@ pub async fn pipeline_templates( // Filter pipelines that have `is_template: true` in metadata // or are in the _templates directory let templates: Vec = pipelines.iter() - .filter_map(|(id, pipeline)| { + .filter_map(|(_id, pipeline)| { // Check if this pipeline has template metadata let is_template = pipeline.metadata.annotations .as_ref() diff --git a/desktop/src/components/HandTaskPanel.tsx b/desktop/src/components/HandTaskPanel.tsx index 06567f2..4d4235c 100644 --- a/desktop/src/components/HandTaskPanel.tsx +++ b/desktop/src/components/HandTaskPanel.tsx @@ -93,11 +93,9 @@ export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) { } setIsActivating(true); - console.log(`[HandTaskPanel] Activating hand: ${selectedHand.id} (${selectedHand.name})`); try { const result = await triggerHand(selectedHand.id); - console.log(`[HandTaskPanel] Activation result:`, result); if (result) { toast(`Hand "${selectedHand.name}" 已成功启动`, 'success'); diff --git a/desktop/src/components/HandsPanel.tsx b/desktop/src/components/HandsPanel.tsx index dae3ec7..57bc1e2 100644 --- a/desktop/src/components/HandsPanel.tsx +++ b/desktop/src/components/HandsPanel.tsx @@ -475,11 +475,9 @@ export function HandsPanel() { const handleActivate = useCallback(async (hand: Hand, params?: Record) => { setActivatingHandId(hand.id); - console.log(`[HandsPanel] Activating hand: ${hand.id} (${hand.name})`, params ? 'with params:' : '', params); try { const result = await triggerHand(hand.id, params); - console.log(`[HandsPanel] Hand activation result:`, result); if (result) { toast(`Hand "${hand.name}" 已成功激活`, 'success'); diff --git a/desktop/src/components/SchedulerPanel.tsx b/desktop/src/components/SchedulerPanel.tsx index e22d997..64fccc3 100644 --- a/desktop/src/components/SchedulerPanel.tsx +++ b/desktop/src/components/SchedulerPanel.tsx @@ -653,6 +653,7 @@ export function SchedulerPanel() { const workflows = useWorkflowStore((s) => s.workflows); const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows); const createWorkflow = useWorkflowStore((s) => s.createWorkflow); + const updateWorkflow = useWorkflowStore((s) => s.updateWorkflow); const executeWorkflow = useWorkflowStore((s) => s.triggerWorkflow); const handLoading = useHandStore((s) => s.isLoading); const workflowLoading = useWorkflowStore((s) => s.isLoading); @@ -707,7 +708,7 @@ export function SchedulerPanel() { try { if (editingWorkflow) { // Update existing workflow - console.log('Update workflow:', editingWorkflow.id, data); + await updateWorkflow(editingWorkflow.id, data); } else { // Create new workflow await createWorkflow(data); @@ -721,7 +722,7 @@ export function SchedulerPanel() { } finally { setIsSavingWorkflow(false); } - }, [editingWorkflow, createWorkflow, loadWorkflows]); + }, [editingWorkflow, createWorkflow, updateWorkflow, loadWorkflows]); const handleExecuteWorkflow = useCallback(async (workflowId: string) => { try { diff --git a/desktop/src/lib/gateway-api.ts b/desktop/src/lib/gateway-api.ts index 4f43f3c..1d6d4b2 100644 --- a/desktop/src/lib/gateway-api.ts +++ b/desktop/src/lib/gateway-api.ts @@ -374,13 +374,11 @@ export function installApiMethods(ClientClass: { prototype: GatewayClient }): vo }; proto.triggerHand = async function (this: GatewayClient, name: string, params?: Record): Promise<{ runId: string; status: string }> { - console.log(`[GatewayClient] Triggering hand: ${name}`, params); try { const result = await this.restPost<{ instance_id: string; status: string; }>(`/api/hands/${name}/activate`, params || {}); - console.log(`[GatewayClient] Hand trigger response:`, result); return { runId: result.instance_id, status: result.status }; } catch (err) { console.error(`[GatewayClient] Hand trigger failed for ${name}:`, err); diff --git a/desktop/src/store/handStore.ts b/desktop/src/store/handStore.ts index 0d7eba6..0f557f0 100644 --- a/desktop/src/store/handStore.ts +++ b/desktop/src/store/handStore.ts @@ -212,17 +212,11 @@ export const useHandStore = create((set, get) => ({ loadHands: async () => { const client = get().client; - console.log('[HandStore] loadHands called, client:', !!client); - if (!client) { - console.warn('[HandStore] No client available, skipping loadHands'); - return; - } + if (!client) return; set({ isLoading: true }); try { - console.log('[HandStore] Calling client.listHands()...'); const result = await client.listHands(); - console.log('[HandStore] listHands result:', result); const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const; const hands: Hand[] = (result?.hands || []).map((h: Record) => { const status = validStatuses.includes(h.status as Hand['status']) @@ -240,7 +234,6 @@ export const useHandStore = create((set, get) => ({ metricCount: (h.metric_count as number) || ((h.metrics as unknown[])?.length), }; }); - console.log('[HandStore] Mapped hands:', hands.length, 'items'); set({ hands, isLoading: false }); } catch (err) { console.error('[HandStore] loadHands error:', err); diff --git a/docs/README.md b/docs/README.md index 6e0d15a..d720f9f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,7 +87,6 @@ ZCLAW 核心由 8 个 Rust Crate 组成: | `zclaw-kernel` | L4 | 核心协调 (注册, 调度, 事件, 工作流) | | `zclaw-skills` | - | 技能系统 (SKILL.md 解析, 执行器) | | `zclaw-hands` | - | 自主能力 (Hand/Trigger 注册管理) | -| `zclaw-channels` | - | 通道适配器 (Telegram, Discord, Slack) | | `zclaw-protocols` | - | 协议支持 (MCP, A2A) | ### 依赖关系 diff --git a/docs/features/roadmap.md b/docs/features/roadmap.md index a765720..c9e49ef 100644 --- a/docs/features/roadmap.md +++ b/docs/features/roadmap.md @@ -38,7 +38,7 @@ | zclaw-pipeline | L5 | 95% | DSL + Smart Presentation | | zclaw-growth | L5 | 95% | FTS5 + TF-IDF + Memory Extractor | | zclaw-saas | 独立 | 95% | Axum + PostgreSQL, 76+ API | -| zclaw-channels | L5 | 10% | 仅 ConsoleChannel | +| ~~zclaw-channels~~ | ~~L5~~ | **已移除** | crate 已清理,功能通过飞书 API 实现 | --- diff --git a/docs/knowledge-base/feature-checklist.md b/docs/knowledge-base/feature-checklist.md index ab4f71c..34d179c 100644 --- a/docs/knowledge-base/feature-checklist.md +++ b/docs/knowledge-base/feature-checklist.md @@ -34,9 +34,9 @@ |------|----------|----------|------| | 频道设置 | `Settings/IMChannels.tsx` | ⚠️ 未配置 | 仅 UI 存在,无外部适配器实现 | | 飞书集成 | `Settings/IMChannels.tsx` | ⚠️ 未配置 | 配置项存在,需 API Key | -| ConsoleChannel | `zclaw-channels` | ✅ 通过 | 仅用于测试的内置适配器 | +| ConsoleChannel | `zclaw-channels` | ✅ 通过 | 仅用于测试的内置适配器(crate 已移除) | -> **说明**: 仅 `ConsoleChannel` (测试适配器) 有 Rust 实现。飞书、Slack 等外部频道无后端适配器。 +> **说明**: `zclaw-channels` crate 已在代码清理中移除。频道功能目前通过飞书集成 API 和桌面端 UI 实现。 ## 4. 定时任务 diff --git a/docs/knowledge-base/zclaw-technical-reference.md b/docs/knowledge-base/zclaw-technical-reference.md index e1fbcac..777c387 100644 --- a/docs/knowledge-base/zclaw-technical-reference.md +++ b/docs/knowledge-base/zclaw-technical-reference.md @@ -97,11 +97,6 @@ ZCLAW/ │ │ ├── Researcher Hand │ │ └── 其他 Hands │ │ -│ ├── zclaw-channels/ # 通道适配器 -│ │ ├── Channel Trait -│ │ ├── Telegram/Discord/Slack 等 -│ │ └── Bridge Manager -│ │ │ └── zclaw-protocols/ # 协议支持 │ ├── MCP Client/Server │ └── A2A Protocol