文档: 看板销账与登记 + 设计档六份 + 索引补录
- 看板: 907 补注/909~941 登记与销账(含 929 模型停用根因与 4 处次生缺口、931 队列丢失、933/935/937 新需求、939 上下文瓶颈清单、941 C-r0 行为变更待拍板) - 归档: 全量完成态条目/段落 + 只读核验销账 4 段迁 todo归档/2026-10-10.md(机械搬运零措辞改动,未完成项零丢失) - 设计档: 输入框列表预览与自动编号 / 定时消息与待发箱持久化(统一 ai_outbox 底座) / 对话内定时延时任务 / 消息渲染管线重构(P0-P2 分期) / 上下文分层组装器落地方案(记账基点单一真值,与并行写者职责划清) / FileTree 大目录方案(推荐后端分页,否决虚拟滚动) / u-relay 运维手册 - 索引与事实修正: docs/INDEX crate 数 8→13 + 四子树补录 25 条 + 专项设计补录 27 条;纠正「项目无独立 ARCHITECTURE.md」等过时声明;统一 bun 口径 - CLAUDE.md: 新增「命名与代码风格」节(变量/方法名精简+准确;DRY·简洁·扩展性·高内聚低耦合·单一真值来源)
This commit is contained in:
+29
-2
@@ -4,7 +4,7 @@
|
||||
> 上级索引:[../INDEX.md](../INDEX.md)
|
||||
> 命名规范:见本目录 [文档记录规范-2026-06-14.md](./滚动规范/文档记录规范-2026-06-14.md) §九「文档命名规范」
|
||||
|
||||
本目录是 DevFlow 的**架构设计 / 设计决策 / 设计方案 / 构想**文档集合(不含实现流水,流水走 `PROGRESS.md`;不含审查报告,审查走 `05-代码审查/`)。
|
||||
本目录是 DevFlow 的**架构设计 / 设计决策 / 设计方案 / 构想**文档集合(不含实现流水,流水走 [`../07-项目管理/PROGRESS.md`](../07-项目管理/PROGRESS.md);不含审查报告,审查走 `05-代码审查/`)。
|
||||
|
||||
按**性质**分四类,每类一个子目录:
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
| 文档 | 状态 | 核心内容 |
|
||||
|---|---|---|
|
||||
| [业务系统设计-2026-06-12.md](./滚动规范/业务系统设计-2026-06-12.md) | 🏗 ARCHITECTURE 载体 | 项目无独立 ARCHITECTURE.md,本文档为实质载体:产品定位 / crate 结构 / 数据模型 / Phase 规划 |
|
||||
| [业务系统设计-2026-06-12.md](./滚动规范/业务系统设计-2026-06-12.md) | 📐 设计 | 产品定位 / crate 结构 / 数据模型 / Phase 规划。项目架构总纲见根 [ARCHITECTURE.md](../../ARCHITECTURE.md) |
|
||||
| [Phase1架构决策-2026-06-12.md](./滚动规范/Phase1架构决策-2026-06-12.md) | 📐 ADR | Phase 1 引擎骨架阶段架构级选型(ADR-001 引擎不绑定业务 等) |
|
||||
| [功能决策记录-2026-06-14.md](./滚动规范/功能决策记录-2026-06-14.md) | 📋 滚动 | 功能**需求规格 + 设计决策规格**(为什么这么定 / 要做什么)。最高频引用(67 入链) |
|
||||
| [功能决策记录-归档-2026-06-14.md](./滚动规范/功能决策记录-归档-2026-06-14.md) | 🗄 归档只读 | 从主文档归档:纯流水 / 老 Sprint 决策 / UX 微调 / 已被取代细节 |
|
||||
@@ -92,6 +92,33 @@
|
||||
| [测试落地实施方案-2026-07-01.md](./专项设计/测试落地实施方案-2026-07-01.md) | 📐 测试 | 各测试用例的落地方式(Rust 单元/集成/vitest/E2E 手动)、文件位置、Mock 策略、CI 自动化路径 |
|
||||
| [devflow集成三方IM-飞书企微钉钉Web-规划设计-2026-10-04.md](./专项设计/devflow集成三方IM-飞书企微钉钉Web-规划设计-2026-10-04.md) | 📐 规划(待评审) | 飞书/企微/钉钉/自定义Web 远程接入 AI Chat:新增 df-im-gateway(WS Commander)复用 u-relay+df-tunnel,桌面端零改动。含必要性分析(§三A)、破坏性分析(§三B)、演进预留(§十 S1-S4:多人/引擎上云/新端)。P0网关→P1 Web→P2飞书→P3钉钉→P4企微 |
|
||||
| [三方IM-Rust接入与SDK调研-2026-10-04.md](./专项设计/三方IM-Rust接入与SDK调研-2026-10-04.md) | ✅ 调研完成 | 三平台 Rust SDK 选型:飞书 larksuite-oapi-sdk-rs / 钉钉 dingtalk-stream / 企微 wecom-aibot-rust-sdk。**重大发现:企微智能机器人走 WS 长连接免公网回调,三平台统一形态**。含统一 adapter 映射表与 5 项落地风险清单 |
|
||||
| [上下文管理演进-任务技术设计-2026-07-20.md](./专项设计/上下文管理演进-任务技术设计-2026-07-20.md) | 📐 设计 | 上下文管理演进任务技术设计 |
|
||||
| [上下文管理演进-执行计划-2026-07-20.md](./专项设计/上下文管理演进-执行计划-2026-07-20.md) | 📐 计划 | 上下文管理演进执行计划 |
|
||||
| [上下文管理演进与发散思考-2026-07-20.md](./专项设计/上下文管理演进与发散思考-2026-07-20.md) | 📐 思考 | 上下文管理演进与发散思考 |
|
||||
| [aichat-plan-driven-设计-2026-08-01.md](./专项设计/aichat-plan-driven-设计-2026-08-01.md) | 📐 设计 | AI Chat plan-driven 模式设计 |
|
||||
| [devflow集成u-claw-可行性研究-2026-09-28.md](./专项设计/devflow集成u-claw-可行性研究-2026-09-28.md) | 📐 研究 | devflow 集成 u-claw 可行性研究 |
|
||||
| [跨会话记忆四层架构-设计-2026-09-29.md](./专项设计/跨会话记忆四层架构-设计-2026-09-29.md) | 📐 设计 | 跨会话记忆四层架构 |
|
||||
| [aichat上下文管理-派工与决策单-2026-09-29.md](./专项设计/aichat上下文管理-派工与决策单-2026-09-29.md) | 📋 决策单 | aichat 上下文管理派工与决策 |
|
||||
| [DRY技术债走查报告-2026-10-01.md](./专项设计/DRY技术债走查报告-2026-10-01.md) | 📋 报告 | 全库 DRY 技术债走查 |
|
||||
| [并行化与异步唤醒-设计-2026-10-04.md](./专项设计/并行化与异步唤醒-设计-2026-10-04.md) | 📐 设计 | 并行化与异步唤醒 |
|
||||
| [无人值守长任务-设计-2026-10-04.md](./专项设计/无人值守长任务-设计-2026-10-04.md) | 📐 设计 | 无人值守长任务 |
|
||||
| [上下文三层记忆-设计-2026-10-04.md](./专项设计/上下文三层记忆-设计-2026-10-04.md) | 📐 设计 | 上下文三层记忆 |
|
||||
| [跨会话召回最小闭环-设计-2026-10-04.md](./专项设计/跨会话召回最小闭环-设计-2026-10-04.md) | 📐 设计 | 跨会话召回最小闭环 |
|
||||
| [context-helpers拆分备勘-设计-2026-10-04.md](./专项设计/context-helpers拆分备勘-设计-2026-10-04.md) | 📐 设计 | context-helpers 拆分备勘 |
|
||||
| [namespace淘汰治理-设计-2026-10-04.md](./专项设计/namespace淘汰治理-设计-2026-10-04.md) | 📐 设计 | namespace 淘汰治理 |
|
||||
| [子代理完成卡已消化降级-设计-2026-10-04.md](./专项设计/子代理完成卡已消化降级-设计-2026-10-04.md) | 📐 设计 | 子代理完成卡消化降级 |
|
||||
| [uclaw两侧勘察补全-2026-10-04.md](./专项设计/uclaw两侧勘察补全-2026-10-04.md) | 📋 勘察 | u-claw 两侧勘察补全 |
|
||||
| [u-relay-wire-protocol-2026-10-04.md](./专项设计/u-relay-wire-protocol-2026-10-04.md) | 📐 协议 | u-relay 线协议规范 |
|
||||
| [数据存储架构-定稿-2026-10-05.md](./专项设计/数据存储架构-定稿-2026-10-05.md) | 📐 定稿 | 数据存储架构定稿 |
|
||||
| [知识库模型重审-精细化设计-2026-10-05.md](./专项设计/知识库模型重审-精细化设计-2026-10-05.md) | 📐 设计 | 知识库模型精细化重审 |
|
||||
| [飞书直连接入-形态修正与问题记录-2026-10-05.md](./专项设计/飞书直连接入-形态修正与问题记录-2026-10-05.md) | 📋 记录 | 飞书直连接入形态修正与问题记录 |
|
||||
| [版本更新与自动升级-设计-2026-10-09.md](./专项设计/版本更新与自动升级-设计-2026-10-09.md) | 📐 设计 | 版本更新与自动升级 |
|
||||
| [u-relay-运维手册-2026-10-10.md](./专项设计/u-relay-运维手册-2026-10-10.md) | 📖 手册 | u-relay 运维手册 |
|
||||
| [上下文分层组装器-落地方案-2026-10-10.md](./专项设计/上下文分层组装器-落地方案-2026-10-10.md) | 📐 方案 | 上下文分层组装器落地方案 |
|
||||
| [定时消息与待发箱持久化-设计-2026-10-10.md](./专项设计/定时消息与待发箱持久化-设计-2026-10-10.md) | 📐 设计 | 定时消息与待发箱持久化 |
|
||||
| [对话内定时延时任务-设计-2026-10-10.md](./专项设计/对话内定时延时任务-设计-2026-10-10.md) | 📐 设计 | 对话内定时延时任务 |
|
||||
| [消息渲染管线重构-设计-2026-10-10.md](./专项设计/消息渲染管线重构-设计-2026-10-10.md) | 📐 设计 | 消息渲染管线重构 |
|
||||
| [输入框列表预览与自动编号-设计-2026-10-10.md](./专项设计/输入框列表预览与自动编号-设计-2026-10-10.md) | 📐 设计 | 输入框列表预览与自动编号 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# FileTree 大目录卡顿 — 可实施方案
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 勘察: 只读实测锚点(Wave Q)
|
||||
> 定位: 「单个目录上千条合法条目」的卡顿风险可实施方案。本文只写方案,不改码。
|
||||
> 触发: Wave Q 实测 — 后端 `get_module_file_tree` 不封顶、前端 FileTree 全量 `v-for`,当前靠三重缓解兜底。
|
||||
> 结论预告: 推荐 **方案 B(后端 offset/limit 分页 + 前端「加载更多」)**,先做期 1 最小可用;虚拟滚动本期否决(见 §二·A 的实证疤痕)。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状与证据(带行号)
|
||||
|
||||
### 1.1 后端:列目录不封顶
|
||||
|
||||
`src-tauri/src/commands/module.rs` 命令 `get_module_file_tree`(707-834):
|
||||
|
||||
- 774-777 `std::fs::read_dir(&target_dir)` 遍历目标目录**全部**条目;
|
||||
- 781-791 过滤噪音目录(`is_noise_entry`,FileTree 档)+ 隐藏文件(`.` 开头);
|
||||
- 810-818 每条 push 进 `items`;
|
||||
- 820-824 排序(目录优先 + 字母序,稳定可预期);
|
||||
- 830-833 一次性返回 `{ path, entries }`。
|
||||
|
||||
**无任何条数上限**。对照同语义的另一档位:AI 侧 `list_dir_recursive`(`src-tauri/src/commands/ai/tool_registry.rs:923-965`)有 `max_entries` 封顶(调用处 `tools/file.rs:347` 传 `1000`,达上限返 `truncated`)。即"列目录"这一语义,**AI 上下文档有封顶、文件浏览器档没有** —— 这是 Wave Q 要补的缺口。
|
||||
|
||||
### 1.2 前端:单层全量 v-for
|
||||
|
||||
`src/components/project/FileTree.vue`:
|
||||
|
||||
- 25-33 `<li v-for="entry in entries" :key="entry.path">` —— 当前层全部条目**一次性渲染**;
|
||||
- 95-106 `FileTree` 自引用递归:展开的子目录再加一层同构 `<ul>`;
|
||||
- 157-205 `entries` 为该层全量(命中 `loadedChildren` 缓存则整层直赋)。
|
||||
|
||||
滚动容器是**共享**的 `.explorer-tree`(`FileExplorer.vue:806-813`,`overflow-y: auto`),**不是每层各自滚动** —— 这决定了虚拟滚动若要落地,需先把递归树拍平成"单一带缩进的可见行列表"(见 §二·A)。
|
||||
|
||||
### 1.3 三重缓解(既定兜底,也是本次要复用的底座)
|
||||
|
||||
1. **按层懒加载**:FileTree 仅在某目录被展开时才拉该层(`loadEntries` 174-205;`toggleDir` 208-237),从不整树加载。
|
||||
2. **噪音 / 隐藏过滤**:后端 `is_noise_entry`(FileTree 档,`tool_registry.rs:1045-1056`)+ `.` 隐藏文件(`module.rs:789`)剔除最大宗条目(`.git` 数千项、`node_modules`/`target`/`dist`/`__pycache__` 等)。
|
||||
3. **KeepAlive 保活**:`src/views/ProjectDetail.vue:83-85` `<KeepAlive>` 包 `FileExplorer`,切 tab 不销毁、不重挂载、不重拉。
|
||||
|
||||
附随机制:`loadedChildren` 缓存(`FileExplorer.vue:280`)+ 在途去重 / 请求序号(`loadSeq`/`inflightEntries`,`FileTree.vue:170-171`)使重复展开秒开、并发请求去重。
|
||||
|
||||
**残留风险**:三重缓解把"上千条"压缩成"**单层**上千条",但对**一个真实含上千条且非噪音**的目录(如日志目录、生成代码目录、扁平资源目录),后端仍全量返回 + 前端仍全量 DOM。
|
||||
|
||||
---
|
||||
|
||||
## 二、方案对比
|
||||
|
||||
| 维度 | A 前端虚拟滚动 | B 后端 offset/limit + 前端「加载更多」 | C 两者结合 |
|
||||
|------|---------------|--------------------------------------|-----------|
|
||||
| 机制 | 只渲染可视窗口(+ overscan) | 后端分页切片,前端累积 | B 分页 + A 窗口化 |
|
||||
| 后端改动 | 无 | `get_module_file_tree` 加 offset/limit/total | 同 B |
|
||||
| 前端改动 | FileTree 重构为扁平窗口化列表 | FileTree 累积 entries + 尾部按钮 | B + 窗口化 |
|
||||
| 依赖 | 需新库(项目现有依赖无)或自研 | 无 | 需新库 |
|
||||
| 与现有架构 | **冲突**(递归树需拍平;共享滚动容器需重排) | **天然契合**(每层本就独立请求) | 中 |
|
||||
| 风险 | 高(高度测量/展开态/选中态同步;项目有自研虚拟滚动翻车疤痕) | 低(改动集中、可回退) | 高 |
|
||||
| 适配量级 | 上万条 | **上千条(本场景)** | 上万条 |
|
||||
|
||||
### A 的风险:项目实证疤痕
|
||||
|
||||
自研虚拟滚动曾落地于消息列表(自研 IntersectionObserver + sentinel + ResizeObserver),因 **IO/RO 时序竞态致消息重叠**,2026-06-18 **彻底移除**(见 `docs/待决策.md:132-136`、`docs/02-架构设计/滚动规范/功能决策记录-2026-06-14.md:362-363`;`MessageList.vue` 至今标注"虚拟滚动已移除,恒渲染")。
|
||||
|
||||
并且 `package.json` 现有依赖**无任何虚拟滚动库**(`@vueuse/core` 也未安装)。再自研 = 重蹈覆辙;引库 = 新增依赖 + 仍需适配递归树拍平 —— 成本与"上千条"的收益不成比例。
|
||||
|
||||
### 推荐:B(最小可用)
|
||||
|
||||
理由:
|
||||
|
||||
1. **零架构冲突**:现有模型就是"每层一个独立请求 + `loadedChildren` 缓存",把"整层"切片成"整层分批"是该模型的自然延伸,不引入新范式。
|
||||
2. **最小 diff、低风险、可回退**(见 §三)。
|
||||
3. **量级匹配**:上千条 DOM 在现代浏览器渲染在数十 ms 量级;首屏限 200~300 条后交互即流畅,足以消除卡顿。无需为"上千条"上虚拟滚动的复杂度(A/C 属 YAGNI,仅当实测 >5000 条再评估)。
|
||||
4. **给用户明确信号**:「加载更多 (已加载/总数)」比"悄悄截断"更符合用户预期。
|
||||
|
||||
---
|
||||
|
||||
## 三、方案 B 改动面 / 风险 / 回退 / 验收
|
||||
|
||||
### 3.1 改动面(期 1 最小可用)
|
||||
|
||||
| 层 | 文件 | 改动 |
|
||||
|----|------|------|
|
||||
| 后端 | `src-tauri/src/commands/module.rs` | `get_module_file_tree` 加 `offset: Option<usize>`、`limit: Option<usize>` 参数;返回加 `total`。**先完整收集 + 过滤 + 排序,再切片**(切片点放在 820-824 排序之后),保证分页稳定。`limit` 缺省 = `None` → 返回全量(保持旧行为)。 |
|
||||
| 前端 API | `src/api/module.ts` | `getModuleFileTree` 加 `offset`/`limit` 参数;`FileTreeResult` 加 `total?: number`。 |
|
||||
| 前端组件 | `src/components/project/FileTree.vue` | `entries` 改为"累积注入":首屏拉 `limit` 条,`loadMore()` 按 `offset=已加载数` 追加,直到 `total`;层尾渲染「加载更多 (N/M)」;分页状态并入 `loadedChildren` 缓存(**建议其值由 `FileTreeEntry[]` 扩展为 `{ entries, total }` 或新增 `loadedTotals` map**,否则重展开丢失"还有更多"状态)。 |
|
||||
| i18n | `src/i18n/zh-CN/fileExplorer.ts`、`src/i18n/en/fileExplorer.ts` | 新增 `loadMore`(「加载更多」/「Load more」)、可选 `allLoaded`。 |
|
||||
|
||||
不改 `FileExplorer.vue`(缓存语义、展开 / 选中逻辑不变)。改动集中、职责单一。
|
||||
|
||||
### 3.2 风险
|
||||
|
||||
- **offset 错位**:目录内容在分页期间变化 → 可能重复 / 漏项。自用场景 + `refresh` 清缓存重拉(`resetTreeState`,`FileExplorer.vue:330-336`)已覆盖,可接受。
|
||||
- **多次请求 git status**:每个分片请求都重跑 `git_status_map_cached`,但有缓存(10s TTL),开销可控。
|
||||
- **`has_children` 开销**:现每目录一次 `read_dir` 短路统计;分页后只对返回的前 N 条目录计算,反而**降**开销。
|
||||
- **`loadMore` 与首屏请求竞争**:需让 `loadMore` 复用同一 inflight 去重键或使用独立键,避免与 `onMounted` 首屏请求互相覆盖(机制见 `FileTree.vue:185-188`)。
|
||||
|
||||
### 3.3 回退
|
||||
|
||||
- 后端 `limit`/`offset` 缺省 = `None` = 全量;前端不传 `limit` 即旧行为。
|
||||
- 或前端恒定不渲染"加载更多"(首屏即全量)。
|
||||
- 改动集中在 3 个源文件 + 2 个 i18n 文件,单次 revert 即可回滚。
|
||||
|
||||
### 3.4 验收口径
|
||||
|
||||
- **造数**:批量子项造 1500 条目录(如 1500 个空文件)。
|
||||
- 首屏 DOM 行数 ≤ `limit`(如 200);层尾显「加载更多 (200/1500)」;点击逐次追加 200 条直到 1500;加载完按钮隐藏。
|
||||
- **回归**:选中文件高亮、git 状态标、空目录标、展开子目录、切工程、刷新,均正常。
|
||||
- **命令**:`cargo check -p devflow` EXIT 0;分页切片纯函数加单测(`module.rs:1574` 已有 `mod tests` 模块可承接);`bunx vue-tsc --noEmit` EXIT 0;`node scripts/check-i18n.cjs` 通过。
|
||||
|
||||
---
|
||||
|
||||
## 四、分期(最小可用优先)
|
||||
|
||||
- **期 1(MVP,必做)**:后端点分页 + 前端「加载更多」按钮。
|
||||
- **期 2(体验,可选)**:触底自动加载(IO sentinel)。**注意**:滚动容器是共享 `.explorer-tree`(非每层独立滚动),触底自动加载需定位"当前层尾部的 sentinel",实现比按钮复杂;且只做"触底追加"、**不做"卸载裁剪"**,以规避历史 IO 竞态。可延后。
|
||||
- **期 3(仅当实测触发)**:若实测 >5000 条仍卡,评估虚拟滚动,且**用成熟库**(如 `@vueuse/core` `useVirtualList`)而非自研,并先把递归树拍平为单一带缩进列表。
|
||||
|
||||
---
|
||||
|
||||
## 五、与既有能力的复用
|
||||
|
||||
- **懒加载缓存**:`loadedChildren`(`FileExplorer.vue:280`,`FileTree.vue:180/197/228`)→ 分页后仍存"累积 entries",展开秒开语义不变。
|
||||
- **噪音过滤**:`is_noise_entry` FileTree 档(`tool_registry.rs:1045-1056`)→ 分页在**过滤之后**切片,过滤语义不变(不会因分页漏滤)。
|
||||
- **KeepAlive**:`ProjectDetail.vue:83-85` → 分页游标随组件状态在 tab 切换间保留,回来不重拉。
|
||||
- **在途去重 / 序号守卫**:`FileTree.vue:170-171/187` → `loadMore` 复用,避免并发覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 六、拍板点
|
||||
|
||||
1. **分页粒度**:首屏 `limit` 与每次追加步长各取多少?(建议各 200 —— 兼顾首屏速度与点击次数)
|
||||
2. **交互形态**:MVP 用「加载更多」按钮(推荐:简单、可测、与共享滚动容器不冲突)vs 触底自动加载(期 2 再评估)?
|
||||
3. **是否本期引入虚拟滚动**:建议否决(YAGNI + 自研疤痕),仅当实测 >5000 条再以成熟库评估 —— 是否认可?
|
||||
@@ -170,6 +170,24 @@ Commander(控制端) ──wss──> nginx(:443 TLS 终结) ──ws──> u-r
|
||||
| **Commander** | 用 pong 更新 `lastPongTime`,看门狗超时则重连 |
|
||||
| **Agent**(跑桌面/移动网络) | 用 pong 重置读死线,死线超时走既有重连 |
|
||||
|
||||
### 4.4 心跳间隔约束
|
||||
|
||||
心跳间隔不是自由值,**链路各层空闲超时中最小的一层决定其上界**:
|
||||
|
||||
| 层 | 空闲超时 | 说明 |
|
||||
|---|---|---|
|
||||
| nginx 反代 | `proxy_read_timeout`(样例 300s;生产实时配置可能更大) | 超时即静默断开 UPSTREAM WS,无任何错误帧 |
|
||||
| NAT / 防火墙 | 数十秒 ~ 数分钟(设备相关) | 空闲连接被回收,后续帧永久不可达 |
|
||||
|
||||
**约定**:间隔须**显著小于最短超时**并留足余量(容忍连续数次心跳丢失 / 网络抖动仍不断链),**推荐 30–60s**。低于 30s 徒增无效流量,超过 60s 在 300s 反代下余量偏薄。
|
||||
|
||||
| 端 | 保活帧 | 间隔 |
|
||||
|---|---|---|
|
||||
| Commander 参考(u-ppt) | 应用层 `{control_kind:"ping"}` | 30s |
|
||||
| Agent(df-tunnel) | WS 协议层 Ping 帧(对端协议栈自动回 Pong) | 30s |
|
||||
|
||||
> Agent 侧用协议层 Ping 即可保活——WS Ping/Pong 由对端协议栈(tungstenite/axum)自动应答,同样刷新反代空闲计时;应用层 `control_kind:"ping"` 主要用于需要显式 pong 观测的控制端看门狗。两端节拍取同值,避免一方看门狗口径与另一方保活周期错配导致误重连。
|
||||
|
||||
---
|
||||
|
||||
## 5. 部署配置
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# u-relay 运维手册(token 轮换 · 一通道一 agent · 心跳约束)
|
||||
|
||||
> 适用:u-relay 跨端中继的日常运维纪律。协议细节见
|
||||
> [u-relay Wire 协议规格](./u-relay-wire-protocol-2026-10-04.md),本手册只讲**运维动作与约束**。
|
||||
> 来源:协议评估遗留三项(单共享 token 可冒充 / 心跳间隔未定值 / 同 channel 多 agent 全投递)。
|
||||
> 本手册**不含**任何 token 值、主机地址等敏感信息——真值只在部署机 `/etc/u-relay.env` 与各客户端本地配置里。
|
||||
|
||||
---
|
||||
|
||||
## 1. 心跳间隔约束
|
||||
|
||||
心跳间隔须显著小于**链路最短空闲超时**(反代 `proxy_read_timeout`,配置样例 300s;生产实时值可能更大),
|
||||
以容忍连续数次心跳丢失仍不断链。**约定 30–60s**,各方实现见协议规格 §4.4:
|
||||
|
||||
- Agent(df-tunnel):`HEARTBEAT_INTERVAL` 常量,30s(WS 协议层 Ping)
|
||||
- Commander 参考(u-ppt):30s(应用层 `control_kind:"ping"`)
|
||||
|
||||
> 改反代 `proxy_read_timeout` 时须同步评估心跳间隔:若把超时压到 300s 以下,间隔也要相应收窄并保持「超时 / 间隔 ≥ 5」量级的余量。
|
||||
|
||||
---
|
||||
|
||||
## 2. 单共享 token 轮换 Runbook
|
||||
|
||||
### 2.1 现状与风险
|
||||
|
||||
u-relay 鉴权是**单一共享 token**(env `U_RELAY_TOKEN`,服务端常量时间比较),**不做 per-channel 隔离**:
|
||||
任何持有该 token 的客户端可声明**任意 `channel_id`** 冒充任一通道。故 token 属敏感凭据,
|
||||
泄露即等于整条中继失守;轮换是唯一的止损手段。
|
||||
|
||||
### 2.2 涉及方与各自持有位置
|
||||
|
||||
token 值散落在「服务端 + 多个客户端」,**没有集中下发通道**:
|
||||
|
||||
| 方 | 角色 | token 持有位置 |
|
||||
|---|---|---|
|
||||
| u-relay 服务端 | 鉴权源 | 部署机 `/etc/u-relay.env`(`U_RELAY_TOKEN`,权限 600)或 systemd unit 的 `Environment=` |
|
||||
| devflow 桌面端 | Agent | `relay_token` 设置项(运行时读,缺省回退硬编码默认值) |
|
||||
| devflow 小程序 | Commander | `apps/df-miniapp/src/config.ts` 缺省 + 设置页可改 |
|
||||
| devflow ask_uclaw | Commander | 设置页 `uclaw_token`(与 relay token 同源) |
|
||||
| u-claw | Agent/Runner | u-claw 侧配置 |
|
||||
| u-ppt | Commander | `relay.ts` 配置 |
|
||||
|
||||
> 新增任何接入端都要登记到本表——漏一端的后果见下节。
|
||||
|
||||
### 2.3 锁步成本(务必先读)
|
||||
|
||||
因为**服务端只认一个 token、客户端各自缓存**,轮换天然是一个**锁步切换**:
|
||||
|
||||
- **无重叠窗口**:服务端不能同时接受新旧两个 token,故不存在「灰度换端」的过渡期。
|
||||
- **顺序无解**:先改服务端 → 全部旧客户端立即 `auth_failed` 掉线;先改客户端 → 全部客户端在服务端切换前拿新 token 一样 `auth_failed`。两条路都有中断窗口。
|
||||
- **漏端即静默失联**:任一客户端未同步换 token,会持续 `auth_failed` 被拒连,表现为**该端静默离线**(无业务错误、仅 relay 日志有 `auth_failed`),排查成本高。
|
||||
- **不可逐端滚动**:无法像多 token 系统那样「先切一半、观察、再切一半」。
|
||||
|
||||
结论:**轮换是一次计划内的协调停机**,中断窗口 = 从「首个客户端改值」到「服务端重启加载新值」的间隔。执行要点是**把客户端改值的动作压到最短、紧接着重启服务端**。
|
||||
|
||||
### 2.4 执行步骤
|
||||
|
||||
前置:准备好新 token,确认上表**每一端**都有人能改、改完可验证。
|
||||
|
||||
1. **写服务端新值(先不重启)**:在部署机把 `/etc/u-relay.env`(或 unit `Environment=`)的 `U_RELAY_TOKEN` 改为新值,保持服务仍在跑旧值。此时新值尚未生效。
|
||||
2. **集中改用各客户端**:将上表每一端改到新值。此步开始后各端会 `auth_failed`,**中断窗口从这里起算**。
|
||||
3. **重启服务端加载新值**:`systemctl restart u-relay`。重启后已改端的客户端重连即通。
|
||||
4. **验证**:见 2.5。
|
||||
5. **未改端收尾**:对仍 `auth_failed` 的端补改(此时只有漏改者受影响,不再影响大局)。
|
||||
|
||||
> 若某些端的 token 是**编译期硬编码**(无运行时配置项),须提前把「新 token + 重新打包」的产物备好,否则第 2 步无法即时完成,窗口会被拉长。
|
||||
|
||||
### 2.5 验证
|
||||
|
||||
- 服务端:`systemctl status u-relay` 正常,`ss -tlnp | grep 9180` 在听。
|
||||
- 连接:拿新 token 走一次握手冒烟(`/ws/agent` 或 `/ws/commander`,收 `hello_ack` 即通)。
|
||||
- 日志:确认无 `auth_failed` 残留——有则说明还有端没换到新 token。
|
||||
|
||||
### 2.6 回滚
|
||||
|
||||
轮换失败(如某端无法改)时,**把服务端 env 回退到旧 token 并再重启一次**,回到轮换前状态;客户端已改的端随之 `auth_failed`,需一并回退。回滚与轮换同样是锁步中断。
|
||||
|
||||
### 2.7 根治方向
|
||||
|
||||
根治是**去掉单共享 token**:服务端改为 per-channel / per-client token(客户端拿自己的凭据只能绑自己的 channel),
|
||||
届时任一端轮换互不影响,锁步成本消失。在此之前,本 runbook 的锁步纪律是必须遵守的现状约束。
|
||||
|
||||
---
|
||||
|
||||
## 3. 运维纪律:一通道一 agent
|
||||
|
||||
### 3.1 现象与根因
|
||||
|
||||
u-relay 路由按 `channel_id` 配对,且**同 channel_id 允许多个 agent 并存、命令全投递**(Vec 索引,无去重、无选举)。
|
||||
即:同 `channel_id` 上跑两个 agent 实例(如两个 devflow 桌面端、或 devflow + u-claw runner 用了同一通道),
|
||||
一条 commander 指令会被**两端各执行一次**——写文件、跑命令、调外部接口全部重复,且两端事件回传互相交错。
|
||||
|
||||
### 3.2 危害
|
||||
|
||||
- 指令重复执行:副作用翻倍(重复落库 / 重复发消息 / 重复改文件),且难以察觉。
|
||||
- 事件串台:commander 收到两份进度/结果,流式 UI 错乱;两 agent 的上下文互相污染。
|
||||
- 隐性化:两端都「看起来正常工作」,问题只在副作用层面暴露,事后才追溯得到。
|
||||
|
||||
### 3.3 纪律
|
||||
|
||||
- **一通道一 agent**:每个 `channel_id` 在任一时刻**只允许一个 active agent**。通道不是「实例池」。
|
||||
- **启动前先查**:起第二个 agent 前,确认目标 `channel_id` 上没有在跑的 agent(换一个独立 `channel_id`,或用不同通道前缀)。
|
||||
- **换装/重启要收尾**:旧 agent 若未干净退出(进程被杀但连接残留),WS 连接可能存活到对端超时,期间新旧两连接同通道并存 = 短暂双投递。换装脚本须确认旧连接已断、或等旧连接超时后再起新实例。
|
||||
- **命名隔离**:不同用途用不同 `channel_id`(如按机器 / 按项目 / 按用户拆通道),从命名上杜绝复用。
|
||||
|
||||
### 3.4 排查手法
|
||||
|
||||
怀疑重复执行时:
|
||||
|
||||
- 看 relay 日志:同一 `channel_id` 是否同时有 >1 条 agent 连接。
|
||||
- 看 commander 侧:同一 `request_id` 是否收到两份 `result` / 事件重复。
|
||||
- 看副作用:文件 mtime、DB 记录、外部调用日志是否成对出现。
|
||||
|
||||
确认后,断开多余 agent 连接(停掉重复实例)即可恢复单投递。
|
||||
|
||||
---
|
||||
|
||||
## 附:关联
|
||||
|
||||
- 协议全貌:[u-relay Wire 协议规格](./u-relay-wire-protocol-2026-10-04.md)(§4 心跳、§5 部署、§3.3 路由)
|
||||
- u-relay 仓 README / 部署技能:中继二进制与 systemd 发布流程
|
||||
- 看板登记:`docs/todo.md`「新能力引入评估纪律」下三项 P3
|
||||
@@ -0,0 +1,265 @@
|
||||
# 上下文分层组装器 — 落地方案
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 勘察: 只读实测锚点
|
||||
> 定位: **《上下文根治设计-20261006.md》§一(L1/L2/L3)/§二(P1 瘦身)/§九(v1 锁定)/§十(检索面统筹)的落地细化**,不另立设计。
|
||||
> 覆盖 Wave 上下文走查实测五病灶:① `history_tokens` 与真实体量脱钩;② 每轮全量深克隆+展开 O(n²);③ 预算恒 128K;④ 裁剪后前缀缓存整体失配;⑤ L1/L2/L3 未落地、窗口外历史无召回。
|
||||
> 前置提醒: `crates/df-ai/**` 正由另一写者改动(P1 工具结果外置 WIP,见 §六),本方案明确与其重叠面与对接协议。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状勘察(锚点实测)
|
||||
|
||||
### 1.1 发送链路的真实数据流(五病灶的公共底座)
|
||||
|
||||
每一轮 LLM 请求的消息来源分四步,散落在两处:
|
||||
|
||||
| 步 | 锚点 | 动作 |
|
||||
|----|------|------|
|
||||
| ① 裁剪 | `crates/df-ai/src/context/mod.rs:185-273` `build_for_request(sys_tokens)` | 按 `history_tokens` 判预算,超则从**头部**淘汰轮次单元 |
|
||||
| ② 展开 | `src-tauri/src/commands/ai/agentic/mod.rs:1927-1944` | 逐条把 `namespace://` 引用**展开回真实内容** |
|
||||
| ③ 拼装 | `agentic/mod.rs:1945-1952` | 前置 system_prompt,尾部 `merge_volatile_tail`(`agentic/stream_relay.rs:789`) |
|
||||
| ④ 瘦身 | `agentic/mod.rs:1968-1988` → `stall_detector.rs:284-385` `summarize_tool_results` | view-only 摘要压缩大 tool_result(`extract_key_info`) |
|
||||
|
||||
### 1.2 五病灶逐条锚点
|
||||
|
||||
**① `history_tokens` 与真实体量脱钩(根因,最重)**
|
||||
|
||||
- 工具结果在 push 前先被引用化:`audit/mod.rs:1184-1191`,`should_use_namespace`(`namespace_store.rs:47-53`)命中 → `store()`(`namespace_store.rs:124-131`)入内存,**消息里只留短串 `namespace://tool/{hash}`**。
|
||||
- `ContextManager::push`(`context/mod.rs:101-143`)对这条短串跑 `estimate_message`(`context_helpers.rs:62-105`),`history_tokens += tokens` 在 `:108-110`。
|
||||
- 但**发送时**才在 `agentic/mod.rs:1927-1944` 展开成真实体量(单条 max 150KB,见 P1 注释 `context_helpers.rs:427-432`)。
|
||||
- 结果:`history_tokens` 记的是引用串(~30 字符),真实发送是全文。预算门 `context/mod.rs:198`(`history_tokens <= available`)与压缩门 `context_lifecycle.rs:133-135`(`available*6/10 < history_tokens`)**双双恒不触发** → 每轮全量重发 → 223:1。
|
||||
|
||||
**② 每轮 O(n) 全量克隆+展开+压缩 → 会话累计 O(n²)**
|
||||
|
||||
- 未超预算分支 `context/mod.rs:198-205` 走 `all_messages_clone()`(`:294-296`,逐条深克隆);
|
||||
- 展开 `agentic/mod.rs:1927-1944` 对**全部**历史逐条 `read_only` + `String` 克隆;
|
||||
- 压缩 `summarize_tool_results`(`stall_detector.rs:313-385`)再遍历全部消息做内容变换。
|
||||
- 三者每轮各 O(n),会话累计 O(n²)。**根因与①同源**:门失效 → 窗口永不收敛 → n 随会话单调涨。
|
||||
|
||||
**③ 预算恒 128K,与模型真实窗口无关**
|
||||
|
||||
- 主会话 `PerConvState::new`(`commands/ai/mod.rs:1241`)= `ContextManager::new(ContextConfig::default())`,`max_tokens=128_000`(`context_helpers.rs:128-136`),`budget_limit` 由它算(`:138-143`)。
|
||||
- 真实窗口在 `ModelConfig.context_window`(`crates/df-ai-core/src/model.rs:138-139`),但只在 router 用,而代码自陈该维度"恒过、失效"(`agentic/mod.rs:2012` 注释)。
|
||||
- 子代理同病:`sub_agent.rs:119` 也是固定 128_000。
|
||||
|
||||
**④ 裁剪后前缀缓存整体失配**
|
||||
|
||||
- 超预算时 `context/mod.rs:239-246` 切 `self.messages[trim_end..]`——头部被切,整个请求前缀位移,provider 自动前缀缓存(DeepSeek/Anthropic)整段失效。
|
||||
- 已有部分缓解:淘汰摘要改尾部追加(`context/mod.rs:259-261`)、`push_system_anchor` 避让悬空头(`:582-595`)、`tool_view_decisions` 冻结压缩决策防跨轮翻转(`stall_detector.rs:352-363`)。但**基础裁剪本身**仍位移前缀,且**淘汰摘要内容逐轮漂移**(被淘汰区单调增长,`:259` 每轮即时重生成)。
|
||||
|
||||
**⑤ L1/L2/L3 未落地 + 窗口外历史无召回**
|
||||
|
||||
- 组装是隐式的:system(L1 内容)由 `build_runtime_prompt`(`agentic/mod.rs:3042`)拼,历史由 `build_for_request` 拼,易变尾由 `merge_volatile_tail` 拼,无统一"组装器"对象、无可观测统计。
|
||||
- 检索仅覆盖知识库:`hybrid_search`(`knowledge_inject.rs:421`)→ `build_knowledge_context`(`:508`)→ `build_knowledge_context_for_conv`(`:587`);**窗口外对话历史与压缩摘要无处可召回**。
|
||||
|
||||
### 1.3 与既有设计的衔接
|
||||
|
||||
- 本方案 = 根治设计 §一 的接口化落地;`上下文三层记忆-设计-2026-10-04.md` 的"记忆卡"(C-m1)是 L3 的**记忆卡来源**,本方案不重复其产出逻辑,只提供召回入口。
|
||||
- §九 v1 锁定范围(P1 瘦身+P2 页池+P3 压缩常规化+P4 评估)不变;本方案细化 **P2**,并把 §九"唯一存储/唯一调度器/唯一召回入口"三收敛落到具体接口。
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与验收口径
|
||||
|
||||
**目标**
|
||||
1. **账实相符**:预算/压缩门依据的 token 数 = 实际发送视图的体量(单一真值来源)。
|
||||
2. **组装显式化**:L1 常驻 / L2 滑动窗口 / L3 检索召回 三段各有明确产出与稳定合并顺序;附带可观测统计。
|
||||
3. **前缀稳定**:稳态下请求前缀只追加、不位移;裁剪以批量滞后发生。
|
||||
4. **预算随模型**:上下文预算取自当前模型的 `context_window`,非恒定 128K。
|
||||
5. **可召回**:窗口外历史/记忆卡经统一检索面按需注入,复用 `knowledge_inject` 管线。
|
||||
|
||||
**验收口径**
|
||||
- 账实相符:`history_tokens` 与"发送视图 `estimate_message` 求和"偏差 ≤ 5%(构造含大 `namespace://` 引用的会话实测)。
|
||||
- 门生效:构造超预算会话,`context_trimmed`(`context/mod.rs:263`)按真实体量触发;压缩门 `context_lifecycle.rs:133` 在正确水位触发。
|
||||
- 前缀稳定:稳态连续 N 轮,请求消息数组的公共前缀长度 = 全量(无裁剪)或 = system 段(裁剪后仅在再裁剪时才变);provider 缓存命中率不再掉到个位数。
|
||||
- 预算随模型:`context_window=256K` 的模型下 `budget_limit` = 256K×0.85−8K 量级,不再恒 128K。
|
||||
- 可召回:窗口外某历史事实,仅凭关键词在下一轮被召回注入(带来源标注)。
|
||||
- 不倒退:单轮/短会话(未超预算)行为与改造前逐字节一致;`df-ctx-*` 开关全关 = 旧路径。
|
||||
|
||||
---
|
||||
|
||||
## 三、设计方案
|
||||
|
||||
### 3.0 核心判断
|
||||
|
||||
五病灶**同源于一个断裂**:"保留形态"与"发送形态"两套尺寸,门只看了前者。因此**不新增存储、不另立调度**——本方案只做三件事:
|
||||
1. 把"门所依据的尺寸"与"发送形态"对齐(**记账基点 = 发送视图尺寸**);
|
||||
2. 把隐式拼装收敛为一个显式**组装器**(三段产出 + 稳定顺序 + 统计);
|
||||
3. 把检索面从"仅知识库"扩为**注册表**(知识库 + 历史页 + 记忆卡同入口),落到 `build_knowledge_context_for_conv` 单点。
|
||||
|
||||
### 3.1 单一真值来源的 token 估值(治 ①,兼解 ②)
|
||||
|
||||
**两法对比**
|
||||
|
||||
| 法 | 做法 | 评价 |
|
||||
|----|------|------|
|
||||
| **A 记账点算"发送视图尺寸"** | 在内容产生点(及 reload)按 `view_form(msg)` 估 token——大工具结果按 `extract_key_info` 后的摘要+句柄尺寸入账 | O(1)/条、纯函数、与发送形态**同函数**(单一真值)、不依赖 namespace LRU 状态 |
|
||||
| B 估值器识别 ref 查真实大小 | `estimate_message` 遇 `namespace://` 前缀 → 查 store 取真实内容再估 | 破坏 `TokenEstimator` 纯函数性(需注入 store)、每轮 O(n) 查+扫(加重②)、引用被淘汰 / 重启后查不到 → 退回串长仍脱钩 |
|
||||
|
||||
**取 A**。理由:A 让"记账"与"发送"共用同一个纯函数,是真值来源唯一化的最短路径;B 把复杂度塞进每轮热路径且对淘汰态脆弱。A 单点落在估值函数,正是 `context_helpers.rs` 的职责域(与既有 `should_summarize_tool_result`/`extract_key_info` 同居一文件,DRY)。
|
||||
|
||||
**接口形态(新增,minimal)**
|
||||
|
||||
```rust
|
||||
// crates/df-ai/src/context_helpers.rs — 与 extract_key_info 同域,共享同一压缩逻辑
|
||||
/// 发送视图尺寸(记账唯一口径):与发送路径"展开→summarize_tool_results"后的形态对齐。
|
||||
/// 大 tool_result → extract_key_info 结果的 token;其余 → 内容 token。
|
||||
pub fn view_form_tokens(estimator: &TokenEstimator, content: &str, tool_name: Option<&str>) -> u32;
|
||||
|
||||
// crates/df-ai/src/context/mod.rs — push 的带尺寸覆盖入口(记账用)
|
||||
impl ContextManager {
|
||||
/// 同 push,但 token_count 用调用方给定的发送视图尺寸(工具名在产生点可得)。
|
||||
pub fn push_with_tokens(&mut self, message: ChatMessage, tokens: u32);
|
||||
}
|
||||
```
|
||||
|
||||
**改动点**
|
||||
- 产生点 `audit/mod.rs:1184-1191`:此处已握有真实 `content` 与 `draft.name`,算 `view_form_tokens(...)` 后走 `push_with_tokens`,取代 `push` 的 content 估值。删掉 push 的隐式 ref 估值这条唯一失真源。
|
||||
- 其余重估入口(`replace_tool_result_content` `context/mod.rs:332`、`replace_last_active_user_content` `:418`)同改走 `view_form_tokens`,保证单一口径。
|
||||
- **reload 一致性**:DB 存的是展开后的真实内容(`namespace_store.rs:3-4` 注释 / `agentic/mod.rs:1922`),故 `restore_from_messages`(`context/mod.rs:310`)须同样按 `view_form_tokens` 记账,否则"新会话(视图尺寸)≠ 重载(真实尺寸)"两态不一致。工具名从历史重建(`build_id_to_name`,`stall_detector.rs:300`)取得(见拍板点 2)。
|
||||
- **② 的化解**:① 修好后门下移到位 → 裁剪按真实体量发生 → 发送窗口收敛为常数 W → 每轮克隆/展开/压缩从 O(n) 降到 O(W),会话累计从 O(n²) 降到 O(n·W)。②无需单独"优化",随①自愈。
|
||||
|
||||
### 3.2 分层组装的接口形态(治 ②④⑤)
|
||||
|
||||
**现状**:拼接散在 `context/mod.rs:185-273`(窗口裁剪)与 `agentic/mod.rs:1945-1952`(system+尾)。改为一个**显式组装器**,产出结构化的三段 + 易变尾 + 统计:
|
||||
|
||||
```rust
|
||||
// src-tauri/src/commands/ai/agentic/assembler.rs (新文件;或收敛进现有 helpers.rs)
|
||||
/// 组装器产出:有序三段 + 易变尾 + 可观测统计。
|
||||
pub struct AssembledContext {
|
||||
pub resident: Vec<ChatMessage>, // L1:system 段(见 3.2.1)
|
||||
pub window: Vec<ChatMessage>, // L2:滑动窗口(三元组对齐的发送视图)
|
||||
pub recall: Vec<ChatMessage>, // L3:检索召回(user 角色,易变,固定置尾)
|
||||
pub volatile: Option<String>, // 既有易变尾(日期/清单/knowledge)
|
||||
pub trimmed: bool,
|
||||
pub stats: AssemblyStats, // 页命中/召回数/换出量/system 估算
|
||||
}
|
||||
|
||||
/// 合并顺序固定:resident → window → volatile → recall,全部只追加不位移前缀。
|
||||
pub fn assemble(ctx: &ContextManager, sys_tokens: u32, recall: Vec<ChatMessage>, volatile: Option<String>)
|
||||
-> AssembledContext;
|
||||
```
|
||||
|
||||
**合并顺序与缓存友好的三条不变式**
|
||||
1. **L1 resident 最前且为 loop 不变量**:`build_runtime_prompt`(`agentic/mod.rs:3042`)产出,pinned_goals/工作上下文/技能/DAG 均在此,整个 loop 文本不变 → 稳定缓存前缀最大块。
|
||||
2. **L2 window 追加为主**:`build_for_request` 的裁剪保留区(`context/mod.rs:243-246`)即窗口。三元组原子性沿用 `build_eviction_units`(`:612-642`)的单元边界,窗口切片必落单元边界。稳态只 push 追加,前缀不变。
|
||||
3. **L3 recall + volatile 严格置尾(role=user)**:召回复用 `merge_volatile_tail`(`stream_relay.rs:789`)的"末尾 user 承载"范式——易变内容不进缓存前缀。**召回绝不插进 window**(避免劈三元组、位移前缀)。
|
||||
|
||||
**与 `build_for_request` 的关系**:`build_for_request` 保留为 L2 的**纯函数内核**(签名与三元组逻辑不动),组装器在其上"包一层":调 `build_for_request(sys_tokens)` 取窗口 → 前置 resident → 追尾 volatile/recall → 填 `AssembledContext`。即"内核不改行为,组装器负责编排"。调用点 `agentic/mod.rs:1910-1953` 从内联拼装改为 `assemble(...)` 单调用。
|
||||
|
||||
**前缀稳定性(治④)**
|
||||
- **滞后水位批量裁剪**:trim 触发用高水位(`high = available`),一次裁到低水位(`low = available × k`,建议 k≈0.6),使裁剪**稀疏发生**;两次裁剪之间窗口 append-only,前缀稳定。替代现行"每轮才够就裁一单元"(`context/mod.rs:211-219`)。
|
||||
- **淘汰摘要冻结**:现行摘要每轮重生成致漂移(`:259`)。改为与 `tool_view_decisions` 同款**按内容哈希冻结**(首次生成后沿用),前缀稳定。
|
||||
- **随①自愈**:① 使门在正确水位触发,裁剪频次本就下降。
|
||||
|
||||
### 3.3 检索召回复用 knowledge_inject 管线(治⑤,兼§十统筹)
|
||||
|
||||
**现状单入口**:两个调用点(`agentic/helpers.rs:150`、`commands/chat.rs:398`)都经 `build_knowledge_context_for_conv`(`knowledge_inject.rs:587`),内部 `hybrid_search`(`:421`)+ `merge_hybrid_results`(`:468`)排序。
|
||||
|
||||
**改为注册表(§十"召回入口统一"的落地)**
|
||||
|
||||
```rust
|
||||
// src-tauri/src/commands/ai/knowledge_inject.rs — 扩为召回面
|
||||
type RecallItem = (df_storage::models::KnowledgeRecord, RecallKind); // 复用既有 record 形态
|
||||
enum RecallKind { Knowledge, HistoryPage, MemoryCard }
|
||||
|
||||
/// 各源实现同一检索签名;合并复用 merge_hybrid_results 的排序/去重/limit 语义。
|
||||
async fn recall(query, limit, ...) -> Vec<RecallItem> // 内部:知识库(既有) + 历史页(新) + 记忆卡(新)
|
||||
```
|
||||
|
||||
- **入口不动**:`build_knowledge_context_for_conv`(`:587`)与 `inject_knowledge_into_prompt`(`:633`)保持签名,内部从"只查知识库"扩为"查注册表全部源",输出**按源标注**(`[知识]`/`[历史]`/`[记忆卡]`)。调用点零变更。
|
||||
- **HistoryPageSource(v1 当前会话)**:query 复用末条 active user 文本(`knowledge_inject.rs:592-601` 已取);扫描当前 `conv.messages` 中 **!is_active(compressed/archived)** 与**窗口外**消息,关键词重叠排序取 top-K。**零新存储**——数据已在 `ContextManager.messages` 全量保留(`context/mod.rs:104-107`)。
|
||||
- **MemoryCardSource**:压缩摘要(`ChatMessage::system(&summary.nl_summary)`,`context_lifecycle.rs:294` 尾部追加)与 `上下文三层记忆` C-m1 的记忆卡,作为可召回条目注册。v1 可与 HistoryPageSource 合并实现。
|
||||
- **配额闸(§十原则4)**:召回页每轮 ≤K(建议 5),配额独立于知识注入既有上限,防信息炸弹。
|
||||
|
||||
### 3.4 预算随模型窗口(治③)
|
||||
|
||||
- 预算来源改为当前模型的 `ModelConfig.context_window`(`df-ai-core/src/model.rs:138-139`)——该值已在 `AiProviderRecord.model_configs`(`df-storage/src/models.rs:306+`,经 `deserialize_model_configs` 解析)可得,loop 顶部已持 `provider_config`。
|
||||
- 落地:`ContextManager` 增 `set_max_tokens` 或在派发前用模型窗口重建 `ContextConfig`;`sys_tokens` 估算点(`agentic/mod.rs:1115`)不变。
|
||||
- 缺省回退:当前模型无 `context_window`/为默认值时,沿用 128K(零回归)。
|
||||
|
||||
### 3.5 边界总览(三段职责)
|
||||
|
||||
```
|
||||
L1 resident ← build_runtime_prompt (agentic/mod.rs:3042) 常驻,loop 不变量,缓存前缀
|
||||
L2 window ← build_for_request (context/mod.rs:185) 滑动,三元组对齐,记账=view_form
|
||||
L3 recall ← recall registry (knowledge_inject.rs:587 扩) 按需,置尾,配额闸
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、分期(每期独立可上线、可回退)
|
||||
|
||||
### P2.1 — 真值 token 估值(治①,兼解②)【S,最先】
|
||||
- 新增 `view_form_tokens`(`context_helpers.rs`)+ `push_with_tokens`(`context/mod.rs`)。
|
||||
- 改 `audit/mod.rs:1191` push 站点与重估入口(`context/mod.rs:332`/`:418`)、reload 路径(`:310`)。
|
||||
- 开关 `df-ctx-accurate-tokens`(默认 **true**;false = 旧 content-based 记账,逐字节回退)。
|
||||
- 验收:账实相符 + 门生效两条。**此期不动发送内容,零行为变化。**
|
||||
|
||||
### P2.2 — 组装器结构化(治④,为 P2.3 铺路)【M】
|
||||
- 新增 `AssembledContext` + `assemble()`;`agentic/mod.rs:1910-1953` 内联拼装改单调用;加 `AssemblyStats` 日志/事件。
|
||||
- trim 滞后水位 + 淘汰摘要冻结。
|
||||
- 行为等价重构(除 trim 时机稀疏化,可单开关 `df-ctx-trim-hysteresis` 默认 true)。
|
||||
- 验收:前缀稳定 + 不倒退两条。
|
||||
|
||||
### P2.3 — L3 检索召回(治⑤)【M】
|
||||
- `knowledge_inject.rs` 扩为召回注册表 + HistoryPageSource(v1 当前会话)+ 源标注;配额闸 ≤K。
|
||||
- 开关 `df-ctx-recall`(默认 **off**,择机灰度开)。
|
||||
- 验收:可召回一条。
|
||||
|
||||
### P2.4 — 预算随模型窗口(治③)【S】
|
||||
- 接 `ModelConfig.context_window`;缺省回退 128K。开关 `df-ctx-model-window`(默认 true)。
|
||||
- 验收:预算随模型一条。
|
||||
|
||||
### P2.5 — 页池统一【北极星,触发制,不在本方案实施】
|
||||
- 触发条件:单会话版实测后仍有跨会话/内存痛点(§九问题2);或将 namespace_store(内存)与 tool_offload(磁盘)合流为唯一内容寻址页池(§九问题1 收敛)。
|
||||
- 本方案只**预留接口边界**(见 §六),不实施。
|
||||
|
||||
**顺序**:P2.1(最大杠杆、最小改动)→ P2.2(结构)→ P2.4(廉、高值,可与 P2.2 并行)→ P2.3。
|
||||
|
||||
---
|
||||
|
||||
## 五、影响面(五维)
|
||||
|
||||
- **兼容**:P2.1 只改记账,不改发送内容;老会话 reload 后按 `view_form_tokens` 重算(与新建会话口径一致,依赖 reload 路径同改)。P2.2 行为等价重构。P2.3/P2.4 均带开关,关=旧路径。无 schema 变更。
|
||||
- **破坏**:裁剪切点由"即时"变"滞后批量"(P2.2)——行为变更但语义等价(仍保三元组、仍有淘汰摘要);开关可退。无不可逆操作。
|
||||
- **安全**:检索召回仅扩"只读召回面",注入内容受既有配额闸约束;不引入新命令执行面。召回的历史页来自本会话已保留内容,无跨权限面(v1 单会话)。
|
||||
- **资源**:① 修正后 `history_tokens` 上升到真实量级,裁剪更积极 → **发送 token 下降**(目标 §一)。召回新增每轮 ≤K 页注入(可控)。无新持久化。
|
||||
- **回退开关**:`df-ctx-accurate-tokens` / `df-ctx-trim-hysteresis` / `df-ctx-recall` / `df-ctx-model-window`(均 KV 热读,默认值见各期)。观测:`AssemblyStats`(召回命中/换出量/window token/trim 次数)+ 既有 `context_trimmed` 日志。
|
||||
|
||||
---
|
||||
|
||||
## 六、与 P1 工具结果瘦身(另一写者)的边界与对接
|
||||
|
||||
**P1 现状(WIP,`git diff`)**:`crates/df-ai/src/lib.rs` 加 `pub mod tool_offload;`;`context_helpers.rs` 加 `READ_FILE_OFFLOAD_BYTES:432` / `TOOL_OFFLOAD_HANDLE_MIN_BYTES:436`,重构 `extract_key_info`(`:472`) 为"小文件豁免 + 超阈值压缩并落盘句柄"。消费点 `stall_detector.rs:342-351`、`sub_agent.rs`。
|
||||
|
||||
**职责二分(不双实现)**
|
||||
| 归属 | 内容 |
|
||||
|------|------|
|
||||
| **P1(内容变换)** | 大工具结果**如何变小**:`extract_key_info` / `offload_with_handle`(`tool_offload.rs:46`)/ `namespace_store` 引用化 |
|
||||
| **本方案(记账+组装)** | 变小后**占多少**、**发什么**、**如何召回**:`view_form_tokens` 记账基点、组装器、召回面 |
|
||||
|
||||
**对接协议(冻结接口)**
|
||||
1. **单一压缩函数**:本方案的 `view_form_tokens` 内部**复用 P1 的 `extract_key_info`**(同域同 crate),不另写压缩逻辑。P1 若改阈值/句柄格式,本方案自动跟随——**一处定义**。
|
||||
2. **单一存储形态(§九问题1)**:`namespace_store`(内存)与 `tool_offload`(磁盘)是**同一原文的两种介质**,构成双实现隐患。**本方案 v1 不碰二者**,只在 §3.3 召回面与 §二验收中把"结果页"抽象为内容哈希寻址(handle 与 ref key 均已是内容哈希:`namespace_store.rs:353` / `tool_offload.rs:37`)——为 P2.5 合流预留,不在 v1 制造第三套。
|
||||
3. **文件重叠**:`context_helpers.rs` 双方都动(P1 改 `extract_key_info`/常量;本方案加 `view_form_tokens`)。约定:本方案新增函数**不改 P1 既有函数签名**;`view_form_tokens` 以只读方式调用 `extract_key_info`。**串行合并**:P1 先合(其 `extract_key_info` 形态是本方案记账的输入),本方案在 P1 之上加记账点。
|
||||
4. **执行序**:P2.2/P2.3/P2.4 与 P1 无重叠文件(改 src-tauri,除消费 P1 常量),可并行;P2.1 与 P1 同文件,**待 P1 合入后落**。
|
||||
|
||||
---
|
||||
|
||||
## 七、拍板点清单
|
||||
|
||||
| # | 决策 | 选项 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 1 | 记账基点(单一真值来源) | A 发送视图尺寸(view_form,与发送同函数);B 保留内容/引用串尺寸 | **A**。B 无法消除账实脱钩;B 是当前病灶本身 |
|
||||
| 2 | reload 路径口径 | A 重建 id→name,reload 亦按 view_form(精确一致);B reload 按真实内容保守估(实现简,与新建会话口径不完全一致) | **A**。一致性是"单一真值"的前提;`build_id_to_name` 已存在可复用 |
|
||||
| 3 | 组装器落地形态 | A 新增 `AssembledContext` 结构 + `assemble()` 单入口;B 保留 `build_for_request` 返回 tuple,拼接仍分散 | **A**。B 延续"拼接分散",统计/顺序无处安放 |
|
||||
| 4 | L3 召回 v1 半径 | A 仅当前会话窗口外页(in-memory 关键词,零新存储);B 纳入跨会话/向量检索 | **A**。跨会话/向量是 §九 v2+ 北极星(触发制),v1 零新存储先证价值 |
|
||||
|
||||
---
|
||||
|
||||
## 八、与既有文档关系
|
||||
|
||||
- 本方案 = 《上下文根治设计-20261006.md》§一/§二/§九/§十的**接口细化与实施序**,不改其 v1 范围与北极星定位。
|
||||
- `上下文三层记忆-设计-2026-10-04.md`:其 C-m1 记忆卡是 L3 的 `MemoryCardSource` 数据源,本方案只提召回入口(§3.3),不重复卡片产出。
|
||||
- `namespace淘汰治理-设计-2026-10-04.md`:namespace_store 的 pin/EvictionLog/owner 治理不变;P2.5 页池统一时作为 RAM 层纳入。
|
||||
@@ -0,0 +1,284 @@
|
||||
# 定时消息与待发箱持久化 — 设计
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 勘察: 只读实测锚点
|
||||
> 覆盖需求: todo **935**(定时消息多条 + 统一「定时列表」)与 todo **931**(发送队列网络超时/重启不丢)。
|
||||
> 关联: todo **937**(对话内定时/延时任务)共用本设计调度器,仅预留扩展点,不在本设计实施。
|
||||
> 目标(用户原话转译): 消息可设为定时发送、可设多条,有一个统一「定时列表」可查看/改期/取消;发队列遇网络超时、杀进程重启都不能丢。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状勘察(锚点实测)
|
||||
|
||||
### 1.1 发送队列(931 病灶)
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 队列数据结构 | `src/stores/ai.ts:104`(类型)、`:136`(`queue: []` 初始化) | **纯内存模块级数组**,字段 `{ text, skill?, enqueuedAt, parts?, spans?, conversationId? }`,无任何持久化 |
|
||||
| 入队 | `src/composables/ai/useAiSend.ts:392-404` | `sendMessage` 查 `ai_is_generating` 得 `backendGenerating` → 忙则 `state.queue.push(...)`;上限 `QUEUE_LIMIT=10`(`:53`),超时 `QUEUE_TIMEOUT_MS=30_000`(`:56`) |
|
||||
| 续发派发 | `useAiSend.ts:308-353` `drainQueue`;监听器 `:607-612` `initDrainQueueListener` | **前端自驱**:`AiCompleted` → `useAiEvents` 发 `ai-drain-queue` → `drainQueue` 取出队首 `sendMessage` |
|
||||
| 显式清队 | `useAiSend.ts:193-196`(regenerate)、`:269-272`(editMessage)、`:488-495` `clearQueue` | 重生成/编辑属用户显式动作,清本会话队列合理;`notifyQueueCleared`(`aiShared.ts:772`)toast 提示 |
|
||||
| 队列 UI | `src/components/AiChat.vue:78-113`;`activeQueue` 计算 `:489` | 按 `activeConversationId` 过滤渲染当前会话队列,含 强制发送/清空/编辑/回填/立即发送/取消 |
|
||||
|
||||
**病灶(与 todo 931 勘察一致)**:①队列内存态,杀进程即丢;②后端 busy 判定 + 后续 `AiCompleted` 续发全依赖前端活着并收到事件——网络超时/事件竞态丢失则队列永久卡住或静默丢;③`state.queue` 无启动恢复入口。修法须从「内存 + 前端自驱」下沉到「持久化 + 后端自驱」。
|
||||
|
||||
### 1.2 后端发送受理链(定时复用的正典入口)
|
||||
|
||||
| 件 | 锚点 | 语义 |
|
||||
|----|------|------|
|
||||
| `ai_chat_send` IPC | `src-tauri/src/commands/ai/commands/chat.rs:636` | 拦截 `/status` → 解析前导技能 → 委派 `spawn_agentic_session` |
|
||||
| `spawn_agentic_session` | `chat.rs:796`(`pub(crate)`) | **发送正典核心**:解析 provider → 目标 conv 入参优先/缺省 active/都无则懒建 → `can_accept_request` 门禁(仅 Idle\|Error 可接,拒 Generating/Stopping/Compressed)→ 写 stop_flag/language/iteration/model_override → spawn `run_agentic_loop`。`pin_active` 决定是否劫持用户活跃面板 |
|
||||
| generating 判定 | `chat.rs:471` `ai_is_generating` | 读无锁 `state.conv_states.is_active(conv)` |
|
||||
| 完成出口(单点) | `src-tauri/src/commands/ai/agentic/mod.rs:2882` `emit_ai_completed_once` → 调 `unattended::notify_batch_done(conv_id)`(`:2917`) | **全部正常收尾汇聚于此单点**,是「会话转空闲」的唯一可靠信号 |
|
||||
|
||||
结论:`spawn_agentic_session` 是后端可无前端直接调用的发送入口(无人值守引擎已这么用),定时到点可直接走它,无需前端在场。
|
||||
|
||||
### 1.3 后端常驻调度器先例(可直接抄形态)
|
||||
|
||||
`src-tauri/src/commands/ai/unattended.rs`:
|
||||
- `start(app)`(`:505` 起)用 `tauri::async_runtime::spawn` + `tokio::time::interval` + `tokio::select!{ interval.tick(), notify.notified() }` 起常驻循环;周期/开关/范围全部**从 `Settings KV` 每轮热读**(`df-unattended-poll-secs` 等)。
|
||||
- 派发走 `spawn_agentic_session(..., Some(new_id()), ..., false)`(`:847-857`):**独立后台会话 + `pin_active=false`**,不劫持用户活跃面板。
|
||||
- `notify_batch_done`(`:483`)在完成出口清在途标志并 `notify_one` 唤醒轮询器。
|
||||
- 单会话单在途双保险:内部 `in_flight` 标志 + `can_accept_request` 兜底清残留(异常退出漏钩子时不永久卡死)。
|
||||
- 挂载点 `src-tauri/src/lib.rs:84` `commands::ai::unattended::start(app.handle().clone())`。
|
||||
- 通知出口:托盘 `notify_session_event`(`commands/ai/notify.rs:47`,`NotifyKind::Completed/Failed/NeedAction`);飞书 `feishu_notify::push_card/push_monitor_alert`,目标 KV `df-monitor-chat-id`(`unattended.rs:52`)。
|
||||
|
||||
### 1.4 生命周期(「应用关闭后是否生效」的事实)
|
||||
|
||||
- `lib.rs:51` `.setup()`:开库+迁移(`AppState::init`)→ `app.manage` → 各 `spawn`(feishu、`unattended::start`、mcp、proxy gateway、tray)。
|
||||
- `lib.rs:614-658` `on_window_event`:`CloseRequested` → **`api.prevent_close()` + `window.hide()` 托盘化,进程不退出**;真正退出只走托盘菜单「退出」(或系统关机)。
|
||||
- 无 `RunEvent::Exit` 级关闭钩子(默认 `.run()`)。
|
||||
|
||||
**关键事实**:窗口关闭 → 托盘常驻 → **进程仍在,后端 tokio 调度器照常跑**;只有进程真退出(托盘退出/关机)调度器才停。故「应用关闭后是否生效」的精确答案是:**关窗(托盘化)期间生效;进程退出期间不生效,重启后按追赶策略补发**——这是必须写进验收口径的边界。
|
||||
|
||||
### 1.5 存储与迁移底座
|
||||
|
||||
- 迁移版本链 `crates/df-storage/src/migrations.rs`,当前 **V50**;`migrations_helpers::{column_exists, table_exists}` 提供幂等探测。
|
||||
- Repo 两种范式:`impl_repo!` 宏(`crates/df-storage/src/crud/mod.rs`,生成 insert/get_by_id/list_all/update_full,适合固定 CRUD);`SettingsRepo`(`crud/settings.rs`)手写 KV(`app_settings` 表,key/value JSON),适合标量偏好。
|
||||
- 会话记录 `AiConversationRecord`(`crates/df-storage/src/models.rs:372`)已有 `loop_state`(V49)/`status`(V50)等生命周期列先例。
|
||||
- 前端会话 CRUD:`ai_conversation_create`(`src/api/ai.ts:318`)、`ai_conversation_list`(`:323`)——定时目标会话可在 composer 处先建。
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与验收口径
|
||||
|
||||
**功能目标**
|
||||
1. Composer 旁「定时」入口:把当前输入(文本/技能/图片)设定为**某绝对时间**发送;可连续设多条。
|
||||
2. 统一「定时列表」:跨会话查看全部定时项,支持 查看/改期/取消/编辑文本;到点自动发送。
|
||||
3. 待发队列**持久化**:入队/出队/取消即落库;网络超时不清队列(仅标记该条可重试);杀进程重启后队列仍在且可续发。
|
||||
4. 定时到点时会话忙 → 顺延到会话空闲再发(不丢、不打断当前生成)。
|
||||
5. 失败可见:连续失败到上限 → 标失败 + 托盘通知(可选飞书),用户可在列表重试/取消。
|
||||
|
||||
**验收口径**
|
||||
- 断网/超时:构造后端生成中入队 → 断网触发错误 → 队列项仍在(标记可重试),恢复网络后续发。
|
||||
- 重启:入队 N 条 → 杀进程 → 重启 → 队列恢复 N 条且自动续发;`sending` 残留被复位为 `pending`。
|
||||
- 定时:设 1 分钟后发送 → 关窗托盘化 → 到点仍在后台发出(session 落库可见);设 1 分钟后发送 → 立刻进程退出 → 重启 → 按追赶策略补发或标记过期。
|
||||
- 多会话:定时项按目标会话独立排队,不误发到当前活跃会话;到点会话忙则顺延。
|
||||
- 不倒退:无定时/无队列时,普通发送链路(L0/L1/L2)行为与改造前逐字节一致。
|
||||
|
||||
---
|
||||
|
||||
## 三、设计方案
|
||||
|
||||
### 3.0 核心判断:统一为后端「待发箱(outbox)」
|
||||
|
||||
931 队列与 935 定时消息本质是**同一实体、不同触发条件**:队列 = 「会话空闲即发」,定时 = 「到点且空闲即发」。因此不建两套调度,统一落一张表 + 一个后端调度器:
|
||||
|
||||
- **触发条件统一为 `due_at`**:队列项 `due_at = NULL`(尽快);定时项 `due_at = epoch_ms`。调度器每次取「`due_at` 已到 且 目标会话空闲」的项派发。
|
||||
- **单一真相源**:表是真相,前端 `state.queue` 退化为该会话 outbox 的**只读视图**,前后端各一套派发逻辑的「草率聚合」不再存在。
|
||||
- **调度落点 = 后端 tokio 常驻**(抄 unattended 形态),复用 `spawn_agentic_session` 正典链 + `emit_ai_completed_once` 空闲钩子。
|
||||
|
||||
### 3.1 数据结构(新表 `ai_outbox`,迁移 V51)
|
||||
|
||||
选**独立表**而非 KV:`app_settings` 是标量偏好,读改写整块 JSON 无索引、并发改期/取消会互相覆盖;outbox 需按 `status+due_at` 索引扫描、按 id 逐条 CAS 更新,表是对的解。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS ai_outbox (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL, -- 目标会话(composer 处确定;无则前端先建)
|
||||
kind TEXT NOT NULL DEFAULT 'queue', -- 'queue'(尽快) | 'scheduled'(定时);预留 'task'(937)
|
||||
text TEXT NOT NULL,
|
||||
skill TEXT, -- /技能名
|
||||
model_override TEXT, -- 用户指定模型(null=路由)
|
||||
parts_json TEXT, -- ContentPart[] JSON(图片;空/NULL=纯文本)
|
||||
spans_json TEXT, -- MentionSpan[] JSON
|
||||
due_at INTEGER, -- epoch ms;NULL=尽快(queue)
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending|sending|sent|failed|canceled
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
sent_at INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_outbox_due ON ai_outbox(status, due_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_outbox_conv ON ai_outbox(conversation_id, status);
|
||||
```
|
||||
|
||||
迁移:V51 = 建表 + 两索引(`table_exists` 幂等守卫)。**可逆**:DOWN 等价 = `DROP TABLE ai_outbox`(纯新增表,不影响旧数据;回退即弃表 + 关调度器开关)。**务必按项逐条落状态标记,禁依赖全局内存推算**(V21 教训:`docs/todo.md:860` 全局 COUNT 幂等窗口中途崩溃致永久残缺)。
|
||||
|
||||
Rust 记录(加 `crates/df-storage/src/models.rs`):
|
||||
|
||||
```rust
|
||||
pub struct AiOutboxRecord {
|
||||
pub id: String,
|
||||
pub conversation_id: String,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
pub skill: Option<String>,
|
||||
pub model_override: Option<String>,
|
||||
pub parts_json: Option<String>,
|
||||
pub spans_json: Option<String>,
|
||||
pub due_at: Option<i64>,
|
||||
pub status: String,
|
||||
pub attempts: i64,
|
||||
pub last_error: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub sent_at: Option<i64>,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 存储层 Repo(手写,不走 `impl_repo!`)
|
||||
|
||||
`crates/df-storage/src/crud/outbox_repo.rs`(新)。因需状态 CAS 与条件更新,手写(同 `SettingsRepo` 范式):
|
||||
|
||||
- `insert(&record) -> id`
|
||||
- `list_by_conv(conv_id, include_done: bool) -> Vec<AiOutboxRecord>`
|
||||
- `list_due(now_ms, limit) -> Vec<AiOutboxRecord>`(status='pending' AND (due_at IS NULL OR due_at<=now) ORDER BY created_at)
|
||||
- `claim(id) -> bool`:CAS `UPDATE ... SET status='sending', updated_at=? WHERE id=? AND status='pending'`(changes==1 才 true,防并发双取)
|
||||
- `mark_sent(id)` / `mark_failed(id, err)` / `revert_pending(id, attempts, err)`
|
||||
- `reschedule(id, due_at)` / `cancel(id)` / `update_text(id, text)` / `clear(conv_id?)`
|
||||
- `reset_stale_sending()`:启动把残留 `sending` 复位为 `pending`(崩溃半途,镜像 unattended 的 `in_flight` 泄漏处置)
|
||||
|
||||
AppState 增 `pub outbox: OutboxRepo`(`state.rs`,`AppState::init` 内 `OutboxRepo::new(&db)`),与 `settings`/`conv_states` 并列。
|
||||
|
||||
### 3.3 后端调度器 + IPC(新文件 `src-tauri/src/commands/ai/outbox.rs`)
|
||||
|
||||
**挂载**:`lib.rs` setup 增 `commands::ai::outbox::start(app.handle().clone())`(与 `unattended::start` 并列);启动即先 `outbox.reset_stale_sending()`。
|
||||
|
||||
**循环**(抄 unattended 形态):
|
||||
|
||||
```
|
||||
start(app): spawn loop {
|
||||
select!{ interval.tick(), notify.notified() } // 周期兜底 + 事件即时唤醒
|
||||
if !enabled(KV) continue
|
||||
for item in list_due(now, 32) { // 按 created_at 排序
|
||||
if dispatched_this_tick(conv) continue // 同会话同轮只派一条(串行保序)
|
||||
if !conv_states.can_accept_request(conv) continue // 忙 → 本轮跳过,下次再取(不丢)
|
||||
if !claim(item.id) continue // CAS 失败(他轮已取)
|
||||
match spawn_agentic_session(app, state, item.text, None, item.skill,
|
||||
item.model_override, Some(conv), parts, spans, false) {
|
||||
Ok(_) => { mark_sent(item.id); emit("ai-outbox-sent", {id, conv}); mark_dispatched(conv) }
|
||||
Err(e) => { // 仅 spawn 前置失败(provider未配/会话忙竞态)
|
||||
if permanent(e) { mark_failed(item.id, e); notify(...) }
|
||||
else { revert_pending(item.id); emit("ai-outbox-changed"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键语义**
|
||||
- `pin_active=false`:定时/队列的自动发送不劫持用户活跃面板(同无人值守);用户切到该会话仍能看到实时流(AiTextDelta 已按 conv 路由)。
|
||||
- **「完成即唤醒」钩子**:`agentic/mod.rs:2917` 现有 `unattended::notify_batch_done(conv_id)` 旁并列加 `outbox::notify_conv_idle(conv_id)`——会话一空闲就唤醒调度器续发,替代前端 `ai-drain-queue` 自驱。
|
||||
- **失败面界定**:`spawn_agentic_session` 返回 `Ok` 即「受理成功」,outbox 立即标 `sent`;LLM 轮次内的失败(流断/工具错)由既有 loop 自愈+错误气泡+看门狗处置,**不回流 outbox**(避免与既有韧性机制重叠)。outbox 的 `failed` 只覆盖**受理失败**(provider 未配、永久 4xx 特征串如 404/额度耗尽——复用 `classifySendError` 同源判据的 Rust 版)。
|
||||
- 重试:`revert_pending` 保留下轮再试(忙场景天然重试);`attempts` 达 `df-outbox-max-attempts`(默认 3)记 `failed` + 通知。
|
||||
- **过期追赶**:调度器只认「`due_at <= now`」,故进程退出期间到期的项在重启后立即命中(补发)。是否对「迟到过久」的项改标过期见拍板点 4。
|
||||
|
||||
**IPC 面**(注册进 `lib.rs` `invoke_handler`):
|
||||
- `ai_outbox_enqueue(conversation_id, text, skill, model_override, parts, spans, due_at?) -> id`(`due_at` 空=队列项)
|
||||
- `ai_outbox_list(conversation_id?, include_done?) -> Vec<AiOutboxItem>`
|
||||
- `ai_outbox_reschedule(id, due_at)` / `ai_outbox_cancel(id)` / `ai_outbox_update_text(id, text)`
|
||||
- `ai_outbox_send_now(id)`(插队=force;后端 CAS claim 后立即派发)
|
||||
- `ai_outbox_clear(conversation_id?)`
|
||||
|
||||
**事件面**:`ai-outbox-changed`(任何变更后前端刷新视图)、`ai-outbox-sent { id, conversation_id }`、`ai-outbox-failed { id, conversation_id, error, permanent }`。
|
||||
|
||||
**通知出口**:失败经 `notify_session_event(app, NotifyKind::Failed/NeedAction, conv, ...)` 托盘补位;需要飞书时复用 `feishu_notify::push_card` + KV `df-monitor-chat-id`(与 unattended 同款,零新通道)。
|
||||
|
||||
**KV 配置**(`app_settings`,热读):`df-outbox-enabled`(默认 true,回退开关)、`df-outbox-poll-secs`(默认 10,下限钳 5)、`df-outbox-max-attempts`(默认 3)、`df-outbox-notify-feishu`(默认 false)。调度器另用 `Notify` 由 enqueue/完成钩子即时唤醒,周期 tick 仅兜底 `due_at` 到点。
|
||||
|
||||
### 3.4 前端改造(队列视图化 + 定时入口)
|
||||
|
||||
**`src/composables/ai/useAiSend.ts`**
|
||||
- `sendMessage` L1 分支(`:392-404`):`state.queue.push(...)` → `await aiApi.outboxEnqueue(...)`,随后刷新本会话视图。
|
||||
- 删前端派发:`drainQueue`(`:308`)、`initDrainQueueListener`(`:607`)、`ai-drain-queue` 监听改由 `ai-outbox-changed`/`ai-outbox-sent` 驱动的**视图刷新**;`useAiEvents` 的 `AiCompleted` 不再发 `ai-drain-queue`。
|
||||
- 保留 `cancelQueued/clearQueue/editQueued/sendQueuedNow/tryForceSend`,后端代理为对应 IPC。
|
||||
- regenerate/editMessage 的「清本会话队列」改为 `ai_outbox_clear(convId)`(语义不变:用户显式动作允许清)。
|
||||
|
||||
**新增 `src/composables/ai/useAiOutbox.ts`**:`state.queue` 保持为「本会话待发视图」数组(shape 兼容 `AiChat.vue`),由 `ai_outbox_list` 拉取 + 事件增量刷新;提供 `scheduleMessage(convId, text, ..., dueAt)`、`listAll()`、`reschedule/cancel` 等。
|
||||
|
||||
**`src/stores/ai.ts`**:`queue` 字段类型不变(视图契约零破裂),来源改为 composable 写入。
|
||||
|
||||
**Composer 入口 `src/components/ai/ChatInput.vue`**:发送按钮区(`:107-108`)旁加「定时」图标按钮 → 弹轻量时间选择(预设 5/30/60 分钟 + 自选时间)→ 调 `scheduleMessage`。空文本仅选时间时提示需输入内容。
|
||||
|
||||
**列表 UI `src/components/AiChat.vue`**:
|
||||
- 现有内联队列条(`:78-113`)保留渲染本会话待发(含定时项可加 `🕐 时间` 角标 + 「改期」按钮)。
|
||||
- 新增**「定时列表」抽屉**:跨会话展示全部 `scheduled`(+可选 queue)项,列 时间/会话/摘要/状态,操作 查看/改期/取消/编辑。可复用 TaskDrawer 抽屉范式(`temp/S4-TaskDrawer跳转勘察-2026-10-03.md` 已有跳转勘察)。
|
||||
|
||||
**启动恢复**:`useAiOutbox` 初始化时拉 `ai_outbox_list`;若存在恢复项,`ai-toast` 提示「已恢复 N 条待发」(参考 `notifyQueueCleared` 文案范式)。
|
||||
|
||||
### 3.5 UI 形态示意(文字稿)
|
||||
|
||||
```
|
||||
┌ ChatInput ─────────────────────────────┐
|
||||
│ [图片预览行] │
|
||||
│ ┌──────────────────────────┐ ⏰ ➤ │ ⏰=定时;➤=立即
|
||||
│ └──────────────────────────┘ │
|
||||
└────────────────────────────────────────┘
|
||||
点 ⏰ → 小面板: [5分钟] [30分钟] [1小时] [自选时间 ▾] (确认后入 outbox kind=scheduled)
|
||||
|
||||
┌ 待发/定时列表(抽屉) ───────────────────┐
|
||||
│ 定时 (2) │
|
||||
│ 🕐 14:30 会话A 「跑一下构建…」 改期 ✕ │
|
||||
│ 🕐 明天9:00 会话B 「/status」 改期 ✕ │
|
||||
│ 待发 (1) │
|
||||
│ ⏳ 会话A 「继续」 立即 ✕ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、与 todo 931 的共用关系(同源方案)
|
||||
|
||||
- **同一持久层**:931 队列项与 935 定时项**同表 `ai_outbox`**,仅 `kind`/`due_at` 区分。931 不再单独造 KV/表。
|
||||
- **同一调度器**:队列=「`due_at` 为空、空闲即发」,定时=「`due_at` 到时、空闲即发」,共用 `outbox.rs` 派发循环与 `spawn_agentic_session`。
|
||||
- **共用修复点**:网络超时/重启不丢(§3.3 reconcile + §3.4 视图化)同时治 931;「错误不清队列」由「outbox 项仅 `sending→pending` 回退,不删」天然满足。
|
||||
- 分期上允许 **P0 先落 `scheduled`(935)**,`queue`(931 迁移)随后并入同表——同表即刻共用,队列侧前端派发下线放在 P1,降低单批爆炸半径。
|
||||
|
||||
---
|
||||
|
||||
## 五、分期与最小可用
|
||||
|
||||
### P0 — 后端待发箱 + 定时(最小可用,独立可验收)
|
||||
- 新增:表 V51 + `models::AiOutboxRecord` + `crud/outbox_repo.rs`;`commands/ai/outbox.rs`(调度器+IPC+事件);`lib.rs` 挂载 `outbox::start` + `agentic/mod.rs:2917` 加 `outbox::notify_conv_idle` 钩子;`state.rs` 加 `outbox`。
|
||||
- 前端:`useAiOutbox.ts` + ChatInput ⏰ 入口 + 定时列表抽屉 + `api/ai.ts` 包装 + i18n。
|
||||
- 验收:§二「定时」两条 + 「多会话」+「不倒退」。此期**只服务 `kind='scheduled'`**;`queue` 仍走旧内存路径(未有回归)。
|
||||
|
||||
### P1 — 队列并入 outbox(治 931)
|
||||
- 前端 `sendMessage` L1 改走 outbox;下线 `drainQueue`/`initDrainQueueListener`/`ai-drain-queue`;regenerate/edit/clear 接 IPC 清队。
|
||||
- 验收:§二「断网/超时」「重启」两条 + 既有队列 UI 交互(编辑/回填/立即发送/取消/超时强发)零弱化。
|
||||
|
||||
### P2 — 进阶
|
||||
- 飞书通知(`df-outbox-notify-feishu`);过期窗口策略(拍板点 4);`kind='task'` 扩展位预留供 937(定时任务/工作流经同一调度器,派发目标改 task/workflow 而非 `spawn_agentic_session`)。
|
||||
|
||||
---
|
||||
|
||||
## 六、影响面(新能力引入评估纪律五维,`docs/todo.md:446`)
|
||||
|
||||
- **兼容性**:纯新增表 + 新增 IPC/事件,旧库由 V51 幂等建表向上迁移,老数据零触碰;前端 `state.queue` shape 不变(视图契约)。迁移可断点续跑(逐条状态标记,无全局 COUNT 推算)、可逆(`DROP TABLE`+关开关)。旧 `ai-drain-queue` 事件在 P1 下线时保留一个版本的空发兜底,防旧前端混跑。
|
||||
- **破坏性**:不可逆操作仅「迁移 V51 建表」(可 `DROP` 回退);P1 下线上前端派发属行为变更,以开关 `df-outbox-enabled=false` 可回退旧路径(过渡期保留旧内存队列代码)。
|
||||
- **安全面**:无新端点/命令执行面。outbox 项若触发工具调用,仍走既有 `audit`/`approval`/边界模型闸门,**定时不等于放行**(与无人值守 L1 同级,不自动升档)。落盘内容(文本/图片 base64)属本地库既有信任域(与 `ai_messages` 同级)。改期/取消为本地 IPC。
|
||||
- **资源面**:DB 增长——`sent/canceled` 行建议保留(胜于删除,便于审计)但**须有上限**:图片 base64 项加体积上限(沿用前端 `PARTS_SIZE_BUDGET` 50MB 量级口径,超限入队即拒并提示);可加「已完成项保留 N 天」的清理点(默认可不做,登记为观测)。调度器 tick 默认 10s,配合 `Notify` 即时唤醒,常驻开销可忽略(低于 unattended 300s 轮询)。
|
||||
- **回退开关**:`df-outbox-enabled`(默认 true,关=调度停=项只入不发,可观测);`df-outbox-poll-secs`/`df-outbox-max-attempts` 热读。灰度顺序:P0 scheduled 先在本机自用验证 → P1 队列迁移 → P2 通知/937。观测指标:outbox 积压条数、`sending` 残留数、失败率、`due→sent` 延迟。
|
||||
|
||||
---
|
||||
|
||||
## 七、拍板点清单
|
||||
|
||||
| # | 决策 | 选项 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 1 | 调度落点与「进程退出」语义 | A 后端 tokio 常驻(关窗托盘化期间生效;进程退出期间不生效,重启追赶补发);B 前端定时器(仅界面活着时生效);C 注册 OS 级计划任务(进程死也触发) | **A**。与无人值守同形态、复用 `spawn_agentic_session`;明确「进程退出=不触发,重启补发」为本设计边界,C 作远期非目标(跨平台成本高,偏移本设计) |
|
||||
| 2 | 存储形态 | A 新表 `ai_outbox`;B 复用 `app_settings` KV | **A**。需按 `status+due_at` 索引扫描 + 逐条 CAS 改期/取消,KV 整块 JSON 读改写有并发覆盖风险且不可索引 |
|
||||
| 3 | 队列与定时是否统一实体 | A 统一并入 outbox(`kind`/`due_at` 区分),前端队列视图化; B 队列保前端内存 + 独立后端定时器 | **A**。931 根因就是内存+前端自驱,B 会留双调度(草率聚合);A 一次治两需求 |
|
||||
| 4 | 进程退出期间到期项的追赶策略 | A 全部到点即补发(迟到多久都补);B 迟到超阈值(如 >12h)标「已过期」+通知不补发;A+B 折衷 | **B(阈值建议 12h,值可调)**。补发全部易在长关机后制造批量突发发送;过期标记保证「不静默丢」(用户可见可手动重发),符合不静默丢失纪律 |
|
||||
| 5 | 定时消息是否允许携带图片 | A 允许(限体积);B 定时项禁图片仅文本 | **A(B 作兜底)**。图片 base64 落库有膨胀,但用户「设好图片定时发」是合理诉求;用体积上限而非一刀切禁用 |
|
||||
@@ -0,0 +1,254 @@
|
||||
# 对话内定时/延时任务 — 设计
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 需求锚点: `docs/todo.md:703` 条目 937 | 勘察: 只读实测(文件:行已核)
|
||||
> 目标一句话: 在对话里直接说「1 小时后跑 X / 明早 9 点把任务 A 推进到审查」,系统登记一条**持久化的 `kind='task'` 待发箱项**,到点自动执行(注入发起会话续跑),任务卡显示「定时中/已触发」,并可在统一「定时列表」里查看/改期/取消。
|
||||
> 边界: 仅设计,不改码。
|
||||
> **与 935 的关系(要害)**: 937 **不新造调度底座**。表 / 调度器 / IPC / 事件 / KV / 「定时列表」抽屉**全部复用** [`定时消息与待发箱持久化-设计-2026-10-10.md`](./定时消息与待发箱持久化-设计-2026-10-10.md)(该设计已覆盖 todo 935 定时消息 + 931 队列,并在其 §3.1 明确预留 `kind='task'` 给 937、§五 P2 交棒)。**本设计只定义 `kind='task'` 这一分支的语义 + AI 工具面 + 触发执行 + 权限边界**。
|
||||
> 关联: 并行化与异步唤醒-设计(§二 wake delay 型 / §6.3 挂机)/无人值守长任务-设计(TaskBoundary 边界)。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状勘察(锚点实测)
|
||||
|
||||
### 1.1 AI 工具面(新工具的正确落点)
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 注册宏 | `crates/df-ai/src/ai_tools_decl.rs` `declare_tool!` | 纯 `macro_rules!`,零新依赖;新工具**只加一个 `tools/<域>.rs` 文件** + 在对应 `register_*` 接一行 |
|
||||
| 注册总入口 | `src-tauri/src/commands/ai/tool_registry.rs:541` `build_ai_tool_registry` | `register_data_tools`(:735) 内 `register_task_tools`(:737)/`register_workflow_tools`(:739);二者已 thin 委托 `tools::task::register`(:767)/`tools::workflow::register`(:813) |
|
||||
| 任务工具 | `src-tauri/src/commands/ai/tools/task.rs` | 6 个:list_tasks(:40)/create_task(:72)/advance_task(:185,状态机推进唯一合法路径)/delete_task/get_task_count。**无任何「延后」维度** |
|
||||
| 工作流工具 | `src-tauri/src/commands/ai/tools/workflow.rs:35` `run_workflow` | High 风险;handler 是防御兜底(返 Err),**真正执行经 `ai_approve` → `execute_run_workflow_for_tool`**(`src-tauri/src/commands/ai/mod.rs:1367`)→ `run_workflow_inner`(`src-tauri/src/commands/workflow.rs:127`);空 dag + target_status 时按 `template_for`(`crates/df-nodes/src/task_workflow_templates.rs:28`)自动选模板 |
|
||||
| conv_id 透传先例 | `execute_run_workflow_for_tool`(`mod.rs:1367`) | AI 工具 handler 无 `State`,**发起会话 id 经调用方(task-local/参数)注入**——`schedule_task` 记录发起会话照此 |
|
||||
| 基线守卫 | `tool_registry.rs` 测试 `test_build_ai_tool_registry_baseline_tool_count` | 钉死工具名集合;**新增工具须同步该断言(+1)** |
|
||||
|
||||
### 1.2 任务体系与推进链(被定时触发的目标能力,均已就位)
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 状态机 | `crates/df-nodes/src/task_state_machine.rs` | 8 态合法转换纯函数;done 唯一终态 |
|
||||
| 原子推进 | `crates/df-nodes/src/task_advance_node.rs:50` `advance_task_atomic` / `:335` `advance_task_with_parent` | 状态机校验 + CAS + review_rounds + 父聚合,已与 IPC/AI 工具三方同源 |
|
||||
|
||||
结论: 「到点后要做的事」(推进任务/跑工作流/执行指令)**能力齐备**,缺的只是**外部时间驱动源**与**工具面的延时表达**。
|
||||
|
||||
### 1.3 后端常驻调度器先例(unattended;935 已定 outbox 抄其形态)
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 常驻轮询 | `src-tauri/src/commands/ai/unattended.rs:508` `start`;`lib.rs:84` 挂载 | `async_runtime::spawn` + `interval.tick()` / `notify.notified()` `select!` |
|
||||
| 共享态/唤醒 | `unattended.rs:459` `EngineShared{notify: Notify, in_flight,…}`;`:483` `notify_batch_done` | 单在途 + 完成钩子即时唤醒 |
|
||||
| 开工链路 | `unattended.rs:847` `spawn_agentic_session`(`chat.rs:796`,10 参,`pin_active=false`) | 后台会话派发正典入口 |
|
||||
| 生命周期事实 | `lib.rs:614` `on_window_event` `prevent_close`+`hide` 托盘化 | **关窗=进程仍在=调度器照跑;进程退出=停,重启补发** |
|
||||
|
||||
**关键缺口(937 视角)**: 引擎作业源是看板文件,**不落库、不看时间点**;真正解决「持久化 + 到点」的是 935 设计的 `ai_outbox`(见下节),937 直接搭车。
|
||||
|
||||
### 1.4 存储 / 恢复 / 事件 / UI 样板(937 复用链)
|
||||
|
||||
| 件 | 锚点 | 现状 / 复用点 |
|
||||
|----|------|--------------|
|
||||
| 迁移框架 | `crates/df-storage/src/migrations.rs:9` `run`;调度数组 `:108-121`(止于 `(50, migrate_v50)` @ `:1483`) | 追加 `(N, migrate_vN)` 即挂;**935 占 V51(建 `ai_outbox`),937 用 V52 加列** |
|
||||
| 加列样板 | `migrations.rs:1366` `migrate_v45` | `column_exists` 守卫 + `ALTER TABLE ADD COLUMN`,幂等可重跑;可 `DROP COLUMN` 回退 |
|
||||
| 启动恢复样板 | `audit/restore.rs:26` `restore_pending_approvals` | 从表重建内存态;outbox 的 `reset_stale_sending` 同款 |
|
||||
| 会话来源 | `models.rs:389` `AiConversationRecord.origin`(现仅 `"sub_agent"`) | 定时触发落后台会话时可用新值 `"schedule"` |
|
||||
| 事件 | `mod.rs:134` `AiChatEvent`(`#[serde(tag="type")]`) | 935 定的 `ai-outbox-changed/sent/failed` 直接复用 |
|
||||
| 卡片范式 | `src/components/ai/TaskParkedCard.vue` | 「定时任务卡」可照此(状态徽标 + 行为后更新) |
|
||||
|
||||
### 1.5 缺口清单(937 专属,底座缺口由 935 覆盖)
|
||||
|
||||
1. **工具面无延时维度**:LLM 无法表达「1 小时后」(`advance_task`/`run_workflow`/`create_task` 均即时)。
|
||||
2. **无 `kind='task'` 分支**:935 的 `ai_outbox` 表/调度器只服务消息派发,`kind='task'` 的**载荷字段**(结构化指令/任务关联/边界快照)与**派发目标**(注入会话 or 直推任务)**尚未定义**。
|
||||
3. **无触发执行的落点语义**:到点「谁来跑、跑哪个会话、结果给谁看」未定。
|
||||
4. **无对话内定时卡片**:工具卡如何呈现「定时中/已触发」未定。
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与验收口径
|
||||
|
||||
| 目标 | 验收口径(可实测) |
|
||||
|------|-------------------|
|
||||
| G1 对话内创建 | LLM 调 `schedule_task(instruction, delay_secs)`,对话出现**定时任务卡**(指令摘要 + 触发时刻/倒计时 + 「定时中」徽标);`ai_outbox` 落一行 `kind='task'`, status=pending |
|
||||
| G2 到点触发 | 到 `due_at` → 调度器把指令注入**发起会话**续跑(等价一条 user 消息注入),卡片变「已触发」,新 assistant 回复出现在原会话 |
|
||||
| G3 持久存活 | **应用关闭跨过 due_at**:关窗托盘化期间照常触发;进程真退出则重启后按 935 追赶策略(§四)补触发或标过期;卡片状态经重启不丢 |
|
||||
| G4 管理面 | 统一「定时列表」(935 抽屉,937 项并列显示,类型标「任务」)可查看/改期/取消;改期/取消即时写库刷新卡片 |
|
||||
| G5 边界合规 | 创建 = Medium 风险(走既有审批);**触发执行零旁路**——写/High 风险仍在触发会话内走既有 `audit`/`path_auth`;黑名单永不放行 |
|
||||
| G6 不打架 | 触发时发起会话正生成 → 轮界排队(不打断);会话已删/不可用 → 回落独立后台会话(origin=`schedule`) |
|
||||
| G7 零回归 | 既有 advance_task/run_workflow/create_task 行为不变;无 task 作业时调度器空转零副作用;935 的 `queue`/`scheduled` 分支不受影响 |
|
||||
|
||||
---
|
||||
|
||||
## 三、设计方案
|
||||
|
||||
### 3.1 总体判断(先说结论)
|
||||
|
||||
1. **新工具 `schedule_task`,不复用 `advance_task` 加调度字段。** `advance_task` 的契约是「此刻」推进状态机(CAS + `applied.matched_target` 断言,`tools/task.rs:185`),loop 收敛判定依赖它;改成「可能延后」会污染该断言并让「任务状态」与「调度时点」耦合。调度是正交能力:指令里可以*提到*任务,但调度本身不是状态迁移。
|
||||
2. **底座全复用 935 的 `ai_outbox` + `outbox.rs` 调度器,937 只加 `kind='task'` 分支。** 不新造表/不新造 loop,与需求「与 935 共用调度器」一致;亦避免「双调度器」的草率聚合。
|
||||
3. **触发执行的通用形态 = 注入发起会话(把指令当一条用户消息续跑)**,由触发后的 agentic loop 统一消化(它本就会 `advance_task`/`run_workflow`);确定性「直推任务状态」作 P2 快路径(§3.6、拍板点 4)。
|
||||
|
||||
### 3.2 数据形态:复用 `ai_outbox` + 迁移 V52 增列
|
||||
|
||||
935 已定义 `ai_outbox`(`定时消息…设计` §3.1,V51),其列 `text / skill / model_override / parts_json / spans_json / due_at / status / attempts` 面向消息。937 复用其 `text`(= 指令原文,供统一列表渲染摘要)、`due_at`(= 触发时刻)、`status`、`attempts`,并**新增三列**承载 task 专属载荷:
|
||||
|
||||
```sql
|
||||
-- 迁移 V52(937):幂等 ALTER(照 migrate_v45 样板),可 DROP COLUMN 回退
|
||||
ALTER TABLE ai_outbox ADD COLUMN title TEXT; -- 卡片/列表标题(缺省取指令前 N 字)
|
||||
ALTER TABLE ai_outbox ADD COLUMN payload_json TEXT; -- 结构化载荷 {instruction, task_id?, target_status?}
|
||||
ALTER TABLE ai_outbox ADD COLUMN boundary_json TEXT; -- 触发执行继承的边界快照(可空=审批模式)
|
||||
```
|
||||
|
||||
- `kind='task'` 行:`text`=指令人读文本(列表摘要用);`payload_json`=`{instruction, task_id?, target_status?}`(instruction 冗余存一份便于派发时直接取;task_id/target_status 供 P2 直推快路径)。
|
||||
- **不新增独立表**。`AiOutboxRecord`(`定时消息…设计` §3.1)对应加三个 `Option` 字段;`outbox_repo.rs` 的 insert/读方法带这三列(其余复用)。
|
||||
- 迁移编号:935 占 V51,937 占 V52;若两需求同批落地,可合入一个迁移函数一次到位(建表 + 增列),但保持「V51 建表 / V52 增列」两段亦可(幂等,顺序无关)。
|
||||
|
||||
### 3.3 工具面:新增 `schedule_task`(`declare_tool!`,Medium)
|
||||
|
||||
新文件 `src-tauri/src/commands/ai/tools/schedule.rs`,仅一个工具;`tool_registry.rs:737` 旁接线(`register_schedule_tools`),基线测试 +1。
|
||||
|
||||
```rust
|
||||
declare_tool!(
|
||||
registry, db: Arc<Database>,
|
||||
"schedule_task",
|
||||
"登记一个定时/延时执行的任务作业:到点后在发起会话里自动执行 instruction(等价到点时我把这句话重新对你说一遍)。\
|
||||
用 delay_secs 表达相对延时(如「1 小时后」=3600),或用 at 表达绝对时刻(RFC3339 或 epoch ms)。二者至少给一个,同时给以 at 为准。\
|
||||
到点执行受既有审批/路径授权约束,不绕过任何安全闸。返回 job_id 与触发时刻。",
|
||||
RiskLevel::Medium, // 登记未来自动执行,须人工知情(对齐 advance_task 的 Medium)
|
||||
meta: ToolMeta {
|
||||
entity_action: Some(("schedule", "create")),
|
||||
approval_hint: Some(("创建定时任务:{}", &["instruction"])),
|
||||
display_verb: Some("创建定时任务"),
|
||||
..Default::default()
|
||||
},
|
||||
schema: object_schema(vec![
|
||||
("instruction", "string", true), // 到点要执行的自然语言任务(必填)
|
||||
("delay_secs", "integer", false), // 相对延时(秒),与 at 至少一个
|
||||
("at", "string", false), // 绝对时刻 RFC3339;优先于 delay_secs
|
||||
("title", "string", false), // 卡片标题(缺省取 instruction 前 N 字)
|
||||
]),
|
||||
args => {
|
||||
// 1. 解析 instruction(必填)/delay_secs/at/title → 折算 due_at(epoch ms)
|
||||
// 2. 发起 conv_id:handler 无 State → 经调用方注入(task-local,对齐 execute_run_workflow_for_tool mod.rs:1367)
|
||||
// 3. outbox_repo::insert(kind="task", text=instruction, payload_json={instruction}, title, due_at, status=pending)
|
||||
// 4. 唤醒调度器 notify → emit("ai-outbox-changed") 前端插卡
|
||||
// 5. 返回 { job_id, due_at }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
- **相对 `delay_secs` 为主**(免时区/解析歧义);`at` 为便利补充(LLM 依 system prompt 里的当前时间折算)。
|
||||
- **conv_id**:AI 工具 handler 是无 `State` 闭包;发起会话 id 沿用 workflow 工具先例经 task-local 注入(`execute_run_workflow_for_tool` 由 `ai_approve` 带 `conv_id` 调,`mod.rs:1367`)。`ai_outbox.conversation_id` 为 `NOT NULL`,而 AI 工具场景必在会话内,故恒可填;极少数拿不到 conv_id 的路径退回「前端/列表入口创建」(935 §3.4 composer 入口)。
|
||||
|
||||
### 3.4 调度器:复用 935 `outbox.rs`,加 `kind='task'` 派发分支
|
||||
|
||||
935 的调度循环(`定时消息…设计` §3.3)按 `kind` 分派。937 **只加一个分支**:
|
||||
|
||||
```
|
||||
// outbox.rs list_due 取项后的派发(schema 由 935 定义,此处补 kind 分支)
|
||||
match item.kind.as_str() {
|
||||
"queue" | "scheduled" => { /* 935 既有:spawn_agentic_session(text,...) */ }
|
||||
"task" => fire_task_job(app, state, &item), // ← 937 新增(§3.5)
|
||||
other => { warn!(unknown kind); }
|
||||
}
|
||||
```
|
||||
|
||||
- **CAS/并发/在途/唤醒钩子全部复用** 935:同会话忙(`can_accept_request=false`)→ 本轮跳过下次再取(不丢);`notify_conv_idle` 完成钩子即时续发;`claim` CAS 防双取;`reset_stale_sending` 启动复位。
|
||||
- **`df-outbox-enabled` 复用**:关 = 所有 kind 冻结(937 的 task 也冻结),一致。
|
||||
|
||||
### 3.5 触发执行语义(`fire_task_job`,937 核心)
|
||||
|
||||
1. **注入文本**:`[定时任务触发] {instruction}`(固定前缀族,便于前端识别 + 完成卡归属)。
|
||||
2. **落点选择**:
|
||||
- 发起会话存在且 `can_accept_request` → **注入发起会话**:走 `spawn_agentic_session(text=<前缀+instruction>, …, Some(conv), …, pin_active=false)`(与 935 派发同源,`chat.rs:796`)。
|
||||
- 发起会话正生成 → **轮界排队**:`revert_pending`(复用 935 的忙回退),下一 tick 重试(不打断流)。
|
||||
- 发起会话已删/不存在 → **回落独立后台会话**:`Some(new_id())` + `origin="schedule"`,不静默丢。
|
||||
3. **落库**:`mark_sent`(复用 935 语义:`spawn` 返回 Ok 即受理成功)+ `emit("ai-outbox-sent", {id, conv})`。
|
||||
4. **失败**:spawn 前置失败(provider 未配/永久 4xx)→ `mark_failed`(`permament(error)` 复用 935 判据)+ 通知;非永久 → `revert_pending` 下轮重试。**LLM 轮次内的失败不回流**(与 935 同界定,交既有 loop 自愈)。
|
||||
|
||||
### 3.6 关闭/重启行为(复用 935 追赶策略,937 不另立规则)
|
||||
|
||||
- 迁移/恢复:复用 935 的 `reset_stale_sending`(启动把残留 `sending` → `pending`);调度器只认 `due_at <= now`,故**进程退出期间到期的 task 项重启后立即命中**。
|
||||
- **迟到追赶策略 = 复用 935 拍板点 4(阈值 12h,建议值)**:迟到 ≤ 阈值 → 补触发;迟到 > 阈值 → 标过期(`status='expired'`? 935 用 `failed`/`canceled`,建议新增 `expired` 或用 `failed`+`last_error="missed"`)+ 通知,列表可见可手动重排。**937 不引入 24h 另一套阈值**,统一走 `df-outbox-*` 配置(如需 task 专属阈值,加 KV `df-outbox-task-missed-grace-secs`,默认同 12h)。
|
||||
- 有界性:任何 task 作业按 `due_at + 宽限` 有界,不无限滞留。
|
||||
|
||||
### 3.7 权限与边界(与既有 audit/boundary 链的关系)
|
||||
|
||||
- **创建**:`schedule_task` Medium → 走既有 `audit/approval`(三档/边界会话按现行规则);**创建 ≠ 授权执行**。
|
||||
- **触发执行**:走**同一** pipeline。`fire_task_job` 只把一句话送进会话,此后 agentic loop 里每个工具照常过 `audit`(写/High 审批)、`path_auth`(路径授权)、黑名单拒绝。**不新增任何旁路,不预授权**(红线)。
|
||||
- **边界快照 `boundary_json`(可选)**:若发起会话已声明 `TaskBoundary`(`boundary.rs`,risk_ceiling≤Medium,落 `ai_conversations.boundary_json` V45),创建时可快照;触发时注入触发会话 → 边界内自动放行、边界外照旧拒绝。未声明 → 不写快照 → 触发走普通审批(首个写操作停靠等人工 + 通知)。
|
||||
- **黑名单/破坏性命令**:永不放行(同 all 档/子代理先例)。
|
||||
- **MCP 面(独立表面,首期不接)**:`crates/df-mcp/src/tools.rs:101` `all_tools()` 是**独立硬编码清单**(非 AI registry 派生);首期不把 `schedule_task` 加入 MCP(定时依赖本机 GUI 进程 + 调度 loop,远程场景语义不闭合),MCP 侧零改动。
|
||||
|
||||
### 3.8 UI 形态(复用 935 抽屉,937 只加卡片)
|
||||
|
||||
- **对话内定时任务卡(工具卡变体)**:`useToolCard.ts:513` 登记 `schedule_task` formatter;卡面 = 标题 + 指令摘要 + 「⏰ 触发于 MM-DD HH:mm(还有 X 分钟)」+ 状态徽标(`定时中`/`已触发`/`已取消`/`已过期`/`失败`)。**状态随 `ai-outbox-changed/sent/failed` 事件更新**(到点晚于工具返回,不能靠静态 tool_result)。形态照 `TaskParkedCard.vue`(per-conv 卡,行为后摘/更新)。
|
||||
- **统一「定时列表」抽屉 = 复用 935**(`定时消息…设计` §3.4):937 的 task 项与 935 的 scheduled/queue 项**同列同抽屉**,仅多一列「类型=任务/消息」+ task 项显示关联任务(若有)。改期/取消复用 935 的 `ai_outbox_reschedule/cancel` IPC。
|
||||
- **可选 task 专属入口**:ChatInput ⏰ 入口(935 已加,`ChatInput.vue:107`)+ 若要在列表里「手动新增定时任务」,复用同一 IPC(`ai_outbox_enqueue` 加 `kind='task'`+payload,或 937 加 `ai_schedule_task` 薄封装,见拍板点)。
|
||||
- 设置页/TopBar 徽标:复用 935 的 `df-outbox-*` 区块与状态查询,不另开。
|
||||
|
||||
### 3.9 与既有链路的关系
|
||||
|
||||
- **worktree / 评审**:触发执行 = 一次普通 agentic run,worktree/review/run_workflow 全照旧;指令若含「推进任务/跑工作流」,由触发后的 loop 调 `advance_task`/`run_workflow`(工作流仍走 High 审批)。
|
||||
- **无人值守引擎**:两者独立 loop、独立作业源;挂机在途(`in_flight`)与 outbox 派发各管各 conv,不冲突。
|
||||
- **P2 快路径(可选)**:确定性作业「到点直推任务状态」(`payload_json.task_id`+`target_status`)可**不起 LLM**,直接 `advance_task_with_parent`(`task_advance_node.rs:335`)——省 token、免 LLM(状态机推进本身非破坏);须在创建时校验状态机合法性。见拍板点 4。
|
||||
|
||||
---
|
||||
|
||||
## 四、分期与最小可用
|
||||
|
||||
| 期 | 范围 | 交付 | 依赖 |
|
||||
|----|------|------|------|
|
||||
| **P1(最小可用)** | 工具面 + task 分支闭环 | V52 增列 + `AiOutboxRecord` 三字段 + repo + `schedule_task` 工具 + `fire_task_job` 注入发起会话 + 启动追赶快照 + 工具卡徽标 | **935 P0 底座(`ai_outbox` 表 + `outbox.rs` 调度器 + 列表抽屉)** |
|
||||
| **P2(管理/快路径)** | 列表类型区分 + 直推 | 列表 task 项类型标 + 关联任务显示;可选确定性直推快路径 + KV 阈值 | P1 稳定 |
|
||||
| **P3** | 边界快照 / 与挂机联动 | `boundary_json` 快照注入触发执行;挂机自领定时任务(可选) | P2 + 边界模型落地 |
|
||||
|
||||
**最小可用定义**: P1 完成即达标——「对话里说 1 小时后跑 X → 卡片定时中 → 到点(含重启补触发)在原会话自动执行」。
|
||||
**关键依赖**: 937 的 `kind='task'` 分支**不能脱离 935 的 `ai_outbox` 底座独立交付**;两需求应**同批或先后紧邻落地**,先 935 P0(表+调度器)后接 937 分支。这是本设计对编排的硬约束。
|
||||
|
||||
---
|
||||
|
||||
## 五、影响面(新能力引入评估纪律 · 五维)
|
||||
|
||||
| 维度 | 判定 |
|
||||
|------|------|
|
||||
| **兼容性** | 复用 `ai_outbox`(V51)+ 937 加三列(V52,`column_exists` 幂等,可 `DROP` 回退);既有工具/任务/工作流零改动。**基线测试 `test_build_ai_tool_registry_baseline_tool_count` 须 +1(schedule_task)**。935 的 `queue`/`scheduled` 分支不受影响(仅多一个 `match` 分支)。**PASS(带迁移)** |
|
||||
| **破坏性** | 无破坏性数据操作;不改 advance_task/run_workflow 语义;触发=发消息/起会话,可逆(取消/停止)。**PASS(低)** |
|
||||
| **安全面** | 新攻击面 = 「未来自动执行 LLM/用户给定指令」。缓解:①创建 Medium 须知情审批 ②触发执行**零旁路**,写/High/路径/黑名单全走既有闸(G5/G6)③边界快照仅在会话已声明边界时生效(不放大权限)④可随时取消。**红线**:不得为「定时」开任何 approval/path 旁路。**PASS(受控)** |
|
||||
| **资源面** | 复用 935 表与 10s tick loop;937 只多三列 + 一个 match 分支,资源增量≈0。task 项随 outbox 统一设上限(935 已定 sent/canceled 有界策略)。**PASS** |
|
||||
| **回退开关** | 复用 `df-outbox-enabled`(置 false → task 项只入不发,冻结);V52 可 `DROP COLUMN` 回退。KV 热读免重启。**PASS(带开关)** |
|
||||
|
||||
---
|
||||
|
||||
## 六、拍板点(需人定,附建议)
|
||||
|
||||
1. **触发执行落点**:注入**发起会话**(结果可见、上下文连续,贴「对话内创建」语义)【建议默认】 vs 独立后台会话(不打扰但结果另起)。建议前者,发起会话忙则轮界排队、不可用则回落后台会话(§3.5 两全)。
|
||||
2. **与 935 的落地顺序**:937 的 `kind='task'` 分支**依附 935 的 `ai_outbox` 底座**——先 935 P0 后接 937【建议】 vs 937 自建一套表/调度器(违反「共用调度器」且制造双调度,否决) vs 两设计合并为一次实施(978/935/937 同批)。建议前者,把「共用底座」写死为编排约束。
|
||||
3. **触发执行授权**:默认**普通审批模式**(未声明边界的发起会话 → 触发后首个写操作停靠等人工并通知)【建议,最安全】 vs 创建即固化「完全信任」(攻击面大) vs 仅当发起会话已声明 `TaskBoundary` 时携带边界快照(边界内免打扰)。建议:默认普通审批 + 「有边界则快照」并存。
|
||||
4. **快路径**:起步只做 `instruction`(通用,触发后 loop 消化一切)【建议】 vs 起步即带 `task_id`+`target_status` 确定性直推(省 LLM/免审批,但限死用途、需额外状态机校验)。建议 instruction 起步,直推作 P2(§3.9)。
|
||||
5. **迟到追赶阈值**:复用 935 拍板点 4 的统一阈值(建议 12h)【建议】 vs 为 task 单设更短阈值(定时任务含副作用,可能希望更保守)。建议统一阈值起步,观测后如需再拆 KV。
|
||||
|
||||
---
|
||||
|
||||
## 七、明确不做
|
||||
|
||||
- 不做 cron 表达式/周期重复任务(首期只做**一次性**定时;周期任务=挂机模式职责,另议)。
|
||||
- 不做跨设备/云端定时(复用 935 边界:关窗托盘化期间生效,进程退出期间不触发,重启补发)。
|
||||
- 不做定时执行结果的独立推送通道(复用 `notify_session_event` + 飞书,与 935 同出口)。
|
||||
- 不为定时开任何 approval/path 旁路(红线)。
|
||||
- 不新造调度底座(表/循环/IPC/事件/抽屉全复用 935)。
|
||||
|
||||
---
|
||||
|
||||
## 附:实施落点清单(供实施会话直接取用;底座项见 935 设计,此处只列 937 增量)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `crates/df-storage/src/migrations.rs` | 增 `migrate_v52`(3 个 `ALTER TABLE ai_outbox ADD COLUMN`,幂等);调度数组追加 `(52, migrate_v52)`;补幂等单测。**注意:V51 由 935 占,顺序不得换** |
|
||||
| `crates/df-storage/src/models.rs` | `AiOutboxRecord`(**935 已加**)再补 `title/payload_json/boundary_json: Option<String>` |
|
||||
| `crates/df-storage/src/crud/outbox_repo.rs`(**935 已建**) | insert/读方法带三新列;`list_by_status`/`list_due` 复用 |
|
||||
| `src-tauri/src/commands/ai/tools/schedule.rs`(新) | `schedule_task` 工具(`declare_tool!`,Medium);`tool_registry.rs:737` 旁接线 + 基线测试 +1 |
|
||||
| `src-tauri/src/commands/ai/outbox.rs`(**935 已建**) | 调度派发 `match kind` 加 `"task" => fire_task_job(...)`;`fire_task_job`(§3.5)+ 追赶快照 |
|
||||
| `src-tauri/src/commands/ai/mod.rs` | 事件复用 `ai-outbox-changed/sent/failed`(935 已定);如卡片需状态细分可加可选字段,不新事件名 |
|
||||
| `src-tauri/src/lib.rs` | `outbox::start` 由 935 挂载;937 无需另挂 |
|
||||
| `src/composables/ai/useToolCard.ts` | `:513` 映射增 `schedule_task` formatter(定时任务卡) |
|
||||
| `src/components/ai/` | 定时任务卡(仿 `TaskParkedCard.vue`);列表抽屉 task 项类型标(复用 935 抽屉) |
|
||||
| `src/api/ai.ts` | 复用 935 `ai_outbox_*` 封装;若新增「手动建定时任务」入口再加薄封装 |
|
||||
| `src/i18n/zh-CN/*` + `en/*` | 卡片状态词(`定时中`/`已触发`/`已过期`…)/类型「任务」 |
|
||||
|
||||
> 参照档: `定时消息与待发箱持久化-设计-2026-10-10.md`(935/931,**共用底座,必读**)· `无人值守长任务-设计-2026-10-04.md`(结构/详略)· `并行化与异步唤醒-设计-2026-10-04.md`(§二 wake delay 型/§6.3 挂机)· `输入框列表预览与自动编号-设计-2026-10-10.md`(同目录同日写法/五维自评)。
|
||||
@@ -0,0 +1,245 @@
|
||||
# 消息渲染管线重构 — 设计
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 勘察: 只读实测锚点(全仓零 `v-memo`,实测 `grep -c v-memo = 0`)
|
||||
> 对象: `src/components/ai/MessageList.vue`(3041→实测 **3054 行** God 组件)
|
||||
> 目标(用户原话转译): 长会话流式掉帧根治 —— 拆分 God 组件、渲染记忆化、引入 `v-memo`、去 `JSON.stringify` 指纹、流式帧降载。
|
||||
> 说明: 本专题为**只读走查 + 设计**,不含代码实施;所有行号均为实测锚点(文件以 `E:/wk-lab/devflow` 为根)。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状勘察(锚点实测)
|
||||
|
||||
### 1.1 文件构成(3054 行 = script 1476 + template 460 + style 1117)
|
||||
|
||||
| 段 | 行区间 | 行数 | 备注 |
|
||||
|----|--------|------|------|
|
||||
| `<script setup>` | `1–1476` | 1476 | 逻辑 + 组合式调用 + 大量内联渲染辅助 |
|
||||
| `<template>` | `1477–1936` | 460 | 单一 `v-for` 扁平渲染 + 6 个 `v-if/v-else-if` 分支 |
|
||||
| `<style scoped>` | `1938–3054` | 1117 | 全组件样式集中于此(拆分后随子组件 scoped 自然分流) |
|
||||
|
||||
职责分类(**>6 类**,单一组件内并存):
|
||||
|
||||
1. 列表编排/分段 —— `messageSegments`(`:837`)、`renderItems`(`:1067`)、时间线 `needsTimeSep`(`:1049`);
|
||||
2. 轨迹卡/步骤 —— trace 分支(`:1510`)/step 分支(`:1587`)+ 聚合助手 `:993–1246` + 轮次导航 `traceNav`(`:1386`);
|
||||
3. 子代理完成卡 + peek 浮层 —— agentFin 分支(`:1601`)+ `openAgentTrace`(`:1280`)+ `agentPeek*`(`:1297–1384`);
|
||||
4. 消息气泡 —— msg 分支(`:1644–1784`),user 走子组件 `MessageItem.vue`,assistant/system/json/长文折叠/错误操作条内联;
|
||||
5. token 用量 —— 派生 `:172–224` + 模板块 `:1792+`;
|
||||
6. 复制/选区 —— `copyMsgContent`(`:603`)、`copyMsgAsImage`(`:640`)、`saveSelection`/`restoreSelection`(`:501/:545`)、`offsetToDOMPosition`(`:481`)、代码块复制(`:728`);
|
||||
7. 滚动/折叠联动 —— `useMessageScroll`(`:21/:230–238`)、`collapseAllToolLists`(`:395`)、`anchorCollapseScroll`(`:354`)、`buildActiveToolIds`(`:380`);
|
||||
8. 展开状态集合 5 组 —— `useToggleSet`(`:869–880`)+ `expanded*`(`:883/:891/:944/:949/:953`)。
|
||||
|
||||
流式渲染(`useStreamRenderer.ts`)与 Markdown(`useMarkdown.ts`)此前已抽出;**本专题针对剩余编排层**。
|
||||
|
||||
### 1.2 热路径触发链(谁在每帧/每次事件重算)
|
||||
|
||||
| 触发源 | 频率 | 影响 | 锚点 |
|
||||
|--------|------|------|------|
|
||||
| `streamingBlocks` 每帧更新(rAF) | **每帧** | 组件 render 函数整体重跑 → `v-for` 对全部 item **逐条新建 vnode**(全仓零 `v-memo`) | `useStreamRenderer.ts:232`(rAF 内赋值)、模板 `:1727–1728` |
|
||||
| 工具事件(status 翻转) | 每次工具事件 | `renderItems` 全量 O(n) 重算(读 `m.toolCalls`/`tc.status`/`tc.completedAt`) | `renderItems :1067`;status 就地改 `useAiToolEvents.ts:51/101/119`、`useAiApproval.ts:87/109`、`useAiConversations.ts:501`、`useAiStreamingEvents.ts:123` |
|
||||
| 指纹 watch | 每次工具事件 / 消息数变 | `map(m=>toolCalls→{id,s})` + `JSON.stringify` 全量 O(n) | **`:429–442`**(source `:430–433`) |
|
||||
| 展开/折叠 toggle | 用户点击 | `renderItems` 重算(trace/seg 集合在 dep 内)+ 全组件 render | `:1116`(expandedTraceIds)、`:1152`(expandedSegmentIds) |
|
||||
|
||||
**关键链路细节**:
|
||||
- `renderItems`(`:1067–1189`)读 `store.state.messages`(数组)、`shouldRenderMsg`(`:159–167`,读 `msg.content?.trim()`)与 `isViewingGenerating`(`:126`)。**流式 delta 期间末条 AI 的 `msg.content` 不被改写**(正文回填在收尾单点 `aiShared.ts:544`),故 `renderItems` 本身**不逐帧重算**;逐帧的开销来自 render 函数里 `v-for` 对全部 item 重建 vnode(无 `v-memo` 跳过)。工具事件则确会触发 `renderItems` 全量重算。
|
||||
- `flushBucket`(`:1075`)对每个桶 `collectTurnStats`(`useTurnTrace.ts:90`,遍历全部 toolCalls)+ trace 桶 `sumTurnTokens`(`useTurnTrace.ts:134`,遍历全部消息 token 字段)+ 模板逐条 `traceTaskRefs(item.traceMsgs)`(`:88`,遍历 toolCalls 提取任务引用)。
|
||||
|
||||
### 1.3 已有的内容级缓存(部分缓解,非本次病灶)
|
||||
|
||||
| 设施 | 锚点 | 粒度 |
|
||||
|------|------|------|
|
||||
| `renderMd` 整段缓存 LRU | `useMarkdown.ts:79`(`_MD_CACHE_LIMIT=200`)、`:195–207` | 文本→HTML |
|
||||
| 代码块装饰缓存 LRU | `MessageList.vue:681–711`(`DECO_CACHE_LIMIT=800`) | 文本→装饰 HTML |
|
||||
| 流式块级 memo + 增量切块 | `useStreamRenderer.ts:59`(`BLOCK_CACHE_LIMIT=300`)、`:67–79` | 单块→HTML |
|
||||
| `MessageItem` 派生 computed | `MessageItem.vue:99–101` | 单条 user 派生值 |
|
||||
|
||||
**缺口(本次目标)**:内容级缓存已就绪,但**列表结构级(renderItems)与 vnode 级(v-memo)缺优化** —— 前者在工具事件时全量重算,后者在每帧重建全列表 vnode。
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与验收口径
|
||||
|
||||
**功能目标**
|
||||
1. `MessageList.vue` 拆分至 **单文件 <400 行**(script+template;style 随子组件 scoped 分流)。
|
||||
2. `renderItems` **记忆化**:按桶缓存,桶内容未变即复用同一 item 对象(稳定引用),不再 O(n) 重建。
|
||||
3. 引入 **`v-memo`**:流式每帧仅重渲"真正变化"的 item,其余 item 跳过 vnode 新建/补丁。
|
||||
4. 指纹 watch **去 `JSON.stringify`**,改 O(1) 版本计数。
|
||||
5. 流式帧渲染**降载**:rAF 合并(已有)+ 离屏段落跳过。
|
||||
|
||||
**验收口径**
|
||||
- 长会话(≥200 条)+ 长回复流式:每帧 patch 的 item 数从"全列表"降到"1(末条)"——Vue DevTools render 计数或 dev 打点可观测;人眼无掉帧、无卡顿。
|
||||
- 工具事件(审批/完成/拒绝)后:轨迹卡统计(`collectTurnStats`)与聚合行正确刷新;`renderItems` 重算次数 ≤ 变更桶数。
|
||||
- 展开/折叠(轨迹卡/思考 pill/JSON/长文/折叠段)即时生效,无"点了没反应"。
|
||||
- 复制/选文字保持/错误重试/子代理 peek/轮次导航跳转等既有交互逐条回归无弱化。
|
||||
- 不倒退:普通单轮问答、历史会话滚动、切换会话行为与改造前逐字节一致。
|
||||
- `vue-tsc --noEmit` 与 `pnpm test:e2e`(Playwright,`tests/e2e/smoke.spec.ts`)全绿。
|
||||
|
||||
---
|
||||
|
||||
## 三、设计方案
|
||||
|
||||
### 3.1 核心判断:三件事正交,可分笔落地
|
||||
|
||||
病灶分处三层,互不耦合,可分笔提交、独立回退:
|
||||
|
||||
- **vnode 层**(每帧) —— `v-memo` 缺 → 补 `v-memo`(不改结构)。
|
||||
- **列表结构层**(每工具事件) —— `renderItems` 全量重算 → 桶 memo 缓存(不改结构)。
|
||||
- **代码组织层**(可维护性) —— God 组件 → 拆 composable / 子组件(行为零变)。
|
||||
|
||||
设计原则:`v-memo` 与 memo 缓存**不改 `renderItems` 输出契约**(仍返回 `RenderItem[]`,`:1018–1028` 类型不变),模板 6 分支结构不变 → 回归面收敛在"渲染策略"而非"渲染语义"。
|
||||
|
||||
### 3.2 `renderItems` 记忆化(稳定 item 对象)
|
||||
|
||||
**输入指纹(桶签名 `sig`)**:仅取 `renderItems` 真正消费的字段(与 `:429` 指纹同源思路,但**按桶**):
|
||||
|
||||
```
|
||||
sig(bucket) =
|
||||
bucket.map(m => m.id + ':' + m.status + ':' + (m.toolCalls ?? [])
|
||||
.map(t => t.id + ':' + t.status + ':' + (t.completedAt ?? 0) + ':' + (t.argsHash ?? '')).join(','))
|
||||
.join('|')
|
||||
+ '#' + finalIndex // 完成态桶的 finalMsg 下标
|
||||
+ '#' + (expandedTraceIds.has(traceKey) ? 1 : 0)
|
||||
```
|
||||
|
||||
**缓存**:组件级 `Map<string, RenderItem[]>`(LRU 有界,如 128,沿用 `_decoCache:681` 同款范式)。命中即回**同一 item 对象数组**(引用稳定);未命中才 `collectTurnStats`/`sumTurnTokens`/`traceTaskRefs` 并写缓存。顶层 `items` 数组仍每次重组(桶数 ≪ 消息数,成本可忽略)。
|
||||
|
||||
**收益**:①工具事件时 O(n)→O(变更桶);②item 引用稳定 → `v-memo` 的 dep 可用 `[item]` 表达(见 3.3)。
|
||||
|
||||
**风险**:签名漏字段 → 命中陈旧 item → 界面不刷新。故签名须覆盖所有影响输出的字段;此风险由 3.3 的保守 dep 兜底(primitive dep 即使签名漏,值变仍会重渲)。
|
||||
|
||||
> 备选(更保守):不引入桶 memo,P0 仅 v-memo。但工具事件时的 O(n) 重算仍在 —— 建议两步都做、分两笔提交。
|
||||
|
||||
### 3.3 `v-memo` 依赖数组口径
|
||||
|
||||
**核心原理**:`renderItems` 是 computed,跨帧被缓存(流式帧不重算)→ item 对象跨帧引用稳定;任其**已读取**的响应式源变化 → 重算 → 新 item 对象 → `[item]` dep 变化 → 该 item 重渲。因此 **dep 只需补"`renderItems` 未读、但模板渲染该 item 时读了"的兄弟响应式源**。
|
||||
|
||||
**已读(自动失效,无需列入)**:`store.state.messages`、`m.status`/`m.role`/`m.content`(经 `shouldRenderMsg`)、`expandedTraceIds`(`:1116`)、`expandedSegmentIds`(`:1152`)、`isViewingGenerating`(读 `streaming`)。
|
||||
|
||||
**未读(必须补入 dep,否则 v-memo 跳过致不刷新)**:
|
||||
|
||||
| 源 | 影响 | 锚点 |
|
||||
|----|------|------|
|
||||
| `expandedThinkIds` | 轨迹卡内思考 pill 展开 | `:953/:1559` |
|
||||
| `expandedJsonIds` | JSON 工具结果展开 | `:944/:1711` |
|
||||
| `expandedLongIds` | 长文折叠/展开 | `:891/:898/:1714` |
|
||||
| `tokenPopoverMsgId` | token 详情面板显隐 | `:180` |
|
||||
| `editingMsgId` | user 编辑按钮显隐 | `:1691` |
|
||||
| `agentPeekLoadingId` | 子代理 peek 转圈 | `:1625` |
|
||||
| `_mdRenderKey` | marked 就绪后首屏重渲 | `:457–458/:718` |
|
||||
| 任务标题源 | trace 任务 chip 文案 | `taskTitleOf :83` |
|
||||
|
||||
**推荐写法**(DRY 单出口,补 dep 一处维护):
|
||||
|
||||
```ts
|
||||
// template: <template v-for="item in renderItems" :key="item.key" v-memo="itemDeps(item)">
|
||||
function itemDeps(it: RenderItem): unknown[] {
|
||||
return [it, expandedThinkIds.value, expandedJsonIds.value, expandedLongIds.value,
|
||||
tokenPopoverMsgId.value, editingMsgId.value, agentPeekLoadingId.value,
|
||||
mdRenderVersion.value, taskTitleVersion.value]
|
||||
}
|
||||
```
|
||||
|
||||
- `[it,...]` 依赖"renderItems 重算即新对象"这一既定事实(无需 primitive 枚举,简洁);兄弟集合用**整体引用**(`useToggleSet` 每次 toggle 替换 Set → 引用变 → 全部 item 重渲)。toggle 为用户点击(低频),全量重渲可接受,换取 dep 永不过期。
|
||||
- `taskTitleVersion`/`mdRenderVersion` 用 O(1) 版本号(见 3.4)表达异步就绪源,避免把整个 `skills`/`tasks` 数组塞进 dep。
|
||||
|
||||
**风险与兜底**:若 Vite/Vue 编译器不支持在 `<template v-for>` 上挂 `v-memo`(与内部 `v-if` 链组合),则**回退方案**=为每个 item 包一层 keyed 子组件(即 P2 的 `<AiMessageBubble :item>` 提前),`v-memo` 落在子组件根 —— 属结构性,故列为风险项而非默认路径。
|
||||
|
||||
### 3.4 指纹 watch 改 O(1) 版本计数
|
||||
|
||||
**现状**(`:429–442`):source(`:430–433`)每次工具状态变动作 `messages.map(...)` + `JSON.stringify`,O(n) 分配 + 序列化。
|
||||
|
||||
**改法**:引入 `toolEpoch`(数字 ref,建议落 `aiShared.ts` 与 `convStreamStates` 同层),在**唯一入口** `bumpToolEpoch()` 自增;所有工具状态变更点调用:
|
||||
|
||||
- `useAiToolEvents.ts:39`(push 新 toolCall)、`:51`(completed)、`:101`(pending_approval)、`:119`(rejected);
|
||||
- `useAiApproval.ts:87`(running/rejected)、`:109`(completed);
|
||||
- `useAiConversations.ts:501`(待审批回填)、`useAiStreamingEvents.ts:123`(待审批)。
|
||||
|
||||
watch 改:
|
||||
|
||||
```ts
|
||||
watch([() => store.state.messages.length, () => toolEpoch.value], () => {
|
||||
if (isFollowingBottom.value) collapseAllToolLists(buildActiveToolIds(store.state.messages))
|
||||
})
|
||||
```
|
||||
|
||||
**收益**:每次工具事件从 O(n) map+stringify 降为 O(1) 计数;`lastMsgSnapshot` 去重串可删(watch 已按值变化触发)。**代价**:需在 ~7 处 mutation 点补 `bumpToolEpoch()` —— 建议收敛到一个 `markToolChanged()` 助手,避免散点漏调(漏调 = 折叠联动不触发,非渲染错误,风险低)。
|
||||
|
||||
### 3.5 流式帧渲染降载
|
||||
|
||||
1. **rAF 合并** —— 已有(`useStreamRenderer.scheduleStreamParse:225`),每帧仅一次 parse;保持不动。
|
||||
2. **v-memo**(3.3)—— 每帧仅末条 item 重建 vnode,是**最主要的降载**(治"逐帧全列表 vnode 重建")。
|
||||
3. **离屏段落跳过** —— 对 `.ai-msg-slot` 加 CSS `content-visibility: auto` + `contain-intrinsic-size`(浏览器级跳过离屏布局/绘制,零 JS)。**风险**:与 `scrollToBottom`(`anchorCollapseScroll:354` 读 `getBoundingClientRect`)可能因离屏高度估算产生滚动抖动 → 建议 P1 引入并实测,回退=去该 CSS 类。
|
||||
4. **renderItems 不读流式正文** —— 现状已满足(正文回填在 `aiShared.ts:544` 收尾单点,delta 期间不改 `msg.content`);设计上**明确此约束不得破坏**(勿在 `renderItems` 内引入 `store.state.currentText` 读取),否则 computed 每 delta 重算,前功尽弃。
|
||||
|
||||
### 3.6 拆分方案(职责切分 + 数据流)
|
||||
|
||||
**数据流不变**:`store.state.messages`(真相)→ `messageSegments` → `renderItems`(`RenderItem[]`)→ 模板分支渲染。拆分仅重排"谁来持有逻辑"。
|
||||
|
||||
**P1 — 抽出 composable(模板不动,纯脚本迁移 + 依赖注入)**:参照已抽的 `useStreamRenderer`(回调注入)范式:
|
||||
|
||||
| 新 composable | 迁出内容 | 入参(注入) | 出参 |
|
||||
|---------------|----------|-----------|------|
|
||||
| `useRenderItems` | `messageSegments`/`renderItems`/时间线/traceNav + 桶 memo | messages、`isViewingGenerating`、各 expanded 集合 | `renderItems`、`traceNav`、`jumpToTrace` |
|
||||
| `useTracePanel` | trace 展开/task refs/轮次导航 + agent peek 浮层 | store、`expandedTraceIds` | `toggleTrace`、`traceTaskRefs`、`openAgentTrace`/`closeAgentTrace`、`agentPeek*` |
|
||||
| `useMessageActions` | 复制(文本/图片)/重试/装 obscura/去设置 | store、`emit`、`scrollToBottom` | `copyMsgContent`、`handleRetry`、… |
|
||||
| `useTextSelection` | 保存/恢复选区 + 偏移定位 + 代码块复制委托 | `messagesContainer` | `saveSelection`/`restoreSelection`、`onMdCodeCopyClick` |
|
||||
| `useTokenUsage` | token 派生 + popover 状态 | — | `token*Of`、`tokenPopoverMsgId`、`toggleTokenPopover` |
|
||||
|
||||
**P2 — 抽子组件(模板分流 + style scoped 随迁)**:
|
||||
|
||||
| 子组件 | 迁出模板 | 目标 |
|
||||
|--------|----------|------|
|
||||
| `TraceCard.vue` | trace(`:1510–1583`)+ step(`:1587–1596`)同构两态 + rail 面板 | 含内部 `v-memo`/computed,思考 pill 展开自治 |
|
||||
| `AgentFinishedCard.vue` | agentFin(`:1601–1627`) | peek 点击 emit 上抛 |
|
||||
| `AiMessageBubble.vue` | msg 的 assistant/system/json/长文折叠/错误操作(`:1697–1784`)+ 流式块(`:1727–1730`) | 单条气泡自治,`item` 作 prop |
|
||||
| `TimeSeparator.vue` / `SegmentSeparator.vue` | timesep(`:1629`)/sep(`:1650–1671`) | 小件,可留 MessageList |
|
||||
|
||||
拆完后 `MessageList.vue` 余:容器 + 滚动 + 编排 `v-for` + 事件委托(目标 <400 行)。
|
||||
|
||||
**高内聚低耦合要点**:子组件只收 `item` 数据 + 经 `emit` 上抛动作(不直接持 `store` 写);`ToolCardList`/`MessageItem` 已是此模式,新组件对齐。
|
||||
|
||||
---
|
||||
|
||||
## 四、分期(每期可独立上线、可回退)
|
||||
|
||||
### P0 — 零风险渲染层(v-memo + 桶 memo + 版本计数)**不改结构**
|
||||
- 补 `v-memo`(`itemDeps`,3.3);`renderItems` 桶 memo(3.2);指纹 watch 改 `toolEpoch`(3.4)。
|
||||
- **不改**模板分支、DOM 契约(`data-msg-id`/role class)、`RenderItem` 类型。
|
||||
- 建议拆 **两笔**提交:`v-memo` 一笔、桶 memo+版本计数一笔 → 可分笔 revert。
|
||||
- 验收:§二"每帧仅末条 patch"人眼/打点;工具事件统计刷新正确;toggle 即时生效;`vue-tsc` + e2e 全绿。
|
||||
|
||||
### P1 — 拆 composable + 离屏降载(模板不动)
|
||||
- 抽 `useRenderItems`/`useTracePanel`/`useMessageActions`/`useTextSelection`/`useTokenUsage`(3.6);`.ai-msg-slot` 加 `content-visibility`(3.5-3)。
|
||||
- 验收:`MessageList.vue` script 显著下降;所有交互回归;滚动锚点无抖动;e2e 全绿。
|
||||
|
||||
### P2 — 拆子组件(结构 + style 分流)
|
||||
- 抽 `TraceCard`/`AgentFinishedCard`/`AiMessageBubble`(3.6);style scoped 随迁。
|
||||
- 验收:单文件 **<400 行**;全交互 + e2e 全绿;`v-memo` 下沉各子组件(可改用 `[item]` 简化)。
|
||||
|
||||
---
|
||||
|
||||
## 五、影响面(五维)
|
||||
|
||||
- **兼容性**:纯前端渲染层重构,不改 IPC / 事件 / DB / 后端协议;`RenderItem[]` 输出契约与模板分支不变;DOM 语义属性保留 → 既有 e2e 选择器/`getBoundingClientRect` 锚点不破坏;`MessageItem`/`ToolCardList` 对外 props/emit 不变。
|
||||
- **破坏性**:无不可逆操作(纯代码,git 可回退)。**行为风险**=`v-memo` dep 遗漏(局部不刷新,可见回归)、桶 memo 签名冲突(串内容)。缓解:dep 保守(3.3)+ 签名含 `id` 唯一性 + LRU 有界。
|
||||
- **安全面**:无新端点/命令执行面;渲染 HTML 仍走既有 `DOMPurify`(`useMarkdown.renderMd`/`parseBlockNoCache`)链,`v-memo` 只跳过补丁、不改变 `v-html` 内容来源,无新 XSS 面。
|
||||
- **资源面**:内存 —— 新增桶 memo Map + dep 数组,有界 LRU(128);CPU —— 每帧 vnode 从 O(全列表) 降 O(1~少数);工具事件 O(n) 重算 → O(变更桶);`content-visibility` 降离屏布局/绘制。工具事件 O(1) 版本计数替代 O(n) stringify。
|
||||
- **回退开关**:`v-memo`/结构拆分属**编译期**,无运行时开关 → 回退粒度=**逐笔 git revert**(故 P0 拆两笔、P1/P2 各自独立)。`toolEpoch` 为本地计数,回退即恢复旧 watch。`content-visibility` 可用 CSS 类开关运行时回退。
|
||||
|
||||
---
|
||||
|
||||
## 六、拍板点清单
|
||||
|
||||
| # | 决策 | 选项 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 1 | `v-memo` 依赖口径 | A 逐 kind primitive 枚举;B `[item, ...兄弟引用]`(依赖 renderItems 缓存保证 item 稳定);C 仅对 msg 分支上 v-memo | **B**。简洁且覆盖充分;前提=3.2 桶 memo 与 3.3 的 sibling 清单正确;A 作理论最保守备选(枚举成本高、易漏) |
|
||||
| 2 | P0 是否同时做桶 memo | A 仅 v-memo(结构零改,工具事件仍 O(n));B v-memo + 桶 memo(拆两笔) | **B**。两者正交、各自可测;桶 memo 治"工具事件级"重算,仅 v-memo 治不了 |
|
||||
| 3 | 离屏 `content-visibility` 时机 | A P0 一起;B P1 引入;C 不做 | **B**。收益大但需实测滚动锚点抖动,单列为 P1 可回退项,风险隔离 |
|
||||
| 4 | P2 子组件边界 | A 抽 TraceCard/AgentFinishedCard/AiMessageBubble;小件(时间线/折叠条)留父;B 仅抽最大件 AiMessageBubble | **A**。目标是 <400 行,需 style scoped 随迁才达标;B 达标不足 |
|
||||
|
||||
---
|
||||
|
||||
## 附:实施纪律
|
||||
|
||||
- `renderItems` 是全仓最热路径,**回归面大** → 遵循"优先级 Bug→重构→新功能、单次 1-2 文件、原子操作"策略(`memory large-refactor-strategy`);每期独立提交 + 独立 e2e。
|
||||
- 改代码一律用 Edit 工具(禁 sed/perl);验证阶段收官统一跑 `vue-tsc` + e2e,不逐文件全量 build(`CLAUDE.md` 编排纪律)。
|
||||
- 存疑处**源码形态优先于文档描述**;`v-memo` dep 清单以本设计 3.3 表为准,实施时逐条 grep 模板引用核对,勿凭空增删。
|
||||
@@ -0,0 +1,230 @@
|
||||
# 输入框列表预览与自动编号 — 交互设计
|
||||
|
||||
> 日期: 2026-10-10 | 状态: 设计稿(待拍板) | 需求锚点: `docs/todo.md:701` 条目 933 | 勘察: 只读实测(文件:行已核)
|
||||
> 目标一句话: ChatInput 输入 `1. ` / `- ` 后,输入框内呈现"列表已就绪"的反馈(高亮/预览)并支持回车续写补号,对齐飞书消息输入框的顺手感,而当前是纯文本无反馈。
|
||||
> 边界: 仅设计,不改码;不涉及后端/协议/落库。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状勘察(锚点实测)
|
||||
|
||||
### 1.1 输入框本体
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 编辑器 | `src/components/ai/ChatInput.vue:20-35` | 单个纯 `<textarea class="ai-input" v-model="inputText">`,`rows="1"`,无富文本、无 overlay、无 mirror。事件: `@keydown/@beforeinput/@input/@paste/@dragover/@drop` |
|
||||
| 键处理 | `ChatInput.vue:814-910` | `handleKeydown` 一条优先级长链:IME 守卫(817)→ Ctrl+R(822)→ 编辑态 Esc(829)→ 技能 Esc(836)→ chip Esc(843)→ 技能导航 ↑↓/Enter/Tab(850-872)→ 技能无匹配 Enter 拦截(875)→ mentions 导航 ↑↓/Enter/Tab(882-905)→ **Enter 发送(906-909)** |
|
||||
| 输入态互斥 | `ChatInput.vue:418-432` | `watch(inputText)`: 以 `/` 开头 → `skillOpen`;否则 `detectMentionTrigger()`。二者互斥(427-431) |
|
||||
| `/` 判据 | `ChatInput.vue:408` | `skillQuery = inputText.startsWith('/') ? slice(1) : ''` —— **仅识别"整串以 `/` 开头"**,行首列表标记 `1. ` 天然不与之撞 |
|
||||
| `@` 判据 | `ChatInput.vue:439-467` | 从光标向前找最近 `@`,要求其前一字符为空/空白;遇空白即停 |
|
||||
| 程序化改文 | `ChatInput.vue:509-532` | 已有成熟范式: `beforeinput` 快照旧文 + `input` 时公共前后缀差分定位编辑区间 → `shiftMentionSpans` 平移/丢弃 `pendingMentionSpans`。注释明确"程序赋值不派发 input 事件"(508-509) |
|
||||
| 高度自适应 | `ChatInput.vue:916-922` | `autoResize()`: `height='auto'` → `min(scrollHeight, INPUT_MAX_HEIGHT=200)` |
|
||||
| 光标定位 | `ChatInput.vue:801-811` | `selectMention` 里 `setSelectionRange` + 同步重算高度,是既有的"程序化插字+回光标"样板 |
|
||||
| 样式 | `ChatInput.vue:1201-1215` | `.ai-input { flex:1; font-size:13px; line-height:1.5; font-family:var(--df-font-sans); resize:vertical; height:20px; max-height:200px; overflow-y:auto }`。外层 `.ai-input-wrap`(1177) `position:relative` + `display:flex; flex-wrap:wrap` |
|
||||
|
||||
### 1.2 既有浮层/提及基础设施(可复用)
|
||||
|
||||
| 件 | 锚点 | 复用点 |
|
||||
|----|------|--------|
|
||||
| 技能浮层 | `SkillMention.vue:43-62` | `.ai-skill-popover` 绝对定位浮层 |
|
||||
| `@` 浮层 | `MentionPopover.vue:39-64` | 同款 `.ai-skill-popover`,仅加组标题 |
|
||||
| 浮层定位 | `src/styles/components.css:254` | `.ai-skill-popover { position:absolute; bottom:calc(100% + 4px); left:0; right:0; max-height:240px; overflow-y:auto; z-index:20 }` —— 浮层锚在输入框**上方**,输入框整体透明底(`.ai-input { background:transparent }`) |
|
||||
| 已选技能/图片预览 | `ChatInput.vue:16-19` | 输入框上方已有 chip/预览条先例(ImageInput),说明"输入框上方加一条反馈带"是既有形态 |
|
||||
|
||||
### 1.3 发送与渲染链(证明"发送侧零改动")
|
||||
|
||||
| 件 | 锚点 | 现状 |
|
||||
|----|------|------|
|
||||
| 发送 | `ChatInput.vue:924-1046` | `handleSend` 取 `inputText.trim()` 原样透传 `store.sendMessage(text, ...)`,不做任何 markdown 预处理 |
|
||||
| 渲染器 | `src/composables/useMarkdown.ts:115-132` | `marked.setOptions({ breaks:true, gfm:true })` —— **有序/无序列表本就渲染为 `<ol>/<ul>`** |
|
||||
| 列表样式 | `src/styles/ai-md.css:8-9` | `.ai-md ul, .ai-md ol { margin:4px 0; padding-left:20px }` / `.ai-md li { margin:2px 0; line-height:1.5 }` |
|
||||
| 文本预处理先例 | `useMarkdown.ts:239-299` `wrapNakedDiff` | marked.parse 前做文本预处理的既有范式;其守卫1(纯 `-` 列表不误包成 diff)说明**已考虑"`- ` 是普通 markdown 列表"的语义** |
|
||||
|
||||
**结论**: 消息落地后列表渲染链完整,本需求是**纯输入侧体验缺口**——输入阶段无任何"这是列表"的反馈、无回车续写、无自动补号。
|
||||
|
||||
### 1.4 缺口清单
|
||||
|
||||
1. 输入 `1. ` / `- ` / `* ` / `+ ` 后无任何视觉反馈(纯文本),与飞书"行首即成型"的手感差距明显。
|
||||
2. 回车续写不补号:按 Shift+Enter 换行后,下一行不会自动带 `2. ` / `- `。
|
||||
3. 空列表项无退出:清空某行内容后标记 `1. ` 残留,需手工删。
|
||||
4. 更深度的是"自动重排号"(中间插一项后续号重排)完全缺失。
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与验收口径
|
||||
|
||||
| 目标 | 验收口径(可实测) |
|
||||
|------|-------------------|
|
||||
| G1 触发识别 | 行首匹配 `^(≤3 空格)(\d+\. )` / `^(≤3 空格)([-*+] )` 即判定为列表项;` ``` ` 围栏内不触发 |
|
||||
| G2 回车续写补号 | 在非空列表项行末按 **Shift+Enter** → 新行自动前缀正确序号(`n` → `n+1`)或同型符号(`- `→`- `),光标落新标记后 |
|
||||
| G3 空项退出 | 空列表项行(仅标记+空格)按 **Shift+Enter** 或 **末位退格** → 标记被移除,退出列表态,不产生新项 |
|
||||
| G4 视觉反馈 | 列表行标记(`1.` / `- `)在输入框内获得区别于正文的样式(高亮/等宽/色块);无法在纯 textarea 内着色时,以"输入框上方列表预览条"替代(见 §3.3) |
|
||||
| G5 不打架 | `/` 技能联想、`@` 提及、编辑态、IME 组词、代码围栏优先级正确;开启本能力后既有行为零回归 |
|
||||
| G6 发送不变 | 发送文本仍为含字面标记的原文,消息气泡经 marked 渲染为列表——发送链/后端/落库零改动 |
|
||||
|
||||
---
|
||||
|
||||
## 三、设计方案
|
||||
|
||||
### 3.1 总体判断(先说结论)
|
||||
|
||||
**纯 `<textarea>` 无法在"框内"渲染真正的列表项**(圆点/编号缩进)——textarea 只能显示等宽字符流,无法对子串应用 bullet + hanging indent。因此"框内所见即所得"只有两条路:
|
||||
|
||||
| 档 | 实现 | 能力 | 成本/风险 |
|
||||
|----|------|------|-----------|
|
||||
| **档一 · 轻量** | 保留 textarea + 逻辑层(识别/补号/退出)+ 可选 mirror 高亮 | 补号、退出、标记高亮(字面仍为 `1.`,但高亮显示) | 低-中;mirror 需 textarea 文字透明化,有选区可见性坑 |
|
||||
| **档二 · 完整** | contenteditable 富文本编辑器(真 `<ol>/<ul>`) | 真 WYSIWYG、可嵌套/拖动 | 高;替换 textarea,击穿 IME/`@`span/粘贴图/autoResize 全部既有机制 |
|
||||
|
||||
**建议先做档一**,档二单独立项(见 §6 拍板点 2)。理由:档一达成飞书体验的 ~90%(续写补号是主力价值),且完全不碰已验证多年的输入链;档二是输入层重写,风险与工作量不成比例。
|
||||
|
||||
### 3.2 逻辑层设计(档一核心,必做)
|
||||
|
||||
抽 `src/composables/ai/useListContinuation.ts`(纯函数,可单测),不改 textarea 渲染。
|
||||
|
||||
**数据结构**(仅内存,不落库):
|
||||
|
||||
```ts
|
||||
export interface ListContext {
|
||||
kind: 'ordered' | 'unordered'
|
||||
indent: string // 行首缩进(≤3 空格,保留)
|
||||
marker: string // 字面标记,如 "3. " / "- "
|
||||
number?: number // ordered 的序号(含 . 或 ) 后解析)
|
||||
bullet?: '-' | '*' | '+'// unordered 的符号字符
|
||||
content: string // 标记后的正文(可空)
|
||||
lineStart: number // 该行在 inputText 中的起始偏移
|
||||
markerEnd: number // 标记结束偏移(标记后第一个正文字符)
|
||||
}
|
||||
|
||||
/** 判定给定文本 + 光标所处行的列表语境;围栏内/无匹配返回 null */
|
||||
export function analyzeListContext(text: string, caret: number): ListContext | null
|
||||
|
||||
/** 续写:返回应插入的标记串(ordered → `${n+1}. `;unordered → 同型) */
|
||||
export function nextMarker(ctx: ListContext): string
|
||||
|
||||
/** (可选增强)重排:给定文本,把连续 ordered 段重编为 1,2,3…;返回新文本 + 需平移偏移 */
|
||||
export function renumberOrdered(text: string): { text: string; renumbered: boolean }
|
||||
```
|
||||
|
||||
**触发判据(实现细节)**:
|
||||
|
||||
- 有序: `/^(\s{0,3})(\d{1,9})[.)]\s(.*)$/`(兼容 `.` 与 `)`;markdown 允许 1-9 数字 + `.`/`)`)
|
||||
- 无序: `/^(\s{0,3})([-*+])\s(.*)$/`
|
||||
- **围栏守卫**: 从文本第 0 行扫到当前行前,统计 `^\s{0,3}(```|~~~)` 出现次数,奇数 → 处于代码围栏内 → 返回 `null`(不在围栏内把 `- 1.` 之类误判为列表)。复用 `useMarkdown.wrapNakedDiff` 同款 `_FENCE_OPEN_RE` 思路。
|
||||
- 光标必须落在当前行(用 `\n` 计数定位行号),避免"光标在别行却对当前行操作"。
|
||||
|
||||
**续写流程(Shift+Enter)**: 在 `handleKeydown` 里新增分支(位置见 §3.4):
|
||||
|
||||
```
|
||||
Shift+Enter 且非 skillOpen/mentionOpen/编辑态:
|
||||
ctx = analyzeListContext(inputText, caret)
|
||||
if !ctx: return(交默认换行)
|
||||
if ctx.content.trim() === '':
|
||||
// 空项 → 退出列表:删除整行标记,留下空行
|
||||
preventDefault; 删除 [lineStart, markerEnd) 区间;
|
||||
shiftMentionSpans(旧, 新); 回光标; return
|
||||
preventDefault
|
||||
insert = '\n' + ctx.indent + nextMarker(ctx)
|
||||
在 caret 处插入 insert; 光标置插入末
|
||||
shiftMentionSpans(旧, 新) // 复用 509-532,列表插字同样会平移其后的 @span
|
||||
autoResize(); updateMirror()
|
||||
```
|
||||
|
||||
> 注意: 程序化赋值不触发 `input` 事件(`ChatInput.vue:508-509` 已注明),故 `shiftMentionSpans` 必须**显式手动调用**(不能依赖 `onInputText`),与 `selectMention` 的处理保持一致。
|
||||
|
||||
**退格退出(B3)**: 在 `handleKeydown` 增 `Backspace` 分支:光标在行末且当前行为空列表项(`^(\s*)([-*+]|\d+[.)])\s$`)→ preventDefault,删标记,退出列表。
|
||||
|
||||
**自动重排号(G-增强,可选)**: 默认**不做**。true "自动编号" 需在中间插入/删除项后重写后续行号(`1. 2. 3.` → 插一行后 `1. 2. 3. 4.`),会改动用户已输入文本 → 触碰光标/`@`span 偏移。若做,须走完整"旧文差分 → 重排 → 重新定位光标与 span",成本与回归面显著上升,建议单独开关(见 §6 拍板点 3)。
|
||||
|
||||
### 3.3 视觉反馈设计(三选一,见拍板点 4)
|
||||
|
||||
| 形态 | 实现 | 优 | 劣 |
|
||||
|------|------|----|----|
|
||||
| **V0 零视觉** | 只做补号/退出 | 零渲染风险,一文件 | 不满足"而非纯文本"的观感诉求 |
|
||||
| **V1 mirror 高亮(推荐目标)** | 在 textarea 下垫一层等宽 mirror div,渲染原文(转义),把列表标记包成 `<span class="ai-list-marker">` 高亮;textarea 文字设 `color:transparent; caret-color:var(--df-text)`,可见字符来自 mirror;同步 `scrollTop`/高度 | 字面仍为 `1.` 但**框内可见高亮 + 悬挂缩进**,最接近飞书 | 需精确对齐 font/line-height(13px/1.5)、尾行高度、`resize:vertical`、滚动;**选区可见性坑**:透明文字下 `::selection` 只剩底色、字形不可见,需 `textarea::selection{ background:… }` 兜且体验打折 |
|
||||
| **V2 预览条** | 输入框上方(ImageInput 同位置范式)一条折叠/淡显预览,复用 `useMarkdown.renderMd(inputText)`(已 DOMPurify 净化)渲染当前输入 | 复用既有渲染单例,零新解析器;能看到真实序号/圆点 | 非"框内",占垂直空间,与飞书形态有差 |
|
||||
|
||||
**建议**: 先 V0 落地 G1-G3/G5/G6(最小可用,零渲染风险),再把 **V1 作为独立增强步**单独评估(透明化对选区/无障碍的影响需实测);V2 作为可选辅助保留。不建议一步到位 V1,因其 textarea 透明化是全局输入体验改动,面比"列表"本身大。
|
||||
|
||||
若采用 V1,样式落 `src/styles/components.css`(与 `.ai-skill-popover` 同处,因 scoped 不渗透):
|
||||
|
||||
```css
|
||||
.ai-input-field { position: relative; flex: 1; display: flex; } /* 新包裹层,仅包 textarea */
|
||||
.ai-input-mirror {
|
||||
position: absolute; inset: 0; overflow: hidden; pointer-events: none;
|
||||
font: inherit; font-size: 13px; line-height: 1.5; font-family: var(--df-font-sans);
|
||||
white-space: pre-wrap; word-break: break-word; color: var(--df-text);
|
||||
padding: 0; /* 与 textarea 完全一致,靠 .ai-input-wrap 的 6px 8px 内边距 */
|
||||
}
|
||||
.ai-list-marker { color: var(--df-accent); font-weight: 600; }
|
||||
```
|
||||
|
||||
> mirror 文本必须用 `document.createTextNode` 逐段拼接(**禁用 v-html 直插原始输入**,防 XSS);仅对已确定的标记区间包 span。若日后想直接渲染整段 markdown 预览,只能走 V2 的 `renderMd`(已 `_purify.sanitize`,189-208)。
|
||||
|
||||
### 3.4 与既有机制的优先级(冲突落地)
|
||||
|
||||
新增分支在 `handleKeydown`(814-910)的**精确插入点**:
|
||||
|
||||
```
|
||||
IME 守卫(817) → Ctrl+R(822) → 编辑态 Esc(829) → 技能 Esc/chip(836-848)
|
||||
→ 技能导航(850-872)/技能无匹配 Enter(875)
|
||||
→ mention 导航(882-905)
|
||||
→ 【新增】Backspace 空项退出 · 【新增】Shift+Enter 列表续写 ← 放在此处
|
||||
→ Enter 发送(906-909)
|
||||
```
|
||||
|
||||
- **`/` 技能**: `skillOpen` 仅当"整串以 `/` 开头"(408/423),列表标记在行首,首行不可能是 `/`;续行也不影响 `startsWith` 判定 → **不撞**。守卫:续写分支前置 `!skillOpen`。
|
||||
- **`@` 提及**: 续写时若光标处 popover 未开,insert 后调 `shiftMentionSpans` 平移其后的 span;若 popover 开着(`mentionOpen`),用户意图是选实体,续写分支前置 `!mentionOpen`。
|
||||
- **代码围栏 ` ``` `**: 由 `analyzeListContext` 的围栏守卫直接排除(§3.2),`- 1.` 在围栏内不触发,与 `wrapNakedDiff` 守卫3一致。
|
||||
- **IME/isComposing(817)**: 续写分支位于其后,组词中的 Shift+Enter 不误触。
|
||||
- **Enter 发送不变**: 本设计**不改 Enter 发送语义**——续写绑定 Shift+Enter(换行链),Enter 仍发送。为最小改动。若要与飞书"Enter 续行"完全一致需改语义,见拍板点 1。
|
||||
|
||||
### 3.5 UI 形态与文案
|
||||
|
||||
- 无新浮层;V1 高亮走 mirror,V2 预览条若采用则置 `.ai-input-wrap` 之后、`EnrichmentPanel`(`ChatInput.vue:113-121`)之前。
|
||||
- 提示语: 可选在 `.ai-input-hint`(122,`aiChat.inputHint` zh:159)追加"列表续行 Shift+Enter",或单独加一行淡提示。新增 i18n key 落 `src/i18n/zh-CN/aiChat.ts` + `src/i18n/en/aiChat.ts`(现有 key 区见 130-165)。
|
||||
|
||||
---
|
||||
|
||||
## 四、分期与最小可用
|
||||
|
||||
| 期 | 范围 | 交付 | 依赖 |
|
||||
|----|------|------|------|
|
||||
| **P1(最小可用)** | 逻辑层 + V0 | `useListContinuation.ts`(analyze/nextMarker)+ ChatInput 接线(Shift+Enter 续写、Backspace 空项退出、围栏守卫)+ 开关 | 单测覆盖判定/续写/围栏 |
|
||||
| **P2(视觉)** | V1 mirror 高亮(或 V2 预览条) | 框内标记高亮 + 悬挂缩进;样式入 components.css | P1 稳定 + 选区/无障碍实测通过 |
|
||||
| **P3(增强,可选)** | 自动重排号(renumber) | 中间插/删项后重编序号;独立开关 | P1/P2 无回归 |
|
||||
|
||||
**最小可用定义**: P1 完成即视为需求达标可发布——触发识别 + 续写补号 + 空项退出三件事齐,发送侧零改动。视觉(V1/V2)与重排(P3)为增量。
|
||||
|
||||
---
|
||||
|
||||
## 五、影响面(新能力引入评估纪律 · 五维)
|
||||
|
||||
| 维度 | 判定 |
|
||||
|------|------|
|
||||
| **兼容性** | 纯前端输入侧行为,**无协议/落库/迁移**。既有草稿(`convDrafts`)、编辑态回填、队列回填文本语义不变(仍是含字面标记的 markdown 原文)。旧会话/历史消息不受影响(渲染链本就支持列表)。**PASS** |
|
||||
| **破坏性** | 增分支只拦 Shift+Enter/Backspace 的**特定空项/列表场景**,其余键行为不变;Enter 发送语义**不变**(拍板点 1 若选改才涉及行为变更,且可逆)。无不可逆操作。**PASS(低)** |
|
||||
| **安全面** | 无新端点/命令执行/路径。**关键**: mirror/预览渲染用户输入时必须转义——mirror 用 textNode 逐段拼接;若用 v-html 走 `renderMd`(已 `DOMPurify.sanitize`,`useMarkdown.ts:201`)。禁裸 `v-html=原始输入`。新增攻击面为零,仅为既有 XSS 防护纪律的又一次适用。**PASS** |
|
||||
| **资源面** | 每次键入一次纯字符串扫描(O(行数),典型 <100 行)一次可选 mirror DOM 更新。无需 rAF/防抖(P3 重排号若开,是 O(n) 文本改写,仍可控)。无磁盘/日志/内存增量。**PASS** |
|
||||
| **回退开关** | 设 appSettings 键(沿用现网 `df-ai-streaming-md` / `df-ai-auto-execute-mode` 范式,`useAppSettingsStore().get<boolean>('df-ai-list-input', false)`),默认**关**再灰度;单点置 false 即完全回退到现状 textarea 行为,零残留。**PASS(带开关)** |
|
||||
|
||||
---
|
||||
|
||||
## 六、拍板点(需人定,附建议)
|
||||
|
||||
1. **Enter 语义**: 保留"Enter=发送,Shift+Enter=换行并续列表"(**建议**,零回归、不违 `aiChat.inputHint` 与 `handleKeydown:906` 既有契约) vs 改为"列表态下 Enter 续行、Cmd/Ctrl+Enter 才发送"(更贴飞书,但变更已固化的发送习惯,置信成本高)。
|
||||
2. **渲染档位**: 先做**档一 轻量**、档二 WYSIWYG 单独立项(**建议**) vs 直接上档二 contenteditable 富文本。档二会击穿 IME/`@`span/粘贴图/autoResize 全链,工作量与回归面远超本需求。
|
||||
3. **范围边界**: 标记仅 `\d+. ` + `- `(**建议**起步) vs 含 `)`、`*`、`+`、嵌套缩进、以及是否做**自动重排号**(renumber 改用户文本,单独开关)。
|
||||
4. **视觉形态**: V0 零视觉先行 → V1 mirror 框内高亮(**建议路径**,但需实测 textarea 透明化的选区/无障碍影响) vs 直接 V1 vs 用 V2 输入框上方预览条替代。(此项与拍板点 2 联动:若否决档二,则 V1 是"框内感"的唯一可达形态。)
|
||||
|
||||
---
|
||||
|
||||
## 附:实施落点清单(供实施会话直接取用)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/composables/ai/useListContinuation.ts` | 新增:`analyzeListContext` / `nextMarker` / 围栏守卫 /(可选)`renumberOrdered` + 单测 |
|
||||
| `src/components/ai/ChatInput.vue` | `handleKeydown`(814-910)插入 Shift+Enter/Backspace 分支;`<script>` 引入 composable;V1 时包一层 `.ai-input-field` + `<div class="ai-input-mirror">` 并同步 scroll/height;读开关 `df-ai-list-input` |
|
||||
| `src/styles/components.css` | V1:`.ai-input-field` / `.ai-input-mirror` / `.ai-list-marker` |
|
||||
| `src/i18n/zh-CN/aiChat.ts` + `src/i18n/en/aiChat.ts` | 可选:续行提示文案 |
|
||||
|
||||
> 参照档: `无人值守长任务-设计-2026-10-04.md`(结构/详略) · `上下文三层记忆-设计-2026-10-04.md` · `跨会话召回最小闭环-设计-2026-10-04.md`(同一目录写法)。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 创建: 2026-06-10 | 状态: 设计中 | 最后重写: 2026-06-15 (DOC-01 硬伤修复)
|
||||
>
|
||||
> **本文档为 ARCHITECTURE.md 的实质载体**(项目无独立 ARCHITECTURE.md 文件)。所有数据模型、设计决策均以源码为基准,已剔除虚构内容。
|
||||
> **项目架构总纲见根 [ARCHITECTURE.md](../../../ARCHITECTURE.md)**,本文档为其补充细节。所有数据模型、设计决策均以源码为基准,已剔除虚构内容。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -287,3 +287,75 @@ graph TD
|
||||
- ✅ **①-补 双取锁合并(2026-10-10 实施完成)**:`keyword_fallback_commit`(context_lifecycle.rs)内单锁一并 `set_compressing(false)` + 兜底提交,消「锁外复位→再取锁提交」的并发二次压缩窗口;复位置于 `KEYWORD_FALLBACK_ENABLED` 门控之前(开关关闭同样复位,否则标志永真致压缩停摆);两处调用点(provider 重建失败/LLM 失败)的独立复位块删除。验收:`cargo check -p devflow` EXIT 0 + `cargo test -p devflow --lib` **749 passed/0 failed**。
|
||||
- ✅ **② 无损归位(2026-10-10 实施完成)**:`crates/df-ai/src/context/sanitize.rs` 新增 step 0.5 `reorder_interlopers_after_tool_run`——楔入 assistant 头与其 tool 响应之间的异类消息(压缩摘要/用户插话/警示 system)**整体移到该组响应之后**(只重排不删内容),归位后 step1 邻接判定即认闭合,工具轮**不再被剥除**;仅对「单头 + 夹层 + 紧随该头的连续 tool 段」生效,遇下一个 tool_call 头即停(不跨单元),其余畸形仍走剥除兜底。幂等。单测替换旧剥除用例为 `sanitize_reorders_interloper_to_keep_tool_round` + 新增 `sanitize_reorder_is_idempotent`。验收:`cargo test -p df-ai --lib context` **156 passed/0 failed**。
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-10 分拆(已完成内容再分离)
|
||||
|
||||
- [x] P2-2 高频可点 div 键盘化:ToolCard:11 卡头/MessageList:1423 轨迹头/AuditLog:67,119 审计行(@click div 全仓 56 处,去 mask 合规项约 20 真缺口,高频改 button 其余 tabindex+keydown) ✅ **2026-10-10 完成(named 高频点)**:四面包裹处均补 `role="button" tabindex="0"` + `@keydown.enter/.space.prevent` 复用原 handler(带展开态的补 `:aria-expanded`;AuditLog 的 `<tr>` 仅补 tabindex+keydown——加 role=button 会抹掉表格行语义,故不加)。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**:全仓另 ~16 处 @click div 未清(见 P3-2)。
|
||||
### 🔍 2026-06-22 miniapp 功能走查(workflow wf_ddd49ac8 · 5 域并行核验 + 主代终态抽查)
|
||||
|
||||
> 用户 /review miniapp + workflow 汇总安排。5 agent 独立 grep/read 核验源码(不信注释/文档) + 主代终态核验(ws.ts 真路径 `src/api/ws.ts`、`relay.rs:255/355`、remote_bridge 命令数 10 非 11)。**18 findings → 去重 P1×5 / P2×3 / P3×1**,2 项 refuted。关联 F-260620-01 阶段4(真机联调前置)。**守 session-role-diagnose-only:本会话仅走查 + 登记,未实施代码。**
|
||||
|
||||
**P1(阻塞真机/可靠性·5 项)**:
|
||||
|
||||
|
||||
**用户补充核验(2026-06-22 用户详读源码+设计文档,主代逐项核源码判真伪)**:
|
||||
|
||||
> ⚠️ 用户分析 P0 三项(协议三重不匹配 / EventBus 未接 / 桥接层占位)**经源码核验全部失实**,系基于 Phase3 阶段1-3 落地前的设计文档「计划态」描述,未核当前代码。证据:
|
||||
> - **P0-1 协议**:`crates/df-tunnel/src/tunnel.rs:457/327/424` tunnel 纯透传 payload `Value` 上抛 on_command,**不**做 `from_value::<TunnelCommand>` 强类型反序列化(方案A 阶段1 已落地 commit 0fb5af5)→ miniapp `{cmd,args}` 反序列化在桥接层 `remote_bridge.rs:96 from_payload` 成功,非阻断。tunnel.rs:457 注释自证「不反序列化为 TunnelCommand 强类型」。
|
||||
> - **P0-2 EventBus**:`src-tauri/src/lib.rs:128` `state.ai_event_bus.subscribe()`(tunnel subscriber 已接)+ `remote_bridge.rs:303/332/358/388` + `commands/ai/mod.rs:209/225` `publish_event` 跨端透传。**已双写接入**(todo line 760 commit 0fb5af5/612de4c emit 双写 chat19/audit12/guard3/stream_recv2/agentic18)。`event_bus.rs:53/115` 的 `#![allow(dead_code)]`+「骨架阶段」注释是**过时历史标注**(publish_event 现有调用方 remote_bridge/mod),非未接入。
|
||||
> - **P0-3 桥接层**:`remote_bridge.rs` 完整实装(`handle_remote_command`:121 + `MiniCommand`:83 + `from_payload`:96 + match 路由 10 条 + `route_list/load_messages` publish_event)+ **已 wire** `lib.rs:150-154` `on_command = Arc::new(...handle_remote_command...)` + `:171` `.connect(&relay_url,&device_id,&token,on_command.clone())`。非占位/设计态。`remote_bridge.rs:40-42` 的 `#![allow(dead_code)]`+「零调用方」注释写于联调前,现 lib.rs 已调(过时标注,待清)。
|
||||
|
||||
**剩余开放项**:P3-B-260622 [P3·真机前置] — **manifest appid 占位**。`PLACEHOLDER` 无法真机调试,需微信公众平台注册的真 appid。— `apps/df-miniapp/src/manifest.json`
|
||||
|
||||
### 🔍 2026-08-01 走查剩余待办(P2/P3/专项·本会话登记)
|
||||
|
||||
> 本次走查剩余项登记,未实施代码(守 session-role-diagnose-only)。专项已落 memory,此处留指针。
|
||||
|
||||
**P2(中期改进)**:
|
||||
|
||||
|
||||
**P3(风格/可读·低优)**:
|
||||
|
||||
|
||||
**专项(已登记 memory,留指针)**:
|
||||
|
||||
- 📌 **WC-专项-AI-router-sanitize** — AI router + sanitize 链路梳理重构 — 详见 memory(已登记,本会话不实施)
|
||||
- 📌 **WC-专项-run_agentic_loop-重构** — `run_agentic_loop` 重构(ARC-260619-05 agentic:318 超长,历史登记) — 详见 memory(已登记,本会话不实施)
|
||||
|
||||
**技能注入(待定位)**:
|
||||
|
||||
- [x] **WC-技能注入-trace定位** — 技能/skill 注入链路待 trace 定位(注入时机、上下文来源、是否重复注入),定位后再决定是否待办化 ✅ **2026-10-10 核验销账**: 已由 4b445e9(2026-10-01)「技能前导斜杠解析与规程包裹语」根治(打字 `/技能名` 视同浮层选中,零触达问题消除);注入链已收敛为 augmentation 模块统一入口 `chat.rs resolve_and_inject`(调用点 :409/:938/:2153/:2415),同名去重取首份 `skills.rs:33`。
|
||||
|
||||
---
|
||||
|
||||
### 🔍 2026-08-09 任务/仪表盘/灵感知识走查(3 路并行 · 阶段②第一批)
|
||||
|
||||
> 详单见 [任务+仪表盘+灵感知识走查-2026-08-09.md](./05-代码审查/任务+仪表盘+灵感知识走查-2026-08-09.md)。共 **59 项**(任务19/Dashboard+Projects17/Ideas+Knowledge23,P0=0/P1=7/P2=25/P3=27)。守 session-role-diagnose-only:仅走查登记,未实施代码。
|
||||
|
||||
**P1(确定性,优先修)**:
|
||||
|
||||
**P2(体验/数据)**: WK-8~13 ✅ 已修(0554cc1):快捷菜单状态机+补cancelled / overflow裁切根治(顶底角裁剪下放) / loadTasks seq守卫+keyword / 搜索态隐藏进度徽章 / 分页越界钳制 / 失败统一toast。剩余 WK-14~18 🟡 待后续
|
||||
|
||||
**P3(27 项)**: 见详单文档(TSK-10~19 / DBP-10~17 / IDEA-10~12 / KNOW-6~10 / C-1~3)
|
||||
|
||||
> **优先级**:WK-1 Dashboard 状态映射 → WK-2/3 Knowledge 确定性 → WK-4/5 Ideas → WK-6 状态徽章 → WK-7 错误条统一。
|
||||
|
||||
### 🔍 2026-08-09 Settings/审计/布局/后端命令走查(3 路并行 · 阶段②第二批)
|
||||
|
||||
> 详单见 [settings+审计+布局后端走查-2026-08-09.md](./05-代码审查/settings+审计+布局后端走查-2026-08-09.md)。共 **95 项**(Settings+AuditLog29 / layout+sidebar21 / 后端命令45,P0=0/P1=9/P2=32/P3=54)。守 session-role-diagnose-only:仅走查登记,未实施代码。
|
||||
|
||||
**P1(确定性/安全,优先修)**:
|
||||
|
||||
**P2(体验/数据·32 项)**: 见详单文档(SA-1~8 审计 i18n/脏表单/竞态、LS-2~10 侧栏偏好/小屏布局/审批浮窗/Paginator、BE-CMD-9~23 后端校验/聚合)
|
||||
**P3(54 项)**: 见详单文档(SA-9~31 / LS-11~21 / BE-CMD-24~45)
|
||||
|
||||
> **优先级**:LW-1 ConfirmDialog 安全 → LW-2/3 update 裸奔 → LW-4 queue → LW-5 count/list → LW-6 软删 → LW-8 父聚合。
|
||||
> **全系统索引**:前端 13 + 后端 9 命令全走查完成 ✅,剩 crate df-mcp/df-execute/df-project/df-ideas/df-tunnel/df-relay/df-types。
|
||||
|
||||
- [x] G6 缺陷9 标题泄漏桌面同症随批修 ✅ **2026-10-10 核验销账**:`title.rs:241 sanitize_title_source`(剥 HTML/Markdown 标签,含 `<details>/<summary>`/代码围栏)已接入 `extract_title` 的 LLM 路径(:280)与兜底路径(:298),桌面与飞书同源,缺陷9 桌面同症已随 70d6e98 一并修。
|
||||
|
||||
|
||||
911. [x] **「流式后气泡被清空」真根因(2026-10-10 定位并修复,历次修补无效)**:用户现象=正文流式进气泡 → 轮末气泡清空回到「等待」,每轮工具后都发生、且文本「彻底丢」。**根因(实测定位)**:`MessageList.vue` `renderItems` 的**完成轮分桶**把整轮(含模型正文)折进「执行轨迹卡」且**默认收起**(`flushBucket` 只把最后一个有正文的消息当 finalMsg 出气泡,其余全进 traceMsgs);叠加 `thinkPlainText` 取 `reasoning_content || content` 把正文也当「思考」塞进单行 pill。故流式期间正文可见(currentText 直渲),轮末消息转轨迹卡即「消失」。**修复**:①过程轮里**有正文**的各成一个常显气泡(剥 `toolCalls`,工具卡仍归轨迹卡不重复);②`thinkPlainText` 只认 `reasoning_content`(思考 pill 不再吞正文)。**教训(必须留痕)**:此前 4-5 次修复(`40c4a21` 闪烁/`4652112` 迟到收尾仲裁/`524120b` 切会话残片/`BUG-260624-01` 重叠/`flushCurrentText` guard)全在**状态清空/骨架层**,从未碰**渲染折叠/分桶层** → 故反复不好;凡「流式后内容消失」类投诉,第一嫌疑应是**渲染折叠/分桶**(`renderItems`/trace 桶),而非 streaming/currentText 状态。
|
||||
|
||||
|
||||
|
||||
+44
-57
@@ -2,7 +2,7 @@
|
||||
|
||||
> 创建: 2026-06-10 | 当前阶段: Phase 2 本地优先开发流程验证
|
||||
> 更新: 2026-08-02 | 05-代码审查 补录 2026-08-02 走查批次:DevFlow AI 全库走查(00-05,12 crate/78 问题)、claude 多代理走查(汇总+nodes/ai-storage/ideas-前端/基建 5 文档)、本次修复摘要(5 根因 + 13 agent P0/P1/P2 + 架构专项 3,提交 fc249ad/f719546)
|
||||
> 历次: 2026-06-19 02-架构设计 核对优化(新增分类索引/补录缺失文档/命名规范);06-17 文档缺陷扫描通过(零死链)
|
||||
> 历次: 2026-10-10 按 ls 对齐现存文件(crate 数 8→13;01/04/05/06 子树补录漏登文档);06-19 02-架构设计 核对优化(新增分类索引/补录缺失文档/命名规范);06-17 文档缺陷扫描通过(零死链)
|
||||
|
||||
---
|
||||
|
||||
@@ -15,57 +15,22 @@ docs/
|
||||
├── todo.md # 工作看板(任务/bug 跟踪,~1200 行含历史)
|
||||
├── 待审查.md # 代码审查队列(CR 登记+回填,67 条已审)
|
||||
├── 待决策.md # 需人工决策项(产品取舍/方向选型/排期)
|
||||
├── 待决策论证-2026-06-16.md # (归档) 7 项决策论证(D-01~04 已定 / DEC-01~03 已闭环)
|
||||
├── 01-技术文档/
|
||||
│ ├── SQLite-CRUD模式-2026-06-12.md
|
||||
│ ├── Tauri-IPC模式-2026-06-12.md
|
||||
│ └── AntV-X6调研报告与引入计划-2026-06-30.md # 图可视化层选型(v3.1.7)+ 依赖图落地计划
|
||||
│ ├── AntV-X6调研报告与引入计划-2026-06-30.md # 图可视化层选型(v3.1.7)+ 依赖图落地计划
|
||||
│ ├── 多代理流水线实战经验-2026-10-03.md # 多代理流水线实战经验
|
||||
│ └── 团队级基建-部署与运维-2026-10-05.md # 团队级基建部署与运维
|
||||
├── 02-架构设计/
|
||||
│ ├── INDEX.md # 本目录分类索引(按性质四类 + 命名规范摘要)
|
||||
│ ├── Phase1架构决策-2026-06-12.md
|
||||
│ ├── 业务系统设计-2026-06-12.md
|
||||
│ ├── 产品定位调整-2026-06-12.md
|
||||
│ ├── 前后端类型对齐-2026-06-12.md
|
||||
│ ├── 对抗论证裁决报告-2026-06-12.md
|
||||
│ ├── 功能决策记录-2026-06-14.md # 需求规格 + 设计决策规格
|
||||
│ ├── 功能决策记录-归档-2026-06-14.md
|
||||
│ ├── 功能创意池-2026-06-14.md # 5 架构创意(演化/时效/回溯/债务/契约)
|
||||
│ ├── 经验记录-2026-06-14.md
|
||||
│ ├── 文档记录规范-2026-06-14.md # 含 §八 文档命名规范
|
||||
│ ├── 规格契约自检机制-2026-06-14.md
|
||||
│ ├── Agent架构说明-2026-06-14.md
|
||||
│ ├── B-03-人工审批响应机制-2026-06-14.md
|
||||
│ ├── aichat审查报告-2026-06-14.md # AI Chat 模块代码审查
|
||||
│ ├── aichat异步审批构想-2026-06-14.md
|
||||
│ ├── aichat交互体验改进方案-2026-06-14.md
|
||||
│ ├── aichat授权体验改进方案-2026-06-14.md
|
||||
│ ├── aichat信息密度构想-2026-06-14.md
|
||||
│ ├── 任务推进构想-2026-06-14.md
|
||||
│ ├── 任务推进链实施路径-2026-06-16.md # 4 阶段实施路径(阶段1决策已定)
|
||||
│ ├── aichat流式Markdown渲染调研-2026-06-15.md # 流式渲染优化方案(rAF节流/块级diff/换库)
|
||||
│ ├── patch_file工具设计-2026-06-15.md # patch_file 完整设计(API/三层防御/边界情况)
|
||||
│ ├── 工作流审批审查报告-2026-06-14.md
|
||||
│ ├── F-07-df-ai-core-trait下沉设计-2026-06-14.md
|
||||
│ ├── 密钥迁移健壮性-2026-06-15.md
|
||||
│ ├── 工作流脚本执行边界-2026-06-15.md
|
||||
│ ├── 条件表达式引擎-2026-06-15.md
|
||||
│ ├── generating状态机加固-2026-06-15.md
|
||||
│ ├── AiNode自审实施方案-2026-06-16.md # AiNode 自审闸门+verdict DAG 阻断
|
||||
│ ├── B-260616-21排查方案-2026-06-16.md # 工具卡重复渲染排查(audit去重+前端幂等)
|
||||
│ ├── F-01-模型能力系统与智能路由设计-2026-06-16.md # 模型能力 Phase 3 设计
|
||||
│ ├── F-02-技能联想使用-实施机制设计-2026-06-16.md # / 联想技能 → AI 调用执行机制
|
||||
│ ├── F-05-多模态实施方案设计-2026-06-16.md # 多模态 Phase2a/2b/2c 完整方案
|
||||
│ ├── F-09-多会话并发架构设计-2026-06-19.md # AiSession 多会话并发 B 阶段(当前真相源)
|
||||
│ ├── F-09B-多会话并发设计-2026-06-16.md # (过时·被 F-09 取代) 多会话 A/B 路线架构设计
|
||||
│ ├── F-15-上下文管理增强设计-2026-06-16.md # 会话分段/压缩/裁剪设计
|
||||
│ ├── secret下沉与provider注入方案-2026-06-16.md # secret 纯密钥下沉 df-storage 方案
|
||||
│ ├── 推进链阶段2实施路径-2026-06-16.md # advance_task 走 df-nodes 实施路径
|
||||
│ ├── 意图识别层论证-2026-06-19.md # 通用前置意图识别层 8 维度论证
|
||||
│ ├── 上下文管理演进与发散思考-2026-07-20.md # 上下文管理现状评估、业界对标、7个发散方向与组合分析
|
||||
│ ├── 上下文管理演进-任务技术设计-2026-07-20.md # T2命名空间/T4工作流DAG/T3结构化摘要技术设计与推演
|
||||
│ ├── 上下文管理演进-执行计划-2026-07-20.md # Wave 划分、agent 并行策略、接口契约、验证 gate、回滚策略
|
||||
│ ├── 多主题上下文管理愿景-2026-06-19.md # 多主题多摘要愿景(远期方向,关联 F-15/意图识别)
|
||||
│ └── 查询效率优化方案-2026-06-19.md # 查询链路优化(SQL 下推/精确拉取/缓存/字段投影)
|
||||
│ ├── 滚动规范/ # 长期维护的真相源 / 规范 / 决策记录
|
||||
│ ├── 已编号方案/ # 带 F/B/CR 编号的方案文档
|
||||
│ ├── 专项设计/ # 无编号的主题设计 / 审查派生方案
|
||||
│ ├── 构想审查/ # 构想 / UX 改进 / 模块审查 / 论证
|
||||
│ ├── 单对话并行多轮-设计-2026-06-20.md
|
||||
│ ├── 单对话并行多轮-Phase0落地路线图-2026-06-20.md
|
||||
│ ├── 上下文根治设计-20261006.md
|
||||
│ └── 无人值守可靠性专项设计-20261006.md
|
||||
├── 03-模块文档/
|
||||
│ ├── df-storage-存储层-2026-06-12.md
|
||||
│ ├── df-workflow-工作流引擎-2026-06-12.md
|
||||
@@ -79,7 +44,9 @@ docs/
|
||||
│ ├── DEVFLOW-1.CRUD层实施-2026-06-12.md
|
||||
│ ├── DEVFLOW-2.IPC桥接实施-2026-06-12.md
|
||||
│ ├── DEVFLOW-3.Store对接实施-2026-06-12.md
|
||||
│ └── DEVFLOW-4.端到端验证-2026-06-12.md
|
||||
│ ├── DEVFLOW-4.端到端验证-2026-06-12.md
|
||||
│ ├── 父子任务支持设计-2026-08-04.md # 父子任务支持设计
|
||||
│ └── u-switch接入分析-2026-09-28.md # u-switch 接入分析
|
||||
├── 05-代码审查/
|
||||
│ ├── 00-全库代码走查汇总-2026-08-02.md # 全库走查汇总(12 crate, 78 问题, 11 P0 / 30 P1 / 37 P2)
|
||||
│ ├── 01-df-nodes-走查-2026-08-02.md # 17 节点走查(20 问题)
|
||||
@@ -112,15 +79,35 @@ docs/
|
||||
│ ├── 任务模块问题分析-2026-06-16.md # 任务模块18项核对(真bug 7+增强5+假3+去重3)
|
||||
│ ├── 任务执行与推进能力分析-2026-06-16.md # 推进能力实现度0%,4阶段路径+决策点
|
||||
│ ├── AI聊天组件极端数据场景分析-2026-06-17.md # aichat 交互层 20 项缺陷(HIGH×2/MED×8/LOW×8)
|
||||
│ ├── 工作流审批审查报告-2026-06-14.md # (归档)工作流审批链路审查
|
||||
│ ├── 前后端类型对齐-2026-06-12.md # (归档)Rust↔TS 类型契约核对
|
||||
│ ├── 对抗论证裁决报告-2026-06-12.md # (归档)df-ideas 对抗评估裁决
|
||||
│ └── 架构与缺陷复核报告-2026-06-14.md # (归档)复核已修项+回归审计
|
||||
│ ├── aichat-技术债审查-2026-06-21.md # aichat 技术债审查(10 模块 0 P0 / 7 P1 / 25 P2 / 40 P3)
|
||||
│ ├── aichat-会话实测分析-2026-06-22.md # aichat 会话实测分析
|
||||
│ ├── 全量走查报告-2026-06-28.md # 全量走查报告
|
||||
│ ├── aichat-会话实测分析-2026-07-07.md # aichat 会话实测分析
|
||||
│ ├── aichat-对话功能走查-2026-07-17.md # aichat 对话功能走查
|
||||
│ ├── UIUX扩展审查-2026-08-02.md # UI/UX 扩展审查
|
||||
│ ├── aichat历史会话实证诊断-2026-08-04.md # aichat 历史会话实证诊断
|
||||
│ ├── AI工具失败画像复盘-2026-08-08.md # AI 工具失败画像复盘
|
||||
│ ├── aichat效率走查-2026-08-09.md # aichat 效率走查
|
||||
│ ├── df-mcp+工具crate走查-2026-08-09.md # df-mcp + 工具 crate 走查
|
||||
│ ├── settings+审计+布局后端走查-2026-08-09.md # settings/审计/布局后端走查
|
||||
│ ├── 任务+仪表盘+灵感知识走查-2026-08-09.md # 任务/仪表盘/灵感知识走查
|
||||
│ ├── 项目详情页走查-2026-08-09.md # 项目详情页走查
|
||||
│ ├── aichat交互走查-2026-09-28.md # aichat 交互走查
|
||||
│ ├── aichat可用性与交互成本研究-2026-09-29.md # aichat 可用性与交互成本研究
|
||||
│ ├── aichat知识库与记忆走查-2026-09-29.md # aichat 知识库与记忆走查
|
||||
│ ├── 整体UIUX三维走查-2026-09-29.md # 整体 UI/UX 三维走查
|
||||
│ ├── aichat三路走查报告-2026-10-02.md # aichat 三路走查报告
|
||||
│ ├── UX走查-Web规范合规-2026-10-05.md # UX 走查 Web 规范合规
|
||||
│ └── 全系统模块走查索引.md # 全系统模块走查索引
|
||||
├── 06-前端开发/
|
||||
│ └── View改造指南-2026-06-12.md
|
||||
│ ├── View改造指南-2026-06-12.md
|
||||
│ └── 前端架构技术债-2026-06-19.md # 前端架构技术债盘点
|
||||
├── 07-项目管理/
|
||||
│ ├── PROGRESS.md # 项目进展与交接记录
|
||||
│ ├── Batch.md # 批次执行记录
|
||||
│ ├── Phase1任务清单-2026-06-12.md
|
||||
│ └── Phase2计划-2026-06-12.md
|
||||
│ ├── Phase2计划-2026-06-12.md
|
||||
│ └── 用户主张思想总纲-2026-10.md # 跨会话用户主张蒸馏(注入用)
|
||||
├── 08-用户指南/
|
||||
│ ├── 使用手册-2026-06-12.md # 用户手册(对齐 06-15 代码基线)
|
||||
│ ├── AI文件操作工具手册.md # AI 工具 API 全量参考(10 工具,2026-06-16 更新)
|
||||
@@ -176,7 +163,7 @@ docs/
|
||||
| 架构设计 | `../ARCHITECTURE.md` | 完整架构文档 |
|
||||
| 项目进展 | `./07-项目管理/PROGRESS.md` | 工作进展与交接 |
|
||||
| 工作看板 | `./todo.md` | 任务/bug 跟踪(含决策记录) |
|
||||
| Crate 结构 | `../ARCHITECTURE.md#四crate-结构` | 8 个 Crate 概览 |
|
||||
| Crate 结构 | `../ARCHITECTURE.md#四crate-结构` | 13 个 Crate 概览 |
|
||||
| 数据模型 | `../ARCHITECTURE.md#六数据模型` | SQLite 表结构定义 |
|
||||
| Phase 规划 | `../ARCHITECTURE.md#八phase-规划` | 5 个 Phase 路线图 |
|
||||
|
||||
@@ -188,7 +175,7 @@ docs/
|
||||
|----|------|------|
|
||||
| Desktop | Tauri v2 | Rust 后端 + WebView 前端 |
|
||||
| Frontend | Vue 3 + TypeScript + Pinia | Arco Design 组件库 |
|
||||
| Engine | Rust Workspace (8 crate) | 多 crate 架构 |
|
||||
| Engine | Rust Workspace (13 crate) | 多 crate 架构 |
|
||||
| Storage | SQLite (rusqlite) | 本地优先,零运维 |
|
||||
| AI | Multi-Provider | Claude/GLM/DeepSeek/OpenAI 兼容 |
|
||||
| Build | npm + Vite | 前端构建(不要使用 bun) |
|
||||
| Build | bun + Vite | 前端构建(`tauri.conf.json` 的 beforeDev/BuildCommand 即 `bun dev` / `bun run build`;单一 lockfile=bun.lock) |
|
||||
|
||||
+47
-69
@@ -15,6 +15,11 @@
|
||||
> **2026-06-27 归档**: 已完成 `[x]`/`✅` 项已迁至 [07-项目管理/todo归档/2026-06-27.md](./07-项目管理/todo归档/2026-06-27.md)。
|
||||
> **2026-10-05 归档**: 7 个实测核验全完成的历史走查段(父子任务支持/对话上下文透明化/文件拆分升级/ToolCard UI 合并/灵感模块诊断/aichat 深度走查/TaskDetail 按钮走查)迁至 [07-项目管理/todo归档/2026-10-05.md](./07-项目管理/todo归档/2026-10-05.md),销账批注与滞后勘误见该文件。
|
||||
> **2026-10-10 归档**: 全量完成态条目/段落(编号 [x] 32 条 + 勾选 [x] + 标题带 ✅ 且段内零未完成项的历史段,含 907/908 与飞书交互面全量盘查批)迁至 [07-项目管理/todo归档/2026-10-10.md](./07-项目管理/todo归档/2026-10-10.md)。迁移为机械行搬运(零措辞改动),核对:未完成项 110 条零丢失、完成项 37 条全入归档、行数守恒。
|
||||
> **2026-10-10 只读核验销账(源码取证,非文档自述)**: 挂机期派 4 路只读核查子代理逐项 grep/read 源码核验历史待办,结论如下(证据见核验记录/归档)。
|
||||
> - **2026-06-21 技术债审查段**: MED-1(bind_directory 白名单刷新)/REFACTOR-260619-08(tool_registry 拆分组注册)/REFACTOR-260619-09(context.rs 已拆 context/ 子模块)/UX-260617-28(双监听器·已决策现状够用) **均已落地可销账**;SMELL-P1-6(executed/completed 双轨已修,newtype 父项待推进)/ ARC-260619-05(run_agentic_loop 仍约 1162 行未拆) **保持未完成**。
|
||||
> - **其余**: WC-技能注入-trace定位(4b445e9 已根治)/ miniapp P3-B appid(已真实值)/ u-claw 集成代码面(ask_uclaw+UclawPanel 齐备,剩真机冒烟) **已落地**。
|
||||
> - **2026-08-09 三域走查 P1**: 27 项核验 **25 done / 2 partial**(partial=PD-P1-3 事件协议无项目维度已降级标注、PRJ-1 normalize_path 小写化)。
|
||||
> - 已核销的四个走查段整段迁归档:`2026-08-01 走查剩余` / `2026-06-22 miniapp 走查` / `2026-08-09 任务·仪表盘·灵感知识走查` / `2026-08-09 Settings·审计·布局·后端命令走查`;**含活口子的段保留主板**(项目详情页走查=PD-P1-3 / df-mcp 走查=PRJ-1 / 技术债审查=SMELL-P1-6·ARC-260619-05 / u-claw=剩真机冒烟)。
|
||||
>
|
||||
> **2026-08-05 编号碰撞登记**: `F-260619-03` 被两个特性复用(消息拆分存储=文档本体 + AI工具路径授权=复用)。子代理审查确认:改 18 处编号无实际价值有风险,**暂不改代码**,后续新增注释编号统一走日期+序号体系、避免复用已有编号。路径授权若后续单独立项再独立编号。
|
||||
> **2026-08-15 编号碰撞登记**: `REFACTOR-260619-04` 被复用(父③③.2 审批状态机拆分=待办 vs ToolCard 拆分=已完成)。后续引用审批状态机拆分时用「审批状态机拆分」描述,不再沿用该编号。
|
||||
@@ -24,7 +29,6 @@
|
||||
> 背景:src 43 个 .vue 文件 UI 规范缺口全量合规走查(Vercel Web Interface Guidelines 适用子集)+ 审批/aichat/项目知识审计三域子代理分组审。质量中上无阻断,缺口集中 4 类系统性模式,其中 2 类可全局样式一刀切收敛。
|
||||
> 详情:[05-代码审查/UX走查-Web规范合规-2026-10-05.md](./05-代码审查/UX走查-Web规范合规-2026-10-05.md)
|
||||
|
||||
- [x] P2-2 高频可点 div 键盘化:ToolCard:11 卡头/MessageList:1423 轨迹头/AuditLog:67,119 审计行(@click div 全仓 56 处,去 mask 合规项约 20 真缺口,高频改 button 其余 tabindex+keydown) ✅ **2026-10-10 完成(named 高频点)**:四面包裹处均补 `role="button" tabindex="0"` + `@keydown.enter/.space.prevent` 复用原 handler(带展开态的补 `:aria-expanded`;AuditLog 的 `<tr>` 仅补 tabindex+keydown——加 role=button 会抹掉表格行语义,故不加)。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**:全仓另 ~16 处 @click div 未清(见 P3-2)。
|
||||
- [~] P3-2 icon-only 按钮缺 aria-label ~15 处(title 非等价读屏补偿;EnrichmentPanel 关闭钮/ProjectDetail:185,189 编辑删除钮等)+剩余 div 键盘化 —— ✅ **2026-10-10 两批共 23 处**:①批次一 3 处(EnrichmentPanel 关闭 / ProjectDetail 模块编辑·删除);②批次二 20 处(TopBar 10 icon-only 按钮 aria-label / ConversationSidebar 4 aria-label + 2 分组标题键盘化 / MessageList 2 icon-only / KnowledgeDetail 3 章节折叠键盘化 / dashboard 两列表项键盘化 / FileExplorer 面包屑·模块下拉项键盘化 / ConflictResolver 冲突文件折叠键盘化)。全部复用已有 `:title`/i18n key,**零新增键**;遮罩类(@click.self)按约定跳过。验收:`bunx vue-tsc --noEmit` EXIT 0。**剩余**少数(会话项 space 键、dblclick 重命名入口等)+「存量 vs 仅本批」拍板点仍在。
|
||||
- 拍板点:修复范围「存量 vs 仅本批」——transition:all/outline:none 大半为存量;本批新代码 a11y 意识已好(svg aria-hidden 批补/MessageList role=log+aria-live/hover-only focus-within 补偿),仅清本批则工作量降 60%+
|
||||
|
||||
@@ -142,43 +146,6 @@
|
||||
|
||||
**Phase3 联调待办**(D1-D6 决策点推荐:A纯透传 / 全19变体透传 / EventBus汇聚 / switch不处理 / 强类型保留 / 桥接层R1兜底):
|
||||
|
||||
### 🔍 2026-06-22 miniapp 功能走查(workflow wf_ddd49ac8 · 5 域并行核验 + 主代终态抽查)
|
||||
|
||||
> 用户 /review miniapp + workflow 汇总安排。5 agent 独立 grep/read 核验源码(不信注释/文档) + 主代终态核验(ws.ts 真路径 `src/api/ws.ts`、`relay.rs:255/355`、remote_bridge 命令数 10 非 11)。**18 findings → 去重 P1×5 / P2×3 / P3×1**,2 项 refuted。关联 F-260620-01 阶段4(真机联调前置)。**守 session-role-diagnose-only:本会话仅走查 + 登记,未实施代码。**
|
||||
|
||||
**P1(阻塞真机/可靠性·5 项)**:
|
||||
|
||||
|
||||
**用户补充核验(2026-06-22 用户详读源码+设计文档,主代逐项核源码判真伪)**:
|
||||
|
||||
> ⚠️ 用户分析 P0 三项(协议三重不匹配 / EventBus 未接 / 桥接层占位)**经源码核验全部失实**,系基于 Phase3 阶段1-3 落地前的设计文档「计划态」描述,未核当前代码。证据:
|
||||
> - **P0-1 协议**:`crates/df-tunnel/src/tunnel.rs:457/327/424` tunnel 纯透传 payload `Value` 上抛 on_command,**不**做 `from_value::<TunnelCommand>` 强类型反序列化(方案A 阶段1 已落地 commit 0fb5af5)→ miniapp `{cmd,args}` 反序列化在桥接层 `remote_bridge.rs:96 from_payload` 成功,非阻断。tunnel.rs:457 注释自证「不反序列化为 TunnelCommand 强类型」。
|
||||
> - **P0-2 EventBus**:`src-tauri/src/lib.rs:128` `state.ai_event_bus.subscribe()`(tunnel subscriber 已接)+ `remote_bridge.rs:303/332/358/388` + `commands/ai/mod.rs:209/225` `publish_event` 跨端透传。**已双写接入**(todo line 760 commit 0fb5af5/612de4c emit 双写 chat19/audit12/guard3/stream_recv2/agentic18)。`event_bus.rs:53/115` 的 `#![allow(dead_code)]`+「骨架阶段」注释是**过时历史标注**(publish_event 现有调用方 remote_bridge/mod),非未接入。
|
||||
> - **P0-3 桥接层**:`remote_bridge.rs` 完整实装(`handle_remote_command`:121 + `MiniCommand`:83 + `from_payload`:96 + match 路由 10 条 + `route_list/load_messages` publish_event)+ **已 wire** `lib.rs:150-154` `on_command = Arc::new(...handle_remote_command...)` + `:171` `.connect(&relay_url,&device_id,&token,on_command.clone())`。非占位/设计态。`remote_bridge.rs:40-42` 的 `#![allow(dead_code)]`+「零调用方」注释写于联调前,现 lib.rs 已调(过时标注,待清)。
|
||||
|
||||
**剩余开放项**:P3-B-260622 [P3·真机前置] — **manifest appid 占位**。`PLACEHOLDER` 无法真机调试,需微信公众平台注册的真 appid。— `apps/df-miniapp/src/manifest.json`
|
||||
|
||||
### 🔍 2026-08-01 走查剩余待办(P2/P3/专项·本会话登记)
|
||||
|
||||
> 本次走查剩余项登记,未实施代码(守 session-role-diagnose-only)。专项已落 memory,此处留指针。
|
||||
|
||||
**P2(中期改进)**:
|
||||
|
||||
|
||||
**P3(风格/可读·低优)**:
|
||||
|
||||
|
||||
**专项(已登记 memory,留指针)**:
|
||||
|
||||
- 📌 **WC-专项-AI-router-sanitize** — AI router + sanitize 链路梳理重构 — 详见 memory(已登记,本会话不实施)
|
||||
- 📌 **WC-专项-run_agentic_loop-重构** — `run_agentic_loop` 重构(ARC-260619-05 agentic:318 超长,历史登记) — 详见 memory(已登记,本会话不实施)
|
||||
|
||||
**技能注入(待定位)**:
|
||||
|
||||
- [ ] **WC-技能注入-trace定位** — 技能/skill 注入链路待 trace 定位(注入时机、上下文来源、是否重复注入),定位后再决定是否待办化
|
||||
|
||||
---
|
||||
|
||||
### 🔍 2026-08-02 UI/UX 扩展审查(wf_wynylx23h · 10 路并行 · 基于任务 0115aea6 原 12 问题角度)
|
||||
|
||||
> 详单:[UIUX扩展审查-2026-08-02.md](./05-代码审查/UIUX扩展审查-2026-08-02.md)。80 条发现(8 高/38 中/34 低;38 全新/29+ 延伸),高严重度主代理 grep 核验**全属实**。完整数据 `~/.claude/jobs/8ff8ccb7/tmp/audit_summary.json`。
|
||||
@@ -276,30 +243,6 @@
|
||||
> **优先级建议**:① C0-1+C0-2 缓存命中率 → ② L0-1 save + L0-2 system 缓存 + L0-3 压缩后台化 → ③ R1-1 全量重放 + S1-1 流式增量 → ④ T1-1 审计批量 + F1-1/2/3 列表刷新收敛。
|
||||
> **memory 指针**:[[aichat-efficiency-walkthrough-2026-08-09]]
|
||||
|
||||
### 🔍 2026-08-09 任务/仪表盘/灵感知识走查(3 路并行 · 阶段②第一批)
|
||||
|
||||
> 详单见 [任务+仪表盘+灵感知识走查-2026-08-09.md](./05-代码审查/任务+仪表盘+灵感知识走查-2026-08-09.md)。共 **59 项**(任务19/Dashboard+Projects17/Ideas+Knowledge23,P0=0/P1=7/P2=25/P3=27)。守 session-role-diagnose-only:仅走查登记,未实施代码。
|
||||
|
||||
**P1(确定性,优先修)**:
|
||||
|
||||
**P2(体验/数据)**: WK-8~13 ✅ 已修(0554cc1):快捷菜单状态机+补cancelled / overflow裁切根治(顶底角裁剪下放) / loadTasks seq守卫+keyword / 搜索态隐藏进度徽章 / 分页越界钳制 / 失败统一toast。剩余 WK-14~18 🟡 待后续
|
||||
|
||||
**P3(27 项)**: 见详单文档(TSK-10~19 / DBP-10~17 / IDEA-10~12 / KNOW-6~10 / C-1~3)
|
||||
|
||||
> **优先级**:WK-1 Dashboard 状态映射 → WK-2/3 Knowledge 确定性 → WK-4/5 Ideas → WK-6 状态徽章 → WK-7 错误条统一。
|
||||
|
||||
### 🔍 2026-08-09 Settings/审计/布局/后端命令走查(3 路并行 · 阶段②第二批)
|
||||
|
||||
> 详单见 [settings+审计+布局后端走查-2026-08-09.md](./05-代码审查/settings+审计+布局后端走查-2026-08-09.md)。共 **95 项**(Settings+AuditLog29 / layout+sidebar21 / 后端命令45,P0=0/P1=9/P2=32/P3=54)。守 session-role-diagnose-only:仅走查登记,未实施代码。
|
||||
|
||||
**P1(确定性/安全,优先修)**:
|
||||
|
||||
**P2(体验/数据·32 项)**: 见详单文档(SA-1~8 审计 i18n/脏表单/竞态、LS-2~10 侧栏偏好/小屏布局/审批浮窗/Paginator、BE-CMD-9~23 后端校验/聚合)
|
||||
**P3(54 项)**: 见详单文档(SA-9~31 / LS-11~21 / BE-CMD-24~45)
|
||||
|
||||
> **优先级**:LW-1 ConfirmDialog 安全 → LW-2/3 update 裸奔 → LW-4 queue → LW-5 count/list → LW-6 软删 → LW-8 父聚合。
|
||||
> **全系统索引**:前端 13 + 后端 9 命令全走查完成 ✅,剩 crate df-mcp/df-execute/df-project/df-ideas/df-tunnel/df-relay/df-types。
|
||||
|
||||
### 🔍 2026-08-09 df-mcp + 工具 crate 走查(阶段②最后一批)
|
||||
|
||||
> 详单见 [df-mcp+工具crate走查-2026-08-09.md](./05-代码审查/df-mcp+工具crate走查-2026-08-09.md)。共 **35 项**(P0=0/P1=2/P2=8/P3=25)。守 session-role-diagnose-only:仅走查登记,未实施代码。
|
||||
@@ -467,10 +410,15 @@
|
||||
- [ ] P2 审计表无执行耗时字段(requested→executed 混入审批等待,耗时统计不可信)【部分缓解:AuditLog.vue:71 ⏱ durationLabel 已显示且诚实标注「审批→执行」;纯执行耗时独立字段归 V49 schema 迁移批】
|
||||
- [ ] P2 messages blob 双存储冗余(89/89,单会话最大 1.7MB)【归 V49 schema 迁移批】
|
||||
- [ ] 观察自动执行模式架空审批(auto 系 81%/人工 15%,auto_takeover_all 含 run_command 886 次)——产品语义与 S2/信任分级联动
|
||||
- [ ] 913 ✅转待办 验收:auto 模式审批架空语义收敛——auto_takeover_all 触发高危工具(run_command 等)须按 S2/信任分级处置(拦截或旁路留痕);审计表可查 auto 会话审批触发/旁路记录,产品语义写入设计档。
|
||||
- [ ] 观察进化系统只写不读:knowledges 1033 条/embedding 全 NULL/83.6% 零复用/knowledge_search 仅 5 次
|
||||
- [ ] 915 ✅转待办 验收:知识库由「只写不读」转读写闭环——embedding_status 回填(或明确弃用向量路径),≥半数知识进检索候选,knowledge_search 调用与复用率实测上升(复用率 >50%、检索调用非个位数);口径=同库 DB 查询核验。
|
||||
- [ ] 观察多模态未走通:贴图落"[图片已忽略]"占位,实际靠 run_command 外部 vision CLI 绕路
|
||||
- [ ] 917 ✅转待办 验收:贴图走原生视觉路径不落占位——非视觉模型收图时明确拒绝或切视觉模型,视觉模型直接出识别结果,不再靠 run_command 调外部 vision CLI 绕路;实测贴图会话零"[图片已忽略]"占位。
|
||||
- [ ] 观察编排面零使用:workflow/node_executions/ai_plans 等 9 张表全 0 行;四会话分工 origin 89/89 全 NULL
|
||||
- [ ] 919 ✅转待办 验收:编排面明确使用或弃用——e2e 跑通一个 workflow 使 node_executions 非空,或判定降级为预留并销账;会话 origin 回填(不再 89/89 NULL);口径=DB 查各表行数与 origin 非空计数。
|
||||
- [ ] 观察数据目录 ~640MB(主库 140.5MB+dev 库 71.2MB+4 份手工备份 346.9MB 散落)
|
||||
- [ ] 921 ✅转待办 验收:数据目录有保留策略——手工备份纳入轮转(定份数与周期),dev 库与主库职责明确(合并或清理);口径=%APPDATA%/top.1216.devflow 目录大小回落且备份数受控。
|
||||
|
||||
# ai-chat UX 走查第五轮(2026-10-05,可访问性视角,详情 temp/UX走查-aiChat缺陷-2026-10-05.md)
|
||||
|
||||
@@ -505,6 +453,7 @@
|
||||
- [ ] **资源面**:token/磁盘/内存/进程开销评估;日志与数据膨胀上限(借鉴日志无轮转教训)
|
||||
- [ ] **回退开关**:新能力默认关或带单点回退(借鉴 GOAL_PIN 系开关范式);灰度顺序与观测指标
|
||||
- [ ] P2 指令离线即丢(§7 不缓存):commander 发指令时 agent 离线=静默丢失,移动端控制场景用户无感知——补业务层送达回执或短缓冲
|
||||
- [ ] 923 ✅转待办 验收:离线指令不静默丢——补业务层送达回执(发送方可见成功/失败)或短缓冲(重连后补投);口径=断网/离线场景实测指令有回执或重连后送达,无静默丢失。
|
||||
- [ ] P2 Hello 无协议版本字段:下次破坏性变更仍是无通知硬切换——加 proto 版本字段(现在加便宜,以后加贵)
|
||||
- [ ] P3 单共享 token 可冒充任意 channel_id(§8 自述):补 token 轮换 runbook(/etc/u-relay.env 改+重启+四方客户端同步换,锁步成本需写明)
|
||||
- [ ] P3 心跳间隔未定值:nginx proxy_read_timeout=300s,间隔须 <300s,文档补建议值(30-60s)
|
||||
@@ -561,7 +510,7 @@
|
||||
|
||||
- [ ] W-g 状态即时同步与通知(设计 §九):W-g1 出口扇出+即时级(依赖飞书通道)/W-g2 摘要级+远控查询命令族/W-g3 每日报告(挂机联动)
|
||||
|
||||
- [ ] W-h 会话间/代理间感知(FileLeaseRegistry:写前登记+冲突指路/query_leases 工具/兄弟感知/知识联动;与 W-a 组合成完整 lease)——设计 §十;根治今日全部互踩
|
||||
- [x] W-h 会话间/代理间感知(FileLeaseRegistry:写前登记+冲突指路/query_leases 工具/兄弟感知/知识联动;与 W-a 组合成完整 lease) ✅ **2026-10-10 核验销账(源码取证,发现早已落地)**:实现在 `src-tauri/src/commands/ai/lease.rs`(345 行,commit cb260ab「断点续跑与日志租约底座」);接线三处齐备——`commands/ai/mod.rs:48 pub mod lease;` / `tool_registry.rs:561 register_lease_tool`(query_leases 只读 Low,工具基线 80 含 lease 且已有断言) / 写类工具前置校验+RAII 释放 `audit/mod.rs:274-297`。本轮增量=把「冲突检测/过期清理」抽为纯函数 `try_register`/`purge_expired` + 补 3 单测(过期回占/清理计数/异路径并存),`lease::` 11 passed;**未新建 file_lease.rs 造双轨**。⚠️ 并行坑留痕:任何「注入未来时间查全局 REGISTRY」的测试会因 query 的 retain 误清他测活跃租约致偶发失败,新增时间敏感测试须走纯函数+独立表。——设计 §十;根治今日全部互踩
|
||||
|
||||
- [ ] W-e2 全局并发池(小:子代理并发上限+预算联动)
|
||||
- [ ] W-d1 挂机最小闭环(领单→执行→销账→循环+轻量续接;W-b wake 随其绑定实施)
|
||||
@@ -669,11 +618,6 @@
|
||||
- [ ] **G3 描述反向核对**:提交前 diff 关键词与描述机械比对,不符打回(cm 技能增强)
|
||||
- [ ] **G4 销账带证据**:勾选 [x] 须附验收命令输出行,无证据视同未完成
|
||||
|
||||
### 桌面能力同步(随本批)
|
||||
- [x] G6 缺陷9 标题泄漏桌面同症随批修 ✅ **2026-10-10 核验销账**:`title.rs:241 sanitize_title_source`(剥 HTML/Markdown 标签,含 `<details>/<summary>`/代码围栏)已接入 `extract_title` 的 LLM 路径(:280)与兜底路径(:298),桌面与飞书同源,缺陷9 桌面同症已随 70d6e98 一并修。
|
||||
|
||||
|
||||
## 无人值守可靠性专项(2026-10-06 swarm 立项,设计档:docs/02-架构设计/无人值守可靠性专项设计-20261006.md+temp/V49断点续跑设计-20261006.md)
|
||||
|
||||
## UI/UX 体验设计实施项(2026-10-06 设计档:temp/异步任务会话UIUX设计-20261006.md,依赖 13/14/16/17 机制先行)
|
||||
- [ ] U1 任务卡生命周期组件(双端):桌面 TaskCard 八态投影枚举(全部由既有事件派生,零引擎新状态)+飞书 header 状态色结构卡(JSON 元素级规格已给,动作 v1 走文本命令)
|
||||
@@ -736,5 +680,39 @@
|
||||
902. [ ] 超时失败后的一键继续入口: 流式首字节超时(120s,服务收了不响应,不可自动重试)报「响应超时,请重试」后流程中断——轮次重入预算耗尽(3/3)即彻底停,用户只见裸文案。修:超时失败落停靠语义(复用 emit_task_parked),停靠卡带「继续」(ai_continue_loop parked 分流已修好)——错误后一键续跑而非中断。体验缺口实证 2026-10-07 晚 u-switch 故障窗口。
|
||||
903. [ ] u-switch 20:38-21:02 故障窗口根因深查: 11 连败+120s 首字节超时风暴(多会话多 provider 同时),21:0x 自愈。查 moyu-ops 机 u-switch 日志定因(上游抖动/服务重启/连接池耗尽),根治或加监控告警。**【21:5x AI-Ops 定因已实锤,journalctl 实测】上游连环降级雪崩非服务故障:api.deepseek.com 402 余额不足(冷却10m)+enterprise.moyu.info 401 认证失效(冷却10m)+sensenova kimi-k3 429/SSE空响应体(30s冷却循环)+bigmodel glm 429/SSE截断缺 message_stop——多通道同时打光致无首字节,冷却走完自愈。剩余工作=根治项:①switch-gw 的 deepseek 系 4 通道确认摘除(用户指认已摘但配置仍在役)②429 风暴时客户端退避协同(30s 冷却对重试风暴无效)③故障窗口告警。**(10-08 已处置:moyu 令牌过期续期+主链切 ds-flash,见 900 区段外 905)
|
||||
|
||||
911. [x] **「流式后气泡被清空」真根因(2026-10-10 定位并修复,历次修补无效)**:用户现象=正文流式进气泡 → 轮末气泡清空回到「等待」,每轮工具后都发生、且文本「彻底丢」。**根因(实测定位)**:`MessageList.vue` `renderItems` 的**完成轮分桶**把整轮(含模型正文)折进「执行轨迹卡」且**默认收起**(`flushBucket` 只把最后一个有正文的消息当 finalMsg 出气泡,其余全进 traceMsgs);叠加 `thinkPlainText` 取 `reasoning_content || content` 把正文也当「思考」塞进单行 pill。故流式期间正文可见(currentText 直渲),轮末消息转轨迹卡即「消失」。**修复**:①过程轮里**有正文**的各成一个常显气泡(剥 `toolCalls`,工具卡仍归轨迹卡不重复);②`thinkPlainText` 只认 `reasoning_content`(思考 pill 不再吞正文)。**教训(必须留痕)**:此前 4-5 次修复(`40c4a21` 闪烁/`4652112` 迟到收尾仲裁/`524120b` 切会话残片/`BUG-260624-01` 重叠/`flushCurrentText` guard)全在**状态清空/骨架层**,从未碰**渲染折叠/分桶层** → 故反复不好;凡「流式后内容消失」类投诉,第一嫌疑应是**渲染折叠/分桶**(`renderItems`/trace 桶),而非 streaming/currentText 状态。
|
||||
925. [ ] **前后端事件契约与 i18n key 全量核查(2026-10-10 只读 Wave D,源码取证)**:全量抽取后端 emit 与前端 listen/分派做对照;另把 src 下 1282 个静态 i18n key 与 zh-CN/en 键集合(各 1569)比对。
|
||||
- ✅ **已修两处**:①`ai.aiTool.rejectedHint` 前缀写错(多一层 `ai.`)→ 2 处调用(`composables/ai/useAiApproval.ts:90` / `useAiToolEvents.ts:120`)改为正确键 `aiTool.rejectedHint`(此前 zh/en 双缺 → UI 渲染裸 key,908 类复发);②`components/layout/UpdateBadge.vue` 监听 Tauri 事件 `app-update-available`,而生产端 `composables/useUpdateCheck.ts` 走 **window CustomEvent** 同名不同通道 → 徽章永不触发;已补 window 监听 + 卸载清理(保留 Tauri 监听以兼容后端将来改走 emit)。
|
||||
- [ ] **待处理·孤儿事件 2**:`AiPlanCreated`(`agentic/mod.rs:1006` emit)/`AiConflictResolved`(`commands/settings.rs:259` emit)——前端零消费;补消费或停发(现状为白 emit + 双写总线开销)。
|
||||
- [ ] **待处理·死变体 3**:`AiMaxRoundsReached`(`commands/ai/mod.rs:239`)/`AiSubTaskStatusChanged`(`:421`)/`AiMergeCompleted`(`:429`)——前后端零构造零消费(前端仅在 types 联合与 NO_RESET_WATCHDOG 名单留残影);清理或补 emit+消费。
|
||||
- [ ] **潜在(低)**:`dashboard.ideaStatus.archived` 键缺失(值域含 archived,当前 list 恒过滤 archived → 不可达);补键或注明。
|
||||
- 备注:跨端 miniapp-only 5 变体(ConversationList/MessageHistory/SkillList/ModelList/EntityList)只走 publish_event→总线→tunnel→miniapp,桌面端不监听属设计,非孤儿。
|
||||
|
||||
927. [ ] **V50 迁移回填的不可逆风险(2026-10-10 只读风险自查,待处置)**:`crates/df-storage/src/migrations.rs:1511-1545` `backfill_checkpoint_token_total` 直接 UPDATE 覆盖 `conversation_checkpoints.token_total` 旧值,**无备份表、无 DOWN**、函数注释自认「无回退」;虽迁移文档论证旧值(消息条数)失真无保留价值,仍属不可逆数据覆盖且无快照兜底。
|
||||
- [ ] **处置(成本极低)**:回填前把 `(id, old_total → new_total)` dump 到 temp 备份文件或日志,给「后悔一次」的机会;顺带 `migrations.rs:1516-1527` 一次性 collect 全部 checkpoints(含 snapshot 全文字符串)进内存 → 改分批/游标,防大表峰值内存膨胀。
|
||||
- 已核安全项:`migrate_v50`(:1483)幂等(column/table_exists 守卫 + 仅 recomputed>0 且 !=old 才 UPDATE + schema_version 闸 + 重跑单测);df-mcp 新增仅写一个 KV,无新端点/帧/权限放大。
|
||||
|
||||
928. [ ] **aichat 前端性能热点与测试覆盖缺口(2026-10-10 只读 Wave F,源码取证)**
|
||||
- **性能(按收益排序)** — 高:①`MessageList.vue` 3041 行 God 组件(职责>6 类);②`renderItems`(:1058) 每次依赖变化全量 O(n) 重建(工具事件级触发);③**全仓零 `v-memo`**(:1498/:1715) → 流式 rAF 每帧重建整个 renderItems vnode,长会话掉帧最直接来源。中:`JSON.stringify` 指纹 watch(:429) 每次工具状态变动全量 map+stringify;每帧逐条 `renderMdDecorated`/`traceTaskRefs` 调用(:1548/1556/1672/1519/1722);`TopBar` 三处 O(n) messages computed(:619/:676/:707) 可合并单次遍历;`useAiSend.ts` 595 行可再拆 `useAiQueue`;`useTurnTrace` switch 缺 default(:59,union 非穷尽)。低:`ConversationSidebar` 分组排序、`ChatInput` status `as string` 拓宽(:601/605/606)、`MessageItem` 模板同函数重复调用(:110-122)、md 缓存「满即全清」悬崖(:699 / useMarkdown:195) → 改 LRU。
|
||||
- **已核良好(非热点)**:`useAiEvents/useAiToolEvents/useAiStreamingEvents/useAiLifecycleEvents` 分派器拆分清晰(均<400 行、switch 带 default);`useStreamRenderer` 块级 memo + 增量 lexer + rAF 节流已完善。**关键澄清**:流式 delta 不写 `messages.content`(仅收尾/新轮/停止/超时 flush),故热点是「工具事件级 + 流式帧级」而非每 token。
|
||||
- [ ] **测试覆盖缺口(建议补最小用例)**:结果/过程分离渲染(renderItems 无任何前端断言,仓库仅有 playwright e2e 两个 spec)、`keyword_fallback_commit` 单锁窗口(context_lifecycle.rs 无测试)、`check-i18n` 门禁自身无反引号用例、`route_conv_event`/`route_scope` 事件改挂(event_bus 测试未覆盖)、`relocate_project_path` 路径校验(`..` 拦截)无用例 —— 对「泄漏风险 × 改动复杂度」高者优先补。
|
||||
|
||||
929. [ ] **模型配置「停用」无效(2026-10-10 用户报,已定位待修)**:用户在设置里停用模型后仍被使用。 ✅ **2026-10-10 主根因已修**:`prompt.rs get_active_provider` 原两条路径均不看 `enabled`(显式活跃 provider 直接返回 / 默认 provider 只按 `is_default` 挑)→ 已收敛为「启用集合」解析(`resolve_enabled_provider`,无启用 provider 时明确报错);✅ **兜底路径已修**:`compress.rs`/`title.rs` 新增 `resolve_fallback_model`(纯函数,title 复用同一实现——单一实现无双轨)+ 4 单测,覆盖「默认模型被停用→回落池内任一启用模型/池内无启用→报错」。⚠️ **同类次生缺口待收口(4 处,均已定位未改)**:`commands/ai/knowledge_inject.rs:231` 与 `:1116`(embedding/extract 兜底)、`commands/project.rs:800`(scan_model 兜底)、`agentic/router_setup.rs:15-16`(`resolve_model_with_override` 的 resolved 兜底;override 本身已校验)——四处兜底均不校验 `enabled`,建议随本项一并收口。**勘察结论**:路由层**是**过滤 enabled 的(`crates/df-ai/src/router.rs:98 .filter(|m| m.enabled)` + 单测 `enabled_false_excluded_from_routing`),故病灶在别处,三处嫌疑按置信排序:①**已选中的 model_override 被无条件采纳**——`agentic/mod.rs:740 let mut resolved_model = match model_override.as_deref()` 优先用前端选中的模型,若该模型随后被停用则**不校验 enabled**,继续照用;②**兜底 `default_model` 不校验 enabled**——`compress.rs:82-83`(及 `title.rs` 同款) 在池内无可选时 `unwrap_or_else(|| provider_config.default_model.clone())`,若 default_model 正是被停用的模型 → 照用;③**provider 级池停用(`p.enabled`,ProviderPanel `onPoolToggle`)不参与路由**——路由只过滤 model.enabled,池禁用可能仅影响前端展示。修法:选中/兜底/池三条路径统一过「enabled 校验」,不可用则回落到池路由(已过滤)或明确报错。验收:停用某模型后,选择器不列出、既有 override 落到其它启用模型、压缩/标题兜底不选它。
|
||||
931. [ ] **消息发送队列会丢失(2026-10-10 用户报,已定位)**:会话遇网络超时等错误后队列消失;重启同样丢。**勘察结论**:`state.queue` 是**纯内存**(`src/stores/ai.ts:136`),无持久化;错误/收尾路径会清或丢弃(`useAiSend.ts:180/256` 重生成/编辑时 drop、`aiShared.ts:773` clearQueue)。修法:①队列按会话**持久化**(KV 或独立表),入队/出队/清空即写,启动与会话加载即恢复;②**错误/超时不得清队列**(只标记该条失败可重试),重生成/编辑类**显式用户动作**才允许清;③恢复后给出「N 条待发已恢复」提示与手动清理入口。验收:断网超时后队列仍在;杀进程重启后队列仍在且可续发。
|
||||
933. [ ] **输入框支持有序列表渲染(`1. ` 类飞书,2026-10-10 用户报)**:ChatInput 内输入 `1. `(或 `- `)后应有**列表预览/自动编号**效果(对齐飞书消息输入框),而非纯文本。**需设计**:触发判据(行首 `\d+. ` / `- ` / `* `)、渲染形态(实时预览块 or 仅高亮)、回车续写自动补序号、退格退出列表、与既有 `/` 技能联想、`@` 提及、代码围栏的优先级冲突处理。设计落 docs 后再实施。
|
||||
935. [ ] **定时消息(多条 + 定时列表,2026-10-10 用户报)**:支持把消息设定为定时发送,可设多条,并有统一「定时列表」管理(查看/改期/取消)。**需设计**:存储(表 or KV)、调度器(前端定时器 or 后端 tokio 任务,需在应用关闭后仍生效→倾向后端)、与队列/生成中状态的关系、失败重试与通知、UI 入口(CHATIN 旁「定时」按钮 + 列表抽屉)。设计落 docs 后再实施。
|
||||
939. [ ] **上下文管理瓶颈清单(2026-10-10 五维只读走查,源码取证)** — 按收益排序,⚠️ `crates/df-ai/**` 被并行写者占用(工具结果外置 WIP)→ 带 ★ 的可先在 src-tauri 侧落,其余待解禁:
|
||||
- **HIGH ①** `history_tokens` 与真实请求体量**脱钩**:工具结果经 namespace 引用化入内存(`audit/mod.rs:1183-1190` + `namespace_store.rs:40-55` ALWAYS_LARGE_TOOLS 命中即引用,与体积无关),估算器只数字符(`context_helpers.rs:62-105`)→ 单条上万 token 的大结果只记 ~15 token;**预算/裁剪门(`context/mod.rs:185-205`)/压缩门(`context_lifecycle.rs:103-108`)三重门槛形同虚设**,每轮实际重发展开后的全量历史(223:1 结构浪费根因)。修:push/外置时按原文体量入账,或估算器识别 ref 取真实大小。
|
||||
- **HIGH ②** 每轮 O(n) 全量深克隆 + sanitize + namespace 展开 + 摘要 + 估算(`context/mod.rs:198-205` / `agentic/mod.rs:1904-1921`,`1945-1957`,`1981-1984`)→ 会话内 O(n²) 累积(全量重发本体);治本=设计档 P2 分层组装。
|
||||
- **★HIGH ③** 预算上限**恒 128K**不随模型窗口(`commands/ai/mod.rs:1239` `ContextManager::new(ContextConfig::default())`;`context_helpers.rs:131/140-142`;子代理同 `sub_agent.rs:117-127`)→ 1M 窗口模型被当 128K 裁(能力降级),小窗口模型预算高于实际窗口。修:`ContextConfig` 字段 pub,可在 src-tauri 侧按 resolved_model 窗口构造(无需改 df-ai)。
|
||||
- **HIGH ④(压缩)** 压缩**单向不可逆**:被压原文对 LLM 永久不可见(`context/mod.rs:664-699` 标 Compressed + `sanitize.rs:42` is_active 过滤出局),设计 C-r1「压缩不丢/可召回」未落地 → 方向:复用 P1 句柄模式,压缩写 pointer 可回读。
|
||||
- MED ⑤ `chars_ratio=0.35` 中文低估 2-3 倍、base64 高估(`context_helpers.rs:29-44/73-74`);⑥ 保护区固定 6 条 + 补偿仅 300 字(`:648/:655`);⑦ 分层组装 P2 未落地(窗口外历史无检索召回);⑧ **★** system 可变段(日期+项目/任务清单)内嵌稳定段致**前缀缓存失效**(`prompt.rs:611-635` 缓存拆分开关默认关;清单描述不截断 `:565/:584`);⑨ 滑动裁剪致前缀缓存整体失配(`context/mod.rs:239-246`);⑩ 一次压尽(windows 式分批未做);⑪ **后台压缩无 epoch/内容版本守卫**——`clear()` 保留 per_conv 条目且复位 is_compressing → 窗口内旧摘要可注入已清空的新上下文(`context_lifecycle.rs:206-323`,loop 侧有 F1 epoch,此 task 侧无);⑫ 后台压缩**无 panic 兜底** → is_compressing 永真致压缩永久停摆。
|
||||
- LOW ⑬ 估值双轨(estimated_prompt 准确用于路由 vs history_tokens 失真用于预算)从不互相校准;⑭ sys_tokens 未计工具 schema。
|
||||
- **已核良好**:裁剪三元组原子性正确(`context/mod.rs:612-642`);909 两处修复(锚点避让/单锁复位)现状有效,未见旧窗口复现。
|
||||
|
||||
941. [ ] **C-r0 验证命令后台化:方案已定,落地需拍板(行为变更)**(2026-10-10 Wave A 5a 勘察,代理按纪律拒改并交方案)
|
||||
- 事实:`run_command` 是 >90s 桶的 **95%**(11.21h/11.84h,n=4250),阻塞点=`agentic/mod.rs:2235` 整轮同步 await 工具 → `audit/mod.rs:246 execute_with_heartbeat`(tokio::select! await 至工具返回)→ `tools/file.rs:1304` → `df-execute/src/shell.rs:464`(等子进程 exit)。
|
||||
- **为何不能只改工具层**:轮次生命周期必须同步改写,否则态不自洽——① 心跳 task 随 `execute_with_heartbeat` 返回即 abort(`audit/mod.rs:393-408`),前端 130s 看门狗(`useAiEvents.ts:262`)靠它续命;② 后台运行期间该会话是 Generating 还是 Idle(属轮次语义,落点在 `mod.rs`+前端);③ 完成回填若为注入消息,**不得楔进 head 与其 tool result 之间**(否则复现 907 类 400);④ 给 LLM 返回「已后台」非终态 tool_result = 核心 ReAct 语义变更。
|
||||
- 建议方案(待拍板):`audit/mod.rs:246` 把 tools future 交 `tokio::spawn`(`tools` 形参升 `&Arc<AiToolRegistry>`,调用点已 clone/持 Arc,签名变更对调用方零改动)+ grace 窗口 select;detached task 自持心跳/失败记忆/完成通知;完成回填复用 `emit_ai_chat_event` + `chat.rs:1133 inject_runtime_message` / `helpers.rs:60 try_continue_agent_loop`;KV 开关 `df-ai-run-command-background`(默认关)+ grace 秒可配;**loop 侧配套改动**(心跳接管/后台期 conv 态定义/收敛判定)在 `mod.rs`。
|
||||
- 拍板点:① 是否引入「非终态 tool_result」这一语义(默认关可回退);② grace 秒默认值;③ 后台期间会话态展示(Generating 还是「后台执行中」)。
|
||||
|
||||
937. [ ] **对话内创建定时/延时任务(2026-10-10 用户报)**:在对话中直接创建「定时执行/延时执行」的任务(如「1 小时后跑 X」)。**需设计**:与既有 Tasks/工作流(advance_task、run_workflow)的关系、调度落点(与 935 共用调度器)、无人值守引擎(unattended)是否复用、UI 呈现(任务卡带定时徽标)。设计落 docs 后再实施。
|
||||
|
||||
Reference in New Issue
Block a user