优化: 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:
@@ -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-10,IPC 层 *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-10,IPC 层 *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 唯一约束冲突,重试 {}/{}",
|
||||
|
||||
Reference in New Issue
Block a user