fix: P0 panic风险修复 + P1编译warnings清零 + P2代码/文档清理
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

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 的引用
This commit is contained in:
iven
2026-03-30 11:33:47 +08:00
parent 813b49a986
commit 834aa12076
17 changed files with 51 additions and 42 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -9,11 +9,11 @@ const SCHEMA_VERSION: i32 = 6;
/// 初始化数据库
pub async fn init_db(database_url: &str) -> SaasResult<PgPool> {
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;

View File

@@ -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::<std::net::SocketAddr>()).await?;
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
.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...");
}

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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<PipelineTemplateInfo> = 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()

View File

@@ -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');

View File

@@ -475,11 +475,9 @@ export function HandsPanel() {
const handleActivate = useCallback(async (hand: Hand, params?: Record<string, unknown>) => {
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');

View File

@@ -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 {

View File

@@ -374,13 +374,11 @@ export function installApiMethods(ClientClass: { prototype: GatewayClient }): vo
};
proto.triggerHand = async function (this: GatewayClient, name: string, params?: Record<string, unknown>): 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);

View File

@@ -212,17 +212,11 @@ export const useHandStore = create<HandStore>((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<string, unknown>) => {
const status = validStatuses.includes(h.status as Hand['status'])
@@ -240,7 +234,6 @@ export const useHandStore = create<HandStore>((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);

View File

@@ -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) |
### 依赖关系

View File

@@ -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 实现 |
---

View File

@@ -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. 定时任务

View File

@@ -97,11 +97,6 @@ ZCLAW/
│ │ ├── Researcher Hand
│ │ └── 其他 Hands
│ │
│ ├── zclaw-channels/ # 通道适配器
│ │ ├── Channel Trait
│ │ ├── Telegram/Discord/Slack 等
│ │ └── Bridge Manager
│ │
│ └── zclaw-protocols/ # 协议支持
│ ├── MCP Client/Server
│ └── A2A Protocol