fix(growth,kernel,runtime,desktop): 50 轮功能链路审计 7 项断链修复
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 修复:
- B-MEM-2: 跨会话记忆丢失 — 添加 IdentityRecall 查询意图检测,
  身份类查询绕过 FTS5/LIKE 文本搜索,直接按 scope 检索全部偏好+知识记忆;
  缓存 GrowthIntegration 到 Kernel 避免每次请求重建空 scorer
- B-HAND-1: Hands 未触发 — 创建 HandTool wrapper 实现 Tool trait,
  在 create_tool_registry() 中注册所有已启用 Hands 为 LLM 可调用工具

P1 修复:
- B-SCHED-4: 一次性定时未拦截 — 添加 RE_ONE_SHOT_TODAY 正则匹配
  "下午3点半提醒我..."类无日期前缀的同日触发模式
- B-CHAT-2: 工具调用循环 — ToolErrorMiddleware 添加连续失败计数器,
  3 次连续失败后自动 AbortLoop 防止无限重试
- B-CHAT-5: Stream 竞态 — cancelStream 后添加 500ms cancelCooldown,
  防止后端 active-stream 检查竞态
This commit is contained in:
iven
2026-04-20 09:43:38 +08:00
parent 24b866fc28
commit f2917366a8
10 changed files with 746 additions and 49 deletions

View File

@@ -129,6 +129,15 @@ static RE_ONE_SHOT: LazyLock<Regex> = LazyLock::new(|| {
)).expect("static regex pattern is valid")
});
/// Matches same-day one-shot triggers: "下午3点半提醒我..." or "上午10点提醒我..."
/// Pattern: period + time + "提醒我" (no date prefix — implied today)
static RE_ONE_SHOT_TODAY: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!(
r"^{}(\d{{1,2}})[点时:](?:(\d{{1,2}})|(半))?.*提醒我",
PERIOD
)).expect("static regex pattern is valid")
});
// ---------------------------------------------------------------------------
// Helper lookups (pure functions, no allocation)
// ---------------------------------------------------------------------------
@@ -395,38 +404,70 @@ fn try_monthly(input: &str, task_desc: &str, agent_id: &AgentId) -> Option<Sched
}
fn try_one_shot(input: &str, task_desc: &str, agent_id: &AgentId) -> Option<ScheduleParseResult> {
let caps = RE_ONE_SHOT.captures(input)?;
let day_offset = match caps.get(1)?.as_str() {
"明天" => 1,
"" => 2,
"后天" => 3,
_ => return None,
};
let period = caps.get(2).map(|m| m.as_str());
let raw_hour: u32 = caps.get(3)?.as_str().parse().ok()?;
let minute: u32 = extract_minute(&caps, 4, 5);
let hour = adjust_hour_for_period(raw_hour, period);
if hour > 23 || minute > 59 {
return None;
// First try explicit date prefix: 明天/后天/大后天 + time
if let Some(caps) = RE_ONE_SHOT.captures(input) {
let day_offset = match caps.get(1)?.as_str() {
"" => 1,
"后天" => 2,
"大后天" => 3,
_ => return None,
};
let period = caps.get(2).map(|m| m.as_str());
let raw_hour: u32 = caps.get(3)?.as_str().parse().ok()?;
let minute: u32 = extract_minute(&caps, 4, 5);
let hour = adjust_hour_for_period(raw_hour, period);
if hour > 23 || minute > 59 {
return None;
}
let target = chrono::Utc::now()
.checked_add_signed(chrono::Duration::days(day_offset))
.unwrap_or_else(chrono::Utc::now)
.with_hour(hour)
.unwrap_or_else(|| chrono::Utc::now())
.with_minute(minute)
.unwrap_or_else(|| chrono::Utc::now())
.with_second(0)
.unwrap_or_else(|| chrono::Utc::now());
return Some(ScheduleParseResult::Exact(ParsedSchedule {
cron_expression: target.to_rfc3339(),
natural_description: format!("{} {:02}:{:02}", caps.get(1)?.as_str(), hour, minute),
confidence: 0.88,
task_description: task_desc.to_string(),
task_target: TaskTarget::Agent(agent_id.to_string()),
}));
}
let target = chrono::Utc::now()
.checked_add_signed(chrono::Duration::days(day_offset))
.unwrap_or_else(chrono::Utc::now)
.with_hour(hour)
.unwrap_or_else(|| chrono::Utc::now())
.with_minute(minute)
.unwrap_or_else(|| chrono::Utc::now())
.with_second(0)
.unwrap_or_else(|| chrono::Utc::now());
// Then try same-day implicit: "下午3点半提醒我..." (no date prefix)
if let Some(caps) = RE_ONE_SHOT_TODAY.captures(input) {
let period = caps.get(1).map(|m| m.as_str());
let raw_hour: u32 = caps.get(2)?.as_str().parse().ok()?;
let minute: u32 = extract_minute(&caps, 3, 4);
let hour = adjust_hour_for_period(raw_hour, period);
if hour > 23 || minute > 59 {
return None;
}
Some(ScheduleParseResult::Exact(ParsedSchedule {
cron_expression: target.to_rfc3339(),
natural_description: format!("{} {:02}:{:02}", caps.get(1)?.as_str(), hour, minute),
confidence: 0.88,
task_description: task_desc.to_string(),
task_target: TaskTarget::Agent(agent_id.to_string()),
}))
let target = chrono::Utc::now()
.with_hour(hour)
.unwrap_or_else(|| chrono::Utc::now())
.with_minute(minute)
.unwrap_or_else(|| chrono::Utc::now())
.with_second(0)
.unwrap_or_else(|| chrono::Utc::now());
let period_desc = period.unwrap_or("");
return Some(ScheduleParseResult::Exact(ParsedSchedule {
cron_expression: target.to_rfc3339(),
natural_description: format!("今天{} {:02}:{:02}", period_desc, hour, minute),
confidence: 0.82,
task_description: task_desc.to_string(),
task_target: TaskTarget::Agent(agent_id.to_string()),
}));
}
None
}
// ---------------------------------------------------------------------------