新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理

后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
2026-06-21 20:51:26 +08:00
parent 330bb7f505
commit bd6a41fe6e
111 changed files with 11932 additions and 1034 deletions

View File

@@ -23,6 +23,52 @@
## 当前队列
### CR-260620-04 Agent 最大轮次支持「不限」(working tree 未提交:0=不限 约定·config.rs clamp 0-50 + mod.rs effective_max 0→usize::MAX + GeneralPanel checkbox + App.vue clamp + i18n labelUnlimited) — ✅ 已审(PASS·⚪2 WATCH·巡检 2026-06-20 独立 grep/read 核验控制流)
- **范围**:新功能。约定 0=不限:后端 config.rs:69 clamp(1,50)→(0,50);mod.rs:542 loop 上界算 effective_max(0→usize::MAX,for 到不了上界,靠 stop_flag/收敛/审批退出,达上限暂停 AiMaxRoundsReached 不触发);前端 GeneralPanel 加「不限」checkbox(绑 agentMaxIterations===0)+ input :disabled + clamp 支持 0 + computed unlimited;App.vue:107 clamp 跟进(iter===0?0:...);i18n zh/en desc 加 0=不限 + labelUnlimited。
- **维度**:正确性(effective_max 语义/达上限暂停分支 1172 在 0 模式是否漏退出)/ 边界(0 全链路 config→store→load→loop 透传)/ 回归(设 50 仍触发暂停?默认 10 不变)/ UI(checkbox 切换+持久化+disabled)/ 一致性(前端 0=不限 vs 后端 0→usize::MAX 对齐)
- **核验方式**:git diff working tree 5 文件逐行 + cargo check EXIT 0 + vue-tsc EXIT 0(主代已跑)
- **待审点**:effective_max 计算位置(542 for 前)/ usize::MAX 作 for 上界无溢出/意外 / unlimited computed set 与 syncAgentMaxIterations 协同 / App.vue 三元与 GeneralPanel clamp 语义一致 / 0 模式 1172 暂停分支确实不触发 / 不限模式无限轮风险仅靠 stop_flag/收敛/审批兜底(无软上限)
---
**复审结论(2026-06-20·巡检独立 grep/read 核验控制流,不信"主代已跑"声明)**: ✅ **PASS** — 🔴0 🟡0 ⚪2 WATCH 1
**① 核心控制流 0 模式暂停分支不误触发 PASS(深度核验,主会话待审点)**:
- `mod.rs:543 effective_max = if max_iterations==0 { usize::MAX } else { max_iterations }`
- `mod.rs:544 for iteration in start_iteration..effective_max` — 0 模式上界 MAX,for 仅能经 return/break 退出,不可能跑满上界
- for 退出仅 3 路径:`stop_flag`→return(:559)/ 审批等待→return(:1162)/ **收敛→break**(`:1143 if !has_tool_calls { converged=true; break; }`)
- `mod.rs:1175 if !converged`(for 结束后)— 0 模式 for 仅经 converged break 到达此处(return 路径直接返回不经此),故 `!converged=false`,**跳过 AiMaxRoundsReached 暂停分支** ✅
- 注释 `:1174` "max_iterations=0 时 effective_max=usize::MAX,for 不会正常结束至此,故不触发暂停" 准确
**② usize::MAX for 上界无溢出 PASS**:
- Rust Range `0..usize::MAX` iterator 到 MAX-1 后 next 返回 None(标准库保证不溢出)
- for 体内 `iteration` 仅作计数,无 `iteration+1` 致 MAX 溢出运算;实际 loop 靠 break/return 退出,不会真迭代至 MAX-1
**③ 0 全链路透传一致性 PASS**:
- 前端 GeneralPanel unlimited→`agentMaxIterations=0` → IPC → `config.rs:69 clamp(0,50)` store 0 → `chat.rs:167 load``run_agentic_loop max_iterations=0``effective_max=MAX`
- `App.vue:107 iter===0 ? 0 : Math.min(50,Math.max(1, iter??10))` 与 GeneralPanel `syncAgentMaxIterations`(`!==0` 透传)语义一致 ✅
- GeneralPanel `unlimited` computed(get `===0` / set 0↔10)+ input `:disabled="unlimited"` + 持久化(appSettings SQLite,App.vue onMounted 读 0 透传后端) ✅
**④ 回归 PASS**:
- 默认 `DEFAULT_MAX_AGENT_ITERATIONS=10`(state.rs:556)不变,clamp 0-50 不影响默认 ✅
- 设 50:effective_max=50,跑满未 break → `:1175 !converged` → 暂停触发(原行为保留)✅
- 设 0:如 ① 不触发暂停 ✅
**⑤ i18n 中英对称 PASS**:en/zh `descAgentMaxIterations` 加 "0=不限" + `labelUnlimited` 新 key 中英对称 ✅
---
**⚪ LOW-1**:`config.rs:60-61` docstring stale — 仍写"范围双 clamp(command 端 1-50 + 前端 input min/max)",实际 `:67-69` 已改 clamp 0-50。文档与代码不符,建议同步。
**⚪ LOW-2**:`GeneralPanel` input `min="1" max="50"` 未改。unlimited 勾选时 `:disabled` 不冲突;用户不勾选直接手输 0 → v-model 允许 + sync `!==0` 透传(=不限语义自洽)。min=1 与"可手输 0"轻微矛盾,有 unlimited 复选框为主路径,影响低。
**WATCH-1**:0 模式(不限)**无软上限** — LLM 持续调工具不收敛时理论无限轮烧 token,仅靠 stop_flag/收敛/审批退出。用户主动选"不限"即接受此风险(设计取舍,非 bug)。建议 UI desc 补风险提示(当前 descAgentMaxIterations 仅"0=不限",未提烧 token 风险)。
- **待修项回流 todo**:**无** 🔴/🟡 项
---
### CR-260620-01 AI 授权目录改进批(working tree 未提交:strip_verbatim比对侧收口+黑名单统一+search_files symlink+bind reload+toast接线+单测·state.rs/tool_registry.rs/mod.rs/project.rs/chat.rs/AllowedDirsPanel.vue/Settings.vue/i18n×2) — ✅ 已审(ISSUES·🟡1⚪2)
- **范围**:三方审查(安全/UX/跨端)交叉印证改进。误弹窗核心根因(strip_verbatim 仅写入侧,比对侧遗漏)收口到 `is_authorized` 单点(candidate strip)。黑名单两套不一致(validate_path contains vs is_in_system_blacklist 分段)统一单一来源 + 补 windows 根/programdata/.ssh。search_files symlink 逃逸(file_type 不跟随)。bind 路径 reload(project.rs create_with_binding/update_project/relocate 三处)。AllowedDirsPanel toast 接线 + pickDir 即加。P0 workspace_root 收紧已**回退**(编译期写死不适合分发,转待决策)。
@@ -1499,9 +1545,9 @@
---
### CR-260619-27 AI Chat 跑题改进 P2(主题检测保守 + tokenize 2-gram·commit a2db5c7) — 🟢 通过
### CR-260619-27 AI Chat 跑题改进 P2(主题检测保守 + tokenize 2-gram·commit a2db5c7) — ✅ 已审(PASS)
- **结论**🟢 通过:6 维度全部核验通过,topic 字段无破坏(TrackedMessage 无 Serialize derive,只 ChatMessage 落库)+ 双高置信保守(任一 topic None 不标)+ tokenize 2-gram 正确修复中文锚点。df-ai 189 passed 独立复跑确认。无 high/med 风险,2 条 low 提示。
- **结论**: ✅ **PASS** — 🔴0 🟡0 ⚪2 — 6 维度全部核验通过,topic 字段无破坏(TrackedMessage 无 Serialize derive,只 ChatMessage 落库)+ 双高置信保守(任一 topic None 不标)+ tokenize 2-gram 正确修复中文锚点。df-ai 189 passed 独立复跑确认。
- **范围**(3 文件):context.rs(TrackedMessage.topic 字段 + pending_topic_marker + push 推断 + last_user_topic/take_topic_marker + 5 主题测)+ context_helpers.rs(tokenize 2-gram 汉字滑窗 + 4 测)+ agentic/mod.rs(TOPIC_MARKER_ENABLED 常量开关 + loop 顶部消费 marker)
- **根因**:多主题交织无检测/分段 + CR-26 🟡1 中文 tokenize 锚点弱
- **维度核验**:
@@ -1589,6 +1635,102 @@
- **待修项回流 todo**: **无** 🔴/🟡 项(🟡 WATCH-1 已修:reload_allowed_dirs 删无条件 insert workspace_root,KV 有配尊重用户 persistent,兑现"用户删白名单后工程根需授权"动态白名单完整语义)
### CR-260621-01 @ + / Input Augmentation 层(df-types Augmentation + Resolver 投影 + path 脱敏 + chip 元数据 + search_files 兜底 + / 修复) — ✅ 已审(PASS·🟡1⚪3)
- **范围**:新功能 + 重构 + 安全增强。df-types 新增 augmentation 领类型(SanitizedPath newtype + MentionRef/Augmentation/MentionSpanDto/ResolveError);后端 augmentation 模块(MentionResolver async trait + ResolverRegistry + 四 impl + ProviderLocality 脱敏 + inject 段构建);skills.rs strip_frontmatter + RwLock 双检锁 + ScanResult conflicts;chat.rs 4 调用方统一 resolve_and_inject;audit/mod.rs search_files 兜底改 Denied;前端 ChatInput chip span 记录 + MessageList 元数据驱动切段(弃正则)+ spans 全链路透传。
- **维度**:协议(serde tag/rename/transparent/newtype 向后兼容)/ Resolver(async trait + 单条失败不阻断)/ 安全(path 脱敏 Local 全路径 Remote basename)/ spans 透传链路完整性(8 环节逐环节核验)/ chat.rs 注入收敛一致性 / search_files 兜底回归(其他文件工具不变)/ 前端 chip 元数据驱动(非正则)
- **核验方式**:独立 grep/read 逐文件核验源码当前形态(11 新文件 + 8 改文件),不信任何声明。
---
**复审结论(2026-06-21·独立 grep/read 核验源码当前形态,不信声明/文档/会话描述)**: ✅ **PASS** — 🔴0 🟡1 ⚪3
**① Augmentation 协议 PASS(serde 严格对齐 + 向后兼容)**:
- `MentionRef` `#[serde(tag="kind", rename_all="snake_case")]` + 字段 `#[serde(rename="id"/"name")]`,变体 `project`/`task`/`idea`/`skill`,round-trip 单测印证(augmentation.rs:350-401)✅
- `Augmentation` 同构 `tag="kind"` + `path: Option<SanitizedPath>``#[serde(default, skip_serializing_if="Option::is_none")]`(:161),无 path 时 JSON 不含 path 字段(单测 :439-453 印证)✅
- `SanitizedPath` `#[serde(transparent)]`(:30)— wire 上是裸字符串无 `{inner}` 包裹,前端 `type SanitizedPath = string`(types.ts:413)对齐 ✅
- `MentionSpanDto.ref_id` `#[serde(rename="refId")]`(:246)— camelCase 对齐前端 TS `refId`(types.ts:402)✅
- **向后兼容**:serde tag=kind + 枚举,老前端遇未知 kind 反序列化失败抛 Err(非"忽略"),但**仅在新 mention 类型新增时**才有此风险——当前 4 变体与前端 TS union 严格对齐(types.ts:400 `kind: 'project'|'task'|'idea'|'skill'`)无未知 kind 场景。Augmentation TS 类型宽松(types.ts:370 不强制校验变体),前端宽容。设计合理 ✅
**② Resolver trait + 四 impl + resolve_all 单条失败不阻断 PASS**:
- `MentionResolver` async_trait(mod.rs:34-48)+ 四 impl(resolvers.rs:56/121/184/246)kind 分别返 "project"/"task"/"idea"/"skill" ✅
- `resolve_all` for 循环逐条 resolve,Ok push augs / Err push errors(**不 break 不 return**,registry.rs:56-72);kind 无对应 resolver 收集 KindMismatch 不 panic(单测 :107-122 印证)✅
- ProjectResolver 经 `ProjectRepo::get_by_id` + path 经 `sanitize_for` 包 SanitizedPath(resolvers.rs:76-104);TaskResolver join `ProjectRepo::get_by_id(task.project_id)` 取 project_name(:156-162);IdeaResolver 无 project_id 无 path;SkillResolver 调 `read_skill_content_stripped` 取剥 frontmatter 正文(:267)✅
- 循环依赖规避:Resolver 持 `Arc<Database>``Arc<AppState>`(resolvers.rs:9-11 注释明确),AppState 持 `Arc<ResolverRegistry>`(state.rs:273),无环 ✅
**③ path 脱敏 PASS(locality 判定 + sanitize_for)**:
- `locality_of(provider)``base_url.to_lowercase()`,LOCAL_MARKERS 含 `localhost`/`127.0.0.1`/`0.0.0.0`/`::1`/`ollama`,任意子串命中即 Local(sanitize.rs:30-44)✅
- 单测覆盖 localhost/127/0.0.0.0/[::1]/ollama/LOCALHOST(大小写不敏感)/api.openai.com/空串(:98-144),边界完整 ✅
- `sanitize_for(raw, Local)` 原样返 / `Remote``Path::file_name` basename,失败回退 `[remote-path-hidden]`(:54-64),防裸泄盘符/用户名/公司目录 + 防 LLM 拿乱码路径瞎调文件工具 ✅
- **实际生效路径核验**:ProjectResolver resolve 内 `sanitize_for(raw, loc)`(resolvers.rs:96)→ `SanitizedPath::new` 包 newtype,loc 由 chat.rs:109 `sanitize::locality_of(provider)` 传入,provider 来自 `get_active_provider` 真实 DB 记录。闭环 ✅
**④ 【重点】spans 透传链路完整性 PASS(8 环节逐环节核验,无断链)**:
| 环节 | file:line | 核验 |
|------|-----------|------|
| 1 ChatInput.selectMention 记 spans | ChatInput.vue:432-438 | push `{start: before.length, length: label.length, kind: item.type, refId: item.id, label}` ✅ |
| 2 ChatInput.handleSend 快照 + trim 偏移修正 | ChatInput.vue:567-571 | `rawLeadingWs = inputText.length - inputText.replace(/^\s+/).length`,span.start 整体左移 rawLeadingWs(剥前导空白致偏移失效修复)✅ |
| 3 ChatInput.handleSend 透传 store.sendMessage | ChatInput.vue:608 | `store.sendMessage(text, skill?.name, false, parts, snapshotSpans.length > 0 ? snapshotSpans : undefined)` ✅ |
| 4 useAiSend.sendMessage 签名 + L0/L2 透传 | useAiSend.ts:291,296,326 | `sendMessage(text, skill, forceMode, parts, spans)`,L0 `doSend(text, skill, false, parts, spans)` / L2 force `doSend(text, skill, true, parts, spans)` ✅ |
| 5 useAiSend.doSend 调 aiApi | useAiSend.ts:107,109 | force: `aiApi.forceSend(text, lang, skill, override, parts, convId, spans)` / normal: `aiApi.sendMessage(..., spans)` ✅ |
| 6 aiApi.sendMessage/forceSend invoke mentionSpans | ai.ts:16-27,31-42 | `invoke('ai_chat_send', {..., mentionSpans: mentionSpans && mentionSpans.length > 0 ? mentionSpans : null})`,空数组不传(后端默认 None)✅ |
| 7 IPC ai_chat_send mention_spans 参数 | chat.rs:303 | `mention_spans: Option<Vec<MentionSpanDto>>`(Tauri 自动 camelCase→snake_case 转换)✅ |
| 8 chat.rs resolve_and_inject 转 MentionRef | chat.rs:88-112,119-138 | spans 经 `span_to_mention_ref` 按 kind 转 MentionRef,合并 skill,`resolve_all` 投影成 Augmentation,`build_augmentation_segment` 拼段 ✅ |
- **【用户重点关注的断链点核验】stores/ai.ts 是否透传 spans**:stores/ai.ts **本身不实现 sendMessage**(它通过 `...useAiSend()` 展开到 useAiStore,stores/ai.ts:226),真正的 sendMessage 在 useAiSend.ts:291 且**已正确透传 spans**(第 4 环节已核验)。**无断链** ✅
- L1 入队续发不挂 spans(useAiSend.ts:316-321 queue.push 无 spans 字段):**设计取舍非 bug**——队列项为韧性保内容,mention 区间在首次发送时已与文本对齐,排队等待后用户可能改输入,续发用旧 spans 语义模糊;注释 :287-289 明确标注,与现有 parts 入队续发的设计一致 ✅
**⑤ chat.rs 注入收敛一致性 PASS(4 调用方统一)**:
- `ai_chat_send`(:382)/ `ai_chat_force_send`(:1380)/ `ai_regenerate`(:229)/ `ai_chat_edit`(:1224)四调用方均经 `resolve_and_inject(&state, &provider_config, &skill/&None, &mention_spans/&None, &lang)`
- regenerate/edit 传 `&None, &None`(无新注入诉求,注释 :227-228/:1223 说明),空结果返 `""` 跳过拼接 ✅
- **旧 skill 注入块替换核验**:grep `read_skill_content|skill_content|注入技能` 全 chat.rs 无残留旧式注入代码,仅 `resolve_and_inject` docstring 提及"取代旧实现直接 read_skill_content 灌全文含 frontmatter"(:78-79 注释)。skill 注入已统一走 SkillResolver → read_skill_content_stripped 剥 frontmatter ✅
**⑥ skills.rs PASS(strip_frontmatter 状态机 + RwLock 双检锁 + ScanResult)**:
- `strip_frontmatter`(skills.rs:62-93)状态机:首行非 `---` 返原文(:69-77)/ 首 `---` 进 in_fm 找下个 `---` 退出取正文(:79-89)/ 未闭合 frontmatter 返空串容错(:91)。单测覆盖 normal/no_fm/empty/only_fm/crlf(:348-376)✅
- `RwLock<Option<Vec<SkillInfo>>>` 双检锁(skills.rs:278,290-311):快路径读锁命中返 guard / 慢路径释放锁扫盘 → 写锁填回(二次检查防并发重复扫)→ 再取读锁。`skills_cached()` clone 返 owned Vec(:317-320),`invalidate_skills()` 写锁置 None(:326-329)支持 `ai_reload_skills` 热重载(config.rs:41-44 invalidate + skills_cached)✅
- `ScanResult{skills, conflicts}`(skills.rs:41-44):按 name 去重(skills>commands>plugins 优先级保留首份)+ 同名 >1 收集 conflicts(:250-266)+ SkillInfo.duplicates 回填(:32-33 serde skip_serializing_if Option::is_none 向后兼容)✅
- `read_skill_content_stripped`(skills.rs:335-341):缓存命中读文件 → strip_frontmatter 剥正文。缓存未命中/文件读失败返 None ✅
**⑦ search_files 兜底 PASS(Denied 不弹窗,其他工具不变)**:
- `check_file_tool_auth`(audit/mod.rs:175-214):paths 任一命中黑名单 → Denied(:191 短路)/ 全授权 → Authorized(:201)/ 有 pending_dirs(未命中白名单非黑名单)→ `tool_name == "search_files"` 分支返 Denied 提示(:202-210)/ 其他 → NeedsAuth 弹窗(:212)✅
- **【用户重点关注的回归核验】其他文件工具行为不变**:`extract_file_tool_paths`(:143-161)单路径工具列表 `read_file | write_file | list_directory | patch_file | file_info | append_file | delete_file | search_files`(:155-156)+ rename_file 双路径(:145-154)。check_file_tool_auth 内只有 `tool_name == "search_files"` 单一特判(:202),**read/write/list_directory/patch_file/file_info/append_file/delete_file/rename_file 八个工具在 NeedsAuth 场景仍走原弹窗逻辑完全不变** ✅
- 黑名单短路在 search_files 特判**之前**(:191 for 循环内),故 search_files 命中黑名单也走 Denied(reason 来自 check_path_authorization 的黑名单原因,非 search_files 特定提示),逻辑一致 ✅
- handler 注册核验:tool_registry.rs:1574 `"search_files"` 注册为真实工具(非空壳),兜底改 Denied 不影响 handler 存在性(LLM 仍可调用,只是未授权目录时返 Err 提示而非弹窗)✅
**⑧ 前端 chip 元数据驱动 PASS(弃正则)**:
- ChatInput.selectMention 记 span(ChatInput.vue:432-438)+ watch skill/@ 共存(:254-268 删原"pendingSkill 非空强制关 mention"分支,skill chip 与 @ 输入态正交)✅
- MessageList.segmentUserContent(MessageList.vue:190-235)**纯元数据驱动非正则**:按 spans start 升序排序 → 逐 span 安全校验 `content.slice(start, end) === span.label`(:213,不等则降级跳过)→ 前导 text + chip 段 + tail text。越界(:209)/重叠(:211)/内容不符(:213)三路降级。极端全降级补完整 text(:230-232)✅
- 模板渲染(MessageList.vue:927-933)`v-for segmentUserContent` 按 seg.type 渲染 text/chip,chip class 按 `seg.span.kind` 区分 ✅
- **trim 偏移修正正确性**(ChatInput.vue:567-571):span.start 原相对 inputText(含前导空白),`text = inputText.trim()` 剥前导空白后偏移失效;`rawLeadingWs` 计算前导空白长度,span.start 整体左移。**尾部 trim 不影响**(chip 不在尾部空白区域)。修复正确,否则所有 chip start 错位致渲染降级纯文本 ✅
---
**🟡 MED-1:prompt.rs:108-111 docstring stale(双套注入机制未标注)**
| file:line | 问题 | 建议 |
|-----------|------|------|
| `src-tauri/src/commands/ai/prompt.rs:108-111` | build_system_prompt docstring 仍宣称「使 LLM 能解析用户消息中的 `[项目: xxx]` / `[任务: xxx]` 标记并对齐到真实实体(名称+状态+描述)」。但本批改动已把"@ mention 对齐到真实实体"的工作转交 augmentation resolver(chat.rs resolve_and_inject → build_augmentation_segment 独立段注入)。当前 build_system_prompt 仍全量注入最近 20 项目 + 20 任务到 system prompt(:118-136 全局清单),这是**两套并行的机制**:全局清单(向 LLM 提供可选项目/任务池)+ augmentation 精准投影(用户 @ 的特定实体详情)。docstring 把两套混为一谈,且"对齐到真实实体"的说法现已不准确(对齐工作已下沉到 resolver)。 | 修订 docstring 区分两套机制:build_system_prompt 注入"全局项目/任务清单(供 LLM 知晓可选范围)";augmentation 段(独立注入,见 chat.rs resolve_and_inject)负责"用户 @ 的特定实体详情对齐"。或者评估全局清单是否仍必要(若 augmentation 已覆盖用户所有 @ 场景,全局清单可能是冗余 context 膨胀——但这属行为评估,本次只标 docstring stale)。降 MED:纯文档不准,不影响行为;但易误导后续维护者以为标记解析仍在 build_system_prompt。 |
**⚪ LOW-1:Augmentation augmentations 字段当前无消费方(预留)**
`src/api/types.ts:370` AiMessage 加 `augmentations?: Augmentation[]` 字段,注释 :364-369 标注"主要用于落库消息回显/调试,前端宽容对待"。当前前端无任何代码读 `msg.augmentations`(grep 全 src/ 仅类型定义处 + 注释)。属**预留字段**(后端 resolve 后的 Augmentation 未来可能回传落库,前端先占位)。零调用方预留保留(对齐 memory dead-code-reserve-keep 原则),不删。
**⚪ LOW-2:ChatInput MentionItem.type 仅 'project'|'task'|'idea' 无 skill(/ 走 selectSkill 不走 selectMention)**
ChatInput.vue:325 `MentionItem.type: 'project' | 'task' | 'idea'` 无 skill 变体。设计正确:/ 技能走 selectSkill(:303)独立 chip 态(不插 inputText,清空输入),不走 @ selectMention。故 pendingMentionSpans 永不含 skill kind。但 chat.rs span_to_mention_ref(chat.rs:133-135)处理了 skill kind——这是**防御性兼容**(若未来 @ 技能也走 mention,或历史消息 spans 含 skill kind 时后端能正确 resolve)。无 bug,设计合理。
**⚪ LOW-3:augmentations/en 字段名混用(Augmentation 内 snake_case,MentionSpan refId camelCase)**
types.ts:383-384 注释明确:"Augmentation 内部字段沿用后端 snake_case(path/project_name 等,与 wire 严格对齐);refId 用 camelCase"。两种命名风格混在同一层。设计取舍:Augmentation 是后端投影镜像(保 snake_case 与 wire 对齐减少转译层),MentionSpan refId 是前端约定(camelCase 对齐 TS 风格)。合理但风格不统一,LOW 标注。
---
- **🟡 MED-1**: prompt.rs:108-111 docstring stale(双套注入机制未标注,易误导维护者)
- **⚪ LOW-1**: AiMessage.augmentations 预留字段无消费方(保留符合预留原则)
- **⚪ LOW-2**: ChatInput MentionItem.type 无 skill(/ 走 selectSkill 非selectMention,设计正确)
- **⚪ LOW-3**: Augmentation snake_case 与 MentionSpan refId camelCase 命名风格混用(设计取舍)
- **待修项回流 todo**: **无** 🔴/🟡 项(🟡 MED-1 纯 docstring stale,建议顺手修订不阻塞)
---
## 已审归档