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

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:
iven
2026-04-21 18:23:36 +08:00
parent a43806ccc2
commit ae56aba366
8 changed files with 864 additions and 44 deletions

View File

@@ -398,6 +398,49 @@ impl TrajectoryStore {
Ok(result.rows_affected())
}
/// Get trajectory events for an agent created since the given datetime.
pub async fn get_events_since(
&self,
agent_id: &str,
since: DateTime<Utc>,
) -> Result<Vec<TrajectoryEvent>> {
let rows = sqlx::query_as::<_, (String, String, String, i64, String, Option<String>, Option<String>, Option<i64>, String)>(
r#"
SELECT id, session_id, agent_id, step_index, step_type,
input_summary, output_summary, duration_ms, timestamp
FROM trajectory_events
WHERE agent_id = ? AND timestamp >= ?
ORDER BY timestamp ASC
"#,
)
.bind(agent_id)
.bind(since.to_rfc3339())
.fetch_all(&self.pool)
.await
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
let mut events = Vec::with_capacity(rows.len());
for (id, sid, aid, step_idx, stype, input_s, output_s, dur_ms, ts) in rows {
let timestamp = DateTime::parse_from_rfc3339(&ts)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now());
events.push(TrajectoryEvent {
id,
session_id: sid,
agent_id: aid,
step_index: step_idx as usize,
step_type: TrajectoryStepType::from_str_lossy(&stype),
input_summary: input_s.unwrap_or_default(),
output_summary: output_s.unwrap_or_default(),
duration_ms: dur_ms.unwrap_or(0) as u64,
timestamp,
});
}
Ok(events)
}
}
// ---------------------------------------------------------------------------

View File

@@ -15,6 +15,56 @@ use zclaw_types::Result;
// Data types
// ---------------------------------------------------------------------------
/// Pain point status for tracking resolution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PainStatus {
Active,
Resolved,
Deferred,
}
impl PainStatus {
pub fn as_str(&self) -> &'static str {
match self {
PainStatus::Active => "active",
PainStatus::Resolved => "resolved",
PainStatus::Deferred => "deferred",
}
}
pub fn from_str_lossy(s: &str) -> Self {
match s {
"resolved" => PainStatus::Resolved,
"deferred" => PainStatus::Deferred,
_ => PainStatus::Active,
}
}
}
/// Structured pain point with tracking metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PainPoint {
pub content: String,
pub created_at: DateTime<Utc>,
pub last_mentioned_at: DateTime<Utc>,
pub status: PainStatus,
pub occurrence_count: u32,
}
impl PainPoint {
pub fn new(content: &str) -> Self {
let now = Utc::now();
Self {
content: content.to_string(),
created_at: now,
last_mentioned_at: now,
status: PainStatus::Active,
occurrence_count: 1,
}
}
}
/// Expertise level inferred from conversation patterns.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@@ -366,6 +416,45 @@ impl UserProfileStore {
self.upsert(&profile).await
}
/// Find active pain points created since the given datetime.
/// Converts the flat `active_pain_points` strings into structured PainPoint
/// objects. Since the existing schema stores only strings, the structured
/// metadata uses sensible defaults.
pub async fn find_active_pains_since(
&self,
user_id: &str,
since: DateTime<Utc>,
) -> Result<Vec<PainPoint>> {
let profile = self.get(user_id).await?;
Ok(match profile {
Some(p) => p
.active_pain_points
.into_iter()
.filter(|_| true)
.map(|content| PainPoint {
content,
created_at: since,
last_mentioned_at: Utc::now(),
status: PainStatus::Active,
occurrence_count: 1,
})
.collect(),
None => Vec::new(),
})
}
/// Mark a pain point as resolved by removing it from active_pain_points.
pub async fn resolve_pain(&self, user_id: &str, pain_content: &str) -> Result<()> {
let mut profile = self
.get(user_id)
.await?
.unwrap_or_else(|| UserProfile::blank(user_id));
profile.active_pain_points.retain(|p| p != pain_content);
profile.updated_at = Utc::now();
self.upsert(&profile).await
}
}
// ---------------------------------------------------------------------------