Compare commits

2 Commits
Author SHA1 Message Date
lxy 0317873a5b 文档: 上下文管理设计/对标u-claw差距分析与看板登记
- 专项设计:M1-M4四批修复蓝图+对标u-claw六能力画像与P0-P2补差
- 看板:M1销账(验收证据)/收尾令/对标P0-P2条目登记
2026-10-19 00:25:52 +08:00
lxy eec5978fa7 修复: 上下文管理第二批(E5注入产物/E13超时收敛/E2降级透出)
- E5:[补充指令]与[系统恢复]同治三处——载入滤出/恢复回放剔除/
  首条任务消息排除(434c8c40占位实证),is_guide_artifact统一判据
- E13:同工具超时连败≥3跨签名聚合,执行前软收敛警告(治换档位调参
  逃逸,434c8c40 run_command 8连败实证)
- E2:压缩失败走关键词摘要兜底时新发AiContextDegraded,前端toast
  「上下文已降级」不再静默失忆(76730ce8 seq166实证)
- vite watch忽略target/(cargo编译写.o触发EBUSY杀dev进程)
- provider-list模板根div缺闭合补齐(拆分半成品断链)

验收:tool_failure 5/keyword_fallback 3/recovery 6 passed;vue-tsc EXIT 0
2026-10-19 00:25:51 +08:00
17 changed files with 432 additions and 22 deletions
@@ -0,0 +1,48 @@
# ai-chat 上下文管理与信息组织·完善设计(2026-10-18)
> 用户令:ai-chat agent 上下文管理/信息组织开发完善;回复别太口水话(2026-10-18)。
> 证据输入(问题全量论证在彼,此处只引编号不重复):`temp/深查-aichat对话质量复测-2026-10-17.md`(E1-E13)、`temp/深查-aichat信息UX-2026-10-17.md`(F1-F6)。
> 现状判定:10-04 三大病(上下文失控/反复压缩/终局缺失)基本治愈;残量集中在**恢复链污染、终局态半闭环、存量脏数据、信息可达性、输出纪律**五个结构性面。
## 一、信息流模型与缺口定位
链路:`发起(直发/inject) → 目标提取(pinned_goals) → 上下文组装(system+摘要+近期) → 执行循环 → 压缩(view/cache) → 恢复(recovery) → 投影(轮桶/轨迹卡) → 呈现(桌面/飞书)`
| 缺口 | 根因(结构性,非散点) | 归并的散点 |
|---|---|---|
| **G-A 恢复链身份未建模** | 恢复消息无类型标记,兼具「系统信号」与「用户输入」双重身份,被目标提取源再次吸收 | E1 套娃、E3 二次恢复、看板 649 行恢复目标完整性守卫 |
| **G-B 终局态半闭环** | boundary_json/loop_state 字段已建、写入闭环未建(填充率 0) | P2-5、E3 竞态窗口 |
| **G-C 写入不保数据语义** | 写入方各自为政,全量重写/双写不保留原义 | E11 created_at 重写污染 1750 条、E5 存量目标不迁移、E12 双写 |
| **G-D 信息可达性瓶颈** | 呈现层硬上限+折叠体内不可见 | F4-1 会话 50 上限(实测 115 条已击穿)、F1-3+F3-2 注入降级藏审批、F6-1 |
| **G-E 输出纪律缺失** | 聚焦准则管「答什么」,不管「怎么答」——主提示词无回复风格条款 | 用户口水话反馈(本次令);摘要提示词已有该纪律可对齐 |
| **G-F 阴性结论无守卫** | 工具 0 命中被当事实断言,无复核引导 | E6 假阴性 3 轮错判链 |
## 二、修复批次(领单视角,M1 先行)
### M1 · P1 上下文正确性
1. **E1+E3 恢复链治本(G-A)**:恢复消息加 source 标记(与看板 961② inject 标记同基建,复用 V52 source 字段);build_recovery_message 过滤 `[系统恢复]` 前缀目标;目标写入侧拒收同前缀。防二次恢复:快照全部目标=completed 时短路,或恢复后 N 分钟窗口禁重复。
2. **E11 created_at 保义(G-C)**:message_repo.rs:285 全量重写保留原 created_at(records 读取带出原值回写)。
3. **E6 阴性存疑(G-F)**:search_files/grep 0 命中时结果强制附「阴性存疑,建议复核路径/glob」标记(seq 1549 hint 雏形转正)。
4. **E4 跑题换装验证**:三源活跃度排序已在树(第五十三段),换装后重放 6a5a3e04 seq1802 场景验证。
### M2 · P2 上下文域
5. **E5 存量目标一次性迁移(G-C)**:active 目标命中「执行/搜索/查看+工具参数」形态或 `[系统恢复]`/`[补充指令]` 前缀 → 降级为非目标或重取首条任务消息。
6. **E2 压缩降级透出**:压缩失败兜底时前端状态条「上下文已降级」,禁静默。
7. **E13 超时连败收敛**:同命令超时连败 N 次转「换方案/问用户」分支(与看板 B5d 退避策略同族,合并实施)。
8. **P2-5 终局闭环(G-B)**:停靠/终局时填充 boundary_json/loop_state,不只落 status。
### M3 · P2/P3 信息组织域
9. **F4-1 会话可达性(G-D)**:列表 offset 游标分页+侧栏「加载更多」;搜索走后端 LIKE(title+消息内容)。
10. **F5-1+F5-2 飞书审批闭环(同根一改双收)**:决议词表归一(trim/剥标点/同义词,复用 normalize_halfwidth)+ 订阅 AiToolCallCompleted 推终态卡;pending 为空时决议回「已失效」提示而非静默放行。
11. **F1-3+F3-2 折叠体可达(G-D,同根)**:executing 判定改「桶内存在 running/pending 工具」;scrollToFirstPending 前按 data-trace-key 预展开含 pending 的轨迹卡。
### M4 · 输出纪律+UX 小批
12. **G-E 提示词输出纪律**:system_prompt_parts 行为准则追加「回复风格」段——结论先行直接给动作/结果;省略复述用户问题、客套开场、总结性收尾;要点优先于段落;用户没问的不展开。中英双语同步+测试断言(对齐 prompt.rs 既有测试范式)。注意别过度约束:需要解释时照常解释,反的是空话不是内容。
13. **UX 确定性小批**:F1-1 i18n 补键 / F1-2+F3-1 主题 token 化 / F2-1 aria-label / F4-2 loaded 标志 / F6-2+F6-3 Esc 关闭 / 看板尾部队列带图项标记(10-18 残留)。
## 三、验收口径
- **复测脚本复跑**(复用 `temp/复测-aichat质量-1017/` 脚本,timestamp 列口径):恢复消息套娃=0、created_at 污染增量=0、boundary_json 填充率>0(新停靠会话 100%)。
- **可达性**:115 条活跃会话全量可从侧栏到达;审批卡在任何折叠形态下 scrollToFirstPending 均可见。
- **输出纪律**:3 个真实会话抽查,开场复述/空收尾/无信息客套=0;结论仍在首屏。
- 每批销账附验收命令输出(G4 惯例)。
@@ -0,0 +1,53 @@
# 上下文管理对标 u-claw 与大批量并行差距分析(2026-10-19)
> 用户令(2026-10-18 飞书):①大批量 AI 任务调度/多会话多任务并行是未来常态核心场景;②战略目标=DevFlow 部署 Linux 替代 u-claw+Claude Code(CC 效率不够高、资源消耗太大);③第一门槛=上下文管理三命题:丢记忆、历史信息召回、高效利用 token;④方法=学习 u-claw 历史提交记录补 devflow 场景不足。
## 一、u-claw 上下文管理能力画像(源自主仓 6 条相关提交,核心是 86e9216)
| # | 能力 | u-claw 实现 | 要点 |
|---|---|---|---|
| 1 | **token 水位判据** | 140K≈200K 窗口 70% 主判据(可配),体积判据降兜底 | 治「换代过早」:350KB 体积≈真实水位 20-35%,体积判据致 23 代/日→水位判据 <5/日 |
| 2 | **usage 真值采集** | 读流解析 message.usage(input+cache_read+cache_creation),失败静默 0 | 水位判据的数据底座 |
| 3 | **大内容外置纪律** | system prompt 处理规则:大结论写盘 out/,回复只给路径+3 行结论 | 治上下文滚雪球(attach/ 86 文件 25MB 实锤) |
| 4 | **换代通知推送** | rollover 等会话层事件经渠道真推送(非仅日志) | 用户对会话滚动有感知 |
| 5 | **/history 归档检索** | 无参列最近 10 代,关键词 grep summary | 跨代记忆召回入口 |
| 6 | **归档检索指引** | sopHint 注明 jsonl 字段与 grep→读 summary 操作路径 | 让 AI 自己会查历史 |
另:0a4f8dd 重上下文估算(防图片堆积)、a3f28dc 群聊上下文增量注入(按决策价值治理进度卡)。
## 二、devflow 现状对照(三命题 × 已治/缺口)
### 命题 1:丢记忆
- ✅ 已治:目标语义重构(用户任务原文入 pinned_goals)、恢复链快照([系统恢复]/[补充指令] 产物双侧过滤,10-18 批)、压缩摘要四段式(意图/决策/文件/约束+失败事实保留)、E2 降级透出(10-18)
- ❌ 缺口:**boundary/loop_state 填充率 0**(终局态建模半闭环)——「代」的显式语义缺失,会话边界靠消息流推断
### 命题 2:历史信息召回
- ✅ 已治:单会话内压缩可召回(archive jsonl+召回句柄行)、知识路标投影进系统提示词
- ❌ 缺口:**跨会话检索无入口**(u-claw /history 等价物缺失)——「上次那个任务怎么弄的」只能靠人翻侧栏;消息全文搜索未做(F4-1 只做了标题+列表分页)
### 命题 3:高效利用 token
- ✅ 已治:压缩 memoization+决策冻结(10-04 病②治愈)、尾部追加摘要保前缀缓存、E13 超时连败收敛
- ❌ 缺口:
- **usage 真值采集未落地**(6 月 P1 敲定至今暂缓;cache_hit≈0% 说明 cache 口径有列无真值)——水位判据缺数据底座
- **压缩触发判据待查**:压缩/请求比 4.8 偏频,疑体积/轮次判据过敏感(对标 u-claw 病灶同型)
- **大内容外置纪律缺失**:工具输出有 truncate,但「大结论写盘+回复给路径」没有
### 大批量并行新命题(u-claw 亦未解,devflow 需原创)
- 多会话同时压缩互不感知(各自预算独立,全局 token 消耗无视图)
- 并行任务的信息投影(侧栏/仪表盘聚合 N 个会话的进行态)
- 调度面:优先级/抢占/停靠恢复已有底座(TaskBoundary/parked),缺**全局调度器**视角
## 三、补差优先级(P0→P2)
| 级 | 项 | 移植来源 | 落点 |
|---|---|---|---|
| **P0** | usage 真值采集(stream_options.include_usage 三口径) | u-claw #2 | 6 月 P1 设计直接复活;先有数据再谈判据 |
| **P0** | 压缩触发改 token 水位主判据(≈窗口 70%,体积兜底) | u-claw #1 | maybe_auto_compress 判据重构,依赖上项 |
| **P1** | 跨会话归档检索:/history 等价(关键词搜消息全文+摘要) | u-claw #5/#6 | ai_messages 全文索引(FTS5 或 LIKE 起步);检索指令+AI 检索指引 |
| **P1** | boundary/loop_state 写入闭环(代语义显式化) | — | 停靠/终局时填充;换代通知推送(u-claw #4 同款) |
| **P2** | 大内容外置纪律(system prompt 处理规则+out/ 目录约定) | u-claw #3 | prompt.rs 处理规则段+workspace 目录约定 |
| **P2** | 并行资源治理:全局 token 预算视图+多会话压缩协防 | 原创 | 调度器域,与 Linux 化路线(看板 10-18 远程容器方向)合流设计 |
## 四、与 Linux 化战略的衔接
上下文管理是第一门槛(用户拍板),Linux 化是最终形态。依赖链:**usage 真值 → 水位判据 → 代语义+归档检索 → 并行调度治理 → server 化无头运行**。前三步在现有 Tauri 架构内即可推进(P0 两项纯 Rust 域),后两步与远程容器/df-tunnel 线合流。建议节奏:P0 两项作下一批(devflow agent 领单),P1 随后,P2 与 Linux 化立项合并设计。
@@ -0,0 +1,132 @@
# DevFlow 布局样式大重构设计(2026-10-18)
> 依据:用户令「布局样式大重构,参考 D:\Downloads\u-agent」+「每页样式 DRY 提取统一定义」
> 输入:u-agent设计系统勘察-2026-10-18.md · 样式走查-1018/报告-布局全局审计.md · 报告-配色全局审计.md
> 原则:DRY 单一真值 · token 化 · 高密度(u-agent 13px 范式)· 双主题保留 · 紫 accent 品牌保留 · 视觉回归可控(dev 模式实时预览)
## 0. 现状诊断(三份勘察合并)
| 维度 | 现状 | u-agent 基准 | 差距 |
|---|---|---|---|
| 基准字号 | html 14px,aichat 已降 9-13,views 仍 11-14 | 13px 全局 | 功能页粗一档,密度断层 |
| 页头 | 三套并存(aichat 46px / Dashboard 18px 标题 / 功能页 24px h1 无固定高) | 46px 定高单行 | 全站统一为紧凑单行 |
| 侧栏 | 192px | 250-280px | 偏窄,功能页导航拥挤 |
| 按钮 | .btn 双基类分叉(global 34px vs settings 30px)+ 图标钮三档 | .cbtn 单基类 | 合一 30px |
| 徽章/tag | 圆角 3/4/6/8/10 五档并存;.df-tag 3px 字面量 | 小标签小圆角一档 | 收敛一档入 token |
| 圆角 token | 覆盖 75.5%,9 处 var 回退值写错(静默降档) | — | 修回退+字面量 97 处归 token |
| 间距 | 76.7% 收敛,7px×8/82px 手算对齐等离群 | 4px 刻度 | 离群归位+魔法数 token 化 |
| 字号 | 半像素 24 处(10.5/11.5/12.5)+17px 孤值 | 纯整数谱 | 归整 |
| 输入框 | 三档并存(21-22 / 26-27 / 30) | 30px 单档为主 | 行内搜索统一 26px 档 |
| 小节头 | 各页自造 | 9-10px uppercase mono 字距 统一范式 | 抽 .df-sec-h |
| 公共技法 | 无 | hairline 网格 kv 卡 / 发丝线行 / 染色两档 | 引入公共类 |
## 1. Token 层扩展(global.css :root,单一真值)
### 1.1 新增 token
```css
/* 字号谱(纯整数,对齐 u-agent) */
--df-fs-micro: 9px; /* 小节头/tag/计数 */
--df-fs-meta: 10px; /* 徽章/元数据 */
--df-fs-aux: 11px; /* 辅助文字/列表次级 */
--df-fs-ctrl: 12px; /* 控件文字/小按钮 */
--df-fs-body: 13px; /* 正文基准 */
--df-fs-title: 16px; /* 页头标题 */
/* 控件高度档 */
--df-h-btn: 30px; /* 主按钮(.btn 合一目标) */
--df-h-input: 26px; /* 行内输入/搜索 */
--df-h-modal-input: 30px;
--df-h-icon-btn: 26px; /* 图标钮统一档(22/26/28 收敛) */
/* 微内距暗刻度(chip/badge 族) */
--df-pad-chip: 1px 6px;
--df-pad-badge: 3px 8px;
/* 图标槽 */
--df-icon-gutter: 42px; /* ToolCard 图标槽(38/42 魔法数归一) */
/* 染色两档(语义色背景/边框透明度统一,双主题各配) */
/* 由 --df-*-bg(12% 底)+ 新增 --df-*-bd(30% 边)承担,消除散落 rgba 字面量 */
--df-success-bd / --df-warning-bd / --df-danger-bd / --df-info-bd / --df-accent-bd
```
### 1.2 修正既有错误
- 9 处 var 回退值写错(lg 8→10 / md 6→8 / xs 3→2):**删回退值**(token 必注入,回退是静默降档源)
- .df-tag 圆角 3px 字面量 → var(--df-radius-xs)
- body 基准 14px → 13px(line-height 1.5 保持);aichat 已对齐,功能页随页头波次跟进
## 2. 公共组件层(global.css/components.css 单一来源)
| 公共类 | 来源 | 定义去向 |
|---|---|---|
| `.btn` 族 | global+settings 双基类合一 30px/8px 16px/13px | global.css 唯一定义,settings.css 删副本 |
| `.df-sec-h` | u-agent 小节头范式(10px uppercase letter-spacing .5px mono tx3) | global.css 新增 |
| `.df-kv` / `.df-kv-i/k/v` | hairline 网格档案卡(gap:1px+bd 背景) | components.css 新增 |
| `.df-row` | 发丝线行(border-bottom+last-child none) | components.css 新增 |
| `.df-note` | 通知条(info/warning/danger 染色) | components.css 新增 |
| `.df-pill` | 状态胶囊(全圆+染色底) | components.css 新增 |
| `.df-tab` / `.df-tab--active` | 下划线 tab(2px accent 底线+计数 chip) | components.css 新增 |
| 既有 `.df-dot/.df-badge/.df-tag/.df-mono/.df-hstat` | 第五十四段已注入 | 保留,随 1.2 修圆角 |
## 3. 页面适配波(DRY 提取:各页 scoped 重复样式→公共类)
- **页头统一**:功能页 8 页 + Dashboard 全部改 `.page-header` 紧凑单行(h1 16px/500 + 定高 46px 等价 padding + header-actions gap 8px),删各页自造页头样式
- **侧栏 192→220px**(AppSidebar 一处)
- **输入框**:GitChanges/Provider 行内 21-22px → var(--df-h-input)
- **列表/详情页**:status-tag/status-* 徽章族(components.css 已有 13 个状态色)染边补齐 + 圆角统一;Details 页 kv 区改 .df-kv;tab 行改 .df-tab
- **settings 族**:settings.css .btn 副本删除并轨;面板内重复 card/row 样式提取
- **ai 族**(已对齐 workbench):仅扫尾——圆角字面量 9px 气泡入注释锚定、7px 代码块归 token、tag 圆角收敛
- **离群修复**:7px 间距→6/8、ProviderPanel 82px 手算对齐改结构、26px 图标位 calc、FilePreview outline-name 补 nowrap(确定 bug)、定宽输入/浮层补 max-width
## 4. 实施波次(文件零交集,逐波门禁 vue-tsc + 目检)
| 波 | 内容 | 文件面 |
|---|---|---|
| R0 底座 | token 扩展+回退修正+公共类新增+.btn 合一+body 13px | styles/global.css + components.css + settings/settings.css |
| R1 功能页族 | Projects/Tasks/Ideas/Knowledge/AuditLog 页头+密度+tag 收敛 | views/*.vue(5 页可再拆并行) |
| R2 详情页族 | ProjectDetail/TaskDetail/IdeaDetail(kv/tab/徽章/页头) | 3 个 Detail.vue 可并行 |
| R3 设置族 | settings.css 并轨+面板 DRY 提取 | components/settings/* |
| R4 ai 扫尾+杂项 | 圆角/token 扫尾+离群修复+FilePreview bug | components/ai/* + 杂项 |
| R5 配色收口 | 按配色审计报告优先级表 10 项实施(报告 temp/样式走查-1018/报告-配色全局审计.md):①幽灵 token 清零(含 --df-bg-secondary 等 6 处声明失效真 bug)②.status-* 徽章系 token 三件套③白叠层 64 处入 token(--df-hover/--df-zebra 浅色覆写黑叠层)④回退值 111 处全删⑤global.css 边框 rgba 改 color-mix⑥--df-neutral-bg⑦views 族 rgba 清零⑧--df-shadow-sm/md⑨alpha 收敛⑩豁免注释 | 按报告分布;R5 先补 token(叠层/中性/阴影)再批量替换 |
每波:前台同步代理实施 → 主代理 diff 核查 + vue-tsc → dev 窗口目检(用户可实时看)。R0 先行(其他波依赖新 token/类),R1-R4 在 R0 后并行。
## 5. 拍板点(按推荐值先行,用户可回改)
1. **基准字号 13px**(功能页降一档,信息密度对齐 u-agent;若觉偏小回 14 仅改一处 token 层 body 声明)
2. **侧栏 220px**(192→220 折中,非 250)
3. **页头 16px 标题+紧凑单行**(废 24px 大标题)
4. **按钮 30px 主档**(对齐 settings 既有 30px 而非 global 34px)
5. tag/badge 圆角统一 xs(2px)/sm(4px) 档
## 6. 单文件拆分轨([补4],与样式波错峰)
行数实测(2026-10-18):MessageList 3297 / ProjectDetail 1750 / TopBar 1700 / Tasks 1540 / ChatInput 1503 / IdeaDetail 1228 / ConversationSidebar 1226 / TaskDetail 1160 / AiChat 1145 / FilePreview 1144 / ProviderPanel 964 / FileExplorer 932 / ToolResultBody 895。
**拆分纪律**:
- 纯拆分不改行为——样式/逻辑原样搬迁,验收=vue-tsc 0 + 构建过 + dev 窗口目检无视觉回归
- 拆分边界按「区块自治」(模板区块+配套样式+相关逻辑成组),优先抽展示型子组件;共享状态经 props/emits 或既有 composable,不新造 store
- **与样式波互斥调度**:同一文件的样式适配与拆分不并行(样式先行,拆分在后——拆完再改样式要重新定位锚点)
- 拆分批次(样式波收口后启动,每批文件零交集):
- C1 ai 族:MessageList(3297→消息项/工具卡区/折叠条带等子件) → ChatInput(1503) → TopBar(1700)
- C2 项目族:ProjectDetail(1750) → FilePreview(1144) → FileExplorer(932)
- C3 任务/想法族:Tasks(1540) → TaskDetail(1160) → IdeaDetail(1228)
- C4 侧栏+设置族:ConversationSidebar(1226) → AiChat(1145) → ProviderPanel(964) → ToolResultBody(895)
- 每文件拆分目标:主文件 ≤600 行,子件单一职责
## 7. 整洁性轨(用户令 2026-10-18:代码整洁性全量处理+样式代码统一抽取,[补1] 共享的样式才抽取)
探针实测(dead-style-scan.json / dup-style-scan.json,脚本在 temp/样式走查-1018/):
- 死样式候选 368 处 / 56 文件(MessageList 34/ToolCard 25/TopBar 20/ActiveProjectsPanel 19 领衔);**假阳性规则**:Vue transition 生成类(xxx-enter/leave/from/to/active 由 `<transition name="xxx">` 隐式消费,非死)、动态拼接类(`'status-' + s`)、子组件透传类——删前须逐处验证
- 跨文件共享重复规则 49 条;典型双轨:`.ai-toast-*`(AiChat/ApprovalPopup)与 `.toast-*`(Knowledge/Settings)同义共存;`.confirm-actions`×3;`@keyframes spin/shimmer` 10/3 文件各写一份
**底座已落**(主代理亲码 global.css):`.df-toast` 四色族+过渡 / `@keyframes df-spin`/`df-shimmer` / `.df-confirm-actions`+过渡 / `.df-diff-pre` 族。
**D 批分路**(R5 收口后启动,文件族零交集,每路两件事:本族死样式验证删除 + 本族共享类消费替换):
- D-A ai 族+根组件(MessageList/ToolCard/TopBar/ConversationSidebar/ChatInput/ToolResultBody/AiChat/ToolCardList):死样式 100+ 处 + ai-toast 族消费 + spin/shimmer keyframes 消费 + .ai-tool-diff-pre 族并轨 .df-diff-pre
- D-B views+project 族(Knowledge/ApprovalPopup/Settings/Projects/ProjectDetail/Tasks/AuditLog/FilePreview/FileExplorer/GitChanges/ProjectCard):死样式 100+ 处 + .toast-* 族消费 + confirm-actions 消费
- D-C settings+layout+dashboard+knowledge 族(AdvancedSection/AllowedDirsPanel/UpdateBadge/ActiveProjectsPanel/KnowledgeDetail 等):死样式残余 + 确认弹层消费
- D4 TS/模板整洁(随各路顺带):未用 import/变量(vue-tsc noUnusedLocals 级)、console.log 残留、死模板分支——纯删不改语义
**验收纪律**:每路 vue-tsc 0 + grep 证据(死样式删除数/共享类消费数);transition 类假阳性必须 grep `<transition name=` 确认后再删。
+28
View File
@@ -756,3 +756,31 @@
961. [~] **桌面轮次交互定位重定义(2026-10-11 用户拍板,补7 三条)**:①轨迹卡片收敛:除主回复气泡外交互收敛到一张「有动态+可折叠」的轨迹卡(现有轮次折叠类型),**修缺陷:正生成时卡片展开不了**;②轮次语义纠正:现「压缩后即新轮次」定位错——正确定义=**一次信息发起=一个任务轮次,中途补充信息(inject)仍属同一轮,全部完成后新发起才是新轮**;③轮次编号稳定化:现编号不稳定(刚是 5 一会下面又出现 1)——应固定为「该对话的第 1 轮/第 2 轮」持久编号(与消息 seq/轮边界锚点绑定,不随视图压缩/折叠漂移)。 961. [~] **桌面轮次交互定位重定义(2026-10-11 用户拍板,补7 三条)**:①轨迹卡片收敛:除主回复气泡外交互收敛到一张「有动态+可折叠」的轨迹卡(现有轮次折叠类型),**修缺陷:正生成时卡片展开不了**;②轮次语义纠正:现「压缩后即新轮次」定位错——正确定义=**一次信息发起=一个任务轮次,中途补充信息(inject)仍属同一轮,全部完成后新发起才是新轮**;③轮次编号稳定化:现编号不稳定(刚是 5 一会下面又出现 1)——应固定为「该对话的第 1 轮/第 2 轮」持久编号(与消息 seq/轮边界锚点绑定,不随视图压缩/折叠漂移)。
- ✅ **①已修(2026-10-11)**:执行中 step 卡支持点击展开(复用 expandedTraceIds,key 与完成态 trace 同构,执行中展开→完成转 trace 态不丢;展开区实时渲染该轮过程工具卡)。 - ✅ **①已修(2026-10-11)**:执行中 step 卡支持点击展开(复用 expandedTraceIds,key 与完成态 trace 同构,执行中展开→完成转 trace 态不丢;展开区实时渲染该轮过程工具卡)。
- **②③设计要点(实施前拍板)**:轮边界判定锚点从「压缩摘要」改为「用户发起消息」——发起=桌面直发/飞书入站的 user 消息,补充=inject_runtime_message 写入的消息(需来源标记,建议复用 V52 source:{channel:"inject"} 与发起区分,归属判定 O(1));轮号=conv 内第 N 条「发起消息」序(数据派生,天然稳定,不随压缩/视图漂移),轨迹卡头/导航条/进度徽标统一取该口径(单一真值来源)。涉及:轮桶分桶逻辑(MessageList genBucketItems 上游)+轮号计算函数收敛+inject 消息 source 标记。 - **②③设计要点(实施前拍板)**:轮边界判定锚点从「压缩摘要」改为「用户发起消息」——发起=桌面直发/飞书入站的 user 消息,补充=inject_runtime_message 写入的消息(需来源标记,建议复用 V52 source:{channel:"inject"} 与发起区分,归属判定 O(1));轮号=conv 内第 N 条「发起消息」序(数据派生,天然稳定,不随压缩/视图漂移),轨迹卡头/导航条/进度徽标统一取该口径(单一真值来源)。涉及:轮桶分桶逻辑(MessageList genBucketItems 上游)+轮号计算函数收敛+inject 消息 source 标记。
# 产品方向定位:执行 agent 运行到远程容器(2026-10-18 用户口述,设计先行未实施)
- **定位**:DevFlow 未来形态=执行 agent 跑在远程容器(本机只做控制面/交互面),对齐 u-claw 分身实例形态——桌面端发起任务→远程容器内 agent 全权执行→结果回传。收益:长任务不占本机/环境隔离/多实例并行/24h 无人值守天然成立。
- **衔接存量**:df-tunnel/u-relay 线(v3 wire 协议+ws 隧道,已在树)正是传输底座;TaskBoundary 无人值守体系(边界内自主)是权限面底座;两者拼合即远程执行骨架。
- **待设计问题**(立项时展开):远程执行体的工具面裁剪(本机路径语义如何映射)/媒体与文件存储归属(远程落盘 vs 回传)/审批与边界在远程侧的执行点/凭据注入方式/断线重连与会话恢复/多容器池调度。
- **本条为方向登记,实施等用户令。**
- [ ] 队列带图项零标记(2026-10-18 走查残留):队列行渲染在 AiChat.vue:81-115,带图队列项无图标记;与 F3-2(ToolCardList scrollToFirstPending 预展开)同批处理。
### 💡 2026-10-18 ai-chat 上下文管理与信息组织完善(设计已定,按批领单)
> 用户令「上下文管理/信息组织开发完善+回复别太口水话」。蓝图:[02-架构设计/专项设计/ai-chat上下文管理与信息组织完善设计-2026-10-18.md](../02-架构设计/专项设计/ai-chat上下文管理与信息组织完善设计-2026-10-18.md);证据:temp/深查-aichat对话质量复测-2026-10-17.md(E系)+temp/深查-aichat信息UX-2026-10-17.md(F系)。M1 为 P1 先行,条目锚点/修法/验收全在设计文档,此处只列索引。
- [x] M1-P1① 恢复链治本:恢复消息 source 标记+[系统恢复]目标双侧过滤+防二次恢复(E1/E3;与 961② 同基建,可并 649 行旧条销账)——✅ 验收:cargo test -p devflow --lib recovery **6 passed**(zero_nesting_with_polluted_goals/all_polluted_falls_back/drops_recovery_artifacts_idempotent 等);conversation.rs:415 双侧过滤+单点拦截在位
- [x] M1-P1② message_repo 全量重写保留原 created_at(E11;污染存量 1750 条不回刷,止增量即可)——✅ 验收:cargo test -p df-storage **14 passed** 含 replace_conversation_preserves_created_at ok
- [x] M1-P1③ search_files/grep 0 命中「阴性存疑」标记转正(E6)——✅ 22:11 AI-Ops 子代理补齐:file.rs negative_hit_hint 共享函数+grep/search_files 两处接入;验收:negative_hit_hint_states_doubt_and_scope ok
- [~] M1-P1④ 跑题修复换装验证(E4;修复在树,换装后重放 6a5a3e04 seq1802 验证)——**已在工作区实施待验收**(prompt.rs list_active_with_activity 三源活跃度排序+指纹缓存对齐;验证方式:跑 dev 后重放场景)
- [ ] M2 存量目标一次性迁移/压缩降级前端透出/超时连败收敛(并 B5d)/boundary_json+loop_state 写入闭环(E5/E2/E13/P2-5)
- [ ] M3 会话 offset 分页+后端搜索/飞书审批决议词归一+终态卡/executing 桶内活性判定+审批预展开(F4-1/F5-1+2/F1-3+F3-2)
- [x] M4-a 提示词回复风格纪律(反口水话,中英双语+测试断言)——✅ 22:11 AI-Ops 子代理补齐:prompt.rs 中英两版行为准则各加「回复风格/Response style」条款(结论先行/省客套复述收尾/要点优先/没问不展开/「省的是空话不是内容」);验收:system_prompt_zh/en_has_response_style_clause 2 passed(共 5 passed)
- [ ] M4-b UX 小批(i18n 补键/token 化/aria/loaded/Esc/队列带图标记)——未动,留板
- [x] **收尾令(用户 2026-10-18 21:41 飞书下达到 AI-Ops,转登记)**:M1 销账后——①按 G3 提交本批改动并 push origin main(推送令已拍板,限本批范围);②启动 dev 模式运行验证(重点验 M4 反口水话与 M1 恢复链);③完成信息经飞书推送用户。若 AI-Ops 监测到 M1 已销账而本条未执行,AI-Ops 代执行。——✅ 23:0x AI-Ops 执行完毕:四笔分组提交(05d0f89 上下文P1批/302c4bc 飞书闭环/25ec9cf 前端令牌批/fb83339 文档)已 push(3bb662f..fb83339);验收 cargo test 2281 passed 0 failed+vue-tsc EXIT 0;dev 已跑(vite:1420 HTTP 200);完成信息已飞书推送。
### 💡 2026-10-19 上下文管理对标 u-claw·大批量并行差距(AI-Ops 分析,待领单)
> 用户战略令(10-18 飞书):DevFlow 上 Linux 替代 u-claw+CC,第一门槛=上下文管理三命题(丢记忆/历史召回/token 高效),大批量多会话并行是常态场景。详情:[02-架构设计/专项设计/上下文管理对标u-claw与大批量并行差距分析-2026-10-19.md](../02-架构设计/专项设计/上下文管理对标u-claw与大批量并行差距分析-2026-10-19.md)
- [ ] P0-A usage 真值采集:stream_options.include_usage 三口径(input/cache_read/cache_creation),6 月 P1 设计复活(当时暂缓,结构层已回退干净)
- [ ] P0-B 压缩触发改 token 水位主判据(≈窗口 70% 可配,体积兜底;u-claw 86e9216 同款,治压缩/请求比 4.8 偏频),依赖 P0-A
- [ ] P1-A 跨会话归档检索(/history 等价):消息全文搜索(FTS5 或 LIKE 起步)+检索指令+AI 检索指引进 sop
- [ ] P1-B boundary/loop_state 写入闭环(代语义显式化+换代通知推送)
- [ ] P2-A 大内容外置纪律(大结论写盘 out/+回复给路径,system prompt 处理规则)
- [ ] P2-B 并行资源治理(全局 token 预算视图/多会话压缩协防)——与 Linux 化/远程容器方向合流设计
@@ -270,7 +270,7 @@ pub(super) async fn maybe_auto_compress(
); );
// 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补: // 复位 is_compressing 收口进 keyword_fallback_commit 单锁(909①-补:
// 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。 // 原先此处单独取锁复位,与提交之间留并发二次压缩窗口)。
keyword_fallback_commit( let degraded = keyword_fallback_commit(
&session_arc, &session_arc,
&conv_id, &conv_id,
protect_start, protect_start,
@@ -281,6 +281,16 @@ pub(super) async fn maybe_auto_compress(
recall_on.then_some(data_dir.as_path()), recall_on.then_some(data_dir.as_path()),
) )
.await; .await;
// E2:降级不再静默(实证 76730ce8 seq166 用户被迫原句重发)
if degraded {
emit_ai_chat_event(
&app_handle,
AiChatEvent::AiContextDegraded {
conversation_id: Some(conv_id.to_string()),
reason: "压缩 provider 重建失败,已降级为关键词摘要".into(),
},
);
}
return; return;
} }
}; };
@@ -384,7 +394,7 @@ pub(super) async fn maybe_auto_compress(
"[ai] 自动压缩失败(后台),降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})", "[ai] 自动压缩失败(后台),降级走关键词摘要兜底(KEYWORD_FALLBACK_ENABLED={})",
KEYWORD_FALLBACK_ENABLED, KEYWORD_FALLBACK_ENABLED,
); );
keyword_fallback_commit( let degraded = keyword_fallback_commit(
&session_arc, &session_arc,
&conv_id, &conv_id,
protect_start, protect_start,
@@ -395,6 +405,17 @@ pub(super) async fn maybe_auto_compress(
recall_on.then_some(data_dir.as_path()), recall_on.then_some(data_dir.as_path()),
) )
.await; .await;
// E2:降级不再静默——AiError 语义是生成结束,此处 loop 仍在跑,
// 用专用事件让前端透出降级状态条/提示(不打断流)
if degraded {
emit_ai_chat_event(
&app_handle,
AiChatEvent::AiContextDegraded {
conversation_id: Some(conv_id.to_string()),
reason: "LLM 压缩失败,已降级为关键词摘要".into(),
},
);
}
} }
} }
}) // 闭 catch_unwind 内 async move }) // 闭 catch_unwind 内 async move
@@ -444,11 +465,11 @@ async fn keyword_fallback_commit(
log_head: &str, log_head: &str,
head_id: &Option<String>, head_id: &Option<String>,
recall: Option<&Path>, recall: Option<&Path>,
) { ) -> bool {
let inserted = { let inserted = {
let mut session = session_arc.lock().await; let mut session = session_arc.lock().await;
if !session.per_conv.contains_key(conv_id) { if !session.per_conv.contains_key(conv_id) {
return; return false;
} }
let conv = session.conv(conv_id); let conv = session.conv(conv_id);
// 939⑪ 内容版本守卫:上下文已被整体替换 → 丢弃兜底提交(标志亦不复位)。 // 939⑪ 内容版本守卫:上下文已被整体替换 → 丢弃兜底提交(标志亦不复位)。
@@ -458,7 +479,7 @@ async fn keyword_fallback_commit(
"[ai] {}兜底提交前上下文版本已变(疑似 clear 重建),丢弃", "[ai] {}兜底提交前上下文版本已变(疑似 clear 重建),丢弃",
log_head log_head
); );
return; return false;
} }
// 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。 // 复位与提交同锁原子(909①-补):消「锁外复位→再取锁提交」之间的并发二次压缩窗口。
conv.messages.set_compressing(false); conv.messages.set_compressing(false);
@@ -495,6 +516,7 @@ async fn keyword_fallback_commit(
log_head log_head
); );
} }
inserted
} }
#[cfg(test)] #[cfg(test)]
+1
View File
@@ -538,6 +538,7 @@ pub(crate) async fn run_agentic_loop(
.find(|m| { .find(|m| {
matches!(m.role, df_ai::provider::MessageRole::User) matches!(m.role, df_ai::provider::MessageRole::User)
&& !m.content.starts_with("[系统恢复]") && !m.content.starts_with("[系统恢复]")
&& !m.content.starts_with("[补充指令]")
&& m.content.chars().count() >= 4 && m.content.chars().count() >= 4
}) })
.map(|m| { .map(|m| {
@@ -419,6 +419,18 @@ fn is_recovery_artifact(text: &str) -> bool {
text.starts_with(RECOVERY_ARTIFACT_PREFIX) text.starts_with(RECOVERY_ARTIFACT_PREFIX)
} }
/// 注入产物判定(E5):[补充指令] 是运行中用户插话的通道标记,非任务意图本身
/// (实证 434c8c40 active goal=「[补充指令] 123 hi」占位)。与恢复产物同治:
/// 载入侧滤出内存,下轮 save 重写即自愈落库。
fn is_injection_artifact(text: &str) -> bool {
text.starts_with("[补充指令]")
}
/// 引导产物总判定(恢复+注入),双侧(载入/回放)统一用。
fn is_guide_artifact(text: &str) -> bool {
is_recovery_artifact(text) || is_injection_artifact(text)
}
/// 目标快照:按最终有效意图生成 parked 恢复引导消息。 /// 目标快照:按最终有效意图生成 parked 恢复引导消息。
/// ///
/// 「按快照续不重放历史目标流」:取 WorkingContext.goals 与 pinned_goals 中仍 Active 的 /// 「按快照续不重放历史目标流」:取 WorkingContext.goals 与 pinned_goals 中仍 Active 的
@@ -435,7 +447,7 @@ pub(crate) fn build_recovery_message(
let mut active: Vec<&str> = Vec::new(); let mut active: Vec<&str> = Vec::new();
for g in &working_context.goals { for g in &working_context.goals {
if matches!(g.status, GoalStatus::Active) if matches!(g.status, GoalStatus::Active)
&& !is_recovery_artifact(&g.text) && !is_guide_artifact(&g.text)
&& seen.insert(g.text.clone()) && seen.insert(g.text.clone())
{ {
active.push(&g.text); active.push(&g.text);
@@ -443,7 +455,7 @@ pub(crate) fn build_recovery_message(
} }
for g in pinned_goals { for g in pinned_goals {
if matches!(g.status, crate::commands::ai::GoalStatus::Active) if matches!(g.status, crate::commands::ai::GoalStatus::Active)
&& !is_recovery_artifact(&g.text) && !is_guide_artifact(&g.text)
&& seen.insert(g.text.clone()) && seen.insert(g.text.clone())
{ {
active.push(&g.text); active.push(&g.text);
@@ -504,7 +516,7 @@ pub(crate) fn parse_pinned_goals(raw: Option<&str>) -> Vec<crate::commands::ai::
Some(crate::commands::ai::GoalEntry::new(text)) Some(crate::commands::ai::GoalEntry::new(text))
} }
}) })
.filter(|e| !is_recovery_artifact(&e.text)) .filter(|e| !is_guide_artifact(&e.text))
.collect(), .collect(),
_ => vec![], _ => vec![],
} }
@@ -1961,4 +1973,18 @@ mod tests {
let cleaned = serde_json::to_string(&parsed).unwrap(); let cleaned = serde_json::to_string(&parsed).unwrap();
assert_eq!(parse_pinned_goals(Some(&cleaned)).len(), 1); assert_eq!(parse_pinned_goals(Some(&cleaned)).len(), 1);
} }
#[test]
fn parse_pinned_goals_drops_injection_artifacts() {
// E5:[补充指令] 是通道标记非任务意图(434c8c40「[补充指令] 123 hi」占位实证),
// 与恢复产物同治——载入侧剔除,下轮 save 重写自愈。
let polluted = serde_json::to_string(&vec![
crate::commands::ai::GoalEntry::new("[补充指令] 123 hi".to_string()),
crate::commands::ai::GoalEntry::new("升级工具链到最新版".to_string()),
])
.unwrap();
let parsed = parse_pinned_goals(Some(&polluted));
assert_eq!(parsed.len(), 1, "注入产物条目须剔除: {parsed:?}");
assert_eq!(parsed[0].text, "升级工具链到最新版");
}
} }
+5
View File
@@ -317,6 +317,11 @@ pub enum AiChatEvent {
/// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。 /// 摘要 system 消息已由后端 insert_at,后续 stream 自然带出,无需整会话刷新。
/// miniapp 仍插摘要气泡(对端发生压缩告知用户)。 /// miniapp 仍插摘要气泡(对端发生压缩告知用户)。
AiAutoCompressed { conversation_id: Option<String>, summary: String }, AiAutoCompressed { conversation_id: Option<String>, summary: String },
/// E2 压缩失败降级透出(2026-10-18):LLM 压缩失败走关键词摘要兜底时,loop 仍在继续
/// (不发 AiError——其 case 无条件 setStreaming(false) 会误判生成结束),但用户须知晓
/// 上下文已降级(实证 76730ce8 seq166:静默降级→AI 失忆→用户被迫原句重发)。
/// 前端弹一次性状态提示,不中断流。
AiContextDegraded { conversation_id: Option<String>, reason: String },
/// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21): /// L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):
/// ///
/// agent 断路器(§2.2)熔断 / 自省(识别"我反复失败")时发,区别于 prompt 教 AI /// agent 断路器(§2.2)熔断 / 自省(识别"我反复失败")时发,区别于 prompt 教 AI
@@ -25,6 +25,25 @@ struct FailureEntry {
last_error: String, last_error: String,
} }
/// 工具级超时连败计数(E13):治「同工具跨签名调参重试」逃逸——run_command 换 timeout
/// 档位即换 key,签名级拦截不触发,8 连败照烧轮次(434c8c40 实证)。此处按工具聚合:
/// 连续超时(错误特征匹配)≥3 次 → 该工具下次执行前注入软收敛警告(不硬拦,防误杀
/// 同工具的轻量调用),信息给足引导 LLM 换方案/问用户。该工具任一次成功即清零。
static TOOL_TIMEOUT_STREAKS: OnceLock<Mutex<HashMap<String, u32>>> = OnceLock::new();
/// 超时连败软收敛阈值(跨签名聚合口径)。
pub const TIMEOUT_STREAK_WARN_AFTER: u32 = 3;
fn timeout_streaks() -> &'static Mutex<HashMap<String, u32>> {
TOOL_TIMEOUT_STREAKS.get_or_init(|| Mutex::new(HashMap::new()))
}
/// 错误摘要是否属超时族(中英特征,大小写不敏感)。
fn is_timeout_error(error: &str) -> bool {
let lower = error.to_lowercase();
lower.contains("超时") || lower.contains("timeout") || lower.contains("timed out")
}
/// 进程级失败记忆(全 conv 共享单锁;锁内零 await,微秒级)。 /// 进程级失败记忆(全 conv 共享单锁;锁内零 await,微秒级)。
static FAILURE_MEMORY: OnceLock<Mutex<HashMap<String, FailureEntry>>> = OnceLock::new(); static FAILURE_MEMORY: OnceLock<Mutex<HashMap<String, FailureEntry>>> = OnceLock::new();
@@ -70,34 +89,50 @@ fn sort_json(v: &serde_json::Value) -> serde_json::Value {
} }
} }
/// 执行前检查:同 key 连败达标 → 拦截,返回结构化失败提示(LLM 可读换路引导)。 /// 执行前检查:同 key 连败达标 → 拦截;工具级超时连败达标 → 软收敛警告(不拦执行)。
/// 未达标/无记忆 → None 放行。 /// 两者都未达标/无记忆 → None 放行。
pub fn precheck(tool: &str, args: &serde_json::Value) -> Option<String> { pub fn precheck(tool: &str, args: &serde_json::Value) -> Option<String> {
let key = memory_key(tool, args); let key = memory_key(tool, args);
let mem = memory().lock().unwrap_or_else(|p| p.into_inner()); let mem = memory().lock().unwrap_or_else(|p| p.into_inner());
let Some(entry) = mem.get(&key) else { return None }; if let Some(entry) = mem.get(&key) {
if entry.consecutive >= ABORT_AFTER_CONSECUTIVE_FAILURES { if entry.consecutive >= ABORT_AFTER_CONSECUTIVE_FAILURES {
Some(format!( return Some(format!(
"{{\"status\":\"blocked_by_failure_memory\",\"tool\":\"{tool}\",\"consecutive_failures\":{},\"last_error\":\"{}\",\"hint\":\"同一调用已连败 {} 次,已自动拦截。请勿原样重试:①检查参数(路径/SQL/命令拼写) ②换工具或换路径 ③若确认无法绕过,向用户报告放弃原因。\"}}", "{{\"status\":\"blocked_by_failure_memory\",\"tool\":\"{tool}\",\"consecutive_failures\":{},\"last_error\":\"{}\",\"hint\":\"同一调用已连败 {} 次,已自动拦截。请勿原样重试:①检查参数(路径/SQL/命令拼写) ②换工具或换路径 ③若确认无法绕过,向用户报告放弃原因。\"}}",
entry.consecutive, entry.consecutive,
entry.last_error.replace('\"', "'"), entry.last_error.replace('\"', "'"),
entry.consecutive entry.consecutive
)) ));
} else {
None
} }
}
// E13 工具级超时连败(跨签名聚合):换参数/换档位逃不出工具维度。
let streaks = timeout_streaks().lock().unwrap_or_else(|p| p.into_inner());
if let Some(streak) = streaks.get(tool) {
if *streak >= TIMEOUT_STREAK_WARN_AFTER {
return Some(format!(
"{{\"status\":\"timeout_streak_warning\",\"tool\":\"{tool}\",\"timeout_streak\":{},\"hint\":\"该工具已连续超时 {} 次(不同参数/档位都超)。继续原路大概率仍超时:①把任务拆小分步执行 ②换更轻量的方式(降低数据量/改后台执行) ③若确实需要长时间运行,向用户说明并确认后再继续。\"}}",
streak, streak
));
}
}
None
} }
/// 执行后回写:成功清零删除,失败递增并记最近错误。 /// 执行后回写:成功清零删除(签名级+工具级超时计数),失败递增并记最近错误;
/// 失败属超时族时工具级超时连败 +1。
pub fn record(tool: &str, args: &serde_json::Value, ok: bool, error: Option<String>) { pub fn record(tool: &str, args: &serde_json::Value, ok: bool, error: Option<String>) {
let key = memory_key(tool, args); let key = memory_key(tool, args);
let mut mem = memory().lock().unwrap_or_else(|p| p.into_inner()); let mut mem = memory().lock().unwrap_or_else(|p| p.into_inner());
let mut streaks = timeout_streaks().lock().unwrap_or_else(|p| p.into_inner());
if ok { if ok {
mem.remove(&key); mem.remove(&key);
streaks.remove(tool);
} else { } else {
let e = mem.entry(key).or_insert(FailureEntry { consecutive: 0, last_error: String::new() }); let e = mem.entry(key).or_insert(FailureEntry { consecutive: 0, last_error: String::new() });
e.consecutive += 1; e.consecutive += 1;
e.last_error = error.unwrap_or_default().chars().take(200).collect(); e.last_error = error.unwrap_or_default().chars().take(200).collect();
if is_timeout_error(&e.last_error) {
*streaks.entry(tool.to_string()).or_insert(0) += 1;
}
} }
} }
@@ -145,4 +180,38 @@ mod tests {
let a = json!({"command": "cargo test", "working_dir": "E:/x"}); let a = json!({"command": "cargo test", "working_dir": "E:/x"});
assert!(memory_key("run_command", &a).contains("cmd=cargo test")); assert!(memory_key("run_command", &a).contains("cmd=cargo test"));
} }
#[test]
fn timeout_streak_aggregates_across_signatures_and_soft_warns() {
clear();
timeout_streaks().lock().unwrap().clear();
// 换档位=换签名(E13 逃逸路径:超时档拼在命令里即不同 key):签名级各 1 次均不拦;
// 工具级 streak 在第 3 次超时发生后达 3,同工具再调用(含当次迭代)即软警告
for (i, timeout) in ["60", "300", "600"].iter().enumerate() {
let args = json!({"command": format!("cargo build --timeout {timeout}")});
record("run_command", &args, false, Some(format!("命令执行超时({timeout}s)")));
if i < 2 {
assert!(precheck("run_command", &args).is_none(), "签名级 1 次且 streak<3 不拦");
}
}
// 新签名(第 4 档):工具级超时连败 ≥3 → 软收敛警告(放行执行但注入换路引导)
let args4 = json!({"command": "cargo build --timeout 900"});
let warn = precheck("run_command", &args4).unwrap();
assert!(warn.contains("timeout_streak_warning"));
assert!(warn.contains("\"timeout_streak\":3"));
// 同工具任一成功 → 工具级清零
record("run_command", &json!({"command": "ls"}), true, None);
assert!(precheck("run_command", &args4).is_none());
}
#[test]
fn non_timeout_failures_do_not_aggregate_to_timeout_streak() {
clear();
timeout_streaks().lock().unwrap().clear();
for path in ["/a.rs", "/b.rs", "/c.rs"] {
record("read_file", &json!({"path": path}), false, Some("文件不存在".into()));
}
// 3 连败但非超时族 → 无工具级警告
assert!(precheck("read_file", &json!({"path": "/d.rs"})).is_none());
}
} }
+3
View File
@@ -539,6 +539,9 @@ export type AiChatEvent = ({
} | { } | {
// 上下文已清空:前端刷新消息列表 + toast 提示 // 上下文已清空:前端刷新消息列表 + toast 提示
type: 'AiContextCleared' type: 'AiContextCleared'
} | {
// E2 压缩失败降级(关键词摘要兜底,loop 仍在跑):前端 toast 提示不中断流
type: 'AiContextDegraded'; reason: string
}) & { }) & {
conversation_id?: ConvId conversation_id?: ConvId
} }
+4
View File
@@ -698,6 +698,10 @@ async function initContextEventListeners(): Promise<void> {
onError: (_convId, message) => { onError: (_convId, message) => {
showToast(t('aiChat.compressError', { msg: message }), 'error') showToast(t('aiChat.compressError', { msg: message }), 'error')
}, },
// E2 压缩失败降级:仅提示不刷新不打断流(loop 仍在跑,实证静默降级致用户原句重发)
onContextDegraded: (_convId, reason) => {
showToast(t('aiChat.contextDegraded', { reason }), 'warning')
},
}) })
} }
</script> </script>
@@ -50,6 +50,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
+6
View File
@@ -46,6 +46,7 @@ async function initContextListener(callbacks: {
onCleared?: (conversationId: string) => void onCleared?: (conversationId: string) => void
onCompressed?: (conversationId: string, summary: string) => void onCompressed?: (conversationId: string, summary: string) => void
onError?: (conversationId: string, message: string) => void onError?: (conversationId: string, message: string) => void
onContextDegraded?: (conversationId: string, reason: string) => void
}): Promise<void> { }): Promise<void> {
setContextCallbacks({ setContextCallbacks({
onCompressing: () => { isCompressing.value = true }, onCompressing: () => { isCompressing.value = true },
@@ -62,6 +63,11 @@ async function initContextListener(callbacks: {
autoCompressedFlashTimer = setTimeout(() => { autoCompressedFlash.value = false }, 4000) autoCompressedFlashTimer = setTimeout(() => { autoCompressedFlash.value = false }, 4000)
}, },
onCleared: (convId) => callbacks.onCleared?.(convId), onCleared: (convId) => callbacks.onCleared?.(convId),
onContextDegraded: (convId, reason) => {
// E2:压缩失败降级为关键词摘要兜底——复位 loading + 上抛 toast(不中断 LLM 流)
isCompressing.value = false
callbacks.onContextDegraded?.(convId, reason)
},
onError: (convId, message) => { onError: (convId, message) => {
// AiError 也用于流式生成失败(useAiEvents 已处理);此处仅在压缩中时复位 + toast, // AiError 也用于流式生成失败(useAiEvents 已处理);此处仅在压缩中时复位 + toast,
// 避免双重处理流式错误。 // 避免双重处理流式错误。
@@ -30,6 +30,8 @@ export interface ContextCallbacks {
onCleared?: (conversationId: string) => void onCleared?: (conversationId: string) => void
/** 错误(AiError,含压缩失败):仅在 loading 时置 false + toast */ /** 错误(AiError,含压缩失败):仅在 loading 时置 false + toast */
onError?: (conversationId: string, message: string) => void onError?: (conversationId: string, message: string) => void
/** E2 压缩失败降级(AiContextDegraded):loop 仍在跑,仅提示上下文已降级不中断流 */
onContextDegraded?: (conversationId: string, reason: string) => void
} }
let _contextCallbacks: ContextCallbacks = {} let _contextCallbacks: ContextCallbacks = {}
@@ -344,6 +346,11 @@ export async function handleLifecycleEvent(event: AiChatEvent): Promise<boolean>
_contextCallbacks.onCleared?.(event.conversation_id || '') _contextCallbacks.onCleared?.(event.conversation_id || '')
return true return true
case 'AiContextDegraded':
// E2:压缩失败已走关键词摘要兜底,loop 继续——透出降级提示而非静默失忆
_contextCallbacks.onContextDegraded?.(event.conversation_id || '', event.reason || '')
return true
default: default:
return false return false
} }
+1
View File
@@ -275,6 +275,7 @@ export default {
// B4: one-shot auto-compress notice (loop auto-compress no longer fully silent) // B4: one-shot auto-compress notice (loop auto-compress no longer fully silent)
autoCompressed: 'Context auto-compressed (earlier messages folded into summary)', autoCompressed: 'Context auto-compressed (earlier messages folded into summary)',
compressError: 'Compression failed: {msg}', compressError: 'Compression failed: {msg}',
contextDegraded: 'Context degraded: {reason} — the AI may be missing some earlier details',
clearError: 'Clear failed: {msg}', clearError: 'Clear failed: {msg}',
initError: 'AI panel init failed: {msg} (reopen panel to retry)', initError: 'AI panel init failed: {msg} (reopen panel to retry)',
initDegraded: 'List load failed, continuing in degraded mode: {msg}', initDegraded: 'List load failed, continuing in degraded mode: {msg}',
+2
View File
@@ -276,6 +276,8 @@ export default {
// B4: 自动压缩完成一次性提示(loop 自动压缩不再全程静默) // B4: 自动压缩完成一次性提示(loop 自动压缩不再全程静默)
autoCompressed: '上下文已自动压缩(较早消息已折叠为摘要)', autoCompressed: '上下文已自动压缩(较早消息已折叠为摘要)',
compressError: '压缩失败:{msg}', compressError: '压缩失败:{msg}',
// E2:压缩失败走关键词摘要兜底,loop 继续——提示降级而非静默失忆
contextDegraded: '上下文已降级:{reason},AI 可能缺失部分早前细节',
clearError: '清空失败:{msg}', clearError: '清空失败:{msg}',
initError: 'AI 面板初始化失败:{msg}(重开面板可重试)', initError: 'AI 面板初始化失败:{msg}(重开面板可重试)',
initDegraded: '列表加载失败,面板降级继续:{msg}', initDegraded: '列表加载失败,面板降级继续:{msg}',
+3 -1
View File
@@ -23,7 +23,9 @@ export default defineConfig(async () => ({
} }
: undefined, : undefined,
watch: { watch: {
ignored: ["**/src-tauri/**"], // target/ 同理须忽略:cargo 编译写 .o/.rlib 时 watcher 触发 EBUSY(文件被写锁),
// chokidar 直接抛异常杀掉 dev 进程(2026-10-18 实证:watch sqlite3.o 崩溃)
ignored: ["**/src-tauri/**", "**/target/**"],
}, },
}, },
})); }));