优化: AI模块(provider重试兼容+aichat交互+工具卡片可读化+diff高亮)
This commit is contained in:
@@ -69,59 +69,84 @@ async fn build_approval_reason(
|
||||
db: &Arc<Database>,
|
||||
) -> String {
|
||||
let s = |key: &str| args.get(key).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let detail = match tool_name {
|
||||
"delete_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("删除项目{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
// 优先级:tool_display_hint(轻量标签) → display_hint_for_tool(模板填充) → 硬编码 fallback
|
||||
let detail = if let Some(hint) = super::tool_registry::tool_display_hint(tool_name) {
|
||||
// 轻量命中:直接用中文动作前缀 + 风险后缀(不含参数细节)
|
||||
hint.to_string()
|
||||
} else if let Some((template, keys)) = super::tool_registry::display_hint_for_tool(tool_name) {
|
||||
// 按 keys 列表取参数值;含 "id"/"project_id" 的值走 resolve_project_label 解析
|
||||
let mut values = Vec::with_capacity(keys.len());
|
||||
for &key in keys {
|
||||
let val = s(key);
|
||||
if val.is_empty() { values.clear(); break; }
|
||||
match key {
|
||||
"id" | "project_id" => values.push(resolve_project_label(db, &val).await),
|
||||
_ => values.push(val.to_string()),
|
||||
}
|
||||
}
|
||||
"restore_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("从回收站恢复项目{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
// 任一关键参数为空则整条 fallback 为空串(与原行为一致)
|
||||
if values.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
// 模板 {} 占位符按位置填充
|
||||
let mut result = template.to_string();
|
||||
for v in &values {
|
||||
result = result.replacen("{}", v, 1);
|
||||
}
|
||||
result
|
||||
}
|
||||
"purge_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("永久删除项目及关联数据,不可恢复{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
} else {
|
||||
// fallback:原有完整硬编码逻辑(保持行为不变)
|
||||
match tool_name {
|
||||
"delete_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("删除项目{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
}
|
||||
"restore_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("从回收站恢复项目{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
}
|
||||
"purge_project" => {
|
||||
let id = s("id");
|
||||
if !id.is_empty() { format!("永久删除项目及关联数据,不可恢复{}", resolve_project_label(db, id).await) } else { String::new() }
|
||||
}
|
||||
"update_project" => {
|
||||
let id = s("id");
|
||||
let field = s("field");
|
||||
if !id.is_empty() {
|
||||
format!("修改项目{}字段「{}」", resolve_project_label(db, id).await, field)
|
||||
} else if !field.is_empty() {
|
||||
format!("修改项目字段「{}」", field)
|
||||
} else { String::new() }
|
||||
}
|
||||
"bind_directory" => {
|
||||
let id = s("id");
|
||||
let path = s("path");
|
||||
if !path.is_empty() && !id.is_empty() {
|
||||
format!("绑定目录:{}(项目{})", path, resolve_project_label(db, id).await)
|
||||
} else if !path.is_empty() {
|
||||
format!("绑定目录:{}", path)
|
||||
} else { String::new() }
|
||||
}
|
||||
"create_task" => {
|
||||
let title = s("title");
|
||||
let pid = s("project_id");
|
||||
if !title.is_empty() && !pid.is_empty() {
|
||||
format!("创建任务:{}(项目{})", title, resolve_project_label(db, pid).await)
|
||||
} else if !title.is_empty() {
|
||||
format!("创建任务:{}", title)
|
||||
} else { String::new() }
|
||||
}
|
||||
"create_project" => {
|
||||
let name = s("name");
|
||||
if !name.is_empty() { format!("创建项目:「{}」", name) } else { String::new() }
|
||||
}
|
||||
"create_idea" => {
|
||||
let title = s("title");
|
||||
if !title.is_empty() { format!("捕获灵感:{}", title) } else { String::new() }
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
"update_project" => {
|
||||
let id = s("id");
|
||||
let field = s("field");
|
||||
if !id.is_empty() {
|
||||
format!("修改项目{}字段「{}」", resolve_project_label(db, id).await, field)
|
||||
} else if !field.is_empty() {
|
||||
format!("修改项目字段「{}」", field)
|
||||
} else { String::new() }
|
||||
}
|
||||
"bind_directory" => {
|
||||
let id = s("id");
|
||||
let path = s("path");
|
||||
if !path.is_empty() && !id.is_empty() {
|
||||
format!("绑定目录:{}(项目{})", path, resolve_project_label(db, id).await)
|
||||
} else if !path.is_empty() {
|
||||
format!("绑定目录:{}", path)
|
||||
} else { String::new() }
|
||||
}
|
||||
"create_task" => {
|
||||
let title = s("title");
|
||||
let pid = s("project_id");
|
||||
if !title.is_empty() && !pid.is_empty() {
|
||||
format!("创建任务:{}(项目{})", title, resolve_project_label(db, pid).await)
|
||||
} else if !title.is_empty() {
|
||||
format!("创建任务:{}", title)
|
||||
} else { String::new() }
|
||||
}
|
||||
"create_project" => {
|
||||
let name = s("name");
|
||||
if !name.is_empty() { format!("创建项目:「{}」", name) } else { String::new() }
|
||||
}
|
||||
"create_idea" => {
|
||||
let title = s("title");
|
||||
if !title.is_empty() { format!("捕获灵感:{}", title) } else { String::new() }
|
||||
}
|
||||
"run_workflow" => {
|
||||
let name = s("name");
|
||||
if !name.is_empty() { format!("运行工作流:{}", name) } else { String::new() }
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
if detail.is_empty() {
|
||||
// fallback:无可读字段,保留风险等级提示模板
|
||||
|
||||
@@ -253,6 +253,42 @@ pub async fn ai_chat_clear(state: State<'_, AppState>) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 强制发送消息(B-260616-02: L2 发送韧性)
|
||||
///
|
||||
/// 当后端 generating=true 残留(HMR/异常退出等)导致 sendMessage 被拦截时,
|
||||
/// 前端可调此命令强制复位 generating + 清审批,然后走 ai_chat_send 同款流程发消息。
|
||||
/// 等价于"先软停止 → 再发送"的原子操作,避免竞态窗口。
|
||||
#[tauri::command]
|
||||
pub async fn ai_chat_force_send(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
message: String,
|
||||
language: Option<String>,
|
||||
skill: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
// 原子复位:清 generating + 清积压审批 + 置 stop_flag,与 ai_chat_stop 审批分支一致
|
||||
let old_conv_id = {
|
||||
let mut session = state.ai_session.lock().await;
|
||||
let old = session.active_conversation_id.clone();
|
||||
session.generating = false;
|
||||
session.pending_approvals.clear();
|
||||
session.stop_flag.store(true, Ordering::SeqCst);
|
||||
old
|
||||
};
|
||||
// 通知前端旧生成已结束(若有残留 conv)
|
||||
if let Some(ref cid) = old_conv_id {
|
||||
let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
conversation_id: Some(cid.clone()),
|
||||
});
|
||||
}
|
||||
// 复位完成后走 ai_chat_send 同款流程(内部会重新设 generating=true 并 spawn loop)
|
||||
// 直接内联而非递归调 ai_chat_send,避免 IPC 嵌套
|
||||
ai_chat_send(app, state, message, language, skill).await
|
||||
}
|
||||
|
||||
/// 停止当前 AI 生成
|
||||
///
|
||||
/// 两种场景:
|
||||
|
||||
@@ -61,7 +61,9 @@ fn system_prompt_parts(lang: &str) -> (&'static str, &'static str) {
|
||||
You can perform the following actions via tool calls:\n\
|
||||
- Create/query projects, tasks, and ideas\n\
|
||||
- Run workflows\n\
|
||||
- Read file contents, list directories, create/write files\n\n\
|
||||
- Read file contents, list directories, create/write files\n\
|
||||
- Get file metadata (size, line count, binary detection)\n\
|
||||
- Append content to files, search files by name pattern\n\n\
|
||||
## Guidelines\n\
|
||||
- Briefly explain your intent before executing actions\n\
|
||||
- Ask for clarification if the user's intent is unclear\n\
|
||||
@@ -76,7 +78,9 @@ fn system_prompt_parts(lang: &str) -> (&'static str, &'static str) {
|
||||
你可以通过工具调用执行以下操作:\n\
|
||||
- 创建/查询项目、任务、灵感\n\
|
||||
- 运行工作流\n\
|
||||
- 读取文件内容、列出目录、创建/写入文件\n\n\
|
||||
- 读取文件内容、列出目录、创建/写入文件\n\
|
||||
- 获取文件元信息(大小、行数、二进制检测)\n\
|
||||
- 追加写入文件、按名称模式搜索文件\n\n\
|
||||
## 行为准则\n\
|
||||
- 执行操作前简要说明你的意图\n\
|
||||
- 如果不确定用户意图,先提问\n\
|
||||
|
||||
Reference in New Issue
Block a user