优化: UI/UX遗留批(审计/AI命令层/前端组件,会话前基线收尾)

- AuditLog +298(筛选/详情/i18n)+ audit 后端 record/mod

- AI 命令层:generate_image +81 / fetch_url / fetch_search / skills / tool_registry / tools/file / provider / conversation

- 前端组件:AiChat/TopBar/ConversationSidebar/GitChanges/ApprovalPopup/Dashboard/ProjectDetail 等 30+ + composables + i18n

- 诊断文档: aichat历史会话实证诊断-2026-08-04 + project_soft_delete 测试
This commit is contained in:
lxy
2026-08-05 22:15:34 +08:00
parent 28de5d6143
commit e68d283651
49 changed files with 1257 additions and 138 deletions
+89 -22
View File
@@ -266,7 +266,6 @@ pub async fn evaluate_idea(
state: State<'_, AppState>,
id: String,
) -> Result<IdeaRecord, String> {
// 取出灵感
let record = state
.ideas
.get_by_id(&id)
@@ -274,13 +273,7 @@ pub async fn evaluate_idea(
.map_err(err_str)?
.ok_or_else(|| format!("灵感不存在: {id}"))?;
let idea = record_to_idea(&record);
// 多维评分(0-10IPC 层 *10 缩放为 0-100
let scores = df_ideas::scoring::ScoringEngine::compute_default(&idea);
// 对抗式评估(构造注入:从 DB 读默认 provider 装配 LLM,无 provider/构造失败 → 启发式兜底)
// F-01 阶段5: 透传 model_configs 池,evaluate_with_llm 经路由选模型(池空兜底 default_model)。
// 构造 engine(单次评估)
let provider = build_default_provider(&state).await;
let engine = match provider {
Some((p, pool)) => {
@@ -288,6 +281,92 @@ pub async fn evaluate_idea(
}
None => df_ideas::adversarial::AdversarialEngine::heuristic(),
};
evaluate_one(&state, record, &engine).await
}
/// 批量评估灵感:复用同一 AdversarialEngine 实例(同 provider/model_pool),逐条评估并持久化。
///
/// - 成功的灵感返回更新后的记录,失败的灵感记录错误信息,不中断后续评估。
/// - provider 构造仅一次(批量场景减少重复初始化开销)。
/// - 返回 BatchEvalResult { success, errors },前端据此展示部分成功/失败。
#[tauri::command]
pub async fn evaluate_ideas_batch(
state: State<'_, AppState>,
ids: Vec<String>,
) -> Result<BatchEvalResult, String> {
let provider = build_default_provider(&state).await;
let engine = match provider {
Some((p, pool)) => {
df_ideas::adversarial::AdversarialEngine::with_pool(Arc::from(p), pool)
}
None => df_ideas::adversarial::AdversarialEngine::heuristic(),
};
let mut success = Vec::new();
let mut errors = Vec::new();
for id in ids {
let record = match state.ideas.get_by_id(&id).await {
Ok(Some(r)) => r,
Ok(None) => {
errors.push(BatchEvalError {
id: id.clone(),
error: format!("灵感不存在: {id}"),
});
continue;
}
Err(e) => {
errors.push(BatchEvalError {
id: id.clone(),
error: e.to_string(),
});
continue;
}
};
match evaluate_one(&state, record, &engine).await {
Ok(updated) => success.push(updated),
Err(e) => errors.push(BatchEvalError { id, error: e }),
}
}
Ok(BatchEvalResult { success, errors })
}
/// 批量评估结果
#[derive(Debug, serde::Serialize)]
pub struct BatchEvalResult {
/// 成功评估的灵感记录
pub success: Vec<IdeaRecord>,
/// 失败的灵感 ID + 错误信息
pub errors: Vec<BatchEvalError>,
}
/// 批量评估单项错误
#[derive(Debug, serde::Serialize)]
pub struct BatchEvalError {
pub id: String,
pub error: String,
}
/// 单条灵感评估内部函数(evaluate_idea / evaluate_ideas_batch 共用)。
///
/// 接收已构造的 AdversarialEngine(批量场景复用同一实例),完成:
/// 1. 多维评分 + 对抗评估
/// 2. 组装 ai_analysis / scores JSON
/// 3. 原子写回主表(update_full
/// 4. 追加评估历史快照(idea_evaluations,含 version 唯一约束重试)
async fn evaluate_one(
state: &State<'_, AppState>,
record: IdeaRecord,
engine: &df_ideas::adversarial::AdversarialEngine,
) -> Result<IdeaRecord, String> {
let id = record.id.clone();
let idea = record_to_idea(&record);
// 多维评分(0-10IPC 层 *10 缩放为 0-100
let scores = df_ideas::scoring::ScoringEngine::compute_default(&idea);
let eval = engine.evaluate(&idea).await.map_err(err_str)?;
// 组装前端扁平结构(与 Ideas.vue 的 AdversarialEval interface 对齐)
@@ -334,7 +413,6 @@ pub async fn evaluate_idea(
let score_value = (scores.overall * 10.0).round() as i64;
// 构造完整记录后单次原子写回(update_full 保留 id 与 created_at)。
// ai_analysis/scores_json 按值 move 进主表记录后,下方历史快照仍需复用 → 此处 clone 保留绑定。
let updated = IdeaRecord {
scores: Some(scores_json.clone()),
ai_analysis: Some(ai_analysis.clone()),
@@ -349,15 +427,8 @@ pub async fn evaluate_idea(
.await
.map_err(err_str)?;
// 追加一条评估历史快照(idea_evaluations 审计表,version 单调递增)。
// 主表 update_full 成功后再追加,保证主表先落;历史表为额外冗余列(evaluated_by
// 独立冗余,ai_analysis JSON 内的 evaluated_by 字段保留不删)。
//
// version 并发重复兜底(V25 唯一约束 + 重试):version 此前由
// `list_by_idea().first().version + 1` 算出,读-改-写非原子,并发评估同一灵感
// 可能写出相同 version。V25 在 idea_evaluations(idea_id, version) 上加了唯一索引,
// 此处捕获唯一约束冲突 → 重新查最新 version 重算并重试(上限 3 次防死循环)。
// 单用户桌面应用并发概率极低,但唯一约束 + 重试是数据完整性兜底,值得做。
// 追加评估历史快照(idea_evaluations 审计表,version 单调递增)。
// version 并发重复兜底(V25 唯一约束 + 重试)。
let mut attempt = 0;
let max_attempts = 3;
loop {
@@ -383,10 +454,6 @@ pub async fn evaluate_idea(
match state.idea_evaluations.insert(eval_record).await {
Ok(_) => break,
Err(e) => {
// 唯一约束冲突(SQLite extended code 2067 / SQLITE_CONSTRAINT_UNIQUE)
// → version 并发重复,命中且未达上限则重试(重新查 version);否则向上抛错。
// 检测逻辑收口到 df_storage::crud::is_unique_constraint_err,集中维护、
// 大小写不敏感,不再散落脆弱的英文文案 contains。
if is_unique_constraint_err(&e) && attempt < max_attempts {
tracing::warn!(
"灵感 {id} 评估历史 version 唯一约束冲突,重试 {}/{}",