优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)
This commit is contained in:
@@ -13,11 +13,12 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
// 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。
|
||||
use df_ai::retry;
|
||||
// F-01 阶段5: 智能路由 helper + TaskRequirements + 维度枚举。
|
||||
// 调用点构造 TaskRequirements(主对话:needs_tool_use=true,min_intelligence=Standard,
|
||||
// 调用点构造 TaskRequirements(主对话:needs_tool_use=true,
|
||||
// 当前仅 Text 模态;后续多模态接入时检测消息内 Part/Image 追加 Vision),
|
||||
// 经 select_model_id 在 provider.model_configs 池中选最优;池空/无匹配兜底 default_model。
|
||||
// 注:路由已解耦(B-260618-03),cost_tier/intelligence 不再参与硬路由。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
select_model_id, Modality, TaskRequirements,
|
||||
};
|
||||
|
||||
use df_storage::db::Database;
|
||||
@@ -344,10 +345,20 @@ pub(crate) async fn run_agentic_loop(
|
||||
// AiProviderRepo::new 仅 clone Arc<Database>(廉价),不复用 AppState.ai_providers
|
||||
// (run_agentic_loop 签名只传 Arc<Database>,改签名会牵动 3 调用点 + try_continue)。
|
||||
let provider_repo = df_storage::crud::AiProviderRepo::new(&db);
|
||||
let pool_providers: Vec<AiProviderRecord> = provider_repo.list_all().await.unwrap_or_default();
|
||||
let pool_providers: Vec<AiProviderRecord> = match provider_repo.list_all().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] list_all providers 失败,负载均衡池退化为入参默认 provider(空池兜底)");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let ranked_candidates: Vec<AiProviderRecord> = super::provider_pool::ProviderPool::select(
|
||||
&pool_providers,
|
||||
None, // 模型亲和此时尚未确定(router 选模型需 provider_config,见下方)
|
||||
// specify 模式(用户指定 model):传 override 作亲和键,ProviderPool 优先选
|
||||
// 「池中含该 model 的 provider」作 primary,打破下方「router 选模型需 provider_config」
|
||||
// 的鸡生蛋——override 此时已知(入参 ← session.model_override),无须等 router。
|
||||
// auto 模式(override=None)→ 全亲和纯权重排序,行为不变(向后兼容)。
|
||||
model_override.as_deref(),
|
||||
);
|
||||
let (primary_provider, candidates): (AiProviderRecord, Vec<AiProviderRecord>) =
|
||||
match ranked_candidates.split_first() {
|
||||
@@ -401,14 +412,12 @@ pub(crate) async fn run_agentic_loop(
|
||||
"[ai] 发起 LLM 请求"
|
||||
);
|
||||
|
||||
// F-01 阶段5: 主对话路由 — TaskRequirements(needs_tool_use=true,min_intelligence=Standard)。
|
||||
// F-01 阶段5: 主对话路由 — TaskRequirements(needs_tool_use=true)。
|
||||
// 模态当前仅 Text(图像消息类型未实现,后续多模态接入时检测 Part/Image 追加 Vision)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model,行为与接入前一致。
|
||||
let agentic_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs)
|
||||
|
||||
@@ -21,6 +21,13 @@ use crate::commands::{err_str, now_millis};
|
||||
use super::{AiChatEvent, AiSession, PendingApproval, ToolCallDraft};
|
||||
use super::tool_registry::generate_diff;
|
||||
|
||||
/// Med/High 工具挂起审批时回填的占位 tool_result 内容。
|
||||
///
|
||||
/// 单一常量避免「写入处」(process_tool_calls) 与「去重排查处」
|
||||
/// (find_cached_high_risk_result) 各持一份字面量致耦合——若两者漂移,
|
||||
/// 去重会把 pending 占位误判为已落定结果命中缓存,污染 LLM 上下文。
|
||||
const PENDING_APPROVAL_PLACEHOLDER: &str = "需要用户审批,等待确认";
|
||||
|
||||
/// RiskLevel → 审计记录字符串(low/medium/high)
|
||||
pub(crate) fn risk_str(r: RiskLevel) -> &'static str {
|
||||
match r {
|
||||
@@ -265,14 +272,16 @@ pub async fn restore_pending_approvals(state: &AppState) {
|
||||
let mut session = state.ai_session.lock().await;
|
||||
for rec in pending {
|
||||
let args: serde_json::Value = serde_json::from_str(&rec.arguments).unwrap_or_default();
|
||||
let Some(risk) = risk_from_str(&rec.risk_level) else { continue };
|
||||
// 过滤 risk_level 解析失败的损坏记录(语义保留:数据异常不恢复)·不再绑定 risk(PendingApproval.risk_level 已删)
|
||||
if risk_from_str(&rec.risk_level).is_none() {
|
||||
continue;
|
||||
}
|
||||
session.pending_approvals.insert(
|
||||
rec.tool_call_id.clone(),
|
||||
PendingApproval {
|
||||
tool_call_id: rec.tool_call_id,
|
||||
tool_name: rec.tool_name,
|
||||
arguments: args,
|
||||
risk_level: risk,
|
||||
conversation_id: rec.conversation_id,
|
||||
recovered: true,
|
||||
// AE-2025-03: 重启恢复的审批不重读旧文件——审批可能跨重启,
|
||||
@@ -325,14 +334,18 @@ pub(crate) async fn audit_tool_call(
|
||||
/// 而 ai_tool_executions 无该列,调用会报 "no such column: created_at" 被 unwrap_or_default 吞掉,
|
||||
/// 导致审批后审计记录永久卡 pending。
|
||||
pub(crate) async fn audit_finalize(state: &AppState, tool_call_id: &str, status: &str, result: Option<String>) {
|
||||
let Some(mut rec) = state
|
||||
.ai_tool_executions
|
||||
.find_by_tool_call_id(tool_call_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
else {
|
||||
tracing::warn!("audit_finalize: 未找到 tool_call_id={} 的审计记录", tool_call_id);
|
||||
return;
|
||||
// 区分 Err(DB 故障)与 Ok(None)(真无记录):原 unwrap_or_default 把 Err 压成 None,
|
||||
// DB 故障被「未找到」日志掩盖,审批后审计记录卡 pending 无确诊线索(B-260617-17 同款吞错)。
|
||||
let mut rec = match state.ai_tool_executions.find_by_tool_call_id(tool_call_id).await {
|
||||
Ok(Some(rec)) => rec,
|
||||
Ok(None) => {
|
||||
tracing::warn!("audit_finalize: 未找到 tool_call_id={} 的审计记录", tool_call_id);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("audit_finalize: 查询 tool_call_id={} 审计记录失败(DB 故障,状态未回填): {}", tool_call_id, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
rec.status = status.to_string();
|
||||
rec.decided_by = Some("human".to_string());
|
||||
@@ -340,7 +353,9 @@ pub(crate) async fn audit_finalize(state: &AppState, tool_call_id: &str, status:
|
||||
if let Some(r) = result {
|
||||
rec.result = Some(r);
|
||||
}
|
||||
let _ = state.ai_tool_executions.update_full(&rec).await;
|
||||
if let Err(e) = state.ai_tool_executions.update_full(&rec).await {
|
||||
tracing::error!("audit_finalize: 回填 tool_call_id={} 审计状态失败: {}", tool_call_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// AR-11(方案A):工具名 → (entity, action) 映射,用于数据变更联动刷新。
|
||||
@@ -386,6 +401,10 @@ pub(crate) fn emit_data_changed(app_handle: &AppHandle, tool_name: &str) {
|
||||
|
||||
/// F-260616-05:高危工具去重(根治 run_command 超时→重试→重新审批循环)。
|
||||
///
|
||||
/// ⚠ 性能注记(CR-260618-11#5):本函数在 session lock 持有期间对每个 high risk 工具
|
||||
/// 串行查 AiToolExecutionRepo::find_by_tool_call_id,工具数多时锁持有线性增长。
|
||||
/// 批量预取(改签名传 tool_call_ids 批量查)属架构改暂未做,后续 High risk 工具增多时优先处理。
|
||||
///
|
||||
/// **根因链**(F-04 batch42 已做超时标注):LLM 调 run_command 超时 → F-04 把超时标注成
|
||||
/// tool_result 回传 LLM → LLM(不可靠)仍重试同命令 → 新 tool_call_id(provider 每轮新 id)
|
||||
/// → `process_tool_calls` 重新 insert pending(audit.rs:418) → 用户被迫重新审批,循环。
|
||||
@@ -403,11 +422,12 @@ pub(crate) fn emit_data_changed(app_handle: &AppHandle, tool_name: &str) {
|
||||
/// - 返回 None 表示无缓存命中(走原审批流程)。
|
||||
///
|
||||
/// `session` 只读扫描 messages(不写),调用方据返回值决定是否跳过 insert pending。
|
||||
fn find_cached_high_risk_result(
|
||||
async fn find_cached_high_risk_result(
|
||||
session: &AiSession,
|
||||
audit_repo: &AiToolExecutionRepo,
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Option<String> {
|
||||
) -> Option<(String, String)> {
|
||||
use df_ai::provider::MessageRole;
|
||||
|
||||
// 规范化新调用的 args 为可比字符串(排序键,键序无关)
|
||||
@@ -454,11 +474,20 @@ fn find_cached_high_risk_result(
|
||||
if msg.tool_call_id.as_deref() != Some(old_id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// 命中旧 tool_result:排除 pending 占位(内容固定为「需要用户审批,等待确认」)
|
||||
if msg.content == "需要用户审批,等待确认" {
|
||||
// 命中旧 tool_result:排除 pending 占位(内容固定为 PENDING_APPROVAL_PLACEHOLDER)
|
||||
if msg.content == PENDING_APPROVAL_PLACEHOLDER {
|
||||
return None;
|
||||
}
|
||||
return Some(msg.content.clone());
|
||||
// SW-260618-16: 查审计表拿缓存来源真实 status(completed/rejected/failed),透传给
|
||||
// audit_tool_call 而非固定 completed(审计语义与结果内容一致,防"rejected/failed 结果
|
||||
// 记 completed"误导安全追溯)。审计记录缺失/查询失败 fallback completed(不阻塞去重,降级原行为)。
|
||||
let status = audit_repo
|
||||
.find_by_tool_call_id(old_id.as_str())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|opt| opt.map(|rec| rec.status))
|
||||
.unwrap_or_else(|| "completed".to_string());
|
||||
return Some((msg.content.clone(), status));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -609,7 +638,7 @@ pub(crate) async fn process_tool_calls(
|
||||
}
|
||||
// F-05:仅 High 查去重缓存;Med 保持原审批流程
|
||||
if matches!(risk_level, RiskLevel::High) {
|
||||
if let Some(cached) = find_cached_high_risk_result(session, &draft.name, &args) {
|
||||
if let Some((cached, status)) = find_cached_high_risk_result(session, &audit_repo, &draft.name, &args).await {
|
||||
// 命中:把缓存结果作为新 tool_call_id 的 tool_result 回传,跳过审批
|
||||
tracing::info!(
|
||||
tool = %draft.name,
|
||||
@@ -622,8 +651,8 @@ pub(crate) async fn process_tool_calls(
|
||||
result: serde_json::Value::String(cached.clone()),
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
});
|
||||
// 审计:去重命中记一条 completed(decided_by=auto_dedup),不进 pending
|
||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, "completed", risk_level, Some(cached), Some("auto_dedup")).await;
|
||||
// 审计:去重命中记一条(status 透传缓存来源 completed/rejected/failed,SW-260618-16;decided_by=auto_dedup),不进 pending
|
||||
audit_tool_call(&audit_repo, conv_id, &draft.id, &draft.name, &draft.args, &status, risk_level, Some(cached), Some("auto_dedup")).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -641,12 +670,11 @@ pub(crate) async fn process_tool_calls(
|
||||
tool_call_id: draft.id.clone(),
|
||||
tool_name: draft.name.clone(),
|
||||
arguments: args.clone(),
|
||||
risk_level,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
recovered: false,
|
||||
diff: approval_diff.clone(),
|
||||
});
|
||||
session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认"));
|
||||
session.messages.push(ChatMessage::tool_result(&draft.id, PENDING_APPROVAL_PLACEHOLDER));
|
||||
let reason = build_approval_reason(&draft.name, &args, risk_level, db).await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiApprovalRequired {
|
||||
id: draft.id.clone(),
|
||||
|
||||
@@ -23,6 +23,33 @@ use super::skills::{read_skill_content, SkillInfo, skills_cached};
|
||||
|
||||
use super::AiChatEvent;
|
||||
|
||||
/// 把当前所有挂起审批的占位 tool_result(内容为 audit.rs:PENDING_APPROVAL_PLACEHOLDER
|
||||
/// "需要用户审批,等待确认")就地替换为终态文本。
|
||||
///
|
||||
/// 背景(SW-260618-02):审批占位文本在 stop/clear/create/delete/force_send 等
|
||||
/// 清 pending_approvals 时若不替换,会残留在 messages 里,下次发送把占位喂给 LLM。
|
||||
/// 占位内容非工具结果语义,污染上下文 + 误导模型继续等待。
|
||||
///
|
||||
/// 调用时机:在 `pending_approvals.clear()` / `retain` 之前调用(遍历即将被丢弃的条目)。
|
||||
/// 在 messages 已 clear 的命令(如 ai_chat_clear)里该方法为 no-op
|
||||
/// (replace_tool_result_content 找不到 tool_result 返回 false),不报错、无副作用。
|
||||
///
|
||||
/// 参数 `final_text` 由调用方按清理场景语义选择(如"会话已停止"/"已取消"/"会话已清除")。
|
||||
fn finalize_pending_placeholders(session: &mut super::AiSession, final_text: &str) {
|
||||
// SW-260618-02: 先 clone pending 的 tool_call_id(借用在此结束),再可变借 messages。
|
||||
// 必须整体借 &mut session 在函数体内做 disjoint field borrow —— 调用方若分别传
|
||||
// &mut messages + &pending 两个引用,函数参数列表不做 disjoint 推断会触发 E0502(2026-06-18 主代修)。
|
||||
let ids: Vec<String> = session
|
||||
.pending_approvals
|
||||
.values()
|
||||
.map(|a| a.tool_call_id.clone())
|
||||
.collect();
|
||||
for id in ids {
|
||||
// replace_tool_result_content 反向遍历,找不到对应 tool_call_id 时返回 false,无副作用
|
||||
session.messages.replace_tool_result_content(&id, final_text);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 发送 / 审批 / 控制
|
||||
// ============================================================
|
||||
@@ -51,6 +78,12 @@ pub async fn ai_regenerate(
|
||||
if session.generating {
|
||||
return Err("AI 正在生成中,请等待完成".to_string());
|
||||
}
|
||||
// 一致性:regenerate 限定当前活跃对话(避免历史快照陈旧时弹错对话的消息)。
|
||||
// 必须在 pop_last_assistant_round 之前校验——否则对话不匹配时已弹出 active 对话
|
||||
// 的 AI 回复却报错退出,内存消息被破坏无法恢复(对齐 ai_chat_edit:637-640 先校验后改)。
|
||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
||||
return Err("对话已切换,无法重新生成".to_string());
|
||||
}
|
||||
session.generating = true;
|
||||
session.stop_flag.store(false, Ordering::SeqCst);
|
||||
session.agent_language = language.clone();
|
||||
@@ -64,11 +97,6 @@ pub async fn ai_regenerate(
|
||||
session.generating = false;
|
||||
return Err("没有可重新生成的回复".to_string());
|
||||
}
|
||||
// 一致性:regenerate 限定当前活跃对话(避免历史快照陈旧时弹错对话的消息)
|
||||
if session.active_conversation_id.as_deref() != Some(conversation_id.as_str()) {
|
||||
session.generating = false;
|
||||
return Err("对话已切换,无法重新生成".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let _tool_defs = state.ai_tools.tool_definitions();
|
||||
@@ -258,11 +286,18 @@ pub async fn ai_approve(
|
||||
Some(a) => a,
|
||||
None => {
|
||||
// F-260616-06: 幂等——内存无挂起审批时查审计表,若已处理则返回成功(非报错)
|
||||
if let Some(rec) = state.ai_tool_executions.find_by_tool_call_id(&tool_call_id).await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if rec.status == "executed" || rec.status == "rejected" || rec.status == "failed" {
|
||||
return Ok(format!("已处理({})", rec.status));
|
||||
// BUG-260618-11: 区分 Err(DB 故障)与 Ok(None)(真无记录),原 unwrap_or_default 把
|
||||
// Err 压成 None,DB 故障被「未找到」误导(实为可重试故障),对齐 audit.rs audit_finalize 三路分流。
|
||||
match state.ai_tool_executions.find_by_tool_call_id(&tool_call_id).await {
|
||||
Ok(Some(rec)) => {
|
||||
if rec.status == "executed" || rec.status == "rejected" || rec.status == "failed" {
|
||||
return Ok(format!("已处理({})", rec.status));
|
||||
}
|
||||
}
|
||||
Ok(None) => { /* 真无记录,落到下方 return Err 未找到挂起审批 */ }
|
||||
Err(e) => {
|
||||
tracing::error!("ai_approve: 查询 tool_call_id={} 审计记录失败(DB 故障): {}", tool_call_id, e);
|
||||
return Err(format!("查询审批记录失败(DB 故障),请重试: {}", e));
|
||||
}
|
||||
}
|
||||
return Err(format!("未找到挂起的审批: {}", tool_call_id));
|
||||
@@ -410,6 +445,9 @@ pub async fn ai_chat_clear(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// 取活跃对话 id 后释放锁(避免持 session 锁调 DB)
|
||||
let active_id = session.active_conversation_id.clone();
|
||||
// SW-260618-02:清 pending 前先把占位 tool_result 终态化(此处 messages 紧随即 clear,
|
||||
// 调用为 no-op,但保持清理点统一口径防顺序变更时占位随 messages 残留 DB)。
|
||||
finalize_pending_placeholders(&mut *session, "会话已清除");
|
||||
session.messages.clear();
|
||||
session.pending_approvals.clear();
|
||||
drop(session);
|
||||
@@ -758,6 +796,9 @@ pub async fn ai_chat_force_send(
|
||||
// ① 软停止复位(照旧实现,与 ai_chat_stop 审批分支一致)
|
||||
let old = session.active_conversation_id.clone();
|
||||
session.generating = false;
|
||||
// SW-260618-02:强制发送丢弃旧审批,先终态化占位 tool_result,防占位随 messages 残留
|
||||
// 下次发送喂给 LLM(此处 messages 不 clear,是真实残留路径)。
|
||||
finalize_pending_placeholders(&mut *session, "已取消");
|
||||
session.pending_approvals.clear();
|
||||
session.stop_flag.store(true, Ordering::SeqCst);
|
||||
// ② 同锁内立即占用 + 追加用户消息(逻辑照 ai_chat_send:160-194,无 generating 拦截——
|
||||
@@ -859,6 +900,9 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<
|
||||
}
|
||||
if !session.pending_approvals.is_empty() {
|
||||
// 审批等待态:loop 已退出,直接清理让会话立即可用
|
||||
// SW-260618-02:messages 不 clear,占位 tool_result 会残留,下次发送喂给 LLM。
|
||||
// 清 pending 前先终态化为"会话已停止"。
|
||||
finalize_pending_placeholders(&mut *session, "会话已停止");
|
||||
session.pending_approvals.clear();
|
||||
session.generating = false;
|
||||
session.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启
|
||||
@@ -1020,7 +1064,10 @@ pub async fn ai_save_provider(
|
||||
api_key: String,
|
||||
default_model: String,
|
||||
provider_type: String,
|
||||
model_configs: Vec<ModelConfig>,
|
||||
) -> Result<String, String> {
|
||||
// F-260618-06:接收前端传入的 model_configs 落库(含用户在 Settings 调的 weight/enabled/label),
|
||||
// 不再硬塞 Vec::new() 丢弃用户配置。新建传 []、编辑传回填+改动后的 providerForm.models。
|
||||
// 编辑已有提供商时保留原 created_at,避免被覆盖
|
||||
// F-260614-04c: 编辑路径同时保留原 enabled/weight(负载均衡池可编辑层)。
|
||||
// 前端经此 IPC 改 enabled/weight 落库;新建走默认 enabled=true/weight=50。
|
||||
@@ -1087,7 +1134,7 @@ pub async fn ai_save_provider(
|
||||
base_url,
|
||||
default_model,
|
||||
models: None,
|
||||
model_configs: Vec::new(),
|
||||
model_configs,
|
||||
is_default,
|
||||
config: None,
|
||||
created_at,
|
||||
@@ -1244,13 +1291,31 @@ pub async fn ai_fetch_models(
|
||||
let api_key = df_storage::secret::resolve_provider_secret(&provider);
|
||||
let fetch_type = normalize_provider_type_for_fetch(&provider.provider_type);
|
||||
|
||||
let configs = df_ai::model_fetch::fetch_and_probe(&fetch_type, &provider.base_url, &api_key)
|
||||
let probed = df_ai::model_fetch::fetch_and_probe(&fetch_type, &provider.base_url, &api_key)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
|
||||
// 合并:新探测 configs 为主(更新能力维度 modalities/capabilities/cost_tier/intelligence/
|
||||
// context_window/probe_source),按 model_id 保留用户在 Settings 调过的 weight/enabled/label。
|
||||
// 否则每次「测试连接/拉取模型」覆盖回探测默认 weight(50),权重失效致 ProviderPool/router
|
||||
// 排序摇摆。对齐 provider 级 enabled/weight 编辑保留逻辑(commands.rs:1041 match existing)。
|
||||
// 新模型(旧池无同 model_id)用探测默认值。
|
||||
let merged: Vec<ModelConfig> = probed
|
||||
.iter()
|
||||
.map(|c| match provider.model_configs.iter().find(|o| o.model_id == c.model_id) {
|
||||
Some(old) => ModelConfig {
|
||||
weight: old.weight,
|
||||
enabled: old.enabled,
|
||||
label: old.label.clone(),
|
||||
..c.clone()
|
||||
},
|
||||
None => c.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 写回 model_configs(更新 updated_at)
|
||||
let mut updated = provider.clone();
|
||||
updated.model_configs = configs.clone();
|
||||
updated.model_configs = merged.clone();
|
||||
updated.updated_at = now_millis();
|
||||
state
|
||||
.ai_providers
|
||||
@@ -1258,7 +1323,7 @@ pub async fn ai_fetch_models(
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
|
||||
Ok(configs)
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
/// 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
|
||||
@@ -1295,6 +1360,9 @@ pub async fn ai_conversation_create(
|
||||
if session.generating {
|
||||
let old_conv = session.active_conversation_id.clone();
|
||||
session.generating = false;
|
||||
// SW-260618-02:清 pending 前终态化旧对话占位 tool_result(下方 messages.clear 会清旧消息,
|
||||
// 此处调用为 no-op,但保持统一口径防清理顺序变更)。
|
||||
finalize_pending_placeholders(&mut *session, "已取消");
|
||||
session.pending_approvals.clear();
|
||||
session.stop_flag.store(true, Ordering::SeqCst);
|
||||
drop(session);
|
||||
@@ -1309,8 +1377,25 @@ pub async fn ai_conversation_create(
|
||||
}
|
||||
session = state.ai_session.lock().await;
|
||||
}
|
||||
// 修复 B-260618-06: 清内存前先持久化当前活跃会话,防「新建会话致旧对话消息丢失」。
|
||||
// 丢失路径1(高频稳定复现):生成中新建 → 本函数 messages.clear() 抹掉旧会话内存消息,
|
||||
// 旧 loop 经 B-260615-11 一致性校验(active_conversation_id≠conv_id)return 不 save → 旧会话整段永久丢失。
|
||||
// 丢失路径2(低概率):loop 正常完成 spawn 后台 save(agentic.rs:928)与 clear 竞态,读空内存覆盖旧会话为空。
|
||||
// 此处 save 在 clear 前 await 完成,内存仍是旧会话完整消息;幂等(已落库则 update 路径不累加 token/不改 model)。
|
||||
let old_conv = session.active_conversation_id.clone();
|
||||
let old_has_msgs = !session.messages.is_empty();
|
||||
drop(session);
|
||||
if let Some(ref oc) = old_conv {
|
||||
if old_has_msgs {
|
||||
save_conversation(&state.ai_session, &state.db, oc.as_str(), None, None).await;
|
||||
}
|
||||
}
|
||||
let mut session = state.ai_session.lock().await;
|
||||
session.active_conversation_id = Some(id.clone());
|
||||
session.active_conv_created_at = Some(now);
|
||||
// SW-260618-02:清 pending 前终态化占位 tool_result(此处 messages 紧即将 clear,调用为 no-op,
|
||||
// 保持清理点统一口径;非生成分支无上方 messages.clear 之外的清场,防占位随新会话残留)。
|
||||
finalize_pending_placeholders(&mut *session, "会话已清除");
|
||||
session.messages.clear();
|
||||
session.pending_approvals.clear();
|
||||
// F-260616-09(A 路线):补漏清字段维持单例软隔离,解「新建会话上下文残留」。
|
||||
@@ -1368,6 +1453,7 @@ pub async fn ai_conversation_list(
|
||||
#[tauri::command]
|
||||
pub async fn ai_conversation_switch(
|
||||
state: State<'_, AppState>,
|
||||
app_handle: AppHandle,
|
||||
conversation_id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let record = state.ai_conversations.get_by_id(&conversation_id).await
|
||||
@@ -1379,6 +1465,8 @@ pub async fn ai_conversation_switch(
|
||||
|
||||
let messages_json = record.messages.clone();
|
||||
let title = record.title.clone();
|
||||
// B-260617-17 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求)
|
||||
let need_title_regen = record.title.is_none();
|
||||
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// 生成中允许只读切换:返回目标对话的 messages 供前端展示,但不修改 session 状态
|
||||
@@ -1402,6 +1490,29 @@ pub async fn ai_conversation_switch(
|
||||
session.model_override = None;
|
||||
// AE-2025-04:切换对话清会话信任,新会话重审(信任上下文相关,不跨对话)。
|
||||
session.session_trust.clear();
|
||||
// 释放 session lock 再做 async provider 查询 + spawn(避免持锁 await DB)
|
||||
drop(session);
|
||||
|
||||
// 历史会话切入且 title 空 → 后台触发标题重新生成(extract 即时兜底 + LLM 优化)。
|
||||
// ensure 内 get_by_id title Some 判断防重复;无可用 provider 静默跳过(增强不阻塞切换)。
|
||||
if need_title_regen {
|
||||
match super::prompt::get_active_provider(&state).await {
|
||||
Ok(provider_config) => {
|
||||
super::title::spawn_ensure_title(
|
||||
&provider_config,
|
||||
&state.db,
|
||||
&conversation_id,
|
||||
&app_handle,
|
||||
&state.ai_session,
|
||||
&state.llm_concurrency,
|
||||
);
|
||||
}
|
||||
Err(e) => tracing::warn!(
|
||||
"切入历史会话触发标题重生成跳过(无可用 provider, conv_id={}): {}",
|
||||
conversation_id, e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"id": record.id,
|
||||
@@ -1419,10 +1530,13 @@ pub async fn ai_conversation_delete(
|
||||
state.ai_conversations.delete(&conversation_id).await.map_err(err_str)?;
|
||||
|
||||
let mut session = state.ai_session.lock().await;
|
||||
// 删除任意对话(含非活跃)都应清理其积压审批:pending_approvals 是单例 HashMap,
|
||||
// 非活跃对话的恢复审批(recovered,conversation_id 指向被删对话)若不 retain 清理,
|
||||
// 会永久残留死审批条目。对齐 ai_conversation_switch 的 retain 口径(仅清目标对话,保留其他)。
|
||||
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
|
||||
if session.active_conversation_id.as_deref() == Some(&conversation_id) {
|
||||
session.active_conversation_id = None;
|
||||
session.messages.clear();
|
||||
session.pending_approvals.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
//! 路由(Lite 智力, 无 tool_use, 标准成本上限)兜底 `default_model`。失败返 `Err`,
|
||||
//! 调用方按场景降级(手动压缩报错给用户;自动压缩降级为原裁剪行为)。
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
// F-15: 压缩路由 — TaskRequirements(Standard 智力即可,无需工具)。压缩是结构化
|
||||
// 摘要任务,选 Standard 智力保摘要质量;不传 tools(纯文本出,避免 LLM 调工具跑偏)。
|
||||
// 池空/无匹配兜底 default_model(与 title.rs 一致,行为可预期)。
|
||||
use df_ai::router::{
|
||||
select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements,
|
||||
select_model_id, Modality, TaskRequirements,
|
||||
};
|
||||
use df_storage::models::AiProviderRecord;
|
||||
|
||||
@@ -31,7 +31,7 @@ use super::prompt::compress_prompt;
|
||||
/// - 输出:`Result<String, String>` — `Ok` 为 LLM 生成的摘要文本(已 trim);
|
||||
/// `Err` 为 provider 调用失败(底层 `anyhow::Error` 转 String,调用方降级不阻塞)。
|
||||
///
|
||||
/// 模型选择:`select_model_id(TaskRequirements{Standard 智力, Medium 成本上限, 无 tool_use})`,
|
||||
/// 模型选择:`select_model_id(TaskRequirements{无 tool_use})`,
|
||||
/// 池空/无匹配兜底 `default_model`(与 `generate_title_via_llm` 一致)。
|
||||
///
|
||||
/// 限流:复用 `LlmConcurrency` 双层 Semaphore(压缩属独立 LLM 调用,纳入全局 + 单对话限流,
|
||||
@@ -54,13 +54,11 @@ pub(crate) async fn compress_via_llm(
|
||||
return Err("无可压缩消息(active 消息为空)".to_string());
|
||||
}
|
||||
|
||||
// 路由选模型:Standard 智力够摘要,无 tool_use,Medium 成本上限。
|
||||
// 路由选模型:无 tool_use(纯文本摘要)。
|
||||
// None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let compress_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: Some(CostTier::Medium),
|
||||
estimated_context: 0,
|
||||
};
|
||||
let model = select_model_id(&compress_req, &provider_config.model_configs)
|
||||
|
||||
@@ -10,7 +10,7 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
// - 嵌入: TaskRequirements(Capability::Embedding,无工具)— 在 embedding provider 的池中选,
|
||||
// 池空兜底 config.embedding_model(行为不变,现有 KnowledgeConfig 配置即生效)。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
select_model_id, Modality, TaskRequirements,
|
||||
};
|
||||
use df_storage::crud::{AiConversationRepo, KnowledgeRepo};
|
||||
use df_storage::db::Database;
|
||||
@@ -58,8 +58,6 @@ async fn resolve_embed_provider(
|
||||
let embed_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Lite,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let model = select_model_id(&embed_req, &rec.model_configs).unwrap_or(fallback_model);
|
||||
@@ -129,7 +127,13 @@ async fn hybrid_search(
|
||||
limit: usize,
|
||||
config: &crate::state::KnowledgeConfig,
|
||||
) -> Vec<df_storage::models::KnowledgeRecord> {
|
||||
let keyword_results = state.knowledge.search(query, None, limit).await.unwrap_or_default();
|
||||
let keyword_results = match state.knowledge.search(query, None, limit).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] 知识关键词检索失败,降级空结果");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
if !config.vector_enabled {
|
||||
return keyword_results;
|
||||
@@ -138,7 +142,13 @@ async fn hybrid_search(
|
||||
Some(v) => v,
|
||||
None => return keyword_results, // embed 失败降级
|
||||
};
|
||||
let vector_results = state.knowledge.search_vector(&query_vec, limit).await.unwrap_or_default();
|
||||
let vector_results = match state.knowledge.search_vector(&query_vec, limit).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] 知识向量检索失败,降级空结果");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
merge_hybrid_results(keyword_results, vector_results, limit)
|
||||
}
|
||||
@@ -311,7 +321,13 @@ async fn extract_knowledge_from_conversation(
|
||||
.filter(|t| !t.trim().is_empty())
|
||||
.unwrap_or_else(|| "未命名对话".to_string());
|
||||
|
||||
let messages: Vec<ChatMessage> = serde_json::from_str(&conv.messages).unwrap_or_default();
|
||||
let messages: Vec<ChatMessage> = match serde_json::from_str(&conv.messages) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] 对话消息 JSON 解析失败,降级空消息(知识提炼跳过)");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
// 过滤 user/assistant,取最后 6 条
|
||||
let recent: Vec<&ChatMessage> = messages
|
||||
.iter()
|
||||
@@ -343,8 +359,6 @@ async fn extract_knowledge_from_conversation(
|
||||
let extract_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let extract_model = select_model_id(&extract_req, &provider_config.model_configs)
|
||||
|
||||
@@ -42,7 +42,7 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use df_ai::ai_tools::RiskLevel;
|
||||
// RiskLevel 不在此 import:PendingApproval.risk_level 删后 mod.rs 不再用(audit 流程在 audit.rs 自 import)
|
||||
use df_ai::context::ContextManager;
|
||||
use df_ai::context::ContextConfig;
|
||||
|
||||
@@ -169,6 +169,7 @@ pub enum AiChatEvent {
|
||||
///
|
||||
/// 优先级:`pending_approvals` 非空(有挂起审批优先报 AwaitingApproval,
|
||||
/// 即使同时在流式)> `generating`(Streaming)> 都不满足(Idle)。
|
||||
#[allow(dead_code)] // SW-260618-21 b:预留读视图(SW-02 类终态化复用·当前 0 消费者·保留扩展点)
|
||||
pub enum SessionState {
|
||||
/// 空闲:既未生成、也无挂起审批
|
||||
Idle,
|
||||
@@ -390,6 +391,7 @@ impl AiSession {
|
||||
/// - `commands.rs:451` — 审批提交/会话复位路径的状态判断
|
||||
///
|
||||
/// 上述三处替换由 P0 任务承接,本方法先就位供其切换。
|
||||
#[allow(dead_code)] // SW-260618-21 b:预留(P0 终态化任务承接·当前 0 调用·保留扩展点)
|
||||
pub fn session_state(&self) -> SessionState {
|
||||
if !self.pending_approvals.is_empty() {
|
||||
SessionState::AwaitingApproval
|
||||
@@ -407,13 +409,13 @@ pub struct PendingApproval {
|
||||
pub tool_call_id: String,
|
||||
pub tool_name: String,
|
||||
pub arguments: serde_json::Value,
|
||||
pub risk_level: RiskLevel,
|
||||
pub conversation_id: Option<String>,
|
||||
/// 重启恢复的积压审批:无 live loop 持有 session.messages,审批后不 save(防空 messages 污染老对话)、不续跑
|
||||
pub recovered: bool,
|
||||
/// AE-2025-03(路径 B):write_file 的行级 unified diff(旧文件 vs 新内容)。
|
||||
/// 仅挂起审批前预读注入,供前端审批卡预览;旧文件不存在(新建)或非 write_file 为 None。
|
||||
/// recovered 审批(启动重建)无 diff(文件可能已变,重读无意义)。
|
||||
#[allow(dead_code)] // IPC 活跃(useAiEvents:252+ToolCard·UX-260618-06)·cargo Rust never-read 误报
|
||||
pub diff: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
// F-01 阶段5: 标题生成路由 — TaskRequirements(Lite + Low 成本,无需工具)。
|
||||
// 标题是短文本摘要任务,选便宜 Lite 模型;池空/无匹配兜底 default_model(行为不变)。
|
||||
// F-01 阶段5: 标题生成路由 — TaskRequirements(无需工具)。
|
||||
// 标题是短文本摘要任务;池空/无匹配兜底 default_model(行为不变)。
|
||||
// 注:路由已解耦(B-260618-03),原 Lite/Low 智力成本约束已删除,纯 weight 选模型。
|
||||
use df_ai::router::{
|
||||
select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements,
|
||||
select_model_id, Modality, TaskRequirements,
|
||||
};
|
||||
use df_storage::crud::AiConversationRepo;
|
||||
use df_storage::db::Database;
|
||||
@@ -56,6 +57,7 @@ pub(crate) async fn ensure_conversation_title(
|
||||
model: None,
|
||||
status: None,
|
||||
reasoning_content: m.reasoning_content.clone(),
|
||||
timestamp: m.timestamp,
|
||||
})
|
||||
.collect();
|
||||
(summary, session.messages.all_messages_clone())
|
||||
@@ -64,35 +66,55 @@ pub(crate) async fn ensure_conversation_title(
|
||||
return;
|
||||
}
|
||||
|
||||
// B-260617-17 修复:进入即 extract_title 兜底落库 + emit,保证侧栏即时有非"新对话"标题。
|
||||
// 原实现仅在 LLM 返回 None(失败)或 provider 构建失败时才落 extract——但 LLM 卡住
|
||||
// (generate_title_via_llm 的 llm_concurrency 信号量 acquire 阻塞 / 网络挂起 / 后台
|
||||
// spawn 未跑完即刷新)时,既不返回 None 也不落库 → 标题长期停留"新对话"。现先落 extract
|
||||
// 兜底,下方 LLM 生成成功后覆盖;LLM 卡住/失败/超时则保留兜底,无论如何不再"新对话"。
|
||||
let fallback_title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
|
||||
if let Err(e) = conv_repo.update_field(conv_id, "title", &fallback_title).await {
|
||||
tracing::error!("对话标题兜底落库失败(conv_id={}, title={}): {}", conv_id, fallback_title, e);
|
||||
}
|
||||
let _ = app_handle.emit("ai-conversation-changed", ());
|
||||
|
||||
// 标题生成是独立一次 LLM 调用,自建 provider(便于后台 spawn,不借主 loop 的 &dyn LlmProvider)
|
||||
// build_provider_for 含空 key 早失败:Err(如 keyring 读不到)→ 跳过 LLM,extract_title 兜底
|
||||
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!("标题生成跳过(provider 密钥不可用,走 extract_title 兜底): {}", e);
|
||||
let title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
|
||||
let _ = conv_repo.update_field(conv_id, "title", &title).await;
|
||||
let _ = app_handle.emit("ai-conversation-changed", ());
|
||||
// 兜底已落库 + emit,LLM 跳过(密钥不可用等)
|
||||
tracing::warn!("标题 LLM 生成跳过(provider 密钥不可用,保留 extract_title 兜底): {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// F-01 阶段5: 标题生成路由 — 选便宜 Lite 模型(Low 成本上限,无工具)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model。
|
||||
// F-01 阶段5: 标题生成路由 — select_model_id None(池空/无匹配)→ 兜底 default_model。
|
||||
let title_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Lite,
|
||||
max_cost: Some(CostTier::Low),
|
||||
estimated_context: 0,
|
||||
};
|
||||
let title_model = select_model_id(&title_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
let title = match generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency).await {
|
||||
Some(t) => t,
|
||||
None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()),
|
||||
// B-260617-17: LLM 调用包 20s 超时,防信号量/网络挂起致后台 task 永久堆积;
|
||||
// 超时或返回 None 均保留上方已落的 extract 兜底,不覆盖。
|
||||
let title = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(20),
|
||||
generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency),
|
||||
).await {
|
||||
Ok(Some(t)) => t,
|
||||
Ok(None) => {
|
||||
tracing::warn!("标题 LLM 生成返回空,保留 extract_title 兜底(conv_id={})", conv_id);
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("标题 LLM 生成超时(20s),保留 extract_title 兜底(conv_id={})", conv_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = conv_repo.update_field(conv_id, "title", &title).await;
|
||||
// LLM 生成成功 → 覆盖 extract 兜底
|
||||
if let Err(e) = conv_repo.update_field(conv_id, "title", &title).await {
|
||||
tracing::error!("对话标题 LLM 落库失败(conv_id={}, title={}): {}", conv_id, title, e);
|
||||
}
|
||||
let _ = app_handle.emit("ai-conversation-changed", ());
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +107,8 @@ pub async fn promote_idea(
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("灵感不存在: {id}"))?;
|
||||
|
||||
if record.promoted_to.is_some() {
|
||||
return Err(format!("灵感已立项: {}", record.promoted_to.unwrap()));
|
||||
if let Some(promoted_to) = &record.promoted_to {
|
||||
return Err(format!("灵感已立项: {}", promoted_to));
|
||||
}
|
||||
|
||||
// 复用 df-project 领域逻辑构造项目实体(create_from_idea)
|
||||
@@ -289,11 +289,16 @@ async fn build_default_provider(
|
||||
|
||||
/// IdeaRecord → df_ideas::Idea(评估用,status/time 不影响评分)
|
||||
fn record_to_idea(record: &IdeaRecord) -> Idea {
|
||||
let tags: Vec<String> = record
|
||||
.tags
|
||||
.as_deref()
|
||||
.and_then(|t| serde_json::from_str(t).ok())
|
||||
.unwrap_or_default();
|
||||
let tags: Vec<String> = match record.tags.as_deref() {
|
||||
Some(t) => match serde_json::from_str::<Vec<String>>(t) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, idea_id = %record.id, "[ideas] tags JSON 解析失败,降级空 tags 继续评估");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
Idea {
|
||||
id: record.id.clone(),
|
||||
title: record.title.clone(),
|
||||
|
||||
@@ -6,14 +6,15 @@ use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest};
|
||||
// F-01 阶段5: 项目扫描描述路由 — TaskRequirements(Standard;采样含图时追加 Vision,
|
||||
// F-01 阶段5: 项目扫描描述路由 — TaskRequirements(采样含图时追加 Vision,
|
||||
// 当前采样(ProjectSample)未含图像,故仅 Text)。池空/无匹配兜底 default_model。
|
||||
// 注:路由已解耦(B-260618-03),原 Standard 智力约束已删除,纯 weight 选模型。
|
||||
use df_ai::router::{
|
||||
select_model_id, IntelligenceTier, Modality, TaskRequirements,
|
||||
select_model_id, Modality, TaskRequirements,
|
||||
};
|
||||
use df_types::types::new_id;
|
||||
use df_project::scan::{
|
||||
collect_sample, detect_stack, discover_projects, extract_description, is_monorepo,
|
||||
collect_sample, detect_stack, discover_projects, extract_description,
|
||||
normalize_path, DiscoveredProject,
|
||||
};
|
||||
use df_storage::models::ProjectRecord;
|
||||
@@ -527,13 +528,11 @@ async fn extract_description_via_llm(
|
||||
.map_err(err_str)?
|
||||
.map_err(err_str)?;
|
||||
|
||||
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
|
||||
// F-01 阶段5: 项目扫描描述路由 — 无工具;采样未含图故仅 Text。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let scan_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let scan_model = select_model_id(&scan_req, &pc.model_configs)
|
||||
@@ -623,13 +622,11 @@ pub async fn scan_project_with_ai(
|
||||
});
|
||||
}
|
||||
};
|
||||
// F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。
|
||||
// F-01 阶段5: 项目扫描描述路由 — 无工具;采样未含图故仅 Text。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。
|
||||
let scan_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: false,
|
||||
min_intelligence: IntelligenceTier::Standard,
|
||||
max_cost: None,
|
||||
estimated_context: 0,
|
||||
};
|
||||
let scan_model = select_model_id(&scan_req, &pc.model_configs)
|
||||
|
||||
@@ -202,11 +202,13 @@ pub struct AppState {
|
||||
pub projects: ProjectRepo,
|
||||
/// 任务表 Repo
|
||||
pub tasks: TaskRepo,
|
||||
/// 发布表 Repo
|
||||
/// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub releases: ReleaseRepo,
|
||||
/// 工作流执行表 Repo
|
||||
pub workflows: WorkflowRepo,
|
||||
/// 节点执行表 Repo
|
||||
/// 节点执行表 Repo(预留:NodeExecutionRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub node_executions: NodeExecutionRepo,
|
||||
/// 工作流事件总线(tokio broadcast)
|
||||
pub event_bus: EventBus,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": ["nsis"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
Reference in New Issue
Block a user