squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
18 KiB
18 KiB
经验记录
DevFlow 开发中沉淀的经验性内容——踩坑、约定、技巧、bug 排查教训。聚焦「这个坑怎么踩的 / 这个约定为什么这么定 / 这个 bug 怎么定位的」,区别于 功能决策记录(记需求规格 + 设计决策规格)。
创建:2026-06-14(从功能决策记录-2026-06-14.md 分流出经验性条目) | 维护:随开发追加
约定
- 按类型分组:踩坑(隐性坑/反直觉)/ 约定(代码实现约定 / 命名约定)/ 技巧(具体技巧/配置)/ bug 排查(bug 定位过程与教训)。
- 每条标题标
[来源日期]+[Sprint](如有),便于回溯原上下文。 - 三要素:现象/决策 → 原因/根因 → 状态/教训。
- 与功能决策记录区分:这里记「怎么实现的细节坑」,不记「为什么这么设计」。
一、踩坑
i18n 模块必须命名空间化导出(扁平导出会断 $t + 键覆盖)[2026-06-14]
- 现象:左侧菜单显示
'nav.tasks'(原样键名);dashboard整页显示 key 名;AiChat的$t('ai.assistant')失效。 - 决策:每个 i18n 模块文件
export default { 命名空间: {...} }(如nav.ts→{ nav: {...} }),禁止扁平导出顶层词条。模板查询走$t('命名空间.key')。 - 根因:
index.ts聚合是Object.assign扁平合并各模块顶层 key(见「locale 拆分 + glob 聚合」决策)。扁平导出导致两个 bug:①$t('nav.tasks')找messages.nav不存在,原样显示键名;② 扁平键(如 nav 的ideas/projects/tasks/knowledge)与同名命名空间模块(ideas.ts/projects.ts/...)按文件名字母序互相覆盖。本次 nav.ts 扁平导出导致 4 个键被覆盖。 - 状态/教训:✅ 系统性修复,共 4 模块扁平已全部改嵌套(zh/en 8 文件):nav / common(8 文件 25 处 $t 引用)/ dashboard / ai。原本正确嵌套:ideas/projects/tasks/settings/knowledge/projectDetail/aiChat。教训:扁平导出是体系性 bug 非单点。排查「$t 显示原样键名」时应优先怀疑模块导出结构(扁平 vs 嵌套),而非 SSR / locale 初始化。
- 绕路纠错:曾误判根因为 SSR(实际 Tauri 纯客户端无 SSR)→ nav 走
getNavTranslations硬编码 map + displayText 绕路 → 清除绕路恢复标准$t。
二、约定
Anthropic 流式 output_tokens 当累计值直接覆盖 / 流式 token 落库走累加模式 [2026-06-13]
- 决策:①
message_delta事件的 output_tokens 直接覆盖 completion_tokens,不像 prompt 那样累加;②save_conversationupsert 路径 token 读旧值叠加(非覆盖);run_agentic_loop局部累加器每轮叠加、退出时一次性传 save。 - 原因:① Anthropic 协议在
message_delta返回的是累计 output_tokens(截至当前总量),非增量;当增量处理会重复计算。OpenAI 则是末 chunk 一次性给全量——两协议语义不同,各自处理。② 审批暂停→恢复 spawn 全新run_agentic_loop实例,新 loop 局部累加器从 0 起;若覆盖写会丢旧 loop 已落库的 token。累加保证跨 loop 实例的对话总用量正确。 - 状态:✅ 2026-06-13(两协议各自语义处理 + 跨 loop 累加保对话总量)
db 字段加列须同步四处:migration + crud 白名单 + AI 工具层白名单 + 工具描述 [2026-06-14]
- 现象:AI 对话让 AI 绑定目录,update_project(path) 报「不允许更新字段 'path'」,但 db schema 和 crud 白名单都已有 path。
- 根因:可更新字段有两套独立白名单——crud.rs::allowed_columns_for(DB 层)+ ai.rs 工具闭包硬编码 match(AI 工具层)。加 path/stack 时只同步 DB 层漏 AI 工具层,两层不一致。
- 教训:加 Record 可变字段同步四处(migration + crud 白名单 + ai.rs 工具白名单 + 工具描述)。排查「DB 有字段但工具报不允许」直查 ai.rs 硬编码。架构债:白名单双份去重。
Tauri 命令文件拆子 module:命令函数必须 glob pub use *,不能逐个显式 [2026-06-14]
- 现象:把含
#[tauri::command]的单文件(如 ai.rs)拆成ai/子 module 时,mod.rs 用pub use self::commands::{ai_chat_send, ...}逐个显式重导出 17 个命令,cargo check报 40 个 E0433:cannot find __cmd__ai_chat_send in ai/cannot find __tauri_command_name_ai_chat_send in ai。 - 根因:
#[tauri::command]宏不只生成命令函数本身,还用paste!宏拼接生成一组同模块定义的内部符号(__cmd__xxx、__tauri_command_name_xxx)。generate_handler!解析commands::ai::ai_chat_send时会查找commands::ai::__cmd__ai_chat_send。逐个pub use self::commands::{ai_chat_send}只拉函数本身,拉不到这些__cmd__内部符号(即使它们在原模块是 pub 的)。 - 教训:拆命令文件时,mod.rs 重导出命令必须用
pub use self::commands::*;(glob 把宏生成的全部符号一起拉到上层路径),不能用逐个显式。非命令 pub 项(如build_ai_tool_registry/restore_pending_approvals)可逐个显式。后续若拆 idea.rs/project.rs/task.rs 等其他含命令的大文件,同此模式。 - 状态:✅ 2026-06-14 验证(ai.rs 拆 11 子 module,glob 重导出后 cargo check 0 error)
跨层模块拆分:super::xxx 路径失效需改全限定 [2026-06-14]
- 现象:ai.rs(commands 直接子模块)拆到
ai/xxx.rs(commands 孙模块)后,6 个子文件use super::now_millis全报 E0425 unresolved import。 - 根因:
super指向当前模块的父——ai.rs 时super=commands(now_millis定义处);拆到ai/xxx.rs后super=commands::ai,now_millis在祖父模块commands。 - 教训:拆层后所有
super::xxx引用需重审。父模块的 helper(如now_millis)改全限定crate::commands::now_millis最稳(不依赖层级)。或拆层前把 helper 下沉到子 mod.rs 内use一次,子文件用super::xxx。 - 状态:✅ 2026-06-14 验证(批量改
crate::commands::now_millis,6 文件 20+ 处)
删文件后被 linter/工具重建为 0 字节触发 E0761 [2026-06-14]
- 现象:
rm commands/ai.rs后某 linter/hook 又建了 0 字节的 ai.rs,触发E0761: file for module ai found at both ai.rs and ai/mod.rs,且 Rust 优先选空文件导致后续 40 个cannot find __cmd__xxx(与 glob 重导出坑叠加,表象一致根因不同)。 - 教训:拆分时删原文件后立即 ls 验证不存在再跑 cargo check,避免空文件 + 目录并存的 E0761 与命令宏符号坑混淆。E0761 出现先查是否有 0 字节残留文件。
- 状态:✅ 2026-06-14 验证(删空 ai.rs 后通过)
ALLOWED_COLUMNS 从全局共享演进为按表隔离 [2026-06-13]
- 决策:
crud.rs列名白名单从单一全局ALLOWED_COLUMNS改为allowed_columns_for(table)按表 match;validate_column_name(field, table)接收表名;宏query/update_field传$table。专用更新路径列(knowledges.embedding 走 set_embedding、projects.deleted_at 走 soft_delete/restore)排除出白名单。 - 演进原因:原原则(见功能决策记录需求澄清「代码审查甄别原则」)基于「全局白名单够防注入」。本轮多代理代码审查发现真实 bug:全局白名单误含 ideas 表没有的
reasoning列(reasoning 属 knowledges/V10),update_idea("reasoning")会 validate 通过但 SQLite 报no such column——错误从「白名单拒绝」退化成「底层 SQL 错」且语义错。按表隔离既修此 bug(ideas 白名单不含 reasoning)又防未来跨表字段(update_task 误传 projects 的name在校验阶段拒绝,非靠 SQL 兜底)。 - 代价/取舍:12 表 × N 列的 match 冗长,但数据驱动、可读、一次写对。规模判断不变(仍不加分页/不拆 LIMIT),仅白名单从「全局防注入」升级为「按表防注入 + 防跨表字段」。
- 状态:✅ 2026-06-13 落地(cargo check + df-storage 32 test 全绿,含
update_field_rejects_cross_table_column_tasks_name用例验证跨表字段被拒)
配置存储:SQLite/AppState Arc,非 Tauri app config [2026-06-13]
- 决策:KnowledgeConfig(提取+注入共 5 项)存 AppState 内存(
knowledge_config: Arc<Mutex<KnowledgeConfig>>),前后端通过knowledge_get_config/knowledge_save_configIPC 读写;不引入 tauri-plugin-store。 - 演进:[2026-06-13 初版设计] 写「存 Tauri app config」 → [2026-06-13 审查修正] 代码实证项目 Cargo.toml 仅 opener+window_state 两插件,从未用过 config/store 机制;现有设置走两条路(SQLite 存 provider / localStorage 存 UI 偏好) → [2026-06-14]
SettingsRepo兑现本条预言:app_settingsKV 表(V13 迁移)+ 手写SettingsRepo(get/set/get_all/delete,不走impl_repo!宏因 KV 无固定 schema)。localStorage 11 key 迁移启动:敏感df-connections+ UI 偏好(theme/language/ai-width/ai-ui/token/concurrency) +df-ai-active-conv;例外df-ai-gen/df-ai-text(流式临时快照,每个 delta 写一次,SQLite 高频写拖慢流式,留 localStorage)。 - 原因:AI Provider 配置已是 SQLite+Repo+IPC 模式,知识库行为配置(后端行为,非 UI 偏好)对齐同模式最一致。引入 tauri-plugin-store 是全新基础设施依赖,与既有 DB 路线割裂。AppState Arc 内存持有 + IPC 读写,启动时
default()初始化(Tier 1 未持久化到 DB,进程重启回默认——够用,因这是行为偏好非数据)。未来要持久化时复用同一套 SettingsRepo 即可。 - 状态:✅ 已实施(Tier 1)
知识删除语义:knowledge_archive 软删除(命名统一)[2026-06-13]
- 决策:知识删除 command 命名
knowledge_archive(执行UPDATE status='archived'),不叫 knowledge_delete。匹配ai_conversation_archive先例;主列表knowledge_list(status=None)默认仅返回 published(library 纯已发布,[2026-06-16] F-260616-02 决策 a 收窄:pending_review 归knowledge_list_candidates收件箱,不再混杂 library)。 - 原因/取舍:idea/task/project 的
delete_xxx都是硬删(DELETE FROM),若 knowledge 也叫 delete 却做归档,API 语义混淆(调用方期望数据消失,实际还在 DB)。conversation 模块已有正确先例(archive 命名表示软删除)。软删除复用 archived 状态,数据保留可追溯,列表默认过滤保证用户感知「已删除」。状态机 published→archived 也走同一路径。 - 状态:✅ 已实施(Tier 1)
Store 状态字段用 getter 替代引用快照 [Sprint 10]
- 决策:
useProjectStore()返回对象的状态字段(projects/tasks/ideas/workflowExecutions/liveEvents/loading/error)改 getter 实时读 state,而非ideas: state.ideas引用快照。 - 原因:引用快照在
loadIdeas()等重新赋值 state.ideas 后,返回对象的 ideas 属性不更新(刷新后视图空,需切菜单再切回才显示);getter 每次读 state,响应链成立。computed(stats/pendingApproval)在 reactive 内仍自动解包,各视图用法零改动。 - 状态:🚧 Sprint 10(编译/构建通过,未 tauri dev 实测,根因通杀 Projects/Tasks/Dashboard)
三、技巧
migrate_v4:PRAGMA table_info 探测列存在性 [Sprint 10]
- 决策:v4 加
archived列时,用PRAGMA table_info幂等探测列是否已存在,而非仅依赖schema_version版本号 gate。 - 原因:历史坏库
schema_version值混乱(早期迁移异常致版本号与实际 schema 不符),版本号不可靠;直接探列存在性最稳——已存在则跳过,不存在则补建,幂等可重入。 - 状态:✅ Sprint 10
Vite 端口 strictPort: true 不自动迁移 [Sprint 1]
- 决策:
vite.config.ts设port: 1420+strictPort: true,端口被占时直接报错退出而非自动 +1 迁移;tauri.conf.json的devUrl写死http://localhost:1420。 - 原因:Tauri webview 启动时按
devUrl加载前端,若 Vite 因冲突静默迁移到 1421 而 devUrl 仍是 1420 → 白屏/连不上,错误难定位(易误判为前端代码 bug)。strictPort让端口冲突当场炸出,定位明确。代价:1420 被占需手动杀进程,但换取「devUrl 与实际端口必一致」的不变量。 - 状态:✅ Sprint 1(本次会话核对:1420 vs 2661 反复折腾后回退到 1420,即此耦合的直接体现)
四、bug 排查
ai_tool_executions 审计回写失效(Med/High 审批后卡 pending)[#54 实测]
- 现象:用户审批 Med/High 工具后执行成功(副作用落库,如 create_project→projects 有记录),但
ai_tool_executions仍status=pending / decided_by=None / executed_at=None / result=None,审计未闭环。Low 工具正常(decided_by=auto完整)。 - 根因(代码层定位):
crud.rs:103宏impl_repo!生成的通用query硬编码ORDER BY created_at DESC,但ai_tool_executions表无created_at列 →audit_finalize的query("tool_call_id", x)SQL 报no such column: created_at→.unwrap_or_default()吞错返回空 →if let Some(rec)为 None → 永不回写。Low 工具不走 query(process_tool_callsLow 分支直接audit_tool_callinsert 完整记录)故不受影响。 - 架构隐患:通用
query的ORDER BY created_at假设所有表都有该列——ai_tool_executions(及潜在其他无created_at的表)任何query()调用都静默失败;unwrap_or_default吞 SQL 错误放大隐患。 - 修复:✅ 已落地(2026-06-13)。采用方向①:
crud.rs给AiToolExecutionRepo加专用find_by_tool_call_id(裸 SQLORDER BY requested_at DESC LIMIT 1,绕过宏的created_at假设);ai.rs audit_finalize改用之,查不到记录改tracing::warn(不再unwrap_or_default静默吞错)。- → 未改宏(方向②影响 7+ 表)/ 未加列(方向③需迁移):隐患仅
ai_tool_executions一处暴露,局部修最小影响。 - → 架构隐患仍存(未根治):通用
query/list_all宏对无created_at的表(ai_tool_executions/node_executions/workflow_executions)调用仍静默失败。当前仅ai_tool_executions有query调用且已绕开,余者暂无query调用点。未来新增调用时,要么该表登记created_at,要么宏做容错。
- → 未改宏(方向②影响 7+ 表)/ 未加列(方向③需迁移):隐患仅
- 教训:宏生成的通用方法对表 schema 的隐式假设(这里「所有表都有 created_at」)是隐蔽的系统性风险;
unwrap_or_default()吞错误让 bug 隐形——关键路径慎用。
reasoning 字段回填(修 bug:prompt 要求但写库丢弃)[2026-06-13]
- 现象/决策:
KnowledgeRecord加reasoning: Option<String>(V10 ALTER),extract_knowledge_from_conversation解析 LLM JSON 的reasoning字段写入主表;前端详情溯源区展示「🤖 AI 判断依据」。 - 根因:
EXTRACTION_SYSTEM_PROMPT早已要求 LLM 输出reasoning: "为何值得沉淀",但提炼循环(ai.rs 旧版)只取 kind/title/content/tags/confidence,reasoning 被 LLM 产出却遭代码丢弃——是信息链断裂的 bug,非缺功能。审核员光看 content 结论,缺 AI 判断依据(尤其 confidence=low 的弱信号更靠 reasoning 解释为何还提炼)。回填后溯源完整。 - 状态:✅ 已实施(reasoning 存主表 + extracted 事件 context.reasoning 双写,前端优先取主表降级取事件)
- 教训:LLM 输出字段与代码消费字段须对账——prompt 要求 LLM 产出的字段,代码侧漏消费是常见隐性 bug。
prompt_tokens=0:深挖证伪非代码 bug(疑 GLM 订阅端点 message_start 缺 input_tokens)[#54 实测发现]
- 现象:
ai_conversations.prompt_tokens=0(completion=1496 正常)。GLM-订阅(anthropic 协议)1 对话 24 消息,所有 assistant 消息usage=None。 - 深挖结论(→ 修正初判):初判「anthropic_compat usage 解析漏 input_tokens,待修」证伪。逐段验证:
anthropic_compatmessage_start 取input_tokens→prompt_tokens有单测(input=42 过);stream_llm(857)final_usage=chunk.usage.clone()累积对;ai.rs:699tokens.add链路对。 代码按标准 Anthropic 协议解析正确。inp as u32(Some→值,None→0):completion 有值说明 message_delta 的 output GLM 返回了,prompt=0 = GLM 订阅端点 message_start 疑未返回usage.input_tokens(协议非标)。勿改 anthropic_compat(改了 = 误改正确实现)。
- 状态:📐 待修(误判)→ 🚫 非代码 bug。待抓 GLM 订阅 SSE 原文确认 input_tokens 在哪个事件/字段(临时打 message_start/message_delta 的 usage JSON 日志,测完删);若确认端点缺则属 provider 兼容性待办,非解析 bug。
- 教训:bug 定位优先用单测/逐段验证证伪代码层假设,不要急着改「看似正确」的实现。深挖证伪避免了一次误改。
相关文档: