feat(hands,desktop): C线差异化 — 管家日报 + 零配置引导优化
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
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
C1 管家日报: - 新增 _daily_report Hand (daily_report.rs) — 5个测试 - 增强 user_profile_store — PainPoint 结构体 + find_active_pains_since + resolve_pain - experience_store 新增 find_since 日期范围查询 - trajectory_store 新增 get_events_since 日期范围查询 - 新增 DailyReportPanel.tsx 前端日报面板 - Sidebar 新增"日报"导航入口 C3 零配置引导: - 修复行业卡点击后阶段推进 bug (industry_discovery → identity_setup) 验证: 940 tests PASS, 0 failures
This commit is contained in:
241
crates/zclaw-hands/src/hands/daily_report.rs
Normal file
241
crates/zclaw-hands/src/hands/daily_report.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
//! Daily Report Hand — generates a personalized daily briefing.
|
||||
//!
|
||||
//! System hand (`_daily_report`) triggered by SchedulerService at 09:00 cron.
|
||||
//! Produces a Markdown daily report containing:
|
||||
//! 1. Yesterday's conversation summary
|
||||
//! 2. Unresolved pain points follow-up
|
||||
//! 3. Recent experience highlights
|
||||
//! 4. Industry news placeholder
|
||||
//!
|
||||
//! Emits `daily-report:ready` Tauri event and persists to VikingStorage.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Hand, HandConfig, HandContext, HandResult, HandStatus};
|
||||
|
||||
/// Internal daily report hand.
|
||||
pub struct DailyReportHand {
|
||||
config: HandConfig,
|
||||
}
|
||||
|
||||
impl DailyReportHand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: HandConfig {
|
||||
id: "_daily_report".to_string(),
|
||||
name: "管家日报".to_string(),
|
||||
description: "Generates personalized daily briefing".to_string(),
|
||||
needs_approval: false,
|
||||
dependencies: vec![],
|
||||
input_schema: None,
|
||||
tags: vec!["system".to_string()],
|
||||
enabled: true,
|
||||
max_concurrent: 0,
|
||||
timeout_secs: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Hand for DailyReportHand {
|
||||
fn config(&self) -> &HandConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
|
||||
let agent_id = input
|
||||
.get("agent_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("default_user");
|
||||
|
||||
let industry = input
|
||||
.get("industry")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let trajectory_summary = input
|
||||
.get("trajectory_summary")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("昨日无对话记录");
|
||||
|
||||
let pain_points = input
|
||||
.get("pain_points")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let recent_experiences = input
|
||||
.get("recent_experiences")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let report = self.build_report(industry, trajectory_summary, &pain_points, &recent_experiences);
|
||||
|
||||
tracing::info!(
|
||||
"[DailyReportHand] Generated report for agent {} ({} pains, {} experiences)",
|
||||
agent_id,
|
||||
pain_points.len(),
|
||||
recent_experiences.len(),
|
||||
);
|
||||
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"agent_id": agent_id,
|
||||
"report": report,
|
||||
"pain_count": pain_points.len(),
|
||||
"experience_count": recent_experiences.len(),
|
||||
})))
|
||||
}
|
||||
|
||||
fn status(&self) -> HandStatus {
|
||||
HandStatus::Idle
|
||||
}
|
||||
}
|
||||
|
||||
impl DailyReportHand {
|
||||
fn build_report(
|
||||
&self,
|
||||
industry: &str,
|
||||
trajectory_summary: &str,
|
||||
pain_points: &[String],
|
||||
recent_experiences: &[String],
|
||||
) -> String {
|
||||
let industry_label = match industry {
|
||||
"healthcare" => "医疗行政",
|
||||
"education" => "教育培训",
|
||||
"garment" => "制衣制造",
|
||||
"ecommerce" => "电商零售",
|
||||
_ => "综合",
|
||||
};
|
||||
|
||||
let date = chrono::Local::now().format("%Y年%m月%d日").to_string();
|
||||
|
||||
let mut sections = vec![
|
||||
format!("# {} 管家日报 — {}", industry_label, date),
|
||||
String::new(),
|
||||
"## 昨日对话摘要".to_string(),
|
||||
trajectory_summary.to_string(),
|
||||
String::new(),
|
||||
];
|
||||
|
||||
if !pain_points.is_empty() {
|
||||
sections.push("## 待解决问题".to_string());
|
||||
for (i, pain) in pain_points.iter().enumerate() {
|
||||
sections.push(format!("{}. {}", i + 1, pain));
|
||||
}
|
||||
sections.push(String::new());
|
||||
}
|
||||
|
||||
if !recent_experiences.is_empty() {
|
||||
sections.push("## 昨日收获".to_string());
|
||||
for exp in recent_experiences {
|
||||
sections.push(format!("- {}", exp));
|
||||
}
|
||||
sections.push(String::new());
|
||||
}
|
||||
|
||||
sections.push("## 今日提醒".to_string());
|
||||
sections.push(self.daily_reminder(industry));
|
||||
sections.push(String::new());
|
||||
sections.push("祝你今天工作顺利!".to_string());
|
||||
|
||||
sections.join("\n")
|
||||
}
|
||||
|
||||
fn daily_reminder(&self, industry: &str) -> String {
|
||||
match industry {
|
||||
"healthcare" => "记得检查今日科室排班,关注耗材库存预警。".to_string(),
|
||||
"education" => "今日有课程安排吗?提前准备教学材料。".to_string(),
|
||||
"garment" => "关注今日生产进度,及时跟进订单交期。".to_string(),
|
||||
"ecommerce" => "检查库存预警和待发货订单,把握促销节奏。".to_string(),
|
||||
_ => "新的一天,新的开始。有什么需要我帮忙的随时说。".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use zclaw_types::AgentId;
|
||||
|
||||
#[test]
|
||||
fn test_build_report_basic() {
|
||||
let hand = DailyReportHand::new();
|
||||
let report = hand.build_report(
|
||||
"healthcare",
|
||||
"讨论了科室排班问题",
|
||||
&["排班冲突".to_string()],
|
||||
&["学会了用数据报表工具".to_string()],
|
||||
);
|
||||
assert!(report.contains("医疗行政"));
|
||||
assert!(report.contains("排班冲突"));
|
||||
assert!(report.contains("学会了用数据报表工具"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_report_empty() {
|
||||
let hand = DailyReportHand::new();
|
||||
let report = hand.build_report("", "昨日无对话记录", &[], &[]);
|
||||
assert!(report.contains("管家日报"));
|
||||
assert!(report.contains("综合"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_report_all_industries() {
|
||||
let hand = DailyReportHand::new();
|
||||
for industry in &["healthcare", "education", "garment", "ecommerce", "unknown"] {
|
||||
let report = hand.build_report(industry, "test", &[], &[]);
|
||||
assert!(!report.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_with_data() {
|
||||
let hand = DailyReportHand::new();
|
||||
let ctx = HandContext {
|
||||
agent_id: AgentId::new(),
|
||||
working_dir: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
timeout_secs: 30,
|
||||
callback_url: None,
|
||||
};
|
||||
let input = serde_json::json!({
|
||||
"agent_id": "test-agent",
|
||||
"industry": "education",
|
||||
"trajectory_summary": "讨论了课程安排",
|
||||
"pain_points": ["学生成绩下降"],
|
||||
"recent_experiences": ["掌握了成绩分析方法"],
|
||||
});
|
||||
|
||||
let result = hand.execute(&ctx, input).await.unwrap();
|
||||
assert!(result.success);
|
||||
let output = result.output;
|
||||
assert_eq!(output["agent_id"], "test-agent");
|
||||
assert!(output["report"].as_str().unwrap().contains("教育培训"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_minimal() {
|
||||
let hand = DailyReportHand::new();
|
||||
let ctx = HandContext {
|
||||
agent_id: AgentId::new(),
|
||||
working_dir: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
timeout_secs: 30,
|
||||
callback_url: None,
|
||||
};
|
||||
let result = hand.execute(&ctx, serde_json::json!({})).await.unwrap();
|
||||
assert!(result.success);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ mod collector;
|
||||
mod clip;
|
||||
mod twitter;
|
||||
pub mod reminder;
|
||||
pub mod daily_report;
|
||||
|
||||
pub use quiz::*;
|
||||
pub use browser::*;
|
||||
@@ -23,3 +24,4 @@ pub use collector::*;
|
||||
pub use clip::*;
|
||||
pub use twitter::*;
|
||||
pub use reminder::*;
|
||||
pub use daily_report::*;
|
||||
|
||||
Reference in New Issue
Block a user