优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)
AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)
agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留
聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡
循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测
错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测
数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)
git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})
安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)
幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)
附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
@@ -62,6 +62,8 @@ pub async fn try_continue_agent_loop(
|
||||
let ev = AiChatEvent::AiCompleted {
|
||||
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
|
||||
prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 0, reasoning_tokens: 0,
|
||||
// 零 token 收敛信号,非估算(无真实 LLM 调用)。
|
||||
is_estimated: false,
|
||||
incomplete: None,
|
||||
conversation_id: Some(emit_conv_id),
|
||||
pinned_goals: snap.pinned_goals_snapshot.clone(),
|
||||
@@ -113,6 +115,8 @@ pub async fn try_continue_agent_loop(
|
||||
let ev = AiChatEvent::AiCompleted {
|
||||
total_tokens: 0, prompt_tokens: 0, completion_tokens: 0,
|
||||
prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 0, reasoning_tokens: 0,
|
||||
// 零 token 收敛信号,非估算(无真实 LLM 调用)。
|
||||
is_estimated: false,
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id_owned.clone()),
|
||||
pinned_goals: snap.pinned_goals_snapshot.clone(),
|
||||
@@ -129,8 +133,15 @@ pub async fn try_continue_agent_loop(
|
||||
round: 0, conversation_id: Some(conv_id_owned.clone()),
|
||||
});
|
||||
|
||||
// F1 并发 epoch:续跑也是新 loop 生命周期,递增 owner token(旧 loop 已 return/disarm)。
|
||||
// conv_id_owned 是 spawn 前 clone,此处仍可在 scope 内借用 state 锁。
|
||||
let loop_epoch = {
|
||||
let mut session = state.ai_session.lock().await;
|
||||
session.conv(&conv_id_owned).loop_epoch.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
super::run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id_owned, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override).await;
|
||||
super::run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id_owned, knowledge_config, llm_concurrency, max_iterations, max_retries, start_iteration, model_override, loop_epoch).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,6 +331,30 @@ pub(crate) fn is_repetitive_exploration(signatures: &[String]) -> bool {
|
||||
counts.values().any(|&c| c >= REPETITION_SINGLE_MAX)
|
||||
}
|
||||
|
||||
/// 纯问候判定:纯社交短文本(你好/谢谢/在吗等)→ true。机制层治弱模型把问候当指令
|
||||
/// 擅自调工具(会话 b4d6b4e0:用户"你好"→ list_project_modules + 探索源码)。
|
||||
/// 规则:去空白后 ≤8 字,且不含动作词/实体引用(@[)。"你好,看看 moyu 项目"含动作词+超长 → false。
|
||||
pub(crate) fn is_pure_greeting(msg: &str) -> bool {
|
||||
// 问候/社交短文本词表(命中任一即可;长度约束兜底)
|
||||
const GREETINGS: &[&str] = &[
|
||||
"你好", "hello", "hi", "哈喽", "嗨", "在吗", "谢谢", "感谢", "嗯",
|
||||
"好的", "ok", "没问题", "辛苦", "拜拜", "再见", "👋", "你好呀",
|
||||
];
|
||||
// 动作/请求意图词:出现任一即非纯问候(放行工具)
|
||||
const ACTION_WORDS: &[&str] = &[
|
||||
"看", "查", "帮", "创建", "新增", "修改", "更新", "删除", "分析",
|
||||
"读取", "写", "执行", "运行", "测试", "构建", "推进", "检查", "搜索",
|
||||
"列出", "绑定", "如何", "怎么", "为什么", "什么是", "有哪些", "怎么办",
|
||||
];
|
||||
let t = msg.trim();
|
||||
if t.is_empty() { return true; }
|
||||
if t.chars().count() > 8 { return false; }
|
||||
if t.contains("@[") { return false; }
|
||||
let lower = t.to_lowercase();
|
||||
if !GREETINGS.iter().any(|g| lower.contains(g)) { return false; }
|
||||
!ACTION_WORDS.iter().any(|a| lower.contains(a))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -398,15 +433,15 @@ mod tests {
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "MAX", "path": "src"})),
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "truncate", "path": "src"})),
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "fetch_url", "path": "src"})),
|
||||
tool_call_signature("grep", &serde_json!({"pattern": "MessageRole", "path": "crates"})),
|
||||
tool_call_signature("grep", &serde_json!({"pattern": "pub enum", "path": "src"})),
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "MessageRole", "path": "crates"})),
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "pub enum", "path": "src"})),
|
||||
tool_call_signature("read_file", &serde_json::json!({"path": "a.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json!({"path": "b.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json!({"path": "c.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json!({"path": "d.rs", "offset": 100, "limit": 50})),
|
||||
tool_call_signature("list_dir", &serde_json!({"path": "src-tauri"})),
|
||||
tool_call_signature("grep", &serde_json!({"pattern": "STALL", "path": "src"})),
|
||||
tool_call_signature("read_file", &serde_json!({"path": "e.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json::json!({"path": "b.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json::json!({"path": "c.rs", "offset": 0, "limit": 50})),
|
||||
tool_call_signature("read_file", &serde_json::json!({"path": "d.rs", "offset": 100, "limit": 50})),
|
||||
tool_call_signature("list_dir", &serde_json::json!({"path": "src-tauri"})),
|
||||
tool_call_signature("grep", &serde_json::json!({"pattern": "STALL", "path": "src"})),
|
||||
tool_call_signature("read_file", &serde_json::json!({"path": "e.rs", "offset": 0, "limit": 50})),
|
||||
];
|
||||
assert_eq!(sigs.len(), 12);
|
||||
assert!(!is_repetitive_exploration(&sigs), "正常代码审查不应判重复");
|
||||
@@ -461,7 +496,7 @@ mod tests {
|
||||
fn boundary_two_alternating_below_ratio() {
|
||||
let a = tool_call_signature("grep", &serde_json::json!({"pattern": "a", "path": "x"}));
|
||||
let b = tool_call_signature("grep", &serde_json::json!({"pattern": "b", "path": "x"}));
|
||||
let sigs = vec![a, b.clone(), a.clone(), b.clone(), a, b];
|
||||
let sigs = vec![a.clone(), b.clone(), a.clone(), b.clone(), a, b];
|
||||
assert!(is_repetitive_exploration(&sigs), "两签名交替唯一率低应判重复");
|
||||
}
|
||||
|
||||
@@ -474,4 +509,48 @@ mod tests {
|
||||
.collect();
|
||||
assert!(!is_repetitive_exploration(&sigs));
|
||||
}
|
||||
|
||||
// ── is_pure_greeting 纯问候判定测试 ──
|
||||
|
||||
#[test]
|
||||
fn greeting_pure_hello_is_greeting() {
|
||||
assert!(is_pure_greeting("你好"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_thanks_is_greeting() {
|
||||
assert!(is_pure_greeting("谢谢"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_with_action_is_not_greeting() {
|
||||
// 含动作词"看"+ 超 8 字 → 放行工具
|
||||
assert!(!is_pure_greeting("你好,看看 moyu 项目"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_view_tasks_is_not_greeting() {
|
||||
assert!(!is_pure_greeting("查看任务"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_entity_ref_is_not_greeting() {
|
||||
assert!(!is_pure_greeting("@[项目]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_empty_string_is_greeting() {
|
||||
assert!(is_pure_greeting(""));
|
||||
assert!(is_pure_greeting(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_help_query_is_not_greeting() {
|
||||
assert!(!is_pure_greeting("你好,帮我查询项目"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeting_ok_is_greeting() {
|
||||
assert!(is_pure_greeting("ok"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user