Files
DevFlow/docs/02-架构设计/经验记录-2026-06-14.md
绝尘 04032a2a8d 重构: 文档汇总+进度看板+孤儿任务清理脚本+gitignore 噪音排除
- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
2026-06-15 05:14:21 +08:00

17 KiB
Raw Blame History

经验记录

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 / common8 文件 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_conversation upsert 路径 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_forDB 层)+ ai.rs 工具闭包硬编码 matchAI 工具层)。加 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 个 E0433cannot 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 子 moduleglob 重导出后 cargo check 0 error

跨层模块拆分:super::xxx 路径失效需改全限定 [2026-06-14]

  • 现象ai.rscommands 直接子模块)拆到 ai/xxx.rscommands 孙模块6 个子文件 use super::now_millis 全报 E0425 unresolved import。
  • 根因super 指向当前模块的父——ai.rs 时 super = commandsnow_millis 定义处);拆到 ai/xxx.rssuper = commands::ainow_millis 在祖父模块 commands
  • 教训:拆层后所有 super::xxx 引用需重审。父模块的 helpernow_millis)改全限定 crate::commands::now_millis 最稳(不依赖层级)。或拆层前把 helper 下沉到子 mod.rs 内 use 一次,子文件用 super::xxx
  • 状态 2026-06-14 验证(批量改 crate::commands::now_millis6 文件 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) 按表 matchvalidate_column_name(field, table) 接收表名;宏 query/update_field$table。专用更新路径列knowledges.embedding 走 set_embedding、projects.deleted_at 走 soft_delete/restore排除出白名单。
  • 演进原因:原原则(见功能决策记录需求澄清「代码审查甄别原则」)基于「全局白名单够防注入」。本轮多代理代码审查发现真实 bug:全局白名单误含 ideas 表没有的 reasoningreasoning 属 knowledges/V10update_idea("reasoning") 会 validate 通过但 SQLite 报 no such column——错误从「白名单拒绝」退化成「底层 SQL 错」且语义错。按表隔离既修此 bugideas 白名单不含 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_config IPC 读写;不引入 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_settings KV 表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) 默认 AND status!='archived' 过滤。
  • 原因/取舍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响应链成立。computedstats/pendingApproval在 reactive 内仍自动解包,各视图用法零改动。
  • 状态🚧 Sprint 10编译/构建通过,未 tauri dev 实测,根因通杀 Projects/Tasks/Dashboard

三、技巧

migrate_v4PRAGMA table_info 探测列存在性 [Sprint 10]

  • 决策v4 加 archived 列时,用 PRAGMA table_info 幂等探测列是否已存在,而非仅依赖 schema_version 版本号 gate。
  • 原因:历史坏库 schema_version 值混乱(早期迁移异常致版本号与实际 schema 不符),版本号不可靠;直接探列存在性最稳——已存在则跳过,不存在则补建,幂等可重入。
  • 状态 Sprint 10

Vite 端口 strictPort: true 不自动迁移 [Sprint 1]

  • 决策vite.config.tsport: 1420 + strictPort: true,端口被占时直接报错退出而非自动 +1 迁移;tauri.conf.jsondevUrl 写死 http://localhost:1420
  • 原因Tauri webview 启动时按 devUrl 加载前端,若 Vite 因冲突静默迁移到 1421 而 devUrl 仍是 1420 → 白屏/连不上,错误难定位(易误判为前端代码 bugstrictPort 让端口冲突当场炸出定位明确。代价1420 被占需手动杀进程但换取「devUrl 与实际端口必一致」的不变量。
  • 状态 Sprint 1本次会话核对1420 vs 2661 反复折腾后回退到 1420即此耦合的直接体现

四、bug 排查

ai_tool_executions 审计回写失效Med/High 审批后卡 pending[#54 实测]

  • 现象:用户审批 Med/High 工具后执行成功(副作用落库,如 create_project→projects 有记录),但 ai_tool_executionsstatus=pending / decided_by=None / executed_at=None / result=None审计未闭环。Low 工具正常(decided_by=auto 完整)。
  • 根因(代码层定位)crud.rs:103impl_repo! 生成的通用 query 硬编码 ORDER BY created_at DESC,但 ai_tool_executionscreated_ataudit_finalizequery("tool_call_id", x) SQL 报 no such column: created_at.unwrap_or_default() 吞错返回空 → if let Some(rec) 为 None → 永不回写。Low 工具不走 queryprocess_tool_calls Low 分支直接 audit_tool_call insert 完整记录)故不受影响。
  • 架构隐患:通用 queryORDER BY created_at 假设所有表都有该列——ai_tool_executions(及潜在其他无 created_at 的表)任何 query() 调用都静默失败;unwrap_or_default 吞 SQL 错误放大隐患。
  • 修复 已落地2026-06-13。采用方向①crud.rsAiToolExecutionRepo 加专用 find_by_tool_call_id(裸 SQL ORDER 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_executionsquery 调用且已绕开,余者暂无 query 调用点。未来新增调用时,要么该表登记 created_at,要么宏做容错。
  • 教训:宏生成的通用方法对表 schema 的隐式假设(这里「所有表都有 created_at」是隐蔽的系统性风险unwrap_or_default() 吞错误让 bug 隐形——关键路径慎用。

reasoning 字段回填(修 bugprompt 要求但写库丢弃)[2026-06-13]

  • 现象/决策KnowledgeRecordreasoning: Option<String>V10 ALTERextract_knowledge_from_conversation 解析 LLM JSON 的 reasoning 字段写入主表;前端详情溯源区展示「🤖 AI 判断依据」。
  • 根因EXTRACTION_SYSTEM_PROMPT 早已要求 LLM 输出 reasoning: "为何值得沉淀"但提炼循环ai.rs 旧版)只取 kind/title/content/tags/confidencereasoning 被 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=0completion=1496 正常。GLM-订阅anthropic 协议1 对话 24 消息,所有 assistant 消息 usage=None
  • 深挖结论(→ 修正初判)初判「anthropic_compat usage 解析漏 input_tokens待修」证伪。逐段验证:
    1. anthropic_compat message_start 取 input_tokens→prompt_tokens 有单测input=42 过);
    2. stream_llm857final_usage=chunk.usage.clone() 累积对;
    3. ai.rs:699 tokens.add 链路对。 代码按标准 Anthropic 协议解析正确。inp as u32Some→值None→0completion 有值说明 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 定位优先用单测/逐段验证证伪代码层假设,不要急着改「看似正确」的实现。深挖证伪避免了一次误改。

相关文档