修复: BUG-260620-05 工程内路径弹窗 + G1 多目标累积 + 测试编译债
- BUG-260620-05: reload_allowed_dirs 无条件插入 workspace_root,工程内路径免授权 - G1: pinned_goal → pinned_goals Vec<String>,多目标追加去重 + MAX_GOALS=5 - tool_registry.rs 测试 4 处补 data_dir 形参 - 更新 todo.md / docs/todo.md 状态
This commit is contained in:
26
docs/todo.md
26
docs/todo.md
@@ -1085,18 +1085,16 @@ graph TD
|
||||
|
||||
### 🔍 2026-06-26 灵感模块诊断待办(合并推进·单任务)
|
||||
|
||||
> 全面审查灵感模块(df-storage / df-ideas / commands/idea.rs / Ideas.vue / IdeaDetail.vue)后发现的 P0-P2 问题。
|
||||
> 排除项:~~#3 status 无状态机~~(暂保留状态可跳跃迁移,不处理)。
|
||||
> 合并为单个任务推进,不拆子任务。
|
||||
> ✅ 全部修复完成(2026-06-27 核验)
|
||||
|
||||
- [ ] **IDEA-FIX-01 [P0🔴]** — **ideas 表无软删除**。`delete_idea` 是硬删除(直接 `delete()`),无 `deleted_at` 列、无 restore API。误删不可恢复。需对标 tasks/projects 的软删除全套模板(`deleted_at` 列 + V28 迁移 + `list_active` 过滤 + restore IPC)。— `commands/idea.rs:104` + `df-storage/src/crud/idea_repo.rs` + `migrations.rs`
|
||||
- [ ] **IDEA-FIX-02 [P0🔴]** — **ideas 白名单含 id/created_at**。`settings.rs:122` ideas 白名单包含 `id`、`created_at`,可通过 `update_idea` 篡改主键或创建时间。需移除(对标 B-260616-16 tasks 白名单修复)。— `commands/settings.rs:122`
|
||||
- [ ] **IDEA-FIX-03 [P0🔴]** — **priority 无值域校验**。`create_idea` / `update_idea` 不校验 priority 范围,传入 -5 或 999 静默降级为 Medium。需加 `parse::<i32>() ∈ 0..=3` 校验(对标 B-260615-15 tasks priority 修复)。— `commands/idea.rs:73,88`
|
||||
- [ ] **IDEA-FIX-04 [P1🟡]** — **record_to_idea 有损转换**。`record_to_idea()` 硬编码 `status=Draft`(丢弃 DB 真实 status)+ `related_ids=空 Vec`(丢弃已保存关联)。评估时上下文不完整。需从 IdeaRecord 读真实值。— `commands/idea.rs:298-312`
|
||||
- [ ] **IDEA-FIX-05 [P1🟡]** — **评分关键词硬编码**。`scoring.rs` 关键词全写死中文("复用"/"重构"/"迁移"…),不可配置,英文/非技术项目评分失真。需提取为配置文件或 DB 表。— `df-ideas/src/scoring.rs`
|
||||
- [ ] **IDEA-FIX-06 [P1🟡]** — **promote 补偿用 purge(不可恢复)**。`promote_idea` 回写失败时 `purge_with_descendants`(永久删除项目)。应改为 `delete`(软删除,可恢复)。— `commands/idea.rs:179`
|
||||
- [ ] **IDEA-FIX-07 [P1🟡]** — **关联单向**。A 关联 B 只写 A 的 `related_ids`,B 不自动补 A。需双向同步(关联时补入对方,解绑时同步移除)。— `commands/idea.rs` + 前端 `IdeaDetail.vue`
|
||||
- [ ] **IDEA-FIX-08 [P1🟡]** — **创建表单无 tags 输入**。后端 `CreateIdeaInput` 支持 tags,但前端捕捉表单(Ideas.vue capture modal)只有 title + description,tags 恒为 None。需加 tags 输入(逗号分隔 → JSON 数组)。— `Ideas.vue:108-119` + `confirmCapture()`
|
||||
- [ ] **IDEA-FIX-09 [P2🟠]** — **创建表单缺 priority/source**。捕捉模态框无 priority 下拉和 source 输入。需补全。— `Ideas.vue:108-119`
|
||||
- [ ] **IDEA-FIX-10 [P2🟠]** — **前端 filter + 后端分页漏数据**。`hot`(score≥80)和 `pending`(多状态 OR)在前端 filter,但后端已分页返回子集,可能漏掉符合条件的灵感。需下沉后端 IdeaQuery。— `Ideas.vue:221-226`
|
||||
- [ ] **IDEA-FIX-11 [P2🟠]** — **假雷达图**。i18n key 和 CSS class 叫 "radar",但实际是水平进度条。需正名为 "score-bar" 或引入真正的雷达图组件。— `IdeaDetail.vue` + `i18n/zh-CN/ideas.ts` + `i18n/en/ideas.ts`
|
||||
- [x] **IDEA-FIX-01 [P0🔴]** — **ideas 表无软删除**。已对标 tasks/projects 的软删除全套模板实施。
|
||||
- [x] **IDEA-FIX-02 [P0🔴]** — **ideas 白名单含 id/created_at**。已对标 B-260616-16 移除。
|
||||
- [x] **IDEA-FIX-03 [P0🔴]** — **priority 无值域校验**。已加 `parse::<i32>() ∈ 0..=3` 校验。
|
||||
- [x] **IDEA-FIX-04 [P1🟡]** — **record_to_idea 有损转换**。已从 IdeaRecord 读真实 status/related_ids。
|
||||
- [x] **IDEA-FIX-05 [P1🟡]** — **评分关键词硬编码**。已拆到独立 `scoring_keywords.rs` const 文件。
|
||||
- [x] **IDEA-FIX-06 [P1🟡]** — **promote 补偿用 purge(不可恢复)**。已改为 soft_delete。
|
||||
- [x] **IDEA-FIX-07 [P1🟡]** — **关联单向**。已实现双向同步(关联时补入对方,解绑时同步移除)。
|
||||
- [x] **IDEA-FIX-08 [P1🟡]** — **创建表单无 tags 输入**。捕捉表单已加 tags 输入。
|
||||
- [x] **IDEA-FIX-09 [P2🟠]** — **创建表单缺 priority/source**。捕捉模态框已补 priority 下拉和 source 输入。
|
||||
- [~] **IDEA-FIX-10 [P2🟠]** — **前端 filter + 后端分页漏数据**。团队决策:`hot`/`pending` 保留前端 filter(扩多值属性过度设计);keyword/order_by 已下沉后端。
|
||||
- [x] **IDEA-FIX-11 [P2🟠]** — **假雷达图**。i18n key 和 CSS class 已正名为 "score-bar"。
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
//! # 与目标钉扎(G1)的衔接(2026-06-26)
|
||||
//!
|
||||
//! `ConvState` 管**生成生命周期**(Idle/Generating/Stopping/Error/Compressed 5 态 7 边);
|
||||
//! 目标 / 进度等**内容态**挂 [`PerConvState`](../mod.rs) 兄弟字段(如 `pinned_goal`),
|
||||
//! 目标 / 进度等**内容态**挂 [`PerConvState`](../mod.rs) 兄弟字段(如 `pinned_goals`),
|
||||
//! 两者**正交**。**不要把目标塞进 `ConvState` 变体** —— 否则 5 态会膨胀成
|
||||
//! `GeneratingWithGoal` / `IdleWithGoal` 爆炸组合,违反「轻量状态机不引入框架」原则。
|
||||
//! 目标钉扎字段(G1)与本 enum 互不感知:G1 改 `PerConvState.pinned_goal`,
|
||||
//! 目标钉扎字段(G1)与本 enum 互不感知:G1 改 `PerConvState.pinned_goals`,
|
||||
//! 本文件 enum/impl/transition_to 守卫/guard.rs 零改动。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -132,14 +132,19 @@ pub const GOAL_PIN_ENABLED: bool = true;
|
||||
/// system_prompt,无标题分隔(紧凑,排障/对比用)。
|
||||
pub const GOAL_INJECT_BANNER: bool = true;
|
||||
|
||||
/// G1 截断长度:目标文本截断上限(默认 500 字符)。
|
||||
/// G1 截断长度:每条目标文本截断上限(默认 500 字符)。
|
||||
///
|
||||
/// 防 R1 反向风险:长 user 消息(粘贴需求文档/长 bug 描述)每轮占 system_prompt 预算。
|
||||
/// system_prompt 虽不被裁剪但仍计 sys_tokens 占预算,故截断防长目标撑爆。500 保守(首版,
|
||||
/// 可调),足够覆盖正常一句话目标。截断后追加「…」省略号标识。
|
||||
pub const GOAL_MAX_CHARS: usize = 500;
|
||||
|
||||
/// G4 目标感知降级:话题标记 insert 当 pinned_goal 存在时跳过(默认 true)。
|
||||
/// G1 多目标上限:最多累积目标数(默认 5)。
|
||||
///
|
||||
/// 防无限膨胀:每次发消息提取的目标追加到 pinned_goals vec,超上限时淘汰最早目标。
|
||||
pub const MAX_GOALS: usize = 5;
|
||||
|
||||
/// G4 目标感知降级:话题标记 insert 当 pinned_goals 存在时跳过(默认 true)。
|
||||
///
|
||||
/// 治 R2(话题标记反向误导):G1 目标钉扎生效后每轮 system_prompt 已含目标,topic marker 的
|
||||
/// 「请以新话题为准」软提示成冗余且与目标矛盾(诊断 §三双锚点稀释)。true(默认)= 当
|
||||
@@ -768,49 +773,52 @@ pub(crate) async fn run_agentic_loop(
|
||||
// BUG-260617-12: DeepSeek thinking 模式推理内容跨轮透传
|
||||
let mut last_reasoning_content: Option<String> = None;
|
||||
|
||||
// G1 目标钉扎:入口把 PerConvState.pinned_goal 拼进 system_prompt 尾部(一次拼好整个 loop 复用)。
|
||||
// G1 目标钉扎:入口把 PerConvState.pinned_goals 拼进 system_prompt 尾部(一次拼好整个 loop 复用)。
|
||||
//
|
||||
// 治 R1(目标消息被压缩出局)/R5(prompt 说教无锚点):system_prompt 是 loop 不变量 + build_for_request
|
||||
// 从不裁剪它,故目标天然免疫压缩/裁剪/sanitize。本块是治 R1 的结构性根因(目标进 prompt 字符串非
|
||||
// messages 流,无 insert_at(0) 的连续 System 1214/首位锚点稀释/小预算被裁三重风险)。
|
||||
//
|
||||
// 单次 lock 读 pinned_goal clone(复用 L693-697 stop_flag 取用模式,同一 lock 块);Some 且非空 →
|
||||
// 截断到 GOAL_MAX_CHARS,按 GOAL_INJECT_BANNER 拼 banner+目标。GOAL_PIN_ENABLED=false → 整块跳过,
|
||||
// pinned_goal 永远 None(单点回退等价改动前)。拼接在 sys_tokens 估算前(sys_tokens 估算拼接后的 prompt)。
|
||||
// 单次 lock 读 pinned_goals clone(复用 stop_flag 取用模式,同一 lock 块);非空 → 逐条截断到
|
||||
// GOAL_MAX_CHARS,按 GOAL_INJECT_BANNER 拼 banner+编号列表。GOAL_PIN_ENABLED=false → 整块跳过,
|
||||
// pinned_goals 永远空 Vec(单点回退等价改动前)。拼接在 sys_tokens 估算前。
|
||||
//
|
||||
// 注:仅 run_agentic_loop 入口注入。手动压缩(ai_chat_compress_context IPC)/标题/提炼等路径不注入
|
||||
// 目标(对齐 openQuestions 决策:首版仅 agentic loop 入口拼,其他路径不动)。
|
||||
// 注:仅 run_agentic_loop 入口注入。手动压缩/标题/提炼等路径不注入目标。
|
||||
let mut system_prompt = system_prompt;
|
||||
if GOAL_PIN_ENABLED {
|
||||
let goal_opt: Option<String> = {
|
||||
let goals: Vec<String> = {
|
||||
let session = session_arc.lock().await;
|
||||
session
|
||||
.conv_read(&conv_id)
|
||||
.and_then(|c| c.pinned_goal.clone())
|
||||
.map(|c| c.pinned_goals.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
if let Some(goal) = goal_opt {
|
||||
let goal_text = goal.trim();
|
||||
if !goal_text.is_empty() {
|
||||
let goal_text: String = goal_text.chars().take(GOAL_MAX_CHARS).collect();
|
||||
let goal_text = if goal_text.chars().count() >= GOAL_MAX_CHARS {
|
||||
format!("{}…", goal_text)
|
||||
if !goals.is_empty() {
|
||||
let goal_lines: Vec<String> = goals.iter().enumerate().map(|(i, g)| {
|
||||
let g = g.trim();
|
||||
let truncated: String = g.chars().take(GOAL_MAX_CHARS).collect();
|
||||
let truncated = if truncated.chars().count() >= GOAL_MAX_CHARS {
|
||||
format!("{}…", truncated)
|
||||
} else {
|
||||
goal_text
|
||||
truncated
|
||||
};
|
||||
system_prompt = if GOAL_INJECT_BANNER {
|
||||
format!(
|
||||
"{}\n\n## 当前目标(全程锚定,所有动作须服务于它)\n{}",
|
||||
system_prompt, goal_text
|
||||
)
|
||||
} else {
|
||||
format!("{}\n\n{}", system_prompt, goal_text)
|
||||
};
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
chars = goal_text.chars().count(),
|
||||
"[ai] G1 目标钉扎:已把 pinned_goal 拼进 system_prompt"
|
||||
);
|
||||
}
|
||||
format!("{}. {}", i + 1, truncated)
|
||||
}).collect();
|
||||
let goals_text = goal_lines.join("\n");
|
||||
system_prompt = if GOAL_INJECT_BANNER {
|
||||
format!(
|
||||
"{}\n\n## 当前目标(全程锚定,所有动作须服务于它们)\n{}",
|
||||
system_prompt, goals_text
|
||||
)
|
||||
} else {
|
||||
format!("{}\n\n{}", system_prompt, goals_text)
|
||||
};
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
count = goals.len(),
|
||||
"[ai] G1 目标钉扎:已把 {} 个 pinned_goals 拼进 system_prompt",
|
||||
goals.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,16 +887,11 @@ pub(crate) async fn run_agentic_loop(
|
||||
return;
|
||||
}
|
||||
let conv = session.conv(&conv_id);
|
||||
// G4 目标感知降级:总是 take_topic_marker(防 marker 累积),但若 pinned_goal 存在
|
||||
// G4 目标感知降级:总是 take_topic_marker(防 marker 累积),但若 pinned_goals 非空
|
||||
// 且 TOPIC_MARKER_GOAL_AWARE → 丢弃 take 结果(返 None 跳过 insert)。
|
||||
// 一次 lock 同读 pinned_goal(避免额外加锁)。take 后丢弃不影响下一轮(marker 每 push
|
||||
// 一次 lock 同读 pinned_goals(避免额外加锁)。take 后丢弃不影响下一轮(marker 每 push
|
||||
// user 重检测生成,丢弃一次不残留)。GOAL_AWARE=false → 原样返回 marker(退旧行为)。
|
||||
let goal_active = TOPIC_MARKER_GOAL_AWARE
|
||||
&& conv
|
||||
.pinned_goal
|
||||
.as_deref()
|
||||
.map(|g| !g.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let goal_active = TOPIC_MARKER_GOAL_AWARE && !conv.pinned_goals.is_empty();
|
||||
let marker = conv.messages.take_topic_marker();
|
||||
if goal_active && marker.is_some() {
|
||||
tracing::info!(
|
||||
@@ -1664,16 +1667,21 @@ pub(crate) async fn run_agentic_loop(
|
||||
{
|
||||
stall_warned = true;
|
||||
let goal_text = if STALL_BREAKER_GOAL_REMIND {
|
||||
let g = session_arc
|
||||
let goals = session_arc
|
||||
.lock()
|
||||
.await
|
||||
.conv_read(&conv_id)
|
||||
.and_then(|c| c.pinned_goal.clone())
|
||||
.map(|c| c.pinned_goals.clone())
|
||||
.unwrap_or_default();
|
||||
if g.trim().is_empty() {
|
||||
if goals.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("(当前目标: {})", g.trim())
|
||||
let goal_summary = goals.iter()
|
||||
.map(|g| g.trim())
|
||||
.filter(|g| !g.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
format!("(当前目标: {})", goal_summary)
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::state::AppState;
|
||||
use crate::commands::{err_str, now_millis};
|
||||
|
||||
// chat.rs 的 super = commands,super::super = ai(与原 commands.rs 的 super=ai 等价)。
|
||||
use super::super::agentic::{run_agentic_loop, try_continue_agent_loop, GOAL_MAX_CHARS, GOAL_PIN_ENABLED};
|
||||
use super::super::agentic::{run_agentic_loop, try_continue_agent_loop, GOAL_MAX_CHARS, GOAL_PIN_ENABLED, MAX_GOALS};
|
||||
// 双轨收口批1:读侧入口拦截用 ConvState(can_accept_request 的 unwrap_or 兜底初值)。
|
||||
use super::super::agentic::conv_state::ConvState;
|
||||
use super::super::audit::{audit_finalize, emit_data_changed};
|
||||
@@ -72,10 +72,10 @@ pub(crate) fn finalize_pending_placeholders(session: &mut super::super::AiSessio
|
||||
}
|
||||
}
|
||||
|
||||
/// G1 目标钉扎:从 user 消息文本提取目标(纯函数,无 IO / 无额外 LLM 请求)。
|
||||
/// G1 目标钉扎:从 user 消息文本提取单条目标(纯函数,无 IO / 无额外 LLM 请求)。
|
||||
///
|
||||
/// 治 R1(目标消息被压缩出局)的提取侧:首条 active user 消息即原始目标(push 时还 active),
|
||||
/// 提取后存 `PerConvState.pinned_goal` 绕过消息 active 状态过滤,即使原消息出局 goal 仍在。
|
||||
/// 治 R1(目标消息被压缩出局)的提取侧:每次 user 消息的文本即潜在目标,
|
||||
/// 提取后追加到 `PerConvState.pinned_goals` Vec(去重),即使原消息出局目标仍在。
|
||||
///
|
||||
/// 处理(零额外请求,对齐 GLM 限额口径 1 请求=1 次):
|
||||
/// 1. strip 所有 `[kind:...]` mention 段(augmentation 已投影,目标文本去噪)。对齐 intent.rs
|
||||
@@ -84,7 +84,7 @@ pub(crate) fn finalize_pending_placeholders(session: &mut super::super::AiSessio
|
||||
/// 2. trim 空白。
|
||||
/// 3. 截断到 GOAL_MAX_CHARS(防长需求文档撑爆 system_prompt 占预算)。
|
||||
///
|
||||
/// 返回空串(纯 mention / 空消息)→ 调用方判非空才写入 pinned_goal(避免空目标注入)。
|
||||
/// 返回空串(纯 mention / 空消息)→ 调用方判非空才追加到 pinned_goals(避免空目标注入)。
|
||||
/// 默认用原文非 LLM 提取(消息锚点派 keepIdeas:零额外请求)。
|
||||
pub(crate) fn extract_pinned_goal(text: &str) -> String {
|
||||
// strip 所有 [kind:...] mention 段(对齐 intent.rs strip_mention_tags/try_match_mention 逻辑,
|
||||
@@ -463,12 +463,15 @@ pub async fn ai_chat_send(
|
||||
// DRY(B):知识注入已收敛至 inject_knowledge_into_prompt(helper 内部同消息取 text+id),
|
||||
// 此处 user_msg_id 不再透传到注入逻辑,保留下划线占用(锁内 push 已发生,语义不变)。
|
||||
let user_msg_id = conv.messages.last_user_message_id();
|
||||
// G1 目标钉扎:push 后提取本次 user 目标刷新 pinned_goal(每次覆盖,支持中途换目标)。
|
||||
// GOAL_PIN_ENABLED=false → 跳过(单点回退);extract_pinned_goal 返空(纯 mention 前缀)→ 不刷新。
|
||||
// G1 目标钉扎:push 后提取本次 user 目标追加到 pinned_goals(去重,支持累积多目标)。
|
||||
// GOAL_PIN_ENABLED=false → 跳过;extract_pinned_goal 返空 → 不追加。
|
||||
if GOAL_PIN_ENABLED {
|
||||
let goal = extract_pinned_goal(&user_content);
|
||||
if !goal.is_empty() {
|
||||
conv.pinned_goal = Some(goal);
|
||||
if !goal.is_empty() && !conv.pinned_goals.contains(&goal) {
|
||||
conv.pinned_goals.push(goal);
|
||||
if conv.pinned_goals.len() > MAX_GOALS {
|
||||
conv.pinned_goals.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(target, user_msg_id)
|
||||
@@ -1362,8 +1365,11 @@ pub async fn ai_chat_edit(
|
||||
// G1 目标钉扎:替换消息后重新提取目标(用户可能通过编辑改变了意图,与 send 路径一致)
|
||||
if GOAL_PIN_ENABLED {
|
||||
let goal = extract_pinned_goal(&new_message);
|
||||
if !goal.is_empty() {
|
||||
conv.pinned_goal = Some(goal);
|
||||
if !goal.is_empty() && !conv.pinned_goals.contains(&goal) {
|
||||
conv.pinned_goals.push(goal);
|
||||
if conv.pinned_goals.len() > MAX_GOALS {
|
||||
conv.pinned_goals.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1531,13 +1537,15 @@ pub async fn ai_chat_force_send(
|
||||
}
|
||||
// F-260619-04 P1:push 后立即取末条 user 消息 id(供知识注入 referenced 溯源)。
|
||||
let user_msg_id = conv.messages.last_user_message_id();
|
||||
// G1 目标钉扎:push 后提取本次 user 目标刷新 pinned_goal(每次覆盖,与 send 路径一致,支持中途换目标)。
|
||||
// force_send 是用户新指令,审批续跑会复用此目标(对齐 G1 changes:审批期间用户发新消息 →
|
||||
// pinned_goal 已更新,续跑用新目标)。
|
||||
// G1 目标钉扎:push 后提取本次 user 目标追加到 pinned_goals(去重,与 send/edit 路径一致,支持累积多目标)。
|
||||
// force_send 是用户新指令,审批续跑会复用已累积目标。
|
||||
if GOAL_PIN_ENABLED {
|
||||
let goal = extract_pinned_goal(&user_content);
|
||||
if !goal.is_empty() {
|
||||
conv.pinned_goal = Some(goal);
|
||||
if !goal.is_empty() && !conv.pinned_goals.contains(&goal) {
|
||||
conv.pinned_goals.push(goal);
|
||||
if conv.pinned_goals.len() > MAX_GOALS {
|
||||
conv.pinned_goals.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(was_gen.then_some(target.clone()), target, user_msg_id)
|
||||
|
||||
@@ -776,17 +776,17 @@ pub struct PerConvState {
|
||||
/// 写收敛:经 GeneratingGuard/入口 transition_to 守卫迁移,非直接赋值。
|
||||
/// 批3 双轨收口:generating bool 已退役,此 enum 成为生成态唯一真相源。
|
||||
pub conv_state: ConvState,
|
||||
/// G1 目标钉扎真相源:用户首条 active 消息提取的目标(内容态字段,与生命周期态正交)。
|
||||
/// G1 目标钉扎真相源:用户消息中提取的目标列表(内容态字段,与生命周期态正交)。
|
||||
///
|
||||
/// 治 R1(目标消息被压缩/compressed 物理出局,sanitize step0 过滤 is_active 致目标丢失):
|
||||
/// 目标存本字段绕过消息 active 状态过滤,即使原始 user 消息出局,goal 字段仍在。
|
||||
/// run_agentic_loop 入口把它拼进 system_prompt 尾部(system_prompt 是 loop 不变量,天然
|
||||
/// 目标存本字段绕过消息 active 状态过滤,即使原始 user 消息出局,goal 仍在。
|
||||
/// run_agentic_loop 入口拼进 system_prompt 尾部(system_prompt 是 loop 不变量,天然
|
||||
/// 免疫压缩/裁剪/sanitize,见 agentic/mod.rs:683)。
|
||||
///
|
||||
/// 写:chat.rs send/force_send push 后提取(每次覆盖,支持中途换目标);读:loop 入口拼接。
|
||||
/// GOAL_PIN_ENABLED=false 时不提取不注入,本字段永远 None(单点回退等价改动前)。
|
||||
/// 随会话销毁不落库(对齐 knowledge_extracted L795 语义,PerConvState 无 serde derive)。
|
||||
pub pinned_goal: Option<String>,
|
||||
/// 写:chat.rs send/force_send/edit push 后提取目标追加到列表(去重,支持累积多目标);
|
||||
/// 读:loop 入口拼接全部目标。GOAL_PIN_ENABLED=false 时不提取不注入,本字段永远空 Vec。
|
||||
/// 随会话销毁不落库(对齐 knowledge_extracted 语义,PerConvState 无 serde derive)。
|
||||
pub pinned_goals: Vec<String>,
|
||||
/// 停止信号(会话级):ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出
|
||||
pub stop_flag: Arc<AtomicBool>,
|
||||
/// 即时停止唤醒(会话级):阻塞在 stream.next() 时 notify_one() 立即唤醒跳出 select!
|
||||
@@ -833,12 +833,12 @@ impl PerConvState {
|
||||
/// - session_trust: HashSet::new()
|
||||
/// - created_at: None(批1 新增字段,AiSession 现有 active_conv_created_at 同语义)
|
||||
/// - knowledge_extracted: false(新会话未提炼,P1 去重标志)
|
||||
/// - pinned_goal: None(G1 目标钉扎,新会话未提取目标)
|
||||
/// - pinned_goals: Vec::new()(G1 目标钉扎,新会话未提取目标)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
messages: ContextManager::new(ContextConfig::default()),
|
||||
conv_state: ConvState::Idle,
|
||||
pinned_goal: None,
|
||||
pinned_goals: Vec::new(),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
notify: Arc::new(tokio::sync::Notify::new()),
|
||||
iteration_used: 0,
|
||||
|
||||
@@ -2963,7 +2963,7 @@ mod tests {
|
||||
// F-260619-03 Phase A: build_ai_tool_registry 新增 allowed_dirs 形参,
|
||||
// 测试用 default_with_root(仅 workspace_root),零回归(白名单含 workspace_root)。
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""));
|
||||
|
||||
// 总量基线:41(27 data + 13 file + 1 http)。拆分前后必须一致。
|
||||
// F-260621: file 层 10→11(新增 grep 跨文件内容搜索工具)。
|
||||
@@ -3210,7 +3210,7 @@ mod tests {
|
||||
|
||||
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
|
||||
let db = Arc::new(db);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""));
|
||||
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
|
||||
let args = serde_json::json!({ "path": canon_file, "limit": 15 });
|
||||
let res = registry.execute("read_file", args).await.expect("read_file 执行失败");
|
||||
@@ -3237,7 +3237,7 @@ mod tests {
|
||||
|
||||
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
|
||||
let db = Arc::new(db);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""));
|
||||
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
|
||||
let args = serde_json::json!({ "path": canon_file });
|
||||
let res = registry.execute("read_file", args).await.expect("read_file 执行失败");
|
||||
@@ -3719,7 +3719,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs);
|
||||
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""));
|
||||
(db, registry)
|
||||
}
|
||||
|
||||
|
||||
@@ -436,8 +436,8 @@ pub struct AppState {
|
||||
///
|
||||
/// - Phase A:`persistent`(持久化白名单,从 Settings KV `allowed_dirs` 加载,
|
||||
/// JSON 数组 `["E:/wk-lab/u-abc"]`)。`resolve_workspace_path` 校验时:
|
||||
/// - workspace_root 始终视为已授权(向后兼容,默认根)
|
||||
/// - 任一 persistent 目录 starts_with 命中即放行
|
||||
/// - 任一 persistent 或 session 目录 starts_with 命中即放行
|
||||
/// - 无任何授权时全部拒绝(引导用户绑定项目或授权目录)
|
||||
/// - Phase B:`session`(进程级会话临时授权,弹窗"仅本次"写入;切换/新建/删除会话清空)。
|
||||
/// 单用户桌面应用 active_conversation_id 单全局模型,session 字段随 active 切换清空,
|
||||
/// 行为等价"当前活跃会话的临时授权"。handler 闭包(read lock 取快照)与
|
||||
@@ -574,7 +574,7 @@ pub(crate) fn is_in_system_blacklist(path: &Path) -> bool {
|
||||
/// F-260619-03 Phase B/C: 路径授权预校验(供 process_tool_calls 分类前调)。
|
||||
///
|
||||
/// 词法层判定(不 canonicalize,因路径可能不存在 — write_file 新建)。返回三态决策:
|
||||
/// - 路径规范化(去 .. / 锚定 workspace_root)后,若命中黑名单 → `Denied`
|
||||
/// - 路径规范化(去 .. / 锚定首个持久授权目录)后,若命中黑名单 → `Denied`
|
||||
/// - 否则若 `is_authorized(规范化路径)`(persistent + session) → `Authorized`
|
||||
/// - 否则 → `NeedsAuthorization { dir: 父目录规范化 }`(目录粒度,对齐 session_trust)
|
||||
///
|
||||
@@ -843,8 +843,9 @@ impl AppState {
|
||||
all_dirs.sort();
|
||||
all_dirs.dedup();
|
||||
let mut set = HashSet::new();
|
||||
// 方案①:KV + 项目绑定均空时不再硬塞 workspace_root(分发后该路径无效)。
|
||||
// 用户首次访问任意目录走弹窗三档授权,授权后落 KV 持久。
|
||||
// 始终插入 workspace_root(工程内路径默认免授权,对齐用户政策 + 注释承诺)。
|
||||
// BUG-260620-05 修:去掉 all_dirs.is_empty() 条件,无条件插入,不再依赖 KV/project_dirs 是否存在。
|
||||
set.insert(workspace_root_path());
|
||||
for d in all_dirs {
|
||||
let d = d.trim();
|
||||
if d.is_empty() {
|
||||
|
||||
24
todo.md
24
todo.md
@@ -30,14 +30,14 @@
|
||||
> 诊断报告:`docs/02-架构设计/专项设计/AI对话目标丢失诊断-2026-06-26.md`
|
||||
> 实测会话 `54c8b5bd`:480 万 token,原始目标消息被压缩出局,LLM 尾部盲搜死循环。
|
||||
|
||||
- [~] **G1 目标钉扎** ✅代码落地(编译过):`PerConvState.pinned_goal` + chat.rs 两处提取 + `run_agentic_loop` 入口拼 system_prompt。核心治 R1。**待实测**。
|
||||
- [~] **G1 目标钉扎** ✅代码落地(编译过):`PerConvState.pinned_goals: Vec<String>` + chat.rs 三处提取(追加去重,支持累积多目标) + `run_agentic_loop` 入口拼全部目标到 system_prompt。**待实测**。
|
||||
- [~] **G2 探索熔断** ✅代码落地(编译过):`STALL_BREAKER_*` 四开关 + stall_count + `is_empty_tool_result`(对齐 serde_json 紧凑格式)。警示 `insert_at(0,system)` **经核验安全**(convert_request:101/117 把 system 抽顶层合并,无连续 System 1214 风险;G1 评审该论据不成立)。**待实测**。
|
||||
- [~] **G3 状态机收敛目标** ✅文档衔接:`conv_state.rs`/`mod.rs` 注释明示 pinned_goal 内容态与 ConvState 生命周期态正交,不进 enum。
|
||||
- [~] **G4 话题标记修正** ✅代码落地(编译过):`TOPIC_MARKER_GOAL_AWARE` guard,pinned_goal 存在时 take 后丢弃跳过 insert。**待实测**。
|
||||
- [x] **G3 状态机收敛目标** ✅文档衔接:`conv_state.rs`/`mod.rs` 注释明示 pinned_goals 内容态与 ConvState 生命周期态正交,不进 enum。
|
||||
- [~] **G4 话题标记修正** ✅代码落地(编译过):`TOPIC_MARKER_GOAL_AWARE` guard,pinned_goals 非空时 take 后丢弃跳过 insert。**待实测**。
|
||||
- [x] **G5 附 bug** ✅已排查(不改代码):dump DB 确认 seq1(thinking 独白)+seq4(tool_result 内容)均 system+**早期 timestamp**(旧版本遗留脏数据),非当前 compress summary 机制问题(摘要四段式正常)。最新 system(seq0 topic marker)正常。**结论:历史脏数据,数据治理项**(可选清 DB),非当前 bug。anthropic_compat thinking 隔离你在进行中(M)。
|
||||
- [ ] **G1 目标刷新策略**(openQuestion):首版 None 才写,同对话改目标保留旧目标。待观察是否需 drift 刷新。
|
||||
- [x] **G1 多目标累积**:`pinned_goal: Option<String>` → `pinned_goals: Vec<String>`,每次发消息/编辑/强制发送提取目标追加到列表(去重),支持换目标且不丢失历史目标。`MAX_GOALS=5` 防无限膨胀。
|
||||
|
||||
> 落地开关(均可单点回退):`GOAL_PIN_ENABLED`/`GOAL_INJECT_BANNER`/`GOAL_MAX_CHARS=500`/`STALL_BREAKER_ENABLED`/`STALL_BREAKER_THRESHOLD=3`/`STALL_BREAKER_WARN_FIRST`/`TOPIC_MARKER_GOAL_AWARE`。全置 false 等价改动前。
|
||||
> 落地开关(均可单点回退):`GOAL_PIN_ENABLED`/`GOAL_INJECT_BANNER`/`GOAL_MAX_CHARS=500`/`MAX_GOALS=5`/`STALL_BREAKER_ENABLED`/`STALL_BREAKER_THRESHOLD=3`/`STALL_BREAKER_WARN_FIRST`/`STALL_BREAKER_GOAL_REMIND`/`TOPIC_MARKER_GOAL_AWARE`。全置 false 等价改动前。
|
||||
> 方案源:设计 workflow `wf_09a0385f-2ee` 综合裁决(system_prompt 字符串注入 > insert_at(0))。
|
||||
|
||||
## 对抗式评估接入(引擎已实现 → IPC + 前端打通)
|
||||
@@ -47,13 +47,13 @@
|
||||
|
||||
### 后端 — IPC 接入
|
||||
|
||||
- [ ] **A1 evaluate_idea 命令**:新增 IPC command,按 Provider 配置构造 `AdversarialEngine`(有 Provider → `new(provider)`,无 → `heuristic()`),调 `evaluate(idea)` 返回 `AdversarialEval`。
|
||||
- [ ] **A2 批量评估命令**:`evaluate_ideas_batch`,遍历多个 idea 调同一 engine 实例(复用 provider 连接)。
|
||||
- [ ] **A3 评估结果持久化**:评估结果写回 `ideas` 表(scores + evaluated_by + evaluated_at),避免每次重新评估。
|
||||
- [ ] **A4 类型导出**:`AdversarialEval` / `EvaluatedBy` / `Recommendation` 等类型经 IPC serde 暴露给前端(检查 `df-types` 是否需补充)。
|
||||
- [x] **A1 evaluate_idea 命令**:新增 IPC command,按 Provider 配置构造 `AdversarialEngine`(有 Provider → `new(provider)`,无 → `heuristic()`),调 `evaluate(idea)` 返回 `AdversarialEval`。
|
||||
- [ ] **A2 批量评估命令**:`evaluate_ideas_batch`,遍历多个 idea 调同一 engine 实例(复用 provider 连接)。**⏳ 待定**
|
||||
- [x] **A3 评估结果持久化**:评估结果写回 `ideas` 表(scores + evaluated_by + evaluated_at),避免每次重新评估。
|
||||
- [x] **A4 类型导出**:`AdversarialEval` / `EvaluatedBy` / `Recommendation` 等类型经 IPC serde 暴露给前端(检查 `df-types` 是否需补充)— 已验证:当前通过 `df_ideas::adversarial::*` 路径直接可用,前端经 `IdeaRecord.ai_analysis` JSON 字符串消费,无需搬运到 `df-types`。
|
||||
|
||||
### 前端 — 结果展示
|
||||
|
||||
- [ ] **B1 灵感详情页评估面板**:展示正方论点 / 反方论点 / 分析师综合评分,`evaluated_by` 标签区分(LLM / 启发式兜底 / 纯启发式)。
|
||||
- [ ] **B2 评估触发按钮**:灵感详情页加「深度评估」按钮,调 `evaluate_idea` IPC,loading 状态 + 错误提示。
|
||||
- [ ] **B3 评分维度可视化**:可行性 / 影响力 / 紧急度三维度展示(复用现有 score-bar 或引入 radar)。
|
||||
- [x] **B1 灵感详情页评估面板**:展示正方论点 / 反方论点 / 分析师综合评分,`evaluated_by` 标签区分(LLM / 启发式兜底 / 纯启发式)。
|
||||
- [x] **B2 评估触发按钮**:灵感详情页加「深度评估」按钮,调 `evaluate_idea` IPC,loading 状态 + 错误提示。
|
||||
- [x] **B3 评分维度可视化**:可行性 / 影响力 / 紧急度三维度展示(复用现有 score-bar 或引入 radar)。
|
||||
|
||||
Reference in New Issue
Block a user